import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
# ============================================================
# 1. PROGRAM VARIABLES
# ============================================================
ConnectionMaterial = 2.5 # meters added at both ends of each connection
TrackDistance = 6.0 # meters between adjacent tracks
RiskFactor = 0.20 # 20% extra fiber
KPtoMeters = 1000 # KP values are in kilometers. 1 KP = 1000 meters
# Length group limits in meters
Limit1 = 50
Limit2 = 100
Limit3 = 200
Limit4 = 500
# ============================================================
# 2. INPUT POINTS
# ============================================================
# The route follows the same order used in this list.
# Point 1 connects to Point 2.
# Point 2 connects to Point 3.
# Point 3 connects to Point 4, etc.
points = [
{"Name": "241/3", "Track": 1, "KP": 37.8},
{"Name": "242/3", "Track": 2, "KP": 37.8},
{"Name": "243/3", "Track": 3, "KP": 37.8},
{"Name": "241/4", "Track": 3, "KP": 37.9},
]
# ============================================================
# 3. FUNCTIONS
# ============================================================
def validate_points(points):
"""
Validates input points before creating connections.
Rules:
- Each point must have Name, Track, and KP.
- At least two route entries are required.
- Repeated names are allowed.
- Same name with different Track or KP is allowed.
"""
if len(points) < 2:
raise ValueError("At least two points are required to create one connection.")
required_keys = ["Name", "Track", "KP"]
for index, point in enumerate(points):
for key in required_keys:
if key not in point:
raise ValueError(
f"Point at position {index + 1} is missing key: {key}"
)
print("Point validation completed successfully.")
def calculate_connection(
p1,
p2,
connection_material,
track_distance,
kp_to_meters
):
"""
Calculates base fiber length between two consecutive points.
Rules:
- Same Track:
Fiber Length = KP difference converted to meters
+ 2 * ConnectionMaterial
- Different Track:
Fiber Length = number of crossed track spaces * TrackDistance
+ 2 * ConnectionMaterial
Note:
RiskFactor is NOT applied here.
RiskFactor is applied later in count_fiber_by_length_group().
"""
same_track = p1["Track"] == p2["Track"]
same_kp = p1["KP"] == p2["KP"]
if same_track:
kp_distance_m = abs(p2["KP"] - p1["KP"]) * kp_to_meters
fiber_length = kp_distance_m + (2 * connection_material)
connection_type = "Same Track"
track_steps = 0
warning = ""
else:
track_steps = abs(p2["Track"] - p1["Track"])
fiber_length = (track_steps * track_distance) + (2 * connection_material)
connection_type = "Track Change"
if same_kp:
warning = ""
else:
warning = "WARNING: Track change with different KP. Diagonal connection detected."
return fiber_length, connection_type, track_steps, warning
def create_connections(
points,
connection_material,
track_distance,
kp_to_meters
):
"""
Creates all connections based on the order of the points list.
Points are identified internally by index.
This allows repeated point names.
Also detects return connections.
"""
connections = []
previous_signatures = set()
previous_signatures.add(
(
points[0]["Name"],
points[0]["Track"],
points[0]["KP"]
)
)
for i in range(len(points) - 1):
p1 = points[i]
p2 = points[i + 1]
fiber_length, connection_type, track_steps, warning = calculate_connection(
p1,
p2,
connection_material,
track_distance,
kp_to_meters
)
destination_signature = (
p2["Name"],
p2["Track"],
p2["KP"]
)
is_return = destination_signature in previous_signatures
connection = {
"From_Index": i,
"To_Index": i + 1,
"From": p1["Name"],
"From_Track": p1["Track"],
"From_KP": p1["KP"],
"To": p2["Name"],
"To_Track": p2["Track"],
"To_KP": p2["KP"],
"Connection_Type": connection_type,
"Track_Steps": track_steps,
"Fiber_Length_m": fiber_length,
"Is_Return": is_return,
"Warning": warning
}
connections.append(connection)
previous_signatures.add(destination_signature)
return connections
def count_fiber_by_length_group(
connections,
risk_factor,
limit1,
limit2,
limit3,
limit4
):
"""
Applies RiskFactor to every connection and groups connections by length.
Returns:
- updated_connections
- grouped_summary
- total_base_fiber
- total_fiber_with_risk
"""
grouped_summary = {
f"0 to {limit1} m": {
"Connections_Count": 0,
"Total_Fiber_m": 0
},
f"{limit1} to {limit2} m": {
"Connections_Count": 0,
"Total_Fiber_m": 0
},
f"{limit2} to {limit3} m": {
"Connections_Count": 0,
"Total_Fiber_m": 0
},
f"{limit3} to {limit4} m": {
"Connections_Count": 0,
"Total_Fiber_m": 0
},
f"Above {limit4} m": {
"Connections_Count": 0,
"Total_Fiber_m": 0
}
}
updated_connections = []
total_base_fiber = 0
total_fiber_with_risk = 0
for connection in connections:
base_length = connection["Fiber_Length_m"]
fiber_length_with_risk = base_length * (1 + risk_factor)
if 0 <= fiber_length_with_risk <= limit1:
length_group = f"0 to {limit1} m"
elif limit1 < fiber_length_with_risk <= limit2:
length_group = f"{limit1} to {limit2} m"
elif limit2 < fiber_length_with_risk <= limit3:
length_group = f"{limit2} to {limit3} m"
elif limit3 < fiber_length_with_risk <= limit4:
length_group = f"{limit3} to {limit4} m"
else:
length_group = f"Above {limit4} m"
connection_copy = connection.copy()
connection_copy["RiskFactor"] = risk_factor
connection_copy["Fiber_Length_With_Risk_m"] = fiber_length_with_risk
connection_copy["Length_Group"] = length_group
updated_connections.append(connection_copy)
grouped_summary[length_group]["Connections_Count"] += 1
grouped_summary[length_group]["Total_Fiber_m"] += fiber_length_with_risk
total_base_fiber += base_length
total_fiber_with_risk += fiber_length_with_risk
return updated_connections, grouped_summary, total_base_fiber, total_fiber_with_risk
def plot_route_representation(
points,
updated_connections,
output_file="fiber_route_representation.png"
):
"""
Creates a visual route representation.
Visual rules:
- Points with the same KP are aligned vertically.
- Repeated point names are allowed.
- KP is NOT shown inside the circle.
- Only Name is shown inside the circle.
- Circle is 60% larger.
- Name text is 60% larger.
- Horizontal KP spacing = 3.2 diameters of the new circle.
This is 20% closer than the previous 4 diameters.
- Same Track: strong green / thick line.
- Track Change: thin green / thinner line.
- Return Connection: blue / thicker line and slightly offset.
- Step labels removed.
"""
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
from matplotlib.lines import Line2D
tracks = sorted({p["Track"] for p in points})
y_map = {
track: -idx
for idx, track in enumerate(tracks)
}
# ------------------------------------------------------------
# Visual sizing variables
# ------------------------------------------------------------
CircleScale = 1.60
BaseCircleRadius = 0.16
CircleRadius = BaseCircleRadius * CircleScale
CircleDiameter = 2 * CircleRadius
BaseNameFontSize = 8
NameFontSize = BaseNameFontSize * CircleScale
# Previous value was 4.
# New value is 20% closer:
# 4 * 0.80 = 3.2
HorizontalSpacingInDiameters = 3.2
KPSpacing = HorizontalSpacingInDiameters * CircleDiameter
KPFontSize = 8
LabelFontSize = 8
# ------------------------------------------------------------
# Keep KP order based on first appearance
# ------------------------------------------------------------
unique_kps = []
for p in points:
if p["KP"] not in unique_kps:
unique_kps.append(p["KP"])
kp_x_map = {
kp: idx * KPSpacing
for idx, kp in enumerate(unique_kps)
}
x_positions = [
kp_x_map[p["KP"]]
for p in points
]
strong_green = "#00A651"
thin_green = "#7CFC00"
return_blue = "#0070C0"
fig, ax = plt.subplots(figsize=(14, 5))
min_x = min(kp_x_map.values())
max_x = max(kp_x_map.values())
# ------------------------------------------------------------
# Draw track guide lines
# ------------------------------------------------------------
for track in tracks:
y = y_map[track]
ax.hlines(
y,
min_x - (2 * CircleDiameter),
max_x + (6 * CircleDiameter),
color="#D0D0D0",
linewidth=1,
linestyles="--"
)
ax.text(
min_x - (2.5 * CircleDiameter),
y,
f"Track {track}",
va="center",
ha="right",
fontsize=10,
fontweight="bold"
)
# ------------------------------------------------------------
# Draw connections
# ------------------------------------------------------------
for connection_number, c in enumerate(updated_connections, start=1):
from_index = c["From_Index"]
to_index = c["To_Index"]
from_point = points[from_index]
to_point = points[to_index]
x1 = x_positions[from_index]
y1 = y_map[from_point["Track"]]
x2 = x_positions[to_index]
y2 = y_map[to_point["Track"]]
if c.get("Is_Return", False):
color = return_blue
linewidth = 5.5
zorder_line = 4
elif c["Connection_Type"] == "Same Track":
color = strong_green
linewidth = 5
zorder_line = 2
else:
color = thin_green
linewidth = 2.5
zorder_line = 3
# --------------------------------------------------------
# Same Track connection
# --------------------------------------------------------
if c["Connection_Type"] == "Same Track":
ax.plot(
[x1, x2],
[y1, y2],
color=color,
linewidth=linewidth,
zorder=zorder_line
)
label_x = (x1 + x2) / 2
label_y = y1 + (1.25 * CircleDiameter)
# --------------------------------------------------------
# Track Change connection
# --------------------------------------------------------
else:
# Same KP / different track
if x1 == x2:
offset_step = 0.55 * CircleDiameter
if c.get("Is_Return", False):
x_offset = x1 + offset_step
label_x = x_offset + (0.65 * CircleDiameter)
else:
x_offset = x1
label_x = x_offset + (0.65 * CircleDiameter)
ax.plot(
[x_offset, x_offset],
[y1, y2],
color=color,
linewidth=linewidth,
zorder=zorder_line
)
ax.scatter(
[x_offset, x_offset],
[y1, y2],
s=45,
color=color,
zorder=zorder_line + 1
)
label_y = (y1 + y2) / 2
# Different KP / different track
else:
mid_x = (x1 + x2) / 2
ax.plot(
[x1, mid_x],
[y1, y1],
color=color,
linewidth=linewidth,
zorder=zorder_line
)
ax.plot(
[mid_x, mid_x],
[y1, y2],
color=color,
linewidth=linewidth,
zorder=zorder_line
)
ax.plot(
[mid_x, x2],
[y2, y2],
color=color,
linewidth=linewidth,
zorder=zorder_line
)
label_x = mid_x
label_y = (y1 + y2) / 2
# --------------------------------------------------------
# Connection label
# --------------------------------------------------------
if c.get("Is_Return", False):
label_title = f"#{connection_number} RETURN"
else:
label_title = f"#{connection_number}"
ax.text(
label_x,
label_y,
f'{label_title}\n{c["Fiber_Length_With_Risk_m"]:.1f} m\n{c["Length_Group"]}',
ha="center",
va="center",
fontsize=LabelFontSize,
bbox={
"facecolor": "white",
"edgecolor": "#BBBBBB",
"boxstyle": "round,pad=0.25",
"alpha": 0.95
},
zorder=6
)
# ------------------------------------------------------------
# Draw points / circles
# ------------------------------------------------------------
for idx, p in enumerate(points):
x = x_positions[idx]
y = y_map[p["Track"]]
circle = Circle(
(x, y),
radius=CircleRadius,
facecolor="white",
edgecolor="black",
linewidth=1.5,
zorder=7
)
ax.add_patch(circle)
# Only NAME inside circle
ax.text(
x,
y,
f'{p["Name"]}',
ha="center",
va="center",
fontsize=NameFontSize,
fontweight="bold",
zorder=8
)
# ------------------------------------------------------------
# KP labels at bottom
# ------------------------------------------------------------
bottom_y = min(y_map.values()) - (1.5 * CircleDiameter)
for kp, x in kp_x_map.items():
ax.text(
x,
bottom_y,
f"KP {kp}",
ha="center",
va="top",
fontsize=KPFontSize,
color="#555555"
)
# ------------------------------------------------------------
# Legend
# ------------------------------------------------------------
legend_elements = [
Line2D(
[0],
[0],
color=strong_green,
lw=5,
label="Same Track - Verde fuerte"
),
Line2D(
[0],
[0],
color=thin_green,
lw=2.5,
label="Track Change - Verde delgado"
),
Line2D(
[0],
[0],
color=return_blue,
lw=5.5,
label="Return Connection - Azul"
),
]
ax.legend(
handles=legend_elements,
loc="upper right"
)
ax.set_title("Fiber Optic Route Representation - KP Aligned")
extra_space = max(1, len(updated_connections) * CircleDiameter)
ax.set_xlim(
min_x - (3 * CircleDiameter),
max_x + (6 * CircleDiameter) + extra_space
)
ax.set_ylim(
min(y_map.values()) - (2.5 * CircleDiameter),
max(y_map.values()) + (3.0 * CircleDiameter)
)
ax.set_yticks([])
ax.set_xticks([])
ax.set_frame_on(False)
plt.tight_layout()
plt.savefig(output_file, dpi=200, bbox_inches="tight")
print(f"Route image generated: {output_file}")
plt.show()
def calculate_route_metrics(points, kp_to_meters):
"""
Calculates general route metrics.
Metrics:
- Total number of connection points / circles.
- Linear distance between the most separated KPs in meters.
"""
total_connection_points = len(points)
min_kp = min(p["KP"] for p in points)
max_kp = max(p["KP"] for p in points)
linear_distance = (max_kp - min_kp) * kp_to_meters
min_kp_points = [
p for p in points
if p["KP"] == min_kp
]
max_kp_points = [
p for p in points
if p["KP"] == max_kp
]
route_metrics = {
"Total_Connection_Points": total_connection_points,
"Min_KP": min_kp,
"Max_KP": max_kp,
"Linear_Distance_m": linear_distance,
"Min_KP_Points": min_kp_points,
"Max_KP_Points": max_kp_points
}
return route_metrics
def print_report(updated_connections, grouped_summary, total_base_fiber, total_fiber_with_risk):
"""
Prints fiber optic connection report.
"""
print("Fiber Optic Conservative Length Report")
print("-" * 135)
print(
f'{"From":<12}'
f'{"From KP":<12}'
f'{"To":<12}'
f'{"To KP":<12}'
f'{"Track Steps":<15}'
f'{"Return":<10}'
f'{"Base Length":<18}'
f'{"Conservative Length":<25}'
f'{"Distance Group":<20}'
f'{"Type":<15}'
)
print("-" * 135)
for c in updated_connections:
print(
f'{c["From"]:<12}'
f'{c["From_KP"]:<12}'
f'{c["To"]:<12}'
f'{c["To_KP"]:<12}'
f'{c["Track_Steps"]:<15}'
f'{str(c["Is_Return"]):<10}'
f'{c["Fiber_Length_m"]:<18.2f}'
f'{c["Fiber_Length_With_Risk_m"]:<25.2f}'
f'{c["Length_Group"]:<20}'
f'{c["Connection_Type"]:<15}'
)
if c["Warning"]:
print(f'WARNING: {c["Warning"]}')
print("-" * 135)
print("\nFiber Length Group Summary")
print("-" * 100)
print(
f'{"Distance Group":<25}'
f'{"Connections":<15}'
f'{"Total Conservative Length":<30}'
)
print("-" * 100)
for group_name, values in grouped_summary.items():
print(
f'{group_name:<25}'
f'{values["Connections_Count"]:<15}'
f'{values["Total_Fiber_m"]:<30.2f}'
)
print("-" * 100)
print(f"Total Base Fiber Length = {total_base_fiber:.2f} m")
print(f"Total Conservative Fiber Length = {total_fiber_with_risk:.2f} m")
def print_route_metrics(route_metrics):
"""
Prints general route metrics.
"""
print("\nRoute General Metrics")
print("-" * 100)
print(
f'Total connection points / circles = '
f'{route_metrics["Total_Connection_Points"]}'
)
print(
f'Linear distance between most separated points = '
f'{route_metrics["Linear_Distance_m"]:.2f} m'
)
print(f'Min KP = {route_metrics["Min_KP"]}')
print("Point(s) at Min KP:")
for p in route_metrics["Min_KP_Points"]:
print(
f' {p["Name"]} | Track {p["Track"]} | KP {p["KP"]}'
)
print(f'Max KP = {route_metrics["Max_KP"]}')
print("Point(s) at Max KP:")
for p in route_metrics["Max_KP_Points"]:
print(
f' {p["Name"]} | Track {p["Track"]} | KP {p["KP"]}'
)
print("-" * 100)
def save_txt_report(
updated_connections,
grouped_summary,
total_base_fiber,
total_fiber_with_risk,
route_metrics,
output_file="fiber_route_report.txt"
):
"""
Saves the same report information printed in console into a TXT file.
"""
with open(output_file, "w", encoding="utf-8") as file:
file.write("Fiber Optic Conservative Length Report\n")
file.write("-" * 135 + "\n")
file.write(
f'{"From":<12}'
f'{"From KP":<12}'
f'{"To":<12}'
f'{"To KP":<12}'
f'{"Track Steps":<15}'
f'{"Return":<10}'
f'{"Base Length":<18}'
f'{"Conservative Length":<25}'
f'{"Distance Group":<20}'
f'{"Type":<15}'
+ "\n"
)
file.write("-" * 135 + "\n")
for c in updated_connections:
file.write(
f'{c["From"]:<12}'
f'{c["From_KP"]:<12}'
f'{c["To"]:<12}'
f'{c["To_KP"]:<12}'
f'{c["Track_Steps"]:<15}'
f'{str(c["Is_Return"]):<10}'
f'{c["Fiber_Length_m"]:<18.2f}'
f'{c["Fiber_Length_With_Risk_m"]:<25.2f}'
f'{c["Length_Group"]:<20}'
f'{c["Connection_Type"]:<15}'
+ "\n"
)
if c["Warning"]:
file.write(f'WARNING: {c["Warning"]}\n')
file.write("-" * 135 + "\n\n")
file.write("Fiber Length Group Summary\n")
file.write("-" * 100 + "\n")
file.write(
f'{"Distance Group":<25}'
f'{"Connections":<15}'
f'{"Total Conservative Length":<30}'
+ "\n"
)
file.write("-" * 100 + "\n")
for group_name, values in grouped_summary.items():
file.write(
f'{group_name:<25}'
f'{values["Connections_Count"]:<15}'
f'{values["Total_Fiber_m"]:<30.2f}'
+ "\n"
)
file.write("-" * 100 + "\n")
file.write(f"Total Base Fiber Length = {total_base_fiber:.2f} m\n")
file.write(f"Total Conservative Fiber Length = {total_fiber_with_risk:.2f} m\n")
file.write("\n\n")
file.write("Route General Metrics\n")
file.write("-" * 100 + "\n")
file.write(
f'Total connection points / circles = '
f'{route_metrics["Total_Connection_Points"]}\n'
)
file.write(
f'Linear distance between most separated points = '
f'{route_metrics["Linear_Distance_m"]:.2f} m\n'
)
file.write(f'Min KP = {route_metrics["Min_KP"]}\n')
file.write("Point(s) at Min KP:\n")
for p in route_metrics["Min_KP_Points"]:
file.write(
f' {p["Name"]} | Track {p["Track"]} | KP {p["KP"]}\n'
)
file.write(f'Max KP = {route_metrics["Max_KP"]}\n')
file.write("Point(s) at Max KP:\n")
for p in route_metrics["Max_KP_Points"]:
file.write(
f' {p["Name"]} | Track {p["Track"]} | KP {p["KP"]}\n'
)
file.write("-" * 100 + "\n")
print(f"TXT report generated: {output_file}")
# ============================================================
# 4. MAIN CODE
# ============================================================
validate_points(points)
connections = create_connections(
points,
ConnectionMaterial,
TrackDistance,
KPtoMeters
)
updated_connections, grouped_summary, total_base_fiber, total_fiber_with_risk = count_fiber_by_length_group(
connections,
RiskFactor,
Limit1,
Limit2,
Limit3,
Limit4
)
route_metrics = calculate_route_metrics(
points,
KPtoMeters
)
plot_route_representation(
points,
updated_connections,
output_file="fiber_route_representation.png"
)
print_report(
updated_connections,
grouped_summary,
total_base_fiber,
total_fiber_with_risk
)
print_route_metrics(route_metrics)
save_txt_report(
updated_connections,
grouped_summary,
total_base_fiber,
total_fiber_with_risk,
route_metrics,
output_file="fiber_route_report.txt"
)
To embed this project on your website, copy the following code and paste it into your website's HTML: