Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Some checks failed
CI / check (push) Has been cancelled

Capture uncommitted solver robustness work (regularization, domain errors, linear solver lifecycle, tube DP/MSH), web workbench updates, and synced BMAD skills across IDE agent folders before starting BPHX pressure-drop.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-19 16:35:31 +02:00
parent 88620790d6
commit 5bd180b5b8
1363 changed files with 101041 additions and 58547 deletions

View File

@@ -0,0 +1,290 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "390509bb",
"metadata": {},
"source": [
"# 🌡️ Entropyk — Simulateur interactif\n",
"\n",
"Configurez les paramètres du cycle frigorifique R410A via les sliders, puis cliquez **▶ Simuler**."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "164510d5",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"✅ Prêt\n"
]
}
],
"source": [
"import json, subprocess, tempfile, os\n",
"import ipywidgets as widgets\n",
"import matplotlib.pyplot as plt\n",
"import matplotlib.patches as mpatches\n",
"from IPython.display import display, clear_output\n",
"\n",
"CLI = r\"C:\\Users\\serameza\\impact\\dev\\Entropyk-main\\entropyk\\target\\debug\\entropyk-cli.exe\"\n",
"BASE_CONFIG = r\"C:\\Users\\serameza\\impact\\dev\\Entropyk-main\\entropyk\\crates\\cli\\examples\\chiller_r410a_full_physics.json\"\n",
"print('✅ Prêt')"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "c55d1ff0",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "36f6e74bf5a64e9f8a4972460fbc2b00",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"VBox(children=(HTML(value='<h3>⚙️ Paramètres du cycle</h3>'), FloatSlider(value=50.0, description='T condensat…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# ── Widgets de saisie ────────────────────────────────────────────────────────\n",
"style = {'description_width': '200px'}\n",
"layout = widgets.Layout(width='500px')\n",
"\n",
"w_t_cond = widgets.FloatSlider(value=50, min=30, max=70, step=1,\n",
" description='T condensation (°C)', style=style, layout=layout)\n",
"\n",
"w_t_evap = widgets.FloatSlider(value=2, min=-20, max=20, step=1,\n",
" description='T évaporation (°C)', style=style, layout=layout)\n",
"\n",
"w_eta = widgets.FloatSlider(value=0.75, min=0.50, max=0.95, step=0.01,\n",
" description='η isentropique ()', style=style, layout=layout,\n",
" readout_format='.2f')\n",
"\n",
"w_superheat = widgets.FloatSlider(value=5, min=0, max=20, step=0.5,\n",
" description='Surchauffe (K)', style=style, layout=layout)\n",
"\n",
"w_ua_cond = widgets.IntSlider(value=5000, min=1000, max=20000, step=500,\n",
" description='UA condenseur (W/K)', style=style, layout=layout)\n",
"\n",
"w_ua_evap = widgets.IntSlider(value=6000, min=1000, max=20000, step=500,\n",
" description='UA évaporateur (W/K)', style=style, layout=layout)\n",
"\n",
"btn_run = widgets.Button(description='▶ Simuler', button_style='success',\n",
" layout=widgets.Layout(width='200px', height='40px'))\n",
"\n",
"out = widgets.Output()\n",
"\n",
"display(\n",
" widgets.VBox([\n",
" widgets.HTML('<h3>⚙️ Paramètres du cycle</h3>'),\n",
" w_t_cond, w_t_evap, w_eta, w_superheat,\n",
" widgets.HTML('<h3>🔁 Échangeurs</h3>'),\n",
" w_ua_cond, w_ua_evap,\n",
" btn_run,\n",
" out\n",
" ])\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "fd926e11",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"🎯 Cliquez ▶ Simuler dans la cellule précédente\n"
]
}
],
"source": [
"def parse_cli_output(text):\n",
" \"\"\"Extract edges and status from CLI stdout.\"\"\"\n",
" edges, status, residual, iterations = [], 'UNKNOWN', None, None\n",
" for line in text.splitlines():\n",
" line = line.strip()\n",
" if 'Status:' in line:\n",
" status = 'CONVERGED' if 'CONVERGED' in line else 'DIVERGED'\n",
" if 'Residual:' in line:\n",
" try: residual = float(line.split()[-1])\n",
" except: pass\n",
" if 'Iterations:' in line:\n",
" try: iterations = int(line.split()[-1])\n",
" except: pass\n",
" if line.startswith('Edge'):\n",
" # Edge 0: P = 30.711 bar, h = 433.16 kJ/kg\n",
" try:\n",
" parts = line.split(':')\n",
" idx = int(parts[0].replace('Edge','').strip())\n",
" nums = [float(x.split()[0]) for x in parts[1].split(',')]\n",
" p_bar, h_kjkg = nums[0], nums[1]\n",
" edges.append({'idx': idx, 'P_bar': p_bar, 'H_kJkg': h_kjkg})\n",
" except: pass\n",
" return {'status': status, 'residual': residual, 'iterations': iterations, 'edges': edges}\n",
"\n",
"\n",
"def run_simulation(b):\n",
" with out:\n",
" clear_output(wait=True)\n",
"\n",
" # Build config from widgets\n",
" t_cond_k = w_t_cond.value + 273.15\n",
" t_evap_k = w_t_evap.value + 273.15\n",
"\n",
" with open(BASE_CONFIG) as f:\n",
" cfg = json.load(f)\n",
"\n",
" for comp in cfg['circuits'][0]['components']:\n",
" if comp['type'] == 'IsentropicCompressor':\n",
" comp['isentropic_efficiency'] = w_eta.value\n",
" comp['t_cond_k'] = t_cond_k\n",
" comp['t_evap_k'] = t_evap_k\n",
" comp['superheat_k'] = w_superheat.value\n",
" elif comp['type'] == 'Condenser':\n",
" comp['ua'] = w_ua_cond.value\n",
" comp['t_sat_k'] = t_cond_k\n",
" elif comp['type'] == 'IsenthalpicExpansionValve':\n",
" comp['t_evap_k'] = t_evap_k\n",
" elif comp['type'] == 'Evaporator':\n",
" comp['ua'] = w_ua_evap.value\n",
" comp['t_sat_k'] = t_evap_k\n",
" comp['superheat_k'] = w_superheat.value\n",
"\n",
" with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:\n",
" json.dump(cfg, f, indent=2)\n",
" tmp_path = f.name\n",
"\n",
" try:\n",
" result = subprocess.run([CLI, 'run', '--config', tmp_path],\n",
" capture_output=True, text=True, timeout=120)\n",
" output = result.stdout + result.stderr\n",
" data = parse_cli_output(output)\n",
"\n",
" if data['status'] == 'CONVERGED':\n",
" print(f\"✅ CONVERGÉ en {data['iterations']} itérations | résidu = {data['residual']:.2e}\")\n",
" print()\n",
"\n",
" edges = data['edges']\n",
" labels = ['Sortie compresseur', 'Sortie condenseur', 'Sortie EXV', 'Sortie évaporateur']\n",
" print(f\"{'Point':<25} {'P (bar)':>10} {'h (kJ/kg)':>12}\")\n",
" print('-' * 50)\n",
" for e in edges[:4]:\n",
" lbl = labels[e['idx']] if e['idx'] < len(labels) else f\"Edge {e['idx']}\"\n",
" print(f\"{lbl:<25} {e['P_bar']:>10.3f} {e['H_kJkg']:>12.2f}\")\n",
"\n",
" # Compute COP\n",
" if len(edges) >= 4:\n",
" h_comp_in = edges[3]['H_kJkg'] # evap outlet = comp inlet\n",
" h_comp_out = edges[0]['H_kJkg'] # comp outlet\n",
" h_exv_in = edges[1]['H_kJkg'] # cond outlet = exv inlet\n",
" w_comp = h_comp_out - h_comp_in\n",
" q_evap = h_comp_in - h_exv_in\n",
" cop = q_evap / w_comp if w_comp > 0 else 0\n",
" print()\n",
" print(f\"{'W compresseur':<25} {w_comp:>10.2f} kJ/kg\")\n",
" print(f\"{'Q évaporateur':<25} {q_evap:>10.2f} kJ/kg\")\n",
" print(f\"{'COP (froid)':<25} {cop:>10.2f}\")\n",
"\n",
" plot_cycle(edges[:4], labels)\n",
"\n",
" else:\n",
" print('❌ DIVERGENCE')\n",
" print(output[-2000:])\n",
"\n",
" except subprocess.TimeoutExpired:\n",
" print('⏱ Timeout')\n",
" finally:\n",
" os.unlink(tmp_path)\n",
"\n",
"\n",
"def plot_cycle(edges, labels):\n",
" if len(edges) < 4:\n",
" return\n",
" h = [e['H_kJkg'] for e in edges]\n",
" p = [e['P_bar'] for e in edges]\n",
"\n",
" # Close the cycle\n",
" h_cycle = h + [h[0]]\n",
" p_cycle = p + [p[0]]\n",
"\n",
" fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
"\n",
" # ── P-h diagram ──────────────────────────────────────────────────────────\n",
" ax = axes[0]\n",
" colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12']\n",
" ax.plot(h_cycle, p_cycle, 'k-', linewidth=2, zorder=2)\n",
" for i, (hi, pi) in enumerate(zip(h[:4], p[:4])):\n",
" ax.scatter([hi], [pi], color=colors[i], s=100, zorder=3)\n",
" ax.annotate(f\"{i+1}. {labels[i]}\\n({pi:.1f} bar, {hi:.0f} kJ/kg)\",\n",
" (hi, pi), textcoords='offset points', xytext=(8, 5), fontsize=8)\n",
" ax.set_xlabel('Enthalpie h (kJ/kg)', fontsize=11)\n",
" ax.set_ylabel('Pression P (bar)', fontsize=11)\n",
" ax.set_title('Diagramme P-h du cycle', fontsize=12)\n",
" ax.grid(True, alpha=0.3)\n",
"\n",
" # ── Bar chart: enthalpies per state ──────────────────────────────────────\n",
" ax2 = axes[1]\n",
" short_labels = ['1\\nSortie\\ncompresseur', '2\\nSortie\\ncondenseur',\n",
" '3\\nSortie\\nEXV', '4\\nSortie\\névaporateur']\n",
" bars = ax2.bar(short_labels, h[:4], color=colors, edgecolor='black', linewidth=0.7)\n",
" for bar, val in zip(bars, h[:4]):\n",
" ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 2,\n",
" f'{val:.1f}', ha='center', va='bottom', fontsize=9)\n",
" ax2.set_ylabel('Enthalpie (kJ/kg)', fontsize=11)\n",
" ax2.set_title('Enthalpies aux points du cycle', fontsize=12)\n",
" ax2.grid(True, axis='y', alpha=0.3)\n",
"\n",
" plt.tight_layout()\n",
" plt.show()\n",
"\n",
"\n",
"btn_run.on_click(run_simulation)\n",
"print('🎯 Cliquez ▶ Simuler dans la cellule précédente')"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ae140c88-c298-4fe6-a13b-53d61640ddcf",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.8"
}
},
"nbformat": 4,
"nbformat_minor": 5
}