Initial commit (python only)

This commit is contained in:
Carla 2026-02-06 19:13:03 +01:00
commit a73a12859b
3 changed files with 2568 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
__pycache__/
*.pyc
MedidasLab/

518
cods/GUI.py Normal file
View file

@ -0,0 +1,518 @@
# GUI.py
import threading
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from pathlib import Path
# =========================
# Backend (robusto)
# =========================
try:
import S_Langmuir as backend
except Exception as e:
backend = None
_BACKEND_IMPORT_ERROR = str(e)
def _get_backend_attr(name: str):
if backend is None:
return None
return getattr(backend, name, None)
run_full_catodos_tokens = _get_backend_attr("run_full_catodos_tokens")
run_full_hilos_tokens = _get_backend_attr("run_full_hilos_tokens")
run_check_ionico = _get_backend_attr("run_check_ionico")
set_sonda = _get_backend_attr("set_sonda")
# ---------------------------
# Helpers (agrupación tokens)
# ---------------------------
def parse_token_map(s: str) -> dict[str, int]:
s = (s or "").strip()
if not s:
return {}
out: dict[str, int] = {}
for chunk in s.split(","):
chunk = chunk.strip()
if not chunk:
continue
if "=" not in chunk:
raise ValueError(f"Formato token_map inválido: '{chunk}'. Usa campo=indice")
k, v = chunk.split("=", 1)
k = k.strip()
v = v.strip()
if not k:
raise ValueError("Hay un campo vacío en token_map.")
out[k] = int(v)
return out
def parse_order(s: str) -> list[str]:
s = (s or "").strip()
if not s:
return []
return [x.strip() for x in s.split(",") if x.strip()]
def parse_group_tokens(filename: str, delimiter: str, token_map: dict[str, int], order: list[str]) -> str:
stem = Path(filename).stem
parts = [p.strip() for p in stem.split(delimiter) if p.strip() != ""]
values = {}
for k, idx in token_map.items():
values[k] = parts[idx] if 0 <= idx < len(parts) else "NA"
folders = []
for k in order:
v = str(values.get(k, "NA")).strip().replace("\\", "-").replace("/", "-")
folders.append(v if v else "NA")
return "/".join(folders) if folders else "otros"
def _has_na(group: str) -> bool:
if not group:
return False
return ("NA/" in group) or ("/NA" in group) or group.startswith("NA") or group.endswith("/NA")
# ---------------------------
# GUI
# ---------------------------
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("IV Processor")
self.geometry("1100x760")
self.minsize(1000, 680)
if backend is None:
messagebox.showerror(
"Backend no disponible",
"No se ha podido importar S_Langmuir.py.\n\n"
f"Error:\n{_BACKEND_IMPORT_ERROR}\n\n"
"Asegúrate de que GUI.py y S_Langmuir.py están en la misma carpeta."
)
# Estado
self.mode = tk.StringVar(value="catodo")
self.folder = tk.StringVar(value="")
self.status = tk.StringVar(value="Listo.")
self.busy = tk.BooleanVar(value=False)
# ✅ Check iónico integrado (sin botón)
self.do_ionico = tk.BooleanVar(value=False)
# Warning banner
self.warn = tk.StringVar(value="")
self._preview_summary = {"bad": 0, "total": 0}
# --------- CATODOS (TOKENS) ---------
self.cat_delim = tk.StringVar(value="_")
self.cat_token_map = tk.StringVar(value="tipo=0,presion=1,vcat=2,pol=3")
self.cat_order = tk.StringVar(value="tipo,presion,vcat,pol")
# --------- HILOS (TOKENS) -----------
self.hil_delim = tk.StringVar(value="_")
self.hil_token_map = tk.StringVar(value="tipo=0,ich=1,vcp=2")
self.hil_order = tk.StringVar(value="tipo,vcp,ich")
# --------- SONDA (GUI) --------------
self.probe_geom = tk.StringVar(value="cilindrica")
self.probe_r_mm = tk.StringVar(value="0.445")
self.probe_L_mm = tk.StringVar(value="22.59")
self._preview_job = None
self._build()
self._wire_traces()
self._refresh_fields(initial=True)
self._refresh_probe_fields()
self._refresh_backend_state()
# ---------------- UI build ----------------
def _build(self):
pad = {"padx": 10, "pady": 6}
# Mode
f_mode = ttk.LabelFrame(self, text="Modo")
f_mode.pack(fill="x", **pad)
ttk.Radiobutton(
f_mode, text="Cátodos", variable=self.mode, value="catodo",
command=self._refresh_fields
).pack(side="left", padx=10, pady=8)
ttk.Radiobutton(
f_mode, text="Hilos", variable=self.mode, value="hilos",
command=self._refresh_fields
).pack(side="left", padx=10, pady=8)
# Folder
f_folder = ttk.LabelFrame(self, text="Carpeta de datos")
f_folder.pack(fill="x", **pad)
row = ttk.Frame(f_folder)
row.pack(fill="x", padx=10, pady=8)
ttk.Entry(row, textvariable=self.folder).pack(side="left", fill="x", expand=True)
ttk.Button(row, text="Elegir…", command=self._pick_folder).pack(side="left", padx=8)
# Warning label
self.lbl_warn = ttk.Label(self, textvariable=self.warn, foreground="darkorange")
self.lbl_warn.pack(fill="x", padx=14, pady=(0, 6))
# ---------------- SONDA SETTINGS ----------------
self.f_probe = ttk.LabelFrame(self, text="Sonda (para n_e): geometría y dimensiones")
self.f_probe.pack(fill="x", **pad)
self.f_probe.columnconfigure(1, weight=1)
self.f_probe.columnconfigure(3, weight=1)
ttk.Label(self.f_probe, text="Geometría:").grid(row=0, column=0, sticky="w", padx=10, pady=6)
rowg = ttk.Frame(self.f_probe)
rowg.grid(row=0, column=1, sticky="w", padx=10, pady=6)
ttk.Radiobutton(rowg, text="Cilíndrica", variable=self.probe_geom, value="cilindrica",
command=self._refresh_probe_fields).pack(side="left", padx=(0, 12))
ttk.Radiobutton(rowg, text="Esférica", variable=self.probe_geom, value="esferica",
command=self._refresh_probe_fields).pack(side="left")
ttk.Label(self.f_probe, text="Radio r (mm):").grid(row=0, column=2, sticky="w", padx=10, pady=6)
ttk.Entry(self.f_probe, textvariable=self.probe_r_mm, width=10).grid(row=0, column=3, sticky="w", padx=10, pady=6)
ttk.Label(self.f_probe, text="Longitud L (mm):").grid(row=1, column=2, sticky="w", padx=10, pady=6)
self.ent_L = ttk.Entry(self.f_probe, textvariable=self.probe_L_mm, width=10)
self.ent_L.grid(row=1, column=3, sticky="w", padx=10, pady=6)
self.lbl_probe_hint = ttk.Label(
self.f_probe,
text="Nota: L solo aplica a sonda cilíndrica. El backend debe exponer set_sonda(...) para que esto tenga efecto.",
foreground="gray"
)
self.lbl_probe_hint.grid(row=2, column=0, columnspan=4, sticky="w", padx=10, pady=(0, 6))
# ---------------- TOKENS SETTINGS: CATODOS ----------------
self.f_catodos = ttk.LabelFrame(self, text="Cátodos: agrupación (Tokens)")
self.f_catodos.columnconfigure(1, weight=1)
self.f_catodos.columnconfigure(3, weight=1)
ttk.Label(self.f_catodos, text="Separador:").grid(row=0, column=0, sticky="w", padx=10, pady=6)
ttk.Entry(self.f_catodos, textvariable=self.cat_delim, width=6).grid(row=0, column=1, sticky="w", padx=10, pady=6)
ttk.Label(self.f_catodos, text="Mapa campo=índice:").grid(row=0, column=2, sticky="w", padx=10, pady=6)
ttk.Entry(self.f_catodos, textvariable=self.cat_token_map).grid(row=0, column=3, sticky="ew", padx=10, pady=6)
ttk.Label(self.f_catodos, text="Orden carpetas (coma):").grid(row=1, column=0, sticky="w", padx=10, pady=6)
ttk.Entry(self.f_catodos, textvariable=self.cat_order).grid(row=1, column=1, columnspan=3, sticky="ew", padx=10, pady=6)
# ---------------- TOKENS SETTINGS: HILOS ----------------
self.f_hilos = ttk.LabelFrame(self, text="Hilos: agrupación (Tokens)")
self.f_hilos.columnconfigure(1, weight=1)
self.f_hilos.columnconfigure(3, weight=1)
ttk.Label(self.f_hilos, text="Separador:").grid(row=0, column=0, sticky="w", padx=10, pady=6)
ttk.Entry(self.f_hilos, textvariable=self.hil_delim, width=6).grid(row=0, column=1, sticky="w", padx=10, pady=6)
ttk.Label(self.f_hilos, text="Mapa campo=índice:").grid(row=0, column=2, sticky="w", padx=10, pady=6)
ttk.Entry(self.f_hilos, textvariable=self.hil_token_map).grid(row=0, column=3, sticky="ew", padx=10, pady=6)
ttk.Label(self.f_hilos, text="Orden carpetas (coma):").grid(row=1, column=0, sticky="w", padx=10, pady=6)
ttk.Entry(self.f_hilos, textvariable=self.hil_order).grid(row=1, column=1, columnspan=3, sticky="ew", padx=10, pady=6)
# ---------------- PREVIEW ----------------
f_prev = ttk.LabelFrame(self, text="Preview de agrupación (primeros 12 archivos)")
f_prev.pack(fill="both", expand=True, **pad)
self.txt_prev = tk.Text(f_prev, height=14, wrap="none")
self.txt_prev.pack(fill="both", expand=True, padx=10, pady=8)
# ---------------- ACTIONS ----------------
f_act = ttk.Frame(self)
f_act.pack(fill="x", **pad)
self.btn_preview = ttk.Button(f_act, text="Actualizar preview", command=self._preview)
self.btn_preview.pack(side="left", padx=(10, 6), pady=10)
self.btn_run = ttk.Button(f_act, text="Ejecutar", command=self._run)
self.btn_run.pack(side="left", padx=6, pady=10)
# ✅ CHECKBOX (en vez de botón)
self.chk_iones = ttk.Checkbutton(
f_act,
text="Hacer check iónico al terminar",
variable=self.do_ionico,
command=self._refresh_backend_state
)
self.chk_iones.pack(side="left", padx=(14, 6), pady=10)
ttk.Label(f_act, textvariable=self.status).pack(side="right", padx=10)
# ---------------- Wiring / state ----------------
def _wire_traces(self):
vars_to_watch = [
self.folder, self.mode,
self.cat_delim, self.cat_token_map, self.cat_order,
self.hil_delim, self.hil_token_map, self.hil_order,
self.probe_geom, self.probe_r_mm, self.probe_L_mm,
]
for v in vars_to_watch:
v.trace_add("write", lambda *_: self._schedule_preview())
def _schedule_preview(self):
if self._preview_job:
try:
self.after_cancel(self._preview_job)
except Exception:
pass
self._preview_job = self.after(350, self._preview)
def _refresh_fields(self, initial: bool = False):
if self.mode.get() == "catodo":
self.f_catodos.pack(fill="x", padx=10, pady=6)
self.f_hilos.pack_forget()
else:
self.f_hilos.pack(fill="x", padx=10, pady=6)
self.f_catodos.pack_forget()
if not initial:
self._schedule_preview()
self._refresh_backend_state()
def _refresh_probe_fields(self):
self.ent_L.config(state=("normal" if self.probe_geom.get() == "cilindrica" else "disabled"))
def _refresh_backend_state(self):
# Run principal
if backend is None or self.busy.get():
self.btn_run.config(state="disabled")
self.chk_iones.config(state="disabled")
return
ok_run = (run_full_catodos_tokens is not None) if self.mode.get() == "catodo" else (run_full_hilos_tokens is not None)
self.btn_run.config(state=("normal" if ok_run else "disabled"))
# Checkbox iónico solo si existe backend iónico
ok_iones = (run_check_ionico is not None)
self.chk_iones.config(state=("normal" if ok_iones else "disabled"))
# Si no existe run_check_ionico, fuerza a False
if not ok_iones:
self.do_ionico.set(False)
def _pick_folder(self):
d = filedialog.askdirectory()
if d:
self.folder.set(d)
def _set_busy(self, busy: bool):
self.busy.set(busy)
self.btn_run.config(state=("disabled" if busy else "normal"))
self.btn_preview.config(state=("disabled" if busy else "normal"))
self.chk_iones.config(state=("disabled" if busy else "normal"))
if busy:
self.status.set("Procesando…")
else:
self._refresh_backend_state()
# ---------------- Probe handling ----------------
def _get_probe_config(self) -> tuple[str, float, float | None]:
geom = (self.probe_geom.get() or "").strip().lower()
if geom not in ("cilindrica", "esferica"):
raise ValueError("Geometría de sonda inválida (cilindrica/esferica).")
try:
r_mm = float(str(self.probe_r_mm.get()).replace(",", "."))
except Exception:
raise ValueError("Radio r (mm) no es numérico.")
if r_mm <= 0:
raise ValueError("Radio r (mm) debe ser > 0.")
L_mm = None
if geom == "cilindrica":
try:
L_mm = float(str(self.probe_L_mm.get()).replace(",", "."))
except Exception:
raise ValueError("Longitud L (mm) no es numérica.")
if L_mm <= 0:
raise ValueError("Longitud L (mm) debe ser > 0.")
return geom, r_mm, L_mm
def _apply_probe_config(self):
geom, r_mm, L_mm = self._get_probe_config()
if set_sonda is None:
raise RuntimeError("El backend no expone set_sonda(...).")
set_sonda(geom=geom, rp_mm=r_mm, L_mm=L_mm)
# ---------------- Preview ----------------
def _preview(self):
folder = self.folder.get().strip()
self.txt_prev.delete("1.0", "end")
self.warn.set("")
self._preview_summary = {"bad": 0, "total": 0}
if not folder:
self.txt_prev.insert("end", "Elige una carpeta.\n")
return
data_dir = Path(folder)
if not data_dir.exists():
self.txt_prev.insert("end", "Carpeta no existe.\n")
return
files = sorted([p for p in data_dir.iterdir() if p.is_file() and p.suffix.lower() in (".txt", ".csv")])
if not files:
self.txt_prev.insert("end", "No hay .txt/.csv en la carpeta.\n")
return
show = files[:12]
total = len(files)
n_otros = 0
n_na = 0
bad_examples: list[str] = []
try:
if self.mode.get() == "catodo":
delim = self.cat_delim.get() or "_"
tmap = parse_token_map(self.cat_token_map.get())
order = parse_order(self.cat_order.get())
else:
delim = self.hil_delim.get() or "_"
tmap = parse_token_map(self.hil_token_map.get())
order = parse_order(self.hil_order.get())
for fp in files:
g = parse_group_tokens(fp.name, delim, tmap, order)
if g == "otros":
n_otros += 1
if len(bad_examples) < 6:
bad_examples.append(fp.name)
if _has_na(g):
n_na += 1
if len(bad_examples) < 6:
bad_examples.append(fp.name)
for fp in show:
g = parse_group_tokens(fp.name, delim, tmap, order)
self.txt_prev.insert("end", f"{fp.name}\n -> {g}\n\n")
msgs = []
bad = 0
if n_otros > 0:
msgs.append(f"{n_otros}/{total} -> 'otros'")
bad += n_otros
if n_na > 0:
msgs.append(f"{n_na}/{total} contienen 'NA'")
bad += n_na
self._preview_summary = {"bad": bad, "total": total}
if msgs:
extra = (" | Ejemplos: " + "; ".join(bad_examples[:6])) if bad_examples else ""
self.warn.set("⚠️ OJO: " + " · ".join(msgs) + extra)
else:
self.warn.set("✅ OK: patrón consistente.")
except Exception as e:
self.warn.set(f"⚠️ Error preview: {e}")
self.txt_prev.insert("end", f"Error preview: {e}\n")
# ---------------- Run pipeline (+ opcional iónico) ----------------
def _run(self):
if self.busy.get():
return
if backend is None:
messagebox.showerror("Backend", "S_Langmuir.py no está importable.")
return
folder = self.folder.get().strip()
if not folder:
messagebox.showwarning("Falta carpeta", "Elige la carpeta de datos.")
return
data_dir = Path(folder)
if not data_dir.exists():
messagebox.showerror("Error", "La carpeta no existe.")
return
# preview + warning
self._preview()
bad = self._preview_summary.get("bad", 0)
total = self._preview_summary.get("total", 0)
if total > 0 and bad > 0:
if not messagebox.askyesno(
"Aviso",
f"Hay {bad}/{total} archivos que NO encajan con el patrón.\n¿Ejecutar igualmente?"
):
return
# aplica sonda
try:
self._apply_probe_config()
except Exception as e:
messagebox.showerror("Sonda", str(e))
return
# backend function
if self.mode.get() == "catodo" and run_full_catodos_tokens is None:
messagebox.showerror("Backend", "Falta run_full_catodos_tokens en S_Langmuir.py")
return
if self.mode.get() == "hilos" and run_full_hilos_tokens is None:
messagebox.showerror("Backend", "Falta run_full_hilos_tokens en S_Langmuir.py")
return
# si el usuario ha marcado iónico pero no existe función
if self.do_ionico.get() and run_check_ionico is None:
messagebox.showerror("Backend", "Has marcado check iónico pero falta run_check_ionico en S_Langmuir.py")
return
self._set_busy(True)
t = threading.Thread(target=self._run_real, args=(data_dir, self.do_ionico.get()), daemon=True)
t.start()
def _run_real(self, data_dir: Path, do_ionico: bool):
try:
# 1) pipeline
if self.mode.get() == "catodo":
delim = self.cat_delim.get() or "_"
tmap = parse_token_map(self.cat_token_map.get())
order = parse_order(self.cat_order.get())
run_full_catodos_tokens(data_dir, delim, tmap, order)
else:
delim = self.hil_delim.get() or "_"
tmap = parse_token_map(self.hil_token_map.get())
order = parse_order(self.hil_order.get())
run_full_hilos_tokens(data_dir, delim, tmap, order)
# 2) iónico opcional (sin botón)
if do_ionico:
out_paths = run_check_ionico(data_dir)
n = len(out_paths) if out_paths is not None else 0
self.after(0, lambda: self.status.set("Terminado + Iones ✅"))
self.after(0, lambda: messagebox.showinfo("OK", f"Proceso completado.\nCheck iónico: {n} Calc_iones.csv"))
else:
self.after(0, lambda: self.status.set("Terminado ✅"))
self.after(0, lambda: messagebox.showinfo("OK", "Proceso completado."))
self.after(0, self._preview)
except Exception as e:
msg = str(e)
self.after(0, lambda: self.status.set("Error ❌"))
self.after(0, lambda m=msg: messagebox.showerror("Error", m))
finally:
self.after(0, lambda: self._set_busy(False))
if __name__ == "__main__":
App().mainloop()

2046
cods/S_Langmuir.py Normal file

File diff suppressed because it is too large Load diff