Fixed Geotechnical Report App
Fixed Geotechnical Report App
class GeotechnicalReportApp:
def __init__(self, root):
self.root = root
self.root.title("Geotechnical Report Generator")
self.root.geometry("1000x700")
self.root.minsize(900, 600)
# Color scheme
self.PRIMARY_COLOR = "#2c3e50" # Dark blue
self.SECONDARY_COLOR = "#3498db" # Light blue
self.ACCENT_COLOR = "#e74c3c" # Red
self.BACKGROUND_COLOR = "#ecf0f1" # Light gray
self.TEXT_COLOR = "#2c3e50" # Dark blue
# Data storage
self.project_data = {}
self.soil_layers = []
self.test_results = {}
# Initialize styles
self.style = ttk.Style()
self.style.configure('TFrame', background=self.BACKGROUND_COLOR)
self.style.configure('TLabel', background=self.BACKGROUND_COLOR,
foreground=self.TEXT_COLOR)
self.style.configure('TButton', background=self.SECONDARY_COLOR,
foreground=self.TEXT_COLOR)
self.style.configure('Accent.TButton', background=self.ACCENT_COLOR,
foreground='white')
self.style.configure('TNotebook', background=self.BACKGROUND_COLOR)
# Setup main menu
self.show_main_menu()
def show_main_menu(self):
# Clear the window
for widget in self.root.winfo_children():
widget.destroy()
# Title
title_label = ttk.Label(main_frame, text="Geotechnical Report Generator",
font=("Arial", 24, "bold"), foreground=self.PRIMARY_COLOR)
title_label.pack(pady=30)
# Menu buttons
btn_width = 25
btn_pady = 10
# Footer
footer_frame = ttk.Frame(main_frame)
footer_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=10)
footer_text = ttk.Label(footer_frame,
text="© 2025 Geotechnical Engineering Solutions",
font=("Arial", 10))
footer_text.pack(side=tk.RIGHT)
def load_project(self):
"""Load an existing project from a file"""
messagebox.showinfo("Load Project", "Project loading functionality will be implemented in
a future version.")
# In a complete implementation, this would open a file dialog to select a project file
# and load the data into the application
def show_about(self):
"""Display information about the application"""
about_window = tk.Toplevel(self.root)
about_window.title("About Geotechnical Report Generator")
about_window.geometry("400x300")
about_window.resizable(False, False)
def create_new_project(self):
# Clear the window
for widget in self.root.winfo_children():
widget.destroy()
# Create tabs
self.notebook = ttk.Notebook(main_frame)
self.notebook.pack(fill=tk.BOTH, expand=True, pady=10)
def setup_project_info_tab(self):
# Create frame with padding
frame = ttk.Frame(self.project_info_tab, padding="20")
frame.pack(fill=tk.BOTH, expand=True)
# Title
title = ttk.Label(frame, text="Project Information", font=("Arial", 16, "bold"))
title.grid(row=0, column=0, columnspan=2, sticky=tk.W, pady=10)
# Form fields
fields = [
("Project Name:", "project_name"),
("Client:", "client"),
("Location:", "location"),
("Project Number:", "project_number"),
("Date:", "project_date"),
("Engineer:", "engineer"),
("Project Description:", "description")
]
self.project_entries = {}
self.project_entries[field_name] = entry
# Save button
save_btn = ttk.Button(frame, text="Save Project Information",
command=self.save_project_info)
save_btn.grid(row=len(fields)+1, column=1, sticky=tk.E, pady=20)
def save_project_info(self):
"""Save project information from the form"""
try:
# Get data from text fields and entries
self.project_data["project_name"] = self.project_entries["project_name"].get()
self.project_data["client"] = self.project_entries["client"].get()
self.project_data["location"] = self.project_entries["location"].get()
self.project_data["project_number"] = self.project_entries["project_number"].get()
self.project_data["project_date"] = self.project_entries["project_date"].get()
self.project_data["engineer"] = self.project_entries["engineer"].get()
self.project_data["description"] = self.project_entries["description"].get("1.0",
tk.END).strip()
messagebox.showinfo("Success", "Project information saved successfully!")
def setup_soil_profile_tab(self):
# Create frame with padding
frame = ttk.Frame(self.soil_profile_tab, padding="20")
frame.pack(fill=tk.BOTH, expand=True)
# Title
title = ttk.Label(frame, text="Soil Profile", font=("Arial", 16, "bold"))
title.grid(row=0, column=0, columnspan=4, sticky=tk.W, pady=10)
self.soil_entries = {}
if field_name == "description":
entry = tk.Text(input_frame, height=3, width=30)
else:
entry = ttk.Entry(input_frame, width=30)
entry.grid(row=i, column=1, sticky=tk.W, pady=5)
self.soil_entries[field_name] = entry
# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.grid(row=len(fields), column=0, columnspan=2, pady=10)
self.soil_tree.pack(side=tk.LEFT, fill=tk.BOTH)
# Scrollbar
scrollbar = ttk.Scrollbar(display_frame, orient=tk.VERTICAL,
command=self.soil_tree.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.soil_tree.configure(yscrollcommand=scrollbar.set)
# Canvas for soil profile visualization (will be populated once layers are added)
viz_label = ttk.Label(viz_frame, text="Soil Profile Visualization:")
viz_label.pack(anchor=tk.W)
def add_soil_layer(self):
"""Add a new soil layer to the profile"""
try:
# Get data from entries
depth = float(self.soil_entries["depth"].get())
thickness = float(self.soil_entries["thickness"].get())
soil_type = self.soil_entries["soil_type"].get()
color = self.soil_entries["color"].get()
moisture = float(self.soil_entries["moisture"].get()) if self.soil_entries["moisture"].get()
else 0
unit_weight = float(self.soil_entries["unit_weight"].get()) if
self.soil_entries["unit_weight"].get() else 0
spt_n = int(self.soil_entries["spt_n"].get()) if self.soil_entries["spt_n"].get() else 0
description = self.soil_entries["description"].get("1.0", tk.END).strip()
# Add to treeview
self.soil_tree.insert("", "end", values=(depth, thickness, soil_type, unit_weight, spt_n))
# Clear entries
for entry in self.soil_entries.values():
if isinstance(entry, tk.Text):
entry.delete("1.0", tk.END)
else:
entry.delete(0, tk.END)
# Update visualization
self.update_soil_profile_viz()
def update_soil_layer(self):
"""Update the selected soil layer"""
selected = self.soil_tree.selection()
if not selected:
messagebox.showwarning("Warning", "Please select a soil layer to update.")
return
try:
# Get selected item index
index = self.soil_tree.index(selected[0])
# Update treeview
self.soil_tree.item(selected[0], values=(depth, thickness, soil_type, unit_weight, spt_n))
# Update visualization
self.update_soil_profile_viz()
def delete_soil_layer(self):
"""Delete the selected soil layer"""
selected = self.soil_tree.selection()
if not selected:
messagebox.showwarning("Warning", "Please select a soil layer to delete.")
return
try:
# Get selected item index
index = self.soil_tree.index(selected[0])
self.soil_entries["thickness"].delete(0, tk.END)
self.soil_entries["thickness"].insert(0, str(layer["thickness"]))
self.soil_entries["soil_type"].set(layer["soil_type"])
self.soil_entries["color"].delete(0, tk.END)
self.soil_entries["color"].insert(0, layer["color"])
self.soil_entries["moisture"].delete(0, tk.END)
self.soil_entries["moisture"].insert(0, str(layer["moisture"]))
self.soil_entries["unit_weight"].delete(0, tk.END)
self.soil_entries["unit_weight"].insert(0, str(layer["unit_weight"]))
self.soil_entries["spt_n"].delete(0, tk.END)
self.soil_entries["spt_n"].insert(0, str(layer["spt_n"]))
self.soil_entries["description"].delete("1.0", tk.END)
self.soil_entries["description"].insert("1.0", layer["description"])
def update_soil_profile_viz(self):
"""Update the soil profile visualization"""
if not self.soil_layers:
return
# Plotting
depths = []
heights = []
colors = []
labels = []
# Set y-axis labels and invert (to have 0 depth at the top)
self.viz_subplot.set_yticks(y_pos)
self.viz_subplot.set_yticklabels([f"{d}m" for d in depths])
self.viz_subplot.invert_yaxis()
# Add grid
self.viz_subplot.grid(True, linestyle='--', alpha=0.7)
# Refresh canvas
self.viz_canvas.draw()
def setup_test_results_tab(self):
# Create frame with padding
frame = ttk.Frame(self.test_results_tab, padding="20")
frame.pack(fill=tk.BOTH, expand=True)
# Title
title = ttk.Label(frame, text="Soil Testing Results", font=("Arial", 16, "bold"))
title.grid(row=0, column=0, columnspan=3, sticky=tk.W, pady=10)
# Configure weight
frame.columnconfigure(0, weight=1)
frame.rowconfigure(1, weight=1)
def setup_grain_size_tab(self):
frame = ttk.Frame(self.grain_size_tab, padding="10")
frame.pack(fill=tk.BOTH, expand=True)
# Sample selection
sample_frame = ttk.Frame(input_frame)
sample_frame.pack(fill=tk.X, pady=5)
col2 = ttk.Frame(sieve_frame)
col2.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10)
summary_labels = [
"D10 (mm):", "D30 (mm):", "D60 (mm):",
"Cu:", "Cc:", "% Gravel:",
"% Sand:", "% Silt and Clay:", "USCS:"
]
self.grain_size_summary = {}
# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.pack(fill=tk.X, pady=10)
#LENI
ttk.Button(btn_frame, text="Calculate",
command=self.calculate_grain_size).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Save Test", command=lambda:
self.save_test_data("grain_size")).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Clear",
command=self.clear_grain_size_data).pack(side=tk.LEFT, padx=5)
# Create canvas
self.gs_canvas = FigureCanvasTkAgg(self.gs_figure, viz_frame)
self.gs_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# Configure weights
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)
def calculate_grain_size(self):
"""Calculate grain size distribution parameters"""
try:
# Get percent passing values
data = []
for size, entry in self.grain_size_entries:
if entry.get().strip():
percent = float(entry.get())
data.append((size, percent))
if not data:
messagebox.showwarning("Warning", "Please enter sieve analysis data.")
return
# D60
if min(y) > 60:
d60 = 0
else:
f = interpolate.interp1d(y, x)
d60 = float(f(60))
self.grain_size_summary["Cu"].delete(0, tk.END)
self.grain_size_summary["Cu"].insert(0, f"{cu:.2f}")
self.grain_size_summary["Cc"].delete(0, tk.END)
self.grain_size_summary["Cc"].insert(0, f"{cc:.2f}")
self.grain_size_summary["USCS"].delete(0, tk.END)
self.grain_size_summary["USCS"].insert(0, uscs)
# Plot grain size distribution curve
self.gs_subplot.clear()
self.gs_subplot.semilogx(x, y, 'o-', linewidth=2)
self.gs_subplot.set_xlabel('Particle Size (mm)')
self.gs_subplot.set_ylabel('Percent Passing (%)')
self.gs_subplot.set_title('Grain Size Distribution')
self.gs_subplot.grid(True, which="both", ls="-")
self.gs_subplot.set_ylim([0, 100])
# Refresh canvas
self.gs_canvas.draw()
def clear_grain_size_data(self):
"""Clear grain size analysis data"""
# Clear sieve entries
for _, entry in self.grain_size_entries:
entry.delete(0, tk.END)
def setup_atterberg_limits_tab(self):
frame = ttk.Frame(self.atterberg_tab, padding="10")
frame.pack(fill=tk.BOTH, expand=True)
# Sample selection
sample_frame = ttk.Frame(input_frame)
sample_frame.pack(fill=tk.X, pady=5)
ll_header_frame = ttk.Frame(ll_frame)
ll_header_frame.pack(fill=tk.X)
self.ll_entries = []
for i in range(4): # 4 measurements for liquid limit
row_frame = ttk.Frame(ll_frame)
row_frame.pack(fill=tk.X)
blows_entry = ttk.Entry(row_frame, width=10)
blows_entry.pack(side=tk.LEFT, padx=5, pady=2)
self.ll_entries.append((blows_entry, mc_entry))
pl_header_frame = ttk.Frame(pl_frame)
pl_header_frame.pack(fill=tk.X)
self.pl_entries = []
for i in range(3): # 3 measurements for plastic limit
row_frame = ttk.Frame(pl_frame)
row_frame.pack(fill=tk.X)
self.pl_entries.append(mc_entry)
# Results section
results_frame = ttk.LabelFrame(input_frame, text="Results")
results_frame.pack(fill=tk.X, pady=10)
results_grid = ttk.Frame(results_frame)
results_grid.pack(fill=tk.X, padx=5, pady=5)
# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.pack(fill=tk.X, pady=10)
ttk.Button(btn_frame, text="Calculate",
command=self.calculate_atterberg).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Save Test", command=lambda:
self.save_test_data("atterberg")).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Clear",
command=self.clear_atterberg_data).pack(side=tk.LEFT, padx=5)
# Create canvas
self.at_canvas = FigureCanvasTkAgg(self.at_figure, viz_frame)
self.at_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# Configure weights
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)
def calculate_atterberg(self):
"""Calculate Atterberg limits"""
try:
# Process liquid limit data
ll_data = []
for blows_entry, mc_entry in self.ll_entries:
if blows_entry.get().strip() and mc_entry.get().strip():
blows = float(blows_entry.get())
mc = float(mc_entry.get())
ll_data.append((blows, mc))
if not ll_data:
messagebox.showwarning("Warning", "Please enter liquid limit test data.")
return
if not pl_values:
messagebox.showwarning("Warning", "Please enter plastic limit test data.")
return
self.pl_result.delete(0, tk.END)
self.pl_result.insert(0, f"{pl:.1f}")
self.pi_result.delete(0, tk.END)
self.pi_result.insert(0, f"{pi:.1f}")
# Determine classification
if pi < 4:
classification = "ML" # Low plasticity silt
elif pi >= 4 and pi < 7:
classification = "CL-ML" # Silty clay
elif pi >= 7 and ll < 50:
classification = "CL" # Low plasticity clay
elif pi >= 7 and ll >= 50:
classification = "CH" # High plasticity clay
else:
classification = "--"
self.atterberg_class.delete(0, tk.END)
self.atterberg_class.insert(0, classification)
# Mark 25 blows
ax1.axvline(x=25, color='green', linestyle='--')
ax1.axhline(y=ll, color='green', linestyle='--')
ax1.set_xlabel('Number of Blows')
ax1.set_ylabel('Moisture Content (%)')
ax1.set_title('Flow Curve')
ax1.grid(True)
ax2.set_xlim([0, 100])
ax2.set_ylim([0, 60])
ax2.set_xlabel('Liquid Limit (%)')
ax2.set_ylabel('Plasticity Index (%)')
ax2.set_title('Plasticity Chart')
ax2.grid(True)
ax2.legend()
# Adjust layout
self.at_figure.tight_layout()
# Refresh canvas
self.at_canvas.draw()
def clear_atterberg_data(self):
"""Clear Atterberg limits test data"""
# Clear liquid limit entries
for blows_entry, mc_entry in self.ll_entries:
blows_entry.delete(0, tk.END)
mc_entry.delete(0, tk.END)
# Clear plot
self.at_subplot.clear()
self.at_canvas.draw()
def setup_direct_shear_tab(self):
frame = ttk.Frame(self.direct_shear_tab, padding="10")
frame.pack(fill=tk.BOTH, expand=True)
# Sample info
sample_frame = ttk.Frame(input_frame)
sample_frame.pack(fill=tk.X, pady=5)
# Test data
test_frame = ttk.Frame(input_frame)
test_frame.pack(fill=tk.X, pady=10)
self.ds_entries = []
for i in range(3): # 3 normal stress levels
normal_entry = ttk.Entry(test_frame, width=15)
normal_entry.grid(row=i+1, column=0, padx=5, pady=2)
# Results
results_frame = ttk.LabelFrame(input_frame, text="Shear Strength Parameters")
results_frame.pack(fill=tk.X, pady=10)
result_grid = ttk.Frame(results_frame)
result_grid.pack(fill=tk.X, padx=5, pady=5)
# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.pack(fill=tk.X, pady=10)
ttk.Button(btn_frame, text="Calculate",
command=self.calculate_direct_shear).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Save Test", command=lambda:
self.save_test_data("direct_shear")).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Clear",
command=self.clear_direct_shear_data).pack(side=tk.LEFT, padx=5)
# Create canvas
self.ds_canvas = FigureCanvasTkAgg(self.ds_figure, viz_frame)
self.ds_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# Configure weights
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)
def calculate_direct_shear(self):
"""Calculate direct shear parameters"""
try:
# Extract test data
data = []
for normal_entry, peak_entry, resid_entry in self.ds_entries:
if normal_entry.get().strip() and peak_entry.get().strip():
normal_stress = float(normal_entry.get())
peak_stress = float(peak_entry.get())
if len(data) < 2:
messagebox.showwarning("Warning", "Please enter at least two data points.")
return
self.peak_friction.delete(0, tk.END)
self.peak_friction.insert(0, f"{peak_phi:.1f}")
resid_c = resid_intercept
resid_phi = np.degrees(np.arctan(resid_slope))
self.resid_friction.delete(0, tk.END)
self.resid_friction.insert(0, f"{resid_phi:.1f}")
else:
# Clear residual parameters if not available
self.resid_cohesion.delete(0, tk.END)
self.resid_friction.delete(0, tk.END)
# Plot results
self.ds_subplot.clear()
# Refresh canvas
self.ds_canvas.draw()
def clear_direct_shear_data(self):
"""Clear direct shear test data"""
# Clear test entries
for normal_entry, peak_entry, resid_entry in self.ds_entries:
normal_entry.delete(0, tk.END)
peak_entry.delete(0, tk.END)
resid_entry.delete(0, tk.END)
# Clear plot
self.ds_subplot.clear()
self.ds_subplot.set_xlabel('Normal Stress (kPa)')
self.ds_subplot.set_ylabel('Shear Stress (kPa)')
self.ds_subplot.set_title('Direct Shear Test Results')
self.ds_canvas.draw()
# Get results
if self.ll_result.get().strip():
atterberg_data["results"]["liquid_limit"] = float(self.ll_result.get())
if self.pl_result.get().strip():
atterberg_data["results"]["plastic_limit"] = float(self.pl_result.get())
if self.pi_result.get().strip():
atterberg_data["results"]["plasticity_index"] = float(self.pi_result.get())
if self.atterberg_class.get().strip():
atterberg_data["results"]["classification"] = self.atterberg_class.get()
self.test_results["atterberg"].append(atterberg_data)
if resid_entry.get().strip():
test_point["residual_shear_stress"] = float(resid_entry.get())
ds_data["test_data"].append(test_point)
# Get results
if self.peak_cohesion.get().strip():
ds_data["results"]["peak_cohesion"] = float(self.peak_cohesion.get())
if self.peak_friction.get().strip():
ds_data["results"]["peak_friction_angle"] = float(self.peak_friction.get())
if self.resid_cohesion.get().strip():
ds_data["results"]["residual_cohesion"] = float(self.resid_cohesion.get())
if self.resid_friction.get().strip():
ds_data["results"]["residual_friction_angle"] = float(self.resid_friction.get())
self.test_results["direct_shear"].append(ds_data)
messagebox.showinfo("Success", f"Direct shear test for Sample {sample_id} saved
successfully!")
def setup_consolidation_tab(self):
frame = ttk.Frame(self.consolidation_tab, padding="10")
frame.pack(fill=tk.BOTH, expand=True)
# Sample info
sample_frame = ttk.Frame(input_frame)
sample_frame.pack(fill=tk.X, pady=5)
# Test data
test_frame = ttk.LabelFrame(input_frame, text="Loading Stages")
test_frame.pack(fill=tk.X, pady=10)
# Headers
ttk.Label(test_frame, text="Pressure (kPa)").grid(row=0, column=0, padx=5, pady=2)
ttk.Label(test_frame, text="Void Ratio").grid(row=0, column=1, padx=5, pady=2)
self.cons_entries.append((pressure_entry, void_ratio_entry))
# Results
results_frame = ttk.LabelFrame(input_frame, text="Consolidation Parameters")
results_frame.pack(fill=tk.X, pady=10)
result_grid = ttk.Frame(results_frame)
result_grid.pack(fill=tk.X, padx=5, pady=5)
# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.pack(fill=tk.X, pady=10)
ttk.Button(btn_frame, text="Calculate",
command=self.calculate_consolidation).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Save Test", command=lambda:
self.save_test_data("consolidation")).pack(side=tk.LEFT, padx=5)
ttk.Button(btn_frame, text="Clear",
command=self.clear_consolidation_data).pack(side=tk.LEFT, padx=5)
# Configure weights
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)
def calculate_consolidation(self):
"""Calculate consolidation parameters"""
try:
# Extract test data
data = []
for pressure_entry, void_ratio_entry in self.cons_entries:
if pressure_entry.get().strip() and void_ratio_entry.get().strip():
pressure = float(pressure_entry.get())
void_ratio = float(void_ratio_entry.get())
data.append((pressure, void_ratio))
if len(data) < 4:
messagebox.showwarning("Warning", "Please enter at least four data points.")
return
# Extract arrays
pressures = np.array([point[0] for point in data])
void_ratios = np.array([point[1] for point in data])
if len(virgin_data) >= 2:
log_p_virgin = np.array([point[0] for point in virgin_data])
e_virgin = np.array([point[1] for point in virgin_data])
# Linear regression
slope_virgin, _, _, _, _ = stats.linregress(log_p_virgin, e_virgin)
if len(recomp_data) >= 2:
log_p_recomp = np.array([point[0] for point in recomp_data])
e_recomp = np.array([point[1] for point in recomp_data])
# Linear regression
slope_recomp, _, _, _, _ = stats.linregress(log_p_recomp, e_recomp)
# Update results
self.compress_index.delete(0, tk.END)
self.compress_index.insert(0, f"{cc:.3f}")
self.recompress_index.delete(0, tk.END)
self.recompress_index.insert(0, f"{cr:.3f}")
self.precons_pressure.delete(0, tk.END)
self.precons_pressure.insert(0, f"{pc:.1f}")
self.initial_void_ratio.delete(0, tk.END)
self.initial_void_ratio.insert(0, f"{e0:.3f}")
# Refresh canvas
self.cons_canvas.draw()
def clear_consolidation_data(self):
"""Clear consolidation test data"""
# Clear test entries
for pressure_entry, void_ratio_entry in self.cons_entries:
pressure_entry.delete(0, tk.END)
void_ratio_entry.delete(0, tk.END)
# Clear plot
self.cons_subplot.clear()
self.cons_subplot.set_xlabel('Pressure (kPa) - Log Scale')
self.cons_subplot.set_ylabel('Void Ratio')
self.cons_subplot.set_title('e-log p Curve')
self.cons_canvas.draw()
def generate_report(self):
"""Generate a comprehensive report with all test results"""
if not hasattr(self, 'project_info') or not self.project_info:
messagebox.showwarning("Warning", "Please enter project information first.")
return
if not self.test_results:
messagebox.showwarning("Warning", "No test results to generate report.")
return
try:
# Ask for save location
file_path = filedialog.asksaveasfilename(
defaultextension=".pdf",
filetypes=[("PDF Files", "*.pdf"), ("All Files", "*.*")],
title="Save Report As"
)
if not file_path:
return
# Create document
doc = SimpleDocTemplate(file_path, pagesize=letter)
elements = []
# Get styles
styles = getSampleStyleSheet()
title_style = styles['Heading1']
heading2_style = styles['Heading2']
normal_style = styles['Normal']
# Add title
elements.append(Paragraph("Geotechnical Laboratory Test Report", title_style))
elements.append(Spacer(1, 12))
project_data = [
["Project Name:", self.project_info.get("name", "")],
["Project Number:", self.project_info.get("number", "")],
["Location:", self.project_info.get("location", "")],
["Client:", self.project_info.get("client", "")],
["Engineer:", self.project_info.get("engineer", "")],
["Date:", self.project_info.get("date", "")]
]
elements.append(project_table)
elements.append(Spacer(1, 12))
# Summary data
summary_data = [["Parameter", "Value"]]
for key, value in test["summary"].items():
summary_data.append([key, str(value)])
elements.append(summary_table)
elements.append(Spacer(1, 12))
# Atterberg Limits
if "atterberg" in self.test_results and self.test_results["atterberg"]:
elements.append(Paragraph("Atterberg Limits Results", heading2_style))
elements.append(Spacer(1, 6))
elements.append(results_table)
elements.append(Spacer(1, 12))
# Direct Shear
if "direct_shear" in self.test_results and self.test_results["direct_shear"]:
elements.append(Paragraph("Direct Shear Test Results", heading2_style))
elements.append(Spacer(1, 6))
# Results
if "results" in test:
results_data = [["Parameter", "Value"]]
for key, value in test["results"].items():
results_data.append([key, str(value)])
elements.append(results_table)
elements.append(Spacer(1, 12))
# Consolidation123
# Continuation of the report generation function for consolidation tests
if "consolidation" in self.test_results and self.test_results["consolidation"]:
elements.append(Paragraph("Consolidation Test Results", heading2_style))
elements.append(Spacer(1, 6))
# Results
if "results" in test:
results_data = [["Parameter", "Value"]]
for key, value in test["results"].items():
results_data.append([key, str(value)])
elements.append(results_table)
elements.append(Spacer(1, 12))
# Add conclusion
elements.append(Paragraph("Conclusions and Recommendations", heading2_style))
elements.append(Spacer(1, 6))
elements.append(Paragraph("This report provides a summary of laboratory test results
for the project. Additional analysis and interpretation may be required for specific design
purposes.", normal_style))
except Exception as e:
messagebox.showerror("Error", f"Failed to generate report: {str(e)}")
# Get results
if self.compress_index.get().strip():
cons_data["results"]["compression_index"] = float(self.compress_index.get())
if self.recompress_index.get().strip():
cons_data["results"]["recompression_index"] = float(self.recompress_index.get())
if self.precons_pressure.get().strip():
cons_data["results"]["preconsolidation_pressure"] = float(self.precons_pressure.get())
if self.initial_void_ratio.get().strip():
cons_data["results"]["initial_void_ratio"] = float(self.initial_void_ratio.get())
self.test_results["consolidation"].append(cons_data)
def setup_triaxial_tab(self):
"""Set up the triaxial test tab"""
frame = ttk.Frame(self.triaxial_tab, padding="10")
frame.pack(fill=tk.BOTH, expand=True)
# Sample info
sample_frame = ttk.Frame(input_frame)
sample_frame.pack(fill=tk.X, pady=5)
# Test type
type_frame = ttk.Frame(input_frame)
type_frame.pack(fill=tk.X, pady=5)
# Test data
test_frame = ttk.LabelFrame(input_frame, text="Test Results for Each Cell Pressure")
test_frame.pack(fill=tk.X, pady=10)
# Headers
headers_frame = ttk.Frame(test_frame)
headers_frame.pack(fill=tk.X)
canvas = tk.Canvas(scrollable_frame)
scrollbar = ttk.Scrollbar(scrollable_frame, orient="vertical", command=canvas.yview)
scrollable_inner = ttk.Frame(canvas)
canvas.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
canvas.create_window((0, 0), window=scrollable_inner, anchor="nw")
scrollable_inner.bind(
"<Configure>",
lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
# Results
results_frame = ttk.LabelFrame(input_frame, text="Triaxial Strength Parameters")
results_frame.pack(fill=tk.X, pady=10)
result_grid = ttk.Frame(results_frame)
result_grid.pack(fill=tk.X, padx=5, pady=5)
# Buttons
btn_frame = ttk.Frame(input_frame)
btn_frame.pack(fill=tk.X, pady=10)
# Create canvas
self.tx_canvas = FigureCanvasTkAgg(self.tx_figure, viz_frame)
self.tx_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
# Configure weights
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
frame.rowconfigure(0, weight=1)
def calculate_triaxial(self):
"""Calculate triaxial test parameters"""
try:
# Extract test data
data = []
for cell_entry, dev_entry, pore_entry in self.tx_entries:
if cell_entry.get().strip() and dev_entry.get().strip():
cell_pressure = float(cell_entry.get())
dev_stress = float(dev_entry.get())
if len(data) < 2:
messagebox.showwarning("Warning", "Please enter at least two data points.")
return
p_values.append(p)
q_values.append(q)
self.tx_friction.delete(0, tk.END)
self.tx_friction.insert(0, f"{phi:.1f}")
# Refresh canvas
self.tx_canvas.draw()
def clear_triaxial_data(self):
"""Clear triaxial test data"""
# Clear test entries
for cell_entry, dev_entry, pore_entry in self.tx_entries:
cell_entry.delete(0, tk.END)
dev_entry.delete(0, tk.END)
pore_entry.delete(0, tk.END)
# Clear result entries
self.tx_cohesion.delete(0, tk.END)
self.tx_friction.delete(0, tk.END)
self.tx_cu.delete(0, tk.END)
# Clear plot
self.tx_subplot.clear()
self.tx_subplot.set_xlabel('Normal Stress (kPa)')
self.tx_subplot.set_ylabel('Shear Stress (kPa)')
self.tx_subplot.set_title('Triaxial Test Results')
self.tx_canvas.draw()
def save_project(self):
"""Save all project data to a JSON file"""
if not hasattr(self, 'project_info') or not self.project_info:
messagebox.showwarning("Warning", "Please enter project information first.")
return
try:
# Ask for save location
file_path = filedialog.asksaveasfilename(
defaultextension=".json",
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
title="Save Project As"
)
if not file_path:
return
# Save to file
with open(file_path, 'w') as f:
json.dump(project_data, f, indent=4)
def load_project(self):
"""Load project data from a JSON file"""
try:
# Ask for file to open
file_path = filedialog.askopenfilename(
filetypes=[("JSON Files", "*.json"), ("All Files", "*.*")],
title="Open Project"
)
if not file_path:
return
if "test_results" in project_data:
self.test_results = project_data["test_results"]
def update_project_info_display(self):
"""Update UI to show loaded project info"""
if hasattr(self, 'project_info') and self.project_info:
# Update project info display in the UI
if hasattr(self, 'project_name'):
self.project_name.delete(0, tk.END)
self.project_name.insert(0, self.project_info.get("name", ""))
if hasattr(self, 'project_number'):
self.project_number.delete(0, tk.END)
self.project_number.insert(0, self.project_info.get("number", ""))
if hasattr(self, 'project_location'):
self.project_location.delete(0, tk.END)
self.project_location.insert(0, self.project_info.get("location", ""))
if hasattr(self, 'project_client'):
self.project_client.delete(0, tk.END)
self.project_client.insert(0, self.project_info.get("client", ""))
if hasattr(self, 'project_engineer'):
self.project_engineer.delete(0, tk.END)
self.project_engineer.insert(0, self.project_info.get("engineer", ""))
if hasattr(self, 'project_date'):
self.project_date.delete(0, tk.END)
self.project_date.insert(0, self.project_info.get("date", ""))
def about_dialog(self):
"""Show about dialog"""
about_window = tk.Toplevel(self.master)
about_window.title("About GeotechLab")
about_window.geometry("400x300")
about_window.resizable(False, False)
# Center window
about_window.update_idletasks()
x = (about_window.winfo_screenwidth() - about_window.winfo_width()) // 2
y = (about_window.winfo_screenheight() - about_window.winfo_height()) // 2
about_window.geometry(f"+{x}+{y}")
# Content
frame = ttk.Frame(about_window, padding="20")
frame.pack(fill=tk.BOTH, expand=True)