diff --git a/.github/workflows/daily_pytest_slack.yml b/.github/workflows/daily_pytest_slack.yml new file mode 100644 index 000000000..d8285bf6b --- /dev/null +++ b/.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/.github/workflows/soak.yaml b/.github/workflows/soak.yaml index 76adc94d4..ff0b3234e 100644 --- a/.github/workflows/soak.yaml +++ b/.github/workflows/soak.yaml @@ -15,7 +15,7 @@ jobs: SOAK_RATE_PER_SEC: "1000" MQTT_TOPIC: "mqtt/soak" KAFKA_TOPIC: "dev-robot-alerts" - LOSS_THRESHOLD_PCT: "1.0" # סף כשל: % אובדן מותר (שני לפי הצורך) + LOSS_THRESHOLD_PCT: "1.0" steps: - name: Checkout @@ -36,6 +36,31 @@ jobs: 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 @@ -223,10 +248,7 @@ jobs: 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/.gitignore b/.gitignore index 7aa2dd1d2..1f4c108c4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +data/rover_samples/** +!data/rover_samples/.gitkeep + +# Ignore data payloads +/data/ # --- Secrets and Certificates --- *.env *.crt @@ -7,15 +12,28 @@ 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/ +<<<<<<< HEAD +======= + + +>>>>>>> 4bb2e60fc0fd9a846955fa89533d661a56b1645a .venv/ venv/ +.coverage # --- VSCode / Editor --- .vscode/ @@ -36,3 +54,49 @@ venv/ 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. +<<<<<<< HEAD +!services/inference_http/models/fence_hole_detector/weights/ +!services/inference_http/models/fence_hole_detector/weights/best.onnx + +# ==== 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 + +>>>>>>> 4bb2e60fc0fd9a846955fa89533d661a56b1645a +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 +<<<<<<< HEAD +======= + +.coverage +>>>>>>> 4bb2e60fc0fd9a846955fa89533d661a56b1645a diff --git a/GUI/grafana/dashboards/ultrasonic-dashboard.json b/GUI/grafana/dashboards/ultrasonic-dashboard.json new file mode 100644 index 000000000..15c98c4b3 --- /dev/null +++ b/GUI/grafana/dashboards/ultrasonic-dashboard.json @@ -0,0 +1,857 @@ +{ + "id": null, + "uid": "ultrasonic-plant-dashboard-bw-01", + "title": "Plant Health Monitoring - Professional Dashboard", + "tags": [ + "postgres", + "ultrasonic", + "plants", + "agriculture" + ], + "timezone": "browser", + "schemaVersion": 36, + "version": 6, + "refresh": "10s", + "time": { + "from": "now-30d", + "to": "now" + }, + "panels": [ + { + "id": 1, + "type": "stat", + "title": "Total Predictions", + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 0 + }, + "targets": [ + { + "expr": "last_over_time(ultrasonic_predictions_total_total[5m])", + "refId": "A", + "legendFormat": "Total" + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "orientation": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "fixed", + "fixedColor": "#000000" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#000000", + "value": null + } + ] + } + } + } + }, + { + "id": 2, + "type": "stat", + "title": "Success Rate", + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 0 + }, + "targets": [ + { + "expr": "avg(last_over_time(ultrasonic_success_rate_by_sensor_success_rate[5m]))", + "refId": "A", + "legendFormat": "Success Rate" + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "orientation": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "unit": "percent", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#DC2626", + "value": null + }, + { + "color": "#000000", + "value": 60 + }, + { + "color": "#15803d", + "value": 90 + } + ] + } + } + } + }, + { + "id": 3, + "type": "stat", + "title": "Healthy Status", + "gridPos": { + "h": 8, + "w": 6, + "x": 12, + "y": 0 + }, + "targets": [ + { + "expr": "last_over_time(ultrasonic_class_distribution_healthy_healthy_percentage[5m])", + "refId": "A", + "legendFormat": "Healthy %" + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "orientation": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "fixed", + "fixedColor": "#000000" + }, + "unit": "percent", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#000000", + "value": null + } + ] + } + } + } + }, + { + "id": 4, + "type": "stat", + "title": "Stress Status", + "gridPos": { + "h": 8, + "w": 6, + "x": 18, + "y": 0 + }, + "targets": [ + { + "expr": "last_over_time(ultrasonic_class_distribution_stress_stress_percentage[5m])", + "refId": "A", + "legendFormat": "Stress %" + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "colorMode": "value", + "graphMode": "none", + "orientation": "auto", + "textMode": "auto" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "fixed", + "fixedColor": "#000000" + }, + "unit": "percent", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#000000", + "value": null + } + ] + } + } + } + }, + { + "id": 5, + "type": "piechart", + "title": "Plant Health Classification", + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 8 + }, + "targets": [ + { + "expr": "ultrasonic_predictions_by_class_count", + "refId": "A", + "legendFormat": "{{predicted_class}}", + "instant": true + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi" + }, + "pieType": "pie", + "displayLabels": [ + "name", + "percent" + ] + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Drought_Plant" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#FBBF24" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Pest_Plant" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#EF4444" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Control_Greenhouse" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#10B981" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Control_Empty" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#3B82F6" + } + } + ] + } + ] + } + }, + { + "id": 6, + "type": "gauge", + "title": "Average Confidence Score", + "gridPos": { + "h": 9, + "w": 6, + "x": 12, + "y": 8 + }, + "targets": [ + { + "expr": "last_over_time(ultrasonic_predictions_avg_confidence_avg_confidence[5m])", + "refId": "A", + "legendFormat": "Avg Confidence" + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "showThresholdLabels": true, + "showThresholdMarkers": true + }, + "fieldConfig": { + "defaults": { + "min": 0, + "max": 1, + "unit": "percentunit", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#DC2626", + "value": null + }, + { + "color": "#6B7280", + "value": 0.5 + }, + { + "color": "#15803d", + "value": 0.85 + } + ] + } + } + } + }, + { + "id": 7, + "type": "barchart", + "title": "Sensor Confidence by ID", + "gridPos": { + "h": 9, + "w": 6, + "x": 18, + "y": 8 + }, + "targets": [ + { + "expr": "ultrasonic_confidence_by_sensor_avg_confidence", + "refId": "A", + "legendFormat": "{{sensor_id}}", + "instant": true + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "legend": { + "displayMode": "hidden", + "placement": "bottom", + "showLegend": false + }, + "orientation": "horizontal", + "xTickLabelRotation": 0, + "xTickLabelSpacing": 0, + "showValue": "always" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 90, + "lineWidth": 1 + }, + "color": { + "mode": "fixed", + "fixedColor": "#374151" + }, + "unit": "percentunit", + "decimals": 2 + } + }, + "transformations": [ + { + "id": "reduce", + "options": { + "calcs": [ + "last" + ] + } + } + ] + }, + { + "id": 8, + "type": "timeseries", + "title": "Predictions per Hour (Last 24h)", + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 17 + }, + "timeFrom": "now-24h", + "targets": [ + { + "expr": "ultrasonic_predictions_per_hour_count", + "refId": "A", + "legendFormat": "Predictions" + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom", + "showLegend": false + }, + "tooltip": { + "mode": "multi" + } + }, + "fieldConfig": { + "defaults": { + "custom": { + "drawStyle": "bars", + "lineInterpolation": "linear", + "fillOpacity": 80, + "lineWidth": 0 + }, + "color": { + "mode": "fixed", + "fixedColor": "#6B7280" + } + } + } + }, + { + "id": 9, + "type": "barchart", + "title": "Daily Stress Events", + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 17 + }, + "targets": [ + { + "expr": "ultrasonic_daily_stress_count_event_count", + "refId": "A", + "legendFormat": "{{event_date}}", + "format": "time_series" + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "legend": { + "displayMode": "hidden", + "placement": "bottom", + "showLegend": false + }, + "orientation": "auto", + "xTickLabelRotation": -45, + "xTickLabelSpacing": 50, + "showValue": "auto" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 85, + "lineWidth": 1 + }, + "color": { + "mode": "fixed", + "fixedColor": "#6B7280" + } + } + } + }, + { + "id": 10, + "type": "piechart", + "title": "Reading Status Distribution", + "gridPos": { + "h": 9, + "w": 8, + "x": 0, + "y": 26 + }, + "targets": [ + { + "expr": "ultrasonic_predictions_by_status_count", + "refId": "A", + "legendFormat": "{{status}}", + "instant": true + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "displayLabels": [ + "name", + "percent" + ] + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Success" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#9CA3AF" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Error" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#DC2626" + } + } + ] + } + ] + } + }, + { + "id": 11, + "type": "barchart", + "title": "Sensor Success Rates", + "gridPos": { + "h": 9, + "w": 8, + "x": 8, + "y": 26 + }, + "targets": [ + { + "expr": "ultrasonic_success_rate_by_sensor_success_rate", + "refId": "A", + "legendFormat": "{{sensor_id}}", + "instant": true + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "legend": { + "displayMode": "hidden", + "showLegend": false + }, + "orientation": "auto", + "xTickLabelRotation": -45, + "showValue": "always" + }, + "fieldConfig": { + "defaults": { + "custom": { + "fillOpacity": 90 + }, + "color": { + "mode": "thresholds" + }, + "unit": "percent", + "decimals": 1, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "#DC2626", + "value": 0 + }, + { + "color": "#6B7280", + "value": 70 + }, + { + "color": "#15803d", + "value": 95 + } + ] + } + } + }, + "transformations": [ + { + "id": "reduce", + "options": { + "calcs": [ + "last" + ] + } + } + ] + }, + { + "id": 12, + "type": "piechart", + "title": "Confidence Distribution", + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 26 + }, + "targets": [ + { + "expr": "ultrasonic_confidence_distribution_count", + "refId": "A", + "legendFormat": "{{confidence_range}}", + "instant": true + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "legend": { + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "displayLabels": [ + "name", + "percent" + ] + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "90-100% (High)" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#34D399" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "75-90% (Good)" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#60A5FA" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "60-75% (Medium)" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#FCD34D" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "< 60% (Low)" + }, + "properties": [ + { + "id": "color", + "value": { + "mode": "fixed", + "fixedColor": "#F87171" + } + } + ] + } + ] + } + }, + { + "id": 13, + "type": "table", + "title": "Sensor Activity Summary", + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 35 + }, + "targets": [ + { + "expr": "ultrasonic_confidence_by_sensor_avg_confidence", + "refId": "A", + "format": "table", + "instant": true + } + ], + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "options": { + "showHeader": true, + "sortBy": [ + { + "displayName": "avg_confidence", + "desc": true + } + ] + }, + "fieldConfig": { + "defaults": { + "custom": { + "align": "auto", + "width": "auto" + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "avg_confidence" + }, + "properties": [ + { + "id": "unit", + "value": "percentunit" + }, + { + "id": "decimals", + "value": 2 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "min_confidence" + }, + "properties": [ + { + "id": "unit", + "value": "percentunit" + }, + { + "id": "decimals", + "value": 2 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "max_confidence" + }, + "properties": [ + { + "id": "unit", + "value": "percentunit" + }, + { + "id": "decimals", + "value": 2 + } + ] + } + ] + }, + "transformations": [] + } + ] +} \ No newline at end of file diff --git a/GUI/mock_db_api.py b/GUI/mock_db_api.py new file mode 100644 index 000000000..c643b4414 --- /dev/null +++ b/GUI/mock_db_api.py @@ -0,0 +1,24 @@ +from http.server import BaseHTTPRequestHandler, HTTPServer +import json + +class H(BaseHTTPRequestHandler): + def do_GET(self): + if self.path == "/health": + body = {"ok": True} + elif self.path.startswith("/api/tables/"): + body = {"data": []} + else: + body = {"data": []} + b = json.dumps(body).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(b))) + self.end_headers() + self.wfile.write(b) + + def log_message(self, *a): + pass + +if __name__ == "__main__": + print("[mock-db-api] listening on 127.0.0.1:8001") + HTTPServer(("127.0.0.1", 8001), H).serve_forever() diff --git a/GUI/requirements.txt b/GUI/requirements.txt index bd9efb1b7..779d432a9 100644 --- a/GUI/requirements.txt +++ b/GUI/requirements.txt @@ -1,18 +1,52 @@ + 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 +# ───── Web/API ───── fastapi>=0.110 uvicorn[standard]>=0.29 flask -# Metrics & HTTP + +# ───── Metrics & HTTP ───── prometheus-client>=0.20 requests>=2.31 httpx==0.27.0 # only needed when you switch to real Flink REST -# gRPC & Protobuf + +# ───── gRPC & Protobuf ───── grpcio>=1.56,<2 grpcio-tools>=1.56,<2 protobuf>=6,<7 -# Validation / crypto + +# ───── 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/GUI/src/vast/alerts/alert_client.py b/GUI/src/vast/alerts/alert_client.py new file mode 100644 index 000000000..7212a10c8 --- /dev/null +++ b/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/GUI/src/vast/alerts/alert_service.py b/GUI/src/vast/alerts/alert_service.py new file mode 100644 index 000000000..8014e0f86 --- /dev/null +++ b/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/GUI/src/vast/dashboard_api.py b/GUI/src/vast/dashboard_api.py index 11f17f337..790605fa3 100644 --- a/GUI/src/vast/dashboard_api.py +++ b/GUI/src/vast/dashboard_api.py @@ -1,54 +1,97 @@ +# -*- 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 urllib.parse import quote 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" -# ---------- CONFIG ---------- -DB_API_BASE = "http://host.docker.internal:8001" -DB_API_AUTH_MODE = "service" -DB_API_TOKEN_FILE = "/app/secrets/db_api_token" -DB_API_TOKEN = "auto" -DB_API_SERVICE_NAME = "GUI_H" +DEFAULT_GROUND_BUCKET = os.getenv("GROUND_BUCKET", "ground") +DEFAULT_GROUND_PREFIX = os.getenv("GROUND_PREFIX", "") -# ---------- TOKEN BOOTSTRAP ---------- +# ========================= +# 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) -> str | None: +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) -> str | 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() + data = r.json() if r.content else {} raw = (data.get("service_account", {}) or {}).get("raw_token") \ - or (data.get("service_account", {}) or {}).get("token") + or (data.get("service_account", {}) or {}).get("token") if raw and isinstance(raw, str) and "***" not in raw: return raw.strip() - except Exception: - time.sleep(backoff * attempt) + 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() -> str | None: - print(f"[DEBUG] Checking for existing token file at: {DB_API_TOKEN_FILE}", flush=True) - +def get_or_bootstrap_token() -> Optional[str]: if DB_API_TOKEN and DB_API_TOKEN.lower() != "auto": - print(f"[DEBUG] Using static token from config", flush=True) + print("[DEBUG] Using static token from DB_API_TOKEN", flush=True) return DB_API_TOKEN token = _read_token_from_file(DB_API_TOKEN_FILE) @@ -56,11 +99,12 @@ def get_or_bootstrap_token() -> str | None: print(f"[DEBUG] Loaded token from {DB_API_TOKEN_FILE}", flush=True) return token - print(f"[DEBUG] No existing token found, bootstrapping via {DB_API_BASE}/auth/_dev_bootstrap", flush=True) + 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: - pathlib.Path(DB_API_TOKEN_FILE).parent.mkdir(parents=True, exist_ok=True) - pathlib.Path(DB_API_TOKEN_FILE).write_text(token, encoding="utf-8") + 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 @@ -68,35 +112,703 @@ def get_or_bootstrap_token() -> str | None: 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 -# ---------- API CLIENT ---------- +# ========================= +# DASHBOARD API +# ========================= class DashboardApi: - def __init__(self): + """ + 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}"}) - self.http.headers.update({"Content-Type": "application/json"}) - self.http.mount("http://", HTTPAdapter(max_retries=Retry(total=5, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504]))) - self.http.mount("https://", HTTPAdapter(max_retries=Retry(total=5, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504]))) - # ---------- METHODS ---------- + 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) + + + # ===================================================== + # ===== AUDIO ANALYTICS METHODS WITH SOUND FILTER ===== + # ===================================================== + + def get_audio_stats(self, time_range: str = 'all', sound_types: List[str] = None) -> 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'", + 'month': "AND r.started_at > NOW() - INTERVAL '30 days'" + }.get(time_range, '') + + sound_filter = "" + if sound_types and len(sound_types) > 0: + sound_list = "'" + "','".join(sound_types) + "'" + sound_filter = f"AND fa.head_pred_label IN ({sound_list})" + + query = f""" + SELECT + COUNT(*) as total_files, + SUM(CASE WHEN head_is_another = true THEN 1 ELSE 0 END) as unknown_count, + AVG(head_pred_prob) as avg_confidence, + AVG(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} {sound_filter} + """ + + results = self.run_query(query) + return results[0] if results else {} + + def get_audio_distribution(self, time_range: str = 'all', limit: int = 10, sound_types: List[str] = None) -> List[Dict]: + """Get distribution of audio classifications""" + 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'", + 'month': "AND r.started_at > NOW() - INTERVAL '30 days'" + }.get(time_range, '') + + sound_filter = "" + if sound_types and len(sound_types) > 0: + sound_list = "'" + "','".join(sound_types) + "'" + sound_filter = f"AND fa.head_pred_label IN ({sound_list})" + + query = f""" + SELECT + 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 head_pred_label IS NOT NULL {time_filter} {sound_filter} + GROUP BY 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, sound_types: List[str] = None) -> List[Dict]: + """Get average confidence levels by classification""" + 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'", + 'month': "AND r.started_at > NOW() - INTERVAL '30 days'" + }.get(time_range, '') + + sound_filter = "" + if sound_types and len(sound_types) > 0: + sound_list = "'" + "','".join(sound_types) + "'" + sound_filter = f"AND fa.head_pred_label IN ({sound_list})" + + query = f""" + SELECT + head_pred_label, + AVG(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 head_pred_label IS NOT NULL + AND head_pred_prob IS NOT NULL + {time_filter} + {sound_filter} + GROUP BY head_pred_label + ORDER BY avg_confidence DESC + LIMIT {limit} + """ + + return self.run_query(query) + + def get_audio_detailed_table(self, time_range: str = 'all', limit: int = 20, sound_types: List[str] = None) -> List[Dict]: + """Get detailed table data with class probabilities""" + 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'", + 'month': "AND r.started_at > NOW() - INTERVAL '30 days'" + }.get(time_range, '') + + sound_filter = "" + if sound_types and len(sound_types) > 0: + sound_list = "'" + "','".join(sound_types) + "'" + sound_filter = f"AND fa.head_pred_label IN ({sound_list})" + + query = f""" + SELECT + head_pred_label, + COUNT(*) as count, + AVG(head_pred_prob) as avg_prob, + AVG((head_probs_json->>'predatory_animals')::float) as p_predatory, + AVG((head_probs_json->>'birds')::float) as p_birds, + AVG((head_probs_json->>'fire')::float) as p_fire, + AVG((head_probs_json->>'screaming')::float) as p_screaming, + AVG((head_probs_json->>'shotgun')::float) as p_shotgun + FROM agcloud_audio.file_aggregates fa + JOIN agcloud_audio.runs r ON fa.run_id = r.run_id + WHERE head_pred_label IS NOT NULL {time_filter} {sound_filter} + GROUP BY head_pred_label + ORDER BY count DESC + LIMIT {limit} + """ + + return self.run_query(query) + + def get_audio_critical_events(self, time_range: str = 'day', limit: int = 100, sound_types: List[str] = None) -> List[Dict]: + """Get critical sound events""" + 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'", + 'month': "AND r.started_at > NOW() - INTERVAL '30 days'" + }.get(time_range, "AND r.started_at > NOW() - INTERVAL '24 hours'") + + if sound_types and len(sound_types) > 0: + sound_list = "'" + "','".join(sound_types) + "'" + sound_filter = f"AND fa.head_pred_label IN ({sound_list})" + else: + sound_filter = "AND fa.head_pred_label IN ('fire', 'screaming', 'shotgun', 'predatory_animals')" + + query = f""" + SELECT + r.run_id, + r.started_at, + f.path as file_path, + 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 agcloud_audio.files f ON fa.file_id = f.file_id + WHERE 1=1 + {time_filter} + {sound_filter} + ORDER BY r.started_at DESC, fa.head_pred_prob DESC + LIMIT {limit} + """ + + return self.run_query(query) + + def get_audio_timeline(self, time_range: str = 'day', sound_types: List[str] = None) -> List[Dict]: + """Get audio alert timeline data grouped by time buckets""" + bucket_interval = { + 'day': 1, + 'week': 6, + 'month': 24 + }.get(time_range, 1) + + time_filter_map = { + 'day': "AND r.started_at > NOW() - INTERVAL '24 hours'", + 'week': "AND r.started_at > NOW() - INTERVAL '7 days'", + 'month': "AND r.started_at > NOW() - INTERVAL '30 days'" + } + time_filter = time_filter_map.get(time_range, "AND r.started_at > NOW() - INTERVAL '24 hours'") + + sound_filter = "" + if sound_types and len(sound_types) > 0: + sound_list = "'" + "','".join(sound_types) + "'" + sound_filter = f"AND fa.head_pred_label IN ({sound_list})" + + query = f""" + SELECT + date_trunc('hour', r.started_at) + + INTERVAL '{bucket_interval} hours' * + (EXTRACT(hour FROM r.started_at)::int / {bucket_interval}) as time_bucket, + 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} + {sound_filter} + GROUP BY time_bucket, fa.head_pred_label + ORDER BY time_bucket ASC, count DESC + """ + + return self.run_query(query) + + def get_audio_heatmap(self, time_range: str = 'week', sound_types: List[str] = None) -> List[Dict]: + """Get audio detection heatmap data - hour of day vs day of week""" + time_filter_map = { + 'day': "AND r.started_at > NOW() - INTERVAL '24 hours'", + 'week': "AND r.started_at > NOW() - INTERVAL '7 days'", + 'month': "AND r.started_at > NOW() - INTERVAL '30 days'" + } + time_filter = time_filter_map.get(time_range, "AND r.started_at > NOW() - INTERVAL '7 days'") + + sound_filter = "" + if sound_types and len(sound_types) > 0: + sound_list = "'" + "','".join(sound_types) + "'" + sound_filter = f"AND fa.head_pred_label IN ({sound_list})" + + query = f""" + SELECT + EXTRACT(HOUR FROM r.started_at) as hour_of_day, + EXTRACT(DOW FROM r.started_at) as day_of_week, + fa.head_pred_label as sound_type, + 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} + {sound_filter} + GROUP BY hour_of_day, day_of_week, fa.head_pred_label + ORDER BY day_of_week, hour_of_day + """ + + return self.run_query(query) + + def get_audio_correlations(self, time_range: str = 'day', sound_types: List[str] = None) -> List[Dict]: + """Get sound detection data for correlation analysis using linked_time from sound_new_sounds_connections""" + bucket_interval = { + 'day': 1, + 'week': 6, + 'month': 24 + }.get(time_range, 1) + + time_filter_map = { + 'day': "AND c.linked_time > NOW() - INTERVAL '24 hours'", + 'week': "AND c.linked_time > NOW() - INTERVAL '7 days'", + 'month': "AND c.linked_time > NOW() - INTERVAL '30 days'" + } + time_filter = time_filter_map.get(time_range, "AND c.linked_time > NOW() - INTERVAL '24 hours'") + + sound_filter = "" + if sound_types and len(sound_types) > 0: + sound_list = "'" + "','".join(sound_types) + "'" + sound_filter = f"AND fa.head_pred_label IN ({sound_list})" + + query = f""" + SELECT + (date_trunc('hour', c.linked_time) + - (INTERVAL '1 hour' * (EXTRACT(hour FROM c.linked_time)::int % {bucket_interval})) + ) AS time_bucket, + fa.head_pred_label AS sound_type, + COUNT(*) AS detection_count + FROM agcloud_audio.file_aggregates fa + JOIN public.sound_new_sounds_connections c + ON c.id = fa.file_id + WHERE fa.head_pred_label IS NOT NULL + {time_filter} + {sound_filter} + GROUP BY time_bucket, fa.head_pred_label + ORDER BY time_bucket ASC + """ + + return self.run_query(query) + + def get_model_health_metrics(self, time_range: str = 'day', sound_types: List[str] = None) -> List[Dict]: + """Get model health metrics over time""" + bucket_interval = { + 'day': 1, + 'week': 6, + 'month': 24 + }.get(time_range, 1) + + time_filter_map = { + 'day': "AND r.started_at > NOW() - INTERVAL '24 hours'", + 'week': "AND r.started_at > NOW() - INTERVAL '7 days'", + 'month': "AND r.started_at > NOW() - INTERVAL '30 days'" + } + time_filter = time_filter_map.get(time_range, "AND r.started_at > NOW() - INTERVAL '24 hours'") + + sound_filter = "" + if sound_types and len(sound_types) > 0: + sound_list = "'" + "','".join(sound_types) + "'" + sound_filter = f"AND fa.head_pred_label IN ({sound_filter})" + + query = f""" + SELECT + date_trunc('hour', r.started_at) + + INTERVAL '{bucket_interval} hours' * + (EXTRACT(hour FROM r.started_at)::int / {bucket_interval}) as time_bucket, + AVG(fa.head_pred_prob) as avg_confidence, + AVG(fa.processing_ms) as avg_processing_ms, + COUNT(*) as total_predictions, + SUM(CASE WHEN fa.head_is_another = true THEN 1 ELSE 0 END) as unknown_count, + (SUM(CASE WHEN fa.head_is_another = true THEN 1 ELSE 0 END)::float / + NULLIF(COUNT(*), 0)) * 100 as error_rate_pct + 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} + {sound_filter} + GROUP BY time_bucket + ORDER BY time_bucket ASC + """ + + return self.run_query(query) + - def list_devices(self, model: str | None = None) -> list[dict]: - - url = f"{self.base}/api/devices" - if model: - url += f"?model={model}" + # ===================================================== + # ===== 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: - r = self.http.get(url, timeout=10) - if r.status_code == 200: - return r.json() - print(f"[API ERROR] {r.status_code}: {r.text[:100]}") + 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"[API FAIL] {e}") - return [] \ No newline at end of file + 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 {} diff --git a/GUI/src/vast/desktop/Dockerfile b/GUI/src/vast/desktop/Dockerfile index 10d413e67..d60b2dc24 100644 --- a/GUI/src/vast/desktop/Dockerfile +++ b/GUI/src/vast/desktop/Dockerfile @@ -1,6 +1,8 @@ FROM python:3.11-slim ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 WORKDIR /app + +# ───────────────────── system deps ───────────────────── 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 \ @@ -10,24 +12,26 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ 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 \ - && rm -rf /var/lib/apt/lists/* + procps iproute2 xauth git vlc libvlc5 libvlccore9 \ + fonts-dejavu-core fonts-noto-core fonts-noto-color-emoji && \ + rm -rf /var/lib/apt/lists/* + +# 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/* + 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; \ + 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 ───────────────────── 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 && \ @@ -35,22 +39,54 @@ RUN mkdir -p /opt && \ rm /tmp/novnc.tar.gz && \ git clone --depth 1 https://github.com/novnc/websockify /opt/noVNC/utils/websockify +# ───────────────────── PulseAudio FIX ───────────────────── +RUN apt-get update && apt-get install -y --no-install-recommends pulseaudio && \ + mkdir -p /etc/pulse && \ + echo "load-module module-native-protocol-unix" >> /etc/pulse/default.pa && \ + echo "load-module module-always-sink" >> /etc/pulse/default.pa && \ + echo "set-default-sink default" >> /etc/pulse/default.pa + +RUN mkdir -p /run/user/1000 && chmod -R 777 /run/user/1000 + +# ───────────────────── Python deps ───────────────────── COPY requirements.txt /app/requirements.txt RUN pip install --no-cache-dir -r requirements.txt -RUN useradd -m -s /bin/bash appuser \ - && mkdir -p /app /tmp/.X11-unix \ - && chown -R appuser:appuser /app /tmp /opt/noVNC /var/tmp +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" \ + argon2-cffi requests numpy \ + --extra-index-url https://pypi.org/simple \ + --prefer-binary \ + --break-system-packages \ + && pip show PyQt6 PyQt6-WebEngine argon2-cffi + --prefer-binary --break-system-packages +RUN pip install plotly 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/* +RUN pip install psycopg2-binary 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 chmod +x /app/start.sh && chown -R appuser:appuser /app +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 + +RUN mkdir -p /run/user/1000 && chmod -R 777 /run/user/1000 + 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/GUI/src/vast/desktop/start.sh b/GUI/src/vast/desktop/start.sh index cd4ca25f9..12884fe5d 100644 --- a/GUI/src/vast/desktop/start.sh +++ b/GUI/src/vast/desktop/start.sh @@ -5,6 +5,10 @@ set -x export DISPLAY=:0 rm -f /tmp/.X0-lock +echo "[INFO] Starting PulseAudio..." +pulseaudio --start --exit-idle-time=-1 --log-target=stderr +sleep 1 + echo "[INFO] Starting Xvfb..." Xvfb :0 -screen 0 1920x1080x24 & sleep 3 diff --git a/GUI/src/vast/dsl/builder.py b/GUI/src/vast/dsl/builder.py index 93ab0785e..cf17cf985 100644 --- a/GUI/src/vast/dsl/builder.py +++ b/GUI/src/vast/dsl/builder.py @@ -24,13 +24,11 @@ class SQLState: 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"]) # extend externally + 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) @@ -75,10 +73,19 @@ def compile(self, plan: Plan | Dict[str, Any]) -> tuple[str, List[Any]]: 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 set(op.keys()) - {"op", "columns", "cond"}: + 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 + + return st.build() diff --git a/GUI/src/vast/dsl/clauses.py b/GUI/src/vast/dsl/clauses.py index f2d0f225b..d2f2ecf48 100644 --- a/GUI/src/vast/dsl/clauses.py +++ b/GUI/src/vast/dsl/clauses.py @@ -27,12 +27,23 @@ 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 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 = [] @@ -42,11 +53,13 @@ def star_aware_quote(col: str) -> str: 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 @@ -65,4 +78,78 @@ 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) \ No newline at end of file + 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 "(" in col or ")" in col: + return col + 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/GUI/src/vast/dsl/dialects.py b/GUI/src/vast/dsl/dialects.py index 078ec65cc..b83ce6c9f 100644 --- a/GUI/src/vast/dsl/dialects.py +++ b/GUI/src/vast/dsl/dialects.py @@ -35,19 +35,33 @@ 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) + def __init__(self, style: str = "named"): """ - - if style not in ("psycopg", "numeric"): - raise ValueError("PostgresDialect.style must be 'psycopg' or 'numeric'") + 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(".") - return ".".join('"' + p.replace('"', '""') + '"' for p in parts) + 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 # PostgreSQL has a real boolean type + return v + def placeholder(self, idx: int) -> str: - return "%s" if self.style == "psycopg" else f"${idx}" \ No newline at end of file + if self.style == "psycopg": + return "%s" + elif self.style == "numeric": + return f"${idx}" + else: # named + return f":p{idx}" diff --git a/GUI/src/vast/dsl/expr.py b/GUI/src/vast/dsl/expr.py index 03abf40d1..d9c41aff3 100644 --- a/GUI/src/vast/dsl/expr.py +++ b/GUI/src/vast/dsl/expr.py @@ -47,8 +47,37 @@ def to_ir(self) -> Dict[str, Any]: return {"col": self.name} class Literal(Expr): """A literal value that becomes a bound parameter (with a placeholder).""" value: Any - def compile(self, ctx: CompileCtx) -> str: return ctx.add_param(self.value) - def to_ir(self) -> Dict[str, Any]: return {"literal": self.value} + + 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} + +@dataclass +class Func(Expr): + name: str + args: list[Expr] + + def compile(self, ctx: CompileCtx) -> str: + compiled = ", ".join(arg.compile(ctx) for arg in self.args) + return f"{self.name.upper()}({compiled})" + + def to_ir(self) -> Dict[str, Any]: + return { + "func": self.name, + "args": [arg.to_ir() for arg in self.args] + } + + def ensure_expr(x: Any) -> Expr: """Coerce Python values to Literal, leave Expr as-is.""" @@ -72,7 +101,7 @@ class Predicate(Cond): op: BinOp right: Expr def __post_init__(self): - if not isinstance(self.left, (Col, Literal)) or not isinstance(self.right, (Col, Literal)): + if not isinstance(self.left, (Col, Literal,Func)) or not isinstance(self.right, (Col, Literal,Func)): 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)})" @@ -96,34 +125,76 @@ 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") + 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}") + + if keys == {"col"}: + return Col(d["col"]) + + if keys == {"literal"}: + return Literal(d["literal"]) + + if keys == {"func", "args"}: + return Func( + d["func"], + [expr_from_ir(arg) for arg in d["args"]] + ) + + raise ValueError(f"Invalid Expr IR: {d}") + def cond_from_ir(d: Dict[str, Any]) -> Cond: - """Decode a strict Cond IR object into Cond.""" - if not isinstance(d, dict): raise TypeError("Cond node must be an object") + """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("all must be an array of Cond") + 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("any must be an array of Cond") + 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"]) # raises ValueError if unknown -> nice error + 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 +OR = Any diff --git a/GUI/src/vast/dsl/ops.py b/GUI/src/vast/dsl/ops.py index 0cbc7678d..cc66239c0 100644 --- a/GUI/src/vast/dsl/ops.py +++ b/GUI/src/vast/dsl/ops.py @@ -7,7 +7,7 @@ from __future__ import annotations from typing import Any, Dict, List, Type from abc import ABC, abstractmethod -from .clauses import SelectClause, WhereClause +from .clauses import SelectClause, WhereClause,OrderByClause,LimitClause,HavingClause,OffsetClause,GroupByClause from .expr import cond_from_ir # from .builder import SQLState @@ -28,11 +28,55 @@ def apply(self, st: "SQLState") -> None: class SelectOp(Op): op_type = "select" def apply(self, st: "SQLState") -> None: - cols = self.payload.get("columns") - st.add_select(cols or []) + cols = self.payload.get("columns", []) + st.add_clause(SelectClause(cols)) + class WhereOp(Op): op_type = "where" def apply(self, st: "SQLState") -> None: - cond_ir: Dict[str, Any] = self.payload["cond"] - st.add_where(cond_from_ir(cond_ir)) + cond_ir = self.payload.get("cond") + if not isinstance(cond_ir, dict): + raise TypeError("Invalid WHERE condition") + expr = cond_from_ir(cond_ir) + st.add_clause(WhereClause(expr)) + + + +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/GUI/src/vast/dsl/query.py b/GUI/src/vast/dsl/query.py index 12d5457f7..b21965bae 100644 --- a/GUI/src/vast/dsl/query.py +++ b/GUI/src/vast/dsl/query.py @@ -26,6 +26,28 @@ def where(self, cond: Cond) -> "Query": 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 diff --git a/GUI/src/vast/dsl/runtime.py b/GUI/src/vast/dsl/runtime.py index 20fcc8aef..15fdaefb8 100644 --- a/GUI/src/vast/dsl/runtime.py +++ b/GUI/src/vast/dsl/runtime.py @@ -9,10 +9,13 @@ class CompileCtx: def __init__(self, dialect: Dialect) -> None: - self.params: List[Any] = [] # Accumulated bound parameters in order of appearance. + self.params: dict[str, Any] = {} self.dialect = dialect def add_param(self, v: Any) -> str: - """Append a value (after dialect-specific normalization) and return its placeholder.""" - self.params.append(self.dialect.normalize_bool(v)) - return self.dialect.placeholder(len(self.params)) + """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/GUI/src/vast/gateway/Dockerfile b/GUI/src/vast/gateway/Dockerfile index 27486d095..29549ecba 100644 --- a/GUI/src/vast/gateway/Dockerfile +++ b/GUI/src/vast/gateway/Dockerfile @@ -1,27 +1,48 @@ +# syntax=docker/dockerfile:1.6 + FROM python:3.11-slim -ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 -WORKDIR /app +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; \ +# 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/* + +# Optional extra CA certs (e.g. NetFree) from ./certs +COPY certs/ /tmp/certs + +RUN 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/; \ + chmod 644 /usr/local/share/ca-certificates/*.crt; \ + update-ca-certificates; \ + else \ + echo "No extra CA certs configured (USE_NETFREE=${USE_NETFREE})."; \ fi -# SSL certs env +# 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 +ENV PIP_DEFAULT_TIMEOUT=600 -# # System CA + add NetFree certs +# # # 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 @@ -37,16 +58,17 @@ RUN pip install --no-cache-dir -r /app/requirements.txt \ --trusted-host pypi.python.org \ --trusted-host files.pythonhosted.org -# Copy source under /app/vast/* +# App code COPY src/vast/gateway /app/vast/gateway COPY src/vast/proto /app/vast/proto -# Generate stubs +# 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/GUI/src/vast/home_view.py b/GUI/src/vast/home_view.py index c7a5a6cc6..b82ef2ef1 100644 --- a/GUI/src/vast/home_view.py +++ b/GUI/src/vast/home_view.py @@ -1,28 +1,56 @@ from __future__ import annotations from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtCore import QUrl, pyqtSignal -from PyQt6.QtWidgets import QWidget, QGridLayout, QVBoxLayout, QLabel, QSizePolicy, QPushButton +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, parent: QWidget | None = None): + 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;") + header.setStyleSheet("font-size: 20px; font-weight: 600; margin-bottom: 8px;") root.addWidget(header) - grid = QGridLayout() - grid.setHorizontalSpacing(12) - grid.setVerticalSpacing(12) - root.addLayout(grid) + # ───────────────────────────── + # 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" @@ -31,20 +59,20 @@ def __init__(self, api, parent: QWidget | None = None): QUrl(f"{base}/d-solo/agcloud-sensors/sensors?orgId=1&panelId=2&from=now-6h&to=now&refresh=10s&theme=light"), ] - for i, url in enumerate(panel_urls): + for url in panel_urls: view = QWebEngineView(self) - view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + view.setFixedHeight(300) view.setUrl(url) - r, c = divmod(i, 2) - grid.addWidget(view, r, c) - - tiles_root = "./src/vast/orthophoto_canvas/data/tiles" - self.viewer = create_orthophoto_viewer(tiles_root, forced_scheme=None, parent=self) - grid.addWidget(self.viewer, 1, 0, 1, 2) + 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, @@ -53,6 +81,96 @@ def __init__(self, api, parent: QWidget | None = None): 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/GUI/src/vast/main.py b/GUI/src/vast/main.py index 394e23859..4517b96d6 100644 --- a/GUI/src/vast/main.py +++ b/GUI/src/vast/main.py @@ -100,36 +100,37 @@ def main() -> int: print("[main] starting QApplication") app = QApplication(sys.argv) - # 1) show auth shell first + + # 1) create the auth shell but do NOT show it shell = AuthShell() shell.setWindowTitle("Sign in") - shell.show() + # shell.show() # disabled to skip the login window + # 2) when login succeeds -> open MainWindow def open_main(user): - api = DashboardApi() # pass user if needed + 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/GUI/src/vast/main_window.py b/GUI/src/vast/main_window.py index d7f815745..726b129c6 100644 --- a/GUI/src/vast/main_window.py +++ b/GUI/src/vast/main_window.py @@ -1,16 +1,938 @@ -from __future__ import annotations +# # 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() + +# from PyQt6.QtCore import Qt, pyqtSignal, QSize +# from PyQt6.QtWidgets import ( +# QMainWindow, QDockWidget, QListWidget, QListWidgetItem, QStatusBar, +# QStackedWidget, QToolButton, QLabel, QWidget, QHBoxLayout, QVBoxLayout, +# QGraphicsDropShadowEffect, QPushButton +# ) +# 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.ground_view import GroundView +# from views.auth_status_view import AuthStatusView +# from dashboard_api import DashboardApi +# from vast.alerts.alert_service import AlertService + +# # === 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.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 +# # ─────────────────────────────── +# 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.triggered.connect(self._logout) +# file_menu.addAction(self.logout_action) + +# # ─────────────────────────────── +# # 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 +# self.alert_button = QToolButton() +# self.alert_button.setToolTip("Show alerts") +# self.alert_button.setText("🔔") +# self.alert_button.setIconSize(QSize(40, 40)) +# self.alert_button.setStyleSheet(""" +# QToolButton { +# font-size: 30px; +# border: none; +# background: transparent; +# padding: 4px; +# border-radius: 8px; +# } +# QToolButton:hover { background-color: #e5e7eb; } +# """) + +# # 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.setStyleSheet(""" +# QLabel { +# background-color: #3b82f6; +# color: white; +# font-size: 10pt; +# font-weight: bold; +# 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.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 +# # ─────────────────────────────── +# 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) + +# 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"]: +# 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) + +# # ─────────────────────────────── +# # 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-radius: 10px; +# } +# """) +# self.alerts_panel.hide() +# self.alert_button.clicked.connect(self.toggle_alert_panel) + +# # ─────────────────────────────── +# # 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.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, +# "Sensors - Live Data": self.sensors_status_summary, +# "Sensors - Sensor Health": self.sensors_health, +# "Sensors - Location Map": self.sensors_main, +# "Notifications": self.notification_view, +# "Fruits": self.fruits_view, +# "Ground Image": self.ground_view, +# "Auth": self.auth_status +# } + +# for view in self.views.values(): +# self.stack.addWidget(view) +# self.stack.setCurrentWidget(self.home) +# self.history = [] + +# # ─────────────────────────────── +# # STATUS BAR +# # ─────────────────────────────── +# sb = QStatusBar(self) +# sb.setStyleSheet("QStatusBar { background-color: #f3f4f6; color: #374151; font-size: 10.5pt; }") +# self.setStatusBar(sb) +# sb.showMessage("Ready") + +# # ─────────────────────────────── +# # 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 +# 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 +# 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 +# # ─────────────────────────────── +# def _on_nav_change(self, row: int) -> None: +# 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 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() + from PyQt6.QtCore import Qt, pyqtSignal, QSize from PyQt6.QtWidgets import ( - QMainWindow, QDockWidget, QListWidget, QListWidgetItem, QStatusBar, QStackedWidget, - QVBoxLayout, QWidget + QMainWindow, QDockWidget, QListWidget, QListWidgetItem, QStatusBar, + QStackedWidget, QToolButton, QLabel, QWidget, QHBoxLayout, QVBoxLayout, + QGraphicsDropShadowEffect, QPushButton ) -from PyQt6.QtGui import QAction, QIcon -from PyQt6.QtWebEngineWidgets import QWebEngineView -from PyQt6.QtCore import QUrl +from PyQt6.QtGui import QAction, QIcon, QFont, QColor +import os + from home_view import HomeView from views.sensors_view import SensorsView -from views.sound_view import SoundView +from views.security.incident_player_vlc import IncidentPlayerVLC +from views.alerts_panel import AlertsPanel +from views.notification_view import NotificationView +from views.fruits_view import FruitsView from dashboard_api import DashboardApi +from vast.alerts.alert_service import AlertService + + + +# === Irrigation imports === +from views.irrigation.irrigation_view import IrrigationView class MainWindow(QMainWindow): @@ -19,72 +941,345 @@ class MainWindow(QMainWindow): def __init__(self, api: DashboardApi, parent=None): super().__init__(parent) self.setWindowTitle("VAST – Dashboard") - self.resize(1100, 700) + self.resize(1280, 760) self.api = api - # ---------- 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") - - # ---------- Dock navigation ---------- + 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.triggered.connect(self._logout) + file_menu.addAction(self.logout_action) + + # ─────────────────────────────── + # TOP BAR + # ─────────────────────────────── + 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) + + # Back button + back_btn = QToolButton() + back_btn.setIcon(QIcon.fromTheme("go-previous")) + back_btn.setIconSize(QSize(28, 28)) + back_btn.setToolTip("Go back") + back_btn.clicked.connect(self.go_back) + + # 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) + + # Bell button + self.alert_button = QToolButton() + self.alert_button.setToolTip("Show alerts") + self.alert_button.setText("🔔") + self.alert_button.setIconSize(QSize(40, 40)) + self.alert_button.setStyleSheet(""" + QToolButton { + font-size: 30px; + border: none; + background: transparent; + padding: 4px; + border-radius: 8px; + } + QToolButton:hover { + background-color: #e5e7eb; + } + """) + + # Larger blue 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.setStyleSheet(""" + QLabel { + background-color: #3b82f6; /* blue */ + color: white; + font-size: 10pt; + font-weight: bold; + border-radius: 12px; + border: 2px solid white; + } + """) + self.alert_badge.hide() + + # Position badge dynamically + def reposition_badge(): + btn_w = self.alert_button.width() + 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 + title_label = QLabel("VAST Dashboard") + title_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + title_label.setStyleSheet(""" + QLabel { + font-size: 17pt; + font-weight: 600; + color: #111827; + } + """) + + # Shadow + 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_label) + top_bar_layout.addStretch() + + toolbar.addWidget(top_bar) + + # ─────────────────────────────── + # NAVIGATION DOCK + # ─────────────────────────────── 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) + + font = QFont() + font.setPointSize(12) + self.nav_list.setFont(font) for name in [ - "Home", "Sensors", "Sound", "Ground Image", - "Aerial Image", "Fruits", "Security", "Settings" + "Home", "Sensors", "Sound", "Ground Image", + "Aerial Image", "Fruits", "Security", "Settings", "Notifications", "Irrigation" ]: - QListWidgetItem(name, self.nav_list) - + QListWidgetItem(f" {name}", self.nav_list) + self.nav_list.setCurrentRow(0) self.nav_list.currentRowChanged.connect(self._on_nav_change) - # ---------- Views ---------- - self.home = HomeView(api, self) + # ─────────────────────────────── + # ALERT SERVICE + # ─────────────────────────────── + 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()) + + # Alerts panel + 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-radius: 10px; + } + """) + self.alerts_panel.hide() + self.alert_button.clicked.connect(self.toggle_alert_panel) + + # ─────────────────────────────── + # CENTRAL STACKED VIEWS + # ─────────────────────────────── + self.home = HomeView(api, self.alert_service, self) self.sensors_view = SensorsView(api, self) - self.sound_view = SoundView(self) + self.notification_view = NotificationView(self) + self.security_view = IncidentPlayerVLC(api, self.alert_service, self) + self.fruits_view = FruitsView(api, 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) + print("[DEBUG] Creating IrrigationView...") + try: + self.irrigation_view = IrrigationView(api, self) + print("[DEBUG] IrrigationView created successfully") + except Exception as e: + print(f"[ERROR] Failed to create IrrigationView: {e}") + import traceback + traceback.print_exc() + self.irrigation_view = QWidget() # Fallback empty widget - # Stack for switching between views self.stack = QStackedWidget() self.setCentralWidget(self.stack) - + self.views = { "Home": self.home, "Sensors": self.sensors_view, - "Sound": self.sound_view, + "Notifications": self.notification_view, + "Security": self.security_view, + "Fruits": self.fruits_view, + "Ground Image": self.ground_view, + "Irrigation": self.irrigation_view, + "Auth": self.auth_status + } - for view in self.views.values(): self.stack.addWidget(view) - self.stack.setCurrentWidget(self.home) + self.history: list = [] - # ---------- History for Back ---------- - self.history = [] - - # ---------- Status bar ---------- + # ─────────────────────────────── + # STATUS BAR + # ─────────────────────────────── sb = QStatusBar(self) + sb.setStyleSheet(""" + QStatusBar { + background-color: #f3f4f6; + color: #374151; + font-size: 10.5pt; + } + """) self.setStatusBar(sb) sb.showMessage("Ready") + # ─────────────────────────────── + # 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 + 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 + 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 + # ─────────────────────────────── def _on_nav_change(self, row: int) -> None: - name = self.nav_list.item(row).text() - print(f"[MainWindow] Navigation changed to: {name}") - + name = self.nav_list.item(row).text().strip() + print(f"[DEBUG] _on_nav_change: row={row}, name='{name}'") + print(f"[DEBUG] Available views: {list(self.views.keys())}") if name in self.views: + print(f"[DEBUG] Navigating to '{name}'") self.navigate_to(self.views[name]) else: + print(f"[DEBUG] Section '{name}' not found in views") self.statusBar().showMessage(f"Section '{name}' not implemented yet.") def navigate_to(self, widget): - print(f"[MainWindow] Navigating to widget: {widget.__class__.__name__}") + print(f"[DEBUG] navigate_to called with widget: {widget.__class__.__name__}") current = self.stack.currentWidget() if current not in self.history: self.history.append(current) + print(f"[DEBUG] Setting current widget to: {widget.__class__.__name__}") self.stack.setCurrentWidget(widget) + print(f"[DEBUG] Current widget is now: {self.stack.currentWidget().__class__.__name__}") def go_back(self): if self.history: @@ -95,4 +1290,4 @@ def go_back(self): def _logout(self) -> None: self.statusBar().showMessage("Logged out (demo)") - self.logoutRequested.emit() + self.logoutRequested.emit() \ No newline at end of file diff --git a/GUI/src/vast/orthophoto_canvas/ui/alert_layer.py b/GUI/src/vast/orthophoto_canvas/ui/alert_layer.py new file mode 100644 index 000000000..defc7612e --- /dev/null +++ b/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/GUI/src/vast/orthophoto_canvas/ui/fields.png b/GUI/src/vast/orthophoto_canvas/ui/fields.png new file mode 100644 index 000000000..a279cc08d Binary files /dev/null and b/GUI/src/vast/orthophoto_canvas/ui/fields.png differ diff --git a/GUI/src/vast/orthophoto_canvas/ui/sensors_layer.py b/GUI/src/vast/orthophoto_canvas/ui/sensors_layer.py index 854610688..4ef7808d6 100644 --- a/GUI/src/vast/orthophoto_canvas/ui/sensors_layer.py +++ b/GUI/src/vast/orthophoto_canvas/ui/sensors_layer.py @@ -311,6 +311,43 @@ def _latlon_to_base_xy_if_inside(viewer, lat: float, lon: float, z: int = None) 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]: @@ -368,4 +405,4 @@ def tile2lat(y): 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) \ No newline at end of file + return (lat_min, lat_max, lon_min, lon_max) diff --git a/GUI/src/vast/orthophoto_canvas/ui/viewer.py b/GUI/src/vast/orthophoto_canvas/ui/viewer.py index c83ffb827..ef2c122c9 100644 --- a/GUI/src/vast/orthophoto_canvas/ui/viewer.py +++ b/GUI/src/vast/orthophoto_canvas/ui/viewer.py @@ -1,122 +1,210 @@ -# agcloud/ui/viewer.py 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 Iterable, List, Optional, Tuple, Union +from typing import Optional, Tuple, Union -from PyQt6.QtCore import Qt, QRectF, QPointF, QTimer -from PyQt6.QtGui import QPixmap, QPainter, QPen, QColor, QBrush +from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtGui import QPixmap, QPainter, QPen, QColor from PyQt6.QtWidgets import ( - QGraphicsView, - QGraphicsScene, - QGraphicsPixmapItem, - QGraphicsRectItem, - QGraphicsEllipseItem, - QToolTip + QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsRectItem ) - +from PyQt6.QtGui import QPixmap, QTransform # ==== Tunables ==== TILE_SIZE = 512 -INITIAL_TILE_PX = 640.0 # initial "tile-size on screen" (px) to compute first zoom -TARGET_TILE_PX_FOR_LOD = 512.0 # target tile size (px) used to pick which z to load -SNAP_CHOICES = (512.0, 384.0, 320.0, 256.0, 192.0, 128.0) # preferred tile sizes on screen to avoid blur +TARGET_TILE_PX_FOR_LOD = 512.0 +SNAP_CHOICES = (512.0, 384.0, 320.0, 256.0, 192.0, 128.0) class OrthophotoViewer(QGraphicsView): - """ - QGraphicsView that renders a pyramidal tile set with lazy loading + LOD, - using a provided TileSet (I/O abstraction). Optionally draws sensor markers. - """ + """Stable orthophoto tile viewer that perfectly fits its container.""" - # ---------- Construction / scene bootstrapping ---------- def __init__(self, tiles: Union[TileStore, str, Path]) -> None: - """ - tileset: object that exposes min_zoom, max_zoom, z_ranges[z], tile_path(z,x,y) - (see agcloud/utils/tiles) - """ super().__init__() + + + # ───────────────────────────── +# Load tiles +# ───────────────────────────── if isinstance(tiles, TileStore): self.ts = tiles else: - self.ts = TileStore(Path(tiles)) - - self.ts.existing_zooms = self.ts.existing_zooms - 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 + 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'}") - self.scene = QGraphicsScene(self) - self.setScene(self.scene) - # Crisp rendering (no smoothing) - - self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform, False) - self.setRenderHint(QPainter.RenderHint.Antialiasing, False) - self.setRenderHint(QPainter.RenderHint.TextAntialiasing, False) - # Interaction / performance + # ───────────────────────────── + # 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) - self.setBackgroundBrush(QColor(220, 220, 220)) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + + # 🔹 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) - - # State - self.current_zoom = self.ts.min_zoom - self.placeholder_color = Qt.GlobalColor.lightGray + + # ───────────────────────────── + # Internal state + # ───────────────────────────── + self.current_zoom = self.ts.max_zoom + self.placeholder_color = QColor("#d1d5db") self.tile_items: dict[Tuple[int, int, int], QGraphicsPixmapItem | QGraphicsRectItem] = {} - # Debounce loads after interaction + # ───────────────────────────── + # Timed updates + # ───────────────────────────── self.update_timer = QTimer(self) self.update_timer.setSingleShot(True) self.update_timer.timeout.connect(self.update_tiles) - self.sensor_layer = SensorLayer(self) + # ───────────────────────────── + # Optional sensor overlay + # ───────────────────────────── + self.sensor_layer = SensorLayer(self) dataset_bbox_latlon(self, z=self.max_zoom_fs) - add_sensors_by_gps_bulk(self.sensor_layer, get_sensors(), z=self.max_zoom_fs, default_radius_px=0.2) - - # Scene rect anchored to base zoom (min z) - self._init_scene_rect_from_min_zoom() + + 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._custom_bg_item: Optional[QGraphicsPixmapItem] = None + self._tiles_visible: bool = True + self.update_tiles() - self._did_initial_zoom = False + def _apply_tile_visibility(self): + """Apply visibility/opacity preference to existing tile items.""" + for item in self.tile_items.values(): + # You can choose to hide or fade; here we just hide/show them. + item.setVisible(self._tiles_visible) + # If you prefer fading: + # item.setOpacity(0.2 if not self._tiles_visible else 1.0) - # First load - self.update_tiles() + - def _init_scene_rect_from_min_zoom(self) -> None: + def set_custom_background_image(self, path: str, hide_tiles: bool = False): """ - Build scene rect from the min zoom range (anchor for all z levels). + Place a single static image as the map background, scaled to the scene extents. + It will zoom & pan together with all other items. """ - z0 = self.ts.min_zoom - x_min, x_max, y_min, y_max = self.ts.z_ranges[z0] + pix = QPixmap(path) + p = Path(path) + print("[OrthophotoViewer] Exists?", p.exists()) + if pix.isNull(): + print(f"[OrthophotoViewer] ❌ Failed to load background image: {path}") + return + + # Remove previous bg if exists + if self._custom_bg_item is not None: + self.scene.removeItem(self._custom_bg_item) + self._custom_bg_item = None + + scene_rect = self.scene.sceneRect() + width = scene_rect.width() + height = scene_rect.height() + + item = QGraphicsPixmapItem(pix) + item.setZValue(-1000) # behind tiles, regions, sensors + + # Scale to fill the entire scene rect + sx = width / pix.width() if pix.width() > 0 else 1.0 + sy = height / pix.height() if pix.height() > 0 else 1.0 + item.setTransform(QTransform().scale(sx, sy)) + + # Position at the scene rect origin (you use a small margin, so respect that) + item.setPos(scene_rect.left(), scene_rect.top()) + + self.scene.addItem(item) + self._custom_bg_item = item + + if hide_tiles: + self._tiles_visible = False + self._apply_tile_visibility() + + # ───────────────────────────── + # 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 - self.scene.setSceneRect(0, 0, width, height) - print(f"[BASE] z={z0} X:[{x_min}-{x_max}] Y:[{y_min}-{y_max}] scene={width}x{height}px") - # ---------- Sensors overlay (optional) ---------- - def set_sensors(self, sensors: list[dict]): - self.sensor_layer.clear() - add_sensors_by_gps_bulk(self.sensor_layer, sensors, z=self.max_zoom_fs, center_on_first=True) - - def clear_sensors(self) -> None: - """Remove all sensor markers from scene.""" - self.sensor_layer.clear() + # 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() + - # ---------- Interaction ---------- def wheelEvent(self, event) -> None: + """Zoom with mouse wheel.""" factor = 1.25 if event.angleDelta().y() > 0: self.scale(factor, factor) @@ -124,97 +212,49 @@ def wheelEvent(self, event) -> None: self.scale(1.0 / factor, 1.0 / factor) self._debounced_update() - def resizeEvent(self, event) -> None: - super().resizeEvent(event) - 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 keyPressEvent(self, event) -> None: - k = event.key() - if k == Qt.Key.Key_C: - self._snap_to_native_scale() - return - if k == Qt.Key.Key_F: - self._fit_data_width() - return - if k in (Qt.Key.Key_1, Qt.Key.Key_2, Qt.Key.Key_3, Qt.Key.Key_4, Qt.Key.Key_5): - choices = { - Qt.Key.Key_1: 192.0, - Qt.Key.Key_2: 256.0, - Qt.Key.Key_3: 320.0, - Qt.Key.Key_4: 384.0, - Qt.Key.Key_5: 512.0, - } - self._smart_initial_focus(target_tile_px=choices[k], found=self._best_focus_tile()) - return - super().keyPressEvent(event) - def _debounced_update(self) -> None: self.update_timer.start(50) - def showEvent(self, e): - super().showEvent(e) - if getattr(self, "_did_initial_fit", False): - return - self._did_initial_fit = True - QTimer.singleShot(0, lambda: self.fit_to_data("width", 0.98)) - - def resizeEvent(self, e): - super().resizeEvent(e) - if not getattr(self, "_did_initial_zoom", False): - self._did_initial_zoom = True - self.fit_to_data("width", 40.0) - # self.zoom_to_tile_px(768.0, at_z="max") - self._debounced_update() - - # ---------- LOD: choose z, load visible tiles ---------- + # ───────────────────────────── + # Level-of-detail (LOD) + # ───────────────────────────── def _calc_zoom_level(self) -> int: - """ - Decide which z to load: pick z so that (tile on screen) ~= TARGET_TILE_PX_FOR_LOD. - """ - scale = max(self.transform().m11(), 1e-6) - z_base = self.ts.min_zoom - zf = z_base + math.log2((scale * float(TILE_SIZE)) / TARGET_TILE_PX_FOR_LOD) - z = int(round(zf)) - return max(self.ts.min_zoom, min(self.ts.max_zoom, z)) + """Force max zoom for small coverage sets.""" + return self.ts.max_zoom def update_tiles(self) -> None: - """ - Core: compute visible keys (z,x,y) and ensure each has an item. - Placeholders are created first; then upgraded to true pixmaps (with parent fallback). - """ + """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.min_zoom)) + 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] - # scene → base indices (anchored to min 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)) + 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.min_zoom) + 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 - # clamp to existing range on disk 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 / upgrade + # Create or upgrade for key in want: if key not in self.tile_items: ph = self._create_placeholder_item_at(key, eff_tile_scene) @@ -222,42 +262,37 @@ def update_tiles(self) -> None: self.scene.addItem(ph) self._try_upgrade_tile_to_pixmap(key, eff_tile_scene) - # Unload others + # 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 ---------- + # 🔹 Ensure visibility style is applied to all tiles (including new ones) + self._apply_tile_visibility() + + # ───────────────────────────── + # Tile placement / upgrade + # ───────────────────────────── def _create_placeholder_item_at(self, key: Tuple[int, int, int], eff_tile_scene: float): - """ - Create a light-gray rect exactly where the tile will go (no rounding) to avoid seams. - """ z, x, y = key - scale_factor = 1 << (z - self.ts.min_zoom) + 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(self.placeholder_color) + 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): - """ - Position the pixmap at exact scene coords (anchored to min z) and scale the item, - not the pixmap (keeps the source un-resampled). - """ z, x, y = key - scale_factor = 1 << (z - self.ts.min_zoom) + 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 @@ -267,18 +302,12 @@ def _place_pixmap_item(self, pm: QPixmap, key: Tuple[int, int, int], eff_tile_sc item.setPos(sx, sy) s = eff_tile_scene / float(pm.width()) item.setScale(s) - item.setTransformationMode(Qt.TransformationMode.FastTransformation) + item.setTransformationMode(Qt.TransformationMode.SmoothTransformation) return item def _try_upgrade_tile_to_pixmap(self, key: Tuple[int, int, int], eff_tile_scene: float) -> None: - """ - Replace placeholder by the right pixmap: - - Try native z/x/y - - If missing, climb to parent(s) until min z, crop the relevant quadrant. - """ z, x, y = key - - # climb parents if needed + # 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: @@ -296,7 +325,9 @@ def _try_upgrade_tile_to_pixmap(self, key: Tuple[int, int, int], eff_tile_scene: v = (y % seg) * h pm = pm.copy(u, v, w, h) break - xx //= 2; yy //= 2; zz -= 1 + xx //= 2 + yy //= 2 + zz -= 1 if not pm: return @@ -307,177 +338,3 @@ def _try_upgrade_tile_to_pixmap(self, key: Tuple[int, int, int], eff_tile_scene: item = self._place_pixmap_item(pm, key, eff_tile_scene) self.scene.addItem(item) self.tile_items[key] = item - - # ---------- Smart focus / fit / snap ---------- - def _smart_initial_focus(self, target_tile_px: float, found: Optional[Tuple[int, int, int]], snap=True) -> None: - """ - Pick a zoom/transform so that one tile would appear ~target_tile_px wide on screen, - and center the view around an existing tile (found). - """ - if not found: - # fallback: fit entire base scene width - self.fitInView(self.scene.sceneRect(), Qt.AspectRatioMode.KeepAspectRatio) - return - - z, x, y = found - eff_scene = TILE_SIZE / float(1 << (z - self.ts.min_zoom)) - s = max(target_tile_px / eff_scene, 1e-6) - - self.resetTransform() - self.scale(s, s) - - if snap: - self._snap_to_native_scale() - - # center on that tile - scale_factor = 1 << (z - self.ts.min_zoom) - x0 = self._x_min_base * scale_factor - y0 = self._y_min_base * scale_factor - tx = x - x0 - ty = y - y0 - sx = tx * eff_scene - sy = ty * eff_scene - self.centerOn(sx + eff_scene * 0.5, sy + eff_scene * 0.5) - - self._debounced_update() - print(f"[FOCUS] center @ {z}/{x}/{y} tile≈{target_tile_px:.0f}px") - - def _snap_to_native_scale(self) -> None: - """ - Snap current transform so that tile size on screen equals one of SNAP_CHOICES, - minimizing resampling blur. - """ - center_scene = self.mapToScene(self.viewport().rect().center()) - z = self._calc_zoom_level() - eff_scene = TILE_SIZE / float(1 << (z - self.ts.min_zoom)) - cur_tile = eff_scene * max(self.transform().m11(), 1e-6) - target = min(SNAP_CHOICES, key=lambda t: abs(t - cur_tile)) - s = max(target / eff_scene, 1e-6) - self.resetTransform() - self.scale(s, s) - self.centerOn(center_scene) - self._debounced_update() - - def _fit_data_width(self, margin: float = 0.95) -> None: - """ - Fit the visible data extent at current z to the viewport width (keep slight margin). - """ - z = self._calc_zoom_level() - x_min, x_max, y_min, y_max = self.ts.z_ranges[z] - eff = TILE_SIZE / float(1 << (z - self.ts.min_zoom)) - - scale_factor = 1 << (z - self.ts.min_zoom) - x0 = self._x_min_base * scale_factor - y0 = self._y_min_base * scale_factor - - left = (x_min - x0) * eff - right = (x_max - x0 + 1) * eff - top = (y_min - y0) * eff - bottom = (y_max - y0 + 1) * eff - rect = QRectF(left, top, right - left, bottom - top) - - self.resetTransform() - if rect.width() > 0: - s_w = (self.viewport().width() / rect.width()) * float(margin) - self.scale(s_w, s_w) - self.centerOn(rect.center()) - self._debounced_update() - print(f"[FIT] width={rect.width():.1f}px scene, scale={s_w:.3f}") - - def fit_to_data(self, how="width", margin=0.98): - """Zoom so the dataset fills the viewport (width/height/all).""" - z = self._calc_zoom_level() - self.current_zoom_level = z - if z not in self.ts.z_ranges: - return - - x_min_z, x_max_z, y_min_z, y_max_z = self.ts.z_ranges[z] - - eff = TILE_SIZE / float(1 << (z - self.ts.min_zoom)) - - base_x_min, _, base_y_min, _ = self.ts.z_ranges[self.ts.min_zoom] - scale_factor = 1 << (z - self.ts.min_zoom) - x0 = base_x_min * scale_factor - y0 = base_y_min * scale_factor - - - left = (x_min_z - x0) * eff - top = (y_min_z - y0) * eff - width = (x_max_z - x_min_z + 1) * eff - height = (y_max_z - y_min_z + 1) * eff - rect = QRectF(left, top, width, height) - if rect.width() <= 0 or rect.height() <= 0: - return - - self.resetTransform() - if how == "width": - s = (self.viewport().width() / rect.width()) * margin - elif how == "height": - s = (self.viewport().height() / rect.height()) * margin - else: # "all" - s = min(self.viewport().width()/rect.width(), - self.viewport().height()/rect.height()) * margin - - self.scale(s, s) - self.centerOn(rect.center()) - self._debounced_update() - - # ---------- Find a good starting tile ---------- - def _best_focus_tile(self, prefer_z: Optional[int] = None, max_x_check: int = 64) -> Optional[Tuple[int,int,int]]: - """ - Heuristic: at the highest available z, choose x that is closest to mid-range, - then choose the y closest to mid-range that actually exists. If that x has no y, - try other x (still ordered by proximity to center). If nothing found, fall back to - the first available (z,x,y). - """ - zs = list(range(self.ts.min_zoom, self.ts.max_zoom + 1)) - if not zs: - return None - z = prefer_z if prefer_z is not None else zs[-1] - - try: - x_min, x_max, y_min, y_max = self.ts.ranges(z) - except Exception: - return None - - x0 = (x_min + x_max) // 2 - y0 = (y_min + y_max) // 2 - - # Prefer X near the center - xs = list(range(x_min, x_max + 1)) - xs.sort(key=lambda xv: abs(xv - x0)) - - # helper to list ys for an x (if tileset doesn’t implement list_y, we try a few probes) - def list_y_for_x(x: int) -> List[int]: - if hasattr(self.ts, "list_y"): - return list(getattr(self.ts, "list_y")(z, x)) # type: ignore - # minimal probe: try a small window around y0 - win = 256 - candidates = [] - for yy in (y0, y0-1, y0+1, y0-2, y0+2, y0-4, y0+4, y0-win, y0+win): - p = self.ts.tile_path(z, x, yy) - if p: - candidates.append(yy) - return sorted(set(candidates)) - - # Try up to max_x_check x-folders near center - for x in xs[:max_x_check]: - ys = list_y_for_x(x) - if not ys: - continue - y = min(ys, key=lambda yv: abs(yv - y0)) - return (z, x, y) - - # Fallback: brute probe a small grid near center - for x in xs: - for y in (y0, y0-1, y0+1, y0-2, y0+2): - if self.ts.tile_path(z, x, y): - return (z, x, y) - - # Last resort: scan all ranges (can be slower on huge sets) - for x in range(x_min, x_max + 1): - for y in range(y_min, y_max + 1): - if self.ts.tile_path(z, x, y): - return (z, x, y) - - return None diff --git a/GUI/src/vast/rel_db.py b/GUI/src/vast/rel_db.py new file mode 100644 index 000000000..8e92c49c1 --- /dev/null +++ b/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/GUI/src/vast/runner/Dockerfile b/GUI/src/vast/runner/Dockerfile index 7b6c3330f..1307a1d59 100644 --- a/GUI/src/vast/runner/Dockerfile +++ b/GUI/src/vast/runner/Dockerfile @@ -9,9 +9,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates 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; \ + echo "Configuring NetFree certificates..."; \ + cp ./certs/*.crt /usr/local/share/ca-certificates/; \ + update-ca-certificates; \ fi # SSL certs env @@ -38,4 +38,4 @@ 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"] +CMD ["python", "vast/runner/runner_server.py"] \ No newline at end of file diff --git a/GUI/src/vast/services/Dockerfile b/GUI/src/vast/services/Dockerfile index 7c7c7d4f0..3b3c03a35 100644 --- a/GUI/src/vast/services/Dockerfile +++ b/GUI/src/vast/services/Dockerfile @@ -19,5 +19,4 @@ 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"] - +CMD ["python", "-m", "vast.services.sensors_metrics_app"] \ No newline at end of file diff --git a/GUI/src/vast/views/SimilarPeriodsSensors.py b/GUI/src/vast/views/SimilarPeriodsSensors.py new file mode 100644 index 000000000..8c0a0c84d --- /dev/null +++ b/GUI/src/vast/views/SimilarPeriodsSensors.py @@ -0,0 +1,345 @@ +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame, QComboBox, + QPushButton, QGridLayout +) +from PyQt6.QtWebEngineWidgets import QWebEngineView +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QColor +import traceback + + +class SimilarPeriodsTab(QWidget): + def __init__(self, api, parent=None): + super().__init__(parent) + self.api = api + + # vector service internal Docker endpoint + self.vector_base = "http://vector_service:8000" + + # ======= MAIN LAYOUT (compact) ======= + main = QVBoxLayout(self) + main.setContentsMargins(20, 20, 20, 20) + main.setSpacing(12) + + # ================================ + # HEADER + # ================================ + title = QLabel("🌾 Similar Sensors Search") + title.setStyleSheet(""" + QLabel { + font-family: 'Inter'; + font-size: 26px; + font-weight: 800; + color: #1a1a1a; + margin-bottom: 2px; + } + """) + + subtitle = QLabel("Find sensors with similar characteristics") + subtitle.setStyleSheet(""" + QLabel { + font-family: 'Inter'; + font-size: 13px; + font-weight: 400; + color: #6B7280; + margin-bottom: 8px; + } + """) + + header_layout = QVBoxLayout() + header_layout.setSpacing(3) + header_layout.addWidget(title) + header_layout.addWidget(subtitle) + main.addLayout(header_layout) + + # ================================ + # SENSOR SELECTOR + # ================================ + sensor_row = QHBoxLayout() + sensor_row.setSpacing(8) + + lbl = QLabel("Sensor:") + lbl.setStyleSheet("font-size: 14px; font-weight: 600; color:#374151;") + + self.sensor_dropdown = QComboBox() + self.sensor_dropdown.setMinimumWidth(230) + self.sensor_dropdown.setStyleSheet(""" + QComboBox { + background: #ffffff; + border: 1px solid #d1d5db; + border-radius: 8px; + padding: 6px 10px; + font-size: 13px; + font-family: 'Inter'; + } + """) + + self._load_sensor_list() + + sensor_row.addWidget(lbl) + sensor_row.addWidget(self.sensor_dropdown) + sensor_row.addStretch() + main.addLayout(sensor_row) + + # ================================ + # COMPACT FILTER CARDS (GREEN) + # ================================ + cards_row = QHBoxLayout() + cards_row.setSpacing(8) + + self.card_same_status = self._create_filter_card("Same Status", "●") + self.card_same_type = self._create_filter_card("Same Type", "●") + self.card_same_day = self._create_filter_card("Same Install Day", "●") + + cards_row.addWidget(self.card_same_status) + cards_row.addWidget(self.card_same_type) + cards_row.addWidget(self.card_same_day) + main.addLayout(cards_row) + + # ================================ + # DATE FILTER (Compact card) + # ================================ + time_box = QFrame() + time_box.setStyleSheet(""" + QFrame { + background: #ffffff; + border-radius: 10px; + border: 1px solid #e5e7eb; + padding: 10px; + } + """) + + time_layout = QVBoxLayout(time_box) + time_layout.setSpacing(4) + + time_label = QLabel("Date Filter:") + time_label.setStyleSheet("font-size: 14px; font-weight:600; color:#374151;") + + self.date_dropdown = QComboBox() + self.date_dropdown.setStyleSheet(""" + QComboBox { + background: white; + border: 1px solid #d1d5db; + padding: 6px 10px; + border-radius: 8px; + font-size: 13px; + font-family: 'Inter'; + } + """) + + self.date_dropdown.addItem("— None —", None) + self.date_dropdown.addItem("Today", "today") + self.date_dropdown.addItem("Yesterday", "yesterday") + + weekdays = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"] + for w in weekdays: + self.date_dropdown.addItem(w, w.lower()) + for w in weekdays: + self.date_dropdown.addItem("Last " + w, "last_" + w.lower()) + + time_layout.addWidget(time_label) + time_layout.addWidget(self.date_dropdown) + + main.addWidget(time_box) + + # ================================ + # SEARCH BUTTON (compact) + # ================================ + self.btn = QPushButton("🔍 Search") + self.btn.setFixedHeight(40) + self.btn.setStyleSheet(""" + QPushButton { + background: #3B82F6; + color: white; + font-size: 15px; + font-weight: 600; + border-radius: 10px; + font-family: 'Inter'; + } + QPushButton:hover { background:#2563EB; } + """) + self.btn.clicked.connect(self._on_search) + main.addWidget(self.btn) + + # ================================ + # RESULTS VIEW — FULL HEIGHT + # ================================ + self.web = QWebEngineView() + self.web.setMinimumHeight(350) # pushes table to full size + main.addWidget(self.web) + + self._placeholder() + + # Background like dashboard + self.setStyleSheet(""" + QWidget { + background: qlineargradient(x1:0,y1:0,x2:0,y2:1, + stop:0 #F8FAFC, stop:1 #F1F5F9); + } + """) + + # ========= FILTER CARD (compact) ========= + def _create_filter_card(self, title, icon): + card = QFrame() + card.setProperty("active", False) + + card.setStyleSheet(""" + QFrame { + background: #F0FDF4; + border-radius: 12px; + border: 1px solid #D1FAE5; + } + QFrame[active="true"] { + border: 2px solid #10B981; + } + """) + + layout = QHBoxLayout(card) + layout.setContentsMargins(12, 8, 12, 8) + layout.setSpacing(8) + + icon_label = QLabel(icon) + icon_label.setStyleSheet(""" + QLabel { + font-size: 22px; + color: #10B981; + font-weight: 900; + } + """) + + text_label = QLabel(title) + text_label.setStyleSheet(""" + QLabel { + font-size: 13px; + font-weight: 600; + color: #374151; + font-family:'Inter'; + } + """) + + layout.addWidget(icon_label) + layout.addWidget(text_label) + + card.mousePressEvent = lambda e: self._toggle_card(card) + return card + + def _toggle_card(self, card): + state = not card.property("active") + card.setProperty("active", state) + card.style().unpolish(card) + card.style().polish(card) + card.update() + + # ========= PLACEHOLDER ========= + def _placeholder(self): + self.web.setHtml(""" +

+ Select filters and search for similar sensors. +

+ """) + + # ========= LOAD SENSOR LIST ========= + def _load_sensor_list(self): + try: + r = self.api.http.get(f"{self.api.base}/api/tables/sensors") + sensors = r.json().get("rows", []) + self.sensor_dropdown.addItem("-- Select Sensor --", None) + + for s in sensors: + sid = s.get("sensor_id") + name = s.get("sensor_name", "") + self.sensor_dropdown.addItem(f"{sid} – {name}", sid) + + except Exception as e: + print("Failed loading sensors:", e) + + # ========= BUILD PARAMS ========= + def _build_query_params(self): + params = {} + if self.card_same_status.property("active"): + params["same_status"] = "true" + if self.card_same_type.property("active"): + params["same_type"] = "true" + if self.card_same_day.property("active"): + params["same_day"] = "true" + + selected = self.date_dropdown.currentData() + if selected: + params["date_filter"] = selected + + return params + + # ========= SEARCH ========= + def _on_search(self): + sid = self.sensor_dropdown.currentData() + if not sid: + self.web.setHtml("

Select a sensor.

") + return + + params = self._build_query_params() + + if not params: + url = f"{self.vector_base}/similar_sensors/{sid}" + else: + q = "&".join([f"{k}={v}" for k, v in params.items()]) + url = f"{self.vector_base}/similar_sensors_advanced?sensor_id={sid}&{q}" + + try: + data = self.api.http.get(url).json() + self._render_results(data) + except Exception as e: + traceback.print_exc() + self.web.setHtml(f"

Error: {e}

") + + # ========= RENDER RESULTS (full-size table) ========= + def _render_results(self, data): + sims = data.get("similar_sensors", []) + + if not sims: + self.web.setHtml("

No results found.

") + return + + rows = "" + for s in sims: + status = s.get("status", "").lower() + active = (status == "active") + status_display = ( + "● ONLINE" + if active else + "● OFFLINE" + ) + + rows += f""" + + {s.get('sensor_id','—')} + {s.get('sensor_name','—')} + {s.get('sensor_type','—')} + {s.get('install_date','—')} + {status_display} + {round(s.get('distance',0),3)} + + """ + + html = f""" + + + + + + + + + + + + + + + {rows} + +
IDNameTypeInstall DateActiveDistance
+ + + """ + + self.web.setHtml(html) diff --git a/GUI/src/vast/views/alerts_panel.py b/GUI/src/vast/views/alerts_panel.py new file mode 100644 index 000000000..e9cfd549d --- /dev/null +++ b/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/GUI/src/vast/views/assets/fields.png b/GUI/src/vast/views/assets/fields.png new file mode 100644 index 000000000..b26f5ecda Binary files /dev/null and b/GUI/src/vast/views/assets/fields.png differ diff --git a/GUI/src/vast/views/assets/leaflet/leaflet-heat.js b/GUI/src/vast/views/assets/leaflet/leaflet-heat.js new file mode 100644 index 000000000..aa8031ab5 --- /dev/null +++ b/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/GUI/src/vast/views/assets/leaflet/leaflet.css b/GUI/src/vast/views/assets/leaflet/leaflet.css new file mode 100644 index 000000000..9ade8dc49 --- /dev/null +++ b/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/GUI/src/vast/views/assets/leaflet/leaflet.js b/GUI/src/vast/views/assets/leaflet/leaflet.js new file mode 100644 index 000000000..a3bf693d0 --- /dev/null +++ b/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/GUI/src/vast/views/assets/sensors_map.html b/GUI/src/vast/views/assets/sensors_map.html new file mode 100644 index 000000000..ce43700bf --- /dev/null +++ b/GUI/src/vast/views/assets/sensors_map.html @@ -0,0 +1,288 @@ + + + + + + + AgCloud – Sensor Map + + + + + +
+
+
Sensor Dashboard
+
+
Normal
+
Soil Moisture
+
Temperature
+
Humidity
+
+
+ +
+
+ + + + + + diff --git a/GUI/src/vast/views/assets/zones.geojson b/GUI/src/vast/views/assets/zones.geojson new file mode 100644 index 000000000..bddcf348b --- /dev/null +++ b/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/GUI/src/vast/views/auth_status_view.py b/GUI/src/vast/views/auth_status_view.py new file mode 100644 index 000000000..1a4ea8563 --- /dev/null +++ b/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/GUI/src/vast/views/fruits_view.py b/GUI/src/vast/views/fruits_view.py new file mode 100644 index 000000000..37b85a508 --- /dev/null +++ b/GUI/src/vast/views/fruits_view.py @@ -0,0 +1,426 @@ +# 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) + # ───────────────────────────────────────────── + # Metrics Section (Grafana panels) + # ───────────────────────────────────────────── + metrics_title = QLabel("Fruit Alerts Metrics") + metrics_title.setStyleSheet("font-size: 18px; font-weight: 600; margin-top: 15px;") + root.addWidget(metrics_title) + + grafana_host = os.getenv("GRAFANA_HOST", "grafana") + base = f"http://{grafana_host}:3000" + + panel_urls = [ + QUrl(f"{base}/d-solo/fruit-metrics/fruit-alerts?orgId=1&panelId=1&refresh=10s&theme=light"), + QUrl(f"{base}/d-solo/fruit-metrics/fruit-alerts?orgId=1&panelId=2&refresh=10s&theme=light"), + QUrl(f"{base}/d-solo/fruit-metrics/fruit-alerts?orgId=1&panelId=3&refresh=10s&theme=light"), + QUrl(f"{base}/d-solo/fruit-metrics/fruit-alerts?orgId=1&panelId=4&refresh=10s&theme=light"), + ] + + metrics_box = QVBoxLayout() + metrics_box.setSpacing(10) + root.addLayout(metrics_box) + + for url in panel_urls: + view = QWebEngineView(self) + view.setMinimumHeight(300) + view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + view.setUrl(url) + metrics_box.addWidget(view) + + 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/GUI/src/vast/views/ground_view.py b/GUI/src/vast/views/ground_view.py new file mode 100644 index 000000000..d4da6f80e --- /dev/null +++ b/GUI/src/vast/views/ground_view.py @@ -0,0 +1,597 @@ +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", "") + +DB_API_BASE = os.getenv("DB_API_BASE", "http://127.0.0.1:8001") +MINIO_ENDPOINT = os.getenv("MINIO_ENDPOINT", "127.0.0.1:9001") +MINIO_SECURE = os.getenv("MINIO_SECURE", "false") +MINIO_ACCESS_KEY = os.getenv("MINIO_ACCESS_KEY", "minioadmin") +MINIO_SECRET_KEY = os.getenv("MINIO_SECRET_KEY", "minioadmin123") +GROUND_BUCKET = os.getenv("GROUND_BUCKET", "ground") +GROUND_PREFIX = os.getenv("GROUND_PREFIX", "") +GATEWAY_URL = os.getenv("GATEWAY_URL", "http://127.0.0.1:8000") +# ---------------------------- +# 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/services/sounds/API-development/src/__init__.py b/GUI/src/vast/views/irrigation/__init__.py similarity index 100% rename from services/sounds/API-development/src/__init__.py rename to GUI/src/vast/views/irrigation/__init__.py diff --git a/GUI/src/vast/views/irrigation/dashboard_bar.py b/GUI/src/vast/views/irrigation/dashboard_bar.py new file mode 100644 index 000000000..9c27077f4 --- /dev/null +++ b/GUI/src/vast/views/irrigation/dashboard_bar.py @@ -0,0 +1,127 @@ +from PyQt6.QtWidgets import QWidget, QHBoxLayout, QLabel, QVBoxLayout, QFrame +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QFont +from datetime import datetime + +class DashboardBar(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + + # Clean minimalistic style: no background, only bottom border + self.setStyleSheet(""" + QWidget { + background: transparent; + border-bottom: 1px solid #e5e7eb; + } + QLabel.title { + color: #6b7280; + font-size: 10pt; + font-weight: 500; + } + QLabel.value { + color: #111827; + font-size: 18pt; + font-weight: 600; + } + """) + + layout = QHBoxLayout(self) + layout.setContentsMargins(20, 16, 20, 16) + layout.setSpacing(32) + + # Helper: metric block + def metric(title_text, value_text, big=True): + container = QWidget() + v = QVBoxLayout(container) + v.setContentsMargins(0, 0, 0, 0) + v.setSpacing(2) + + title = QLabel(title_text) + title.setProperty("class", "title") + + value = QLabel(value_text) + value.setProperty("class", "value" if big else "title") + + v.addWidget(title) + v.addWidget(value) + return container, value + + # Metric 1 + block, self.active_value = metric("Active Sprinklers", "0") + layout.addWidget(block) + + layout.addWidget(self._separator()) + + # Metric 2 + block, self.area_value = metric("Irrigated Area", "0%") + layout.addWidget(block) + + layout.addWidget(self._separator()) + + # Metric 3 + block, self.time_value = metric("Last Update", "--", big=False) + layout.addWidget(block) + + layout.addStretch() + + # Status container (clean version) + self.status_container = QWidget() + s = QHBoxLayout(self.status_container) + s.setContentsMargins(0, 0, 0, 0) + s.setSpacing(6) + + self.status_indicator = QLabel("●") + self.status_indicator.setStyleSheet("color: #dc2626; font-size: 12px;") + + self.status_text = QLabel("Disconnected") + self.status_text.setFont(QFont('Segoe UI', 10)) + self.status_text.setStyleSheet("color: #dc2626; font-weight: 600;") + + s.addWidget(self.status_indicator) + s.addWidget(self.status_text) + layout.addWidget(self.status_container) + + def _separator(self): + sep = QFrame() + sep.setFrameShape(QFrame.Shape.VLine) + sep.setStyleSheet("color: #e5e7eb;") + sep.setFixedWidth(1) + return sep + + def update_status(self, active_count, area_percent, last_update, healthy): + self.active_value.setText(str(active_count)) + self.area_value.setText(f"{area_percent:.1f}%") + + if last_update: + try: + if isinstance(last_update, str): + last_dt = datetime.fromisoformat(last_update.replace('Z', '+00:00')) + else: + last_dt = last_update + + now = datetime.now(last_dt.tzinfo) if last_dt.tzinfo else datetime.now() + delta = now - last_dt + seconds = int(delta.total_seconds()) + + if seconds < 60: + t = f"{seconds}s ago" + elif seconds < 3600: + t = f"{seconds // 60}m ago" + else: + t = f"{seconds // 3600}h ago" + + self.time_value.setText(t) + except: + self.time_value.setText("--") + else: + self.time_value.setText("--") + + # Status indicator styling + if healthy: + self.status_indicator.setStyleSheet("color: #16a34a; font-size: 12px;") + self.status_text.setText("Connected") + self.status_text.setStyleSheet("color: #16a34a; font-weight: 600;") + else: + self.status_indicator.setStyleSheet("color: #dc2626; font-size: 12px;") + self.status_text.setText("Disconnected") + self.status_text.setStyleSheet("color: #dc2626; font-weight: 600;") diff --git a/GUI/src/vast/views/irrigation/history_panel.py b/GUI/src/vast/views/irrigation/history_panel.py new file mode 100644 index 000000000..e5556a911 --- /dev/null +++ b/GUI/src/vast/views/irrigation/history_panel.py @@ -0,0 +1,34 @@ +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem, QLabel, QHeaderView +from PyQt6.QtCore import Qt + +class HistoryPanel(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowFlags(Qt.WindowType.Widget) + self.setFixedWidth(400) + layout = QVBoxLayout(self) + self.title = QLabel("Sprinkler History") + self.title.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(self.title) + self.table = QTableWidget(0, 6) + self.table.setHorizontalHeaderLabels(['Timestamp','Device','Decision','Confidence','Dry Ratio','Image']) + # self.table.horizontalHeader().setSectionResizeMode(QTableWidget.ResizeMode.Stretch) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + self.table.setAlternatingRowColors(True) + self.table.setStyleSheet('alternate-background-color: #f9fafb; background: white;') + self.table.verticalHeader().hide() + self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + layout.addWidget(self.table) + + def show_history(self, records): + self.table.setRowCount(0) + for row in records: + r = self.table.rowCount() + self.table.insertRow(r) + self.table.setItem(r,0, QTableWidgetItem(str(row['ts']))) + self.table.setItem(r,1, QTableWidgetItem(row['device_id'])) + dec_item = QTableWidgetItem(row['decision']) + self.table.setItem(r,2, dec_item) + self.table.setItem(r,3, QTableWidgetItem(f"{row['confidence']:.2f}")) + self.table.setItem(r,4, QTableWidgetItem(f"{row['dry_ratio']:.2f}")) + self.table.setItem(r,5, QTableWidgetItem("View Image")) diff --git a/GUI/src/vast/views/irrigation/irrigation_dashboard.py b/GUI/src/vast/views/irrigation/irrigation_dashboard.py new file mode 100644 index 000000000..7b59b1c9c --- /dev/null +++ b/GUI/src/vast/views/irrigation/irrigation_dashboard.py @@ -0,0 +1,969 @@ +# from pathlib import Path +# import sys, random + +# # Add current directory to path to allow direct execution +# script_dir = Path(__file__).parent +# sys.path.insert(0, str(script_dir)) + +# from PyQt6.QtWidgets import ( +# QApplication, QWidget, QVBoxLayout, QDialog, QFormLayout, QScrollArea, +# QComboBox, QLineEdit, QHBoxLayout, QPushButton, QTableWidget, QTableWidgetItem, +# QHeaderView, QProgressBar, QDialogButtonBox, QGraphicsDropShadowEffect, QLabel +# ) +# from map_widget import MapWidget +# from history_panel import HistoryPanel +# # from .sprinkler_details_dialog import SprinklerDetailsDialog +# from sprinkler_details_dialog_new import SprinklerDetailsDialogNew +# from dashboard_bar import DashboardBar +# from PyQt6.QtGui import QPixmap, QMovie, QFont, QColor, QIcon +# from PyQt6.QtCore import Qt, QTimer, QSize, QPropertyAnimation, QRect, QEasingCurve + +# import psycopg2 +# from psycopg2.extras import RealDictCursor + +# # ----------------------- DB helpers (unchanged) ----------------------- +# def get_db_connection(): +# return psycopg2.connect( +# # host="postgres", +# host = "localhost", +# port=5432, +# user="missions_user", +# password="pg123", +# dbname="missions_db", +# cursor_factory=RealDictCursor +# ) + +# # NOTE: original fetch functions retained (they must exist and behave the same) +# # For brevity we keep their original implementations from the provided file. + +# def fetch_sprinkler_history(limit=500): +# query = f""" +# SELECT e.device_id, e.dry_ratio, e.decision, e.confidence, e.patch_count, e.extra, +# p.prev_state, e.ts +# FROM soil_moisture_events e +# LEFT JOIN irrigation_policies p ON e.device_id = p.device_id +# ORDER BY e.ts DESC +# LIMIT {limit} +# """ +# with get_db_connection() as conn: +# with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: +# cur.execute(query) +# rows = cur.fetchall() + +# for row in rows: +# prev_state = row.get('prev_state') or 'stop' +# decision = row['decision'] +# if decision == "noop": +# new_state = prev_state +# elif decision in ("run", "stop"): +# new_state = decision +# else: +# new_state = "stop" +# row['prev_state'] = prev_state +# row['new_state'] = new_state + +# return rows + + +# def fetch_sprinkler_data(): +# query = """ +# SELECT e.device_id, e.dry_ratio, e.decision, e.confidence, e.patch_count, e.extra, +# e.ts, p.prev_state +# FROM soil_moisture_events e +# LEFT JOIN irrigation_policies p ON e.device_id = p.device_id +# WHERE e.ts = ( +# SELECT MAX(ts) +# FROM soil_moisture_events e2 +# WHERE e2.device_id = e.device_id +# ) +# ORDER BY e.device_id +# """ + +# with get_db_connection() as conn: +# with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: +# cur.execute(query) +# rows = cur.fetchall() + +# for row in rows: +# prev_state = row.get('prev_state') or 'stop' +# decision = row['decision'] +# if decision == "noop": +# new_state = prev_state +# elif decision in ("run", "stop"): +# new_state = decision +# else: +# new_state = "stop" +# row['prev_state'] = prev_state +# row['new_state'] = new_state + +# return rows + + +# # ----------------------- UI Components ----------------------- +# class ImageModal(QDialog): +# """Modal to show image with dark overlay and details""" +# def __init__(self, parent, pixmap: QPixmap, details: dict): +# super().__init__(parent) +# self.setModal(True) +# self.setWindowFlag(Qt.WindowType.FramelessWindowHint) +# self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + +# self.main = QWidget(self) +# self.main.setStyleSheet("background: white; border-radius: 12px;") +# layout = QVBoxLayout(self.main) +# self.main.setLayout(layout) + +# img_label = QLabel() +# max_w = int(parent.width() * 0.8) +# max_h = int(parent.height() * 0.8) +# img_label.setPixmap(pixmap.scaled(max_w, max_h, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) +# img_label.setAlignment(Qt.AlignmentFlag.AlignCenter) +# layout.addWidget(img_label) + +# info = QLabel(f"Timestamp: {details.get('ts')} — Decision: {details.get('decision')} — Confidence: {details.get('confidence'):.2f}") +# layout.addWidget(info) + +# btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) +# btns.rejected.connect(self.close) +# layout.addWidget(btns) + +# # position central +# w, h = int(parent.width()*0.85), int(parent.height()*0.85) +# self.setGeometry((parent.width()-w)//2, (parent.height()-h)//2, w, h) +# self.main.setGeometry(0, 0, w, h) + +# class FloatingDetailsCard(QWidget): +# """Floating centered card (replaces drawer)""" +# def __init__(self, parent, device_id, details): +# super().__init__(parent) +# self.device_id = device_id +# self.details = details +# self.setWindowFlags(Qt.WindowType.FramelessWindowHint) +# self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + +# self.card = QWidget(self) +# self.card.setStyleSheet(""" +# background: white; +# border-radius: 14px; +# """) + +# shadow = QGraphicsDropShadowEffect(self.card) +# shadow.setBlurRadius(30) +# shadow.setOffset(0, 8) +# shadow.setColor(QColor(0,0,0,100)) +# self.card.setGraphicsEffect(shadow) + +# # layout +# l = QVBoxLayout(self.card) +# l.setContentsMargins(20, 20, 20, 20) +# # title with gradient-like color +# title = QLabel(f"Sprinkler {device_id}") +# title.setFont(QFont('Segoe UI', 14, QFont.Weight.Bold)) +# title.setStyleSheet("color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #06b6d4, stop:1 #0891b2);") +# l.addWidget(title) + +# # status row +# status_row = QHBoxLayout() +# status_dot = QLabel('●') +# status_dot.setFont(QFont('Arial', 16)) +# status_color = '#10b981' if details.get('active') else '#6b7280' +# status_dot.setStyleSheet(f"color: {status_color}") +# status_row.addWidget(status_dot) +# status_row.addWidget(QLabel('Active' if details.get('active') else 'Inactive')) +# status_row.addStretch() +# l.addLayout(status_row) + +# # dry ratio progress +# dry_label = QLabel('💧 Dry Ratio') +# l.addWidget(dry_label) +# pb = QProgressBar() +# pb.setRange(0, 100) +# pb.setValue(int(details.get('dry_ratio',0)*100)) +# pb.setTextVisible(True) +# pb.setFormat('%.0f%%' % (pb.value(),)) +# pb.setFixedHeight(18) +# pb.setStyleSheet("border-radius:9px;") +# l.addWidget(pb) + +# # confidence with color +# conf = details.get('confidence',0) +# conf_label = QLabel(f'🎯 Confidence: {conf:.2f}') +# if conf >= 0.75: +# conf_color = '#10b981' +# elif conf >= 0.4: +# conf_color = '#f59e0b' +# else: +# conf_color = '#ef4444' +# conf_label.setStyleSheet(f'color: {conf_color}; font-weight: bold') +# l.addWidget(conf_label) + +# # decision +# decision_label = QLabel(f'⚙️ Decision: {details.get("decision")}') +# l.addWidget(decision_label) + +# # button row (bottom) +# btn_row = QHBoxLayout() +# btn_row.setSpacing(16) +# btn_row.addStretch() +# # Show History button +# self.show_history_btn = QPushButton('Show History') +# self.show_history_btn.setStyleSheet('background:#06b6d4; color:white; padding:8px; border-radius:8px') +# btn_row.addWidget(self.show_history_btn) +# # Close button +# self.close_btn = QPushButton('Close') +# self.close_btn.clicked.connect(self.close_card) +# self.close_btn.setFixedWidth(120) +# self.close_btn.setStyleSheet('background:#3b82f6; color:white; padding:8px; border-radius:10px') +# btn_row.addWidget(self.close_btn) +# l.addLayout(btn_row) + +# # size and position +# w, h = int(parent.width()*0.6), int(parent.height()*0.5) +# self.setGeometry((parent.width()-w)//2, (parent.height()-h)//2, w, h) +# self.card.setGeometry(0,0,w,h) + +# # fade-in animation +# self.opacity_anim = QPropertyAnimation(self, b"windowOpacity") +# self.opacity_anim.setDuration(220) +# self.opacity_anim.setStartValue(0.0) +# self.opacity_anim.setEndValue(1.0) +# self.setWindowOpacity(0.0) +# self.opacity_anim.start() + +# def close_card(self): +# self.close() + +# class IrrigationDashboard(QWidget): +# def __init__(self): +# super().__init__() +# print("[DEBUG] IrrigationDashboard.__init__ called") +# self.setWindowTitle("Irrigation Dashboard 🌾") +# self.setGeometry(100, 100, 1100, 820) +# QApplication.setFont(QFont('Roboto', 10)) +# main = QVBoxLayout(self) +# main.setContentsMargins(0,0,0,0) +# self.setStyleSheet(""" +# QWidget#root { background: qlineargradient(x1:0,y1:0,x2:0,y2:1, stop:0 #f0f9ff, stop:1 #e0f2fe); } +# QLabel.title { font-family: 'Segoe UI'; font-weight: bold; font-size: 18px; color: #111827 } +# """) +# self.setObjectName('root') + +# # MapWidget replaces map logic +# print("[DEBUG] Creating DashboardBar...") +# self.dashboard_bar = DashboardBar(self) +# main.addWidget(self.dashboard_bar) +# print("[DEBUG] Creating MapWidget...") +# self.map_widget = MapWidget(self, map_path=str(script_dir / "map_field.jpg")) +# self.sprinklers = {} # device_id -> info, for non-map logic (details, etc.) +# # print("[DEBUG] Creating HistoryPanel...") +# # self.history_panel = HistoryPanel(self) +# # self.history_panel.hide() + +# # Create map layout with minimal margins to push sprinklers down +# map_container = QWidget() +# map_layout = QVBoxLayout(map_container) +# map_layout.setContentsMargins(0, 10, 0, 0) # Minimal top margin (10px instead of default) +# map_layout.setSpacing(0) +# map_layout.addWidget(self.map_widget) + +# main.addWidget(map_container, stretch=3) +# # main.addWidget(self.history_panel, stretch=1) + +# # timer for data refresh +# print("[DEBUG] Setting up timer...") +# self.timer = QTimer() +# self.timer.timeout.connect(self.safe_update_data) +# # Delay first update to avoid issues during init +# self.timer.singleShot(1000, self.safe_update_data) +# self.timer.start(3000) +# print("[DEBUG] IrrigationDashboard.__init__ completed") + +# def resizeEvent(self, event): +# super().resizeEvent(event) +# # Delegate map resizing to MapWidget +# if hasattr(self, 'map_widget') and self.map_widget: +# self.map_widget.resizeEvent(event) + +# def set_sprinkler_state(self, s, active): +# btn: QPushButton = s['widget'] +# # create / reuse label for movie +# if 'label' not in s: +# lbl = QLabel(btn) +# lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) +# lbl.setGeometry(0,0,btn.width(),btn.height()) +# lbl.raise_() # Raise label above button to display image +# s['label'] = lbl +# lbl = s['label'] + +# if active: +# movie = QMovie(str(script_dir / 'sprinkler_on.gif')) +# movie.setScaledSize(QSize(btn.width(), btn.height())) +# lbl.setMovie(movie) +# movie.start() +# lbl.raise_() # Ensure label stays on top +# lbl.show() +# btn.setStyleSheet(self.sprinkler_style(active=True)) +# s['movie'] = movie +# else: +# # static image +# pix = QPixmap(str(script_dir / 'sprinkler_off.png')) if (script_dir / 'sprinkler_off.png').exists() else QPixmap(btn.width(), btn.height()) +# if pix.isNull(): +# pix = QPixmap(btn.width(), btn.height()); pix.fill(QColor('#6b7280')) +# lbl.setPixmap(pix.scaled(btn.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) +# lbl.raise_() # Ensure label stays on top +# lbl.show() +# btn.setStyleSheet(self.sprinkler_style(active=False)) +# s['movie'] = None +# s['active'] = active + +# def sprinkler_style(self, active=False): +# # circular button with colored background (not transparent) +# base = f""" +# QPushButton {{ +# width: 56px; height:56px; border-radius:28px; + +# font-weight: bold; +# color: white; +# }} +# QPushButton:hover {{ }} +# """ +# return base + +# def update_history(self): +# # Populate filters (preserve current selection) and refresh table +# rows = fetch_sprinkler_history() +# prev_dev = self.filter_device.currentText() if self.filter_device.count()>0 else 'All' +# prev_dec = self.filter_decision.currentText() if self.filter_decision.count()>0 else 'All' + +# unique_devices = sorted({row['device_id'] for row in rows}) +# unique_decisions = sorted({row['decision'] for row in rows}) + +# # repopulate while preserving selection when possible +# self.filter_device.blockSignals(True) +# self.filter_decision.blockSignals(True) +# self.filter_device.clear(); self.filter_device.addItem('All'); self.filter_device.addItems(unique_devices) +# self.filter_decision.clear(); self.filter_decision.addItem('All'); self.filter_decision.addItems(unique_decisions) +# idx = self.filter_device.findText(prev_dev) +# if idx >= 0: +# self.filter_device.setCurrentIndex(idx) +# idx = self.filter_decision.findText(prev_dec) +# if idx >= 0: +# self.filter_decision.setCurrentIndex(idx) +# self.filter_device.blockSignals(False) +# self.filter_decision.blockSignals(False) + +# # apply currently selected filters to populate the table +# self.apply_filters() + +# def apply_filters(self): +# # fetch and filter rows locally +# rows = fetch_sprinkler_history() +# dev = self.filter_device.currentText() +# dec = self.filter_decision.currentText() +# if dev and dev != 'All': +# rows = [r for r in rows if r.get('device_id') == dev] +# if dec and dec != 'All': +# rows = [r for r in rows if r.get('decision') == dec] + +# # date filter +# try: +# dfrom = self.filter_date_from.date().toPyDate() +# dto = self.filter_date_to.date().toPyDate() +# # only apply if user set meaningful range (non-default) +# if dfrom and dto and dfrom <= dto: +# rows = [r for r in rows if r.get('ts') and dfrom <= r['ts'].date() <= dto] +# except Exception: +# pass + +# # numeric filters +# try: +# drymin = float(self.filter_dry_min.text()) if self.filter_dry_min.text() else None +# confmin = float(self.filter_conf_min.text()) if self.filter_conf_min.text() else None +# if drymin is not None: +# rows = [r for r in rows if r.get('dry_ratio', 0) >= drymin] +# if confmin is not None: +# rows = [r for r in rows if r.get('confidence', 0) >= confmin] +# except Exception: +# pass + +# self.populate_history_table(rows) + +# def populate_history_table(self, rows): +# self.history_table.setRowCount(0) +# for row in rows: +# r = self.history_table.rowCount() +# self.history_table.insertRow(r) +# self.history_table.setItem(r,0, QTableWidgetItem(str(row['ts']))) +# self.history_table.setItem(r,1, QTableWidgetItem(row['device_id'])) +# dec_item = QTableWidgetItem(row['decision']) +# if row['decision']=='run': dec_item.setBackground(QColor('#10b981')) +# elif row['decision']=='stop': dec_item.setBackground(QColor('#ef4444')) +# else: dec_item.setBackground(QColor('#f59e0b')) +# self.history_table.setItem(r,2, dec_item) +# self.history_table.setItem(r,3, QTableWidgetItem(f"{row['confidence']:.2f}")) +# self.history_table.setItem(r,4, QTableWidgetItem(f"{row['dry_ratio']:.2f}")) +# view_btn = QPushButton('View Image') +# view_btn.clicked.connect(lambda _checked, rr=row: self.show_image_modal(rr)) +# self.history_table.setCellWidget(r,5, view_btn) + +# def show_image_modal(self, row): +# # for demo: try to load an image from disk or show placeholder +# img_path = script_dir / 'sprinkler_image_placeholder.jpg' +# if img_path.exists(): +# pix = QPixmap(str(img_path)) +# else: +# pix = QPixmap(400,300) +# pix.fill(QColor('#ddd')) +# m = ImageModal(self, pix, row) +# m.exec() + +# def update_data(self): +# try: +# rows = fetch_sprinkler_data() +# healthy = True +# except Exception as e: +# print('Error fetching data:', e) +# import traceback +# traceback.print_exc() +# rows = [] +# healthy = False + +# # Update sprinklers on map +# active_count = 0 +# irrigated_area = 0.0 +# last_update = '--' +# max_timestamp = None +# for i, row in enumerate(rows): +# device_id = row['device_id'] +# dry_ratio = row['dry_ratio'] +# decision = row['decision'] +# confidence = row['confidence'] +# new_state = row['new_state'] +# active = new_state == 'run' +# if active: +# active_count += 1 +# irrigated_area += dry_ratio if active else 0 + +# # Track the most recent timestamp across all sprinklers +# ts = row.get('ts') +# if ts and (max_timestamp is None or ts > max_timestamp): +# max_timestamp = ts + +# # MapWidget handles display and GIF/image ONLY (no shapes) +# if device_id not in self.map_widget.sprinklers: +# rel_x = (100 + i*120 + random.randint(-20, 20)) / max(1, self.map_widget.map_pixmap.width()) +# rel_y = (300 + random.randint(-100, 100)) / max(1, self.map_widget.map_pixmap.height()) +# rel_size = 0.025 +# btn = self.map_widget.add_sprinkler(device_id, rel_x, rel_y, rel_size, active, name=device_id) +# try: +# btn.clicked.connect(lambda _e, d=device_id: self.show_details(d)) +# except Exception as e: +# print(f"[ERROR] Failed to connect clicked signal for {device_id}: {e}") + +# s = self.map_widget.sprinklers[device_id] +# # ALWAYS set sprinkler state on every update - ensures images/GIFs shown for new sprinklers added after startup +# self.set_sprinkler_state(s, active) +# s.update({'dry_ratio': dry_ratio, 'decision': decision, 'confidence': confidence}) +# # Update main window's sprinkler info for details dialog +# self.sprinklers[device_id] = s + +# # Set last_update to the most recent timestamp +# if max_timestamp: +# last_update = str(max_timestamp) + +# total_area = len(rows) +# irrigated_percent = (irrigated_area / total_area * 100) if total_area else 0.0 +# self.dashboard_bar.update_status(active_count, irrigated_percent, last_update, healthy) + +# def safe_update_data(self): +# """Safe wrapper for update_data that catches exceptions""" +# try: +# self.update_data() +# except Exception as e: +# print(f"[ERROR] safe_update_data failed: {e}") +# import traceback +# traceback.print_exc() + +# def show_details(self, device_id): +# print(f"[DEBUG] show_details() called for device_id={device_id}") +# if device_id not in self.sprinklers: +# print(f"[DEBUG] Device {device_id} not found in sprinklers dict") +# return +# # Close previous details dialog if open +# if hasattr(self, '_details_card') and self._details_card: +# print(f"[DEBUG] Closing previous details card") +# self._details_card.close() +# s = self.sprinklers[device_id] +# details = { +# 'active': s.get('active', False), +# 'dry_ratio': s.get('dry_ratio', 0), +# 'confidence': s.get('confidence', 0), +# 'decision': s.get('decision', 'noop') +# } +# print(f"[DEBUG] Creating new SprinklerDetailsDialogNew with details: {details}") +# # Use new tabbed dialog +# card = SprinklerDetailsDialogNew(self, device_id, details) +# card.history_requested.connect(lambda d: self.show_history_panel(d)) +# self._details_card = card +# print(f"[DEBUG] Calling card.show()") +# card.show() +# print(f"[DEBUG] Dialog shown") + +# def show_history_panel(self, device_id): +# # Close previous details dialog if open +# if hasattr(self, '_details_card') and self._details_card: +# self._details_card.close() +# # Fetch last 10 records for this sprinkler +# all_history = fetch_sprinkler_history(limit=100) +# records = [r for r in all_history if r['device_id'] == device_id][:10] +# self.history_panel.show_history(records) +# self.history_panel.show() +# self.map_widget.setFixedWidth(int(self.width() * 0.6)) +# # Add close button to history panel +# if not hasattr(self.history_panel, 'close_btn'): +# from PyQt6.QtWidgets import QPushButton +# close_btn = QPushButton('Close History') +# close_btn.setStyleSheet('background:#ef4444; color:white; padding:6px; border-radius:8px') +# close_btn.clicked.connect(self.hide_history_panel) +# layout = self.history_panel.layout() +# layout.addWidget(close_btn) +# self.history_panel.close_btn = close_btn + +# def hide_history_panel(self): +# self.history_panel.hide() +# self.map_widget.setFixedWidth(self.width()) +# # Ensure details dialog can be reopened after closing history +# self._details_card = None + +# def on_history_clicked(self, row, col): +# # show full details card when any non-button cell is clicked +# try: +# device = self.history_table.item(row,1).text() +# if device: +# self.show_details(device) +# except Exception: +# pass + +# if __name__ == '__main__': +# app = QApplication(sys.argv) +# window = IrrigationDashboard() +# window.show() +# sys.exit(app.exec()) + + +from pathlib import Path +import sys, random +from PyQt6.QtWidgets import ( + QApplication, QWidget, QVBoxLayout, QDialog, QFormLayout, QScrollArea, + QComboBox, QLineEdit, QHBoxLayout, QPushButton, QTableWidget, QTableWidgetItem, + QHeaderView, QProgressBar, QDialogButtonBox, QGraphicsDropShadowEffect, QLabel +) +from .map_widget import MapWidget +from .history_panel import HistoryPanel +from .sprinkler_details_dialog_new import SprinklerDetailsDialogNew +from .dashboard_bar import DashboardBar +from PyQt6.QtGui import QPixmap, QMovie, QFont, QColor, QIcon +from PyQt6.QtCore import Qt, QTimer, QSize, QPropertyAnimation, QRect, QEasingCurve + +import psycopg2 +from psycopg2.extras import RealDictCursor + +# ----------------------- DB helpers (unchanged) ----------------------- +def get_db_connection(): + return psycopg2.connect( + host="postgres", + port=5432, + user="missions_user", + password="pg123", + dbname="missions_db", + cursor_factory=RealDictCursor + ) + +# NOTE: original fetch functions retained (they must exist and behave the same) +# For brevity we keep their original implementations from the provided file. + +def fetch_sprinkler_history(limit=500): + query = f""" + SELECT e.device_id, e.dry_ratio, e.decision, e.confidence, e.patch_count, e.extra, + p.prev_state, e.ts + FROM soil_moisture_events e + LEFT JOIN irrigation_policies p ON e.device_id = p.device_id + ORDER BY e.ts DESC + LIMIT {limit} + """ + with get_db_connection() as conn: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute(query) + rows = cur.fetchall() + + for row in rows: + prev_state = row.get('prev_state') or 'stop' + decision = row['decision'] + if decision == "noop": + new_state = prev_state + elif decision in ("run", "stop"): + new_state = decision + else: + new_state = "stop" + row['prev_state'] = prev_state + row['new_state'] = new_state + + return rows + + +def fetch_sprinkler_data(): + query = """ + SELECT e.device_id, e.dry_ratio, e.decision, e.confidence, e.patch_count, e.extra, + e.ts, p.prev_state + FROM soil_moisture_events e + LEFT JOIN irrigation_policies p ON e.device_id = p.device_id + WHERE e.ts = ( + SELECT MAX(ts) + FROM soil_moisture_events e2 + WHERE e2.device_id = e.device_id + ) + ORDER BY e.device_id + """ + + with get_db_connection() as conn: + with conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) as cur: + cur.execute(query) + rows = cur.fetchall() + + for row in rows: + prev_state = row.get('prev_state') or 'stop' + decision = row['decision'] + if decision == "noop": + new_state = prev_state + elif decision in ("run", "stop"): + new_state = decision + else: + new_state = "stop" + row['prev_state'] = prev_state + row['new_state'] = new_state + + return rows + + +script_dir = Path(__file__).parent + +# # ----------------------- UI Components ----------------------- +# class ImageModal(QDialog): +# """Modal to show image with dark overlay and details""" +# def __init__(self, parent, pixmap: QPixmap, details: dict): +# super().__init__(parent) +# self.setModal(True) +# self.setWindowFlag(Qt.WindowType.FramelessWindowHint) +# self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + +# self.main = QWidget(self) +# self.main.setStyleSheet("background: white; border-radius: 12px;") +# layout = QVBoxLayout(self.main) +# self.main.setLayout(layout) + +# img_label = QLabel() +# max_w = int(parent.width() * 0.8) +# max_h = int(parent.height() * 0.8) +# img_label.setPixmap(pixmap.scaled(max_w, max_h, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) +# img_label.setAlignment(Qt.AlignmentFlag.AlignCenter) +# layout.addWidget(img_label) + +# info = QLabel(f"Timestamp: {details.get('ts')} — Decision: {details.get('decision')} — Confidence: {details.get('confidence'):.2f}") +# layout.addWidget(info) + +# btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) +# btns.rejected.connect(self.close) +# layout.addWidget(btns) + +# # position central +# w, h = int(parent.width()*0.85), int(parent.height()*0.85) +# self.setGeometry((parent.width()-w)//2, (parent.height()-h)//2, w, h) +# self.main.setGeometry(0, 0, w, h) + +class IrrigationDashboard(QWidget): + def __init__(self): + super().__init__() + self.setWindowTitle("Irrigation Dashboard 🌾") + self.setGeometry(100, 100, 1100, 820) + QApplication.setFont(QFont('Roboto', 10)) + main = QVBoxLayout(self) + main.setContentsMargins(0,0,0,0) + self.setStyleSheet(""" + QWidget#root { background: qlineargradient(x1:0,y1:0,x2:0,y2:1, stop:0 #f0f9ff, stop:1 #e0f2fe); } + QLabel.title { font-family: 'Segoe UI'; font-weight: bold; font-size: 18px; color: #111827 } + """) + self.setObjectName('root') + + # MapWidget replaces map logic + self.dashboard_bar = DashboardBar(self) + main.addWidget(self.dashboard_bar) + self.map_widget = MapWidget(self, map_path=str(script_dir / "map_field.jpg")) + self.sprinklers = {} # device_id -> info, for non-map logic (details, etc.) + self.history_panel = HistoryPanel(self) + self.history_panel.hide() + + # Create map layout with minimal margins to push sprinklers down + map_container = QWidget() + map_layout = QVBoxLayout(map_container) + map_layout.setContentsMargins(0, 10, 0, 0) # Minimal top margin (10px instead of default) + map_layout.setSpacing(0) + map_layout.addWidget(self.map_widget) + + main.addWidget(map_container, stretch=3) + main.addWidget(self.history_panel, stretch=1) + + # timer for data refresh + self.timer = QTimer() + self.timer.timeout.connect(self.update_data) + self.timer.start(3000) + self.update_data() + + def resizeEvent(self, event): + super().resizeEvent(event) + # Delegate map resizing to MapWidget + if hasattr(self, 'map_widget') and self.map_widget: + self.map_widget.resizeEvent(event) + + def set_sprinkler_state(self, s, active): + #print(f"Setting sprinkler {s['name']} state to {'active' if active else 'inactive'}") + btn: QPushButton = s['widget'] + # create / reuse label for movie + if 'label' not in s: + lbl = QLabel(btn) + lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + lbl.setGeometry(0,0,btn.width(),btn.height()) + s['label'] = lbl + lbl = s['label'] + + if active: + movie = QMovie(str(script_dir / 'sprinkler_on.gif')) + movie.setScaledSize(QSize(btn.width(), btn.height())) + lbl.setMovie(movie) + movie.start() + lbl.show() + btn.setStyleSheet(self.sprinkler_style(active=True)) + s['movie'] = movie + else: + # static image + pix = QPixmap(str(script_dir / 'sprinkler_off.png')) if (script_dir / 'sprinkler_off.png').exists() else QPixmap(btn.width(), btn.height()) + if pix.isNull(): + pix = QPixmap(btn.width(), btn.height()); pix.fill(QColor('#6b7280')) + lbl.setPixmap(pix.scaled(btn.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) + lbl.show() + btn.setStyleSheet(self.sprinkler_style(active=False)) + s['movie'] = None + s['active'] = active + + def sprinkler_style(self, active=False): + #background: {"#86c7db26" if active else "#6b72801c"}; + # circular button with shadow and badge placeholder + base = f""" + QPushButton {{ + width: 120px; height:120px; border-radius:28px; + + font-weight: bold; + color: white; + }} + QPushButton:hover {{ }} + """ + return base + + # def update_history(self): + # # Populate filters (preserve current selection) and refresh table + # rows = fetch_sprinkler_history() + # prev_dev = self.filter_device.currentText() if self.filter_device.count()>0 else 'All' + # prev_dec = self.filter_decision.currentText() if self.filter_decision.count()>0 else 'All' + + # unique_devices = sorted({row['device_id'] for row in rows}) + # unique_decisions = sorted({row['decision'] for row in rows}) + + # # repopulate while preserving selection when possible + # self.filter_device.blockSignals(True) + # self.filter_decision.blockSignals(True) + # self.filter_device.clear(); self.filter_device.addItem('All'); self.filter_device.addItems(unique_devices) + # self.filter_decision.clear(); self.filter_decision.addItem('All'); self.filter_decision.addItems(unique_decisions) + # idx = self.filter_device.findText(prev_dev) + # if idx >= 0: + # self.filter_device.setCurrentIndex(idx) + # idx = self.filter_decision.findText(prev_dec) + # if idx >= 0: + # self.filter_decision.setCurrentIndex(idx) + # self.filter_device.blockSignals(False) + # self.filter_decision.blockSignals(False) + + # # apply currently selected filters to populate the table + # self.apply_filters() + + # def apply_filters(self): + # # fetch and filter rows locally + # rows = fetch_sprinkler_history() + # dev = self.filter_device.currentText() + # dec = self.filter_decision.currentText() + # if dev and dev != 'All': + # rows = [r for r in rows if r.get('device_id') == dev] + # if dec and dec != 'All': + # rows = [r for r in rows if r.get('decision') == dec] + + # # date filter + # try: + # dfrom = self.filter_date_from.date().toPyDate() + # dto = self.filter_date_to.date().toPyDate() + # # only apply if user set meaningful range (non-default) + # if dfrom and dto and dfrom <= dto: + # rows = [r for r in rows if r.get('ts') and dfrom <= r['ts'].date() <= dto] + # except Exception: + # pass + + # # numeric filters + # try: + # drymin = float(self.filter_dry_min.text()) if self.filter_dry_min.text() else None + # confmin = float(self.filter_conf_min.text()) if self.filter_conf_min.text() else None + # if drymin is not None: + # rows = [r for r in rows if r.get('dry_ratio', 0) >= drymin] + # if confmin is not None: + # rows = [r for r in rows if r.get('confidence', 0) >= confmin] + # except Exception: + # pass + + # self.populate_history_table(rows) + + # def populate_history_table(self, rows): + # self.history_table.setRowCount(0) + # for row in rows: + # r = self.history_table.rowCount() + # self.history_table.insertRow(r) + # self.history_table.setItem(r,0, QTableWidgetItem(str(row['ts']))) + # self.history_table.setItem(r,1, QTableWidgetItem(row['device_id'])) + # dec_item = QTableWidgetItem(row['decision']) + # if row['decision']=='run': dec_item.setBackground(QColor('#10b981')) + # elif row['decision']=='stop': dec_item.setBackground(QColor('#ef4444')) + # else: dec_item.setBackground(QColor('#f59e0b')) + # self.history_table.setItem(r,2, dec_item) + # self.history_table.setItem(r,3, QTableWidgetItem(f"{row['confidence']:.2f}")) + # self.history_table.setItem(r,4, QTableWidgetItem(f"{row['dry_ratio']:.2f}")) + # view_btn = QPushButton('View Image') + # view_btn.clicked.connect(lambda _checked, rr=row: self.show_image_modal(rr)) + # self.history_table.setCellWidget(r,5, view_btn) + + # def show_image_modal(self, row): + # # for demo: try to load an image from disk or show placeholder + # img_path = script_dir / 'sprinkler_image_placeholder.jpg' + # if img_path.exists(): + # pix = QPixmap(str(img_path)) + # else: + # pix = QPixmap(400,300) + # pix.fill(QColor('#ddd')) + # m = ImageModal(self, pix, row) + # m.exec() + + def update_data(self): + try: + rows = fetch_sprinkler_data() + healthy = True + except Exception as e: + print('Error fetching data:', e) + rows = [] + healthy = False + + # Update sprinklers on map + active_count = 0 + irrigated_area = 0.0 + last_update = '--' + max_timestamp = None + for i, row in enumerate(rows): + device_id = row['device_id'] + dry_ratio = row['dry_ratio'] + decision = row['decision'] + confidence = row['confidence'] + new_state = row['new_state'] + active = new_state == 'run' + if active: + active_count += 1 + irrigated_area += dry_ratio if active else 0 + + # Track the most recent timestamp across all sprinklers + ts = row.get('ts') + if ts and (max_timestamp is None or ts > max_timestamp): + max_timestamp = ts + + # MapWidget handles display and GIF/image ONLY (no shapes) + if device_id not in self.map_widget.sprinklers: + rel_x = (100 + i*233 + random.randint(-20, 20)) / max(1, self.map_widget.map_pixmap.width()) + rel_y = (550 + random.randint(-230, 230)) / max(1, self.map_widget.map_pixmap.height()) + rel_size = 0.025 + btn = self.map_widget.add_sprinkler(device_id, rel_x, rel_y, rel_size, active, name=device_id) + btn.clicked.connect(lambda _e, d=device_id: self.show_details(d)) + s = self.map_widget.sprinklers[device_id] + # Always set sprinkler state to ensure image/GIF is shown, never fallback to shapes + self.set_sprinkler_state(s, active) + s.update({'dry_ratio': dry_ratio, 'decision': decision, 'confidence': confidence}) + # Update main window's sprinkler info for details dialog + self.sprinklers[device_id] = s + + # Set last_update to the most recent timestamp + if max_timestamp: + last_update = str(max_timestamp) + + total_area = len(rows) + irrigated_percent = (irrigated_area / total_area * 100) if total_area else 0.0 + self.dashboard_bar.update_status(active_count, irrigated_percent, last_update, healthy) + + def show_details(self, device_id): + if device_id not in self.sprinklers: + return + # Close previous details dialog if open + if hasattr(self, '_details_card') and self._details_card: + self._details_card.close() + s = self.sprinklers[device_id] + details = { + 'active': s.get('active', False), + 'dry_ratio': s.get('dry_ratio', 0), + 'confidence': s.get('confidence', 0), + 'decision': s.get('decision', 'noop') + } + # Use new tabbed dialog + card = SprinklerDetailsDialogNew(self, device_id, details) + card.history_requested.connect(lambda d: self.show_history_panel(d)) + self._details_card = card + card.show() + + # def show_history_panel(self, device_id): + # # Close previous details dialog if open + # if hasattr(self, '_details_card') and self._details_card: + # self._details_card.close() + # # Fetch last 10 records for this sprinkler + # all_history = fetch_sprinkler_history(limit=100) + # records = [r for r in all_history if r['device_id'] == device_id][:10] + # self.history_panel.show_history(records) + # self.history_panel.show() + # self.map_widget.setFixedWidth(int(self.width() * 0.6)) + # # Add close button to history panel + # if not hasattr(self.history_panel, 'close_btn'): + # from PyQt6.QtWidgets import QPushButton + # close_btn = QPushButton('Close History') + # close_btn.setStyleSheet('background:#ef4444; color:white; padding:6px; border-radius:8px') + # close_btn.clicked.connect(self.hide_history_panel) + # layout = self.history_panel.layout() + # layout.addWidget(close_btn) + # self.history_panel.close_btn = close_btn + + # def hide_history_panel(self): + # self.history_panel.hide() + # self.map_widget.setFixedWidth(self.width()) + # # Ensure details dialog can be reopened after closing history + # self._details_card = None + + # def on_history_clicked(self, row, col): + # # show full details card when any non-button cell is clicked + # try: + # device = self.history_table.item(row,1).text() + # if device: + # self.show_details(device) + # except Exception: + # pass + +if __name__ == '__main__': + app = QApplication(sys.argv) + window = IrrigationDashboard() + window.show() + sys.exit(app.exec()) diff --git a/GUI/src/vast/views/irrigation/irrigation_view.py b/GUI/src/vast/views/irrigation/irrigation_view.py new file mode 100644 index 000000000..5eefcf05d --- /dev/null +++ b/GUI/src/vast/views/irrigation/irrigation_view.py @@ -0,0 +1,42 @@ +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QLabel +from PyQt6.QtCore import Qt +from .irrigation_dashboard import IrrigationDashboard +from dashboard_api import DashboardApi + + +class IrrigationView(QWidget): + """Wrapper to integrate IrrigationDashboard into MainWindow""" + + def __init__(self, api: DashboardApi, parent=None): + super().__init__(parent) + print("[DEBUG] IrrigationView.__init__ called") + self.api = api + + print("[DEBUG] Creating layout...") + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) + + # Create the irrigation dashboard + print("[DEBUG] Creating IrrigationDashboard...") + try: + self.dashboard = IrrigationDashboard() + print("[DEBUG] IrrigationDashboard created successfully") + print("[DEBUG] Adding dashboard to layout...") + layout.addWidget(self.dashboard) + except Exception as e: + print(f"[ERROR] Failed to create IrrigationDashboard: {e}") + import traceback + traceback.print_exc() + # Create error placeholder + error_label = QLabel(f"Failed to load Irrigation Dashboard:\n{str(e)}") + error_label.setStyleSheet("color: red; padding: 20px;") + layout.addWidget(error_label) + + print("[DEBUG] Setting stylesheet...") + self.setStyleSheet(""" + QWidget { + background: qlineargradient(x1:0,y1:0,x2:0,y2:1, stop:0 #f0f9ff, stop:1 #e0f2fe); + } + """) + print("[DEBUG] IrrigationView.__init__ completed") diff --git a/GUI/src/vast/views/irrigation/map_field.jpg b/GUI/src/vast/views/irrigation/map_field.jpg new file mode 100644 index 000000000..5c528b122 Binary files /dev/null and b/GUI/src/vast/views/irrigation/map_field.jpg differ diff --git a/GUI/src/vast/views/irrigation/map_widget.py b/GUI/src/vast/views/irrigation/map_widget.py new file mode 100644 index 000000000..a93cb2114 --- /dev/null +++ b/GUI/src/vast/views/irrigation/map_widget.py @@ -0,0 +1,104 @@ +from pathlib import Path +from PyQt6.QtWidgets import QWidget, QPushButton, QLabel +from PyQt6.QtGui import QPixmap, QMovie, QColor +from PyQt6.QtCore import Qt, QSize + +script_dir = Path(__file__).parent + +class MapWidget(QWidget): + def __init__(self, parent=None, map_path=None): + super().__init__(parent) + self.sprinklers = {} + self.map_pixmap = QPixmap(map_path) if map_path else QPixmap() + self.map_label = QLabel(self) + self.map_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + self.map_label.setStyleSheet('background: transparent;') + self.map_label.setPixmap(self.map_pixmap) + self.map_aspect = self.map_pixmap.width() / self.map_pixmap.height() if not self.map_pixmap.isNull() else 16/9 + self.last_scaled_pixmap = self.map_pixmap + self.resizeEvent(None) + + def resizeEvent(self, event): + if self.map_pixmap.isNull(): + return + available_w = max(100, self.width() - 40) + scaled_h = int(available_w / self.map_aspect) + scaled_pixmap = self.map_pixmap.scaled(available_w, scaled_h, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation) + self.last_scaled_pixmap = scaled_pixmap + self.map_label.setPixmap(scaled_pixmap) + self.map_label.setFixedHeight(scaled_pixmap.height()) + self.map_label.setGeometry(0, 0, available_w, scaled_h) + for s in self.sprinklers.values(): + # Increased size: multiplied rel_size by 1.5 and increased minimum from 40 to 60 + new_size = max(int(s['rel_size'] * scaled_pixmap.width() * 1.5), 60) + new_x = int(s['rel_x'] * scaled_pixmap.width() - new_size/2) + new_y = int(s['rel_y'] * scaled_pixmap.height() - new_size/2) + s['widget'].setGeometry(new_x, new_y, new_size, new_size) + if s.get('movie') and isinstance(s['movie'], QMovie): + s['movie'].setScaledSize(QSize(new_size, new_size)) + + def add_sprinkler(self, device_id, rel_x, rel_y, rel_size=0.025, active=False, name=None): + btn = QPushButton('', self) + btn.setObjectName(f'spr_{device_id}') + btn.setToolTip(name or device_id) + btn.setFixedSize(120, 120) + btn.setStyleSheet(self.sprinkler_style(active)) + btn.installEventFilter(self) + self.sprinklers[device_id] = { + 'widget': btn, 'movie': None, 'active': active, + 'rel_x': rel_x, 'rel_y': rel_y, 'rel_size': rel_size, + 'name': name or device_id + } + self.set_sprinkler_state(self.sprinklers[device_id], active) + self.resizeEvent(None) + return btn + + def eventFilter(self, obj, event): + # Show tooltip on hover + if event.type() == event.Type.Enter: + for s in self.sprinklers.values(): + if s['widget'] is obj: + obj.setToolTip(s.get('name', '')) + return super().eventFilter(obj, event) + + def set_sprinkler_state(self, s, active): + btn = s['widget'] + if 'label' not in s: + lbl = QLabel(btn) + lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + lbl.setGeometry(0,0,btn.width(),btn.height()) + s['label'] = lbl + lbl = s['label'] + if active: + movie = QMovie(str(script_dir / 'sprinkler_on.gif')) + movie.setScaledSize(QSize(btn.width(), btn.height())) + lbl.setMovie(movie) + movie.start() + lbl.show() + btn.setStyleSheet(self.sprinkler_style(True)) + s['movie'] = movie + else: + pix = QPixmap(str(script_dir / 'sprinkler_off.png')) + if pix.isNull(): + pix = QPixmap(btn.width(), btn.height()); pix.fill(QColor('#6b7280')) + lbl.setPixmap(pix.scaled(btn.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)) + lbl.show() + btn.setStyleSheet(self.sprinkler_style(False)) + s['movie'] = None + s['active'] = active + + def sprinkler_style(self, active=False): + base = f""" + QPushButton {{ + width: 120px; height: 120px; border-radius: 60px; background: {'#10b981' if active else '#6b7280'}; + font-weight: bold; + color: white; + }} + QPushButton:hover {{}} + """ + return base + + def show_tooltip(self, device_id): + s = self.sprinklers.get(device_id) + if s: + s['widget'].setToolTip(s.get('name', device_id)) diff --git a/GUI/src/vast/views/irrigation/sprinkler_details_dialog.py b/GUI/src/vast/views/irrigation/sprinkler_details_dialog.py new file mode 100644 index 000000000..837b648b4 --- /dev/null +++ b/GUI/src/vast/views/irrigation/sprinkler_details_dialog.py @@ -0,0 +1,21 @@ +# from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QPushButton +# from PyQt6.QtCore import Qt + +# class SprinklerDetailsDialog(QDialog): +# def __init__(self, parent, device_id, details): +# super().__init__(parent) +# self.setWindowTitle(f"Sprinkler {device_id} Details") +# self.setModal(True) +# layout = QVBoxLayout(self) +# self.label = QLabel(f"Details for {device_id}") +# layout.addWidget(self.label) +# self.details_label = QLabel(str(details)) +# layout.addWidget(self.details_label) +# self.show_history_btn = QPushButton("Show History") +# layout.addWidget(self.show_history_btn) +# self.show_history_btn.clicked.connect(self.on_show_history) +# self.history_callback = None + +# def on_show_history(self): +# if self.history_callback: +# self.history_callback() diff --git a/GUI/src/vast/views/irrigation/sprinkler_details_dialog_new.py b/GUI/src/vast/views/irrigation/sprinkler_details_dialog_new.py new file mode 100644 index 000000000..4b43dc205 --- /dev/null +++ b/GUI/src/vast/views/irrigation/sprinkler_details_dialog_new.py @@ -0,0 +1,834 @@ +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QProgressBar, + QTableWidget, QTableWidgetItem, QLineEdit, QFormLayout, QScrollArea, QGraphicsDropShadowEffect +) +from PyQt6.QtCore import Qt, QPropertyAnimation, pyqtSignal, QTimer +from PyQt6.QtGui import QFont, QColor, QPixmap +import psycopg2 +from psycopg2.extras import RealDictCursor +from datetime import datetime +from io import BytesIO +try: + from minio import Minio + MINIO_AVAILABLE = True +except ImportError: + MINIO_AVAILABLE = False + +# DB connection helper (same as in main app) +def get_db_connection(): + return psycopg2.connect( + # host="postgres", + host="postgres", + port=5432, + user="missions_user", + password="pg123", + dbname="missions_db", + cursor_factory=RealDictCursor + ) + +# MinIO connection helper +def get_minio_client(): + """Get MinIO client for accessing imagery bucket.""" + if not MINIO_AVAILABLE: + print("[DEBUG] MinIO client not available (minio package not installed)") + return None + try: + # Try multiple endpoints for flexibility + endpoints = [ + "minio-hot:9000", # Docker internal + "localhost:9001", # Desktop/host + "127.0.0.1:9001", # Loopback + ] + + for endpoint in endpoints: + try: + client = Minio( + endpoint=endpoint, + access_key="minioadmin", + secret_key="minioadmin123", + secure=False + ) + # Test connection + client.list_buckets() + print(f"[DEBUG] MinIO connected successfully at {endpoint}") + return client + except Exception as e: + print(f"[DEBUG] MinIO endpoint {endpoint} failed: {type(e).__name__}") + continue + + print("[DEBUG] MinIO: all endpoints failed") + return None + except Exception as e: + print(f"[DEBUG] MinIO connection error: {e}") + return None + +def fetch_image_from_minio(image_path: str) -> bytes: + """ + Fetch an image from MinIO 'imagery' bucket. + + Args: + image_path: The key/path to the image in MinIO (e.g., 'folder/image.jpg') + + Returns: + Image bytes, or None if fetch fails + """ + if not MINIO_AVAILABLE: + print("MinIO client not available") + return None + + try: + client = get_minio_client() + if client is None: + return None + + response = client.get_object("imagery", image_path) + image_bytes = response.read() + return image_bytes + except Exception as e: + print(f"Error fetching image from MinIO: {e}") + return None + +class SprinklerDetailsDialogNew(QWidget): + """Tabbed sprinkler details dialog with Details, History, Parameters, Last Image, and Close tabs.""" + + history_requested = pyqtSignal(str) # device_id + parameters_saved = pyqtSignal(str, dict) # device_id, params + + def __init__(self, parent, device_id, details): + super().__init__(parent) + print(f"[DEBUG] SprinklerDetailsDialogNew.__init__ called for device_id={device_id}") + self.device_id = device_id + self.details = details + self.current_tab = 'Details' + self.edited_params = {} + + self.setWindowFlags(Qt.WindowType.FramelessWindowHint) + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground) + + print(f"[DEBUG] Setting up window for {device_id}") + + # Main card + self.card = QWidget(self) + self.card.setStyleSheet(""" + background: white; + border-radius: 14px; + """) + + shadow = QGraphicsDropShadowEffect(self.card) + shadow.setBlurRadius(30) + shadow.setOffset(0, 8) + shadow.setColor(QColor(0, 0, 0, 100)) + self.card.setGraphicsEffect(shadow) + + # Main layout + main_layout = QVBoxLayout(self.card) + main_layout.setContentsMargins(20, 20, 20, 20) + main_layout.setSpacing(14) + + # Header section with title only + header_layout = QVBoxLayout() + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.setSpacing(4) + + # Title + title = QLabel(f"Sprinkler {device_id}") + title.setFont(QFont('Segoe UI', 16, QFont.Weight.Bold)) + title.setStyleSheet("color: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #06b6d4, stop:1 #0891b2);") + header_layout.addWidget(title) + + main_layout.addLayout(header_layout) + + # Content area (will be replaced based on active tab) + self.content_area = QWidget() + self.content_layout = QVBoxLayout(self.content_area) + self.content_layout.setContentsMargins(0, 0, 0, 0) + main_layout.addWidget(self.content_area, stretch=1) + + # Buttons row at bottom + btn_row = QHBoxLayout() + btn_row.setSpacing(12) + btn_row.addStretch() + + self.tab_buttons = {} + self.create_tab_buttons(btn_row) + + main_layout.addLayout(btn_row) + + # Sizing and positioning + w, h = int(parent.width() * 0.6), int(parent.height() * 0.65) + self.setGeometry((parent.width() - w) // 2, (parent.height() - h) // 2, w, h) + self.card.setGeometry(0, 0, w, h) + + # Fade-in animation + self.opacity_anim = QPropertyAnimation(self, b"windowOpacity") + self.opacity_anim.setDuration(220) + self.opacity_anim.setStartValue(0.0) + self.opacity_anim.setEndValue(1.0) + self.setWindowOpacity(0.0) + self.opacity_anim.start() + + print(f"[DEBUG] Dialog initialized, showing Details tab") + # Show Details tab initially + self.show_tab('Details') + + def create_tab_buttons(self, layout): + """Create all 5 tab buttons and add them to the layout.""" + tabs = ['Details', 'History', 'Parameters', 'Last Image', 'Close'] + for tab in tabs: + btn = QPushButton(tab) + btn.setFixedHeight(36) + btn.setMinimumWidth(100) + btn.setCursor(Qt.CursorShape.PointingHandCursor) + btn.setStyleSheet(self.get_button_style(tab == self.current_tab)) + btn.clicked.connect(lambda checked, t=tab: self.switch_tab(t)) + self.tab_buttons[tab] = btn + # Add all buttons to layout + layout.addWidget(btn) + + def get_button_style(self, active=False): + if active: + return """ + QPushButton { + background: #06b6d4; + color: white; + border: none; + border-radius: 8px; + font-weight: bold; + padding: 8px 16px; + } + """ + else: + return """ + QPushButton { + background: #e5e7eb; + color: #374151; + border: none; + border-radius: 8px; + padding: 8px 16px; + } + QPushButton:hover { + background: #d1d5db; + } + """ + + def switch_tab(self, tab_name): + """Switch to a different tab.""" + print(f"[DEBUG] switch_tab() called: {tab_name}") + if tab_name == 'Close': + self.close() + return + + self.current_tab = tab_name + + # Clear current content safely using takeAt and deleteLater + while self.content_layout.count(): + item = self.content_layout.takeAt(0) + if item.widget(): + item.widget().deleteLater() + + # Show selected tab + self.show_tab(tab_name) + + # Update button styling + self.update_button_styles() + print(f"[DEBUG] Switched to {tab_name} tab") + + def update_button_styles(self): + """Update button styles: active button highlighted, others normal.""" + for tab_name, btn in self.tab_buttons.items(): + btn.setStyleSheet(self.get_button_style(tab_name == self.current_tab)) + + def show_tab(self, tab_name): + """Display content for a tab.""" + if tab_name == 'Details': + self.show_details_tab() + elif tab_name == 'History': + self.show_history_tab() + elif tab_name == 'Parameters': + self.show_parameters_tab() + elif tab_name == 'Last Image': + self.show_last_image_tab() + + def show_details_tab(self): + """Display the details tab with improved layout and styling.""" + # Status section + status_container = QWidget() + status_layout = QHBoxLayout(status_container) + status_layout.setContentsMargins(0, 0, 0, 0) + status_layout.setSpacing(10) + + status_dot = QLabel('●') + status_dot.setFont(QFont('Arial', 14)) + status_color = '#10b981' if self.details.get('active') else '#9ca3af' + status_dot.setStyleSheet(f"color: {status_color}") + + status_text = QLabel('Active' if self.details.get('active') else 'Inactive') + status_text.setFont(QFont('Segoe UI', 11, QFont.Weight.Bold)) + status_text.setStyleSheet(f"color: {status_color}") + + status_layout.addWidget(status_dot) + status_layout.addWidget(status_text) + status_layout.addStretch() + + self.content_layout.addWidget(status_container) + self.content_layout.addSpacing(12) + + # Soil Moisture section + moisture_label = QLabel('Soil Moisture') + moisture_label.setFont(QFont('Segoe UI', 11, QFont.Weight.Bold)) + moisture_label.setStyleSheet("color: #1f2937;") + self.content_layout.addWidget(moisture_label) + + dry_ratio = self.details.get('dry_ratio', 0) + moisture_percent = int(dry_ratio * 100) + + # Percentage label above bar + moisture_value_label = QLabel(f"{moisture_percent}%") + moisture_value_label.setFont(QFont('Segoe UI', 10, QFont.Weight.Bold)) + moisture_value_label.setStyleSheet("color: #06b6d4;") + self.content_layout.addWidget(moisture_value_label) + + # Progress bar + moisture_bar = QProgressBar() + moisture_bar.setRange(0, 100) + moisture_bar.setValue(moisture_percent) + moisture_bar.setFixedHeight(12) + moisture_bar.setStyleSheet(""" + QProgressBar { + border: 1px solid #e5e7eb; + border-radius: 6px; + background: #f3f4f6; + } + QProgressBar::chunk { + background: qlineargradient(x1:0, y1:0, x2:1, y2:0, + stop:0 #06b6d4, stop:1 #0891b2); + border-radius: 5px; + } + """) + self.content_layout.addWidget(moisture_bar) + self.content_layout.addSpacing(16) + + # Confidence section + confidence_label = QLabel('Model Confidence') + confidence_label.setFont(QFont('Segoe UI', 11, QFont.Weight.Bold)) + confidence_label.setStyleSheet("color: #1f2937;") + self.content_layout.addWidget(confidence_label) + + conf = self.details.get('confidence', 0) + conf_percent = int(conf * 100) + + # Determine color based on confidence + if conf >= 0.75: + conf_color = '#10b981' + elif conf >= 0.4: + conf_color = '#f59e0b' + else: + conf_color = '#ef4444' + + # Percentage label above bar + conf_value_label = QLabel(f"{conf_percent}%") + conf_value_label.setFont(QFont('Segoe UI', 10, QFont.Weight.Bold)) + conf_value_label.setStyleSheet(f"color: {conf_color};") + self.content_layout.addWidget(conf_value_label) + + # Confidence progress bar + conf_bar = QProgressBar() + conf_bar.setRange(0, 100) + conf_bar.setValue(conf_percent) + conf_bar.setFixedHeight(12) + conf_bar.setStyleSheet(f""" + QProgressBar {{ + border: 1px solid #e5e7eb; + border-radius: 6px; + background: #f3f4f6; + }} + QProgressBar::chunk {{ + background: {conf_color}; + border-radius: 5px; + }} + """) + self.content_layout.addWidget(conf_bar) + self.content_layout.addSpacing(16) + + # Decision section + decision = self.details.get('decision', 'noop') + # Replace 'noop' with 'no operation' + if decision == 'noop': + decision = 'no operation' + + decision_label = QLabel('Decision') + decision_label.setFont(QFont('Segoe UI', 11, QFont.Weight.Bold)) + decision_label.setStyleSheet("color: #1f2937;") + self.content_layout.addWidget(decision_label) + + # Decision badge + decision_badge = QLabel(decision.upper()) + decision_badge.setFont(QFont('Segoe UI', 10, QFont.Weight.Bold)) + decision_badge.setAlignment(Qt.AlignmentFlag.AlignCenter) + decision_badge.setFixedHeight(32) + + if decision == 'run': + badge_color = '#10b981' + bg_color = '#d1fae5' + elif decision == 'stop': + badge_color = '#ef4444' + bg_color = '#fee2e2' + else: # no operation + badge_color = '#f59e0b' + bg_color = '#fef3c7' + + decision_badge.setStyleSheet(f""" + background: {bg_color}; + color: {badge_color}; + border-radius: 6px; + padding: 6px; + """) + self.content_layout.addWidget(decision_badge) + + self.content_layout.addStretch() + + def show_history_tab(self): + """Display the history tab with a beautifully styled table inside the dialog.""" + records = [] + try: + # Direct database query to avoid import issues + query = """ + SELECT e.device_id, e.dry_ratio, e.decision, e.confidence, e.patch_count, e.ts + FROM soil_moisture_events e + WHERE e.device_id = %s + ORDER BY e.ts DESC + LIMIT 10 + """ + with get_db_connection() as conn: + with conn.cursor() as cur: + cur.execute(query, (self.device_id,)) + records = cur.fetchall() + # Convert tuples to dicts if needed + if records and not isinstance(records[0], dict): + columns = ['device_id', 'dry_ratio', 'decision', 'confidence', 'patch_count', 'ts'] + records = [dict(zip(columns, row)) for row in records] + except Exception as e: + print(f"Error fetching history: {e}") + records = [] + + # Add title label + title = QLabel('Last 10 Events') + title.setFont(QFont('Segoe UI', 9, QFont.Weight.Bold)) + title.setStyleSheet("color: #374151; margin-bottom: 8px;") + self.content_layout.addWidget(title) + + # History table with better column layout + table = QTableWidget(0, 5) + table.setHorizontalHeaderLabels(['Time', 'Soil %', 'Confidence %', 'Decision', 'Patches']) + table.setAlternatingRowColors(True) + table.setColumnWidth(0, 270) # Time + table.setColumnWidth(1, 70) # Soil % + table.setColumnWidth(2, 70) # Confidence % + table.setColumnWidth(3, 130) # Decision (increased from 100 to 130) + table.setColumnWidth(4, 70) # Patches + table.resizeRowsToContents() + table.setWordWrap(True) + # Set minimum row height for better visibility + table.verticalHeader().setDefaultSectionSize(32) + + # Professional styling with better padding + table.setStyleSheet(""" + QTableWidget { + background: white; + alternate-background-color: #f9fafb; + gridline-color: #e5e7eb; + border: 1px solid #e5e7eb; + border-radius: 6px; + } + QHeaderView::section { + background: #f3f4f6; + padding: 12px; + border: none; + font-weight: bold; + color: #374151; + border-right: 1px solid #d1d5db; + } + QTableWidget::item { + padding: 12px; + border-bottom: 1px solid #e5e7eb; + } + """) + table.verticalHeader().hide() + table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows) + table.setSelectionMode(QTableWidget.SelectionMode.SingleSelection) + + # Populate rows + for row in records: + r = table.rowCount() + table.insertRow(r) + + # Column 0: Timestamp + ts_item = QTableWidgetItem(str(row['ts'])) + ts_item.setFont(QFont('Courier', 9)) + ts_item.setForeground(QColor('#6b7280')) + table.setItem(r, 0, ts_item) + + # Column 1: Soil Moisture % (dry_ratio as %) + dry_percent = int(row['dry_ratio'] * 100) + moisture_item = QTableWidgetItem(f"{dry_percent}%") + moisture_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + moisture_item.setFont(QFont('Courier', 9)) + table.setItem(r, 1, moisture_item) + + # Column 2: Confidence % + conf_percent = int(row['confidence'] * 100) + conf_item = QTableWidgetItem(f"{conf_percent}%") + conf_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + conf_item.setFont(QFont('Courier', 9)) + table.setItem(r, 2, conf_item) + + # Column 3: Decision with color coding + decision = row['decision'] + if decision == 'noop': + decision = 'no operation' + + # Use shorter display text for better fit in cell + decision_text = { + 'run': 'RUN', + 'stop': 'STOP', + 'no operation': 'NO OPERATION' + }.get(decision, decision.upper()) + + dec_item = QTableWidgetItem(decision_text) + dec_item.setFont(QFont('Segoe UI', 9, QFont.Weight.Bold)) + dec_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + + # Set background and text colors with proper contrast + if decision == 'run': + dec_item.setBackground(QColor("#dde9e5")) + dec_item.setForeground(QColor("#58b7e4")) # White text on green + elif decision == 'stop': + dec_item.setBackground(QColor('#fee2e2')) # Light red background + dec_item.setForeground(QColor('#dc2626')) # Dark red text + else: # no operation + dec_item.setBackground(QColor('#f59e0b')) + dec_item.setForeground(QColor('#1f2937')) # Dark text for better contrast on orange + + table.setItem(r, 3, dec_item) + + # Column 4: Patches (patch count) + patch_item = QTableWidgetItem(f"{row.get('patch_count', 0)}") + patch_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + patch_item.setFont(QFont('Courier', 9)) + table.setItem(r, 4, patch_item) + + self.content_layout.addWidget(table, stretch=1) + + def show_parameters_tab(self): + """Display the parameters tab with editable fields.""" + try: + params = self.fetch_parameters() + except Exception: + params = {} + + # Form for parameters + form = QFormLayout() + form.setSpacing(16) + form.setContentsMargins(0, 0, 0, 0) + + # Define input styling + input_style = """ + QLineEdit { + border: 2px solid #e5e7eb; + border-radius: 8px; + padding: 6px; + background: #f9fafb; + color: #1f2937; + font-size: 11pt; + font-weight: 500; + } + QLineEdit:focus { + border: 2px solid #06b6d4; + background: #ffffff; + } + QLineEdit:hover { + border: 2px solid #d1d5db; + } + """ + + # Define label styling + label_style = """ + color: #374151; + font-weight: bold; + font-size: 11pt; + """ + + self.param_inputs = {} + + # Dry Ratio High + dry_high_label = QLabel('Dry Ratio High:') + dry_high_label.setStyleSheet(label_style) + dry_high_input = QLineEdit() + dry_high_input.setText(str(params.get('dry_ratio_high', 0.7))) + dry_high_input.setStyleSheet(input_style) + dry_high_input.setMinimumHeight(40) + self.param_inputs['dry_ratio_high'] = dry_high_input + form.addRow(dry_high_label, dry_high_input) + + # Dry Ratio Low + dry_low_label = QLabel('Dry Ratio Low:') + dry_low_label.setStyleSheet(label_style) + dry_low_input = QLineEdit() + dry_low_input.setText(str(params.get('dry_ratio_low', 0.4))) + dry_low_input.setStyleSheet(input_style) + dry_low_input.setMinimumHeight(40) + self.param_inputs['dry_ratio_low'] = dry_low_input + form.addRow(dry_low_label, dry_low_input) + + # Min Patches + min_patches_label = QLabel('Min Patches:') + min_patches_label.setStyleSheet(label_style) + min_patches_input = QLineEdit() + min_patches_input.setText(str(params.get('min_patches', 1))) + min_patches_input.setStyleSheet(input_style) + min_patches_input.setMinimumHeight(40) + self.param_inputs['min_patches'] = min_patches_input + form.addRow(min_patches_label, min_patches_input) + + # Duration (minutes) + # duration_label = QLabel('Duration (min):') + # duration_label.setStyleSheet(label_style) + # duration_input = QLineEdit() + # duration_input.setText(str(params.get('duration_min', 30))) + # duration_input.setStyleSheet(input_style) + # duration_input.setMinimumHeight(40) + # self.param_inputs['duration_min'] = duration_input + # form.addRow(duration_label, duration_input) + + # Save button + save_btn = QPushButton('Save Parameters') + save_btn.setStyleSheet(""" + QPushButton { + background: #10b981; + color: white; + border: none; + border-radius: 8px; + padding: 12px 16px; + font-weight: bold; + font-size: 11pt; + } + QPushButton:hover { + background: #059669; + } + QPushButton:pressed { + background: #047857; + } + """) + save_btn.setMinimumHeight(44) + save_btn.clicked.connect(self.save_parameters) + + scroll = QScrollArea() + scroll_widget = QWidget() + scroll_widget.setLayout(form) + scroll.setWidget(scroll_widget) + scroll.setWidgetResizable(True) + scroll.setStyleSheet("QScrollArea { border: none; background: transparent; }") + + self.content_layout.addWidget(scroll) + self.content_layout.addSpacing(12) + self.content_layout.addWidget(save_btn) + + def fetch_parameters(self): + """Fetch parameters from irrigation_policies table.""" + try: + with get_db_connection() as conn: + with conn.cursor() as cur: + query = """ + SELECT dry_ratio_high, dry_ratio_low, min_patches, duration_min + FROM irrigation_policies + WHERE device_id = %s + """ + cur.execute(query, (self.device_id,)) + row = cur.fetchone() + + if row: + # Handle both tuple and dict-like responses + if isinstance(row, (tuple, list)): + return { + 'dry_ratio_high': float(row[0]) if row[0] is not None else 0.7, + 'dry_ratio_low': float(row[1]) if row[1] is not None else 0.4, + 'min_patches': int(row[2]) if row[2] is not None else 1, + 'duration_min': int(row[3]) if row[3] is not None else 30 + } + else: + # Dictionary-like response + return { + 'dry_ratio_high': float(row.get('dry_ratio_high', 0.7)), + 'dry_ratio_low': float(row.get('dry_ratio_low', 0.4)), + 'min_patches': int(row.get('min_patches', 1)), + 'duration_min': int(row.get('duration_min', 30)) + } + except Exception as e: + print(f"Error fetching parameters: {type(e).__name__}: {str(e)}") + return {} + + def save_parameters(self): + """Save parameters to irrigation_policies table.""" + try: + # Validate and convert inputs + dry_ratio_high = float(self.param_inputs['dry_ratio_high'].text()) + dry_ratio_low = float(self.param_inputs['dry_ratio_low'].text()) + min_patches = int(self.param_inputs['min_patches'].text()) + duration_min = 10 #int(self.param_inputs['duration_min'].text()) + + # Basic validation + if not (0 <= dry_ratio_high <= 1): + raise ValueError("Dry Ratio High must be between 0 and 1") + if not (0 <= dry_ratio_low <= 1): + raise ValueError("Dry Ratio Low must be between 0 and 1") + if min_patches < 0: + raise ValueError("Min Patches must be positive") + if duration_min < 0: + raise ValueError("Duration must be positive") + + with get_db_connection() as conn: + with conn.cursor() as cur: + query = """ + INSERT INTO irrigation_policies + (device_id, dry_ratio_high, dry_ratio_low, min_patches, duration_min) + VALUES (%s, %s, %s, %s, %s) + ON CONFLICT (device_id) DO UPDATE SET + dry_ratio_high = EXCLUDED.dry_ratio_high, + dry_ratio_low = EXCLUDED.dry_ratio_low, + min_patches = EXCLUDED.min_patches, + duration_min = EXCLUDED.duration_min + """ + cur.execute(query, (self.device_id, dry_ratio_high, dry_ratio_low, min_patches, duration_min)) + conn.commit() + + # Show success message + msg_label = QLabel("✓ Parameters saved successfully!") + msg_label.setStyleSheet("color: #10b981; font-weight: bold; padding: 8px;") + self.content_layout.addWidget(msg_label) + print(f"Parameters saved for device {self.device_id}") + + except ValueError as ve: + print(f"Validation error: {ve}") + msg_label = QLabel(f"✗ Validation error: {ve}") + msg_label.setStyleSheet("color: #ef4444; font-weight: bold; padding: 8px;") + self.content_layout.addWidget(msg_label) + except Exception as e: + print(f"Error saving parameters: {type(e).__name__}: {str(e)}") + msg_label = QLabel(f"✗ Error: {type(e).__name__}") + msg_label.setStyleSheet("color: #ef4444; font-weight: bold; padding: 8px;") + self.content_layout.addWidget(msg_label) + + def show_last_image_tab(self): + """Display the last image tab with image from MinIO.""" + print(f"[DEBUG] show_last_image_tab() called for {self.device_id}") + try: + # Query database for the most recent image path + query = """ + SELECT extra->>'image_path' as image_path, ts + FROM soil_moisture_events + WHERE device_id = %s AND extra->>'image_path' IS NOT NULL + ORDER BY ts DESC + LIMIT 1 + """ + with get_db_connection() as conn: + with conn.cursor() as cur: + cur.execute(query, (self.device_id,)) + row = cur.fetchone() + print(f"[DEBUG] Query result: {row}") + + if row and row.get('image_path'): + image_path = row['image_path'] + ts = row.get('ts', 'Unknown') + print(f"[DEBUG] Found image path: {image_path}, ts: {ts}") + + # Fetch image from MinIO + image_bytes = fetch_image_from_minio(image_path) + + if image_bytes: + print(f"[DEBUG] Successfully fetched image, size: {len(image_bytes)} bytes") + # Display the image + pixmap = QPixmap() + pixmap.loadFromData(image_bytes) + + if not pixmap.isNull(): + # Create container for image display + image_container = QWidget() + image_layout = QVBoxLayout(image_container) + image_layout.setContentsMargins(0, 0, 0, 0) + image_layout.setSpacing(12) + + # Timestamp label + ts_label = QLabel(f"Timestamp: {ts}") + ts_label.setFont(QFont('Segoe UI', 10, QFont.Weight.Bold)) + ts_label.setStyleSheet("color: #6b7280;") + image_layout.addWidget(ts_label) + + # Image path label + path_label = QLabel(f"Path: {image_path}") + path_label.setFont(QFont('Courier', 9)) + path_label.setStyleSheet("color: #9ca3af; word-wrap: true;") + path_label.setWordWrap(True) + image_layout.addWidget(path_label) + + # Scale image to fit in dialog + max_width = int(self.card.width() * 0.8) + max_height = 400 + scaled_pixmap = pixmap.scaledToWidth( + max_width, + Qt.TransformationMode.SmoothTransformation + ) + if scaled_pixmap.height() > max_height: + scaled_pixmap = scaled_pixmap.scaledToHeight( + max_height, + Qt.TransformationMode.SmoothTransformation + ) + + # Image label + img_label = QLabel() + img_label.setPixmap(scaled_pixmap) + img_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + img_label.setStyleSheet("border: 1px solid #e5e7eb; border-radius: 6px;") + image_layout.addWidget(img_label, stretch=1) + + self.content_layout.addWidget(image_container, stretch=1) + print(f"[DEBUG] Image displayed successfully") + return + else: + error_msg = "Failed to load image from bytes" + print(f"[DEBUG] {error_msg}") + self.show_image_error(error_msg) + return + else: + error_msg = f"Could not fetch image from MinIO: {image_path}" + print(f"[DEBUG] {error_msg}") + self.show_image_error(error_msg) + return + else: + # No image path found in recent events + error_msg = "No image data available for this device" + print(f"[DEBUG] {error_msg}") + self.show_image_error(error_msg) + return + except Exception as e: + error_msg = f"Error fetching image: {str(e)}" + print(f"[DEBUG] {error_msg}") + import traceback + traceback.print_exc() + self.show_image_error(error_msg) + + def show_image_error(self, error_message: str): + """Display error message in the image tab.""" + error_container = QWidget() + error_layout = QVBoxLayout(error_container) + error_layout.setContentsMargins(0, 0, 0, 0) + error_layout.addStretch() + + error_label = QLabel(error_message) + error_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + error_label.setFont(QFont('Segoe UI', 12)) + error_label.setStyleSheet("color: #f59e0b; padding: 20px;") + error_label.setWordWrap(True) + error_layout.addWidget(error_label) + + error_layout.addStretch() + self.content_layout.addWidget(error_container, stretch=1) diff --git a/GUI/src/vast/views/irrigation/sprinkler_off.png b/GUI/src/vast/views/irrigation/sprinkler_off.png new file mode 100644 index 000000000..1807f873c Binary files /dev/null and b/GUI/src/vast/views/irrigation/sprinkler_off.png differ diff --git a/GUI/src/vast/views/irrigation/sprinkler_on.gif b/GUI/src/vast/views/irrigation/sprinkler_on.gif new file mode 100644 index 000000000..4401de4bd Binary files /dev/null and b/GUI/src/vast/views/irrigation/sprinkler_on.gif differ diff --git a/GUI/src/vast/views/leaf_diseases.py b/GUI/src/vast/views/leaf_diseases.py new file mode 100644 index 000000000..7b9fe59fb --- /dev/null +++ b/GUI/src/vast/views/leaf_diseases.py @@ -0,0 +1,708 @@ +# 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, time, timezone +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() + # If Grafana card is open and we have a selected disease, reload with new range + if getattr(self, "grafana_card", None) and self.grafana_card.isVisible(): + if hasattr(self, "_sel_disease"): + self._on_disease_clicked(self._sel_disease) + + 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 + + def _grafana_range_params(self) -> str: + """ + Build Grafana from/to range based on current date_from/date_to widgets. + + Grafana expects timestamps in milliseconds since epoch. + We take: + - from = start of date_from day (00:00) + - to = end of date_to day (23:59:59.999) + """ + d_from = self.date_from.date().toPyDate() + d_to = self.date_to.date().toPyDate() + + # Start of day (UTC) + start_dt = datetime.combine(d_from, time.min).replace(tzinfo=timezone.utc) + # End of day (UTC) + end_dt = datetime.combine(d_to, time.max).replace(tzinfo=timezone.utc) + + start_ms = int(start_dt.timestamp() * 1000) + end_ms = int(end_dt.timestamp() * 1000) + + return f"&from={start_ms}&to={end_ms}&timezone=browser" + + # ───────── 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})") + + # Save selected disease so we can refresh on range change if needed + self._sel_disease = 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 + date range + range_params = self._grafana_range_params() + 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}" + f"{range_params}" + ) + + 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/GUI/src/vast/views/sound_view.py b/GUI/src/vast/views/notification_view.py similarity index 82% rename from GUI/src/vast/views/sound_view.py rename to GUI/src/vast/views/notification_view.py index f41d21690..a1552fd15 100644 --- a/GUI/src/vast/views/sound_view.py +++ b/GUI/src/vast/views/notification_view.py @@ -5,7 +5,7 @@ from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtCore import QUrl -class SoundView(QWidget): +class NotificationView(QWidget): def __init__(self, parent=None): super().__init__(parent) layout = QVBoxLayout(self) @@ -14,7 +14,7 @@ def __init__(self, parent=None): web_view = QWebEngineView(self) notification_api_url = "http://notification_api:5000" - print(f"[SoundView] Loading URL: {notification_api_url}") + 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/GUI/src/vast/views/security/analytics/analytics_page.py b/GUI/src/vast/views/security/analytics/analytics_page.py new file mode 100644 index 000000000..0bf2ae31e --- /dev/null +++ b/GUI/src/vast/views/security/analytics/analytics_page.py @@ -0,0 +1,561 @@ +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 AgGuardMessageBox(QFrame): + def __init__(self, parent=None, title="Message", text=""): + super().__init__(parent) + + self.setWindowFlags( + Qt.WindowType.Dialog | + Qt.WindowType.FramelessWindowHint | + Qt.WindowType.WindowStaysOnTopHint + ) + # no translucent background → no black corners + self.setAutoFillBackground(True) + pal = self.palette() + pal.setColor(self.backgroundRole(), QColor("#fefefe")) + self.setPalette(pal) + + # Light drop shadow + shadow = QGraphicsDropShadowEffect() + shadow.setBlurRadius(28) + shadow.setOffset(0, 3) + shadow.setColor(QColor(200, 200, 200, 90)) + self.setGraphicsEffect(shadow) + + self.setStyleSheet(""" + QFrame { + background-color: #fefefe; + border-radius: 16px; + border: 1px solid #f3f4f6; + } + QLabel#title { + font-size: 18px; + font-weight: 700; + color: #1f2937; + } + QLabel#text { + font-size: 14px; + color: #4b5563; + } + QPushButton#ok_btn { + background-color: #10b981; + color: white; + border-radius: 10px; + padding: 6px 18px; + font-weight: 600; + min-width: 80px; + } + QPushButton#ok_btn:hover { + background-color: #059669; + } + """) + + layout = QVBoxLayout(self) + layout.setContentsMargins(22, 22, 22, 22) + layout.setSpacing(14) + + title_lbl = QLabel(title) + title_lbl.setObjectName("title") + + text_lbl = QLabel(text) + text_lbl.setObjectName("text") + text_lbl.setWordWrap(True) + + btn = QPushButton("OK") + btn.setObjectName("ok_btn") + btn.clicked.connect(self.close) + + layout.addWidget(title_lbl) + layout.addWidget(text_lbl) + layout.addStretch() + layout.addWidget(btn, alignment=Qt.AlignmentFlag.AlignRight) + + self.adjustSize() + self.setMinimumWidth(360) + + def show_centered(self): + if self.parent(): + parent_geo = self.parent().geometry() + x = parent_geo.x() + (parent_geo.width() - self.width()) // 2 + y = parent_geo.y() + (parent_geo.height() - self.height()) // 2 + self.move(x, y) + self.show() + + +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) + + 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) + + 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) + splitter.setStyleSheet(""" + QSplitter::handle { + background-color: #e5e7eb; + } + QSplitter::handle:hover { + background-color: #10b981; + } + """) + + # ───────────────────────────── + # LEFT SIDE — map card + query box + # ───────────────────────────── + map_container = QWidget() + map_container_layout = QVBoxLayout(map_container) + map_container_layout.setContentsMargins(0, 0, 0, 0) + map_container_layout.setSpacing(0) + + # Outer shell (white card with strong round corners + shadow) + map_shell = QFrame() + map_shell.setObjectName("mapShell") + map_shell.setStyleSheet(""" + QFrame#mapShell { + background-color: #ffffff; + border-radius: 24px; + border: 1px solid #e5e7eb; + } + """) + map_shell_shadow = QGraphicsDropShadowEffect() + map_shell_shadow.setBlurRadius(26) + map_shell_shadow.setOffset(0, 8) + map_shell_shadow.setColor(QColor(15, 23, 42, 55)) + map_shell.setGraphicsEffect(map_shell_shadow) + + shell_layout = QVBoxLayout(map_shell) + shell_layout.setContentsMargins(14, 14, 14, 14) + shell_layout.setSpacing(10) + + # Inner rounded frame that actually clips the map + map_frame = QFrame() + map_frame.setObjectName("mapFrame") + map_frame.setStyleSheet(""" + QFrame#mapFrame { + background-color: #020617; /* dark slate */ + border-radius: 18px; + border: none; + } + """) + map_frame.setContentsMargins(0, 0, 0, 0) + map_frame_layout = QVBoxLayout(map_frame) + map_frame_layout.setContentsMargins(0, 0, 0, 0) + map_frame_layout.setSpacing(0) + + tiles_root = "./src/vast/orthophoto_canvas/data/tiles" + self.viewer = create_orthophoto_viewer(tiles_root, forced_scheme=None, parent=self) + + # Make the view itself frameless and rounded + self.viewer.setFrameShape(QFrame.Shape.NoFrame) + self.viewer.setStyleSheet(""" + QGraphicsView { + background-color: transparent; + border: none; + } + QGraphicsView::viewport { + border-radius: 18px; /* real rounded viewport */ + background-color: #020617; + } + """) + self.viewer.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, True) + + # Optional static background instead of tiles + self.viewer.set_custom_background_image( + "./src/vast/orthophoto_canvas/ui/fields.png", + hide_tiles=True + ) + self.viewer.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + + map_frame_layout.addWidget(self.viewer, stretch=1) + + # Query bar sits under the map, inside the same shell + query_frame.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) + + shell_layout.addWidget(map_frame, stretch=1) + shell_layout.addWidget(query_frame, stretch=0) + + map_container_layout.addWidget(map_shell) + 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() + self._initial_fit_done = False + QtCore.QTimer.singleShot(0, self._fit_map_to_container) + + # Ensure map always fills its container nicely + def resizeEvent(self, event): + super().resizeEvent(event) + QtCore.QTimer.singleShot(0, self._fit_map_to_container) + + def _fit_map_to_container(self): + """ + Make the map fill the entire map container (no gray bars). + Run after layouts/child resizes using QTimer.singleShot. + """ + if not hasattr(self, "viewer") or self.viewer is None: + return + + scene_rect = self.viewer.scene.sceneRect() + if scene_rect.isNull(): + return + + self.viewer.fitInView(scene_rect, Qt.AspectRatioMode.KeepAspectRatioByExpanding) + self.viewer.centerOn(scene_rect.center()) + + # ───────────────────────────── + # 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("...") + QApplication.processEvents() + + try: + result = ap.select_entities_from_prompt(prompt) + except Exception as e: + dlg = AgGuardMessageBox( + self, + title="Invalid Query", + text="We couldn't understand your request.\nPlease try rephrasing your query or using clearer terms." + ) + dlg.show_centered() + + # restore state and abort + 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/GUI/src/vast/views/security/analytics/analytics_provider.py b/GUI/src/vast/views/security/analytics/analytics_provider.py new file mode 100644 index 000000000..9c9b9a599 --- /dev/null +++ b/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/services/sounds/API-development/src/backend/__init__.py b/GUI/src/vast/views/security/analytics/map_layers/__init__.py similarity index 100% rename from services/sounds/API-development/src/backend/__init__.py rename to GUI/src/vast/views/security/analytics/map_layers/__init__.py diff --git a/GUI/src/vast/views/security/analytics/map_layers/device_layer.py b/GUI/src/vast/views/security/analytics/map_layers/device_layer.py new file mode 100644 index 000000000..127249c81 --- /dev/null +++ b/GUI/src/vast/views/security/analytics/map_layers/device_layer.py @@ -0,0 +1,179 @@ +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 + + +# ───────────────────────────────────────────── +# 🗺️ 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 = _Camera360Marker( + device["device_id"], + device.get("active", True), + self.on_select, + radius=10.0, # tweak for size + ) + 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}") +from PyQt6.QtWidgets import QGraphicsObject, QGraphicsDropShadowEffect +from PyQt6.QtGui import QColor, QPen, QBrush, QPainter +from PyQt6.QtCore import QRectF, QPropertyAnimation, QEasingCurve, pyqtSlot + + +class _Camera360Marker(QGraphicsObject): + """360° camera marker: donut + center dot + glow + pulse.""" + + def __init__(self, device_id: str, active: bool, on_select=None, radius: float = 10.0): + super().__init__() + self.device_id = device_id + self.active = active + self.on_select = on_select + self.selected = False + self._radius = radius + + # Colors + self.normal_color = QColor("#10b981") if active else QColor("#9ca3af") # green / gray + self.alert_color = QColor("#ef4444") # red for alerts if you use it + self.selected_color = QColor("#ffffff") + + # We draw in local coords around (0,0); QGraphicsView will position us + self.setZValue(1000) + self.setFlag(self.GraphicsItemFlag.ItemIgnoresTransformations, True) # stay same size on zoom + self.setAcceptHoverEvents(True) + + # Drop shadow halo for glow + self.halo = QGraphicsDropShadowEffect() + self.halo.setBlurRadius(32) + self.halo.setOffset(0, 0) + self.halo.setColor(QColor(16, 185, 129, 180)) + self.setGraphicsEffect(None) # only on selection + + # Pulse animation on opacity + self.pulse = QPropertyAnimation(self, b"opacity") + self.pulse.setDuration(1000) + self.pulse.setStartValue(1.0) + self.pulse.setEndValue(0.6) + self.pulse.setEasingCurve(QEasingCurve.Type.InOutQuad) + self.pulse.setLoopCount(-1) + + # ───────────────────────────────────────────── + # Required overrides + # ───────────────────────────────────────────── + def boundingRect(self) -> QRectF: + # a bit larger than radius to accommodate the stroke + halo + r = self._radius + 4 + return QRectF(-r, -r, 2 * r, 2 * r) + + def paint(self, painter: QPainter, option, widget=None): + painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) + + # Base color depends on state + base = self.normal_color if not self.selected else self.normal_color + + # Outer ring (donut) + outer_r = self._radius + inner_r = self._radius * 0.55 + + # Outer circle stroke + pen = QPen(base) + pen.setWidthF(2.0 if not self.selected else 3.0) + painter.setPen(pen) + painter.setBrush(Qt.BrushStyle.NoBrush) + painter.drawEllipse(QRectF(-outer_r, -outer_r, 2 * outer_r, 2 * outer_r)) + + # Soft filled ring (semi-transparent) + ring_color = QColor(base.red(), base.green(), base.blue(), 80) + painter.setBrush(QBrush(ring_color)) + painter.setPen(Qt.PenStyle.NoPen) + painter.drawEllipse(QRectF(-outer_r, -outer_r, 2 * outer_r, 2 * outer_r)) + + # Cut inner circle to make donut effect (by drawing a solid inner circle of background color) + inner_bg = QColor("#0f172a") # same tone as map background / dark outline + painter.setBrush(inner_bg) + painter.drawEllipse(QRectF(-inner_r, -inner_r, 2 * inner_r, 2 * inner_r)) + + # Center dot – represents the physical camera + center_r = inner_r * 0.5 + painter.setBrush(base if not self.selected else self.selected_color) + painter.setPen(Qt.PenStyle.NoPen) + painter.drawEllipse(QRectF(-center_r, -center_r, 2 * center_r, 2 * center_r)) + + # ───────────────────────────────────────────── + # Interaction + # ───────────────────────────────────────────── + def mousePressEvent(self, event): + self.toggle_selected() + if self.on_select: + self.on_select(self.device_id, self.selected) + super().mousePressEvent(event) + + @pyqtSlot() + def toggle_selected(self): + self.selected = not self.selected + + if self.selected: + # enable glow + pulse + self.setGraphicsEffect(self.halo) + self.pulse.start() + else: + # disable glow + pulse + self.setGraphicsEffect(None) + self.pulse.stop() + self.setOpacity(1.0) + self.update() diff --git a/GUI/src/vast/views/security/analytics/map_layers/region_layer.py b/GUI/src/vast/views/security/analytics/map_layers/region_layer.py new file mode 100644 index 000000000..b457c5e6c --- /dev/null +++ b/GUI/src/vast/views/security/analytics/map_layers/region_layer.py @@ -0,0 +1,186 @@ +from __future__ import annotations + +import json +from typing import List, Optional, Tuple + +from PyQt6.QtWidgets import QGraphicsPolygonItem +from PyQt6.QtGui import QColor, QPen, QPolygonF +from PyQt6.QtCore import Qt, QPointF + +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. + Uses tile-based projection when possible, and falls back to a linear + lon/lat → scene mapping based on the known map coverage if needed. + """ + + def __init__(self, viewer, on_select=None): + self.viewer = viewer + self.scene = viewer.scene + self.regions: List[QGraphicsPolygonItem] = [] + 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_width = width + self._scene_height = height + + # For reference; not used for clipping anymore + self.scene_bounds = (0, 0, width, height) + + # 🔹 Map lon/lat bounds – SAME as you used in your SQL + # [COVERAGE z=18] lon:[34.844513..34.855499] lat:[31.895049..31.904376] + self._map_min_lon = 34.844513 + self._map_max_lon = 34.855499 + self._map_min_lat = 31.895049 + self._map_max_lat = 31.904376 + + # ───────────────────────────────────────────── + # Helper: robust projection + # ───────────────────────────────────────────── + def _project_lon_lat(self, lon: float, lat: float) -> Optional[Tuple[float, float]]: + """ + Try the original tile-based projection first. + If it fails (None), fall back to a simple linear mapping + from [min_lon..max_lon] × [min_lat..max_lat] → [0..width] × [0..height]. + """ + # 1) Try existing helper – keeps consistency with tiles/sensors when it works + pos = _latlon_to_xy_at_max_zoom(self.viewer, lat, lon) + if pos: + xb, yb = pos + sx = (xb - self.viewer._x_min_base) * TILE_SIZE + sy = (yb - self.viewer._y_min_base) * TILE_SIZE + return sx, sy + + # 2) Fallback: linear mapping using known map bounds + # Guard against division by zero + if ( + self._map_max_lon == self._map_min_lon + or self._map_max_lat == self._map_min_lat + ): + return None + + # Normalize lon/lat into [0,1] + x_norm = (lon - self._map_min_lon) / (self._map_max_lon - self._map_min_lon) + y_norm = (lat - self._map_min_lat) / (self._map_max_lat - self._map_min_lat) + + # Clip just in case (should already be in [0,1]) + x_norm = max(0.0, min(1.0, x_norm)) + y_norm = max(0.0, min(1.0, y_norm)) + + # Scene coords: x grows right, y grows down → flip y + sx = x_norm * self._scene_width + sy = (1.0 - y_norm) * self._scene_height + + return sx, sy + + # ───────────────────────────────────────────── + def add_region(self, region: dict, start_date=None, end_date=None, selected_ids=None): + """Add a region polygon to the orthophoto map.""" + 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 + + coords = geom_json["coordinates"] + gtype = geom_json.get("type") + + # Handle Polygon / MultiPolygon + if gtype == "Polygon": + outer_ring = coords[0] + elif gtype == "MultiPolygon": + # Pick the largest polygon’s outer ring + outer_ring = max(coords, key=lambda c: len(c[0]))[0] + else: + print(f"[RegionLayer] ⚠️ Unsupported geom type {gtype} for {region.get('name')}") + return + + if len(outer_ring) < 3: + print(f"[RegionLayer] ⚠️ Region {region.get('name')} has too few points") + return + + # ── Project region to scene coordinates + scene_points: list[QPointF] = [] + print(f"[RegionLayer] ▶ Projecting region '{region.get('name')}'...") + for lon, lat in outer_ring: + proj = self._project_lon_lat(lon, lat) + print(f" - vertex lon={lon:.6f}, lat={lat:.6f} -> {proj}") + if not proj: + continue + sx, sy = proj + scene_points.append(QPointF(sx, sy)) + + if len(scene_points) < 3: + print(f"[RegionLayer] ⚠️ Region {region.get('name')} has too few valid projected points") + return + + polygon = QPolygonF(scene_points) + + # ── Create graphics item + item = QGraphicsPolygonItem(polygon) + item.region_id = region["id"] + item.region_name = region["name"] + item.selected = False + item.setZValue(900) + + pen = QPen(QColor("#111827")) + pen.setWidthF(8) + item.setPen(pen) + + 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) + + 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}' " + f"(ID {item.region_id}) with {len(scene_points)} vertices" + ) + + # ───────────────────────────────────────────── + def _toggle_selection(self, item: QGraphicsPolygonItem): + """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) + + # ───────────────────────────────────────────── + 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/GUI/src/vast/views/security/analytics/popup_panel.py b/GUI/src/vast/views/security/analytics/popup_panel.py new file mode 100644 index 000000000..f2586e39a --- /dev/null +++ b/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/GUI/src/vast/views/security/analytics/sql_generator.py b/GUI/src/vast/views/security/analytics/sql_generator.py new file mode 100644 index 000000000..99580f666 --- /dev/null +++ b/GUI/src/vast/views/security/analytics/sql_generator.py @@ -0,0 +1,393 @@ + + +# #!/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 +} + + + +SYSTEM_PROMPT = """ +You are an expert DSL generator for the AgGuard Analytics Dashboard. +Your task: convert a natural-language request into a strict JSON object +compatible with the AgGuard DSL (no SQL, JSON only). + +──────────────────────── +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() +); + +Use only these columns. Do not invent new columns or tables. + +──────────────────────── +DSL STRUCTURE +──────────────────────── +The output MUST follow this shape: + +{ + "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}, + {"op": "offset", "offset": 0} + ] +} + +Rules for fields: +- "source" must always be "alerts". +- "_ops" is an ordered array of operations. +- "op" is one of: "select", "where", "group_by", "having", "order_by", "limit", "offset". +- "columns" is ALWAYS an array of strings (1–2 items, no duplicates). + - For functions/expressions in SELECT/GROUP_BY/ORDER_BY, write SQL text strings, e.g.: + "COUNT(alert_id)", "COUNT(*)", "DATE_TRUNC('month', started_at)". + - Never put {"func": ...} objects inside "columns". +- "directions" aligns with "columns" and contains only "ASC" or "DESC". +- "limit" is 1–500; "offset" is >= 0. + +SELECT rules (very important): +- When selecting entities, SELECT must contain only: + - ["device_id"], or + - ["area"]. +- Never put aggregates or functions into SELECT (no COUNT(*), SUM, etc. in SELECT). + +──────────────────────── +CONDITION TREE FORMAT +──────────────────────── +Conditions ("cond") are boolean trees: + +1. Logical AND: + { "all": [ , ... ] } + +2. Logical OR: + { "any": [ , ... ] } + +3. Predicate: + { "op": "", "left": , "right": } + +Allowed operators: "=", "!=", "<", "<=", ">", ">=". + +In conditions, an may be: +- {"col": ""} +- {"literal": } +- {"func": "", "args": [ , ... ]} + +Notes: +- Use {"col": "..."} only with real columns from the alerts table. +- Use {"literal": ...} for numbers, strings, booleans, and time expressions like: + "now() - interval '1 month'". +- Use {"func": ...} only in WHERE/HAVING, not in SELECT/GROUP_BY/ORDER_BY. +- Do NOT use window functions or OVER() (no row_number, no "max(avg) over ()"). + +──────────────────────── +EXAMPLES +──────────────────────── + +Example 1 +User: "show devices with fence_hole alerts from the last month" +→ +{ + "source": "alerts", + "_ops": [ + {"op": "select", "columns": ["device_id"]}, + { + "op": "where", + "cond": { + "all": [ + { + "op": "=", + "left": {"col": "alert_type"}, + "right": {"literal": "fence_hole"} + }, + { + "op": ">", + "left": {"col": "started_at"}, + "right": {"literal": "now() - interval '1 month'"} + } + ] + } + }, + {"op": "group_by", "columns": ["device_id"]} + ] +} + +──────────────────────── + +Example 2 +User: "show areas with more than 3 severe alerts (severity > 3) in the last month" +→ +{ + "source": "alerts", + "_ops": [ + {"op": "select", "columns": ["area"]}, + { + "op": "where", + "cond": { + "all": [ + { + "op": ">", + "left": {"col": "severity"}, + "right": {"literal": 3} + }, + { + "op": ">", + "left": {"col": "started_at"}, + "right": {"literal": "now() - interval '1 month'"} + } + ] + } + }, + {"op": "group_by", "columns": ["area"]}, + { + "op": "having", + "cond": { + "op": ">", + "left": {"func": "count", "args": [ {"literal": "*"} ]}, + "right": {"literal": 3} + } + } + ] +} + + +──────────────────────── + +Example 4 +User: "top 5 devices with highest number of climbing_fence alerts" +→ +{ + "source": "alerts", + "_ops": [ + {"op": "select", "columns": ["device_id"]}, + { + "op": "where", + "cond": { + "op": "=", + "left": {"col": "alert_type"}, + "right": {"literal": "climbing_fence"} + } + }, + {"op": "group_by", "columns": ["device_id"]}, + { + "op": "order_by", + "columns": ["COUNT(alert_id)"], + "directions": ["DESC"] + }, + {"op": "limit", "limit": 5} + ] +} + + +──────────────────────── + +Example 5 +User: "list devices with unacknowledged masked_person alerts in the last week, newest first, limit 20" +→ +{ + "source": "alerts", + "_ops": [ + {"op": "select", "columns": ["device_id"]}, + { + "op": "where", + "cond": { + "all": [ + { + "op": "=", + "left": {"col": "alert_type"}, + "right": {"literal": "masked_person"} + }, + { + "op": "=", + "left": {"col": "ack"}, + "right": {"literal": false} + }, + { + "op": ">", + "left": {"col": "started_at"}, + "right": {"literal": "now() - interval '1 week'"} + } + ] + } + }, + { + "op": "order_by", + "columns": ["started_at"], + "directions": ["DESC"] + }, + {"op": "limit", "limit": 20} + ] +} + +──────────────────────── +GLOBAL RULES +──────────────────────── +1. Always use only the columns of the alerts table. +2. Never reference table aliases like "a." or "r." and never reference other tables. +3. Output only VALID JSON, with "source" and "_ops" at the top level. +4. Use WHERE for row filters, GROUP_BY for aggregations, HAVING for aggregate conditions. +5. For queries that rank entities by number of alerts (e.g. "top", "most", "highest", "least", "fewest", "lowest"): + - Use GROUP_BY on the entity ("device_id" or "area"). + - Use ORDER_BY with "COUNT(alert_id)" or "COUNT(*)". + • For "top / most / highest": use direction "DESC". + • For "least / fewest / lowest": use direction "ASC". + - Use LIMIT N (or LIMIT 1 if the user asks for a single best/worst entity). +6. alert_type is one of: "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) + print(obj) + 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/GUI/src/vast/views/security/events_history_page.py b/GUI/src/vast/views/security/events_history_page.py new file mode 100644 index 000000000..889191de4 --- /dev/null +++ b/GUI/src/vast/views/security/events_history_page.py @@ -0,0 +1,857 @@ +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) + + # self.table.setCellWidget(r, 7, feedback_widget) + + + + + + + + + + + + + + print("[TABLE] Done populating alerts table.") + + + + + + + + + + + # def _open_video_player(self, info): + # print(f"[VIDEO] Opening video player for alert={info.get('alert_id')}") + # url = info.get("vod") + # if not url: + # QtWidgets.QMessageBox.warning(self, "No Video", "This alert has no VOD URL.") + # return + + + 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(1280, 720) + 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 _show_vlc_popup(self, url): + # print(f"[VIDEO] Playing URL: {url}") + + # popup = QtWidgets.QDialog(self) + # popup.setWindowTitle("Incident Video Playback") + # popup.setMinimumSize(1280, 720) + # vbox = QtWidgets.QVBoxLayout(popup) + + # player = QtWidgets.QFrame() + # player.setStyleSheet("background:black;border-radius:8px;") + # vbox.addWidget(player, 1) + + # # --- VLC instance with MP4-friendly options --- + # inst = vlc.Instance([ + # "--quiet", + # "--no-video-title-show", + # "--demux=avformat", # important for MP4 + # "--network-caching=800", + # "--file-caching=800", + # "--avcodec-hw=none", # safer decoding + # ]) + + # mp = inst.media_player_new() + + # # --- Media object with same options --- + # media = inst.media_new(url) + # media.add_option(":no-audio") + # media.add_option(":network-caching=800") + # media.add_option(":file-caching=800") + # media.add_option(":demux=avformat") + + # mp.set_media(media) + + # 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}") + + diff --git a/GUI/src/vast/views/security/incident_player_vlc.py b/GUI/src/vast/views/security/incident_player_vlc.py new file mode 100644 index 000000000..7c9efe9b9 --- /dev/null +++ b/GUI/src/vast/views/security/incident_player_vlc.py @@ -0,0 +1,2103 @@ +#!/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). +- DVR seek/scrub still works *after* incident is marked resolved, using the + segments already buffered in memory. +- No-cache headers on HLS endpoints. +- Smoother stream: + * DVR poll interval tightened (REFRESH_MS default 300 ms). + * VLC network caching default increased (800 ms). +""" + +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 aiohttp.client_exceptions import ClientConnectionError, ClientPayloadError + +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) + # ↓ Previously 20000 ms, which caused "segment → pause → segment" behaviour in DVR. + REFRESH_MS = int(os.getenv("REFRESH_MS", "300")) + + # 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) — slightly higher default for smoother playback + NETWORK_CACHING = int(os.getenv("NETWORK_CACHING", "800")) + + 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 VlcWidget(QtWidgets.QFrame): + positionChanged = QtCore.pyqtSignal(float) + timeChanged = QtCore.pyqtSignal(int) + ended = QtCore.pyqtSignal() # <-- final, real signal + + 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) + + # Timer that emits position/time + self._timer = QtCore.QTimer(self) + self._timer.setInterval(200) + self._timer.timeout.connect(self._on_tick) + self._timer.start() + + # Attach VLC "end reached" event + em = self.mediaplayer.event_manager() + em.event_attach(vlc.EventType.MediaPlayerEndReached, self._on_media_end) + + def _on_media_end(self, _event): + print("[VLC] Media end reached") + # Emit Qt signal so IncidentPlayerVLC can react + self.ended.emit() + + 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 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, + headers=self._nocache_headers(), + ) + + # 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) + 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: + try: + await proxy_resp.write_eof() + except Exception: + pass + + 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 Exception: + pass + elif l.startswith("#EXT-X-MEDIA-SEQUENCE:"): + try: + media_seq = int(l.split(":", 1)[1]) + except Exception: + 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: + # 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): + """Mark incident as resolved: stop polling upstream, + keep buffered segments for DVR scrubbing.""" + if self.resolved: + return + self.resolved = True + if self.dvr: + try: + self.dvr.stop() # stop adding more segments, keep existing + except Exception: + pass + # Do NOT clear upstream_index or dvr here; DVR is still usable. + print("[SRC] incident resolved; upstream disabled; DVR segments kept for scrubbing") + + # 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 global DVR playlist anymore + 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): + # 1. After resolve: serve a static DVR playlist from buffered segments + if self.resolved: + if not self.dvr or not self.dvr.segments: + # No DVR buffer -> nothing to play + return web.Response( + text="#EXTM3U\n#EXT-X-ENDLIST\n", + content_type="application/vnd.apple.mpegurl", + status=410, + headers=self._nocache_headers(), + ) + + m3u8_body, total = self.dvr.render_dvr_vod_playlist(endlist=True) + print( + f"[HTTP] live.m3u8 (resolved) serving DVR snapshot: " + f"{len(self.dvr.segments)} segs, total={total:.3f}s" + ) + return web.Response( + text=m3u8_body, + content_type="application/vnd.apple.mpegurl", + status=200, + headers=self._nocache_headers(), + ) + + # 2. No source yet -> nothing to serve + if 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(), + ) + + # 3. Normal live mode (your existing logic...) + 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): + # IMPORTANT: allow DVR seek even when resolved, as long as we still have a DVR buffer. + if 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) + 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} " + f"intra={int(intra_ms)}ms segs={len(trimmed)} resolved={self.resolved}" + ) + 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() + + # Use the configured port, not a hardcoded one + if is_port_in_use(self.port, self.bind): + print(f"[INFO] DVR proxy already running on port {self.port}, 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: DVR seek allowed after 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 # 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() + + # 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 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) + + # Live page only; history/analytics in external navigation + livePage = QtWidgets.QWidget() + liveLayout = QtWidgets.QVBoxLayout(livePage) + liveLayout.setContentsMargins(0, 0, 0, 0) + liveLayout.setSpacing(0) + liveLayout.addWidget(splitter) + + outer.addWidget(livePage) + + # 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.current_alert: Optional[dict] = None + + + 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) + + # grows DVR slider while paused/seeked + 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.vlcw.ended.connect(self._on_vlc_end) + self.alertList.clicked.connect(self._on_pick_alert_from_list) + + self._show_player(False) + self._set_idle() + def _on_vlc_end(self): + print("[VLC] end reached (proxy.resolved=%s, alerts_empty=%s)" + % (self.proxy.resolved, self.alertModel.is_empty())) + + # If incident is resolved and there are no *firing* alerts left, + # we can fully tear down the player and hide the panels. + if self.proxy.resolved: + have_firing = any( + (a.get("status") or "firing").lower() == "firing" + for a in self.alertModel._items + ) + + if not have_firing: + # Stop VLC + try: + self.vlcw.mediaplayer.stop() + except Exception: + pass + + # Optionally drop the current resolved incident from the list, + # so the left list is also "clean". + if self.current_camera and self.current_incident: + self.alertModel.remove_by_key(self.current_camera, + self.current_incident) + + # Clear current state + self.current_camera = None + self.current_incident = None + self.current_alert = None + self.dvr_duration_ms = 0 + + # Reset UI + hide player + self._set_idle() + self._show_player(False) # switch to NO-ALERTS pane + print("[MODE] DVR clip finished; no firing alerts → hiding player") + return + + # Case 2: still have firing alerts or proxy not resolved — normal DVR mode + self.mode_live = False + self._set_live_badge(False) + print("[MODE] clip finished; staying in DVR mode") + + + + def showEvent(self, event): + super().showEvent(event) + + 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) + + 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 Exception: + 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") + 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): + print(f"[AlertService] Alert removed: {alert_id}") + + is_current = (self.current_incident == alert_id) + + new_items: list[dict] = [] + for a in self.alertModel._items: + if a.get("incident_id") != alert_id: + new_items.append(a) + else: + # This is the removed alert + if is_current and self.proxy.dvr and len(self.proxy.dvr.segments) > 0: + updated = dict(a) + updated["status"] = "resolved" + new_items.append(updated) + # else: drop it completely + + self.alertModel.set_alerts(new_items) + + if is_current: + self.current_status = "resolved" + # Update cached alert metadata if we still have it + self.current_alert = next( + (a for a in new_items if a.get("incident_id") == alert_id), + self.current_alert, + ) + try: + self.proxy.mark_resolved() + except Exception as e: + print(f"[IncidentPlayer] mark_resolved failed: {e!r}") + + # UI: DVR-only mode + self.mode_live = False + self._set_live_badge(False) + + 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_list = not self.alertModel.is_empty() + + # We also keep the player visible if we have a "current" incident + # (even if it is resolved) and we still have a DVR buffer to scrub. + have_current_dvr = ( + self.current_camera is not None + and self.current_incident is not None + and self.proxy.dvr is not None + and len(self.proxy.dvr.segments) > 0 + ) + + print( + "_update_right_pane_visibility called: " + f"have_any_list={have_any_list}, have_current_dvr={have_current_dvr}" + ) + + if not have_any_list and not have_current_dvr: + # Nothing in the list and nothing to scrub → fully idle. + try: + self.vlcw.mediaplayer.stop() + except Exception: + pass + self._set_idle() + self._show_player(False) + else: + # Either we have firing alerts or a resolved incident with DVR buffer. + self._show_player(True) + + + # ───── 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): + try: + meta = json.loads(meta) + except Exception: + meta = {} + ended = ( + it.get("ended_at") or + it.get("endsAt") or + it.get("endedAt") + ) + 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"), + "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": ended, + "status": "resolved" if ended else "firing", + } + + # Only expand DVR while paused/seeked (DVR mode). Never move the thumb. + def _maybe_grow_dvr_range_only(self): + 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] + + # only keep desired alert types that are still firing + 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 + ] + + # Track currently selected item (optional; same as before) + 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 + + cur_cam = self.current_camera + cur_inc = self.current_incident or self.cfg.INCIDENT + has_current = bool(cur_cam and cur_inc) + + still_there = any( + it.get('camera') == cur_cam and + (it.get('incident_id') or it.get('alert_id')) == cur_inc + for it in firing + ) if has_current else False + + effective_list = list(firing) + + # Case: the current incident is no longer "firing" in the upstream list → resolved + if has_current and not still_there: + self.current_status = "resolved" + try: + self.proxy.mark_resolved() + except Exception as e: + print(f"[IncidentPlayer] mark_resolved failed in _apply_firing_list: {e!r}") + + # Keep it in the left list while we still have DVR segments + if self.proxy.dvr is not None and len(self.proxy.dvr.segments) > 0: + base = dict(self.current_alert or {}) + base.setdefault("camera", cur_cam) + base.setdefault("incident_id", cur_inc) + base["status"] = "resolved" + if (base.get("anomaly") or "").lower() in self.cfg.ALLOWED_TYPES: + key = (base.get("camera"), base.get("incident_id")) + if not any( + it.get("camera") == key[0] and it.get("incident_id") == key[1] + for it in effective_list + ): + effective_list.append(base) + + self.alertModel.set_alerts(effective_list) + self._update_right_pane_visibility() + return + + # Normal case: just "firing" alerts + (possibly) already kept resolved ones + self.alertModel.set_alerts(effective_list) + self._update_right_pane_visibility() + + # Autoplay only when there was NO current incident at all (e.g., first load) + if firing and not has_current: + 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) + status = (it.get("status") or "firing").lower() + + if status == 'firing': + if current.get(k) != it: + current[k] = it + changed = True + else: + # resolved / non-firing + keep_on_left = ( + (self.current_camera, self.current_incident) == k + and self.proxy.dvr is not None + and len(self.proxy.dvr.segments) > 0 + ) + if keep_on_left: + stored = dict(current.get(k, it)) + stored.update(it) + stored["status"] = "resolved" + current[k] = stored + changed = True + else: + if k in current: + current.pop(k, None) + changed = True + + if (self.current_camera, self.current_incident) == k and status != 'firing': + # Current incident resolved: stop polling upstream, + # but keep DVR and keep it in the list so UI doesn't "blink out". + self.current_status = "resolved" + self.current_alert = it + self.proxy.mark_resolved() + print("[IncidentPlayer] Current incident resolved via delta; DVR-only mode") + # Do NOT remove from model and do NOT stop the mediaplayer here. + + if not changed: + return + + # Same selection-restore logic as before + 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: + selected_key = None + + self.alertModel.replace_with(current) + + 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) + + # Restore selection for any remaining alert (same as before) + if self.alertList.selectionModel() and self.alertList.selectionModel().currentIndex().isValid(): + selected_key = None + try: + cur = self.alertModel.get(self.alertList.selectionModel().currentIndex().row()) + selected_key = self._key(cur) + except Exception: + selected_key = None + + 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 + + # Now decide visibility using the helper (which also checks DVR) + self._update_right_pane_visibility() + + 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) + + + + # ───── 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): + # Remember the previous "current" before we switch + old_cam = self.current_camera + old_inc = self.current_incident + old_status = getattr(self, "current_status", "firing") + + # 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 + + # If we are leaving a resolved incident that we were keeping only + # for DVR scrubbing, remove it from the list now (no video anymore). + if old_cam and old_inc and old_status == "resolved": + self.alertModel.remove_by_key(old_cam, old_inc) + + self.current_camera = cam + self.current_incident = inc + self.current_status = (it.get('status') or 'firing').lower() + self.current_alert = it # keep the latest metadata for this incident + + 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): + # If the proxy is already resolved, there is no live stream any more. + # We keep DVR scrubbing only – do not try to attach /live.m3u8. + if self.proxy.resolved: + print("[MODE] proxy resolved; live disabled, staying in DVR-only mode") + self.mode_live = False + self._set_live_badge(False) + return + + # resume live sync; stop DVR growth + if not self._live_sync.isActive(): + self._live_sync.start() + self._dvr_growth.stop() + + # ❌ OLD: blocked when current_status == "resolved" + # 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() + + # IMPORTANT: allow seeking even when current_status == "resolved". + # Only block if there is no DVR. + if not self.proxy.dvr: + print("[SEEK] ignored (no DVR)") + 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/GUI/src/vast/views/sensorDetailsTab.py b/GUI/src/vast/views/sensorDetailsTab.py new file mode 100644 index 000000000..9fb041576 --- /dev/null +++ b/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/GUI/src/vast/views/sensorsMainView.py b/GUI/src/vast/views/sensorsMainView.py new file mode 100644 index 000000000..1c8176d7e --- /dev/null +++ b/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/GUI/src/vast/views/sensorsMapView.py b/GUI/src/vast/views/sensorsMapView.py new file mode 100644 index 000000000..78a61c790 --- /dev/null +++ b/GUI/src/vast/views/sensorsMapView.py @@ -0,0 +1,259 @@ + +import os +os.environ["QTWEBENGINE_DISABLE_GPU"] = "1" +os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu --disable-software-rasterizer --disable-webgl" +import 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 +from dashboard_api import DashboardApi +from pathlib import Path + + +class SensorsMapView(QWidget): + """Stable, auto-refreshing sensors dashboard.""" + def __init__(self, api: DashboardApi, parent=None): + super().__init__(parent) + self.api = api + self._last_sensor_data = [] + self._map_ready = False + self._closing = False + self._visible = False + self._initialized = False + + layout = QVBoxLayout(self) + layout.setContentsMargins(12, 12, 12, 12) + layout.setSpacing(10) + + title = QLabel("🌾 Sensor Dashboard") + title.setStyleSheet(""" + font-size:22px; + font-weight:800; + color:#0f172a; + margin-bottom:4px; + """) + layout.addWidget(title, alignment=Qt.AlignmentFlag.AlignLeft) + + map_frame = QFrame() + map_layout = QVBoxLayout(map_frame) + map_layout.setContentsMargins(0, 0, 0, 0) + map_layout.setSpacing(0) + map_frame.setStyleSheet("background:#f1f5f9;border-radius:12px;border:1px solid #cbd5e1;") + + self.web = QWebEngineView() + self.web.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + html_path = Path(__file__).resolve().parent / "assets" / "sensors_map.html" + self.web.setUrl(QUrl.fromLocalFile(str(html_path))) + map_layout.addWidget(self.web) + layout.addWidget(map_frame, stretch=2) + + self.table = QTableWidget() + self.table.setAlternatingRowColors(True) + self.table.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self.table.setStyleSheet(""" + QHeaderView::section { + background:#e2e8f0; + color:#0f172a; + font-weight:700; + border:none; + padding:6px 8px; + } + QTableWidget { + background:#ffffff; + border:1px solid #cbd5e1; + border-radius:10px; + gridline-color:#cbd5e1; + font-size:13px; + } + QTableWidget::item { padding:4px 6px; } + """) + layout.addWidget(self.table, stretch=1) + + btn = QPushButton("⟳ Load Zone Stats") + btn.setStyleSheet(""" + QPushButton { + background:#2563eb; + color:white; + font-weight:700; + padding:8px 14px; + border-radius:8px; + } + QPushButton:hover { background:#1d4ed8; } + QPushButton:pressed { background:#1e40af; } + """) + btn.clicked.connect(self.load_zone_stats) + layout.addWidget(btn, alignment=Qt.AlignmentFlag.AlignRight) + + self.web.loadFinished.connect(self._on_map_ready) + + self.timer = QTimer(self) + self.timer.timeout.connect(self.refresh_all) + + self._initialized = True + + # --------------------------- Events --------------------------- # + def resizeEvent(self, event): + super().resizeEvent(event) + if self._map_ready: + self.web.page().runJavaScript("if (window.map) map.invalidateSize();") + QTimer.singleShot(800, lambda: self.web.page().runJavaScript(""" + if (window.map) { + map.invalidateSize(); + if (window.image) image.setBounds(map.getBounds()); + } + """)) + + def showEvent(self, event): + super().showEvent(event) + self._visible = True + if not self._closing and not self.timer.isActive(): + self.timer.start(15_000) + QTimer.singleShot(800, self.refresh_all) + + def hideEvent(self, event): + self._visible = False + if self.timer.isActive(): + self.timer.stop() + super().hideEvent(event) + + def closeEvent(self, event): + self._closing = True + self._visible = False + if self.timer.isActive(): + self.timer.stop() + super().closeEvent(event) + + # --------------------------- Map --------------------------- # + def _on_map_ready(self): + self._map_ready = True + QTimer.singleShot(800, self._inject_data) + + def _inject_data(self): + if not self._map_ready or self._closing: + return + try: + token = getattr(self.api, "token", None) + if not token: + return + + # --- Fetch zones from DB and render on map --- + rz = self.api.http.get(f"{self.api.base}/api/tables/zones") + rz.raise_for_status() + zones = rz.json().get("rows", []) + print(zones) + self.web.page().runJavaScript(f"if (typeof renderZones==='function') renderZones({json.dumps(zones)});") + self._update_zones(zones) + # --- Fetch zones from DB and render on map --- + rz = self.api.http.get(f"{self.api.base}/api/tables/zones") + rz.raise_for_status() + zones = rz.json().get("rows", []) + print(zones) + self.web.page().runJavaScript( + f"if (typeof renderZones==='function') renderZones({json.dumps(zones)});" + ) + self._update_zones(zones) + + r = self.api.http.get(f"{self.api.base}/api/tables/sensor_anomalies") + r.raise_for_status() + data = r.json() + self._update_map(data) + except Exception as e: + print("[SensorsView] Failed to inject data:", e) + + def _update_zones(self, zones_data): + if not self._map_ready or self._closing or not zones_data: + return + try: + js_data = json.dumps(zones_data) + js = f""" + if (typeof renderZones === 'function') {{ + renderZones({js_data}); + }} + """ + self.web.page().runJavaScript(js) + except Exception as e: + print("[SensorsView] Failed to update zones:", e) + + def _update_map(self, sensor_data): + if not self._map_ready or self._closing or not self._visible: + return + rows = sensor_data.get("rows", sensor_data) + js_data = json.dumps(rows) + js = f""" + window.SENSOR_DATA = {js_data}; + if (typeof renderSensors === 'function') {{ + renderSensors(window.SENSOR_DATA); + }} + if (window.map) map.invalidateSize(); + """ + self.web.page().runJavaScript(js) + + # --------------------------- Table --------------------------- # + def load_zone_stats(self): + if self._closing or not self._initialized or not self._visible: + return + try: + r = self.api.http.get(f"{self.api.base}/api/tables/sensor_zone_stats?limit=10&order_by=inserted_at&order_dir=desc") + r.raise_for_status() + data = r.json() + rows = data.get("rows", data if isinstance(data, list) else []) + except Exception as e: + print("[SensorsView] API error:", e) + self._show_no_data("API Error") + return + + if not rows: + self._show_no_data("No data") + return + + self.table.clear() + keys = list(rows[0].keys()) + self.table.setColumnCount(len(keys)) + self.table.setHorizontalHeaderLabels(keys) + self.table.setRowCount(len(rows)) + + for r_idx, row in enumerate(rows): + for c_idx, key in enumerate(keys): + val = row.get(key, "") + item = QTableWidgetItem(str(val)) + item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + if not self._closing: + self.table.setItem(r_idx, c_idx, item) + + self.table.resizeColumnsToContents() + self.table.horizontalHeader().setStretchLastSection(True) + + def _show_no_data(self, msg): + if self._closing: + return + self.table.clear() + self.table.setRowCount(0) + self.table.setColumnCount(1) + self.table.setHorizontalHeaderLabels([msg]) + + # --------------------------- Refresh --------------------------- # + def refresh_all(self): + if self._closing or not self._visible: + return + print("[SensorsView] Refreshing data…") + try: + self.load_zone_stats() + except Exception as e: + print("[SensorsView] refresh error:", e) + return + try: + r = self.api.http.get(f"{self.api.base}/api/tables/sensor_anomalies?limit=10&order_by=inserted_at&order_dir=desc") + r.raise_for_status() + data = r.json() + new_rows = data.get("rows", data) + if json.dumps(new_rows, sort_keys=True) != json.dumps(self._last_sensor_data, sort_keys=True): + self._last_sensor_data = new_rows + self._update_map(data) + else: + print("[SensorsView] No sensor changes detected — skipping update.") + except Exception as e: + print("[SensorsView] Failed to refresh sensors:", e) + diff --git a/GUI/src/vast/views/sensors_status_summary.py b/GUI/src/vast/views/sensors_status_summary.py new file mode 100644 index 000000000..4d3728c41 --- /dev/null +++ b/GUI/src/vast/views/sensors_status_summary.py @@ -0,0 +1,489 @@ +from math import ceil +from datetime import datetime, timezone +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QFrame, QTableWidget, + QTableWidgetItem, QHeaderView, QScrollArea +) +from PyQt6.QtCore import Qt, QTimer +from PyQt6.QtGui import QFont, QColor, QPainter, QPen, QPainterPath + +# ============================================================ +# 🎨 CIRCLE ICON (Stroke icons drawn manually) +# ============================================================ +class CircleIcon(QWidget): + def __init__(self, icon_type: str, color: str, size: int = 58): + super().__init__() + self.icon_type = icon_type + self.color = color + self.size = size + self.setFixedSize(size, size) + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + # Background circle + bg = QColor(self._lighten(self.color, 0.70)) + painter.setBrush(bg) + painter.setPen(Qt.PenStyle.NoPen) + painter.drawEllipse(0, 0, self.size, self.size) + + # Icon stroke + painter.setPen(QPen(QColor(self._dark(self.color)), 3, + Qt.PenStyle.SolidLine, + Qt.PenCapStyle.RoundCap, + Qt.PenJoinStyle.RoundJoin)) + + # Build path + path = self._icon_path() + scale = self.size / 58 + painter.scale(scale, scale) + painter.drawPath(path) + + # ---------------- Icon shapes ---------------- + def _icon_path(self): + p = QPainterPath() + + if self.icon_type == "people": + p.addEllipse(14, 14, 12, 12) + p.addEllipse(32, 14, 12, 12) + p.moveTo(10, 40) + p.cubicTo(18, 30, 28, 30, 36, 40) + p.moveTo(22, 40) + p.cubicTo(30, 30, 40, 30, 48, 40) + + elif self.icon_type == "bell": + p.moveTo(29, 12) + p.arcTo(19, 12, 20, 20, 0, 180) + p.lineTo(19, 38) + p.lineTo(39, 38) + p.lineTo(39, 32) + p.addEllipse(26, 44, 6, 6) + + elif self.icon_type == "graph": + p.moveTo(12, 40) + p.lineTo(24, 28) + p.lineTo(32, 34) + p.lineTo(46, 20) + + elif self.icon_type == "bolt": + p.moveTo(28, 12) + p.lineTo(20, 30) + p.lineTo(30, 30) + p.lineTo(22, 48) + p.lineTo(40, 26) + p.lineTo(30, 26) + p.closeSubpath() + + return p + + # ---------------- Color helpers ---------------- + def _lighten(self, hex_color, factor): + hex_color = hex_color.lstrip('#') + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + r = int(r + (255 - r) * factor) + g = int(g + (255 - g) * factor) + b = int(b + (255 - b) * factor) + return f"#{r:02X}{g:02X}{b:02X}" + + def _dark(self, hex_color): + hex_color = hex_color.lstrip('#') + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + r = int(r * 0.35) + g = int(g * 0.35) + b = int(b * 0.35) + return f"#{r:02X}{g:02X}{b:02X}" + + +# ============================================================ +# 🔵 RING INDICATOR +# ============================================================ +class RingIndicator(QWidget): + def __init__(self, label, value, color="#10B981", max_value=100, size=200): + super().__init__() + self.label = label + self.value = value + self.color = color + self.max_value = max_value + self.size = size + self.setFixedSize(size, size) + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + rect = self.rect() + + radius = int(min(rect.width(), rect.height()) / 2 - 15) + center = rect.center() + + # Background circle + painter.setPen(QPen(QColor("#E5E7EB"), 16)) + painter.drawEllipse(center, radius, radius) + + # Colored ring + pen = QPen(QColor(self.color), 16) + pen.setCapStyle(Qt.PenCapStyle.RoundCap) + painter.setPen(pen) + angle_span = int(360 * (self.value / self.max_value)) + painter.drawArc(rect.adjusted(15, 15, -15, -15), 90 * 16, -angle_span * 16) + + # Text + painter.setPen(QColor("#1E293B")) + painter.setFont(QFont("Segoe UI", 30, QFont.Weight.Bold)) + painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, f"{ceil(self.value)}%") +# ============================================================ +# 🌿 MAIN DASHBOARD – BUILD UI +# ============================================================ +class SensorsStatusSummary(QWidget): + def __init__(self, api, parent=None): + super().__init__(parent) + self.api = api + self._events_cache = [] + self._last_event_id = 0 + self._first_load = True + + self._build_ui() + self.refresh_data() + + # Auto refresh every 30 sec + self.timer = QTimer(self) + self.timer.timeout.connect(self.refresh_data) + self.timer.start(30000) + + # ============================================================ + # 🧱 BUILD UI + # ============================================================ + def _build_ui(self): + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(40, 40, 40, 40) + main_layout.setSpacing(30) + + # ------------------------------------------------------------ + # Header + # ------------------------------------------------------------ + header_row = QHBoxLayout() + header_row.setSpacing(16) + + icon_lbl = QLabel("🩺") + icon_lbl.setFont(QFont("Segoe UI Emoji", 28)) + header_row.addWidget(icon_lbl) + + title = QLabel("Sensors Health Overview") + title.setFont(QFont("Segoe UI", 24, QFont.Weight.Bold)) + title.setStyleSheet("color:#0F172A; letter-spacing: -0.5px;") + header_row.addWidget(title) + + header_row.addStretch() + + self.status_badge = QLabel("Live • Updated just now") + self.status_badge.setFont(QFont("Segoe UI", 11, QFont.Weight.Medium)) + self.status_badge.setStyleSheet(""" + QLabel { + color: #047857; + background-color: #D1FAE5; + border: 1px solid #A7F3D0; + border-radius: 12px; + padding: 8px 16px; + } + """) + header_row.addWidget(self.status_badge) + main_layout.addLayout(header_row) + + # ------------------------------------------------------------ + # Status cards row + # ------------------------------------------------------------ + cards_row = QHBoxLayout() + cards_row.setSpacing(20) + + self.active_card = self._create_status_card("Active Sensors", "0", "#22C55E", "people") + self.inactive_card = self._create_status_card("Inactive (No KeepAlive)", "0", "#F97316", "bell") + self.outofrange_card = self._create_status_card("Out of Range", "0", "#EF4444", "graph") + self.corrupted_card = self._create_status_card("Corrupted", "0", "#8B5CF6", "bolt") + + cards_row.addWidget(self.active_card) + cards_row.addWidget(self.inactive_card) + cards_row.addWidget(self.outofrange_card) + cards_row.addWidget(self.corrupted_card) + + main_layout.addLayout(cards_row) + + # ------------------------------------------------------------ + # LOWER SECTION + # ------------------------------------------------------------ + lower_row = QHBoxLayout() + lower_row.setSpacing(24) + + # === LEFT: ring === + left_frame = QFrame() + left_frame.setStyleSheet("background:white; border-radius:16px;") + left_l = QVBoxLayout(left_frame) + left_l.setContentsMargins(30, 30, 30, 30) + left_l.setSpacing(10) + + ring_title = QLabel("System Health") + ring_title.setFont(QFont("Segoe UI", 16)) + ring_title.setStyleSheet("color:#1E293B;") + left_l.addWidget(ring_title) + + self.health_ring = RingIndicator("Health", 0, "#10B981", 100, 220) + left_l.addWidget(self.health_ring, alignment=Qt.AlignmentFlag.AlignCenter) + + self.ring_bottom = QLabel("Loading...") + self.ring_bottom.setFont(QFont("Segoe UI", 11)) + self.ring_bottom.setStyleSheet("color: #94A3B8; margin-top: 6px;") + self.ring_bottom.setAlignment(Qt.AlignmentFlag.AlignCenter) + left_l.addWidget(self.ring_bottom) + + lower_row.addWidget(left_frame, 1) + + # === RIGHT: Inactive table === + right_frame = QFrame() + right_frame.setStyleSheet("background:white; border-radius:16px;") + right_l = QVBoxLayout(right_frame) + right_l.setContentsMargins(30, 30, 30, 30) + right_l.setSpacing(10) + + table_title = QLabel("Inactive Sensors (Missing KeepAlive)") + table_title.setFont(QFont("Segoe UI", 16)) + table_title.setStyleSheet("color:#1E293B; margin-bottom:10px;") + right_l.addWidget(table_title) + + self.table = QTableWidget(0, 3) + self.table.setHorizontalHeaderLabels(["ID", "Sensor Type", "Last Seen"]) + + header = self.table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeMode.Fixed) + header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) + header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) + self.table.setColumnWidth(0, 60) + + self.table.verticalHeader().setVisible(False) + self.table.setStyleSheet(""" + QTableWidget { + background-color: white; + font-family: 'Segoe UI'; + font-size: 13px; + border: none; + border-radius: 12px; + gridline-color: #E5E7EB; + } + QHeaderView::section { + background-color: #F8FAFC; + color: #64748B; + font-weight: 600; + padding: 10px; + border: none; + } + QTableWidget::item { + padding: 10px; + } + """) + + right_l.addWidget(self.table) + lower_row.addWidget(right_frame, 2) + + main_layout.addLayout(lower_row) + + self.setStyleSheet("background-color:#F8FAFC;") + + self.setLayout(main_layout) + + # ============================================================ + # CARD WIDGET + # ============================================================ + def _create_status_card(self, title, value, color, icon_type): + frame = QFrame() + frame.setStyleSheet("background:white; border-radius:16px; padding:4px;") + layout = QVBoxLayout(frame) + layout.setContentsMargins(20, 20, 20, 16) + layout.setSpacing(8) + layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + # Icon drawn via QPainterPath + icon = CircleIcon(icon_type, color) + layout.addWidget(icon, alignment=Qt.AlignmentFlag.AlignCenter) + + # Number + val_label = QLabel(value) + val_label.setFont(QFont("Segoe UI", 28, QFont.Weight.Bold)) + val_label.setStyleSheet("color:#1E293B;") + val_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + frame.value_label = val_label + layout.addWidget(val_label) + + # Title + title_lbl = QLabel(title) + title_lbl.setFont(QFont("Segoe UI", 11)) + title_lbl.setStyleSheet("color:#64748B;") + title_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter) + title_lbl.setWordWrap(True) + layout.addWidget(title_lbl) + + return frame + # ============================================================ + # 🔄 REFRESH DATA (MAIN LOGIC) + # ============================================================ + def refresh_data(self): + try: + print("\n[SensorsStatusSummary] ===== REFRESH DATA START =====") + + # === Load sensors === + res_sensors = self.api.http.get(f"{self.api.base}/api/tables/devices_sensor") + sensors = res_sensors.json().get("rows", []) + print(f"[SensorsStatusSummary] Loaded {len(sensors)} sensors") + + # === Load new events incrementally === + if self._first_load: + print("[SensorsStatusSummary] Initial load - fetching events...") + res_events = self.api.http.get( + f"{self.api.base}/api/tables/event_logs_sensors?limit=500&order_by=id&order_dir=desc" + ) + self._events_cache = res_events.json().get("rows", []) + if self._events_cache: + self._last_event_id = max(e.get("id", 0) for e in self._events_cache) + self._first_load = False + + else: + print(f"[SensorsStatusSummary] Fetching new events after ID={self._last_event_id}") + res_events = self.api.http.get( + f"{self.api.base}/api/tables/event_logs_sensors?limit=500&order_by=id&order_dir=asc" + ) + new_events = [ + e for e in res_events.json().get("rows", []) + if e.get("id", 0) > self._last_event_id + ] + + if new_events: + print(f"[SensorsStatusSummary] Found {len(new_events)} new events") + self._events_cache.extend(new_events) + self._last_event_id = max( + self._last_event_id, + max(e.get("id", 0) for e in new_events) + ) + self._events_cache = self._events_cache[-5000:] # Cap + else: + print("[SensorsStatusSummary] No new events") + + events = self._events_cache + print(f"[SensorsStatusSummary] Total cached events: {len(events)}") + + # === Analyze events === + latest_issues = self._get_latest_open_issues(events) + + inactive = [e for e in latest_issues if e.get("issue_type") == "missing_keepalive"] + outofrange = [e for e in latest_issues if e.get("issue_type") == "out_of_range"] + corrupted = [e for e in latest_issues if e.get("issue_type") in ["corrupted", "stuck_sensor"]] + + print(f"[SensorsStatusSummary] Categorized:") + print(f" - Inactive: {len(inactive)}") + print(f" - Out of range: {len(outofrange)}") + print(f" - Corrupted: {len(corrupted)}") + + # Sensors with issues + problematic_ids = {str(e.get("device_id")) for e in latest_issues if e.get("device_id")} + total = len(sensors) + active = [s for s in sensors if str(s.get("id")) not in problematic_ids] + + health = int((len(active) / total * 100)) if total else 0 + + print(f" Total sensors: {total}") + print(f" Active sensors: {len(active)}") + print(f" Health: {health}%") + + # === Update UI === + self.active_card.value_label.setText(str(len(active))) + self.inactive_card.value_label.setText(str(len(inactive))) + self.outofrange_card.value_label.setText(str(len(outofrange))) + self.corrupted_card.value_label.setText(str(len(corrupted))) + + # Ring + self.health_ring.value = health + self.health_ring.update() + self.ring_bottom.setText(f"{len(active)} active sensors of {total}") + + # Timestamp + now = datetime.now(timezone.utc) + self.status_badge.setText(f"Live • Updated at {now.strftime('%H:%M:%S')}") + + # Table + self._update_inactive_table(inactive, sensors) + + print("[SensorsStatusSummary] ===== REFRESH DATA END =====\n") + + except Exception as e: + print(f"[SensorsStatusSummary] ❌ ERROR refreshing data: {e}") + import traceback + traceback.print_exc() + + + # ============================================================ + # 🔍 ANALYZE — find latest open issue per sensor + # ============================================================ + def _get_latest_open_issues(self, events): + issue_types = ["missing_keepalive", "out_of_range", "corrupted", "stuck_sensor"] + + last_events = {} + + for e in events: + dev = e.get("device_id") + itype = e.get("issue_type") + if not dev or itype not in issue_types: + continue + + key = (str(dev), itype) + if key not in last_events or e.get("id", 0) > last_events[key].get("id", 0): + last_events[key] = e + + # Choose highest-severity open issue per device + priority = {"corrupted": 3, "stuck_sensor": 3, "out_of_range": 2, "missing_keepalive": 1} + result = {} + + for (dev, itype), ev in last_events.items(): + if ev.get("end_ts") is None: # Still open + if dev not in result or priority[itype] > priority[result[dev]["issue_type"]]: + result[dev] = ev + + return list(result.values()) + + + # ============================================================ + # 📋 UPDATE TABLE (inactive sensors only) + # ============================================================ + def _update_inactive_table(self, inactive_events, sensors): + self.table.setRowCount(0) + + sensor_map = {str(s.get("id")): s for s in sensors} + sorted_events = sorted(inactive_events, key=lambda e: int(e.get("device_id", 0))) + + for ev in sorted_events: + dev_id = str(ev.get("device_id")) + sensor = sensor_map.get(dev_id) + if not sensor: + continue + + row = self.table.rowCount() + self.table.insertRow(row) + + last_seen = sensor.get("last_seen") + try: + dt = datetime.fromisoformat(last_seen.replace("Z", "+00:00")) + time_str = dt.strftime("%H:%M:%S") + except: + time_str = "—" + + # ID + id_item = QTableWidgetItem(dev_id) + self.table.setItem(row, 0, id_item) + + # Type + self.table.setItem(row, 1, QTableWidgetItem(sensor.get("sensor_type", "Unknown"))) + + # Last seen + ts_item = QTableWidgetItem(time_str) + ts_item.setFont(QFont("Segoe UI", 12, QFont.Weight.Bold)) + ts_item.setForeground(QColor("#DC2626")) + self.table.setItem(row, 2, ts_item) diff --git a/GUI/src/vast/views/sensors_view.py b/GUI/src/vast/views/sensors_view.py index fe980a31d..5a6dc952c 100644 --- a/GUI/src/vast/views/sensors_view.py +++ b/GUI/src/vast/views/sensors_view.py @@ -1,44 +1,339 @@ -from PyQt6.QtWidgets import QWidget, QVBoxLayout, QPushButton, QLabel, QTableWidget, QTableWidgetItem -from dashboard_api import DashboardApi +from PyQt6.QtWidgets import ( + QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, + QScrollArea, QGridLayout, QFrame, QDialog, QDialogButtonBox, + QFormLayout, QComboBox +) +from PyQt6.QtCore import Qt, QTimer +import traceback -class SensorsView(QWidget): - def __init__(self, api: DashboardApi, parent=None): - super().__init__(parent) - self.api = api +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) - title = QLabel("Sensor Types") - title.setStyleSheet("font-size: 18px; font-weight: 600;") + 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) - self.table = QTableWidget() - layout.addWidget(self.table) + # ← 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) - refresh_btn = QPushButton("Load Sensor Types") - refresh_btn.clicked.connect(self.load_sensors) - layout.addWidget(refresh_btn) + 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: - data = self.api.http.get(f"{self.api.base}/api/files").json() - except Exception as e: - print(f"[SensorsView] API error: {e}") - data = [] - - if not data: - self.table.setRowCount(0) - self.table.setColumnCount(1) - self.table.setHorizontalHeaderLabels(["No data"]) + 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 - keys = list(data[0].keys()) - self.table.setColumnCount(len(keys)) - self.table.setHorizontalHeaderLabels(keys) - self.table.setRowCount(len(data)) + 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) - for r, row in enumerate(data): - for c, key in enumerate(keys): - self.table.setItem(r, c, QTableWidgetItem(str(row[key]))) + # ------------------------------------------------------------ + def _show_alert_history(self, sensor): + dlg = AlertDialog(sensor) + dlg.exec() diff --git a/GUI/src/vast/views/sound/map_background.png b/GUI/src/vast/views/sound/map_background.png new file mode 100644 index 000000000..2f71da42e Binary files /dev/null and b/GUI/src/vast/views/sound/map_background.png differ diff --git a/GUI/src/vast/views/sound/sound_view.py b/GUI/src/vast/views/sound/sound_view.py new file mode 100644 index 000000000..1d47cd9c9 --- /dev/null +++ b/GUI/src/vast/views/sound/sound_view.py @@ -0,0 +1,1997 @@ +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 PyQt6.QtWebEngineWidgets import QWebEngineView +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 +import tempfile + +MINIO_BASE = os.getenv("MINIO_PUBLIC_BASE", "http://minio-hot:9000") + +def normalize_minio_url(url: str) -> str: + if not url: + return "" + if url.startswith("http://") or url.startswith("https://"): + return url + url = url.lstrip("/") + if url.startswith("sounds/"): + url = "sound/" + url + return f"{MINIO_BASE.rstrip('/')}/{url}" + + +# ========================================================== +# 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: none; + border-radius: 12px; + """) + + 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: + 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, api=None): + super().__init__(parent) + self.api = api + 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() + + self.map_page = QWidget() + layout = QVBoxLayout(self.map_page) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(15) + + 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) + + self._load_map_image() + + 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): + 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_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, + api=self.api, + ) + 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 + + 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) + + filter_frame = self._create_filter_frame() + + list_label = QLabel("Available Recordings") + list_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #333; padding: 5px;") + + self.file_table = self._create_table() + + waveform_container = self._create_waveform_container() + + 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; + """) + + self.player = QMediaPlayer() + self.audio_output = QAudioOutput() + self.audio_output.setVolume(1.0) + self.player.setAudioOutput(self.audio_output) + self.player.playbackStateChanged.connect(self.on_playback_state_changed) + self._current_temp_file = None + self._current_play_btn = None + self._current_stop_btn = None + + layout.addWidget(filter_frame) + layout.addWidget(list_label) + layout.addWidget(self.file_table, 1) + + if self.recording_type == "audio": + 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-radius: 12px; padding: 15px; } + """) + filters_layout = QVBoxLayout(filter_frame) + filters_layout.setSpacing(12) + + filter_row = QHBoxLayout() + filter_row.setSpacing(8) + filter_row.setContentsMargins(0, 0, 0, 0) + + type_label = QLabel("Type:") + type_label.setStyleSheet("font-weight: bold; color: #333; font-size: 11px;") + filter_row.addWidget(type_label) + + self.noise_filter = QComboBox() + self.noise_filter.setMaximumWidth(180) + self.noise_filter.setStyleSheet(""" + QComboBox { + padding: 6px 10px; + border: 1px solid #d1d5da; + border-radius: 4px; + background: white; + font-size: 12px; + } + QComboBox:hover { border: 1px solid #4A90E2; } + """) + + if self.recording_type == "ultrasound": + self.noise_filter.addItems([ + "All signals", "Drought-stressed plant", + "Empty Pot", "Greenhouse Noises" + ]) + else: + self.noise_filter.addItems([ + "All types", "predatory_animals", "non_predatory_animals", + "birds", "fire", "footsteps", "insects", "screaming", + "shotgun", "stormy_weather", "streaming_water", "vehicle", "Other" + ]) + + filter_row.addWidget(self.noise_filter) + + date_label = QLabel(" From:") + date_label.setStyleSheet("font-weight: bold; color: #333; font-size: 11px;") + filter_row.addWidget(date_label) + + today = QDate.currentDate() + first_day = QDate(today.year(), today.month(), 1) + + self.date_from = QDateEdit() + self.date_from.setCalendarPopup(True) + self.date_from.setDate(first_day) + self.date_from.setMaximumWidth(120) + self.date_from.setStyleSheet(""" + QDateEdit { + padding: 6px 8px; + border: 1px solid #d1d5da; + border-radius: 4px; + background: white; + font-size: 12px; + } + """) + filter_row.addWidget(self.date_from) + + filter_row.addWidget(QLabel("→")) + + self.date_to = QDateEdit() + self.date_to.setCalendarPopup(True) + self.date_to.setDate(today) + self.date_to.setMaximumWidth(120) + self.date_to.setStyleSheet(self.date_from.styleSheet()) + filter_row.addWidget(self.date_to) + + self.search_box = QLineEdit() + self.search_box.setPlaceholderText("Search filename...") + self.search_box.setMaximumWidth(200) + self.search_box.setStyleSheet(""" + QLineEdit { + padding: 6px 10px; + border: 1px solid #d1d5da; + border-radius: 4px; + background: white; + font-size: 12px; + } + QLineEdit:focus { border: 1px solid #4A90E2; } + """) + filter_row.addWidget(self.search_box) + + filter_row.addStretch() + + filter_row.addWidget(QLabel("sort by:")) + self.sort_by = QComboBox() + self.sort_by.addItems(["date", "name", "device"]) + self.sort_by.setMaximumWidth(130) + self.sort_by.setStyleSheet(""" + QComboBox { + padding: 6px 10px; + border: 1px solid #d1d5da; + border-radius: 4px; + background: white; + font-size: 12px; + } + """) + filter_row.addWidget(self.sort_by) + + 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: 6px; + padding: 8px 16px; + font-weight: bold; + font-size: 12px; + } + QPushButton:hover { background-color: #357ABD; } + """) + filter_row.addWidget(self.refresh_button) + + filters_layout.addLayout(filter_row) + + return filter_frame + + def _create_table(self): + table = QTableWidget() + + if self.recording_type == "ultrasound": + table.setColumnCount(6) + table.setHorizontalHeaderLabels([ + "File", "Device", "Predicted Label", "Confidence", "Watering Status", "Format" + ]) + else: + table.setColumnCount(6) + table.setHorizontalHeaderLabels([ + "File", "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: transparent; + border: none; + padding: 0px; + } + """) + + waveform_layout = QVBoxLayout(waveform_container) + waveform_layout.setSpacing(5) + + self.waveform = AudioWaveform() + self.waveform.setMinimumHeight(100) + self.waveform.setMaximumHeight(120) + + waveform_layout.addWidget(self.waveform) + + return waveform_container + + def on_playback_state_changed(self, 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") + if hasattr(self, '_current_play_btn') and self._current_play_btn: + self._reset_button_pair(self._current_play_btn, self._current_stop_btn) + self._current_play_btn = None + self._current_stop_btn = None + + def _map_ultrasound_label(self, raw: str) -> str: + if not raw: + return "Unknown" + lower = raw.lower() + if "tomato" in lower or "tobacco" in lower: + return "Drought-stressed plant" + return raw + + 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 + } + + filter_value = self.noise_filter.currentText() + if self.recording_type == "ultrasound": + if filter_value in ("Empty Pot", "Greenhouse Noises"): + params["predicted_class"] = filter_value + else: + if filter_value not in ("All types", "All signals"): + params["type"] = filter_value + + if self.mic_ids: + params["device_ids"] = ",".join(self.mic_ids) + + try: + # Check if API is available and authenticated + if not self.api or not hasattr(self.api, 'http'): + self.status_label.setText("⚠ API connection not available") + QMessageBox.warning( + self, + "Authentication Required", + "Please login first to access recordings." + ) + return + + # Use the authenticated session + response = self.api.http.get(self.api_url, params=params, timeout=10) + response.raise_for_status() + data = response.json() + + print(f"[DEBUG] Successfully fetched {len(data)} records from {self.api_url}") + for f in data: + row = self.file_table.rowCount() + self.file_table.insertRow(row) + self.file_table.setRowHeight(row, 60) + + filename = f.get("filename") or f.get("file", "") + is_compressed = f.get("is_compressed", False) + + text_color = QColor("#888888") if is_compressed else QColor("#000000") + + if self.recording_type == "ultrasound": + device_id = f.get("device_id", "N/A") + pred_class_raw = f.get("predicted_class", "Unknown") + pred_class = self._map_ultrasound_label(pred_class_raw) + confidence = f.get("confidence", 0) + watering_status = f.get("watering_status", "N/A") + url = normalize_minio_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(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) + + item5 = QTableWidgetItem(format_str) + item5.setForeground(text_color) + self.file_table.setItem(row, 5, item5) + else: + device_id = f.get("device_id", "N/A") + label = f.get("predicted_label", "Unknown") + prob = f.get("probability", 0) + url = normalize_minio_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) + + if self.recording_type == "audio": + 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) + + 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; } + """) + + 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) + + 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.HTTPError as e: + if e.response.status_code == 401: + self.status_label.setText("⚠ Authentication required") + QMessageBox.warning(self, "Authentication Error", + "API requires authentication. Please check your credentials.") + else: + self.status_label.setText(f"⚠ HTTP Error {e.response.status_code}") + QMessageBox.warning(self, "HTTP Error", + f"Server returned error {e.response.status_code}:\n{str(e)}") + 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): + if not url: + QMessageBox.warning(self, "No URL", "Audio file URL not available") + return + + self.player.stop() + self.waveform.stop_animation() + + try: + if self._current_temp_file: + if os.path.exists(self._current_temp_file): + os.remove(self._current_temp_file) + except Exception: + pass + self._current_temp_file = None + + playback_url = url + if url.startswith("http://localhost") or url.startswith("http://127.0.0.1"): + parts = url.split("/", 3) + if len(parts) > 3: + path = parts[3] + playback_url = f"http://minio-hot:9000/{path}" + + 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 + + try: + session = self.api.http if (self.api and getattr(self.api, "http", None)) else requests + resp = session.get(playback_url, timeout=15) + resp.raise_for_status() + suffix = ".ogg" if is_compressed else ".wav" + tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix) + tmp.write(resp.content) + tmp.flush() + tmp_path = tmp.name + tmp.close() + self._current_temp_file = tmp_path + except requests.exceptions.RequestException as e: + self.status_label.setText("⚠ Unable to download file") + QMessageBox.warning(self, "Download Error", f"Could not download audio file:\n{e}") + return + except Exception as e: + self.status_label.setText("⚠ Error downloading file") + QMessageBox.warning(self, "Error", f"Failed to download audio file:\n{e}") + return + + try: + self._reset_all_buttons() + + play_btn.setEnabled(False) + play_btn.setStyleSheet(""" + QPushButton { + background-color: #888888; + color: white; + border-radius: 4px; + font-weight: bold; + } + """) + + stop_btn.setEnabled(True) + stop_btn.setStyleSheet(""" + QPushButton { + background-color: #dc3545; + color: white; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { background-color: #c82333; } + """) + + self.player.setSource(QUrl.fromLocalFile(self._current_temp_file)) + self.player.play() + self.waveform.start_animation() + self.status_label.setText(f"Playing: {filename}") + + self._current_play_btn = play_btn + self._current_stop_btn = stop_btn + + except Exception as e: + self.status_label.setText("⚠ Playback error") + QMessageBox.warning(self, "Playback Error", f"Playback failed:\n{e}") + self._reset_all_buttons() + + def stop_row_audio(self, play_btn, stop_btn): + self.player.stop() + self.waveform.stop_animation() + self.status_label.setText("⏹ Stopped") + self._reset_button_pair(play_btn, stop_btn) + + def _reset_button_pair(self, play_btn, stop_btn): + if play_btn.toolTip() and "Compressed" in play_btn.toolTip(): + 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; } + """) + 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; } + """) + play_btn.setEnabled(True) + + 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; } + """) + + def _reset_all_buttons(self): + if self.recording_type != "audio": + return + + actions_col = 5 + for row in range(self.file_table.rowCount()): + widget = self.file_table.cellWidget(row, actions_col) + 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 and stop_btn and isinstance(play_btn, QPushButton): + self._reset_button_pair(play_btn, stop_btn) + + +# ========================================================== +# Sound Analytics View - NEW TAB from first document +# ========================================================== +class SoundAnalyticsView(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" + ] + + CYAN_PALETTE = [ + '#003366', '#004d99', '#0066cc', '#1a80e5', + '#3399ff', '#53A0E5', '#66b3ff', '#80ccff', + '#99e6ff', '#b3f0ff', '#ccf7ff' + ] + + PRIMARY_CYAN = '#53A0E5' + ACCENT_CYAN = '#3399ff' + + LIGHT_THEME = { + 'bg': '#f8f9fa', + 'card': '#ffffff', + 'text': '#333333', + 'border': '#e0e0e0', + 'primary': PRIMARY_CYAN, + 'accent': ACCENT_CYAN + } + + DARK_THEME = { + 'bg': '#1e1e1e', + 'card': '#2d2d2d', + 'text': '#e0e0e0', + 'border': '#444444', + 'primary': '#64B5F6', + 'accent': ACCENT_CYAN + } + + def __init__(self, api: DashboardApi, parent=None): + super().__init__(parent) + self.api = api + + print(f"[INIT] API object: {self.api}", flush=True) + print(f"[INIT] API has http: {hasattr(self.api, 'http')}", flush=True) + + try: + test_query = "SELECT 1 as test" + result = self.api.run_query(test_query) + print(f"[INIT] DB test result: {result}", flush=True) + except Exception as e: + print(f"[INIT] DB connection error: {e}", flush=True) + + self.current_time_range = 'day' + self.current_sound_types = [] + self.is_dark_theme = False + self.current_theme = self.LIGHT_THEME.copy() + + self.setWindowTitle("Sound Detection Analytics") + self.setMinimumSize(QSize(1350, 1000)) + + main_layout = QVBoxLayout() + main_layout.setContentsMargins(0, 0, 0, 0) + main_layout.setSpacing(0) + + content_frame = QFrame() + content_layout = QVBoxLayout() + content_layout.setContentsMargins(12, 12, 12, 12) + content_layout.setSpacing(12) + + 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_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_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(f"color: {self.PRIMARY_CYAN}; 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(f""" + QPushButton {{ + background-color: {self.PRIMARY_CYAN}; + color: white; + font-weight: bold; + }} + QPushButton:hover {{ + background-color: {self.CYAN_PALETTE[2]}; + }} + """) + apply_btn.clicked.connect(self._refresh_data) + sound_header_row.addWidget(apply_btn) + filter_layout.addLayout(sound_header_row) + + checkbox_container = QFrame() + checkbox_container.setObjectName("checkboxContainer") + checkbox_container.setStyleSheet(f""" + QFrame#checkboxContainer {{ + background-color: white; + border: 2px solid {self.PRIMARY_CYAN}; + 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) + + calendar_frame = self._create_activity_calendar() + content_layout.addWidget(calendar_frame) + + 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) + + 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 + + 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) + + 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) + + 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) + + 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) + + self.refresh_timer = QTimer() + self.refresh_timer.timeout.connect(self._refresh_data) + self.refresh_timer.start(30000) + + self._refresh_data() + + def _create_activity_calendar(self) -> QFrame: + 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) + + 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)) + + 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: + if intensity < 0.2: + return self.CYAN_PALETTE[0] + elif intensity < 0.4: + return self.CYAN_PALETTE[2] + elif intensity < 0.6: + return self.CYAN_PALETTE[4] + elif intensity < 0.8: + return self.CYAN_PALETTE[7] + else: + return self.CYAN_PALETTE[10] + + def _create_canvas(self, figsize=(5.5, 4.5)): + 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: + 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(f"color: {self.PRIMARY_CYAN}; margin-bottom: 4px;") + layout.addWidget(title_label) + + layout.addWidget(widget, 1) + frame.setLayout(layout) + return frame + + def _create_stats_frame(self) -> QFrame: + 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(f"color: {self.PRIMARY_CYAN}; 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) + + 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: + 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(f"color: {self.PRIMARY_CYAN};") + 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): + selected = [] + for sound_name, checkbox in self.sound_checkboxes.items(): + if checkbox.isChecked(): + selected.append(sound_name) + + self.current_sound_types = selected + + 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") + + def _clear_sound_selection(self): + 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' + self._refresh_data() + + def _on_filter_changed(self): + time_map = {'1 Day': 'day', '1 Week': 'week', '1 Month': 'month'} + self.current_time_range = time_map.get(self.time_filter.currentText(), 'day') + self._refresh_data() + + def _refresh_data(self): + try: + sound_filter = self.current_sound_types if self.current_sound_types else None + + 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"[SoundAnalyticsView] Refresh error: {e}", flush=True) + + def _update_distribution_chart(self): + 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 + ) + + print(f"[DEBUG] Distribution data: {len(data) if data else 0} items", flush=True) + + 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] + + self.dist_canvas.figure.clear() + ax = self.dist_canvas.figure.add_subplot(111) + + colors = [self.CYAN_PALETTE[i % len(self.CYAN_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') + + 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() + print("[DEBUG] Distribution chart drawn successfully", flush=True) + + except Exception as e: + print(f"[ERROR] Distribution chart error: {e}", flush=True) + import traceback + traceback.print_exc() + self._show_no_data(self.dist_canvas) + + def _update_timeline_chart(self): + 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 + ) + + print(f"[DEBUG] Timeline data: {len(data) if data else 0} items", flush=True) + + if not data: + self._show_no_data(self.timeline_canvas) + return + + 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] + + self.timeline_canvas.figure.clear() + ax = self.timeline_canvas.figure.add_subplot(111) + + ax.plot(times, counts, marker='o', linewidth=2, markersize=5, color=self.ACCENT_CYAN) + ax.fill_between(range(len(times)), counts, alpha=0.2, color=self.PRIMARY_CYAN) + 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() + print("[DEBUG] Timeline chart drawn successfully", flush=True) + + except Exception as e: + print(f"[ERROR] Timeline chart error: {e}", flush=True) + import traceback + traceback.print_exc() + self._show_no_data(self.timeline_canvas) + + def _update_confidence_chart(self): + 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=self.PRIMARY_CYAN) + ax1.plot(times, avg_conf, color=self.ACCENT_CYAN, marker="o", linewidth=2, label="Avg Confidence %") + ax1.fill_between(range(len(avg_conf)), avg_conf, alpha=0.15, color=self.PRIMARY_CYAN) + 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="--") + + ax2 = ax1.twinx() + proc_color = self.CYAN_PALETTE[7] + ax2.plot(times, avg_proc, color=proc_color, marker="^", linestyle="--", linewidth=2, label="Avg Processing (ms)") + ax2.set_ylabel("Processing Time (ms)", fontsize=9, fontweight="bold", color=proc_color) + ax2.tick_params(axis='y', labelcolor=proc_color) + + 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"[SoundAnalyticsView] Model Health Monitor chart error: {e}", flush=True) + self._show_no_data(self.confidence_canvas) + + def _update_stats_boxes(self): + 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 + ) + + 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"[SoundAnalyticsView] Stats update error: {e}", flush=True) + + def _clear_canvas(self, canvas): + canvas.figure.clear() + + def _show_no_data(self, 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): + self.refresh_timer.stop() + super().closeEvent(event) + + def _update_heatmap_chart(self): + 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 + ) + + print(f"[DEBUG] Heatmap data: {len(data) if data else 0} items", flush=True) + + if not data: + self._show_no_data(self.heatmap_canvas) + return + + heatmap_data = np.zeros((24, 7)) + + for row in data: + hour = int(row['hour_of_day']) + day = int(row['day_of_week']) + count = row['count'] + heatmap_data[hour, day] += count + + self.heatmap_canvas.figure.clear() + ax = self.heatmap_canvas.figure.add_subplot(111) + + im = ax.imshow(heatmap_data, cmap='GnBu', aspect='auto', interpolation='nearest') + + 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') + + cbar = self.heatmap_canvas.figure.colorbar(im, ax=ax, pad=0.02) + cbar.set_label('Detections', fontsize=8) + cbar.ax.tick_params(labelsize=7) + + 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() + print("[DEBUG] Heatmap chart drawn successfully", flush=True) + + except Exception as e: + print(f"[ERROR] Heatmap chart error: {e}", flush=True) + import traceback + traceback.print_exc() + self._show_no_data(self.heatmap_canvas) + + def _update_correlation_chart(self): + 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"[DEBUG] Correlation data: {len(data) if data else 0} items", flush=True) + + if not data or len(data) < 1: + self._show_no_data(self.correlation_canvas) + return + + time_buckets = sorted(list(set(row['time_bucket'] for row in data))) + sound_types = sorted(list(set(row['sound_type'] for row in data))) + + if len(time_buckets) < 2 or len(sound_types) < 2: + self._show_no_data(self.correlation_canvas) + return + + n_times = len(time_buckets) + n_sounds = len(sound_types) + data_matrix = np.zeros((n_times, n_sounds)) + + 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'] + + corr_matrix = np.corrcoef(data_matrix.T) + corr_matrix = np.nan_to_num(corr_matrix, nan=0.0) + + self.correlation_canvas.figure.clear() + ax = self.correlation_canvas.figure.add_subplot(111) + + im = ax.imshow(corr_matrix, cmap='Blues', aspect='auto', vmin=-1, vmax=1) + + 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) + + for i in range(len(sound_types)): + for j in range(len(sound_types)): + value = corr_matrix[i, j] + 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') + + 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("[DEBUG] Correlation chart drawn successfully", flush=True) + + except Exception as e: + print(f"[ERROR] Correlation chart error: {e}", flush=True) + import traceback + traceback.print_exc() + self._show_no_data(self.correlation_canvas) + +# ========================================================== +# Sound2 View - Displays Grafana dashboard +# ========================================================== +class Sound2View(QWidget): + def __init__(self, api: DashboardApi, parent=None): + super().__init__(parent) + self.api = api + self.setup_ui() + + def setup_ui(self): + layout = QVBoxLayout(self) + layout.setContentsMargins(0, 0, 0, 0) + + header = QHBoxLayout() + + title = QLabel("🌿 Ultrasonic Plant Predictions Dashboard") + title.setStyleSheet(""" + font-size: 18px; + font-weight: bold; + padding: 10px; + color: #2C3E50; + """) + header.addWidget(title) + + header.addStretch() + + refresh_btn = QPushButton("🔄 Refresh") + refresh_btn.setStyleSheet(""" + QPushButton { + background-color: #3498DB; + color: white; + border: none; + padding: 8px 16px; + border-radius: 4px; + font-weight: bold; + } + QPushButton:hover { + background-color: #2980B9; + } + """) + refresh_btn.clicked.connect(self.refresh_dashboard) + header.addWidget(refresh_btn) + + layout.addLayout(header) + + self.web_view = QWebEngineView() + + grafana_url = ( + "http://grafana:3000/d/ultrasonic-predictions/" + "ultrasonic-plant-predictions" + "?orgId=1&refresh=5s&kiosk=tv&theme=light" + ) + + self.web_view.setUrl(QUrl(grafana_url)) + layout.addWidget(self.web_view) + + self.status_label = QLabel("📊 Loading dashboard...") + self.status_label.setStyleSheet(""" + height: 24px; + padding: 5px; + color: #7F8C8D; + font-size: 12px; + """) + layout.addWidget(self.status_label) + + self.web_view.loadFinished.connect(self.on_load_finished) + + def refresh_dashboard(self): + self.status_label.setText("🔄 Refreshing dashboard...") + self.web_view.reload() + + def on_load_finished(self, success: bool): + if success: + self.status_label.setText("✓ Dashboard loaded successfully | Refreshes every 5s") + else: + self.status_label.setText( + "⚠ Failed to load dashboard. Please check Grafana server." + ) + + +# ========================================================== +# 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(api=self.api) + self.env_tab = RecordingsTab(recording_type="audio", api=self.api) + self.plant_tab = RecordingsTab(recording_type="ultrasound", api=self.api) + self.dashboard_tab = Sound2View(api=self.api) + self.analytics_tab = SoundAnalyticsView(api=self.api) + + self.tabs.addTab(self.map_tab, "🗺️ Interactive Map") + self.tabs.addTab(self.env_tab, "🎵 Environment Sounds") + self.tabs.addTab(self.plant_tab, "🌿 Plant Ultrasounds") + self.tabs.addTab(self.dashboard_tab, "📊 Ultrasonic Dashboard") + self.tabs.addTab(self.analytics_tab, "📈 Sound Analytics") + + layout.addWidget(self.tabs) diff --git a/README.md b/README.md new file mode 100644 index 000000000..06d285baf --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# 🌱 AgCloud -- Smart Cloud Agriculture Platform + +AgCloud is a smart cloud-based agriculture platform designed to +understand, monitor, and predict field conditions in real time. The +system integrates data from heterogeneous field sources and processes it +through a scalable, distributed data architecture to enable accurate, +data-driven decision making. + +## 🚜 System Overview + +The platform continuously collects data from a wide range of field +sources, including IoT sensors, soil and environmental measurements, +audio signals, cameras, drones, and agronomic and atmospheric data. + +The system unifies numeric, spatial, visual, and acoustic data into a +single, coherent view of the field. + +## 🔗 Data Ingestion & Streaming + +- MQTT is used as a lightweight, efficient, and reliable communication + protocol for ingesting data from edge devices. +- Apache Kafka serves as the backbone of the data layer, enabling + scalable, fault-tolerant, and distributed processing. + +## ⚙️ Data Processing Layer + +Data is consumed by: - Apache Flink for real-time stream processing - +Apache Airflow for scheduled and batch workflows + +Processing responsibilities include data cleaning, normalization, +enrichment, analytics, and predictive modeling. + +## 🗄️ Storage Layer + +Processed data is stored according to its characteristics: - PostgreSQL +(structured and time-series data) - Object Storage (images, video, +audio) - GIS-compatible storage (spatial data) - Vector Databases +(advanced analytics and embeddings) + +## 📊 Analytics & Visualization + +The platform enables anomaly detection, risk identification, and +long-term pattern analysis. Insights are exposed via a centralized API +and visualized through a unified dashboard with maps, metrics, alerts, +and forecasts. + +## 🐳 Running the Project + +### Prerequisites + +- Docker +- Docker Compose + +### Run the platform + +``` bash +docker compose up -d --build +``` + +This command builds and starts the entire system, including ingestion, +streaming, processing, storage, and API layers. diff --git a/RelDB/Dockerfile b/RelDB/Dockerfile index 10781dcc0..4c2f5bdba 100644 --- a/RelDB/Dockerfile +++ b/RelDB/Dockerfile @@ -38,6 +38,7 @@ COPY build_tables/02_event_logs_partition.sql /docker-entrypoint-initdb.d/02_eve 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 diff --git a/RelDB/build_tables/05_agcloud_audio_init.sql b/RelDB/build_tables/05_agcloud_audio_init.sql new file mode 100644 index 000000000..d2b3afc83 --- /dev/null +++ b/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/RelDB/build_tables/loader.sql b/RelDB/build_tables/loader.sql index c58dc99c6..aa5fc4eea 100644 --- a/RelDB/build_tables/loader.sql +++ b/RelDB/build_tables/loader.sql @@ -1,34 +1,54 @@ -- 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) +INSERT INTO devices (device_id, model, owner, active, location_lat, location_lon) VALUES + ('dev-a','drone-x','TeamA',true,NULL,NULL), + ('dev-b','drone-x','TeamA',true,NULL,NULL), + ('dev-c','rover-y','TeamB',true,NULL,NULL), + ('dev-d','rover-y','TeamB',true,NULL,NULL), + ('dev-e','sensor-z','TeamC',true,NULL,NULL), + ('dev-f','sensor-z','TeamC',true,NULL,NULL), + ('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,NULL,NULL), + ('mic-2','sound-a','TeamD',true,NULL,NULL), + ('mic-33','sound-a','TeamD',true,NULL,NULL), + ('mic-u-2','sound-ul','TeamD',true,NULL,NULL) ON CONFLICT DO NOTHING; --- Insert synthetic sensors -INSERT INTO sensors ( - sensor_name, - sensor_type, - owner_name, - location_lat, - location_lon, - install_date, - status, - description, - last_maintenance -) -VALUES - ('SoilMoistureSensor_A1', 'moisture', 'TeamA', 32.051, 34.871, NOW() - INTERVAL '120 days', 'active', 'Soil probe at north field section A1', NOW() - INTERVAL '20 days'), - ('TempSensor_B2', 'temperature', 'TeamA', 32.057, 34.885, NOW() - INTERVAL '95 days', 'active', 'Temperature monitor - greenhouse B2', NOW() - INTERVAL '15 days'), - ('HumiditySensor_C1', 'humidity', 'TeamB', 31.982, 34.945, NOW() - INTERVAL '200 days', 'maintenance', 'Humidity node C1 (low battery)', NOW() - INTERVAL '3 days'), - ('NDVI_Camera_01', 'NDVI', 'TeamB', 32.015, 34.980, NOW() - INTERVAL '60 days', 'active', 'Multispectral NDVI drone-mounted camera', NOW() - INTERVAL '10 days'), - ('WeatherStation_Main', 'weather', 'TeamC', 32.000, 34.760, NOW() - INTERVAL '365 days', 'active', 'Main weather station at south field', NOW() - INTERVAL '30 days'), - ('SoilProbe_Edge', 'moisture', 'TeamC', 32.010, 34.910, NOW() - INTERVAL '40 days', 'inactive', 'Edge field soil probe - disconnected', NOW() - INTERVAL '60 days') -ON CONFLICT (sensor_name) DO NOTHING; + +INSERT INTO zones (name, geom) VALUES + ('Zone A', ST_GeomFromText('POLYGON((34.75 32.00, 34.90 32.00, 34.90 32.10, 34.75 32.10, 34.75 32.00))', 4326)), + ('Zone B', 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; + +-- Seed data for devices_sensor table +-- This file is automatically executed during database initialization + +INSERT INTO devices_sensor (id, plant_id, sensor_type, last_seen) VALUES + ('1', 101, 'temperature', NOW()), + ('2', 101, 'humidity', NOW()), + ('3', 101, 'soil_moisture', NOW()), + ('4', 102, 'co2', NOW()), + ('5', 102, 'light_intensity', NOW()), + ('6', 103, 'rainfall', NOW()), + ('7', 103, 'ph', NOW()), + ('8', 104, 'temperature', NOW()), + ('9', 104, 'humidity', NOW()), + ('10', 105, 'soil_moisture', NOW()), + ('11', 105, 'co2', NOW()), + ('12', 106, 'light_intensity', NOW()), + ('13', 106, 'wind_speed', NOW()), + ('14', 107, 'ph', NOW()), + ('15', 107, 'temperature', NOW()), + ('16', 107, 'ph', NOW()), + ('17', 107, 'temperature', NOW()) +ON CONFLICT (id) DO UPDATE SET + plant_id = EXCLUDED.plant_id, + sensor_type = EXCLUDED.sensor_type, + last_seen = NOW(); -- Insert some regions INSERT INTO regions (name, geom) @@ -46,6 +66,26 @@ VALUES ('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, @@ -142,7 +182,6 @@ SELECT 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( @@ -151,3 +190,46 @@ SELECT ARRAY( ) 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 := '2025-10-25 00:00:00+00'::timestamptz; + end_date timestamptz := '2025-11-25 23:59:59+00'::timestamptz; + 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 1000 random reports + FOR i IN 1..1000 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() * (end_date - start_date)); + 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 $$; \ No newline at end of file diff --git a/RelDB/build_tables/schema.sql b/RelDB/build_tables/schema.sql index df7db1a3f..b6464b643 100644 --- a/RelDB/build_tables/schema.sql +++ b/RelDB/build_tables/schema.sql @@ -11,7 +11,15 @@ CREATE TABLE IF NOT EXISTS devices ( device_id text PRIMARY KEY, model text, owner text, - active boolean DEFAULT true + active boolean DEFAULT true, + location_lat DOUBLE PRECISION, + location_lon DOUBLE PRECISION +); + +CREATE TABLE IF NOT EXISTS public.zones ( + id SERIAL PRIMARY KEY, + name VARCHAR(128) NOT NULL, + geom geometry(POLYGON, 4326) NOT NULL ); -- Predefined regions (optional: for missions crossing multiple regions) @@ -28,8 +36,23 @@ CREATE TABLE IF NOT EXISTS anomaly_types ( 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, @@ -128,13 +151,13 @@ CREATE TABLE IF NOT EXISTS users ( ); 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() + 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 ( @@ -210,10 +233,62 @@ CREATE TABLE IF NOT EXISTS inference_logs ( 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(device_id), + 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(), @@ -225,23 +300,427 @@ CREATE TABLE IF NOT EXISTS event_logs_sensors( -CREATE TABLE IF NOT EXISTS sensors ( +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, - 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 + 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 fruit_detections ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + original_key TEXT NOT NULL, + cropped_key TEXT NOT NULL, + bucket TEXT NOT NULL, + device_id TEXT NOT NULL, + timestamp TIMESTAMPTZ NOT NULL, + x1 INT NOT NULL, + y1 INT NOT NULL, + x2 INT NOT NULL, + y2 INT NOT NULL, + latency_ms_model INT, + label TEXT, + created_at TIMESTAMPTZ DEFAULT now() +); + +CREATE INDEX idx_fruit_original_key ON fruit_detections(original_key); +CREATE INDEX idx_fruit_device_ts ON fruit_detections(device_id, timestamp); + +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_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_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_securixxxxxxxxctions ( + id BIGSERIAL PRIMARY KEY, + file_name VARCHAR(255), + key TEXT, + linked_time TIMESTAMPTZ +); + + +-- === Indexes for performance optimization === -- Spatial CREATE INDEX IF NOT EXISTS ix_missions_area_geom_gist ON missions USING GIST (area_geom); @@ -283,3 +762,122 @@ CREATE INDEX IF NOT EXISTS ix_event_logs_sensors_device_start ON event_logs_sens 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); + +-- ============================================ +-- 🔹 SENSORS TABLES (zones, sensors, sensors_anomalies_modal) +-- ============================================ + +-- 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 +CREATE TABLE IF NOT EXISTS public.sensors ( + sensor_id SERIAL 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 +); + +-- Sensors anomalies modal (aggregated anomaly detection model) +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() +); + +-- ============================================ +-- 🔹 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_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 (lat, lon); +CREATE TABLE IF NOT EXISTS sensor_embeddings ( + id BIGSERIAL PRIMARY KEY, + sensor_id BIGINT NOT NULL, + vec vector(5), + created_at TIMESTAMP DEFAULT NOW() +); +CREATE OR REPLACE FUNCTION generate_sensor_embedding() +RETURNS trigger AS $$ +DECLARE + name_len INT; + type_len INT; + status_score FLOAT; + vec vector(5); +BEGIN + name_len := COALESCE(LENGTH(NEW.sensor_name), 0); + type_len := COALESCE(LENGTH(NEW.sensor_type), 0); + status_score := CASE WHEN LOWER(COALESCE(NEW.status, '')) = 'active' THEN 1 ELSE 0 END; + + vec := ARRAY[ + COALESCE(NEW.lat, 0), + COALESCE(NEW.lon, 0), + name_len, + type_len, + status_score + ]::vector; + + DELETE FROM sensor_embeddings WHERE sensor_id = NEW.sensor_id; + + INSERT INTO sensor_embeddings(sensor_id, vec) + VALUES (NEW.sensor_id, vec); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER trg_generate_embedding +AFTER INSERT OR UPDATE ON sensors +FOR EACH ROW +EXECUTE FUNCTION generate_sensor_embedding(); diff --git a/RelDB/docker-compose.yml b/RelDB/docker-compose.yml index 8b26401fd..e8ac85ae0 100644 --- a/RelDB/docker-compose.yml +++ b/RelDB/docker-compose.yml @@ -34,6 +34,39 @@ services: - 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: diff --git a/RelDB/graphs/postgres-queries.yml b/RelDB/graphs/postgres-queries.yml index b2e34316a..85037af90 100644 --- a/RelDB/graphs/postgres-queries.yml +++ b/RelDB/graphs/postgres-queries.yml @@ -134,3 +134,504 @@ pg_database_size: - 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 '365 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, + MAX(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 + 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: "Last detection hour (epoch)" + - count: + usage: "GAUGE" + description: "Detections count in last 7 days" + + +# 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)" + + +# ============================================ +# ULTRASONIC PLANT PREDICTIONS METRICS +# S3 Path format: s3://sound/plants/microphone-MIC-02/2025-11-13/1763018877231/MIC-U-01_20251113T073457Z.wav +# ============================================ + +ultrasonic_predictions_total: + query: "SELECT COUNT(*)::float as total FROM public.ultrasonic_plant_predictions" + master: true + metrics: + - total: + usage: "GAUGE" + description: "Total number of ultrasonic predictions" + +ultrasonic_predictions_by_class: + query: | + SELECT + CASE predicted_class + WHEN 'Healthy_Tomato' THEN 'Healthy_Plant' + WHEN 'Healthy_Tobacco' THEN 'Healthy_Plant' + + WHEN 'Drought_Tomato' THEN 'Drought_Plant' + WHEN 'Drought_Tobacco' THEN 'Drought_Plant' + + WHEN 'Pest_Tomato' THEN 'Pest_Plant' + WHEN 'Pest_Tobacco' THEN 'Pest_Plant' + + ELSE COALESCE(predicted_class, 'unknown') + END as predicted_class, + COUNT(*)::float as count + FROM public.ultrasonic_plant_predictions + GROUP BY 1 + master: true + metrics: + - predicted_class: + usage: "LABEL" + description: "Predicted class (Drought_Plant, Healthy_Plant, Pest_Plant, Control, etc.)" + - count: + usage: "GAUGE" + description: "Number of predictions per class" + +ultrasonic_predictions_by_watering: + query: | + SELECT + COALESCE(watering_status, 'unknown') as watering_status, + COUNT(*)::float as count + FROM public.ultrasonic_plant_predictions + GROUP BY watering_status + master: true + metrics: + - watering_status: + usage: "LABEL" + description: "Watering status (Yes, No, etc.)" + - count: + usage: "GAUGE" + description: "Number of predictions per watering status" + +ultrasonic_predictions_by_status: + query: | + SELECT + COALESCE(status, 'unknown') as status, + COUNT(*)::float as count + FROM public.ultrasonic_plant_predictions + GROUP BY status + master: true + metrics: + - status: + usage: "LABEL" + description: "Record status (Success, Error, etc.)" + - count: + usage: "GAUGE" + description: "Number of predictions per status" + +ultrasonic_predictions_avg_confidence: + query: | + SELECT + AVG(confidence)::float as avg_confidence + FROM public.ultrasonic_plant_predictions + WHERE confidence IS NOT NULL + AND TO_TIMESTAMP( + SUBSTRING( + REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') + FROM 'MIC-U-\d+_(\d{8}T\d{6})Z' + ), + 'YYYYMMDD"T"HH24MISS' + ) > NOW() - INTERVAL '7 days' + master: true + metrics: + - avg_confidence: + usage: "GAUGE" + description: "Average confidence score of predictions (Last 7 days)" + +ultrasonic_predictions_recent: + query: | + SELECT + COUNT(*)::float as recent_count + FROM ( + SELECT 1 + FROM public.ultrasonic_plant_predictions + ORDER BY prediction_time DESC + LIMIT 100 + ) sub + master: true + metrics: + - recent_count: + usage: "GAUGE" + description: "Count of recent predictions (last 100)" + +ultrasonic_confidence_by_sensor: + query: | + SELECT + SUBSTRING(REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') FROM '^(MIC-U-\d+)_') as sensor_id, + AVG(confidence)::float as avg_confidence, + COUNT(*)::float as sample_count, + MIN(confidence)::float as min_confidence, + MAX(confidence)::float as max_confidence + FROM public.ultrasonic_plant_predictions + WHERE status = 'Success' + AND confidence IS NOT NULL + AND file ~ 'MIC-U-\d+_\d{8}T\d{6}Z\.wav$' + GROUP BY SUBSTRING(REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') FROM '^(MIC-U-\d+)_') + ORDER BY avg_confidence DESC + master: true + metrics: + - sensor_id: + usage: "LABEL" + description: "Sensor ID extracted from file (e.g., MIC-U-01, MIC-U-02)" + - avg_confidence: + usage: "GAUGE" + description: "Average confidence level per sensor" + - sample_count: + usage: "GAUGE" + description: "Number of successful samples per sensor" + - min_confidence: + usage: "GAUGE" + description: "Minimum confidence per sensor" + - max_confidence: + usage: "GAUGE" + description: "Maximum confidence per sensor" + +ultrasonic_daily_stress_count: + query: | + SELECT + DATE_TRUNC('hour', + TO_TIMESTAMP( + SUBSTRING( + REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') + FROM 'MIC-U-\d+_(\d{8}T\d{6})Z' + ), + 'YYYYMMDD"T"HH24MISS' + ) + ) as event_time, + EXTRACT(EPOCH FROM DATE_TRUNC('hour', + TO_TIMESTAMP( + SUBSTRING( + REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') + FROM 'MIC-U-\d+_(\d{8}T\d{6})Z' + ), + 'YYYYMMDD"T"HH24MISS' + ) + ))::float as date_timestamp, + COUNT(*)::float as event_count + FROM public.ultrasonic_plant_predictions + WHERE predicted_class IN ('Drought_Tomato', 'Pest_Tomato') + AND status = 'Success' + AND TO_TIMESTAMP( + SUBSTRING( + REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') + FROM 'MIC-U-\d+_(\d{8}T\d{6})Z' + ), + 'YYYYMMDD"T"HH24MISS' + ) >= DATE_TRUNC('day', NOW()) + GROUP BY DATE_TRUNC('hour', + TO_TIMESTAMP( + SUBSTRING( + REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') + FROM 'MIC-U-\d+_(\d{8}T\d{6})Z' + ), + 'YYYYMMDD"T"HH24MISS' + ) + ) + ORDER BY date_timestamp DESC + master: true + metrics: + - event_time: + usage: "LABEL" + description: "Hour of stress event extracted from file name" + - date_timestamp: + usage: "GAUGE" + description: "Hour timestamp (Unix epoch)" + - event_count: + usage: "GAUGE" + description: "Number of stress events per hour (Today)" + +ultrasonic_success_rate_by_sensor: + query: | + SELECT + SUBSTRING(REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') FROM '^(MIC-U-\d+)_') as sensor_id, + SUM(CASE WHEN status = 'Success' THEN 1.0 ELSE 0.0 END) / + NULLIF(COUNT(*), 0) * 100.0 as success_rate + FROM public.ultrasonic_plant_predictions + WHERE file ~ 'MIC-U-\d+_\d{8}T\d{6}Z\.wav$' + GROUP BY SUBSTRING(REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') FROM '^(MIC-U-\d+)_') + master: true + metrics: + - sensor_id: + usage: "LABEL" + description: "Sensor ID" + - success_rate: + usage: "GAUGE" + description: "Success rate percentage per sensor" + +ultrasonic_class_distribution_healthy: + query: | + SELECT + COUNT(*) as healthy_count, + COUNT(*) * 100.0 / + NULLIF((SELECT COUNT(*) FROM public.ultrasonic_plant_predictions WHERE status = 'Success'), 0) + as healthy_percentage + FROM public.ultrasonic_plant_predictions + WHERE predicted_class IN ('Healthy_Tomato', 'Healthy_Tobacco', 'Control_Greenhouse', 'Control_Empty') AND status = 'Success' + master: true + metrics: + - healthy_count: + usage: "GAUGE" + description: "Count of healthy plant predictions" + - healthy_percentage: + usage: "GAUGE" + description: "Percentage of healthy plant predictions" + +ultrasonic_class_distribution_stress: + query: | + SELECT + COUNT(*) as stress_count, + COUNT(*) * 100.0 / + NULLIF((SELECT COUNT(*) FROM public.ultrasonic_plant_predictions WHERE status = 'Success'), 0) + as stress_percentage + FROM public.ultrasonic_plant_predictions + WHERE predicted_class IN ('Drought_Tomato', 'Pest_Tomato') AND status = 'Success' + master: true + metrics: + - stress_count: + usage: "GAUGE" + description: "Count of stress plant predictions" + - stress_percentage: + usage: "GAUGE" + description: "Percentage of stress plant predictions" + +ultrasonic_confidence_distribution: + query: | + SELECT + CASE + WHEN confidence >= 0.9 THEN '90-100% (High)' + WHEN confidence >= 0.75 THEN '75-90% (Good)' + WHEN confidence >= 0.6 THEN '60-75% (Medium)' + ELSE '< 60% (Low)' + END as confidence_range, + COUNT(*)::float as count + FROM public.ultrasonic_plant_predictions + WHERE status = 'Success' + GROUP BY confidence_range + ORDER BY confidence_range DESC + master: true + metrics: + - confidence_range: + usage: "LABEL" + description: "Confidence level range" + - count: + usage: "GAUGE" + description: "Number of predictions in confidence range" + +ultrasonic_sensor_activity_timeline: + query: | + SELECT + SUBSTRING(REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') FROM '^(MIC-U-\d+)_') as sensor_id, + TO_CHAR(DATE_TRUNC('day', + TO_TIMESTAMP( + SUBSTRING( + REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') + FROM 'MIC-U-\d+_(\d{8}T\d{6})Z' + ), + 'YYYYMMDD"T"HH24MISS' + ) + ), 'YYYY-MM-DD') as activity_date, + COUNT(*)::float as daily_readings + FROM public.ultrasonic_plant_predictions + WHERE status = 'Success' + AND file ~ 'MIC-U-\d+_\d{8}T\d{6}Z\.wav$' + GROUP BY + SUBSTRING(REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') FROM '^(MIC-U-\d+)_'), + DATE_TRUNC('day', + TO_TIMESTAMP( + SUBSTRING( + REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') + FROM 'MIC-U-\d+_(\d{8}T\d{6})Z' + ), + 'YYYYMMDD"T"HH24MISS' + ) + ) + ORDER BY activity_date DESC, sensor_id + master: true + metrics: + - sensor_id: + usage: "LABEL" + description: "Sensor ID" + - activity_date: + usage: "LABEL" + description: "Activity date extracted from file name" + - daily_readings: + usage: "GAUGE" + description: "Daily reading count per sensor" + +ultrasonic_predictions_per_hour: + query: | + SELECT + EXTRACT(EPOCH FROM DATE_TRUNC('hour', + TO_TIMESTAMP( + SUBSTRING( + REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') + FROM 'MIC-U-\d+_(\d{8}T\d{6})Z' + ), + 'YYYYMMDD"T"HH24MISS' + ) + ))::float as hour_timestamp, + COUNT(*)::float as count + FROM public.ultrasonic_plant_predictions + WHERE TO_TIMESTAMP( + SUBSTRING( + REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') + FROM 'MIC-U-\d+_(\d{8}T\d{6})Z' + ), + 'YYYYMMDD"T"HH24MISS' + ) >= DATE_TRUNC('day', NOW()) + AND file ~ 'MIC-U-\d+_\d{8}T\d{6}Z\.wav$' + GROUP BY DATE_TRUNC('hour', + TO_TIMESTAMP( + SUBSTRING( + REGEXP_REPLACE(file, '^.*/([^/]+)$', '\1') + FROM 'MIC-U-\d+_(\d{8}T\d{6})Z' + ), + 'YYYYMMDD"T"HH24MISS' + ) + ) + ORDER BY hour_timestamp DESC + master: true + metrics: + - hour_timestamp: + usage: "LABEL" + description: "Hour timestamp (Unix epoch) extracted from file name" + - count: + usage: "GAUGE" + description: "Predictions count per hour (Today's Events)" diff --git a/RelDB/graphs/prometheus.yml b/RelDB/graphs/prometheus.yml index 67d79f26c..7ebb44c3c 100644 --- a/RelDB/graphs/prometheus.yml +++ b/RelDB/graphs/prometheus.yml @@ -14,3 +14,8 @@ 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/RelDB/initdb/05_rover_images.sql b/RelDB/initdb/05_rover_images.sql new file mode 100644 index 000000000..93ca57da9 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/.gitattributes b/airflow_bundle/leaf-pipeline/.gitattributes new file mode 100644 index 000000000..ada57005e --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/.gitignore b/airflow_bundle/leaf-pipeline/.gitignore new file mode 100644 index 000000000..b5c271165 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/Dockerfile b/airflow_bundle/leaf-pipeline/Dockerfile new file mode 100644 index 000000000..53af135cf --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/README b/airflow_bundle/leaf-pipeline/README new file mode 100644 index 000000000..2d7d28b57 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/airflow/airflow.cfg b/airflow_bundle/leaf-pipeline/airflow/airflow.cfg new file mode 100644 index 000000000..8f1408c24 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/airflow/dags/configs/config.docker.yaml b/airflow_bundle/leaf-pipeline/airflow/dags/configs/config.docker.yaml new file mode 100644 index 000000000..465a10ca7 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/airflow/dags/leaf_pipeline_dag.py b/airflow_bundle/leaf-pipeline/airflow/dags/leaf_pipeline_dag.py new file mode 100644 index 000000000..f58a61d53 --- /dev/null +++ b/airflow_bundle/leaf-pipeline/airflow/dags/leaf_pipeline_dag.py @@ -0,0 +1,533 @@ +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) }}' +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/airflow_bundle/leaf-pipeline/airflow/webserver_config.py b/airflow_bundle/leaf-pipeline/airflow/webserver_config.py new file mode 100644 index 000000000..3048bb21f --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/dags/configs/disease_monitor.yaml b/airflow_bundle/leaf-pipeline/dags/configs/disease_monitor.yaml new file mode 100644 index 000000000..832d6daf7 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/docker-compose.yml b/airflow_bundle/leaf-pipeline/docker-compose.yml new file mode 100644 index 000000000..5ededec6f --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/.gitignore b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/.gitignore new file mode 100644 index 000000000..ffc9f8e27 --- /dev/null +++ b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/.gitignore @@ -0,0 +1,53 @@ +# ==== 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 + +.pytest_cache/ +.coverage +coverage.xml +htmlcov/ + +server/embed_pb2.py +server/embed_pb2_grpc.py diff --git a/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/.dockerignore b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/.dockerignore new file mode 100644 index 000000000..641f56876 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/.gitignore b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/.gitignore new file mode 100644 index 000000000..b0e9b0028 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/README.md b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/README.md new file mode 100644 index 000000000..b2e46aec5 --- /dev/null +++ b/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/services/sounds/API-development/tests/__init__.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/__init__.py similarity index 100% rename from services/sounds/API-development/tests/__init__.py rename to airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/__init__.py diff --git a/services/sounds/compression/scripts/__init__.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/__init__.py similarity index 100% rename from services/sounds/compression/scripts/__init__.py rename to airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/__init__.py diff --git a/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/requirements.txt b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/requirements.txt new file mode 100644 index 000000000..8df4df0ba --- /dev/null +++ b/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/services/sounds/compression/tests/__init__.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/scripts/__init__.py similarity index 100% rename from services/sounds/compression/tests/__init__.py rename to airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/scripts/__init__.py diff --git a/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/scripts/run_batch.py b/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/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/services/db_api_service/certs/.keep b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/__init__.py similarity index 100% rename from services/db_api_service/certs/.keep rename to airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/__init__.py diff --git a/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/batch_runner.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/batch_runner.py new file mode 100644 index 000000000..56575d206 --- /dev/null +++ b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/batch_runner.py @@ -0,0 +1,514 @@ +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]: + + 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: + 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: + + 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: + + 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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/base.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/cnn_multi_classifier.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/disease_model.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/detectors/train/dictionary.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/config.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/db.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/logging_setup.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/pipeline/utils.py b/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/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/services/sounds/compression/compressed/cat.flac b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/__init__.py similarity index 100% rename from services/sounds/compression/compressed/cat.flac rename to airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/__init__.py diff --git a/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/minio_client.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/storage/minio_sync.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/validator/rules.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/agri_baseline/src/validator/validator.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/docker-compose.yml b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/docker-compose.yml new file mode 100644 index 000000000..18e1cc31c --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/dockerfile b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/dockerfile new file mode 100644 index 000000000..32623dc45 --- /dev/null +++ b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/dockerfile @@ -0,0 +1,46 @@ +FROM mcr.microsoft.com/devcontainers/python:1-3.11-bullseye + +ENV PIP_NO_CACHE_DIR=0 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=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/* + +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 + +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} + +WORKDIR /app +COPY agri_baseline/requirements.txt /app/requirements.txt + +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 + +COPY agri_baseline /app/agri_baseline +COPY models /app/models +COPY tests /app/tests + +ENV PYTHONPATH=/app + +CMD ["python", "agri_baseline/src/batch_runner.py"] diff --git a/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/pytest.ini b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/pytest.ini new file mode 100644 index 000000000..89313dd9b --- /dev/null +++ b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +pythonpath = . +testpaths = tests +addopts = -v diff --git a/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/cnn_binary_classifier.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/dataset_binary.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/dataset_binary.py new file mode 100644 index 000000000..d63bf5208 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/disease.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/disease.py new file mode 100644 index 000000000..653f673ae --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/eval_multi_levels.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/finetune_multi.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/finetune_multi_stage3.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/research/detectors/train/train_binary_multi.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/conftest.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/conftest.py new file mode 100644 index 000000000..486e4fe52 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_batch_runner.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_batch_runner.py new file mode 100644 index 000000000..db88f63bc --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_disease_model.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_disease_model.py new file mode 100644 index 000000000..c2cb78625 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_minio_integration_mock.py b/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/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_run_detectors.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_run_detectors.py new file mode 100644 index 000000000..6e0f13e26 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_utils_local.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_utils_local.py new file mode 100644 index 000000000..ab76fe8cf --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_validator.py b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Detection_Jobs/tests/test_validator.py new file mode 100644 index 000000000..17957c530 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Makefile b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/Makefile new file mode 100644 index 000000000..94e2c9d4c --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/VENDORED_FROM.txt b/airflow_bundle/leaf-pipeline/projects/Detection_Jobs/VENDORED_FROM.txt new file mode 100644 index 000000000..b1786ac32 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/.gitignore b/airflow_bundle/leaf-pipeline/projects/disease-monitor/.gitignore new file mode 100644 index 000000000..4fb10b81c --- /dev/null +++ b/airflow_bundle/leaf-pipeline/projects/disease-monitor/.gitignore @@ -0,0 +1,54 @@ +# ==== 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/ + +server/embed_pb2.py +server/embed_pb2_grpc.py diff --git a/airflow_bundle/leaf-pipeline/projects/disease-monitor/Makefile b/airflow_bundle/leaf-pipeline/projects/disease-monitor/Makefile new file mode 100644 index 000000000..94e2c9d4c --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/VENDORED_FROM.txt b/airflow_bundle/leaf-pipeline/projects/disease-monitor/VENDORED_FROM.txt new file mode 100644 index 000000000..b1786ac32 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/.dockerignore b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/.dockerignore new file mode 100644 index 000000000..9bd273d4f --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/.gitignore b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/.gitignore new file mode 100644 index 000000000..da73fe5e4 --- /dev/null +++ b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/.gitignore @@ -0,0 +1,2 @@ +# Ignore local data +data/ diff --git a/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/Dockerfile b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/Dockerfile new file mode 100644 index 000000000..49d99c76c --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/Dockerfile.local b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/Dockerfile.local new file mode 100644 index 000000000..3622ef88d --- /dev/null +++ b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/Dockerfile.local @@ -0,0 +1,27 @@ +FROM docker.io/library/python@sha256:e0c4fae70d550834a40f6c3e0326e02cfe239c2351d922e1fb1577a3c6ebde02 + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl python3-requests \ + && 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 +ENV PIP_CERT=/etc/ssl/certs/ca-certificates.crt + +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 + +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 . + +COPY configs /app/configs + +ENV PYTHONUNBUFFERED=1 +ENTRYPOINT ["python", "-m", "disease_monitor.cli"] diff --git a/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/README.md b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/README.md new file mode 100644 index 000000000..a0eb9bda6 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/config.docker.yaml b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/config.docker.yaml new file mode 100644 index 000000000..fb46acdfe --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/config.example.yaml b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/config.example.yaml new file mode 100644 index 000000000..6d4d8d699 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/disease_monitor.yaml b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/configs/disease_monitor.yaml new file mode 100644 index 000000000..832d6daf7 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/docker-compose.yml b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/docker-compose.yml new file mode 100644 index 000000000..15a0a0baa --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/pyproject.toml b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/pyproject.toml new file mode 100644 index 000000000..063c0697f --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/requirements.txt b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/requirements.txt new file mode 100644 index 000000000..46a776a5b --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/__init__.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/__init__.py new file mode 100644 index 000000000..a9a2c5b3b --- /dev/null +++ b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/__init__.py @@ -0,0 +1 @@ +__all__ = [] diff --git a/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/alerting.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/alerting.py new file mode 100644 index 000000000..fe3802cba --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/baseline.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/baseline.py new file mode 100644 index 000000000..71e976c08 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/cli.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/cli.py new file mode 100644 index 000000000..74817d4cb --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/config.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/config.py new file mode 100644 index 000000000..a2e0aa83f --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/io.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/io.py new file mode 100644 index 000000000..98b100f77 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/logging_utils.py b/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/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/models.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/models.py new file mode 100644 index 000000000..4796fecd8 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/base.py b/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/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/emailer.py b/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/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/kafka_notifier.py b/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/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/slack.py b/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/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/notifiers/webhook.py b/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/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/rules.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/src/disease_monitor/rules.py new file mode 100644 index 000000000..eeba8ec4c --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/conftest.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/conftest.py new file mode 100644 index 000000000..043dc1712 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_aggregation.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_aggregation.py new file mode 100644 index 000000000..bb0a42558 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_alerting.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_alerting.py new file mode 100644 index 000000000..48126beaf --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_anomaly_rules.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_anomaly_rules.py new file mode 100644 index 000000000..23f595a7a --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_worsening_rules.py b/airflow_bundle/leaf-pipeline/projects/disease-monitor/disease-monitor/tests/test_worsening_rules.py new file mode 100644 index 000000000..e17b34e57 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf spot/05.jpg b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf spot/05.jpg differ diff --git a/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf spot/bacterialspot3_600px.jpg b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf spot/bacterialspot3_600px.jpg differ diff --git a/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf/DSCN3768.JPG.jpg b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf/DSCN3768.JPG.jpg differ diff --git a/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf/IMG_3891.JPG_1492073147.jpg b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Bell_pepper leaf/IMG_3891.JPG_1492073147.jpg differ diff --git a/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Potato leaf late blight/Late-blight-infected-potato-plants_2.jpg b/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/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Potato leaf late blight/blight-on-potato-leaves.jpg b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Potato leaf late blight/blight-on-potato-leaves.jpg differ diff --git a/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Early blight leaf/dscn3175.jpg b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Early blight leaf/dscn3175.jpg differ diff --git a/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Septoria leaf spot/tomato-badleaves.jpg b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Septoria leaf spot/tomato-badleaves.jpg differ diff --git a/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Septoria leaf spot/tomato_septoria_05_zoom.jpg b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato Septoria leaf spot/tomato_septoria_05_zoom.jpg differ diff --git a/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato leaf/late_blight_tomato_leaf4x1200.jpg b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato leaf/late_blight_tomato_leaf4x1200.jpg differ diff --git a/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/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/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato mold leaf/Leaf-mold3.jpg b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato mold leaf/Leaf-mold3.jpg differ diff --git a/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato mold leaf/tomato_plants_1_original.JPG_1407178095.jpg b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/demo_images/10/Tomato mold leaf/tomato_plants_1_original.JPG_1407178095.jpg differ diff --git a/airflow_bundle/leaf-pipeline/projects/leaf-counting/requirements.orig.txt b/airflow_bundle/leaf-pipeline/projects/leaf-counting/requirements.orig.txt new file mode 100644 index 000000000..81958653a --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/requirements.txt b/airflow_bundle/leaf-pipeline/projects/leaf-counting/requirements.txt new file mode 100644 index 000000000..0c27d4902 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/__init__.py b/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/__init__.py new file mode 100644 index 000000000..ee49d4339 --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/common.py b/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/common.py new file mode 100644 index 000000000..ea30f64ea --- /dev/null +++ b/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/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/crop_only.py b/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/crop_only.py new file mode 100644 index 000000000..517481860 --- /dev/null +++ b/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") + 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}" + 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="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/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/detect_only.py b/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/detect_only.py new file mode 100644 index 000000000..9be1da3d5 --- /dev/null +++ b/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/detect_only.py @@ -0,0 +1,139 @@ + +from __future__ import annotations +import json, argparse +from pathlib import Path +from typing import Optional +from datetime import datetime + +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] mini 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/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/minio_io.py b/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/minio_io.py new file mode 100644 index 000000000..999ce7571 --- /dev/null +++ b/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/minio_io.py @@ -0,0 +1,37 @@ +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): + + 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/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/predict_pyramid_wbf.py b/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/predict_pyramid_wbf.py new file mode 100644 index 000000000..33a2f4b8c --- /dev/null +++ b/airflow_bundle/leaf-pipeline/projects/leaf-counting/src/predict_pyramid_wbf.py @@ -0,0 +1,230 @@ + +from __future__ import annotations +import argparse, json +from pathlib import Path +from typing import List, Tuple, Optional +from datetime import datetime + +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]]: + 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="(YY/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/docker-compose.yml b/docker-compose.yml index e0462557d..87cd954c8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,23 @@ - # ========================== # 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 # -------------------------- @@ -22,6 +28,7 @@ volumes: gui_data: minio-hot-data: {} minio-cold-data: {} + contracts: {} # ========================== # Services @@ -38,7 +45,7 @@ services: POSTGRES_USER: missions_user POSTGRES_PASSWORD: pg123 POSTGRES_DB: missions_db - PGHOST: 127.0.0.1 + PGHOST: 0.0.0.0 PGPORT: 5432 PGDATA: /var/lib/postgresql/data WAL_DIR: /var/lib/postgresql/wal_archive @@ -52,7 +59,7 @@ services: - wal_archive:/var/lib/postgresql/wal_archive - backups:/var/lib/postgresql/backups healthcheck: - test: ["CMD", "pg_isready", "-U", "missions_user", "-d", "missions_db"] + test: [ "CMD", "pg_isready", "-U", "missions_user", "-d", "missions_db" ] interval: 10s timeout: 5s retries: 5 @@ -75,34 +82,87 @@ services: - "9187:9187" networks: - ag_cloud + # ------------------------- + # Sound Metrics Service + # ------------------------- - plant_stress: - build: ./services/plant_stress + sound_metrics: + build: + context: ./services/sound_metrics + dockerfile: Dockerfile environment: - - INPUT_DIR=/data/inbox - - MODEL_DIR=/models - - POSTGRES_DSN=postgresql://missions_user:pg123@postgres:5432/missions_db - - PERIOD_DAYS=0 # אפשר לשנות ל-7 לעיבוד "שבוע אחרון" בלבד - - CONFIDENCE_THRESHOLD=0.6 - # - TF_ENABLE_ONEDNN_OPTS=0 # אופציונלי אם תרצי עקביות מספרית - volumes: - - "./services/plant_stress/models:/models:ro" - - "./services/plant_stress/samples:/data/inbox:ro" + - 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: - - postgres - command: ["python", "-u", "/app/app.py"] + - 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 + Connect + Init + # MQTT + Kafka + MQTT-router # ------------------------- kafka: - build: + build: context: ./mqtt_and_kafka/kafka dockerfile: dockerfile container_name: kafka @@ -127,10 +187,7 @@ services: networks: - ag_cloud healthcheck: - test: ["CMD-SHELL", "echo 'Checking Kafka...' && \ - /opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --list >/dev/null 2>&1 || \ - /opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9094 --list >/dev/null 2>&1 || \ - exit 1"] + 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 @@ -138,7 +195,7 @@ services: mosquitto: image: eclipse-mosquitto:2.0 container_name: mosquitto - command: ["mosquitto", "-c", "/mqtt_and_kafka/mosquitto/config/mosquitto.conf"] + command: [ "mosquitto", "-c", "/mqtt_and_kafka/mosquitto/config/mosquitto.conf" ] ports: - "1883:1883" volumes: @@ -149,60 +206,37 @@ services: networks: - ag_cloud healthcheck: - test: ["CMD", "mosquitto_sub", "-h", "localhost", "-p", "1883", "-t", "$$SYS/#", "-C", "1", "-W", "15"] + test: [ "CMD", "mosquitto_sub", "-h", "localhost", "-p", "1883", "-t", "$$SYS/#", "-C", "1", "-W", "15" ] interval: 10s timeout: 5s retries: 12 - connect: - build: - context: ./mqtt_and_kafka - 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: - - ag_cloud - healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:8083/connectors"] - 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/# - init-connector: - image: curlimages/curl:8.7.1 - depends_on: - connect: - condition: service_healthy - volumes: - - ./mqtt_and_kafka/connectors:/connectors - networks: - - ag_cloud - 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.'; - " + - 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 @@ -292,6 +326,15 @@ services: networks: - ag_cloud + pushgateway: + image: prom/pushgateway:v1.8.0 + container_name: pushgateway + ports: + - "9091:9091" + networks: + - ag_cloud + restart: unless-stopped + # -------------------------- # Desktop App # -------------------------- @@ -305,19 +348,24 @@ services: - 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 + - ./GUI/src/vast:/app/src/vast + - ./templates:/app/templates:ro networks: - ag_cloud restart: unless-stopped - # -------------------------- # Large Mosquitto # -------------------------- @@ -343,12 +391,51 @@ services: 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] + - "9001:9000" # HOT S3 + - "9002:9001" # HOT Console + networks: [ ag_cloud ] healthcheck: - test: ["CMD", "curl", "-fsS", "http://localhost:9000/minio/health/ready"] + test: [ "CMD", "curl", "-fsS", "http://localhost:9000/minio/health/ready" ] interval: 3s timeout: 2s retries: 40 @@ -364,11 +451,11 @@ services: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin123 ports: - - "9101:9000" # COLD S3 - - "9102:9001" # COLD Console - networks: [ag_cloud] + - "9101:9000" # COLD S3 + - "9102:9001" # COLD Console + networks: [ ag_cloud ] healthcheck: - test: ["CMD", "curl", "-fsS", "http://localhost:9000/minio/health/ready"] + test: [ "CMD", "curl", "-fsS", "http://localhost:9000/minio/health/ready" ] interval: 3s timeout: 2s retries: 40 @@ -378,6 +465,7 @@ services: 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 @@ -386,7 +474,8 @@ services: condition: service_healthy minio-cold: condition: service_healthy - command: ["/bin/bash","-lc","/entrypoint/init.sh; tail -f /dev/null"] + kafka: + condition: service_healthy environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin123 @@ -395,8 +484,8 @@ services: MC_ALIAS_HOT: hot MC_ALIAS_COLD: cold BUCKET_IMAGERY: imagery - BUCKET_TELEMETRY: telemetry - networks: [ag_cloud] + BUCKET_SOUND: sound + networks: [ ag_cloud ] restart: unless-stopped # -------------------------- @@ -411,7 +500,7 @@ services: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin123 BUCKET_IMAGERY: imagery - BUCKET_TELEMETRY: telemetry + BUCKET_SOUND: sound MQTT_BROKER: large-mosquitto MQTT_PORT: 1885 MQTT_TOPIC: MQTT/imagery/# @@ -438,6 +527,83 @@ services: - 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 @@ -459,32 +625,158 @@ services: - 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=imagery + - 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: ./services/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@host.docker.internal:5432/missions_db + 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 - depends_on: - - postgres notification_api: build: - context: ./services/sounds/API-development/src + context: ./services/API-notifications/src dockerfile: Dockerfile container_name: notification_api environment: @@ -495,4 +787,1142 @@ services: - postgres networks: - ag_cloud - restart: unless-stopped \ No newline at end of file + + 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 + - ./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=sound + 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: 4 + jobmanager.memory.process.size: 1600m + taskmanager.memory.process.size: 2048m + 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 + + # -------------------------- + # 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 + + rover-inference-http: + image: rover-inference-http:latest + container_name: rover-inference-http + environment: + - TEAM=rover + # MinIO + - MINIO_ENDPOINT=minio-hot:9000 + - MINIO_ACCESS_KEY=minioadmin + - MINIO_SECRET_KEY=minioadmin123 + - MINIO_SECURE=0 + # Model runtime + - FENCE_ONNX_PATH=/app/models/fence_hole_detector/weights/best.onnx + - FENCE_CONF=0.30 + - FENCE_ROI=none + - FENCE_VOTE_N=1 + - FENCE_VOTE_M=1 + - FENCE_VOTE_COOLDOWN=0 + networks: [ag_cloud] + # Expose internally on 8004; map to 8019 on host (for curl from host) + ports: + - "8019:8004" + 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=inference.dispatched.camera + - 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 inference.dispatched.camera --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=image.new.fruits + - 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 image.new.fruits --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-dispatcher-rover: + image: agcloud-flink-py:1.18 + container_name: flink-dispatcher-rover + depends_on: + flink-jobmanager: { condition: service_started } + flink-taskmanager: { condition: service_started } + rover-inference-http: { condition: service_started } + networks: [ag_cloud] + environment: + - KAFKA_BOOTSTRAP=kafka:9092 + - INPUT_TOPIC=imagery.new.rover + - TEAM=rover + - HTTP_URL=http://rover-inference-http:8004/infer_json + - DLQ_TOPIC=dlq.inference.http + - GROUP_ID=http-dispatcher-rover + - 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...'; \ + until /opt/flink/bin/flink list --jobmanager flink-jobmanager:8081 >/dev/null 2>&1; do \ + echo 'still waiting...'; sleep 3; \ + done; echo 'JobManager 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_BOOTSTRAP \ + --input-topic $$INPUT_TOPIC \ + --team $$TEAM \ + --http-url $$HTTP_URL \ + --group-id $$GROUP_ID \ + --dlq-topic $$DLQ_TOPIC; \ + tail -f /dev/null" + ] + restart: always + + 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 + + fruit-defect-sink: + build: + context: ./services/fruit_defect_sink + dockerfile: Dockerfile + environment: + - KAFKA_BOOTSTRAP=kafka:9092 + - INPUT_TOPIC=inference.dispatched.fruit + - ALERTS_TOPIC=alerts + - GROUP_ID=fruit-defect-sink + - AUTO_OFFSET_RESET=earliest + - MAX_POLL_INTERVAL_MS=900000 + - SESSION_TIMEOUT_MS=45000 + - HEARTBEAT_INTERVAL_MS=3000 + - PGHOST=postgres + - PGPORT=5432 + - PGDATABASE=missions_db + - PGUSER=missions_user + - PGPASSWORD=pg123 + depends_on: + kafka: { condition: service_healthy } + postgres: { condition: service_healthy } + networks: [ ag_cloud ] + restart: unless-stopped + + fruit_metrics: + build: + context: ./services/fruit_metrics + container_name: fruit_metrics + networks: [ ag_cloud ] + ports: + - "8050:8050" + restart: unless-stopped + + + fruit_alert_generator: + build: + context: ./services/fruit_alert_generator + dockerfile: Dockerfile + container_name: fruit_alert_generator + networks: [ ag_cloud ] + environment: + PGHOST: postgres + PGPORT: 5432 + PGUSER: missions_user + PGPASSWORD: pg123 + PGDATABASE: missions_db + restart: unless-stopped + + + ripeness-exporter: + build: + context: ./services/ripeness_exporter + container_name: ripeness-exporter + restart: unless-stopped + networks: [ ag_cloud ] + depends_on: + - postgres + environment: + - PYTHONUNBUFFERED=1 + ports: + - "9128:9128" + + # -------------------------- + # 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: sensoers_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 + - kafka + ports: + - "8900:8081" + 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: sensors_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: + - "8006: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 + + edge-sensors: + build: + context: ./mqtt_and_kafka/Sensor_edge_device + dockerfile: Dockerfile.edge + container_name: edge-sensors + depends_on: + mosquitto: + condition: service_healthy + mqtt-router: + condition: service_healthy + environment: + - BROKER=mosquitto + - PORT=1883 + - TOPIC=sensors + networks: + - ag_cloud + restart: unless-stopped diff --git a/grafana/dashboards/dashboard-fruit.json b/grafana/dashboards/dashboard-fruit.json new file mode 100644 index 000000000..74a5eb859 --- /dev/null +++ b/grafana/dashboards/dashboard-fruit.json @@ -0,0 +1,161 @@ +{ + "uid": "fruit-metric", + "title": "Fruit Disease Monitoring — Pro Edition", + "timezone": "browser", + "schemaVersion": 36, + "version": 1, + "refresh": "10s", + + "panels": [ + + { + "type": "stat", + "title": "Total Disease Alerts — Since System Start", + "description": "Total number of fruit disease alerts generated since the monitoring system was activated.", + "id": 1, + "datasource": "Prometheus", + "targets": [ + { "expr": "fruit_alerts_total" } + ], + "options": { + "colorMode": "value", + "textMode": "value", + "reduceOptions": { "calcs": ["lastNotNull"] } + }, + "gridPos": { "x": 0, "y": 0, "w": 6, "h": 4 } + }, + + { + "type": "stat", + "title": "Active Disease Events — Real Time", + "description": "Number of live disease alerts currently active in the orchard.", + "id": 2, + "datasource": "Prometheus", + "targets": [ + { "expr": "fruit_alerts_active" } + ], + "options": { + "colorMode": "value", + "textMode": "value" + }, + "gridPos": { "x": 6, "y": 0, "w": 6, "h": 4 } + }, + + { + "type": "stat", + "title": "New Cases (Last 24h)", + "description": "How many new disease alerts appeared during the last 24 hours.", + "id": 3, + "datasource": "Prometheus", + "targets": [ + { "expr": "increase(fruit_alerts_total[24h])" } + ], + "options": { + "colorMode": "value", + "textMode": "value" + }, + "gridPos": { "x": 12, "y": 0, "w": 6, "h": 4 } + }, + + { + "type": "stat", + "title": "Critical Cases (Severity 4)", + "description": "Current number of high-severity alerts that require immediate attention.", + "id": 4, + "datasource": "Prometheus", + "targets": [ + { "expr": "fruit_alerts_by_severity{severity=\"4\"}" } + ], + "options": { + "colorMode": "value", + "textMode": "value" + }, + "gridPos": { "x": 18, "y": 0, "w": 6, "h": 4 } + }, + + { + "type": "timeseries", + "title": "Hourly Alert Growth — How Fast Are New Cases Appearing?", + "description": "Tracks the hour-over-hour increase in disease alerts. A spike indicates rapid disease spread.", + "id": 5, + "datasource": "Prometheus", + "targets": [ + { + "expr": "increase(fruit_alerts_total[1h])", + "legendFormat": "New Alerts Per Hour" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { "lineWidth": 2 } + } + }, + "options": { + "legend": { "displayMode": "list", "placement": "bottom" }, + "tooltip": { "mode": "single" } + }, + "gridPos": { "x": 0, "y": 4, "w": 12, "h": 8 } + }, + + { + "type": "timeseries", + "title": "Weekly Trend — This Week vs Last Week", + "description": "Clear comparison of disease alerts between this week and the same period last week. Helps identify worsening or improving field conditions.", + "id": 6, + "datasource": "Prometheus", + "targets": [ + { + "expr": "fruit_alerts_total", + "legendFormat": "This Week — Total Alerts" + }, + { + "expr": "fruit_alerts_total offset 7d", + "legendFormat": "Last Week — Total Alerts" + } + ], + "fieldConfig": { + "defaults": { + "color": { "mode": "palette-classic" }, + "custom": { "lineWidth": 3 } + } + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "gridPos": { "x": 12, "y": 4, "w": 12, "h": 8 } + }, + + { + "type": "piechart", + "title": "Disease Severity Breakdown", + "description": "Distribution of disease alerts by severity level. Shows the current risk profile of the orchard.", + "id": 7, + "datasource": "Prometheus", + "targets": [ + { "expr": "fruit_alerts_by_severity" } + ], + "gridPos": { "x": 0, "y": 12, "w": 12, "h": 10 } + }, + + { + "type": "barchart", + "title": "Alerts Per Camera — Identifying Hot Zones", + "description": "Which camera detects the most disease activity? Helps locate sick areas in the field.", + "id": 8, + "datasource": "Prometheus", + "targets": [ + { "expr": "fruit_alerts_by_device" } + ], + "gridPos": { "x": 12, "y": 12, "w": 12, "h": 10 } + } + + ] +} diff --git a/grafana/dashboards/dashboard-ripeness.json b/grafana/dashboards/dashboard-ripeness.json new file mode 100644 index 000000000..cd64a9d9d --- /dev/null +++ b/grafana/dashboards/dashboard-ripeness.json @@ -0,0 +1,222 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "id": null, + "links": [], + "liveNow": false, + + "panels": [ + + { + "type": "gauge", + "title": "Weekly Ripeness %", + "description": "Shows the latest weekly ripeness score for the selected device.", + "gridPos": { "x": 0, "y": 0, "w": 8, "h": 6 }, + "datasource": "Prometheus", + "targets": [ + { + "expr": "fruit_ripeness_pct{device=\"$device\"} * 100", + "legendFormat": "Ripeness %", + "interval": "" + } + ], + "options": { + "reduceOptions": { "calcs": ["last"], "values": false }, + "showThresholdLabels": true, + "showThresholdMarkers": true + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "min": 0, + "max": 100, + "thresholds": { + "mode": "percentage", + "steps": [ + { "color": "red", "value": 0 }, + { "color": "yellow", "value": 60 }, + { "color": "green", "value": 75 } + ] + } + } + } + }, + + { + "type": "timeseries", + "title": "Ripeness Trend (12 Weeks)", + "description": "Tracks ripeness vs. threshold over time across 12 weeks.", + "gridPos": { "x": 8, "y": 0, "w": 16, "h": 6 }, + "datasource": "Prometheus", + "targets": [ + { + "expr": "fruit_ripeness_pct{device=\"$device\"} * 100", + "legendFormat": "Ripeness %", + "interval": "" + }, + { + "expr": "fruit_ripeness_threshold * 100", + "legendFormat": "Threshold", + "interval": "" + } + ], + "options": { + "legend": { "displayMode": "table", "placement": "right" } + }, + "fieldConfig": { + "defaults": { "unit": "percent" } + } + }, + + { + "type": "bargauge", + "title": "Ripeness by Device", + "description": "Compares ripeness across devices for the most recent week.", + "gridPos": { "x": 0, "y": 6, "w": 12, "h": 8 }, + "datasource": "Prometheus", + "targets": [ + { + "expr": "fruit_ripeness_pct * 100", + "legendFormat": "{{device}}" + } + ], + "options": { + "displayMode": "gradient", + "orientation": "horizontal", + "reduceOptions": { "calcs": ["last"], "values": false }, + "thresholdsStyle": { "mode": "line" } + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "thresholds": { + "mode": "absolute", + "steps": [ + { "value": 0, "color": "#ff4d4d" }, + { "value": 60, "color": "#ffe066" }, + { "value": 75, "color": "#4caf50" } + ] + } + } + } + }, + + { + "type": "heatmap", + "title": "Ripeness Heatmap (Devices × Weeks)", + "description": "Shows ripeness scores across devices and weeks for easy comparison.", + "gridPos": { "x": 12, "y": 6, "w": 12, "h": 8 }, + "datasource": "Prometheus", + "targets": [ + { + "expr": "fruit_ripeness_pct * 100", + "legendFormat": "{{device}}" + } + ], + "options": { + "color": { "mode": "scheme", "scheme": "interpolateRdYlGn" } + } + }, + + { + "type": "stat", + "title": "Weekly Change", + "description": "Shows the difference between the oldest and latest ripeness values.", + "gridPos": { "x": 0, "y": 14, "w": 8, "h": 4 }, + "datasource": "Prometheus", + "targets": [ + { + "expr": "fruit_ripeness_pct{device=\"$device\"} * 100" + } + ], + "options": { + "reduceOptions": { "calcs": ["first", "last"], "values": false } + } + }, + + { + "type": "table", + "title": "Ripeness Alerts Log", + "description": "Shows the number of alerts, the last alert severity, and active status for each device.", + "gridPos": { "x": 8, "y": 14, "w": 16, "h": 8 }, + "datasource": "Prometheus", + "targets": [ + { "expr": "fruit_ripeness_alerts_total" }, + { "expr": "fruit_ripeness_alert_last_pct" }, + { "expr": "fruit_ripeness_alert_active" } + ], + "transformations": [ + { "id": "merge", "options": {} }, + { + "id": "organize", + "options": { + "renameByName": { + "Value #A": "alerts_total", + "Value #B": "last_pct", + "Value #C": "active" + }, + "indexByName": { + "device": 0, + "alerts_total": 1, + "last_pct": 2, + "active": 3 + } + } + } + ], + "fieldConfig": { + "defaults": { + "custom": { + "align": "center" + } + }, + "overrides": [ + { + "matcher": { "id": "byName", "options": "active" }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-text", + "color": "value", + "thresholds": [ + { "value": 0, "color": "red" }, + { "value": 1, "color": "green" } + ] + } + } + ] + } + ] + } + } + + ], + + "refresh": "30s", + "schemaVersion": 38, + "style": "dark", + "tags": ["fruit", "ripeness"], + "templating": { + "list": [ + { + "type": "query", + "name": "device", + "datasource": "Prometheus", + "query": "label_values(fruit_ripeness_pct, device)", + "includeAll": false, + "multi": false, + "refresh": 2 + } + ] + }, + + "time": { "from": "now-90d", "to": "now" }, + "timezone": "", + "title": "Fruit Ripeness Dashboard", + "version": 2 +} diff --git a/grafana/dashboards/leaf-disease-dashboard.json b/grafana/dashboards/leaf-disease-dashboard.json new file mode 100644 index 000000000..57b12b74e --- /dev/null +++ b/grafana/dashboards/leaf-disease-dashboard.json @@ -0,0 +1,199 @@ +{ + "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, Dynamic Range)", + "gridPos": { "h": 6, "w": 12, "x": 0, "y": 0 }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "targets": [ + { + "refId": "A", + "expr": "sum(sum_over_time(leaf_disease_daily_progression_sick_count{disease_id=\"$disease_id\"}[$__range]))", + "range": true, + "legendFormat": "Total cases" + } + ], + "options": { + "colorMode": "background", + "graphMode": "area", + "orientation": "auto", + "textMode": "valueAndName" + }, + "fieldConfig": { + "defaults": { + "unit": "short", + "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": "bargauge", + "title": "Disease Severity by Device (%)", + "gridPos": { "h": 9, "w": 12, "x": 0, "y": 6 }, + "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": 3, + "type": "piechart", + "title": "All Diseases Distribution", + "gridPos": { "h": 9, "w": 12, "x": 12, "y": 6 }, + "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" } + } + } + }, + { + "id": 4, + "type": "stat", + "title": "Share of Selected Disease (%) — Dynamic Range", + "gridPos": { "h": 6, "w": 12, "x": 12, "y": 0 }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "targets": [ + { + "refId": "A", + "expr": "100 * sum(sum_over_time(leaf_disease_daily_progression_sick_count{disease_id=\"$disease_id\"}[$__range])) / sum(sum_over_time(leaf_disease_daily_progression_sick_count[$__range]))", + "range": true, + "legendFormat": "Share" + } + ], + "options": { + "colorMode": "background", + "graphMode": "area", + "orientation": "auto", + "textMode": "valueAndName" + }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "decimals": 1, + "color": { "mode": "thresholds" }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 10 }, + { "color": "orange", "value": 25 }, + { "color": "red", "value": 50 } + ] + } + } + } + }, + { + "id": 5, + "type": "table", + "title": "Device Statistics", + "gridPos": { "h": 9, "w": 24, "x": 0, "y": 15 }, + "targets": [ + { + "expr": "leaf_reports_by_device_sick_reports", + "refId": "A", + "format": "table" + } + ], + "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" + } + } + } + ] + } + ], + "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 + } + ] + } +} diff --git a/grafana/dashboards/security.json b/grafana/dashboards/security.json new file mode 100644 index 000000000..6e6e0b728 --- /dev/null +++ b/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/grafana/dashboards/sound_dashboard.json b/grafana/dashboards/sound_dashboard.json new file mode 100644 index 000000000..3f78e3ac8 --- /dev/null +++ b/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/grafana/provisioning/datasources/prometheus.yaml b/grafana/provisioning/datasources/prometheus.yaml index 168dd4594..c6229c97c 100644 --- a/grafana/provisioning/datasources/prometheus.yaml +++ b/grafana/provisioning/datasources/prometheus.yaml @@ -1,6 +1,7 @@ apiVersion: 1 datasources: - name: Prometheus + uid: prometheus type: prometheus access: proxy isDefault: true diff --git a/grafana/sensors-to-pushgateway.ps1 b/grafana/sensors-to-pushgateway.ps1 new file mode 100644 index 000000000..2a87b1623 --- /dev/null +++ b/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/grafana/simulate-sound-metrics.ps1 b/grafana/simulate-sound-metrics.ps1 new file mode 100644 index 000000000..769a954bf --- /dev/null +++ b/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/mqtt_and_kafka/Batch_reprocessing/daily_aggregator.py b/mqtt_and_kafka/Batch_reprocessing/daily_aggregator.py index b482705a8..ac91b5bbf 100644 --- a/mqtt_and_kafka/Batch_reprocessing/daily_aggregator.py +++ b/mqtt_and_kafka/Batch_reprocessing/daily_aggregator.py @@ -227,95 +227,3 @@ def read_5min_rows_between(cutoff_start_dt, cutoff_end_dt): 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/mqtt_and_kafka/README.md b/mqtt_and_kafka/README.md index 2bd0e4cdb..ee83d5da5 100644 --- a/mqtt_and_kafka/README.md +++ b/mqtt_and_kafka/README.md @@ -1,4 +1,4 @@ -# AgCloud-Sounds +# AgCloud-telemetrys ## AgCloud – End-to-End MQTT → Kafka (Quickstart) diff --git a/mqtt_and_kafka/Sensor_edge_device/Crop_recommendationV2.csv b/mqtt_and_kafka/Sensor_edge_device/Crop_recommendationV2.csv new file mode 100644 index 000000000..3de7ccc07 --- /dev/null +++ b/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/mqtt_and_kafka/Sensor_edge_device/Dockerfile.edge b/mqtt_and_kafka/Sensor_edge_device/Dockerfile.edge new file mode 100644 index 000000000..7db016068 --- /dev/null +++ b/mqtt_and_kafka/Sensor_edge_device/Dockerfile.edge @@ -0,0 +1,28 @@ +# ---------- 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 + +# ---------- Install dependencies ---------- +RUN pip install --no-cache-dir \ + --trusted-host pypi.org \ + --trusted-host files.pythonhosted.org \ + paho-mqtt pandas + +# ---------- Copy simulation scripts ---------- +COPY ./run_sim.py ./fill_system_with_fake_data.py ./Crop_recommendationV2.csv ./place.csv ./ + +# ---------- Default command ---------- +CMD ["python", "run_sim.py"] diff --git a/mqtt_and_kafka/Sensor_edge_device/fill_system_with_fake_data.py b/mqtt_and_kafka/Sensor_edge_device/fill_system_with_fake_data.py new file mode 100644 index 000000000..4237bfc9f --- /dev/null +++ b/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}", + "sensor_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/mqtt_and_kafka/Sensor_edge_device/place.csv b/mqtt_and_kafka/Sensor_edge_device/place.csv new file mode 100644 index 000000000..8ea9d97a7 --- /dev/null +++ b/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/mqtt_and_kafka/Sensor_edge_device/run_sim.py b/mqtt_and_kafka/Sensor_edge_device/run_sim.py new file mode 100644 index 000000000..74e4bb8b7 --- /dev/null +++ b/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 = "mqtt/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/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/assets/mqtt.png b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/assets/mqtt.png deleted file mode 100644 index d06210321..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/assets/mqtt.png and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/README.md b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/README.md deleted file mode 100644 index 64c1d8bee..000000000 --- a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/README.md +++ /dev/null @@ -1,95 +0,0 @@ -# Introduction - -This project provides connectors for Kafka Connect to read and write data to a MQTT broker. - -# Using with AWS IOT - -```bash -sudo mkdir /opt/aws-iot/ -sudo aws iot create-keys-and-certificate --set-as-active --certificate-pem-outfile /opt/aws-iot/cert.crt --private-key-outfile /opt/aws-iot/private.key --public-key-outfile /opt/aws-iot/public.key --region us-east-1 -sudo openssl pkcs12 -export -in /opt/aws-iot/cert.crt -inkey /opt/aws-iot/private.key -out /opt/aws-iot/p12.keystore -name alias -(type in the export password) - -$ keytool -importkeystore -srckeystore p12.keystore -srcstoretype PKCS12 -srcstorepass -alias alias -deststorepass -destkeypass -destkeystore my.keystore - -$ openssl x509 -outform der -in certificate.pem -out certificate.der -$ keytool -import -alias your-alias -keystore cacerts -file certificate.der -``` - -# Building - -To build the plugin archive, ensure you have [Artifactory credentials](https://github.com/confluentinc/connect-plugins-common#artifactory-credentials-for-building-plugins) set up on the build machine and use the following mvn command: - -``` -$ mvn clean package -... -output truncated -... -[INFO] Building zip: /Users/arjun/Sandbox/clones/kafka-connect-mqtt-wicknicks/target/components/packages/confluentinc-kafka-connect-mqtt-1.0.0-SNAPSHOT.zip -[INFO] -[INFO] --- maven-jar-plugin:3.0.2:test-jar (default) @ kafka-connect-mqtt --- -[INFO] Building jar: /Users/arjun/Sandbox/clones/kafka-connect-mqtt-wicknicks/target/kafka-connect-mqtt-1.0.0-SNAPSHOT-tests.jar -``` - -The location of the plugin archive is shown above in the `target/components/packages` directory. - -## Integration Tests - -To run integration tests from the terminal, start a Docker daemon locally, and run the following command: - -``` -$ mvn clean integration-test - -... -output truncated -... -[INFO] ------------------------------------------------------- -[INFO] T E S T S -[INFO] ------------------------------------------------------- -[INFO] Running io.confluent.connect.mqtt.integration.MqttSourceIntegrationTest -[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 12.817 s - in io.confluent.connect.mqtt.integration.MqttSourceIntegrationTest -[INFO] Running io.confluent.connect.mqtt.integration.MqttSinkIntegrationTest -[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 9.083 s - in io.confluent.connect.mqtt.integration.MqttSinkIntegrationTest -... -``` - -To **skip** running integration tests when running maven commands, enable the `skipIntegrationTests` flag. For example: - -``` -mvn clean install -DskipIntegrationTests -``` - -# Documentation - -## Location -Documentation on the connector is hosted on Confluent's -[docs site](https://docs.confluent.io/current/connect/kafka-connect-mqtt/). - -Source code is located in Confluent's -[docs repo](https://github.com/confluentinc/docs/tree/master/connect/kafka-connect-mqtt). If changes -are made to configuration options for the connector, be sure to generate the RST docs (as described -below) and open a PR against the docs repo to publish those changes! - -## Configs -Documentation on the configurations for each connector can be autotomatically generated via Maven. - -To generate documentation for the sink connector: -```bash -mvn -Pdocs exec:java@sink-config-docs -``` - -To generate documentation for the source connector: -```bash -mvn -Pdocs exec:java@source-config-docs -``` - -# Compatibility Matrix: - -This mqtt connector has been tested against the following versions of AK and Eclipse Mosquitto -Broker: - -| | AK 1.0 | AK 1.1 | AK 2.0 | -| ----------------------------- | ------------------ | ------------- | ------------- | -| **Eclipse Mosquitto v1.4.12** | NOT COMPATIBLE (1) | OK | OK | - -1. The connector needs header support in Connect. diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-EDL-org.eclipse.paho.client.mqttv3-1.2.0.txt b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-EDL-org.eclipse.paho.client.mqttv3-1.2.0.txt deleted file mode 100644 index cf989f145..000000000 --- a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-EDL-org.eclipse.paho.client.mqttv3-1.2.0.txt +++ /dev/null @@ -1,15 +0,0 @@ - -Eclipse Distribution License - v 1.0 - -Copyright (c) 2007, Eclipse Foundation, Inc. and its licensors. - -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - Neither the name of the Eclipse Foundation, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-connect-utils-0.3.140.txt b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-connect-utils-0.3.140.txt deleted file mode 100644 index 412a9e9d8..000000000 --- a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-connect-utils-0.3.140.txt +++ /dev/null @@ -1,422 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Apache License, Version 2.0 - - - - - - - - - - - -
- -
- -
-
-
- Apache Logo -
-
- - - -
-
-
- - -
- The Apache Way - Contribute - ASF Sponsors -
-
-
-
-

Apache License

Version 2.0, January 2004

-http://www.apache.org/licenses/

-

TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

-

1. Definitions.

-

"License" shall mean the terms and conditions for use, reproduction, and -distribution as defined by Sections 1 through 9 of this document.

-

"Licensor" shall mean the copyright owner or entity authorized by the -copyright owner that is granting the License.

-

"Legal Entity" shall mean the union of the acting entity and all other -entities that control, are controlled by, or are under common control with -that entity. For the purposes of this definition, "control" means (i) the -power, direct or indirect, to cause the direction or management of such -entity, whether by contract or otherwise, or (ii) ownership of fifty -percent (50%) or more of the outstanding shares, or (iii) beneficial -ownership of such entity.

-

"You" (or "Your") shall mean an individual or Legal Entity exercising -permissions granted by this License.

-

"Source" form shall mean the preferred form for making modifications, -including but not limited to software source code, documentation source, -and configuration files.

-

"Object" form shall mean any form resulting from mechanical transformation -or translation of a Source form, including but not limited to compiled -object code, generated documentation, and conversions to other media types.

-

"Work" shall mean the work of authorship, whether in Source or Object form, -made available under the License, as indicated by a copyright notice that -is included in or attached to the work (an example is provided in the -Appendix below).

-

"Derivative Works" shall mean any work, whether in Source or Object form, -that is based on (or derived from) the Work and for which the editorial -revisions, annotations, elaborations, or other modifications represent, as -a whole, an original work of authorship. For the purposes of this License, -Derivative Works shall not include works that remain separable from, or -merely link (or bind by name) to the interfaces of, the Work and Derivative -Works thereof.

-

"Contribution" shall mean any work of authorship, including the original -version of the Work and any modifications or additions to that Work or -Derivative Works thereof, that is intentionally submitted to Licensor for -inclusion in the Work by the copyright owner or by an individual or Legal -Entity authorized to submit on behalf of the copyright owner. For the -purposes of this definition, "submitted" means any form of electronic, -verbal, or written communication sent to the Licensor or its -representatives, including but not limited to communication on electronic -mailing lists, source code control systems, and issue tracking systems that -are managed by, or on behalf of, the Licensor for the purpose of discussing -and improving the Work, but excluding communication that is conspicuously -marked or otherwise designated in writing by the copyright owner as "Not a -Contribution."

-

"Contributor" shall mean Licensor and any individual or Legal Entity on -behalf of whom a Contribution has been received by Licensor and -subsequently incorporated within the Work.

-

2. Grant of Copyright License. Subject to the -terms and conditions of this License, each Contributor hereby grants to You -a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable -copyright license to reproduce, prepare Derivative Works of, publicly -display, publicly perform, sublicense, and distribute the Work and such -Derivative Works in Source or Object form.

-

3. Grant of Patent License. Subject to the terms -and conditions of this License, each Contributor hereby grants to You a -perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable -(except as stated in this section) patent license to make, have made, use, -offer to sell, sell, import, and otherwise transfer the Work, where such -license applies only to those patent claims licensable by such Contributor -that are necessarily infringed by their Contribution(s) alone or by -combination of their Contribution(s) with the Work to which such -Contribution(s) was submitted. If You institute patent litigation against -any entity (including a cross-claim or counterclaim in a lawsuit) alleging -that the Work or a Contribution incorporated within the Work constitutes -direct or contributory patent infringement, then any patent licenses -granted to You under this License for that Work shall terminate as of the -date such litigation is filed.

-

4. Redistribution. You may reproduce and -distribute copies of the Work or Derivative Works thereof in any medium, -with or without modifications, and in Source or Object form, provided that -You meet the following conditions:

-
    -
  1. You must give any other recipients of the Work or Derivative Works a -copy of this License; and
  2. - -
  3. You must cause any modified files to carry prominent notices stating -that You changed the files; and
  4. - -
  5. You must retain, in the Source form of any Derivative Works that You -distribute, all copyright, patent, trademark, and attribution notices from -the Source form of the Work, excluding those notices that do not pertain to -any part of the Derivative Works; and
  6. - -
  7. If the Work includes a "NOTICE" text file as part of its distribution, -then any Derivative Works that You distribute must include a readable copy -of the attribution notices contained within such NOTICE file, excluding -those notices that do not pertain to any part of the Derivative Works, in -at least one of the following places: within a NOTICE text file distributed -as part of the Derivative Works; within the Source form or documentation, -if provided along with the Derivative Works; or, within a display generated -by the Derivative Works, if and wherever such third-party notices normally -appear. The contents of the NOTICE file are for informational purposes only -and do not modify the License. You may add Your own attribution notices -within Derivative Works that You distribute, alongside or as an addendum to -the NOTICE text from the Work, provided that such additional attribution -notices cannot be construed as modifying the License. -
    -
    -You may add Your own copyright statement to Your modifications and may -provide additional or different license terms and conditions for use, -reproduction, or distribution of Your modifications, or for any such -Derivative Works as a whole, provided Your use, reproduction, and -distribution of the Work otherwise complies with the conditions stated in -this License. -
  8. - -
- -

5. Submission of Contributions. Unless You -explicitly state otherwise, any Contribution intentionally submitted for -inclusion in the Work by You to the Licensor shall be under the terms and -conditions of this License, without any additional terms or conditions. -Notwithstanding the above, nothing herein shall supersede or modify the -terms of any separate license agreement you may have executed with Licensor -regarding such Contributions.

-

6. Trademarks. This License does not grant -permission to use the trade names, trademarks, service marks, or product -names of the Licensor, except as required for reasonable and customary use -in describing the origin of the Work and reproducing the content of the -NOTICE file.

-

7. Disclaimer of Warranty. Unless required by -applicable law or agreed to in writing, Licensor provides the Work (and -each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT -WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, -without limitation, any warranties or conditions of TITLE, -NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You -are solely responsible for determining the appropriateness of using or -redistributing the Work and assume any risks associated with Your exercise -of permissions under this License.

-

8. Limitation of Liability. In no event and -under no legal theory, whether in tort (including negligence), contract, or -otherwise, unless required by applicable law (such as deliberate and -grossly negligent acts) or agreed to in writing, shall any Contributor be -liable to You for damages, including any direct, indirect, special, -incidental, or consequential damages of any character arising as a result -of this License or out of the use or inability to use the Work (including -but not limited to damages for loss of goodwill, work stoppage, computer -failure or malfunction, or any and all other commercial damages or losses), -even if such Contributor has been advised of the possibility of such -damages.

-

9. Accepting Warranty or Additional Liability. -While redistributing the Work or Derivative Works thereof, You may choose -to offer, and charge a fee for, acceptance of support, warranty, indemnity, -or other liability obligations and/or rights consistent with this License. -However, in accepting such obligations, You may act only on Your own behalf -and on Your sole responsibility, not on behalf of any other Contributor, -and only if You agree to indemnify, defend, and hold each Contributor -harmless for any liability incurred by, or claims asserted against, such -Contributor by reason of your accepting any such warranty or additional -liability.

-

END OF TERMS AND CONDITIONS

-

APPENDIX: How to apply the Apache License to your work

-

To apply the Apache License to your work, attach the following boilerplate -notice, with the fields enclosed by brackets "[]" replaced with your own -identifying information. (Don't include the brackets!) The text should be -enclosed in the appropriate comment syntax for the file format. We also -recommend that a file or class name and description of purpose be included -on the same "printed page" as the copyright notice for easier -identification within third-party archives.

-
Copyright [yyyy] [name of copyright owner]
-
-Licensed 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.
-
- - - - - - - - - - - diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-freemarker-2.3.25-incubating.txt b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-freemarker-2.3.25-incubating.txt deleted file mode 100644 index d64569567..000000000 --- a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-freemarker-2.3.25-incubating.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed 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. diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-guava-20.0.txt b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-guava-20.0.txt deleted file mode 100644 index d64569567..000000000 --- a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-guava-20.0.txt +++ /dev/null @@ -1,202 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed 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. diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-org.eclipse.paho.client.mqttv3-1.2.0.txt b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-org.eclipse.paho.client.mqttv3-1.2.0.txt deleted file mode 100644 index 79e486c3d..000000000 --- a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-org.eclipse.paho.client.mqttv3-1.2.0.txt +++ /dev/null @@ -1,70 +0,0 @@ -Eclipse Public License - v 1.0 - -THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. - -1. DEFINITIONS - -"Contribution" means: - -a) in the case of the initial Contributor, the initial code and documentation distributed under this Agreement, and -b) in the case of each subsequent Contributor: -i) changes to the Program, and -ii) additions to the Program; -where such changes and/or additions to the Program originate from and are distributed by that particular Contributor. A Contribution 'originates' from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include additions to the Program which: (i) are separate modules of software distributed in conjunction with the Program under their own license agreement, and (ii) are not derivative works of the Program. -"Contributor" means any person or entity that distributes the Program. - -"Licensed Patents" mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. - -"Program" means the Contributions distributed in accordance with this Agreement. - -"Recipient" means anyone who receives the Program under this Agreement, including all Contributors. - -2. GRANT OF RIGHTS - -a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, distribute and sublicense the Contribution of such Contributor, if any, and such derivative works, in source code and object code form. -b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in source code and object code form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. -c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. -d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. -3. REQUIREMENTS - -A Contributor may choose to distribute the Program in object code form under its own license agreement, provided that: - -a) it complies with the terms and conditions of this Agreement; and -b) its license agreement: -i) effectively disclaims on behalf of all Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; -ii) effectively excludes on behalf of all Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; -iii) states that any provisions which differ from this Agreement are offered by that Contributor alone and not by any other party; and -iv) states that source code for the Program is available from such Contributor, and informs licensees how to obtain it in a reasonable manner on or through a medium customarily used for software exchange. -When the Program is made available in source code form: - -a) it must be made available under this Agreement; and -b) a copy of this Agreement must be included with each copy of the Program. -Contributors may not remove or alter any copyright notices contained within the Program. - -Each Contributor must identify itself as the originator of its Contribution, if any, in a manner that reasonably allows subsequent Recipients to identify the originator of the Contribution. - -4. COMMERCIAL DISTRIBUTION - -Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor ("Commercial Contributor") hereby agrees to defend and indemnify every other Contributor ("Indemnified Contributor") against any losses, damages and costs (collectively "Losses") arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. - -For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. - -5. NO WARRANTY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement , including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. - -6. DISCLAIMER OF LIABILITY - -EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -7. GENERAL - -If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. - -If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. - -All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. - -Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to distribute the Program (including its Contributions) under the new version. Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. - -This Agreement is governed by the laws of the State of New York and the intellectual property laws of the United States of America. No party to this Agreement will bring a legal action under this Agreement more than one year after the cause of action arose. Each party waives its rights to a jury trial in any resulting litigation. diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-slf4j-1.7.25.txt b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-slf4j-1.7.25.txt deleted file mode 100644 index 315bd4979..000000000 --- a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/doc/licenses/LICENSE-slf4j-1.7.25.txt +++ /dev/null @@ -1,24 +0,0 @@ -Copyright (c) 2004-2017 QOS.ch -All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/etc/connect-avro-docker.properties b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/etc/connect-avro-docker.properties deleted file mode 100644 index 482880401..000000000 --- a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/etc/connect-avro-docker.properties +++ /dev/null @@ -1,31 +0,0 @@ -# -# Copyright [2018 - 2020] Confluent Inc. -# - -# Sample configuration for a standalone Kafka Connect worker that uses Avro serialization and -# integrates the the SchemaConfig Registry. This sample configuration assumes a local installation of -# Confluent Platform with all services running on their default ports. -# Bootstrap Kafka servers. If multiple servers are specified, they should be comma-separated. -bootstrap.servers=kafka:9092 -# The converters specify the format of data in Kafka and how to translate it into Connect data. -# Every Connect user will need to configure these based on the format they want their data in -# when loaded from or stored into Kafka -# key.converter=io.confluent.connect.avro.AvroConverter -key.converter.schema.registry.url=http://schema-registry:8081 -# value.converter=io.confluent.connect.avro.AvroConverter -value.converter.schema.registry.url=http://schema-registry:8081 -# The internal converter used for offsets and config data is configurable and must be specified, -# but most users will always want to use the built-in default. Offset and config data is never -# visible outside of Connect in this format. -internal.key.converter=org.apache.kafka.connect.json.JsonConverter -internal.value.converter=org.apache.kafka.connect.json.JsonConverter -internal.key.converter.schemas.enable=false -internal.value.converter.schemas.enable=false -# Local storage file for offset data -offset.storage.file.filename=/tmp/connect.offsets -# Confuent Control Center Integration -- uncomment these lines to enable Kafka client interceptors -# that will report audit data that can be displayed and analyzed in Confluent Control Center -# producer.interceptor.classes=io.confluent.monitoring.clients.interceptor.MonitoringProducerInterceptor -# consumer.interceptor.classes=io.confluent.monitoring.clients.interceptor.MonitoringConsumerInterceptor -consumer.max.poll.records=100 -plugin.path=target/components/packages/ \ No newline at end of file diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/etc/source-anonymous.properties b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/etc/source-anonymous.properties deleted file mode 100644 index caee1a9f2..000000000 --- a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/etc/source-anonymous.properties +++ /dev/null @@ -1,9 +0,0 @@ -# -# Copyright [2018 - 2020] Confluent Inc. -# - -name=anonymous -tasks.max=1 -connector.class=io.confluent.connect.mqtt.MqttSourceConnector -mqtt.server.uri=tcp://127.0.0.1:32790 -mqtt.topics=foo diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/etc/source-password.properties b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/etc/source-password.properties deleted file mode 100644 index 6e70fe86f..000000000 --- a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/etc/source-password.properties +++ /dev/null @@ -1,11 +0,0 @@ -# -# Copyright [2018 - 2020] Confluent Inc. -# - -name=anonymous -tasks.max=1 -connector.class=io.confluent.connect.mqtt.MqttSourceConnector -mqtt.server.uri=tcp://127.0.0.1:32792 -mqtt.topics=foo -mqtt.username=test -mqtt.password=test \ No newline at end of file diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/checker-qual-3.33.0.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/checker-qual-3.33.0.jar deleted file mode 100644 index 61761fdcb..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/checker-qual-3.33.0.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/confluent-licensing-new-7.5.8-18-ce.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/confluent-licensing-new-7.5.8-18-ce.jar deleted file mode 100644 index 029b90421..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/confluent-licensing-new-7.5.8-18-ce.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/confluent-serializers-new-7.5.8-18-ce.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/confluent-serializers-new-7.5.8-18-ce.jar deleted file mode 100644 index ace3404ee..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/confluent-serializers-new-7.5.8-18-ce.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-ak-non-public-0.24.0.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-ak-non-public-0.24.0.jar deleted file mode 100644 index 77c66c22b..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-ak-non-public-0.24.0.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-licensing-extensions-0.9.35.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-licensing-extensions-0.9.35.jar deleted file mode 100644 index 61f03838b..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-licensing-extensions-0.9.35.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-utils-0.3.3.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-utils-0.3.3.jar deleted file mode 100644 index 4cc6f9c5b..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-utils-0.3.3.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-utils-1.1.0.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-utils-1.1.0.jar deleted file mode 100644 index 8ad2fa21c..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/connect-utils-1.1.0.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/error_prone_annotations-2.18.0.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/error_prone_annotations-2.18.0.jar deleted file mode 100644 index e072fe029..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/error_prone_annotations-2.18.0.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/failureaccess-1.0.1.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/failureaccess-1.0.1.jar deleted file mode 100644 index 9b56dc751..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/failureaccess-1.0.1.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/freemarker-2.3.31.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/freemarker-2.3.31.jar deleted file mode 100644 index 8fb169b21..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/freemarker-2.3.31.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/gson-2.9.0.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/gson-2.9.0.jar deleted file mode 100644 index fb62e0565..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/gson-2.9.0.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/guava-32.1.1-jre.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/guava-32.1.1-jre.jar deleted file mode 100644 index 8f2b3f5d4..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/guava-32.1.1-jre.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/j2objc-annotations-2.8.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/j2objc-annotations-2.8.jar deleted file mode 100644 index 3595c4f9b..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/j2objc-annotations-2.8.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/jose4j-0.9.5.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/jose4j-0.9.5.jar deleted file mode 100644 index ee3b65c10..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/jose4j-0.9.5.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/jsr305-3.0.2.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/jsr305-3.0.2.jar deleted file mode 100644 index 59222d9ca..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/jsr305-3.0.2.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/kafka-connect-mqtt-1.7.6.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/kafka-connect-mqtt-1.7.6.jar deleted file mode 100644 index 427eca9c2..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/kafka-connect-mqtt-1.7.6.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar deleted file mode 100644 index 45832c052..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/log4j-api-2.25.0.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/log4j-api-2.25.0.jar deleted file mode 100644 index 848f393a4..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/log4j-api-2.25.0.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/log4j-core-2.25.0.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/log4j-core-2.25.0.jar deleted file mode 100644 index 8184432ba..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/log4j-core-2.25.0.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/log4j-slf4j2-impl-2.25.0.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/log4j-slf4j2-impl-2.25.0.jar deleted file mode 100644 index 1a398dce0..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/log4j-slf4j2-impl-2.25.0.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/metrics-core-2.2.0.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/metrics-core-2.2.0.jar deleted file mode 100644 index 0f6d1cb0e..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/metrics-core-2.2.0.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/org.eclipse.paho.client.mqttv3-1.2.5.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/org.eclipse.paho.client.mqttv3-1.2.5.jar deleted file mode 100644 index 66f1278e4..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/org.eclipse.paho.client.mqttv3-1.2.5.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/protobuf-java-3.25.5.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/protobuf-java-3.25.5.jar deleted file mode 100644 index d76648859..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/protobuf-java-3.25.5.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/protobuf-java-util-3.25.5.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/protobuf-java-util-3.25.5.jar deleted file mode 100644 index 5f97266a4..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/protobuf-java-util-3.25.5.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/slf4j-api-1.7.36.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/slf4j-api-1.7.36.jar deleted file mode 100644 index 7d3ce68d2..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/slf4j-api-1.7.36.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/value-2.8.2.jar b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/value-2.8.2.jar deleted file mode 100644 index 6f4cec3c5..000000000 Binary files a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/lib/value-2.8.2.jar and /dev/null differ diff --git a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/manifest.json b/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/manifest.json deleted file mode 100644 index 31b258b82..000000000 --- a/mqtt_and_kafka/connect/plugins/confluentinc-kafka-connect-mqtt-1.7.6/manifest.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name" : "kafka-connect-mqtt", - "version" : "1.7.6", - "title" : "Kafka Connect MQTT", - "description" : "A Kafka Connect plugin for sending and receiving data from a Mqtt broker.", - "owner" : { - "username" : "confluentinc", - "name" : "Confluent, Inc." - }, - "support" : { - "summary" : "This connector is a Confluent Commercial Connector and supported by Confluent. The requires purchase of a Confluent Platform subscription, including a license to this Commercial Connector. You can also use this connector for a 30-day trial without an enterprise license key - after 30 days, you need to purchase a subscription. Please contact your Confluent account manager for details.", - "url" : "https://docs.confluent.io/kafka-connect-mqtt/current/index.html" - }, - "tags" : [ "MQTT", "Internet of Things", "IOT" ], - "features" : { - "supported_encodings" : [ "any" ], - "single_message_transforms" : true, - "confluent_control_center_integration" : true, - "kafka_connect_api" : true - }, - "logo" : "assets/mqtt.png", - "documentation_url" : "https://docs.confluent.io/kafka-connect-mqtt/current/index.html", - "docker_image" : { }, - "license" : [ { - "name" : "Confluent Software Evaluation License", - "url" : "https://www.confluent.io/software-evaluation-license" - } ], - "component_types" : [ "source", "sink" ], - "release_date" : "2025-08-09" -} \ No newline at end of file diff --git a/mqtt_and_kafka/connectors/mqtt-source.json b/mqtt_and_kafka/connectors/mqtt-source.json deleted file mode 100644 index 0940826ef..000000000 --- a/mqtt_and_kafka/connectors/mqtt-source.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "mqtt-source", - "config": { - "connector.class": "io.confluent.connect.mqtt.MqttSourceConnector", - "tasks.max": "1", - - "mqtt.server.uri": "tcp://mosquitto:1883", - "mqtt.topics": "mqtt/#", - "mqtt.qos": "0", - "clean.session": "true", - - "kafka.topic": "dev-robot-alerts", - - "key.converter": "org.apache.kafka.connect.storage.StringConverter", - "key.converter.schemas.enable": "false", - - "value.converter": "org.apache.kafka.connect.converters.ByteArrayConverter", - "value.converter.schemas.enable": "false", - - "errors.tolerance": "all", - "errors.log.enable": "true", - - "topic.creation.enable": "false", - "topic.creation.default.replication.factor": "1", - "topic.creation.default.partitions": "1", - - "confluent.topic.bootstrap.servers": "kafka:9092", - "confluent.topic.replication.factor": "1", - "producer.override.bootstrap.servers": "kafka:9092" - } -} diff --git a/mqtt_and_kafka/docker-compose.yml b/mqtt_and_kafka/docker-compose.yml index 50b15c70a..38ea9f513 100644 --- a/mqtt_and_kafka/docker-compose.yml +++ b/mqtt_and_kafka/docker-compose.yml @@ -25,6 +25,7 @@ services: - "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 @@ -104,3 +105,6 @@ networks: mesh: name: agcloud_mesh driver: bridge + minionet: + external: true + name: storage_with_mqtt_minionet diff --git a/mqtt_and_kafka/kafka/kafka-files/app-start.sh b/mqtt_and_kafka/kafka/kafka-files/app-start.sh index 598b736d2..ad6f253a0 100644 --- a/mqtt_and_kafka/kafka/kafka-files/app-start.sh +++ b/mqtt_and_kafka/kafka/kafka-files/app-start.sh @@ -46,4 +46,4 @@ BOOTSTRAP="${BOOTSTRAP}" /opt/bitnami/smoke-test.sh || { } # Stay attached to Kafka process -wait ${KAFKA_PID} +wait ${KAFKA_PID} \ No newline at end of file diff --git a/mqtt_and_kafka/kafka/kafka-files/create-topics.sh b/mqtt_and_kafka/kafka/kafka-files/create-topics.sh index 836edc5e1..8b82a0999 100644 --- a/mqtt_and_kafka/kafka/kafka-files/create-topics.sh +++ b/mqtt_and_kafka/kafka/kafka-files/create-topics.sh @@ -24,19 +24,61 @@ for i in {1..60}; do fi done -# Required topics with 7-day retention 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 - summaries.5m + 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 + inference.dispatched.camera + inference.dispatched.fruit + dlq.inference.http + event_logs_sensors + sensors + sensors_anomalies_modal + aerial_images_keys ) # Idempotent creation with retention.ms diff --git a/mqtt_and_kafka/mqtt-router/Dockerfile b/mqtt_and_kafka/mqtt-router/Dockerfile new file mode 100644 index 000000000..55297f563 --- /dev/null +++ b/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/mqtt_and_kafka/mqtt-router/app.py b/mqtt_and_kafka/mqtt-router/app.py new file mode 100644 index 000000000..3383671aa --- /dev/null +++ b/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/mqtt_and_kafka/mqtt-router/requirements.txt b/mqtt_and_kafka/mqtt-router/requirements.txt new file mode 100644 index 000000000..3d853480c --- /dev/null +++ b/mqtt_and_kafka/mqtt-router/requirements.txt @@ -0,0 +1,2 @@ +paho-mqtt==2.1.0 +confluent-kafka>=2.4 diff --git a/mqtt_and_kafka/simulator/data_simulator.py b/mqtt_and_kafka/simulator/air_simulator.py similarity index 100% rename from mqtt_and_kafka/simulator/data_simulator.py rename to mqtt_and_kafka/simulator/air_simulator.py diff --git a/prometheus/prometheus.yml b/prometheus/prometheus.yml index bf50ee0a5..f74fe9d40 100644 --- a/prometheus/prometheus.yml +++ b/prometheus/prometheus.yml @@ -31,3 +31,28 @@ scrape_configs: 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: 'fruit_metric' + static_configs: + - targets: ['fruit_metrics:8050'] + + - job_name: 'security-models' + static_configs: + - targets: + - 'mega-detector:8007' + - 'animal-classifier:8008' + - 'anomalies-classifier:8011' + - 'mask-classifier:8012' + + - job_name: "fruit_ripeness" + static_configs: + - targets: ["ripeness-exporter:9128"] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 000000000..4e7ce1af4 --- /dev/null +++ b/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/results/benchmarks.csv b/results/benchmarks.csv new file mode 100644 index 000000000..4f54ff285 --- /dev/null +++ b/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/script_to_minio/data_upload_simulator.py b/script_to_minio/data_upload_simulator.py new file mode 100644 index 000000000..be2ad9a4a --- /dev/null +++ b/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/services/sounds/API-development/README.md b/services/API-notifications/README.md similarity index 100% rename from services/sounds/API-development/README.md rename to services/API-notifications/README.md diff --git a/services/sounds/API-development/jest.config.js b/services/API-notifications/jest.config.js similarity index 100% rename from services/sounds/API-development/jest.config.js rename to services/API-notifications/jest.config.js diff --git a/services/sounds/API-development/package-lock.json b/services/API-notifications/package-lock.json similarity index 100% rename from services/sounds/API-development/package-lock.json rename to services/API-notifications/package-lock.json diff --git a/services/sounds/API-development/package.json b/services/API-notifications/package.json similarity index 100% rename from services/sounds/API-development/package.json rename to services/API-notifications/package.json diff --git a/services/sounds/API-development/pytest.ini b/services/API-notifications/pytest.ini similarity index 100% rename from services/sounds/API-development/pytest.ini rename to services/API-notifications/pytest.ini diff --git a/services/sounds/API-development/src/Dockerfile b/services/API-notifications/src/Dockerfile similarity index 100% rename from services/sounds/API-development/src/Dockerfile rename to services/API-notifications/src/Dockerfile diff --git a/services/API-notifications/src/__init__.py b/services/API-notifications/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/API-notifications/src/backend/__init__.py b/services/API-notifications/src/backend/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/sounds/API-development/src/backend/app.py b/services/API-notifications/src/backend/app.py similarity index 100% rename from services/sounds/API-development/src/backend/app.py rename to services/API-notifications/src/backend/app.py diff --git a/services/sounds/API-development/src/backend/requirements.txt b/services/API-notifications/src/backend/requirements.txt similarity index 100% rename from services/sounds/API-development/src/backend/requirements.txt rename to services/API-notifications/src/backend/requirements.txt diff --git a/services/sounds/API-development/src/requirements.txt b/services/API-notifications/src/requirements.txt similarity index 100% rename from services/sounds/API-development/src/requirements.txt rename to services/API-notifications/src/requirements.txt diff --git a/services/sounds/API-development/src/window-client/README.md b/services/API-notifications/src/window-client/README.md similarity index 100% rename from services/sounds/API-development/src/window-client/README.md rename to services/API-notifications/src/window-client/README.md diff --git a/services/sounds/API-development/src/window-client/notification-popup.html b/services/API-notifications/src/window-client/notification-popup.html similarity index 93% rename from services/sounds/API-development/src/window-client/notification-popup.html rename to services/API-notifications/src/window-client/notification-popup.html index 7f6b5a203..6d21ea502 100644 --- a/services/sounds/API-development/src/window-client/notification-popup.html +++ b/services/API-notifications/src/window-client/notification-popup.html @@ -60,13 +60,13 @@

Your Notifications

Add New Notification

- +
diff --git a/services/sounds/API-development/src/window-client/package.json b/services/API-notifications/src/window-client/package.json similarity index 100% rename from services/sounds/API-development/src/window-client/package.json rename to services/API-notifications/src/window-client/package.json diff --git a/services/sounds/API-development/src/window-client/script.js b/services/API-notifications/src/window-client/script.js similarity index 99% rename from services/sounds/API-development/src/window-client/script.js rename to services/API-notifications/src/window-client/script.js index 8bfdac577..66fc45b59 100644 --- a/services/sounds/API-development/src/window-client/script.js +++ b/services/API-notifications/src/window-client/script.js @@ -462,4 +462,4 @@ if (typeof window !== 'undefined') { window.openNotificationPopup = () => { document.getElementById("popupOverlay").style.display = "flex"; }; -} \ No newline at end of file +} diff --git a/services/sounds/API-development/src/window-client/styles.css b/services/API-notifications/src/window-client/styles.css similarity index 100% rename from services/sounds/API-development/src/window-client/styles.css rename to services/API-notifications/src/window-client/styles.css diff --git a/services/API-notifications/tests/__init__.py b/services/API-notifications/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/sounds/API-development/tests/notification-manager.test.js b/services/API-notifications/tests/notification-manager.test.js similarity index 100% rename from services/sounds/API-development/tests/notification-manager.test.js rename to services/API-notifications/tests/notification-manager.test.js diff --git a/services/sounds/API-development/tests/test_app.py b/services/API-notifications/tests/test_app.py similarity index 100% rename from services/sounds/API-development/tests/test_app.py rename to services/API-notifications/tests/test_app.py diff --git a/services/Cross-Sensor System-Level Anomalies/.github/CODEOWNERS b/services/Cross-Sensor System-Level Anomalies/.github/CODEOWNERS new file mode 100644 index 000000000..3371c6fe8 --- /dev/null +++ b/services/Cross-Sensor System-Level Anomalies/.github/CODEOWNERS @@ -0,0 +1 @@ +* @KamaTechOrg @SaraShimon @hadasaGIT @tamarmar diff --git a/services/Cross-Sensor System-Level Anomalies/.gitignore b/services/Cross-Sensor System-Level Anomalies/.gitignore new file mode 100644 index 000000000..80522fd97 --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/Dockerfile.flink b/services/Cross-Sensor System-Level Anomalies/Dockerfile.flink new file mode 100644 index 000000000..f599dbce7 --- /dev/null +++ b/services/Cross-Sensor System-Level Anomalies/Dockerfile.flink @@ -0,0 +1,68 @@ +# Dockerfile.flink +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 apt-get update && apt-get install -y python3 python3-venv python3-pip +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 printf "[global]\ntrusted-host = pypi.org\n files.pythonhosted.org\ncert = /etc/ssl/certs/ca-certificates.crt\n" > /etc/pip.conf +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/services/Cross-Sensor System-Level Anomalies/README.md b/services/Cross-Sensor System-Level Anomalies/README.md new file mode 100644 index 000000000..e12181bc4 --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/check_models.py b/services/Cross-Sensor System-Level Anomalies/check_models.py new file mode 100644 index 000000000..db534a8cf --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/conf/flink-conf.yaml b/services/Cross-Sensor System-Level Anomalies/conf/flink-conf.yaml new file mode 100644 index 000000000..18cf61e32 --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/convert_model.py b/services/Cross-Sensor System-Level Anomalies/convert_model.py new file mode 100644 index 000000000..2cf8e5be4 --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/data/Crop_recommendationV2.csv b/services/Cross-Sensor System-Level Anomalies/data/Crop_recommendationV2.csv new file mode 100644 index 000000000..3de7ccc07 --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/detect_iforest_pca.py b/services/Cross-Sensor System-Level Anomalies/detect_iforest_pca.py new file mode 100644 index 000000000..ff3bd84d5 --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/detect_residuals_and_hybrid.py b/services/Cross-Sensor System-Level Anomalies/detect_residuals_and_hybrid.py new file mode 100644 index 000000000..f34982325 --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/docker-compose.yml b/services/Cross-Sensor System-Level Anomalies/docker-compose.yml new file mode 100644 index 000000000..b2ca00545 --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/dockerfile b/services/Cross-Sensor System-Level Anomalies/dockerfile new file mode 100644 index 000000000..6c15f7432 --- /dev/null +++ b/services/Cross-Sensor System-Level Anomalies/dockerfile @@ -0,0 +1,29 @@ +# Dockerfile +FROM python:3.11-slim + +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 + +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/services/Cross-Sensor System-Level Anomalies/entrypoint.sh b/services/Cross-Sensor System-Level Anomalies/entrypoint.sh new file mode 100644 index 000000000..ddcf5180f --- /dev/null +++ b/services/Cross-Sensor System-Level Anomalies/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e + +if [ "$1" = "jobmanager" ]; then + echo ">>> Waiting for Kafka to be ready..." + + 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/services/Cross-Sensor System-Level Anomalies/flink_job.py b/services/Cross-Sensor System-Level Anomalies/flink_job.py new file mode 100644 index 000000000..0c3e6b6c3 --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/requirements.txt b/services/Cross-Sensor System-Level Anomalies/requirements.txt new file mode 100644 index 000000000..f32d99731 --- /dev/null +++ b/services/Cross-Sensor System-Level Anomalies/requirements.txt @@ -0,0 +1,6 @@ +numpy>=2.0.0 +scipy>=1.11 +pandas>=2.2 +scikit-learn>=1.4 +matplotlib>=3.8 +joblib>=1.4 diff --git a/services/Cross-Sensor System-Level Anomalies/tests/conftest.py b/services/Cross-Sensor System-Level Anomalies/tests/conftest.py new file mode 100644 index 000000000..2ac86fa13 --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/tests/test_detect_iforest_pca.py b/services/Cross-Sensor System-Level Anomalies/tests/test_detect_iforest_pca.py new file mode 100644 index 000000000..869c5da68 --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/tests/test_detect_residuals_and_hybrid.py b/services/Cross-Sensor System-Level Anomalies/tests/test_detect_residuals_and_hybrid.py new file mode 100644 index 000000000..a699f4efc --- /dev/null +++ b/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/services/Cross-Sensor System-Level Anomalies/tests/test_low_anomaly_rate.py b/services/Cross-Sensor System-Level Anomalies/tests/test_low_anomaly_rate.py new file mode 100644 index 000000000..08e5ec6ec --- /dev/null +++ b/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/services/__init__.py b/services/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/air/.gitignore b/services/air/.gitignore new file mode 100644 index 000000000..a2661ad0d --- /dev/null +++ b/services/air/.gitignore @@ -0,0 +1 @@ +certs/ \ No newline at end of file diff --git a/services/air/Dockerfile.flink b/services/air/Dockerfile.flink new file mode 100644 index 000000000..c89ca99ae --- /dev/null +++ b/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/services/air/README.md b/services/air/README.md new file mode 100644 index 000000000..1b32cd6a4 --- /dev/null +++ b/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/services/air/anomaly_detection_api/.gitignore b/services/air/anomaly_detection_api/.gitignore new file mode 100644 index 000000000..12cfcc3f3 --- /dev/null +++ b/services/air/anomaly_detection_api/.gitignore @@ -0,0 +1,2 @@ +certs/ +models/ \ No newline at end of file diff --git a/services/air/anomaly_detection_api/Dockerfile.anomaly b/services/air/anomaly_detection_api/Dockerfile.anomaly new file mode 100644 index 000000000..32a0609de --- /dev/null +++ b/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/services/air/anomaly_detection_api/readme.md b/services/air/anomaly_detection_api/readme.md new file mode 100644 index 000000000..e10e4ad7d --- /dev/null +++ b/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/services/air/anomaly_detection_api/service.py b/services/air/anomaly_detection_api/service.py new file mode 100644 index 000000000..11a4dfb73 --- /dev/null +++ b/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/services/air/job.py b/services/air/job.py new file mode 100644 index 000000000..aa12c2cab --- /dev/null +++ b/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, + "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/services/air/object_detection_api/.gitignore b/services/air/object_detection_api/.gitignore new file mode 100644 index 000000000..33534c66b --- /dev/null +++ b/services/air/object_detection_api/.gitignore @@ -0,0 +1,2 @@ +certs/ +model/ \ No newline at end of file diff --git a/services/air/object_detection_api/Dockerfile.infer b/services/air/object_detection_api/Dockerfile.infer new file mode 100644 index 000000000..2bcf7c91f --- /dev/null +++ b/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/services/air/object_detection_api/README.md b/services/air/object_detection_api/README.md new file mode 100644 index 000000000..78bd5bda6 --- /dev/null +++ b/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/services/air/object_detection_api/app.py b/services/air/object_detection_api/app.py new file mode 100644 index 000000000..686d6edc2 --- /dev/null +++ b/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/services/air/object_detection_api/model_wrapper.py b/services/air/object_detection_api/model_wrapper.py new file mode 100644 index 000000000..1a355887e --- /dev/null +++ b/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/services/air/object_detection_api/requirements.txt b/services/air/object_detection_api/requirements.txt new file mode 100644 index 000000000..630c65412 --- /dev/null +++ b/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/services/air/segmentation_api/.gitignore b/services/air/segmentation_api/.gitignore new file mode 100644 index 000000000..3a8be3108 Binary files /dev/null and b/services/air/segmentation_api/.gitignore differ diff --git a/services/air/segmentation_api/dockerfile.segmentation b/services/air/segmentation_api/dockerfile.segmentation new file mode 100644 index 000000000..b4d2f07e1 --- /dev/null +++ b/services/air/segmentation_api/dockerfile.segmentation @@ -0,0 +1,66 @@ +# ========================================================= +# 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 + +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/services/air/segmentation_api/infer_api.py b/services/air/segmentation_api/infer_api.py new file mode 100644 index 000000000..d3e895bea --- /dev/null +++ b/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/services/air/segmentation_api/oem_palette.py b/services/air/segmentation_api/oem_palette.py new file mode 100644 index 000000000..711b737b0 --- /dev/null +++ b/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/services/alertmanager_service/README.md b/services/alertmanager_service/README.md new file mode 100644 index 000000000..743bef952 --- /dev/null +++ b/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/services/alertmanager_service/compose/alertmanager.yml b/services/alertmanager_service/compose/alertmanager.yml new file mode 100644 index 000000000..84acb4590 --- /dev/null +++ b/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/services/alertmanager_service/compose/docker-compose.yml b/services/alertmanager_service/compose/docker-compose.yml new file mode 100644 index 000000000..91100a5b1 --- /dev/null +++ b/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/services/alertmanager_service/src/Dockerfile b/services/alertmanager_service/src/Dockerfile new file mode 100644 index 000000000..76b1b51db --- /dev/null +++ b/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/services/alertmanager_service/src/alert_service.py b/services/alertmanager_service/src/alert_service.py new file mode 100644 index 000000000..27b6e1978 --- /dev/null +++ b/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/services/alertmanager_service/src/app.py b/services/alertmanager_service/src/app.py new file mode 100644 index 000000000..b9fc3447a --- /dev/null +++ b/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/services/alertmanager_service/src/gateway.py b/services/alertmanager_service/src/gateway.py new file mode 100644 index 000000000..872b3704d --- /dev/null +++ b/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/services/flink_writer_db/flink_writer_db/Dockerfile.flink b/services/alerts_forwarder/Dockerfile.flink similarity index 51% rename from services/flink_writer_db/flink_writer_db/Dockerfile.flink rename to services/alerts_forwarder/Dockerfile.flink index 2ba18670e..11b18c593 100644 --- a/services/flink_writer_db/flink_writer_db/Dockerfile.flink +++ b/services/alerts_forwarder/Dockerfile.flink @@ -4,8 +4,8 @@ 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 +# 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 @@ -14,12 +14,35 @@ 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/* +# Optional extra CA certs (NetFree, etc.) from ./certs +# (If no certs exist, this block just prints a message and continues.) +COPY certs/ /tmp/certs + +RUN set -eux; \ + if [ -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/; \ + chmod 644 /usr/local/share/ca-certificates/*.crt; \ + update-ca-certificates; \ + else \ + echo "No extra CA certs found for alerts_forwarder, skipping."; \ + fi + # 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 +# RUN pip install --upgrade pip certifi && pip install --prefer-binary apache-flink==2.1.0 requests urllib3 +# Install PyFlink (DataStream API) and requests stack +RUN pip install --no-cache-dir \ + --trusted-host pypi.org \ + --trusted-host pypi.python.org \ + --trusted-host files.pythonhosted.org \ + apache-flink==2.1.0 \ + requests \ + urllib3 \ + certifi # Kafka connector jar matching Flink 1.20 (connector v3 series) # Reference: flink-connector-kafka-3.x for Flink 1.20 @@ -27,13 +50,15 @@ RUN curl -fSL https://repo1.maven.org/maven2/org/apache/flink/flink-connector-ka -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 -# App WORKDIR /opt/app -COPY app.py /opt/app/app.py +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 app.py"] +CMD ["bash", "-lc", "python alerts_forwarder.py"] + diff --git a/services/alerts_forwarder/alerts_forwarder.py b/services/alerts_forwarder/alerts_forwarder.py new file mode 100644 index 000000000..537aa5815 --- /dev/null +++ b/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/services/alerts_forwarder/docker-compose.yml b/services/alerts_forwarder/docker-compose.yml new file mode 100644 index 000000000..4f45542b7 --- /dev/null +++ b/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/services/compression/Dockerfile b/services/compression/Dockerfile new file mode 100644 index 000000000..de3ebd8f2 --- /dev/null +++ b/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/services/sounds/compression/README.md b/services/compression/README.md similarity index 100% rename from services/sounds/compression/README.md rename to services/compression/README.md diff --git a/services/compression/compressed/cat.flac b/services/compression/compressed/cat.flac new file mode 100644 index 000000000..e69de29bb diff --git a/services/compression/docker-entrypoint.sh b/services/compression/docker-entrypoint.sh new file mode 100644 index 000000000..997109f08 --- /dev/null +++ b/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/services/sounds/compression/pytest.ini b/services/compression/pytest.ini similarity index 100% rename from services/sounds/compression/pytest.ini rename to services/compression/pytest.ini diff --git a/services/compression/requirements.txt b/services/compression/requirements.txt new file mode 100644 index 000000000..68cfe7aa3 --- /dev/null +++ b/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/services/compression/src/__init__.py b/services/compression/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/compression/src/minio_client.py b/services/compression/src/minio_client.py new file mode 100644 index 000000000..2133784fd --- /dev/null +++ b/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", "sound") + +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/services/compression/src/prototype_lib.py b/services/compression/src/prototype_lib.py new file mode 100644 index 000000000..5d9b6b32d --- /dev/null +++ b/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 = "" + +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/services/compression/src/run_bench.py b/services/compression/src/run_bench.py new file mode 100644 index 000000000..19d39568d --- /dev/null +++ b/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/services/compression/src/run_tiering.sh b/services/compression/src/run_tiering.sh new file mode 100644 index 000000000..2ea1fc7ab --- /dev/null +++ b/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/services/compression/src/tiering_job.py b/services/compression/src/tiering_job.py new file mode 100644 index 000000000..ef42665bf --- /dev/null +++ b/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/services/compression/tests/__init__.py b/services/compression/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/compression/tests/test_prototype_lib.py b/services/compression/tests/test_prototype_lib.py new file mode 100644 index 000000000..5ecaa127d --- /dev/null +++ b/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/services/compression/tests/test_run_bench.py b/services/compression/tests/test_run_bench.py new file mode 100644 index 000000000..3697749ac --- /dev/null +++ b/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/services/compression/tests/test_tiering_job.py b/services/compression/tests/test_tiering_job.py new file mode 100644 index 000000000..6bdb133be --- /dev/null +++ b/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/services/db_api_service/.env.example b/services/db_api_service/.env.example new file mode 100644 index 000000000..a8f7feb5c --- /dev/null +++ b/services/db_api_service/.env.example @@ -0,0 +1,9 @@ +DB_DSN=postgresql+psycopg://missions_user:pg123@host.docker.internal:5432/missions_db + +PORT=8080 + +CONTRACTS_DIR=app/contracts + +ALLOWED_TABLES=["event_logs_sensors","devices","image_new_aerial_connections","sound_new_sounds_connections","sounds_metadata","sounds_ultra_metadata","sound_new_plants_connections","aerial_images_metadata","aerial_image_object_detections","aerial_image_anomaly_detections","aerial_images_complete_metadata","field_polygons","aerial_image_segmentation","image_new_security_connections","alerts"] + +STRICT_UNKNOWN_FIELDS=true diff --git "a/services/db_api_service/\342\200\216.gitignore" b/services/db_api_service/.gitignore similarity index 86% rename from "services/db_api_service/\342\200\216.gitignore" rename to services/db_api_service/.gitignore index 994e44a10..7f5d2fbc0 100644 --- "a/services/db_api_service/\342\200\216.gitignore" +++ b/services/db_api_service/.gitignore @@ -1,40 +1,38 @@ -# Byte-compiled / cache -__pycache__/ -*.py[cod] -*.pyo -*.pyd - -# Virtual environments -.venv/ -env/ -venv/ -.env -.env.* - -# 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 +# 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/services/db_api_service/Dockerfile b/services/db_api_service/Dockerfile index acd1891ca..f10cb2ab4 100644 --- a/services/db_api_service/Dockerfile +++ b/services/db_api_service/Dockerfile @@ -1,57 +1,29 @@ FROM python:3.12-slim + WORKDIR /app -# Build argument -ARG USE_NETFREE=true -# Install basic packages -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl && \ + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential curl ca-certificates && \ rm -rf /var/lib/apt/lists/* -# Copy certificates -COPY certs /app/certs -# Install NetFree certificates -RUN if [ "$USE_NETFREE" = "true" ]; then \ - echo "Configuring NetFree certificates..."; \ - ls -la /app/certs/; \ - if [ -f /app/certs/netfree-ca.crt ]; then \ - echo "Found NetFree certificate, installing..."; \ - cp /app/certs/netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt; \ - chmod 644 /usr/local/share/ca-certificates/netfree-ca.crt; \ - update-ca-certificates; \ - echo "Certificate installed successfully"; \ - else \ - echo "WARNING: No NetFree certificate found at /app/certs/netfree-ca.crt, continuing without it."; \ - fi; \ - fi - -# SSL cert environment variables + +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 \ - CURL_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 -# Upgrade pip with trusted hosts -RUN python -m pip install --no-cache-dir --upgrade pip --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org -# Update certifi with system certificates -RUN pip install --no-cache-dir certifi --trusted-host pypi.org --trusted-host pypi.python.org --trusted-host files.pythonhosted.org && \ - cat /etc/ssl/certs/ca-certificates.crt >> $(python -m certifi) && \ - echo "Updated certifi with system certificates" -COPY requirements.txt . -# add trusted hosts during package installation -RUN pip install --no-cache-dir -r requirements.txt \ - --trusted-host pypi.org \ - --trusted-host pypi.python.org \ - --trusted-host files.pythonhosted.org -COPY app ./app -EXPOSE 8001 -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8001"] - - - - - +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/services/db_api_service/README.md b/services/db_api_service/README.md index cfc0a5b25..254588308 100644 --- a/services/db_api_service/README.md +++ b/services/db_api_service/README.md @@ -2,17 +2,11 @@ A FastAPI microservice for managing image/file metadata in the **AgCloud** platform. -## Quickstart (Dockerfile) +## Quickstart (Docker Compose) Build: ```bash -docker build -t db-api-service:latest ./services/db_api_service -``` - -Run: -**Host access (publish port, for development only):** -```bash -docker run --rm -d --name db-api-service-run -p 8001:8001 --env-file .env db-api-service:latest +docker compose up -d --build ``` Check health: @@ -21,6 +15,18 @@ 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 @@ -87,31 +93,107 @@ Invoke-WebRequest "http://localhost:8001/api/files?limit=2" ` --- -## Networking & Access +## 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 | -**Host access (publish port, for development only):** +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 -docker run --rm -d --name db-api-service-run -p 127.0.0.1:8001:8001 --env-file .env db-api-service:latest +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":{...}} ``` -Use `http://localhost:8001`. Bind to `127.0.0.1` for local-only, or change the host port (e.g. `8081`) to avoid conflicts. -**Inter-container access (same network, no published port required):** +Create — batch (POST /api/tables/{resource}/rows:batch) ```bash -docker network create api_net || true +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} +``` -docker run -d --name db-api --network api_net --env-file .env db-api-service:latest -docker run --rm --network api_net curlimages/curl:8.9.1 curl -s http://db-api:8001/healthz +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} ``` -Both containers must be on the same Docker network to resolve `db-api` by name. ---- +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":[...]} +``` -## Testing readme in /test +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 -- 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. +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/services/db_api_service/app/auth.py b/services/db_api_service/app/auth.py index 3f57556b7..3995f9ee2 100644 --- a/services/db_api_service/app/auth.py +++ b/services/db_api_service/app/auth.py @@ -1,187 +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", - }, - } +# 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/services/db_api_service/app/config.py b/services/db_api_service/app/config.py new file mode 100644 index 000000000..21190041d --- /dev/null +++ b/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/services/db_api_service/app/contracts/Dockerfile b/services/db_api_service/app/contracts/Dockerfile new file mode 100644 index 000000000..ed6d31775 --- /dev/null +++ b/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/services/db_api_service/app/contracts/__init__.py b/services/db_api_service/app/contracts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/db_api_service/app/contracts/loader.py b/services/db_api_service/app/contracts/loader.py new file mode 100644 index 000000000..fb9f3d685 --- /dev/null +++ b/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/services/db_api_service/app/contracts/requirements.txt b/services/db_api_service/app/contracts/requirements.txt new file mode 100644 index 000000000..fac21d5c5 --- /dev/null +++ b/services/db_api_service/app/contracts/requirements.txt @@ -0,0 +1,4 @@ +SQLAlchemy==2.0.36 +pathlib + + diff --git a/services/db_api_service/app/main.py b/services/db_api_service/app/main.py index 5dfd835f9..0a5c6463b 100644 --- a/services/db_api_service/app/main.py +++ b/services/db_api_service/app/main.py @@ -1,24 +1,36 @@ -from fastapi import FastAPI -from app.router import api -from app.auth import router as auth_router -from app.db import engine -from app.models import Base - -app = FastAPI(title="Storage DB API", version="1.1.0") - -@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(api) - - - - - +# 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/services/db_api_service/app/router.py b/services/db_api_service/app/router.py index 053e5504b..f775341ef 100644 --- a/services/db_api_service/app/router.py +++ b/services/db_api_service/app/router.py @@ -1,15 +1,25 @@ -from fastapi import APIRouter, Depends -from app.auth import require_auth -from app.tables.files.router import router as files_router - -api = APIRouter(prefix="/api", tags=["api"], dependencies=[Depends(require_auth)]) - -api.include_router(files_router) - -@api.get("/me") -def me(principal = Depends(require_auth)): - kind, obj = principal - if kind == "user": - return {"type": "user", "id": obj.id, "username": obj.username} - else: - return {"type": "service", "name": obj.name} +# 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/services/db_api_service/app/tables/devices/repo.py b/services/db_api_service/app/tables/devices/repo.py new file mode 100644 index 000000000..ad449d931 --- /dev/null +++ b/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/services/db_api_service/app/tables/devices/router.py b/services/db_api_service/app/tables/devices/router.py new file mode 100644 index 000000000..be950ebf3 --- /dev/null +++ b/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/services/db_api_service/app/tables/devices/schemas.py b/services/db_api_service/app/tables/devices/schemas.py new file mode 100644 index 000000000..f3b4bf2b7 --- /dev/null +++ b/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/services/db_api_service/app/tables/files/repo.py b/services/db_api_service/app/tables/files/repo.py index 62bec86ed..35fd28f1a 100644 --- a/services/db_api_service/app/tables/files/repo.py +++ b/services/db_api_service/app/tables/files/repo.py @@ -1,4 +1,3 @@ - # app/tables/files/repo.py import os import json @@ -23,12 +22,11 @@ def _spool(name: str, payload: Dict[str, Any]): 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 + return obj def upsert_file(payload: Dict[str, Any]) -> None: @@ -36,11 +34,10 @@ def upsert_file(payload: Dict[str, Any]) -> None: _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) @@ -106,7 +103,6 @@ def update_file(bucket: str, object_key: str, updates: Dict[str, Any]) -> bool: params["tile_id"] = updates["tile_id"] if "footprint" in updates: - fp = updates["footprint"] params["footprint"] = (None if not fp else fp) sets.append( @@ -136,12 +132,13 @@ def update_file(bucket: str, object_key: str, updates: Dict[str, Any]) -> bool: 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, content_type, size_bytes, etag, + 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 @@ -154,6 +151,53 @@ def get_file(bucket: str, object_key: str) -> Optional[Dict[str, Any]]: 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 [] @@ -173,7 +217,9 @@ def list_files(bucket: Optional[str], device_id: Optional[str], limit: int) -> L q = text(f""" SELECT - file_id, bucket, object_key, content_type, size_bytes, etag, + 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 @@ -201,3 +247,16 @@ def delete_file(bucket: str, object_key: str) -> bool: 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/services/db_api_service/app/tables/files/router.py b/services/db_api_service/app/tables/files/router.py index 89bef5ed9..7f85e62ba 100644 --- a/services/db_api_service/app/tables/files/router.py +++ b/services/db_api_service/app/tables/files/router.py @@ -1,48 +1,359 @@ - -from typing import Optional -from urllib.parse import unquote +# 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 +import requests router = APIRouter(prefix="/files", tags=["files"]) +# PUBLIC_S3_BASE = os.getenv("PUBLIC_S3_BASE") +PUBLIC_S3_BASE = os.getenv("PUBLIC_S3_BASE", "http://localhost:9001") +MINIO_BASE = "http://minio-hot:9000" + +def _check_if_compressed_in_minio(bucket: str, object_key: str) -> bool: + """ + Check in MinIO if the file is compressed (OPUS format) + Uses direct file path checking via MinIO container + """ + try: + base_key = object_key.replace('.wav', '').replace('.opus', '') + + possible_extensions = ['.opus', '.wav', ''] + + print(f"[DEBUG] Checking MinIO for bucket={bucket}, base={base_key}") + + for ext in possible_extensions: + test_key = f"{base_key}{ext}" + + # MinIO S3 API endpoint format + url = f"{MINIO_BASE}/{bucket}/{test_key}" + + try: + print(f"[DEBUG] HEAD request to: {url}") + + response = requests.get(url, timeout=3, stream=True) + + if response.status_code == 200: + is_opus = test_key.lower().endswith('.opus') + print(f"[DEBUG] ✓ Found: {url} (OPUS={is_opus})") + response.close() + return is_opus + elif response.status_code == 404: + print(f"[DEBUG] Not found (404): {url}") + else: + print(f"[DEBUG] Status {response.status_code}: {url}") + + except requests.exceptions.Timeout: + print(f"[DEBUG] Timeout for: {url}") + continue + except requests.exceptions.ConnectionError as e: + print(f"[DEBUG] Connection error for {url}: {e}") + continue + except Exception as e: + print(f"[DEBUG] Error for {url}: {e}") + continue + + print(f"[DEBUG] ✗ No file found for {bucket}/{object_key}") + return False + + except Exception as e: + print(f"[ERROR] MinIO check failed: {e}") + import traceback + traceback.print_exc() + return False + +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 + @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.put("/{bucket}/{object_key:path}") -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: +@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 + 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 + 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", + "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: + print("===== SQL QUERY =====") + print(query) + print("===== PARAMS =====") + print(params) + + rows = repo.db_query(query, params) + results = [] + + for r in rows: + bucket = r.get("bucket") + object_key = r.get("object_key") + filename = r.get("filename", "") + + url = None + if bucket and object_key: + url = f"{PUBLIC_S3_BASE.rstrip('/')}/{quote(bucket, safe='')}/{quote(object_key, safe='/')}" + + is_compressed = False + if bucket and object_key: + is_compressed = _check_if_compressed_in_minio(bucket, object_key) + print(f"[DEBUG] File {filename}: is_compressed={is_compressed}") + + results.append({ + "file_id": r.get("file_id"), + "filename": filename, + "predicted_label": r.get("head_pred_label"), + "probability": r.get("head_pred_prob"), + "device_id": (filename or "").split("_")[0] if filename else "Unknown", + "url": url, + "is_compressed": is_compressed, + }) + + 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"), + sort_by: Optional[str] = Query("Date (Newest)", description="Sort field"), + limit: int = Query(100, ge=1, le=500), +): + conditions = [] + params: Dict[str, Any] = {} + + 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}%" + + if search: + conditions.append("upp.file ILIKE :search") + params["search"] = f"%{search}%" + + 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_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") + + 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: + bucket = r.get("bucket") + object_key = r.get("object_key") + filename = r.get("file", "") + + url = None + if bucket and object_key: + url = f"{PUBLIC_S3_BASE.rstrip('/')}/{quote(bucket, safe='')}/{quote(object_key, safe='/')}" + + is_compressed = False + if bucket and object_key: + is_compressed = _check_if_compressed_in_minio(bucket, object_key) + + results.append({ + "id": r.get("id"), + "file": filename, + "predicted_class": r.get("predicted_class"), + "confidence": r.get("confidence"), + "watering_status": r.get("watering_status"), + "status": r.get("status"), + "device_id": ((filename or "").split("_")[0] or "Unknown"), + "url": url, + "is_compressed": is_compressed, + }) + + 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 {"status": "ok"} + 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}") +@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 row + return _attach_url_if_possible(row) -@router.get("") -def list_files( - bucket: Optional[str] = None, - device_id: Optional[str] = None, - limit: int = Query(50, ge=1, le=500), -): - - if bucket is not None: - bucket = unquote(bucket) - return repo.list_files(bucket, device_id, limit) -@router.delete("/{bucket}/{object_key:path}") +@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) @@ -50,4 +361,3 @@ def delete_file(bucket: str, object_key: str): if not ok: raise HTTPException(status_code=404, detail="not found") return {"status": "deleted"} - diff --git a/services/db_api_service/app/tables/files/schemas.py b/services/db_api_service/app/tables/files/schemas.py index b75e377f9..e3d43810e 100644 --- a/services/db_api_service/app/tables/files/schemas.py +++ b/services/db_api_service/app/tables/files/schemas.py @@ -1,28 +1,30 @@ -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 - 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 +# 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/services/db_api_service/app/tables/generic/_init_.py b/services/db_api_service/app/tables/generic/_init_.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/db_api_service/app/tables/generic/repo.py b/services/db_api_service/app/tables/generic/repo.py new file mode 100644 index 000000000..0306b5172 --- /dev/null +++ b/services/db_api_service/app/tables/generic/repo.py @@ -0,0 +1,461 @@ +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: + contract = _load_contract(resource) + props = contract.get("properties", {}) + for k, v in filters.items(): + if k not in allowed_cols: + raise ValidationFailed("invalid filter key", {"key": k}) + # Cast filter value to correct type based on contract + prop = props.get(k, {}) + t = (prop.get("type") or "string").lower() + fmt = (prop.get("format") or "").lower() + try: + if t == "integer": + v = int(v) + elif t == "number": + v = float(v) + elif t == "boolean": + if isinstance(v, str): + v = v.lower() in ("true", "1", "yes") + else: + v = bool(v) + # Add more type conversions if needed + except Exception: + raise ValidationFailed(f"Failed to cast filter value for {k} to {t}", {"value": v}) + 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/services/db_api_service/app/tables/generic/router.py b/services/db_api_service/app/tables/generic/router.py new file mode 100644 index 000000000..21696fdc1 --- /dev/null +++ b/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/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/__init__.py b/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/repo.py b/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/repo.py new file mode 100644 index 000000000..4060d1d26 --- /dev/null +++ b/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/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/router.py b/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/router.py new file mode 100644 index 000000000..c0a11db18 --- /dev/null +++ b/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/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/schemas.py b/services/db_api_service/app/tables/ripeness_weekly_rollups_ts/schemas.py new file mode 100644 index 000000000..5fbbbfce4 --- /dev/null +++ b/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/services/db_api_service/app/tables/task_thresholds/_init_.py b/services/db_api_service/app/tables/task_thresholds/_init_.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/db_api_service/app/tables/task_thresholds/repo.py b/services/db_api_service/app/tables/task_thresholds/repo.py new file mode 100644 index 000000000..3105e4c24 --- /dev/null +++ b/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/services/db_api_service/app/tables/task_thresholds/router.py b/services/db_api_service/app/tables/task_thresholds/router.py new file mode 100644 index 000000000..da842e66b --- /dev/null +++ b/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/services/db_api_service/app/tables/task_thresholds/schemas.py b/services/db_api_service/app/tables/task_thresholds/schemas.py new file mode 100644 index 000000000..71e15edc6 --- /dev/null +++ b/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/services/db_api_service/app/tools/__init__.py b/services/db_api_service/app/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/db_api_service/app/tools/generate_contracts.py b/services/db_api_service/app/tools/generate_contracts.py new file mode 100644 index 000000000..343b3c6d0 --- /dev/null +++ b/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/services/db_api_service/docker-compose.yml b/services/db_api_service/docker-compose.yml new file mode 100644 index 000000000..4fbff34eb --- /dev/null +++ b/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/services/db_api_service/requirements.txt b/services/db_api_service/requirements.txt index 798e3c5a4..b37f0a4b6 100644 --- a/services/db_api_service/requirements.txt +++ b/services/db_api_service/requirements.txt @@ -1,11 +1,14 @@ - -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.9.2 +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 +requests>=2.31.0 diff --git a/services/db_api_service/tests/Dockerfile b/services/db_api_service/tests/Dockerfile index 2d368d4d4..308decd88 100644 --- a/services/db_api_service/tests/Dockerfile +++ b/services/db_api_service/tests/Dockerfile @@ -5,8 +5,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates build-essential gcc curl && \ rm -rf /var/lib/apt/lists/* -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 +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 diff --git a/services/fence_hole_detector/weights/best.onnx b/services/fence_hole_detector/weights/best.onnx new file mode 100644 index 000000000..fddf21ac1 Binary files /dev/null and b/services/fence_hole_detector/weights/best.onnx differ diff --git a/services/flink_writer_db/Dockerfile.flink b/services/flink_writer_db/Dockerfile.flink new file mode 100644 index 000000000..bd043ed8e --- /dev/null +++ b/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/services/flink_writer_db/flink_writer_db/README.txt b/services/flink_writer_db/README.txt similarity index 95% rename from services/flink_writer_db/flink_writer_db/README.txt rename to services/flink_writer_db/README.txt index af756000c..6eb24d039 100644 --- a/services/flink_writer_db/flink_writer_db/README.txt +++ b/services/flink_writer_db/README.txt @@ -7,7 +7,7 @@ with the original message body (JSON). ## Quick start -1) Put your `netfree-ca.crt` next to `Dockerfile.flink` (required for HTTPS trust). +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 diff --git a/services/flink_writer_db/app.py b/services/flink_writer_db/app.py new file mode 100644 index 000000000..ff13cf371 --- /dev/null +++ b/services/flink_writer_db/app.py @@ -0,0 +1,235 @@ +import os, json, pathlib, requests, queue, threading +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", 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", flush=True) + return None + +# ------------------------- SHARED HEADERS ------------------------- + +shared_headers = {"Content-Type": "application/json"} +token_value = get_or_bootstrap_token() + +if token_value: + if DB_API_AUTH_MODE == "service": + shared_headers["X-Service-Token"] = token_value + else: + shared_headers["Authorization"] = f"Bearer {token_value}" + +# ------------------------- PER-TOPIC WORKERS ------------------------- + +topic_workers = {} +topic_queues = {} +topic_worker_counts = {} +topic_scale_locks = {} + +MAX_WORKERS_PER_TOPIC = 5 +MIN_WORKERS_PER_TOPIC = 1 +SCALE_UP_THRESHOLD = 8000 +SCALE_DOWN_THRESHOLD = 2000 + +def build_retry_session(headers: dict): + s = requests.Session() + s.headers.update(headers) + + retry_cfg = Retry( + total=5, + backoff_factor=0.5, + status_forcelist=[500, 502, 503, 504] + ) + adapter = HTTPAdapter(max_retries=retry_cfg) + s.mount("http://", adapter) + s.mount("https://", adapter) + s.headers.update(headers) + return s + +def start_topic_worker(topic: str): + if topic not in topic_worker_counts: + topic_worker_counts[topic] = 0 + topic_scale_locks[topic] = threading.Lock() + + if topic not in topic_queues: + topic_queues[topic] = queue.Queue(maxsize=30000) + + q = topic_queues[topic] + + def spawn_worker(): + session = build_retry_session(shared_headers) + base = DB_API_BASE.rstrip("/") + url = f"{base}/api/tables/{topic}" + + def worker_loop(): + while True: + payload = q.get() + try: + r = session.post(url, json=payload, timeout=10) + if not (200 <= r.status_code < 300): + print(f"[DB][{topic}] Error {r.status_code}: {r.text[:150]}") + except Exception as e: + print(f"[DB][{topic}] Exception: {e}") + finally: + q.task_done() + + t = threading.Thread(target=worker_loop, daemon=True) + t.start() + topic_worker_counts[topic] += 1 + + if topic_worker_counts[topic] == 0: + spawn_worker() + + if topic not in topic_workers: + def autoscaler(): + while True: + size = q.qsize() + with topic_scale_locks[topic]: + if size > SCALE_UP_THRESHOLD and topic_worker_counts[topic] < MAX_WORKERS_PER_TOPIC: + spawn_worker() + + if size < SCALE_DOWN_THRESHOLD and topic_worker_counts[topic] > MIN_WORKERS_PER_TOPIC: + topic_worker_counts[topic] = max(topic_worker_counts[topic] - 1, MIN_WORKERS_PER_TOPIC) + + import time + time.sleep(2) + + t = threading.Thread(target=autoscaler, daemon=True) + t.start() + topic_workers[topic] = t + +def enqueue_for_db(topic: str, payload: dict) -> bool: + if DUMMY_DB: + print(f"[DB-DUMMY] {topic}: {json.dumps(payload)[:200]}") + return True + + start_topic_worker(topic) + q = topic_queues[topic] + + try: + q.put_nowait(payload) + return True + except queue.Full: + print(f"[WARN][{topic}] queue full - dropping") + return False + +# ------------------------- MESSAGE HANDLER ------------------------- + +def handle_message(topic: str, raw: str) -> bool: + try: + data = json.loads(raw) + except json.JSONDecodeError: + print(f"[WARN] invalid JSON on topic={topic}") + return False + return enqueue_for_db(topic, data) + +# ------------------------- MAIN FLINK JOB ------------------------- + +def main(): + 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}", 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() diff --git a/services/flink_writer_db/flink_writer_db/docker-compose.yml b/services/flink_writer_db/docker-compose.yml similarity index 78% rename from services/flink_writer_db/flink_writer_db/docker-compose.yml rename to services/flink_writer_db/docker-compose.yml index 525bafeab..376c39b88 100644 --- a/services/flink_writer_db/flink_writer_db/docker-compose.yml +++ b/services/flink_writer_db/docker-compose.yml @@ -9,7 +9,7 @@ services: container_name: flink_writer_db environment: - KAFKA_BROKERS=kafka:9092 - - TOPICS=files + - 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 diff --git a/services/flink_writer_db/flink_writer_db/app.py b/services/flink_writer_db/flink_writer_db/app.py deleted file mode 100644 index 7ef353bf9..000000000 --- a/services/flink_writer_db/flink_writer_db/app.py +++ /dev/null @@ -1,154 +0,0 @@ -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 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", "/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", "files").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/{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"))) - - 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()) - .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() diff --git a/services/fruit-orchestration/Dockerfile.airflow b/services/fruit-orchestration/Dockerfile.airflow new file mode 100644 index 000000000..7f5e87016 --- /dev/null +++ b/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/services/fruit-orchestration/dags/ag_compose_scheduler.py b/services/fruit-orchestration/dags/ag_compose_scheduler.py new file mode 100644 index 000000000..643eda5d5 --- /dev/null +++ b/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/services/fruit-orchestration/docker-compose.yml b/services/fruit-orchestration/docker-compose.yml new file mode 100644 index 000000000..7c1786778 --- /dev/null +++ b/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/services/fruit-orchestration/scripts/run_classifier.sh b/services/fruit-orchestration/scripts/run_classifier.sh new file mode 100644 index 000000000..cf5e93ffe --- /dev/null +++ b/services/fruit-orchestration/scripts/run_classifier.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +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}" + +echo "[run_classifier] processing images under ${IMAGES_DIR} ..." +sleep 1 +echo "[run_classifier] done." + +exit 0 diff --git a/services/fruit-orchestration/scripts/run_ripeness.sh b/services/fruit-orchestration/scripts/run_ripeness.sh new file mode 100644 index 000000000..ae098fd5b --- /dev/null +++ b/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/services/fruit-orchestration/services/classifier/.dockerignore b/services/fruit-orchestration/services/classifier/.dockerignore new file mode 100644 index 000000000..77b0c25d3 --- /dev/null +++ b/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/services/fruit-orchestration/services/classifier/.gitignore b/services/fruit-orchestration/services/classifier/.gitignore new file mode 100644 index 000000000..09568e037 --- /dev/null +++ b/services/fruit-orchestration/services/classifier/.gitignore @@ -0,0 +1,2 @@ +.env +.netfree-ca.crt \ No newline at end of file diff --git a/services/fruit-orchestration/services/classifier/Dockerfile b/services/fruit-orchestration/services/classifier/Dockerfile new file mode 100644 index 000000000..48234e0cc --- /dev/null +++ b/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/services/fruit-orchestration/services/classifier/README.md b/services/fruit-orchestration/services/classifier/README.md new file mode 100644 index 000000000..bc4853e6a --- /dev/null +++ b/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/services/fruit-orchestration/services/classifier/configs/fruit_cls.yaml b/services/fruit-orchestration/services/classifier/configs/fruit_cls.yaml new file mode 100644 index 000000000..ef0495fc2 --- /dev/null +++ b/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/services/fruit-orchestration/services/classifier/docker-compose.yml b/services/fruit-orchestration/services/classifier/docker-compose.yml new file mode 100644 index 000000000..43ae27229 --- /dev/null +++ b/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/services/fruit-orchestration/services/classifier/inference/infer_minio_batch.py b/services/fruit-orchestration/services/classifier/inference/infer_minio_batch.py new file mode 100644 index 000000000..00df42eeb --- /dev/null +++ b/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), + 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/services/fruit-orchestration/services/classifier/inference/service.py b/services/fruit-orchestration/services/classifier/inference/service.py new file mode 100644 index 000000000..5aea706e7 --- /dev/null +++ b/services/fruit-orchestration/services/classifier/inference/service.py @@ -0,0 +1,137 @@ +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/services/fruit-orchestration/services/classifier/inference/utils_infer.py b/services/fruit-orchestration/services/classifier/inference/utils_infer.py new file mode 100644 index 000000000..ef39c5bef --- /dev/null +++ b/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/services/fruit-orchestration/services/classifier/metrics_db/__init__.py b/services/fruit-orchestration/services/classifier/metrics_db/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/fruit-orchestration/services/classifier/metrics_db/db.py b/services/fruit-orchestration/services/classifier/metrics_db/db.py new file mode 100644 index 000000000..5170bb6d0 --- /dev/null +++ b/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/services/fruit-orchestration/services/classifier/models/best.pt b/services/fruit-orchestration/services/classifier/models/best.pt new file mode 100644 index 000000000..81a91795b Binary files /dev/null and b/services/fruit-orchestration/services/classifier/models/best.pt differ diff --git a/services/fruit-orchestration/services/classifier/models/labels.json b/services/fruit-orchestration/services/classifier/models/labels.json new file mode 100644 index 000000000..83b09546f --- /dev/null +++ b/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/services/fruit-orchestration/services/classifier/requirements.txt b/services/fruit-orchestration/services/classifier/requirements.txt new file mode 100644 index 000000000..13d19cd80 --- /dev/null +++ b/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/services/fruit-orchestration/services/fruit_ripeness_alert/Dockerfile b/services/fruit-orchestration/services/fruit_ripeness_alert/Dockerfile new file mode 100644 index 000000000..cc211d8d7 --- /dev/null +++ b/services/fruit-orchestration/services/fruit_ripeness_alert/Dockerfile @@ -0,0 +1,18 @@ +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 + +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/services/fruit-orchestration/services/fruit_ripeness_alert/app.py b/services/fruit-orchestration/services/fruit_ripeness_alert/app.py new file mode 100644 index 000000000..4a3d07217 --- /dev/null +++ b/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/services/fruit-orchestration/services/fruit_ripeness_alert/docker-compose.yml b/services/fruit-orchestration/services/fruit_ripeness_alert/docker-compose.yml new file mode 100644 index 000000000..9fa01c2f4 --- /dev/null +++ b/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/services/fruit-orchestration/services/fruit_ripeness_alert/requirements.txt b/services/fruit-orchestration/services/fruit_ripeness_alert/requirements.txt new file mode 100644 index 000000000..d9e23d1df --- /dev/null +++ b/services/fruit-orchestration/services/fruit_ripeness_alert/requirements.txt @@ -0,0 +1,2 @@ +requests +kafka-python diff --git a/services/fruit-orchestration/services/fruit_ripeness_alert/secret/db_api_token b/services/fruit-orchestration/services/fruit_ripeness_alert/secret/db_api_token new file mode 100644 index 000000000..f5d3b3f0d --- /dev/null +++ b/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/services/fruit-orchestration/services/fruit_ripeness_alert/token_bootstrap.py b/services/fruit-orchestration/services/fruit_ripeness_alert/token_bootstrap.py new file mode 100644 index 000000000..cf3af7531 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/.gitignore b/services/fruit-orchestration/services/ripeness/.gitignore new file mode 100644 index 000000000..17751b717 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/Dockerfile b/services/fruit-orchestration/services/ripeness/Dockerfile new file mode 100644 index 000000000..d700be237 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/README.md b/services/fruit-orchestration/services/ripeness/README.md new file mode 100644 index 000000000..19de71518 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/api/ripeness_api.py b/services/fruit-orchestration/services/ripeness/api/ripeness_api.py new file mode 100644 index 000000000..12fe8f5fb --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/checkpoints/eval/classification_report.txt b/services/fruit-orchestration/services/ripeness/checkpoints/eval/classification_report.txt new file mode 100644 index 000000000..f2e2f5a54 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/checkpoints/eval/metrics.json b/services/fruit-orchestration/services/ripeness/checkpoints/eval/metrics.json new file mode 100644 index 000000000..00bdd6890 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional.pt b/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/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional.pt differ diff --git a/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional_frozen.pt b/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/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional_frozen.pt differ diff --git a/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional_unfrozen.pt b/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/services/fruit-orchestration/services/ripeness/checkpoints/mobilenet_v3_large/best_conditional_unfrozen.pt differ diff --git a/services/fruit-orchestration/services/ripeness/configs/config.yaml b/services/fruit-orchestration/services/ripeness/configs/config.yaml new file mode 100644 index 000000000..bfd6c7863 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/docker-compose.yml b/services/fruit-orchestration/services/ripeness/docker-compose.yml new file mode 100644 index 000000000..6ac2eca11 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/jobs/weekly_ripeness_job.py b/services/fruit-orchestration/services/ripeness/jobs/weekly_ripeness_job.py new file mode 100644 index 000000000..387b7c034 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/model/architecture/mobilenet_v3_large_head.py b/services/fruit-orchestration/services/ripeness/model/architecture/mobilenet_v3_large_head.py new file mode 100644 index 000000000..3457d6953 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/model/data/data_multitask.py b/services/fruit-orchestration/services/ripeness/model/data/data_multitask.py new file mode 100644 index 000000000..9b01959a3 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/model/data/transforms.py b/services/fruit-orchestration/services/ripeness/model/data/transforms.py new file mode 100644 index 000000000..a8f2c4b5b --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/model/training/evaluate_conditional.py b/services/fruit-orchestration/services/ripeness/model/training/evaluate_conditional.py new file mode 100644 index 000000000..e10977e01 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/model/training/train_conditional.py b/services/fruit-orchestration/services/ripeness/model/training/train_conditional.py new file mode 100644 index 000000000..c2bf56357 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/model/training/utils.py b/services/fruit-orchestration/services/ripeness/model/training/utils.py new file mode 100644 index 000000000..23e47edc7 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/requirements.txt b/services/fruit-orchestration/services/ripeness/requirements.txt new file mode 100644 index 000000000..0aa2be591 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/tools/data_prep/ingest_kaggle_multitask.py b/services/fruit-orchestration/services/ripeness/tools/data_prep/ingest_kaggle_multitask.py new file mode 100644 index 000000000..46daeed76 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/tools/data_prep/prepare_from_minio.py b/services/fruit-orchestration/services/ripeness/tools/data_prep/prepare_from_minio.py new file mode 100644 index 000000000..0afb84c54 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/tools/export/export_onnx_conditional.py b/services/fruit-orchestration/services/ripeness/tools/export/export_onnx_conditional.py new file mode 100644 index 000000000..1431ea0e6 --- /dev/null +++ b/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/services/fruit-orchestration/services/ripeness/tools/inference/infer_minio_batch.py b/services/fruit-orchestration/services/ripeness/tools/inference/infer_minio_batch.py new file mode 100644 index 000000000..0206a3791 --- /dev/null +++ b/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/services/fruit_alert_generator/Dockerfile b/services/fruit_alert_generator/Dockerfile new file mode 100644 index 000000000..34f8ecff7 --- /dev/null +++ b/services/fruit_alert_generator/Dockerfile @@ -0,0 +1,6 @@ + +FROM python:3.12-slim +WORKDIR /app +RUN pip install psycopg2-binary +COPY generator.py /app/generator.py +CMD ["python", "generator.py"] diff --git a/services/fruit_alert_generator/README.txt b/services/fruit_alert_generator/README.txt new file mode 100644 index 000000000..25472b151 --- /dev/null +++ b/services/fruit_alert_generator/README.txt @@ -0,0 +1,16 @@ + +Fruit Alert Generator +===================== + +A small service that inserts random fruit alerts into the 'alerts' table every 30 seconds. + +Devices used: +- fruit-camera-01 +- fruit-camera-02 +- fruit-camera-03 + +How to build: + docker compose build fruit_alert_generator + +How to run: + docker compose up -d fruit_alert_generator diff --git a/services/fruit_alert_generator/generator.py b/services/fruit_alert_generator/generator.py new file mode 100644 index 000000000..16db3af98 --- /dev/null +++ b/services/fruit_alert_generator/generator.py @@ -0,0 +1,85 @@ + +import psycopg2 +from datetime import datetime, timedelta, timezone +import uuid, random, json, time, os + +DB_HOST = os.getenv("PGHOST", "postgres") +DB_PORT = os.getenv("PGPORT", "5432") +DB_USER = os.getenv("PGUSER", "missions_user") +DB_PASS = os.getenv("PGPASSWORD", "pg123") +DB_NAME = os.getenv("PGDATABASE", "missions_db") + +def get_conn(): + return psycopg2.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASS, + dbname=DB_NAME + ) + +# updated device list +devices = ["fruit-camera-01", "fruit-camera-02", "fruit-camera-03"] +severities = [1,2,3,4] + +def insert_alert(): + conn = get_conn() + cur = conn.cursor() + + alert_id = str(uuid.uuid4()) + alert_type = "fruit_defect_detected" + device_id = random.choice(devices) + started_at = datetime.now(timezone.utc) + + ended_at = (started_at + timedelta(minutes=random.randint(1,5))) if random.random() < 0.3 else None + severity = random.choice(severities) + confidence = round(random.uniform(0.4, 0.99), 2) + + lat = 31.751 + random.uniform(-0.005, 0.005) + lon = 35.022 + random.uniform(-0.005, 0.005) + image_url = f"https://minio-hot:9000/fake/{alert_id}.jpg" + + meta = {"generator": "fruit_alert_generator"} + + cur.execute( + ''' + INSERT INTO alerts ( + alert_id, alert_type, device_id, started_at, ended_at, + confidence, severity, image_url, lat, lon, area, meta, + created_at, updated_at + ) + VALUES ( + %(alert_id)s, %(alert_type)s, %(device_id)s, %(started_at)s, %(ended_at)s, + %(confidence)s, %(severity)s, %(image_url)s, %(lat)s, %(lon)s, + %(area)s, %(meta)s, now(), now() + ) + ''', + { + "alert_id": alert_id, + "alert_type": alert_type, + "device_id": device_id, + "started_at": started_at, + "ended_at": ended_at, + "confidence": confidence, + "severity": severity, + "image_url": image_url, + "lat": lat, + "lon": lon, + "area": None, + "meta": json.dumps(meta) + } + ) + + conn.commit() + cur.close() + conn.close() + print(f"[GENERATOR] Inserted alert {alert_id} device={device_id} severity={severity}") + +def main(): + print("Fruit Alert Generator started.") + while True: + insert_alert() + time.sleep(30) + +if __name__ == "__main__": + main() diff --git a/services/fruit_classifier/.gitignore b/services/fruit_classifier/.gitignore index 09568e037..b70a32d67 100644 --- a/services/fruit_classifier/.gitignore +++ b/services/fruit_classifier/.gitignore @@ -1,2 +1,2 @@ .env -.netfree-ca.crt \ No newline at end of file +.*.crt \ No newline at end of file diff --git a/services/fruit_classifier/dockerfile b/services/fruit_classifier/dockerfile index 10ab2283d..b523afba3 100644 --- a/services/fruit_classifier/dockerfile +++ b/services/fruit_classifier/dockerfile @@ -18,7 +18,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates rm -rf /var/lib/apt/lists/* # <<< Add: Organization CA certificate >>> -COPY netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt +COPY *.crt /usr/local/share/ca-certificates/ RUN update-ca-certificates ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ @@ -56,7 +56,7 @@ 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 +COPY *.crt /usr/local/share/ca-certificates/ RUN update-ca-certificates # Install dependencies from Stage 1 (including torch) + wheels diff --git a/services/fruit_defect_sink/Dockerfile b/services/fruit_defect_sink/Dockerfile new file mode 100644 index 000000000..c4889df5c --- /dev/null +++ b/services/fruit_defect_sink/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.11-slim +RUN pip install --no-cache-dir confluent-kafka psycopg2-binary +WORKDIR /app +COPY fruit_defect_sink.py ./ +CMD ["python","-u","fruit_defect_sink.py"] diff --git a/services/fruit_defect_sink/fruit_defect_sink.py b/services/fruit_defect_sink/fruit_defect_sink.py new file mode 100644 index 000000000..a6d0108be --- /dev/null +++ b/services/fruit_defect_sink/fruit_defect_sink.py @@ -0,0 +1,400 @@ +#!/usr/bin/env python3 +import os +import sys +import json +import signal +import logging +from datetime import datetime, timezone +from uuid import uuid4 + +from confluent_kafka import Consumer, Producer +import psycopg2 +import psycopg2.extras + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [%(levelname)s] %(message)s", + stream=sys.stdout, +) + +# ========================== +# Config from environment +# ========================== + +KAFKA_BOOTSTRAP_SERVERS = os.getenv("KAFKA_BOOTSTRAP_SERVERS", "kafka:9092") +CONSUME_TOPIC = os.getenv("FRUIT_DISPATCHED_TOPIC", "inference.dispatched.fruit") +ALERTS_TOPIC = os.getenv("ALERTS_TOPIC", "alerts") +KAFKA_GROUP_ID = os.getenv("KAFKA_GROUP_ID", "fruit-defect-sink") + +PG_HOST = os.getenv("PGHOST", "postgres") +PG_PORT = int(os.getenv("PGPORT", "5432")) +PG_DB = os.getenv("PGDATABASE", "missions_db") +PG_USER = os.getenv("PGUSER", "missions_user") +PG_PASSWORD = os.getenv("PGPASSWORD", "pg123") + + +# ========================== +# Postgres helpers +# ========================== + +def get_pg_conn(): + logging.info( + "connecting to Postgres: host=%s db=%s user=%s", + PG_HOST, + PG_DB, + PG_USER, + ) + conn = psycopg2.connect( + host=PG_HOST, + port=PG_PORT, + dbname=PG_DB, + user=PG_USER, + password=PG_PASSWORD, + ) + conn.autocommit = True + return conn + + +INSERT_SQL = """ +INSERT INTO public.fruit_defect_predictions ( + ts, + bucket, + object_key, + image_uri, + label, + score, + confidence, + latency_ms_http, + latency_ms_model, + device_id, + idem_key, + corr_id, + extra +) +VALUES ( + %(ts)s, + %(bucket)s, + %(object_key)s, + %(image_uri)s, + %(label)s, + %(score)s, + %(confidence)s, + %(latency_ms_http)s, + %(latency_ms_model)s, + %(device_id)s, + %(idem_key)s, + %(corr_id)s, + %(extra)s +) +ON CONFLICT (idem_key) DO UPDATE +SET + ts = EXCLUDED.ts, + bucket = EXCLUDED.bucket, + object_key = EXCLUDED.object_key, + image_uri = EXCLUDED.image_uri, + label = EXCLUDED.label, + score = EXCLUDED.score, + confidence = EXCLUDED.confidence, + latency_ms_http = EXCLUDED.latency_ms_http, + latency_ms_model = EXCLUDED.latency_ms_model, + device_id = EXCLUDED.device_id, + corr_id = EXCLUDED.corr_id, + extra = EXCLUDED.extra; +""" + + +# ========================== +# Kafka + processing +# ========================== + +def parse_timestamp(ts_str: str | None) -> datetime: + """ + Try to parse an ISO timestamp from the message. + Falls back to 'now' in UTC if not provided / invalid. + """ + if not ts_str: + return datetime.now(timezone.utc) + try: + # handle "Z" + if ts_str.endswith("Z"): + ts_str = ts_str.replace("Z", "+00:00") + return datetime.fromisoformat(ts_str) + except Exception: + return datetime.now(timezone.utc) + + +def handle_message(envelope: dict, conn, producer: Producer): + """ + Process a single message envelope: upsert to Postgres and maybe send alert. + """ + # basic sanity + if not envelope.get("ok", True): + logging.warning("skipping message with ok=false: %s", envelope) + return + + status = envelope.get("status") + if status and status != 200: + logging.warning("skipping message with non-200 status: %s", envelope) + return + + body_raw = envelope.get("body") + if not body_raw: + logging.warning("no 'body' field in message, skipping") + return + + # Support dict or string JSON + if isinstance(body_raw, dict): + body = body_raw + else: + try: + body = json.loads(body_raw) + except Exception as e: + logging.error("failed to parse body JSON: %s; error=%s", body_raw, e) + return + # Parse result — can be missing (fruit-ok detection) + result = body.get("result", {}) or {} + + event = envelope.get("event", {}) or {} + + # Extract bucket/key from body first + bucket = body.get("bucket") + object_key = body.get("key") + + # If missing — try event (legacy) + if not bucket: + bucket = event.get("bucket") + if not object_key: + object_key = event.get("key") + + # If still missing — try MinIO Key format: "imagery/fruit/tree/...jpg" + minio_key = event.get("Key") + if minio_key and (not bucket or not object_key): + parts = minio_key.split("/", 1) + if len(parts) == 2: + bucket = bucket or parts[0] + object_key = object_key or parts[1] + + image_uri = body.get("image_uri") + if not image_uri and bucket and object_key: + image_uri = f"s3://{bucket}/{object_key}" + + label = result.get("label") or body.get("label") + score = result.get("score") + confidence = result.get("confidence") + + try: + score = float(score) if score is not None else None + except (TypeError, ValueError): + score = None + + try: + confidence = float(confidence) if confidence is not None else None + except (TypeError, ValueError): + confidence = None + + latency_ms_model = result.get("latency_ms_model") or body.get("latency_ms_model") + latency_ms_http = body.get("latency_ms") + + device_id = ( + event.get("device_id") + or body.get("device_id") + or envelope.get("device_id") + ) + + idem_key = body.get("idempotency_key") or envelope.get("event_id") or str(uuid4()) + corr_id = body.get("correlation_id") or envelope.get("event_id") + + ts_str = body.get("timestamp") or envelope.get("timestamp") + ts = parse_timestamp(ts_str) + + extra = { + "envelope": envelope, + "body_parsed": body, + } + + params = { + "ts": ts, + "bucket": bucket, + "object_key": object_key, + "image_uri": image_uri, + "label": label, + "score": score, + "confidence": confidence, + "latency_ms_http": latency_ms_http, + "latency_ms_model": latency_ms_model, + "device_id": device_id, + "idem_key": idem_key, + "corr_id": corr_id, + "extra": json.dumps(extra), + } + + # ========================== + # Insert / upsert to Postgres + # ========================== + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + cur.execute(INSERT_SQL, params) + + logging.info( + "[sink] upserted fruit_defect_predictions idem_key=%s label=%s bucket=%s key=%s", + idem_key, + label, + bucket, + object_key, + ) + + # ========================== + # Send alert if needed + # ========================== + maybe_send_alert( + producer=producer, + label=label, + confidence=confidence, + ts=ts, + device_id=device_id, + bucket=bucket, + object_key=object_key, + image_uri=image_uri, + latency_ms_model=latency_ms_model, + idem_key=idem_key, + ) + + +def maybe_send_alert( + producer: Producer, + label: str | None, + confidence: float | None, + ts: datetime, + device_id: str | None, + bucket: str | None, + object_key: str | None, + image_uri: str | None, + latency_ms_model: float | None, + idem_key: str, +): + """ + Send an alert to the alerts topic if the label indicates a defect. + """ + + if label != "defect": + return + + alert_id = idem_key or str(uuid4()) + + alert = { + # --- Required fields --- + "alert_id": alert_id, + "alert_type": "fruit_defect_detected", + "device_id": device_id or "unknown-device", + "started_at": ts.astimezone(timezone.utc).isoformat(), + + # --- Optional / dynamic fields --- + "ended_at": None, + "confidence": confidence, + # "severity": 3, + # "area": None, + # "lat": None, + # "lon": None, + "image_url": image_uri, + # "vod": None, + # "hls": None, + "meta": { + "bucket": bucket, + "object_key": object_key, + "latency_ms_model": latency_ms_model, + "alert_source": "fruit_defect_sink", + }, + } + + payload = json.dumps(alert).encode("utf-8") + producer.produce(ALERTS_TOPIC, payload) + producer.flush() + + logging.info( + "[sink] sent alert to topic '%s' alert_id=%s device_id=%s label=%s", + ALERTS_TOPIC, + alert_id, + device_id, + label, + ) + + +# ========================== +# Main +# ========================== + +running = True + + +def _handle_sig(signum, frame): + global running + logging.info("received signal %s, shutting down...", signum) + running = False + + +def main(): + global running + + signal.signal(signal.SIGINT, _handle_sig) + signal.signal(signal.SIGTERM, _handle_sig) + + consumer_conf = { + "bootstrap.servers": KAFKA_BOOTSTRAP_SERVERS, + "group.id": KAFKA_GROUP_ID, + "auto.offset.reset": "earliest", + "enable.auto.commit": True, + } + + consumer = Consumer(consumer_conf) + producer = Producer({"bootstrap.servers": KAFKA_BOOTSTRAP_SERVERS}) + + conn = get_pg_conn() + + logging.info( + "[sink] listening on %s → Postgres.fruit_defect_predictions + alerts", + CONSUME_TOPIC, + ) + consumer.subscribe([CONSUME_TOPIC]) + + try: + while running: + msg = consumer.poll(1.0) + if msg is None: + continue + + if msg.error(): + logging.error("Kafka error: %s", msg.error()) + continue + + try: + envelope = json.loads(msg.value().decode("utf-8")) + except Exception as e: + logging.error("failed to decode message value: %s", e) + continue + + try: + handle_message(envelope, conn, producer) + except psycopg2.Error as e: + logging.error("Postgres error: %s", e) + try: + conn.close() + except Exception: + pass + conn = get_pg_conn() + except Exception as e: + logging.exception("processing error: %s", e) + + finally: + logging.info("closing consumer and postgres connection...") + try: + consumer.close() + except Exception: + pass + try: + conn.close() + except Exception: + pass + + +if __name__ == "__main__": + main() + diff --git a/services/fruit_metrics/Dockerfile b/services/fruit_metrics/Dockerfile new file mode 100644 index 000000000..cda6f0bd6 --- /dev/null +++ b/services/fruit_metrics/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY fruit_metrics.py . + +EXPOSE 8050 +CMD ["uvicorn", "fruit_metrics:app", "--host", "0.0.0.0", "--port", "8050"] diff --git a/services/fruit_metrics/fruit_metrics.py b/services/fruit_metrics/fruit_metrics.py new file mode 100644 index 000000000..81e0886a7 --- /dev/null +++ b/services/fruit_metrics/fruit_metrics.py @@ -0,0 +1,106 @@ +from fastapi import FastAPI +from starlette.responses import Response +from prometheus_client import Gauge, CollectorRegistry, generate_latest, CONTENT_TYPE_LATEST +import psycopg2 +import psycopg2.extras +import threading +import time +import os + +REFRESH_EVERY = 10 # seconds + +# ───────────────────────────────────────────── +# Connect to PostgreSQL directly +# ───────────────────────────────────────────── +def get_db_connection(): + return psycopg2.connect( + host="postgres", + port=5432, + user="missions_user", + password="pg123", + dbname="missions_db" + ) + +# ───────────────────────────────────────────── +# Create Prometheus registry +# ───────────────────────────────────────────── +reg = CollectorRegistry() + +g_total = Gauge("fruit_alerts_total", "Total fruit alerts", registry=reg) +g_active = Gauge("fruit_alerts_active", "Active fruit alerts", registry=reg) +g_by_type = Gauge("fruit_alerts_by_type", "Alerts by type", ["type"], registry=reg) +g_by_severity = Gauge("fruit_alerts_by_severity", "Alerts by severity", ["severity"], registry=reg) +g_by_device = Gauge("fruit_alerts_by_device", "Alerts by device", ["device"], registry=reg) + +# ───────────────────────────────────────────── +# Query the database +# ───────────────────────────────────────────── +def fetch_alerts(): + try: + conn = get_db_connection() + cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + cur.execute("SELECT * FROM alerts") + rows = cur.fetchall() + cur.close() + conn.close() + return rows + except Exception as e: + print("DB ERROR:", e) + return [] + +# ───────────────────────────────────────────── +# Metrics refresher loop +# ───────────────────────────────────────────── +def refresh_loop(): + while True: + alerts = fetch_alerts() + + types = {} + severities = {} + devices = {} + + total = len(alerts) + active = 0 + + for a in alerts: + # count active alerts + if a.get("ended_at") is None: + active += 1 + + # count by type + t = a.get("alert_type") or "unknown" + types[t] = types.get(t, 0) + 1 + + # count by severity + sev = str(a.get("severity") or 1) + severities[sev] = severities.get(sev, 0) + 1 + + # count by device + dev = a.get("device_id") or "unknown" + devices[dev] = devices.get(dev, 0) + 1 + + # update gauges + g_total.set(total) + g_active.set(active) + + for t, count in types.items(): + g_by_type.labels(t).set(count) + + for sev, count in severities.items(): + g_by_severity.labels(sev).set(count) + + for dev, count in devices.items(): + g_by_device.labels(dev).set(count) + + time.sleep(REFRESH_EVERY) + +threading.Thread(target=refresh_loop, daemon=True).start() + +# ───────────────────────────────────────────── +# FastAPI endpoint for Prometheus +# ───────────────────────────────────────────── +app = FastAPI() + +@app.get("/metrics") +def metrics(): + return Response(generate_latest(reg), media_type=CONTENT_TYPE_LATEST) diff --git a/services/fruit_metrics/requirements.txt b/services/fruit_metrics/requirements.txt new file mode 100644 index 000000000..a45218664 --- /dev/null +++ b/services/fruit_metrics/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +prometheus_client +starlette +psycopg2-binary diff --git a/services/fruit_ripeness_alert/Dockerfile b/services/fruit_ripeness_alert/Dockerfile new file mode 100644 index 000000000..0603b1cba --- /dev/null +++ b/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/services/fruit_ripeness_alert/app.py b/services/fruit_ripeness_alert/app.py new file mode 100644 index 000000000..eff4c84c7 --- /dev/null +++ b/services/fruit_ripeness_alert/app.py @@ -0,0 +1,121 @@ +#!/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): + 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): + + 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/services/fruit_ripeness_alert/docker-compose.yml b/services/fruit_ripeness_alert/docker-compose.yml new file mode 100644 index 000000000..7a6966ee9 --- /dev/null +++ b/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/services/fruit_ripeness_alert/requirements.txt b/services/fruit_ripeness_alert/requirements.txt new file mode 100644 index 000000000..d9e23d1df --- /dev/null +++ b/services/fruit_ripeness_alert/requirements.txt @@ -0,0 +1,2 @@ +requests +kafka-python diff --git a/services/fruit_ripeness_alert/token_bootstrap.py b/services/fruit_ripeness_alert/token_bootstrap.py new file mode 100644 index 000000000..a1dee593b --- /dev/null +++ b/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/services/image-linker/Dockerfile.flink b/services/image-linker/Dockerfile.flink new file mode 100644 index 000000000..3e9a58b41 --- /dev/null +++ b/services/image-linker/Dockerfile.flink @@ -0,0 +1,53 @@ +# syntax=docker/dockerfile:1 +# Base Flink image +FROM flink:1.19.3-scala_2.12-java11 + +USER root + +# Optional local CA (NetFree or others) - works with or without certs +COPY certs/ /tmp/certs + +RUN if [ -n "$(ls /tmp/certs/*.crt 2>/dev/null)" ]; then \ + echo "Configuring extra CA certificates from /tmp/certs..."; \ + 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 certificates found, skipping CA setup."; \ + 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 + +# --- 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/services/image-linker/config/topics.yaml b/services/image-linker/config/topics.yaml new file mode 100644 index 000000000..bbdd3588e --- /dev/null +++ b/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/services/image-linker/docker-compose.yml b/services/image-linker/docker-compose.yml new file mode 100644 index 000000000..007e1ed7f --- /dev/null +++ b/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/services/image-linker/job_linker.py b/services/image-linker/job_linker.py new file mode 100644 index 000000000..e1f66dd2c --- /dev/null +++ b/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/services/image-linker/readme.md b/services/image-linker/readme.md new file mode 100644 index 000000000..d5ec07880 --- /dev/null +++ b/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/services/image-linker/send-matching-minio.ps1 b/services/image-linker/send-matching-minio.ps1 new file mode 100644 index 000000000..729186d39 --- /dev/null +++ b/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/services/inference_http/Dockerfile b/services/inference_http/Dockerfile new file mode 100644 index 000000000..fe091eb58 --- /dev/null +++ b/services/inference_http/Dockerfile @@ -0,0 +1,70 @@ +# ============================================================ +# 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 + +# System deps (keep all original runtime + dev packages for other teams) +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl \ + libglib2.0-0 libglib2.0-dev \ + libsm6 libxrender1 libxext6 libgl1 \ + libopenblas-dev liblapack-dev \ + && rm -rf /var/lib/apt/lists/* + +# --- Trust enterprise CAs BEFORE any pip install (works with/without certs folder) --- +# If ./certs exists in build context, copy it (may be empty) and trust *.crt silently. +COPY certs/ /tmp/certs/ +RUN 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 + +# Make pip/requests/curl use the system trust store +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 + +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 + +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/services/inference_http/adapters/__init__.py b/services/inference_http/adapters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/inference_http/adapters/fruit_defect_runner.py b/services/inference_http/adapters/fruit_defect_runner.py new file mode 100644 index 000000000..8b51ebf24 --- /dev/null +++ b/services/inference_http/adapters/fruit_defect_runner.py @@ -0,0 +1,98 @@ +# 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"), +# } +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"), + # } + + 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) + bucket = extra.get("bucket") if extra else None + key = extra.get("key") if extra else None + device_id = extra.get("device_id") if extra else None + timestamp = extra.get("timestamp") if extra else None + + return { + "ok": True, + "label": result.get("status"), + "score": result.get("prob_defect"), + "confidence": result.get("confidence"), + "latency_ms_model": result.get("latency_ms_model"), + "bucket": bucket, + "key": key, + "device_id": device_id, + "timestamp": timestamp, + } diff --git a/services/inference_http/adapters/fruit_segmentation_runner.py b/services/inference_http/adapters/fruit_segmentation_runner.py new file mode 100644 index 000000000..621a23362 --- /dev/null +++ b/services/inference_http/adapters/fruit_segmentation_runner.py @@ -0,0 +1,194 @@ +import os, tempfile, hashlib, cv2, numpy as np, boto3, torch, json +import re +from datetime import datetime +from kafka import KafkaProducer +import psycopg2 + +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 +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") + ) + self.producer = KafkaProducer( + bootstrap_servers=os.getenv("KAFKA_BOOTSTRAP", "kafka:9092"), + value_serializer=lambda v: json.dumps(v).encode("utf-8") + ) + self.output_topic = "flink-dispatcher-fruit" + self.db = psycopg2.connect( + host=os.getenv("PGHOST", "postgres"), + port=os.getenv("PGPORT", "5432"), + dbname=os.getenv("PGDATABASE", "missions_db"), + user=os.getenv("PGUSER", "missions_user"), + password=os.getenv("PGPASSWORD", "pg123") + ) + self.db.autocommit = True + + + + def publish_event(self, data: dict): + """Publish fruit metadata to Kafka topic.""" + try: + self.producer.send(self.output_topic, data) + self.producer.flush() + except Exception as e: + print(f"[ERROR] failed to publish event: {e}", flush=True) + + def write_fruit_record( + self, + original_key, + cropped_key, + bucket, + device_id, + timestamp_str, + x1, y1, x2, y2, + label, + latency + ): + cur = self.db.cursor() + cur.execute( + """ + INSERT INTO fruit_detections ( + original_key, + cropped_key, + bucket, + device_id, + timestamp, + x1, y1, x2, y2, + label, + latency_ms_model + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s); + """, + ( + original_key, + cropped_key, + bucket, + device_id, + timestamp_str, + x1, y1, x2, y2, + label, + latency + ) + ) + cur.close() + + + 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 + + base_name = os.path.splitext(os.path.basename(key))[0] + match = re.match(r"([a-zA-Z0-9-]+)_(\d{8}T\d{6}Z)", base_name) + if match: + device_id, timestamp_str = match.groups() + timestamp = datetime.strptime(timestamp_str, "%Y%m%dT%H%M%SZ") + date_part = timestamp.strftime("%Y-%m-%d") + else: + device_id = "unknown_device" + date_part = "unknown_date" + count += 1 + out_name = f"{base_name}.jpg" + out_key = f"fruit/fruits/{device_id}/{date_part}/{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) + + event = { + "ok": True, + "team": "camera", + "bucket": bucket_in, + "key": out_key, + "label": "fruit", + "device_id": device_id, + "timestamp": timestamp_str, + "latency_ms_model": latency_ms, + "original_key": key, + "bbox": { + "x1": int(x1), + "y1": int(y1), + "x2": int(x2), + "y2": int(y2) + } + } + self.publish_event(event) + self.write_fruit_record( + original_key=key, + cropped_key=out_key, + bucket=bucket_in, + device_id=device_id, + timestamp_str=timestamp_str, + x1=x1, y1=y1, x2=x2, y2=y2, + label="fruit", + latency=latency_ms + ) + + pass \ No newline at end of file diff --git a/services/inference_http/adapters/rover_runner.py b/services/inference_http/adapters/rover_runner.py new file mode 100644 index 000000000..c5bfefaef --- /dev/null +++ b/services/inference_http/adapters/rover_runner.py @@ -0,0 +1,178 @@ +# services/inference_http/adapters/rover_runner.py +# Bridge between unified HTTP /infer_json and fence_hole_detector service logic. + +from __future__ import annotations + +from typing import Any, Dict, Optional +from datetime import datetime +import json +import logging + +from models.fence_hole_detector.service import InferenceInput, infer_from_minio + +logger = logging.getLogger(__name__) + + +def _to_plain_dict(obj: Any) -> Any: + """Best-effort conversion of arbitrary objects to a plain dict or JSON-like object.""" + if isinstance(obj, (dict, str, bytes, bytearray)): + return obj + + # Pydantic v2 models + if hasattr(obj, "model_dump"): + try: + return obj.model_dump() + except Exception: + pass + + # Pydantic v1 or similar objects + if hasattr(obj, "dict"): + try: + return obj.dict() + except Exception: + pass + + # Generic Python objects with __dict__ + if hasattr(obj, "__dict__"): + try: + return vars(obj) + except Exception: + pass + + return obj + + +def _search_bucket_key(node: Any) -> Optional[Dict[str, Any]]: + """ + Recursively search for a dict that contains both 'bucket' and 'key'. + + This walks nested dicts and lists/tuples and returns the first match. + """ + node = _to_plain_dict(node) + + if isinstance(node, dict): + if "bucket" in node and "key" in node: + return node + + for value in node.values(): + found = _search_bucket_key(value) + if found is not None: + return found + + elif isinstance(node, (list, tuple)): + for item in node: + found = _search_bucket_key(item) + if found is not None: + return found + + return None + + +def _extract_payload(obj: Any) -> Dict[str, Any]: + """ + Normalize a generic event/envelope into a flat dict with bucket/key. + + Supports: + - {"bucket": "...", "key": "...", ...} + - {"event": {...}}, {"data": {...}}, etc. + - Pydantic models, nested dicts, lists + - JSON strings/bytes + """ + obj = _to_plain_dict(obj) + + # If this is a JSON string or bytes, try to parse it. + if isinstance(obj, (str, bytes, bytearray)): + try: + obj = json.loads(obj) + except Exception: + logger.warning("rover_runner: failed to json.loads extra payload: %r", obj) + return {} + + # For dicts/lists/tuples, search for bucket/key recursively. + if isinstance(obj, (dict, list, tuple)): + found = _search_bucket_key(obj) + if isinstance(found, dict): + return found + + # As a fallback, if this is a dict without bucket/key, just return it. + if isinstance(obj, dict): + return obj + + return {} + + +class RoverRunner: + """ + Adapter that executes the fence_hole_detector service logic. + + The unified HTTP layer usually: + 1. Parses the JSON body into a dict. + 2. Downloads the image bytes from MinIO. + 3. Calls runner.run(image_bytes, extra=parsed_body). + + We ignore the image bytes and call infer_from_minio again using bucket/key + from the extra payload, so the existing service code does not need changes. + """ + + def __init__(self, weights_path: Optional[str] = None, + model_tag: Optional[str] = None) -> None: + # Weights are controlled via FENCE_ONNX_PATH env var inside the service. + self.model_tag = model_tag or "yolov8n-onnx" + + def run( + self, + image_bytes: Optional[bytes] = None, + model_tag: Optional[str] = None, + extra: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + """ + Main entrypoint for the unified /infer_json service. + + Expected call pattern: + run(image_bytes=, extra=) + + We: + - Extract bucket/key (and optional metadata) from extra. + - Build InferenceInput and call infer_from_minio(). + - Attach a standard 'model' field to the result. + """ + logger.info( + "RoverRunner.run: got image_bytes_type=%s extra=%r", + type(image_bytes), + extra, + ) + + payload: Dict[str, Any] = _extract_payload(extra or {}) + logger.info("RoverRunner.run: extracted payload=%r", payload) + + # Accept common alternative field names as a backup. + bucket = payload.get("bucket") or payload.get("s3_bucket") + key = payload.get("key") or payload.get("s3_key") or payload.get("object_key") + + if not bucket or not key: + msg = f"Missing required fields: bucket/key in payload={payload!r}" + logger.error("RoverRunner.run: %s", msg) + raise ValueError(msg) + + # Optional metadata: timestamp and geo / device info. + ts = payload.get("captured_at") + captured_at: Optional[datetime] = None + if ts: + try: + captured_at = datetime.fromisoformat(str(ts).replace("Z", "+00:00")) + except Exception: + captured_at = None + + inp = InferenceInput( + bucket=bucket, + key=key, + captured_at=captured_at, + device_id=payload.get("device_id"), + area=payload.get("area"), + lat=payload.get("lat"), + lon=payload.get("lon"), + ) + + result = infer_from_minio(inp) + result["model"] = self.model_tag + return result diff --git a/services/inference_http/adapters/soil_moisture_runner.py b/services/inference_http/adapters/soil_moisture_runner.py new file mode 100644 index 000000000..7c1624c70 --- /dev/null +++ b/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/services/inference_http/app.py b/services/inference_http/app.py new file mode 100644 index 000000000..b01e91831 --- /dev/null +++ b/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, + **result, + "team": TEAM, + "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}") \ No newline at end of file diff --git a/services/inference_http/model_registry.py b/services/inference_http/model_registry.py new file mode 100644 index 000000000..c5a150586 --- /dev/null +++ b/services/inference_http/model_registry.py @@ -0,0 +1,17 @@ +from adapters.fruit_defect_runner import FruitDefectRunner +from adapters.fruit_segmentation_runner import FruitSegmentationRunner +from adapters.soil_moisture_runner import SoilMoistureRunner +from adapters.rover_runner import RoverRunner + + +def get_model_runner(team: str): + t = (team or "").lower() + if t == "fruit": + return FruitDefectRunner() + if t == "camera": + return FruitSegmentationRunner() + if t == "soil_moisture": + return SoilMoistureRunner() + if t == "rover": + return RoverRunner() + raise ValueError(f"unknown TEAM {t}") diff --git a/services/inference_http/models/__init__.py b/services/inference_http/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/inference_http/models/fence_hole_detector/.env.example b/services/inference_http/models/fence_hole_detector/.env.example new file mode 100644 index 000000000..96eeac6b8 --- /dev/null +++ b/services/inference_http/models/fence_hole_detector/.env.example @@ -0,0 +1,20 @@ +# Model +FENCE_ONNX_PATH=runs_fence/y8n_cpu_v1/weights/best.onnx +FENCE_CONF=0.30 +FENCE_ROI=none +FENCE_MIN_OVERLAP=0.30 +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/services/inference_http/models/fence_hole_detector/Dockerfile b/services/inference_http/models/fence_hole_detector/Dockerfile new file mode 100644 index 000000000..86e65a336 --- /dev/null +++ b/services/inference_http/models/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/services/inference_http/models/fence_hole_detector/__init__.py b/services/inference_http/models/fence_hole_detector/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/inference_http/models/fence_hole_detector/alert_client.py b/services/inference_http/models/fence_hole_detector/alert_client.py new file mode 100644 index 000000000..eb9e9d905 --- /dev/null +++ b/services/inference_http/models/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/services/inference_http/models/fence_hole_detector/app.py b/services/inference_http/models/fence_hole_detector/app.py new file mode 100644 index 000000000..0cee99a35 --- /dev/null +++ b/services/inference_http/models/fence_hole_detector/app.py @@ -0,0 +1,177 @@ +# 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.30")) + +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 + +# --------------------------------------------------------------------------- +# Compatibility wrapper for the generic Flink HTTP dispatcher: +# Exposes /infer_json to match the expected contract in the README. +# It simply converts the input to ProcessReq and delegates to /process_minio. +# --------------------------------------------------------------------------- + +class InferJsonReq(BaseModel): + """Generic request schema expected by Flink HTTP dispatcher (/infer_json).""" + bucket: str + key: str + # Optional metadata (passthrough to alerts, if enabled) + captured_at: Optional[datetime] = None + device_id: Optional[str] = None + area: Optional[str] = None + lat: Optional[float] = None + lon: Optional[float] = None + +@app.post("/infer_json", response_model=ProcessResp) +def infer_json(req: InferJsonReq): + """ + Thin wrapper to keep a stable endpoint name for the Flink dispatcher. + Internally it reuses the exact same inference flow as /process_minio. + """ + # Convert to the internal request model and call the existing logic. + internal = ProcessReq( + bucket=req.bucket, + key=req.key, + captured_at=req.captured_at, + device_id=req.device_id, + area=req.area, + lat=req.lat, + lon=req.lon, + ) + return process_minio(internal) + +# --- 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/services/inference_http/models/fence_hole_detector/config.py b/services/inference_http/models/fence_hole_detector/config.py new file mode 100644 index 000000000..3ea5e60d4 --- /dev/null +++ b/services/inference_http/models/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.30")) + +# 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/services/inference_http/models/fence_hole_detector/infer.py b/services/inference_http/models/fence_hole_detector/infer.py new file mode 100644 index 000000000..cfc4873ee --- /dev/null +++ b/services/inference_http/models/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/services/inference_http/models/fence_hole_detector/minio_io.py b/services/inference_http/models/fence_hole_detector/minio_io.py new file mode 100644 index 000000000..456e79860 --- /dev/null +++ b/services/inference_http/models/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/services/inference_http/models/fence_hole_detector/requirements.txt b/services/inference_http/models/fence_hole_detector/requirements.txt new file mode 100644 index 000000000..5a0ccb8cb --- /dev/null +++ b/services/inference_http/models/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/services/inference_http/models/fence_hole_detector/service.py b/services/inference_http/models/fence_hole_detector/service.py new file mode 100644 index 000000000..eb6c0f566 --- /dev/null +++ b/services/inference_http/models/fence_hole_detector/service.py @@ -0,0 +1,136 @@ +# services/inference_http/models/fence_hole_detector/service.py +# Purpose: pure-Python service entrypoints used by both the model's local FastAPI +# and the unified inference_http adapter. No web-specific code here. + +from __future__ import annotations +import os +from dataclasses import dataclass +from typing import Optional, Dict, Any +import numpy as np +import cv2 +from minio import Minio +from datetime import datetime, timezone +from dotenv import load_dotenv + +from .infer import OnnxDetector # reuses the existing ONNX runtime code +from .alert_client import post_alert + +# Load .env next to this file so ALERT_ENABLED / KAFKA_BOOTSTRAP etc. are set. +BASE_DIR = os.path.dirname(__file__) +load_dotenv(os.path.join(BASE_DIR, ".env")) + +# ---- Config (env-first) ----------------------------------------------------- +# We keep model path under models/<...>/weights/best.onnx inside the image. +DEFAULT_ONNX = "/app/models/fence_hole_detector/weights/best.onnx" +FENCE_ONNX_PATH = os.getenv("FENCE_ONNX_PATH", DEFAULT_ONNX) +FENCE_CONF = float(os.getenv("FENCE_CONF", "0.30")) +FENCE_ROI = os.getenv("FENCE_ROI", "none") +FENCE_VOTE_N = int(os.getenv("FENCE_VOTE_N", "1")) +FENCE_VOTE_M = int(os.getenv("FENCE_VOTE_M", "1")) +FENCE_VOTE_COOLDOWN = int(os.getenv("FENCE_VOTE_COOLDOWN", "0")) + +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" + +# Public base URL for building image_url in alerts +MINIO_PUBLIC_BASE_URL = os.getenv( + "MINIO_PUBLIC_BASE_URL", f"http://{MINIO_ENDPOINT}" +).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")) +# Flag: enable/disable Kafka alerts +ALERT_ENABLED = os.getenv("ALERT_ENABLED", "0") == "1" + +# ---- Singletons -------------------------------------------------------------- +_detector = OnnxDetector( + onnx_path=FENCE_ONNX_PATH, + conf=FENCE_CONF, + roi=FENCE_ROI, + vote_n=FENCE_VOTE_N, + vote_m=FENCE_VOTE_M, + cooldown=FENCE_VOTE_COOLDOWN, +) + +_minio = Minio( + MINIO_ENDPOINT, + access_key=MINIO_ACCESS_KEY, + secret_key=MINIO_SECRET_KEY, + secure=MINIO_SECURE, +) + +# ---- Types ------------------------------------------------------------------- +@dataclass +class InferenceInput: + bucket: str + key: str + captured_at: Optional[datetime] = None + device_id: Optional[str] = None + area: Optional[str] = None + lat: Optional[float] = None + lon: Optional[float] = None + +def _read_minio_image(bucket: str, key: str) -> np.ndarray: + """Download object from MinIO and decode as BGR image.""" + resp = _minio.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 ValueError("Failed to decode image from MinIO") + return img + +# ---- Main entrypoint used by adapter ---------------------------------------- +def infer_from_minio(inp: InferenceInput) -> Dict[str, Any]: + """ + Perform inference from a MinIO key and return a normalized dict. + + This function is used by the unified inference_http adapter. + It may also optionally send a Kafka alert when a hole is detected. + """ + # 1) Load image from MinIO + img = _read_minio_image(inp.bucket, inp.key) + + # 2) Run model inference + det, detected, fired, max_conf = _detector.infer_image(img) + + # 3) Build base result for HTTP / Flink + result: Dict[str, Any] = { + "detected_boxes": int(len(det)), + "fired_alert": int(1 if detected else 0), + "max_conf": float(max_conf), + "device_id": inp.device_id or DEFAULT_DEVICE_ID, + "area": inp.area or DEFAULT_AREA, + "bucket": inp.bucket, + "key": inp.key, + "captured_at": ( + inp.captured_at or datetime.now(timezone.utc) + ).isoformat().replace("+00:00", "Z"), + } + + # 4) Optionally send Kafka alert (same behavior as old FastAPI service) + if detected and ALERT_ENABLED: + try: + image_url = f"{MINIO_PUBLIC_BASE_URL}/{inp.bucket}/{inp.key}" + alert_id = post_alert( + device_id=result["device_id"], + started_at=inp.captured_at or datetime.now(timezone.utc), + confidence=max_conf, + image_url=image_url, + area=result["area"], + lat=inp.lat, + lon=inp.lon, + severity=DEFAULT_SEVERITY, + extra={"bucket": inp.bucket, "key": inp.key}, + ) + result["alert_id"] = alert_id + result["image_url"] = image_url + except Exception as exc: + # Never fail inference because of alert pipeline + print(f"[WARN] alert send failed: {exc}", flush=True) + + return result \ No newline at end of file diff --git a/services/inference_http/models/fence_hole_detector/weights/best.onnx b/services/inference_http/models/fence_hole_detector/weights/best.onnx new file mode 100644 index 000000000..fddf21ac1 Binary files /dev/null and b/services/inference_http/models/fence_hole_detector/weights/best.onnx differ diff --git a/services/inference_http/models/fruit_defect/Docs/DATASET_SPEC.md b/services/inference_http/models/fruit_defect/Docs/DATASET_SPEC.md new file mode 100644 index 000000000..346e553a0 --- /dev/null +++ b/services/inference_http/models/fruit_defect/Docs/DATASET_SPEC.md @@ -0,0 +1,48 @@ +# 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) + + +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/services/inference_http/models/fruit_defect/Docs/USAGE.md b/services/inference_http/models/fruit_defect/Docs/USAGE.md new file mode 100644 index 000000000..6e341ddd7 --- /dev/null +++ b/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/services/inference_http/models/fruit_defect/__init__.py b/services/inference_http/models/fruit_defect/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/inference_http/models/fruit_defect/configs/fruit_defect.yaml b/services/inference_http/models/fruit_defect/configs/fruit_defect.yaml new file mode 100644 index 000000000..ca6f2e41a --- /dev/null +++ b/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/services/inference_http/models/fruit_defect/inference/__init__.py b/services/inference_http/models/fruit_defect/inference/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/inference_http/models/fruit_defect/inference/infer_fruit_defect.py b/services/inference_http/models/fruit_defect/inference/infer_fruit_defect.py new file mode 100644 index 000000000..0c7ef1662 --- /dev/null +++ b/services/inference_http/models/fruit_defect/inference/infer_fruit_defect.py @@ -0,0 +1,366 @@ +# 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 + +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")) + +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/services/inference_http/models/fruit_defect/requirements.txt b/services/inference_http/models/fruit_defect/requirements.txt new file mode 100644 index 000000000..16a84df06 --- /dev/null +++ b/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/services/inference_http/models/fruit_defect/scripts/eval_cls.py b/services/inference_http/models/fruit_defect/scripts/eval_cls.py new file mode 100644 index 000000000..07aff87ec --- /dev/null +++ b/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/services/inference_http/models/fruit_defect/scripts/export_torchscript.py b/services/inference_http/models/fruit_defect/scripts/export_torchscript.py new file mode 100644 index 000000000..52202e061 --- /dev/null +++ b/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/services/inference_http/models/fruit_defect/scripts/extract_mistakes.py b/services/inference_http/models/fruit_defect/scripts/extract_mistakes.py new file mode 100644 index 000000000..05422a802 --- /dev/null +++ b/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/services/inference_http/models/fruit_defect/training/train_fruit_defect_cls.py b/services/inference_http/models/fruit_defect/training/train_fruit_defect_cls.py new file mode 100644 index 000000000..12ac0de83 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/.gitignore b/services/inference_http/models/soil_moisture/.gitignore new file mode 100644 index 000000000..62d87daf0 --- /dev/null +++ b/services/inference_http/models/soil_moisture/.gitignore @@ -0,0 +1 @@ +samples/ \ No newline at end of file diff --git a/services/inference_http/models/soil_moisture/Dockerfile b/services/inference_http/models/soil_moisture/Dockerfile new file mode 100644 index 000000000..9b1bb644d --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/README.md b/services/inference_http/models/soil_moisture/README.md new file mode 100644 index 000000000..0aacb3f61 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/artifacts/best.pt b/services/inference_http/models/soil_moisture/artifacts/best.pt new file mode 100644 index 000000000..4fbed017d Binary files /dev/null and b/services/inference_http/models/soil_moisture/artifacts/best.pt differ diff --git a/services/inference_http/models/soil_moisture/artifacts/label_mapping.json b/services/inference_http/models/soil_moisture/artifacts/label_mapping.json new file mode 100644 index 000000000..7b688603a --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/artifacts/model.onnx b/services/inference_http/models/soil_moisture/artifacts/model.onnx new file mode 100644 index 000000000..6052e846e Binary files /dev/null and b/services/inference_http/models/soil_moisture/artifacts/model.onnx differ diff --git a/services/inference_http/models/soil_moisture/configs/zones.yaml b/services/inference_http/models/soil_moisture/configs/zones.yaml new file mode 100644 index 000000000..fa76afbb7 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/docker-compose.yml b/services/inference_http/models/soil_moisture/docker-compose.yml new file mode 100644 index 000000000..c23e6b3e3 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/requirements-api.txt b/services/inference_http/models/soil_moisture/requirements-api.txt new file mode 100644 index 000000000..6e3ae713a --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/requirements-train.txt b/services/inference_http/models/soil_moisture/requirements-train.txt new file mode 100644 index 000000000..21a78fde4 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/.dockerignore b/services/inference_http/models/soil_moisture/src/.dockerignore new file mode 100644 index 000000000..f5bddaa38 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/app/__init__.py b/services/inference_http/models/soil_moisture/src/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/inference_http/models/soil_moisture/src/app/config.py b/services/inference_http/models/soil_moisture/src/app/config.py new file mode 100644 index 000000000..719e03fa3 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/app/db.py b/services/inference_http/models/soil_moisture/src/app/db.py new file mode 100644 index 000000000..356f523b2 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/app/inference.py b/services/inference_http/models/soil_moisture/src/app/inference.py new file mode 100644 index 000000000..95c4d948c --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/app/inference_logic.py b/services/inference_http/models/soil_moisture/src/app/inference_logic.py new file mode 100644 index 000000000..8da8bae44 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/app/kafka_producer.py b/services/inference_http/models/soil_moisture/src/app/kafka_producer.py new file mode 100644 index 000000000..b6052accd --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/app/metrics.py b/services/inference_http/models/soil_moisture/src/app/metrics.py new file mode 100644 index 000000000..ce38f34de --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/app/onnx_model.py b/services/inference_http/models/soil_moisture/src/app/onnx_model.py new file mode 100644 index 000000000..c78db3f26 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/app/schemas.py b/services/inference_http/models/soil_moisture/src/app/schemas.py new file mode 100644 index 000000000..336e7c323 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/app/service.py b/services/inference_http/models/soil_moisture/src/app/service.py new file mode 100644 index 000000000..36e09cc20 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/app/utils.py b/services/inference_http/models/soil_moisture/src/app/utils.py new file mode 100644 index 000000000..2853c5250 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/scripts/consume_once.py b/services/inference_http/models/soil_moisture/src/scripts/consume_once.py new file mode 100644 index 000000000..b035f4017 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/scripts/demo_feed.py b/services/inference_http/models/soil_moisture/src/scripts/demo_feed.py new file mode 100644 index 000000000..3be42924a --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/scripts/eval_test_set.py b/services/inference_http/models/soil_moisture/src/scripts/eval_test_set.py new file mode 100644 index 000000000..875834dea --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/scripts/print_db_events.py b/services/inference_http/models/soil_moisture/src/scripts/print_db_events.py new file mode 100644 index 000000000..07b41b875 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/sql/init_db.sql b/services/inference_http/models/soil_moisture/src/sql/init_db.sql new file mode 100644 index 000000000..1476755fc --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/src/train/train_torch.py b/services/inference_http/models/soil_moisture/src/train/train_torch.py new file mode 100644 index 000000000..db7c89d9a --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/tests/conftest.py b/services/inference_http/models/soil_moisture/tests/conftest.py new file mode 100644 index 000000000..5cfe605bc --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/tests/test_config_and_schemas.py b/services/inference_http/models/soil_moisture/tests/test_config_and_schemas.py new file mode 100644 index 000000000..0fb9abae9 --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/tests/test_inference.py b/services/inference_http/models/soil_moisture/tests/test_inference.py new file mode 100644 index 000000000..5906bacfe --- /dev/null +++ b/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/services/inference_http/models/soil_moisture/tests/test_utils.py b/services/inference_http/models/soil_moisture/tests/test_utils.py new file mode 100644 index 000000000..a45ea10e4 --- /dev/null +++ b/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/services/inference_http/requirements.txt b/services/inference_http/requirements.txt new file mode 100644 index 000000000..2aa5e14e7 --- /dev/null +++ b/services/inference_http/requirements.txt @@ -0,0 +1,21 @@ +fastapi +uvicorn +pydantic +minio +requests +torch +numpy<2 +opencv-python-headless +ultralytics==8.2.34 +boto3 +pillow +numpy==1.26.4 +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 diff --git a/services/inference_http/weights/fruit_cls_best.ts b/services/inference_http/weights/fruit_cls_best.ts new file mode 100644 index 000000000..d190cf3d9 Binary files /dev/null and b/services/inference_http/weights/fruit_cls_best.ts differ diff --git a/services/inference_http/weights/yolov8-fruits.pt b/services/inference_http/weights/yolov8-fruits.pt new file mode 100644 index 000000000..d61ef50d3 Binary files /dev/null and b/services/inference_http/weights/yolov8-fruits.pt differ diff --git a/services/mqtt_gateway/Dockerfile b/services/mqtt_gateway/Dockerfile new file mode 100644 index 000000000..4a0d1c1ad --- /dev/null +++ b/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/services/mqtt_gateway/adapters/kafka_producer.py b/services/mqtt_gateway/adapters/kafka_producer.py new file mode 100644 index 000000000..4703085b3 --- /dev/null +++ b/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/services/mqtt_gateway/adapters/minio_store.py b/services/mqtt_gateway/adapters/minio_store.py new file mode 100644 index 000000000..ab9074e85 --- /dev/null +++ b/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/services/mqtt_gateway/config.py b/services/mqtt_gateway/config.py new file mode 100644 index 000000000..126c75ee8 --- /dev/null +++ b/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/services/mqtt_gateway/docs/contracts.md b/services/mqtt_gateway/docs/contracts.md new file mode 100644 index 000000000..42dc6f280 --- /dev/null +++ b/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/services/mqtt_gateway/io.py b/services/mqtt_gateway/io.py new file mode 100644 index 000000000..d0b9f3c4d --- /dev/null +++ b/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/services/mqtt_gateway/main.py b/services/mqtt_gateway/main.py new file mode 100644 index 000000000..612f8f8f3 --- /dev/null +++ b/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/services/mqtt_gateway/mapper.py b/services/mqtt_gateway/mapper.py new file mode 100644 index 000000000..c8d77b359 --- /dev/null +++ b/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/services/mqtt_gateway/models.py b/services/mqtt_gateway/models.py new file mode 100644 index 000000000..327ed9f2b --- /dev/null +++ b/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/services/mqtt_gateway/requirements.txt b/services/mqtt_gateway/requirements.txt new file mode 100644 index 000000000..3bdb4f438 --- /dev/null +++ b/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/services/mqtt_gateway/service.py b/services/mqtt_gateway/service.py new file mode 100644 index 000000000..a7e4a70f5 --- /dev/null +++ b/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/services/mqtt_gateway/tests/fixtures/sample_event.json b/services/mqtt_gateway/tests/fixtures/sample_event.json new file mode 100644 index 000000000..b291d7530 --- /dev/null +++ b/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/services/mqtt_gateway/tests/test_adapters_unit.py b/services/mqtt_gateway/tests/test_adapters_unit.py new file mode 100644 index 000000000..19c9a4122 --- /dev/null +++ b/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/services/mqtt_gateway/tests/test_mapper.py b/services/mqtt_gateway/tests/test_mapper.py new file mode 100644 index 000000000..598e62c7a --- /dev/null +++ b/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/services/mqtt_gateway/tests/test_service.py b/services/mqtt_gateway/tests/test_service.py new file mode 100644 index 000000000..1a7278b92 --- /dev/null +++ b/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/services/mqtt_gateway/tests/test_service_retry.py b/services/mqtt_gateway/tests/test_service_retry.py new file mode 100644 index 000000000..5571fed97 --- /dev/null +++ b/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/services/plant_stress/Dockerfile b/services/plant_stress/Dockerfile index 59490295b..d456611aa 100644 --- a/services/plant_stress/Dockerfile +++ b/services/plant_stress/Dockerfile @@ -1,40 +1,45 @@ FROM python:3.11-slim ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 + PYTHONUNBUFFERED=1 \ + TZ=Asia/Jerusalem RUN apt-get update && apt-get install -y --no-install-recommends \ - libsndfile1 ffmpeg gcc \ + cron tzdata ca-certificates curl libsndfile1 ffmpeg \ && rm -rf /var/lib/apt/lists/* -WORKDIR /app +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -COPY certs /app/certs +COPY certs/ /usr/local/share/ca-certificates/ -RUN if [ -f /app/certs/netfree-ca.crt ]; then \ - echo "Installing NetFree certificate..."; \ - cp /app/certs/netfree-ca.crt /usr/local/share/ca-certificates/netfree-ca.crt && \ - update-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 "⚠️ WARNING: netfree-ca.crt not found, continuing without it."; \ + 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 \ - PIP_CERT=/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 requirements.txt ./ -RUN pip install --no-cache-dir -r requirements.txt \ - --trusted-host pypi.org \ - --trusted-host pypi.python.org \ - --trusted-host files.pythonhosted.org +COPY src /app/src +COPY models /models +ENV MODEL_DIR=/models -COPY src/ /app/ +COPY cronjob /etc/cron.d/plant-stress-cron +COPY run_job.sh /app/run_job.sh +COPY entrypoint.sh /entrypoint.sh -ENV INPUT_DIR=/data/inbox \ - MODEL_DIR=/models \ - PERIOD_DAYS=7 \ - CONF_THRESHOLD=0.0 \ - POSTGRES_DSN="postgresql://postgres:pg123@host.docker.internal:5432/missions_db" +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 ["python", "/app/app.py"] +CMD ["/entrypoint.sh"] diff --git a/services/plant_stress/README.md b/services/plant_stress/README.md index e69de29bb..78a071d61 100644 --- a/services/plant_stress/README.md +++ b/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/services/plant_stress/cronjob b/services/plant_stress/cronjob new file mode 100644 index 000000000..de3cd28ab --- /dev/null +++ b/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/services/plant_stress/entrypoint.sh b/services/plant_stress/entrypoint.sh new file mode 100644 index 000000000..41727b16f --- /dev/null +++ b/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/services/plant_stress/requirements.txt b/services/plant_stress/requirements.txt index e29f9b682..869c5ea97 100644 --- a/services/plant_stress/requirements.txt +++ b/services/plant_stress/requirements.txt @@ -4,4 +4,8 @@ soundfile tensorflow psycopg2-binary requests -urllib3 \ No newline at end of file +urllib3 +pytz +minio +keras +kafka-python \ No newline at end of file diff --git a/services/plant_stress/run_job.sh b/services/plant_stress/run_job.sh new file mode 100644 index 000000000..f6de17b98 --- /dev/null +++ b/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/services/plant_stress/run_plant_stress_daily.sh b/services/plant_stress/run_plant_stress_daily.sh new file mode 100644 index 000000000..3d449ad43 --- /dev/null +++ b/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/services/plant_stress/samples/id_89_sound_14.wav b/services/plant_stress/samples/id_89_sound_14.wav deleted file mode 100644 index bd55e276c..000000000 Binary files a/services/plant_stress/samples/id_89_sound_14.wav and /dev/null differ diff --git a/services/plant_stress/samples/id_89_sound_15.wav b/services/plant_stress/samples/id_89_sound_15.wav deleted file mode 100644 index e0cef995e..000000000 Binary files a/services/plant_stress/samples/id_89_sound_15.wav and /dev/null differ diff --git a/services/plant_stress/samples/id_89_sound_16.wav b/services/plant_stress/samples/id_89_sound_16.wav deleted file mode 100644 index 28ae9b01a..000000000 Binary files a/services/plant_stress/samples/id_89_sound_16.wav and /dev/null differ diff --git a/services/plant_stress/samples/id_89_sound_17.wav b/services/plant_stress/samples/id_89_sound_17.wav deleted file mode 100644 index ea418e663..000000000 Binary files a/services/plant_stress/samples/id_89_sound_17.wav and /dev/null differ diff --git a/services/plant_stress/samples/id_89_sound_18.wav b/services/plant_stress/samples/id_89_sound_18.wav deleted file mode 100644 index 05fd1f354..000000000 Binary files a/services/plant_stress/samples/id_89_sound_18.wav and /dev/null differ diff --git a/services/plant_stress/src/app.py b/services/plant_stress/src/app.py deleted file mode 100644 index 4afbbb809..000000000 --- a/services/plant_stress/src/app.py +++ /dev/null @@ -1,207 +0,0 @@ -import os, sys, time, glob, pickle, datetime as dt -from pathlib import Path -import numpy as np -import librosa -import tensorflow as tf -import psycopg2 -import psycopg2.extras - -# ======== Environment Config ======== -INPUT_DIR = os.environ.get("INPUT_DIR", "/data/inbox") -MODEL_DIR = os.environ.get("MODEL_DIR", "/models") -POSTGRES_DSN = os.environ.get("POSTGRES_DSN", "postgresql://postgres:postgres@localhost:5432/postgres") -PERIOD_DAYS = int(os.environ.get("PERIOD_DAYS", "0")) # 0 = process all files - -# ======== Audio Parameters (2ms @ 500kHz) ======== -SAMPLE_RATE = 500_000 -DURATION_MS = 2 -N_SAMPLES = int(SAMPLE_RATE * DURATION_MS / 1000) # 1000 samples -N_FFT = 256 -HOP_LENGTH = 64 -N_MELS = 64 - -# ======== Watering Status Mapping ======== -CLASS_TO_STATUS = { - "Drought_Tomato": "Watering required", - "Drought_Tobacco": "Watering required", - "Control_Empty": "Normal / Empty", - "Control_Greenhouse": "Greenhouse noise / Normal", -} -CONFIDENCE_THRESHOLD = float(os.environ.get("CONFIDENCE_THRESHOLD", "0.60")) - -# ======== Load Model / Scaler / LabelEncoder ======== -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") - -print(f"INPUT_DIR={INPUT_DIR}") -print(f"MODEL_DIR={MODEL_DIR}") -print(f"POSTGRES_DSN={POSTGRES_DSN}") - -MODEL = tf.keras.models.load_model(model_path) - -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) - -# ======== Helper Functions ======== -def load_and_preprocess_audio(file_path: str): - """Load audio, resample to 500kHz, crop/pad to 2ms (1000 samples).""" - audio, sr = librosa.load(file_path, sr=None, mono=True) - 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 - -def extract_ultrasonic_features(audio: np.ndarray, sr: int): - """Short time/frequency features for 2ms window.""" - feats = [] - feats.extend([np.mean(audio), np.std(audio), np.max(audio), np.min(audio), - np.var(audio), np.median(audio)]) - zcr = librosa.feature.zero_crossing_rate(audio, hop_length=HOP_LENGTH)[0] - feats.extend([np.mean(zcr), np.std(zcr), np.max(zcr)]) - fft = np.abs(np.fft.fft(audio))[:len(audio)//2] - feats.extend([np.mean(fft), np.std(fft), np.max(fft), np.argmax(fft)]) - try: - spectral_centroids = librosa.feature.spectral_centroid(y=audio, sr=sr, hop_length=HOP_LENGTH)[0] - spectral_rolloff = librosa.feature.spectral_rolloff(y=audio, sr=sr, hop_length=HOP_LENGTH)[0] - feats.extend([np.mean(spectral_centroids), np.mean(spectral_rolloff)]) - except Exception: - feats.extend([0.0, 0.0]) - rms = librosa.feature.rms(y=audio, hop_length=HOP_LENGTH)[0] - feats.extend([np.mean(rms), np.std(rms)]) - return np.array(feats, dtype=np.float32) - -def create_spectrogram_features(audio: np.ndarray, sr: int): - """Small Mel-spectrogram adapted for 2ms.""" - 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 - -def files_to_process(root: str, period_days: int): - p = Path(root) - if not p.exists(): - print(f"[!] INPUT_DIR not found: {root}") - return [] - exts = {".wav", ".WAV"} - allf = [str(pp) for pp in p.rglob("*") if pp.suffix in exts] - if period_days <= 0: - print("[i] Time filter disabled (PERIOD_DAYS<=0): processing ALL .wav files") - return sorted(allf) - cutoff = time.time() - period_days * 86400 - return sorted([f for f in allf if Path(f).stat().st_mtime >= cutoff]) - -def ensure_table(conn): - sql = """ - 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() - ); - """ - with conn.cursor() as cur: - cur.execute(sql) - conn.commit() - -def insert_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() - -# ======== Main Run ======== -def main(): - files = files_to_process(INPUT_DIR, PERIOD_DAYS) - if not files: - print("No files to process. Exiting.") - return 0 - - try: - conn = psycopg2.connect(POSTGRES_DSN) - ensure_table(conn) - except Exception as e: - print(f"[!] Postgres connection error: {e}") - return 2 - - batch = [] - ok, fail = 0, 0 - start = time.time() - - for fpath in files: - try: - audio, sr = load_and_preprocess_audio(fpath) - 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] - - 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)" - - batch.append(( - fpath, str(pred_class), conf, watering_status, "Success", - dt.datetime.utcnow() - )) - ok += 1 - print(f"OK {fpath} -> {pred_class} ({conf:.3f})") - except Exception as e: - fail += 1 - batch.append((fpath, "", None, "", f"Error: {e}", dt.datetime.utcnow())) - print(f"[ERR] {fpath} -> {e}") - - - # Write to Postgres - try: - if batch: - insert_rows(conn, batch) - print(f"Inserted {len(batch)} rows to Postgres.") - except Exception as e: - print(f"[!] Insert error: {e}") - return 3 - finally: - try: - conn.close() - except Exception: - pass - - elapsed = time.time() - start - print(f"Done. processed={len(files)} ok={ok} fail={fail} elapsed_sec={elapsed:.1f}") - return 0 - -if __name__ == "__main__": - sys.exit(main()) diff --git a/services/plant_stress/src/db_api_client.py b/services/plant_stress/src/db_api_client.py deleted file mode 100644 index f9d9e370b..000000000 --- a/services/plant_stress/src/db_api_client.py +++ /dev/null @@ -1,223 +0,0 @@ -# # import json -# # import time -# # import pathlib -# # import requests -# # from urllib.parse import quote -# # from requests.adapters import HTTPAdapter -# # from urllib3.util.retry import Retry -# # # ---------- CONFIG ---------- -# # DB_API_BASE = "http://host.docker.internal:8001" -# # DB_API_AUTH_MODE = "service" -# # DB_API_TOKEN_FILE = "/app/secret/db_api_token" -# # DB_API_TOKEN = "auto" -# # DB_API_SERVICE_NAME = "GUI_H" -# # # ---------- 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: -# # 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) -> 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 in (200, 201): -# # 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 "***" not in raw: -# # return raw.strip() -# # except Exception: -# # time.sleep(backoff * attempt) -# # return None -# # def get_or_bootstrap_token() -> str | None: -# # print(f"[DEBUG] Checking for existing token file at: {DB_API_TOKEN_FILE}", flush=True) -# # if DB_API_TOKEN and DB_API_TOKEN.lower() != "auto": -# # print(f"[DEBUG] Using static token from config", 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 existing token found, bootstrapping via {DB_API_BASE}/auth/_dev_bootstrap", flush=True) -# # token = _fetch_token_via_dev_bootstrap(DB_API_BASE) -# # if token: -# # pathlib.Path(DB_API_TOKEN_FILE).parent.mkdir(parents=True, exist_ok=True) -# # pathlib.Path(DB_API_TOKEN_FILE).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 -# # # ---------- API CLIENT ---------- -# # class DashboardApi: -# # def __init__(self): -# # self.base = DB_API_BASE.rstrip("/") -# # self.http = requests.Session() -# # token = get_or_bootstrap_token() -# # 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}"}) -# # self.http.headers.update({"Content-Type": "application/json"}) -# # self.http.mount("http://", HTTPAdapter(max_retries=Retry(total=5, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504]))) -# # self.http.mount("https://", HTTPAdapter(max_retries=Retry(total=5, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504]))) -# # # ---------- METHODS ---------- -# # def list_devices(self, model: str | None = None) -> list[dict]: -# # url = f"{self.base}/api/devices" -# # if model: -# # url += f"?model={model}" -# # try: -# # r = self.http.get(url, timeout=10) -# # if r.status_code == 200: -# # return r.json() -# # print(f"[API ERROR] {r.status_code}: {r.text[:100]}") -# # except Exception as e: -# # print(f"[API FAIL] {e}") -# # return [] - - -# # services/plant_stress/src/db_api_client.py -# # services/plant_stress/src/db_api_client.py -# import os, pathlib, time, requests - -# DB_API_BASE = os.getenv("DB_API_BASE", "http://db_api_service:8001").rstrip("/") -# DB_API_AUTH_MODE = os.getenv("DB_API_AUTH_MODE", "service") -# DB_API_TOKEN_FILE = os.getenv("DB_API_TOKEN_FILE", "/tmp/db_api_token") -# DB_API_TOKEN = os.getenv("DB_API_TOKEN", "auto") -# DB_API_SERVICE_NAME = os.getenv("DB_API_SERVICE_NAME", "plant_stress") - -# def _join(b, p): -# return f"{b.rstrip('/')}/{p.lstrip('/')}" - -# def _read_token(path): -# p = pathlib.Path(path) -# return p.read_text(encoding="utf-8").strip() if p.exists() else None - -# def _bootstrap_token(base, retries=3, backoff=0.8): -# url = _join(base, "/auth/_dev_bootstrap") -# payload = {"service_name": DB_API_SERVICE_NAME, "rotate_if_exists": True} -# for i in range(1, retries+1): -# try: -# r = requests.post(url, json=payload, timeout=10) -# if r.status_code in (200, 201): -# sa = (r.json().get("service_account", {}) or {}) -# raw = sa.get("raw_token") or sa.get("token") -# if raw and isinstance(raw, str) and "***" not in raw: -# return raw.strip() -# except Exception: -# time.sleep(backoff * i) -# return None - -# def get_or_bootstrap_token(): -# if DB_API_TOKEN and DB_API_TOKEN.lower() != "auto": -# return DB_API_TOKEN -# tok = _read_token(DB_API_TOKEN_FILE) -# if tok: -# return tok -# tok = _bootstrap_token(DB_API_BASE) -# if tok: -# pathlib.Path(DB_API_TOKEN_FILE).parent.mkdir(parents=True, exist_ok=True) -# pathlib.Path(DB_API_TOKEN_FILE).write_text(tok, encoding="utf-8") -# print(f"[BOOTSTRAP] wrote token to {DB_API_TOKEN_FILE}", flush=True) -# return tok -# print("[BOOTSTRAP][ERROR] Failed to obtain token.", flush=True) -# return None - -# # ---- requests.Session() גלובלי ---- -# _SESSION = None -# def get_session(): -# global _SESSION -# if _SESSION is None: -# s = requests.Session() -# tok = get_or_bootstrap_token() -# if tok: -# if DB_API_AUTH_MODE == "service": -# s.headers.update({"X-Service-Token": tok}) -# else: -# s.headers.update({"Authorization": f"Bearer {tok}"}) -# s.headers.update({"Content-Type": "application/json"}) -# _SESSION = s -# return _SESSION - -# FILES_POST = "/api/files" -# FILES_PUT_TPL = "/api/files/{bucket}/{object_key}" - -# def _variants_for_create(meta: dict): -# """נפיק כמה וריאציות פייפ/load נפוצות ל-POST /api/files""" -# b = meta.get("bucket"); k = meta.get("object_key") -# mime = meta.get("mime", "audio/wav") -# tags = meta.get("tags", []) -# mdata = { -# "service": meta.get("service"), -# "timestamp": meta.get("timestamp"), -# "predicted_class": meta.get("predicted_class"), -# "confidence": meta.get("confidence"), -# } -# return [ -# {"bucket": b, "object_key": k, "mime": mime, "tags": tags, "metadata": mdata}, -# {"bucket": b, "object_key": k, "content_type": mime, "tags": tags, "metadata": mdata}, -# {"bucket": b, "object_key": k, "mime": mime, "labels": tags, "metadata": mdata}, -# {"bucket": b, "object_key": k, "mime": mime, "meta": mdata}, -# {"bucket": b, "object_key": k}, # מינימלי מאוד (ייתכן שהסכמה דורשת עוד – נבדוק 422) -# ] - -# def _variants_for_update(meta: dict): -# """וריאציות PUT /api/files/{bucket}/{object_key}""" -# mime = meta.get("mime", "audio/wav") -# tags = meta.get("tags", []) -# mdata = { -# "service": meta.get("service"), -# "timestamp": meta.get("timestamp"), -# "predicted_class": meta.get("predicted_class"), -# "confidence": meta.get("confidence"), -# } -# return [ -# {"mime": mime, "tags": tags, "metadata": mdata}, -# {"content_type": mime, "tags": tags, "metadata": mdata}, -# {"metadata": mdata}, -# {"tags": tags}, -# ] - -# def write_db_entry(meta: dict) -> bool: -# """ -# רושם/מעדכן רשומת קובץ + מטא-דטה ב-DB API דרך Files API. -# """ -# s = get_session() -# b = meta.get("bucket"); k = meta.get("object_key") -# if not b or not k: -# print("[API ERROR] meta must include 'bucket' and 'object_key'") -# return False - -# # 1) נסה POST /api/files עם כמה וריאציות -# for payload in _variants_for_create(meta): -# try: -# r = s.post(_join(DB_API_BASE, FILES_POST), json=payload, timeout=15) -# if r.status_code in (200, 201): -# return True -# # אם האובייקט כבר קיים או הסכמה לא תואמת – ננסה וריאציה אחרת / נגלוש ל-PUT -# except Exception as e: -# print(f"[API ERROR] POST /api/files: {e}") - -# # 2) PUT /api/files/{bucket}/{object_key} (upsert/update) -# path = FILES_PUT_TPL.format(bucket=b, object_key=k) -# for payload in _variants_for_update(meta): -# try: -# r = s.put(_join(DB_API_BASE, path), json=payload, timeout=15) -# if r.status_code in (200, 201): -# return True -# # ננסה וריאציה הבאה -# except Exception as e: -# print(f"[API ERROR] PUT {path}: {e}") - -# try: -# print(f"[API ERROR] all variants failed for {b}/{k}. Last status={r.status_code} body={r.text[:300]}") -# except Exception: -# pass -# return False - diff --git a/services/plant_stress/src/predict_minio_daily.py b/services/plant_stress/src/predict_minio_daily.py new file mode 100644 index 000000000..0345af93a --- /dev/null +++ b/services/plant_stress/src/predict_minio_daily.py @@ -0,0 +1,822 @@ +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" + +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 ======== + +def get_meta_for_alert(conn, device_id: str, file_name: str): + area = None + lat = None + lon = None + + short = file_name.split("/")[-1] + + 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}") + + 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}") + + 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/services/ripeness-baseline/README.md b/services/ripeness-baseline/README.md new file mode 100644 index 000000000..223eb7f65 --- /dev/null +++ b/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/services/ripeness-baseline/deploy/Dockerfile b/services/ripeness-baseline/deploy/Dockerfile new file mode 100644 index 000000000..b1009b496 --- /dev/null +++ b/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/services/ripeness-baseline/deploy/k8s-cronjob.yaml b/services/ripeness-baseline/deploy/k8s-cronjob.yaml new file mode 100644 index 000000000..511078bb6 --- /dev/null +++ b/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/services/ripeness-baseline/deploy/sql/01_schema.sql b/services/ripeness-baseline/deploy/sql/01_schema.sql new file mode 100644 index 000000000..c3233c594 --- /dev/null +++ b/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/services/ripeness-baseline/deploy/sql/02_rollup_view.sql b/services/ripeness-baseline/deploy/sql/02_rollup_view.sql new file mode 100644 index 000000000..1d22e814e --- /dev/null +++ b/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/services/ripeness-baseline/deploy/sql/03_weekly_upsert.sql b/services/ripeness-baseline/deploy/sql/03_weekly_upsert.sql new file mode 100644 index 000000000..7805f69af --- /dev/null +++ b/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/services/ripeness-baseline/docker-compose.yml b/services/ripeness-baseline/docker-compose.yml new file mode 100644 index 000000000..d10ea5cc1 --- /dev/null +++ b/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/services/ripeness-baseline/eval/apple_argmax/metrics.json b/services/ripeness-baseline/eval/apple_argmax/metrics.json new file mode 100644 index 000000000..02a660f9b --- /dev/null +++ b/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/services/ripeness-baseline/eval/apple_argmax/per_image.csv b/services/ripeness-baseline/eval/apple_argmax/per_image.csv new file mode 100644 index 000000000..28c174189 --- /dev/null +++ b/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/services/ripeness-baseline/eval/apple_argmax/roc_curves.png b/services/ripeness-baseline/eval/apple_argmax/roc_curves.png new file mode 100644 index 000000000..b0eff4b97 Binary files /dev/null and b/services/ripeness-baseline/eval/apple_argmax/roc_curves.png differ diff --git a/services/ripeness-baseline/eval/apple_test/metrics.json b/services/ripeness-baseline/eval/apple_test/metrics.json new file mode 100644 index 000000000..c56066221 --- /dev/null +++ b/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/services/ripeness-baseline/eval/apple_test/per_image.csv b/services/ripeness-baseline/eval/apple_test/per_image.csv new file mode 100644 index 000000000..ce05f11f8 --- /dev/null +++ b/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/services/ripeness-baseline/eval/apple_test/roc_curves.png b/services/ripeness-baseline/eval/apple_test/roc_curves.png new file mode 100644 index 000000000..efaf43700 Binary files /dev/null and b/services/ripeness-baseline/eval/apple_test/roc_curves.png differ diff --git a/services/ripeness-baseline/eval/apple_tuned/metrics.json b/services/ripeness-baseline/eval/apple_tuned/metrics.json new file mode 100644 index 000000000..a56d0c00e --- /dev/null +++ b/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/services/ripeness-baseline/eval/apple_tuned/per_image.csv b/services/ripeness-baseline/eval/apple_tuned/per_image.csv new file mode 100644 index 000000000..3bb1442b7 --- /dev/null +++ b/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/services/ripeness-baseline/eval/apple_tuned/roc_curves.png b/services/ripeness-baseline/eval/apple_tuned/roc_curves.png new file mode 100644 index 000000000..b0eff4b97 Binary files /dev/null and b/services/ripeness-baseline/eval/apple_tuned/roc_curves.png differ diff --git a/services/ripeness-baseline/eval/banana_argmax/metrics.json b/services/ripeness-baseline/eval/banana_argmax/metrics.json new file mode 100644 index 000000000..550519d20 --- /dev/null +++ b/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/services/ripeness-baseline/eval/banana_argmax/per_image.csv b/services/ripeness-baseline/eval/banana_argmax/per_image.csv new file mode 100644 index 000000000..2e0a95964 --- /dev/null +++ b/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/services/ripeness-baseline/eval/banana_argmax/roc_curves.png b/services/ripeness-baseline/eval/banana_argmax/roc_curves.png new file mode 100644 index 000000000..9bc631052 Binary files /dev/null and b/services/ripeness-baseline/eval/banana_argmax/roc_curves.png differ diff --git a/services/ripeness-baseline/eval/banana_test/metrics.json b/services/ripeness-baseline/eval/banana_test/metrics.json new file mode 100644 index 000000000..258384beb --- /dev/null +++ b/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/services/ripeness-baseline/eval/banana_test/per_image.csv b/services/ripeness-baseline/eval/banana_test/per_image.csv new file mode 100644 index 000000000..07b29a2b5 --- /dev/null +++ b/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/services/ripeness-baseline/eval/banana_test/roc_curves.png b/services/ripeness-baseline/eval/banana_test/roc_curves.png new file mode 100644 index 000000000..14be548a6 Binary files /dev/null and b/services/ripeness-baseline/eval/banana_test/roc_curves.png differ diff --git a/services/ripeness-baseline/eval/banana_tuned/metrics.json b/services/ripeness-baseline/eval/banana_tuned/metrics.json new file mode 100644 index 000000000..b8592e177 --- /dev/null +++ b/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/services/ripeness-baseline/eval/banana_tuned/per_image.csv b/services/ripeness-baseline/eval/banana_tuned/per_image.csv new file mode 100644 index 000000000..babbeb96f --- /dev/null +++ b/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/services/ripeness-baseline/eval/banana_tuned/roc_curves.png b/services/ripeness-baseline/eval/banana_tuned/roc_curves.png new file mode 100644 index 000000000..e66f9d094 Binary files /dev/null and b/services/ripeness-baseline/eval/banana_tuned/roc_curves.png differ diff --git a/services/ripeness-baseline/eval/orange_argmax/metrics.json b/services/ripeness-baseline/eval/orange_argmax/metrics.json new file mode 100644 index 000000000..8fa7265a5 --- /dev/null +++ b/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/services/ripeness-baseline/eval/orange_argmax/per_image.csv b/services/ripeness-baseline/eval/orange_argmax/per_image.csv new file mode 100644 index 000000000..10b008a2d --- /dev/null +++ b/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/services/ripeness-baseline/eval/orange_argmax/roc_curves.png b/services/ripeness-baseline/eval/orange_argmax/roc_curves.png new file mode 100644 index 000000000..4840e45f2 Binary files /dev/null and b/services/ripeness-baseline/eval/orange_argmax/roc_curves.png differ diff --git a/services/ripeness-baseline/eval/orange_test/metrics.json b/services/ripeness-baseline/eval/orange_test/metrics.json new file mode 100644 index 000000000..8d5793e6e --- /dev/null +++ b/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/services/ripeness-baseline/eval/orange_test/per_image.csv b/services/ripeness-baseline/eval/orange_test/per_image.csv new file mode 100644 index 000000000..229aabdcc --- /dev/null +++ b/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/services/ripeness-baseline/eval/orange_test/roc_curves.png b/services/ripeness-baseline/eval/orange_test/roc_curves.png new file mode 100644 index 000000000..4b4b17050 Binary files /dev/null and b/services/ripeness-baseline/eval/orange_test/roc_curves.png differ diff --git a/services/ripeness-baseline/eval/orange_tuned/metrics.json b/services/ripeness-baseline/eval/orange_tuned/metrics.json new file mode 100644 index 000000000..e1c698a49 --- /dev/null +++ b/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/services/ripeness-baseline/eval/orange_tuned/per_image.csv b/services/ripeness-baseline/eval/orange_tuned/per_image.csv new file mode 100644 index 000000000..bce96a877 --- /dev/null +++ b/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/services/ripeness-baseline/eval/orange_tuned/roc_curves.png b/services/ripeness-baseline/eval/orange_tuned/roc_curves.png new file mode 100644 index 000000000..3543d7174 Binary files /dev/null and b/services/ripeness-baseline/eval/orange_tuned/roc_curves.png differ diff --git a/services/ripeness-baseline/requirements.txt b/services/ripeness-baseline/requirements.txt new file mode 100644 index 000000000..4786e192b --- /dev/null +++ b/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/services/ripeness-baseline/samples/apple-colorful-vector-design_341269-1123.jpg b/services/ripeness-baseline/samples/apple-colorful-vector-design_341269-1123.jpg new file mode 100644 index 000000000..36e4a1b77 Binary files /dev/null and b/services/ripeness-baseline/samples/apple-colorful-vector-design_341269-1123.jpg differ diff --git a/services/ripeness-baseline/samples/red-apple-isolated-clipping-path-19130134.webp b/services/ripeness-baseline/samples/red-apple-isolated-clipping-path-19130134.webp new file mode 100644 index 000000000..30ee03370 Binary files /dev/null and b/services/ripeness-baseline/samples/red-apple-isolated-clipping-path-19130134.webp differ diff --git a/services/ripeness-baseline/src/config.py b/services/ripeness-baseline/src/config.py new file mode 100644 index 000000000..fbc7daa24 --- /dev/null +++ b/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/services/ripeness-baseline/src/db.py b/services/ripeness-baseline/src/db.py new file mode 100644 index 000000000..2b1b9fa62 --- /dev/null +++ b/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/services/ripeness-baseline/src/evaluate_minio.py b/services/ripeness-baseline/src/evaluate_minio.py new file mode 100644 index 000000000..4296dd806 --- /dev/null +++ b/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/services/ripeness-baseline/src/heuristics.py b/services/ripeness-baseline/src/heuristics.py new file mode 100644 index 000000000..f21671b55 --- /dev/null +++ b/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/services/ripeness-baseline/src/main.py b/services/ripeness-baseline/src/main.py new file mode 100644 index 000000000..574b42af8 --- /dev/null +++ b/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/services/ripeness-baseline/src/quality.py b/services/ripeness-baseline/src/quality.py new file mode 100644 index 000000000..df94b1d2e --- /dev/null +++ b/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/services/ripeness-baseline/src/segment.py b/services/ripeness-baseline/src/segment.py new file mode 100644 index 000000000..61b97c2ad --- /dev/null +++ b/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/services/ripeness-baseline/src/tune_thresholds_minio.py b/services/ripeness-baseline/src/tune_thresholds_minio.py new file mode 100644 index 000000000..4088330a3 --- /dev/null +++ b/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/services/ripeness-baseline/thresholds.apple.json b/services/ripeness-baseline/thresholds.apple.json new file mode 100644 index 000000000..78af32dec --- /dev/null +++ b/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/services/ripeness-baseline/thresholds.banana.json b/services/ripeness-baseline/thresholds.banana.json new file mode 100644 index 000000000..026865080 --- /dev/null +++ b/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/services/ripeness-baseline/thresholds.orange.json b/services/ripeness-baseline/thresholds.orange.json new file mode 100644 index 000000000..17ba13a6a --- /dev/null +++ b/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/services/ripeness-ml/.gitignore b/services/ripeness-ml/.gitignore new file mode 100644 index 000000000..17751b717 --- /dev/null +++ b/services/ripeness-ml/.gitignore @@ -0,0 +1,7 @@ +__pycache__/ +*.onnx +datasets/ +*.png +.venv/ +venv/ +ENV/ \ No newline at end of file diff --git a/services/ripeness-ml/README.md b/services/ripeness-ml/README.md new file mode 100644 index 000000000..19de71518 --- /dev/null +++ b/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/services/ripeness-ml/api/ripeness_api.py b/services/ripeness-ml/api/ripeness_api.py new file mode 100644 index 000000000..0ea1f6991 --- /dev/null +++ b/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/services/ripeness-ml/checkpoints/eval/classification_report.txt b/services/ripeness-ml/checkpoints/eval/classification_report.txt new file mode 100644 index 000000000..f2e2f5a54 --- /dev/null +++ b/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/services/ripeness-ml/checkpoints/eval/metrics.json b/services/ripeness-ml/checkpoints/eval/metrics.json new file mode 100644 index 000000000..00bdd6890 --- /dev/null +++ b/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/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional.pt b/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional.pt new file mode 100644 index 000000000..0637d2fd0 Binary files /dev/null and b/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional.pt differ diff --git a/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_frozen.pt b/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_frozen.pt new file mode 100644 index 000000000..29b9cfb1c Binary files /dev/null and b/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_frozen.pt differ diff --git a/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_unfrozen.pt b/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_unfrozen.pt new file mode 100644 index 000000000..c9ef651e1 Binary files /dev/null and b/services/ripeness-ml/checkpoints/mobilenet_v3_large/best_conditional_unfrozen.pt differ diff --git a/services/ripeness-ml/configs/config.yaml b/services/ripeness-ml/configs/config.yaml new file mode 100644 index 000000000..bfd6c7863 --- /dev/null +++ b/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/services/ripeness-ml/deploy/Dockerfile b/services/ripeness-ml/deploy/Dockerfile new file mode 100644 index 000000000..a232874d7 --- /dev/null +++ b/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/services/ripeness-ml/deploy/docker-compose.ripeness.yml b/services/ripeness-ml/deploy/docker-compose.ripeness.yml new file mode 100644 index 000000000..0ac0a30f2 --- /dev/null +++ b/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/services/ripeness-ml/jobs/weekly_ripeness_job.py b/services/ripeness-ml/jobs/weekly_ripeness_job.py new file mode 100644 index 000000000..387b7c034 --- /dev/null +++ b/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/services/ripeness-ml/model/architecture/mobilenet_v3_large_head.py b/services/ripeness-ml/model/architecture/mobilenet_v3_large_head.py new file mode 100644 index 000000000..3457d6953 --- /dev/null +++ b/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/services/ripeness-ml/model/data/data_multitask.py b/services/ripeness-ml/model/data/data_multitask.py new file mode 100644 index 000000000..9b01959a3 --- /dev/null +++ b/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/services/ripeness-ml/model/data/transforms.py b/services/ripeness-ml/model/data/transforms.py new file mode 100644 index 000000000..a8f2c4b5b --- /dev/null +++ b/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/services/ripeness-ml/model/training/evaluate_conditional.py b/services/ripeness-ml/model/training/evaluate_conditional.py new file mode 100644 index 000000000..e10977e01 --- /dev/null +++ b/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/services/ripeness-ml/model/training/train_conditional.py b/services/ripeness-ml/model/training/train_conditional.py new file mode 100644 index 000000000..c2bf56357 --- /dev/null +++ b/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/services/ripeness-ml/model/training/utils.py b/services/ripeness-ml/model/training/utils.py new file mode 100644 index 000000000..23e47edc7 --- /dev/null +++ b/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/services/ripeness-ml/requirements.txt b/services/ripeness-ml/requirements.txt new file mode 100644 index 000000000..0aa2be591 --- /dev/null +++ b/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/services/ripeness-ml/tools/data_prep/ingest_kaggle_multitask.py b/services/ripeness-ml/tools/data_prep/ingest_kaggle_multitask.py new file mode 100644 index 000000000..46daeed76 --- /dev/null +++ b/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/services/ripeness-ml/tools/data_prep/prepare_from_minio.py b/services/ripeness-ml/tools/data_prep/prepare_from_minio.py new file mode 100644 index 000000000..0afb84c54 --- /dev/null +++ b/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/services/ripeness-ml/tools/export/export_onnx_conditional.py b/services/ripeness-ml/tools/export/export_onnx_conditional.py new file mode 100644 index 000000000..1431ea0e6 --- /dev/null +++ b/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/services/ripeness-ml/tools/inference/infer_minio_batch.py b/services/ripeness-ml/tools/inference/infer_minio_batch.py new file mode 100644 index 000000000..0206a3791 --- /dev/null +++ b/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/services/ripeness_exporter/Dockerfile b/services/ripeness_exporter/Dockerfile new file mode 100644 index 000000000..a2e91e0f9 --- /dev/null +++ b/services/ripeness_exporter/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt /app/ +RUN pip install --no-cache-dir -r requirements.txt + +COPY ripeness_exporter.py /app/ + +CMD ["python", "ripeness_exporter.py"] diff --git a/services/ripeness_exporter/requirements.txt b/services/ripeness_exporter/requirements.txt new file mode 100644 index 000000000..416c12aa4 --- /dev/null +++ b/services/ripeness_exporter/requirements.txt @@ -0,0 +1,2 @@ +prometheus_client +psycopg2-binary diff --git a/services/ripeness_exporter/ripeness_exporter.py b/services/ripeness_exporter/ripeness_exporter.py new file mode 100644 index 000000000..60f42215b --- /dev/null +++ b/services/ripeness_exporter/ripeness_exporter.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 +import time +import psycopg2 +from prometheus_client import start_http_server, Gauge +import signal +import sys + +# ====== CONFIG ====== +DB_HOST = "postgres" +DB_PORT = 5432 +DB_USER = "missions_user" +DB_PASS = "pg123" +DB_NAME = "missions_db" + +SCRAPE_INTERVAL = 30 # seconds +THRESHOLD_TASK = "ripeness" +THRESHOLD_LABEL = "" # empty label as defined +PROMETHEUS_PORT = 9128 + +# ====== METRICS ====== + +# Ripeness weekly rollups +g_pct = Gauge("fruit_ripeness_pct", "Weekly ripeness pct (0-1)", ["device"]) +g_cnt_ripe = Gauge("fruit_ripeness_cnt_ripe", "Count ripe", ["device"]) +g_cnt_unripe = Gauge("fruit_ripeness_cnt_unripe", "Count unripe", ["device"]) +g_cnt_overripe = Gauge("fruit_ripeness_cnt_overripe", "Count overripe", ["device"]) +g_cnt_total = Gauge("fruit_ripeness_cnt_total", "Total fruits counted", ["device"]) + +# Threshold + alert state (based on rollup) +g_alert_state = Gauge("fruit_ripeness_alert_state", "1 if below threshold", ["device"]) +g_threshold = Gauge("fruit_ripeness_threshold", "Ripeness threshold global") + +# ====== NEW ALERT METRICS (From alerts table) ====== + +g_alerts_total = Gauge( + "fruit_ripeness_alerts_total", + "Total number of ripeness alerts for a device", + ["device"] +) + +g_alert_last_pct = Gauge( + "fruit_ripeness_alert_last_pct", + "pct_ripe from the last ripeness alert", + ["device"] +) + +g_alert_active = Gauge( + "fruit_ripeness_alert_active", + "1 if device currently has a ripeness alert (based on alerts table)", + ["device"] +) + + +# ====== DB CONNECT ====== +def get_conn(): + return psycopg2.connect( + host=DB_HOST, + port=DB_PORT, + user=DB_USER, + password=DB_PASS, + dbname=DB_NAME, + ) + + +# ====== MAIN LOOP ====== +def run_loop(): + while True: + try: + conn = get_conn() + cur = conn.cursor() + + # ======================= + # FETCH threshold + # ======================= + cur.execute(""" + SELECT threshold + FROM task_thresholds + WHERE task = %s AND label = %s + LIMIT 1; + """, (THRESHOLD_TASK, THRESHOLD_LABEL)) + row = cur.fetchone() + threshold = float(row[0]) if row else 0.75 + g_threshold.set(threshold) + + # ======================= + # FETCH latest weekly rollups + # ======================= + cur.execute(""" + SELECT device_id, pct_ripe, cnt_ripe, cnt_unripe, + cnt_overripe, cnt_total + FROM ripeness_weekly_rollups_ts + WHERE window_end = ( + SELECT MAX(window_end) + FROM ripeness_weekly_rollups_ts + ); + """) + rows = cur.fetchall() + + for device_id, pct, c_r, c_u, c_o, c_t in rows: + dev = device_id or "unknown" + + # SET rollup metrics + g_pct.labels(dev).set(pct) + g_cnt_ripe.labels(dev).set(c_r) + g_cnt_unripe.labels(dev).set(c_u) + g_cnt_overripe.labels(dev).set(c_o) + g_cnt_total.labels(dev).set(c_t) + + # SET alert state from rollup + alert_state = 1 if pct < threshold else 0 + g_alert_state.labels(dev).set(alert_state) + + # ======================= + # ALERTS TABLE → Prometheus + # ======================= + + # 1) Count alerts + cur.execute(""" + SELECT COUNT(*) + FROM alerts + WHERE alert_type LIKE 'ripeness%%' + AND device_id = %s; + """, (dev,)) + alert_count = cur.fetchone()[0] + g_alerts_total.labels(dev).set(alert_count) + + # 2) Last alert pct_ripe + cur.execute(""" + SELECT meta->>'pct_ripe' + FROM alerts + WHERE alert_type LIKE 'ripeness%%' + AND device_id = %s + ORDER BY started_at DESC + LIMIT 1; + """, (dev,)) + last_alert = cur.fetchone() + + if last_alert and last_alert[0] is not None: + g_alert_last_pct.labels(dev).set(float(last_alert[0])) + g_alert_active.labels(dev).set(1) + else: + g_alert_last_pct.labels(dev).set(0) + g_alert_active.labels(dev).set(0) + + cur.close() + conn.close() + + except Exception as e: + print("ERROR:", e) + + time.sleep(SCRAPE_INTERVAL) + + +def shutdown(sig, frame): + print("Shutting down exporter...") + sys.exit(0) + + +if __name__ == "__main__": + signal.signal(signal.SIGINT, shutdown) + signal.signal(signal.SIGTERM, shutdown) + + print(f"Starting Prometheus exporter on port {PROMETHEUS_PORT} ...") + start_http_server(PROMETHEUS_PORT) + run_loop() diff --git a/services/security/.dockerignore b/services/security/.dockerignore new file mode 100644 index 000000000..139597f9c --- /dev/null +++ b/services/security/.dockerignore @@ -0,0 +1,2 @@ + + diff --git a/services/security/.gitignore b/services/security/.gitignore new file mode 100644 index 000000000..092b0f3d7 --- /dev/null +++ b/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/services/security/README.md b/services/security/README.md new file mode 100644 index 000000000..e178059e1 --- /dev/null +++ b/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/services/security/agguard/__init__.py b/services/security/agguard/__init__.py new file mode 100644 index 000000000..883256898 --- /dev/null +++ b/services/security/agguard/__init__.py @@ -0,0 +1,9 @@ +# __all__ = [ +# "logging_utils", +# "types", +# "roi", +# "motion", +# "detector", +# "tracker", +# "dispatch", +# ] diff --git a/services/security/agguard/adapters/__init__.py b/services/security/agguard/adapters/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/security/agguard/adapters/s3_client.py b/services/security/agguard/adapters/s3_client.py new file mode 100644 index 000000000..905bca200 --- /dev/null +++ b/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/services/security/agguard/app/Dockerfile b/services/security/agguard/app/Dockerfile new file mode 100644 index 000000000..c7a526983 --- /dev/null +++ b/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/services/security/agguard/app/__init__.py b/services/security/agguard/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/security/agguard/app/media_proxy.py b/services/security/agguard/app/media_proxy.py new file mode 100644 index 000000000..43311b564 --- /dev/null +++ b/services/security/agguard/app/media_proxy.py @@ -0,0 +1,232 @@ +# 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 botocore.exceptions import ClientError +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) +): + _require_auth(request) + key = _object_key_for_hls(camera, incident, "final.mp4") + + + + try: + obj = s3.get_object_stream(BUCKET, key, range_header=range) + + except ClientError as e: + code = e.response["Error"]["Code"] + + # File NOT found → real 404 + if code in ("NoSuchKey", "404"): + raise HTTPException(status_code=404, detail="VOD not found") + + # VLC requests ranges beyond file end → MUST return 416, NOT 404 + if code == "InvalidRange": + raise HTTPException(status_code=416, detail="Invalid Range") + + # Any other S3 problem is a server issue + raise HTTPException(status_code=502, detail=f"S3 error: {code}") + + # Non-S3 exceptions (timeout, disconnect, etc.) + except Exception as e: + raise HTTPException(status_code=502, detail=f"Internal proxy error: {type(e).__name__}") + + + body = obj["Body"] + content_length = obj.get("ContentLength") + content_range = obj.get("ContentRange") or obj.get("Content-Range") + + headers = { + "Accept-Ranges": "bytes", + "Content-Type": _MP4_CT, + } + + # Important for VLC + if "ContentLength" in obj: + headers["Content-Length"] = str(obj["ContentLength"]) + + + status = 200 + if content_range: + headers["Content-Range"] = content_range + status = 206 + if content_length and status == 200: + headers["Content-Length"] = str(content_length) + + # --- SAFE STREAMING GENERATOR --- + def stream_body(): + try: + while True: + chunk = body.read(256 * 1024) + + # S3 can return b'' BEFORE true EOF → retry once + if chunk == b"": + more = body.read(256 * 1024) + if more == b"": + break + yield more + continue + + if not chunk: + break + + yield chunk + + except Exception as e: + print("[MEDIA_PROXY][STREAM ERROR]", e) + + return StreamingResponse( + stream_body(), + 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/services/security/agguard/app/requirements.txt b/services/security/agguard/app/requirements.txt new file mode 100644 index 000000000..df7b90ec6 --- /dev/null +++ b/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/services/security/agguard/core/__init__.py b/services/security/agguard/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/security/agguard/core/events/__init__.py b/services/security/agguard/core/events/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/security/agguard/core/events/aggregator.py b/services/security/agguard/core/events/aggregator.py new file mode 100644 index 000000000..01150e869 --- /dev/null +++ b/services/security/agguard/core/events/aggregator.py @@ -0,0 +1,698 @@ +# 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, threading + +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. + """ + 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 + + # ------------- HLS deletion scheduler ------------- + + def _schedule_hls_deletion(self, hls_recorder, delay_sec: float = 120.0) -> None: + """ + Delete HLS content after a delay (default 2 minutes), in a background thread. + """ + if not hls_recorder: + return + + def _worker(): + try: + log.info( + "[_schedule_hls_deletion] Sleeping %.1f seconds before deleting HLS...", + delay_sec, + ) + time.sleep(delay_sec) + + try: + hls_recorder.delete_remote_hls() + except Exception as e: + log.exception("[_schedule_hls_deletion] remote delete error: %s", e) + + try: + hls_recorder.delete_hls_files_only() + except Exception as e: + log.exception("[_schedule_hls_deletion] local delete error: %s", e) + + log.info("[_schedule_hls_deletion] HLS cleanup finished.") + except Exception as e: + log.exception("[_schedule_hls_deletion] Unexpected error: %s", e) + + threading.Thread(target=_worker, daemon=True).start() + + # ------------- open / close incidents ------------- + + 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, + ) + else: + hls_url = None + vod_url = None + + 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), + } + 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, + ) + + mp4_key = None + + 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, + ) + + 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 hasattr(st, "_mp4") and st._mp4 else None + log.info( + "[_close_incident] finalize_to_mp4() returned mp4_key=%s", + mp4_key, + ) + + # 3️⃣ Schedule HLS deletion in 2 minutes + self._schedule_hls_deletion(st._hls, delay_sec=120.0) + + except Exception as e: + log.exception("[_close_incident] Cleanup error: %s", e) + except Exception as e: + log.exception("[_close_incident] Error finalizing MP4: %s", e) + else: + log.info( + "[_close_incident] Skipping MP4 finalization — missing s3/video_bucket or no _hls" + ) + + # Reset state for this (camera, rule) + 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, []) + + 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) diff --git a/services/security/agguard/core/events/models.py b/services/security/agguard/core/events/models.py new file mode 100644 index 000000000..307719827 --- /dev/null +++ b/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/services/security/agguard/core/motion.py b/services/security/agguard/core/motion.py new file mode 100644 index 000000000..41ebccb8d --- /dev/null +++ b/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/services/security/agguard/core/roi.py b/services/security/agguard/core/roi.py new file mode 100644 index 000000000..0b7f3a07d --- /dev/null +++ b/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/services/security/agguard/core/tracker.py b/services/security/agguard/core/tracker.py new file mode 100644 index 000000000..df92e44cc --- /dev/null +++ b/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/services/security/agguard/core/types.py b/services/security/agguard/core/types.py new file mode 100644 index 000000000..cee091213 --- /dev/null +++ b/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/services/security/agguard/logging_utils.py b/services/security/agguard/logging_utils.py new file mode 100644 index 000000000..b1154e2c8 --- /dev/null +++ b/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/services/security/agguard/media/hls_recorder.py b/services/security/agguard/media/hls_recorder.py new file mode 100644 index 000000000..2a91a95c7 --- /dev/null +++ b/services/security/agguard/media/hls_recorder.py @@ -0,0 +1,626 @@ +# 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) + + # Minimum delay between stop() and any delete_*() + MIN_DELETE_DELAY: float = 120.0 # seconds + + _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) + + # State after stop() + _stopped_tmpdir: Optional[str] = None + _stopped_at: Optional[float] = None + + # ──────────────────────────────────────────────────────────────────────── + # 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) + + # reset stop metadata on reuse + self._stopped_tmpdir = None + self._stopped_at = None + + 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. + 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 + 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 + + existing = {q.name for q in segs} + if self.cfg.use_cmaf: + existing |= {q.name for q in init_files} + + publishable = self._make_publishable_index(p, existing) + if not publishable: + 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 + + # ──────────────────────────────────────────────────────────────────────── + # Stop + # ──────────────────────────────────────────────────────────────────────── + def stop(self): + """Stop all threads and kill ffmpeg reliably (no deletion here).""" + 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 Exception: + 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 Exception: + pass + + finally: + # Always mark tmpdir as stopped + self._stopped_tmpdir = self._tmpdir + self._stopped_at = time.time() + self._tmpdir = None + self._proc = None + print(f"[HlsRecorder] stop() called; _stopped_at={self._stopped_at:.3f}, dir={self._stopped_tmpdir}") + + # ──────────────────────────────────────────────────────────────────────── + # Playlist freezing / reconstruction for finalize (unchanged) + # ──────────────────────────────────────────────────────────────────────── + def _freeze_local_playlist(self, m3u8_path: pathlib.Path, workdir: pathlib.Path) -> pathlib.Path: + 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) + continue + 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 + + # ──────────────────────────────────────────────────────────────────────── + # Deletion helpers (with hard guard) + # ──────────────────────────────────────────────────────────────────────── + def _can_delete(self, kind: str) -> bool: + """ + Common guard: we only allow delete if stop() was called and at least + MIN_DELETE_DELAY seconds have passed. + """ + if self._stopped_at is None: + print(f"[HlsRecorder] ⛔ {kind}: stop() was never called → NOT deleting.") + return False + + dt = time.time() - self._stopped_at + if dt < self.MIN_DELETE_DELAY: + print( + f"[HlsRecorder] ⏳ {kind}: only {dt:.1f}s since stop " + f"(need {self.MIN_DELETE_DELAY}s) → NOT deleting." + ) + return False + + print(f"[HlsRecorder] ✅ {kind}: {dt:.1f}s since stop → OK to delete.") + return True + + 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. + """ + if not self._can_delete("local delete"): + return + + base = self._stopped_tmpdir 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 (local):") + 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 (local):") + 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 the underlying boto client for efficiency. + """ + if not self._can_delete("remote delete"): + return + + prefix = self.prefix.rstrip("/") + "/" + + print("[HLS] Deleting remote HLS under prefix:", 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 + + 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 + + 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/services/security/agguard/media/mp4_recorder.py b/services/security/agguard/media/mp4_recorder.py new file mode 100644 index 000000000..2d9f26712 --- /dev/null +++ b/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/services/security/agguard/metrics/__init__.py b/services/security/agguard/metrics/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/security/agguard/metrics/monitoring.py b/services/security/agguard/metrics/monitoring.py new file mode 100644 index 000000000..829ed9fed --- /dev/null +++ b/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/services/security/agguard/pipeline/Dockerfile b/services/security/agguard/pipeline/Dockerfile new file mode 100644 index 000000000..c3543193f --- /dev/null +++ b/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/services/security/agguard/pipeline/__init__.py b/services/security/agguard/pipeline/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/security/agguard/pipeline/flink_job.py b/services/security/agguard/pipeline/flink_job.py new file mode 100644 index 000000000..ce835db7c --- /dev/null +++ b/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/services/security/agguard/pipeline/flink_manager.py b/services/security/agguard/pipeline/flink_manager.py new file mode 100644 index 000000000..3f22b9a0a --- /dev/null +++ b/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/services/security/agguard/proto/__init__.py b/services/security/agguard/proto/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/security/agguard/proto/ingest.proto b/services/security/agguard/proto/ingest.proto new file mode 100644 index 000000000..4685d4160 --- /dev/null +++ b/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/services/security/agguard/proto/mask_classifier.proto b/services/security/agguard/proto/mask_classifier.proto new file mode 100644 index 000000000..134eaa5f6 --- /dev/null +++ b/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/services/security/agguard/proto/mega_detector.proto b/services/security/agguard/proto/mega_detector.proto new file mode 100644 index 000000000..4477399d9 --- /dev/null +++ b/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/services/security/agguard/proto_gen/__init__.py b/services/security/agguard/proto_gen/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/security/agguard/specialists/__init__.py b/services/security/agguard/specialists/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/security/agguard/specialists/animal_service/Dockerfile.animal-classifier b/services/security/agguard/specialists/animal_service/Dockerfile.animal-classifier new file mode 100644 index 000000000..a4e70176e --- /dev/null +++ b/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/services/security/agguard/specialists/animal_service/requirements.txt b/services/security/agguard/specialists/animal_service/requirements.txt new file mode 100644 index 000000000..fdeb34e04 --- /dev/null +++ b/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/services/security/agguard/specialists/animal_service/server.py b/services/security/agguard/specialists/animal_service/server.py new file mode 100644 index 000000000..2a2782df5 --- /dev/null +++ b/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/services/security/agguard/specialists/anomalies_service/Dockerfile.anomalies-classifier b/services/security/agguard/specialists/anomalies_service/Dockerfile.anomalies-classifier new file mode 100644 index 000000000..a92b67060 --- /dev/null +++ b/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/services/security/agguard/specialists/anomalies_service/server.py b/services/security/agguard/specialists/anomalies_service/server.py new file mode 100644 index 000000000..3af07421a --- /dev/null +++ b/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/services/security/agguard/specialists/clients/animal.py b/services/security/agguard/specialists/clients/animal.py new file mode 100644 index 000000000..042e8ab19 --- /dev/null +++ b/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/services/security/agguard/specialists/clients/anomalies.py b/services/security/agguard/specialists/clients/anomalies.py new file mode 100644 index 000000000..025a9619a --- /dev/null +++ b/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/services/security/agguard/specialists/clients/mask.py b/services/security/agguard/specialists/clients/mask.py new file mode 100644 index 000000000..3d016766d --- /dev/null +++ b/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/services/security/agguard/specialists/clients/megadetector.py b/services/security/agguard/specialists/clients/megadetector.py new file mode 100644 index 000000000..b41ad5cd7 --- /dev/null +++ b/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/services/security/agguard/specialists/dispatch.py b/services/security/agguard/specialists/dispatch.py new file mode 100644 index 000000000..a01c70e68 --- /dev/null +++ b/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/services/security/agguard/specialists/mask_service/Dockerfile.mask-classifier b/services/security/agguard/specialists/mask_service/Dockerfile.mask-classifier new file mode 100644 index 000000000..a3e8e52e8 --- /dev/null +++ b/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/services/security/agguard/specialists/mask_service/mask_classifier.py b/services/security/agguard/specialists/mask_service/mask_classifier.py new file mode 100644 index 000000000..3298cbc48 --- /dev/null +++ b/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/services/security/agguard/specialists/mask_service/requirements.txt b/services/security/agguard/specialists/mask_service/requirements.txt new file mode 100644 index 000000000..90663ea1d --- /dev/null +++ b/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/services/security/agguard/specialists/mask_service/server.py b/services/security/agguard/specialists/mask_service/server.py new file mode 100644 index 000000000..10fefeeca --- /dev/null +++ b/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/services/security/agguard/specialists/megadetector_service/Dockerfile.mega-detector b/services/security/agguard/specialists/megadetector_service/Dockerfile.mega-detector new file mode 100644 index 000000000..efda2605b --- /dev/null +++ b/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/services/security/agguard/specialists/megadetector_service/__init__.py b/services/security/agguard/specialists/megadetector_service/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/security/agguard/specialists/megadetector_service/requirements.txt b/services/security/agguard/specialists/megadetector_service/requirements.txt new file mode 100644 index 000000000..a587197e4 --- /dev/null +++ b/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/services/security/agguard/specialists/megadetector_service/server.py b/services/security/agguard/specialists/megadetector_service/server.py new file mode 100644 index 000000000..3dd50dbf9 --- /dev/null +++ b/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/services/security/configs/default.yaml b/services/security/configs/default.yaml new file mode 100644 index 000000000..2460eabb7 --- /dev/null +++ b/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/services/sensorAnomalyPro/.gitignore b/services/sensorAnomalyPro/.gitignore new file mode 100644 index 000000000..3f591f5ad --- /dev/null +++ b/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/services/sensorAnomalyPro/Dockerfile.flink b/services/sensorAnomalyPro/Dockerfile.flink new file mode 100644 index 000000000..801086691 --- /dev/null +++ b/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/services/sensorAnomalyPro/README.md b/services/sensorAnomalyPro/README.md new file mode 100644 index 000000000..e41cfba47 --- /dev/null +++ b/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/services/sensorAnomalyPro/docker-compose.yml b/services/sensorAnomalyPro/docker-compose.yml new file mode 100644 index 000000000..bf4ac0fd4 --- /dev/null +++ b/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/services/sensorAnomalyPro/requirements.txt b/services/sensorAnomalyPro/requirements.txt new file mode 100644 index 000000000..12ce1818c --- /dev/null +++ b/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/services/sensorAnomalyPro/sensorAnomalyPro/Dockerfile b/services/sensorAnomalyPro/sensorAnomalyPro/Dockerfile new file mode 100644 index 000000000..0600f3286 --- /dev/null +++ b/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/services/sensorAnomalyPro/sensorAnomalyPro/__init__.py b/services/sensorAnomalyPro/sensorAnomalyPro/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/sensorAnomalyPro/sensorAnomalyPro/analyze_sensors.py b/services/sensorAnomalyPro/sensorAnomalyPro/analyze_sensors.py new file mode 100644 index 000000000..329dbd11a --- /dev/null +++ b/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/services/sensorAnomalyPro/sensorAnomalyPro/app.py b/services/sensorAnomalyPro/sensorAnomalyPro/app.py new file mode 100644 index 000000000..675796b1e --- /dev/null +++ b/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_name") + 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("sensor_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", "sensors") + + 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/services/sensorAnomalyPro/sensorAnomalyPro/data/plant_health_data.csv b/services/sensorAnomalyPro/sensorAnomalyPro/data/plant_health_data.csv new file mode 100644 index 000000000..59c68ee4e --- /dev/null +++ b/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/services/sensorAnomalyPro/sensorAnomalyPro/profiles_runtime.py b/services/sensorAnomalyPro/sensorAnomalyPro/profiles_runtime.py new file mode 100644 index 000000000..36dfcef67 --- /dev/null +++ b/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/services/sensorAnomalyPro/sensorAnomalyPro/requirements.txt b/services/sensorAnomalyPro/sensorAnomalyPro/requirements.txt new file mode 100644 index 000000000..12ce1818c --- /dev/null +++ b/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/services/sensorAnomalyPro/sensorAnomalyPro/tests/conftest.py b/services/sensorAnomalyPro/sensorAnomalyPro/tests/conftest.py new file mode 100644 index 000000000..a6ae30046 --- /dev/null +++ b/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/services/sensorAnomalyPro/sensorAnomalyPro/tests/pytest.ini b/services/sensorAnomalyPro/sensorAnomalyPro/tests/pytest.ini new file mode 100644 index 000000000..de40b9629 --- /dev/null +++ b/services/sensorAnomalyPro/sensorAnomalyPro/tests/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = -q --maxfail=1 --disable-warnings diff --git a/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_app_score.py b/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_app_score.py new file mode 100644 index 000000000..bb3357a7c --- /dev/null +++ b/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/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_detect_flags.py b/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_detect_flags.py new file mode 100644 index 000000000..a79c13d00 --- /dev/null +++ b/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/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_profiles_runtime.py b/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_profiles_runtime.py new file mode 100644 index 000000000..ea8147234 --- /dev/null +++ b/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/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_read_clean.py b/services/sensorAnomalyPro/sensorAnomalyPro/tests/test_read_clean.py new file mode 100644 index 000000000..d8b39c3d5 --- /dev/null +++ b/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/services/sensorAnomalyPro/sensorAnomalyPro/zones.geojson b/services/sensorAnomalyPro/sensorAnomalyPro/zones.geojson new file mode 100644 index 000000000..423245ec0 --- /dev/null +++ b/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/services/sensorGuard/Dockerfile.flink b/services/sensorGuard/Dockerfile.flink new file mode 100644 index 000000000..8d94314f5 --- /dev/null +++ b/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/services/sensorGuard/README.md b/services/sensorGuard/README.md new file mode 100644 index 000000000..e69de29bb diff --git a/services/sensorGuard/docker-compose.yml b/services/sensorGuard/docker-compose.yml new file mode 100644 index 000000000..bacbeb99d --- /dev/null +++ b/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/services/sensorGuard/entrypoint.sh b/services/sensorGuard/entrypoint.sh new file mode 100644 index 000000000..a301d88f6 --- /dev/null +++ b/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/services/sensorGuard/flink_app/__init__.py b/services/sensorGuard/flink_app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/sensorGuard/flink_app/api/__init__.py b/services/sensorGuard/flink_app/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/sensorGuard/flink_app/api/auth.py b/services/sensorGuard/flink_app/api/auth.py new file mode 100644 index 000000000..3458232aa --- /dev/null +++ b/services/sensorGuard/flink_app/api/auth.py @@ -0,0 +1,99 @@ +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}") + +# === 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]}") + 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") + return raw.strip() + except Exception as e: + print(f"[AUTH] Exception: {e}") + time.sleep(backoff * attempt) + print("[AUTH] Failed to bootstrap service token") + return None + +# === PUBLIC API === +def validate_token(base: str, token: str) -> bool: + """ + Test if token is valid by making a simple API call. + Returns True if token works, False otherwise. + + NOTE: Disabled because /api/me endpoint doesn't exist. + We just assume the token is valid and let actual API calls fail if needed. + """ + # url = _safe_join_url(base, "/api/me") + # try: + # r = requests.get(url, headers={"X-Service-Token": token}, timeout=5) + # return r.status_code == 200 + # except Exception: + # return False + + # Skip validation - assume token is valid + return True if token else False + + +def get_access_token(base_url: str | None = None) -> str: + """ + Loads token from file if exists and valid, 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 exists, validate it first + if token: + if validate_token(base, token): + print("[AUTH] Existing token is valid") + return token + else: + print("[AUTH] Existing token is invalid, fetching new one...") + + 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/services/sensorGuard/flink_app/api/devices_client.py b/services/sensorGuard/flink_app/api/devices_client.py new file mode 100644 index 000000000..a46a4f55f --- /dev/null +++ b/services/sensorGuard/flink_app/api/devices_client.py @@ -0,0 +1,81 @@ +""" +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 +from api.auth import get_access_token + + +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 = None, 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. If None, uses host.docker.internal (same as PATCH). + timeout: Request timeout. + + Returns: + List of dicts like: [{"id": "dev-a", "sensor_type": "temp", "last_seen": "2025-11-11T13:00:00Z"}, ...] + """ + # Use same URL pattern as update_device_last_seen (PATCH) + if api_base is None: + api_base = "http://host.docker.internal:8001" + + # Get fresh token each time (same pattern as update_device_last_seen) + token = get_access_token(api_base) + + 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/services/sensorGuard/flink_app/api/devices_updater.py b/services/sensorGuard/flink_app/api/devices_updater.py new file mode 100644 index 000000000..534d2893f --- /dev/null +++ b/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/services/sensorGuard/flink_app/config/__init__.py b/services/sensorGuard/flink_app/config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/sensorGuard/flink_app/config/rules.yaml b/services/sensorGuard/flink_app/config/rules.yaml new file mode 100644 index 000000000..dd718a617 --- /dev/null +++ b/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/services/sensorGuard/flink_app/config/settings.py b/services/sensorGuard/flink_app/config/settings.py new file mode 100644 index 000000000..59853c063 --- /dev/null +++ b/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/services/sensorGuard/flink_app/core/__init__.py b/services/sensorGuard/flink_app/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/sensorGuard/flink_app/core/engine.py b/services/sensorGuard/flink_app/core/engine.py new file mode 100644 index 000000000..f4b897b93 --- /dev/null +++ b/services/sensorGuard/flink_app/core/engine.py @@ -0,0 +1,182 @@ +# 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 +import os + +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 = os.getenv("DB_API_BASE", "http://db_api_service: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 - uses same pattern as PATCH (host.docker.internal) + sensors = get_sensors_last_seen() + + 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/services/sensorGuard/flink_app/core/rules.py b/services/sensorGuard/flink_app/core/rules.py new file mode 100644 index 000000000..36f22d537 --- /dev/null +++ b/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/services/sensorGuard/flink_app/core/state.py b/services/sensorGuard/flink_app/core/state.py new file mode 100644 index 000000000..5efa13420 --- /dev/null +++ b/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/services/sensorGuard/flink_app/core/types.py b/services/sensorGuard/flink_app/core/types.py new file mode 100644 index 000000000..b256537fb --- /dev/null +++ b/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/services/sensorGuard/flink_app/io_mod/__init__.py b/services/sensorGuard/flink_app/io_mod/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/sensorGuard/flink_app/io_mod/writer_console.py b/services/sensorGuard/flink_app/io_mod/writer_console.py new file mode 100644 index 000000000..2f62cb0d4 --- /dev/null +++ b/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/services/sensorGuard/flink_app/io_mod/writer_kafka.py b/services/sensorGuard/flink_app/io_mod/writer_kafka.py new file mode 100644 index 000000000..2ffb6cfd4 --- /dev/null +++ b/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/services/sensorGuard/flink_app/main.py b/services/sensorGuard/flink_app/main.py new file mode 100644 index 000000000..2fe44b93b --- /dev/null +++ b/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("sensor_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("DB_API_BASE", "http://db_api_service: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/services/sensorGuard/flink_app/resources/synthetic_stream.csv b/services/sensorGuard/flink_app/resources/synthetic_stream.csv new file mode 100644 index 000000000..71ee492ee --- /dev/null +++ b/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/services/sensorGuard/pytest.ini b/services/sensorGuard/pytest.ini new file mode 100644 index 000000000..678c989ca --- /dev/null +++ b/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/services/sensorGuard/requirements.txt b/services/sensorGuard/requirements.txt new file mode 100644 index 000000000..e70d76a76 --- /dev/null +++ b/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/services/sensorGuard/test-requirements.txt b/services/sensorGuard/test-requirements.txt new file mode 100644 index 000000000..59cd202e4 --- /dev/null +++ b/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/services/sensorGuard/tests/__init__.py b/services/sensorGuard/tests/__init__.py new file mode 100644 index 000000000..1d342c1a8 --- /dev/null +++ b/services/sensorGuard/tests/__init__.py @@ -0,0 +1 @@ +# Test package init \ No newline at end of file diff --git a/services/sensorGuard/tests/test_engine.py b/services/sensorGuard/tests/test_engine.py new file mode 100644 index 000000000..ba34aaaab --- /dev/null +++ b/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/services/sensorGuard/tests/test_engine_quality.py b/services/sensorGuard/tests/test_engine_quality.py new file mode 100644 index 000000000..6f6c7e9e1 --- /dev/null +++ b/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/services/sensorGuard/tests/test_state.py b/services/sensorGuard/tests/test_state.py new file mode 100644 index 000000000..bb2e385fa --- /dev/null +++ b/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/services/sensorGuard/tests/test_state_fixed.py b/services/sensorGuard/tests/test_state_fixed.py new file mode 100644 index 000000000..bb2e385fa --- /dev/null +++ b/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/services/sensorGuard/tests/test_state_quality.py b/services/sensorGuard/tests/test_state_quality.py new file mode 100644 index 000000000..a068af528 --- /dev/null +++ b/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/services/sensorGuard/tests/test_types.py b/services/sensorGuard/tests/test_types.py new file mode 100644 index 000000000..d049f6da3 --- /dev/null +++ b/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/services/sensorGuard/tests/test_types_fixed.py b/services/sensorGuard/tests/test_types_fixed.py new file mode 100644 index 000000000..d049f6da3 --- /dev/null +++ b/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/services/sensorGuard/tests/test_types_quality.py b/services/sensorGuard/tests/test_types_quality.py new file mode 100644 index 000000000..d72e23ce8 --- /dev/null +++ b/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/services/sound_metrics/Dockerfile b/services/sound_metrics/Dockerfile new file mode 100644 index 000000000..50c86e7ae --- /dev/null +++ b/services/sound_metrics/Dockerfile @@ -0,0 +1,62 @@ +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 + +<<<<<<< HEAD + +# COPY certs/ /app/certs/ +# RUN if [ -d /app/certs ] && ls /app/certs/*.crt >/dev/null 2>&1; then \ +# cp /app/certs/*.crt /usr/local/share/ca-certificates/ && update-ca-certificates; \ +======= +>>>>>>> 4bb2e60fc0fd9a846955fa89533d661a56b1645a +# 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 + + +COPY requirements.txt . +# 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 \ + # CURL_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/services/sound_metrics/requirements.txt b/services/sound_metrics/requirements.txt new file mode 100644 index 000000000..fb09ba3d6 --- /dev/null +++ b/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/services/sound_metrics/src/metrics.py b/services/sound_metrics/src/metrics.py new file mode 100644 index 000000000..126ef5fe9 --- /dev/null +++ b/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/services/sounds/API-development/.coverage b/services/sounds/API-development/.coverage deleted file mode 100644 index 0a26d044f..000000000 Binary files a/services/sounds/API-development/.coverage and /dev/null differ diff --git a/services/sounds/compression/.coverage b/services/sounds/compression/.coverage deleted file mode 100644 index 61d94bc14..000000000 Binary files a/services/sounds/compression/.coverage and /dev/null differ diff --git a/services/sounds/compression/results/benchmarks.csv b/services/sounds/compression/results/benchmarks.csv deleted file mode 100644 index b940286a6..000000000 --- a/services/sounds/compression/results/benchmarks.csv +++ /dev/null @@ -1,3 +0,0 @@ -file,codec,orig_bytes,encoded_bytes,compression_ratio_orig_over_encoded,encode_time_sec,encode_cpu_avg_percent -crying-animal-84745.mp3,flac,74400,314441,0.237,0.121,0.0 -crying-animal-84745.mp3,opus,74400,41630,1.787,0.121,0.0 diff --git a/services/sounds/compression/scripts/minio_client.py b/services/sounds/compression/scripts/minio_client.py deleted file mode 100644 index 585d4c8d2..000000000 --- a/services/sounds/compression/scripts/minio_client.py +++ /dev/null @@ -1,16 +0,0 @@ -from minio import Minio - -MINIO_ENDPOINT = "localhost:9001" -ACCESS_KEY = "minioadmin" -SECRET_KEY = "minioadmin123" -BUCKET_NAME = "compression" - -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/services/sounds/compression/scripts/prototype_lib.py b/services/sounds/compression/scripts/prototype_lib.py deleted file mode 100644 index bde806334..000000000 --- a/services/sounds/compression/scripts/prototype_lib.py +++ /dev/null @@ -1,58 +0,0 @@ -from pathlib import Path -import subprocess -import tempfile -import time -from minio_client import client, BUCKET_NAME - -INPUT_EXTS = {".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".opus"} - -RAW_PREFIX = "raw/" -COMP_PREFIX = "compressed/" - -def iter_input_files(): - """Yield MinIO object names in RAW_PREFIX with accepted extensions.""" - for obj in client.list_objects(BUCKET_NAME, prefix=RAW_PREFIX, recursive=True): - if any(obj.object_name.lower().endswith(ext) for ext in INPUT_EXTS): - yield obj.object_name - -def build_ffmpeg_cmds(in_local_path: Path, codec="all", flac_level="5", opus_bitrate="96k"): - """ - Return ffmpeg commands to encode a local 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 upload_compressed(local_path: Path): - """Upload encoded file to MinIO compressed folder.""" - target_name = f"{COMP_PREFIX}{int(time.time())}_{local_path.name}" - client.fput_object(BUCKET_NAME, target_name, str(local_path)) - return target_name - -def delete_object(obj_name: str): - client.remove_object(BUCKET_NAME, obj_name) \ No newline at end of file diff --git a/services/sounds/compression/scripts/run_bench.py b/services/sounds/compression/scripts/run_bench.py deleted file mode 100644 index d21f0f29c..000000000 --- a/services/sounds/compression/scripts/run_bench.py +++ /dev/null @@ -1,86 +0,0 @@ -from pathlib import Path -import time -import csv -from statistics import mean -import subprocess -from prototype_lib import iter_input_files, build_ffmpeg_cmds, download_raw_to_temp, upload_compressed, delete_object -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): - 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_input_files()) - if not files: - print("No raw files found in MinIO") - return - - for obj_name in files: - local_file = download_raw_to_temp(obj_name) - orig_size = local_file.stat().st_size - - for codec, cmd, outp in build_ffmpeg_cmds(local_file): - rc, wall, cpu, _ = run_and_profile(cmd) - if rc != 0: - print(f"[FAIL] {obj_name} ({codec})") - continue - - target_obj = upload_compressed(outp) - enc_size = file_size_minio(target_obj) - ratio = (orig_size / enc_size) if enc_size else 0.0 - - print(f"[OK] {obj_name} | {codec.upper()}: {enc_size} bytes, {wall:.2f}s, CPU~{cpu:.1f}% (ratio={ratio:.3f})") - - 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), - }) - - # Clean up local encoded file - outp.unlink() - - local_file.unlink() - - 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(f"Saved CSV: {out_csv}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/services/sounds/compression/scripts/tiering_job.py b/services/sounds/compression/scripts/tiering_job.py deleted file mode 100644 index b1c4b0e70..000000000 --- a/services/sounds/compression/scripts/tiering_job.py +++ /dev/null @@ -1,101 +0,0 @@ -from pathlib import Path -import time -import argparse -import subprocess -import tempfile -from prototype_lib import ( - iter_input_files, build_ffmpeg_cmds, download_raw_to_temp, - upload_compressed, delete_object, INPUT_EXTS -) -from minio_client import client, BUCKET_NAME - -DEFAULT_RAW_MAX_AGE_HOURS = 24 -DEFAULT_COMP_MAX_AGE_DAYS = 90 -DEFAULT_LONG_TERM_CODEC = "opus" - -def get_age_seconds(obj_name: str, mode: str = "mtime") -> float: - stat = client.stat_object(BUCKET_NAME, obj_name) - ts = getattr(stat, "last_modified", None) - if ts is None: - return 0 - # last_modified is datetime, convert to timestamp - return time.time() - ts.timestamp() - -def is_older_than(obj_name: str, age_seconds: int, mode: str) -> bool: - return get_age_seconds(obj_name, mode) >= age_seconds - -def encode_and_upload(obj_name: str, codec: str) -> str: - local_file = download_raw_to_temp(obj_name) - for c, cmd, outp in build_ffmpeg_cmds(local_file, codec=codec): - rc = subprocess.call(cmd) - if rc != 0: - raise RuntimeError(f"Encode failed: {obj_name} -> {codec}") - target_obj = upload_compressed(outp) - outp.unlink() - local_file.unlink() - return target_obj - -def cleanup_compressed(max_age_days: int, dry_run: bool) -> int: - if max_age_days <= 0: - return 0 - cutoff_sec = max_age_days * 86400 - deleted = 0 - for obj in client.list_objects(BUCKET_NAME, prefix="compressed/", recursive=True): - age = get_age_seconds(obj.object_name) - if age >= cutoff_sec: - if dry_run: - print(f"[DRY] would delete compressed: {obj.object_name}") - else: - delete_object(obj.object_name) - deleted += 1 - print(f"[DEL] compressed old: {obj.object_name}") - return deleted - -def main(): - ap = argparse.ArgumentParser(description="Two-tier storage job with MinIO") - ap.add_argument("--raw-max-age-hours", type=int, default=None) - ap.add_argument("--raw-max-age-minutes", type=int, default=None) - ap.add_argument("--age-mode", choices=["mtime", "ctime"], default="mtime") - ap.add_argument("--codec", choices=["opus", "flac"], default=DEFAULT_LONG_TERM_CODEC) - ap.add_argument("--delete-raw-after", action="store_true") - ap.add_argument("--compressed-max-age-days", type=int, default=DEFAULT_COMP_MAX_AGE_DAYS) - ap.add_argument("--dry-run", action="store_true") - args = ap.parse_args() - - if args.raw_max_age_minutes is not None: - raw_age_seconds = args.raw_max_age_minutes * 60 - elif args.raw_max_age_hours is not None: - raw_age_seconds = args.raw_max_age_hours * 3600 - else: - raw_age_seconds = DEFAULT_RAW_MAX_AGE_HOURS * 3600 - - processed, raw_deleted = 0, 0 - - for obj_name in iter_input_files(): - if is_older_than(obj_name, raw_age_seconds, args.age_mode): - if args.dry_run: - print(f"[DRY] would encode {obj_name} -> {args.codec.upper()}, delete RAW={args.delete_raw_after}") - processed += 1 - continue - - try: - target_obj = encode_and_upload(obj_name, args.codec) - print(f"[OK] {obj_name} -> {target_obj} ({args.codec})") - processed += 1 - - if args.delete_raw_after: - delete_object(obj_name) - raw_deleted += 1 - print(f"[DEL] raw: {obj_name}") - - except Exception as e: - print(f"[FAIL] {obj_name}: {e}") - - comp_deleted = cleanup_compressed(args.compressed_max_age_days, args.dry_run) - - print(f"Done. Processed={processed}, Raw deletions={raw_deleted}, Compressed deletions={comp_deleted}, " - f"Mode={args.age_mode}, Threshold={raw_age_seconds}s") - - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/services/sounds/compression/tests/test_prototype_lib.py b/services/sounds/compression/tests/test_prototype_lib.py deleted file mode 100644 index 6b4b21f50..000000000 --- a/services/sounds/compression/tests/test_prototype_lib.py +++ /dev/null @@ -1,515 +0,0 @@ -from pathlib import Path -import pytest -import tempfile -import shutil -from unittest.mock import patch, MagicMock -from src.prototype_lib import iter_input_files, build_ffmpeg_cmds, INPUT_EXTS, RAW_DIR, COMP_DIR - -# Test for iter_input_files function to ensure it retrieves all valid audio files. -def test_iter_input_files(): - files = list(iter_input_files()) # Get list of files returned by the function - assert len(files) > 0, "No input files found in the raw directory." # Ensure at least one file is found - for file in files: - assert file.suffix.lower() in {".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".opus"}, \ - f"Unsupported file format: {file.suffix}" # Ensure file has a valid audio extension - -# Test for build_ffmpeg_cmds function to validate the generated ffmpeg commands. -def test_build_ffmpeg_cmds(): - input_path = Path("data/raw/cat.wav") # Sample audio file - flac_cmd, opus_cmd = build_ffmpeg_cmds(input_path) # Generate ffmpeg commands for FLAC and Opus - - # Test FLAC command - assert flac_cmd[1][0] == "ffmpeg", "FLAC command does not start with 'ffmpeg'" # Check if the command starts correctly - assert "-c:a" in flac_cmd[1], "FLAC command does not specify codec" # Check if codec is specified - assert "flac" in flac_cmd[1], "FLAC command does not contain the 'flac' codec" # Ensure FLAC codec is used - - # Test Opus command - assert opus_cmd[1][0] == "ffmpeg", "Opus command does not start with 'ffmpeg'" # Check if the command starts correctly - assert "-c:a" in opus_cmd[1], "Opus command does not specify codec" # Check if codec is specified - assert "libopus" in opus_cmd[1], "Opus command does not contain the 'libopus' codec" # Ensure Opus codec is used - -# Test when the directory is empty, ensuring no files are returned. -def test_iter_input_files_empty_directory(): - """Test case where no input files are available.""" - empty_dir = Path("data/empty") # Path to an empty directory - # Ensure the directory is empty - if not empty_dir.exists(): - empty_dir.mkdir(parents=True) - - # Simulate the empty directory scenario - assert len(list(iter_input_files("data/empty"))) == 0, "The directory is empty, but files were found." - -# Test to ensure that invalid file extensions are not returned. -def test_iter_input_files_invalid_extension(): - """Test case where files with invalid extensions are present.""" - invalid_file = Path("data/raw/invalid_file.txt") # Invalid file extension - invalid_file.touch() # Create the invalid file - - files = list(iter_input_files()) # Get files from the directory - assert invalid_file not in files, f"Unexpected file with invalid extension: {invalid_file}" # Assert invalid file is not included - -# Additional tests for improving code coverage - -# Test with a custom directory to ensure files are handled properly. -def test_iter_input_files_with_custom_directory(): - """Test with a custom directory""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Create a valid audio file - audio_file = temp_path / "test.wav" - audio_file.touch() - - files = list(iter_input_files(temp_dir)) # Get files from the custom directory - assert len(files) == 1 # Ensure only one file is found - assert files[0].name == "test.wav" # Verify the file name - -# Test case for non-existent directory, should raise an error. -def test_iter_input_files_nonexistent_directory(): - """Test with a non-existent directory""" - nonexistent_dir = "/path/that/does/not/exist" # Path that doesn't exist - - with pytest.raises(ValueError, match="does not exist"): # Expecting a ValueError - list(iter_input_files(nonexistent_dir)) # Try accessing the non-existent directory - -# Test case to ensure case-insensitive handling of file extensions. -def test_iter_input_files_case_insensitive(): - """Test that the function handles extensions with uppercase letters""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Create files with both lowercase and uppercase extensions - files_to_create = ["test.WAV", "audio.MP3", "music.flac", "sound.OGG"] - - for filename in files_to_create: - (temp_path / filename).touch() - - found_files = list(iter_input_files(temp_dir)) # Get files from the directory - assert len(found_files) == 4 # Ensure 4 files are found - - # Verify that all expected files were found - found_names = [f.name for f in found_files] - for expected_file in files_to_create: - assert expected_file in found_names - -# Test to ensure subdirectories are ignored when iterating files. -def test_iter_input_files_with_subdirectories(): - """Test that the function ignores subdirectories""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Create a file in the main directory - audio_file = temp_path / "main.wav" - audio_file.touch() - - # Create a subdirectory with a file inside - sub_dir = temp_path / "subdirectory" - sub_dir.mkdir() - sub_audio = sub_dir / "sub.wav" - sub_audio.touch() - - files = list(iter_input_files(temp_dir)) # Get files from the main directory - # Should only find the file in the main directory - assert len(files) == 1 - assert files[0].name == "main.wav" - -# Test build_ffmpeg_cmds with custom parameters to ensure command customization works. -def test_build_ffmpeg_cmds_custom_parameters(): - """Test build_ffmpeg_cmds with custom parameters""" - input_path = Path("test_audio.mp3") # Sample audio file - - flac_cmd, opus_cmd = build_ffmpeg_cmds(input_path, flac_level="8", opus_bitrate="128k") # Custom parameters - - # Check if custom FLAC compression level is included - assert "8" in flac_cmd[1], "Custom FLAC level not found in command" - - # Check if custom Opus bitrate is included - assert "128k" in opus_cmd[1], "Custom Opus bitrate not found in command" - -# Test if the output paths for FLAC and Opus are built correctly. -def test_build_ffmpeg_cmds_output_paths(): - """Test that output paths are built correctly""" - input_path = Path("my_audio_file.wav") # Sample audio file - - flac_cmd, opus_cmd = build_ffmpeg_cmds(input_path) # Generate ffmpeg commands - - # Check if FLAC output path is correct - expected_flac_path = str(COMP_DIR / "my_audio_file.flac") - assert expected_flac_path in flac_cmd[1], "FLAC output path is incorrect" - - # Check if Opus output path is correct - expected_opus_path = str(COMP_DIR / "my_audio_file.opus") - assert expected_opus_path in opus_cmd[1], "Opus output path is incorrect" - -# Test to validate the structure of the return values from build_ffmpeg_cmds. -def test_build_ffmpeg_cmds_return_structure(): - """Test that the return structure is valid""" - input_path = Path("test.wav") # Sample audio file - - flac_result, opus_result = build_ffmpeg_cmds(input_path) # Generate ffmpeg commands - - # Ensure each result is a tuple of 3 elements - assert len(flac_result) == 3, "FLAC result should have 3 elements" - assert len(opus_result) == 3, "Opus result should have 3 elements" - - # Check if the first element is the codec name - assert flac_result[0] == "flac", "First element should be codec name" - assert opus_result[0] == "opus", "First element should be codec name" - - # Ensure the third element is a Path object - assert isinstance(flac_result[2], Path), "Third element should be Path object" - assert isinstance(opus_result[2], Path), "Third element should be Path object" - -# Test to ensure that the INPUT_EXTS constant matches the expected set of extensions. -def test_input_exts_constant(): - """Test that the INPUT_EXTS constant matches the expected formats""" - expected_formats = {".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".opus"} - assert INPUT_EXTS == expected_formats, "INPUT_EXTS constant doesn't match expected formats" - -# Test to validate that only valid audio files are returned from a directory containing mixed files. -def test_iter_input_files_mixed_files(): - """Test with a mix of valid and invalid files""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Create valid audio files - valid_files = ["audio1.wav", "music.mp3", "sound.flac"] - for filename in valid_files: - (temp_path / filename).touch() - - # Create invalid files - invalid_files = ["document.txt", "image.jpg", "video.mp4"] - for filename in invalid_files: - (temp_path / filename).touch() - - found_files = list(iter_input_files(temp_dir)) # Get files from the directory - - # Should find only the valid files - assert len(found_files) == 3 - found_names = [f.name for f in found_files] - - for valid_file in valid_files: - assert valid_file in found_names - - for invalid_file in invalid_files: - assert invalid_file not in found_names - -def test_build_ffmpeg_cmds_all_parameters(): - """Test all parameters in the ffmpeg command.""" - input_path = Path("test_file.wav") - - # Generate FFmpeg commands for FLAC and Opus with specific parameters - flac_result, opus_result = build_ffmpeg_cmds(input_path, flac_level="3", opus_bitrate="64k") - - flac_cmd = flac_result[1] - opus_cmd = opus_result[1] - - # Detailed tests for FLAC command - assert "-y" in flac_cmd, "Missing -y parameter in FLAC command" - assert "-hide_banner" in flac_cmd, "Missing -hide_banner parameter in FLAC command" - assert "-loglevel" in flac_cmd, "Missing -loglevel parameter in FLAC command" - assert "error" in flac_cmd, "Missing error loglevel in FLAC command" - assert "-i" in flac_cmd, "Missing -i parameter in FLAC command" - assert "-compression_level" in flac_cmd, "Missing compression_level in FLAC command" - assert "3" in flac_cmd, "Missing custom compression level in FLAC command" - - # Detailed tests for Opus command - assert "-y" in opus_cmd, "Missing -y parameter in Opus command" - assert "-hide_banner" in opus_cmd, "Missing -hide_banner parameter in Opus command" - assert "-loglevel" in opus_cmd, "Missing -loglevel parameter in Opus command" - assert "error" in opus_cmd, "Missing error loglevel in Opus command" - assert "-i" in opus_cmd, "Missing -i parameter in Opus command" - assert "-b:a" in opus_cmd, "Missing -b:a parameter in Opus command" - assert "64k" in opus_cmd, "Missing custom bitrate in Opus command" - -def test_iter_input_files_all_supported_formats(): - """Test that all supported formats are correctly identified.""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Create a file for each supported format - supported_formats = [".wav", ".mp3", ".flac", ".ogg", ".m4a", ".aac", ".wma", ".opus"] - - for ext in supported_formats: - test_file = temp_path / f"test{ext}" - test_file.touch() - - found_files = list(iter_input_files(temp_dir)) - assert len(found_files) == len(supported_formats), f"Expected {len(supported_formats)} files, found {len(found_files)}" - - # Ensure all formats were found - found_extensions = {f.suffix.lower() for f in found_files} - expected_extensions = set(supported_formats) - assert found_extensions == expected_extensions, "Not all supported formats were found" - -def test_build_ffmpeg_cmds_default_parameters(): - """Test that default parameters work correctly.""" - input_path = Path("default_test.mp3") - - flac_result, opus_result = build_ffmpeg_cmds(input_path) - - # Check default parameters - assert "5" in flac_result[1], "Default FLAC compression level (5) not found" - assert "96k" in opus_result[1], "Default Opus bitrate (96k) not found" - -def test_path_handling_with_special_characters(): - """Test handling of file names with special characters.""" - special_names = [ - "file with spaces.wav", - "file-with-dashes.mp3", - "file_with_underscores.flac", - "file.with.dots.ogg" - ] - - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - for name in special_names: - (temp_path / name).touch() - - found_files = list(iter_input_files(temp_dir)) - assert len(found_files) == len(special_names) - - found_names = [f.name for f in found_files] - for expected_name in special_names: - assert expected_name in found_names - -def test_comp_dir_creation_in_build_ffmpeg_cmds(): - """Test that the output paths point to the correct compression directory.""" - input_path = Path("some_audio.wav") - - flac_result, opus_result = build_ffmpeg_cmds(input_path) - - flac_output_path = flac_result[2] - opus_output_path = opus_result[2] - - # Check that the paths point to COMP_DIR - assert flac_output_path.parent == COMP_DIR, "FLAC output path parent is not COMP_DIR" - assert opus_output_path.parent == COMP_DIR, "Opus output path parent is not COMP_DIR" - - # Check that the extensions are correct - assert flac_output_path.suffix == ".flac", "FLAC output doesn't have .flac extension" - assert opus_output_path.suffix == ".opus", "Opus output doesn't have .opus extension" - -def test_iter_input_files_path_object_handling(): - """Test correct handling of Path objects.""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - audio_file = temp_path / "path_test.wav" - audio_file.touch() - - # Test with string - files_str = list(iter_input_files(temp_dir)) - - # Test with Path object - files_path = list(iter_input_files(temp_path)) - - assert len(files_str) == len(files_path) == 1 - assert files_str[0].name == files_path[0].name == "path_test.wav" - -def test_build_ffmpeg_cmds_extreme_parameters(): - """Test with extreme parameters.""" - input_path = Path("extreme_test.wav") - - # Extreme parameters - flac_result, opus_result = build_ffmpeg_cmds( - input_path, - flac_level="12", # Maximum - opus_bitrate="512k" # Very high - ) - - assert "12" in flac_result[1], "Extreme FLAC level not found" - assert "512k" in opus_result[1], "Extreme Opus bitrate not found" - -def test_constants_and_globals(): - """Test constants and global variables.""" - from scripts.prototype_lib import ROOT, RAW_DIR, COMP_DIR, INPUT_EXTS - - # Check that all constants are Path objects or sets - assert isinstance(ROOT, Path), "ROOT should be a Path object" - assert isinstance(RAW_DIR, Path), "RAW_DIR should be a Path object" - assert isinstance(COMP_DIR, Path), "COMP_DIR should be a Path object" - assert isinstance(INPUT_EXTS, set), "INPUT_EXTS should be a set" - - # Check that the paths are logical - assert RAW_DIR.name == "raw", "RAW_DIR should end with 'raw'" - assert COMP_DIR.name == "compressed", "COMP_DIR should end with 'compressed'" - - # Check that all extensions in INPUT_EXTS start with a dot - for ext in INPUT_EXTS: - assert ext.startswith("."), f"Extension {ext} should start with dot" - assert ext.islower(), f"Extension {ext} should be lowercase" - -def test_iter_input_files_file_vs_directory(): - """Test that the function ignores directories that look like audio files.""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Create a valid file - valid_file = temp_path / "audio.wav" - valid_file.touch() - - # Create a directory with a name that looks like an audio file - fake_audio_dir = temp_path / "fake_audio.mp3" - fake_audio_dir.mkdir() - - files = list(iter_input_files(temp_dir)) - - # Should find only the valid file - assert len(files) == 1 - assert files[0].name == "audio.wav" - -def test_build_ffmpeg_cmds_file_stem_handling(): - """Test that handling of file stems works correctly.""" - test_cases = [ - ("simple.wav", "simple"), - ("file.with.dots.mp3", "file.with.dots"), - ("no_extension", "no_extension"), - ("multiple.dots.in.name.flac", "multiple.dots.in.name") - ] - - for input_name, expected_stem in test_cases: - input_path = Path(input_name) - flac_result, opus_result = build_ffmpeg_cmds(input_path) - - flac_output = flac_result[2] - opus_output = opus_result[2] - - assert flac_output.stem == expected_stem, f"FLAC stem mismatch for {input_name}" - assert opus_output.stem == expected_stem, f"Opus stem mismatch for {input_name}" - -def test_iter_input_files_empty_files(): - """Test with empty files - should still be considered valid.""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Create empty files with valid extensions - empty_files = ["empty1.wav", "empty2.mp3", "empty3.flac"] - - for filename in empty_files: - (temp_path / filename).touch() - - files = list(iter_input_files(temp_dir)) - - assert len(files) == len(empty_files) - found_names = [f.name for f in files] - - for expected_file in empty_files: - assert expected_file in found_names - -def test_iter_input_files_hidden_files(): - """Test with hidden files (starting with a dot).""" - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - # Create regular files - normal_file = temp_path / "normal.wav" - normal_file.touch() - - # Create a hidden file - hidden_file = temp_path / ".hidden.mp3" - hidden_file.touch() - - files = list(iter_input_files(temp_dir)) - found_names = [f.name for f in files] - - # Hidden files should be found if they have a valid extension - assert "normal.wav" in found_names - assert ".hidden.mp3" in found_names - assert len(files) == 2 - -def test_build_ffmpeg_cmds_command_order(): - """Test that the order of parameters in the ffmpeg command is correct.""" - input_path = Path("test.wav") - flac_result, opus_result = build_ffmpeg_cmds(input_path) - - flac_cmd = flac_result[1] - opus_cmd = opus_result[1] - - # Check parameter order for FLAC - assert flac_cmd[0] == "ffmpeg" - assert flac_cmd[1] == "-y" - assert flac_cmd[2] == "-hide_banner" - assert "-i" in flac_cmd - assert "-c:a" in flac_cmd - - # Check parameter order for Opus - assert opus_cmd[0] == "ffmpeg" - assert opus_cmd[1] == "-y" - assert opus_cmd[2] == "-hide_banner" - assert "-i" in opus_cmd - assert "-c:a" in opus_cmd - -def test_input_path_as_string_in_build_ffmpeg_cmds(): - """Test that the function handles string paths correctly.""" - input_path = Path("string_path.wav") - - flac_result, opus_result = build_ffmpeg_cmds(input_path) - - # The path should appear as a string in the command - flac_cmd = flac_result[1] - opus_cmd = opus_result[1] - - input_str = str(input_path) - assert input_str in flac_cmd, "Input path not found as string in FLAC command" - assert input_str in opus_cmd, "Input path not found as string in Opus command" - -def test_comp_dir_mkdir_functionality(): - """Test that COMP_DIR is created correctly.""" - from scripts.prototype_lib import COMP_DIR - - # COMP_DIR should be defined and accessible - assert COMP_DIR is not None - assert isinstance(COMP_DIR, Path) - - # Check that the path is logical - assert "compressed" in str(COMP_DIR) - -def test_root_path_calculation(): - """Test that ROOT path calculation is correct.""" - from scripts.prototype_lib import ROOT - - # ROOT should be a Path object - assert isinstance(ROOT, Path) - - # ROOT should be an absolute path - assert ROOT.is_absolute() - -def test_various_audio_extensions_case_combinations(): - """Test different combinations of uppercase and lowercase audio extensions.""" - test_extensions = [ - ".wav", ".WAV", ".Wav", ".wAv", - ".mp3", ".MP3", ".Mp3", ".mP3", - ".flac", ".FLAC", ".Flac", ".fLaC" - ] - - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - - for i, ext in enumerate(test_extensions): - test_file = temp_path / f"test{i}{ext}" - test_file.touch() - - files = list(iter_input_files(temp_dir)) - - # All files should be found (case insensitive) - assert len(files) == len(test_extensions) - -def test_build_ffmpeg_cmds_output_file_extensions(): - """Detailed test of output file extensions.""" - test_inputs = [ - ("audio.wav", ".flac", ".opus"), - ("music.mp3", ".flac", ".opus"), - ("sound.m4a", ".flac", ".opus") - ] - - for input_name, expected_flac_ext, expected_opus_ext in test_inputs: - input_path = Path(input_name) - flac_result, opus_result = build_ffmpeg_cmds(input_path) - - flac_output = flac_result[2] - opus_output = opus_result[2] - - assert flac_output.suffix == expected_flac_ext, f"Wrong FLAC extension for {input_name}" - assert opus_output.suffix == expected_opus_ext, f"Wrong Opus extension for {input_name}" diff --git a/services/sounds/compression/tests/test_run_bench.py b/services/sounds/compression/tests/test_run_bench.py deleted file mode 100644 index 0fd0a88d6..000000000 --- a/services/sounds/compression/tests/test_run_bench.py +++ /dev/null @@ -1,54 +0,0 @@ -from pathlib import Path -from src.run_bench import run_and_profile, file_size, main -import subprocess -import csv - -# Test the `run_and_profile` function -def test_run_and_profile(): - # Set up a test command for profiling - cmd = ["ffmpeg", "-version"] # Test with a simple command (you can replace it with your actual compression command) - - # Run the command and get results - rc, wall_time, cpu, output = run_and_profile(cmd, shell=True) - - # 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 "ffmpeg" in output, "Expected output not found" # Check if the output contains 'ffmpeg' - -# Test the `file_size` function -def test_file_size(): - test_file = Path("data/raw/cat.wav") # Ensure this file exists for the test - - # Make sure the test file exists - assert test_file.exists(), "Test file does not exist" - - # Get the file size - size = file_size(test_file) - - # Test if the file size is greater than zero - assert size > 0, f"File size should be greater than zero, but got {size}" - -# Test the `main` function to check if the CSV is created correctly -def test_main(): - result_path = Path("results/benchmarks.csv") - - # Run the main function - main() - - # Check if the results file was created - assert result_path.exists(), "The result CSV file was not created" - - # Optionally 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" - -# Additional tests for edge cases -def test_file_size_invalid_file(): - """Test the file size function with a non-existent file""" - invalid_file = Path("data/raw/nonexistent_file.wav") - size = file_size(invalid_file) - assert size == 0, f"Expected file size to be 0, but got {size}" \ No newline at end of file diff --git a/services/sounds/compression/tests/test_tiering_job.py b/services/sounds/compression/tests/test_tiering_job.py deleted file mode 100644 index b81a13d29..000000000 --- a/services/sounds/compression/tests/test_tiering_job.py +++ /dev/null @@ -1,418 +0,0 @@ -import pytest -from pathlib import Path -import time -import subprocess -import tempfile -import shutil -import os -from unittest.mock import patch, MagicMock, mock_open, Mock -from src.tiering_job import ( - is_older_than, encode, cleanup_compressed, get_age_seconds, main -) - - -class TestGetAgeSeconds: - """Tests for the function get_age_seconds""" - - def test_get_age_seconds_mtime(self, tmp_path): - """Test the file age based on mtime""" - test_file = tmp_path / "test_file.txt" - test_file.write_text("test") - - age = get_age_seconds(test_file, "mtime") - assert age >= 0 - assert age < 1 # A newly created file should be less than a second old - - def test_get_age_seconds_ctime(self, tmp_path): - """Test the file age based on ctime""" - test_file = tmp_path / "test_file.txt" - test_file.write_text("test") - - age = get_age_seconds(test_file, "ctime") - assert age >= 0 - assert age < 1 # A newly created file should be less than a second old - - @patch('time.time') - @patch('os.stat') - def test_get_age_seconds_old_file(self, mock_stat, mock_time): - """Test the file age for an old file""" - mock_time.return_value = 1000000 # Simulated current time - - # Creating a mock stat object - mock_stat_result = Mock() - mock_stat_result.st_mtime = 996400 # 1000000 - 3600 (1 hour) - mock_stat_result.st_ctime = 996400 - mock_stat.return_value = mock_stat_result - - test_file = Path("old_file.txt") - - age_mtime = get_age_seconds(test_file, "mtime") - age_ctime = get_age_seconds(test_file, "ctime") - - assert age_mtime == 3600 # File is 3600 seconds (1 hour) old - assert age_ctime == 3600 # File is 3600 seconds (1 hour) old - - -class TestIsOlderThan: - """Tests for the function is_older_than""" - - @patch('scripts.tiering_job.get_age_seconds') - def test_is_older_than_true(self, mock_get_age): - """Test when the file is older than the threshold""" - mock_get_age.return_value = 7200 # 2 hours - test_path = Path("test.txt") - - result = is_older_than(test_path, 3600, "mtime") # Threshold of 1 hour - assert result is True - mock_get_age.assert_called_once_with(test_path, "mtime") - - @patch('scripts.tiering_job.get_age_seconds') - def test_is_older_than_false(self, mock_get_age): - """Test when the file is younger than the threshold""" - mock_get_age.return_value = 1800 # 30 minutes - test_path = Path("test.txt") - - result = is_older_than(test_path, 3600, "mtime") # Threshold of 1 hour - assert result is False - - @patch('scripts.tiering_job.get_age_seconds') - def test_is_older_than_equal(self, mock_get_age): - """Test when the file is exactly at the threshold age""" - mock_get_age.return_value = 3600 # Exactly 1 hour - test_path = Path("test.txt") - - result = is_older_than(test_path, 3600, "mtime") # Threshold of 1 hour - assert result is True # >= should return True - - def test_is_older_than_invalid_mode(self): - """Test invalid mode scenario""" - test_path = Path("test.txt") - - with pytest.raises(ValueError, match="Invalid mode: invalid"): - is_older_than(test_path, 3600, "invalid") - - def test_is_older_than_ctime_mode(self, tmp_path): - """Test the ctime mode scenario""" - test_file = tmp_path / "test_file.txt" - test_file.write_text("test") - - # Since the file is new, it should not be older than 10 seconds - result = is_older_than(test_file, 10, "ctime") - assert result is False - -class TestEncode: - """Tests for the 'encode' function""" - - @patch('scripts.tiering_job.build_ffmpeg_cmds') - @patch('subprocess.call') - def test_encode_success(self, mock_subprocess, mock_build_cmds): - """Test for successful encoding""" - test_path = Path("input.wav") # Input file - output_path = Path("output.flac") # Expected output file - - # Mock the ffmpeg command building - mock_build_cmds.return_value = [ - ("flac", ["ffmpeg", "-i", "input.wav", "output.flac"], output_path) - ] - mock_subprocess.return_value = 0 # Simulate success - - result = encode(test_path, "flac") # Call the encode function - - assert result == output_path # Check that the result matches the expected output - mock_build_cmds.assert_called_once_with(test_path) # Ensure the ffmpeg command was built with the correct input - mock_subprocess.assert_called_once() # Ensure subprocess.call was called once - - @patch('scripts.tiering_job.build_ffmpeg_cmds') - @patch('subprocess.call') - def test_encode_subprocess_failure(self, mock_subprocess, mock_build_cmds): - """Test for subprocess failure during encoding""" - test_path = Path("input.wav") - output_path = Path("output.flac") - - # Mock the ffmpeg command building - mock_build_cmds.return_value = [ - ("flac", ["ffmpeg", "-i", "input.wav", "output.flac"], output_path) - ] - mock_subprocess.return_value = 1 # Simulate failure (non-zero return code) - - # Assert that an exception is raised when encoding fails - with pytest.raises(RuntimeError, match="Encode failed: input.wav -> flac"): - encode(test_path, "flac") - - @patch('scripts.tiering_job.build_ffmpeg_cmds') - def test_encode_unsupported_codec(self, mock_build_cmds): - """Test for unsupported codec during encoding""" - test_path = Path("input.wav") - - # Mock the ffmpeg command building with supported codecs (flac, opus) - mock_build_cmds.return_value = [ - ("flac", ["ffmpeg", "-i", "input.wav", "output.flac"], Path("output.flac")), - ("opus", ["ffmpeg", "-i", "input.wav", "output.opus"], Path("output.opus")) - ] - - # Test for an unsupported codec (mp3) - with pytest.raises(ValueError, match="Unsupported codec: mp3"): - encode(test_path, "mp3") - - @patch('scripts.tiering_job.build_ffmpeg_cmds') - @patch('subprocess.call') - def test_encode_opus(self, mock_subprocess, mock_build_cmds): - """Test for encoding to opus format""" - test_path = Path("input.wav") - output_path = Path("output.opus") - - # Mock the ffmpeg command building for opus encoding - mock_build_cmds.return_value = [ - ("opus", ["ffmpeg", "-i", "input.wav", "output.opus"], output_path) - ] - mock_subprocess.return_value = 0 # Simulate success - - result = encode(test_path, "opus") # Call the encode function - assert result == output_path # Check that the result matches the expected output - -class TestCleanupCompressed: - """Tests for the cleanup_compressed function""" - - def test_cleanup_compressed_negative_age(self): - """Test for negative age""" - with pytest.raises(ValueError, match="max_age_days cannot be negative"): - cleanup_compressed(-1, dry_run=False) # This should raise an error because the age can't be negative - - def test_cleanup_compressed_zero_age(self): - """Test for zero age - should return 0 without performing any action""" - result = cleanup_compressed(0, dry_run=False) - assert result == 0 # No deletion should happen, so result should be 0 - - @patch('scripts.tiering_job.COMP_DIR') - @patch('time.time') - def test_cleanup_compressed_dry_run(self, mock_time, mock_comp_dir, capsys): - """Test for dry run mode""" - mock_time.return_value = 1000000 # Mocking the current time - - # Creating mock files - old_file = Mock() - old_file.name = "old_file.flac" - old_file.is_file.return_value = True - old_file_stat = Mock() - old_file_stat.st_mtime = 1000000 - (2 * 86400) # 2 days ago - old_file.stat.return_value = old_file_stat - - new_file = Mock() - new_file.name = "new_file.flac" - new_file.is_file.return_value = True - new_file_stat = Mock() - new_file_stat.st_mtime = 1000000 - 3600 # 1 hour ago - new_file.stat.return_value = new_file_stat - - mock_comp_dir.iterdir.return_value = [old_file, new_file] # Mocking the directory contents - - result = cleanup_compressed(1, dry_run=True) # In dry run mode, no files should be deleted - - assert result == 0 # Nothing should be deleted in dry run mode - captured = capsys.readouterr() - assert "[DRY] would delete compressed:" in captured.out # We expect a dry run message - - @patch('scripts.tiering_job.COMP_DIR') - @patch('time.time') - def test_cleanup_compressed_actual_deletion(self, mock_time, mock_comp_dir, capsys): - """Test for actual deletion""" - mock_time.return_value = 1000000 # Mocking the current time - - # Creating mock file to delete - old_file = Mock() - old_file.name = "old_file.flac" - old_file.is_file.return_value = True - old_file_stat = Mock() - old_file_stat.st_mtime = 1000000 - (2 * 86400) # 2 days ago - old_file.stat.return_value = old_file_stat - - mock_comp_dir.iterdir.return_value = [old_file] # Mocking the directory contents - - result = cleanup_compressed(1, dry_run=False) # In real mode, files should be deleted if older than 1 day - - assert result == 1 # One file should be deleted - old_file.unlink.assert_called_once() # Checking that unlink was called on the old file - captured = capsys.readouterr() - assert "[DEL] compressed old:" in captured.out # We expect a deletion message - - @patch('scripts.tiering_job.COMP_DIR') - @patch('time.time') - def test_cleanup_compressed_deletion_error(self, mock_time, mock_comp_dir, capsys): - """Test for deletion error""" - mock_time.return_value = 1000000 # Mocking the current time - - old_file = Mock() - old_file.name = "old_file.flac" - old_file.is_file.return_value = True - old_file_stat = Mock() - old_file_stat.st_mtime = 1000000 - (2 * 86400) # 2 days ago - old_file.stat.return_value = old_file_stat - old_file.unlink.side_effect = OSError("Permission denied") # Mocking an error when trying to delete - - mock_comp_dir.iterdir.return_value = [old_file] # Mocking the directory contents - - result = cleanup_compressed(1, dry_run=False) # Trying to delete with an error - - assert result == 0 # No file should be deleted due to the error - captured = capsys.readouterr() - assert "[WARN] failed to delete" in captured.out # We expect a warning message - - @patch('scripts.tiering_job.COMP_DIR') - @patch('time.time') - def test_cleanup_compressed_no_old_files(self, mock_time, mock_comp_dir): - """Test when there are no old files""" - mock_time.return_value = 1000000 # Mocking the current time - - new_file = Mock() - new_file.name = "new_file.flac" - new_file.is_file.return_value = True - new_file_stat = Mock() - new_file_stat.st_mtime = 1000000 - 3600 # 1 hour ago - new_file.stat.return_value = new_file_stat - - mock_comp_dir.iterdir.return_value = [new_file] # Mocking the directory contents - - result = cleanup_compressed(1, dry_run=False) # Trying to delete files older than 1 day, but no old files - - assert result == 0 # No files should be deleted since they are all too new - -class TestMain: - """Tests for the main function in the 'tiering_job.py' script.""" - - @patch('sys.argv', ['tiering_job.py', '--dry-run']) - @patch('scripts.tiering_job.iter_input_files') - @patch('scripts.tiering_job.cleanup_compressed') - def test_main_dry_run_default_settings(self, mock_cleanup, mock_iter, capsys): - """Test dry run with default settings""" - mock_iter.return_value = [] # Mock that no files are returned - mock_cleanup.return_value = 0 # Mock that cleanup doesn't delete anything - - main() # Run the main function - - # Assert that cleanup was called once with default parameters (90 days, dry_run=True) - mock_cleanup.assert_called_once_with(90, True) - captured = capsys.readouterr() # Capture the output printed to the console - assert "Done. Processed=0" in captured.out # Assert the output indicates no files were processed - - @patch('sys.argv', ['tiering_job.py', '--raw-max-age-minutes', '30', '--codec', 'opus']) - @patch('scripts.tiering_job.iter_input_files') - @patch('scripts.tiering_job.is_older_than') - @patch('scripts.tiering_job.encode') - @patch('scripts.tiering_job.cleanup_compressed') - def test_main_with_minutes_and_opus(self, mock_cleanup, mock_encode, mock_older, mock_iter): - """Test with custom settings: max age in minutes and codec set to opus""" - test_file = Mock() # Mock a file object - test_file.name = "test.wav" # Set the file name - output_file = Mock() # Mock an output file after encoding - output_file.exists.return_value = True # Mock that the encoded file exists - - mock_iter.return_value = [test_file] # Mock iter_input_files to return our test file - mock_older.return_value = True # Mock that the file is older than 30 minutes - mock_encode.return_value = output_file # Mock the encoding function - mock_cleanup.return_value = 1 # Mock cleanup indicating one file was processed - - main() # Run the main function - - # Assert that is_older_than was called with the correct arguments - mock_older.assert_called_with(test_file, 30 * 60, 'mtime') # 30 minutes in seconds - # Assert that encode was called with the correct codec ('opus') - mock_encode.assert_called_with(test_file, 'opus') - # Assert that cleanup was called with custom settings (90 days, not dry_run) - mock_cleanup.assert_called_with(90, False) - - @patch('sys.argv', ['tiering_job.py', '--delete-raw-after', '--age-mode', 'ctime']) - @patch('scripts.tiering_job.iter_input_files') - @patch('scripts.tiering_job.is_older_than') - @patch('scripts.tiering_job.encode') - @patch('scripts.tiering_job.cleanup_compressed') - def test_main_delete_raw_after(self, mock_cleanup, mock_encode, mock_older, mock_iter, capsys): - """Test deleting raw files after encoding""" - test_file = Mock() # Mock a file object - test_file.name = "test.wav" - output_file = Mock() # Mock the encoded output file - output_file.exists.return_value = True # Mock that the encoded file exists - - mock_iter.return_value = [test_file] # Mock iter_input_files to return our test file - mock_older.return_value = True # Mock that the file is older than 30 minutes - mock_encode.return_value = output_file # Mock the encoding function - mock_cleanup.return_value = 0 # Mock cleanup showing no files processed - - main() # Run the main function - - # Assert that the raw file was deleted after encoding - test_file.unlink.assert_called_once() - captured = capsys.readouterr() # Capture the console output - # Assert that the raw file deletion was logged in the output - assert "[DEL] raw:" in captured.out - - @patch('sys.argv', ['tiering_job.py', '--compressed-max-age-days', '30']) - @patch('scripts.tiering_job.iter_input_files') - @patch('scripts.tiering_job.cleanup_compressed') - def test_main_custom_compressed_age(self, mock_cleanup, mock_iter): - """Test setting a custom age for compressed files""" - mock_iter.return_value = [] # Mock that no files are returned - mock_cleanup.return_value = 5 # Mock that 5 files were processed - - main() # Run the main function - - # Assert that cleanup was called with the custom compressed file age (30 days) - mock_cleanup.assert_called_once_with(30, False) - - @patch('sys.argv', ['tiering_job.py']) - @patch('scripts.tiering_job.iter_input_files') - @patch('scripts.tiering_job.is_older_than') - @patch('scripts.tiering_job.encode') - @patch('scripts.tiering_job.cleanup_compressed') - def test_main_encode_failure(self, mock_cleanup, mock_encode, mock_older, mock_iter, capsys): - """Test handling of encoding failure""" - test_file = Mock() # Mock a file object - test_file.name = "test.wav" - - mock_iter.return_value = [test_file] # Mock iter_input_files to return our test file - mock_older.return_value = True # Mock that the file is older than 30 minutes - mock_encode.side_effect = RuntimeError("Encoding failed") # Mock encoding failure - mock_cleanup.return_value = 0 # Mock cleanup showing no files processed - - main() # Run the main function - - # Capture the console output - captured = capsys.readouterr() - # Assert that the failure was logged in the output - assert "[FAIL]" in captured.out - assert "Encoding failed" in captured.out - - @patch('sys.argv', ['tiering_job.py', '--raw-max-age-hours', '12']) - @patch('scripts.tiering_job.iter_input_files') - @patch('scripts.tiering_job.is_older_than') - @patch('scripts.tiering_job.cleanup_compressed') - def test_main_with_hours_setting(self, mock_cleanup, mock_older, mock_iter): - """Test setting file age in hours""" - mock_iter.return_value = [] # Mock that no files are returned - mock_older.return_value = False # Mock that the file is not older than the specified hours - mock_cleanup.return_value = 0 # Mock cleanup showing no files processed - - main() # Run the main function - - # Assert that cleanup was called with default settings (90 days, not dry_run) - mock_cleanup.assert_called_once_with(90, False) - - @patch('sys.argv', ['tiering_job.py', '--dry-run']) - @patch('scripts.tiering_job.iter_input_files') - @patch('scripts.tiering_job.is_older_than') - @patch('scripts.tiering_job.cleanup_compressed') - def test_main_dry_run_with_files(self, mock_cleanup, mock_older, mock_iter, capsys): - """Test dry run with files""" - test_file = Mock() # Mock a file object - test_file.name = "test.wav" - - mock_iter.return_value = [test_file] # Mock iter_input_files to return our test file - mock_older.return_value = True # Mock that the file is older than 30 minutes - mock_cleanup.return_value = 0 # Mock cleanup showing no files processed - - main() # Run the main function - - # Capture the console output - captured = capsys.readouterr() - # Assert that the dry run simulation was logged in the output - assert "[DRY] would encode" in captured.out - assert "Done. Processed=1" in captured.out \ No newline at end of file diff --git a/services/sounds_classifier/Dockerfile.classifier-svc b/services/sounds_classifier/Dockerfile.classifier-svc new file mode 100644 index 000000000..14fc3ff43 --- /dev/null +++ b/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/services/sounds_classifier/README.md b/services/sounds_classifier/README.md new file mode 100644 index 000000000..aa7dda159 --- /dev/null +++ b/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/services/sounds_classifier/requirements.txt b/services/sounds_classifier/requirements.txt new file mode 100644 index 000000000..cb7054ecc --- /dev/null +++ b/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/services/sounds_classifier/src/classification/__init__.py b/services/sounds_classifier/src/classification/__init__.py new file mode 100644 index 000000000..d163ffe0c --- /dev/null +++ b/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/services/sounds_classifier/src/classification/app.py b/services/sounds_classifier/src/classification/app.py new file mode 100644 index 000000000..a2b526747 --- /dev/null +++ b/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/services/sounds_classifier/src/classification/backbones/__init__.py b/services/sounds_classifier/src/classification/backbones/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/sounds_classifier/src/classification/backbones/cnn14.py b/services/sounds_classifier/src/classification/backbones/cnn14.py new file mode 100644 index 000000000..afc7e327e --- /dev/null +++ b/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/services/sounds_classifier/src/classification/core/__init__.py b/services/sounds_classifier/src/classification/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/sounds_classifier/src/classification/core/db_gis_helpers.py b/services/sounds_classifier/src/classification/core/db_gis_helpers.py new file mode 100644 index 000000000..5788698a6 --- /dev/null +++ b/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/services/sounds_classifier/src/classification/core/db_io_pg.py b/services/sounds_classifier/src/classification/core/db_io_pg.py new file mode 100644 index 000000000..5a1789e4c --- /dev/null +++ b/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/services/sounds_classifier/src/classification/core/db_utils.py b/services/sounds_classifier/src/classification/core/db_utils.py new file mode 100644 index 000000000..f2b7f139e --- /dev/null +++ b/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/services/sounds_classifier/src/classification/core/model_io.py b/services/sounds_classifier/src/classification/core/model_io.py new file mode 100644 index 000000000..fcfa3a0f6 --- /dev/null +++ b/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/services/sounds_classifier/src/classification/models/custom_labels.csv b/services/sounds_classifier/src/classification/models/custom_labels.csv new file mode 100644 index 000000000..028674305 --- /dev/null +++ b/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/services/sounds_classifier/src/classification/models/head/head_cnn14_rf.joblib b/services/sounds_classifier/src/classification/models/head/head_cnn14_rf.joblib new file mode 100644 index 000000000..535fa1218 Binary files /dev/null and b/services/sounds_classifier/src/classification/models/head/head_cnn14_rf.joblib differ diff --git a/services/sounds_classifier/src/classification/models/head/head_cnn14_rf.joblib.meta.json b/services/sounds_classifier/src/classification/models/head/head_cnn14_rf.joblib.meta.json new file mode 100644 index 000000000..fc380654f --- /dev/null +++ b/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/services/sounds_classifier/src/classification/scripts/__init__.py b/services/sounds_classifier/src/classification/scripts/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/sounds_classifier/src/classification/scripts/alerts.py b/services/sounds_classifier/src/classification/scripts/alerts.py new file mode 100644 index 000000000..bccf69048 --- /dev/null +++ b/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/services/sounds_classifier/src/classification/scripts/classify.py b/services/sounds_classifier/src/classification/scripts/classify.py new file mode 100644 index 000000000..81a829837 --- /dev/null +++ b/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/services/sounds_classifier/tests/conftest.py b/services/sounds_classifier/tests/conftest.py new file mode 100644 index 000000000..afb6a527e --- /dev/null +++ b/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/services/sounds_classifier/tests/test_alerts.py b/services/sounds_classifier/tests/test_alerts.py new file mode 100644 index 000000000..f2dd89850 --- /dev/null +++ b/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/services/sounds_classifier/tests/test_app.py b/services/sounds_classifier/tests/test_app.py new file mode 100644 index 000000000..6056cf4ef --- /dev/null +++ b/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/services/sounds_classifier/tests/test_classify_core.py b/services/sounds_classifier/tests/test_classify_core.py new file mode 100644 index 000000000..8c9423af3 --- /dev/null +++ b/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/services/sounds_classifier/tests/test_classify_head_loading.py b/services/sounds_classifier/tests/test_classify_head_loading.py new file mode 100644 index 000000000..60064879c --- /dev/null +++ b/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/services/sounds_classifier/tests/test_cnn14.py b/services/sounds_classifier/tests/test_cnn14.py new file mode 100644 index 000000000..44330e533 --- /dev/null +++ b/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/services/sounds_classifier/tests/test_db_io_pg.py b/services/sounds_classifier/tests/test_db_io_pg.py new file mode 100644 index 000000000..235f5a5e5 --- /dev/null +++ b/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/services/sounds_classifier/tests/test_db_utils.py b/services/sounds_classifier/tests/test_db_utils.py new file mode 100644 index 000000000..bbe6e8f43 --- /dev/null +++ b/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/services/sounds_classifier/tests/test_model_io.py b/services/sounds_classifier/tests/test_model_io.py new file mode 100644 index 000000000..4634a9804 --- /dev/null +++ b/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/services/sounds_flink/Dockerfile b/services/sounds_flink/Dockerfile new file mode 100644 index 000000000..9fc4a5a5c --- /dev/null +++ b/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/services/sounds_flink/config.py b/services/sounds_flink/config.py new file mode 100644 index 000000000..989f8677e --- /dev/null +++ b/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/services/sounds_flink/flink_job.py b/services/sounds_flink/flink_job.py new file mode 100644 index 000000000..de2429cc9 --- /dev/null +++ b/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/services/sounds_flink/processor.py b/services/sounds_flink/processor.py new file mode 100644 index 000000000..1b4c77814 --- /dev/null +++ b/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/services/sounds_flink/requirements.txt b/services/sounds_flink/requirements.txt new file mode 100644 index 000000000..f5de43b68 --- /dev/null +++ b/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/services/tests/test_db_upsert.py b/services/tests/test_db_upsert.py new file mode 100644 index 000000000..15d5ec27f --- /dev/null +++ b/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/services/tests/test_metrics.py b/services/tests/test_metrics.py new file mode 100644 index 000000000..864522304 --- /dev/null +++ b/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/services/tests/test_schema.py b/services/tests/test_schema.py new file mode 100644 index 000000000..a1ae80750 --- /dev/null +++ b/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/services/tests/test_validators.py b/services/tests/test_validators.py new file mode 100644 index 000000000..96c82d119 --- /dev/null +++ b/services/tests/test_validators.py @@ -0,0 +1,31 @@ +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/services/vector_service/Dockerfile b/services/vector_service/Dockerfile new file mode 100644 index 000000000..1ee96b19d --- /dev/null +++ b/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/services/vector_service/README b/services/vector_service/README new file mode 100644 index 000000000..0ed58aca6 --- /dev/null +++ b/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/services/vector_service/main.py b/services/vector_service/main.py new file mode 100644 index 000000000..1b33713b8 --- /dev/null +++ b/services/vector_service/main.py @@ -0,0 +1,324 @@ +from fastapi import FastAPI, HTTPException +import asyncpg +import numpy as np +import os +from datetime import datetime, timedelta + +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") + + +# ========================================================= +# STARTUP – CREATE CONNECTION POOL (SAFE) +# ========================================================= +@app.on_event("startup") +async def startup(): + print("⏳ Creating Postgres pool...") + try: + app.state.pool = await asyncpg.create_pool( + user=DB_USER, + password=DB_PASS, + host=DB_HOST, + port=DB_PORT, + database=DB_NAME, + min_size=1, + max_size=10, + ) + # ensure pgvector exists + async with app.state.pool.acquire() as conn: + await conn.execute("CREATE EXTENSION IF NOT EXISTS vector;") + + print("✅ Postgres connection pool ready.") + except Exception as e: + print(f"❌ Failed to connect to Postgres: {e}") + raise + + +@app.on_event("shutdown") +async def shutdown(): + await app.state.pool.close() + print("🛑 Connection pool closed") + + +# ========================================================= +# Helper: Validate Embedding Vector +# ========================================================= +def validate_vector(vec): + if not isinstance(vec, list): + raise HTTPException(400, "Vector must be a list of floats.") + if len(vec) != 5: + raise HTTPException(400, "Embedding vector must be length 5.") + try: + return [float(x) for x in vec] + except: + raise HTTPException(400, "Vector contains non-numeric values.") + + + +# ========================================================= +# INSERT VECTOR (sensor_embeddings) +# ========================================================= +@app.post("/add_embedding") +async def add_embedding(sensor_id: int, vector: list[float]): + vec = validate_vector(vector) + vec_str = "[" + ",".join(str(x) for x in vec) + "]" + + async with app.state.pool.acquire() as conn: + await conn.execute( + """ + INSERT INTO sensor_embeddings (sensor_id, vec) + VALUES ($1, $2::vector) + """, + sensor_id, vec_str + ) + + return {"status": "ok"} + + + +# ========================================================= +# VECTOR SEARCH +# ========================================================= +@app.post("/search") +async def search(vector: list[float], limit: int = 5): + vec = validate_vector(vector) + vec_str = "[" + ",".join(str(x) for x in vec) + "]" + + async with app.state.pool.acquire() as conn: + rows = await conn.fetch( + """ + SELECT id, vec <-> $1::vector AS distance + FROM sensor_embeddings + ORDER BY distance + LIMIT $2; + """, + vec_str, limit + ) + + return {"results": [{"id": r["id"], "distance": r["distance"]} for r in rows]} + + + +# ========================================================= +# GENERATE EMBEDDINGS FROM SENSORS +# ========================================================= +@app.post("/generate_embeddings_from_sensors") +async def generate_embeddings_from_sensors(): + + async with app.state.pool.acquire() as conn: + rows = await conn.fetch( + """ + SELECT sensor_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["sensor_id"] + name_len = len(r["sensor_name"] or "") + type_len = len(r["sensor_type"] or "") + lat = r["lat"] or 0.0 + lon = r["lon"] or 0.0 + status_score = 1.0 if (r["status"] or "").lower() == "active" else 0.0 + + # vector(5) is still your default + vector = np.array([lat, lon, name_len, type_len, status_score], dtype=float) + vec_str = "[" + ",".join(str(x) for x in vector) + "]" + + await conn.execute( + """ + INSERT INTO sensor_embeddings (sensor_id, vec) + VALUES ($1, $2::vector) + """, + sensor_id, vec_str + ) + inserted += 1 + + print(f"✅ {inserted} embeddings created.") + return {"message": f"{inserted} embeddings generated."} + + + +# ========================================================= +# FIND SIMILAR SENSORS BY SENSOR ID +# ========================================================= +@app.get("/similar_sensors/{sensor_id}") +async def similar_sensors(sensor_id: int, limit: int = 5): + + async with app.state.pool.acquire() as conn: + + row = await conn.fetchrow( + "SELECT vec FROM sensor_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 conn.fetch( + """ + SELECT e.sensor_id, e.vec <-> $1 AS distance, + s.sensor_name, s.sensor_type, s.lat, s.lon, s.status + FROM sensor_embeddings e + JOIN sensors s ON e.sensor_id = s.sensor_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 + ] + } + + + +# ========================================================= +# ADVANCED SIMILARITY SEARCH +# ========================================================= +@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 +): + + async with app.state.pool.acquire() as conn: + + sensor = await conn.fetchrow("SELECT * FROM sensors WHERE sensor_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"] + + row = await conn.fetchrow( + "SELECT vec FROM sensor_embeddings WHERE sensor_id=$1;", + sensor_id + ) + if not row: + return {"message": f"No embedding found for sensor {sensor_id}."} + + vec = row["vec"] + + # --- date logic --- + today = datetime.utcnow().date() + weekdays = { + "monday": 0, "tuesday": 1, "wednesday": 2, + "thursday": 3, "friday": 4, "saturday": 5, "sunday": 6 + } + + start_date = end_date = None + + if date_filter: + df = date_filter.lower() + + if df == "today": + start_date = end_date = today + + elif df == "yesterday": + start_date = end_date = today - timedelta(days=1) + + elif df.startswith("last_"): + day = df.replace("last_", "") + if day in weekdays: + today_wd = today.weekday() + days_back = (today_wd - weekdays[day] + 7) % 7 + 7 + target = today - timedelta(days=days_back) + start_date = end_date = target + + elif df in weekdays: + today_wd = today.weekday() + days_back = (today_wd - weekdays[df] + 7) % 7 + target = today - timedelta(days=days_back) + start_date = end_date = target + + # Base query + query = """ + SELECT e.sensor_id, e.vec <-> $1 AS distance, + s.sensor_name, s.sensor_type, s.install_date, s.status + FROM sensor_embeddings e + JOIN sensors s ON e.sensor_id = s.sensor_id + WHERE e.sensor_id <> $2 + """ + params = [vec, sensor_id] + idx = 3 + + if same_day: + query += f" AND DATE(s.install_date) = ${idx}" + params.append(base_date) + idx += 1 + + if same_type: + query += f" AND s.sensor_type = ${idx}" + params.append(sensor_type) + idx += 1 + + if same_status: + query += f" AND s.status = ${idx}" + params.append(status) + idx += 1 + + if start_date and end_date: + query += f" AND DATE(s.install_date) BETWEEN ${idx} AND ${idx+1}" + params.extend([start_date, end_date]) + idx += 2 + + query += f" ORDER BY distance LIMIT ${idx}" + params.append(limit) + + results = await 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/services/vector_service/vector_service/Dockerfile b/services/vector_service/vector_service/Dockerfile new file mode 100644 index 000000000..1ee96b19d --- /dev/null +++ b/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/services/vector_service/vector_service/README b/services/vector_service/vector_service/README new file mode 100644 index 000000000..0ed58aca6 --- /dev/null +++ b/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/services/vector_service/vector_service/main.py b/services/vector_service/vector_service/main.py new file mode 100644 index 000000000..1b33713b8 --- /dev/null +++ b/services/vector_service/vector_service/main.py @@ -0,0 +1,324 @@ +from fastapi import FastAPI, HTTPException +import asyncpg +import numpy as np +import os +from datetime import datetime, timedelta + +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") + + +# ========================================================= +# STARTUP – CREATE CONNECTION POOL (SAFE) +# ========================================================= +@app.on_event("startup") +async def startup(): + print("⏳ Creating Postgres pool...") + try: + app.state.pool = await asyncpg.create_pool( + user=DB_USER, + password=DB_PASS, + host=DB_HOST, + port=DB_PORT, + database=DB_NAME, + min_size=1, + max_size=10, + ) + # ensure pgvector exists + async with app.state.pool.acquire() as conn: + await conn.execute("CREATE EXTENSION IF NOT EXISTS vector;") + + print("✅ Postgres connection pool ready.") + except Exception as e: + print(f"❌ Failed to connect to Postgres: {e}") + raise + + +@app.on_event("shutdown") +async def shutdown(): + await app.state.pool.close() + print("🛑 Connection pool closed") + + +# ========================================================= +# Helper: Validate Embedding Vector +# ========================================================= +def validate_vector(vec): + if not isinstance(vec, list): + raise HTTPException(400, "Vector must be a list of floats.") + if len(vec) != 5: + raise HTTPException(400, "Embedding vector must be length 5.") + try: + return [float(x) for x in vec] + except: + raise HTTPException(400, "Vector contains non-numeric values.") + + + +# ========================================================= +# INSERT VECTOR (sensor_embeddings) +# ========================================================= +@app.post("/add_embedding") +async def add_embedding(sensor_id: int, vector: list[float]): + vec = validate_vector(vector) + vec_str = "[" + ",".join(str(x) for x in vec) + "]" + + async with app.state.pool.acquire() as conn: + await conn.execute( + """ + INSERT INTO sensor_embeddings (sensor_id, vec) + VALUES ($1, $2::vector) + """, + sensor_id, vec_str + ) + + return {"status": "ok"} + + + +# ========================================================= +# VECTOR SEARCH +# ========================================================= +@app.post("/search") +async def search(vector: list[float], limit: int = 5): + vec = validate_vector(vector) + vec_str = "[" + ",".join(str(x) for x in vec) + "]" + + async with app.state.pool.acquire() as conn: + rows = await conn.fetch( + """ + SELECT id, vec <-> $1::vector AS distance + FROM sensor_embeddings + ORDER BY distance + LIMIT $2; + """, + vec_str, limit + ) + + return {"results": [{"id": r["id"], "distance": r["distance"]} for r in rows]} + + + +# ========================================================= +# GENERATE EMBEDDINGS FROM SENSORS +# ========================================================= +@app.post("/generate_embeddings_from_sensors") +async def generate_embeddings_from_sensors(): + + async with app.state.pool.acquire() as conn: + rows = await conn.fetch( + """ + SELECT sensor_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["sensor_id"] + name_len = len(r["sensor_name"] or "") + type_len = len(r["sensor_type"] or "") + lat = r["lat"] or 0.0 + lon = r["lon"] or 0.0 + status_score = 1.0 if (r["status"] or "").lower() == "active" else 0.0 + + # vector(5) is still your default + vector = np.array([lat, lon, name_len, type_len, status_score], dtype=float) + vec_str = "[" + ",".join(str(x) for x in vector) + "]" + + await conn.execute( + """ + INSERT INTO sensor_embeddings (sensor_id, vec) + VALUES ($1, $2::vector) + """, + sensor_id, vec_str + ) + inserted += 1 + + print(f"✅ {inserted} embeddings created.") + return {"message": f"{inserted} embeddings generated."} + + + +# ========================================================= +# FIND SIMILAR SENSORS BY SENSOR ID +# ========================================================= +@app.get("/similar_sensors/{sensor_id}") +async def similar_sensors(sensor_id: int, limit: int = 5): + + async with app.state.pool.acquire() as conn: + + row = await conn.fetchrow( + "SELECT vec FROM sensor_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 conn.fetch( + """ + SELECT e.sensor_id, e.vec <-> $1 AS distance, + s.sensor_name, s.sensor_type, s.lat, s.lon, s.status + FROM sensor_embeddings e + JOIN sensors s ON e.sensor_id = s.sensor_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 + ] + } + + + +# ========================================================= +# ADVANCED SIMILARITY SEARCH +# ========================================================= +@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 +): + + async with app.state.pool.acquire() as conn: + + sensor = await conn.fetchrow("SELECT * FROM sensors WHERE sensor_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"] + + row = await conn.fetchrow( + "SELECT vec FROM sensor_embeddings WHERE sensor_id=$1;", + sensor_id + ) + if not row: + return {"message": f"No embedding found for sensor {sensor_id}."} + + vec = row["vec"] + + # --- date logic --- + today = datetime.utcnow().date() + weekdays = { + "monday": 0, "tuesday": 1, "wednesday": 2, + "thursday": 3, "friday": 4, "saturday": 5, "sunday": 6 + } + + start_date = end_date = None + + if date_filter: + df = date_filter.lower() + + if df == "today": + start_date = end_date = today + + elif df == "yesterday": + start_date = end_date = today - timedelta(days=1) + + elif df.startswith("last_"): + day = df.replace("last_", "") + if day in weekdays: + today_wd = today.weekday() + days_back = (today_wd - weekdays[day] + 7) % 7 + 7 + target = today - timedelta(days=days_back) + start_date = end_date = target + + elif df in weekdays: + today_wd = today.weekday() + days_back = (today_wd - weekdays[df] + 7) % 7 + target = today - timedelta(days=days_back) + start_date = end_date = target + + # Base query + query = """ + SELECT e.sensor_id, e.vec <-> $1 AS distance, + s.sensor_name, s.sensor_type, s.install_date, s.status + FROM sensor_embeddings e + JOIN sensors s ON e.sensor_id = s.sensor_id + WHERE e.sensor_id <> $2 + """ + params = [vec, sensor_id] + idx = 3 + + if same_day: + query += f" AND DATE(s.install_date) = ${idx}" + params.append(base_date) + idx += 1 + + if same_type: + query += f" AND s.sensor_type = ${idx}" + params.append(sensor_type) + idx += 1 + + if same_status: + query += f" AND s.status = ${idx}" + params.append(status) + idx += 1 + + if start_date and end_date: + query += f" AND DATE(s.install_date) BETWEEN ${idx} AND ${idx+1}" + params.extend([start_date, end_date]) + idx += 2 + + query += f" ORDER BY distance LIMIT ${idx}" + params.append(limit) + + results = await 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/services/weed_detection/Dockerfile b/services/weed_detection/Dockerfile new file mode 100644 index 000000000..5821909cc --- /dev/null +++ b/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/services/weed_detection/README.md b/services/weed_detection/README.md new file mode 100644 index 000000000..2d1de8659 --- /dev/null +++ b/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/services/weed_detection/config/config.yaml b/services/weed_detection/config/config.yaml new file mode 100644 index 000000000..e69de29bb diff --git a/services/weed_detection/docker-compose.yml b/services/weed_detection/docker-compose.yml new file mode 100644 index 000000000..4e8ced77f --- /dev/null +++ b/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/services/weed_detection/migrations/001_init.sql b/services/weed_detection/migrations/001_init.sql new file mode 100644 index 000000000..e69de29bb diff --git a/services/weed_detection/models/__init__.py b/services/weed_detection/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/weed_detection/models/dataset.py b/services/weed_detection/models/dataset.py new file mode 100644 index 000000000..93b75ef3f --- /dev/null +++ b/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/services/weed_detection/models/evaluate.py b/services/weed_detection/models/evaluate.py new file mode 100644 index 000000000..21016e67d --- /dev/null +++ b/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/services/weed_detection/models/ml_model.py b/services/weed_detection/models/ml_model.py new file mode 100644 index 000000000..5472e192e --- /dev/null +++ b/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/services/weed_detection/models/train.py b/services/weed_detection/models/train.py new file mode 100644 index 000000000..55d927e93 --- /dev/null +++ b/services/weed_detection/models/train.py @@ -0,0 +1,329 @@ +# 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): + + 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/services/weed_detection/models/train_ml_refiner.py b/services/weed_detection/models/train_ml_refiner.py new file mode 100644 index 000000000..4ee5d6749 --- /dev/null +++ b/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/services/weed_detection/models/unet_model.py b/services/weed_detection/models/unet_model.py new file mode 100644 index 000000000..0952c47b8 --- /dev/null +++ b/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/services/weed_detection/models/weights_refiner.pth b/services/weed_detection/models/weights_refiner.pth new file mode 100644 index 000000000..05c31cbcd Binary files /dev/null and b/services/weed_detection/models/weights_refiner.pth differ diff --git a/services/weed_detection/requirements.txt b/services/weed_detection/requirements.txt new file mode 100644 index 000000000..0d011903c --- /dev/null +++ b/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/services/weed_detection/run_weekly.ps1 b/services/weed_detection/run_weekly.ps1 new file mode 100644 index 000000000..9f10eee66 --- /dev/null +++ b/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/services/weed_detection/scripts/cron_job_config.yaml b/services/weed_detection/scripts/cron_job_config.yaml new file mode 100644 index 000000000..55006c170 --- /dev/null +++ b/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/services/weed_detection/scripts/make_masks_auto.py b/services/weed_detection/scripts/make_masks_auto.py new file mode 100644 index 000000000..0c1b03810 --- /dev/null +++ b/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/services/weed_detection/scripts/run_detection.py b/services/weed_detection/scripts/run_detection.py new file mode 100644 index 000000000..e5bc9f234 --- /dev/null +++ b/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/services/weed_detection/services/io.py b/services/weed_detection/services/io.py new file mode 100644 index 000000000..57014b783 --- /dev/null +++ b/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/services/weed_detection/services/minio_client.py b/services/weed_detection/services/minio_client.py new file mode 100644 index 000000000..dd5effd69 --- /dev/null +++ b/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/services/weed_detection/services/minio_sync.py b/services/weed_detection/services/minio_sync.py new file mode 100644 index 000000000..5812fecac --- /dev/null +++ b/services/weed_detection/services/minio_sync.py @@ -0,0 +1,85 @@ + +# 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/services/weed_detection/src/__init__.py b/services/weed_detection/src/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/weed_detection/src/batch_runner.py b/services/weed_detection/src/batch_runner.py new file mode 100644 index 000000000..361e85ac2 --- /dev/null +++ b/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/services/weed_detection/src/detectors/__init__.py b/services/weed_detection/src/detectors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/weed_detection/src/detectors/disease_model.py b/services/weed_detection/src/detectors/disease_model.py new file mode 100644 index 000000000..2428dc4b0 --- /dev/null +++ b/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/services/weed_detection/src/pipeline/__init__.py b/services/weed_detection/src/pipeline/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/services/weed_detection/src/pipeline/config.py b/services/weed_detection/src/pipeline/config.py new file mode 100644 index 000000000..18d696e0a --- /dev/null +++ b/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/services/weed_detection/src/pipeline/db.py b/services/weed_detection/src/pipeline/db.py new file mode 100644 index 000000000..1734b5f1c --- /dev/null +++ b/services/weed_detection/src/pipeline/db.py @@ -0,0 +1,26 @@ +# /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") + return create_engine(db_url, future=True) + + +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/services/weed_detection/src/pipeline/logging_setup.py b/services/weed_detection/src/pipeline/logging_setup.py new file mode 100644 index 000000000..9904581c8 --- /dev/null +++ b/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/services/weed_detection/src/pipeline/utils.py b/services/weed_detection/src/pipeline/utils.py new file mode 100644 index 000000000..426ba28af --- /dev/null +++ b/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 + +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/simulators/.env.example b/simulators/.env.example new file mode 100644 index 000000000..429fc70fc --- /dev/null +++ b/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/simulators/Dockerfile b/simulators/Dockerfile new file mode 100644 index 000000000..7eeed2e60 --- /dev/null +++ b/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/simulators/data/air/images/DRN-482A_20251019T081522Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081522Z.jpg new file mode 100644 index 000000000..e455afb5e Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081522Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081532Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081532Z.jpg new file mode 100644 index 000000000..666b88b1a Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081532Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081542Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081542Z.jpg new file mode 100644 index 000000000..041fd4752 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081542Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081552Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081552Z.jpg new file mode 100644 index 000000000..900499f7b Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081552Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081602Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081602Z.jpg new file mode 100644 index 000000000..3795cef26 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081602Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081612Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081612Z.jpg new file mode 100644 index 000000000..659cca8ca Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081612Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081622Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081622Z.jpg new file mode 100644 index 000000000..8fb3b24b3 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081622Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081632Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081632Z.jpg new file mode 100644 index 000000000..762713c06 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081632Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081642Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081642Z.jpg new file mode 100644 index 000000000..3e5df8308 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081642Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081652Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081652Z.jpg new file mode 100644 index 000000000..af2cda331 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081652Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081702Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081702Z.jpg new file mode 100644 index 000000000..347533a27 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081702Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081712Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081712Z.jpg new file mode 100644 index 000000000..9f71df2f7 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081712Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081722Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081722Z.jpg new file mode 100644 index 000000000..5bd2dd3b2 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081722Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081732Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081732Z.jpg new file mode 100644 index 000000000..5fe4d6cb2 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081732Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081742Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081742Z.jpg new file mode 100644 index 000000000..7e974e0a4 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081742Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081752Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081752Z.jpg new file mode 100644 index 000000000..a3e7d6a59 Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081752Z.jpg differ diff --git a/simulators/data/air/images/DRN-482A_20251019T081802Z.jpg b/simulators/data/air/images/DRN-482A_20251019T081802Z.jpg new file mode 100644 index 000000000..c49bd162d Binary files /dev/null and b/simulators/data/air/images/DRN-482A_20251019T081802Z.jpg differ diff --git a/simulators/data/air/images/f123.jpg b/simulators/data/air/images/f123.jpg new file mode 100644 index 000000000..39b028ae8 Binary files /dev/null and b/simulators/data/air/images/f123.jpg differ diff --git a/simulators/data/air/metadata/DRN-482A_20251019T081522Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081522Z.json new file mode 100644 index 000000000..ab64f2775 --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081532Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081532Z.json new file mode 100644 index 000000000..331839829 --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081542Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081542Z.json new file mode 100644 index 000000000..16ba28f85 --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081552Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081552Z.json new file mode 100644 index 000000000..6d923e2df --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081602Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081602Z.json new file mode 100644 index 000000000..a21af0638 --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081612Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081612Z.json new file mode 100644 index 000000000..c1ce72ba0 --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081622Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081622Z.json new file mode 100644 index 000000000..31cf9e323 --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081632Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081632Z.json new file mode 100644 index 000000000..0d25cde7a --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081642Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081642Z.json new file mode 100644 index 000000000..5a1e829f6 --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081652Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081652Z.json new file mode 100644 index 000000000..40e204264 --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081702Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081702Z.json new file mode 100644 index 000000000..0c8144e0b --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081712Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081712Z.json new file mode 100644 index 000000000..eac5d8daa --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081722Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081722Z.json new file mode 100644 index 000000000..c01d65f21 --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081732Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081732Z.json new file mode 100644 index 000000000..b153a4f4e --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081742Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081742Z.json new file mode 100644 index 000000000..18817e550 --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081752Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081752Z.json new file mode 100644 index 000000000..49ab29568 --- /dev/null +++ b/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/simulators/data/air/metadata/DRN-482A_20251019T081802Z.json b/simulators/data/air/metadata/DRN-482A_20251019T081802Z.json new file mode 100644 index 000000000..d3deb9bc5 --- /dev/null +++ b/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/simulators/data/air/metadata/f123.json b/simulators/data/air/metadata/f123.json new file mode 100644 index 000000000..e6cb22408 --- /dev/null +++ b/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/simulators/data/fruit/images/-RLW8KRN_jpg.rf.0043ffb17d38c36fbef82f22630f9768.jpg b/simulators/data/fruit/images/-RLW8KRN_jpg.rf.0043ffb17d38c36fbef82f22630f9768.jpg new file mode 100644 index 000000000..9c303ab5e Binary files /dev/null and b/simulators/data/fruit/images/-RLW8KRN_jpg.rf.0043ffb17d38c36fbef82f22630f9768.jpg differ diff --git a/simulators/data/fruit/images/-RQC6OEZ_jpg.rf.8f1e079c0e6c0a0a14ea3b5269e4580e.jpg b/simulators/data/fruit/images/-RQC6OEZ_jpg.rf.8f1e079c0e6c0a0a14ea3b5269e4580e.jpg new file mode 100644 index 000000000..407da72e5 Binary files /dev/null and b/simulators/data/fruit/images/-RQC6OEZ_jpg.rf.8f1e079c0e6c0a0a14ea3b5269e4580e.jpg differ diff --git a/simulators/data/fruit/images/-RR9Z6U4_jpg.rf.78ba36c8ee4eae58dae8c6549b7ea326.jpg b/simulators/data/fruit/images/-RR9Z6U4_jpg.rf.78ba36c8ee4eae58dae8c6549b7ea326.jpg new file mode 100644 index 000000000..e316a906b Binary files /dev/null and b/simulators/data/fruit/images/-RR9Z6U4_jpg.rf.78ba36c8ee4eae58dae8c6549b7ea326.jpg differ diff --git a/simulators/data/fruit/images/-RT4BLPA_jpg.rf.59add8a5cf958bcf24019f3a4772e80d.jpg b/simulators/data/fruit/images/-RT4BLPA_jpg.rf.59add8a5cf958bcf24019f3a4772e80d.jpg new file mode 100644 index 000000000..3e8beee39 Binary files /dev/null and b/simulators/data/fruit/images/-RT4BLPA_jpg.rf.59add8a5cf958bcf24019f3a4772e80d.jpg differ diff --git a/simulators/data/fruit/images/-RTFNWAO_jpg.rf.a27d99127a2b04bdeb778dd1ba23f641.jpg b/simulators/data/fruit/images/-RTFNWAO_jpg.rf.a27d99127a2b04bdeb778dd1ba23f641.jpg new file mode 100644 index 000000000..fb854670b Binary files /dev/null and b/simulators/data/fruit/images/-RTFNWAO_jpg.rf.a27d99127a2b04bdeb778dd1ba23f641.jpg differ diff --git a/simulators/data/fruit/images/-RULPROO_jpg.rf.8c2253b84fc7e3790e7f1acbd9f788cf.jpg b/simulators/data/fruit/images/-RULPROO_jpg.rf.8c2253b84fc7e3790e7f1acbd9f788cf.jpg new file mode 100644 index 000000000..9b1026da3 Binary files /dev/null and b/simulators/data/fruit/images/-RULPROO_jpg.rf.8c2253b84fc7e3790e7f1acbd9f788cf.jpg differ diff --git a/simulators/data/fruit/images/-RVME7J6_jpg.rf.ed09b93edfe0596bf7d5360d0a7f580c.jpg b/simulators/data/fruit/images/-RVME7J6_jpg.rf.ed09b93edfe0596bf7d5360d0a7f580c.jpg new file mode 100644 index 000000000..fd185bd98 Binary files /dev/null and b/simulators/data/fruit/images/-RVME7J6_jpg.rf.ed09b93edfe0596bf7d5360d0a7f580c.jpg differ diff --git a/simulators/data/fruit/images/-RW36ZHG_jpg.rf.3c2b18d9069e050c24826fe8d1209505.jpg b/simulators/data/fruit/images/-RW36ZHG_jpg.rf.3c2b18d9069e050c24826fe8d1209505.jpg new file mode 100644 index 000000000..553085130 Binary files /dev/null and b/simulators/data/fruit/images/-RW36ZHG_jpg.rf.3c2b18d9069e050c24826fe8d1209505.jpg differ diff --git a/simulators/data/fruit/images/0074691e66aa85c0_jpg.rf.363957b75c8f2a3ba887e82f1f695174.jpg b/simulators/data/fruit/images/0074691e66aa85c0_jpg.rf.363957b75c8f2a3ba887e82f1f695174.jpg new file mode 100644 index 000000000..60cc00adb Binary files /dev/null and b/simulators/data/fruit/images/0074691e66aa85c0_jpg.rf.363957b75c8f2a3ba887e82f1f695174.jpg differ diff --git a/simulators/data/fruit/images/100005_jpg.rf.591107992ad36c0fd3194c3e17697834.jpg b/simulators/data/fruit/images/100005_jpg.rf.591107992ad36c0fd3194c3e17697834.jpg new file mode 100644 index 000000000..793338da6 Binary files /dev/null and b/simulators/data/fruit/images/100005_jpg.rf.591107992ad36c0fd3194c3e17697834.jpg differ diff --git a/simulators/data/fruit/images/100127_jpg.rf.ea8ed351d019a6ad5063d0fb9d439df0.jpg b/simulators/data/fruit/images/100127_jpg.rf.ea8ed351d019a6ad5063d0fb9d439df0.jpg new file mode 100644 index 000000000..7d4236ba0 Binary files /dev/null and b/simulators/data/fruit/images/100127_jpg.rf.ea8ed351d019a6ad5063d0fb9d439df0.jpg differ diff --git a/simulators/data/fruit/images/100143_jpg.rf.406a9dd2811da5d8e05e4fd9439d8c34.jpg b/simulators/data/fruit/images/100143_jpg.rf.406a9dd2811da5d8e05e4fd9439d8c34.jpg new file mode 100644 index 000000000..c1a9f31dd Binary files /dev/null and b/simulators/data/fruit/images/100143_jpg.rf.406a9dd2811da5d8e05e4fd9439d8c34.jpg differ diff --git a/simulators/data/fruit/images/100145_jpg.rf.7634390a6f9fe6149795b10268f61321.jpg b/simulators/data/fruit/images/100145_jpg.rf.7634390a6f9fe6149795b10268f61321.jpg new file mode 100644 index 000000000..e02b6b297 Binary files /dev/null and b/simulators/data/fruit/images/100145_jpg.rf.7634390a6f9fe6149795b10268f61321.jpg differ diff --git a/simulators/data/fruit/images/100191_jpg.rf.54612a69ef046b79550d72992978b7a1.jpg b/simulators/data/fruit/images/100191_jpg.rf.54612a69ef046b79550d72992978b7a1.jpg new file mode 100644 index 000000000..893508871 Binary files /dev/null and b/simulators/data/fruit/images/100191_jpg.rf.54612a69ef046b79550d72992978b7a1.jpg differ diff --git a/simulators/data/fruit/images/100283_jpg.rf.d746e1de0fb9478dee8044eacfff367a.jpg b/simulators/data/fruit/images/100283_jpg.rf.d746e1de0fb9478dee8044eacfff367a.jpg new file mode 100644 index 000000000..91ff5dca3 Binary files /dev/null and b/simulators/data/fruit/images/100283_jpg.rf.d746e1de0fb9478dee8044eacfff367a.jpg differ diff --git a/simulators/data/fruit/images/1019a34d846fd607_jpg.rf.3a7ac34afa4eb72983eee80377de8725.jpg b/simulators/data/fruit/images/1019a34d846fd607_jpg.rf.3a7ac34afa4eb72983eee80377de8725.jpg new file mode 100644 index 000000000..2eb1695f9 Binary files /dev/null and b/simulators/data/fruit/images/1019a34d846fd607_jpg.rf.3a7ac34afa4eb72983eee80377de8725.jpg differ diff --git a/simulators/data/fruit/images/1621928100_6-pibig_info-p-zimnie-sorta-grush-priroda-krasivo-foto-9_png_jpg.rf.08de7bf9f5819ad187b4b987f09781b6.jpg b/simulators/data/fruit/images/1621928100_6-pibig_info-p-zimnie-sorta-grush-priroda-krasivo-foto-9_png_jpg.rf.08de7bf9f5819ad187b4b987f09781b6.jpg new file mode 100644 index 000000000..95b84cdbc Binary files /dev/null and b/simulators/data/fruit/images/1621928100_6-pibig_info-p-zimnie-sorta-grush-priroda-krasivo-foto-9_png_jpg.rf.08de7bf9f5819ad187b4b987f09781b6.jpg differ diff --git a/simulators/data/fruit/images/200116_jpg.rf.e62de0914b35ebc18f44fa2414002db1.jpg b/simulators/data/fruit/images/200116_jpg.rf.e62de0914b35ebc18f44fa2414002db1.jpg new file mode 100644 index 000000000..892ff605a Binary files /dev/null and b/simulators/data/fruit/images/200116_jpg.rf.e62de0914b35ebc18f44fa2414002db1.jpg differ diff --git a/simulators/data/fruit/images/200118_jpg.rf.21c00a39c7a259288747d39f20889d25.jpg b/simulators/data/fruit/images/200118_jpg.rf.21c00a39c7a259288747d39f20889d25.jpg new file mode 100644 index 000000000..1461de0b2 Binary files /dev/null and b/simulators/data/fruit/images/200118_jpg.rf.21c00a39c7a259288747d39f20889d25.jpg differ diff --git a/simulators/data/fruit/images/200195_jpg.rf.937f61f1271744f9d2539b04bd2f9912.jpg b/simulators/data/fruit/images/200195_jpg.rf.937f61f1271744f9d2539b04bd2f9912.jpg new file mode 100644 index 000000000..8673d456c Binary files /dev/null and b/simulators/data/fruit/images/200195_jpg.rf.937f61f1271744f9d2539b04bd2f9912.jpg differ diff --git a/simulators/data/fruit/images/300191_jpg.rf.d3e44067a676c323188d7f7a0a12168c.jpg b/simulators/data/fruit/images/300191_jpg.rf.d3e44067a676c323188d7f7a0a12168c.jpg new file mode 100644 index 000000000..cfdc2a0c6 Binary files /dev/null and b/simulators/data/fruit/images/300191_jpg.rf.d3e44067a676c323188d7f7a0a12168c.jpg differ diff --git a/simulators/data/fruit/images/31535e8b9ec3c325_jpg.rf.4bcc90f53aaee35ea6da9f68bcf5d86b.jpg b/simulators/data/fruit/images/31535e8b9ec3c325_jpg.rf.4bcc90f53aaee35ea6da9f68bcf5d86b.jpg new file mode 100644 index 000000000..fce68dd87 Binary files /dev/null and b/simulators/data/fruit/images/31535e8b9ec3c325_jpg.rf.4bcc90f53aaee35ea6da9f68bcf5d86b.jpg differ diff --git a/simulators/data/fruit/images/335137_5ba2b1f7efb2f5ba2b1f7efb6a_jpeg_jpg.rf.cdf8d0bfe9cad464ed0134fbbccf3b0a.jpg b/simulators/data/fruit/images/335137_5ba2b1f7efb2f5ba2b1f7efb6a_jpeg_jpg.rf.cdf8d0bfe9cad464ed0134fbbccf3b0a.jpg new file mode 100644 index 000000000..dadb0e762 Binary files /dev/null and b/simulators/data/fruit/images/335137_5ba2b1f7efb2f5ba2b1f7efb6a_jpeg_jpg.rf.cdf8d0bfe9cad464ed0134fbbccf3b0a.jpg differ diff --git a/simulators/data/fruit/images/35765a6ec72af8ea_jpg.rf.0f5265cce363bcce5d0785521164cccf.jpg b/simulators/data/fruit/images/35765a6ec72af8ea_jpg.rf.0f5265cce363bcce5d0785521164cccf.jpg new file mode 100644 index 000000000..41b8ec9ca Binary files /dev/null and b/simulators/data/fruit/images/35765a6ec72af8ea_jpg.rf.0f5265cce363bcce5d0785521164cccf.jpg differ diff --git a/simulators/data/fruit/images/62_jpg.rf.8abc7c6730ae13b0e21058e3492fd1e5.jpg b/simulators/data/fruit/images/62_jpg.rf.8abc7c6730ae13b0e21058e3492fd1e5.jpg new file mode 100644 index 000000000..efe75abf4 Binary files /dev/null and b/simulators/data/fruit/images/62_jpg.rf.8abc7c6730ae13b0e21058e3492fd1e5.jpg differ diff --git a/simulators/data/fruit/images/718a423f1a9b7aba_jpg.rf.c5a58b085bec3e7849b413ebe5451f36.jpg b/simulators/data/fruit/images/718a423f1a9b7aba_jpg.rf.c5a58b085bec3e7849b413ebe5451f36.jpg new file mode 100644 index 000000000..699dc178d Binary files /dev/null and b/simulators/data/fruit/images/718a423f1a9b7aba_jpg.rf.c5a58b085bec3e7849b413ebe5451f36.jpg differ diff --git a/simulators/data/fruit/images/74b1d6fed09b9423_jpg.rf.4e0753c49c731bacea2c7f4af97358b4.jpg b/simulators/data/fruit/images/74b1d6fed09b9423_jpg.rf.4e0753c49c731bacea2c7f4af97358b4.jpg new file mode 100644 index 000000000..81ceb3b57 Binary files /dev/null and b/simulators/data/fruit/images/74b1d6fed09b9423_jpg.rf.4e0753c49c731bacea2c7f4af97358b4.jpg differ diff --git a/simulators/data/fruit/images/793933d88c63ed93_jpg.rf.bd953405b7fb5881ba0670f46ee32428.jpg b/simulators/data/fruit/images/793933d88c63ed93_jpg.rf.bd953405b7fb5881ba0670f46ee32428.jpg new file mode 100644 index 000000000..08eedb83d Binary files /dev/null and b/simulators/data/fruit/images/793933d88c63ed93_jpg.rf.bd953405b7fb5881ba0670f46ee32428.jpg differ diff --git a/simulators/data/fruit/images/9f52969b8e086196_jpg.rf.b3fcc2bfeeff5a2b7cc47cff041c1cb8.jpg b/simulators/data/fruit/images/9f52969b8e086196_jpg.rf.b3fcc2bfeeff5a2b7cc47cff041c1cb8.jpg new file mode 100644 index 000000000..7305ee4a1 Binary files /dev/null and b/simulators/data/fruit/images/9f52969b8e086196_jpg.rf.b3fcc2bfeeff5a2b7cc47cff041c1cb8.jpg differ diff --git a/simulators/data/fruit/images/apple-114-_jpg.rf.a4e544fcd49e96f2dfae23b1570fa6a7.jpg b/simulators/data/fruit/images/apple-114-_jpg.rf.a4e544fcd49e96f2dfae23b1570fa6a7.jpg new file mode 100644 index 000000000..4f25fd4ea Binary files /dev/null and b/simulators/data/fruit/images/apple-114-_jpg.rf.a4e544fcd49e96f2dfae23b1570fa6a7.jpg differ diff --git a/simulators/data/fruit/images/apple-116-_jpg.rf.d038ba3bffa413f78433a562ea42274f.jpg b/simulators/data/fruit/images/apple-116-_jpg.rf.d038ba3bffa413f78433a562ea42274f.jpg new file mode 100644 index 000000000..8d873c5d0 Binary files /dev/null and b/simulators/data/fruit/images/apple-116-_jpg.rf.d038ba3bffa413f78433a562ea42274f.jpg differ diff --git a/simulators/data/fruit/images/apple-133-_jpg.rf.6b2c93eccff7b5fa836e4a22565f6e4e.jpg b/simulators/data/fruit/images/apple-133-_jpg.rf.6b2c93eccff7b5fa836e4a22565f6e4e.jpg new file mode 100644 index 000000000..5a558d814 Binary files /dev/null and b/simulators/data/fruit/images/apple-133-_jpg.rf.6b2c93eccff7b5fa836e4a22565f6e4e.jpg differ diff --git a/simulators/data/fruit/images/apple-153-_jpg.rf.e96773435aefeb0b1842e45a114c6ce1.jpg b/simulators/data/fruit/images/apple-153-_jpg.rf.e96773435aefeb0b1842e45a114c6ce1.jpg new file mode 100644 index 000000000..46cd2700f Binary files /dev/null and b/simulators/data/fruit/images/apple-153-_jpg.rf.e96773435aefeb0b1842e45a114c6ce1.jpg differ diff --git a/simulators/data/fruit/images/apple-32-_jpg.rf.2c3206f933adce572e7533c890732139.jpg b/simulators/data/fruit/images/apple-32-_jpg.rf.2c3206f933adce572e7533c890732139.jpg new file mode 100644 index 000000000..bd09193ec Binary files /dev/null and b/simulators/data/fruit/images/apple-32-_jpg.rf.2c3206f933adce572e7533c890732139.jpg differ diff --git a/simulators/data/fruit/images/apple-40-_jpg.rf.b6ece741bb18902eb58ceb92e70c9256.jpg b/simulators/data/fruit/images/apple-40-_jpg.rf.b6ece741bb18902eb58ceb92e70c9256.jpg new file mode 100644 index 000000000..ea8a2a438 Binary files /dev/null and b/simulators/data/fruit/images/apple-40-_jpg.rf.b6ece741bb18902eb58ceb92e70c9256.jpg differ diff --git a/simulators/data/fruit/images/apple-46-_jpg.rf.0e7eada84451f690497bf5d6c1199567.jpg b/simulators/data/fruit/images/apple-46-_jpg.rf.0e7eada84451f690497bf5d6c1199567.jpg new file mode 100644 index 000000000..bdc4d7bba Binary files /dev/null and b/simulators/data/fruit/images/apple-46-_jpg.rf.0e7eada84451f690497bf5d6c1199567.jpg differ diff --git a/simulators/data/fruit/images/apple-58-_jpg.rf.98a8c7ec4810c06485a92f7e5575d3f9.jpg b/simulators/data/fruit/images/apple-58-_jpg.rf.98a8c7ec4810c06485a92f7e5575d3f9.jpg new file mode 100644 index 000000000..79369c7ec Binary files /dev/null and b/simulators/data/fruit/images/apple-58-_jpg.rf.98a8c7ec4810c06485a92f7e5575d3f9.jpg differ diff --git a/simulators/data/fruit/images/apple-59-_jpg.rf.af2bfdb62643bb432ddf139d19adba64.jpg b/simulators/data/fruit/images/apple-59-_jpg.rf.af2bfdb62643bb432ddf139d19adba64.jpg new file mode 100644 index 000000000..256cd68a4 Binary files /dev/null and b/simulators/data/fruit/images/apple-59-_jpg.rf.af2bfdb62643bb432ddf139d19adba64.jpg differ diff --git a/simulators/data/fruit/images/b3b8570aa1f72c7d_jpg.rf.a1ea3c86f61166666e8748421240f6da.jpg b/simulators/data/fruit/images/b3b8570aa1f72c7d_jpg.rf.a1ea3c86f61166666e8748421240f6da.jpg new file mode 100644 index 000000000..80acebda1 Binary files /dev/null and b/simulators/data/fruit/images/b3b8570aa1f72c7d_jpg.rf.a1ea3c86f61166666e8748421240f6da.jpg differ diff --git a/simulators/data/fruit/images/damaged_apple-271-_jpg.rf.2c60f3d9a97707a0c39b1c254cc8f909.jpg b/simulators/data/fruit/images/damaged_apple-271-_jpg.rf.2c60f3d9a97707a0c39b1c254cc8f909.jpg new file mode 100644 index 000000000..7edac1fee Binary files /dev/null and b/simulators/data/fruit/images/damaged_apple-271-_jpg.rf.2c60f3d9a97707a0c39b1c254cc8f909.jpg differ diff --git a/simulators/data/fruit/images/damaged_apple-44-_jpg.rf.026aba6068bf86237e06d5bd352991c8.jpg b/simulators/data/fruit/images/damaged_apple-44-_jpg.rf.026aba6068bf86237e06d5bd352991c8.jpg new file mode 100644 index 000000000..0d62967ab Binary files /dev/null and b/simulators/data/fruit/images/damaged_apple-44-_jpg.rf.026aba6068bf86237e06d5bd352991c8.jpg differ diff --git a/simulators/data/fruit/images/damaged_apple-45-_jpg.rf.5586dda61e0101f4c6109c9f9d093698.jpg b/simulators/data/fruit/images/damaged_apple-45-_jpg.rf.5586dda61e0101f4c6109c9f9d093698.jpg new file mode 100644 index 000000000..4af17398e Binary files /dev/null and b/simulators/data/fruit/images/damaged_apple-45-_jpg.rf.5586dda61e0101f4c6109c9f9d093698.jpg differ diff --git a/simulators/data/fruit/images/damaged_apple-46-_jpg.rf.05d05d2c5b783b186d5736c533e6e7ce.jpg b/simulators/data/fruit/images/damaged_apple-46-_jpg.rf.05d05d2c5b783b186d5736c533e6e7ce.jpg new file mode 100644 index 000000000..249f3fc1d Binary files /dev/null and b/simulators/data/fruit/images/damaged_apple-46-_jpg.rf.05d05d2c5b783b186d5736c533e6e7ce.jpg differ diff --git a/simulators/data/fruit/images/damaged_apple-47-_jpg.rf.4182463fe1813f07131d1690bd39f0bd.jpg b/simulators/data/fruit/images/damaged_apple-47-_jpg.rf.4182463fe1813f07131d1690bd39f0bd.jpg new file mode 100644 index 000000000..7df8fb860 Binary files /dev/null and b/simulators/data/fruit/images/damaged_apple-47-_jpg.rf.4182463fe1813f07131d1690bd39f0bd.jpg differ diff --git a/simulators/data/fruit/images/damaged_apple-52-_jpg.rf.c1277d2413ed2525eed2eb9302a02022.jpg b/simulators/data/fruit/images/damaged_apple-52-_jpg.rf.c1277d2413ed2525eed2eb9302a02022.jpg new file mode 100644 index 000000000..189761e01 Binary files /dev/null and b/simulators/data/fruit/images/damaged_apple-52-_jpg.rf.c1277d2413ed2525eed2eb9302a02022.jpg differ diff --git a/simulators/data/fruit/images/damaged_apple-53-_jpg.rf.1f9dc605770124696df7cdec92ea92f2.jpg b/simulators/data/fruit/images/damaged_apple-53-_jpg.rf.1f9dc605770124696df7cdec92ea92f2.jpg new file mode 100644 index 000000000..b894a94a2 Binary files /dev/null and b/simulators/data/fruit/images/damaged_apple-53-_jpg.rf.1f9dc605770124696df7cdec92ea92f2.jpg differ diff --git a/simulators/data/fruit/images/damaged_apple-54-_jpg.rf.adfb5f6cf4b5fb2c4b0632b412263d75.jpg b/simulators/data/fruit/images/damaged_apple-54-_jpg.rf.adfb5f6cf4b5fb2c4b0632b412263d75.jpg new file mode 100644 index 000000000..61cd86040 Binary files /dev/null and b/simulators/data/fruit/images/damaged_apple-54-_jpg.rf.adfb5f6cf4b5fb2c4b0632b412263d75.jpg differ diff --git a/simulators/data/fruit/images/damaged_apple-66-_jpg.rf.7d59ec3feefc83d7005a8998e65a6ebc.jpg b/simulators/data/fruit/images/damaged_apple-66-_jpg.rf.7d59ec3feefc83d7005a8998e65a6ebc.jpg new file mode 100644 index 000000000..c98a7816d Binary files /dev/null and b/simulators/data/fruit/images/damaged_apple-66-_jpg.rf.7d59ec3feefc83d7005a8998e65a6ebc.jpg differ diff --git a/simulators/data/fruit/images/damaged_apple-72-_jpg.rf.d8cb01f5200747fae284329111d297be.jpg b/simulators/data/fruit/images/damaged_apple-72-_jpg.rf.d8cb01f5200747fae284329111d297be.jpg new file mode 100644 index 000000000..d139e171e Binary files /dev/null and b/simulators/data/fruit/images/damaged_apple-72-_jpg.rf.d8cb01f5200747fae284329111d297be.jpg differ diff --git a/simulators/data/fruit/images/pomegranate-haku-botan-1-2_jpg.rf.9ab31e3c1b313ea38bd2da546fdb309a.jpg b/simulators/data/fruit/images/pomegranate-haku-botan-1-2_jpg.rf.9ab31e3c1b313ea38bd2da546fdb309a.jpg new file mode 100644 index 000000000..de10127ea Binary files /dev/null and b/simulators/data/fruit/images/pomegranate-haku-botan-1-2_jpg.rf.9ab31e3c1b313ea38bd2da546fdb309a.jpg differ diff --git a/simulators/data/fruit/images/wonderful-pomegranate-1-1_jpg.rf.cada9a72952c058afa8f2ad0f573b791.jpg b/simulators/data/fruit/images/wonderful-pomegranate-1-1_jpg.rf.cada9a72952c058afa8f2ad0f573b791.jpg new file mode 100644 index 000000000..d22314eee Binary files /dev/null and b/simulators/data/fruit/images/wonderful-pomegranate-1-1_jpg.rf.cada9a72952c058afa8f2ad0f573b791.jpg differ diff --git a/simulators/data/fruit/images/z_p20-Pomegranate_jpg.rf.5eb6284b9d5c861c63d11347b7f8c7f2.jpg b/simulators/data/fruit/images/z_p20-Pomegranate_jpg.rf.5eb6284b9d5c861c63d11347b7f8c7f2.jpg new file mode 100644 index 000000000..605bbfa10 Binary files /dev/null and b/simulators/data/fruit/images/z_p20-Pomegranate_jpg.rf.5eb6284b9d5c861c63d11347b7f8c7f2.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0021.jpg b/simulators/data/security/cam-central-01/images/frame_0021.jpg new file mode 100644 index 000000000..a612abf35 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0021.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0022.jpg b/simulators/data/security/cam-central-01/images/frame_0022.jpg new file mode 100644 index 000000000..2a8c78282 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0022.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0023.jpg b/simulators/data/security/cam-central-01/images/frame_0023.jpg new file mode 100644 index 000000000..89f217ced Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0023.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0024.jpg b/simulators/data/security/cam-central-01/images/frame_0024.jpg new file mode 100644 index 000000000..5a9c99b7d Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0024.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0025.jpg b/simulators/data/security/cam-central-01/images/frame_0025.jpg new file mode 100644 index 000000000..eb493ecc3 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0025.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0026.jpg b/simulators/data/security/cam-central-01/images/frame_0026.jpg new file mode 100644 index 000000000..d33eda186 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0026.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0027.jpg b/simulators/data/security/cam-central-01/images/frame_0027.jpg new file mode 100644 index 000000000..eb7a6f9e1 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0027.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0028.jpg b/simulators/data/security/cam-central-01/images/frame_0028.jpg new file mode 100644 index 000000000..eb388051a Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0028.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0029.jpg b/simulators/data/security/cam-central-01/images/frame_0029.jpg new file mode 100644 index 000000000..1dd8ce557 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0029.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0030.jpg b/simulators/data/security/cam-central-01/images/frame_0030.jpg new file mode 100644 index 000000000..9f5148b82 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0030.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0031.jpg b/simulators/data/security/cam-central-01/images/frame_0031.jpg new file mode 100644 index 000000000..f3c5cdff9 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0031.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0032.jpg b/simulators/data/security/cam-central-01/images/frame_0032.jpg new file mode 100644 index 000000000..166d45aac Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0032.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0033.jpg b/simulators/data/security/cam-central-01/images/frame_0033.jpg new file mode 100644 index 000000000..25bb68499 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0033.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0034.jpg b/simulators/data/security/cam-central-01/images/frame_0034.jpg new file mode 100644 index 000000000..86f37a6a9 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0034.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0035.jpg b/simulators/data/security/cam-central-01/images/frame_0035.jpg new file mode 100644 index 000000000..434876b35 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0035.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0036.jpg b/simulators/data/security/cam-central-01/images/frame_0036.jpg new file mode 100644 index 000000000..f6060f342 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0036.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0037.jpg b/simulators/data/security/cam-central-01/images/frame_0037.jpg new file mode 100644 index 000000000..d70f6ba88 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0037.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0038.jpg b/simulators/data/security/cam-central-01/images/frame_0038.jpg new file mode 100644 index 000000000..21c19b781 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0038.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0039.jpg b/simulators/data/security/cam-central-01/images/frame_0039.jpg new file mode 100644 index 000000000..919d31311 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0039.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0040.jpg b/simulators/data/security/cam-central-01/images/frame_0040.jpg new file mode 100644 index 000000000..8fb938e74 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0040.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0041.jpg b/simulators/data/security/cam-central-01/images/frame_0041.jpg new file mode 100644 index 000000000..4f29f280e Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0041.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0042.jpg b/simulators/data/security/cam-central-01/images/frame_0042.jpg new file mode 100644 index 000000000..32416df4b Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0042.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0043.jpg b/simulators/data/security/cam-central-01/images/frame_0043.jpg new file mode 100644 index 000000000..09a55fad6 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0043.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0044.jpg b/simulators/data/security/cam-central-01/images/frame_0044.jpg new file mode 100644 index 000000000..f99ad1cad Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0044.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0045.jpg b/simulators/data/security/cam-central-01/images/frame_0045.jpg new file mode 100644 index 000000000..ff5574583 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0045.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0046.jpg b/simulators/data/security/cam-central-01/images/frame_0046.jpg new file mode 100644 index 000000000..6a244097f Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0046.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0047.jpg b/simulators/data/security/cam-central-01/images/frame_0047.jpg new file mode 100644 index 000000000..c074b39fb Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0047.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0048.jpg b/simulators/data/security/cam-central-01/images/frame_0048.jpg new file mode 100644 index 000000000..cade6b9a6 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0048.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0049.jpg b/simulators/data/security/cam-central-01/images/frame_0049.jpg new file mode 100644 index 000000000..41d13e137 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0049.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0050.jpg b/simulators/data/security/cam-central-01/images/frame_0050.jpg new file mode 100644 index 000000000..ab37ac242 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0050.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0051.jpg b/simulators/data/security/cam-central-01/images/frame_0051.jpg new file mode 100644 index 000000000..23249f758 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0051.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0052.jpg b/simulators/data/security/cam-central-01/images/frame_0052.jpg new file mode 100644 index 000000000..8e9a34caf Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0052.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0053.jpg b/simulators/data/security/cam-central-01/images/frame_0053.jpg new file mode 100644 index 000000000..327e2748c Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0053.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0054.jpg b/simulators/data/security/cam-central-01/images/frame_0054.jpg new file mode 100644 index 000000000..1cf75e5fa Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0054.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0055.jpg b/simulators/data/security/cam-central-01/images/frame_0055.jpg new file mode 100644 index 000000000..86afad386 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0055.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0056.jpg b/simulators/data/security/cam-central-01/images/frame_0056.jpg new file mode 100644 index 000000000..3a52453ed Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0056.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0057.jpg b/simulators/data/security/cam-central-01/images/frame_0057.jpg new file mode 100644 index 000000000..df1ed93a7 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0057.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0058.jpg b/simulators/data/security/cam-central-01/images/frame_0058.jpg new file mode 100644 index 000000000..20d26557b Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0058.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0059.jpg b/simulators/data/security/cam-central-01/images/frame_0059.jpg new file mode 100644 index 000000000..40ac46bf1 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0059.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0060.jpg b/simulators/data/security/cam-central-01/images/frame_0060.jpg new file mode 100644 index 000000000..e7136dbd0 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0060.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0061.jpg b/simulators/data/security/cam-central-01/images/frame_0061.jpg new file mode 100644 index 000000000..3b2c4d9f2 Binary files /dev/null and b/simulators/data/security/cam-central-01/images/frame_0061.jpg differ diff --git a/simulators/data/security/cam-central-01/images/frame_0062.jpg b/simulators/data/security/cam-central-01/images/frame_0062.jpg new file mode 100644 index 0000{"code":"deadline_exceeded","msg":"operation timed out"}