同步仿真框架并接入AMESim气动组件

This commit is contained in:
huojiarong committed 2026-07-30 03:53:10 +00:00
1 parent 420bafeb4e
commit db4bdb4b70
109 files changed
+26920 -420

No files matched your search

@@ -0,0 +1,4 @@
mytank.p: max_abs_error=134.960858, max_rel_error=0.006798%
mytank.T: max_abs_error=0.035507, max_rel_error=0.009016%
mycylinder.p: max_abs_error=1391.986349, max_rel_error=0.009447%
mycylinder.T: max_abs_error=0.009069, max_rel_error=0.003870%
@@ -0,0 +1,202 @@
time_s,mytank.p,mytank.T,mycylinder.p,mycylinder.T
0.0,99999.9999998181,299.9999999994543,35000000.0,300.0
0.1,113767.94796194455,310.9637987101763,34857995.191961475,299.65190130981887
0.2,127490.75529043324,320.1741038867186,34716455.97160761,299.3039342009557
0.30000000000000004,141168.22832317703,327.9540327439692,34575384.336400226,298.9561065982938
0.4,154800.2272891848,334.6028385113769,34434781.727959625,298.60842486666263
0.5,168386.6833862148,340.3440160419025,34294648.85592305,298.26089368263627
0.6000000000000001,181927.56886228317,345.34597934661645,34154986.006528884,297.9135169662631
0.7000000000000001,195422.8867284584,349.7379443903134,34015793.14872051,297.5662980942661
0.8,208872.67226727083,353.62071952544704,33877069.91858829,297.2192381374397
0.9,222277.06297793236,357.0742683513725,33738814.897943415,296.8723276411647
1.0,235635.98150118813,360.16219760966084,33601028.88468161,296.5255848498291
1.1,248949.42783703818,362.9362624751423,33463711.878802836,296.1790180498007
1.2000000000000002,262217.75812283135,365.43943715392686,33326860.20704978,295.8325994898035
1.3,275440.80324992316,367.7064864598734,33190475.613635924,295.48635453607636
1.4000000000000001,288618.55460473493,369.766650344722,33054558.187403098,295.14029246566224
1.5,301751.3171281971,371.6448887937757,32919104.783141974,294.79439468638526
1.6,314839.14341954247,373.36206261131525,32784114.858335685,294.44866485903736
1.7000000000000002,327882.06907680153,374.93594217338176,32649588.04582047,294.10310819940065
1.8,340880.20077679906,376.3818124575356,32515523.2453146,293.7577259967503
1.9000000000000001,353833.6270518691,377.7128659999238,32381919.54368144,293.41251959012544
2.0,366742.42291137256,378.94054912194076,32248776.16726251,293.06749036423287
2.1,379606.6730674497,380.0748368029417,32116092.24232331,292.72263968200787
2.2,392426.50707225554,381.12448095768656,31983866.432642274,292.37796667994184
2.3000000000000003,405202.0671563814,382.0971825271288,31852097.271230437,292.03346983037886
2.4000000000000004,417933.4155500976,382.9997115435689,31720784.11623488,291.6891515780112
2.5,430620.6510589185,383.838081019396,31589925.948559783,291.3450125479824
2.6,443263.883710829,384.61764092425494,31459521.633358993,291.0010529534012
2.7,455863.2542884696,385.3431706778388,31329569.71857793,290.6572718762472
2.8000000000000003,468418.84583467664,386.0189391423481,31200069.34769955,290.31367046787994
2.9000000000000004,480930.76401047717,386.64878296917834,31071019.430919208,289.9702490545257
3.0,493399.1147767602,387.2361564731079,30942418.8753394,289.627007973335
3.1,505824.0120818939,387.7841699997202,30814266.50567852,289.283948167411
3.2,518205.5585633492,388.29565044444143,30686561.263317347,288.9410696919777
3.3000000000000003,530543.860941083,388.7731587646163,30559302.04752924,288.5983729053581
3.4000000000000004,542839.025935053,389.21902406251183,30432487.75758757,288.2558581528899
3.5,555091.1532282452,389.63535331051384,30306117.365346134,287.9135275289302
3.6,567300.3523937837,390.0240928732544,30180189.74065028,287.5713806480376
3.7,579466.730902255,390.3870196149787,30054703.77503127,287.2294176336731
3.8000000000000003,591590.3951155862,390.7257605811333,29929658.371455234,286.88763887333175
3.9000000000000004,603671.4513957056,391.0418105423231,29805052.432888325,286.5460447413264
4.0,615710.0061045411,391.3365451815722,29680884.862296656,286.20463559857706
4.1000000000000005,627706.1656040212,391.61123273483645,29557154.562646367,285.8634117923989
4.2,639660.0356829006,391.86704273384044,29433860.4428154,285.5223738511199
4.3,651571.716232906,392.1050410519721,29311001.472504564,285.1815242995298
4.4,663441.3179459961,392.32624321858174,29188576.510019373,284.840861980108
4.5,675268.9468771729,392.53157932311984,29066584.46149164,284.5003872946073
4.6000000000000005,687054.7090814385,392.72191331429804,28945024.2330532,284.16010063101663
4.7,698798.7106137947,392.8980487868277,28823894.730835862,283.8200023633402
4.800000000000001,710501.057529244,393.06073417111315,28703194.86097143,283.48009285137505
4.9,722161.8558827877,393.21066739655134,28582923.529591747,283.1403724404863
5.0,733781.2088290841,393.3484951327475,28463079.672743235,282.8008422184954
5.1000000000000005,745359.2175579917,393.47481852250473,28343662.24673756,282.4615037792963
5.2,756895.9922909128,393.5902150621596,28224670.114733644,282.1223563449153
5.300000000000001,768391.6386869826,393.6952183545109,28106102.186946325,281.78340031124986
5.4,779846.2624053361,393.7903288312906,27987957.373590477,281.44463605959197
5.5,791259.96910511,393.8760162992802,27870234.58488091,281.10606395639473
5.6000000000000005,802632.8644454395,393.9527222554955,27752932.731032487,280.76768435303745
5.7,813965.0540854601,394.02086199551263,27636050.72226007,280.4294975855874
5.800000000000001,825256.6344365194,394.08081904264304,27519587.564161647,280.0915054160418
5.9,836507.7093909897,394.13296077650165,27403542.18517475,279.75370843660113
6.0,847718.3891245491,394.17763867488134,27287913.44892988,279.4161062418491
6.1000000000000005,858888.7786008355,394.2151805066602,27172700.272815377,279.0786992215644
6.2,870018.9827834871,394.2458959595685,27057901.57421954,278.7414877501898
6.300000000000001,881109.1066361419,394.27007787647636,26943516.2705307,278.40447218658807
6.4,892159.2551224378,394.2880033916485,26829543.279137183,278.0676528737956
6.5,903169.5332060129,394.2999349762812,26715981.5174273,277.73103013877227
6.6000000000000005,914139.9829839376,394.30609016406663,26602830.551205155,277.3946116451124
6.7,925070.7482002805,394.30672506364067,26490088.897871558,277.0583932025842
6.800000000000001,935961.9434794089,394.30206871636136,26377755.375172496,276.7223739769823
6.9,946813.6729747849,394.2923336718555,26265828.9088526,276.3865543393284
7.0,957626.040839869,394.2777219546483,26154308.424656466,276.05093464451556
7.1000000000000005,968399.1512281233,394.2584257130283,26043192.84832872,275.7155152310525
7.2,979133.1082930088,394.23462782052076,25932481.10561397,275.3802964208052
7.300000000000001,989828.0161879869,394.2065024339623,25822172.12225682,275.04527851873627
7.4,1000483.9790665191,394.17421551178546,25712264.824001882,274.71046181264023
7.5,1011101.1010820667,394.13792529578427,25602758.136593778,274.3758465728772
7.6000000000000005,1021679.4863880915,394.09778275932865,25493650.985777102,274.04143305210266
7.7,1032219.239138054,394.0539320247227,25384942.297296483,273.707221484995
7.800000000000001,1042720.4634854163,394.0065107521557,25276630.996896524,273.37321208797937
7.9,1053183.2635836392,393.95565050247825,25168716.01032184,273.0394050589494
8.0,1063607.7435861847,393.9014770758345,25061196.263317037,272.705800576985
8.1,1073994.0076465139,393.8441108280058,24954070.681626726,272.3723988020679
8.200000000000001,1084342.0075784405,393.78360959211744,24847339.76225091,272.03921728278755
8.3,1094651.950514763,393.7201237653114,24741001.368788917,271.7062444960846
8.4,1104923.9566483083,393.65376548392067,24635054.261552785,271.3734787513138
8.5,1115158.1282653515,393.5846357175065,24529497.385545585,271.0409203804046
8.6,1125354.567652169,393.5128313057523,24424329.685770374,270.7085696976142
8.700000000000001,1135513.3770950353,393.43844517133164,24319550.10723021,270.37642699925055
8.8,1145634.6588802272,393.3615665197664,24215157.594928175,270.0444925633909
8.9,1155718.5152940191,393.2822810271899,24111151.093867306,269.712766649598
9.0,1165765.0486226876,393.2006710168648,24007529.54905069,269.3812494986329
9.1,1175774.361152508,393.11681562522904,23904291.90548136,269.04994133216485
9.200000000000001,1185746.555169756,393.03079095819226,23801437.108162407,268.7188423524781
9.3,1195681.732960707,392.94267023834027,23698964.10209688,268.3879527421748
9.4,1205579.9968116365,392.8525239436615,23596871.83228785,268.05727266387584
9.5,1215441.4490088206,392.7604199383583,23495159.243738372,267.72680225991735
9.600000000000001,1225266.1918385345,392.66642359626576,23393825.281451505,267.39654165204473
9.700000000000001,1235054.3275870536,392.57059791736026,23292868.89043032,267.0664909411029
9.8,1244805.7424247153,392.47293675670085,23192291.244732097,266.7366751290975
9.9,1254520.6963966596,392.3735487251935,23092089.662209835,266.40707625875064
10.0,1264199.3049931854,392.2724947546991,22992262.951678198,266.07769289844276
10.100000000000001,1273841.6679760527,392.1698289153906,22892810.0841785,265.74852540655036
10.200000000000001,1283447.8851070204,392.0656034102887,22793730.030752078,265.41957412301247
10.3,1293018.0561478487,391.9598686580462,22695021.76244025,265.0908393690368
10.4,1302552.2808602974,391.8526733713811,22596684.250284337,264.76232144680336
10.5,1312050.6590061258,391.7440646314241,22498716.465325654,264.4340206391643
10.600000000000001,1321513.290347094,391.63408795822716,22401117.37860553,264.10593720934105
10.700000000000001,1330940.2746449616,391.5227873776613,22303885.961165283,263.7780714006174
10.8,1340331.711661488,391.41020548491883,22207021.184046242,263.4504234360302
10.9,1349687.7011584332,391.29638350481804,22110522.01828973,263.1229935180553
11.0,1359008.342897557,391.18136134909884,22014387.434937052,262.79578182829164
11.100000000000001,1368293.7366406189,391.0651776708804,21918616.405029543,262.4687885271409
11.200000000000001,1377543.9821493784,390.9478699164463,21823207.89960853,262.1420137534841
11.3,1386759.1791855951,390.82947437450605,21728160.88971532,261.81545762435434
11.4,1395939.2416852904,390.70997702085253,21633476.26302751,261.4891421257888
11.5,1405084.4058089943,390.5894488404711,21539151.583747786,261.1630514204039
11.600000000000001,1414194.7727817593,390.46792355037144,21445185.807824824,260.83718544108797
11.700000000000001,1423270.4378832965,390.3454323238288,21351577.952528503,260.5115448028764
11.8,1432311.4962235806,390.22200536933536,21258327.036879413,260.1861301247683
11.9,1441318.042742851,390.0976719679259,21165432.0816488,259.86094202976824
12.0,1450290.1722116095,389.97246050877925,21072892.1093586,259.5359811449295
12.100000000000001,1459227.9792306225,389.84639852319333,20980706.14428146,259.2112481013979
12.200000000000001,1468131.5582309205,389.71951271701596,20888873.212440677,258.88674353445447
12.3,1477001.0034737969,389.59182900161403,20797392.34161027,258.5624680835606
12.4,1485836.4090508097,389.46337252345796,20706262.561314918,258.23842239240224
12.5,1494637.8688837802,389.3341676923915,20615482.902829994,257.9146071089349
12.600000000000001,1503405.4767247946,389.20423820865625,20525052.399181567,257.5910228854296
12.700000000000001,1512139.3261562001,389.07360708873284,20434970.085146382,257.26767037851835
12.8,1520839.5105906113,388.94229669006046,20345234.997251876,256.9445502492407
12.9,1529506.1233409408,388.81032876227204,20255846.173053782,256.62166315000906
13.0,1538139.2687433015,388.67772882293264,20166802.53641284,256.2990076411302
13.100000000000001,1546739.030251722,388.5445136335602,20078103.22657088,255.97658615427704
13.200000000000001,1555305.5007360894,388.4107031827765,19989747.285653118,255.65439933992917
13.3,1563838.7729005008,388.27631690939495,19901733.757494725,255.332447852161
13.4,1572338.9392832702,388.14137372147235,19814061.687640797,255.0107323486784
13.5,1580806.0922569225,388.0058920145713,19726730.123346385,254.68925349085504
13.600000000000001,1589240.3240281995,387.86988968927403,19639738.113576483,254.36801194376957
13.700000000000001,1597641.7266380545,387.73338416798015,19553084.709006038,254.04700837624307
13.8,1606010.3919616563,387.59639241102605,19466768.962019928,253.72624346087662
13.9,1614346.4117083861,387.458930932153,19380789.926712975,253.40571787408948
14.0,1622649.8774218408,387.32101581335974,19295146.658889957,253.0854322961577
14.100000000000001,1630920.8804798292,387.18266271916264,19209838.21606559,252.76538741125262
14.200000000000001,1639159.5120943757,387.0438869102949,19124863.657464534,252.4455839074805
14.3,1647365.8633117182,386.90470325686863,19040222.0440214,252.12602247692183
14.4,1655540.0250123062,386.76512625102305,18955912.438380726,251.8067038156715
14.5,1663682.087910807,386.6251700190859,18871933.904897016,251.48762862387906
14.600000000000001,1671792.1425560997,386.4848483332645,18788285.509634707,251.16879760578948
14.700000000000001,1679870.2793312764,386.3441746228917,18704966.320368182,250.85021146978437
14.8,1687916.588453644,386.2031619852454,18621975.406581767,250.5318709284234
14.9,1695931.1599747243,386.06182319595837,18539311.83946974,250.21377669848638
15.0,1703914.0837802505,385.920170719039,18456974.691936314,249.8959295010154
15.100000000000001,1711865.4494154856,385.77821670970445,18374963.040397402,249.57833006564294
15.200000000000001,1719785.3379250832,385.6359727064743,18293276.048945524,249.26097933285033
15.3,1727673.8459201432,385.4934505781353,18211912.721118417,248.94387786218365
15.4,1735531.0625176337,385.3506616315768,18130872.137749474,248.6270263958575
15.5,1743357.0766656764,385.2076169067951,18050153.381413613,248.31042568075102
15.600000000000001,1751151.9771435438,385.06432718490083,17969755.53642727,247.99407646845765
15.700000000000001,1758915.8525616606,384.9208029958382,17889677.688848406,247.67797951533504
15.8,1766648.7913616048,384.77705462583015,17809918.926476505,247.36213558255577
15.9,1774350.881816107,384.63309212455715,17730478.33885257,247.04654543615806
16.0,1782022.2120290494,384.4889253120841,17651355.017259125,246.73120984709743
16.1,1789662.8699354655,384.3445637855432,17572548.05472022,246.41612959129878
16.2,1797272.9433015438,384.2000169255848,17494056.546001427,246.10130544970855
16.3,1804852.5197246224,384.05529390260284,17415879.58760983,245.78673820834797
16.400000000000002,1812401.6866331936,383.9104036827472,17338016.277794052,245.47242865836654
16.5,1819920.5312869006,383.76535503372884,17260465.716544226,245.15837759609565
16.6,1827409.1407765402,383.6201565304272,17183227.005592,244.84458582310356
16.7,1834867.6020240602,383.47481656030743,17106299.24841057,244.53105414625009
16.8,1842296.0017825624,383.32934332865534,17029681.550214626,244.2177833777421
16.900000000000002,1849694.4266362996,383.1837448636361,16953373.017960392,243.9047743351897
17.0,1857062.9630006768,383.03802902118434,16877372.76034562,243.59202784166294
17.1,1864401.697122252,382.8922034897328,16801679.887809563,243.27954472574828
17.2,1871710.7150787353,382.7462757947841,16726293.512533028,242.96732582160686
17.3,1878990.1030524147,382.60025337999576,16651212.745618157,242.65537190477806
17.400000000000002,1886239.954455942,382.4541455990818,16576436.623591991,242.343682012199
17.5,1893460.3482686526,382.3079576538116,16501964.331849081,242.03225853722532
17.6,1900651.3699906773,382.16169648335216,16427794.988527464,241.72110231102408
17.7,1907813.104956132,382.0153688850827,16353927.713477474,241.41021416968596
17.8,1914945.6383331183,381.86898151834066,16280361.628261749,241.09959495427398
17.900000000000002,1922049.0551237254,381.7225409080495,16207095.856155202,240.78924551087363
18.0,1929123.4401640275,381.57605344823065,16134129.522145053,240.47916669064318
18.1,1936168.8781240864,381.4295254054061,16061461.752930798,240.16935934986387
18.2,1943185.4535079484,381.28296292189384,15989091.67692425,239.8598243499916
18.3,1950173.2506536485,381.1363720190009,15917018.424249483,239.55056255770802
18.400000000000002,1957132.3537332045,380.98975860011734,15845241.126742886,239.24157484497272
18.5,1964062.8467526236,380.84312845371574,15773758.917953137,238.93286208907583
18.6,1970964.8135518986,380.6964872562572,15702570.933141202,238.6244251726908
18.7,1977838.3378050062,380.54984057500997,15631676.309280336,238.31626498392774
18.8,1984683.5030199126,380.4031938707821,15561074.185056096,238.00838241638746
18.900000000000002,1991500.3925385692,380.25655250057133,15490763.700866321,237.7007783692156
19.0,1998289.0895369116,380.10992172013556,15420743.998821149,237.39345374715774
19.1,2005049.677024864,379.96330668648653,15351014.222743012,237.08640946061425
19.200000000000003,2011782.237846337,379.81671246030885,15281573.518166626,236.77964642569634
19.3,2018486.8546792252,379.67014400830794,15212421.032339014,236.47316556428217
19.400000000000002,2025163.610035411,379.5236062054879,15143555.914219463,236.16696780407335
19.5,2031812.5863479478,379.3771038539995,15074977.31358037,235.861054060844
19.6,2038433.8680519294,379.2306420805889,15006684.359544702,235.55542481191287
19.700000000000003,2045027.536485746,379.084225365391,14938676.213175347,235.25008113730536
19.8,2051593.6739729291,378.9378583245209,14870952.025374293,234.9450238874766
19.900000000000002,2058132.3627231135,378.79154550141357,14803510.948218277,234.64025390703284
20.0,2064643.6848320398,378.64529136859073,14736352.134958768,234.33577203462033
1 time_s mytank.p mytank.T mycylinder.p mycylinder.T
2 0.0 99999.9999998181 299.9999999994543 35000000.0 300.0
3 0.1 113767.94796194455 310.9637987101763 34857995.191961475 299.65190130981887
4 0.2 127490.75529043324 320.1741038867186 34716455.97160761 299.3039342009557
5 0.30000000000000004 141168.22832317703 327.9540327439692 34575384.336400226 298.9561065982938
6 0.4 154800.2272891848 334.6028385113769 34434781.727959625 298.60842486666263
7 0.5 168386.6833862148 340.3440160419025 34294648.85592305 298.26089368263627
8 0.6000000000000001 181927.56886228317 345.34597934661645 34154986.006528884 297.9135169662631
9 0.7000000000000001 195422.8867284584 349.7379443903134 34015793.14872051 297.5662980942661
10 0.8 208872.67226727083 353.62071952544704 33877069.91858829 297.2192381374397
11 0.9 222277.06297793236 357.0742683513725 33738814.897943415 296.8723276411647
12 1.0 235635.98150118813 360.16219760966084 33601028.88468161 296.5255848498291
13 1.1 248949.42783703818 362.9362624751423 33463711.878802836 296.1790180498007
14 1.2000000000000002 262217.75812283135 365.43943715392686 33326860.20704978 295.8325994898035
15 1.3 275440.80324992316 367.7064864598734 33190475.613635924 295.48635453607636
16 1.4000000000000001 288618.55460473493 369.766650344722 33054558.187403098 295.14029246566224
17 1.5 301751.3171281971 371.6448887937757 32919104.783141974 294.79439468638526
18 1.6 314839.14341954247 373.36206261131525 32784114.858335685 294.44866485903736
19 1.7000000000000002 327882.06907680153 374.93594217338176 32649588.04582047 294.10310819940065
20 1.8 340880.20077679906 376.3818124575356 32515523.2453146 293.7577259967503
21 1.9000000000000001 353833.6270518691 377.7128659999238 32381919.54368144 293.41251959012544
22 2.0 366742.42291137256 378.94054912194076 32248776.16726251 293.06749036423287
23 2.1 379606.6730674497 380.0748368029417 32116092.24232331 292.72263968200787
24 2.2 392426.50707225554 381.12448095768656 31983866.432642274 292.37796667994184
25 2.3000000000000003 405202.0671563814 382.0971825271288 31852097.271230437 292.03346983037886
26 2.4000000000000004 417933.4155500976 382.9997115435689 31720784.11623488 291.6891515780112
27 2.5 430620.6510589185 383.838081019396 31589925.948559783 291.3450125479824
28 2.6 443263.883710829 384.61764092425494 31459521.633358993 291.0010529534012
29 2.7 455863.2542884696 385.3431706778388 31329569.71857793 290.6572718762472
30 2.8000000000000003 468418.84583467664 386.0189391423481 31200069.34769955 290.31367046787994
31 2.9000000000000004 480930.76401047717 386.64878296917834 31071019.430919208 289.9702490545257
32 3.0 493399.1147767602 387.2361564731079 30942418.8753394 289.627007973335
33 3.1 505824.0120818939 387.7841699997202 30814266.50567852 289.283948167411
34 3.2 518205.5585633492 388.29565044444143 30686561.263317347 288.9410696919777
35 3.3000000000000003 530543.860941083 388.7731587646163 30559302.04752924 288.5983729053581
36 3.4000000000000004 542839.025935053 389.21902406251183 30432487.75758757 288.2558581528899
37 3.5 555091.1532282452 389.63535331051384 30306117.365346134 287.9135275289302
38 3.6 567300.3523937837 390.0240928732544 30180189.74065028 287.5713806480376
39 3.7 579466.730902255 390.3870196149787 30054703.77503127 287.2294176336731
40 3.8000000000000003 591590.3951155862 390.7257605811333 29929658.371455234 286.88763887333175
41 3.9000000000000004 603671.4513957056 391.0418105423231 29805052.432888325 286.5460447413264
42 4.0 615710.0061045411 391.3365451815722 29680884.862296656 286.20463559857706
43 4.1000000000000005 627706.1656040212 391.61123273483645 29557154.562646367 285.8634117923989
44 4.2 639660.0356829006 391.86704273384044 29433860.4428154 285.5223738511199
45 4.3 651571.716232906 392.1050410519721 29311001.472504564 285.1815242995298
46 4.4 663441.3179459961 392.32624321858174 29188576.510019373 284.840861980108
47 4.5 675268.9468771729 392.53157932311984 29066584.46149164 284.5003872946073
48 4.6000000000000005 687054.7090814385 392.72191331429804 28945024.2330532 284.16010063101663
49 4.7 698798.7106137947 392.8980487868277 28823894.730835862 283.8200023633402
50 4.800000000000001 710501.057529244 393.06073417111315 28703194.86097143 283.48009285137505
51 4.9 722161.8558827877 393.21066739655134 28582923.529591747 283.1403724404863
52 5.0 733781.2088290841 393.3484951327475 28463079.672743235 282.8008422184954
53 5.1000000000000005 745359.2175579917 393.47481852250473 28343662.24673756 282.4615037792963
54 5.2 756895.9922909128 393.5902150621596 28224670.114733644 282.1223563449153
55 5.300000000000001 768391.6386869826 393.6952183545109 28106102.186946325 281.78340031124986
56 5.4 779846.2624053361 393.7903288312906 27987957.373590477 281.44463605959197
57 5.5 791259.96910511 393.8760162992802 27870234.58488091 281.10606395639473
58 5.6000000000000005 802632.8644454395 393.9527222554955 27752932.731032487 280.76768435303745
59 5.7 813965.0540854601 394.02086199551263 27636050.72226007 280.4294975855874
60 5.800000000000001 825256.6344365194 394.08081904264304 27519587.564161647 280.0915054160418
61 5.9 836507.7093909897 394.13296077650165 27403542.18517475 279.75370843660113
62 6.0 847718.3891245491 394.17763867488134 27287913.44892988 279.4161062418491
63 6.1000000000000005 858888.7786008355 394.2151805066602 27172700.272815377 279.0786992215644
64 6.2 870018.9827834871 394.2458959595685 27057901.57421954 278.7414877501898
65 6.300000000000001 881109.1066361419 394.27007787647636 26943516.2705307 278.40447218658807
66 6.4 892159.2551224378 394.2880033916485 26829543.279137183 278.0676528737956
67 6.5 903169.5332060129 394.2999349762812 26715981.5174273 277.73103013877227
68 6.6000000000000005 914139.9829839376 394.30609016406663 26602830.551205155 277.3946116451124
69 6.7 925070.7482002805 394.30672506364067 26490088.897871558 277.0583932025842
70 6.800000000000001 935961.9434794089 394.30206871636136 26377755.375172496 276.7223739769823
71 6.9 946813.6729747849 394.2923336718555 26265828.9088526 276.3865543393284
72 7.0 957626.040839869 394.2777219546483 26154308.424656466 276.05093464451556
73 7.1000000000000005 968399.1512281233 394.2584257130283 26043192.84832872 275.7155152310525
74 7.2 979133.1082930088 394.23462782052076 25932481.10561397 275.3802964208052
75 7.300000000000001 989828.0161879869 394.2065024339623 25822172.12225682 275.04527851873627
76 7.4 1000483.9790665191 394.17421551178546 25712264.824001882 274.71046181264023
77 7.5 1011101.1010820667 394.13792529578427 25602758.136593778 274.3758465728772
78 7.6000000000000005 1021679.4863880915 394.09778275932865 25493650.985777102 274.04143305210266
79 7.7 1032219.239138054 394.0539320247227 25384942.297296483 273.707221484995
80 7.800000000000001 1042720.4634854163 394.0065107521557 25276630.996896524 273.37321208797937
81 7.9 1053183.2635836392 393.95565050247825 25168716.01032184 273.0394050589494
82 8.0 1063607.7435861847 393.9014770758345 25061196.263317037 272.705800576985
83 8.1 1073994.0076465139 393.8441108280058 24954070.681626726 272.3723988020679
84 8.200000000000001 1084342.0075784405 393.78360959211744 24847339.76225091 272.03921728278755
85 8.3 1094651.950514763 393.7201237653114 24741001.368788917 271.7062444960846
86 8.4 1104923.9566483083 393.65376548392067 24635054.261552785 271.3734787513138
87 8.5 1115158.1282653515 393.5846357175065 24529497.385545585 271.0409203804046
88 8.6 1125354.567652169 393.5128313057523 24424329.685770374 270.7085696976142
89 8.700000000000001 1135513.3770950353 393.43844517133164 24319550.10723021 270.37642699925055
90 8.8 1145634.6588802272 393.3615665197664 24215157.594928175 270.0444925633909
91 8.9 1155718.5152940191 393.2822810271899 24111151.093867306 269.712766649598
92 9.0 1165765.0486226876 393.2006710168648 24007529.54905069 269.3812494986329
93 9.1 1175774.361152508 393.11681562522904 23904291.90548136 269.04994133216485
94 9.200000000000001 1185746.555169756 393.03079095819226 23801437.108162407 268.7188423524781
95 9.3 1195681.732960707 392.94267023834027 23698964.10209688 268.3879527421748
96 9.4 1205579.9968116365 392.8525239436615 23596871.83228785 268.05727266387584
97 9.5 1215441.4490088206 392.7604199383583 23495159.243738372 267.72680225991735
98 9.600000000000001 1225266.1918385345 392.66642359626576 23393825.281451505 267.39654165204473
99 9.700000000000001 1235054.3275870536 392.57059791736026 23292868.89043032 267.0664909411029
100 9.8 1244805.7424247153 392.47293675670085 23192291.244732097 266.7366751290975
101 9.9 1254520.6963966596 392.3735487251935 23092089.662209835 266.40707625875064
102 10.0 1264199.3049931854 392.2724947546991 22992262.951678198 266.07769289844276
103 10.100000000000001 1273841.6679760527 392.1698289153906 22892810.0841785 265.74852540655036
104 10.200000000000001 1283447.8851070204 392.0656034102887 22793730.030752078 265.41957412301247
105 10.3 1293018.0561478487 391.9598686580462 22695021.76244025 265.0908393690368
106 10.4 1302552.2808602974 391.8526733713811 22596684.250284337 264.76232144680336
107 10.5 1312050.6590061258 391.7440646314241 22498716.465325654 264.4340206391643
108 10.600000000000001 1321513.290347094 391.63408795822716 22401117.37860553 264.10593720934105
109 10.700000000000001 1330940.2746449616 391.5227873776613 22303885.961165283 263.7780714006174
110 10.8 1340331.711661488 391.41020548491883 22207021.184046242 263.4504234360302
111 10.9 1349687.7011584332 391.29638350481804 22110522.01828973 263.1229935180553
112 11.0 1359008.342897557 391.18136134909884 22014387.434937052 262.79578182829164
113 11.100000000000001 1368293.7366406189 391.0651776708804 21918616.405029543 262.4687885271409
114 11.200000000000001 1377543.9821493784 390.9478699164463 21823207.89960853 262.1420137534841
115 11.3 1386759.1791855951 390.82947437450605 21728160.88971532 261.81545762435434
116 11.4 1395939.2416852904 390.70997702085253 21633476.26302751 261.4891421257888
117 11.5 1405084.4058089943 390.5894488404711 21539151.583747786 261.1630514204039
118 11.600000000000001 1414194.7727817593 390.46792355037144 21445185.807824824 260.83718544108797
119 11.700000000000001 1423270.4378832965 390.3454323238288 21351577.952528503 260.5115448028764
120 11.8 1432311.4962235806 390.22200536933536 21258327.036879413 260.1861301247683
121 11.9 1441318.042742851 390.0976719679259 21165432.0816488 259.86094202976824
122 12.0 1450290.1722116095 389.97246050877925 21072892.1093586 259.5359811449295
123 12.100000000000001 1459227.9792306225 389.84639852319333 20980706.14428146 259.2112481013979
124 12.200000000000001 1468131.5582309205 389.71951271701596 20888873.212440677 258.88674353445447
125 12.3 1477001.0034737969 389.59182900161403 20797392.34161027 258.5624680835606
126 12.4 1485836.4090508097 389.46337252345796 20706262.561314918 258.23842239240224
127 12.5 1494637.8688837802 389.3341676923915 20615482.902829994 257.9146071089349
128 12.600000000000001 1503405.4767247946 389.20423820865625 20525052.399181567 257.5910228854296
129 12.700000000000001 1512139.3261562001 389.07360708873284 20434970.085146382 257.26767037851835
130 12.8 1520839.5105906113 388.94229669006046 20345234.997251876 256.9445502492407
131 12.9 1529506.1233409408 388.81032876227204 20255846.173053782 256.62166315000906
132 13.0 1538139.2687433015 388.67772882293264 20166802.53641284 256.2990076411302
133 13.100000000000001 1546739.030251722 388.5445136335602 20078103.22657088 255.97658615427704
134 13.200000000000001 1555305.5007360894 388.4107031827765 19989747.285653118 255.65439933992917
135 13.3 1563838.7729005008 388.27631690939495 19901733.757494725 255.332447852161
136 13.4 1572338.9392832702 388.14137372147235 19814061.687640797 255.0107323486784
137 13.5 1580806.0922569225 388.0058920145713 19726730.123346385 254.68925349085504
138 13.600000000000001 1589240.3240281995 387.86988968927403 19639738.113576483 254.36801194376957
139 13.700000000000001 1597641.7266380545 387.73338416798015 19553084.709006038 254.04700837624307
140 13.8 1606010.3919616563 387.59639241102605 19466768.962019928 253.72624346087662
141 13.9 1614346.4117083861 387.458930932153 19380789.926712975 253.40571787408948
142 14.0 1622649.8774218408 387.32101581335974 19295146.658889957 253.0854322961577
143 14.100000000000001 1630920.8804798292 387.18266271916264 19209838.21606559 252.76538741125262
144 14.200000000000001 1639159.5120943757 387.0438869102949 19124863.657464534 252.4455839074805
145 14.3 1647365.8633117182 386.90470325686863 19040222.0440214 252.12602247692183
146 14.4 1655540.0250123062 386.76512625102305 18955912.438380726 251.8067038156715
147 14.5 1663682.087910807 386.6251700190859 18871933.904897016 251.48762862387906
148 14.600000000000001 1671792.1425560997 386.4848483332645 18788285.509634707 251.16879760578948
149 14.700000000000001 1679870.2793312764 386.3441746228917 18704966.320368182 250.85021146978437
150 14.8 1687916.588453644 386.2031619852454 18621975.406581767 250.5318709284234
151 14.9 1695931.1599747243 386.06182319595837 18539311.83946974 250.21377669848638
152 15.0 1703914.0837802505 385.920170719039 18456974.691936314 249.8959295010154
153 15.100000000000001 1711865.4494154856 385.77821670970445 18374963.040397402 249.57833006564294
154 15.200000000000001 1719785.3379250832 385.6359727064743 18293276.048945524 249.26097933285033
155 15.3 1727673.8459201432 385.4934505781353 18211912.721118417 248.94387786218365
156 15.4 1735531.0625176337 385.3506616315768 18130872.137749474 248.6270263958575
157 15.5 1743357.0766656764 385.2076169067951 18050153.381413613 248.31042568075102
158 15.600000000000001 1751151.9771435438 385.06432718490083 17969755.53642727 247.99407646845765
159 15.700000000000001 1758915.8525616606 384.9208029958382 17889677.688848406 247.67797951533504
160 15.8 1766648.7913616048 384.77705462583015 17809918.926476505 247.36213558255577
161 15.9 1774350.881816107 384.63309212455715 17730478.33885257 247.04654543615806
162 16.0 1782022.2120290494 384.4889253120841 17651355.017259125 246.73120984709743
163 16.1 1789662.8699354655 384.3445637855432 17572548.05472022 246.41612959129878
164 16.2 1797272.9433015438 384.2000169255848 17494056.546001427 246.10130544970855
165 16.3 1804852.5197246224 384.05529390260284 17415879.58760983 245.78673820834797
166 16.400000000000002 1812401.6866331936 383.9104036827472 17338016.277794052 245.47242865836654
167 16.5 1819920.5312869006 383.76535503372884 17260465.716544226 245.15837759609565
168 16.6 1827409.1407765402 383.6201565304272 17183227.005592 244.84458582310356
169 16.7 1834867.6020240602 383.47481656030743 17106299.24841057 244.53105414625009
170 16.8 1842296.0017825624 383.32934332865534 17029681.550214626 244.2177833777421
171 16.900000000000002 1849694.4266362996 383.1837448636361 16953373.017960392 243.9047743351897
172 17.0 1857062.9630006768 383.03802902118434 16877372.76034562 243.59202784166294
173 17.1 1864401.697122252 382.8922034897328 16801679.887809563 243.27954472574828
174 17.2 1871710.7150787353 382.7462757947841 16726293.512533028 242.96732582160686
175 17.3 1878990.1030524147 382.60025337999576 16651212.745618157 242.65537190477806
176 17.400000000000002 1886239.954455942 382.4541455990818 16576436.623591991 242.343682012199
177 17.5 1893460.3482686526 382.3079576538116 16501964.331849081 242.03225853722532
178 17.6 1900651.3699906773 382.16169648335216 16427794.988527464 241.72110231102408
179 17.7 1907813.104956132 382.0153688850827 16353927.713477474 241.41021416968596
180 17.8 1914945.6383331183 381.86898151834066 16280361.628261749 241.09959495427398
181 17.900000000000002 1922049.0551237254 381.7225409080495 16207095.856155202 240.78924551087363
182 18.0 1929123.4401640275 381.57605344823065 16134129.522145053 240.47916669064318
183 18.1 1936168.8781240864 381.4295254054061 16061461.752930798 240.16935934986387
184 18.2 1943185.4535079484 381.28296292189384 15989091.67692425 239.8598243499916
185 18.3 1950173.2506536485 381.1363720190009 15917018.424249483 239.55056255770802
186 18.400000000000002 1957132.3537332045 380.98975860011734 15845241.126742886 239.24157484497272
187 18.5 1964062.8467526236 380.84312845371574 15773758.917953137 238.93286208907583
188 18.6 1970964.8135518986 380.6964872562572 15702570.933141202 238.6244251726908
189 18.7 1977838.3378050062 380.54984057500997 15631676.309280336 238.31626498392774
190 18.8 1984683.5030199126 380.4031938707821 15561074.185056096 238.00838241638746
191 18.900000000000002 1991500.3925385692 380.25655250057133 15490763.700866321 237.7007783692156
192 19.0 1998289.0895369116 380.10992172013556 15420743.998821149 237.39345374715774
193 19.1 2005049.677024864 379.96330668648653 15351014.222743012 237.08640946061425
194 19.200000000000003 2011782.237846337 379.81671246030885 15281573.518166626 236.77964642569634
195 19.3 2018486.8546792252 379.67014400830794 15212421.032339014 236.47316556428217
196 19.400000000000002 2025163.610035411 379.5236062054879 15143555.914219463 236.16696780407335
197 19.5 2031812.5863479478 379.3771038539995 15074977.31358037 235.861054060844
198 19.6 2038433.8680519294 379.2306420805889 15006684.359544702 235.55542481191287
199 19.700000000000003 2045027.536485746 379.084225365391 14938676.213175347 235.25008113730536
200 19.8 2051593.6739729291 378.9378583245209 14870952.025374293 234.9450238874766
201 19.900000000000002 2058132.3627231135 378.79154550141357 14803510.948218277 234.64025390703284
202 20.0 2064643.6848320398 378.64529136859073 14736352.134958768 234.33577203462033
@@ -0,0 +1,41 @@
from __future__ import annotations
import unittest
from pathlib import Path
from PythonModels.systems import test_mql
MATRIX_PATH = Path(__file__).resolve().parents[1] / "docs" / "amesim-component-migration-matrix.md"
class AmesimComponentMigrationMatrixTests(unittest.TestCase):
def test_matrix_covers_every_test_mql_submodel_family(self) -> None:
text = MATRIX_PATH.read_text(encoding="utf-8")
families = set(test_mql.SUBMODEL_COUNTS) | set(test_mql.LINE_SUBMODEL_COUNTS)
missing = sorted(family for family in families if f"`{family}`" not in text)
self.assertEqual(missing, [])
def test_unsupported_domains_are_not_marked_as_public_candidates(self) -> None:
text = MATRIX_PATH.read_text(encoding="utf-8")
unsupported = {
"F000",
"FORC",
"LMECHN1",
"LSTP00A",
"MECMAS21",
"PNRP17",
"STEP0",
"UD00",
}
for family in unsupported:
with self.subTest(family=family):
row = next(line for line in text.splitlines() if f"`{family}`" in line)
self.assertIn("暂不公开", row)
if __name__ == "__main__":
unittest.main()
+96
View File
@@ -0,0 +1,96 @@
from __future__ import annotations
import unittest
from app.simulation.components.amesim.storage.chambers import AmesimPnch023
from app.simulation.core.medium import IdealGasMedium
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
class AmesimPnch023ComponentTests(unittest.TestCase):
def setUp(self) -> None:
self.medium = IdealGasMedium()
def test_default_create_preserves_amesim_parameter_contract(self) -> None:
chamber = COMPONENT_MODEL_REGISTRY["amesim_pnch023"].create(
"chamber_1",
self.medium,
{},
)
self.assertIsInstance(chamber, AmesimPnch023)
self.assertEqual(set(chamber.ports), {"port_1", "port_2"})
self.assertEqual(
set(chamber.parameter_values),
{"cvol", "kth", "sth", "extemp", "gi", "p0", "T0"},
)
self.assertEqual(chamber.parameter_values["cvol"], 0.057)
self.assertEqual(chamber.gi, 1)
def test_rejects_fractional_gas_index(self) -> None:
with self.assertRaisesRegex(ValueError, "gi must be an integer"):
COMPONENT_MODEL_REGISTRY["amesim_pnch023"].create(
"chamber_1",
self.medium,
{"gi": 1.5},
)
def test_initial_state_matches_requested_pressure_temperature(self) -> None:
chamber = AmesimPnch023(
"chamber_1",
self.medium,
cvol=0.057,
p0=15300000.0,
T0=293.15,
)
props = chamber.properties()
self.assertAlmostEqual(props.p, 15300000.0)
self.assertAlmostEqual(props.T, 293.15)
self.assertAlmostEqual(chamber.port_1.p, props.p)
self.assertAlmostEqual(chamber.port_2.p, props.p)
def test_two_port_derivative_conserves_mass_and_adds_heat_transfer(self) -> None:
chamber = AmesimPnch023(
"chamber_1",
self.medium,
kth=10.0,
sth=2.0,
extemp=310.0,
T0=300.0,
)
props = chamber.properties()
chamber.port_1.m_flow = 0.2
chamber.port_2.m_flow = -0.1
derivative = chamber.state_derivative_from_ports(
{
"port_1": props.h + 1000.0,
"port_2": props.h - 1000.0,
}
)
self.assertAlmostEqual(derivative[0], 0.1)
self.assertAlmostEqual(
derivative[1],
0.2 * (props.h + 1000.0) - 0.1 * props.h + 10.0 * 2.0 * 10.0,
)
def test_pressure_residuals_bind_both_ports_to_state(self) -> None:
chamber = AmesimPnch023("chamber_1", self.medium, p0=200000.0, T0=300.0)
chamber.port_1.p = 200100.0
chamber.port_2.p = 199900.0
residuals = {
residual.id.rsplit(":", 1)[1]: residual
for residual in chamber.pressure_flow_equation_residuals()
}
self.assertEqual(set(residuals), {"port_1_pressure_state", "port_2_pressure_state"})
self.assertAlmostEqual(residuals["port_1_pressure_state"].value, 100.0)
self.assertAlmostEqual(residuals["port_2_pressure_state"].value, -100.0)
if __name__ == "__main__":
unittest.main()
+107
View File
@@ -0,0 +1,107 @@
from __future__ import annotations
import unittest
from app.main import (
ReactFlowProjectPayload,
build_reactflow_system_xml,
compile_reactflow_network,
run_system_xml_simulation,
)
from app.system_xml import validate_system_xml_document
from tests.test_generic_system_xml_simulation import component_node, physical_edge
from tests.test_system_xml_protocol import physical_port
def amesim_pnch023_project() -> ReactFlowProjectPayload:
return ReactFlowProjectPayload(
name="amesim-pnch023-smoke",
nodes=[
component_node(
"cylinder_1",
"cylinder",
[physical_port("port_b", "outlet", "right")],
{"volume": 0.01, "p0": 101000.0, "T0": 300.0},
),
component_node(
"orifice_1",
"orifice",
[
physical_port("port_a", "inlet", "left"),
physical_port("port_b", "outlet", "right"),
],
{"K": 1e-8, "opening": 1.0},
),
component_node(
"chamber_1",
"amesim_pnch023",
[
physical_port("port_1", "bidirectional", "left"),
physical_port("port_2", "bidirectional", "right"),
],
{
"cvol": 0.057,
"kth": 0.0,
"sth": 0.1,
"extemp": 293.15,
"gi": 1.0,
"p0": 100000.0,
"T0": 293.15,
},
),
component_node(
"pipe_1",
"amesim_pnl00r",
[
physical_port("port_1", "bidirectional", "left"),
physical_port("port_2", "bidirectional", "right"),
],
{"diam": 0.02, "le": 1.0, "rr": 1.0e-5, "gi": 1.0},
),
component_node(
"tank_1",
"tank",
[physical_port("port_a", "inlet", "left")],
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
),
],
edges=[
physical_edge("edge-1", "cylinder_1", "port_b", "orifice_1", "port_a"),
physical_edge("edge-2", "orifice_1", "port_b", "chamber_1", "port_1"),
physical_edge("edge-3", "chamber_1", "port_2", "pipe_1", "port_1"),
physical_edge("edge-4", "pipe_1", "port_2", "tank_1", "port_a"),
],
simulation={
"t_start": 0.0,
"t_stop": 0.002,
"step": 0.001,
"max_step": 0.001,
"method": "BDF",
},
)
class AmesimPnch023XmlTests(unittest.TestCase):
def test_pnch023_reactflow_project_compiles(self) -> None:
network = compile_reactflow_network(amesim_pnch023_project())
self.assertIn("chamber_1", network.components)
self.assertEqual(network.components["chamber_1"].model_type, "amesim_pnch023")
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
def test_pnch023_system_xml_validates_and_simulates(self) -> None:
xml = build_reactflow_system_xml(amesim_pnch023_project())
report = validate_system_xml_document(xml)
self.assertTrue(report.valid, report.as_dict())
result = run_system_xml_simulation(xml)
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
self.assertIn("chamber_1.p", result["series"])
self.assertIn("chamber_1.port_1.m_flow", result["series"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,94 @@
from __future__ import annotations
import unittest
from app.simulation.components.amesim.junctions.nodes import (
AmesimP4Node2,
AmesimPn3Node2,
)
from app.simulation.core.medium import IdealGasMedium
class AmesimPneumaticNodeComponentTests(unittest.TestCase):
def test_pn3node2_declares_bidirectional_pneumatic_ports(self) -> None:
node = AmesimPn3Node2.create(
name="pn3_1",
medium=IdealGasMedium(),
parameters={},
)
self.assertEqual(set(node.ports), {"port_1", "port_2", "port_3"})
self.assertTrue(
all(
definition.domain == "pneumatic"
and definition.nominal_role == "bidirectional"
for definition in node.port_definitions
)
)
self.assertEqual(node.parameter_values, {})
def test_pn3node2_residuals_use_port_2_as_pressure_reference(self) -> None:
node = AmesimPn3Node2("pn3_1")
node.port_1.p = 101000.0
node.port_2.p = 100000.0
node.port_3.p = 99000.0
node.port_1.m_flow = 0.2
node.port_2.m_flow = -0.3
node.port_3.m_flow = 0.1
residuals = {residual.id.rsplit(":", 1)[1]: residual for residual in node.pressure_flow_equation_residuals()}
self.assertEqual(set(residuals), {"port_1_pressure_reference", "port_3_pressure_reference", "mass_flow_balance"})
self.assertEqual(residuals["port_1_pressure_reference"].value, 1000.0)
self.assertEqual(residuals["port_3_pressure_reference"].value, -1000.0)
self.assertAlmostEqual(residuals["mass_flow_balance"].value, 0.0)
def test_p4node2_residuals_cover_four_ports(self) -> None:
node = AmesimP4Node2("p4_1")
node.port_1.p = 100000.0
node.port_2.p = 100000.0
node.port_3.p = 100000.0
node.port_4.p = 100000.0
node.port_1.m_flow = 0.1
node.port_2.m_flow = 0.2
node.port_3.m_flow = -0.4
node.port_4.m_flow = 0.1
residuals = node.pressure_flow_equation_residuals()
self.assertEqual(len(residuals), 4)
self.assertTrue(all(residual.value == 0.0 for residual in residuals))
self.assertEqual(
residuals[-1].variables,
(
"p4_1.port_1.m_flow",
"p4_1.port_2.m_flow",
"p4_1.port_3.m_flow",
"p4_1.port_4.m_flow",
),
)
def test_node_stream_outflow_uses_incoming_weighted_mix(self) -> None:
node = AmesimP4Node2("p4_1")
node.port_1.m_flow = 0.25
node.port_2.m_flow = 0.75
node.port_3.m_flow = -0.5
node.port_4.m_flow = -0.5
node.update_stream_outflows(
{
"port_1": 100.0,
"port_2": 300.0,
"port_3": 900.0,
"port_4": 1200.0,
}
)
self.assertEqual(node.port_1.h_outflow, 250.0)
self.assertEqual(node.port_2.h_outflow, 250.0)
self.assertEqual(node.port_3.h_outflow, 250.0)
self.assertEqual(node.port_4.h_outflow, 250.0)
if __name__ == "__main__":
unittest.main()
+127
View File
@@ -0,0 +1,127 @@
from __future__ import annotations
import unittest
from app.main import (
ReactFlowProjectPayload,
build_reactflow_system_xml,
compile_reactflow_network,
run_system_xml_simulation,
)
from app.system_xml import validate_system_xml_document
from tests.test_generic_system_xml_simulation import component_node, physical_edge
from tests.test_system_xml_protocol import physical_port
def amesim_pn3node_project() -> ReactFlowProjectPayload:
return ReactFlowProjectPayload(
name="amesim-pn3node-smoke",
nodes=[
component_node(
"cylinder_1",
"cylinder",
[physical_port("port_b", "outlet", "right")],
{"volume": 0.01, "p0": 500000.0, "T0": 300.0},
),
component_node(
"orifice_1",
"orifice",
[
physical_port("port_a", "inlet", "left"),
physical_port("port_b", "outlet", "right"),
],
{"K": 1e-5, "opening": 1.0},
),
component_node(
"node_1",
"amesim_pn3node2",
[
physical_port("port_1", "bidirectional", "left"),
physical_port("port_2", "bidirectional", "right"),
physical_port("port_3", "bidirectional", "right"),
],
),
component_node(
"pipe_1",
"pipe",
[
physical_port("port_a", "inlet", "left"),
physical_port("port_b", "outlet", "right"),
],
{
"length": 1.0,
"diameter": 0.02,
"lambda_darcy": 0.02,
"p0": 100000.0,
"T0": 300.0,
},
),
component_node(
"tank_1",
"tank",
[physical_port("port_a", "inlet", "left")],
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
),
component_node(
"pipe_2",
"pipe",
[
physical_port("port_a", "inlet", "left"),
physical_port("port_b", "outlet", "right"),
],
{
"length": 1.0,
"diameter": 0.02,
"lambda_darcy": 0.02,
"p0": 100000.0,
"T0": 300.0,
},
),
component_node(
"tank_2",
"tank",
[physical_port("port_a", "inlet", "left")],
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
),
],
edges=[
physical_edge("edge-1", "cylinder_1", "port_b", "orifice_1", "port_a"),
physical_edge("edge-2", "orifice_1", "port_b", "node_1", "port_2"),
physical_edge("edge-3", "node_1", "port_1", "pipe_1", "port_a"),
physical_edge("edge-4", "pipe_1", "port_b", "tank_1", "port_a"),
physical_edge("edge-5", "node_1", "port_3", "pipe_2", "port_a"),
physical_edge("edge-6", "pipe_2", "port_b", "tank_2", "port_a"),
],
simulation={
"t_start": 0.0,
"t_stop": 0.002,
"step": 0.001,
"max_step": 0.001,
"method": "BDF",
},
)
class AmesimPneumaticNodeXmlTests(unittest.TestCase):
def test_pn3node_reactflow_project_compiles(self) -> None:
network = compile_reactflow_network(amesim_pn3node_project())
self.assertIn("node_1", network.components)
self.assertEqual(network.components["node_1"].model_type, "amesim_pn3node2")
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
def test_pn3node_system_xml_validates_and_simulates(self) -> None:
xml = build_reactflow_system_xml(amesim_pn3node_project())
report = validate_system_xml_document(xml)
self.assertTrue(report.valid, report.as_dict())
result = run_system_xml_simulation(xml)
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
self.assertIn("node_1.port_2.p", result["series"])
if __name__ == "__main__":
unittest.main()
+82
View File
@@ -0,0 +1,82 @@
from __future__ import annotations
import unittest
from app.simulation.components.amesim.flow.pipes import AmesimPnl00r
from app.simulation.core.medium import IdealGasMedium
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
class AmesimPnl00rComponentTests(unittest.TestCase):
def setUp(self) -> None:
self.medium = IdealGasMedium()
def test_default_create_preserves_amesim_parameter_contract(self) -> None:
pipe = COMPONENT_MODEL_REGISTRY["amesim_pnl00r"].create(
"pnl_1",
self.medium,
{},
)
self.assertIsInstance(pipe, AmesimPnl00r)
self.assertEqual(set(pipe.ports), {"port_1", "port_2"})
self.assertEqual(set(pipe.parameter_values), {"diam", "le", "rr", "gi"})
self.assertEqual(pipe.parameter_values["diam"], 0.01)
self.assertEqual(pipe.gi, 1)
def test_rejects_fractional_gas_index(self) -> None:
with self.assertRaisesRegex(ValueError, "gi must be an integer"):
COMPONENT_MODEL_REGISTRY["amesim_pnl00r"].create(
"pnl_1",
self.medium,
{"gi": 1.5},
)
def test_zero_pressure_drop_has_zero_flow_and_finite_results(self) -> None:
pipe = AmesimPnl00r("pnl_1", self.medium)
pipe.port_1.p = 100000.0
pipe.port_2.p = 100000.0
pipe.port_1.m_flow = 0.0
pipe.port_2.m_flow = 0.0
residuals = {
residual.id.rsplit(":", 1)[1]: residual
for residual in pipe.pressure_flow_equation_residuals()
}
self.assertEqual(pipe.mass_flow(100000.0, 100000.0), 0.0)
self.assertEqual(residuals["mass_flow_balance"].value, 0.0)
self.assertEqual(residuals["pressure_flow_relation"].value, 0.0)
self.assertEqual(set(pipe.component_result_values()), {"re", "cm", "v", "ff"})
def test_mass_flow_is_bidirectional_by_pressure_order(self) -> None:
pipe = AmesimPnl00r("pnl_1", self.medium, diam=0.014, le=1.0, rr=0.045 / 14.0)
forward = pipe.mass_flow(501000.0, 500000.0)
reverse = pipe.mass_flow(500000.0, 501000.0)
self.assertGreater(forward, 0.0)
self.assertLess(reverse, 0.0)
self.assertAlmostEqual(forward, -reverse, delta=abs(forward) * 0.02)
def test_friction_factor_transitions_from_laminar_to_turbulent(self) -> None:
pipe = AmesimPnl00r("pnl_1", self.medium, rr=1e-5)
self.assertAlmostEqual(pipe.friction_factor(100.0), 64.0 / 100.0)
self.assertGreater(pipe.friction_factor(100000.0), 0.0)
self.assertLess(pipe.friction_factor(100000.0), 0.1)
def test_darcy_pressure_drop_uses_flow_sign(self) -> None:
pipe = AmesimPnl00r("pnl_1", self.medium)
density = self.medium.density(500000.0, 300.0)
forward = pipe.darcy_pressure_drop(0.01, density=density, temperature=300.0)
reverse = pipe.darcy_pressure_drop(-0.01, density=density, temperature=300.0)
self.assertGreater(forward, 0.0)
self.assertLess(reverse, 0.0)
self.assertAlmostEqual(forward, -reverse)
if __name__ == "__main__":
unittest.main()
+79
View File
@@ -0,0 +1,79 @@
from __future__ import annotations
import unittest
from app.main import (
ReactFlowProjectPayload,
build_reactflow_system_xml,
compile_reactflow_network,
run_system_xml_simulation,
)
from app.system_xml import validate_system_xml_document
from tests.test_generic_system_xml_simulation import component_node, physical_edge
from tests.test_system_xml_protocol import physical_port
def amesim_pnl00r_project() -> ReactFlowProjectPayload:
return ReactFlowProjectPayload(
name="amesim-pnl00r-smoke",
nodes=[
component_node(
"cylinder_1",
"cylinder",
[physical_port("port_b", "outlet", "right")],
{"volume": 0.01, "p0": 500000.0, "T0": 300.0},
),
component_node(
"pnl_1",
"amesim_pnl00r",
[
physical_port("port_1", "bidirectional", "left"),
physical_port("port_2", "bidirectional", "right"),
],
{"diam": 0.01, "le": 1.0, "rr": 1.0e-5, "gi": 1.0},
),
component_node(
"tank_1",
"tank",
[physical_port("port_a", "inlet", "left")],
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
),
],
edges=[
physical_edge("edge-1", "cylinder_1", "port_b", "pnl_1", "port_1"),
physical_edge("edge-2", "pnl_1", "port_2", "tank_1", "port_a"),
],
simulation={
"t_start": 0.0,
"t_stop": 0.002,
"step": 0.001,
"max_step": 0.001,
"method": "BDF",
},
)
class AmesimPnl00rXmlTests(unittest.TestCase):
def test_pnl00r_reactflow_project_compiles(self) -> None:
network = compile_reactflow_network(amesim_pnl00r_project())
self.assertIn("pnl_1", network.components)
self.assertEqual(network.components["pnl_1"].model_type, "amesim_pnl00r")
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
def test_pnl00r_system_xml_validates_and_simulates(self) -> None:
xml = build_reactflow_system_xml(amesim_pnl00r_project())
report = validate_system_xml_document(xml)
self.assertTrue(report.valid, report.as_dict())
result = run_system_xml_simulation(xml)
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
self.assertIn("pnl_1.port_1.m_flow", result["series"])
self.assertIn("pnl_1.re", result["series"])
if __name__ == "__main__":
unittest.main()
+84
View File
@@ -0,0 +1,84 @@
from __future__ import annotations
import unittest
from app.simulation.components.amesim.flow.orifices import AmesimPnor001
from app.simulation.core.medium import IdealGasMedium
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
class AmesimPnor001ComponentTests(unittest.TestCase):
def setUp(self) -> None:
self.medium = IdealGasMedium()
def test_default_create_preserves_amesim_parameter_contract(self) -> None:
orifice = COMPONENT_MODEL_REGISTRY["amesim_pnor001"].create(
"pnor_1",
self.medium,
{},
)
self.assertIsInstance(orifice, AmesimPnor001)
self.assertEqual(set(orifice.ports), {"port_1", "port_2"})
self.assertEqual(
set(orifice.parameter_values),
{"cq", "area", "Cv", "Kv", "gi", "flowset"},
)
self.assertEqual(orifice.parameter_values["area"], 5.0e-6)
self.assertEqual(orifice.flowset, 1)
def test_rejects_fractional_integer_parameters(self) -> None:
with self.assertRaisesRegex(ValueError, "flowset must be an integer"):
COMPONENT_MODEL_REGISTRY["amesim_pnor001"].create(
"pnor_1",
self.medium,
{"flowset": 1.5},
)
def test_zero_pressure_drop_has_zero_flow_and_finite_results(self) -> None:
orifice = AmesimPnor001("pnor_1", self.medium)
orifice.port_1.p = 100000.0
orifice.port_2.p = 100000.0
orifice.port_1.m_flow = 0.0
orifice.port_2.m_flow = 0.0
residuals = {
residual.id.rsplit(":", 1)[1]: residual
for residual in orifice.pressure_flow_equation_residuals()
}
self.assertEqual(orifice.mass_flow(100000.0, 100000.0), 0.0)
self.assertEqual(residuals["mass_flow_balance"].value, 0.0)
self.assertEqual(residuals["pressure_flow_relation"].value, 0.0)
self.assertEqual(set(orifice.component_result_values()), {"cm", "gasvel"})
def test_mass_flow_is_bidirectional_by_pressure_order(self) -> None:
orifice = AmesimPnor001("pnor_1", self.medium)
forward = orifice.mass_flow(500000.0, 100000.0)
reverse = orifice.mass_flow(100000.0, 500000.0)
self.assertGreater(forward, 0.0)
self.assertLess(reverse, 0.0)
self.assertAlmostEqual(forward, -reverse)
def test_cv_and_kv_modes_produce_positive_equivalent_area(self) -> None:
cv_orifice = AmesimPnor001(
"pnor_cv",
self.medium,
flowset=2.0,
Cv=0.5,
)
kv_orifice = AmesimPnor001(
"pnor_kv",
self.medium,
flowset=3.0,
Kv=0.4,
)
self.assertGreater(cv_orifice.effective_area, 0.0)
self.assertGreater(kv_orifice.effective_area, 0.0)
if __name__ == "__main__":
unittest.main()
+102
View File
@@ -0,0 +1,102 @@
from __future__ import annotations
import unittest
from app.main import (
ReactFlowProjectPayload,
build_reactflow_system_xml,
compile_reactflow_network,
run_system_xml_simulation,
)
from app.system_xml import validate_system_xml_document
from tests.test_generic_system_xml_simulation import component_node, physical_edge
from tests.test_system_xml_protocol import physical_port
def amesim_pnor001_project() -> ReactFlowProjectPayload:
return ReactFlowProjectPayload(
name="amesim-pnor001-smoke",
nodes=[
component_node(
"cylinder_1",
"cylinder",
[physical_port("port_b", "outlet", "right")],
{"volume": 0.01, "p0": 500000.0, "T0": 300.0},
),
component_node(
"pnor_1",
"amesim_pnor001",
[
physical_port("port_1", "bidirectional", "left"),
physical_port("port_2", "bidirectional", "right"),
],
{
"cq": 0.72,
"area": 5.0e-6,
"Cv": 0.5,
"Kv": 0.4,
"gi": 1.0,
"flowset": 1.0,
},
),
component_node(
"pipe_1",
"pipe",
[
physical_port("port_a", "inlet", "left"),
physical_port("port_b", "outlet", "right"),
],
{
"length": 1.0,
"diameter": 0.02,
"lambda_darcy": 0.02,
"p0": 100000.0,
"T0": 300.0,
},
),
component_node(
"tank_1",
"tank",
[physical_port("port_a", "inlet", "left")],
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
),
],
edges=[
physical_edge("edge-1", "cylinder_1", "port_b", "pnor_1", "port_1"),
physical_edge("edge-2", "pnor_1", "port_2", "pipe_1", "port_a"),
physical_edge("edge-3", "pipe_1", "port_b", "tank_1", "port_a"),
],
simulation={
"t_start": 0.0,
"t_stop": 0.002,
"step": 0.001,
"max_step": 0.001,
"method": "BDF",
},
)
class AmesimPnor001XmlTests(unittest.TestCase):
def test_pnor001_reactflow_project_compiles(self) -> None:
network = compile_reactflow_network(amesim_pnor001_project())
self.assertIn("pnor_1", network.components)
self.assertEqual(network.components["pnor_1"].model_type, "amesim_pnor001")
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
def test_pnor001_system_xml_validates_and_simulates(self) -> None:
xml = build_reactflow_system_xml(amesim_pnor001_project())
report = validate_system_xml_document(xml)
self.assertTrue(report.valid, report.as_dict())
result = run_system_xml_simulation(xml)
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
self.assertIn("pnor_1.port_1.m_flow", result["series"])
self.assertIn("pnor_1.gasvel", result["series"])
if __name__ == "__main__":
unittest.main()
+45
View File
@@ -0,0 +1,45 @@
from __future__ import annotations
import unittest
from app.simulation.components.amesim.boundary.sources import AmesimPnpl01
from app.simulation.core.medium import IdealGasMedium
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
class AmesimPnpl01ComponentTests(unittest.TestCase):
def setUp(self) -> None:
self.medium = IdealGasMedium()
def test_default_create_preserves_zero_flow_boundary_contract(self) -> None:
source = COMPONENT_MODEL_REGISTRY["amesim_pnpl01"].create(
"plug_1",
self.medium,
{},
)
self.assertIsInstance(source, AmesimPnpl01)
self.assertEqual(set(source.ports), {"port_1"})
self.assertEqual(source.parameter_values, {})
def test_zero_flow_residual_uses_port_mass_flow(self) -> None:
source = AmesimPnpl01("plug_1")
source.port_1.m_flow = 0.125
residuals = source.pressure_flow_equation_residuals()
self.assertEqual(len(residuals), 1)
self.assertEqual(residuals[0].id, "plug_1:zero_mass_flow")
self.assertEqual(residuals[0].variables, ("plug_1.port_1.m_flow",))
self.assertEqual(residuals[0].value, 0.125)
def test_stream_outflow_tracks_connected_enthalpy_when_available(self) -> None:
source = AmesimPnpl01("plug_1")
source.update_stream_outflows({"port_1": 123.0})
self.assertEqual(source.port_1.h_outflow, 123.0)
if __name__ == "__main__":
unittest.main()
+87
View File
@@ -0,0 +1,87 @@
from __future__ import annotations
import unittest
from app.main import (
ReactFlowProjectPayload,
build_reactflow_system_xml,
compile_reactflow_network,
run_system_xml_simulation,
)
from app.system_xml import validate_system_xml_document
from tests.test_generic_system_xml_simulation import component_node, physical_edge
from tests.test_system_xml_protocol import physical_port
def amesim_pnpl01_project() -> ReactFlowProjectPayload:
return ReactFlowProjectPayload(
name="amesim-pnpl01-smoke",
nodes=[
component_node(
"chamber_1",
"amesim_pnch023",
[
physical_port("port_1", "bidirectional", "left"),
physical_port("port_2", "bidirectional", "right"),
],
{
"cvol": 0.057,
"kth": 0.0,
"sth": 0.1,
"extemp": 293.15,
"gi": 1.0,
"p0": 100000.0,
"T0": 293.15,
},
),
component_node(
"plug_1",
"amesim_pnpl01",
[physical_port("port_1", "bidirectional", "left")],
),
component_node(
"plug_2",
"amesim_pnpl01",
[physical_port("port_1", "bidirectional", "left")],
),
],
edges=[
physical_edge("edge-1", "chamber_1", "port_1", "plug_1", "port_1"),
physical_edge("edge-2", "chamber_1", "port_2", "plug_2", "port_1"),
],
simulation={
"t_start": 0.0,
"t_stop": 0.002,
"step": 0.001,
"max_step": 0.001,
"method": "BDF",
},
)
class AmesimPnpl01XmlTests(unittest.TestCase):
def test_pnpl01_reactflow_project_compiles(self) -> None:
network = compile_reactflow_network(amesim_pnpl01_project())
self.assertIn("plug_1", network.components)
self.assertEqual(network.components["plug_1"].model_type, "amesim_pnpl01")
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
def test_pnpl01_system_xml_validates_and_simulates(self) -> None:
xml = build_reactflow_system_xml(amesim_pnpl01_project())
report = validate_system_xml_document(xml)
self.assertTrue(report.valid, report.as_dict())
result = run_system_xml_simulation(xml)
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
self.assertTrue(
all(abs(value) < 1.0e-12 for value in result["series"]["plug_1.port_1.m_flow"])
)
self.assertIn("chamber_1.p", result["series"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,66 @@
from __future__ import annotations
import unittest
from app.simulation.components.amesim.flow.orifices import AmesimPnvo001FixedOpening
from app.simulation.core.medium import IdealGasMedium
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
class AmesimPnvo001FixedOpeningComponentTests(unittest.TestCase):
def setUp(self) -> None:
self.medium = IdealGasMedium()
def test_default_create_preserves_fixed_opening_contract(self) -> None:
valve = COMPONENT_MODEL_REGISTRY["amesim_pnvo001_fixed"].create(
"valve_1",
self.medium,
{},
)
self.assertIsInstance(valve, AmesimPnvo001FixedOpening)
self.assertEqual(set(valve.ports), {"port_2", "port_3"})
self.assertEqual(
set(valve.parameter_values),
{"cq", "area0", "Cv", "Kv", "gi", "flowset", "opening"},
)
self.assertEqual(valve.opening, 1.0)
def test_rejects_fractional_flowset(self) -> None:
with self.assertRaisesRegex(ValueError, "flowset must be an integer"):
COMPONENT_MODEL_REGISTRY["amesim_pnvo001_fixed"].create(
"valve_1",
self.medium,
{"flowset": 1.5},
)
def test_opening_scales_effective_area(self) -> None:
valve = AmesimPnvo001FixedOpening(
"valve_1",
self.medium,
area0=2.0e-6,
opening=0.25,
)
self.assertAlmostEqual(valve.maximum_area, 2.0e-6)
self.assertAlmostEqual(valve.effective_area, 0.5e-6)
self.assertEqual(valve.component_result_values()["xv"], 0.25)
def test_zero_opening_blocks_flow(self) -> None:
valve = AmesimPnvo001FixedOpening("valve_1", self.medium, opening=0.0)
self.assertEqual(valve.mass_flow(500000.0, 100000.0), 0.0)
def test_mass_flow_is_bidirectional_by_pressure_order(self) -> None:
valve = AmesimPnvo001FixedOpening("valve_1", self.medium, opening=0.5)
forward = valve.mass_flow(500000.0, 100000.0)
reverse = valve.mass_flow(100000.0, 500000.0)
self.assertGreater(forward, 0.0)
self.assertLess(reverse, 0.0)
self.assertAlmostEqual(forward, -reverse)
if __name__ == "__main__":
unittest.main()
+97
View File
@@ -0,0 +1,97 @@
from __future__ import annotations
import unittest
from app.main import (
ReactFlowProjectPayload,
build_reactflow_system_xml,
compile_reactflow_network,
run_system_xml_simulation,
)
from app.system_xml import validate_system_xml_document
from tests.test_generic_system_xml_simulation import component_node, physical_edge
from tests.test_system_xml_protocol import physical_port
def amesim_pnvo001_fixed_project() -> ReactFlowProjectPayload:
return ReactFlowProjectPayload(
name="amesim-pnvo001-fixed-smoke",
nodes=[
component_node(
"cylinder_1",
"cylinder",
[physical_port("port_b", "outlet", "right")],
{"volume": 0.01, "p0": 150000.0, "T0": 300.0},
),
component_node(
"valve_1",
"amesim_pnvo001_fixed",
[
physical_port("port_2", "bidirectional", "left"),
physical_port("port_3", "bidirectional", "right"),
],
{
"cq": 0.72,
"area0": 5.0e-6,
"Cv": 0.5,
"Kv": 0.4,
"gi": 1.0,
"flowset": 1.0,
"opening": 0.5,
},
),
component_node(
"pipe_1",
"amesim_pnl00r",
[
physical_port("port_1", "bidirectional", "left"),
physical_port("port_2", "bidirectional", "right"),
],
{"diam": 0.02, "le": 1.0, "rr": 1.0e-5, "gi": 1.0},
),
component_node(
"tank_1",
"tank",
[physical_port("port_a", "inlet", "left")],
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
),
],
edges=[
physical_edge("edge-1", "cylinder_1", "port_b", "valve_1", "port_2"),
physical_edge("edge-2", "valve_1", "port_3", "pipe_1", "port_1"),
physical_edge("edge-3", "pipe_1", "port_2", "tank_1", "port_a"),
],
simulation={
"t_start": 0.0,
"t_stop": 0.002,
"step": 0.001,
"max_step": 0.001,
"method": "BDF",
},
)
class AmesimPnvo001FixedOpeningXmlTests(unittest.TestCase):
def test_pnvo001_fixed_reactflow_project_compiles(self) -> None:
network = compile_reactflow_network(amesim_pnvo001_fixed_project())
self.assertIn("valve_1", network.components)
self.assertEqual(network.components["valve_1"].model_type, "amesim_pnvo001_fixed")
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
def test_pnvo001_fixed_system_xml_validates_and_simulates(self) -> None:
xml = build_reactflow_system_xml(amesim_pnvo001_fixed_project())
report = validate_system_xml_document(xml)
self.assertTrue(report.valid, report.as_dict())
result = run_system_xml_simulation(xml)
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["time"], [0.0, 0.001, 0.002])
self.assertIn("valve_1.xv", result["series"])
self.assertIn("valve_1.port_2.m_flow", result["series"])
if __name__ == "__main__":
unittest.main()
+138
View File
@@ -0,0 +1,138 @@
from __future__ import annotations
import json
from pathlib import Path
import unittest
from app.main import get_component_catalog
from app.simulation.registry import (
COMPONENT_MODEL_REGISTRY,
validate_component_registries,
)
class ComponentCatalogTests(unittest.TestCase):
def setUp(self) -> None:
self.catalog = get_component_catalog()
self.libraries = {
library["id"]: library for library in self.catalog["libraries"]
}
self.library = self.libraries["experimental"]
self.components = {
component["modelType"]: component
for library in self.catalog["libraries"]
for component in library["components"]
}
def test_experimental_library_is_exposed_as_temporary(self) -> None:
self.assertEqual(self.catalog["schemaVersion"], 1)
self.assertEqual(self.library["id"], "experimental")
self.assertEqual(self.library["label"], "临时测试组件库")
self.assertEqual(self.library["version"], "0.1.0")
self.assertTrue(self.library["temporary"])
self.assertEqual(
self.library["sourcePackage"],
"app.simulation.components.experimental",
)
self.assertEqual(
[category["id"] for category in self.library["categories"]],
["storage", "flow", "junctions"],
)
def test_amesim_library_exposes_pneumatic_models(self) -> None:
library = self.libraries["amesim"]
components = {
component["modelType"]: component for component in library["components"]
}
self.assertEqual(library["label"], "AMESim 组件库")
self.assertEqual([category["id"] for category in library["categories"]], ["storage", "flow", "junctions", "boundary"])
self.assertEqual(set(components), {"amesim_pnpl01", "amesim_pnch023", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnl00r", "amesim_pn3node2", "amesim_p4node2"})
self.assertEqual(
[port["name"] for port in components["amesim_p4node2"]["ports"]],
["port_1", "port_2", "port_3", "port_4"],
)
self.assertEqual(components["amesim_pnpl01"]["category"]["id"], "boundary")
self.assertEqual([port["name"] for port in components["amesim_pnpl01"]["ports"]], ["port_1"])
pnch_parameters = {
parameter["name"]: parameter
for parameter in components["amesim_pnch023"]["parameters"]
}
self.assertEqual(components["amesim_pnch023"]["category"]["id"], "storage")
self.assertEqual(pnch_parameters["cvol"]["quantity"], "volume")
self.assertEqual(pnch_parameters["kth"]["quantity"], "heat_transfer_coefficient")
pnor_parameters = {
parameter["name"]: parameter
for parameter in components["amesim_pnor001"]["parameters"]
}
self.assertEqual(components["amesim_pnor001"]["category"]["id"], "flow")
self.assertEqual(pnor_parameters["area"]["quantity"], "area")
self.assertEqual(pnor_parameters["area"]["unit"], "m2")
pnvo_parameters = {
parameter["name"]: parameter
for parameter in components["amesim_pnvo001_fixed"]["parameters"]
}
self.assertEqual(components["amesim_pnvo001_fixed"]["category"]["id"], "flow")
self.assertEqual(pnvo_parameters["opening"]["maximum"], 1.0)
self.assertEqual([port["name"] for port in components["amesim_pnvo001_fixed"]["ports"]], ["port_2", "port_3"])
pnl_parameters = {
parameter["name"]: parameter
for parameter in components["amesim_pnl00r"]["parameters"]
}
self.assertEqual(components["amesim_pnl00r"]["category"]["id"], "flow")
self.assertEqual(pnl_parameters["diam"]["quantity"], "length")
self.assertEqual(pnl_parameters["le"]["unit"], "m")
def test_every_registered_component_is_in_the_catalog(self) -> None:
self.assertEqual(
set(self.components),
set(COMPONENT_MODEL_REGISTRY),
)
def test_catalog_reuses_model_parameter_contracts(self) -> None:
cylinder = self.components["cylinder"]
parameters = {
parameter["name"]: parameter for parameter in cylinder["parameters"]
}
self.assertEqual(cylinder["label"], "气瓶")
self.assertEqual(cylinder["modelVersion"], "1.0.0")
self.assertEqual(parameters["volume"]["default"], 0.01)
self.assertEqual(parameters["volume"]["quantity"], "volume")
self.assertEqual(parameters["p0"]["unit"], "Pa")
self.assertTrue(parameters["p0"]["minimumExclusive"])
def test_catalog_adds_port_layout_without_changing_physical_contract(self) -> None:
tee_ports = {
port["name"]: port for port in self.components["tee"]["ports"]
}
self.assertEqual(tee_ports["port_in"]["side"], "left")
self.assertEqual(tee_ports["port_out1"]["side"], "right")
self.assertEqual(tee_ports["port_out2"]["side"], "right")
self.assertEqual(tee_ports["port_in"]["kind"], "physical")
self.assertEqual(tee_ports["port_in"]["domain"], "pneumatic")
self.assertEqual(tee_ports["port_in"]["nominalRole"], "bidirectional")
def test_registry_can_repeat_discovery_and_validation(self) -> None:
validate_component_registries()
def test_catalog_schema_is_committed_and_targets_version_one(self) -> None:
schema_path = (
Path(__file__).resolve().parent.parent
/ "schemas"
/ "component-catalog-v1.schema.json"
)
schema = json.loads(schema_path.read_text(encoding="utf-8"))
self.assertEqual(schema["properties"]["schemaVersion"]["const"], 1)
self.assertIn("modelVersion", schema["$defs"]["component"]["required"])
self.assertIn("version", schema["$defs"]["library"]["required"])
if __name__ == "__main__":
unittest.main()
+185
View File
@@ -0,0 +1,185 @@
from __future__ import annotations
import unittest
from app.main import compile_reactflow_network
from app.simulation.components.experimental.junctions.tee import Tee
from app.simulation.components.experimental.storage.cylinder import Cylinder
from app.simulation.components.experimental.storage.tank import Tank
from app.simulation.core.base import Component
from app.simulation.core.medium import IdealGasMedium
from app.simulation.core.ports import PortDefinition, PortState, PortVariableDefinition
from app.simulation.examples.testmodel.system import TestModelSystem
from app.simulation.systems.network import SimulationNetwork
from tests.test_system_xml_protocol import physical_connection_project
class ComponentInterfaceTests(unittest.TestCase):
def test_pneumatic_port_exposes_equation_roles_and_signed_flow(self) -> None:
port = PortState.pneumatic("port_a", nominal_role="bidirectional")
assert port.definition is not None
self.assertEqual(
[
(variable.name, variable.role, variable.connection_rule)
for variable in port.definition.variables
],
[
("p", "effort", "equal"),
("m_flow", "flow", "sumToZero"),
("h_outflow", "stream", "streamMix"),
],
)
port.m_flow = 10.0
self.assertEqual(port.actual_direction(), "in")
self.assertEqual(port.inflow_rate, 10.0)
self.assertEqual(port.outflow_rate, 0.0)
port.m_flow = -4.0
self.assertEqual(port.actual_direction(), "out")
self.assertEqual(port.inflow_rate, 0.0)
self.assertEqual(port.outflow_rate, 4.0)
def test_physical_connection_is_neutral_and_canonical(self) -> None:
forward = compile_reactflow_network(physical_connection_project())
reverse = compile_reactflow_network(
physical_connection_project(reverse_edge=True)
)
self.assertEqual(
forward.connections[0].undirected_key,
reverse.connections[0].undirected_key,
)
self.assertEqual(
forward.connections[0].as_interface_dict(),
reverse.connections[0].as_interface_dict(),
)
self.assertNotIn("source", forward.connections[0].as_interface_dict())
def test_compiler_instantiates_parameters_and_actual_edges(self) -> None:
project = physical_connection_project()
network = compile_reactflow_network(project)
self.assertEqual(set(network.components), {"cylinder_1", "tank_1"})
self.assertEqual(network.components["cylinder_1"].V, 0.01)
self.assertEqual(network.components["tank_1"].V, 0.1)
self.assertEqual(len(network.connections), 1)
self.assertEqual(network.connections[0].id, "edge-1")
self.assertEqual(network.connections[0].kind, "physical")
self.assertEqual(network.connections[0].domain, "pneumatic")
equation_system = network.as_interface_dict()["pressureFlowSystem"]
self.assertEqual(equation_system["unknownCount"], 4)
self.assertEqual(equation_system["equationCount"], 4)
self.assertTrue(equation_system["isSquare"])
def test_physical_port_requires_a_tee_for_branching(self) -> None:
medium = IdealGasMedium()
network = SimulationNetwork("branch-check")
network.add_component(Cylinder("cylinder_1", medium))
network.add_component(Tank("tank_1", medium))
network.add_component(Tank("tank_2", medium))
network.connect("cylinder_1", "port_b", "tank_1", "port_a")
with self.assertRaisesRegex(ValueError, "Use a junction component"):
network.connect("cylinder_1", "port_b", "tank_2", "port_a")
def test_connection_requires_the_same_variable_contract(self) -> None:
medium = IdealGasMedium()
network = SimulationNetwork("contract-check")
network.add_component(Cylinder("cylinder_1", medium))
incompatible = Component("incompatible_1")
incompatible.register_port(
PortState(
definition=PortDefinition(
name="port_a",
kind="physical",
domain="pneumatic",
nominal_role="bidirectional",
positive_flow_direction="intoComponent",
variables=(PortVariableDefinition("p", "effort", "equal"),),
)
)
)
network.add_component(incompatible)
with self.assertRaisesRegex(ValueError, "variable contracts do not match"):
network.connect("cylinder_1", "port_b", "incompatible_1", "port_a")
def test_connection_equations_are_executable_and_endpoint_neutral(self) -> None:
medium = IdealGasMedium()
network = SimulationNetwork("residual-check")
cylinder = Cylinder("cylinder_1", medium)
tank = Tank("tank_1", medium)
network.add_component(cylinder)
network.add_component(tank)
network.connect("tank_1", "port_a", "cylinder_1", "port_b")
cylinder.port_b.p = 2.0e5
cylinder.port_b.m_flow = -3.0
tank.port_a.p = 2.0e5
tank.port_a.m_flow = 3.0
residuals = {
residual.variables[0].rsplit(".", 1)[-1]: residual
for residual in network.connection_equation_residuals()
}
self.assertEqual(set(residuals), {"p", "m_flow"})
self.assertEqual(residuals["p"].relation, "equal")
self.assertEqual(residuals["m_flow"].relation, "sumToZero")
self.assertEqual(residuals["p"].value, 0.0)
self.assertEqual(residuals["m_flow"].value, 0.0)
def test_tee_ports_are_physical_and_bidirectional(self) -> None:
tee = Tee("tee_1")
self.assertEqual(
{definition.name for definition in tee.port_definitions},
{"port_in", "port_out1", "port_out2"},
)
self.assertTrue(
all(
definition.kind == "physical"
and definition.nominal_role == "bidirectional"
for definition in tee.port_definitions
)
)
def test_existing_testmodel_topology_still_builds(self) -> None:
system = TestModelSystem()
self.assertEqual(len(system.network.components), 8)
self.assertEqual(len(system.network.connections), 8)
self.assertTrue(
all(connection.kind == "physical" for connection in system.network.connections)
)
self.assertEqual(len(system.initial_state_vector()), 8)
def test_testmodel_snapshot_satisfies_acausal_connection_equations(self) -> None:
system = TestModelSystem()
state = system.consistent_initial_state_vector()
system.snapshot(state)
residuals = system.network.pressure_flow_equation_residuals()
self.assertEqual(len(residuals), 4 * len(system.network.connections))
self.assertEqual(len(system.network.pressure_flow_unknowns()), len(residuals))
for residual in residuals:
tolerance = 1e-6
self.assertLessEqual(
abs(residual.value),
tolerance,
msg=f"{residual.id} residual is {residual.value}",
)
upstream_tee = system.mytee
self.assertAlmostEqual(
upstream_tee.port_in.m_flow
+ upstream_tee.port_out1.m_flow
+ upstream_tee.port_out2.m_flow,
0.0,
places=12,
)
self.assertAlmostEqual(upstream_tee.port_in.p, upstream_tee.port_out1.p)
self.assertAlmostEqual(upstream_tee.port_in.p, upstream_tee.port_out2.p)
if __name__ == "__main__":
unittest.main()
+145
View File
@@ -0,0 +1,145 @@
from __future__ import annotations
import unittest
from app.simulation.components.experimental.flow.orifice import Orifice
from app.simulation.components.experimental.flow.resistive_pipe import ResistivePipe
from app.simulation.components.experimental.junctions.tee import Tee
from app.simulation.components.experimental.storage.cylinder import Cylinder
from app.simulation.components.experimental.storage.tank import Tank
from app.simulation.core.medium import IdealGasMedium
from app.simulation.core.ports import PortVariableDefinition
from app.simulation.examples.testmodel.dynamic_pipe import Pipe
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
class ComponentMetadataTests(unittest.TestCase):
def setUp(self) -> None:
self.medium = IdealGasMedium()
def component_instances(self):
return (
Cylinder("cylinder_1", self.medium),
Tank("tank_1", self.medium),
Pipe("pipe_dynamic_1", self.medium),
ResistivePipe("pipe_1", self.medium),
Orifice("orifice_1"),
Tee("tee_1"),
)
def test_every_component_declares_a_complete_model_contract(self) -> None:
for component in self.component_instances():
with self.subTest(component=component.name):
for attribute in (
"MODEL_TYPE",
"MODEL_VERSION",
"PORTS",
"PARAMETERS",
"RESULT_VARIABLES",
):
self.assertIn(attribute, type(component).__dict__)
self.assertTrue(component.MODEL_TYPE)
self.assertTrue(component.MODEL_VERSION)
self.assertEqual(component.model_type, component.MODEL_TYPE)
self.assertEqual(component.port_definitions, component.PORTS)
self.assertEqual(
set(component.parameter_values),
{definition.name for definition in component.PARAMETERS},
)
self.assertTrue(
all(definition.label for definition in component.PARAMETERS)
)
self.assertTrue(
all(definition.quantity for definition in component.PARAMETERS)
)
def test_registry_reuses_component_parameter_and_port_declarations(self) -> None:
registered_classes = {
"cylinder": Cylinder,
"tank": Tank,
"pipe": ResistivePipe,
"orifice": Orifice,
"tee": Tee,
}
for model_type, component_class in registered_classes.items():
with self.subTest(model_type=model_type):
spec = COMPONENT_MODEL_REGISTRY[model_type]
self.assertEqual(spec.model_type, component_class.MODEL_TYPE)
self.assertEqual(
spec.model_version,
component_class.MODEL_VERSION,
)
self.assertEqual(spec.ports, component_class.PORTS)
self.assertEqual(spec.parameters, component_class.PARAMETERS)
self.assertIs(spec.display, component_class.DISPLAY)
self.assertIs(spec.component_class, component_class)
self.assertIn("create", component_class.__dict__)
component = spec.create(f"{model_type}_1", self.medium, {})
self.assertEqual(
component.parameter_values,
{
definition.name: definition.default
for definition in component_class.PARAMETERS
},
)
def test_result_metadata_exactly_describes_each_exposed_value(self) -> None:
for component in self.component_instances():
with self.subTest(component=component.name):
values = component.result_values()
metadata = component.result_variable_metadata()
keys = [variable.key for variable in metadata]
self.assertEqual(len(keys), len(set(keys)))
self.assertEqual(
set(values),
{
variable.key.removeprefix(f"{component.name}.")
for variable in metadata
},
)
self.assertTrue(all(variable.label for variable in metadata))
self.assertTrue(all(variable.quantity for variable in metadata))
def test_storage_and_port_variables_include_display_metadata(self) -> None:
cylinder = Cylinder("cylinder_1", self.medium)
metadata = {
variable.key: variable
for variable in cylinder.result_variable_metadata()
}
pressure = metadata["cylinder_1.p"]
self.assertEqual(pressure.scope, "component")
self.assertEqual(pressure.label, "压力")
self.assertEqual(pressure.quantity, "pressure")
self.assertEqual(pressure.unit, "Pa")
mass_flow = metadata["cylinder_1.port_b.m_flow"]
self.assertEqual(mass_flow.scope, "port")
self.assertEqual(mass_flow.port_name, "port_b")
self.assertEqual(mass_flow.label, "质量流量")
self.assertEqual(mass_flow.quantity, "mass_flow")
self.assertEqual(mass_flow.unit, "kg/s")
def test_port_display_metadata_is_not_part_of_the_physical_contract(self) -> None:
first = PortVariableDefinition(
"p",
"effort",
"equal",
label="压力",
quantity="pressure",
unit="Pa",
)
second = PortVariableDefinition(
"p",
"effort",
"equal",
label="Pressure",
quantity="absolute_pressure",
unit="kPa",
)
self.assertEqual(first, second)
if __name__ == "__main__":
unittest.main()
+115
View File
@@ -0,0 +1,115 @@
from __future__ import annotations
import unittest
from app.simulation.components.experimental.library import LIBRARY
from app.simulation.components.amesim.library import LIBRARY as AMESIM_LIBRARY
from app.simulation.core.base import AlgebraicComponent
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
from app.simulation.core.medium import IdealGasMedium
from app.simulation.core.ports import PortDefinition
from app.simulation.registry import (
COMPONENT_LIBRARY_REGISTRY,
COMPONENT_MODEL_REGISTRY,
discover_component_registries,
validate_component_model_class,
)
class ComponentRegistryTests(unittest.TestCase):
def test_manifest_is_the_only_public_model_source(self) -> None:
libraries, models = discover_component_registries()
self.assertEqual(libraries, COMPONENT_LIBRARY_REGISTRY)
self.assertEqual(models, COMPONENT_MODEL_REGISTRY)
self.assertEqual(
tuple(models),
(
"cylinder",
"tank",
"pipe",
"orifice",
"tee",
"amesim_pnpl01",
"amesim_pnch023",
"amesim_pnor001",
"amesim_pnvo001_fixed",
"amesim_pnl00r",
"amesim_pn3node2",
"amesim_p4node2",
),
)
self.assertNotIn(
"app.simulation.components.experimental.flow.pipe:Pipe",
LIBRARY.models,
)
def test_amesim_library_is_enabled_as_separate_public_boundary(self) -> None:
libraries, models = discover_component_registries()
self.assertIn("amesim", libraries)
self.assertEqual(libraries["amesim"], AMESIM_LIBRARY)
self.assertEqual(models["amesim_pnpl01"].library.id, "amesim")
self.assertEqual(models["amesim_pnch023"].library.id, "amesim")
self.assertEqual(models["amesim_pnor001"].library.id, "amesim")
self.assertEqual(models["amesim_pnvo001_fixed"].library.id, "amesim")
self.assertEqual(models["amesim_pnl00r"].library.id, "amesim")
self.assertEqual(models["amesim_pn3node2"].library.id, "amesim")
self.assertEqual(models["amesim_p4node2"].library.id, "amesim")
def test_every_public_model_owns_its_catalog_contract(self) -> None:
for model_type, spec in COMPONENT_MODEL_REGISTRY.items():
with self.subTest(model_type=model_type):
declarations = spec.component_class.__dict__
self.assertIn("MODEL_VERSION", declarations)
self.assertIn("DISPLAY", declarations)
self.assertIn("create", declarations)
self.assertEqual(spec.display.library_id, spec.library.id)
self.assertIn(
spec.display.category_id,
spec.library.category_by_id,
)
def test_display_ports_must_match_physical_ports(self) -> None:
class BrokenDisplayComponent(AlgebraicComponent):
MODEL_TYPE = "broken_display"
MODEL_VERSION = "1.0.0"
PORTS = (
PortDefinition.pneumatic(
"port_a",
nominal_role="bidirectional",
),
)
PARAMETERS = ()
RESULT_VARIABLES = ()
DISPLAY = ComponentDisplaySpec(
label="错误显示模型",
library_id="experimental",
category_id="flow",
symbol="generic",
ports=(PortDisplaySpec("port_b", "right"),),
)
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters,
):
return cls(name)
with self.assertRaisesRegex(
ValueError,
"display ports must exactly match",
):
validate_component_model_class(
BrokenDisplayComponent,
library=LIBRARY,
)
if __name__ == "__main__":
unittest.main()
+12
View File
@@ -63,6 +63,18 @@ class IntegrateOdeTests(unittest.TestCase):
self.assertEqual(calls[0]["max_step"], 1.0e-3)
self.assertNotIn("first_step", calls[0])
def test_scipy_stepwise_solver_can_cancel_before_start(self) -> None:
result = integrate_ode(
rhs=lambda _time, state: state,
initial_state=[1.0],
config=SolveIVPConfig(t_start=0.0, t_stop=1.0),
cancel_check=lambda: True,
)
self.assertFalse(result.success)
self.assertEqual(result.status, "cancelled")
self.assertEqual(result.t, [0.0])
if __name__ == "__main__":
unittest.main()
+579
View File
@@ -0,0 +1,579 @@
from __future__ import annotations
import asyncio
import json
import threading
import time
import unittest
from unittest.mock import patch
from uuid import uuid4
from fastapi import HTTPException, Request
from app.main import (
ReactFlowEdgePayload,
ReactFlowProjectPayload,
SimulationCancellationPayload,
_mark_simulation_task_result,
_register_simulation_task,
build_reactflow_system_xml,
cancel_system_xml_simulation,
compile_reactflow_network,
get_system_xml_simulation,
simulation_event_stream,
simulate_system_xml,
)
from app.simulation.components.experimental.flow.resistive_pipe import ResistivePipe
from app.simulation.solvers.solver import SolveIVPConfig
from app.simulation.systems.generic import (
GenericFluidSystem,
SimulationPreparationError,
)
from tests.test_system_xml_protocol import physical_port
def component_node(
component_id: str,
model_type: str,
ports: list[dict[str, str]],
parameters: dict[str, float] | None = None,
) -> dict[str, object]:
return {
"id": component_id,
"type": "simulationComponent",
"position": {"x": 0.0, "y": 0.0},
"data": {
"label": component_id,
"componentType": model_type,
"modelType": model_type,
"ports": ports,
"parameters": parameters or {},
},
}
def physical_edge(
edge_id: str,
first_component: str,
first_port: str,
second_component: str,
second_port: str,
) -> dict[str, str]:
return {
"id": edge_id,
"source": first_component,
"sourceHandle": first_port,
"target": second_component,
"targetHandle": second_port,
}
def two_port_definitions() -> list[dict[str, str]]:
return [
physical_port("port_a", "inlet", "left"),
physical_port("port_b", "outlet", "right"),
]
def tee_port_definitions() -> list[dict[str, str]]:
return [
physical_port("port_in", "bidirectional", "left"),
physical_port("port_out1", "bidirectional", "right"),
physical_port("port_out2", "bidirectional", "right"),
]
def chain_project(*, reverse_edges: bool = False) -> ReactFlowProjectPayload:
edges = [
physical_edge("edge-1", "cylinder_1", "port_b", "orifice_1", "port_a"),
physical_edge("edge-2", "orifice_1", "port_b", "pipe_1", "port_a"),
physical_edge("edge-3", "pipe_1", "port_b", "tank_1", "port_a"),
]
if reverse_edges:
edges = [
physical_edge(
edge["id"],
edge["target"],
edge["targetHandle"],
edge["source"],
edge["sourceHandle"],
)
for edge in edges
]
return ReactFlowProjectPayload(
name="generic-chain",
nodes=[
component_node(
"cylinder_1",
"cylinder",
[physical_port("port_b", "outlet", "right")],
{"volume": 0.01, "p0": 500000.0, "T0": 300.0},
),
component_node(
"orifice_1",
"orifice",
two_port_definitions(),
{"K": 1e-5, "opening": 1.0},
),
component_node(
"pipe_1",
"pipe",
two_port_definitions(),
{
"length": 1.0,
"diameter": 0.02,
"lambda_darcy": 0.02,
"p0": 100000.0,
"T0": 300.0,
},
),
component_node(
"tank_1",
"tank",
[physical_port("port_a", "inlet", "left")],
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
),
],
edges=edges,
simulation={
"t_start": 0.0,
"t_stop": 0.01,
"step": 0.005,
"max_step": 0.001,
"method": "BDF",
},
)
def branched_project() -> ReactFlowProjectPayload:
return ReactFlowProjectPayload(
name="generic-branch",
nodes=[
component_node(
"cylinder_1",
"cylinder",
[physical_port("port_b", "outlet", "right")],
{"volume": 0.01, "p0": 500000.0, "T0": 300.0},
),
component_node("tee_1", "tee", tee_port_definitions()),
component_node(
"orifice_1",
"orifice",
two_port_definitions(),
{"K": 1e-5, "opening": 1.0},
),
component_node(
"pipe_1",
"pipe",
two_port_definitions(),
{
"length": 1.0,
"diameter": 0.02,
"lambda_darcy": 0.02,
"p0": 100000.0,
"T0": 300.0,
},
),
component_node(
"orifice_2",
"orifice",
two_port_definitions(),
{"K": 1e-5, "opening": 1.0},
),
component_node(
"pipe_2",
"pipe",
two_port_definitions(),
{
"length": 1.0,
"diameter": 0.02,
"lambda_darcy": 0.02,
"p0": 100000.0,
"T0": 300.0,
},
),
component_node("tee_2", "tee", tee_port_definitions()),
component_node(
"tank_1",
"tank",
[physical_port("port_a", "inlet", "left")],
{"volume": 0.1, "p0": 100000.0, "T0": 300.0},
),
],
edges=[
physical_edge("edge-1", "cylinder_1", "port_b", "tee_1", "port_in"),
physical_edge("edge-2", "tee_1", "port_out1", "orifice_1", "port_a"),
physical_edge("edge-3", "orifice_1", "port_b", "pipe_1", "port_a"),
physical_edge("edge-4", "pipe_1", "port_b", "tee_2", "port_out1"),
physical_edge("edge-5", "tee_1", "port_out2", "orifice_2", "port_a"),
physical_edge("edge-6", "orifice_2", "port_b", "pipe_2", "port_a"),
physical_edge("edge-7", "pipe_2", "port_b", "tee_2", "port_out2"),
physical_edge("edge-8", "tee_2", "port_in", "tank_1", "port_a"),
],
simulation={
"t_start": 0.0,
"t_stop": 0.005,
"step": 0.005,
"max_step": 0.001,
"method": "BDF",
},
)
def xml_request(body: bytes) -> Request:
delivered = False
async def receive():
nonlocal delivered
if delivered:
return {"type": "http.disconnect"}
delivered = True
return {"type": "http.request", "body": body, "more_body": False}
return Request(
{
"type": "http",
"method": "POST",
"path": "/api/system-xml/simulate",
"headers": [(b"content-type", b"application/xml")],
},
receive,
)
class GenericSystemXmlSimulationTests(unittest.TestCase):
def test_xml_pipe_compiles_to_quasi_steady_resistance(self) -> None:
network = compile_reactflow_network(chain_project())
self.assertIsInstance(network.components["pipe_1"], ResistivePipe)
structure = network.pressure_flow_structure_dict()
self.assertEqual(structure["unknownCount"], 12)
self.assertEqual(structure["equationCount"], 12)
self.assertTrue(structure["isSquare"])
def test_generic_chain_simulation_conserves_mass_and_moves_pressures(self) -> None:
network = compile_reactflow_network(chain_project())
progress: list[tuple[float, str]] = []
result = GenericFluidSystem(network).simulate(
SolveIVPConfig(
t_start=0.0,
t_stop=0.01,
method="BDF",
max_step=0.001,
),
sample_step=0.005,
progress_callback=lambda value, phase: progress.append((value, phase)),
)
self.assertTrue(result.success)
self.assertEqual(progress[0], (0.0, "initializing"))
self.assertEqual(progress[-1], (1.0, "complete"))
self.assertTrue(
all(
current[0] <= following[0]
for current, following in zip(progress, progress[1:])
)
)
self.assertIn("integrating", {phase for _, phase in progress})
self.assertIn("postprocessing", {phase for _, phase in progress})
self.assertLess(result.series["cylinder_1.p"][-1], 500000.0)
self.assertGreater(result.series["tank_1.p"][-1], 100000.0)
total_mass = [
cylinder + tank
for cylinder, tank in zip(
result.series["cylinder_1.m"],
result.series["tank_1.m"],
)
]
self.assertLess(max(total_mass) - min(total_mass), 1e-12)
total_energy = [
cylinder + tank
for cylinder, tank in zip(
result.series["cylinder_1.U"],
result.series["tank_1.U"],
)
]
self.assertLess(max(total_energy) - min(total_energy), 1e-6)
self.assertLess(
result.diagnostics["pressureFlow"]["maxScaledResidual"],
1e-7,
)
def test_cancelled_simulation_returns_accepted_partial_samples(self) -> None:
cancel_event = threading.Event()
def request_cancel_after_progress(progress: float, phase: str) -> None:
if phase == "integrating" and progress >= 0.2:
cancel_event.set()
result = GenericFluidSystem(
compile_reactflow_network(chain_project())
).simulate(
SolveIVPConfig(t_stop=0.05, method="BDF", max_step=0.001),
sample_step=0.005,
progress_callback=request_cancel_after_progress,
cancel_check=cancel_event.is_set,
)
self.assertFalse(result.success)
self.assertEqual(result.status, "cancelled")
self.assertGreaterEqual(result.diagnostics["sampleCount"], 2)
self.assertGreater(result.simulated_until, 0.0)
self.assertLess(result.simulated_until, 0.05)
self.assertEqual(
len(result.series["time"]),
len(result.series["cylinder_1.p"]),
)
def test_task_registry_distinguishes_user_stop_and_stalled_stop(self) -> None:
for reason, expected_status in (("user", "stopped"), ("stalled", "stalled")):
simulation_id = f"test-{uuid4().hex}"
task = _register_simulation_task(simulation_id)
response = cancel_system_xml_simulation(
simulation_id,
SimulationCancellationPayload(reason=reason),
)
self.assertTrue(response["accepted"])
result = _mark_simulation_task_result(
task,
{
"success": False,
"status": "cancelled",
"partial": True,
"series": {"time": [0.0, 0.1]},
},
)
snapshot = get_system_xml_simulation(simulation_id)
self.assertEqual(result["status"], expected_status)
self.assertEqual(snapshot["status"], expected_status)
self.assertEqual(snapshot["cancelReason"], reason)
self.assertEqual(snapshot["result"]["series"]["time"], [0.0, 0.1])
def test_physical_edge_order_does_not_change_simulation(self) -> None:
forward = GenericFluidSystem(
compile_reactflow_network(chain_project())
).simulate(
SolveIVPConfig(t_stop=0.005, method="BDF", max_step=0.001),
sample_step=0.005,
)
reverse = GenericFluidSystem(
compile_reactflow_network(chain_project(reverse_edges=True))
).simulate(
SolveIVPConfig(t_stop=0.005, method="BDF", max_step=0.001),
sample_step=0.005,
)
self.assertAlmostEqual(
forward.final["tank_1.p"],
reverse.final["tank_1.p"],
places=7,
)
def test_branched_topology_is_solved_without_fixed_testmodel_closure(self) -> None:
network = compile_reactflow_network(branched_project())
result = GenericFluidSystem(network).simulate(
SolveIVPConfig(t_stop=0.005, method="BDF", max_step=0.001),
sample_step=0.005,
)
self.assertTrue(result.success)
self.assertAlmostEqual(
result.final["pipe_1.port_a.m_flow"],
result.final["pipe_2.port_a.m_flow"],
places=10,
)
self.assertGreater(result.final["tank_1.p"], 100000.0)
def test_directly_coupled_storage_components_are_rejected(self) -> None:
project = chain_project()
project.nodes = [project.nodes[0], project.nodes[-1]]
project.edges = [
ReactFlowEdgePayload(
**physical_edge(
"edge-1",
"cylinder_1",
"port_b",
"tank_1",
"port_a",
)
)
]
with self.assertRaises(SimulationPreparationError) as caught:
GenericFluidSystem(compile_reactflow_network(project))
self.assertIn(
"IDEAL_STORAGE_COUPLING_UNSUPPORTED",
{issue.code for issue in caught.exception.issues},
)
def test_raw_system_xml_runs_through_generic_simulation_endpoint(self) -> None:
xml = build_reactflow_system_xml(chain_project())
response = asyncio.run(simulate_system_xml(xml_request(xml)))
self.assertTrue(response["success"])
self.assertEqual(response["simulation"]["method"], "BDF")
self.assertEqual(response["model"]["pressureFlowSystem"]["unknownCount"], 12)
self.assertEqual(response["diagnostics"]["stateCount"], 4)
self.assertGreater(response["final"]["tank_1.p"], 100000.0)
variables = {variable["key"]: variable for variable in response["variables"]}
self.assertEqual(set(variables), set(response["series"]) - {"time"})
self.assertEqual(variables["cylinder_1.p"]["componentId"], "cylinder_1")
self.assertEqual(variables["cylinder_1.p"]["label"], "压力")
self.assertEqual(variables["cylinder_1.p"]["unit"], "Pa")
self.assertEqual(variables["pipe_1.port_a.m_flow"]["scope"], "port")
self.assertEqual(
variables["pipe_1.port_a.m_flow"]["portName"],
"port_a",
)
def test_streaming_endpoint_events_have_monotonic_progress_and_result(self) -> None:
project = chain_project()
xml = build_reactflow_system_xml(project)
events = [json.loads(line) for line in simulation_event_stream(xml)]
progress_events = [
event for event in events if event["event"] == "progress"
]
progress = [event["progress"] for event in progress_events]
self.assertGreater(len(progress), 3)
self.assertTrue(
all(current <= following for current, following in zip(progress, progress[1:]))
)
preparation_events = [
event
for event in progress_events
if event["phase"] in {"validation", "compilation", "initialization"}
]
self.assertTrue(preparation_events)
self.assertTrue(all(event["progress"] == 0 for event in preparation_events))
timed_events = [
event
for event in progress_events
if event.get("simulatedTime") is not None
]
self.assertGreater(len(timed_events), 1)
for event in timed_events:
self.assertEqual(event["totalTime"], project.simulation.t_stop)
expected_progress = round(
100
* (event["simulatedTime"] - project.simulation.t_start)
/ (project.simulation.t_stop - project.simulation.t_start)
)
self.assertEqual(event["progress"], expected_progress)
self.assertEqual(events[-1]["event"], "result")
self.assertEqual(events[-1]["progress"], 100)
self.assertTrue(events[-1]["result"]["success"])
def test_streaming_task_stop_returns_partial_result(self) -> None:
project = branched_project()
project.simulation.t_stop = 2.0
project.simulation.step = 0.02
project.simulation.max_step = 0.01
xml = build_reactflow_system_xml(project)
simulation_id = f"test-{uuid4().hex}"
task = _register_simulation_task(simulation_id)
events: list[dict[str, object]] = []
stop_requested = False
for line in simulation_event_stream(xml, task=task):
event = json.loads(line)
events.append(event)
if (
not stop_requested
and event["event"] == "progress"
and event["phase"] == "integrating"
and event["progress"] >= 20
):
response = cancel_system_xml_simulation(
simulation_id,
SimulationCancellationPayload(reason="user"),
)
self.assertTrue(response["accepted"])
stop_requested = True
result_event = next(event for event in events if event["event"] == "result")
result = result_event["result"]
snapshot = get_system_xml_simulation(simulation_id)
self.assertTrue(stop_requested)
self.assertEqual(result["status"], "stopped")
self.assertTrue(result["partial"])
self.assertGreaterEqual(result["diagnostics"]["sampleCount"], 2)
self.assertGreater(result["simulatedUntil"], 0.0)
self.assertLess(result["simulatedUntil"], 2.0)
self.assertEqual(snapshot["status"], "stopped")
self.assertEqual(snapshot["result"]["status"], "stopped")
def test_streaming_endpoint_keeps_quiet_solver_connection_alive(self) -> None:
def delayed_simulation(_xml_bytes, progress_callback, _cancel_check=None):
progress_callback(49, "integrating", "正在进行时间积分与压力流量求解")
time.sleep(0.03)
return {"success": True}
with (
patch("app.main.SIMULATION_STREAM_HEARTBEAT_SECONDS", 0.005),
patch(
"app.main.run_system_xml_simulation",
side_effect=delayed_simulation,
),
):
events = [
json.loads(line)
for line in simulation_event_stream(b"<System />")
]
heartbeats = [event for event in events if event.get("heartbeat") is True]
self.assertGreaterEqual(len(heartbeats), 1)
self.assertTrue(all(event["progress"] == 49 for event in heartbeats))
self.assertTrue(all(event["phase"] == "integrating" for event in heartbeats))
self.assertEqual(events[-1]["event"], "result")
def test_streaming_endpoint_returns_structured_validation_error(self) -> None:
events = [
json.loads(line)
for line in simulation_event_stream(b"<System schemaVersion='2'>")
]
self.assertEqual(events[-1]["event"], "error")
self.assertEqual(events[-1]["status"], 422)
self.assertIn("issues", events[-1]["detail"])
def test_simulation_endpoint_returns_422_for_ideal_storage_coupling(self) -> None:
project = chain_project()
project.nodes = [project.nodes[0], project.nodes[-1]]
project.edges = [
ReactFlowEdgePayload(
**physical_edge(
"edge-1",
"cylinder_1",
"port_b",
"tank_1",
"port_a",
)
)
]
with self.assertRaises(HTTPException) as caught:
asyncio.run(
simulate_system_xml(
xml_request(build_reactflow_system_xml(project))
)
)
self.assertEqual(caught.exception.status_code, 422)
self.assertIn(
"IDEAL_STORAGE_COUPLING_UNSUPPORTED",
{
issue["code"]
for issue in caught.exception.detail["issues"]
},
)
if __name__ == "__main__":
unittest.main()
+94
View File
@@ -0,0 +1,94 @@
from __future__ import annotations
import csv
import io
import unittest
from fastapi import HTTPException
from app.main import (
SimulationResultCsvPayload,
SimulationResultVariablePayload,
export_simulation_results_csv,
)
def result_variable(
key: str,
component_id: str,
name: str,
label: str,
unit: str,
) -> SimulationResultVariablePayload:
return SimulationResultVariablePayload(
key=key,
componentId=component_id,
componentType="tank",
scope="component",
name=name,
label=label,
quantity="pressure",
unit=unit,
)
class ResultCsvExportTests(unittest.TestCase):
def valid_payload(self) -> SimulationResultCsvPayload:
return SimulationResultCsvPayload(
projectName="储气系统",
variables=[
result_variable(
"cylinder_1.p",
"cylinder_1",
"p",
"压力",
"Pa",
),
result_variable("tank_1.p", "tank_1", "p", "压力", "Pa"),
],
series={
"time": [0.0, 0.1],
"cylinder_1.p": [35000000.0, 34900000.0],
"tank_1.p": [100000.0, 101000.0],
},
)
def test_csv_export_preserves_result_keys_and_rows(self) -> None:
response = export_simulation_results_csv(self.valid_payload())
text = response.body.decode("utf-8-sig")
rows = list(csv.reader(io.StringIO(text)))
self.assertEqual(
rows[0],
["time", "cylinder_1.p", "tank_1.p"],
)
self.assertEqual(rows[1], ["0.0", "35000000.0", "100000.0"])
self.assertEqual(rows[2], ["0.1", "34900000.0", "101000.0"])
self.assertIn(
"filename*=UTF-8''",
response.headers["content-disposition"],
)
def test_csv_export_rejects_inconsistent_column_lengths(self) -> None:
payload = self.valid_payload()
payload.series["tank_1.p"] = [100000.0]
with self.assertRaises(HTTPException) as caught:
export_simulation_results_csv(payload)
self.assertEqual(caught.exception.status_code, 422)
self.assertIn("inconsistent length", str(caught.exception.detail))
def test_csv_export_requires_metadata_for_every_result_column(self) -> None:
payload = self.valid_payload()
payload.series["orphan.value"] = [1.0, 2.0]
with self.assertRaises(HTTPException) as caught:
export_simulation_results_csv(payload)
self.assertEqual(caught.exception.status_code, 422)
self.assertIn("unmapped orphan.value", str(caught.exception.detail))
if __name__ == "__main__":
unittest.main()
+32
View File
@@ -0,0 +1,32 @@
from __future__ import annotations
import unittest
from app.simulation.examples.testmodel.run import prepare_testmodel_run
from app.simulation.paths import (
PROJECT_ROOT,
SIMULATION_BASELINES_DIR,
SIMULATION_RUNS_DIR,
)
class SimulationPathTests(unittest.TestCase):
def test_default_run_paths_are_independent_of_module_depth(self) -> None:
prepared = prepare_testmodel_run()
self.assertEqual(prepared.repo_root, PROJECT_ROOT)
self.assertEqual(prepared.output_dir.parent, SIMULATION_RUNS_DIR)
def test_testmodel_baselines_live_with_the_test_assets(self) -> None:
baseline_dir = SIMULATION_BASELINES_DIR / "testmodel"
self.assertTrue((baseline_dir / "testmodel_primary_series.csv").is_file())
self.assertTrue(
(
baseline_dir / "testmodel_modelica_comparison_summary.txt"
).is_file()
)
if __name__ == "__main__":
unittest.main()
+241
View File
@@ -0,0 +1,241 @@
from __future__ import annotations
import asyncio
import unittest
from fastapi import HTTPException, Request
from app.main import (
ReactFlowEdgePayload,
ReactFlowNodePayload,
ReactFlowProjectPayload,
build_reactflow_system_xml,
compile_system_xml_model,
compile_reactflow_network,
parse_system_xml,
)
from app.system_xml import validate_system_xml_document
from tests.test_system_xml_protocol import (
physical_connection_project,
physical_port,
)
def valid_xml() -> bytes:
return build_reactflow_system_xml(physical_connection_project())
def issue_codes(xml: bytes | str) -> set[str]:
return {
issue.code
for issue in validate_system_xml_document(xml).issues
}
def xml_request(body: bytes) -> Request:
delivered = False
async def receive():
nonlocal delivered
if delivered:
return {"type": "http.disconnect"}
delivered = True
return {"type": "http.request", "body": body, "more_body": False}
return Request(
{
"type": "http",
"method": "POST",
"path": "/api/system-xml/compile-model",
"headers": [(b"content-type", b"application/xml")],
},
receive,
)
class SystemXmlParserTests(unittest.TestCase):
def test_valid_v2_xml_round_trips_and_compiles(self) -> None:
report = validate_system_xml_document(valid_xml())
self.assertTrue(report.valid)
self.assertEqual(report.issues, ())
assert report.document is not None
project = ReactFlowProjectPayload(**report.document.as_project_data())
network = compile_reactflow_network(project)
self.assertEqual(project.name, "protocol-test")
self.assertEqual(project.simulation.method, "BDF")
self.assertEqual(set(network.components), {"cylinder_1", "tank_1"})
self.assertEqual(len(network.connections), 1)
self.assertEqual(network.connections[0].id, "edge-1")
def test_compile_api_consumes_raw_xml_body(self) -> None:
response = asyncio.run(compile_system_xml_model(xml_request(valid_xml())))
self.assertTrue(response["success"])
self.assertEqual(response["name"], "protocol-test")
self.assertEqual(len(response["components"]), 2)
self.assertEqual(len(response["connections"]), 1)
self.assertTrue(response["validation"]["valid"])
def test_parse_api_returns_structured_422_diagnostics(self) -> None:
with self.assertRaises(HTTPException) as caught:
asyncio.run(parse_system_xml(xml_request(b"<broken>")))
self.assertEqual(caught.exception.status_code, 422)
detail = caught.exception.detail
self.assertEqual(detail["message"], "System XML validation failed.")
self.assertEqual(detail["issues"][0]["layer"], "xml")
self.assertEqual(detail["issues"][0]["code"], "XML_SYNTAX_ERROR")
def test_malformed_xml_reports_xml_layer_error(self) -> None:
report = validate_system_xml_document("<System><broken></System>")
self.assertFalse(report.valid)
self.assertIsNone(report.document)
self.assertEqual(report.issues[0].layer, "xml")
self.assertEqual(report.issues[0].code, "XML_SYNTAX_ERROR")
self.assertIsNotNone(report.issues[0].line)
def test_dtd_is_rejected_before_schema_validation(self) -> None:
xml = valid_xml().replace(
b"<System ",
b"<!DOCTYPE System [<!ENTITY demo 'unsafe'>]>\n<System ",
1,
)
report = validate_system_xml_document(xml)
self.assertFalse(report.valid)
self.assertEqual(report.issues[0].layer, "xml")
self.assertEqual(report.issues[0].code, "XML_DTD_NOT_ALLOWED")
def test_wrong_schema_version_reports_xsd_error(self) -> None:
xml = valid_xml().replace(b'schemaVersion="2"', b'schemaVersion="1"')
report = validate_system_xml_document(xml)
self.assertFalse(report.valid)
self.assertIsNone(report.document)
self.assertTrue(report.issues)
self.assertTrue(all(issue.layer == "schema" for issue in report.issues))
self.assertIn("XSD_VALIDATION_ERROR", issue_codes(xml))
def test_parameter_range_is_checked_semantically(self) -> None:
xml = valid_xml().replace(
b'<Parameter name="volume" value="0.01"',
b'<Parameter name="volume" value="-1"',
1,
)
report = validate_system_xml_document(xml)
self.assertFalse(report.valid)
self.assertIn("PARAMETER_VALUE_INVALID", issue_codes(xml))
issue = next(
issue for issue in report.issues if issue.code == "PARAMETER_VALUE_INVALID"
)
self.assertEqual(issue.layer, "semantic")
self.assertIn("cylinder_1.volume", issue.message)
def test_zero_initial_pressure_is_rejected_before_model_creation(self) -> None:
xml = valid_xml().replace(
b'<Parameter name="p0" value="35000000"',
b'<Parameter name="p0" value="0"',
1,
)
report = validate_system_xml_document(xml)
self.assertFalse(report.valid)
self.assertIn("PARAMETER_VALUE_INVALID", issue_codes(xml))
def test_registered_port_contract_is_checked_semantically(self) -> None:
xml = valid_xml().replace(
b'nominalRole="outlet"',
b'nominalRole="bidirectional"',
1,
)
self.assertIn("PORT_NOMINAL_ROLE_MISMATCH", issue_codes(xml))
def test_unknown_connection_endpoint_is_reported(self) -> None:
xml = valid_xml().replace(
b'component="tank_1" port="port_a"',
b'component="missing_tank" port="port_a"',
1,
)
self.assertIn("ENDPOINT_COMPONENT_UNKNOWN", issue_codes(xml))
def test_physical_port_cannot_be_used_by_two_connections(self) -> None:
project = physical_connection_project()
project.nodes.append(
ReactFlowNodePayload(**{
"id": "tank_2",
"type": "simulationComponent",
"position": {"x": 420, "y": 180},
"data": {
"label": "receiver-tank-2",
"componentType": "tank",
"modelType": "tank",
"ports": [physical_port("port_a", "inlet", "left")],
"parameters": {"volume": 0.1, "p0": 100000, "T0": 300},
},
})
)
project.edges.append(
ReactFlowEdgePayload(**{
"id": "edge-2",
"source": "cylinder_1",
"sourceHandle": "port_b",
"target": "tank_2",
"targetHandle": "port_a",
})
)
xml = build_reactflow_system_xml(project)
self.assertIn("PHYSICAL_PORT_ALREADY_CONNECTED", issue_codes(xml))
def test_unconnected_registered_port_is_a_warning(self) -> None:
project = physical_connection_project()
project.edges.clear()
report = validate_system_xml_document(build_reactflow_system_xml(project))
self.assertTrue(report.valid)
self.assertEqual(report.as_dict()["warningCount"], 2)
self.assertEqual(
{issue.code for issue in report.issues},
{"PORT_UNCONNECTED"},
)
def test_missing_required_parameter_is_reported(self) -> None:
xml = valid_xml().replace(
b' <Parameter name="T0" value="300" />\n',
b"",
1,
)
self.assertIn("PARAMETER_REQUIRED_MISSING", issue_codes(xml))
def test_unsupported_solver_method_is_reported(self) -> None:
xml = valid_xml().replace(b'method="BDF"', b'method="unknown"')
self.assertIn("SIMULATION_METHOD_UNSUPPORTED", issue_codes(xml))
def test_non_positive_simulation_steps_are_rejected_by_schema(self) -> None:
xml = valid_xml().replace(b'step="0.2"', b'step="0"')
xml = xml.replace(b'maxStep="0.01"', b'maxStep="-1"')
report = validate_system_xml_document(xml)
self.assertFalse(report.valid)
self.assertEqual(
[issue.code for issue in report.issues].count("XSD_VALIDATION_ERROR"),
2,
)
if __name__ == "__main__":
unittest.main()
+230
View File
@@ -0,0 +1,230 @@
from __future__ import annotations
import unittest
from xml.etree import ElementTree as ET
from app.main import ReactFlowProjectPayload, build_reactflow_system_xml
def physical_port(
name: str,
role: str,
side: str,
*,
domain: str = "pneumatic",
) -> dict[str, str]:
return {
"name": name,
"kind": "physical",
"domain": domain,
"nominalRole": role,
"positiveFlowDirection": "intoComponent",
"side": side,
}
def physical_connection_project(*, reverse_edge: bool = False) -> ReactFlowProjectPayload:
source = {
"id": "cylinder_1",
"type": "simulationComponent",
"position": {"x": 12.5, "y": 24.0},
"data": {
"label": "source-cylinder",
"componentType": "cylinder",
"modelType": "cylinder",
"ports": [physical_port("port_b", "outlet", "right")],
"parameters": {"volume": 0.01, "p0": 35000000, "T0": 300},
},
}
target = {
"id": "tank_1",
"type": "simulationComponent",
"position": {"x": 420.0, "y": 24.0},
"data": {
"label": "receiver-tank",
"componentType": "tank",
"modelType": "tank",
"ports": [physical_port("port_a", "inlet", "left")],
"parameters": {"volume": 0.1, "p0": 100000, "T0": 300},
},
}
edge = (
{
"id": "edge-1",
"source": "tank_1",
"sourceHandle": "port_a",
"target": "cylinder_1",
"targetHandle": "port_b",
}
if reverse_edge
else {
"id": "edge-1",
"source": "cylinder_1",
"sourceHandle": "port_b",
"target": "tank_1",
"targetHandle": "port_a",
}
)
return ReactFlowProjectPayload(
name="protocol-test",
nodes=[source, target],
edges=[edge],
simulation={
"t_start": 1.0,
"t_stop": 5.0,
"step": 0.2,
"max_step": 0.01,
"method": "BDF",
},
)
class SystemXmlProtocolTests(unittest.TestCase):
def test_v2_xml_contains_port_metadata_and_neutral_physical_endpoints(self) -> None:
root = ET.fromstring(build_reactflow_system_xml(physical_connection_project()))
self.assertEqual(root.tag, "System")
self.assertEqual(root.attrib["name"], "protocol-test")
self.assertEqual(root.attrib["schemaVersion"], "2")
self.assertEqual(root.attrib["unitSystem"], "SI")
self.assertEqual(
[child.tag for child in root],
["Simulation", "Components", "Connections"],
)
simulation = root.find("Simulation")
assert simulation is not None
self.assertEqual(
simulation.attrib,
{
"tStart": "1.0",
"tStop": "5.0",
"step": "0.2",
"maxStep": "0.01",
"method": "BDF",
},
)
port = root.find("./Components/Component[@id='cylinder_1']/Port")
assert port is not None
component = root.find("./Components/Component[@id='cylinder_1']")
assert component is not None
self.assertEqual(component.attrib["rotation"], "0")
self.assertEqual(component.attrib["mirrored"], "false")
self.assertEqual(
port.attrib,
{
"name": "port_b",
"kind": "physical",
"domain": "pneumatic",
"nominalRole": "outlet",
"side": "right",
"positiveFlowDirection": "intoComponent",
},
)
connection = root.find("./Connections/Connection")
assert connection is not None
self.assertEqual(
connection.attrib,
{"id": "edge-1", "kind": "physical", "domain": "pneumatic"},
)
self.assertNotIn("source", connection.attrib)
self.assertEqual(
{
(endpoint.attrib["component"], endpoint.attrib["port"])
for endpoint in connection.findall("Endpoint")
},
{("cylinder_1", "port_b"), ("tank_1", "port_a")},
)
self.assertTrue(
all("role" not in endpoint.attrib for endpoint in connection.findall("Endpoint"))
)
def test_physical_connection_meaning_does_not_depend_on_drag_direction(self) -> None:
forward = ET.fromstring(
build_reactflow_system_xml(physical_connection_project())
)
reverse = ET.fromstring(
build_reactflow_system_xml(physical_connection_project(reverse_edge=True))
)
def endpoint_set(root: ET.Element) -> set[tuple[str, str]]:
return {
(endpoint.attrib["component"], endpoint.attrib["port"])
for endpoint in root.findall("./Connections/Connection/Endpoint")
}
self.assertEqual(endpoint_set(forward), endpoint_set(reverse))
def test_component_orientation_is_exported_as_layout_metadata(self) -> None:
project = physical_connection_project()
project.nodes[0].data.rotation = 90
project.nodes[0].data.mirrored = True
root = ET.fromstring(build_reactflow_system_xml(project))
component = root.find("./Components/Component[@id='cylinder_1']")
assert component is not None
self.assertEqual(component.attrib["rotation"], "90")
self.assertEqual(component.attrib["mirrored"], "true")
def test_legacy_string_port_is_migrated_when_exporting_v2(self) -> None:
project = ReactFlowProjectPayload(
name="legacy-port",
nodes=[
{
"id": "tank_1",
"data": {
"label": "tank_1",
"componentType": "tank",
"modelType": "tank",
"ports": ["port_a"],
},
}
],
)
root = ET.fromstring(build_reactflow_system_xml(project))
port = root.find("./Components/Component/Port")
assert port is not None
self.assertEqual(port.attrib["kind"], "physical")
self.assertEqual(port.attrib["domain"], "pneumatic")
self.assertEqual(port.attrib["nominalRole"], "inlet")
self.assertEqual(port.attrib["positiveFlowDirection"], "intoComponent")
self.assertEqual(port.attrib["side"], "left")
def test_legacy_tee_port_names_do_not_restore_direction_constraints(self) -> None:
project = ReactFlowProjectPayload(
name="legacy-tee",
nodes=[
{
"id": "tee_1",
"data": {
"label": "tee_1",
"componentType": "tee",
"modelType": "tee",
"ports": ["port_in", "port_out1", "port_out2"],
},
}
],
)
root = ET.fromstring(build_reactflow_system_xml(project))
ports = root.findall("./Components/Component/Port")
self.assertEqual(
[port.attrib["nominalRole"] for port in ports],
["bidirectional", "bidirectional", "bidirectional"],
)
def test_incompatible_physical_domains_are_rejected(self) -> None:
project = physical_connection_project()
tank_port = project.nodes[1].data.ports[0]
assert not isinstance(tank_port, str)
tank_port.domain = "hydraulic"
with self.assertRaisesRegex(ValueError, "incompatible domains"):
build_reactflow_system_xml(project)
if __name__ == "__main__":
unittest.main()
+58
View File
@@ -0,0 +1,58 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from app.main import ReactFlowProjectPayload, run_reactflow_test_mql
from app.simulation.examples.test_mql.run import prepare_test_mql_run, run_test_mql
from app.simulation.paths import PROJECT_ROOT, SIMULATION_RUNS_DIR
from PythonModels.systems.test_mql import TestMqlRunConfig
class TestMqlExampleRunnerTests(unittest.TestCase):
def test_default_run_paths_use_app_simulation_runs_dir(self) -> None:
prepared = prepare_test_mql_run()
self.assertEqual(prepared.repo_root, PROJECT_ROOT)
self.assertEqual(prepared.output_dir.parent, SIMULATION_RUNS_DIR)
def test_zero_duration_run_writes_summary(self) -> None:
with tempfile.TemporaryDirectory() as directory:
output_dir = Path(directory) / "test_mql"
result = run_test_mql(
run_config=TestMqlRunConfig(t_start=0.0, t_stop=0.0),
output_dir=output_dir,
)
self.assertEqual(result.result.t, [0.0])
self.assertEqual(result.summary_path, output_dir / "test_mql_model_summary.txt")
self.assertIn(
"Model: test_mql",
result.summary_path.read_text(encoding="utf-8"),
)
def test_reactflow_helper_runs_fixed_topology_test_mql(self) -> None:
payload = ReactFlowProjectPayload(
name="test-mql-fixed",
simulation={
"t_start": 0.0,
"t_stop": 0.2,
"step": 0.1,
"max_step": 0.001,
"method": "BDF",
},
)
result = run_reactflow_test_mql(payload)
self.assertTrue(result["success"])
self.assertEqual(result["model"]["name"], "test_mql")
self.assertEqual(result["model"]["componentCount"], 117)
self.assertEqual(result["series"]["time"], [0.0, 0.1, 0.2])
self.assertIn("summary", result["artifacts"])
if __name__ == "__main__":
unittest.main()