Source code for pyphyschemtools.orgachem

import csv
import random
import threading
import time
import requests
import difflib 
from rdkit import Chem
from rdkit.Chem import AllChem 
from rdkit.Chem.Draw import rdMolDraw2D
from rdkit import rdBase
from IPython.display import display, SVG, clear_output, HTML
import ipywidgets as widgets
from pathlib import Path

rdBase.DisableLog('rdApp.error') 
rdBase.DisableLog('rdApp.warning')

try:
    import py3Dmol
    HAS_3D = True
except ImportError:
    HAS_3D = False


TRADUCTIONS = {
    'fr': {
        'micro': 'Micro', 'macro': 'Macro',
        'iupac_btn': 'Nom IUPAC', 'iupac_result': 'Nom officiel (PubChem) :',
        'nomenclature': 'Nomenclature', 'notes': 'Notes',
        'smiles_label': 'SMILES :',
        'find_function': 'Quelle est la famille chimique en <span style="color:#ff6b6b">ROSE</span> ?',
        'score': 'Score', 'streak': 'Série', 'timer': 'Temps',
        'verify': 'Vérifier', 'next': 'Suivant',
        'bravo': 'Excellent !', 'wrong': 'Dommage !',
        'structural_motif': 'Motif Structural :',
        'name': 'Nom', 'family': 'Famille',
        'prefix': 'Préfixe', 'suffix': 'Suffixe', 'note_label': 'Note',
        'expected': 'Attendu :',
        'explorer_tab': '🔍 Explorateur',
        'quiz_struct_tab': '🔬 Quiz Structure',
        'quiz_nom_tab': '📝 Quiz Nomenclature',
        'searching': 'Recherche...', 'not_found': 'Inconnu',
        'time_up': '⏰ TEMPS ÉCOULÉ !',
        'correct_answer_was': 'La réponse était :',
        'game_over': 'PERDU ! Plus de vies.',
        'no_match': 'Aucune fonction détectée.',
        'timer_opt': '⏱️ Chrono',
        'survival_opt': '❤️ Survie',
        'none_label': '(Aucun)',
        '3d_btn': 'Vue 3D',
        '3d_warning': '⚠️ <b>Note :</b> Structure optimisée par champ de force (UFF).<br>Ceci est une approximation théorique, pas une structure expérimentale.',
        'compare_err': 'Comparaison :',
        'you_thought': 'Vous avez dit :',
        'it_was': 'C\'était :',
        'select_placeholder': '--- Tapez ou Choisissez ---' 
    },
    'en': {
        'micro': 'Micro', 'macro': 'Macro',
        'iupac_btn': 'IUPAC Name', 'iupac_result': 'Official Name (PubChem):',
        'nomenclature': 'Nomenclature', 'notes': 'Notes',
        'smiles_label': 'SMILES:',
        'find_function': 'What is the chemical family in <span style="color:#ff6b6b">PINK</span>?',
        'score': 'Score', 'streak': 'Streak', 'timer': 'Time',
        'verify': 'Check', 'next': 'Next',
        'bravo': 'Excellent!', 'wrong': 'Too bad!',
        'structural_motif': 'Structural Motif:',
        'name': 'Name', 'family': 'Family',
        'prefix': 'Prefix', 'suffix': 'Suffix', 'note_label': 'Note',
        'expected': 'Expected:',
        'explorer_tab': '🔍 Explorer',
        'quiz_struct_tab': '🔬 Structure Quiz',
        'quiz_nom_tab': '📝 Nomenclature Quiz',
        'searching': 'Searching...', 'not_found': 'Unknown',
        'time_up': '⏰ TIME IS UP!',
        'correct_answer_was': 'The answer was:',
        'game_over': 'GAME OVER! No lives left.',
        'no_match': 'No function detected.',
        'timer_opt': '⏱️ Timer',
        'survival_opt': '❤️ Survival',
        'none_label': '(None)',
        '3d_btn': '3D View',
        '3d_warning': '⚠️ <b>Note:</b> Structure optimized by force field (UFF).<br>This is a theoretical approximation, not an experimental structure.',
        'compare_err': 'Comparison:',
        'you_thought': 'You said:',
        'it_was': 'It was:',
        'select_placeholder': '--- Type or Select ---'
    }
}


