Enhance report generation by adding peak lightning activity analysis and key findings section. Introduce new functions for summarizing storm activity and update report strings for improved clarity. Refactor analysis radius logic to ensure accurate boundary calculations based on distance rings.

This commit is contained in:
erdemerikci 2026-06-08 17:03:11 +03:00
parent 6d6e2e4d7c
commit 29622b48fb
6 changed files with 290 additions and 45 deletions

View File

@ -28,6 +28,29 @@ def filter_lightning_by_distance(lightning_df, centroid_lat, centroid_lng):
mask = dists <= max_distance mask = dists <= max_distance
return lightning_df.loc[mask].copy() return lightning_df.loc[mask].copy()
def get_peak_activity(lightning_df, centroid_lat, centroid_lng):
"""
Return the single busiest minute within the analysis radius.
Returns a dict {'when': 'DD-MM-YYYY HH:MM', 'count': int} or None when there
is no activity to summarize.
"""
filtered_df = filter_lightning_by_distance(lightning_df, centroid_lat, centroid_lng)
if len(filtered_df) == 0:
return None
f = filtered_df.copy()
if f['local_time'].dtype == 'object':
f['local_time'] = pd.to_datetime(f['local_time'])
f['minute_rounded'] = f['local_time'].dt.floor('min')
counts = f.groupby('minute_rounded').size()
if counts.empty:
return None
peak_minute = counts.idxmax()
return {'when': peak_minute.strftime('%d-%m-%Y %H:%M'), 'count': int(counts.max())}
def find_activity_periods(df, min_gap_minutes=30, min_events_per_period=10): def find_activity_periods(df, min_gap_minutes=30, min_events_per_period=10):
""" """
Find concentrated activity periods based on time gaps. Find concentrated activity periods based on time gaps.
@ -198,7 +221,7 @@ def _build_histogram_figure_for_periods(periods_chunk, max_distance_km, lang=Non
period_str = f"{start.strftime('%d-%m-%Y %H:%M')}-{end.strftime('%d-%m-%Y %H:%M')}" period_str = f"{start.strftime('%d-%m-%Y %H:%M')}-{end.strftime('%d-%m-%Y %H:%M')}"
# Use line breaks to fit within histogram width # Use line breaks to fit within histogram width
return f"Date: {date_str}<br>Period: {period_str}<br>Total lightnings: {event_count}" return s.hist_period_title.format(date=date_str, period=period_str, total=event_count)
fig = make_subplots( fig = make_subplots(
rows=n_rows, rows=n_rows,
@ -226,7 +249,7 @@ def _build_histogram_figure_for_periods(periods_chunk, max_distance_km, lang=Non
if p_type in minute_counts.columns: if p_type in minute_counts.columns:
counts = minute_counts[p_type].values counts = minute_counts[p_type].values
p_type_name = "Cloud-to-Ground" if p_type == '0' else "Intercloud" p_type_name = s.hist_cg_legend if p_type == '0' else s.hist_ic_legend
fig.add_trace( fig.add_trace(
go.Bar( go.Bar(

View File

@ -18,7 +18,7 @@ from docx.oxml import OxmlElement
from docx.oxml.ns import qn from docx.oxml.ns import qn
from docx.shared import Cm, Inches, Pt, RGBColor from docx.shared import Cm, Inches, Pt, RGBColor
from src.analysis.histogram import create_lightning_histogram_pages from src.analysis.histogram import create_lightning_histogram_pages, get_peak_activity
from src.analysis.risk import calculate_turbine_risks from src.analysis.risk import calculate_turbine_risks
from src.analysis.statistics import calculate_lightning_statistics from src.analysis.statistics import calculate_lightning_statistics
from src.config import config from src.config import config
@ -36,7 +36,7 @@ from src.utils import (
now_in_timezone, now_in_timezone,
get_analysis_radius_m, get_analysis_radius_m,
) )
from src.analysis.geospatial import haversine_distance from src.analysis.geospatial import haversine_distance, haversine_distance_vectorized
from src.visualization.maps import plot_cloud_to_ground_coordinate_plane, plot_intercloud_coordinate_plane from src.visualization.maps import plot_cloud_to_ground_coordinate_plane, plot_intercloud_coordinate_plane
from src.visualization.storm_cells import ( from src.visualization.storm_cells import (
create_storm_cells_map, create_storm_cells_map,
@ -169,6 +169,103 @@ def _add_bullets(doc: Document, items: Iterable[str], size_pt: int = 10) -> None
r.font.size = Pt(size_pt) r.font.size = Pt(size_pt)
def _build_key_findings(
lightning_df: pd.DataFrame,
turbine_df: pd.DataFrame,
stats: dict[str, Any],
centroid_lat: float,
centroid_lng: float,
s,
storm_cell_count: int = 0,
storm_closest_to_centroid_km: float | None = None,
) -> tuple[list[str], str]:
radius_m = get_analysis_radius_m()
if radius_m <= 0:
radius_m = int(max(config.distance_rings)) if config.distance_rings else 0
innermost_km = (float(min(config.distance_rings)) / 1000.0) if config.distance_rings else (radius_m / 1000.0)
turbine_count = len(turbine_df)
total_events = int(stats.get("total_events", 0) or 0)
cg_total = 0
ic_total = 0
innermost_count = 0
within_df = lightning_df.iloc[0:0]
if len(lightning_df) > 0 and radius_m > 0:
dists_m = haversine_distance_vectorized(
centroid_lat,
centroid_lng,
lightning_df["lat"].values,
lightning_df["lng"].values,
)
within_mask = dists_m <= radius_m
within_df = lightning_df.loc[within_mask].copy()
within_dists = dists_m[within_mask]
ptype = within_df["p_type"].astype(str)
cg_total = int((ptype == "0").sum())
ic_total = int((ptype == "1").sum())
innermost_count = int((within_dists <= (innermost_km * 1000.0)).sum())
strongest_ka: float | None = None
nearest_km: float | None = None
nearest_turbine: str | None = None
if len(within_df) > 0 and "current" in within_df.columns:
cg_df = within_df[within_df["p_type"].astype(str) == "0"]
if len(cg_df) > 0:
currents = pd.to_numeric(cg_df["current"], errors="coerce").abs()
if bool(currents.notna().any()):
strongest_ka = float(currents.max()) / 1000.0
if turbine_count > 0 and {"lat", "lng"}.issubset(turbine_df.columns):
cg_lat = cg_df["lat"].values
cg_lng = cg_df["lng"].values
best = float("inf")
best_name: str | None = None
for _, t in turbine_df.iterrows():
d = haversine_distance_vectorized(float(t["lat"]), float(t["lng"]), cg_lat, cg_lng)
m = float(d.min())
if m < best:
best = m
best_name = str(t.get("name", "N/A"))
if best != float("inf"):
nearest_km = best / 1000.0
nearest_turbine = best_name
high_plus_count = 0
if "risk_log" in turbine_df.columns and turbine_count > 0:
high_plus_count = int((pd.to_numeric(turbine_df["risk_log"], errors="coerce") >= 0.8).sum())
active_days = len(stats.get("daily_lightning_by_rings") or {})
peak = get_peak_activity(lightning_df, centroid_lat, centroid_lng)
bullets: list[str] = [
s.kf_total_events.format(total=total_events, cg=cg_total, ic=ic_total),
s.kf_innermost.format(radius=innermost_km, count=innermost_count),
]
if peak:
bullets.append(s.kf_peak_activity.format(when=peak["when"], count=peak["count"]))
if strongest_ka is not None:
bullets.append(s.kf_strongest_strike.format(ka=strongest_ka))
if nearest_km is not None and nearest_turbine is not None:
bullets.append(s.kf_nearest_strike.format(distance=nearest_km, name=nearest_turbine))
if turbine_count > 0:
bullets.append(s.kf_high_risk_turbines.format(count=high_plus_count, total=turbine_count))
if active_days > 1:
bullets.append(s.kf_active_days.format(days=active_days))
if storm_cell_count > 0 and storm_closest_to_centroid_km is not None:
bullets.append(
s.kf_storm_distance.format(count=storm_cell_count, distance=storm_closest_to_centroid_km)
)
if total_events == 0:
recommendation = s.recommendation_no_activity
elif high_plus_count > 0:
recommendation = s.recommendation_high_plus.format(count=high_plus_count, total=turbine_count)
else:
recommendation = s.recommendation_routine
return bullets, recommendation
def _lighten_hex_color(hex_color: str, factor: float = 0.6) -> str: def _lighten_hex_color(hex_color: str, factor: float = 0.6) -> str:
s = hex_color.strip() s = hex_color.strip()
if s.startswith("#"): if s.startswith("#"):
@ -602,6 +699,26 @@ def create_docx_report(
storm_closest_distance_km: float | None = None storm_closest_distance_km: float | None = None
storm_over_threshold_km = 1.0 storm_over_threshold_km = 1.0
# Closest storm-cell centroid to the farm centroid (independent of turbines): lets the
# report comment on detected storms even when no turbine was directly affected.
storm_cell_count = len(storm_data) if storm_data else 0
storm_closest_to_centroid_km: float | None = None
if storm_data:
min_centroid_dist = float("inf")
for storm in storm_data:
wkt_string = storm.get("cell_polygon_wkt", "") or ""
if not wkt_string:
continue
centroid = calculate_storm_cell_centroid(wkt_string)
if centroid is None:
continue
c_lat, c_lng = centroid
dist_km = haversine_distance(centroid_lat, centroid_lng, c_lat, c_lng) / 1000.0
if dist_km < min_centroid_dist:
min_centroid_dist = dist_km
if min_centroid_dist != float("inf"):
storm_closest_to_centroid_km = min_centroid_dist
if storm_data and top_turbine_name is not None and "risk_log" in turbine_df.columns and len(turbine_df) > 0: if storm_data and top_turbine_name is not None and "risk_log" in turbine_df.columns and len(turbine_df) > 0:
# Use the coordinates of the highest-risk turbine for storm-distance calculations. # Use the coordinates of the highest-risk turbine for storm-distance calculations.
if "lat" in turbine_df.columns and "lng" in turbine_df.columns and top_turbine_name is not None: if "lat" in turbine_df.columns and "lng" in turbine_df.columns and top_turbine_name is not None:
@ -645,6 +762,9 @@ def create_docx_report(
"storm_near_turbine_count": storm_near_turbine_count, "storm_near_turbine_count": storm_near_turbine_count,
"storm_closest_distance_km": round(float(storm_closest_distance_km), 1) if storm_closest_distance_km is not None else None, "storm_closest_distance_km": round(float(storm_closest_distance_km), 1) if storm_closest_distance_km is not None else None,
"storm_over_threshold_km": storm_over_threshold_km, "storm_over_threshold_km": storm_over_threshold_km,
"storm_detected": storm_cell_count > 0,
"storm_cell_count": storm_cell_count,
"storm_closest_to_centroid_km": round(float(storm_closest_to_centroid_km), 1) if storm_closest_to_centroid_km is not None else None,
"turbine_risk_counts": turbine_risk_counts, "turbine_risk_counts": turbine_risk_counts,
"storm_summary": storm_summary, "storm_summary": storm_summary,
} }
@ -656,16 +776,31 @@ def create_docx_report(
if commentary_text.startswith(prefix): if commentary_text.startswith(prefix):
commentary_text = commentary_text[len(prefix):].strip() commentary_text = commentary_text[len(prefix):].strip()
break break
key_findings, recommendation_text = _build_key_findings(
lightning_df,
turbine_df,
stats,
centroid_lat,
centroid_lng,
s,
storm_cell_count=storm_cell_count,
storm_closest_to_centroid_km=storm_closest_to_centroid_km,
)
# Keep the commentary close to the Turbine Information table (same page if possible). # Keep the commentary close to the Turbine Information table (same page if possible).
doc.add_paragraph("") doc.add_paragraph("")
doc.add_paragraph("") doc.add_paragraph("")
_add_title(doc, s.report_summary, size_pt=14, bold=True, align=WD_ALIGN_PARAGRAPH.LEFT) _add_title(doc, s.report_summary, size_pt=14, bold=True, align=WD_ALIGN_PARAGRAPH.LEFT)
_add_paragraph(doc, s.key_findings_heading, size_pt=11, bold=True)
_add_bullets(doc, key_findings, size_pt=10)
# DOCX line spacing for the Gemini commentary paragraph. # DOCX line spacing for the Gemini commentary paragraph.
p = doc.add_paragraph() p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.LEFT p.alignment = WD_ALIGN_PARAGRAPH.LEFT
r = p.add_run(commentary_text) r = p.add_run(commentary_text)
r.font.size = Pt(10) r.font.size = Pt(10)
p.paragraph_format.line_spacing = 1.5 p.paragraph_format.line_spacing = 1.5
_add_paragraph(doc, s.recommendation_heading, size_pt=11, bold=True)
_add_paragraph(doc, recommendation_text, size_pt=10)
doc.add_page_break() doc.add_page_break()
@ -692,6 +827,7 @@ def create_docx_report(
_add_title(doc, s.cloud_to_ground_current_vs_distance, size_pt=14) _add_title(doc, s.cloud_to_ground_current_vs_distance, size_pt=14)
if start_date and end_date: if start_date and end_date:
_add_paragraph(doc, f"{start_date} - {end_date}", size_pt=10) _add_paragraph(doc, f"{start_date} - {end_date}", size_pt=10)
_add_paragraph(doc, s.cg_current_vs_distance_desc, size_pt=10)
cg_chart = _build_current_vs_distance_chart( cg_chart = _build_current_vs_distance_chart(
lightning_df, lightning_df,
pre["dists_km"], pre["dists_km"],
@ -717,6 +853,7 @@ def create_docx_report(
_add_title(doc, s.intercloud_current_vs_distance, size_pt=14) _add_title(doc, s.intercloud_current_vs_distance, size_pt=14)
if start_date and end_date: if start_date and end_date:
_add_paragraph(doc, f"{start_date} - {end_date}", size_pt=10) _add_paragraph(doc, f"{start_date} - {end_date}", size_pt=10)
_add_paragraph(doc, s.ic_current_vs_distance_desc, size_pt=10)
ic_chart = _build_current_vs_distance_chart( ic_chart = _build_current_vs_distance_chart(
lightning_df, lightning_df,
pre["dists_km"], pre["dists_km"],
@ -734,6 +871,7 @@ def create_docx_report(
risk_table_data, risk_row_colors = build_risk_table_data(turbine_df, lang) risk_table_data, risk_row_colors = build_risk_table_data(turbine_df, lang)
if risk_table_data and len(risk_table_data) > 1: if risk_table_data and len(risk_table_data) > 1:
_add_title(doc, s.turbine_risk_assessment, size_pt=14) _add_title(doc, s.turbine_risk_assessment, size_pt=14)
_add_paragraph(doc, s.turbine_risk_assessment_desc, size_pt=10)
_add_table(doc, risk_table_data, row_colors=risk_row_colors) _add_table(doc, risk_table_data, row_colors=risk_row_colors)
doc.add_page_break() doc.add_page_break()
@ -797,8 +935,6 @@ def create_docx_report(
size_pt=9, size_pt=9,
) )
_add_paragraph(doc, s.ring_section_intro, size_pt=10)
_add_paragraph(doc, s.ring_closer_risk.format(closest=closest_km), size_pt=10)
ring_items: list[str] = [] ring_items: list[str] = []
for ring_name, ring_data in sorted((stats.get("lightning_by_distance_rings") or {}).items()): for ring_name, ring_data in sorted((stats.get("lightning_by_distance_rings") or {}).items()):
if ring_data.get("total", 0) > 0: if ring_data.get("total", 0) > 0:
@ -854,9 +990,6 @@ def create_docx_report(
size_pt=10, size_pt=10,
) )
_add_paragraph(doc, s.avg_direction.format(value=summary.get("avg_direction", 0)), size_pt=10)
_add_paragraph(doc, s.avg_speed.format(value=summary.get("avg_speed", 0)), size_pt=10)
daily_breakdown: dict[str, int] = summary.get("daily_breakdown", {}) or {} daily_breakdown: dict[str, int] = summary.get("daily_breakdown", {}) or {}
_add_paragraph(doc, s.daily_storm_breakdown, size_pt=10, bold=True) _add_paragraph(doc, s.daily_storm_breakdown, size_pt=10, bold=True)
sorted_days = sorted(daily_breakdown.keys()) sorted_days = sorted(daily_breakdown.keys())

View File

@ -104,6 +104,8 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
storm_near_turbine_count = context.get("storm_near_turbine_count") storm_near_turbine_count = context.get("storm_near_turbine_count")
storm_closest_distance_km = context.get("storm_closest_distance_km") storm_closest_distance_km = context.get("storm_closest_distance_km")
storm_over_threshold_km = context.get("storm_over_threshold_km", 1.0) storm_over_threshold_km = context.get("storm_over_threshold_km", 1.0)
storm_detected = context.get("storm_detected")
storm_closest_to_centroid_km = context.get("storm_closest_to_centroid_km")
ring_lines = _ring_lines(context, s) ring_lines = _ring_lines(context, s)
storm_lines = _storm_lines(context) storm_lines = _storm_lines(context)
@ -142,6 +144,8 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
f"- storm_near_turbine_count: {storm_near_turbine_count}\n" f"- storm_near_turbine_count: {storm_near_turbine_count}\n"
f"- storm_closest_distance_km: {storm_closest_distance_km}\n" f"- storm_closest_distance_km: {storm_closest_distance_km}\n"
f"- storm_over_threshold_km: {storm_over_threshold_km}\n" f"- storm_over_threshold_km: {storm_over_threshold_km}\n"
f"- storm_detected: {storm_detected}\n"
f"- storm_closest_to_centroid_km: {storm_closest_to_centroid_km}\n"
+ (f"\n- storm_summary:\n{chr(10).join(storm_lines)}" if storm_lines else "\n- storm_summary: not available") + (f"\n- storm_summary:\n{chr(10).join(storm_lines)}" if storm_lines else "\n- storm_summary: not available")
+ "\n\n" + "\n\n"
"Paragraf gereksinimleri:\n" "Paragraf gereksinimleri:\n"
@ -150,7 +154,9 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
"- is_single_turbine_report true ise: \"{top_turbine_name} türbini için log-risk skoru ... sınıfındadır\" gibi doğal bir ifade kullan.\n" "- is_single_turbine_report true ise: \"{top_turbine_name} türbini için log-risk skoru ... sınıfındadır\" gibi doğal bir ifade kullan.\n"
"- is_single_turbine_report false ise: analiz alanında en yüksek riskli türbini ve risk sınıfını belirt.\n" "- is_single_turbine_report false ise: analiz alanında en yüksek riskli türbini ve risk sınıfını belirt.\n"
"- top_turbine_name adını aynen kullan ve max_risk_definition ile ilişkilendir.\n" "- top_turbine_name adını aynen kullan ve max_risk_definition ile ilişkilendir.\n"
"- Fırtına hücresi etkileşimini uygun olduğunda belirt; veri yoksa kısaca belirt.\n" "- Fırtına hücresi etkileşimini yalnızca veri varken belirt.\n"
"- Fırtına tespit edildiyse (storm_detected true) ancak hiçbir türbinin üzerine gelmediyse, en yakın fırtına hücresinin merkeze uzaklığını (storm_closest_to_centroid_km) belirt.\n"
"- storm_detected false ise fırtınadan hiç bahsetme.\n"
"- Sayıları yuvarla: yoğunluk 3 ondalık, log risk 2 ondalık, mesafeler 1 ondalık km, sayılar tam sayı.\n" "- Sayıları yuvarla: yoğunluk 3 ondalık, log risk 2 ondalık, mesafeler 1 ondalık km, sayılar tam sayı.\n"
"- Ton analitik, net ve alarmist olmayan olsun.\n" "- Ton analitik, net ve alarmist olmayan olsun.\n"
"\n" "\n"
@ -189,6 +195,8 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
f"- storm_near_turbine_count: {storm_near_turbine_count}\n" f"- storm_near_turbine_count: {storm_near_turbine_count}\n"
f"- storm_closest_distance_km: {storm_closest_distance_km}\n" f"- storm_closest_distance_km: {storm_closest_distance_km}\n"
f"- storm_over_threshold_km: {storm_over_threshold_km}\n" f"- storm_over_threshold_km: {storm_over_threshold_km}\n"
f"- storm_detected: {storm_detected}\n"
f"- storm_closest_to_centroid_km: {storm_closest_to_centroid_km}\n"
+ (f"\n- storm_summary:\n{chr(10).join(storm_lines)}" if storm_lines else "\n- storm_summary: not available") + (f"\n- storm_summary:\n{chr(10).join(storm_lines)}" if storm_lines else "\n- storm_summary: not available")
+ "\n\n" + "\n\n"
"Requirements for the paragraph:\n" "Requirements for the paragraph:\n"
@ -202,6 +210,8 @@ def build_gemini_prompt(context: dict[str, Any], language: ReportLanguage | None
"- Mention storm-cell interaction with the turbine when storm information is available:\n" "- Mention storm-cell interaction with the turbine when storm information is available:\n"
" - If storm_over_turbine is true: say that storm cells were very close to/over the turbine (based on centroid distance <= storm_over_threshold_km).\n" " - If storm_over_turbine is true: say that storm cells were very close to/over the turbine (based on centroid distance <= storm_over_threshold_km).\n"
" - If storm_over_turbine is false and storm_closest_distance_km is provided: say the closest storm cell centroid came within storm_closest_distance_km km of the turbine.\n" " - If storm_over_turbine is false and storm_closest_distance_km is provided: say the closest storm cell centroid came within storm_closest_distance_km km of the turbine.\n"
" - If storms were detected (storm_detected true) but did not pass over any turbine, mention the closest storm cell's distance to the centroid (storm_closest_to_centroid_km).\n"
" - If storm_detected is false, do not mention storms at all.\n"
"- If storm_summary is available, mention total storm cells and at least one severity count.\n" "- If storm_summary is available, mention total storm cells and at least one severity count.\n"
"- Round numeric values as follows (use the rounded values you are given, avoid long decimals):\n" "- Round numeric values as follows (use the rounded values you are given, avoid long decimals):\n"
" - lightning density to 3 decimals (events/km²)\n" " - lightning density to 3 decimals (events/km²)\n"
@ -231,6 +241,7 @@ def fallback_commentary(context: dict[str, Any], language: ReportLanguage | None
storm_closest_distance_km = context.get("storm_closest_distance_km") storm_closest_distance_km = context.get("storm_closest_distance_km")
storm_over_threshold_km = context.get("storm_over_threshold_km", 1.0) storm_over_threshold_km = context.get("storm_over_threshold_km", 1.0)
storm_near_turbine_count = context.get("storm_near_turbine_count") storm_near_turbine_count = context.get("storm_near_turbine_count")
storm_closest_to_centroid_km = context.get("storm_closest_to_centroid_km")
outermost_ring = top_rings[-1] if top_rings else None outermost_ring = top_rings[-1] if top_rings else None
best_ring = top_rings[0] if top_rings else None best_ring = top_rings[0] if top_rings else None
@ -243,13 +254,14 @@ def fallback_commentary(context: dict[str, Any], language: ReportLanguage | None
if severity_counts: if severity_counts:
severity = max(severity_counts.items(), key=lambda kv: kv[1])[0] severity = max(severity_counts.items(), key=lambda kv: kv[1])[0]
count = severity_counts.get(severity, 0) count = severity_counts.get(severity, 0)
severity_label = s.storm_severity_names.get(str(severity).strip().lower(), str(severity).title())
if s.gemini_write_turkish: if s.gemini_write_turkish:
storm_line = ( storm_line = (
f"Toplam {total_cells} fırtına hücresi kaydedilmiş; en fazla {count} hücre {severity} şiddetindedir." f"Toplam {total_cells} fırtına hücresi kaydedilmiş; en fazla {count} hücre {severity_label} şiddetindedir."
) )
else: else:
storm_line = ( storm_line = (
f"Storm data indicates {total_cells} storm cells, with the highest share in {severity} ({count} cells)." f"Storm data indicates {total_cells} storm cells, with the highest share in {severity_label} ({count} cells)."
) )
elif s.gemini_write_turkish: elif s.gemini_write_turkish:
storm_line = f"Toplam {total_cells} fırtına hücresi kaydedilmiştir." storm_line = f"Toplam {total_cells} fırtına hücresi kaydedilmiştir."
@ -289,13 +301,18 @@ def fallback_commentary(context: dict[str, Any], language: ReportLanguage | None
f"Fırtına hücrelerinden {storm_near_turbine_count} tanesinin merkezi türbine " f"Fırtına hücrelerinden {storm_near_turbine_count} tanesinin merkezi türbine "
f"{storm_over_threshold_km:.1f} km'den yakın konumlanmıştır. " f"{storm_over_threshold_km:.1f} km'den yakın konumlanmıştır. "
) )
elif isinstance(storm_closest_to_centroid_km, (int, float)):
storm_interaction_sentence = (
f"Tespit edilen fırtına hücreleri herhangi bir türbinin üzerine denk gelmemiş; "
f"en yakın hücre merkeze {storm_closest_to_centroid_km:.1f} km mesafeye kadar yaklaşmıştır. "
)
elif isinstance(storm_closest_distance_km, (int, float)): elif isinstance(storm_closest_distance_km, (int, float)):
storm_interaction_sentence = ( storm_interaction_sentence = (
f"En yakın fırtına hücresi merkezi türbine {storm_closest_distance_km:.1f} km mesafededir. " f"En yakın fırtına hücresi merkezi türbine {storm_closest_distance_km:.1f} km mesafededir. "
) )
else: else:
storm_interaction_sentence = "" storm_interaction_sentence = ""
storm_severity_sentence = storm_line if storm_line else "Bu raporda fırtına hücresi verisi bulunmamaktadır." storm_severity_sentence = storm_line
else: else:
radius_txt = f"within {analysis_radius_km:.1f} km" if isinstance(analysis_radius_km, (int, float)) else "" radius_txt = f"within {analysis_radius_km:.1f} km" if isinstance(analysis_radius_km, (int, float)) else ""
events_txt = f"{total_events} total lightning events" if total_events is not None else "N/A" events_txt = f"{total_events} total lightning events" if total_events is not None else "N/A"
@ -317,13 +334,17 @@ def fallback_commentary(context: dict[str, Any], language: ReportLanguage | None
storm_interaction_sentence = ( storm_interaction_sentence = (
f"Storm interaction: storm-cell centroids came within {storm_over_threshold_km:.1f} km of the turbine (count={storm_near_turbine_count}). " f"Storm interaction: storm-cell centroids came within {storm_over_threshold_km:.1f} km of the turbine (count={storm_near_turbine_count}). "
) )
elif isinstance(storm_closest_to_centroid_km, (int, float)):
storm_interaction_sentence = (
f"Detected storm cells did not pass directly over any turbine; the nearest cell came within {storm_closest_to_centroid_km:.1f} km of the centroid. "
)
elif isinstance(storm_closest_distance_km, (int, float)): elif isinstance(storm_closest_distance_km, (int, float)):
storm_interaction_sentence = ( storm_interaction_sentence = (
f"Storm interaction: the closest storm-cell centroid came within {storm_closest_distance_km:.1f} km of the turbine. " f"Storm interaction: the closest storm-cell centroid came within {storm_closest_distance_km:.1f} km of the turbine. "
) )
else: else:
storm_interaction_sentence = "" storm_interaction_sentence = ""
storm_severity_sentence = storm_line if storm_line else "Storm severity distribution is not available for this report." storm_severity_sentence = storm_line
return (paragraph_intro + turbine_sentence + method_sentence + storm_interaction_sentence + storm_severity_sentence).strip() return (paragraph_intro + turbine_sentence + method_sentence + storm_interaction_sentence + storm_severity_sentence).strip()

View File

@ -37,13 +37,29 @@ class ReportStrings:
turbine_information: str turbine_information: str
turbine_information_desc: str turbine_information_desc: str
report_summary: str report_summary: str
key_findings_heading: str
kf_total_events: str
kf_innermost: str
kf_peak_activity: str
kf_strongest_strike: str
kf_nearest_strike: str
kf_high_risk_turbines: str
kf_active_days: str
kf_storm_distance: str
recommendation_heading: str
recommendation_high_plus: str
recommendation_routine: str
recommendation_no_activity: str
cloud_to_ground_lightnings: str cloud_to_ground_lightnings: str
cloud_to_ground_current_vs_distance: str cloud_to_ground_current_vs_distance: str
cg_chart_title: str cg_chart_title: str
cg_current_vs_distance_desc: str
intercloud_lightnings: str intercloud_lightnings: str
intercloud_current_vs_distance: str intercloud_current_vs_distance: str
ic_chart_title: str ic_chart_title: str
ic_current_vs_distance_desc: str
turbine_risk_assessment: str turbine_risk_assessment: str
turbine_risk_assessment_desc: str
lightning_breakdown_rings: str lightning_breakdown_rings: str
period_total_events: str period_total_events: str
area_covered: str area_covered: str
@ -192,8 +208,13 @@ class ReportStrings:
map_cg_lightning: str map_cg_lightning: str
map_ic_lightning: str map_ic_lightning: str
map_distance_ring: str map_distance_ring: str
map_severity_legend: str
storm_severity_names: dict[str, str]
hist_minutes_from_start: str hist_minutes_from_start: str
hist_lightning_count: str hist_lightning_count: str
hist_period_title: str
hist_cg_legend: str
hist_ic_legend: str
storm_cells_day_title: str storm_cells_day_title: str
heatmap_title: str heatmap_title: str
heatmap_subtitle: str heatmap_subtitle: str
@ -223,13 +244,29 @@ _STRINGS_EN = ReportStrings(
turbine_information="Turbine Information", turbine_information="Turbine Information",
turbine_information_desc="This table contains detailed information about all turbines in the wind farm.", turbine_information_desc="This table contains detailed information about all turbines in the wind farm.",
report_summary="Report Summary", report_summary="Report Summary",
key_findings_heading="Key Findings",
kf_total_events="Total events: {total} ({cg} cloud-to-ground, {ic} intercloud)",
kf_innermost="Events within {radius:.1f} km of the centroid: {count}",
kf_peak_activity="Peak activity: {when} ({count} events in one minute)",
kf_strongest_strike="Strongest cloud-to-ground strike: {ka:.1f} kA",
kf_nearest_strike="Nearest cloud-to-ground strike to a turbine: {distance:.1f} km ({name})",
kf_high_risk_turbines="Turbines at High risk or above: {count} of {total}",
kf_active_days="Days with recorded activity: {days}",
kf_storm_distance="Storm cells detected: {count} (closest {distance:.1f} km from the centroid)",
recommendation_heading="Recommendation",
recommendation_high_plus="{count} of {total} turbines fall in the High risk category or above. Inspection of the lightning protection systems on these turbines is recommended.",
recommendation_routine="No turbines fall in the High risk category or above; routine monitoring is sufficient for this period.",
recommendation_no_activity="No lightning activity was recorded within the analysis area during this period.",
cloud_to_ground_lightnings="Cloud-to-Ground Lightnings", cloud_to_ground_lightnings="Cloud-to-Ground Lightnings",
cloud_to_ground_current_vs_distance="Cloud-to-Ground — Current vs Distance", cloud_to_ground_current_vs_distance="Cloud-to-Ground — Current vs Distance",
cg_chart_title="Cloud-to-Ground Lightning — Current vs Distance from Centroid", cg_chart_title="Cloud-to-Ground Lightning — Current and Distance to Centroid",
cg_current_vs_distance_desc="The chart below shows the distance to the center point and the current magnitude of the cloud-to-ground events that occurred during the analyzed time period.",
intercloud_lightnings="Intercloud Lightnings", intercloud_lightnings="Intercloud Lightnings",
intercloud_current_vs_distance="Intercloud — Current vs Distance", intercloud_current_vs_distance="Intercloud — Current vs Distance",
ic_chart_title="Intercloud Lightning — Current vs Distance from Centroid", ic_chart_title="Intercloud Lightning — Current and Distance to Centroid",
ic_current_vs_distance_desc="The chart below shows the distance to the center point and the current magnitude of the intercloud events that occurred during the analyzed time period.",
turbine_risk_assessment="Turbine Risk Assessment", turbine_risk_assessment="Turbine Risk Assessment",
turbine_risk_assessment_desc="You can find the explanation of how the risk assessment is performed in Appendices 1, 2 and 3.",
lightning_breakdown_rings="Lightning Breakdown by Distance Rings", lightning_breakdown_rings="Lightning Breakdown by Distance Rings",
period_total_events="Period: {start} - {end} (Total: {total} lightning events)", period_total_events="Period: {start} - {end} (Total: {total} lightning events)",
area_covered="Area covered within {radius:.1f} km radius: {area:.1f} km²", area_covered="Area covered within {radius:.1f} km radius: {area:.1f} km²",
@ -277,7 +314,7 @@ _STRINGS_EN = ReportStrings(
alpha_desc="α (distance decay factor): controls how risk decays with distance (smaller = slower decay)", alpha_desc="α (distance decay factor): controls how risk decays with distance (smaller = slower decay)",
included_events="Included events: only cloud-to-ground lightning (p_type = 0)", included_events="Included events: only cloud-to-ground lightning (p_type = 0)",
per_strike_contribution="Per-strike contribution: increases with |I| and decreases exponentially with distance", per_strike_contribution="Per-strike contribution: increases with |I| and decreases exponentially with distance",
turbine_risk_sum="Turbine's risk score: sum of per-strike contributions for all included strikes (typically within the outermost distance ring)", turbine_risk_sum="Turbine's risk score: the sum of per-strike contributions from all cloud-to-ground strikes within the outermost distance ring",
log_transformed="For visualization and reporting, we use the log-transformed score", log_transformed="For visualization and reporting, we use the log-transformed score",
risk_interpretation="2. Risk Score Interpretation", risk_interpretation="2. Risk Score Interpretation",
understanding_risk="Understanding Risk Score Values:", understanding_risk="Understanding Risk Score Values:",
@ -304,7 +341,7 @@ _STRINGS_EN = ReportStrings(
centroid_bullet_1="Farm centroid is provided by the monitoring workflow and shared by every map and table in this report", centroid_bullet_1="Farm centroid is provided by the monitoring workflow and shared by every map and table in this report",
centroid_bullet_2="Distance rings are drawn from the farm centroid; innermost rings represent the highest proximity risk", centroid_bullet_2="Distance rings are drawn from the farm centroid; innermost rings represent the highest proximity risk",
centroid_bullet_3="Lightning proximity for each strike is measured from the farm centroid using the Haversine formula", centroid_bullet_3="Lightning proximity for each strike is measured from the farm centroid using the Haversine formula",
centroid_bullet_4="The monitoring boundary defines the outer analysis radius — strikes beyond it are excluded", centroid_bullet_4="The analysis is limited to the outermost distance ring; strikes beyond it are excluded from all statistics and charts",
freq_lightning_algo="5. Frequent Lightning Activity Period Detection Algorithm", freq_lightning_algo="5. Frequent Lightning Activity Period Detection Algorithm",
how_period_timespans="How Period Timespans Are Determined:", how_period_timespans="How Period Timespans Are Determined:",
gap_based_algo="The algorithm uses a gap-based approach to identify concentrated lightning activity periods.", gap_based_algo="The algorithm uses a gap-based approach to identify concentrated lightning activity periods.",
@ -371,15 +408,20 @@ _STRINGS_EN = ReportStrings(
map_longitude="Longitude", map_longitude="Longitude",
map_latitude="Latitude", map_latitude="Latitude",
map_legend="Legend", map_legend="Legend",
map_cg_plane_title="Cloud-to-Ground Lightning - Coordinate Plane View - Central Turbine: {name}", map_cg_plane_title="CG Event Distribution",
map_ic_plane_title="Intercloud Lightning - Coordinate Plane View - Central Turbine: {name}", map_ic_plane_title="IC Event Distribution",
map_wind_turbines="Wind Turbines", map_wind_turbines="Wind Turbines",
map_storm_cells="Storm Cells", map_storm_cells="Storm Cells",
map_cg_lightning="Cloud-to-Ground Lightning", map_cg_lightning="Cloud-to-Ground Lightning",
map_ic_lightning="Intercloud Lightning", map_ic_lightning="Intercloud Lightning",
map_distance_ring="{radius:.1f}km Distance Ring", map_distance_ring="{radius:.1f}km Distance Ring",
map_severity_legend="{severity} Severity",
storm_severity_names={"high": "High", "medium": "Medium", "low": "Low", "unknown": "Unknown"},
hist_minutes_from_start="Minutes from start", hist_minutes_from_start="Minutes from start",
hist_lightning_count="Lightning Count", hist_lightning_count="Lightning Count",
hist_period_title="Date: {date}<br>Period: {period}<br>Total lightnings: {total}",
hist_cg_legend="Cloud-to-Ground",
hist_ic_legend="Intercloud",
storm_cells_day_title="Storm Cells - {day}", storm_cells_day_title="Storm Cells - {day}",
heatmap_title="Risk Score Heatmap", heatmap_title="Risk Score Heatmap",
heatmap_subtitle="Current Magnitude vs Distance (0.1-{max_km:.1f}km) - Higher values (red) = Higher risk", heatmap_subtitle="Current Magnitude vs Distance (0.1-{max_km:.1f}km) - Higher values (red) = Higher risk",
@ -418,21 +460,37 @@ _STRINGS_TR = ReportStrings(
turbine_information="Türbin Bilgileri", turbine_information="Türbin Bilgileri",
turbine_information_desc="Bu tablo, rüzgar çiftliğindeki tüm türbinlere ait ayrıntılı bilgileri içerir.", turbine_information_desc="Bu tablo, rüzgar çiftliğindeki tüm türbinlere ait ayrıntılı bilgileri içerir.",
report_summary="Rapor Özeti", report_summary="Rapor Özeti",
key_findings_heading="Öne Çıkan Bulgular",
kf_total_events="Toplam olay: {total} ({cg} yıldırım, {ic} şimşek)",
kf_innermost="Merkeze {radius:.1f} km mesafe içinde: {count} olay",
kf_peak_activity="En yoğun an: {when} (bir dakikada {count} olay)",
kf_strongest_strike="En güçlü yıldırım darbesi: {ka:.1f} kA",
kf_nearest_strike="Bir türbine en yakın yıldırım: {distance:.1f} km ({name})",
kf_high_risk_turbines="Yüksek ve üzeri riskli türbin: {total} türbinden {count} adet",
kf_active_days="Aktivite görülen gün sayısı: {days}",
kf_storm_distance="Tespit edilen fırtına hücresi: {count} (merkeze en yakın {distance:.1f} km)",
recommendation_heading="Öneri",
recommendation_high_plus="{total} türbinden {count} adedi Yüksek risk ve üzeri sınıfta yer almaktadır. Bu türbinlerin yıldırımdan korunma sistemlerinin incelenmesi önerilir.",
recommendation_routine="Yüksek risk ve üzeri sınıfta türbin bulunmamaktadır; bu dönem için rutin izleme yeterlidir.",
recommendation_no_activity="Bu dönemde analiz alanında yıldırım-şimşek aktivitesi kaydedilmemiştir.",
cloud_to_ground_lightnings="Yıldırım", cloud_to_ground_lightnings="Yıldırım",
cloud_to_ground_current_vs_distance="Yıldırım — Akım ve Mesafe", cloud_to_ground_current_vs_distance="Yıldırım — Akım ve Mesafe",
cg_chart_title="Şimşek — Merkezden Akım ve Mesafe", cg_chart_title="Yıldırım — Akım ve Merkeze olan Mesafe",
cg_current_vs_distance_desc="Aşağıdaki grafik, incelenen zaman dilimi içinde gerçekleşen yıldırımların merkez noktaya mesafesini ve akım miktarını göstermektedir.",
intercloud_lightnings="Şimşek", intercloud_lightnings="Şimşek",
intercloud_current_vs_distance="Şimşek — Akım ve Mesafe", intercloud_current_vs_distance="Şimşek — Akım ve Mesafe",
ic_chart_title="Şimşek — Merkezden Akım ve Mesafe", ic_chart_title="Şimşek — Akım ve Merkeze olan Mesafe",
ic_current_vs_distance_desc="Aşağıdaki grafik, incelenen zaman dilimi içinde gerçekleşen şimşeklerin merkez noktaya mesafesini ve akım miktarını göstermektedir.",
turbine_risk_assessment="Türbin Risk Değerlendirmesi", turbine_risk_assessment="Türbin Risk Değerlendirmesi",
turbine_risk_assessment_desc="Risk değerlendirmesinin nasıl yapıldığına dair açıklamayı Ek. 1, 2 ve 3'te bulabilirsiniz.",
lightning_breakdown_rings="Mesafe Halkalarına Göre Yıldırım Dağılımı", lightning_breakdown_rings="Mesafe Halkalarına Göre Yıldırım Dağılımı",
period_total_events="Dönem: {start} - {end} (Toplam: {total} yıldırım olayı)", period_total_events="Dönem: {start} - {end} (Toplam: {total} yıldırım-şimşek olayı)",
area_covered="{radius:.1f} km yarıçap içinde kapsanan alan: {area:.1f} km²", area_covered="{radius:.1f} km yarıçap içinde kapsanan alan: {area:.1f} km²",
total_lightnings_radius="{radius:.1f} km yarıçap içindeki toplam yıldırım: {total} olay", total_lightnings_radius="{radius:.1f} km yarıçap içindeki toplam yıldırım-şimşek: {total} olay",
total_density="Toplam yıldırım yoğunluğu: {density:.3f} olay/km²", total_density="Toplam yıldırım-şimşek yoğunluğu: {density:.3f} olay/km²",
density_calc="(Hesaplama: {total} toplam yıldırım / {area:.1f} km² alan)", density_calc="(Hesaplama: {total} toplam yıldırım-şimşek olayı / {area:.1f} km² alan)",
daily_density="Günlük eşdeğer yoğunluk: {density:.3f} olay/km²/gün", daily_density="Günlük eşdeğer yoğunluk: {density:.3f} olay/km²/gün",
daily_density_calc="(Hesaplama: {total} toplam yıldırım / {area:.1f} km² alan / {days_label})", daily_density_calc="(Hesaplama: {total} toplam yıldırım-şimşek olayı / {area:.1f} km² alan / {days_label})",
day_in_period="dönemde 1 gün", day_in_period="dönemde 1 gün",
days_in_period="dönemde {days} gün", days_in_period="dönemde {days} gün",
ring_section_intro="Bu bölüm, analiz dönemindeki yıldırım olaylarını türbinlere olan mesafeye göre gösterir.", ring_section_intro="Bu bölüm, analiz dönemindeki yıldırım olaylarını türbinlere olan mesafeye göre gösterir.",
@ -440,7 +498,7 @@ _STRINGS_TR = ReportStrings(
ring_item="{ring}: {total} toplam ({cg} yıldırım, {ic} şimşek)", ring_item="{ring}: {total} toplam ({cg} yıldırım, {ic} şimşek)",
frequent_lightning_report="Sık Yıldırım Aktivite Raporu", frequent_lightning_report="Sık Yıldırım Aktivite Raporu",
lightning_activity_overview="Yıldırım Aktivite Genel Bakış:", lightning_activity_overview="Yıldırım Aktivite Genel Bakış:",
histogram_overview="Aşağıdaki grafik, {radius:.1f} km yarıçap içindeki zaman içi yıldırım aktivite örüntülerini göstererek yüksek riskli dönemleri ve yoğun fırtına olaylarını belirlemeye yardımcı olur. Tepe noktaları dikkat gerektiren yoğun yıldırım aktivitesini gösterir.", histogram_overview="Aşağıdaki grafik, {radius:.1f} km yarıçap içindeki yıldırım-şimşek aktivitesinin zaman içindeki dağılımını göstermekte ve yüksek riskli anların belirlenmesine yardımcı olmaktadır.",
histogram_appendix_ref='Ayrıntılı bilgi için ek bölümdeki "Sık Yıldırım Aktivite Dönemi Tespit Algoritması" bölümüne bakınız.', histogram_appendix_ref='Ayrıntılı bilgi için ek bölümdeki "Sık Yıldırım Aktivite Dönemi Tespit Algoritması" bölümüne bakınız.',
frequent_lightning_periods="Sık Yıldırım Aktivite Dönemleri", frequent_lightning_periods="Sık Yıldırım Aktivite Dönemleri",
storm_cells_analysis_summary="Fırtına Hücreleri Analiz Özeti", storm_cells_analysis_summary="Fırtına Hücreleri Analiz Özeti",
@ -463,16 +521,16 @@ _STRINGS_TR = ReportStrings(
appendix="Ek", appendix="Ek",
risk_calc_method="1. Risk Hesaplama Yöntemi", risk_calc_method="1. Risk Hesaplama Yöntemi",
how_risk_determined="Risk Skorları Nasıl Belirlenir:", how_risk_determined="Risk Skorları Nasıl Belirlenir:",
risk_exposure_1="Bir türbinin risk skoru, analiz döneminde yıldırıma maruz kalmasını temsil eder.", risk_exposure_1="Bir türbinin risk skoru, analiz dönemi boyunca buluttan yere düşen yıldırımlara ne ölçüde maruz kaldığını gösterir.",
risk_exposure_2="Her yıldırım darbesi, türbine olan mesafe ve akım büyüklüğüne göre bir risk katkısı yapar; bu katkılar toplanır.", risk_exposure_2="Her yıldırım darbesi, türbine olan mesafesine ve akım büyüklüğüne bağlı olarak bir risk katkısı sağlar; bu katkıların tümü toplanarak skor elde edilir.",
p0_desc="P0 (temel faktör): her darbenin katkısına uygulanan temel ölçekleme", p0_desc="P0 (temel faktör): her darbenin katkısına uygulanan temel ölçek katsayısı",
current_weight_desc="current_weight: akım büyüklüğünün riski ne kadar artırdığını kontrol eder (büyük = |I| üzerinde daha fazla ağırlık)", current_weight_desc="current_weight: akım büyüklüğünün riski ne kadar artırdığını kontrol eder (büyük = |I| üzerinde daha fazla ağırlık)",
distance_desc="Mesafe (d): türbin-darbe mesafesi (kilometre)", distance_desc="Mesafe (d): türbin-darbe mesafesi (kilometre)",
current_desc="Akım (|I|): mutlak akım büyüklüğü |I| (amper)", current_desc="Akım (|I|): mutlak akım büyüklüğü |I| (amper)",
alpha_desc="α (mesafe azalma faktörü): riskin mesafeyle nasıl azaldığını kontrol eder (küçük = daha yavaş azalma)", alpha_desc="α (mesafe azalma faktörü): riskin mesafeyle nasıl azaldığını kontrol eder (küçük = daha yavaş azalma)",
included_events="Dahil edilen olaylar: yalnızca yıldırım (p_type = 0)", included_events="Dahil edilen olaylar: yalnızca buluttan yere düşen yıldırımlar (p_type = 0)",
per_strike_contribution="Darbe başına katkı: |I| ile artar, mesafe ile üstel olarak azalır", per_strike_contribution="Darbe başına katkı: |I| ile artar, mesafe ile üstel olarak azalır",
turbine_risk_sum="Türbin risk skoru: dahil edilen tüm darbelerin katkılarının toplamı (genellikle en dış mesafe halkası içinde)", turbine_risk_sum="Türbin risk skoru: en dış mesafe halkası içindeki tüm yıldırım (buluttan yere) darbelerinin katkılarının toplamıdır",
log_transformed="Görselleştirme ve raporlama için log dönüşümlü skor kullanılır", log_transformed="Görselleştirme ve raporlama için log dönüşümlü skor kullanılır",
risk_interpretation="2. Risk Skoru Yorumu", risk_interpretation="2. Risk Skoru Yorumu",
understanding_risk="Risk Skoru Değerlerini Anlama:", understanding_risk="Risk Skoru Değerlerini Anlama:",
@ -499,17 +557,17 @@ _STRINGS_TR = ReportStrings(
centroid_bullet_1="Çiftlik merkezi izleme iş akışı tarafından sağlanır ve bu rapordaki her harita ve tabloda kullanılır", centroid_bullet_1="Çiftlik merkezi izleme iş akışı tarafından sağlanır ve bu rapordaki her harita ve tabloda kullanılır",
centroid_bullet_2="Mesafe halkaları çiftlik merkezinden çizilir; en iç halkalar en yüksek yakınlık riskini temsil eder", centroid_bullet_2="Mesafe halkaları çiftlik merkezinden çizilir; en iç halkalar en yüksek yakınlık riskini temsil eder",
centroid_bullet_3="Her darbenin yakınlığı, çiftlik merkezinden Haversine formülü ile ölçülür", centroid_bullet_3="Her darbenin yakınlığı, çiftlik merkezinden Haversine formülü ile ölçülür",
centroid_bullet_4="İzleme sınırı dış analiz yarıçapını tanımlar — dışındaki darbeler hariç tutulur", centroid_bullet_4="Analiz, en dış mesafe halkası ile sınırlıdır; bu halkanın dışında kalan darbeler tüm istatistik ve grafiklerin dışında bırakılır",
freq_lightning_algo="5. Sık Yıldırım Aktivite Dönemi Tespit Algoritması", freq_lightning_algo="5. Sık Yıldırım Aktivite Dönemi Tespit Algoritması",
how_period_timespans="Dönem Zaman Aralıkları Nasıl Belirlenir:", how_period_timespans="Dönem Zaman Aralıkları Nasıl Belirlenir:",
gap_based_algo="Algoritma, yoğun yıldırım aktivite dönemlerini belirlemek için boşluk tabanlı bir yaklaşım kullanır.", gap_based_algo="Algoritma, yoğun yıldırım aktivitesi dönemlerini belirlemek için olaylar arasındaki zaman boşluklarına dayalı bir yaklaşım kullanır.",
step_by_step="Adım Adım Süreç:", step_by_step="Adım Adım Süreç:",
algo_chronological="Kronolojik Sıralama: Tüm yıldırım olayları zaman damgasına (local_time) göre sıralanır", algo_chronological="Kronolojik Sıralama: Tüm yıldırım olayları zaman damgasına (local_time) göre sıralanır",
algo_gap="Boşluk Hesaplama: Ardışık yıldırım olayları arasındaki zaman farkları hesaplanır", algo_gap="Boşluk Hesaplama: Ardışık yıldırım olayları arasındaki zaman farkları hesaplanır",
algo_period_boundary="Dönem Sınırı Tespiti: Ardışık iki olay arasındaki boşluk {gap} dakikayı aştığında bir dönem biter ve diğeri başlar", algo_period_boundary="Dönem Sınırı Tespiti: Ardışık iki olay arasındaki boşluk {gap} dakikayı aştığında bir dönem biter ve diğeri başlar",
algo_period_validation="Dönem Doğrulama: Yalnızca ≥{min_events} yıldırım olayı içeren dönemler anlamlı kabul edilir", algo_period_validation="Dönem Doğrulama: Yalnızca ≥{min_events} yıldırım olayı içeren dönemler anlamlı kabul edilir",
algo_timespan="Zaman Aralığı Tanımı: Başlangıç = ilk olay; Bitiş = son olay; Süre ilkten sona gerçek süredir", algo_timespan="Zaman Aralığı Tanımı: Başlangıç, dönemdeki ilk olay; bitiş ise son olaydır. Süre, ilk olaydan son olaya kadar geçen gerçek zamandır",
algo_peak="Tepe Alt-Dönem Tespiti: 3 dakikalık hareketli ortalama; aktivite ortalama + 1 standart sapmayı aştığında tepe; histogramda sarı ile vurgulanır", algo_peak="Tepe Alt-Dönem Tespiti: 3 dakikalık hareketli ortalama kullanılır; aktivite, ortalamanın 1 standart sapma üzerine çıktığında tepe dönem olarak kabul edilir ve histogramda sarı ile vurgulanır",
entln_title="6. EarthNetworks Toplam Yıldırım Ağı (ENTLN)", entln_title="6. EarthNetworks Toplam Yıldırım Ağı (ENTLN)",
entln_block_1="Bu rapordaki yıldırım verileri doğrudan EarthNetworks Toplam Yıldırım Ağı'ndan (ENTLN) alınmıştır; hem bulut içi (şimşek) hem buluttan yere (yıldırım) darbeleri izler. ENTLN, küresel ölçekte konuşlandırılan ilk şimşek ve yıldırım tespit ağıdır. 1.500'den fazla sensör içerir.", entln_block_1="Bu rapordaki yıldırım verileri doğrudan EarthNetworks Toplam Yıldırım Ağı'ndan (ENTLN) alınmıştır; hem bulut içi (şimşek) hem buluttan yere (yıldırım) darbeleri izler. ENTLN, küresel ölçekte konuşlandırılan ilk şimşek ve yıldırım tespit ağıdır. 1.500'den fazla sensör içerir.",
entln_block_2="Ulusal Hava Durumu Servisi, Hava Kuvvetleri Hava Durumu Ajansı ve kamu güvenliği, acil müdahale, havaalanları ve enerji kuruluşları gibi birçok kurum ENTLN bilgisine güvenir.", entln_block_2="Ulusal Hava Durumu Servisi, Hava Kuvvetleri Hava Durumu Ajansı ve kamu güvenliği, acil müdahale, havaalanları ve enerji kuruluşları gibi birçok kurum ENTLN bilgisine güvenir.",
@ -566,15 +624,20 @@ _STRINGS_TR = ReportStrings(
map_longitude="Boylam", map_longitude="Boylam",
map_latitude="Enlem", map_latitude="Enlem",
map_legend="Gösterge", map_legend="Gösterge",
map_cg_plane_title="Yıldırım - Koordinat Düzlemi - Merkez Türbin: {name}", map_cg_plane_title="Yıldırım Dağılımı",
map_ic_plane_title="Şimşek - Koordinat Düzlemi - Merkez Türbin: {name}", map_ic_plane_title="Şimşek Dağılımı",
map_wind_turbines="Rüzgar Türbinleri", map_wind_turbines="Rüzgar Türbinleri",
map_storm_cells="Fırtına Hücreleri", map_storm_cells="Fırtına Hücreleri",
map_cg_lightning="Yıldırım", map_cg_lightning="Yıldırım",
map_ic_lightning="Şimşek", map_ic_lightning="Şimşek",
map_distance_ring="{radius:.1f} km Mesafe Halkası", map_distance_ring="{radius:.1f} km Mesafe Halkası",
hist_minutes_from_start="Başlangıçtan dakika", map_severity_legend="{severity} Şiddet",
storm_severity_names={"high": "Yüksek", "medium": "Orta", "low": "Düşük", "unknown": "Bilinmeyen"},
hist_minutes_from_start="Başlangıçtan itibaren geçen dakika",
hist_lightning_count="Yıldırım Sayısı", hist_lightning_count="Yıldırım Sayısı",
hist_period_title="Tarih: {date}<br>Dönem: {period}<br>Toplam yıldırım-şimşek: {total}",
hist_cg_legend="Yıldırım",
hist_ic_legend="Şimşek",
storm_cells_day_title="Fırtına Hücreleri - {day}", storm_cells_day_title="Fırtına Hücreleri - {day}",
heatmap_title="Risk Skoru Isı Haritası", heatmap_title="Risk Skoru Isı Haritası",
heatmap_subtitle="Akım Büyüklüğü ve Mesafe (0.1-{max_km:.1f} km) - Yüksek değerler (kırmızı) = Yüksek risk", heatmap_subtitle="Akım Büyüklüğü ve Mesafe (0.1-{max_km:.1f} km) - Yüksek değerler (kırmızı) = Yüksek risk",

View File

@ -130,11 +130,14 @@ def format_period_display_for_report(start_value: Optional[str], end_value: Opti
def get_analysis_radius_m() -> int: def get_analysis_radius_m() -> int:
from .config import config from .config import config
rings = config.distance_rings or []
outermost_ring = int(max(rings)) if rings else 0
boundary = config.analysis_boundary_m boundary = config.analysis_boundary_m
if isinstance(boundary, (int, float)) and boundary > 0: if isinstance(boundary, (int, float)) and boundary > 0:
if outermost_ring > 0:
return min(int(boundary), outermost_ring)
return int(boundary) return int(boundary)
rings = config.distance_rings or [] return outermost_ring
return int(max(rings)) if rings else 0
def get_turbine_color_by_fixed_intervals(risk_log_value: float) -> str: def get_turbine_color_by_fixed_intervals(risk_log_value: float) -> str:
""" """

View File

@ -170,7 +170,7 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
mode='lines', mode='lines',
line=dict(color=color, width=4), line=dict(color=color, width=4),
opacity=0.6, opacity=0.6,
name=f'{radius/1000:.0f}km Ring', name=s.map_distance_ring.format(radius=radius / 1000),
showlegend=True showlegend=True
)) ))
@ -195,7 +195,7 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
text=turbine_df['name'].tolist(), text=turbine_df['name'].tolist(),
textfont=dict(size=12, color='black'), textfont=dict(size=12, color='black'),
textposition='middle center', textposition='middle center',
name='Wind Turbines', name=s.map_wind_turbines,
showlegend=True, showlegend=True,
hovertemplate=( hovertemplate=(
"<b>Wind Turbine</b><br>" "<b>Wind Turbine</b><br>"
@ -274,7 +274,9 @@ def create_storm_cells_coordinate_plane(storm_data: List[Dict], turbine_df: pd.D
y=[None], y=[None],
mode='lines', mode='lines',
line=dict(color=color, width=3), line=dict(color=color, width=3),
name=f"{severity.title()} Severity", name=s.map_severity_legend.format(
severity=s.storm_severity_names.get(severity, severity.title())
),
showlegend=True, showlegend=True,
hoverinfo='skip' # No hover for legend entries hoverinfo='skip' # No hover for legend entries
)) ))