Virtual Plant Care Simulator

import pygame

import sys

import json

import math

import time

from datetime import datetime


# -------------------------

# Config

# -------------------------

SAVE_FILE = "plant_save.json"

WIDTH, HEIGHT = 720, 560

FPS = 60


# In-game time scale: how many in-game minutes pass per real second

TIME_SCALE_MIN_PER_SEC = 5.0            # 1 real sec = 5 in-game minutes

TURBO_MULTIPLIER = 10.0                 # Turbo mode speed-up


# Decay & growth tuning (per in-game minute)

MOISTURE_DECAY = 0.10                   # moisture decreases per minute

NUTRIENT_DECAY = 0.02                   # nutrients decreases per minute

HEALTH_INC_RATE = 0.02                  # health regen per minute (good care)

HEALTH_DEC_RATE = 0.05                  # health loss per minute (bad care)

HEALTH_SEVERE_PENALTY = 0.20            # extra loss if moisture is 0


WATER_AMOUNT = 35                       # moisture gained per watering

FERTILIZER_AMOUNT = 25                  # nutrients gained per fertilize


GOOD_MOISTURE = 50

GOOD_NUTRIENTS = 50

LOW_MOISTURE = 30

LOW_NUTRIENTS = 30


# Growth stages by age (in-game minutes)

STAGES = [

    ("Seed", 0),

    ("Sprout", 12 * 60),         # 12 hours

    ("Young", 36 * 60),          # 1.5 days

    ("Mature", 3 * 24 * 60),     # 3 days

    ("Blooming", 7 * 24 * 60),   # 1 week

]


# Colors

WHITE = (255, 255, 255)

BG = (238, 245, 239)

TEXT = (34, 40, 49)

CARD = (245, 250, 245)

GREEN = (72, 157, 77)

YELLOW = (240, 190, 40)

RED = (220, 76, 70)

BLUE = (66, 135, 245)

BROWN = (99, 68, 48)

SOIL = (84, 59, 42)

GREY = (200, 205, 210)


pygame.init()

pygame.display.set_caption("Virtual Plant Care Simulator ๐ŸŒฑ")

screen = pygame.display.set_mode((WIDTH, HEIGHT))

clock = pygame.time.Clock()

FONT = pygame.font.SysFont("consolas", 18)

FONT_BIG = pygame.font.SysFont("consolas", 24, bold=True)


# -------------------------

# Helpers

# -------------------------

def clamp(v, lo=0.0, hi=100.0):

    return max(lo, min(hi, v))


def now_iso():

    return datetime.now().isoformat()


def load_state():

    try:

        with open(SAVE_FILE, "r", encoding="utf-8") as f:

            data = json.load(f)

        # basic sanity defaults

        data.setdefault("name", "Leafy")

        data.setdefault("age_minutes", 0.0)

        data.setdefault("moisture", 70.0)

        data.setdefault("nutrients", 60.0)

        data.setdefault("health", 80.0)

        data.setdefault("alive", True)

        data.setdefault("last_update_iso", now_iso())

        data.setdefault("turbo", False)

        return data

    except FileNotFoundError:

        return {

            "name": "Leafy",

            "age_minutes": 0.0,

            "moisture": 70.0,

            "nutrients": 60.0,

            "health": 80.0,

            "alive": True,

            "last_update_iso": now_iso(),

            "turbo": False

        }


def save_state(state):

    with open(SAVE_FILE, "w", encoding="utf-8") as f:

        json.dump(state, f, indent=2)


def stage_from_age(age_min):

    current = STAGES[0][0]

    for name, threshold in STAGES:

        if age_min >= threshold:

            current = name

        else:

            break

    return current


