Spaces:
Sleeping
Sleeping
| """Όλη η μη-UI λογική: καιρός, φυσική (ζόρι), οικονομικά, σκορ ανωμαλίας.""" | |
| import numpy as np | |
| import requests | |
| # ========================================================================= | |
| # ΚΑΙΡΟΣ (πραγματικά δεδομένα — Open-Meteo, δωρεάν, χωρίς key) | |
| # ========================================================================= | |
| def get_weather(lat, lon): | |
| """Σημειακός καιρός για το panel ανάλυσης.""" | |
| out = {} | |
| try: | |
| r = requests.get("https://marine-api.open-meteo.com/v1/marine", | |
| params={"latitude": lat, "longitude": lon, | |
| "current": "wave_height,wave_period,wave_direction," | |
| "sea_surface_temperature,ocean_current_velocity"}, | |
| timeout=10).json().get("current", {}) | |
| out.update(wave_height=r.get("wave_height"), wave_period=r.get("wave_period"), | |
| wave_direction=r.get("wave_direction"), | |
| sst=r.get("sea_surface_temperature"), | |
| current=r.get("ocean_current_velocity")) | |
| except Exception: | |
| pass | |
| try: | |
| r = requests.get("https://api.open-meteo.com/v1/forecast", | |
| params={"latitude": lat, "longitude": lon, | |
| "current": "wind_speed_10m,wind_direction_10m,wind_gusts_10m", | |
| "wind_speed_unit": "kn"}, | |
| timeout=10).json().get("current", {}) | |
| out.update(wind_speed=r.get("wind_speed_10m"), | |
| wind_direction=r.get("wind_direction_10m"), | |
| wind_gusts=r.get("wind_gusts_10m")) | |
| except Exception: | |
| pass | |
| return out | |
| def get_marine_grid(lats, lons): | |
| """Φέρνει wave_height & ocean_current_velocity για ΠΛΕΓΜΑ σημείων. | |
| Επιστρέφει (wave[H,W], current[H,W]) με NaN στη στεριά (όπου δεν υπάρχουν marine data). | |
| Κάνει batching γιατί το API δέχεται πολλά σημεία ανά κλήση.""" | |
| H, W = len(lats), len(lons) | |
| LAT = np.repeat(lats, W) # row-major | |
| LON = np.tile(lons, H) | |
| wave = np.full(H * W, np.nan) | |
| curr = np.full(H * W, np.nan) | |
| CHUNK = 120 | |
| for i in range(0, H * W, CHUNK): | |
| la = LAT[i:i + CHUNK]; lo = LON[i:i + CHUNK] | |
| try: | |
| data = requests.get( | |
| "https://marine-api.open-meteo.com/v1/marine", | |
| params={"latitude": ",".join(f"{x:.4f}" for x in la), | |
| "longitude": ",".join(f"{x:.4f}" for x in lo), | |
| "current": "wave_height,ocean_current_velocity"}, | |
| timeout=20).json() | |
| if isinstance(data, dict): | |
| data = [data] | |
| for j, d in enumerate(data): | |
| cur = d.get("current", {}) | |
| wave[i + j] = cur.get("wave_height", np.nan) | |
| curr[i + j] = cur.get("ocean_current_velocity", np.nan) | |
| except Exception: | |
| pass # αφήνει NaN -> θα θεωρηθεί αδιάβατο | |
| return wave.reshape(H, W), curr.reshape(H, W) | |
| def get_point_waves(lat_list, lon_list): | |
| """wave_height για ζεύγη σημείων (όχι πλέγμα). NaN σε στεριά/αποτυχία. | |
| Κάνει batching γιατί το API δέχεται πολλά σημεία ανά κλήση.""" | |
| n = len(lat_list) | |
| out = np.full(n, np.nan) | |
| CHUNK = 100 | |
| for i in range(0, n, CHUNK): | |
| la = lat_list[i:i + CHUNK]; lo = lon_list[i:i + CHUNK] | |
| try: | |
| data = requests.get( | |
| "https://marine-api.open-meteo.com/v1/marine", | |
| params={"latitude": ",".join(f"{x:.3f}" for x in la), | |
| "longitude": ",".join(f"{x:.3f}" for x in lo), | |
| "current": "wave_height"}, | |
| timeout=20).json() | |
| if isinstance(data, dict): | |
| data = [data] | |
| for j, d in enumerate(data): | |
| out[i + j] = d.get("current", {}).get("wave_height", np.nan) | |
| except Exception: | |
| pass | |
| return out | |
| def geocode_port(name): | |
| """Προσπαθεί να βρει συντεταγμένες λιμανιού από όνομα. Επιστρέφει (lat, lon) ή None.""" | |
| if not name: | |
| return None | |
| try: | |
| r = requests.get("https://geocoding-api.open-meteo.com/v1/search", | |
| params={"name": name, "count": 1}, timeout=10).json() | |
| res = r.get("results") | |
| if res: | |
| return res[0]["latitude"], res[0]["longitude"] | |
| except Exception: | |
| pass | |
| return None | |
| # ========================================================================= | |
| # ΦΥΣΙΚΗ — εκτίμηση ζορίσματος (ΟΧΙ μέτρηση) | |
| # ========================================================================= | |
| WEATHER_DRAG_K = 0.03 | |
| def _safe_type(t): | |
| """Μετατρέπει τον τύπο πλοίου σε int με ασφάλεια. None αν λείπει/NaN.""" | |
| if t is None: | |
| return None | |
| try: | |
| if isinstance(t, float) and np.isnan(t): | |
| return None | |
| return int(t) | |
| except (ValueError, TypeError): | |
| return None | |
| def estimate_cb(t): | |
| t = _safe_type(t) | |
| if not t: | |
| return 0.68 | |
| return {6: 0.62, 7: 0.75, 8: 0.82}.get(t // 10, 0.68) | |
| def estimate_displacement(L, B, T, cb): | |
| if not (L and B and T): | |
| return None | |
| return L * B * T * cb * 1.025 | |
| def estimate_power_calm(disp_t, speed_kn, admiralty_c): | |
| if not disp_t or not speed_kn or speed_kn <= 0: | |
| return None | |
| return (disp_t ** (2 / 3)) * (speed_kn ** 3) / admiralty_c | |
| def estimate_resistance(power_kw, speed_kn): | |
| if not power_kw or not speed_kn or speed_kn <= 0: | |
| return None | |
| return power_kw * 1000 / (speed_kn * 0.514444) / 1000 | |
| def weather_factor(wave_h): | |
| return 1.0 if wave_h is None else 1.0 + WEATHER_DRAG_K * (wave_h ** 2) | |
| # ========================================================================= | |
| # ΟΙΚΟΝΟΜΙΚΑ — εκτίμηση (ΟΧΙ ο πραγματικός ναύλος του πλοίου) | |
| # ========================================================================= | |
| DWT_COEFF = 0.80 | |
| CO2_PER_FUEL = 3.114 # τόνοι CO2 ανά τόνο VLSFO | |
| DEFAULT_RATE = { | |
| "Capesize": 18000, "Panamax": 12000, "Supramax": 11000, "Handysize": 10000, | |
| "VLCC": 40000, "Suezmax": 35000, "Aframax": 30000, | |
| "Products/Small tanker": 18000, "Tanker": 25000, "Bulk (άγνωστο)": 12000, | |
| "Άλλο": 12000, | |
| } | |
| def estimate_dwt(disp_t): | |
| return disp_t * DWT_COEFF if disp_t else None | |
| def vessel_class(dwt, ship_type): | |
| st = _safe_type(ship_type) | |
| if st and st // 10 == 8: | |
| if not dwt: | |
| return "Tanker" | |
| if dwt >= 200000: return "VLCC" | |
| if dwt >= 120000: return "Suezmax" | |
| if dwt >= 80000: return "Aframax" | |
| return "Products/Small tanker" | |
| if st and st // 10 == 7: | |
| if not dwt: | |
| return "Bulk (άγνωστο)" | |
| if dwt >= 100000: return "Capesize" | |
| if dwt >= 65000: return "Panamax" | |
| if dwt >= 40000: return "Supramax" | |
| return "Handysize" | |
| return "Άλλο" | |
| def fuel_per_day(power_kw, sfoc): | |
| return power_kw * sfoc * 24 / 1e6 if power_kw else None | |
| def co2_per_day(fuel_t): | |
| return fuel_t * CO2_PER_FUEL if fuel_t else None | |
| def laden_status(draught, L): | |
| if not draught or not L: | |
| return "άγνωστο" | |
| t_design = L / 14.0 | |
| return "έμφορτο (laden)" if draught >= 0.85 * t_design else "πιθανώς έρμα (ballast)" | |
| # ========================================================================= | |
| # ΑΝΩΜΑΛΙΑ — μπλε (ομαλό) -> κόκκινο (στραβά), από τα συσσωρευμένα δεδομένα | |
| # ========================================================================= | |
| def fleet_baseline(log): | |
| moving = log[log["speed_kn"] > 1] if len(log) else log | |
| return { | |
| "speed_std": (moving["speed_kn"].std() if len(moving) else 1.0) or 1.0, | |
| "speed_mean": (moving["speed_kn"].mean() if len(moving) else 8.0) or 8.0, | |
| "n": len(log), | |
| } | |
| def behaviour_score(track, base): | |
| pts = track.sort_values("timestamp") | |
| speeds = pts["speed_kn"].dropna() | |
| score, reasons = 0.0, [] | |
| base_std = base.get("speed_std") or 1.0 | |
| if len(speeds) >= 2: | |
| if speeds.std() > 2.5 * base_std: | |
| score += 0.35; reasons.append("ασταθής ταχύτητα") | |
| if speeds.max() > 5 and speeds.min() < 0.5 and (speeds.max() - speeds.min()) > 5: | |
| score += 0.30; reasons.append("απότομη ακινητοποίηση") | |
| if len(pts) >= 3: | |
| cc = pts["course"].dropna().values | |
| if len(cc) >= 2: | |
| d = np.abs(np.diff(cc)); d = np.minimum(d, 360 - d) | |
| if d.mean() > 30: | |
| score += 0.25; reasons.append("ελιγμοί/ζιγκ-ζαγκ") | |
| if len(speeds) and speeds.iloc[-1] < 0.3: | |
| score += 0.10; reasons.append("σταματημένο") | |
| return min(score, 1.0), reasons | |
| def color_for(s): | |
| return [int(40 + s * 180), max(int(120 - s * 100), 20), max(int(255 - s * 230), 20)] | |
| # ========================================================================= | |
| # EU ETS — κόστος ρύπων ανά ταξίδι (ΕΚΤΙΜΗΣΗ από δημόσιο σήμα) | |
| # ========================================================================= | |
| import math | |
| # Χώρες EU + EEA (Ισλανδία/Νορβηγία/Λίχτενσταϊν) — όπου ισχύει το EU ETS ναυτιλίας | |
| EU_EEA = {"AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE", "GR", | |
| "HU", "IE", "IT", "LV", "LT", "LU", "MT", "NL", "PL", "PT", "RO", "SK", | |
| "SI", "ES", "SE", "IS", "NO", "LI"} | |
| EUA_DEFAULT = 78.0 # €/τόνο CO2 (τρέχουσα τιμή ~77-80, Ιούν 2026) | |
| # Κατανάλωση καυσίμου σε έμφορτη ταχύτητα υπηρεσίας (t/ημέρα), μέσος όρος κλάδου | |
| FUEL_BY_CLASS = { | |
| "VLCC": 80, "Suezmax": 62, "Aframax": 50, "Products/Small tanker": 28, | |
| "Tanker": 45, "Capesize": 55, "Panamax": 38, "Supramax": 30, | |
| "Handysize": 24, "Bulk (άγνωστο)": 35, "Άλλο": 38, | |
| } | |
| # Τυπική ταχύτητα υπηρεσίας (kn) — όταν το πλοίο είναι σταματημένο/αργό | |
| SERVICE_SPEED = { | |
| "VLCC": 14.0, "Suezmax": 13.5, "Aframax": 12.5, "Products/Small tanker": 12.0, | |
| "Tanker": 12.5, "Capesize": 12.0, "Panamax": 12.5, "Supramax": 12.5, | |
| "Handysize": 12.0, "Bulk (άγνωστο)": 12.0, "Άλλο": 12.0, | |
| } | |
| # Σταδιακή εφαρμογή EU ETS: ποσοστό εκπομπών που μετράει ανά έτος | |
| PHASE_IN = {2024: 0.40, 2025: 0.70} # 2026 και μετά = 100% | |
| def geocode_port_full(name): | |
| """Όνομα λιμανιού -> dict(lat, lon, country_code, name) ή None.""" | |
| if not name: | |
| return None | |
| try: | |
| r = requests.get("https://geocoding-api.open-meteo.com/v1/search", | |
| params={"name": name, "count": 1}, timeout=10).json() | |
| res = r.get("results") | |
| if res: | |
| x = res[0] | |
| return {"lat": x["latitude"], "lon": x["longitude"], | |
| "country_code": x.get("country_code"), "name": x.get("name")} | |
| except Exception: | |
| pass | |
| return None | |
| def is_eu(country_code): | |
| return (country_code or "").upper() in EU_EEA | |
| def haversine_nm(lat1, lon1, lat2, lon2): | |
| """Απόσταση μεγίστου κύκλου σε ναυτικά μίλια.""" | |
| R = 3440.065 | |
| p1, p2 = math.radians(lat1), math.radians(lat2) | |
| dp = math.radians(lat2 - lat1) | |
| dl = math.radians(lon2 - lon1) | |
| a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2 | |
| return 2 * R * math.asin(math.sqrt(a)) | |
| def phase_in_factor(year): | |
| return PHASE_IN.get(int(year), 1.0) | |
| def ets_coverage(n_eu_ports): | |
| """0 λιμάνια ΕΕ -> 0%, 1 -> 50%, 2 -> 100%.""" | |
| return {0: 0.0, 1: 0.5, 2: 1.0}.get(int(n_eu_ports), 0.0) | |
| def ets_estimate(dist_nm, speed_kn, fuel_t_day, n_eu_ports, eua, year): | |
| """Επιστρέφει dict με ημέρες, καύσιμο, CO2, κάλυψη, κόστος. None αν λείπουν δεδομένα.""" | |
| if not (dist_nm and speed_kn and fuel_t_day): | |
| return None | |
| days = dist_nm / (speed_kn * 24) | |
| fuel = days * fuel_t_day | |
| co2 = fuel * CO2_PER_FUEL | |
| cov = ets_coverage(n_eu_ports) | |
| yf = phase_in_factor(year) | |
| covered = co2 * cov * yf | |
| cost = covered * eua | |
| return {"days": days, "fuel": fuel, "co2": co2, "coverage": cov, | |
| "phase": yf, "covered": covered, "cost": cost} | |