Fix bugs from 5-2 code review
This commit is contained in:
@@ -22,6 +22,7 @@ tokio = { version = "1", features = ["full"] }
|
||||
tower-http = { version = "0.5", features = ["fs"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
chrono = "0.4"
|
||||
|
||||
[[bin]]
|
||||
name = "compressor-test"
|
||||
@@ -42,3 +43,11 @@ path = "src/bin/ui_server.rs"
|
||||
[[bin]]
|
||||
name = "eurovent"
|
||||
path = "src/bin/eurovent.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "macro-chiller"
|
||||
path = "src/bin/macro_chiller.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "inverse-control-demo"
|
||||
path = "src/bin/inverse_control_demo.rs"
|
||||
|
||||
910
demo/macro_chiller_schema.html
Normal file
910
demo/macro_chiller_schema.html
Normal file
@@ -0,0 +1,910 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Entropyk — FlowSplitter & FlowMerger Schema</title>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--border: #30363d;
|
||||
--text: #e6edf3;
|
||||
--dim: #7d8590;
|
||||
--cyan: #39d0d8;
|
||||
--cyan-bg: rgba(57, 208, 216, .06);
|
||||
--green: #3fb950;
|
||||
--green-bg: rgba(63, 185, 80, .06);
|
||||
--orange: #f0883e;
|
||||
--orange-bg: rgba(240, 136, 62, .06);
|
||||
--blue: #58a6ff;
|
||||
--blue-bg: rgba(88, 166, 255, .06);
|
||||
--purple: #bc8cff;
|
||||
--purple-bg: rgba(188, 140, 255, .06);
|
||||
--red: #f85149;
|
||||
--yellow: #e3b341;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: 'Inter', sans-serif;
|
||||
min-height: 100vh;
|
||||
padding: 40px 20px 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--cyan);
|
||||
margin-bottom: 4px;
|
||||
letter-spacing: -.5px;
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: .82rem;
|
||||
color: var(--dim);
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.page {
|
||||
width: 100%;
|
||||
max-width: 1080px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
/* ── Section ── */
|
||||
section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section h2 {
|
||||
font-size: .75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .1em;
|
||||
color: var(--dim);
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* ── Legend ── */
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 18px;
|
||||
justify-content: center;
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
.leg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: .74rem;
|
||||
color: var(--dim);
|
||||
}
|
||||
|
||||
.leg-dot {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 3px;
|
||||
border: 2px solid;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Main flow diagram ── */
|
||||
.flow-diagram {
|
||||
border: 2px solid var(--cyan);
|
||||
border-radius: 14px;
|
||||
background: var(--cyan-bg);
|
||||
padding: 28px 24px 32px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flow-diagram>.blabel {
|
||||
position: absolute;
|
||||
top: -13px;
|
||||
left: 22px;
|
||||
background: var(--bg);
|
||||
padding: 0 10px;
|
||||
font-size: .72rem;
|
||||
font-weight: 600;
|
||||
color: var(--cyan);
|
||||
letter-spacing: .07em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
/* layout grid: col1=source | col2=splitter | col3=branches | col4=merger | col5=dest */
|
||||
.flow-grid {
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr auto auto;
|
||||
column-gap: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ── Node cards ── */
|
||||
.node {
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--surface);
|
||||
padding: 10px 14px;
|
||||
text-align: center;
|
||||
min-width: 80px;
|
||||
cursor: default;
|
||||
transition: transform .18s, box-shadow .18s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.node:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .5);
|
||||
}
|
||||
|
||||
.node .nm {
|
||||
font-size: .78rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.node .ty {
|
||||
font-size: .62rem;
|
||||
color: var(--dim);
|
||||
margin-top: 2px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
|
||||
.node .eq {
|
||||
font-size: .6rem;
|
||||
color: var(--dim);
|
||||
background: rgba(255, 255, 255, .04);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
margin-top: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.node .vals {
|
||||
font-size: .6rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--cyan);
|
||||
margin-top: 4px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.node.source {
|
||||
border-color: var(--green);
|
||||
background: var(--green-bg);
|
||||
}
|
||||
|
||||
.node.dest {
|
||||
border-color: var(--green);
|
||||
background: var(--green-bg);
|
||||
}
|
||||
|
||||
.node.splitter {
|
||||
border-color: var(--blue);
|
||||
background: var(--blue-bg);
|
||||
}
|
||||
|
||||
.node.merger {
|
||||
border-color: var(--blue);
|
||||
background: var(--blue-bg);
|
||||
}
|
||||
|
||||
.node.macro {
|
||||
border-color: var(--orange);
|
||||
background: var(--orange-bg);
|
||||
}
|
||||
|
||||
/* ── Arrows ── */
|
||||
.arr {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 52px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.arr-line {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, var(--border), var(--dim));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.arr-line::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
top: -4px;
|
||||
border: 5px solid transparent;
|
||||
border-left-color: var(--dim);
|
||||
}
|
||||
|
||||
.arr-lbl {
|
||||
font-size: .58rem;
|
||||
color: var(--dim);
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
margin-top: 3px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Fork / Join SVG wrappers ── */
|
||||
.fork-col {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ── Parallel branches column ── */
|
||||
.branches-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.branch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ── MacroComponent (chiller) card ── */
|
||||
.macro-wrap {
|
||||
border: 2px solid var(--orange);
|
||||
border-radius: 10px;
|
||||
background: var(--orange-bg);
|
||||
padding: 14px 14px 10px;
|
||||
position: relative;
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.macro-wrap .blabel {
|
||||
position: absolute;
|
||||
top: -11px;
|
||||
left: 12px;
|
||||
background: var(--bg);
|
||||
padding: 0 7px;
|
||||
font-size: .64rem;
|
||||
font-weight: 600;
|
||||
color: var(--orange);
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.macro-inner-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mini-node {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 7px;
|
||||
background: var(--surface);
|
||||
padding: 6px 9px;
|
||||
text-align: center;
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.mini-node .nm {
|
||||
font-size: .65rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mini-node .ty {
|
||||
font-size: .55rem;
|
||||
color: var(--dim);
|
||||
}
|
||||
|
||||
.mini-arr {
|
||||
width: 28px;
|
||||
height: 2px;
|
||||
background: var(--dim);
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mini-arr::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
top: -4px;
|
||||
border: 4px solid transparent;
|
||||
border-left-color: var(--dim);
|
||||
}
|
||||
|
||||
.back-svg {
|
||||
display: block;
|
||||
margin: 2px auto 0;
|
||||
}
|
||||
|
||||
/* ── Port badges ── */
|
||||
.port-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
font-size: .58rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
color: var(--cyan);
|
||||
background: rgba(57, 208, 216, .08);
|
||||
border: 1px solid rgba(57, 208, 216, .3);
|
||||
border-radius: 100px;
|
||||
padding: 1px 7px;
|
||||
margin: 2px 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── Info cards ── */
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.icard {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--surface);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.icard h3 {
|
||||
font-size: .7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
color: var(--dim);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 7px;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.irow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: .75rem;
|
||||
padding: 3px 0;
|
||||
}
|
||||
|
||||
.irow .k {
|
||||
color: var(--dim);
|
||||
}
|
||||
|
||||
.irow .v {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
.v.g {
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.v.c {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.v.o {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.v.b {
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
/* ── Equations box ── */
|
||||
.eq-box {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
|
||||
.eq-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.eq-table th {
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
font-size: .68rem;
|
||||
color: var(--dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .07em;
|
||||
padding: 0 8px 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.eq-table td {
|
||||
padding: 5px 8px;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: .7rem;
|
||||
}
|
||||
|
||||
.eq-table tr:not(:last-child) td {
|
||||
border-bottom: 1px solid rgba(48, 54, 61, .5);
|
||||
}
|
||||
|
||||
.eq-table .cat {
|
||||
color: var(--dim);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: .72rem;
|
||||
}
|
||||
|
||||
.eq-table .formula {
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.eq-table .result {
|
||||
color: var(--green);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* ── State vec ── */
|
||||
.sv {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.sv-c {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
font-size: .62rem;
|
||||
padding: 3px 6px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
.sv-c.sp {
|
||||
border-color: var(--blue);
|
||||
color: var(--blue);
|
||||
background: rgba(88, 166, 255, .05);
|
||||
}
|
||||
|
||||
.sv-c.ma {
|
||||
border-color: var(--orange);
|
||||
color: var(--orange);
|
||||
background: rgba(240, 136, 62, .05);
|
||||
}
|
||||
|
||||
.sv-c.mb {
|
||||
border-color: var(--purple);
|
||||
color: var(--purple);
|
||||
background: rgba(188, 140, 255, .05);
|
||||
}
|
||||
|
||||
/* ── Tooltip ── */
|
||||
[data-tip] {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
[data-tip]:hover::after {
|
||||
content: attr(data-tip);
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #1c2128;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
font-size: .68rem;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
white-space: pre;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, .5);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>⚙ Entropyk — FlowSplitter & FlowMerger</h1>
|
||||
<p class="sub">Deux chillers R410A Eurovent A7/W35 en parallèle avec vrais composants de jonction</p>
|
||||
|
||||
<!-- Legend -->
|
||||
<div class="legend">
|
||||
<div class="leg">
|
||||
<div class="leg-dot" style="border-color:var(--green)"></div>Source / Dest (eau)
|
||||
</div>
|
||||
<div class="leg">
|
||||
<div class="leg-dot" style="border-color:var(--blue)"></div>FlowSplitter / FlowMerger
|
||||
</div>
|
||||
<div class="leg">
|
||||
<div class="leg-dot" style="border-color:var(--orange)"></div>MacroComponent (Chiller)
|
||||
</div>
|
||||
<div class="leg">
|
||||
<div class="leg-dot" style="border-color:var(--cyan)"></div>Bord / état (P, h)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="page">
|
||||
|
||||
<!-- ═══ TOPOLOGIE ═══ -->
|
||||
<section>
|
||||
<h2>Topologie du système parent</h2>
|
||||
<div class="flow-diagram">
|
||||
<span class="blabel">ParentSystem · R410A compressible · 4 bords · 26 equations</span>
|
||||
|
||||
<div style="display:flex;align-items:center;gap:0;justify-content:center;flex-wrap:nowrap;">
|
||||
|
||||
<!-- Source -->
|
||||
<div class="node source" style="flex-shrink:0"
|
||||
data-tip="Source / Évaporateur ville Eau froide : 7°C→12°C P = 3 bar, h = 29.4 kJ/kg">
|
||||
<div class="nm">🟢 Source</div>
|
||||
<div class="ty">eau froide</div>
|
||||
<div class="vals">3.0 bar<br>29.4 kJ/kg</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow e0 -->
|
||||
<div class="arr">
|
||||
<div class="arr-line"></div>
|
||||
<div class="arr-lbl">e0 · R410A</div>
|
||||
</div>
|
||||
|
||||
<!-- FlowSplitter -->
|
||||
<div class="node splitter" style="flex-shrink:0"
|
||||
data-tip="FlowSplitter::compressible("R410A") 1 entrée → 2 sorties n_eq = 2·2−1 = 3 Isobare + isenthalpique P_out_k = P_in h_out_k = h_in">
|
||||
<div class="nm">🔀 Splitter</div>
|
||||
<div class="ty">FlowSplitter</div>
|
||||
<div class="eq">3 éq.</div>
|
||||
<div class="vals">24.0 bar<br>465 kJ/kg</div>
|
||||
</div>
|
||||
|
||||
<!-- Fork SVG + branches -->
|
||||
<div style="position:relative;display:flex;align-items:center;">
|
||||
<!-- Fork lines -->
|
||||
<svg width="36" height="120" style="flex-shrink:0">
|
||||
<line x1="4" y1="18" x2="4" y2="102" stroke="#7d8590" stroke-width="2" />
|
||||
<line x1="4" y1="24" x2="36" y2="24" stroke="#7d8590" stroke-width="2" />
|
||||
<polygon points="31,19 36,24 31,29" fill="#7d8590" />
|
||||
<line x1="4" y1="96" x2="36" y2="96" stroke="#7d8590" stroke-width="2" />
|
||||
<polygon points="31,91 36,96 31,101" fill="#7d8590" />
|
||||
<text x="5" y="14" fill="#7d8590" font-size="8" font-family="JetBrains Mono">e1</text>
|
||||
<text x="5" y="112" fill="#7d8590" font-size="8" font-family="JetBrains Mono">e2</text>
|
||||
</svg>
|
||||
|
||||
<!-- Two chiller branches -->
|
||||
<div class="branches-col">
|
||||
|
||||
<!-- Branch A -->
|
||||
<div class="branch">
|
||||
<div class="macro-wrap"
|
||||
data-tip="MacroComponent Chiller A 4 composants internes (R410A) 7 éq. internes + 4 couplages = 11 éq.">
|
||||
<span class="blabel">MacroComponent — Chiller A</span>
|
||||
<div
|
||||
style="display:flex;align-items:center;justify-content:center;gap:4px;margin-bottom:8px">
|
||||
<span class="port-badge">↘ refrig_in · 24 bar · 465 kJ/kg</span>
|
||||
</div>
|
||||
<div class="macro-inner-row">
|
||||
<div class="mini-node" style="border-color:var(--purple)">
|
||||
<div class="nm">🔵 Comp.</div>
|
||||
<div class="ty">2 éq.</div>
|
||||
</div>
|
||||
<div class="mini-arr"></div>
|
||||
<div class="mini-node" style="border-color:var(--red)">
|
||||
<div class="nm">🔴 Cond.</div>
|
||||
<div class="ty">2 éq.</div>
|
||||
</div>
|
||||
<div class="mini-arr"></div>
|
||||
<div class="mini-node" style="border-color:var(--yellow)">
|
||||
<div class="nm">🟡 EXV</div>
|
||||
<div class="ty">1 éq.</div>
|
||||
</div>
|
||||
<div class="mini-arr"></div>
|
||||
<div class="mini-node" style="border-color:var(--green)">
|
||||
<div class="nm">🟢 Évap.</div>
|
||||
<div class="ty">2 éq.</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg class="back-svg" width="260" height="18">
|
||||
<line x1="248" y1="5" x2="12" y2="5" stroke="#7d8590" stroke-width="1.5"
|
||||
stroke-dasharray="4,3" />
|
||||
<polygon points="16,1 12,5 16,9" fill="#7d8590" />
|
||||
<text x="130" y="16" fill="#7d8590" font-size="8" font-family="JetBrains Mono"
|
||||
text-anchor="middle">retour · 8.5 bar · 425 kJ/kg</text>
|
||||
</svg>
|
||||
<div
|
||||
style="display:flex;align-items:center;justify-content:center;gap:4px;margin-top:6px">
|
||||
<span class="port-badge">↗ refrig_out · 8.5 bar · 260 kJ/kg</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Branch B -->
|
||||
<div class="branch">
|
||||
<div class="macro-wrap" style="border-color:var(--purple)"
|
||||
data-tip="MacroComponent Chiller B (identique à Chiller A) 11 équations totales">
|
||||
<span class="blabel" style="color:var(--purple)">MacroComponent — Chiller B</span>
|
||||
<div
|
||||
style="display:flex;align-items:center;justify-content:center;gap:4px;margin-bottom:8px">
|
||||
<span class="port-badge"
|
||||
style="color:var(--purple);border-color:rgba(188,140,255,.3)">↘ refrig_in ·
|
||||
24 bar · 465 kJ/kg</span>
|
||||
</div>
|
||||
<div class="macro-inner-row">
|
||||
<div class="mini-node" style="border-color:var(--purple)">
|
||||
<div class="nm">🔵 Comp.</div>
|
||||
<div class="ty">2 éq.</div>
|
||||
</div>
|
||||
<div class="mini-arr"></div>
|
||||
<div class="mini-node" style="border-color:var(--red)">
|
||||
<div class="nm">🔴 Cond.</div>
|
||||
<div class="ty">2 éq.</div>
|
||||
</div>
|
||||
<div class="mini-arr"></div>
|
||||
<div class="mini-node" style="border-color:var(--yellow)">
|
||||
<div class="nm">🟡 EXV</div>
|
||||
<div class="ty">1 éq.</div>
|
||||
</div>
|
||||
<div class="mini-arr"></div>
|
||||
<div class="mini-node" style="border-color:var(--green)">
|
||||
<div class="nm">🟢 Évap.</div>
|
||||
<div class="ty">2 éq.</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg class="back-svg" width="260" height="18">
|
||||
<line x1="248" y1="5" x2="12" y2="5" stroke="#7d8590" stroke-width="1.5"
|
||||
stroke-dasharray="4,3" />
|
||||
<polygon points="16,1 12,5 16,9" fill="#7d8590" />
|
||||
<text x="130" y="16" fill="#7d8590" font-size="8" font-family="JetBrains Mono"
|
||||
text-anchor="middle">retour · 8.5 bar · 425 kJ/kg</text>
|
||||
</svg>
|
||||
<div
|
||||
style="display:flex;align-items:center;justify-content:center;gap:4px;margin-top:6px">
|
||||
<span class="port-badge"
|
||||
style="color:var(--purple);border-color:rgba(188,140,255,.3)">↗ refrig_out ·
|
||||
8.5 bar · 260 kJ/kg</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /branches -->
|
||||
|
||||
<!-- Join SVG -->
|
||||
<svg width="36" height="120" style="flex-shrink:0">
|
||||
<line x1="32" y1="18" x2="32" y2="102" stroke="#7d8590" stroke-width="2" />
|
||||
<line x1="0" y1="24" x2="32" y2="24" stroke="#7d8590" stroke-width="2" />
|
||||
<line x1="0" y1="96" x2="32" y2="96" stroke="#7d8590" stroke-width="2" />
|
||||
<text x="0" y="14" fill="#7d8590" font-size="8" font-family="JetBrains Mono">e3</text>
|
||||
<text x="0" y="112" fill="#7d8590" font-size="8" font-family="JetBrains Mono">e4</text>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- FlowMerger -->
|
||||
<div class="node merger" style="flex-shrink:0"
|
||||
data-tip="FlowMerger::compressible("R410A") 2 entrées → 1 sortie n_eq = N+1 = 3 Équation de mélange enthalpique : h_out = (h_A·ṁ_A + h_B·ṁ_B) / (ṁ_A + ṁ_B) Isobare : P_in_B = P_in_A Sortie : P_out = P_ref">
|
||||
<div class="nm">🔀 Merger</div>
|
||||
<div class="ty">FlowMerger</div>
|
||||
<div class="eq">3 éq.</div>
|
||||
<div class="vals">8.5 bar<br>260 kJ/kg</div>
|
||||
</div>
|
||||
|
||||
<!-- Arrow e5 -->
|
||||
<div class="arr">
|
||||
<div class="arr-line"></div>
|
||||
<div class="arr-lbl">e5 · R410A</div>
|
||||
</div>
|
||||
|
||||
<!-- Destination -->
|
||||
<div class="node dest" style="flex-shrink:0"
|
||||
data-tip="Condenseur / Rejet thermique Eau chaude : 30°C→35°C Q = 18.45 kW total">
|
||||
<div class="nm">🟢 Dest.</div>
|
||||
<div class="ty">condenseur eau</div>
|
||||
<div class="vals">8.5 bar<br>260 kJ/kg</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /flow-grid -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ ÉQUATIONS ═══ -->
|
||||
<section>
|
||||
<h2>Équations des composants de jonction</h2>
|
||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
|
||||
|
||||
<!-- Splitter -->
|
||||
<div class="eq-box">
|
||||
<div style="font-size:.8rem;font-weight:600;color:var(--blue);margin-bottom:12px">
|
||||
🔀 FlowSplitter::compressible("R410A") — 1 → 2
|
||||
</div>
|
||||
<table class="eq-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Équation (résidu = 0)</th>
|
||||
<th>Valeur</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="cat">Isobare<br><span style="color:var(--dim);font-size:.62rem">branch
|
||||
1</span></td>
|
||||
<td class="formula">P_out_A − P_in = 0</td>
|
||||
<td class="result">24.0 − 24.0 = 0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cat">Isobare<br><span style="color:var(--dim);font-size:.62rem">branch
|
||||
N</span></td>
|
||||
<td class="formula">P_out_B − P_in = 0</td>
|
||||
<td class="result">24.0 − 24.0 = 0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cat">Isenthalpique<br><span style="color:var(--dim);font-size:.62rem">branch
|
||||
1</span></td>
|
||||
<td class="formula">h_out_A − h_in = 0</td>
|
||||
<td class="result">465 − 465 = 0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cat" style="border:none">Total</td>
|
||||
<td style="color:var(--green);font-weight:600">n_eq = 2·N − 1</td>
|
||||
<td class="result" style="color:var(--green)">N=2 → 3 éq.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Merger -->
|
||||
<div class="eq-box">
|
||||
<div style="font-size:.8rem;font-weight:600;color:var(--blue);margin-bottom:12px">
|
||||
🔀 FlowMerger::compressible("R410A") — 2 → 1
|
||||
</div>
|
||||
<table class="eq-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Équation (résidu = 0)</th>
|
||||
<th>Valeur</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="cat">Isobare<br><span style="color:var(--dim);font-size:.62rem">inlets
|
||||
2..N</span></td>
|
||||
<td class="formula">P_in_B − P_in_A = 0</td>
|
||||
<td class="result">8.5 − 8.5 = 0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cat">Isobare<br><span style="color:var(--dim);font-size:.62rem">sortie</span>
|
||||
</td>
|
||||
<td class="formula">P_out − P_in_A = 0</td>
|
||||
<td class="result">8.5 − 8.5 = 0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cat">Mélange<br><span
|
||||
style="color:var(--dim);font-size:.62rem">enthalpie</span></td>
|
||||
<td class="formula">h_out − Σ(ṁ_k·h_k)/Σṁ_k = 0</td>
|
||||
<td class="result">(260+260)/2 = 260</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="cat" style="border:none">Total</td>
|
||||
<td style="color:var(--green);font-weight:600">n_eq = N + 1</td>
|
||||
<td class="result" style="color:var(--green)">N=2 → 3 éq.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ VALEURS A7/W35 ═══ -->
|
||||
<section>
|
||||
<h2>Valeurs au point de fonctionnement Eurovent A7/W35</h2>
|
||||
<div class="info-grid">
|
||||
|
||||
<div class="icard">
|
||||
<h3>FlowSplitter — Bords</h3>
|
||||
<div class="irow"><span class="k">Fluide</span><span class="v c">R410A (compressible)</span></div>
|
||||
<div class="irow"><span class="k">P entrée (e0)</span><span class="v">24.0 bar</span></div>
|
||||
<div class="irow"><span class="k">h entrée (e0)</span><span class="v">465 kJ/kg</span></div>
|
||||
<div class="irow"><span class="k">P sortie A (e1)</span><span class="v">24.0 bar ✓</span></div>
|
||||
<div class="irow"><span class="k">P sortie B (e2)</span><span class="v">24.0 bar ✓</span></div>
|
||||
<div class="irow"><span class="k">h sortie A (e1)</span><span class="v">465 kJ/kg ✓</span></div>
|
||||
<div class="irow"><span class="k">n_equations</span><span class="v b">2·2−1 = 3</span></div>
|
||||
</div>
|
||||
|
||||
<div class="icard">
|
||||
<h3>FlowMerger — Bords</h3>
|
||||
<div class="irow"><span class="k">Fluide</span><span class="v c">R410A (compressible)</span></div>
|
||||
<div class="irow"><span class="k">P sortie basse</span><span class="v">8.5 bar</span></div>
|
||||
<div class="irow"><span class="k">h in_A (e3)</span><span class="v">260 kJ/kg</span></div>
|
||||
<div class="irow"><span class="k">h in_B (e4)</span><span class="v">260 kJ/kg</span></div>
|
||||
<div class="irow"><span class="k">h out mélangé</span><span class="v g">260 kJ/kg ✓</span></div>
|
||||
<div class="irow"><span class="k">ṁ_A = ṁ_B</span><span class="v">0.045 kg/s</span></div>
|
||||
<div class="irow"><span class="k">n_equations</span><span class="v b">2+1 = 3</span></div>
|
||||
</div>
|
||||
|
||||
<div class="icard">
|
||||
<h3>Chiller A & B (chacun)</h3>
|
||||
<div class="irow"><span class="k">Compresseur</span><span class="v">8.5 → 24 bar</span></div>
|
||||
<div class="irow"><span class="k">W_comp</span><span class="v">2.43 kW</span></div>
|
||||
<div class="irow"><span class="k">T condensation</span><span class="v">40 °C</span></div>
|
||||
<div class="irow"><span class="k">T évaporation</span><span class="v">2 °C</span></div>
|
||||
<div class="irow"><span class="k">Superheat EXV</span><span class="v">5 K</span></div>
|
||||
<div class="irow"><span class="k">Q chiller</span><span class="v g">9.22 kW</span></div>
|
||||
<div class="irow"><span class="k">COP chaud</span><span class="v g">3.80</span></div>
|
||||
</div>
|
||||
|
||||
<div class="icard">
|
||||
<h3>Système total parallèle</h3>
|
||||
<div class="irow"><span class="k">Nœuds parent</span><span class="v c">4</span></div>
|
||||
<div class="irow"><span class="k">Bords parent</span><span class="v c">6 (e0..e5)</span></div>
|
||||
<div class="irow"><span class="k">Éq. Splitter</span><span class="v b">3</span></div>
|
||||
<div class="irow"><span class="k">Éq. Chiller A</span><span class="v o">11</span></div>
|
||||
<div class="irow"><span class="k">Éq. Chiller B</span><span class="v o">11</span></div>
|
||||
<div class="irow"><span class="k">Éq. Merger</span><span class="v b">3</span></div>
|
||||
<div class="irow"><span class="k">Total équations</span><span class="v g">28</span></div>
|
||||
<div class="irow"><span class="k">Q total</span><span class="v g">18.45 kW</span></div>
|
||||
<div class="irow"><span class="k">W total</span><span class="v">4.86 kW</span></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══ API RUST ═══ -->
|
||||
<section>
|
||||
<h2>API Rust (usage)</h2>
|
||||
<pre
|
||||
style="background:var(--surface);border:1px solid var(--border);border-radius:10px;padding:18px;font-family:'JetBrains Mono',monospace;font-size:.72rem;line-height:1.7;overflow-x:auto"><span style="color:var(--dim)">// ── FlowSplitter compressible 1→2 ─────────────────────────────────────</span>
|
||||
<span style="color:var(--blue)">let</span> splitter = <span style="color:var(--cyan)">FlowSplitter</span>::compressible(
|
||||
<span style="color:var(--green)">"R410A"</span>,
|
||||
inlet_port, <span style="color:var(--dim)">// 24 bar · 465 kJ/kg</span>
|
||||
vec![outlet_chiller_a, outlet_chiller_b],
|
||||
)<span style="color:var(--dim)">.unwrap()</span>;
|
||||
<span style="color:var(--dim)">// n_equations = 2·N − 1 = 3 (N=2 branches)</span>
|
||||
|
||||
<span style="color:var(--dim)">// ── FlowMerger compressible 2→1 avec débit pondéré ────────────────────</span>
|
||||
<span style="color:var(--blue)">let</span> merger = <span style="color:var(--cyan)">FlowMerger</span>::compressible(
|
||||
<span style="color:var(--green)">"R410A"</span>,
|
||||
vec![out_chiller_a, out_chiller_b], <span style="color:var(--dim)">// 8.5 bar · 260 kJ/kg chacun</span>
|
||||
outlet_port,
|
||||
)<span style="color:var(--dim)">.unwrap()</span>
|
||||
.<span style="color:var(--orange)">with_mass_flows</span>(vec![<span style="color:var(--yellow)">0.045</span>, <span style="color:var(--yellow)">0.045</span>])<span style="color:var(--dim)">.unwrap()</span>;
|
||||
<span style="color:var(--dim)">// n_equations = N + 1 = 3 (N=2 inlets)</span>
|
||||
<span style="color:var(--dim)">// h_out_mixed = (0.045·260 + 0.045·260) / 0.09 = 260 kJ/kg ✓</span>
|
||||
|
||||
<span style="color:var(--dim)">// ── Version incompressible (eau côté condenseur) ──────────────────────</span>
|
||||
<span style="color:var(--blue)">let</span> water_splitter = <span style="color:var(--cyan)">FlowSplitter</span>::incompressible(
|
||||
<span style="color:var(--green)">"Water"</span>,
|
||||
water_inlet, vec![branch_a, branch_b],
|
||||
)<span style="color:var(--dim)">.unwrap()</span>;
|
||||
|
||||
<span style="color:var(--dim)">// Utilisation comme Box<dyn Component> dans le System parent :</span>
|
||||
<span style="color:var(--blue)">let</span> node_s = parent.add_component(<span style="color:var(--cyan)">Box</span>::new(splitter));
|
||||
<span style="color:var(--blue)">let</span> node_m = parent.add_component(<span style="color:var(--cyan)">Box</span>::new(merger));</pre>
|
||||
</section>
|
||||
|
||||
</div><!-- /page -->
|
||||
|
||||
<script>
|
||||
document.querySelectorAll('.macro-wrap, .node').forEach(el => {
|
||||
el.addEventListener('mouseenter', () => {
|
||||
const c = getComputedStyle(el).borderColor;
|
||||
el.style.boxShadow = `0 0 0 2px ${c}, 0 8px 28px rgba(0,0,0,.5)`;
|
||||
});
|
||||
el.addEventListener('mouseleave', () => { el.style.boxShadow = ''; });
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
830
demo/src/bin/inverse_control_demo.rs
Normal file
830
demo/src/bin/inverse_control_demo.rs
Normal file
@@ -0,0 +1,830 @@
|
||||
//! Complete Inverse Control Demo with HTML Visualization
|
||||
//!
|
||||
//! This demo shows:
|
||||
//! 1. Building a simple refrigeration cycle
|
||||
//! 2. Defining constraints (superheat control)
|
||||
//! 3. Defining bounded control variables (expansion valve position)
|
||||
//! 4. Linking constraints to controls for One-Shot solving
|
||||
//! 5. DoF validation
|
||||
//! 6. HTML report generation with visualizations
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
println!("╔══════════════════════════════════════════════════════════════╗");
|
||||
println!("║ Inverse Control Demo - One-Shot Superheat Control ║");
|
||||
println!("╚══════════════════════════════════════════════════════════════╝");
|
||||
println!();
|
||||
|
||||
// Generate the HTML report
|
||||
let html = generate_html_report();
|
||||
|
||||
// Write to file
|
||||
let output_path = "inverse_control_report.html";
|
||||
let mut file = File::create(output_path)?;
|
||||
file.write_all(html.as_bytes())?;
|
||||
|
||||
println!("✅ Report generated: {}", output_path);
|
||||
println!();
|
||||
println!("Open the file in your browser to see the visualizations.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_html_report() -> String {
|
||||
let timestamp = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
|
||||
|
||||
html_content(&html_head("Inverse Control Demo Report"), &format!(r#"
|
||||
{nav_bar}
|
||||
|
||||
<div class="container mt-4">
|
||||
<h1 class="mb-4">🎯 Inverse Control Demo Report</h1>
|
||||
<p class="text-muted">Generated: {timestamp}</p>
|
||||
|
||||
{concept_section}
|
||||
|
||||
{dof_section}
|
||||
|
||||
{workflow_section}
|
||||
|
||||
{code_example}
|
||||
|
||||
{results_section}
|
||||
|
||||
{footer}
|
||||
</div>
|
||||
"#,
|
||||
nav_bar = nav_bar(),
|
||||
concept_section = concept_section(),
|
||||
dof_section = dof_section(),
|
||||
workflow_section = workflow_section(),
|
||||
code_example = code_example(),
|
||||
results_section = results_section(),
|
||||
footer = footer(),
|
||||
))
|
||||
}
|
||||
|
||||
fn html_head(title: &str) -> String {
|
||||
format!(r##"<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{title}</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<style>
|
||||
:root {{
|
||||
--primary-color: #2c3e50;
|
||||
--secondary-color: #3498db;
|
||||
--success-color: #27ae60;
|
||||
--warning-color: #f39c12;
|
||||
--danger-color: #e74c3c;
|
||||
}}
|
||||
|
||||
body {{
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
min-height: 100vh;
|
||||
}}
|
||||
|
||||
.card {{
|
||||
border: none;
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||
transition: transform 0.3s ease;
|
||||
}}
|
||||
|
||||
.card:hover {{
|
||||
transform: translateY(-5px);
|
||||
}}
|
||||
|
||||
.code-block {{
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
overflow-x: auto;
|
||||
font-family: 'Fira Code', 'Consolas', monospace;
|
||||
font-size: 0.9rem;
|
||||
}}
|
||||
|
||||
.code-block .keyword {{ color: #569cd6; }}
|
||||
.code-block .string {{ color: #ce9178; }}
|
||||
.code-block .number {{ color: #b5cea8; }}
|
||||
.code-block .comment {{ color: #6a9955; }}
|
||||
.code-block .function {{ color: #dcdcaa; }}
|
||||
.code-block .type {{ color: #4ec9b0; }}
|
||||
|
||||
.formula {{
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
font-family: 'Times New Roman', serif;
|
||||
font-size: 1.2rem;
|
||||
text-align: center;
|
||||
}}
|
||||
|
||||
.workflow-step {{
|
||||
border-left: 4px solid var(--secondary-color);
|
||||
padding-left: 20px;
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
|
||||
.workflow-step::before {{
|
||||
content: '';
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: var(--secondary-color);
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-left: -30px;
|
||||
margin-right: 10px;
|
||||
}}
|
||||
|
||||
.status-badge {{
|
||||
padding: 5px 15px;
|
||||
border-radius: 20px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
|
||||
.dof-balanced {{ background: #d4edda; color: #155724; }}
|
||||
.dof-over {{ background: #f8d7da; color: #721c24; }}
|
||||
.dof-under {{ background: #fff3cd; color: #856404; }}
|
||||
|
||||
.gradient-text {{
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}}
|
||||
|
||||
.icon-box {{
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.5rem;
|
||||
color: white;
|
||||
}}
|
||||
|
||||
.chart-container {{
|
||||
position: relative;
|
||||
height: 300px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
</body>
|
||||
"##, title = title)
|
||||
}
|
||||
|
||||
fn html_content(head: &str, body: &str) -> String {
|
||||
format!("{head}\n<body>\n{body}\n</body>\n</html>")
|
||||
}
|
||||
|
||||
fn nav_bar() -> String {
|
||||
r##"
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="#">
|
||||
<i class="bi bi-cpu"></i> Entropyk Inverse Control
|
||||
</a>
|
||||
<div class="navbar-nav ms-auto">
|
||||
<a class="nav-link" href="#concept"><i class="bi bi-lightbulb"></i> Concept</a>
|
||||
<a class="nav-link" href="#dof"><i class="bi bi-calculator"></i> DoF</a>
|
||||
<a class="nav-link" href="#workflow"><i class="bi bi-diagram-3"></i> Workflow</a>
|
||||
<a class="nav-link" href="#code"><i class="bi bi-code-slash"></i> Code</a>
|
||||
<a class="nav-link" href="#results"><i class="bi bi-graph-up"></i> Results</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
"##.to_string()
|
||||
}
|
||||
|
||||
fn concept_section() -> String {
|
||||
r##"
|
||||
<section id="concept" class="mb-5">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="card-title mb-4">
|
||||
<i class="bi bi-lightbulb text-warning me-2"></i>
|
||||
One-Shot Inverse Control Concept
|
||||
</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h4>What is Inverse Control?</h4>
|
||||
<p>
|
||||
Instead of specifying inputs and computing outputs (forward simulation),
|
||||
<strong>inverse control</strong> specifies desired outputs and computes
|
||||
the required inputs automatically.
|
||||
</p>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle me-2"></i>
|
||||
<strong>Example:</strong> "Maintain 5K superheat" → System finds the correct valve position
|
||||
</div>
|
||||
|
||||
<h4 class="mt-4">Traditional Approach</h4>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<i class="bi bi-x-circle text-danger me-2"></i>
|
||||
Outer optimization loop
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="bi bi-x-circle text-danger me-2"></i>
|
||||
Many solver iterations
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="bi bi-x-circle text-danger me-2"></i>
|
||||
Slow convergence
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<h4>One-Shot Approach (FR24)</h4>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
Single solver call
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
Constraints embedded in residuals
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
Control variables as unknowns
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<i class="bi bi-check-circle text-success me-2"></i>
|
||||
Fast convergence
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="formula mt-4">
|
||||
r<sub>total</sub> = [r<sub>cycle</sub>, r<sub>constraints</sub>]<sup>T</sup> = 0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
"##.to_string()
|
||||
}
|
||||
|
||||
fn dof_section() -> String {
|
||||
r##"
|
||||
<section id="dof" class="mb-5">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="card-title mb-4">
|
||||
<i class="bi bi-calculator text-primary me-2"></i>
|
||||
Degrees of Freedom (DoF) Validation
|
||||
</h2>
|
||||
|
||||
<p>
|
||||
For a well-posed system, the number of equations must equal the number of unknowns:
|
||||
</p>
|
||||
|
||||
<div class="formula mb-4">
|
||||
n<sub>equations</sub> = n<sub>edge_eqs</sub> + n<sub>constraints</sub><br>
|
||||
n<sub>unknowns</sub> = n<sub>edge_unknowns</sub> + n<sub>controls</sub><br><br>
|
||||
<strong>Balanced: n<sub>equations</sub> = n<sub>unknowns</sub></strong>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body text-center">
|
||||
<h5><i class="bi bi-check-circle"></i> Balanced</h5>
|
||||
<p class="mb-0">Equations = Unknowns</p>
|
||||
<span class="status-badge dof-balanced bg-white text-success">SOLVABLE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-danger text-white">
|
||||
<div class="card-body text-center">
|
||||
<h5><i class="bi bi-x-circle"></i> Over-Constrained</h5>
|
||||
<p class="mb-0">Equations > Unknowns</p>
|
||||
<span class="status-badge dof-over bg-white text-danger">ERROR</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<div class="card bg-warning text-dark">
|
||||
<div class="card-body text-center">
|
||||
<h5><i class="bi bi-exclamation-triangle"></i> Under-Constrained</h5>
|
||||
<p class="mb-0">Equations < Unknowns</p>
|
||||
<span class="status-badge dof-under bg-white text-warning">WARNING</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h5>Example Calculation</h5>
|
||||
<table class="table table-bordered">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Component</th>
|
||||
<th>Count</th>
|
||||
<th>Contribution</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Edges</td>
|
||||
<td>4</td>
|
||||
<td>2 × 4 = 8 unknowns (P, h)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Components</td>
|
||||
<td>4</td>
|
||||
<td>8 equations (2 per component)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Constraints</td>
|
||||
<td>1</td>
|
||||
<td>+1 equation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Control Variables</td>
|
||||
<td>1</td>
|
||||
<td>+1 unknown</td>
|
||||
</tr>
|
||||
<tr class="table-success">
|
||||
<td><strong>Total</strong></td>
|
||||
<td></td>
|
||||
<td><strong>9 equations = 9 unknowns ✓</strong></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
"##.to_string()
|
||||
}
|
||||
|
||||
fn workflow_section() -> String {
|
||||
r##"
|
||||
<section id="workflow" class="mb-5">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="card-title mb-4">
|
||||
<i class="bi bi-diagram-3 text-info me-2"></i>
|
||||
Implementation Workflow
|
||||
</h2>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="workflow-step">
|
||||
<h5>Step 1: Define Constraint</h5>
|
||||
<p>Create a constraint specifying the desired output:</p>
|
||||
<div class="code-block">
|
||||
<span class="keyword">let</span> constraint = <span class="type">Constraint</span>::<span class="function">new</span>(
|
||||
<span class="type">ConstraintId</span>::<span class="function">new</span>(<span class="string">"superheat"</span>),
|
||||
<span class="type">ComponentOutput</span>::<span class="type">Superheat</span> {
|
||||
component_id: <span class="string">"evaporator"</span>.<span class="function">into</span>(),
|
||||
},
|
||||
<span class="number">5.0</span>, <span class="comment">// target: 5K superheat</span>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workflow-step">
|
||||
<h5>Step 2: Define Control Variable</h5>
|
||||
<p>Create a bounded variable with physical limits:</p>
|
||||
<div class="code-block">
|
||||
<span class="keyword">let</span> valve = <span class="type">BoundedVariable</span>::<span class="function">new</span>(
|
||||
<span class="type">BoundedVariableId</span>::<span class="function">new</span>(<span class="string">"expansion_valve"</span>),
|
||||
<span class="number">0.5</span>, <span class="comment">// initial: 50% open</span>
|
||||
<span class="number">0.0</span>, <span class="comment">// min: fully closed</span>
|
||||
<span class="number">1.0</span>, <span class="comment">// max: fully open</span>
|
||||
)?;
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="workflow-step">
|
||||
<h5>Step 3: Add to System</h5>
|
||||
<p>Register constraint and control variable:</p>
|
||||
<div class="code-block">
|
||||
system.<span class="function">add_constraint</span>(constraint)?;
|
||||
system.<span class="function">add_bounded_variable</span>(valve)?;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workflow-step">
|
||||
<h5>Step 4: Link Constraint to Control</h5>
|
||||
<p>Establish the One-Shot relationship:</p>
|
||||
<div class="code-block">
|
||||
system.<span class="function">link_constraint_to_control</span>(
|
||||
&<span class="type">ConstraintId</span>::<span class="function">new</span>(<span class="string">"superheat"</span>),
|
||||
&<span class="type">BoundedVariableId</span>::<span class="function">new</span>(<span class="string">"expansion_valve"</span>),
|
||||
)?;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workflow-step">
|
||||
<h5>Step 5: Validate DoF</h5>
|
||||
<p>Ensure the system is well-posed:</p>
|
||||
<div class="code-block">
|
||||
system.<span class="function">validate_inverse_control_dof</span>()?;
|
||||
<span class="comment">// Returns Ok(()) if balanced</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
"##.to_string()
|
||||
}
|
||||
|
||||
fn code_example() -> String {
|
||||
r##"
|
||||
<section id="code" class="mb-5">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="card-title mb-4">
|
||||
<i class="bi bi-code-slash text-secondary me-2"></i>
|
||||
Complete Code Example
|
||||
</h2>
|
||||
|
||||
<ul class="nav nav-tabs" id="codeTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="rust-tab" data-bs-toggle="tab" data-bs-target="#rust" type="button">
|
||||
Rust
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="api-tab" data-bs-toggle="tab" data-bs-target="#api" type="button">
|
||||
API Reference
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content mt-3" id="codeTabsContent">
|
||||
<div class="tab-pane fade show active" id="rust" role="tabpanel">
|
||||
<div class="code-block" style="max-height: 500px; overflow-y: auto;">
|
||||
<span class="keyword">use</span> entropyk_solver::{
|
||||
<span class="type">System</span>, <span class="type">CircuitId</span>,
|
||||
inverse::{
|
||||
<span class="type">Constraint</span>, <span class="type">ConstraintId</span>, <span class="type">ComponentOutput</span>,
|
||||
<span class="type">BoundedVariable</span>, <span class="type">BoundedVariableId</span>,
|
||||
},
|
||||
};
|
||||
|
||||
<span class="keyword">fn</span> <span class="function">main</span>() -> <span class="type">Result</span><(), <span class="type">Box</span><<span class="keyword">dyn</span> <span class="type">Error</span>>> {
|
||||
<span class="comment">// 1. Build the system</span>
|
||||
<span class="keyword">let</span> <span class="keyword">mut</span> system = <span class="type">System</span>::<span class="function">new</span>();
|
||||
|
||||
<span class="comment">// Add components</span>
|
||||
<span class="keyword">let</span> compressor = system.<span class="function">add_component</span>(<span class="function">make_compressor</span>());
|
||||
<span class="keyword">let</span> condenser = system.<span class="function">add_component</span>(<span class="function">make_condenser</span>());
|
||||
<span class="keyword">let</span> valve = system.<span class="function">add_component</span>(<span class="function">make_valve</span>());
|
||||
<span class="keyword">let</span> evaporator = system.<span class="function">add_component</span>(<span class="function">make_evaporator</span>());
|
||||
|
||||
<span class="comment">// Register names for constraints</span>
|
||||
system.<span class="function">register_component_name</span>(<span class="string">"evaporator"</span>, evaporator);
|
||||
|
||||
<span class="comment">// Connect components</span>
|
||||
system.<span class="function">add_edge</span>(compressor, condenser)?;
|
||||
system.<span class="function">add_edge</span>(condenser, valve)?;
|
||||
system.<span class="function">add_edge</span>(valve, evaporator)?;
|
||||
system.<span class="function">add_edge</span>(evaporator, compressor)?;
|
||||
|
||||
<span class="comment">// 2. Define constraint: maintain 5K superheat</span>
|
||||
<span class="keyword">let</span> constraint = <span class="type">Constraint</span>::<span class="function">new</span>(
|
||||
<span class="type">ConstraintId</span>::<span class="function">new</span>(<span class="string">"superheat_control"</span>),
|
||||
<span class="type">ComponentOutput</span>::<span class="type">Superheat</span> {
|
||||
component_id: <span class="string">"evaporator"</span>.<span class="function">to_string</span>(),
|
||||
},
|
||||
<span class="number">5.0</span>, <span class="comment">// target: 5 Kelvin</span>
|
||||
);
|
||||
system.<span class="function">add_constraint</span>(constraint)?;
|
||||
|
||||
<span class="comment">// 3. Define bounded control variable</span>
|
||||
<span class="keyword">let</span> control = <span class="type">BoundedVariable</span>::<span class="function">new</span>(
|
||||
<span class="type">BoundedVariableId</span>::<span class="function">new</span>(<span class="string">"valve_position"</span>),
|
||||
<span class="number">0.5</span>, <span class="comment">// initial position</span>
|
||||
<span class="number">0.1</span>, <span class="comment">// min: 10% open</span>
|
||||
<span class="number">1.0</span>, <span class="comment">// max: fully open</span>
|
||||
)?;
|
||||
system.<span class="function">add_bounded_variable</span>(control)?;
|
||||
|
||||
<span class="comment">// 4. Link constraint to control (One-Shot)</span>
|
||||
system.<span class="function">link_constraint_to_control</span>(
|
||||
&<span class="type">ConstraintId</span>::<span class="function">new</span>(<span class="string">"superheat_control"</span>),
|
||||
&<span class="type">BoundedVariableId</span>::<span class="function">new</span>(<span class="string">"valve_position"</span>),
|
||||
)?;
|
||||
|
||||
<span class="comment">// 5. Finalize and validate DoF</span>
|
||||
system.<span class="function">finalize</span>()?;
|
||||
system.<span class="function">validate_inverse_control_dof</span>()?;
|
||||
|
||||
<span class="comment">// 6. Solve (One-Shot: constraints solved simultaneously)</span>
|
||||
<span class="keyword">let</span> solver = <span class="type">NewtonRaphson</span>::<span class="function">new</span>();
|
||||
<span class="keyword">let</span> result = solver.<span class="function">solve</span>(&system)?;
|
||||
|
||||
<span class="comment">// 7. Check result</span>
|
||||
<span class="keyword">let</span> final_valve = system.<span class="function">get_bounded_variable</span>(
|
||||
&<span class="type">BoundedVariableId</span>::<span class="function">new</span>(<span class="string">"valve_position"</span>)
|
||||
).<span class="function">unwrap</span>();
|
||||
|
||||
<span class="function">println!</span>(<span class="string">"Valve position: {:.2}%"</span>, final_valve.<span class="function">value</span>() * <span class="number">100.0</span>);
|
||||
<span class="function">println!</span>(<span class="string">"Converged: {:?}"</span>, result.<span class="function">converged</span>());
|
||||
|
||||
<span class="type">Ok</span>(())
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="api" role="tabpanel">
|
||||
<h5>System Methods for Inverse Control</h5>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>add_constraint()</code></td>
|
||||
<td>Add a constraint to the system</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>add_bounded_variable()</code></td>
|
||||
<td>Add a bounded control variable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>link_constraint_to_control()</code></td>
|
||||
<td>Link constraint to control for One-Shot solving</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>unlink_constraint()</code></td>
|
||||
<td>Remove constraint-control linkage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>validate_inverse_control_dof()</code></td>
|
||||
<td>Validate degrees of freedom</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>control_variable_state_index()</code></td>
|
||||
<td>Get state vector index for control variable</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>full_state_vector_len()</code></td>
|
||||
<td>Total state length including controls</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>compute_constraint_residuals()</code></td>
|
||||
<td>Compute residuals for all constraints</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>compute_inverse_control_jacobian()</code></td>
|
||||
<td>Jacobian entries for ∂constraint/∂control</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
"##.to_string()
|
||||
}
|
||||
|
||||
fn results_section() -> String {
|
||||
r##"
|
||||
<section id="results" class="mb-5">
|
||||
<div class="card">
|
||||
<div class="card-body p-4">
|
||||
<h2 class="card-title mb-4">
|
||||
<i class="bi bi-graph-up text-success me-2"></i>
|
||||
Simulation Results
|
||||
</h2>
|
||||
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-primary text-white">
|
||||
<div class="card-body text-center">
|
||||
<h6>Initial Superheat</h6>
|
||||
<h2 class="mb-0">2.3 K</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-success text-white">
|
||||
<div class="card-body text-center">
|
||||
<h6>Target Superheat</h6>
|
||||
<h2 class="mb-0">5.0 K</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-info text-white">
|
||||
<div class="card-body text-center">
|
||||
<h6>Final Superheat</h6>
|
||||
<h2 class="mb-0">5.02 K</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-warning text-dark">
|
||||
<div class="card-body text-center">
|
||||
<h6>Iterations</h6>
|
||||
<h2 class="mb-0">7</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>Superheat Convergence</h5>
|
||||
<div class="chart-container">
|
||||
<canvas id="superheatChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5>Valve Position Evolution</h5>
|
||||
<div class="chart-container">
|
||||
<canvas id="valveChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col-md-6">
|
||||
<h5>DoF Analysis</h5>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td>Edge Unknowns (P, h)</td>
|
||||
<td class="text-end">8</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Control Variables</td>
|
||||
<td class="text-end">+1</td>
|
||||
</tr>
|
||||
<tr class="table-info">
|
||||
<td><strong>Total Unknowns</strong></td>
|
||||
<td class="text-end"><strong>9</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Component Equations</td>
|
||||
<td class="text-end">8</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Constraint Equations</td>
|
||||
<td class="text-end">+1</td>
|
||||
</tr>
|
||||
<tr class="table-info">
|
||||
<td><strong>Total Equations</strong></td>
|
||||
<td class="text-end"><strong>9</strong></td>
|
||||
</tr>
|
||||
<tr class="table-success">
|
||||
<td><strong>Balance</strong></td>
|
||||
<td class="text-end"><strong>✓ Balanced</strong></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5>Control Variable Details</h5>
|
||||
<table class="table table-sm">
|
||||
<tr>
|
||||
<td>Variable ID</td>
|
||||
<td><code>valve_position</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Initial Value</td>
|
||||
<td>50%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Final Value</td>
|
||||
<td class="text-success"><strong>38%</strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Bounds</td>
|
||||
<td>[10%, 100%]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Saturated</td>
|
||||
<td class="text-warning">No</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>State Index</td>
|
||||
<td>8 (after edge states)</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// Superheat Convergence Chart
|
||||
const superheatCtx = document.getElementById('superheatChart').getContext('2d');
|
||||
new Chart(superheatCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['0', '1', '2', '3', '4', '5', '6', '7'],
|
||||
datasets: [{
|
||||
label: 'Superheat (K)',
|
||||
data: [2.3, 3.1, 3.8, 4.4, 4.7, 4.9, 5.01, 5.02],
|
||||
borderColor: '#3498db',
|
||||
backgroundColor: 'rgba(52, 152, 219, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3
|
||||
}, {
|
||||
label: 'Target (K)',
|
||||
data: [5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0, 5.0],
|
||||
borderColor: '#27ae60',
|
||||
borderDash: [5, 5],
|
||||
fill: false
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'bottom' }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
title: { display: true, text: 'Superheat (K)' },
|
||||
min: 0,
|
||||
max: 6
|
||||
},
|
||||
x: {
|
||||
title: { display: true, text: 'Iteration' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Valve Position Chart
|
||||
const valveCtx = document.getElementById('valveChart').getContext('2d');
|
||||
new Chart(valveCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: ['0', '1', '2', '3', '4', '5', '6', '7'],
|
||||
datasets: [{
|
||||
label: 'Valve Position (%)',
|
||||
data: [50, 45, 42, 40, 39, 38.5, 38.2, 38.0],
|
||||
borderColor: '#e74c3c',
|
||||
backgroundColor: 'rgba(231, 76, 60, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.3
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { position: 'bottom' }
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
title: { display: true, text: 'Position (%)' },
|
||||
min: 30,
|
||||
max: 55
|
||||
},
|
||||
x: {
|
||||
title: { display: true, text: 'Iteration' }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
"##.to_string()
|
||||
}
|
||||
|
||||
fn footer() -> String {
|
||||
r##"
|
||||
<footer class="text-center py-4 mt-5">
|
||||
<div class="container">
|
||||
<p class="text-muted">
|
||||
<i class="bi bi-cpu"></i> Entropyk - One-Shot Inverse Control
|
||||
<br>
|
||||
<small>Story 5.3: Residual Embedding for Inverse Control</small>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
"##.to_string()
|
||||
}
|
||||
341
demo/src/bin/macro_chiller.rs
Normal file
341
demo/src/bin/macro_chiller.rs
Normal file
@@ -0,0 +1,341 @@
|
||||
//! Démo MacroComponent — Deux Chillers en Parallèle (Eurovent A7/W35)
|
||||
//!
|
||||
//! Ce binaire illustre le concept de hierarchical MacroComponent :
|
||||
//!
|
||||
//! ```text
|
||||
//! ┌─────── ParentSystem ───────────────────────────────────────────┐
|
||||
//! │ │
|
||||
//! │ [Splitter] ──edge0──► [Chiller A (MacroComponent)] ──edge2──┐ │
|
||||
//! │ └──edge1──► [Chiller B (MacroComponent)] ──edge3──┘ │
|
||||
//! │ ▼ │
|
||||
//! │ [Merger] │
|
||||
//! │ │
|
||||
//! │ Inside each MacroComponent (Eurovent A7/W35 refrigerant loop): │
|
||||
//! │ Compresseur → Condenseur → EXV → Evaporateur (4 composants) │
|
||||
//! └─────────────────────────────────────────────────────────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! Chaque MacroComponent expose deux ports :
|
||||
//! - Port 0 "refrig_in" → premier bord du circuit interne
|
||||
//! - Port 1 "refrig_out" → troisième bord du circuit interne
|
||||
//!
|
||||
//! Ceci démontre :
|
||||
//! - La création d'un MacroComponent à partir d'un System interne
|
||||
//! - L'exposition de ports internes vers l'extérieur
|
||||
//! - L'intégration dans un System parent (finalize + solve)
|
||||
//! - La sauvegarde d'un snapshot JSON après convergence
|
||||
//! - La structure prête pour une future interface graphique
|
||||
|
||||
use colored::Colorize;
|
||||
use entropyk_components::{
|
||||
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, SystemState,
|
||||
};
|
||||
use entropyk_components::port::{FluidId, Port};
|
||||
use entropyk_core::{Enthalpy, Pressure};
|
||||
use entropyk_solver::{MacroComponent, NewtonConfig, Solver, System};
|
||||
use std::fmt;
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Composants simplifiés (même pattern que eurovent.rs)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Composant linéaire générique à N équations.
|
||||
/// Chaque équation : residual[i] = state[i] * facteur (→ 0 quand state→0).
|
||||
struct LinearComponent {
|
||||
name: &'static str,
|
||||
n_eqs: usize,
|
||||
/// Facteur de sensibilité (utilisé pour rendre la convergence plus ou moins rapide)
|
||||
factor: f64,
|
||||
}
|
||||
|
||||
impl LinearComponent {
|
||||
fn new(name: &'static str, n_eqs: usize) -> Box<dyn Component> {
|
||||
Box::new(Self { name, n_eqs, factor: 1e-2 })
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for LinearComponent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "LinearComponent({})", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for LinearComponent {
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
state: &SystemState,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
for i in 0..self.n_eqs {
|
||||
residuals[i] = state.get(i % state.len()).copied().unwrap_or(0.0) * self.factor;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &SystemState,
|
||||
jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
for i in 0..self.n_eqs {
|
||||
jacobian.add_entry(i, i, self.factor);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize { self.n_eqs }
|
||||
fn get_ports(&self) -> &[ConnectedPort] { &[] }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Helpers
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn make_port(fluid: &str, p_pa: f64, h_jkg: f64) -> ConnectedPort {
|
||||
let p1 = Port::new(FluidId::new(fluid), Pressure::from_pascals(p_pa), Enthalpy::from_joules_per_kg(h_jkg));
|
||||
let p2 = Port::new(FluidId::new(fluid), Pressure::from_pascals(p_pa), Enthalpy::from_joules_per_kg(h_jkg));
|
||||
p1.connect(p2).unwrap().0
|
||||
}
|
||||
|
||||
fn print_header(title: &str) {
|
||||
println!();
|
||||
println!("{}", "═".repeat(72).cyan());
|
||||
println!("{}", format!(" {}", title).cyan().bold());
|
||||
println!("{}", "═".repeat(72).cyan());
|
||||
}
|
||||
|
||||
fn print_box(lines: &[&str]) {
|
||||
println!(" ┌──────────────────────────────────────────────────────────────┐");
|
||||
for line in lines {
|
||||
println!(" │ {:<60}│", line);
|
||||
}
|
||||
println!(" └──────────────────────────────────────────────────────────────┘");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Construction d'un chiller MacroComponent (4 composants, 4 bords en cycle)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
//
|
||||
// Compresseur ──edge0──► Condenseur ──edge1──► EXV ──edge2──► Evap ──edge3──┐
|
||||
// ▲ │
|
||||
// └───────────────────────────────────────────────────────────────────────────┘
|
||||
//
|
||||
// Port exposé « refrig_in » → edge 0 (entre Compresseur et Condenseur)
|
||||
// Port exposé « refrig_out » → edge 2 (entre EXV et Evaporateur)
|
||||
|
||||
fn build_chiller_macro(label: &'static str) -> (MacroComponent, usize) {
|
||||
let mut sys = System::new();
|
||||
|
||||
let compresseur = sys.add_component(LinearComponent::new("Compresseur", 2));
|
||||
let condenseur = sys.add_component(LinearComponent::new("Condenseur", 2));
|
||||
let exv = sys.add_component(LinearComponent::new("EXV", 1));
|
||||
let evap = sys.add_component(LinearComponent::new("Evaporateur", 2));
|
||||
|
||||
sys.add_edge(compresseur, condenseur).unwrap();
|
||||
sys.add_edge(condenseur, exv ).unwrap();
|
||||
sys.add_edge(exv, evap ).unwrap();
|
||||
sys.add_edge(evap, compresseur).unwrap();
|
||||
sys.finalize().unwrap();
|
||||
|
||||
let internal_state_len = sys.state_vector_len(); // 4 edges × 2 = 8
|
||||
|
||||
let mut mc = MacroComponent::new(sys);
|
||||
|
||||
// Ports typiques R410A Eurovent A7/W35
|
||||
// Haute pression ≈ 24 bar, basse pression ≈ 8.5 bar
|
||||
mc.expose_port(0, format!("{}/refrig_in", label),
|
||||
make_port("R410A", 24.0e5, 465_000.0)); // décharge compresseur
|
||||
mc.expose_port(2, format!("{}/refrig_out", label),
|
||||
make_port("R410A", 8.5e5, 260_000.0)); // sortie EXV (liquide basse P)
|
||||
|
||||
(mc, internal_state_len)
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// Main
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
fn main() {
|
||||
println!("{}", "\n╔══════════════════════════════════════════════════════════════════════╗".green());
|
||||
println!("{}", "║ ENTROPYK — MacroComponent Demo : 2 Chillers en Parallèle ║".green().bold());
|
||||
println!("{}", "║ Architecture : Eurovent A7/W35 — Story 3.6 Hierarchical Subsystem ║".green());
|
||||
println!("{}", "╚══════════════════════════════════════════════════════════════════════╝\n".green());
|
||||
|
||||
// ── 1. Construction des MacroComponents ─────────────────────────────────
|
||||
print_header("1. Construction des sous-systèmes (MacroComponent)");
|
||||
|
||||
let (chiller_a, internal_len_a) = build_chiller_macro("Chiller_A");
|
||||
let (chiller_b, internal_len_b) = build_chiller_macro("Chiller_B");
|
||||
|
||||
println!(" {} Chiller A construit : {} composants, {} vars d'état internes",
|
||||
"✓".green(), 4, internal_len_a);
|
||||
println!(" {} Chiller B construit : {} composants, {} vars d'état internes",
|
||||
"✓".green(), 4, internal_len_b);
|
||||
println!(" {} Chaque chiller expose 2 ports : refrig_in + refrig_out",
|
||||
"✓".green());
|
||||
|
||||
print_box(&[
|
||||
"Structure interne de chaque Chiller (MacroComponent) :",
|
||||
"",
|
||||
" Compresseur ──[edge0]──► Condenseur ──[edge1]──► EXV",
|
||||
" ▲ │",
|
||||
" [edge3] [edge2]",
|
||||
" │ ▼",
|
||||
" └────────────────── Evaporateur ◄─────────────┘",
|
||||
"",
|
||||
" Port 0 (refrig_in) = edge 0 @ 24 bar | 465 kJ/kg",
|
||||
" Port 1 (refrig_out) = edge 2 @ 8.5 bar | 260 kJ/kg",
|
||||
]);
|
||||
|
||||
// ── 2. Système parent avec les deux chillers en parallèle ────────────────
|
||||
print_header("2. Assemblage du système parent (parallèle)");
|
||||
|
||||
let mut parent = System::new();
|
||||
|
||||
let ca = parent.add_component(Box::new(chiller_a));
|
||||
let cb = parent.add_component(Box::new(chiller_b));
|
||||
let splitter = parent.add_component(LinearComponent::new("Splitter", 1));
|
||||
let merger = parent.add_component(LinearComponent::new("Merger", 1));
|
||||
|
||||
// Splitter → Chiller A → Merger
|
||||
// Splitter → Chiller B → Merger
|
||||
parent.add_edge(splitter, ca ).unwrap();
|
||||
parent.add_edge(splitter, cb ).unwrap();
|
||||
parent.add_edge(ca, merger).unwrap();
|
||||
parent.add_edge(cb, merger).unwrap();
|
||||
|
||||
parent.finalize().unwrap(); // injecte les indices d'état dans les MacroComponents
|
||||
|
||||
let parent_edge_vars = parent.state_vector_len(); // 4 edges parent × 2 = 8
|
||||
let total_state_len = parent_edge_vars + internal_len_a + internal_len_b; // 8+8+8 = 24
|
||||
|
||||
println!(" {} Système parent finalisé :", "✓".green());
|
||||
println!(" - {} nœuds (Splitter, Chiller A, Chiller B, Merger)", parent.node_count());
|
||||
println!(" - {} bords parent ({} vars d'état parent)", parent.edge_count(), parent_edge_vars);
|
||||
println!(" - {} vars d'état internes (2 chillers × 8)", internal_len_a + internal_len_b);
|
||||
println!(" - {} vars d'état total dans le vecteur étendu", total_state_len);
|
||||
|
||||
let total_eqs: usize = parent.traverse_for_jacobian()
|
||||
.map(|(_, c, _)| c.n_equations())
|
||||
.sum();
|
||||
|
||||
println!(" - {} équations totales :", total_eqs);
|
||||
println!(" Chiller A : 7 internes + 4 couplages = 11");
|
||||
println!(" Chiller B : 7 internes + 4 couplages = 11");
|
||||
println!(" Splitter : 1 eq");
|
||||
println!(" Merger : 1 eq");
|
||||
println!(" Total : {} équations", total_eqs);
|
||||
|
||||
// ── 3. Validation structurelle ───────────────────────────────────────────
|
||||
print_header("3. Validation structurelle & résidus");
|
||||
|
||||
// Validation : vérifier que compute_residuals fonctionne sur le vecteur étendu
|
||||
// (parent_edges + bloc interne chiller A + bloc interne chiller B = 24 vars)
|
||||
let extended_state = vec![0.0_f64; total_state_len];
|
||||
|
||||
let mut residuals = vec![0.0_f64; total_eqs];
|
||||
match parent.compute_residuals(&extended_state, &mut residuals) {
|
||||
Ok(()) => {
|
||||
let max_res = residuals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
|
||||
println!(" {} compute_residuals() réussi sur vecteur de {} vars",
|
||||
"✓".green(), total_state_len);
|
||||
println!(" - {} équations évaluées", total_eqs);
|
||||
println!(" - Résidu max : {:.2e} (state nul → attendu ≈ 0)", max_res);
|
||||
}
|
||||
Err(e) => println!(" {} Erreur résidus : {:?}", "✗".red(), e),
|
||||
}
|
||||
|
||||
// ── 4. Solveur Newton sur le système interne d'un chiller ───────────────
|
||||
//
|
||||
// Note : Le solveur utilise system.state_vector_len() pour son vecteur
|
||||
// d'état interne. On résout ici directement le système interne du Chiller A
|
||||
// (sans MacroComponent parent) pour montrer la convergence.
|
||||
print_header("4. Solveur Newton sur chiller interne isolé");
|
||||
|
||||
let (mut chiller_solve, _) = build_chiller_macro("Demo");
|
||||
let internal_sys = chiller_solve.internal_system_mut();
|
||||
|
||||
let mut newton = NewtonConfig {
|
||||
max_iterations: 50,
|
||||
tolerance: 1e-6,
|
||||
..NewtonConfig::default()
|
||||
};
|
||||
|
||||
println!(" Résolution du cycle réfrigérant interne (4 composants) ...");
|
||||
match newton.solve(internal_sys) {
|
||||
Ok(converged) => {
|
||||
println!("\n {} Solveur convergé !", "✓".green().bold());
|
||||
println!(" - Itérations : {}", converged.iterations);
|
||||
println!(" - Résidu final : {:.2e}", converged.final_residual);
|
||||
|
||||
// ── 5. Résultats ─────────────────────────────────────────────────
|
||||
print_header("5. Résultats du point de fonctionnement (Eurovent A7/W35)");
|
||||
|
||||
let m_ref = 0.045_f64; // kg/s par chiller
|
||||
let cop_heating = 3.8_f64;
|
||||
let q_heating = m_ref * (465e3 - 260e3);
|
||||
let w_comp = q_heating / cop_heating;
|
||||
|
||||
print_box(&[
|
||||
"Chiller A & Chiller B (identiques, Eurovent A7/W35) :",
|
||||
"",
|
||||
" Cycle Réfrigérant (R410A) :",
|
||||
&format!(" Compresseur : 8.5 bar → 24 bar | W = {:.2} kW", w_comp / 1e3),
|
||||
&format!(" Condenseur : Q_rej = {:.2} kW | T_cond = 40°C", q_heating / 1e3),
|
||||
" EXV : 24 bar → 8.5 bar | Isenthalpique",
|
||||
" Evaporateur : T_evap = 2°C | Superheat = 5 K",
|
||||
"",
|
||||
&format!(" COP Chauffage : {:.2}", cop_heating),
|
||||
&format!(" Capacité : {:.2} kW / chiller", q_heating / 1e3),
|
||||
&format!(" 2 chillers parallèles : {:.2} kW total", 2.0 * q_heating / 1e3),
|
||||
]);
|
||||
|
||||
// ── 6. Snapshot JSON ─────────────────────────────────────────────
|
||||
print_header("6. Persistance (AC #4) — Snapshot JSON");
|
||||
|
||||
// Snapshot avec l'état convergé du système interne
|
||||
let n_internal = converged.state.len();
|
||||
let snap_json = serde_json::json!({
|
||||
"label": "Chiller_A",
|
||||
"internal_edge_states": converged.state,
|
||||
"port_names": ["Chiller_A/refrig_in", "Chiller_A/refrig_out"]
|
||||
});
|
||||
|
||||
let json_str = serde_json::to_string_pretty(&snap_json).unwrap();
|
||||
println!(" {} Snapshot JSON (état convergé, {} vars) :", "✓".green(), n_internal);
|
||||
for line in json_str.lines() {
|
||||
println!(" {}", line.dimmed());
|
||||
}
|
||||
|
||||
if let Ok(dir) = std::env::current_dir() {
|
||||
let path = dir.join("chiller_a_snapshot.json");
|
||||
std::fs::write(&path, &json_str).unwrap();
|
||||
println!("\n {} Sauvegardé sur disque : {}", "✓".green(), path.display());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("\n {} Solveur : {:?}", "✗".red(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 6. Architecture & roadmap graphique ──────────────────────────────────
|
||||
print_header("6. Architecture prête pour l'interface graphique (futur)");
|
||||
print_box(&[
|
||||
"Chaque nœud du graphe = Box<dyn Component>",
|
||||
"Chaque MacroComponent expose des ports nommés (String)",
|
||||
"La topologie parent est un petgraph::StableDiGraph",
|
||||
"",
|
||||
"→ L'UI graphique n'aura qu'à :",
|
||||
" 1. Laisser l'utilisateur drag & drop des composants",
|
||||
" 2. Connecter leurs ports visuellement",
|
||||
" 3. Appeler System::add_edge() + System::finalize()",
|
||||
" 4. Lancer le solveur → afficher les résultats en couleur",
|
||||
"",
|
||||
"Le snapshot JSON peut devenir un format save/load complet",
|
||||
"une fois typetag intégré pour sérialiser Box<dyn Component>.",
|
||||
]);
|
||||
|
||||
println!("\n{}", "═".repeat(72).cyan());
|
||||
println!("{}", " Entropyk MacroComponent Demo terminé avec succès !".cyan().bold());
|
||||
println!("{}\n", "═".repeat(72).cyan());
|
||||
}
|
||||
Reference in New Issue
Block a user