[docs] class FunctionalGroupExplorer: """ A comprehensive educational tool for organic chemistry within the pyphyschemtools suite. This class provides an interactive interface for: - Exploring functional groups using SMARTS patterns. - Retrieving IUPAC nomenclature via PubChem API. - Visualizing molecules in 2D (with highlighting) and 3D. - Training via a Quiz system with fuzzy logic validation and survival mechanics. """ def __init__(self): """ Initialize the application with language support and data loading. Args: lang (str): 'FR' for French or 'EN' for English. Defaults to 'FR'. """ self.lang = 'fr' self.data_funcs = [] self.data_mols = [] self.cache_iupac = {} self.map_nom_id = {} self.last_category = 'Micro' self.base_path = Path(__file__).parent self.data_dir = self.base_path / "resources" / "orgachem" self.score = 0 self.streak = 0 self.lives = 3 self.stop_timer_flag = False self.current_q_struct = None self.current_mol_struct_data = None self.current_svg_struct = None self.current_q_nom = None self.current_svg_nom = None self._charger_donnees() self.setup_ui()
[docs] def T(self, key): return TRADUCTIONS[self.lang].get(key, key)
[docs] def detecter_delimiteur(self, fichier): try: with open(fichier, 'r', encoding='utf-8-sig') as f: return ';' if ';' in f.readline() else ',' except: return ';'
def _charger_donnees(self): exemples = {} path_examples = self.data_dir / "Functions_Examples.csv" path_funcs = self.data_dir / "Functions.csv" path_mols = self.data_dir / "Molecules.csv" try: delim = self.detecter_delimiteur(path_examples) with open(path_examples, mode='r', encoding='utf-8-sig') as f: reader = csv.DictReader(f, delimiter=delim) if reader.fieldnames: reader.fieldnames = [n.strip() for n in reader.fieldnames] for row in reader: if row.get('id'): exemples[row['id']] = row.get('smiles') except: pass self.data_funcs = [] try: delim = self.detecter_delimiteur(path_funcs) with open(path_funcs, mode='r', encoding='utf-8-sig') as f: reader = csv.DictReader(f, delimiter=delim) if reader.fieldnames: reader.fieldnames = [n.strip() for n in reader.fieldnames] for row in reader: if not row.get('smarts'): continue smarts = Chem.MolFromSmarts(row['smarts']) if smarts: try: prio = int(row.get('Priorite', 999)) except: prio = 999 item = { 'id': row.get('id', 'Inconnu'), 'nom_fr': row.get('nom_fr', '').strip() or row.get('id'), 'smarts': smarts, 'macro_micro': row.get('Macro-Micro', 'Micro'), 'priorite': prio, 'prefixe': row.get('Prefixe', '-'), 'suffixe': row.get('Suffixe', '-'), 'famille': row.get('Famille', 'Inconnue'), 'commentaires_fr': row.get('commentaires_fr', ''), 'commentaires_en': row.get('commentaires_en', ''), 'smiles_exemple': exemples.get(row.get('id', '')) } self.data_funcs.append(item) self.data_funcs.sort(key=lambda x: x['priorite']) except Exception as e: print(f"⚠️ Note: Fichiers CSV non trouvés ou erreur. Mode démo activé.") if not self.data_funcs: self.data_funcs = [ {'id': 'AcideCarbo', 'nom_fr': 'Acide Carboxylique', 'smarts': Chem.MolFromSmarts('[CX3](=O)[OX2H1]'), 'macro_micro': 'Micro', 'priorite': 1, 'prefixe': 'carboxy', 'suffixe': 'oïque', 'famille': 'Acides', 'commentaires_fr': 'COOH', 'commentaires_en': 'COOH'}, {'id': 'Cetone', 'nom_fr': 'Cétone', 'smarts': Chem.MolFromSmarts('[#6][CX3](=O)[#6]'), 'macro_micro': 'Micro', 'priorite': 7, 'prefixe': 'oxo', 'suffixe': 'one', 'famille': 'Composés carbonylés', 'commentaires_fr': 'C=O lié à 2 C', 'commentaires_en': 'C=O bonded to 2 C'}, {'id': 'Alcool', 'nom_fr': 'Alcool', 'smarts': Chem.MolFromSmarts('[#6][OX2H]'), 'macro_micro': 'Micro', 'priorite': 9, 'prefixe': 'hydroxy', 'suffixe': 'ol', 'famille': 'Composés oxygénés', 'commentaires_fr': 'Groupe -OH', 'commentaires_en': '-OH group'}, ] self.data_funcs.sort(key=lambda x: x['priorite']) self.data_mols = [] try: delim = self.detecter_delimiteur(path_mols) with open(path_mols, mode='r', encoding='utf-8-sig') as f: reader = csv.DictReader(f, delimiter=delim) if reader.fieldnames: reader.fieldnames = [n.strip() for n in reader.fieldnames] for row in reader: if row.get('smiles'): n_fr = row.get('nom_fr', row.get('nom', 'Inconnu')) self.data_mols.append({ 'fr': n_fr, 'en': row.get('nom_en', n_fr), 'smiles': row['smiles'] }) except: pass if not self.data_mols: self.data_mols = [ {'fr':'Paracétamol', 'en':'Paracetamol', 'smiles':'CC(=O)NC1=CC=C(O)C=C1'}, {'fr':'Acétone', 'en':'Acetone', 'smiles':'CC(=O)C'}, {'fr':'Acide Acétique', 'en':'Acetic Acid', 'smiles':'CC(=O)O'}, {'fr':'Éthanol', 'en':'Ethanol', 'smiles':'CCO'} ]
[docs] def fuzzy_match(self, user_input, expected): """Retourne Vrai si la réponse est ressemblante à > 80%""" if expected in ['-', '']: return user_input == expected seq = difflib.SequenceMatcher(None, user_input, expected) return seq.ratio() > 0.8
[docs] def mol_to_svg(self, mol, highlightAtoms=None, highlightBonds=None, size=(300, 200)): d2d = rdMolDraw2D.MolDraw2DSVG(size[0], size[1]) opts = d2d.drawOptions() opts.addStereoAnnotation = True try: mol.UpdatePropertyCache(strict=False) except: pass try: if mol.GetNumConformers() == 0: Chem.Compute2DCoords(mol) except: pass if highlightAtoms: col = (1.0, 0.65, 0.65) # Rose Saumon hc_a = {x: col for x in highlightAtoms} hc_b = {x: col for x in highlightBonds} if highlightBonds else {} d2d.DrawMolecule(mol, highlightAtoms=highlightAtoms, highlightBonds=highlightBonds, highlightAtomColors=hc_a, highlightBondColors=hc_b) else: d2d.DrawMolecule(mol) d2d.FinishDrawing() return d2d.GetDrawingText()
[docs] def show_3d_mol(self, smiles): if not HAS_3D: return widgets.HTML("<b>Py3Dmol not installed</b>") mol = Chem.MolFromSmiles(smiles) if not mol: return widgets.HTML("Invalid SMILES") mol = Chem.AddHs(mol) try: res = AllChem.EmbedMolecule(mol, randomSeed=42) if res == -1: res = AllChem.EmbedMolecule(mol, randomSeed=42, useRandomCoords=True) if res != -1: AllChem.UFFOptimizeMolecule(mol, maxIters=200) except Exception as e: print(f"Erreur d'optimisation 3D : {e}") display(widgets.HTML( f"<div style='background-color:#fff3cd; color:#856404; padding:10px; " f"border:1px solid #ffeeba; border-radius:5px; margin-bottom:10px; font-size:0.9em;'>" f"{self.T('3d_warning')}</div>" )) mblock = Chem.MolToMolBlock(mol) view = py3Dmol.view(width=400, height=300) view.addModel(mblock, 'mol') view.setStyle({'stick': {'radius': 0.15}, 'sphere': {'scale': 0.25}}) # Style amélioré view.zoomTo() return view
[docs] def get_match_svg(self, smiles, query): mol = Chem.MolFromSmiles(smiles) if not mol: return None matches = mol.GetSubstructMatches(query['smarts']) if not matches: return None match = random.choice(matches) atoms = list(match) bonds = [] for b in query['smarts'].GetBonds(): try: aid1, aid2 = match[b.GetBeginAtomIdx()], match[b.GetEndAtomIdx()] mb = mol.GetBondBetweenAtoms(aid1, aid2) if mb: bonds.append(mb.GetIdx()) except: pass return self.mol_to_svg(mol, atoms, bonds)
[docs] def setup_ui(self): self.w_lang = widgets.ToggleButtons( options=[('🇫🇷 Français', 'fr'), ('🇬🇧 English', 'en')], value='fr', button_style='', style={'button_width': '150px'} ) self.w_lang.observe(self.on_lang_change, names='value') self.ex_smiles = widgets.Text(value='NC(CC1=CC=C(O)C=C1)C(=O)O', description='SMILES:', layout={'width':'400px'}) self.ex_out = widgets.Output() self.ex_btn_micro = widgets.Button(description='Micro', button_style='danger') self.ex_btn_macro = widgets.Button(description='Macro', button_style='success') self.ex_btn_iupac = widgets.Button(description='IUPAC', button_style='info', icon='search') self.ex_btn_3d = widgets.Button(description='3D', button_style='warning', icon='cube') self.ex_toggle_nom = widgets.ToggleButton(value=True, description='Nomenclature', button_style='primary', icon='tags') self.ex_toggle_notes = widgets.ToggleButton(value=True, description='Notes', button_style='warning', icon='edit') self.ex_btn_micro.on_click(lambda b: self.update_explorer('Micro')) self.ex_btn_macro.on_click(lambda b: self.update_explorer('Macro')) self.ex_btn_iupac.on_click(self.run_iupac) self.ex_btn_3d.on_click(self.run_3d) self.ex_toggle_nom.observe(lambda c: self.update_explorer(self.last_category), names='value') self.ex_toggle_notes.observe(lambda c: self.update_explorer(self.last_category), names='value') ui_explorateur = widgets.VBox([ widgets.HBox([self.ex_smiles, self.ex_btn_micro, self.ex_btn_macro, self.ex_btn_iupac, self.ex_btn_3d]), widgets.HBox([self.ex_toggle_nom, self.ex_toggle_notes]), self.ex_out ]) self.qs_out = widgets.Output() self.qs_drop = widgets.Combobox( placeholder=self.T('select_placeholder'), options=[], ensure_option=True, layout={'width': '300px'} ) self.qs_drop.on_submit(self.check_struct) self.qs_btn_check = widgets.Button(description='Vérifier', button_style='primary', icon='check') self.qs_btn_next = widgets.Button(description='Suivant', icon='arrow-right') self.qs_check_timer = widgets.Checkbox(value=False, description='⏱️ Chrono', indent=False) self.qs_check_survival = widgets.Checkbox(value=False, description='❤️ Survie', indent=False) self.qs_lbl_lives = widgets.HTML(value=self.get_lives_html()) self.qs_lbl_total_score = widgets.HTML(value="<b>🏆 Score : 0</b>", layout={'margin': '0 0 0 20px'}) self.qs_bar_score = widgets.IntProgress(value=0, min=0, max=100, description='', bar_style='success', layout={'width':'150px'}) self.qs_bar_timer = widgets.IntProgress(value=100, min=0, max=100, description='⏱️', bar_style='info') self.qs_btn_check.on_click(self.check_struct) self.qs_btn_next.on_click(self.next_struct) self.qs_check_survival.observe(self.on_survival_change, names='value') ui_struct = widgets.VBox([ widgets.HBox([self.qs_bar_score, self.qs_lbl_total_score, self.qs_lbl_lives], layout={'justify_content': 'center', 'align_items': 'center'}), widgets.HBox([self.qs_bar_timer, self.qs_check_timer, self.qs_check_survival], layout={'justify_content': 'center'}), self.qs_out, widgets.HTML("<hr>"), widgets.HBox([self.qs_drop, self.qs_btn_check, self.qs_btn_next], layout={'justify_content': 'center'}) ]) self.qn_out = widgets.Output() self.qn_in_pref = widgets.Text() self.qn_label_pref = widgets.Label(value='Préfixe :', layout={'width': '80px'}) self.qn_in_suff = widgets.Text() self.qn_label_suff = widgets.Label(value='Suffixe :', layout={'width': '80px'}) self.qn_in_pref.on_submit(self.check_nom) self.qn_in_suff.on_submit(self.check_nom) self.qn_box_pref = widgets.HBox([self.qn_label_pref, self.qn_in_pref], layout={'justify_content': 'center'}) self.qn_box_suff = widgets.HBox([self.qn_label_suff, self.qn_in_suff], layout={'justify_content': 'center'}) self.qn_btn_check = widgets.Button(description='Vérifier', button_style='primary', icon='check') self.qn_btn_next = widgets.Button(description='Suivant', icon='arrow-right') self.qn_bar_score = widgets.IntProgress(value=0, min=0, max=100, description='Score:', bar_style='success') self.qn_btn_check.on_click(self.check_nom) self.qn_btn_next.on_click(self.next_nom) ui_nom = widgets.VBox([ self.qn_bar_score, self.qn_out, widgets.HTML("<hr>"), self.qn_box_pref, self.qn_box_suff, widgets.HBox([self.qn_btn_check, self.qn_btn_next], layout={'justify_content': 'center', 'margin_top': '10px'}) ]) self.tabs = widgets.Tab(children=[ui_explorateur, ui_struct, ui_nom]) self.update_labels() display(widgets.HTML("<style>.widget-label { font-weight: bold; }</style>")) display(widgets.VBox([self.w_lang, self.tabs])) self.next_struct() self.next_nom()
[docs] def update_labels(self): # Titres self.tabs.set_title(0, self.T('explorer_tab')) self.tabs.set_title(1, self.T('quiz_struct_tab')) self.tabs.set_title(2, self.T('quiz_nom_tab')) self.ex_btn_micro.description = self.T('micro') self.ex_btn_macro.description = self.T('macro') self.ex_btn_iupac.description = self.T('iupac_btn') self.ex_btn_3d.description = self.T('3d_btn') self.ex_smiles.description = self.T('smiles_label') self.ex_toggle_nom.description = self.T('nomenclature') self.ex_toggle_notes.description = self.T('notes') self.qs_btn_check.description = self.T('verify') self.qs_btn_next.description = self.T('next') self.qs_drop.placeholder = self.T('select_placeholder') self.qs_check_timer.description = self.T('timer_opt') self.qs_check_survival.description = self.T('survival_opt') self.qn_label_pref.value = self.T('prefix') + " :" self.qn_label_suff.value = self.T('suffix') + " :" self.qn_btn_check.description = self.T('verify') self.qn_btn_next.description = self.T('next') self.qn_bar_score.description = self.T('score') if self.qn_in_pref.disabled: self.qn_in_pref.placeholder = self.T('none_label') if self.qn_in_suff.disabled: self.qn_in_suff.placeholder = self.T('none_label')
[docs] def on_lang_change(self, change): self.lang = change['new'] self.update_labels() self.update_explorer(self.last_category) if self.current_q_struct: self.update_combobox_options() self.afficher_quiz_struct() self.afficher_quiz_nom()
[docs] def get_lives_html(self): if not self.qs_check_survival.value: return "" hearts = "❤️" * self.lives + "🖤" * (3 - self.lives) return f"<span style='font-size:1.5em; vertical-align:middle; margin-left:10px'>{hearts}</span>"
[docs] def on_survival_change(self, change): if change['new']: self.lives = 3 self.score = 0 self.qs_bar_score.value = 0 self.qs_lbl_total_score.value = f"<b>🏆 {self.T('score')} : 0</b>" self.qs_lbl_lives.value = self.get_lives_html()
[docs] def start_timer_thread(self, progress_bar, callback_timeout): self.stop_timer_flag = True time.sleep(0.1) self.stop_timer_flag = False def _timer_run(): progress_bar.value = 100 progress_bar.bar_style = 'info' while progress_bar.value > 0: if self.stop_timer_flag: return time.sleep(0.15) progress_bar.value -= 1 if progress_bar.value < 30: progress_bar.bar_style = 'warning' if progress_bar.value < 10: progress_bar.bar_style = 'danger' if not self.stop_timer_flag: callback_timeout() t = threading.Thread(target=_timer_run) t.start()
[docs] def run_iupac(self, b): smi = self.ex_smiles.value with self.ex_out: clear_output(wait=True) if smi in self.cache_iupac: nom = self.cache_iupac[smi] else: print(self.T('searching')) try: url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/smiles/{smi}/property/IUPACName/TXT" r = requests.get(url, timeout=3) nom = r.text.strip() if r.status_code == 200 else self.T('not_found') self.cache_iupac[smi] = nom except: nom = self.T('not_found') display(widgets.HTML(f"<div style='background:#e3f2fd;padding:10px;border-radius:8px;margin-bottom:10px;'><b>{self.T('iupac_result')}</b><br>{nom}</div>")) mol = Chem.MolFromSmiles(smi) if mol: display(SVG(self.mol_to_svg(mol, size=(300, 200))))
[docs] def run_3d(self, b): smi = self.ex_smiles.value with self.ex_out: clear_output(wait=True) print(self.T('searching')) view = self.show_3d_mol(smi) if isinstance(view, widgets.HTML): display(view) elif view: view.show()
[docs] def update_explorer(self, cat): self.last_category = cat with self.ex_out: clear_output(wait=True) smi = self.ex_smiles.value mol = Chem.MolFromSmiles(smi) if not mol: print("SMILES Invalide"); return found = False atomes_utilises = set() for q in self.data_funcs: if q['macro_micro'] != cat: continue matches = mol.GetSubstructMatches(q['smarts']) valid_matches = [] for m in matches: if not any(idx in atomes_utilises for idx in m): valid_matches.append(m) atomes_utilises.update(m) if valid_matches: found = True atoms_all = set() bonds_all = set() for m in valid_matches: atoms_all.update(m) for b in q['smarts'].GetBonds(): try: a1, a2 = m[b.GetBeginAtomIdx()], m[b.GetEndAtomIdx()] mb = mol.GetBondBetweenAtoms(a1, a2) if mb: bonds_all.add(mb.GetIdx()) except: pass nom_aff = q['nom_fr'] if self.lang == 'fr' else q['id'] display(widgets.HTML(f"<h4>🔹 {nom_aff} </h4>")) display(SVG(self.mol_to_svg(mol, list(atoms_all), list(bonds_all), size=(300, 200)))) info_html = "" if self.ex_toggle_nom.value: info_html += f"<div style='background:#f0f4f8;padding:5px;margin:5px;border-left:4px solid #2196F3;font-size:0.9em'><b>{self.T('prefix')}:</b> {q['prefixe']} | <b>{self.T('suffix')}:</b> {q['suffixe']}</div>" if self.ex_toggle_notes.value: comm = q['commentaires_fr'] if self.lang == 'fr' else (q['commentaires_en'] or q['commentaires_fr']) info_html += f"<div style='background:#fff3e0;padding:5px;margin:5px;border-left:4px solid #ff9800;font-size:0.9em'><b>{self.T('note_label')}:</b> {comm}</div>" if info_html: display(widgets.HTML(info_html)) if not found: print(self.T('no_match'))
[docs] def update_combobox_options(self): """Mise à jour des options de la Combobox et du dictionnaire de correspondance""" self.map_nom_id = {} opts = [] for q in self.data_funcs: nom = f"{q['nom_fr']} ({q['id']})" if self.lang == 'fr' else q['id'] self.map_nom_id[nom] = q['id'] opts.append(nom) self.qs_drop.options = sorted(opts)
[docs] def afficher_quiz_struct(self): if not self.current_q_struct: return with self.qs_out: clear_output(wait=True) nom_mol = self.current_mol_struct_data['fr'] if self.lang == 'fr' else self.current_mol_struct_data['en'] display(widgets.HTML(f""" <div style='background:#f1f8e9; padding:15px; border-radius:10px; border:1px solid #8bc34a; text-align:center;'> <h3 style='margin:0'>{self.T('find_function')}</h3> <small>{nom_mol}</small> </div>""")) if self.current_svg_struct: display(SVG(self.current_svg_struct))
[docs] def next_struct(self, b=None): if self.qs_check_survival.value and self.lives <= 0: with self.qs_out: clear_output(wait=True) # Affichage du score final en grand display(widgets.HTML(f"<h1 style='color:red;text-align:center'>☠️ {self.T('game_over')} ☠️</h1>")) display(widgets.HTML(f"<h3 style='text-align:center'>Score Final : {self.score}</h3>")) self.qs_btn_check.disabled = True return self.stop_timer_flag = True self.qs_btn_check.disabled = False self.qs_btn_check.button_style = 'primary' self.qs_btn_check.description = self.T('verify') self.qs_drop.value = '' if not self.data_mols: return mol_data = random.choice(self.data_mols) mol = Chem.MolFromSmiles(mol_data['smiles']) atomes_utilises = set() candidates = [] for q in self.data_funcs: matches = mol.GetSubstructMatches(q['smarts']) for m in matches: if not any(idx in atomes_utilises for idx in m): candidates.append((q, m)) atomes_utilises.update(m) if not candidates: self.next_struct() return q, match = random.choice(candidates) bonds = [] for b in q['smarts'].GetBonds(): try: aid1, aid2 = match[b.GetBeginAtomIdx()], match[b.GetEndAtomIdx()] mb = mol.GetBondBetweenAtoms(aid1, aid2) if mb: bonds.append(mb.GetIdx()) except: pass svg = self.mol_to_svg(mol, list(match), bonds) self.current_q_struct = q self.current_mol_struct_data = mol_data self.current_svg_struct = svg self.update_combobox_options() self.afficher_quiz_struct() if self.qs_check_timer.value: self.start_timer_thread(self.qs_bar_timer, self.timeout_struct) else: self.qs_bar_timer.value = 100 self.qs_bar_timer.bar_style = 'success'
[docs] def timeout_struct(self): self.qs_btn_check.disabled = True if self.qs_check_survival.value: self.lives -= 1 self.qs_lbl_lives.value = self.get_lives_html() with self.qs_out: display(widgets.HTML(f"<div style='color:white;background:#d32f2f;padding:10px;text-align:center;border-radius:5px;'><b>{self.T('time_up')}</b><br>{self.T('game_over')}</div>"))
[docs] def check_struct(self, b): if self.qs_btn_check.disabled: return self.qs_btn_check.disabled = True self.stop_timer_flag = True user_text = self.qs_drop.value user_id = self.map_nom_id.get(user_text) if not user_id: self.qs_btn_check.disabled = False return correct_id = self.current_q_struct['id'] is_correct = (user_id == correct_id) with self.qs_out: if is_correct: self.score += 10 self.qs_btn_check.button_style = 'success' self.qs_btn_check.description = self.T('bravo') display(widgets.HTML(f"<div style='font-size:24px;color:#2e7d32;text-align:center;margin-top:10px;'>✅ {self.T('bravo')}</div>")) else: self.qs_btn_check.button_style = 'danger' self.qs_btn_check.description = self.T('wrong') if self.qs_check_survival.value: self.lives -= 1 self.qs_lbl_lives.value = self.get_lives_html() vrai_nom = self.current_q_struct['nom_fr'] if self.lang == 'fr' else self.current_q_struct['id'] wrong_item = next((item for item in self.data_funcs if item['id'] == user_id), None) wrong_svg = None wrong_name = "Inconnu" if wrong_item: wrong_name = wrong_item['nom_fr'] if self.lang == 'fr' else wrong_item['id'] if wrong_item.get('smarts'): try: mol_wrong = Chem.Mol(wrong_item['smarts']) mol_wrong.UpdatePropertyCache(strict=False) wrong_svg = self.mol_to_svg(mol_wrong, size=(150, 150)) except Exception as e: pass html_compare = f""" <div style='font-size:18px;color:#c62828;text-align:center;margin-top:10px;background:#ffcdd2;padding:10px;border-radius:5px;'> ❌ {self.T('wrong')} </div> <br> <div style="display: flex; justify-content: center; align-items: center; gap: 20px;"> <div style="text-align:center; border: 2px solid #e57373; padding: 5px; border-radius: 8px;"> <div style="font-weight:bold; color: #c62828;">{self.T('you_thought')}</div> <div>{wrong_name}</div> {wrong_svg if wrong_svg else ""} </div> <div style="font-size: 2em;">👉</div> <div style="text-align:center; border: 2px solid #81c784; padding: 5px; border-radius: 8px; min-width: 150px;"> <div style="font-weight:bold; color: #2e7d32;">{self.T('it_was')}</div> <div style="font-size: 1.2em; margin: 10px;">{vrai_nom}</div> </div> </div> """ display(widgets.HTML(html_compare)) self.qs_bar_score.value = min(self.score, 100) self.qs_lbl_total_score.value = f"<b>🏆 {self.T('score')} : {self.score}</b>"
[docs] def afficher_quiz_nom(self): if not self.current_q_nom: return with self.qn_out: clear_output(wait=True) nom_f = self.current_q_nom['nom_fr'] if self.lang == 'fr' else self.current_q_nom['id'] display(widgets.HTML(f""" <div style='background:#e3f2fd; padding:15px; border-radius:10px; border:2px solid #2196F3; text-align:center; width: 60%; margin: auto;'> <h3 style='margin:0; color:#1565c0'>{self.T('name')} : <span style='color:#e91e63'>{nom_f}</span></h3> <small>{self.T('family')} : {self.current_q_nom['famille']}</small> </div> <br>""")) if self.current_svg_nom: display(SVG(self.current_svg_nom))
[docs] def next_nom(self, b=None): self.qn_btn_check.disabled = False self.qn_btn_check.button_style = 'primary' self.qn_btn_check.description = self.T('verify') self.qn_in_pref.value = '' self.qn_in_suff.value = '' self.qn_in_pref.placeholder = '' self.qn_in_suff.placeholder = '' self.qn_in_pref.disabled = False self.qn_in_suff.disabled = False self.current_q_nom = random.choice(self.data_funcs) p_val = self.current_q_nom['prefixe'].strip() s_val = self.current_q_nom['suffixe'].strip() if p_val in ['-', '']: self.qn_box_pref.layout.display = 'none' else: self.qn_box_pref.layout.display = 'flex' if s_val in ['-', '']: self.qn_box_suff.layout.display = 'none' else: self.qn_box_suff.layout.display = 'flex' svg = None if self.current_q_nom.get('smiles_exemple'): svg = self.get_match_svg(self.current_q_nom['smiles_exemple'], self.current_q_nom) if not svg: svg = self.mol_to_svg(Chem.Mol(self.current_q_nom['smarts']), size=(200,150)) self.current_svg_nom = svg self.afficher_quiz_nom()
[docs] def check_nom(self, b): if self.qn_btn_check.disabled: return self.qn_btn_check.disabled = True p_att = self.current_q_nom['prefixe'].strip().lower() s_att = self.current_q_nom['suffixe'].strip().lower() p_usr = self.qn_in_pref.value.strip().lower() s_usr = self.qn_in_suff.value.strip().lower() is_pref_hidden = (self.qn_box_pref.layout.display == 'none') is_suff_hidden = (self.qn_box_suff.layout.display == 'none') ok_p = True if not is_pref_hidden: ok_p = self.fuzzy_match(p_usr, p_att) ok_s = True if not is_suff_hidden: ok_s = self.fuzzy_match(s_usr, s_att) res_html = "<div style='background:#f5f5f5;padding:15px;border-radius:10px;margin-top:15px;border:1px solid #ccc;'>" if not is_pref_hidden: if ok_p: msg = f"✅ <b>{self.T('prefix')}:</b> Correct (+5)<br>" if p_usr != p_att: msg = f"✅ <b>{self.T('prefix')}:</b> Correct (<i>{p_att}</i>) (+5)<br>" res_html += msg else: res_html += f"❌ <b>{self.T('prefix')}:</b> {self.T('expected')} <b>{p_att}</b><br>" if not is_suff_hidden: if ok_s: msg = f"✅ <b>{self.T('suffix')}:</b> Correct (+5)<br>" if s_usr != s_att: msg = f"✅ <b>{self.T('suffix')}:</b> Correct (<i>{s_att}</i>) (+5)<br>" res_html += msg else: res_html += f"❌ <b>{self.T('suffix')}:</b> {self.T('expected')} <b>{s_att}</b>" res_html += "</div>" pts = 0 if not is_pref_hidden and ok_p: pts += 5 if not is_suff_hidden and ok_s: pts += 5 self.qn_btn_check.button_style = 'success' if (ok_p and ok_s) else 'warning' with self.qn_out: display(widgets.HTML(res_html)) self.qn_bar_score.value = min(self.qn_bar_score.value + pts, 100)