def draw_bar(surface, x, y, w, h, pct, color, label):

    pygame.draw.rect(surface, GREY, (x, y, w, h), border_radius=6)

    inner_w = int(w * clamp(pct/100.0, 0, 1))

    pygame.draw.rect(surface, color, (x, y, inner_w, h), border_radius=6)

    txt = FONT.render(f"{label}: {int(pct)}%", True, TEXT)

    surface.blit(txt, (x + 8, y + h//2 - txt.get_height()//2))


def draw_button(surface, rect, text, active=True):

    col = CARD if active else (230,230,230)

    pygame.draw.rect(surface, col, rect, border_radius=8)

    pygame.draw.rect(surface, GREY, rect, width=1, border_radius=8)

    t = FONT.render(text, True, TEXT if active else (140,140,140))

    surface.blit(t, (rect.x + rect.w//2 - t.get_width()//2,

                     rect.y + rect.h//2 - t.get_height()//2))


def nice_time(minutes):

    d = int(minutes // (24*60))

    h = int((minutes % (24*60)) // 60)

    m = int(minutes % 60)

    parts = []

    if d: parts.append(f"{d}d")

    if h: parts.append(f"{h}h")

    parts.append(f"{m}m")

    return " ".join(parts)


# -------------------------

# Plant rendering

# -------------------------

def draw_pot(surface, cx, base_y):

    pot_w = 160

    pot_h = 70

    rim_h = 18

    # pot body

    pygame.draw.rect(surface, BROWN, (cx - pot_w//2, base_y - pot_h, pot_w, pot_h), border_radius=10)

    # soil

    pygame.draw.rect(surface, SOIL, (cx - pot_w//2 + 10, base_y - pot_h + rim_h, pot_w - 20, pot_h - rim_h - 6), border_radius=8)

    # rim

    pygame.draw.rect(surface, (120, 84, 60), (cx - pot_w//2, base_y - pot_h - 6, pot_w, rim_h), border_radius=8)


def draw_plant(surface, cx, base_y, age_min, health, moisture, nutrients, t):

    """

    Procedural plant:

    - Height scales with age & health

    - Stem sways using sin wave

    - Leaves hue via moisture/nutrients

    """

    # growth factor

    stage = stage_from_age(age_min)

    stage_index = [s[0] for s in STAGES].index(stage)

    height = 40 + stage_index * 35 + (health/100.0) * 30  # 40..~220


    # sway

    sway = math.sin(t * 2.0) * 8  # px

    top_x = cx + sway

    top_y = base_y - height


    # stem

    points = []

    segments = 8

    for i in range(segments+1):

        y = base_y - (height * i / segments)

        x = cx + math.sin(t*2 + i*0.6) * (8 * (i/segments))

        points.append((x, y))

    # draw stem as polyline (thicker at base)

    for i in range(len(points)-1):

        w = int(8 - 6*(i/segments))

        pygame.draw.line(surface, (46, 120, 52), points[i], points[i+1], max(2, w))


    # leaf color based on care

    care_score = (moisture/100.0 + nutrients/100.0 + health/100.0) / 3.0

    leaf_col = (

        int(60 + 120 * care_score),    # R

        int(120 + 120 * care_score),   # G

        int(60 + 80 * care_score)      # B

    )


    # leaves along stem

    for i in range(2, segments):

        px, py = points[i]

        lr = 18 + 6*(i/segments)

        angle = math.sin(t*3 + i) * 0.5 + (1 if i%2==0 else -1)*0.6

        # left leaf

        leaf_poly(surface, px, py, lr, angle, leaf_col)

        # right leaf

        leaf_poly(surface, px, py, lr, -angle, leaf_col)


    # bud/flower at top for late stages

    if stage in ("Mature", "Blooming"):

        r = 10 if stage == "Mature" else 14 + 4*math.sin(t*4)

        pygame.draw.circle(surface, (240, 110, 130), (int(points[-1][0]), int(points[-1][1])-8), int(abs(r)))


def leaf_poly(surface, x, y, r, angle, col):

    # Draw a simple rotated leaf (ellipse-ish polygon)

    pts = []

    steps = 10

    for i in range(steps+1):

        theta = -math.pi/2 + math.pi * (i/steps)

        px = x + r * math.cos(theta) * 0.4

        py = y + r * math.sin(theta)

        # rotate around (x,y)

        rx = x + (px - x) * math.cos(angle) - (py - y) * math.sin(angle)

        ry = y + (px - x) * math.sin(angle) + (py - y) * math.cos(angle)

        pts.append((rx, ry))

    pygame.draw.polygon(surface, col, pts)


# -------------------------

# Update logic

# -------------------------

def update_state(state, dt_minutes):

    if not state["alive"]:

        return


    # age grows faster when healthy

    health_factor = 0.5 + (state["health"]/100.0) * 0.5  # 0.5..1.0

    state["age_minutes"] += dt_minutes * health_factor


    # resource decay

    state["moisture"] = clamp(state["moisture"] - MOISTURE_DECAY * dt_minutes)

    state["nutrients"] = clamp(state["nutrients"] - NUTRIENT_DECAY * dt_minutes)


    # health dynamics

    good_care = (state["moisture"] >= GOOD_MOISTURE) and (state["nutrients"] >= GOOD_NUTRIENTS)

    low_care = (state["moisture"] < LOW_MOISTURE) or (state["nutrients"] < LOW_NUTRIENTS)


    if good_care:

        state["health"] = clamp(state["health"] + HEALTH_INC_RATE * dt_minutes)

    if low_care:

        penalty = HEALTH_DEC_RATE * dt_minutes

        # severe dehydration hurts more

        if state["moisture"] <= 0.1:

            penalty += HEALTH_SEVERE_PENALTY * dt_minutes

        state["health"] = clamp(state["health"] - penalty)


    if state["health"] <= 0.0:

        state["alive"] = False


def water(state):

    if not state["alive"]: return

    state["moisture"] = clamp(state["moisture"] + WATER_AMOUNT)


def fertilize(state):

    if not state["alive"]: return

    state["nutrients"] = clamp(state["nutrients"] + FERTILIZER_AMOUNT)


def revive(state):

    # Soft revive to keep testing

    state["alive"] = True

    state["health"] = max(state["health"], 40.0)

    state["moisture"] = max(state["moisture"], 40.0)


# -------------------------

# UI setup

# -------------------------

BUTTONS = {

    "water": pygame.Rect(40, 460, 140, 36),

    "fertilize": pygame.Rect(200, 460, 140, 36),

    "save": pygame.Rect(360, 460, 120, 36),

    "turbo": pygame.Rect(500, 460, 160, 36),

    "revive": pygame.Rect(40, 510, 120, 34),

    "new": pygame.Rect(180, 510, 120, 34),

    "quit": pygame.Rect(320, 510, 120, 34),

}


def reset_new_plant():

    return {

        "name": "Leafy",

        "age_minutes": 0.0,

        "moisture": 70.0,

        "nutrients": 60.0,

        "health": 80.0,

        "alive": True,

        "last_update_iso": now_iso(),

        "turbo": False

    }


# -------------------------

# Main loop

# -------------------------

def main():

    state = load_state()


    last_real = time.time()

    running = True


    while running:

        real_now = time.time()

        real_dt = real_now - last_real

        last_real = real_now


        # Time scale

        speed = TIME_SCALE_MIN_PER_SEC * (TURBO_MULTIPLIER if state.get("turbo") else 1.0)

        dt_minutes = real_dt * speed


        # Update plant based on dt

        update_state(state, dt_minutes)


        # Draw

        screen.fill(BG)


        # Left info card

        pygame.draw.rect(screen, CARD, (24, 24, 280, 410), border_radius=12)

        pygame.draw.rect(screen, GREY, (24, 24, 280, 410), width=1, border_radius=12)


        title = FONT_BIG.render("Plant Status", True, TEXT)

        screen.blit(title, (40, 36))


        # Bars

        draw_bar(screen, 40, 80, 240, 20, state["health"], GREEN if state["health"] >= 50 else YELLOW if state["health"] >= 25 else RED, "Health")

        draw_bar(screen, 40, 120, 240, 20, state["moisture"], BLUE if state["moisture"] >= 40 else YELLOW if state["moisture"] >= 20 else RED, "Moisture")

        draw_bar(screen, 40, 160, 240, 20, state["nutrients"], GREEN if state["nutrients"] >= 40 else YELLOW if state["nutrients"] >= 20 else RED, "Nutrients")


        # Text stats

        lines = [

            f"Name: {state['name']}",

            f"Stage: {stage_from_age(state['age_minutes'])}",

            f"Age: {nice_time(state['age_minutes'])}",

            f"Alive: {'Yes' if state['alive'] else 'No'}",

            f"Speed: {'Turbo' if state.get('turbo') else 'Normal'} (x{int(TURBO_MULTIPLIER) if state.get('turbo') else 1})",

            "",

            "Tips:",

            "- Keep moisture & nutrients ≥ 50.",

            "- Turbo to fast-forward growth.",

            "- Save to keep progress.",

            "- Revive for testing if it dies."

        ]

        y = 200

        for ln in lines:

            txt = FONT.render(ln, True, TEXT)

            screen.blit(txt, (40, y))

            y += 22


        # Plant area (right)

        pygame.draw.rect(screen, CARD, (324, 24, 372, 410), border_radius=12)

        pygame.draw.rect(screen, GREY, (324, 24, 372, 410), width=1, border_radius=12)

        arena = pygame.Rect(324, 24, 372, 410)

        cx = arena.x + arena.w//2

        base_y = arena.y + arena.h - 40


        # Ground line

        pygame.draw.line(screen, (180, 180, 180), (arena.x + 20, base_y), (arena.right - 20, base_y), 2)


        # Draw pot & plant

        draw_pot(screen, cx, base_y)

        t = pygame.time.get_ticks() / 1000.0

        draw_plant(screen, cx, base_y, state["age_minutes"], state["health"], state["moisture"], state["nutrients"], t)


        # Buttons

        draw_button(screen, BUTTONS["water"], "๐Ÿ’ง Water", active=state["alive"])

        draw_button(screen, BUTTONS["fertilize"], "๐Ÿงช Fertilize", active=state["alive"])

        draw_button(screen, BUTTONS["save"], "๐Ÿ’พ Save")

        draw_button(screen, BUTTONS["turbo"], f"⚡ Turbo: {'ON' if state.get('turbo') else 'OFF'}")

        draw_button(screen, BUTTONS["revive"], "❤️ Revive")

        draw_button(screen, BUTTONS["new"], "๐ŸŒฑ New Plant")

        draw_button(screen, BUTTONS["quit"], "๐Ÿšช Quit")


        # Warning labels

        warn_y = 430

        if state["moisture"] < LOW_MOISTURE:

            wtxt = FONT.render("Low Moisture! Water me ๐Ÿ’ง", True, BLUE)

            screen.blit(wtxt, (40, warn_y)); warn_y += 22

        if state["nutrients"] < LOW_NUTRIENTS:

            wtxt = FONT.render("Low Nutrients! Fertilize ๐Ÿงช", True, (150, 100, 20))

            screen.blit(wtxt, (40, warn_y)); warn_y += 22

        if not state["alive"]:

            wtxt = FONT_BIG.render("The plant has died. Try Revive or New Plant.", True, RED)

            screen.blit(wtxt, (40, warn_y))


        pygame.display.flip()


        # Events

        for event in pygame.event.get():

            if event.type == pygame.QUIT:

                save_state(state)

                running = False

            elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:

                mx, my = event.pos

                if BUTTONS["water"].collidepoint(mx, my) and state["alive"]:

                    water(state)

                elif BUTTONS["fertilize"].collidepoint(mx, my) and state["alive"]:

                    fertilize(state)

                elif BUTTONS["save"].collidepoint(mx, my):

                    save_state(state)

                elif BUTTONS["turbo"].collidepoint(mx, my):

                    state["turbo"] = not state.get("turbo", False)

                elif BUTTONS["revive"].collidepoint(mx, my):

                    revive(state)

                elif BUTTONS["new"].collidepoint(mx, my):

                    state = reset_new_plant()

                elif BUTTONS["quit"].collidepoint(mx, my):

                    save_state(state)

                    running = False

            elif event.type == pygame.KEYDOWN:

                if event.key == pygame.K_w and state["alive"]:

                    water(state)

                elif event.key == pygame.K_f and state["alive"]:

                    fertilize(state)

                elif event.key == pygame.K_s:

                    save_state(state)

                elif event.key == pygame.K_t:

                    state["turbo"] = not state.get("turbo", False)

                elif event.key == pygame.K_r:

                    revive(state)

                elif event.key == pygame.K_n:

                    state = reset_new_plant()

                elif event.key == pygame.K_ESCAPE:

                    save_state(state)

                    running = False


        clock.tick(FPS)


    pygame.quit()

    sys.exit()


if __name__ == "__main__":

    main()


Handwritten Math Solver

 Install requirements

pip install tensorflow Pillow opencv-python numpy sympy

Train a digit model once (MNIST) — train_mnist_cnn.py

This trains a small CNN on MNIST and saves mnist_cnn.h5.

# train_mnist_cnn.py

import tensorflow as tf

from tensorflow import keras

from tensorflow.keras import layers


def build_model():

    model = keras.Sequential([

        layers.Input(shape=(28, 28, 1)),

        layers.Conv2D(32, 3, activation='relu'),

        layers.Conv2D(32, 3, activation='relu'),

        layers.MaxPooling2D(),

        layers.Dropout(0.25),


        layers.Conv2D(64, 3, activation='relu'),

        layers.Conv2D(64, 3, activation='relu'),

        layers.MaxPooling2D(),

        layers.Dropout(0.25),


        layers.Flatten(),

        layers.Dense(128, activation='relu'),

        layers.Dropout(0.5),

        layers.Dense(10, activation='softmax')

    ])

    model.compile(optimizer='adam',

                  loss='sparse_categorical_crossentropy',

                  metrics=['accuracy'])

    return model


def main():

    (x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()

    x_train = x_train.astype("float32") / 255.0

    x_test = x_test.astype("float32") / 255.0

    x_train = x_train[..., None]

    x_test = x_test[..., None]


    model = build_model()

    model.fit(x_train, y_train, batch_size=128, epochs=5, validation_split=0.1)

    test_loss, test_acc = model.evaluate(x_test, y_test, verbose=0)

    print(f"Test accuracy: {test_acc:.4f}")

    model.save("mnist_cnn.h5")

    print("Saved model to mnist_cnn.h5")


if __name__ == "__main__":

    main()

The GUI solver — handwritten_math_solver.py

# handwritten_math_solver.py
import tkinter as tk
from tkinter import messagebox
from PIL import Image, ImageDraw, ImageOps
import numpy as np
import cv2
import io
from sympy import sympify, simplify
from tensorflow.keras.models import load_model
# ---- Config ----
MODEL_PATH = "mnist_cnn.h5"
CANVAS_SIZE = 400           # drawing canvas (square)
DRAW_WIDTH = 14             # brush thickness (thicker = easier OCR)
MIN_CONTOUR_AREA = 60       # filter noise
PADDING = 8                 # pad per glyph before resize to 28x28
# Heuristics thresholds for operators
MINUS_AR_THRESH = 2.0       # width/height > this → likely '-'
MINUS_HEIGHT_FRAC = 0.45    # symbol height relative to median digit height (shorter → minus)
PLUS_PEAK_FRAC = 0.6        # vertical and horizontal central peaks to consider '+'
class MathSolverApp:
    def __init__(self, root):
        self.root = root
        self.root.title("Handwritten Math Solver (digits + +/−)")
        # Canvas to draw
        self.canvas = tk.Canvas(root, width=CANVAS_SIZE, height=CANVAS_SIZE, bg="white", cursor="cross")
        self.canvas.grid(row=0, column=0, columnspan=3, padx=10, pady=10)
        # PIL image to accumulate strokes (black on white)
        self.image = Image.new("L", (CANVAS_SIZE, CANVAS_SIZE), color=255)
        self.draw = ImageDraw.Draw(self.image)
        # Bind drawing
        self.last_x, self.last_y = None, None
        self.canvas.bind("<ButtonPress-1>", self.pen_down)
        self.canvas.bind("<B1-Motion>", self.paint)
        self.canvas.bind("<ButtonRelease-1>", self.pen_up)
        # Buttons
        tk.Button(root, text="Recognize & Solve", command=self.recognize_and_solve).grid(row=1, column=0, pady=6)
        tk.Button(root, text="Clear", command=self.clear_canvas).grid(row=1, column=1, pady=6)
        tk.Button(root, text="Quit", command=root.quit).grid(row=1, column=2, pady=6)
        # Output
        self.expr_var = tk.StringVar(value="Expression: ")
        self.result_var = tk.StringVar(value="Result: ")
        self.step_text = tk.Text(root, width=60, height=10, wrap="word")
        tk.Label(root, textvariable=self.expr_var, anchor="w").grid(row=2, column=0, columnspan=3, sticky="w", padx=10)
        tk.Label(root, textvariable=self.result_var, anchor="w").grid(row=3, column=0, columnspan=3, sticky="w", padx=10)
        tk.Label(root, text="Steps:").grid(row=4, column=0, sticky="w", padx=10)
        self.step_text.grid(row=5, column=0, columnspan=3, padx=10, pady=4)
        # Load model
        try:
            self.model = load_model(MODEL_PATH)
        except Exception as e:
            messagebox.showerror("Model Error",
                                 f"Could not load {MODEL_PATH}.\nTrain it first with train_mnist_cnn.py.\n\n{e}")
            self.model = None
    # ---------- Drawing handlers ----------
    def pen_down(self, event):
        self.last_x, self.last_y = event.x, event.y
    def paint(self, event):
        if self.last_x is not None and self.last_y is not None:
            # Draw on Tk canvas
            self.canvas.create_line(self.last_x, self.last_y, event.x, event.y,
                                    width=DRAW_WIDTH, fill="black", capstyle=tk.ROUND, smooth=True)
            # Draw on PIL image
            self.draw.line([self.last_x, self.last_y, event.x, event.y],
                           fill=0, width=DRAW_WIDTH)
        self.last_x, self.last_y = event.x, event.y
    def pen_up(self, event):
        self.last_x, self.last_y = None, None
    def clear_canvas(self):
        self.canvas.delete("all")
        self.image = Image.new("L", (CANVAS_SIZE, CANVAS_SIZE), color=255)
        self.draw = ImageDraw.Draw(self.image)
        self.expr_var.set("Expression: ")
        self.result_var.set("Result: ")
        self.step_text.delete("1.0", tk.END)
    # ---------- Core pipeline ----------
    def recognize_and_solve(self):
        if self.model is None:
            messagebox.showwarning("Model", "Model not loaded.")
            return
        # Convert PIL to OpenCV
        img = np.array(self.image)
        expr, tokens_dbg = self.image_to_expression(img)
        if not expr:
            messagebox.showwarning("Parse", "Could not parse any symbols. Try writing bigger/cleaner.")
            return
        self.expr_var.set(f"Expression: {expr}")
        try:
            # Use sympy to evaluate
            sym_expr = sympify(expr)
            simplified = simplify(sym_expr)
            self.result_var.set(f"Result: {simplified}")
            # Show steps (simple for now)
            self.step_text.delete("1.0", tk.END)
            self.step_text.insert(tk.END, "Tokens (left→right):\n")
            self.step_text.insert(tk.END, " ".join(tokens_dbg) + "\n\n")
            self.step_text.insert(tk.END, f"SymPy parsed: {sym_expr}\n")
            if str(sym_expr) != str(simplified):
                self.step_text.insert(tk.END, f"Simplified: {simplified}\n")
            else:
                self.step_text.insert(tk.END, "No further simplification needed.\n")
        except Exception as e:
            messagebox.showerror("Evaluation Error", f"Failed to evaluate expression:\n{e}")
    def image_to_expression(self, gray_img: np.ndarray) -> tuple[str, list]:
        """
        Segment symbols, classify digits with CNN, infer + / - with projection heuristics.
        Returns (expression_string, debug_tokens)
        """
        # 1) Binarize & clean
        # Invert: handwriting is black (0), background white (255) => for OpenCV we want white-on-black for morphology ops.
        inv = 255 - gray_img
        # Threshold
        _, th = cv2.threshold(inv, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        # Morph open small noise
        kernel = np.ones((3,3), np.uint8)
        th = cv2.morphologyEx(th, cv2.MORPH_OPEN, kernel, iterations=1)
        # 2) Find contours (symbols)
        contours, _ = cv2.findContours(th, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        boxes = []
        for cnt in contours:
            x, y, w, h = cv2.boundingRect(cnt)
            area = w * h
            if area < MIN_CONTOUR_AREA:
                continue
            boxes.append((x, y, w, h))
        if not boxes:
            return "", []
        # Sort left-to-right
        boxes.sort(key=lambda b: b[0])
        # Median height (helps operator heuristics)
        med_h = np.median([h for (_, _, _, h) in boxes])
        tokens = []
        debug_tokens = []
        for (x, y, w, h) in boxes:
            crop = th[y:y+h, x:x+w]  # white ink on black background
            # Operator heuristic first (minus / plus)
            op = self.classify_operator(crop, w, h, med_h)
            if op is not None:
                tokens.append(op)
                debug_tokens.append(f"[{op}]")
                continue
            # Otherwise, digit classification
            digit = self.classify_digit(crop)
            if digit is None:
                # If not digit and not recognized operator, skip (or treat as minus attempt)
                # Safer to skip
                continue
            tokens.append(str(digit))
            debug_tokens.append(str(digit))
        # Merge digits & operators into expression string
        expr = self.tokens_to_expression(tokens)
        return expr, debug_tokens
    def classify_digit(self, crop_bin: np.ndarray) -> int | None:
        """
        Prepare glyph for MNIST CNN (28x28, centered), and predict 0-9.
        crop_bin: white ink on black background (binary)
        """
        # Make sure it's binary (0/255)
        crop = (crop_bin > 0).astype(np.uint8) * 255
        # Add padding
        crop = cv2.copyMakeBorder(crop, PADDING, PADDING, PADDING, PADDING, cv2.BORDER_CONSTANT, value=0)
        # Find tight box again after pad
        ys, xs = np.where(crop > 0)
        if len(xs) == 0 or len(ys) == 0:
            return None
        x0, x1 = xs.min(), xs.max()
        y0, y1 = ys.min(), ys.max()
        crop = crop[y0:y1+1, x0:x1+1]
        # Resize to 20x20 then center in 28x28 (like MNIST preprocessing)
        h, w = crop.shape
        if h > w:
            new_h = 20
            new_w = int(w * (20.0 / h))
        else:
            new_w = 20
            new_h = int(h * (20.0 / w))
        if new_h <= 0: new_h = 1
        if new_w <= 0: new_w = 1
        resized = cv2.resize(crop, (new_w, new_h), interpolation=cv2.INTER_AREA)
        canvas = np.zeros((28, 28), dtype=np.uint8)
        y_off = (28 - new_h) // 2
        x_off = (28 - new_w) // 2
        canvas[y_off:y_off+new_h, x_off:x_off+new_w] = resized
        # Normalize for model: MNIST is black background (0) with white strokes (1)
        img = canvas.astype("float32") / 255.0
        img = img[..., None]  # (28,28,1)
        pred = self.model.predict(img[None, ...], verbose=0)[0]
        cls = int(np.argmax(pred))
        conf = float(np.max(pred))
        # Optional confidence filtering
        if conf < 0.40:
            return None
        return cls
    def classify_operator(self, crop_bin: np.ndarray, w: int, h: int, med_h: float) -> str | None:
        """
        Very lightweight heuristics:
        - '-' : wide, short, one thick horizontal stroke (width/height large, height << median digit height)
        - '+' : strong central vertical and horizontal projections (peaks)
        """
        # Work on binary with 1s where stroke is present
        b = (crop_bin > 0).astype(np.uint8)
        # Aspect ratio heuristic for '-'
        if h > 0:
            ar = w / float(h)
        else:
            ar = 0
        # height relative to median digit height
        h_frac = h / float(med_h) if med_h > 0 else 1.0
        # Horizontal projection profile (sum along columns) and vertical profile (sum along rows)
        vproj = b.sum(axis=0)  # per column
        hproj = b.sum(axis=1)  # per row
        v_center_peak = vproj[len(vproj)//2] / (b.shape[0] + 1e-6)
        h_center_peak = hproj[len(hproj)//2] / (b.shape[1] + 1e-6)
        # Minus: flat, wide, short
        if ar >= MINUS_AR_THRESH and h_frac <= MINUS_HEIGHT_FRAC:
            return "-"
        # Plus: vertical & horizontal strong central strokes
        if v_center_peak >= PLUS_PEAK_FRAC and h_center_peak >= PLUS_PEAK_FRAC:
            return "+"
        return None
    def tokens_to_expression(self, tokens: list[str]) -> str:
        """
        Combine tokens into a valid expression.
        - Collapse consecutive digits into multi-digit numbers.
        - Keep '+' and '-' as operators.
        - Remove illegal leading/trailing operators.
        """
        # Collapse digits
        out = []
        num_buf = []
        for t in tokens:
            if t.isdigit():
                num_buf.append(t)
            else:
                # flush number
                if num_buf:
                    out.append("".join(num_buf))
                    num_buf = []
                # operator allowed only if last is number
                if len(out) > 0 and out[-1][-1].isdigit() and t in {"+", "-"}:
                    out.append(t)
        # flush at end
        if num_buf:
            out.append("".join(num_buf))
        # Join safely
        expr = ""
        for item in out:
            if item in {"+", "-"}:
                expr += f" {item} "
            else:
                expr += item
        return expr.strip()
if __name__ == "__main__":
    root = tk.Tk()
    app = MathSolverApp(root)
    root.mainloop()


AI Resume Ranker

import os

import re

import argparse

import glob

from typing import List, Tuple, Dict, Optional


import pandas as pd

import numpy as np


from sklearn.feature_extraction.text import TfidfVectorizer, ENGLISH_STOP_WORDS

from sklearn.metrics.pairwise import cosine_similarity


# Optional parsers

import docx2txt

import PyPDF2


# Optional NLP

try:

    import spacy

    SPACY_OK = True

except Exception:

    SPACY_OK = False



# --------------------------- File Readers ---------------------------


def read_txt(path: str) -> str:

    with open(path, "r", encoding="utf-8", errors="ignore") as f:

        return f.read()


def read_docx(path: str) -> str:

    try:

        return docx2txt.process(path) or ""

    except Exception:

        return ""


def read_pdf(path: str) -> str:

    text = []

    try:

        with open(path, "rb") as f:

            reader = PyPDF2.PdfReader(f)

            for page in reader.pages:

                t = page.extract_text() or ""

                text.append(t)

    except Exception:

        pass

    return "\n".join(text)


def load_text_any(path: str) -> str:

    ext = os.path.splitext(path)[1].lower()

    if ext == ".txt":

        return read_txt(path)

    elif ext == ".docx":

        return read_docx(path)

    elif ext == ".pdf":

        return read_pdf(path)

    else:

        return ""



# --------------------------- Skills ---------------------------


DEFAULT_SKILLS = [

    # Generic

    "python","java","c++","javascript","typescript","sql","nosql","git","docker","kubernetes","linux",

    "aws","azure","gcp","bash","shell","rest","graphql","microservices",

    # Data/AI

    "pandas","numpy","scikit-learn","sklearn","tensorflow","pytorch","keras","nltk","spacy",

    "spark","hadoop","airflow","dbt","powerbi","tableau","matplotlib","seaborn",

    # Web/Backend

    "django","flask","fastapi","spring","node","express","react","angular","vue",

    # DevOps/Cloud

    "terraform","ansible","jenkins","ci/cd","prometheus","grafana","elk","rabbitmq","kafka",

    # Testing

    "pytest","unittest","selenium","cypress",

    # Security & Other

    "oauth","jwt","scrum","agile","jira"

]


def load_skills_file(path: Optional[str]) -> List[str]:

    if not path:

        return DEFAULT_SKILLS

    skills = []

    with open(path, "r", encoding="utf-8", errors="ignore") as f:

        for line in f:

            s = line.strip().lower()

            if s:

                skills.append(s)

    return sorted(set(skills))



# --------------------------- NLP Cleaning ---------------------------


def build_spacy_pipeline(use_spacy: bool):

    if use_spacy and SPACY_OK:

        try:

            nlp = spacy.load("en_core_web_sm", disable=["ner","parser","textcat"])

            return nlp

        except Exception:

            return None

    return None


CLEAN_RE = re.compile(r"[^a-z0-9+#./\- ]+")


def normalize_text(text: str) -> str:

    text = text.lower()

    text = text.replace("\n", " ").replace("\t", " ")

    text = CLEAN_RE.sub(" ", text)

    text = re.sub(r"\s+", " ", text).strip()

    return text


def lemmatize_spacy(nlp, text: str) -> str:

    if not nlp:

        return text

    doc = nlp(text)

    return " ".join(tok.lemma_ for tok in doc if not tok.is_space)



# --------------------------- Feature Engineering ---------------------------


def skill_overlap_score(text: str, jd_skills: List[str]) -> float:

    """

    Compute a skill overlap score (0..1) = Jaccard-like:

    |skills_in_resume ∩ skills_in_jd| / |skills_in_jd|

    """

    text_tokens = set(re.findall(r"[a-z0-9+#.\-]+", text.lower()))

    resume_skills = set()

    for skill in jd_skills:

        tokens = skill.split()

        if len(tokens) == 1:

            if skill in text_tokens:

                resume_skills.add(skill)

        else:

            if skill in text:

                resume_skills.add(skill)

    if not jd_skills:

        return 0.0

    return len(resume_skills) / float(len(set(jd_skills)))



# --------------------------- Ranking ---------------------------


def rank_resumes(

    jd_text: str,

    resume_texts: Dict[str, str],

    use_spacy: bool = False,

    weights: Tuple[float, float] = (0.7, 0.3),

    custom_skills: Optional[List[str]] = None

) -> pd.DataFrame:


    w_sem, w_skill = weights

    assert abs((w_sem + w_skill) - 1.0) < 1e-6, "weights must sum to 1"


    # Prepare spaCy if requested

    nlp = build_spacy_pipeline(use_spacy)


    # Normalize & (optionally) lemmatize

    jd_clean = normalize_text(jd_text)

    if nlp:

        jd_clean = lemmatize_spacy(nlp, jd_clean)


    cleaned = {}

    for fname, txt in resume_texts.items():

        t = normalize_text(txt)

        if nlp:

            t = lemmatize_spacy(nlp, t)

        cleaned[fname] = t


    # TF-IDF across JD + Resumes

    vectorizer = TfidfVectorizer(stop_words="english", max_features=40000, ngram_range=(1,2))

    corpus = [jd_clean] + [cleaned[f] for f in cleaned]

    tfidf = vectorizer.fit_transform(corpus)


    # Cosine similarity of resumes against JD (index 0)

    sims = cosine_similarity(tfidf[0:1], tfidf[1:]).flatten()


    # Skills from JD text (intersect default skills + those present in JD)

    base_skills = custom_skills if custom_skills is not None else DEFAULT_SKILLS

    jd_skill_candidates = [s for s in base_skills if s in jd_clean]

    # Fallback: if no skills found in JD, keep base set (less strict)

    jd_skillset = jd_skill_candidates if jd_skill_candidates else base_skills


    # Skill overlap score per resume

    files = list(cleaned.keys())

    skill_scores = []

    for f in files:

        s = skill_overlap_score(cleaned[f], jd_skillset)

        skill_scores.append(s)


    # Final score

    final = w_sem * sims + w_skill * np.array(skill_scores)


    df = pd.DataFrame({

        "resume_file": files,

        "semantic_similarity": np.round(sims, 4),

        "skill_overlap": np.round(skill_scores, 4),

        "final_score": np.round(final, 4)

    }).sort_values("final_score", ascending=False).reset_index(drop=True)


    return df



# --------------------------- CLI ---------------------------


def main():

    parser = argparse.ArgumentParser(description="AI Resume Ranker — rank resumes against a job description.")

    parser.add_argument("--jd", required=True, help="Job description file (.txt/.pdf/.docx)")

    parser.add_argument("--resumes", required=True, help="Folder containing resumes (.txt/.pdf/.docx)")

    parser.add_argument("--export", default="ranked_resumes.csv", help="Path to export CSV results")

    parser.add_argument("--skills", default=None, help="Optional skills file (one skill per line)")

    parser.add_argument("--use-spacy", action="store_true", help="Enable spaCy lemmatization (install en_core_web_sm)")

    parser.add_argument("--weights", nargs=2, type=float, default=[0.7, 0.3],

                        help="Weights for [semantic_similarity skill_overlap], must sum to 1.0 (default 0.7 0.3)")

    args = parser.parse_args()


    # Load JD

    jd_text = load_text_any(args.jd)

    if not jd_text.strip():

        raise SystemExit(f"Could not read job description: {args.jd}")


    # Load resumes

    patterns = ["*.pdf", "*.docx", "*.txt"]

    files = []

    for p in patterns:

        files.extend(glob.glob(os.path.join(args.resumes, p)))

    if not files:

        raise SystemExit(f"No resumes found in: {args.resumes}")


    resume_texts = {}

    for f in files:

        txt = load_text_any(f)

        if txt.strip():

            resume_texts[os.path.basename(f)] = txt


    # Load skills (optional)

    custom_skills = load_skills_file(args.skills) if args.skills else None


    # Rank

    df = rank_resumes(

        jd_text=jd_text,

        resume_texts=resume_texts,

        use_spacy=args.use_spacy,

        weights=(args.weights[0], args.weights[1]),

        custom_skills=custom_skills

    )


    # Save

    df.to_csv(args.export, index=False)

    print("\nTop matches:")

    print(df.head(10).to_string(index=False))

    print(f"\nSaved results to: {args.export}")



if __name__ == "__main__":

    main()


Interactive Periodic Table

import json

import os

import tkinter as tk

from tkinter import ttk, messagebox

import webbrowser

from functools import partial

import threading


import matplotlib.pyplot as plt

from mpl_toolkits.mplot3d import Axes3D  # noqa: F401 unused import (needed by matplotlib 3D)

import numpy as np


# ----------------- Config -----------------

DATA_FILE = "elements.json"

GRID_COLUMNS = 18

GRID_ROWS = 9  # we'll use rows 1-7 + an extra for Lanthanides/Actinides placeholder


# ----------------- Default JSON Creator -----------------

DEFAULT_ELEMENTS = {

    # symbol: data

    "H": {"name": "Hydrogen", "atomic_number": 1, "atomic_mass": 1.008, "group": 1, "period": 1,

          "category": "Nonmetal", "uses": ["Fuel", "Chemical feedstock"], "electron_configuration": "1s1"},

    "He": {"name": "Helium", "atomic_number": 2, "atomic_mass": 4.0026, "group": 18, "period": 1,

           "category": "Noble Gas", "uses": ["Balloons", "Cryogenics"], "electron_configuration": "1s2"},

    "Li": {"name": "Lithium", "atomic_number": 3, "atomic_mass": 6.94, "group": 1, "period": 2,

           "category": "Alkali Metal", "uses": ["Batteries"], "electron_configuration": "[He] 2s1"},

    "Be": {"name": "Beryllium", "atomic_number": 4, "atomic_mass": 9.0122, "group": 2, "period": 2,

           "category": "Alkaline Earth Metal", "uses": ["Aerospace alloys"], "electron_configuration": "[He] 2s2"},

    "B": {"name": "Boron", "atomic_number": 5, "atomic_mass": 10.81, "group": 13, "period": 2,

          "category": "Metalloid", "uses": ["Glass", "Detergents"], "electron_configuration": "[He] 2s2 2p1"},

    "C": {"name": "Carbon", "atomic_number": 6, "atomic_mass": 12.011, "group": 14, "period": 2,

          "category": "Nonmetal", "uses": ["Organic chemistry", "Materials"], "electron_configuration": "[He] 2s2 2p2"},

    "N": {"name": "Nitrogen", "atomic_number": 7, "atomic_mass": 14.007, "group": 15, "period": 2,

          "category": "Nonmetal", "uses": ["Fertilizers", "Industrial gas"], "electron_configuration": "[He] 2s2 2p3"},

    "O": {"name": "Oxygen", "atomic_number": 8, "atomic_mass": 15.999, "group": 16, "period": 2,

          "category": "Nonmetal", "uses": ["Respiration", "Combustion"], "electron_configuration": "[He] 2s2 2p4"},

    "F": {"name": "Fluorine", "atomic_number": 9, "atomic_mass": 18.998, "group": 17, "period": 2,

          "category": "Halogen", "uses": ["Toothpaste (fluoride)"], "electron_configuration": "[He] 2s2 2p5"},

    "Ne": {"name": "Neon", "atomic_number": 10, "atomic_mass": 20.1797, "group": 18, "period": 2,

           "category": "Noble Gas", "uses": ["Neon signs"], "electron_configuration": "[He] 2s2 2p6"},

    "Na": {"name": "Sodium", "atomic_number": 11, "atomic_mass": 22.989, "group": 1, "period": 3,

           "category": "Alkali Metal", "uses": ["Salt", "Chemicals"], "electron_configuration": "[Ne] 3s1"},

    "Mg": {"name": "Magnesium", "atomic_number": 12, "atomic_mass": 24.305, "group": 2, "period": 3,

           "category": "Alkaline Earth Metal", "uses": ["Alloys", "Electronics"], "electron_configuration": "[Ne] 3s2"},

    "Al": {"name": "Aluminium", "atomic_number": 13, "atomic_mass": 26.9815, "group": 13, "period": 3,

           "category": "Post-transition Metal", "uses": ["Packaging", "Aerospace"], "electron_configuration": "[Ne] 3s2 3p1"},

    "Si": {"name": "Silicon", "atomic_number": 14, "atomic_mass": 28.085, "group": 14, "period": 3,

           "category": "Metalloid", "uses": ["Semiconductors"], "electron_configuration": "[Ne] 3s2 3p2"},

    "P": {"name": "Phosphorus", "atomic_number": 15, "atomic_mass": 30.9738, "group": 15, "period": 3,

          "category": "Nonmetal", "uses": ["Fertilizers"], "electron_configuration": "[Ne] 3s2 3p3"},

    "S": {"name": "Sulfur", "atomic_number": 16, "atomic_mass": 32.06, "group": 16, "period": 3,

          "category": "Nonmetal", "uses": ["Sulfuric acid", "Fertilizers"], "electron_configuration": "[Ne] 3s2 3p4"},

    "Cl": {"name": "Chlorine", "atomic_number": 17, "atomic_mass": 35.45, "group": 17, "period": 3,

           "category": "Halogen", "uses": ["Water purification"], "electron_configuration": "[Ne] 3s2 3p5"},

    "Ar": {"name": "Argon", "atomic_number": 18, "atomic_mass": 39.948, "group": 18, "period": 3,

           "category": "Noble Gas", "uses": ["Lighting", "Welding"], "electron_configuration": "[Ne] 3s2 3p6"},

    "K": {"name": "Potassium", "atomic_number": 19, "atomic_mass": 39.0983, "group": 1, "period": 4,

          "category": "Alkali Metal", "uses": ["Fertilizers"], "electron_configuration": "[Ar] 4s1"},

    "Ca": {"name": "Calcium", "atomic_number": 20, "atomic_mass": 40.078, "group": 2, "period": 4,

           "category": "Alkaline Earth Metal", "uses": ["Bones", "Construction"], "electron_configuration": "[Ar] 4s2"}

}


def ensure_data_file():

    if not os.path.exists(DATA_FILE):

        with open(DATA_FILE, "w", encoding="utf-8") as f:

            json.dump(DEFAULT_ELEMENTS, f, indent=2)

        print(f"Created default {DATA_FILE} with first 20 elements. You can edit/expand it.")


# ----------------- Utility: Load Data -----------------

def load_elements():

    ensure_data_file()

    with open(DATA_FILE, "r", encoding="utf-8") as f:

        data = json.load(f)

    # Normalize: attach group/period if missing

    elements = {}

    for sym, info in data.items():

        elements[sym] = info

    return elements


# ----------------- Orbital Visualizer -----------------

def show_orbital_visual(element_data):

    """

    Very simple 3D shells visualization.

    Each shell (n) is drawn as a translucent sphere surface of radius proportional to n.

    Electron counts per shell approximated from electron_configuration (simple parse).

    """

    # parse approximate shells from electron_configuration string by counting numbers in brackets or 's','p','d'

    cfg = element_data.get("electron_configuration", "")

    # crude parsing to get principal quantum numbers occurrences like 1s2 2s2 2p6 etc.

    shells = []

    import re

    matches = re.findall(r'(\d)(?:[spdf])\d*', cfg)

    if matches:

        # get unique shell numbers and sort

        shells = sorted(set(int(m) for m in matches))

    else:

        # fallback: estimate shells from atomic number

        an = element_data.get("atomic_number", 1)

        # very rough heuristic: shell count = ceil(sqrt(Z)/2) — just for visualization

        shells = list(range(1, int(max(1, round((an**0.5)/2))) + 2))


    fig = plt.figure(figsize=(6,6))

    ax = fig.add_subplot(111, projection='3d')

    ax.set_facecolor("white")

    ax._axis3don = False


    # Draw concentric sphere surfaces for shells

    for n in shells:

        # create sphere

        u = np.linspace(0, 2 * np.pi, 60)

        v = np.linspace(0, np.pi, 30)

        r = 0.6 * n  # radius scale

        x = r * np.outer(np.cos(u), np.sin(v))

        y = r * np.outer(np.sin(u), np.sin(v))

        z = r * np.outer(np.ones_like(u), np.cos(v))

        ax.plot_surface(x, y, z, color=plt.cm.viridis(n/ max(1, len(shells))), alpha=0.15, linewidth=0)

        # put some electron points randomly distributed on shell

        np.random.seed(n*10)

        m = min(12, 4 + n*4)

        theta = np.random.uniform(0, 2*np.pi, m)

        phi = np.random.uniform(0, np.pi, m)

        ex = r * np.sin(phi) * np.cos(theta)

        ey = r * np.sin(phi) * np.sin(theta)

        ez = r * np.cos(phi)

        ax.scatter(ex, ey, ez, s=30, color=plt.cm.viridis(n / max(1, len(shells))))


    ax.set_title(f"Shells for {element_data.get('name', '')} ({element_data.get('symbol','')})", pad=20)

    max_r = 0.6 * (max(shells) + 1)

    ax.set_xlim(-max_r, max_r)

    ax.set_ylim(-max_r, max_r)

    ax.set_zlim(-max_r, max_r)

    plt.show()


# ----------------- GUI -----------------

class PeriodicTableApp:

    def __init__(self, root):

        self.root = root

        self.root.title("Interactive Periodic Table")

        self.elements = load_elements()  # key: symbol

        # create a mapping by atomic_number for quick lookup

        self.by_atomic = {info["atomic_number"]: sym for sym, info in self.elements.items() if "atomic_number" in info}

        self.buttons = {}  # symbol -> button

        self.build_ui()


    def build_ui(self):

        top = tk.Frame(self.root)

        top.pack(padx=8, pady=8, anchor="w")


        tk.Label(top, text="Search (symbol / name / atomic #):").pack(side="left")

        self.search_var = tk.StringVar()

        search_entry = tk.Entry(top, textvariable=self.search_var)

        search_entry.pack(side="left", padx=4)

        search_entry.bind("<Return>", lambda e: self.do_search())

        tk.Button(top, text="Search", command=self.do_search).pack(side="left", padx=4)

        tk.Button(top, text="Open elements.json", command=self.open_data_file).pack(side="left", padx=6)


        # Table frame

        table_frame = tk.Frame(self.root)

        table_frame.pack(padx=8, pady=8)


        # create empty grid

        for r in range(1, 8):  # periods 1..7

            for c in range(1, GRID_COLUMNS + 1):

                placeholder = tk.Frame(table_frame, width=46, height=36, bd=0)

                placeholder.grid(row=r, column=c, padx=1, pady=1)


        # Place element buttons based on group/period columns

        for sym, info in self.elements.items():

            group = info.get("group")

            period = info.get("period")

            if group is None or period is None:

                # skip elements without position (lanthanides/actinides not positioned)

                continue

            r = period

            c = group

            btn = tk.Button(table_frame, text=f"{sym}\n{info.get('atomic_number','')}",

                            width=6, height=3, command=partial(self.show_element_popup, sym))

            btn.grid(row=r, column=c, padx=1, pady=1)

            self.buttons[sym] = btn


        # Add legend / info area

        info_frame = tk.Frame(self.root)

        info_frame.pack(fill="x", padx=8, pady=6)

        tk.Label(info_frame, text="Click an element to view details and 3D shell visualization.", fg="gray").pack(side="left")


    def open_data_file(self):

        path = os.path.abspath(DATA_FILE)

        if os.path.exists(path):

            webbrowser.open(f"file://{path}")

        else:

            messagebox.showinfo("Info", f"{DATA_FILE} not found.")


    def do_search(self):

        q = self.search_var.get().strip().lower()

        if not q:

            return

        # search by symbol

        sym_match = None

        for sym, info in self.elements.items():

            if sym.lower() == q:

                sym_match = sym

                break

        if sym_match:

            self.flash_button(sym_match)

            self.show_element_popup(sym_match)

            return

        # search by name

        for sym, info in self.elements.items():

            if info.get("name", "").lower() == q:

                self.flash_button(sym)

                self.show_element_popup(sym)

                return

        # search by atomic number

        if q.isdigit():

            an = int(q)

            sym = self.by_atomic.get(an)

            if sym:

                self.flash_button(sym)

                self.show_element_popup(sym)

                return

        # partial name match

        for sym, info in self.elements.items():

            if q in info.get("name", "").lower():

                self.flash_button(sym)

                self.show_element_popup(sym)

                return

        messagebox.showinfo("Search", "No matching element found.")


    def flash_button(self, sym):

        btn = self.buttons.get(sym)

        if not btn:

            return

        orig = btn.cget("bg")

        def _flash():

            for _ in range(4):

                btn.config(bg="yellow")

                btn.update()

                self.root.after(200)

                btn.config(bg=orig)

                btn.update()

                self.root.after(200)

        threading.Thread(target=_flash, daemon=True).start()


    def show_element_popup(self, sym):

        info = self.elements.get(sym)

        if not info:

            messagebox.showerror("Error", "Element data not found.")

            return

        popup = tk.Toplevel(self.root)

        popup.title(f"{sym} - {info.get('name','')}")

        popup.geometry("420x320")


        left = tk.Frame(popup)

        left.pack(side="left", fill="both", expand=True, padx=8, pady=8)


        tk.Label(left, text=f"{info.get('name','')} ({sym})", font=("Arial", 16, "bold")).pack(anchor="w")

        tk.Label(left, text=f"Atomic Number: {info.get('atomic_number','')}", font=("Arial", 12)).pack(anchor="w")

        tk.Label(left, text=f"Atomic Mass: {info.get('atomic_mass','')}", font=("Arial", 12)).pack(anchor="w")

        tk.Label(left, text=f"Category: {info.get('category','')}", font=("Arial", 12)).pack(anchor="w")

        tk.Label(left, text=f"Electron Configuration: {info.get('electron_configuration','')}", font=("Arial", 11)).pack(anchor="w", pady=6)

        uses = info.get("uses", [])

        tk.Label(left, text="Uses / Applications:", font=("Arial", 12, "underline")).pack(anchor="w", pady=(6,0))

        uses_text = tk.Text(left, height=6, wrap="word")

        uses_text.pack(fill="both", expand=True)

        uses_text.insert("1.0", "\n".join(uses) if isinstance(uses, list) else str(uses))

        uses_text.config(state="disabled")


        # right side controls

        right = tk.Frame(popup)

        right.pack(side="right", fill="y", padx=6, pady=6)

        tk.Button(right, text="Visualize Shells (3D)", command=lambda: threading.Thread(target=show_orbital_visual, args=(info,), daemon=True).start()).pack(pady=6)

        tk.Button(right, text="Close", command=popup.destroy).pack(pady=6)


# ----------------- Run App -----------------

def main():

    root = tk.Tk()

    app = PeriodicTableApp(root)

    root.mainloop()


if __name__ == "__main__":

    main()


Smart Receipt Scanner & Expense Categorizer

import cv2

import pytesseract

import pandas as pd

import matplotlib.pyplot as plt

import re

import os


# Set tesseract path if needed (Windows users)

# pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"


# Predefined keywords for categorization

CATEGORY_KEYWORDS = {

    "Food": ["restaurant", "cafe", "pizza", "burger", "coffee", "food", "dine"],

    "Travel": ["uber", "ola", "taxi", "flight", "airlines", "train", "bus", "travel"],

    "Utilities": ["electricity", "water", "gas", "internet", "wifi", "bill", "utility"],

    "Shopping": ["mall", "store", "supermarket", "shopping", "market", "groceries"],

    "Other": []

}


def preprocess_image(image_path):

    """Convert image to grayscale and apply threshold for better OCR results."""

    img = cv2.imread(image_path)

    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    gray = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1]

    return gray


def extract_text(image):

    """Extract text from the image using pytesseract."""

    return pytesseract.image_to_string(image)


def categorize_expense(text):

    """Categorize based on keywords found in the text."""

    text_lower = text.lower()

    for category, keywords in CATEGORY_KEYWORDS.items():

        for word in keywords:

            if word in text_lower:

                return category

    return "Other"


def extract_amount(text):

    """Find the largest number in text assuming it's the total amount."""

    amounts = re.findall(r"\d+\.\d{2}", text)

    if amounts:

        return max(map(float, amounts))

    return 0.0


def process_receipts(folder_path):

    """Process all receipt images in a folder."""

    records = []

    for file in os.listdir(folder_path):

        if file.lower().endswith((".png", ".jpg", ".jpeg")):

            img_path = os.path.join(folder_path, file)

            pre_img = preprocess_image(img_path)

            text = extract_text(pre_img)

            category = categorize_expense(text)

            amount = extract_amount(text)

            records.append({"File": file, "Category": category, "Amount": amount, "Text": text})

    return pd.DataFrame(records)


def plot_expenses(df):

    """Plot expenses by category."""

    category_totals = df.groupby("Category")["Amount"].sum()

    category_totals.plot(kind="bar", color="skyblue")

    plt.title("Expenses by Category")

    plt.xlabel("Category")

    plt.ylabel("Total Amount")

    plt.xticks(rotation=45)

    plt.tight_layout()

    plt.show()


if __name__ == "__main__":

    folder = "receipts"  # Folder containing receipt images

    df = process_receipts(folder)

    print(df)

    plot_expenses(df)