Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Some checks failed
CI / check (push) Has been cancelled
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:
290
simulation_interactive.ipynb
Normal file
290
simulation_interactive.ipynb
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user