Compare commits
130
Commits
bbc88a6bbb
...
503394f3ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
503394f3ed | ||
|
|
410ef535e8 | ||
|
|
e7177ab03e | ||
|
|
3e4f466114 | ||
|
|
127ec36a55 | ||
|
|
e87df789b2 | ||
|
|
2ddaaeb56a | ||
|
|
3f51a124d6 | ||
|
|
db4bdb4b70 | ||
|
|
420bafeb4e | ||
|
|
e17a952bee | ||
|
|
a8a68f6dd4 | ||
|
|
71da73ef70 | ||
|
|
cd2fa800b9 | ||
|
|
532fdb4e6b | ||
|
|
bd7c8c99e9 | ||
|
|
bf4dc0af3d | ||
|
|
200d4593e8 | ||
|
|
890bf1d483 | ||
|
|
137a4cf7d6 | ||
|
|
920ee336e9 | ||
|
|
fc46c4272a | ||
|
|
8b9f229b80 | ||
|
|
918952f130 | ||
|
|
d67fb251d2 | ||
|
|
e473162d4c | ||
|
|
8a2d2ae190 | ||
|
|
7182f929e8 | ||
|
|
021ea63a2b | ||
|
|
f8af99ace1 | ||
|
|
01b606df30 | ||
|
|
331435e78a | ||
|
|
a219cddc0b | ||
|
|
1602e0dc51 | ||
|
|
84d1675292 | ||
|
|
f0f8f40c7a | ||
|
|
2120901909 | ||
|
|
a0855018d5 | ||
|
|
7c68caed27 | ||
|
|
65f6d6dd87 | ||
|
|
2c2952cff1 | ||
|
|
a6e1faa2d8 | ||
|
|
3951bc372c | ||
|
|
eaafe5eafc | ||
|
|
6953c864c4 | ||
|
|
958d813300 | ||
|
|
d4edf1ccb3 | ||
|
|
65cdd2284e | ||
|
|
bb4a71c612 | ||
|
|
c8de46fac2 | ||
|
|
8782f96baf | ||
|
|
0035243361 | ||
|
|
716a1c572a | ||
|
|
3d1de6d5d0 | ||
|
|
7cb050d235 | ||
|
|
3d572767d2 | ||
|
|
be1eec1348 | ||
|
|
cdd17d29c8 | ||
|
|
a52019c5e9 | ||
|
|
777f943660 | ||
|
|
65623fb1dd | ||
|
|
cf33d445a2 | ||
|
|
13f37bfd0f | ||
|
|
d02dacae0f | ||
|
|
7b9d28c0ee | ||
|
|
2622bb8c92 | ||
|
|
dd2efff487 | ||
|
|
acee83fe53 | ||
|
|
bdbac6fbb5 | ||
|
|
09fb3a5f72 | ||
|
|
2afd5eeb11 | ||
|
|
4242057bd8 | ||
|
|
492562a517 | ||
|
|
a1bddfcd98 | ||
|
|
8e1263833b | ||
|
|
b44bbbd110 | ||
|
|
a09a8c8daf | ||
|
|
8639b42574 | ||
|
|
f74b414b24 | ||
|
|
8c8b4f26e9 | ||
|
|
62c8aabeab | ||
|
|
95ba5a5e81 | ||
|
|
853ae3043d | ||
|
|
43e238034a | ||
|
|
8af0a27e4c | ||
|
|
fafce0af87 | ||
|
|
a071896834 | ||
|
|
7569917924 | ||
|
|
5c3ad50090 | ||
|
|
78013b25c9 | ||
|
|
568be2e632 | ||
|
|
c4c1b35dee | ||
|
|
1193b2dcee | ||
|
|
2d33a8f4bc | ||
|
|
c0f28520b4 | ||
|
|
95ebca89f6 | ||
|
|
2cea81739e | ||
|
|
314b65872e | ||
|
|
94badfb56f | ||
|
|
a1870f2789 | ||
|
|
0cd4359964 | ||
|
|
bef9dce55e | ||
|
|
3ac83d9f00 | ||
|
|
999d840615 | ||
|
|
b3feb5c7e0 | ||
|
|
77e2f10cd5 | ||
|
|
d35e85e066 | ||
|
|
6ccbf7eb5e | ||
|
|
9eb7d293f7 | ||
|
|
143097dc28 | ||
|
|
c84a809f77 | ||
|
|
f6871aefac | ||
|
|
6f9e7084dd | ||
|
|
7c820cbada | ||
|
|
c7b92f99c6 | ||
|
|
2199f1c11e | ||
|
|
f6dda509a2 | ||
|
|
a63fda313b | ||
|
|
6f34ac16bf | ||
|
|
8f14a3840f | ||
|
|
03a6db10ad | ||
|
|
3818b011d5 | ||
|
|
33c8bfae3d | ||
|
|
1f47171d07 | ||
|
|
796433c275 | ||
|
|
a598fcd5c3 | ||
|
|
cdead78977 | ||
|
|
056b71fead | ||
|
|
c745fd2a5c | ||
|
|
2c38da16b5 |
No files matched your search
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,388 @@
|
||||
/* Submodel PNCH012 skeleton created by AME Submodel editing utility
|
||||
mar. oct. 9 14:41:15 2018 */
|
||||
|
||||
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "ameutils.h"
|
||||
/* *******************************************************************************
|
||||
TITLE : PNCH012
|
||||
--------------------------------------------------------------------------------
|
||||
DESCRIPTION :
|
||||
This submodel represents a pneumatic chamber with a variable volume
|
||||
and pressure dynamics.
|
||||
|
||||
Each port receives a mass flow rate and an enthalpy flow rate as
|
||||
input and gives the pressure and the temperature of the chamber as
|
||||
output. Each port receives also the volume and volume variation as
|
||||
input. The total volume is calculated by summing the four volume
|
||||
inputs and a dead volume which is a parameter of PNCH012.
|
||||
|
||||
The model takes into account heat exchange. It express the variation
|
||||
of internal energy U using the first law of thermodynamics applied to
|
||||
an open system. Therefore, this model should be preferred to the simple
|
||||
polytropic chamber PNCH011.
|
||||
|
||||
The total volume of the chamber is limited to a lower value equal to
|
||||
the dead volume divided by 100.
|
||||
--------------------------------------------------------------------------------
|
||||
USAGE :
|
||||
Use this submodel to simulate a pneumatic chamber in a jack, spool
|
||||
valve or any pneumatic chamber in which the volume can vary.
|
||||
|
||||
This submodel can be directly connected to any pneumatic PCD
|
||||
component or standard pneumatic component.
|
||||
|
||||
The submodels PNGD001, PNGD002, PNGD003, PNGD004 or PNRGD00 should be
|
||||
included in your circuit to define the characteristics of the gas.
|
||||
--------------------------------------------------------------------------------
|
||||
PARAMETER SETTINGS:
|
||||
The dead volume is the volume of the pneumatic fluid when all the input
|
||||
volumes are zero. It is essential that this volume must be greater
|
||||
than zero.
|
||||
--------------------------------------------------------------------------------
|
||||
DATE OF CREATION / AUTHOR :
|
||||
2002 FS from PNCH12
|
||||
--------------------------------------------------------------------------------
|
||||
INDEX OF REVISIONS :
|
||||
2008 OBA - Real gas improvements : the mass and volume were considered as
|
||||
internal state variable, they are now coded as internal basic
|
||||
variable. The mass initialisation was removed as it was linked
|
||||
to the perfect gas formulation.
|
||||
--------------------------------------------------------------------------------
|
||||
LIST OF FUNCTIONS USED :
|
||||
pn2getatp : get atmospheric pressure
|
||||
firstc_ : checks if this is the first call to this submodel
|
||||
pn2vol_ : pneumatic chamber with heat exchange
|
||||
stepdn_ : reduce simulation step
|
||||
--------------------------------------------------------------------------------
|
||||
SOURCE :
|
||||
|
||||
This material contains trade secrets or otherwise confidential
|
||||
information owned by Siemens Industry Software Inc. or its
|
||||
affiliates (collectively, "Siemens"), or its licensors. Access to
|
||||
and use of this information is strictly limited as set forth in the
|
||||
Customer's applicable agreements with Siemens.
|
||||
|
||||
Unpublished work. Copyright 2023 Siemens
|
||||
|
||||
******************************************************************************* */
|
||||
|
||||
#define _SUBMODELNAME_ "PNCH012"
|
||||
|
||||
/* >>>>>>>>>>>>Insert Private Code Here. */
|
||||
/* <<<<<<<<<<<<End of Private Code. */
|
||||
|
||||
|
||||
/* There are 4 real parameters:
|
||||
|
||||
cvol0 dead volume [L -> m**3]
|
||||
kth thermal exchange coefficient [J/m**2/K/s -> W/m**2/K]
|
||||
sth thermal exchange area [m**2]
|
||||
extemp external temperature [K]
|
||||
*/
|
||||
|
||||
|
||||
/* There is 1 integer parameter:
|
||||
|
||||
gi gas type index
|
||||
*/
|
||||
|
||||
void pnch012in_(int *n, double rp[4], int ip[1], double c[2]
|
||||
, int ic[2], double *temp, double *press, double *dvol1
|
||||
, double *vol1, double *dvol2, double *vol2, double *dvol3
|
||||
, double *vol3, double *dvol4, double *vol4)
|
||||
|
||||
{
|
||||
int loop, error;
|
||||
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
|
||||
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
|
||||
int gi;
|
||||
double cvol0, kth, sth, extemp;
|
||||
|
||||
gi = ip[0];
|
||||
|
||||
cvol0 = rp[0];
|
||||
kth = rp[1];
|
||||
sth = rp[2];
|
||||
extemp = rp[3];
|
||||
loop = 0;
|
||||
error = 0;
|
||||
|
||||
/* Assign default values to input(s) with default. */
|
||||
|
||||
*dvol1 = 0.00000000000000e+000;
|
||||
*vol1 = 0.00000000000000e+000;
|
||||
*dvol2 = 0.00000000000000e+000;
|
||||
*vol2 = 0.00000000000000e+000;
|
||||
*dvol3 = 0.00000000000000e+000;
|
||||
*vol3 = 0.00000000000000e+000;
|
||||
*dvol4 = 0.00000000000000e+000;
|
||||
*vol4 = 0.00000000000000e+000;
|
||||
|
||||
/*
|
||||
If necessary, check values of the following:
|
||||
|
||||
rp[0..3]
|
||||
*temp
|
||||
*press
|
||||
*/
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Check Statements. */
|
||||
|
||||
pn2_valid_gas_(&gi, &error);
|
||||
|
||||
if (cvol0 <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nVolume chamber must be strictly positive.\n");
|
||||
}
|
||||
|
||||
if (kth < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nthermal exchange coefficient must be positive.\n");
|
||||
}
|
||||
|
||||
if (sth < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nthermal exchange area must be positive.\n");
|
||||
}
|
||||
|
||||
if (extemp <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nExternal temperature must be strictly positive.\n");
|
||||
}
|
||||
|
||||
if (*temp <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nInitial temperature must be strictly positive.\n");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Check Statements. */
|
||||
|
||||
/* Integer parameter checking: */
|
||||
|
||||
if (gi < 1 || gi > 99)
|
||||
{
|
||||
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
|
||||
error = 2;
|
||||
}
|
||||
|
||||
if(error == 1)
|
||||
{
|
||||
amefprintf(stderr, "\nWarning in %s instance %d.\n", _SUBMODELNAME_, *n);
|
||||
}
|
||||
else if(error == 2)
|
||||
{
|
||||
amefprintf(stderr, "\nFatal error in %s instance %d.\n", _SUBMODELNAME_, *n);
|
||||
amefprintf(stderr, "Terminating the program.\n");
|
||||
AmeExit(1);
|
||||
}
|
||||
|
||||
/* Common -> SI units conversions. */
|
||||
|
||||
rp[0] *= 1.00000000000000e-003;
|
||||
cvol0 = rp[0];
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
|
||||
|
||||
c[0] = cvol0 / 100;
|
||||
|
||||
/* Set initial value for the test of limited volume :
|
||||
ic[1] = 1 when the chamber volume is limited to cvol0 / 100 else ic[1] = 0*/
|
||||
ic[1] = 0;
|
||||
|
||||
/* set atmospheric pressure */
|
||||
c[1] = pn2getatp_();
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
|
||||
}
|
||||
|
||||
/* There are 4 ports.
|
||||
|
||||
Port 1 has 6 variables:
|
||||
|
||||
1 temp temperature [K] explicit state (derivative `dtemp')
|
||||
2 press pressure [Pa] explicit state (derivative `dpress')
|
||||
3 dh1 enthalpy flow rate at port 1 [J/s -> W] basic variable input
|
||||
4 dm1 mass flow rate at port 1 [g/s -> kg/s] basic variable input
|
||||
5 dvol1 derivative of volume at port 1 [L/min -> m**3/s] basic variable input with default 0.000000e+000
|
||||
6 vol1 volume at port 1 [cm**3 -> m**3] basic variable input with default 0.000000e+000
|
||||
|
||||
Port 2 has 6 variables:
|
||||
|
||||
1 temp2 duplicate of temp
|
||||
2 press2 duplicate of press
|
||||
3 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable input
|
||||
4 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable input
|
||||
5 dvol2 derivative of volume at port 2 [L/min -> m**3/s] basic variable input with default 0.000000e+000
|
||||
6 vol2 volume at port 2 [cm**3 -> m**3] basic variable input with default 0.000000e+000
|
||||
|
||||
Port 3 has 6 variables:
|
||||
|
||||
1 temp3 duplicate of temp
|
||||
2 press3 duplicate of press
|
||||
3 dh3 enthalpy flow rate at port 3 [J/s -> W] basic variable input
|
||||
4 dm3 mass flow rate at port 3 [g/s -> kg/s] basic variable input
|
||||
5 dvol3 derivative of volume at port 3 [L/min -> m**3/s] basic variable input with default 0.000000e+000
|
||||
6 vol3 volume at port 3 [cm**3 -> m**3] basic variable input with default 0.000000e+000
|
||||
|
||||
Port 4 has 6 variables:
|
||||
|
||||
1 temp4 duplicate of temp
|
||||
2 press4 duplicate of press
|
||||
3 dh4 enthalpy flow rate at port 4 [J/s -> W] basic variable input
|
||||
4 dm4 mass flow rate at port 4 [g/s -> kg/s] basic variable input
|
||||
5 dvol4 derivative of volume at port 4 [L/min -> m**3/s] basic variable input with default 0.000000e+000
|
||||
6 vol4 volume at port 4 [cm**3 -> m**3] basic variable input with default 0.000000e+000
|
||||
*/
|
||||
|
||||
/* There are 2 internal variables.
|
||||
|
||||
1 vol volume of pneumatic chamber [cm**3 -> m**3] basic variable
|
||||
2 mgas1 mass of gas in chamber [g -> kg] basic variable
|
||||
*/
|
||||
|
||||
void pnch012_(int *n, double *temp, double *dtemp, double *press
|
||||
, double *dpress, double *dh1, double *dm1, double *dvol1
|
||||
, double *vol1, double *dh2, double *dm2, double *dvol2
|
||||
, double *vol2, double *dh3, double *dm3, double *dvol3
|
||||
, double *vol3, double *dh4, double *dm4, double *dvol4
|
||||
, double *vol4, double *vol, double *mgas1, double rp[4]
|
||||
, int ip[1], double c[2], int ic[2])
|
||||
|
||||
{
|
||||
int loop;
|
||||
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
|
||||
double dvol;
|
||||
double sdm, sdh;
|
||||
double dq;
|
||||
double pressa;
|
||||
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
|
||||
int gi;
|
||||
double cvol0, kth, sth, extemp;
|
||||
|
||||
gi = ip[0];
|
||||
|
||||
cvol0 = rp[0];
|
||||
kth = rp[1];
|
||||
sth = rp[2];
|
||||
extemp = rp[3];
|
||||
loop = 0;
|
||||
|
||||
/* Common -> SI units conversions. */
|
||||
|
||||
*dm1 *= 1.00000000000000e-003;
|
||||
*dvol1 *= 1.66666666666667e-005;
|
||||
*vol1 *= 1.00000000000000e-006;
|
||||
*dm2 *= 1.00000000000000e-003;
|
||||
*dvol2 *= 1.66666666666667e-005;
|
||||
*vol2 *= 1.00000000000000e-006;
|
||||
*dm3 *= 1.00000000000000e-003;
|
||||
*dvol3 *= 1.66666666666667e-005;
|
||||
*vol3 *= 1.00000000000000e-006;
|
||||
*dm4 *= 1.00000000000000e-003;
|
||||
*dvol4 *= 1.66666666666667e-005;
|
||||
*vol4 *= 1.00000000000000e-006;
|
||||
|
||||
/*
|
||||
Set all submodel outputs below:
|
||||
|
||||
*dtemp = ??;
|
||||
*dpress = ??;
|
||||
*vol = ??;
|
||||
*mgas1 = ??;
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
|
||||
|
||||
/* set absolute pressure */
|
||||
pressa = *press + c[1];
|
||||
|
||||
/*** sum of the volume variation and volume ***/
|
||||
dvol = *dvol1 + *dvol2 + *dvol3 + *dvol4;
|
||||
|
||||
/*** setup the initial mass of the gaz inside of the chamber ***/
|
||||
*vol = *vol1 + *vol2 + *vol3 + *vol4 + cvol0;
|
||||
|
||||
/*** sum of the flows ***/
|
||||
sdm = *dm1 + *dm2 + *dm3 + *dm4; /* mass flow */
|
||||
sdh = *dh1 + *dh2 + *dh3 + *dh4; /* heat flow */
|
||||
|
||||
/*** V, M, T and P can not be lower than zero ***/
|
||||
*vol = llimit_(vol, &c[0], &ic[0]);
|
||||
|
||||
if (ic[0] == -1)
|
||||
{
|
||||
dvol = 0.;
|
||||
if (ic[1] == 0)
|
||||
{
|
||||
amefprintf(stderr, "\nWarning in %s instance %d chamber volume is limited by cvol0 / 100 = %g cm**3.\n", _SUBMODELNAME_, *n, c[0]*1E+6);
|
||||
ic[1] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (*vol < c[0]/10)
|
||||
{
|
||||
*vol = c[0]/10;
|
||||
}
|
||||
|
||||
if ( (*mgas1 <= 1.0e-10) && (!firstc_()) )
|
||||
{
|
||||
/* panic step reduction */
|
||||
stepdn_();
|
||||
*mgas1 = 1.0e-10;
|
||||
}
|
||||
|
||||
if (pressa <= 1.0e-10)
|
||||
{
|
||||
/* panic step reduction */
|
||||
stepdn_();
|
||||
*press = 1.0e-10 - c[1];
|
||||
}
|
||||
|
||||
if (*temp <= 1.0e-10)
|
||||
{
|
||||
/* panic step reduction */
|
||||
stepdn_();
|
||||
*temp = 1.0e-10;
|
||||
}
|
||||
|
||||
/*** temperature & pressure variation ***/
|
||||
dq = kth*sth*(extemp-*temp);
|
||||
|
||||
pn2vol_(dtemp, dpress, mgas1, temp, &pressa,
|
||||
&sdm, &sdh, vol, &dvol, &dq, &gi);
|
||||
|
||||
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
|
||||
|
||||
/* SI -> Common units conversions. */
|
||||
|
||||
*dm1 /= 1.00000000000000e-003;
|
||||
*dvol1 /= 1.66666666666667e-005;
|
||||
*vol1 /= 1.00000000000000e-006;
|
||||
*dm2 /= 1.00000000000000e-003;
|
||||
*dvol2 /= 1.66666666666667e-005;
|
||||
*vol2 /= 1.00000000000000e-006;
|
||||
*dm3 /= 1.00000000000000e-003;
|
||||
*dvol3 /= 1.66666666666667e-005;
|
||||
*vol3 /= 1.00000000000000e-006;
|
||||
*dm4 /= 1.00000000000000e-003;
|
||||
*dvol4 /= 1.66666666666667e-005;
|
||||
*vol4 /= 1.00000000000000e-006;
|
||||
*vol /= 1.00000000000000e-006;
|
||||
*mgas1 /= 1.00000000000000e-003;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<!DOCTYPE SPE>
|
||||
<SPE DOC_VERSION="2" AME_VERSION="16.0.0 - 68387-65635 2017">
|
||||
|
||||
<SUBMODEL>
|
||||
<SUB_TYPE>0</SUB_TYPE>
|
||||
<SUB_ID_MAX>33</SUB_ID_MAX>
|
||||
<DEFAULT_ICON>pn_c1</DEFAULT_ICON>
|
||||
<SUB_LABEL>variable volume pneumatic chamber with heat exchange (preferred)</SUB_LABEL>
|
||||
<SUB_UNIT>0</SUB_UNIT>
|
||||
<R_STORES_NUMBER>2</R_STORES_NUMBER>
|
||||
<I_STORES_NUMBER>2</I_STORES_NUMBER>
|
||||
<OUTPUT_TYPE>1</OUTPUT_TYPE>
|
||||
<RPARAMS_LIST>
|
||||
<RPARAM>
|
||||
<SUB_ID>27</SUB_ID>
|
||||
<TITLE>dead volume</TITLE>
|
||||
<VARNAME>cvol0</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e+00</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e+00</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+005</MAX_VALUE>
|
||||
<UNITS>L</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>28</SUB_ID>
|
||||
<TITLE>thermal exchange coefficient</TITLE>
|
||||
<VARNAME>kth</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>0.00000000000000e+00</DEF_VALUE>
|
||||
<VALUE>0.00000000000000e+00</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
|
||||
<UNITS>J/m**2/K/s</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>29</SUB_ID>
|
||||
<TITLE>thermal exchange area</TITLE>
|
||||
<VARNAME>sth</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e-01</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e-01</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+002</MAX_VALUE>
|
||||
<UNITS>m**2</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>30</SUB_ID>
|
||||
<TITLE>external temperature</TITLE>
|
||||
<VARNAME>extemp</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>2.93150000000000e+02</DEF_VALUE>
|
||||
<VALUE>2.93150000000000e+02</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+003</MAX_VALUE>
|
||||
<UNITS>K</UNITS>
|
||||
</RPARAM>
|
||||
</RPARAMS_LIST>
|
||||
<IPARAMS_LIST>
|
||||
<IPARAM>
|
||||
<SUB_ID>31</SUB_ID>
|
||||
<TITLE>gas type index</TITLE>
|
||||
<VARNAME>gi</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1</DEF_VALUE>
|
||||
<VALUE>1</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>99</MAX_VALUE>
|
||||
</IPARAM>
|
||||
</IPARAMS_LIST>
|
||||
<IVARS_LIST>
|
||||
<IVAR>
|
||||
<SUB_ID>32</SUB_ID>
|
||||
<TITLE>volume of pneumatic chamber</TITLE>
|
||||
<VARNAME>vol</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>cm**3</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>33</SUB_ID>
|
||||
<TITLE>mass of gas in chamber</TITLE>
|
||||
<VARNAME>mgas1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>g</UNITS>
|
||||
</IVAR>
|
||||
</IVARS_LIST>
|
||||
<EVARS_LIST>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>1</SUB_ID>
|
||||
<TITLE>temperature</TITLE>
|
||||
<VARNAME>temp</VARNAME>
|
||||
<VARNAME2>dtemp</VARNAME2>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>1</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>K</UNITS>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+003</MAX_VALUE>
|
||||
<DEF_VALUE>2.93150000000000e+002</DEF_VALUE>
|
||||
<VALUE>2.93150000000000e+002</VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>2</SUB_ID>
|
||||
<TITLE>pressure</TITLE>
|
||||
<VARNAME>press</VARNAME>
|
||||
<VARNAME2>dpress</VARNAME2>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>1</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
<MIN_VALUE>-1.01300000000000e+005</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+012</MAX_VALUE>
|
||||
<DEF_VALUE>0.00000000000000e+000</DEF_VALUE>
|
||||
<VALUE>0.00000000000000e+000</VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>3</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 1</TITLE>
|
||||
<VARNAME>dh1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>4</SUB_ID>
|
||||
<TITLE>mass flow rate at port 1</TITLE>
|
||||
<VARNAME>dm1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>5</SUB_ID>
|
||||
<TITLE>derivative of volume at port 1</TITLE>
|
||||
<VARNAME>dvol1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>3</IO>
|
||||
<UNITS>L/min</UNITS>
|
||||
<DEF_VALUE>0</DEF_VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>6</SUB_ID>
|
||||
<TITLE>volume at port 1</TITLE>
|
||||
<VARNAME>vol1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>3</IO>
|
||||
<UNITS>cm**3</UNITS>
|
||||
<DEF_VALUE>0</DEF_VALUE>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>7</SUB_ID>
|
||||
<VARNAME>temp2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>0</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>0</PRIMARY_VAR>
|
||||
<DUP_TYPE>0</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>8</SUB_ID>
|
||||
<VARNAME>press2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>0</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>1</PRIMARY_VAR>
|
||||
<DUP_TYPE>0</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>9</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 2</TITLE>
|
||||
<VARNAME>dh2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>10</SUB_ID>
|
||||
<TITLE>mass flow rate at port 2</TITLE>
|
||||
<VARNAME>dm2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>11</SUB_ID>
|
||||
<TITLE>derivative of volume at port 2</TITLE>
|
||||
<VARNAME>dvol2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>3</IO>
|
||||
<UNITS>L/min</UNITS>
|
||||
<DEF_VALUE>0</DEF_VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>12</SUB_ID>
|
||||
<TITLE>volume at port 2</TITLE>
|
||||
<VARNAME>vol2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>3</IO>
|
||||
<UNITS>cm**3</UNITS>
|
||||
<DEF_VALUE>0</DEF_VALUE>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>13</SUB_ID>
|
||||
<VARNAME>temp3</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>0</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>0</PRIMARY_VAR>
|
||||
<DUP_TYPE>0</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>14</SUB_ID>
|
||||
<VARNAME>press3</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>0</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>1</PRIMARY_VAR>
|
||||
<DUP_TYPE>0</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>15</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 3</TITLE>
|
||||
<VARNAME>dh3</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>16</SUB_ID>
|
||||
<TITLE>mass flow rate at port 3</TITLE>
|
||||
<VARNAME>dm3</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>17</SUB_ID>
|
||||
<TITLE>derivative of volume at port 3</TITLE>
|
||||
<VARNAME>dvol3</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>3</IO>
|
||||
<UNITS>L/min</UNITS>
|
||||
<DEF_VALUE>0</DEF_VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>18</SUB_ID>
|
||||
<TITLE>volume at port 3</TITLE>
|
||||
<VARNAME>vol3</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>3</IO>
|
||||
<UNITS>cm**3</UNITS>
|
||||
<DEF_VALUE>0</DEF_VALUE>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>19</SUB_ID>
|
||||
<VARNAME>temp4</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>0</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>0</PRIMARY_VAR>
|
||||
<DUP_TYPE>0</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>20</SUB_ID>
|
||||
<VARNAME>press4</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>0</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>1</PRIMARY_VAR>
|
||||
<DUP_TYPE>0</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>21</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 4</TITLE>
|
||||
<VARNAME>dh4</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>22</SUB_ID>
|
||||
<TITLE>mass flow rate at port 4</TITLE>
|
||||
<VARNAME>dm4</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>23</SUB_ID>
|
||||
<TITLE>derivative of volume at port 4</TITLE>
|
||||
<VARNAME>dvol4</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>3</IO>
|
||||
<UNITS>L/min</UNITS>
|
||||
<DEF_VALUE>0</DEF_VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>24</SUB_ID>
|
||||
<TITLE>volume at port 4</TITLE>
|
||||
<VARNAME>vol4</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>3</IO>
|
||||
<UNITS>cm**3</UNITS>
|
||||
<DEF_VALUE>0</DEF_VALUE>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
</EVARS_LIST>
|
||||
<SUBIDS_RESET>0</SUBIDS_RESET>
|
||||
</SUBMODEL>
|
||||
</SPE>
|
||||
@@ -0,0 +1,348 @@
|
||||
/* Submodel PNL0001 skeleton created by AME Submodel editing utility
|
||||
mer. juin 20 14:20:39 2018 */
|
||||
|
||||
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "ameutils.h"
|
||||
/* *******************************************************************************
|
||||
TITLE : PNL0001 (C-R)
|
||||
------------------------------------------------------------------------------
|
||||
DESCRIPTION :
|
||||
PNL0001 is a submodel of a pneumatic pipe with only compressibility
|
||||
and friction effects taking into account heat exchange.
|
||||
|
||||
The compressibility of the gas is taken into account by using a
|
||||
simple polytropic model or a more complex one taking into account
|
||||
heat exchange.
|
||||
|
||||
The polytropic model is a simplified form of the general internal
|
||||
energy model based on the first law of thermodynamics. The polytropic
|
||||
approach is obtained by representing the thermal exchange phenomena
|
||||
by a polytropic constant k. In that case, the temperature and
|
||||
pressure are no more independent variables.
|
||||
|
||||
The reduction of the complexity of the model implies a lack of
|
||||
accuracy. For general studies, you'd better use the heat exchange
|
||||
approach.
|
||||
|
||||
Pipe friction is taken into account using a friction factor based on
|
||||
the Reynolds number and the relative roughness.
|
||||
|
||||
The temperature and pressure in the volume are state variables.
|
||||
------------------------------------------------------------------------------
|
||||
USAGE :
|
||||
Use this submodel to simulate a pneumatic pipe with compressibility
|
||||
and friction effects, when the Mach number is low, ie gas velocity
|
||||
< 0.3 * speed of sound .
|
||||
|
||||
The submodels PNGD001 or PNGD002 should be included in your circuit to
|
||||
define the characteristics of the gas.
|
||||
------------------------------------------------------------------------------
|
||||
PARAMETER SETTINGS :
|
||||
------------------------------------------------------------------------------
|
||||
DATE OF CREATION / AUTHOR :
|
||||
2002 FS from PNL01 SN.
|
||||
------------------------------------------------------------------------------
|
||||
REVISIONS :
|
||||
------------------------------------------------------------------------------
|
||||
LIST OF FUNCTIONS USED :
|
||||
pn2getatp_() : get atmospheric pressure
|
||||
pn2ri_() : get perfect gas constant
|
||||
pn2vol1_() : polytropic model for chambers
|
||||
pn2vol_() : heat exchange model for chambers
|
||||
pn2pipefr_() : frictional coeffitient in pneumatic pipes
|
||||
------------------------------------------------------------------------------
|
||||
SOURCE :
|
||||
|
||||
This material contains trade secrets or otherwise confidential
|
||||
information owned by Siemens Industry Software Inc. or its
|
||||
affiliates (collectively, "Siemens"), or its licensors. Access to
|
||||
and use of this information is strictly limited as set forth in the
|
||||
Customer's applicable agreements with Siemens.
|
||||
|
||||
Unpublished work. Copyright 2023 Siemens
|
||||
|
||||
******************************************************************************* */
|
||||
|
||||
#define _SUBMODELNAME_ "PNL0001"
|
||||
|
||||
/* >>>>>>>>>>>>Insert Private Code Here. */
|
||||
#define TABFR 0 /* real store 0, 1 & 2 are used by pn2pipefr */
|
||||
#define PATM 3
|
||||
#define AREA 4
|
||||
#define VOL 5
|
||||
#define AREAEX 6
|
||||
|
||||
#define SPL_FR 0
|
||||
/* <<<<<<<<<<<<End of Private Code. */
|
||||
|
||||
|
||||
/* There are 6 real parameters:
|
||||
|
||||
diam diameter of pipe [mm -> m]
|
||||
le pipe length [m]
|
||||
rr relative roughness [null]
|
||||
k polytropic constant [null]
|
||||
kth thermal exchange coefficient [J/m**2/K/s -> W/m**2/K]
|
||||
extemp external temperature [K]
|
||||
*/
|
||||
|
||||
|
||||
/* There are 2 integer parameters:
|
||||
|
||||
gi gas type index
|
||||
mode model
|
||||
*/
|
||||
|
||||
void pnl0001in_(int *n, double rp[6], int ip[2], double c[7]
|
||||
, int ic[1], double *t2, double *p2)
|
||||
|
||||
{
|
||||
int loop, error;
|
||||
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
|
||||
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
|
||||
int gi, mode;
|
||||
double diam, le, rr, k, kth, extemp;
|
||||
|
||||
gi = ip[0];
|
||||
mode = ip[1];
|
||||
|
||||
diam = rp[0];
|
||||
le = rp[1];
|
||||
rr = rp[2];
|
||||
k = rp[3];
|
||||
kth = rp[4];
|
||||
extemp = rp[5];
|
||||
loop = 0;
|
||||
error = 0;
|
||||
|
||||
/*
|
||||
If necessary, check values of the following:
|
||||
|
||||
rp[0..5]
|
||||
*t2
|
||||
*p2
|
||||
*/
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Check Statements. */
|
||||
|
||||
pn2_valid_gas_(&gi, &error);
|
||||
|
||||
if (*p2 < -GPATMOS)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nInitial pressure at port 2 should be > 0 [barA].\n");
|
||||
}
|
||||
|
||||
if (*t2 <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nInitial temperature at port 2 should be > 0 [K].\n");
|
||||
}
|
||||
|
||||
if (diam <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nDiameter of pipe should be > 0 [mm].\n");
|
||||
}
|
||||
|
||||
if (le <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nPipe length should be > 0 [m].\n");
|
||||
}
|
||||
|
||||
if (rr < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nRelative roughness should be >= 0.\n");
|
||||
}
|
||||
|
||||
if (mode == 1)
|
||||
{
|
||||
if (k <= 0.)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nPolytropic constant should be > 0.\n");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (kth < 0.)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nThermal exchange coefficient should be >= 0 [J/m**2/K/s].\n");
|
||||
}
|
||||
|
||||
if (extemp <= 0.)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nExternal temperature should be > 0 [K].\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Check Statements. */
|
||||
|
||||
/* Integer parameter checking: */
|
||||
|
||||
if (gi < 1 || gi > 99)
|
||||
{
|
||||
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
|
||||
error = 2;
|
||||
}
|
||||
if (mode < 1 || mode > 2)
|
||||
{
|
||||
amefprintf(stderr, "\nmodel must be in range [1..2].\n");
|
||||
error = 2;
|
||||
}
|
||||
|
||||
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
|
||||
|
||||
/* Common -> SI units conversions. */
|
||||
|
||||
rp[0] *= 1.00000000000000e-003;
|
||||
diam = rp[0];
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
|
||||
|
||||
/* get atmospheric pressure */
|
||||
c[PATM] = pn2getatp_();
|
||||
|
||||
/* Compute the cross-sectional area of pipe. */
|
||||
c[AREA] = M_PI * (diam) * (diam) / 4.0;
|
||||
|
||||
/* Compute volume of pipe. */
|
||||
c[VOL] = c[AREA] * le;
|
||||
|
||||
/* Compute exchange area of pipe. */
|
||||
c[AREAEX] = M_PI * diam * le;
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
|
||||
}
|
||||
|
||||
/* There are 2 ports.
|
||||
|
||||
Port 1 has 4 variables:
|
||||
|
||||
1 dh1 enthalpy flow rate at port 1 [J/s -> W] basic variable output
|
||||
2 dm1 mass flow rate at port 1 [g/s -> kg/s] basic variable output
|
||||
3 t1 temperature at port 1 [K] basic variable input
|
||||
4 p1 pressure at port 1 [Pa] basic variable input
|
||||
|
||||
Port 2 has 4 variables:
|
||||
|
||||
1 t2 temperature at port 2 [K] explicit state (derivative `dt2')
|
||||
2 p2 pressure at port 2 [Pa] explicit state (derivative `dp2')
|
||||
3 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable input
|
||||
4 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable input
|
||||
*/
|
||||
|
||||
/* There are 5 internal variables.
|
||||
|
||||
1 mgas mass of gas in pipe [g -> kg] basic variable
|
||||
2 re Reynolds number [null] basic variable
|
||||
3 cm mass flow parameter (cm) [(kg*K/J)**(1/2)] basic variable
|
||||
4 v mean gas velocity [m/s] basic variable
|
||||
5 ff friction factor [null] basic variable
|
||||
*/
|
||||
|
||||
void pnl0001_(int *n, double *dh1, double *dm1, double *t1, double *p1
|
||||
, double *t2, double *dt2, double *p2, double *dp2, double *dh2
|
||||
, double *dm2, double *mgas, double *re, double *cm, double *v
|
||||
, double *ff, double rp[6], int ip[2], double c[7], int ic[1])
|
||||
|
||||
{
|
||||
int loop;
|
||||
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
|
||||
static double zero = 0.0;
|
||||
double sdh;
|
||||
double dh2i, dm2i;
|
||||
double dq;
|
||||
double pa1, pa2, dmgas;
|
||||
double r;
|
||||
int dummyreg;
|
||||
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
|
||||
int gi, mode;
|
||||
double diam, le, rr, k, kth, extemp;
|
||||
|
||||
gi = ip[0];
|
||||
mode = ip[1];
|
||||
|
||||
diam = rp[0];
|
||||
le = rp[1];
|
||||
rr = rp[2];
|
||||
k = rp[3];
|
||||
kth = rp[4];
|
||||
extemp = rp[5];
|
||||
loop = 0;
|
||||
|
||||
/* Common -> SI units conversions. */
|
||||
|
||||
*dm2 *= 1.00000000000000e-003;
|
||||
|
||||
/*
|
||||
Set all submodel outputs below:
|
||||
|
||||
*dh1 = ??;
|
||||
*dm1 = ??;
|
||||
*dt2 = ??;
|
||||
*dp2 = ??;
|
||||
*mgas = ??;
|
||||
*re = ??;
|
||||
*cm = ??;
|
||||
*v = ??;
|
||||
*ff = ??;
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
|
||||
|
||||
/* set absolute pressures */
|
||||
pa1 = *p1 + c[PATM];
|
||||
pa2 = *p2 + c[PATM];
|
||||
|
||||
/* Compute flows through the pipe */
|
||||
pn2pipefr_(&pa1, t1, &pa2, t2, &diam, &rr, &le, &c[AREA], re, v,ff,
|
||||
dh1, dm1, &dh2i, &dm2i, cm, &c[TABFR], &gi, &ic[SPL_FR], &dummyreg);
|
||||
|
||||
/* Compute mass variation */
|
||||
dmgas = (*dm2) + dm2i;
|
||||
|
||||
/* sum of enthalpy flows */
|
||||
sdh = (*dh2) + dh2i;
|
||||
|
||||
/*** temperature & pressure variation ***/
|
||||
|
||||
if (mode == 1) /* Polytropic model. */
|
||||
{
|
||||
r = pn2ri_(&gi);
|
||||
/* Compute initial mass of gas inside the pipe */
|
||||
*mgas = (pa2) * c[VOL] / ((*t2) * r);
|
||||
|
||||
pn2vol1_(dt2, dp2, t2, &pa2,
|
||||
&dmgas, mgas, &zero, &c[VOL], &k, &gi);
|
||||
}
|
||||
else /* Heat exchange. */
|
||||
{
|
||||
dq = kth * c[AREAEX] * (extemp - *t2);
|
||||
|
||||
pn2vol_(dt2, dp2, mgas, t2, &pa2,
|
||||
&dmgas, &sdh, &c[VOL], &zero, &dq, &gi);
|
||||
}
|
||||
|
||||
|
||||
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
|
||||
|
||||
/* SI -> Common units conversions. */
|
||||
|
||||
*dm1 /= 1.00000000000000e-003;
|
||||
*dm2 /= 1.00000000000000e-003;
|
||||
*mgas /= 1.00000000000000e-003;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<!DOCTYPE SPE>
|
||||
<SPE DOC_VERSION="2" AME_VERSION="16.0.0 - 68387-65635 2017">
|
||||
|
||||
<SUBMODEL>
|
||||
<SUB_TYPE>0</SUB_TYPE>
|
||||
<SUB_ID_MAX>22</SUB_ID_MAX>
|
||||
<DEFAULT_ICON>p2port</DEFAULT_ICON>
|
||||
<SUB_LABEL>Compressibility + friction submodel of pneumatic pipe (C-R)</SUB_LABEL>
|
||||
<SUB_UNIT>0</SUB_UNIT>
|
||||
<R_STORES_NUMBER>7</R_STORES_NUMBER>
|
||||
<I_STORES_NUMBER>1</I_STORES_NUMBER>
|
||||
<OUTPUT_TYPE>1</OUTPUT_TYPE>
|
||||
<RPARAMS_LIST>
|
||||
<RPARAM>
|
||||
<SUB_ID>14</SUB_ID>
|
||||
<TITLE>diameter of pipe</TITLE>
|
||||
<VARNAME>diam</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e+01</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e+01</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e-003</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+007</MAX_VALUE>
|
||||
<UNITS>mm</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>15</SUB_ID>
|
||||
<TITLE>pipe length</TITLE>
|
||||
<VARNAME>le</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e+00</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e+00</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e-006</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
|
||||
<UNITS>m</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>16</SUB_ID>
|
||||
<TITLE>relative roughness</TITLE>
|
||||
<VARNAME>rr</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e-05</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e-05</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e-001</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>17</SUB_ID>
|
||||
<TITLE>polytropic constant</TITLE>
|
||||
<VARNAME>k</VARNAME>
|
||||
<VISIBILITY>(mode == 1)</VISIBILITY>
|
||||
<DEF_VALUE>1.35000000000000e+00</DEF_VALUE>
|
||||
<VALUE>1.35000000000000e+00</VALUE>
|
||||
<MIN_VALUE>5.00000000000000e-001</MIN_VALUE>
|
||||
<MAX_VALUE>2.00000000000000e+000</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>18</SUB_ID>
|
||||
<TITLE>thermal exchange coefficient</TITLE>
|
||||
<VARNAME>kth</VARNAME>
|
||||
<VISIBILITY>(mode == 2)</VISIBILITY>
|
||||
<DEF_VALUE>0.00000000000000e+00</DEF_VALUE>
|
||||
<VALUE>0.00000000000000e+00</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
|
||||
<UNITS>J/m**2/K/s</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>19</SUB_ID>
|
||||
<TITLE>external temperature</TITLE>
|
||||
<VARNAME>extemp</VARNAME>
|
||||
<VISIBILITY>(mode == 2)</VISIBILITY>
|
||||
<DEF_VALUE>2.93150000000000e+02</DEF_VALUE>
|
||||
<VALUE>2.93150000000000e+02</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+003</MAX_VALUE>
|
||||
<UNITS>K</UNITS>
|
||||
</RPARAM>
|
||||
</RPARAMS_LIST>
|
||||
<IPARAMS_LIST>
|
||||
<IPARAM>
|
||||
<SUB_ID>20</SUB_ID>
|
||||
<TITLE>gas type index</TITLE>
|
||||
<VARNAME>gi</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1</DEF_VALUE>
|
||||
<VALUE>1</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>99</MAX_VALUE>
|
||||
</IPARAM>
|
||||
<IPARAM>
|
||||
<SUB_ID>21</SUB_ID>
|
||||
<TITLE>model</TITLE>
|
||||
<VARNAME>mode</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>2</DEF_VALUE>
|
||||
<VALUE>2</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>2</MAX_VALUE>
|
||||
<ENUM_LIST>
|
||||
<ENUM>
|
||||
<ENUM_STRING>polytropic</ENUM_STRING>
|
||||
</ENUM>
|
||||
<ENUM>
|
||||
<ENUM_STRING>with thermal exchange</ENUM_STRING>
|
||||
</ENUM>
|
||||
</ENUM_LIST>
|
||||
</IPARAM>
|
||||
</IPARAMS_LIST>
|
||||
<IVARS_LIST>
|
||||
<IVAR>
|
||||
<SUB_ID>22</SUB_ID>
|
||||
<TITLE>mass of gas in pipe</TITLE>
|
||||
<VARNAME>mgas</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>g</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>10</SUB_ID>
|
||||
<TITLE>Reynolds number</TITLE>
|
||||
<VARNAME>re</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>null</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>11</SUB_ID>
|
||||
<TITLE>mass flow parameter (cm)</TITLE>
|
||||
<VARNAME>cm</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>(kg*K/J)**(1/2)</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>12</SUB_ID>
|
||||
<TITLE>mean gas velocity</TITLE>
|
||||
<VARNAME>v</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>m/s</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>13</SUB_ID>
|
||||
<TITLE>friction factor</TITLE>
|
||||
<VARNAME>ff</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>null</UNITS>
|
||||
</IVAR>
|
||||
</IVARS_LIST>
|
||||
<EVARS_LIST>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>1</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 1</TITLE>
|
||||
<VARNAME>dh1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>2</SUB_ID>
|
||||
<TITLE>mass flow rate at port 1</TITLE>
|
||||
<VARNAME>dm1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>3</SUB_ID>
|
||||
<TITLE>temperature at port 1</TITLE>
|
||||
<VARNAME>t1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>K</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>4</SUB_ID>
|
||||
<TITLE>pressure at port 1</TITLE>
|
||||
<VARNAME>p1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>5</SUB_ID>
|
||||
<TITLE>temperature at port 2</TITLE>
|
||||
<VARNAME>t2</VARNAME>
|
||||
<VARNAME2>dt2</VARNAME2>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>1</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>K</UNITS>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
|
||||
<DEF_VALUE>2.93150000000000e+002</DEF_VALUE>
|
||||
<VALUE>2.93150000000000e+002</VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>6</SUB_ID>
|
||||
<TITLE>pressure at port 2</TITLE>
|
||||
<VARNAME>p2</VARNAME>
|
||||
<VARNAME2>dp2</VARNAME2>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>1</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
<MIN_VALUE>-1.01300000000000e+005</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+012</MAX_VALUE>
|
||||
<DEF_VALUE>0.00000000000000e+000</DEF_VALUE>
|
||||
<VALUE>0.00000000000000e+000</VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>7</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 2</TITLE>
|
||||
<VARNAME>dh2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>8</SUB_ID>
|
||||
<TITLE>mass flow rate at port 2</TITLE>
|
||||
<VARNAME>dm2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
</EVARS_LIST>
|
||||
<SUBIDS_RESET>0</SUBIDS_RESET>
|
||||
</SUBMODEL>
|
||||
</SPE>
|
||||
@@ -0,0 +1,368 @@
|
||||
/* Submodel PNL0002 skeleton created by AME Submodel editing utility
|
||||
mer. juin 20 14:35:13 2018 */
|
||||
|
||||
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "ameutils.h"
|
||||
/* *******************************************************************************
|
||||
TITLE : PNL0002 (R-C-R)
|
||||
------------------------------------------------------------------------------
|
||||
DESCRIPTION :
|
||||
PNL0002 is a submodel of a pneumatic pipe with only compressibility
|
||||
and friction effects taking into account heat exchange.
|
||||
|
||||
The compressibility of the gas is taken into account by using a
|
||||
simple polytropic model or a more complex one taking into account
|
||||
heat exchange.
|
||||
|
||||
The polytropic model is a simplified form of the general internal
|
||||
energy model based on the first law of thermodynamics. The polytropic
|
||||
approach is obtained by representing the thermal exchange phenomena
|
||||
by a polytropic constant k. In that case, the temperature and
|
||||
pressure are no more independent variables.
|
||||
|
||||
The reduction of the complexity of the model implies a lack of
|
||||
accuracy. For general studies, you'd better use the heat exchange
|
||||
approach.
|
||||
|
||||
Pipe friction is taken into account using a friction factor based on
|
||||
the Reynolds number and the relative roughness.
|
||||
|
||||
The temperature and pressure in the middle volume are state variables.
|
||||
------------------------------------------------------------------------------
|
||||
USAGE :
|
||||
Use this submodel to simulate a pneumatic pipe with compressibility
|
||||
and friction effects, when the Mach number is low, ie gas velocity
|
||||
< 0.3 * speed of sound .
|
||||
|
||||
PNL0002 is basically similar to PNL0001 and PNL0003 differing only in the
|
||||
input and output requirements.
|
||||
|
||||
The submodels PNGD01 or PNGD02 should be included in your circuit to
|
||||
define the characteristics of the gas.
|
||||
------------------------------------------------------------------------------
|
||||
PARAMETER SETTINGS :
|
||||
------------------------------------------------------------------------------
|
||||
DATE OF CREATION / AUTHOR :
|
||||
2002 FS from PNL02 SN.
|
||||
------------------------------------------------------------------------------
|
||||
REVISIONS :
|
||||
------------------------------------------------------------------------------
|
||||
LIST OF FUNCTIONS USED :
|
||||
pn2getatp_() : get atmospheric pressure
|
||||
pn2ri_() : get perfect gas constant
|
||||
pn2vol1_() : polytropic model for chambers
|
||||
pn2vol_() : heat exchange model for chambers
|
||||
pn2pipefr_() : frictional coeffitient in pneumatic pipes
|
||||
------------------------------------------------------------------------------
|
||||
SOURCE :
|
||||
|
||||
This material contains trade secrets or otherwise confidential
|
||||
information owned by Siemens Industry Software Inc. or its
|
||||
affiliates (collectively, "Siemens"), or its licensors. Access to
|
||||
and use of this information is strictly limited as set forth in the
|
||||
Customer's applicable agreements with Siemens.
|
||||
|
||||
Unpublished work. Copyright 2023 Siemens
|
||||
|
||||
******************************************************************************* */
|
||||
|
||||
#define _SUBMODELNAME_ "PNL0002"
|
||||
|
||||
/* >>>>>>>>>>>>Insert Private Code Here. */
|
||||
#define TABFR 0 /* real store 0, 1 & 2 are used by pn2pipefr */
|
||||
#define PATM 3
|
||||
#define AREA 4
|
||||
#define VOL 5
|
||||
#define HALFLE 6
|
||||
#define AREAEX 7
|
||||
|
||||
|
||||
#define SPL_FR 0
|
||||
/* <<<<<<<<<<<<End of Private Code. */
|
||||
|
||||
|
||||
/* There are 6 real parameters:
|
||||
|
||||
diam diameter of pipe [mm -> m]
|
||||
le pipe length [m]
|
||||
rr relative roughness [null]
|
||||
k polytropic constant [null]
|
||||
kth thermal exchange coefficient [J/m**2/K/s -> W/m**2/K]
|
||||
extemp external temperature [K]
|
||||
*/
|
||||
|
||||
|
||||
/* There are 2 integer parameters:
|
||||
|
||||
gi gas type index
|
||||
mode model
|
||||
*/
|
||||
|
||||
void pnl0002in_(int *n, double rp[6], int ip[2], double c[8]
|
||||
, int ic[1], double *tctr, double *pctr)
|
||||
|
||||
{
|
||||
int loop, error;
|
||||
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
|
||||
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
|
||||
int gi, mode;
|
||||
double diam, le, rr, k, kth, extemp;
|
||||
|
||||
gi = ip[0];
|
||||
mode = ip[1];
|
||||
|
||||
diam = rp[0];
|
||||
le = rp[1];
|
||||
rr = rp[2];
|
||||
k = rp[3];
|
||||
kth = rp[4];
|
||||
extemp = rp[5];
|
||||
loop = 0;
|
||||
error = 0;
|
||||
|
||||
/*
|
||||
If necessary, check values of the following:
|
||||
|
||||
rp[0..5]
|
||||
*tctr
|
||||
*pctr
|
||||
*/
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Check Statements. */
|
||||
|
||||
pn2_valid_gas_(&gi, &error);
|
||||
|
||||
if (*pctr < -GPATMOS)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nInitial pressure at center of pipe should be > 0 [barA].\n");
|
||||
}
|
||||
|
||||
if (*tctr <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nInitial temperature at center of pipe should be > 0 [K].\n");
|
||||
}
|
||||
|
||||
if (diam <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nDiameter of pipe should be > 0 [mm].\n");
|
||||
}
|
||||
|
||||
if (le <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nPipe length should be > 0 [m].\n");
|
||||
}
|
||||
|
||||
if (rr < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nRelative roughness should be >= 0.\n");
|
||||
}
|
||||
|
||||
if (mode == 1)
|
||||
{
|
||||
if (k <= 0.)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nPolytropic constant should be > 0.\n");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (kth < 0.)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nThermal exchange coefficient should be >= 0 [J/m**2/K/s].\n");
|
||||
}
|
||||
|
||||
if (extemp <= 0.)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nExternal temperature should be > 0 [K].\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Check Statements. */
|
||||
|
||||
/* Integer parameter checking: */
|
||||
|
||||
if (gi < 1 || gi > 99)
|
||||
{
|
||||
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
|
||||
error = 2;
|
||||
}
|
||||
if (mode < 1 || mode > 2)
|
||||
{
|
||||
amefprintf(stderr, "\nmodel must be in range [1..2].\n");
|
||||
error = 2;
|
||||
}
|
||||
|
||||
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
|
||||
|
||||
/* Common -> SI units conversions. */
|
||||
|
||||
rp[0] *= 1.00000000000000e-003;
|
||||
diam = rp[0];
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
|
||||
|
||||
/* get atmospheric pressure */
|
||||
c[PATM] = pn2getatp_();
|
||||
|
||||
/* Compute the cross-sectional area of pipe. */
|
||||
c[AREA] = M_PI * (diam) * (diam) / 4.0;
|
||||
|
||||
/* Compute volume of pipe. */
|
||||
c[VOL] = c[AREA] * le;
|
||||
|
||||
/* Divide the restriction in 2 identical restrictions */
|
||||
c[HALFLE] = 0.5 * le;
|
||||
|
||||
/* Compute exchange area of pipe. */
|
||||
c[AREAEX] = M_PI * diam * le;
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
|
||||
}
|
||||
|
||||
/* There are 2 ports.
|
||||
|
||||
Port 1 has 4 variables:
|
||||
|
||||
1 dh1 enthalpy flow rate at port 1 [J/s -> W] basic variable output
|
||||
2 dm1 mass flow rate at port 1 [g/s -> kg/s] basic variable output
|
||||
3 t1 temperature at port 1 [K] basic variable input
|
||||
4 p1 pressure at port 1 [Pa] basic variable input
|
||||
|
||||
Port 2 has 4 variables:
|
||||
|
||||
1 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable output
|
||||
2 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable output
|
||||
3 t2 temperature at port 2 [K] basic variable input
|
||||
4 p2 pressure at port 2 [Pa] basic variable input
|
||||
*/
|
||||
|
||||
/* There are 7 internal variables.
|
||||
|
||||
1 tctr temperature at center of pipe [K] explicit state (derivative `dtctr')
|
||||
2 pctr pressure at center of pipe [Pa] explicit state (derivative `dpctr')
|
||||
3 mgas mass of gas in pipe [g -> kg] basic variable
|
||||
4 re mean Reynolds number [null] basic variable
|
||||
5 cm mean mass flow parameter [(kg*K/J)**(1/2)] basic variable
|
||||
6 v mean gas velocity [m/s] basic variable
|
||||
7 ff mean friction factor [null] basic variable
|
||||
*/
|
||||
|
||||
void pnl0002_(int *n, double *dh1, double *dm1, double *t1, double *p1
|
||||
, double *dh2, double *dm2, double *t2, double *p2, double *tctr
|
||||
, double *dtctr, double *pctr, double *dpctr, double *mgas
|
||||
, double *re, double *cm, double *v, double *ff, double rp[6]
|
||||
, int ip[2], double c[8], int ic[1])
|
||||
|
||||
{
|
||||
int loop;
|
||||
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
|
||||
static double zero = 0.0;
|
||||
double sdh;
|
||||
double dh1i, dm1i;
|
||||
double dh2i, dm2i;
|
||||
double ff1, ff2, re1, re2, cm1, cm2;
|
||||
double dq;
|
||||
double pa1, pa2, pactr;
|
||||
double v1, v2;
|
||||
double dmgas;
|
||||
double r;
|
||||
int dummyreg;
|
||||
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
|
||||
int gi, mode;
|
||||
double diam, le, rr, k, kth, extemp;
|
||||
|
||||
gi = ip[0];
|
||||
mode = ip[1];
|
||||
|
||||
diam = rp[0];
|
||||
le = rp[1];
|
||||
rr = rp[2];
|
||||
k = rp[3];
|
||||
kth = rp[4];
|
||||
extemp = rp[5];
|
||||
loop = 0;
|
||||
|
||||
/*
|
||||
Set all submodel outputs below:
|
||||
|
||||
*dh1 = ??;
|
||||
*dm1 = ??;
|
||||
*dh2 = ??;
|
||||
*dm2 = ??;
|
||||
*dtctr = ??;
|
||||
*dpctr = ??;
|
||||
*mgas = ??;
|
||||
*re = ??;
|
||||
*cm = ??;
|
||||
*v = ??;
|
||||
*ff = ??;
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
|
||||
|
||||
/* set absolute pressure */
|
||||
pa1 = *p1 + c[PATM];
|
||||
pa2 = *p2 + c[PATM];
|
||||
pactr = *pctr + c[PATM];
|
||||
|
||||
/* Compute flows through the pipe */
|
||||
pn2pipefr_(&pa1, t1, &pactr, tctr, &diam, &rr, &c[HALFLE], &c[AREA], &re1, &v1, &ff1,
|
||||
dh1, dm1, &dh1i, &dm1i, &cm1, &c[TABFR], &gi, &ic[SPL_FR], &dummyreg);
|
||||
|
||||
pn2pipefr_(&pactr, tctr, &pa2, t2, &diam, &rr, &c[HALFLE], &c[AREA], &re2, &v2, &ff2,
|
||||
&dh2i, &dm2i, dh2, dm2, &cm2, &c[TABFR], &gi, &ic[SPL_FR], &dummyreg);
|
||||
|
||||
/* Mean variables */
|
||||
*ff = 0.5 * (ff1 + ff2);
|
||||
*re = 0.5 * (re1 + re2);
|
||||
*cm = 0.5 * (cm1 + cm2);
|
||||
*v = 0.5 * (fabs(v1) + fabs(v2));
|
||||
|
||||
/* Compute mass variation */
|
||||
dmgas = dm1i + dm2i;
|
||||
|
||||
/* sum of enthalpy flows */
|
||||
sdh = dh1i + dh2i;
|
||||
|
||||
/*** temperature & pressure variation ***/
|
||||
if (mode == 1) /* Polytropic model. */
|
||||
{
|
||||
r = pn2ri_(&gi);
|
||||
*mgas = (pactr) * c[VOL] / ((*tctr) * r);
|
||||
|
||||
pn2vol1_(dtctr, dpctr, tctr, &pactr,
|
||||
&dmgas, mgas, &zero, &c[VOL], &k, &gi);
|
||||
}
|
||||
else /* Heat exchange. */
|
||||
{
|
||||
dq = kth * c[AREAEX] * (extemp-*tctr);
|
||||
|
||||
pn2vol_(dtctr, dpctr, mgas, tctr, &pactr,
|
||||
&dmgas, &sdh, &c[VOL], &zero, &dq, &gi);
|
||||
}
|
||||
|
||||
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
|
||||
|
||||
/* SI -> Common units conversions. */
|
||||
|
||||
*dm1 /= 1.00000000000000e-003;
|
||||
*dm2 /= 1.00000000000000e-003;
|
||||
*mgas /= 1.00000000000000e-003;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<!DOCTYPE SPE>
|
||||
<SPE DOC_VERSION="2" AME_VERSION="16.0.0 - 68387-65635 2017">
|
||||
|
||||
<SUBMODEL>
|
||||
<SUB_TYPE>0</SUB_TYPE>
|
||||
<SUB_ID_MAX>24</SUB_ID_MAX>
|
||||
<DEFAULT_ICON>p2port</DEFAULT_ICON>
|
||||
<SUB_LABEL>Compressibility + friction submodel of pneumatic pipe (R-C-R)</SUB_LABEL>
|
||||
<SUB_UNIT>0</SUB_UNIT>
|
||||
<R_STORES_NUMBER>8</R_STORES_NUMBER>
|
||||
<I_STORES_NUMBER>1</I_STORES_NUMBER>
|
||||
<OUTPUT_TYPE>1</OUTPUT_TYPE>
|
||||
<RPARAMS_LIST>
|
||||
<RPARAM>
|
||||
<SUB_ID>16</SUB_ID>
|
||||
<TITLE>diameter of pipe</TITLE>
|
||||
<VARNAME>diam</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e+01</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e+01</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e-003</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+007</MAX_VALUE>
|
||||
<UNITS>mm</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>17</SUB_ID>
|
||||
<TITLE>pipe length</TITLE>
|
||||
<VARNAME>le</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e+00</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e+00</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e-006</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
|
||||
<UNITS>m</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>18</SUB_ID>
|
||||
<TITLE>relative roughness</TITLE>
|
||||
<VARNAME>rr</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e-05</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e-05</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e-001</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>19</SUB_ID>
|
||||
<TITLE>polytropic constant</TITLE>
|
||||
<VARNAME>k</VARNAME>
|
||||
<VISIBILITY>(mode == 1)</VISIBILITY>
|
||||
<DEF_VALUE>1.35000000000000e+00</DEF_VALUE>
|
||||
<VALUE>1.35000000000000e+00</VALUE>
|
||||
<MIN_VALUE>5.00000000000000e-001</MIN_VALUE>
|
||||
<MAX_VALUE>2.00000000000000e+000</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>20</SUB_ID>
|
||||
<TITLE>thermal exchange coefficient</TITLE>
|
||||
<VARNAME>kth</VARNAME>
|
||||
<VISIBILITY>(mode == 2)</VISIBILITY>
|
||||
<DEF_VALUE>0.00000000000000e+00</DEF_VALUE>
|
||||
<VALUE>0.00000000000000e+00</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
|
||||
<UNITS>J/m**2/K/s</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>21</SUB_ID>
|
||||
<TITLE>external temperature</TITLE>
|
||||
<VARNAME>extemp</VARNAME>
|
||||
<VISIBILITY>(mode == 2)</VISIBILITY>
|
||||
<DEF_VALUE>2.93150000000000e+02</DEF_VALUE>
|
||||
<VALUE>2.93150000000000e+02</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+003</MAX_VALUE>
|
||||
<UNITS>K</UNITS>
|
||||
</RPARAM>
|
||||
</RPARAMS_LIST>
|
||||
<IPARAMS_LIST>
|
||||
<IPARAM>
|
||||
<SUB_ID>22</SUB_ID>
|
||||
<TITLE>gas type index</TITLE>
|
||||
<VARNAME>gi</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1</DEF_VALUE>
|
||||
<VALUE>1</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>99</MAX_VALUE>
|
||||
</IPARAM>
|
||||
<IPARAM>
|
||||
<SUB_ID>23</SUB_ID>
|
||||
<TITLE>model</TITLE>
|
||||
<VARNAME>mode</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>2</DEF_VALUE>
|
||||
<VALUE>2</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>2</MAX_VALUE>
|
||||
<ENUM_LIST>
|
||||
<ENUM>
|
||||
<ENUM_STRING>polytropic</ENUM_STRING>
|
||||
</ENUM>
|
||||
<ENUM>
|
||||
<ENUM_STRING>with thermal exchange</ENUM_STRING>
|
||||
</ENUM>
|
||||
</ENUM_LIST>
|
||||
</IPARAM>
|
||||
</IPARAMS_LIST>
|
||||
<IVARS_LIST>
|
||||
<IVAR>
|
||||
<SUB_ID>9</SUB_ID>
|
||||
<TITLE>temperature at center of pipe</TITLE>
|
||||
<VARNAME>tctr</VARNAME>
|
||||
<VARNAME2>dtctr</VARNAME2>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>1</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>K</UNITS>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
|
||||
<DEF_VALUE>2.931500e+02</DEF_VALUE>
|
||||
<VALUE>2.931500e+02</VALUE>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>10</SUB_ID>
|
||||
<TITLE>pressure at center of pipe</TITLE>
|
||||
<VARNAME>pctr</VARNAME>
|
||||
<VARNAME2>dpctr</VARNAME2>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>1</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>Pa</UNITS>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
|
||||
<DEF_VALUE>1.013000e+00</DEF_VALUE>
|
||||
<VALUE>1.013000e+00</VALUE>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>24</SUB_ID>
|
||||
<TITLE>mass of gas in pipe</TITLE>
|
||||
<VARNAME>mgas</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>g</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>12</SUB_ID>
|
||||
<TITLE>mean Reynolds number</TITLE>
|
||||
<VARNAME>re</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>null</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>13</SUB_ID>
|
||||
<TITLE>mean mass flow parameter</TITLE>
|
||||
<VARNAME>cm</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>(kg*K/J)**(1/2)</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>14</SUB_ID>
|
||||
<TITLE>mean gas velocity</TITLE>
|
||||
<VARNAME>v</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>m/s</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>15</SUB_ID>
|
||||
<TITLE>mean friction factor</TITLE>
|
||||
<VARNAME>ff</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>null</UNITS>
|
||||
</IVAR>
|
||||
</IVARS_LIST>
|
||||
<EVARS_LIST>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>1</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 1</TITLE>
|
||||
<VARNAME>dh1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>2</SUB_ID>
|
||||
<TITLE>mass flow rate at port 1</TITLE>
|
||||
<VARNAME>dm1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>3</SUB_ID>
|
||||
<TITLE>temperature at port 1</TITLE>
|
||||
<VARNAME>t1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>K</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>4</SUB_ID>
|
||||
<TITLE>pressure at port 1</TITLE>
|
||||
<VARNAME>p1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>5</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 2</TITLE>
|
||||
<VARNAME>dh2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>6</SUB_ID>
|
||||
<TITLE>mass flow rate at port 2</TITLE>
|
||||
<VARNAME>dm2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>7</SUB_ID>
|
||||
<TITLE>temperature at port 2</TITLE>
|
||||
<VARNAME>t2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>K</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>8</SUB_ID>
|
||||
<TITLE>pressure at port 2</TITLE>
|
||||
<VARNAME>p2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
</EVARS_LIST>
|
||||
<SUBIDS_RESET>0</SUBIDS_RESET>
|
||||
</SUBMODEL>
|
||||
</SPE>
|
||||
@@ -0,0 +1,399 @@
|
||||
/* Submodel PNL0003 skeleton created by AME Submodel editing utility
|
||||
mer. juin 20 14:17:28 2018 */
|
||||
|
||||
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "ameutils.h"
|
||||
/* *******************************************************************************
|
||||
TITLE : PNL0003 (C-R-C)
|
||||
------------------------------------------------------------------------------
|
||||
DESCRIPTION :
|
||||
PNL0003 is a submodel of a pneumatic pipe with only compressibility
|
||||
and friction effects taking into account heat exchange.
|
||||
|
||||
The compressibility of the gas is taken into account by using a
|
||||
simple polytropic model or a more complex one taking into account
|
||||
heat exchange.
|
||||
|
||||
The polytropic model is a simplified form of the general internal
|
||||
energy model based on the first law of thermodynamics. The polytropic
|
||||
approach is obtained by representing the thermal exchange phenomena
|
||||
by a polytropic constant k. In that case, the temperature and
|
||||
pressure are no more independent variables.
|
||||
|
||||
The reduction of the complexity of the model implies a lack of
|
||||
accuracy. For general studies, you'd better use the heat exchange
|
||||
approach.
|
||||
|
||||
Pipe friction is taken into account using a friction factor based on
|
||||
the Reynolds number and the relative roughness.
|
||||
|
||||
The temperature and pressure in each two volumes are state variables.
|
||||
------------------------------------------------------------------------------
|
||||
USAGE :
|
||||
Use this submodel to simulate a pneumatic pipe with compressibility
|
||||
and friction effects, when the Mach number is low, ie gas velocity
|
||||
< 0.3 * speed of sound .
|
||||
|
||||
PNL0003 is basically similar to PNL0001 and PNL0002 differing only in the
|
||||
input and output requirements.
|
||||
|
||||
The submodels PNGD01 or PNGD02 should be included in your circuit to
|
||||
define the characteristics of the gas.
|
||||
------------------------------------------------------------------------------
|
||||
PARAMETER SETTINGS :
|
||||
------------------------------------------------------------------------------
|
||||
DATE OF CREATION / AUTHOR :
|
||||
2002 FS from PNL03 SN.
|
||||
------------------------------------------------------------------------------
|
||||
REVISIONS :
|
||||
------------------------------------------------------------------------------
|
||||
LIST OF FUNCTIONS USED :
|
||||
pn2getatp_() : get atmospheric pressure
|
||||
pn2ri_() : get perfect gas constant
|
||||
pn2vol1_() : polytropic model for chambers
|
||||
pn2vol_() : heat exchange model for chambers
|
||||
pn2pipefr_() : frictional coefficient in pneumatic pipes
|
||||
------------------------------------------------------------------------------
|
||||
SOURCE :
|
||||
|
||||
This material contains trade secrets or otherwise confidential
|
||||
information owned by Siemens Industry Software Inc. or its
|
||||
affiliates (collectively, "Siemens"), or its licensors. Access to
|
||||
and use of this information is strictly limited as set forth in the
|
||||
Customer's applicable agreements with Siemens.
|
||||
|
||||
Unpublished work. Copyright 2023 Siemens
|
||||
|
||||
******************************************************************************* */
|
||||
|
||||
#define _SUBMODELNAME_ "PNL0003"
|
||||
|
||||
/* >>>>>>>>>>>>Insert Private Code Here. */
|
||||
#define TABFR 0 /* real store 0, 1 & 2 are used by pn2pipefr */
|
||||
#define PATM 3
|
||||
#define AREA 4
|
||||
#define HALFVOL 5
|
||||
#define HALFAREAEX 6
|
||||
|
||||
|
||||
#define SPL_FR 0
|
||||
/* <<<<<<<<<<<<End of Private Code. */
|
||||
|
||||
|
||||
/* There are 6 real parameters:
|
||||
|
||||
diam diameter of pipe [mm -> m]
|
||||
le pipe length [m]
|
||||
rr relative roughness [null]
|
||||
k polytropic constant [null]
|
||||
kth thermal exchange coefficient [J/m**2/K/s -> W/m**2/K]
|
||||
extemp external temperature [K]
|
||||
*/
|
||||
|
||||
|
||||
/* There are 2 integer parameters:
|
||||
|
||||
gi gas type index
|
||||
mode model
|
||||
*/
|
||||
|
||||
void pnl0003in_(int *n, double rp[6], int ip[2], double c[7]
|
||||
, int ic[1], double *t1, double *p1, double *t2, double *p2)
|
||||
|
||||
{
|
||||
int loop, error;
|
||||
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
|
||||
double vol, areaex;
|
||||
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
|
||||
int gi, mode;
|
||||
double diam, le, rr, k, kth, extemp;
|
||||
|
||||
gi = ip[0];
|
||||
mode = ip[1];
|
||||
|
||||
diam = rp[0];
|
||||
le = rp[1];
|
||||
rr = rp[2];
|
||||
k = rp[3];
|
||||
kth = rp[4];
|
||||
extemp = rp[5];
|
||||
loop = 0;
|
||||
error = 0;
|
||||
|
||||
/*
|
||||
If necessary, check values of the following:
|
||||
|
||||
rp[0..5]
|
||||
*t1
|
||||
*p1
|
||||
*t2
|
||||
*p2
|
||||
*/
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Check Statements. */
|
||||
|
||||
pn2_valid_gas_(&gi, &error);
|
||||
|
||||
if (*p1 < -GPATMOS)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nInitial pressure at port 1 should be > 0 [barA].\n");
|
||||
}
|
||||
|
||||
if (*t1 <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nInitial temperature at port 1 should be > 0 [K].\n");
|
||||
}
|
||||
|
||||
if (*p2 < -GPATMOS)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nInitial pressure at port 2 should be > 0 [barA].\n");
|
||||
}
|
||||
|
||||
if (*t2 <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nInitial temperature at port 2 should be > 0 [K].\n");
|
||||
}
|
||||
|
||||
if (diam <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nDiameter of pipe should be > 0 [mm].\n");
|
||||
}
|
||||
|
||||
if (le <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nPipe length should be > 0 [m].\n");
|
||||
}
|
||||
|
||||
if (rr < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nRelative roughness should be >= 0.\n");
|
||||
}
|
||||
|
||||
if (mode == 1)
|
||||
{
|
||||
if (k <= 0.)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nPolytropic constant should be > 0.\n");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (kth < 0.)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nThermal exchange coefficient should be >= 0 [J/m**2/K/s].\n");
|
||||
}
|
||||
|
||||
if (extemp <= 0.)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nExternal temperature should be > 0 [K].\n");
|
||||
}
|
||||
}
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Check Statements. */
|
||||
|
||||
/* Integer parameter checking: */
|
||||
|
||||
if (gi < 1 || gi > 99)
|
||||
{
|
||||
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
|
||||
error = 2;
|
||||
}
|
||||
if (mode < 1 || mode > 2)
|
||||
{
|
||||
amefprintf(stderr, "\nmodel must be in range [1..2].\n");
|
||||
error = 2;
|
||||
}
|
||||
|
||||
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
|
||||
|
||||
/* Common -> SI units conversions. */
|
||||
|
||||
rp[0] *= 1.00000000000000e-003;
|
||||
diam = rp[0];
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
|
||||
/* set atmospheric pressure */
|
||||
c[PATM] = pn2getatp_();
|
||||
|
||||
/* Compute the cross-sectional area of pipe. */
|
||||
c[AREA] = M_PI * (diam) * (diam) / 4.0;
|
||||
|
||||
/* Compute volume of pipe. */
|
||||
vol = c[AREA] * le;
|
||||
|
||||
/* Divide the volume in 2 identical volumes */
|
||||
c[HALFVOL] = 0.5 * vol;
|
||||
|
||||
/* Compute exchange area of pipe. */
|
||||
areaex = M_PI * diam * le;
|
||||
|
||||
/* Divide the exchange area of pipe in 2 identical areas */
|
||||
c[HALFAREAEX] = 0.5 * areaex;
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
|
||||
}
|
||||
|
||||
/* There are 2 ports.
|
||||
|
||||
Port 1 has 4 variables:
|
||||
|
||||
1 t1 temperature at port 1 [K] explicit state (derivative `dt1')
|
||||
2 p1 pressure at port 1 [Pa] explicit state (derivative `dp1')
|
||||
3 dh1 enthalpy flow rate at port 1 [J/s -> W] basic variable input
|
||||
4 dm1 mass flow rate at port 1 [g/s -> kg/s] basic variable input
|
||||
|
||||
Port 2 has 4 variables:
|
||||
|
||||
1 t2 temperature at port 2 [K] explicit state (derivative `dt2')
|
||||
2 p2 pressure at port 2 [Pa] explicit state (derivative `dp2')
|
||||
3 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable input
|
||||
4 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable input
|
||||
*/
|
||||
|
||||
/* There are 7 internal variables.
|
||||
|
||||
1 dhctr enthalpy flow at center of pipe [J/s -> W] basic variable
|
||||
2 dmctr mass flow at center of pipe [g/s -> kg/s] basic variable
|
||||
3 mgas mass of gas in pipe [g -> kg] basic variable
|
||||
4 re Reynolds number [null] basic variable
|
||||
5 cm mass flow parameter (cm) [(kg*K/J)**(1/2)] basic variable
|
||||
6 v mean gas velocity [m/s] basic variable
|
||||
7 ff friction factor [null] basic variable
|
||||
*/
|
||||
|
||||
void pnl0003_(int *n, double *t1, double *dt1, double *p1, double *dp1
|
||||
, double *dh1, double *dm1, double *t2, double *dt2, double *p2
|
||||
, double *dp2, double *dh2, double *dm2, double *dhctr
|
||||
, double *dmctr, double *mgas, double *re, double *cm, double *v
|
||||
, double *ff, double rp[6], int ip[2], double c[7], int ic[1])
|
||||
|
||||
{
|
||||
int loop;
|
||||
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
|
||||
static double zero = 0.0;
|
||||
double dh1i, dm1i;
|
||||
double dh2i, dm2i;
|
||||
double sdh1, sdm1;
|
||||
double sdh2, sdm2;
|
||||
double m1, m2;
|
||||
double dq1, dq2;
|
||||
double pa1, pa2;
|
||||
double dmgas;
|
||||
double r;
|
||||
int dummyreg;
|
||||
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
|
||||
int gi, mode;
|
||||
double diam, le, rr, k, kth, extemp;
|
||||
|
||||
gi = ip[0];
|
||||
mode = ip[1];
|
||||
|
||||
diam = rp[0];
|
||||
le = rp[1];
|
||||
rr = rp[2];
|
||||
k = rp[3];
|
||||
kth = rp[4];
|
||||
extemp = rp[5];
|
||||
loop = 0;
|
||||
|
||||
/* Common -> SI units conversions. */
|
||||
|
||||
*dm1 *= 1.00000000000000e-003;
|
||||
*dm2 *= 1.00000000000000e-003;
|
||||
|
||||
/*
|
||||
Set all submodel outputs below:
|
||||
|
||||
*dt1 = ??;
|
||||
*dp1 = ??;
|
||||
*dt2 = ??;
|
||||
*dp2 = ??;
|
||||
*dhctr = ??;
|
||||
*dmctr = ??;
|
||||
*mgas = ??;
|
||||
*re = ??;
|
||||
*cm = ??;
|
||||
*v = ??;
|
||||
*ff = ??;
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
|
||||
|
||||
/* set absolute pressures */
|
||||
pa1 = *p1 + c[PATM];
|
||||
pa2 = *p2 + c[PATM];
|
||||
|
||||
/* Compute flow through the pipe */
|
||||
pn2pipefr_(&pa1, t1, &pa2, t2, &diam, &rr, &le, &c[AREA], re, v, ff,
|
||||
&dh1i, &dm1i, &dh2i, &dm2i, cm, &c[TABFR], &gi, &ic[SPL_FR], &dummyreg);
|
||||
|
||||
/* Enthalpy flow and mass flow at center of pipe */
|
||||
*dhctr = dh1i; /* = -dh2i */
|
||||
*dmctr = dm1i; /* = -dm2i */
|
||||
|
||||
/* Compute the sum of the flows inside each volume */
|
||||
sdm1 = *dm1 + dm1i;
|
||||
sdh1 = *dh1 + dh1i;
|
||||
sdm2 = *dm2 + dm2i;
|
||||
sdh2 = *dh2 + dh2i;
|
||||
|
||||
dmgas = sdm1 + sdm2;
|
||||
|
||||
/*** temperature & pressure variation ***/
|
||||
if (mode == 1) /* Polytropic model. */
|
||||
{
|
||||
r = pn2ri_(&gi);
|
||||
|
||||
/* Current mass in each volume */
|
||||
m1 = pa1 * c[HALFVOL] / (*t1 * r);
|
||||
m2 = pa2 * c[HALFVOL] / (*t2 * r);
|
||||
|
||||
pn2vol1_(dt1, dp1, t1, &pa1,
|
||||
&sdm1, &m1, &zero, &c[HALFVOL], &k,&gi);
|
||||
|
||||
pn2vol1_(dt2, dp2, t2, &pa2,
|
||||
&sdm2, &m2, &zero, &c[HALFVOL], &k,&gi);
|
||||
}
|
||||
else /* Heat exchange. */
|
||||
{
|
||||
dq1 = kth * c[HALFAREAEX] * (extemp - *t1);
|
||||
|
||||
pn2vol_(dt1, dp1, &m1, t1, &pa1,
|
||||
&sdm1, &sdh1, &c[HALFVOL], &zero, &dq1, &gi);
|
||||
|
||||
dq2 = kth * c[HALFAREAEX] * (extemp - *t2);
|
||||
|
||||
pn2vol_(dt2, dp2, &m2, t2, &pa2,
|
||||
&sdm2, &sdh2, &c[HALFVOL], &zero, &dq2, &gi);
|
||||
}
|
||||
|
||||
*mgas = m1 + m2;
|
||||
|
||||
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
|
||||
|
||||
/* SI -> Common units conversions. */
|
||||
|
||||
*dm1 /= 1.00000000000000e-003;
|
||||
*dm2 /= 1.00000000000000e-003;
|
||||
*dmctr /= 1.00000000000000e-003;
|
||||
*mgas /= 1.00000000000000e-003;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<!DOCTYPE SPE>
|
||||
<SPE DOC_VERSION="2" AME_VERSION="16.0.0 - 68387-65635 2017">
|
||||
|
||||
<SUBMODEL>
|
||||
<SUB_TYPE>0</SUB_TYPE>
|
||||
<SUB_ID_MAX>24</SUB_ID_MAX>
|
||||
<DEFAULT_ICON>p2port</DEFAULT_ICON>
|
||||
<SUB_LABEL>Compressibility + friction submodel of pneumatic pipe (C-R-C)</SUB_LABEL>
|
||||
<SUB_UNIT>0</SUB_UNIT>
|
||||
<R_STORES_NUMBER>7</R_STORES_NUMBER>
|
||||
<I_STORES_NUMBER>1</I_STORES_NUMBER>
|
||||
<OUTPUT_TYPE>1</OUTPUT_TYPE>
|
||||
<RPARAMS_LIST>
|
||||
<RPARAM>
|
||||
<SUB_ID>16</SUB_ID>
|
||||
<TITLE>diameter of pipe</TITLE>
|
||||
<VARNAME>diam</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e+01</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e+01</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e-003</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+007</MAX_VALUE>
|
||||
<UNITS>mm</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>17</SUB_ID>
|
||||
<TITLE>pipe length</TITLE>
|
||||
<VARNAME>le</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e+00</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e+00</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e-006</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
|
||||
<UNITS>m</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>18</SUB_ID>
|
||||
<TITLE>relative roughness</TITLE>
|
||||
<VARNAME>rr</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e-05</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e-05</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e-001</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>19</SUB_ID>
|
||||
<TITLE>polytropic constant</TITLE>
|
||||
<VARNAME>k</VARNAME>
|
||||
<VISIBILITY>(mode == 1)</VISIBILITY>
|
||||
<DEF_VALUE>1.35000000000000e+00</DEF_VALUE>
|
||||
<VALUE>1.35000000000000e+00</VALUE>
|
||||
<MIN_VALUE>5.00000000000000e-001</MIN_VALUE>
|
||||
<MAX_VALUE>2.00000000000000e+000</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>20</SUB_ID>
|
||||
<TITLE>thermal exchange coefficient</TITLE>
|
||||
<VARNAME>kth</VARNAME>
|
||||
<VISIBILITY>(mode == 2)</VISIBILITY>
|
||||
<DEF_VALUE>0.00000000000000e+00</DEF_VALUE>
|
||||
<VALUE>0.00000000000000e+00</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
|
||||
<UNITS>J/m**2/K/s</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>21</SUB_ID>
|
||||
<TITLE>external temperature</TITLE>
|
||||
<VARNAME>extemp</VARNAME>
|
||||
<VISIBILITY>(mode == 2)</VISIBILITY>
|
||||
<DEF_VALUE>2.93150000000000e+02</DEF_VALUE>
|
||||
<VALUE>2.93150000000000e+02</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+003</MAX_VALUE>
|
||||
<UNITS>K</UNITS>
|
||||
</RPARAM>
|
||||
</RPARAMS_LIST>
|
||||
<IPARAMS_LIST>
|
||||
<IPARAM>
|
||||
<SUB_ID>22</SUB_ID>
|
||||
<TITLE>gas type index</TITLE>
|
||||
<VARNAME>gi</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1</DEF_VALUE>
|
||||
<VALUE>1</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>99</MAX_VALUE>
|
||||
</IPARAM>
|
||||
<IPARAM>
|
||||
<SUB_ID>23</SUB_ID>
|
||||
<TITLE>model</TITLE>
|
||||
<VARNAME>mode</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>2</DEF_VALUE>
|
||||
<VALUE>2</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>2</MAX_VALUE>
|
||||
<ENUM_LIST>
|
||||
<ENUM>
|
||||
<ENUM_STRING>polytropic</ENUM_STRING>
|
||||
</ENUM>
|
||||
<ENUM>
|
||||
<ENUM_STRING>with thermal exchange</ENUM_STRING>
|
||||
</ENUM>
|
||||
</ENUM_LIST>
|
||||
</IPARAM>
|
||||
</IPARAMS_LIST>
|
||||
<IVARS_LIST>
|
||||
<IVAR>
|
||||
<SUB_ID>9</SUB_ID>
|
||||
<TITLE>enthalpy flow at center of pipe</TITLE>
|
||||
<VARNAME>dhctr</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>J/s</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>10</SUB_ID>
|
||||
<TITLE>mass flow at center of pipe</TITLE>
|
||||
<VARNAME>dmctr</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>g/s</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>24</SUB_ID>
|
||||
<TITLE>mass of gas in pipe</TITLE>
|
||||
<VARNAME>mgas</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>g</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>12</SUB_ID>
|
||||
<TITLE>Reynolds number</TITLE>
|
||||
<VARNAME>re</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>null</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>13</SUB_ID>
|
||||
<TITLE>mass flow parameter (cm)</TITLE>
|
||||
<VARNAME>cm</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>(kg*K/J)**(1/2)</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>14</SUB_ID>
|
||||
<TITLE>mean gas velocity</TITLE>
|
||||
<VARNAME>v</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>m/s</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>15</SUB_ID>
|
||||
<TITLE>friction factor</TITLE>
|
||||
<VARNAME>ff</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>null</UNITS>
|
||||
</IVAR>
|
||||
</IVARS_LIST>
|
||||
<EVARS_LIST>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>1</SUB_ID>
|
||||
<TITLE>temperature at port 1</TITLE>
|
||||
<VARNAME>t1</VARNAME>
|
||||
<VARNAME2>dt1</VARNAME2>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>1</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>K</UNITS>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
|
||||
<DEF_VALUE>2.93150000000000e+002</DEF_VALUE>
|
||||
<VALUE>2.93150000000000e+002</VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>2</SUB_ID>
|
||||
<TITLE>pressure at port 1</TITLE>
|
||||
<VARNAME>p1</VARNAME>
|
||||
<VARNAME2>dp1</VARNAME2>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>1</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
<MIN_VALUE>-1.01300000000000e+005</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+012</MAX_VALUE>
|
||||
<DEF_VALUE>0.00000000000000e+000</DEF_VALUE>
|
||||
<VALUE>0.00000000000000e+000</VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>3</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 1</TITLE>
|
||||
<VARNAME>dh1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>4</SUB_ID>
|
||||
<TITLE>mass flow rate at port 1</TITLE>
|
||||
<VARNAME>dm1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>5</SUB_ID>
|
||||
<TITLE>temperature at port 2</TITLE>
|
||||
<VARNAME>t2</VARNAME>
|
||||
<VARNAME2>dt2</VARNAME2>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>1</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>K</UNITS>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
|
||||
<DEF_VALUE>2.93150000000000e+002</DEF_VALUE>
|
||||
<VALUE>2.93150000000000e+002</VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>6</SUB_ID>
|
||||
<TITLE>pressure at port 2</TITLE>
|
||||
<VARNAME>p2</VARNAME>
|
||||
<VARNAME2>dp2</VARNAME2>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>1</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
<MIN_VALUE>-1.01300000000000e+005</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+012</MAX_VALUE>
|
||||
<DEF_VALUE>0.00000000000000e+000</DEF_VALUE>
|
||||
<VALUE>0.00000000000000e+000</VALUE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>7</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 2</TITLE>
|
||||
<VARNAME>dh2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>8</SUB_ID>
|
||||
<TITLE>mass flow rate at port 2</TITLE>
|
||||
<VARNAME>dm2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
</EVARS_LIST>
|
||||
<SUBIDS_RESET>0</SUBIDS_RESET>
|
||||
</SUBMODEL>
|
||||
</SPE>
|
||||
@@ -0,0 +1,224 @@
|
||||
/* Submodel PNL00R skeleton created by AME Submodel editing utility
|
||||
ven. 5. août 14:34:41 2016 */
|
||||
|
||||
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "ameutils.h"
|
||||
/* *******************************************************************************
|
||||
TITLE : PNL00R
|
||||
------------------------------------------------------------------------------
|
||||
DESCRIPTION :
|
||||
PNL00R is a submodel of a pneumatic pipe with only friction effects.
|
||||
|
||||
Pipe friction is taken into account using a friction factor based on
|
||||
the Reynolds number and the relative roughness.
|
||||
------------------------------------------------------------------------------
|
||||
USAGE :
|
||||
Use this submodel to simulate a pneumatic pipe with friction effects,
|
||||
when the Mach number is low, ie gas velocity < 0.3 * speed of sound .
|
||||
|
||||
The submodels PNGD001 or PNGD002 should be included in your circuit to
|
||||
define the characteristics of the gas.
|
||||
------------------------------------------------------------------------------
|
||||
PARAMETER SETTINGS :
|
||||
------------------------------------------------------------------------------
|
||||
DATE OF CREATION / AUTHOR :
|
||||
2002 FS from PNL0R SN
|
||||
------------------------------------------------------------------------------
|
||||
REVISIONS :
|
||||
------------------------------------------------------------------------------
|
||||
LIST OF FUNCTIONS USED :
|
||||
pn2pipefr_() : frictional coefficient in pneumatic pipes
|
||||
pn2getatp_() : get atmospheric pressure
|
||||
------------------------------------------------------------------------------
|
||||
SOURCE :
|
||||
|
||||
This material contains trade secrets or otherwise confidential
|
||||
information owned by Siemens Industry Software Inc. or its
|
||||
affiliates (collectively, "Siemens"), or its licensors. Access to
|
||||
and use of this information is strictly limited as set forth in the
|
||||
Customer's applicable agreements with Siemens.
|
||||
|
||||
Unpublished work. Copyright 2023 Siemens
|
||||
|
||||
******************************************************************************* */
|
||||
|
||||
#define _SUBMODELNAME_ "PNL00R"
|
||||
|
||||
/* >>>>>>>>>>>>Insert Private Code Here. */
|
||||
#define TABFR 0 /* real store 0, 1 & 2 are used by pn2pipefr */
|
||||
#define PATM 3
|
||||
#define AREA 4
|
||||
|
||||
|
||||
#define SPL_FR 0
|
||||
/* <<<<<<<<<<<<End of Private Code. */
|
||||
|
||||
|
||||
/* There are 3 real parameters:
|
||||
|
||||
diam diameter of pipe [mm -> m]
|
||||
le pipe length [m]
|
||||
rr relative roughness [null]
|
||||
*/
|
||||
|
||||
|
||||
/* There is 1 integer parameter:
|
||||
|
||||
gi gas type index
|
||||
*/
|
||||
|
||||
void pnl00rin_(int *n, double rp[3], int ip[1], double c[5], int ic[1])
|
||||
|
||||
{
|
||||
int loop, error;
|
||||
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
|
||||
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
|
||||
int gi;
|
||||
double diam, le, rr;
|
||||
|
||||
gi = ip[0];
|
||||
|
||||
diam = rp[0];
|
||||
le = rp[1];
|
||||
rr = rp[2];
|
||||
loop = 0;
|
||||
error = 0;
|
||||
|
||||
/*
|
||||
If necessary, check values of the following:
|
||||
|
||||
rp[0..2]
|
||||
*/
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Check Statements. */
|
||||
|
||||
pn2_valid_gas_(&gi, &error);
|
||||
|
||||
if (diam <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nDiameter of pipe should be > 0 [mm].\n");
|
||||
}
|
||||
|
||||
if (le <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nPipe length should be > 0 [m].\n");
|
||||
}
|
||||
|
||||
if (rr < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nRelative roughness should be >= 0.\n");
|
||||
}
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Check Statements. */
|
||||
|
||||
/* Integer parameter checking: */
|
||||
|
||||
if (gi < 1 || gi > 99)
|
||||
{
|
||||
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
|
||||
error = 2;
|
||||
}
|
||||
|
||||
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
|
||||
|
||||
/* Common -> SI units conversions. */
|
||||
|
||||
rp[0] *= 1.00000000000000e-003;
|
||||
diam = rp[0];
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
|
||||
|
||||
c[PATM] = pn2getatp_();
|
||||
|
||||
/* Compute the cross-sectional area of pipe. */
|
||||
c[AREA] = M_PI * (diam) * (diam) / 4.0;
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
|
||||
}
|
||||
|
||||
/* There are 2 ports.
|
||||
|
||||
Port 1 has 4 variables:
|
||||
|
||||
1 dh1 duplicate of dh2 (sign reversed)
|
||||
2 dm1 duplicate of dm2 (sign reversed)
|
||||
3 t1 temperature at port 1 [K] basic variable input
|
||||
4 p1 pressure at port 1 [Pa] basic variable input
|
||||
|
||||
Port 2 has 4 variables:
|
||||
|
||||
1 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable output
|
||||
2 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable output
|
||||
3 t2 temperature at port 2 [K] basic variable input
|
||||
4 p2 pressure at port 2 [Pa] basic variable input
|
||||
*/
|
||||
|
||||
/* There are 4 internal variables.
|
||||
|
||||
1 re Reynolds number [null] basic variable
|
||||
2 cm mass flow parameter (cm) [(kg*K/J)**(1/2)] basic variable
|
||||
3 v mean gas velocity [m/s] basic variable
|
||||
4 ff friction factor [null] basic variable
|
||||
*/
|
||||
|
||||
void pnl00r_(int *n, double *t1, double *p1, double *dh2, double *dm2
|
||||
, double *t2, double *p2, double *re, double *cm, double *v
|
||||
, double *ff, double rp[3], int ip[1], double c[5], int ic[1])
|
||||
|
||||
{
|
||||
int loop;
|
||||
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
|
||||
double pa1, pa2;
|
||||
double dh1loc, dm1loc;
|
||||
int dummyreg;
|
||||
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
|
||||
int gi;
|
||||
double diam, le, rr;
|
||||
|
||||
gi = ip[0];
|
||||
|
||||
diam = rp[0];
|
||||
le = rp[1];
|
||||
rr = rp[2];
|
||||
loop = 0;
|
||||
|
||||
/*
|
||||
Set all submodel outputs below:
|
||||
|
||||
*dh2 = ??;
|
||||
*dm2 = ??;
|
||||
*re = ??;
|
||||
*cm = ??;
|
||||
*v = ??;
|
||||
*ff = ??;
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
|
||||
|
||||
/* set absolute pressure */
|
||||
pa1 = *p1 + c[PATM];
|
||||
pa2 = *p2 + c[PATM];
|
||||
|
||||
/* Compute flow through the pipe */
|
||||
|
||||
pn2pipefr_(&pa2, t2, &pa1, t1, &diam, &rr, &le, &c[AREA], re, v, ff,
|
||||
dh2, dm2, &dh1loc, &dm1loc, cm, &c[TABFR], &gi, &ic[SPL_FR], &dummyreg);
|
||||
|
||||
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
|
||||
|
||||
/* SI -> Common units conversions. */
|
||||
|
||||
*dm2 /= 1.00000000000000e-003;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<!DOCTYPE SPE>
|
||||
<SPE DOC_VERSION="2" AME_VERSION="14.0.0 - 42489-40361 2015">
|
||||
|
||||
<SUBMODEL>
|
||||
<SUB_TYPE>0</SUB_TYPE>
|
||||
<SUB_ID_MAX>18</SUB_ID_MAX>
|
||||
<DEFAULT_ICON>p2port</DEFAULT_ICON>
|
||||
<SUB_LABEL>Friction submodel of pneumatic pipe (R)</SUB_LABEL>
|
||||
<SUB_UNIT>0</SUB_UNIT>
|
||||
<R_STORES_NUMBER>5</R_STORES_NUMBER>
|
||||
<I_STORES_NUMBER>1</I_STORES_NUMBER>
|
||||
<OUTPUT_TYPE>1</OUTPUT_TYPE>
|
||||
<RPARAMS_LIST>
|
||||
<RPARAM>
|
||||
<SUB_ID>13</SUB_ID>
|
||||
<TITLE>diameter of pipe</TITLE>
|
||||
<VARNAME>diam</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e+01</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e+01</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e-003</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+007</MAX_VALUE>
|
||||
<UNITS>mm</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>14</SUB_ID>
|
||||
<TITLE>pipe length</TITLE>
|
||||
<VARNAME>le</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e+00</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e+00</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e-006</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+004</MAX_VALUE>
|
||||
<UNITS>m</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>15</SUB_ID>
|
||||
<TITLE>relative roughness</TITLE>
|
||||
<VARNAME>rr</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1.00000000000000e-05</DEF_VALUE>
|
||||
<VALUE>1.00000000000000e-05</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e-001</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
</RPARAMS_LIST>
|
||||
<IPARAMS_LIST>
|
||||
<IPARAM>
|
||||
<SUB_ID>16</SUB_ID>
|
||||
<TITLE>gas type index</TITLE>
|
||||
<VARNAME>gi</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1</DEF_VALUE>
|
||||
<VALUE>1</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>99</MAX_VALUE>
|
||||
</IPARAM>
|
||||
</IPARAMS_LIST>
|
||||
<IVARS_LIST>
|
||||
<IVAR>
|
||||
<SUB_ID>9</SUB_ID>
|
||||
<TITLE>Reynolds number</TITLE>
|
||||
<VARNAME>re</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>null</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>10</SUB_ID>
|
||||
<TITLE>mass flow parameter (cm)</TITLE>
|
||||
<VARNAME>cm</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>(kg*K/J)**(1/2)</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>11</SUB_ID>
|
||||
<TITLE>mean gas velocity</TITLE>
|
||||
<VARNAME>v</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>m/s</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>12</SUB_ID>
|
||||
<TITLE>friction factor</TITLE>
|
||||
<VARNAME>ff</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>null</UNITS>
|
||||
</IVAR>
|
||||
</IVARS_LIST>
|
||||
<EVARS_LIST>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>17</SUB_ID>
|
||||
<VARNAME>dh1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>1</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>0</PRIMARY_VAR>
|
||||
<DUP_TYPE>1</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>18</SUB_ID>
|
||||
<VARNAME>dm1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>1</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>1</PRIMARY_VAR>
|
||||
<DUP_TYPE>1</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>3</SUB_ID>
|
||||
<TITLE>temperature at port 1</TITLE>
|
||||
<VARNAME>t1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>K</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>4</SUB_ID>
|
||||
<TITLE>pressure at port 1</TITLE>
|
||||
<VARNAME>p1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>5</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 2</TITLE>
|
||||
<VARNAME>dh2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>6</SUB_ID>
|
||||
<TITLE>mass flow rate at port 2</TITLE>
|
||||
<VARNAME>dm2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>7</SUB_ID>
|
||||
<TITLE>temperature at port 2</TITLE>
|
||||
<VARNAME>t2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>K</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>8</SUB_ID>
|
||||
<TITLE>pressure at port 2</TITLE>
|
||||
<VARNAME>p2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
</EVARS_LIST>
|
||||
<SUBIDS_RESET>0</SUBIDS_RESET>
|
||||
</SUBMODEL>
|
||||
</SPE>
|
||||
@@ -0,0 +1,240 @@
|
||||
/* Submodel PNOR001 skeleton created by AME Submodel editing utility
|
||||
lun. 10. juil. 17:22:57 2017 */
|
||||
|
||||
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "ameutils.h"
|
||||
/* *******************************************************************************
|
||||
TITLE : PNOR001
|
||||
------------------------------------------------------------------------------
|
||||
DATE OF CREATION / AUTHOR :
|
||||
2002 : Created by FS from PNOR01
|
||||
------------------------------------------------------------------------------
|
||||
SOURCE :
|
||||
|
||||
This material contains trade secrets or otherwise confidential
|
||||
information owned by Siemens Industry Software Inc. or its
|
||||
affiliates (collectively, "Siemens"), or its licensors. Access to
|
||||
and use of this information is strictly limited as set forth in the
|
||||
Customer's applicable agreements with Siemens.
|
||||
|
||||
Unpublished work. Copyright 2023 Siemens
|
||||
|
||||
******************************************************************************* */
|
||||
|
||||
#define _SUBMODELNAME_ "PNOR001"
|
||||
|
||||
/* >>>>>>>>>>>>Insert Private Code Here. */
|
||||
|
||||
/* real stores */
|
||||
#define PATM 0
|
||||
#define AREA 1
|
||||
#define CQ 2
|
||||
|
||||
/* integer stores */
|
||||
#define DISC_ORIF 0
|
||||
|
||||
/* <<<<<<<<<<<<End of Private Code. */
|
||||
|
||||
|
||||
/* There are 4 real parameters:
|
||||
|
||||
cq flow coefficient (Cq) [null]
|
||||
area orifice area [mm**2 -> m**2]
|
||||
Cv flow coefficient (Cv) [null]
|
||||
Kv flow coefficient (Kv) [null]
|
||||
*/
|
||||
|
||||
|
||||
/* There are 2 integer parameters:
|
||||
|
||||
gi gas type index
|
||||
flowset flow coefficient setting
|
||||
*/
|
||||
|
||||
void pnor001in_(int *n, double rp[4], int ip[2], double c[3]
|
||||
, int ic[1])
|
||||
|
||||
{
|
||||
int loop, error;
|
||||
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
|
||||
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
|
||||
int gi, flowset;
|
||||
double cq, area, Cv, Kv;
|
||||
|
||||
gi = ip[0];
|
||||
flowset = ip[1];
|
||||
|
||||
cq = rp[0];
|
||||
area = rp[1];
|
||||
Cv = rp[2];
|
||||
Kv = rp[3];
|
||||
loop = 0;
|
||||
error = 0;
|
||||
|
||||
/*
|
||||
If necessary, check values of the following:
|
||||
|
||||
rp[0..3]
|
||||
*/
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Check Statements. */
|
||||
|
||||
pn2_valid_gas_(&gi, &error);
|
||||
|
||||
if (flowset == 1)
|
||||
{
|
||||
if (area < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nOrifice area should be positive.\n");
|
||||
}
|
||||
|
||||
if (cq <= 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nFlow coefficient should be strictly positive.\n");
|
||||
}
|
||||
}
|
||||
else if (flowset == 2)
|
||||
{
|
||||
if (Cv < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nFlow coefficient (Cv) should be positive (value is %g).\n", Cv);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Kv < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nFlow coefficient (Kv) should be positive (value is %g).\n", Kv);
|
||||
}
|
||||
}
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Check Statements. */
|
||||
|
||||
/* Integer parameter checking: */
|
||||
|
||||
if (gi < 1 || gi > 99)
|
||||
{
|
||||
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
|
||||
error = 2;
|
||||
}
|
||||
if (flowset < 1 || flowset > 3)
|
||||
{
|
||||
amefprintf(stderr, "\nflow coefficient setting must be in range [1..3].\n");
|
||||
error = 2;
|
||||
}
|
||||
|
||||
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
|
||||
|
||||
/* Common -> SI units conversions. */
|
||||
|
||||
rp[1] *= 1.00000000000000e-006;
|
||||
area = rp[1];
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
|
||||
|
||||
/* get atmospheric pressure */
|
||||
c[PATM] = pn2getatp_();
|
||||
|
||||
if (flowset == 1)
|
||||
{
|
||||
c[CQ] = cq;
|
||||
c[AREA] = area;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* calculation of equivalent area with Cv or Kv.
|
||||
Default value of cq; the same value will be used in pn2rcqfix. */
|
||||
c[CQ] = 0.72;
|
||||
|
||||
if (flowset == 2) /* Cv */
|
||||
orif_areafromcv_(&Cv, &c[CQ], &c[AREA]);
|
||||
else
|
||||
orif_areafromkv_(&Kv, &c[CQ], &c[AREA]);
|
||||
}
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
|
||||
}
|
||||
|
||||
/* There are 2 ports.
|
||||
|
||||
Port 1 has 4 variables:
|
||||
|
||||
1 dh1 enthalpy flow rate at port 1 [J/s -> W] basic variable output
|
||||
2 dm1 mass flow rate at port 1 [g/s -> kg/s] basic variable output
|
||||
3 temp1 temperature at port 1 [K] basic variable input
|
||||
4 press1 pressure at port 1 [Pa] basic variable input
|
||||
|
||||
Port 2 has 4 variables:
|
||||
|
||||
1 dh2 duplicate of dh1 (sign reversed)
|
||||
2 dm2 duplicate of dm1 (sign reversed)
|
||||
3 temp2 temperature at port 2 [K] basic variable input
|
||||
4 press2 pressure at port 2 [Pa] basic variable input
|
||||
*/
|
||||
|
||||
/* There are 2 internal variables.
|
||||
|
||||
1 cm mass flow parameter (cm) [(kg*K/J)**(1/2)] basic variable
|
||||
2 gasvel vena contracta gas velocity [m/s] basic variable
|
||||
*/
|
||||
|
||||
void pnor001_(int *n, double *dh1, double *dm1, double *temp1
|
||||
, double *press1, double *temp2, double *press2, double *cm
|
||||
, double *gasvel, double rp[4], int ip[2], double c[3]
|
||||
, int ic[1])
|
||||
|
||||
{
|
||||
int loop;
|
||||
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
|
||||
double pressa1, pressa2;
|
||||
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
|
||||
int gi, flowset;
|
||||
double cq, area, Cv, Kv;
|
||||
|
||||
gi = ip[0];
|
||||
flowset = ip[1];
|
||||
|
||||
cq = rp[0];
|
||||
area = rp[1];
|
||||
Cv = rp[2];
|
||||
Kv = rp[3];
|
||||
loop = 0;
|
||||
|
||||
/*
|
||||
Set all submodel outputs below:
|
||||
|
||||
*dh1 = ??;
|
||||
*dm1 = ??;
|
||||
*cm = ??;
|
||||
*gasvel = ??;
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
|
||||
|
||||
/* set absolute pressures */
|
||||
pressa1 = *press1 + c[PATM];
|
||||
pressa2 = *press2 + c[PATM];
|
||||
|
||||
/* calculation of the flows */
|
||||
pn2rcqfix_( dh1, dm1, temp1, &pressa1, temp2, &pressa2, &c[AREA], &c[CQ], &gi,
|
||||
cm, gasvel, &ic[DISC_ORIF]);
|
||||
|
||||
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
|
||||
|
||||
/* SI -> Common units conversions. */
|
||||
|
||||
*dm1 /= 1.00000000000000e-003;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<!DOCTYPE SPE>
|
||||
<SPE DOC_VERSION="2" AME_VERSION="14.0.0 - 42489-40361 2015">
|
||||
|
||||
<SUBMODEL>
|
||||
<SUB_TYPE>0</SUB_TYPE>
|
||||
<SUB_ID_MAX>18</SUB_ID_MAX>
|
||||
<DEFAULT_ICON>pn_orifice</DEFAULT_ICON>
|
||||
<SUB_LABEL>pneumatic orifice (constant flow coefficient)</SUB_LABEL>
|
||||
<SUB_UNIT>0</SUB_UNIT>
|
||||
<R_STORES_NUMBER>3</R_STORES_NUMBER>
|
||||
<I_STORES_NUMBER>1</I_STORES_NUMBER>
|
||||
<OUTPUT_TYPE>1</OUTPUT_TYPE>
|
||||
<RPARAMS_LIST>
|
||||
<RPARAM>
|
||||
<SUB_ID>12</SUB_ID>
|
||||
<TITLE>flow coefficient (Cq)</TITLE>
|
||||
<VARNAME>cq</VARNAME>
|
||||
<VISIBILITY>flowset==1</VISIBILITY>
|
||||
<DEF_VALUE>7.20000000000000e-01</DEF_VALUE>
|
||||
<VALUE>7.20000000000000e-01</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e-010</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+000</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>11</SUB_ID>
|
||||
<TITLE>orifice area</TITLE>
|
||||
<VARNAME>area</VARNAME>
|
||||
<VISIBILITY>flowset==1</VISIBILITY>
|
||||
<DEF_VALUE>5.00000000000000e+00</DEF_VALUE>
|
||||
<VALUE>5.00000000000000e+00</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
|
||||
<UNITS>mm**2</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>17</SUB_ID>
|
||||
<TITLE>flow coefficient (Cv)</TITLE>
|
||||
<VARNAME>Cv</VARNAME>
|
||||
<VISIBILITY>flowset==2</VISIBILITY>
|
||||
<DEF_VALUE>5.00000000000000e-01</DEF_VALUE>
|
||||
<VALUE>5.00000000000000e-01</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+030</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>18</SUB_ID>
|
||||
<TITLE>flow coefficient (Kv)</TITLE>
|
||||
<VARNAME>Kv</VARNAME>
|
||||
<VISIBILITY>flowset==3</VISIBILITY>
|
||||
<DEF_VALUE>4.00000000000000e-01</DEF_VALUE>
|
||||
<VALUE>4.00000000000000e-01</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+030</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
</RPARAMS_LIST>
|
||||
<IPARAMS_LIST>
|
||||
<IPARAM>
|
||||
<SUB_ID>13</SUB_ID>
|
||||
<TITLE>gas type index</TITLE>
|
||||
<VARNAME>gi</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1</DEF_VALUE>
|
||||
<VALUE>1</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>99</MAX_VALUE>
|
||||
</IPARAM>
|
||||
<IPARAM>
|
||||
<SUB_ID>14</SUB_ID>
|
||||
<TITLE>flow coefficient setting</TITLE>
|
||||
<VARNAME>flowset</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1</DEF_VALUE>
|
||||
<VALUE>1</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>3</MAX_VALUE>
|
||||
<ENUM_LIST>
|
||||
<ENUM>
|
||||
<ENUM_STRING>Cq</ENUM_STRING>
|
||||
</ENUM>
|
||||
<ENUM>
|
||||
<ENUM_STRING>Cv</ENUM_STRING>
|
||||
</ENUM>
|
||||
<ENUM>
|
||||
<ENUM_STRING>Kv</ENUM_STRING>
|
||||
</ENUM>
|
||||
</ENUM_LIST>
|
||||
</IPARAM>
|
||||
</IPARAMS_LIST>
|
||||
<IVARS_LIST>
|
||||
<IVAR>
|
||||
<SUB_ID>9</SUB_ID>
|
||||
<TITLE>mass flow parameter (cm)</TITLE>
|
||||
<VARNAME>cm</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>(kg*K/J)**(1/2)</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>10</SUB_ID>
|
||||
<TITLE>vena contracta gas velocity</TITLE>
|
||||
<VARNAME>gasvel</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>m/s</UNITS>
|
||||
</IVAR>
|
||||
</IVARS_LIST>
|
||||
<EVARS_LIST>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>1</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 1</TITLE>
|
||||
<VARNAME>dh1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>2</SUB_ID>
|
||||
<TITLE>mass flow rate at port 1</TITLE>
|
||||
<VARNAME>dm1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>3</SUB_ID>
|
||||
<TITLE>temperature at port 1</TITLE>
|
||||
<VARNAME>temp1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>K</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>4</SUB_ID>
|
||||
<TITLE>pressure at port 1</TITLE>
|
||||
<VARNAME>press1</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>5</SUB_ID>
|
||||
<VARNAME>dh2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>0</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>0</PRIMARY_VAR>
|
||||
<DUP_TYPE>1</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>6</SUB_ID>
|
||||
<VARNAME>dm2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>0</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>1</PRIMARY_VAR>
|
||||
<DUP_TYPE>1</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>7</SUB_ID>
|
||||
<TITLE>temperature at port 2</TITLE>
|
||||
<VARNAME>temp2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>K</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>8</SUB_ID>
|
||||
<TITLE>pressure at port 2</TITLE>
|
||||
<VARNAME>press2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
</EVARS_LIST>
|
||||
<SUBIDS_RESET>0</SUBIDS_RESET>
|
||||
</SUBMODEL>
|
||||
</SPE>
|
||||
@@ -0,0 +1,254 @@
|
||||
/* Submodel PNVO001 skeleton created by AME Submodel editing utility
|
||||
ven. 6. oct. 11:10:58 2017 */
|
||||
|
||||
|
||||
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include "ameutils.h"
|
||||
/* *******************************************************************************
|
||||
TITLE : PNVO001
|
||||
------------------------------------------------------------------------------
|
||||
DATE OF CREATION / AUTHOR :
|
||||
2002 : Created by FS from PNVO01
|
||||
------------------------------------------------------------------------------
|
||||
SOURCE :
|
||||
|
||||
This material contains trade secrets or otherwise confidential
|
||||
information owned by Siemens Industry Software Inc. or its
|
||||
affiliates (collectively, "Siemens"), or its licensors. Access to
|
||||
and use of this information is strictly limited as set forth in the
|
||||
Customer's applicable agreements with Siemens.
|
||||
|
||||
Unpublished work. Copyright 2023 Siemens
|
||||
|
||||
******************************************************************************* */
|
||||
|
||||
#define _SUBMODELNAME_ "PNVO001"
|
||||
|
||||
/* >>>>>>>>>>>>Insert Private Code Here. */
|
||||
|
||||
/* real stores */
|
||||
#define PATM 0
|
||||
#define AREAMAX 1
|
||||
#define CQ 2
|
||||
|
||||
/* integer stores */
|
||||
#define DISC_LIMIT 0
|
||||
#define DISC_ORIF 1
|
||||
|
||||
/* <<<<<<<<<<<<End of Private Code. */
|
||||
|
||||
|
||||
/* There are 4 real parameters:
|
||||
|
||||
cq flow coefficient (Cq) [null]
|
||||
area0 orifice area at maximum opening [mm**2 -> m**2]
|
||||
Cv maximum flow coefficient (Cv) [null]
|
||||
Kv maximum flow coefficient (Kv) [null]
|
||||
*/
|
||||
|
||||
|
||||
/* There are 2 integer parameters:
|
||||
|
||||
gi gas type index
|
||||
flowset flow coefficient setting
|
||||
*/
|
||||
|
||||
void pnvo001in_(int *n, double rp[4], int ip[2], double c[3]
|
||||
, int ic[2])
|
||||
|
||||
{
|
||||
int loop, error;
|
||||
/* >>>>>>>>>>>>Extra Initialization Function Declarations Here. */
|
||||
/* <<<<<<<<<<<<End of Extra Initialization declarations. */
|
||||
int gi, flowset;
|
||||
double cq, area0, Cv, Kv;
|
||||
|
||||
gi = ip[0];
|
||||
flowset = ip[1];
|
||||
|
||||
cq = rp[0];
|
||||
area0 = rp[1];
|
||||
Cv = rp[2];
|
||||
Kv = rp[3];
|
||||
loop = 0;
|
||||
error = 0;
|
||||
|
||||
/*
|
||||
If necessary, check values of the following:
|
||||
|
||||
rp[0..3]
|
||||
*/
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Check Statements. */
|
||||
|
||||
pn2_valid_gas_(&gi, &error);
|
||||
|
||||
if (flowset == 1)
|
||||
{
|
||||
if (area0 < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nOrifice area at maximum opening should be positive.\n");
|
||||
}
|
||||
|
||||
if (cq <= 0.0 )
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nFlow coefficient should be strictly positive.\n");
|
||||
}
|
||||
}
|
||||
else if (flowset == 2)
|
||||
{
|
||||
if (Cv < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nMaximum flow coefficient (Cv) should be positive (value is %g).\n", Cv);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Kv < 0.0)
|
||||
{
|
||||
error = 2;
|
||||
amefprintf(stderr, "\nMaximum flow coefficient (Kv) should be positive (value is %g).\n", Kv);
|
||||
}
|
||||
}
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Check Statements. */
|
||||
|
||||
/* Integer parameter checking: */
|
||||
|
||||
if (gi < 1 || gi > 99)
|
||||
{
|
||||
amefprintf(stderr, "\ngas type index must be in range [1..99].\n");
|
||||
error = 2;
|
||||
}
|
||||
if (flowset < 1 || flowset > 3)
|
||||
{
|
||||
amefprintf(stderr, "\nflow coefficient setting must be in range [1..3].\n");
|
||||
error = 2;
|
||||
}
|
||||
|
||||
SUBMODEL_HANDLE_AND_RESET_ERROR(_SUBMODELNAME_, n, error)
|
||||
|
||||
/* Common -> SI units conversions. */
|
||||
|
||||
rp[1] *= 1.00000000000000e-006;
|
||||
area0 = rp[1];
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Initialization Function Executable Statements. */
|
||||
|
||||
/* get atmospheric pressure */
|
||||
c[PATM] = pn2getatp_();
|
||||
|
||||
if (flowset == 1)
|
||||
{
|
||||
c[CQ] = cq;
|
||||
c[AREAMAX] = area0;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* calculation of equivalent maximal area with Cv or Kv.
|
||||
Default value of cq; the same value will be used in pn2rcqfix. */
|
||||
c[CQ] = 0.72;
|
||||
|
||||
if (flowset == 2) /* Cv */
|
||||
orif_areafromcv_(&Cv, &c[CQ], &c[AREAMAX]);
|
||||
else
|
||||
orif_areafromkv_(&Kv, &c[CQ], &c[AREAMAX]);
|
||||
}
|
||||
|
||||
/* <<<<<<<<<<<<End of Initialization Executable Statements. */
|
||||
}
|
||||
|
||||
/* There are 3 ports.
|
||||
|
||||
Port 1 has 1 variable:
|
||||
|
||||
1 res input signal [null] basic variable input
|
||||
|
||||
Port 2 has 4 variables:
|
||||
|
||||
1 dh2 enthalpy flow rate at port 2 [J/s -> W] basic variable output
|
||||
2 dm2 mass flow rate at port 2 [g/s -> kg/s] basic variable output
|
||||
3 temp2 temperature at port 2 [K] basic variable input
|
||||
4 press2 pressure at port 2 [Pa] basic variable input
|
||||
|
||||
Port 3 has 4 variables:
|
||||
|
||||
1 dh3 duplicate of dh2 (sign reversed)
|
||||
2 dm3 duplicate of dm2 (sign reversed)
|
||||
3 temp3 temperature at port 3 [K] basic variable input
|
||||
4 press3 pressure at port 3 [Pa] basic variable input
|
||||
*/
|
||||
|
||||
/* There are 3 internal variables.
|
||||
|
||||
1 xv fractional opening [null] basic variable
|
||||
2 cm mass flow parameter (cm) [(kg*K/J)**(1/2)] basic variable
|
||||
3 gasvel vena contracta gas velocity [m/s] basic variable
|
||||
*/
|
||||
|
||||
void pnvo001_(int *n, double *res, double *dh2, double *dm2
|
||||
, double *temp2, double *press2, double *temp3, double *press3
|
||||
, double *xv, double *cm, double *gasvel, double rp[4]
|
||||
, int ip[2], double c[3], int ic[2])
|
||||
|
||||
{
|
||||
int loop;
|
||||
/* >>>>>>>>>>>>Extra Calculation Function Declarations Here. */
|
||||
double marea; /* modulated area */
|
||||
double pressa2, pressa3;
|
||||
static double zero = 0.0, one = 1.0;
|
||||
/* <<<<<<<<<<<<End of Extra Calculation declarations. */
|
||||
int gi, flowset;
|
||||
double cq, area0, Cv, Kv;
|
||||
|
||||
gi = ip[0];
|
||||
flowset = ip[1];
|
||||
|
||||
cq = rp[0];
|
||||
area0 = rp[1];
|
||||
Cv = rp[2];
|
||||
Kv = rp[3];
|
||||
loop = 0;
|
||||
|
||||
/*
|
||||
Set all submodel outputs below:
|
||||
|
||||
*dh2 = ??;
|
||||
*dm2 = ??;
|
||||
*xv = ??;
|
||||
*cm = ??;
|
||||
*gasvel = ??;
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/* >>>>>>>>>>>>Calculation Function Executable Statements. */
|
||||
|
||||
/* set absolute pressure */
|
||||
pressa2 = *press2 + c[PATM];
|
||||
pressa3 = *press3 + c[PATM];
|
||||
|
||||
*xv = dlimit_(res, &zero, &one, &ic[DISC_LIMIT]);
|
||||
|
||||
/* limitation of the modulated area */
|
||||
marea = *xv * c[AREAMAX];
|
||||
|
||||
/*** calculation of the flows ***/
|
||||
pn2rcqfix_( dh2, dm2, temp2, &pressa2, temp3, &pressa3, &marea, &c[CQ], &gi,
|
||||
cm, gasvel, &ic[DISC_ORIF] );
|
||||
|
||||
/* <<<<<<<<<<<<End of Calculation Executable Statements. */
|
||||
|
||||
/* SI -> Common units conversions. */
|
||||
|
||||
*dm2 /= 1.00000000000000e-003;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,220 @@
|
||||
<?xml version="1.0" encoding="ISO-8859-1"?>
|
||||
<!DOCTYPE SPE>
|
||||
<SPE DOC_VERSION="2" AME_VERSION="14.0.0 - 42489-40361 2015">
|
||||
|
||||
<SUBMODEL>
|
||||
<SUB_TYPE>0</SUB_TYPE>
|
||||
<SUB_ID_MAX>20</SUB_ID_MAX>
|
||||
<DEFAULT_ICON>pn_morifice</DEFAULT_ICON>
|
||||
<SUB_LABEL>modulated pneumatic orifice (constant flow coefficient)</SUB_LABEL>
|
||||
<SUB_UNIT>0</SUB_UNIT>
|
||||
<R_STORES_NUMBER>3</R_STORES_NUMBER>
|
||||
<I_STORES_NUMBER>2</I_STORES_NUMBER>
|
||||
<OUTPUT_TYPE>1</OUTPUT_TYPE>
|
||||
<RPARAMS_LIST>
|
||||
<RPARAM>
|
||||
<SUB_ID>12</SUB_ID>
|
||||
<TITLE>flow coefficient (Cq)</TITLE>
|
||||
<VARNAME>cq</VARNAME>
|
||||
<VISIBILITY>flowset==1</VISIBILITY>
|
||||
<DEF_VALUE>7.20000000000000e-01</DEF_VALUE>
|
||||
<VALUE>7.20000000000000e-01</VALUE>
|
||||
<MIN_VALUE>1.00000000000000e-010</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+000</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>13</SUB_ID>
|
||||
<TITLE>orifice area at maximum opening</TITLE>
|
||||
<VARNAME>area0</VARNAME>
|
||||
<VISIBILITY>flowset==1</VISIBILITY>
|
||||
<DEF_VALUE>5.00000000000000e+00</DEF_VALUE>
|
||||
<VALUE>5.00000000000000e+00</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+006</MAX_VALUE>
|
||||
<UNITS>mm**2</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>18</SUB_ID>
|
||||
<TITLE>maximum flow coefficient (Cv)</TITLE>
|
||||
<VARNAME>Cv</VARNAME>
|
||||
<VISIBILITY>flowset==2</VISIBILITY>
|
||||
<DEF_VALUE>5.00000000000000e-01</DEF_VALUE>
|
||||
<VALUE>5.00000000000000e-01</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+030</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
<RPARAM>
|
||||
<SUB_ID>19</SUB_ID>
|
||||
<TITLE>maximum flow coefficient (Kv)</TITLE>
|
||||
<VARNAME>Kv</VARNAME>
|
||||
<VISIBILITY>flowset==3</VISIBILITY>
|
||||
<DEF_VALUE>4.00000000000000e-01</DEF_VALUE>
|
||||
<VALUE>4.00000000000000e-01</VALUE>
|
||||
<MIN_VALUE>0.00000000000000e+000</MIN_VALUE>
|
||||
<MAX_VALUE>1.00000000000000e+030</MAX_VALUE>
|
||||
<UNITS>null</UNITS>
|
||||
</RPARAM>
|
||||
</RPARAMS_LIST>
|
||||
<IPARAMS_LIST>
|
||||
<IPARAM>
|
||||
<SUB_ID>14</SUB_ID>
|
||||
<TITLE>gas type index</TITLE>
|
||||
<VARNAME>gi</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1</DEF_VALUE>
|
||||
<VALUE>1</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>99</MAX_VALUE>
|
||||
</IPARAM>
|
||||
<IPARAM>
|
||||
<SUB_ID>15</SUB_ID>
|
||||
<TITLE>flow coefficient setting</TITLE>
|
||||
<VARNAME>flowset</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<DEF_VALUE>1</DEF_VALUE>
|
||||
<VALUE>1</VALUE>
|
||||
<MIN_VALUE>1</MIN_VALUE>
|
||||
<MAX_VALUE>3</MAX_VALUE>
|
||||
<ENUM_LIST>
|
||||
<ENUM>
|
||||
<ENUM_STRING>Cq</ENUM_STRING>
|
||||
</ENUM>
|
||||
<ENUM>
|
||||
<ENUM_STRING>Cv</ENUM_STRING>
|
||||
</ENUM>
|
||||
<ENUM>
|
||||
<ENUM_STRING>Kv</ENUM_STRING>
|
||||
</ENUM>
|
||||
</ENUM_LIST>
|
||||
</IPARAM>
|
||||
</IPARAMS_LIST>
|
||||
<IVARS_LIST>
|
||||
<IVAR>
|
||||
<SUB_ID>20</SUB_ID>
|
||||
<TITLE>fractional opening</TITLE>
|
||||
<VARNAME>xv</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>null</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>10</SUB_ID>
|
||||
<TITLE>mass flow parameter (cm)</TITLE>
|
||||
<VARNAME>cm</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>(kg*K/J)**(1/2)</UNITS>
|
||||
</IVAR>
|
||||
<IVAR>
|
||||
<SUB_ID>11</SUB_ID>
|
||||
<TITLE>vena contracta gas velocity</TITLE>
|
||||
<VARNAME>gasvel</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<UNITS>m/s</UNITS>
|
||||
</IVAR>
|
||||
</IVARS_LIST>
|
||||
<EVARS_LIST>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>1</SUB_ID>
|
||||
<TITLE>input signal</TITLE>
|
||||
<VARNAME>res</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>null</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>2</SUB_ID>
|
||||
<TITLE>enthalpy flow rate at port 2</TITLE>
|
||||
<VARNAME>dh2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>J/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>3</SUB_ID>
|
||||
<TITLE>mass flow rate at port 2</TITLE>
|
||||
<VARNAME>dm2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>2</IO>
|
||||
<UNITS>g/s</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>4</SUB_ID>
|
||||
<TITLE>temperature at port 2</TITLE>
|
||||
<VARNAME>temp2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>K</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>5</SUB_ID>
|
||||
<TITLE>pressure at port 2</TITLE>
|
||||
<VARNAME>press2</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
<PORT>
|
||||
<EVAR>
|
||||
<SUB_ID>6</SUB_ID>
|
||||
<VARNAME>dh3</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>1</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>0</PRIMARY_VAR>
|
||||
<DUP_TYPE>1</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>7</SUB_ID>
|
||||
<VARNAME>dm3</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>4</TYPE>
|
||||
<PRIMARY_PORT>1</PRIMARY_PORT>
|
||||
<PRIMARY_VAR>1</PRIMARY_VAR>
|
||||
<DUP_TYPE>1</DUP_TYPE>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>8</SUB_ID>
|
||||
<TITLE>temperature at port 3</TITLE>
|
||||
<VARNAME>temp3</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>K</UNITS>
|
||||
</EVAR>
|
||||
<EVAR>
|
||||
<SUB_ID>9</SUB_ID>
|
||||
<TITLE>pressure at port 3</TITLE>
|
||||
<VARNAME>press3</VARNAME>
|
||||
<VISIBILITY>True</VISIBILITY>
|
||||
<TYPE>0</TYPE>
|
||||
<DIMENSION>1</DIMENSION>
|
||||
<IO>1</IO>
|
||||
<UNITS>Pa</UNITS>
|
||||
</EVAR>
|
||||
</PORT>
|
||||
</EVARS_LIST>
|
||||
<SUBIDS_RESET>0</SUBIDS_RESET>
|
||||
</SUBMODEL>
|
||||
</SPE>
|
||||
File diff suppressed because it is too large.
Load diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,140 @@
|
||||
# test_mql
|
||||
|
||||
本目录记录 AMESim 模型 `test_mql.ame` 向 `app.simulation` 迁移时使用的源模型信息、结果对齐约定和当前进度。
|
||||
|
||||
## 目标
|
||||
|
||||
迁移目标不是复制 AMESim `.results` 中的观测值,而是建立真实的 Python 仿真链路:
|
||||
|
||||
`组件方程 -> SimulationNetwork/系统装配 -> closure -> snapshot/端口写回 -> RHS -> solver -> reporting/comparison`
|
||||
|
||||
Python 输出只有通过 AMESim baseline 对比后才能作为数值一致性依据。duplicate、反号或派生观测可以用于验证端口方向,但不能替代组件方程和网络闭合。
|
||||
|
||||
## 模型与源数据
|
||||
|
||||
- AMESim 源模型:`AmesimModels/test_mql.ame`
|
||||
- Python 系统类:`app.simulation.examples.test_mql.system.TestMqlSystem`
|
||||
- 结构运行入口:`app.simulation.examples.test_mql.run`
|
||||
- 132 状态比较入口:`app.simulation.examples.test_mql.run_full_state_comparison`
|
||||
- AMESim 组件数:117
|
||||
- LINE 连接数:84
|
||||
- 连续状态数:132
|
||||
- 离散状态数:24
|
||||
- 全局参数:`D1=20`、`D2=20`、`D3=14`、`P0=153`、`Pdq=1`、`V=15`、`cf=0.45`
|
||||
|
||||
`.ame` 文件是 tar 包,迁移和校验主要使用其中的:
|
||||
|
||||
- `test_mql_.cir`:组件、连接、参数表达式和生成代码线索。
|
||||
- `test_mql_.param` / `test_mql_.data`:参数和结果配套数据。
|
||||
- `test_mql_.modelinfo` / `test_mql_.sim`:状态数和仿真设置。
|
||||
- `test_mql_.var` / `test_mql_.results`:AMESim `Data_Path` 目录和 baseline 时序。
|
||||
|
||||
当前结果解析器可读取 1002 个时间点和 1116 个保存变量,无需先转换成 CSV。
|
||||
|
||||
## 当前实现
|
||||
|
||||
当前已经完成:
|
||||
|
||||
- AMESim 组件、LINE 连接、直接组件接触、全局参数和变量目录解析。
|
||||
- 氦气 Peng-Robinson 物性;内部使用绝对压力,AMESim `press` 按相对 `101300 Pa` 的表压输出。
|
||||
- `PNCH023 / PNCH012 / PNOR001 / PNVO001` 气动组件。
|
||||
- `PNL0001 / PNL0002 / PNL0003 / PNL00R` 管路动态或阻性关系。
|
||||
- `PN3NODE2 / P4NODE2` 代数节点、真实邻接拓扑、canonical flow 和端口写回。
|
||||
- `PNRP17 / MECMAS21 / LSTP00A / LMECHN1 / UD00 / FORC` 当前工况可确认的机械行为。
|
||||
- 112 个气动状态和 20 个机械状态组成的 132 状态总闭包。
|
||||
- 活塞运动学、变容气室机械反馈、气动力、外力、端止动和质量约束耦合。
|
||||
- 关键 `Data_Path` 序列导出、schema validation、AMESim 插值 comparison、误差排序、端点诊断和变容气室 RHS 拆解。
|
||||
|
||||
主要实现位置:
|
||||
|
||||
- `app/simulation/examples/test_mql/system.py`
|
||||
- `app/simulation/examples/test_mql/closure.py`
|
||||
- `app/simulation/examples/test_mql/pneumatic.py`
|
||||
- `app/simulation/examples/test_mql/mechanical.py`
|
||||
- `app/simulation/examples/test_mql/lines.py`
|
||||
- `app/simulation/examples/test_mql/primitives/`
|
||||
- `app/simulation/examples/test_mql/structural_network.py`
|
||||
- `app/simulation/reporting/amesim_results.py`
|
||||
- `app/simulation/reporting/test_mql_comparison.py`
|
||||
- `app/simulation/examples/test_mql/run_full_state_comparison.py`
|
||||
|
||||
## 当前对比结果
|
||||
|
||||
默认 comparison 已从区间内插值的 `t=1e-5 s` 改为与 AMESim
|
||||
首个保存时刻精确对齐的 `t=0.01 s`,当前比较 13 个关键信号,并
|
||||
新增 `xv@pn_morifice_1`、`dm2@pn_morifice_1` 的结构化 PNVO 诊断。
|
||||
|
||||
事件前 `t=0.01 s` 对比:
|
||||
|
||||
- `press@pn_c1_8`:Python `-1286.601221 Pa`,AMESim
|
||||
`-1288.253185 Pa`,绝对误差约 `1.651964 Pa`。
|
||||
- `dm1@pneumatic_69`:Python `-0.002485112 g/s`,AMESim
|
||||
`-0.002054255 g/s`;换算后的 canonical 流量绝对误差约
|
||||
`4.30857e-7 kg/s`。
|
||||
- `xv@pn_morifice_1` 和 `dm2@pn_morifice_1` 在两侧均为 0,确认
|
||||
STEP0 事件前 PNVO 保持关闭。
|
||||
- `vol1@pn_brp2_8` 绝对误差约 `8.76e-5`,机械位移、速度和加速度
|
||||
仍保持较小误差;当前较明显的累计差异集中在 PNL0001 流量和
|
||||
PNCH012 压力/能量链路。
|
||||
|
||||
新增 `--pnvo-event-boundary` 诊断:先积分到 `0.04 s` 的左极限,
|
||||
再按 STEP0 的右连续语义在事件时刻读取开度和流量。结果为:
|
||||
|
||||
- `xv@pn_morifice_1`:Python/AMESim 均为 `1`。
|
||||
- `dm2@pn_morifice_1`:Python `502.945005 g/s`,AMESim
|
||||
`497.823823 g/s`,绝对误差约 `5.121182 g/s`,相对约 `1.03%`。
|
||||
- `press@pn_c1_8` 绝对误差约 `55.4815 Pa`;
|
||||
`dm1@pneumatic_69` 绝对误差约 `0.00281880 g/s`。
|
||||
|
||||
这说明 PNVO 开启瞬间的开度语义和主流量量级已经对齐,但事件前
|
||||
累积压力/支路流量仍有偏差。常规积分直接跨越事件到 `0.05 s` 时,
|
||||
当前进程会被系统终止,尚未形成可信的事件后结果;不能据此声明
|
||||
完整 `0.04 -> 0.05 s` 窗口已经可运行。
|
||||
|
||||
## 下一步
|
||||
|
||||
1. 优先定位 `0 -> 0.04 s` 累积的 `pneumatic_69` 流量与
|
||||
`pn_c1_8` 压力偏差,区分 PNL0001 阻力和 PNCH012 能量方程。
|
||||
2. 采用显式事件分段或针对事件后的局部数值策略,解决跨越
|
||||
`t=0.04 s` 后积分进程被终止的问题,再验证 `t=0.05 s` 保存点。
|
||||
3. 保留 `press@pn_c1_8`、`dm1@pneumatic_69`、PNVO `xv/dm2`
|
||||
和 chamber RHS breakdown 作为同一条诊断链。
|
||||
4. 在完整开启窗口稳定后,再判断是否需要校准 PNVO 流量系数。
|
||||
|
||||
`pn_c1_8` 的直接主线是 `pneumatic_69`;`pneumatic_96`
|
||||
属于另一条固定气室支路,不是该诊断对象。
|
||||
|
||||
## 运行与验证
|
||||
|
||||
运行默认短时域 comparison:
|
||||
|
||||
```bash
|
||||
python3 -m app.simulation.examples.test_mql.run_full_state_comparison
|
||||
```
|
||||
|
||||
运行 PNVO 事件边界诊断:
|
||||
|
||||
```bash
|
||||
python3 -m app.simulation.examples.test_mql.run_full_state_comparison --pnvo-event-boundary
|
||||
```
|
||||
|
||||
运行相关测试:
|
||||
|
||||
```bash
|
||||
python3 -m unittest tests.test_run_test_mql_full_state_comparison tests.test_test_mql_pnl0001_segment
|
||||
```
|
||||
|
||||
运行全量测试:
|
||||
|
||||
```bash
|
||||
python3 -m unittest discover -s tests -t .
|
||||
```
|
||||
|
||||
## 对齐约定
|
||||
|
||||
- 组件 alias 和输出名优先保持 AMESim 原名及 `Data_Path`。
|
||||
- `PortState.m_flow > 0` 表示流入当前组件。
|
||||
- closure 先定义 canonical flow,再按各组件端口方向写回 `m_flow`。
|
||||
- AMESim 管路质量流量通常以 `g/s` 保存,Python 内部统一使用 `kg/s`。
|
||||
- CSV 是人工检查和交换格式,不是读取 AMESim baseline 的前置条件。
|
||||
- 未完成真实 Python 输出对比前,不使用“与 AMESim 完全一致”之类结论。
|
||||
@@ -8,15 +8,16 @@ ReactFlow 系统建模与 `app.simulation` 仿真后端。
|
||||
- `POST /api/reactflow/system-xml`:导出 System XML v2。
|
||||
- `POST /api/reactflow/compile-model`:将 ReactFlow 节点、参数和连线编译为仿真网络,并返回组件端口、无方向物理连接、压力-流量方程结构及未连接端口。
|
||||
- `POST /api/reactflow/simulate-testmodel`:运行现有固定拓扑 TestModel;该接口暂时不是任意拓扑求解器。
|
||||
- `POST /api/reactflow/simulate-test-mql`:返回固定拓扑 AMESim `test_mql` 的结构与采样摘要;132 状态数值对比使用独立 comparison 入口。AMESim 子模型已有 19 个第一版公开模型,但该接口本身不是任意拖拽拓扑求解器。
|
||||
- `POST /api/system-xml/validate`:接收原始 System XML v2,返回 XML、XSD 和模型语义三层诊断。
|
||||
- `POST /api/system-xml/parse`:校验 XML 并返回规范化的 ReactFlow 工程对象。
|
||||
- `POST /api/system-xml/compile-model`:校验并解析 XML,然后创建 `app.simulation` 组件网络。
|
||||
- `POST /api/system-xml/simulate`:按 XML 中的组件、物理连接、参数和仿真设置运行通用气动网络 MVP,并返回组件及端口时间序列。
|
||||
- `POST /api/system-xml/simulate`:按 XML 中的组件、连接、参数和仿真设置运行当前支持的气动、标量信号及一维机械网络 MVP,并返回组件及端口时间序列。
|
||||
- `POST /api/simulation-results/csv`:校验结构化结果快照并导出 UTF-8 CSV 文件。
|
||||
|
||||
气动端口的后端契约采用 `p` 势变量相等、`m_flow` 流变量代数和为零、`h_outflow` 按 stream 规则混合。所有组件统一规定 `m_flow > 0` 表示流入组件,物理连接的端点顺序不表示流向。
|
||||
|
||||
当前网络层可以从组件和连接生成压力-流量残差,使用 SciPy 完成非线性代数闭合和时间积分,并按实际流向传播 stream 焓。XML 通用仿真当前采用半显式 ODE/代数 MVP:气瓶和贮箱作为储能元件,孔板及 XML 管段作为阻性元件,三通作为等压零结点。它不是完整 DAE 求解器,也不等价于严格 Modelica.Fluid 实现。
|
||||
当前网络层可按端口域处理气动压力-流量残差与 stream 焓、标量信号传播,以及一维机械 `x/v` 等值和 `f` 平衡,并使用 SciPy 完成非线性代数闭合和时间积分。XML 通用仿真当前采用半显式 ODE/代数 MVP:气瓶和贮箱作为储能元件,孔板及 XML 管段作为阻性元件,三通作为等压零结点,同时支持已登记的信号和机械基础件。它不是完整 DAE 或事件求解器,也不等价于严格 Modelica.Fluid 实现。
|
||||
|
||||
XML 解析依赖 `lxml` 执行本地 XSD 校验。安装或更新 Python 环境时使用:
|
||||
|
||||
|
||||
+158
-6
@@ -28,6 +28,7 @@ from app.system_xml import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.simulation.components.amesim.gases import AmesimGasRegistry
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.systems.network import SimulationNetwork
|
||||
|
||||
@@ -135,6 +136,7 @@ class ReactFlowSimulationConfig(BaseModel):
|
||||
|
||||
class ReactFlowProjectPayload(BaseModel):
|
||||
name: str = "untitled"
|
||||
mediumReferenceVersion: Literal[1] | None = None
|
||||
nodes: list[ReactFlowNodePayload] = Field(default_factory=list)
|
||||
edges: list[ReactFlowEdgePayload] = Field(default_factory=list)
|
||||
simulation: ReactFlowSimulationConfig = Field(default_factory=ReactFlowSimulationConfig)
|
||||
@@ -388,6 +390,15 @@ def simulate_reactflow_testmodel(payload: ReactFlowProjectPayload) -> dict[str,
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/reactflow/simulate-test-mql")
|
||||
def simulate_reactflow_test_mql(payload: ReactFlowProjectPayload) -> dict[str, object]:
|
||||
try:
|
||||
result = run_reactflow_test_mql(payload)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/reactflow/compile-model")
|
||||
def compile_reactflow_model(payload: ReactFlowProjectPayload) -> dict[str, object]:
|
||||
try:
|
||||
@@ -630,6 +641,11 @@ def run_system_xml_simulation(
|
||||
t_start=project.simulation.t_start,
|
||||
t_stop=project.simulation.t_stop,
|
||||
method=project.simulation.method,
|
||||
# The pressure-flow closure is solved to a scaled 1e-7
|
||||
# residual. Asking the outer adaptive integrator for 1e-6
|
||||
# relative accuracy makes its finite-difference Jacobian chase
|
||||
# algebraic solver noise after discontinuous signal events.
|
||||
rtol=1.0e-5,
|
||||
max_step=project.simulation.max_step,
|
||||
),
|
||||
sample_step=project.simulation.step,
|
||||
@@ -882,12 +898,15 @@ def _compile_xml_document_or_422(
|
||||
|
||||
|
||||
def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
|
||||
from app.simulation.registry import get_component_model_spec
|
||||
|
||||
system = ET.Element(
|
||||
"System",
|
||||
{
|
||||
"name": project.name,
|
||||
"schemaVersion": SYSTEM_XML_SCHEMA_VERSION,
|
||||
"unitSystem": SYSTEM_XML_UNIT_SYSTEM,
|
||||
"mediumReferenceVersion": "1",
|
||||
},
|
||||
)
|
||||
ET.SubElement(
|
||||
@@ -944,7 +963,20 @@ def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
|
||||
port_definition.positiveFlowDirection or "intoComponent"
|
||||
)
|
||||
ET.SubElement(component_node, "Port", port_attributes)
|
||||
for name, value in node.data.parameters.items():
|
||||
parameter_values = dict(node.data.parameters)
|
||||
try:
|
||||
component_spec = get_component_model_spec(node.data.modelType)
|
||||
except ValueError:
|
||||
component_spec = None
|
||||
if (
|
||||
component_spec is not None
|
||||
and component_spec.display.role == "amesimGasMediumDefinition"
|
||||
):
|
||||
for parameter in component_spec.parameters:
|
||||
if parameter.editor == "amesimGasPropertyModel":
|
||||
parameter_values.setdefault(parameter.name, parameter.default)
|
||||
|
||||
for name, value in parameter_values.items():
|
||||
ET.SubElement(
|
||||
component_node,
|
||||
"Parameter",
|
||||
@@ -1077,14 +1109,34 @@ def validate_compatible_ports(
|
||||
)
|
||||
|
||||
|
||||
def compile_reactflow_network(project: ReactFlowProjectPayload) -> "SimulationNetwork":
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
def compile_reactflow_network(
|
||||
project: ReactFlowProjectPayload,
|
||||
*,
|
||||
amesim_gas_registry: "AmesimGasRegistry | None" = None,
|
||||
) -> "SimulationNetwork":
|
||||
from app.simulation.components.amesim.gases import (
|
||||
AMESIM_BUILTIN_AIR_GAS_INDEX,
|
||||
default_amesim_gas_registry,
|
||||
)
|
||||
from app.simulation.components.amesim.media.properties import (
|
||||
AmesimGasMediumDefinitionComponent,
|
||||
)
|
||||
from app.simulation.registry import get_component_model_spec
|
||||
from app.simulation.systems.network import SimulationNetwork
|
||||
|
||||
medium = IdealGasMedium()
|
||||
network = SimulationNetwork(name=project.name)
|
||||
if amesim_gas_registry is None:
|
||||
amesim_gas_registry = default_amesim_gas_registry()
|
||||
else:
|
||||
amesim_gas_registry = amesim_gas_registry.copy()
|
||||
|
||||
network = SimulationNetwork(name=project.name)
|
||||
resolved_nodes = []
|
||||
specs_by_component_id = {}
|
||||
gas_indices_by_component_id = {}
|
||||
|
||||
# Phase 1: normalize every node and consume compile-time medium definitions.
|
||||
# A definition node represents one complete property-method instance and is
|
||||
# intentionally not added to the equation network.
|
||||
for node in project.nodes:
|
||||
spec = get_component_model_spec(node.data.modelType)
|
||||
parameter_values = {
|
||||
@@ -1098,7 +1150,73 @@ def compile_reactflow_network(project: ReactFlowProjectPayload) -> "SimulationNe
|
||||
+ ", ".join(sorted(unknown_parameters))
|
||||
+ "."
|
||||
)
|
||||
component = spec.create(node.id, medium, parameter_values)
|
||||
if issubclass(
|
||||
spec.component_class,
|
||||
AmesimGasMediumDefinitionComponent,
|
||||
):
|
||||
definition_component = spec.create(
|
||||
node.id,
|
||||
amesim_gas_registry.resolve(AMESIM_BUILTIN_AIR_GAS_INDEX),
|
||||
parameter_values,
|
||||
)
|
||||
assert isinstance(
|
||||
definition_component,
|
||||
AmesimGasMediumDefinitionComponent,
|
||||
)
|
||||
validate_component_port_interface(
|
||||
node,
|
||||
definition_component.port_definitions,
|
||||
)
|
||||
try:
|
||||
amesim_gas_registry.register(
|
||||
definition_component.gas_definition()
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"AMESim medium definition component '{node.id}' is invalid: "
|
||||
f"{exc}"
|
||||
) from exc
|
||||
continue
|
||||
|
||||
resolved_nodes.append((node, spec, parameter_values))
|
||||
specs_by_component_id[node.id] = spec
|
||||
gas_indices_by_component_id[node.id] = (
|
||||
parameter_values["gi"]
|
||||
if spec.library.id == "amesim" and "gi" in spec.parameter_by_name
|
||||
else None
|
||||
)
|
||||
|
||||
# Phase 2: resolve one gas property model for each pneumatic circuit before
|
||||
# any dynamic component initializes its thermodynamic state.
|
||||
pneumatic_connections = []
|
||||
for edge in project.edges:
|
||||
source_spec = specs_by_component_id.get(edge.source)
|
||||
target_spec = specs_by_component_id.get(edge.target)
|
||||
if source_spec is None or target_spec is None:
|
||||
continue
|
||||
source_ports = {port.name: port for port in source_spec.ports}
|
||||
target_ports = {port.name: port for port in target_spec.ports}
|
||||
source_port = source_ports.get(edge.sourceHandle or "")
|
||||
target_port = target_ports.get(edge.targetHandle or "")
|
||||
if (
|
||||
source_port is not None
|
||||
and target_port is not None
|
||||
and source_port.domain == "pneumatic"
|
||||
and target_port.domain == "pneumatic"
|
||||
):
|
||||
pneumatic_connections.append((edge.source, edge.target))
|
||||
|
||||
media_by_component_id = amesim_gas_registry.resolve_network_media(
|
||||
gas_indices_by_component_id,
|
||||
pneumatic_connections,
|
||||
)
|
||||
|
||||
for node, spec, parameter_values in resolved_nodes:
|
||||
component = spec.create(
|
||||
node.id,
|
||||
media_by_component_id[node.id],
|
||||
parameter_values,
|
||||
)
|
||||
validate_component_port_interface(node, component.port_definitions)
|
||||
network.add_component(component)
|
||||
|
||||
@@ -1249,6 +1367,40 @@ def run_reactflow_testmodel(project: ReactFlowProjectPayload) -> dict[str, objec
|
||||
}
|
||||
|
||||
|
||||
def run_reactflow_test_mql(project: ReactFlowProjectPayload) -> dict[str, object]:
|
||||
from app.simulation.examples.test_mql.run import run_test_mql
|
||||
from app.simulation.examples.test_mql.system import TestMqlRunConfig
|
||||
|
||||
run_config = TestMqlRunConfig(
|
||||
t_start=project.simulation.t_start,
|
||||
t_stop=project.simulation.t_stop,
|
||||
sample_step=project.simulation.step,
|
||||
)
|
||||
result = run_test_mql(run_config=run_config)
|
||||
series = {
|
||||
key: [float(value) for value in values]
|
||||
for key, values in result.result.series.items()
|
||||
}
|
||||
final = {key: values[-1] for key, values in series.items() if values}
|
||||
snapshot = result.system.snapshot()
|
||||
return {
|
||||
"success": True,
|
||||
"message": "test_mql fixed-topology simulation completed.",
|
||||
"model": {
|
||||
"name": snapshot.model_name,
|
||||
"componentCount": snapshot.component_count,
|
||||
"connectionCount": snapshot.connection_count,
|
||||
"continuousStateCount": snapshot.continuous_state_count,
|
||||
"discreteStateCount": snapshot.discrete_state_count,
|
||||
},
|
||||
"final": final,
|
||||
"series": series,
|
||||
"artifacts": {
|
||||
"summary": str(result.summary_path),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def first_node(
|
||||
nodes_by_type: dict[str, list[ReactFlowNodePayload]],
|
||||
model_type: str,
|
||||
|
||||
+37
-14
@@ -2,9 +2,9 @@
|
||||
|
||||
`app.simulation` 是 SystemSimulationApp 的仿真子包,用于承接模型定义、系统装配、数值求解和结果导出。
|
||||
|
||||
目标不是把 `.mo` 文件逐行翻译成 Python,而是建立一个可运行、可对比、可逐步逼近 `OpenModelica` 行为的 Python 仿真框架。
|
||||
目标不是逐行翻译源模型,而是建立可运行、可测试、可导出,并能与 OpenModelica 或 AMESim baseline 对比的 Python 仿真框架。
|
||||
|
||||
当前状态不是“只有骨架”,而是“`Testmodel` 已有一版可运行的 ODE 近似实现,并具备基础结果导出与对比能力”。
|
||||
当前包含两条模型线:`Testmodel` 已有可运行的 ODE 近似和 OpenModelica 对比能力;`test_mql` 已形成 132 状态气动机械总闭包,正在按 AMESim baseline 做数值校准。
|
||||
|
||||
## 当前目录
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
- `components/experimental/storage/`: 气瓶和贮箱等储能元件。
|
||||
- `components/experimental/flow/`: 对外注册的阻性管道和孔板等流动元件。
|
||||
- `components/experimental/junctions/`: 三通等连接节点。
|
||||
- `components/amesim/`: AMESim 气动、信号和机械组件原语。
|
||||
- `systems/`: 通用仿真网络与 XML 驱动系统装配。
|
||||
- `examples/testmodel/`: 固定 TestModel、专用闭合逻辑和运行入口。
|
||||
- `reporting/`: CSV、SVG、运行报告和 Modelica 对比结果导出。
|
||||
- `examples/testmodel/`: 固定 TestModel、专用闭合逻辑和基线运行入口。
|
||||
- `examples/test_mql/`: AMESim `test_mql` 的系统装配、校准原语、诊断和运行入口。
|
||||
- `reporting/`: CSV、SVG、运行报告、Modelica 对比结果、AMESim 结果读取和诊断报告导出。
|
||||
- `registry.py`: 从已启用库清单受控发现、校验和实例化组件。
|
||||
- `paths.py`: 项目、运行产物、基准和 Modelica 参考结果路径。
|
||||
|
||||
@@ -27,7 +29,8 @@
|
||||
FastAPI 的 `GET /api/components/catalog` 会把注册表转换成前端组件目录。ReactFlow
|
||||
启动时自动读取该接口;接口暂时不可用时使用内置的同结构兜底定义。
|
||||
|
||||
临时组件库的声明入口是 `components/experimental/library.py`。公开模型必须在
|
||||
临时组件库的声明入口是 `components/experimental/library.py`,AMESim 第一版
|
||||
公开临时库入口是 `components/amesim/library.py`。公开模型必须在
|
||||
模型类中声明 `MODEL_TYPE / MODEL_VERSION / PORTS / PARAMETERS /
|
||||
RESULT_VARIABLES / DISPLAY / create()`,再把类路径加入库清单。完整规范参见
|
||||
[`组件模型建模规范 v1`](../../docs/component-model-authoring-spec-v1.md)和
|
||||
@@ -35,17 +38,25 @@ RESULT_VARIABLES / DISPLAY / create()`,再把类路径加入库清单。完整
|
||||
|
||||
当前关键文件:
|
||||
|
||||
- `core/medium.py`: 理想气体近似介质 `IdealGasMedium`
|
||||
- `core/medium.py`: 温度相关的空气近似介质 `IdealGasMedium`
|
||||
- `core/medium.py`: 气体介质协议 `GasMedium` 与通用理想气体实现 `IdealGasMedium`
|
||||
- `components/amesim/media/`: AMESim 零端口介质物性定义元件;具体类型确定介质,`property_model` 下拉参数选择计算方法,当前提供空气理想气体和氦气 Peng-Robinson
|
||||
- `components/amesim/gases.py`: AMESim `gi` 介质物性实例注册表;`gi=0` 固定为空气(理想气体,内置默认),`gi=1..99` 引用画布中的显式介质定义
|
||||
- `core/peng_robinson.py`: `test_mql` 与公开氦气介质共用的 Peng-Robinson 状态方程
|
||||
- `systems/network.py`: `SimulationNetwork`,负责组件注册、连接拓扑和状态向量拼装
|
||||
- `solvers/solver.py`: `integrate_ode()`,优先走 `SciPy solve_ivp`,缺依赖时回退到内置 RK4,并支持 `t_start == t_stop` 的零时长返回
|
||||
- `examples/testmodel/dynamic_pipe.py`: TestModel 专用单阻容管道近似,入口压降 + 出口直连内容腔
|
||||
- `components/experimental/junctions/tee.py`: 三通的最小 stream 混合 helper
|
||||
- `examples/testmodel/system.py`: `Testmodel` 的系统装配壳与外部运行入口
|
||||
- `examples/testmodel/closure.py`: `Testmodel` 当前专用的闭合、初始化投影、分支求解与端口回写
|
||||
- `examples/test_mql/system.py`: `test_mql` 系统装配、132 状态总闭包和关键输出映射
|
||||
- `examples/test_mql/closure.py`: `test_mql` 气动网络 closure、snapshot、流量计算和端口写回
|
||||
- `examples/test_mql/primitives/`: 固定算例专用的 Peng-Robinson 氦气、管路和机械校准原语
|
||||
- `examples/test_mql/structural_network.py`: 固定算例专用的结构网络;不替代带端口契约校验的通用网络
|
||||
- `reporting/testmodel_outputs.py`: `Testmodel` 的 CSV/SVG/对比摘要导出
|
||||
- `examples/testmodel/run.py`: 基线运行与程序化执行入口
|
||||
- `tests/`: 当前组件契约、XML、通用系统和结果导出测试
|
||||
- `reporting/amesim_results.py`: AMESim 结果读取入口
|
||||
- `examples/test_mql/run_full_state_comparison.py`: `test_mql` 短时域 AMESim comparison 和诊断入口
|
||||
- `examples/test_mql/run.py`: `test_mql` 结构运行与程序化执行入口
|
||||
- `tests/`: 当前组件契约、XML、通用系统、AMESim 迁移和结果导出测试
|
||||
|
||||
## 当前阶段进度
|
||||
|
||||
@@ -272,14 +283,26 @@ print(result.used_modelica_reference)
|
||||
|
||||
当前四个主变量的最大误差为:
|
||||
|
||||
- `mytank.p`: `max_abs_error = 134.960857 Pa`, `max_rel_error = 0.006798%`
|
||||
- `mytank.p`: `max_abs_error = 134.960858 Pa`, `max_rel_error = 0.006798%`
|
||||
- `mytank.T`: `max_abs_error = 0.035507 K`, `max_rel_error = 0.009016%`
|
||||
- `mycylinder.p`: `max_abs_error = 1391.986349 Pa`, `max_rel_error = 0.009447%`
|
||||
- `mycylinder.T`: `max_abs_error = 0.009069 K`, `max_rel_error = 0.003870%`
|
||||
|
||||
这说明在当前基线工况下,Python 版主变量已经能较好贴近 OpenModelica 结果。
|
||||
|
||||
## 当前架构判断
|
||||
## AMESim test_mql 当前进度
|
||||
|
||||
`test_mql` 从 `AmesimModels/test_mql.ame` 迁移,并与旧 `testmodel` 保持独立。
|
||||
固定算例实现统一位于 `examples/test_mql/`,结果读取和比较能力位于
|
||||
`reporting/`;公开拖拽组件由 `components/amesim/library.py` 单独登记。
|
||||
|
||||
当前已形成 112 个气动状态和 20 个机械状态的总闭包、AMESim 原生结果读取、
|
||||
`Data_Path` 输出校验及短时域 comparison。这里不再复制易过期的数值进度;
|
||||
最新对比结果、运行命令、限制和下一步校准路径以
|
||||
[`AmesimModels/test_mql/README.md`](../../AmesimModels/test_mql/README.md)
|
||||
为唯一说明。当前仍不能宣称 Python 时域仿真与 AMESim 全局一致。
|
||||
|
||||
## Testmodel 当前架构判断
|
||||
|
||||
如果按“组件正确 -> 网络闭合 -> 积分可跑 -> 结果对齐 -> 去近似”来看,当前大致处于:
|
||||
|
||||
@@ -293,7 +316,7 @@ print(result.used_modelica_reference)
|
||||
|
||||
`Testmodel` 已有一版可运行、可导出、可对比的 Python 近似实现。
|
||||
|
||||
## 已知限制
|
||||
## Testmodel 已知限制
|
||||
|
||||
当前最主要的限制可以直接理解成下面几条:
|
||||
|
||||
@@ -319,7 +342,7 @@ print(result.used_modelica_reference)
|
||||
- 作为最终工程结论的唯一依据
|
||||
- 直接扩展到更复杂拓扑而不补通用连接器语义
|
||||
|
||||
## 文件级现状
|
||||
## Testmodel 文件级现状
|
||||
|
||||
按代码现状逐项看:
|
||||
|
||||
@@ -342,7 +365,7 @@ print(result.used_modelica_reference)
|
||||
- `tests/baselines/simulation/`: 是当前稳定基线,不应该随着日常运行频繁改动。
|
||||
- `app/data/simulation-runs/`: 是默认运行产物目录,不是手写源代码,也不应该提交。
|
||||
|
||||
## 当前主技术债
|
||||
## Testmodel 当前主技术债
|
||||
|
||||
目前最主要的技术债,可以直接理解成下面 4 件事:
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from __future__ import annotations
|
||||
|
||||
__all__: list[str] = []
|
||||
@@ -0,0 +1 @@
|
||||
"""AMESim pneumatic boundary components."""
|
||||
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class AmesimPnpl01(AlgebraicComponent):
|
||||
"""AMESim PNPL01 zero pneumatic flow source.
|
||||
|
||||
The component behaves as a sealed pneumatic boundary in the current acausal
|
||||
solver: it does not prescribe pressure, and only constrains its port mass
|
||||
flow to zero.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnpl01"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),)
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNPL01 零气动流边界",
|
||||
library_id="amesim",
|
||||
category_id="boundary",
|
||||
symbol="amesim_pnpl01",
|
||||
ports=(PortDisplaySpec("port_1", "left", order=10),),
|
||||
order=10,
|
||||
)
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({})
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPnpl01:
|
||||
return cls(name=name)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:zero_mass_flow",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_1.m_flow",),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow,
|
||||
),
|
||||
)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
if "port_1" in connected_h:
|
||||
self.port_1.h_outflow = connected_h["port_1"]
|
||||
@@ -0,0 +1 @@
|
||||
"""AMESim pneumatic flow components."""
|
||||
@@ -0,0 +1,700 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from math import isclose, sqrt
|
||||
|
||||
from app.simulation.components.amesim.gases import (
|
||||
AMESIM_GAS_INDEX_PARAMETER,
|
||||
normalize_amesim_gas_index,
|
||||
)
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import (
|
||||
ParameterDefinition,
|
||||
ResultVariableDefinition,
|
||||
)
|
||||
from app.simulation.core.medium import GasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class AmesimPnor001(AlgebraicComponent):
|
||||
"""AMESim PNOR001 constant-flow-coefficient pneumatic orifice.
|
||||
|
||||
This public component preserves the PNOR001 catalog/XML contract and uses a
|
||||
finite bidirectional compressible-orifice approximation. The Siemens
|
||||
`pn2rcqfix_` details remain a later calibration target.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnor001"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
AMESIM_GAS_INDEX_PARAMETER,
|
||||
ParameterDefinition(
|
||||
"cq",
|
||||
0.72,
|
||||
label="流量系数 Cq",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0e-10,
|
||||
maximum=1.0,
|
||||
description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"area",
|
||||
5.0e-6,
|
||||
label="孔口面积",
|
||||
quantity="area",
|
||||
unit="m2",
|
||||
minimum=0.0,
|
||||
maximum=1.0,
|
||||
description="选择 Cq/面积方式时用于流量计算的有效孔口面积。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"Cv",
|
||||
0.5,
|
||||
label="流量系数 Cv",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
description="选择 Cv 方式时使用的英制流量系数。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"Kv",
|
||||
0.4,
|
||||
label="流量系数 Kv",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
description="选择 Kv 方式时使用的公制流量系数。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"flowset",
|
||||
1.0,
|
||||
label="流量系数设置",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=3.0,
|
||||
description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。",
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition(
|
||||
"cm",
|
||||
label="质量流量参数",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=10,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"gasvel",
|
||||
label="缩流截面气体速度",
|
||||
quantity="velocity",
|
||||
unit="m/s",
|
||||
category="derived",
|
||||
order=20,
|
||||
),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNOR001 常系数气动孔口",
|
||||
library_id="amesim",
|
||||
category_id="flow",
|
||||
symbol="amesim_pnor001",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=10,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
*,
|
||||
cq: float = 0.72,
|
||||
area: float = 5.0e-6,
|
||||
Cv: float = 0.5,
|
||||
Kv: float = 0.4,
|
||||
gi: float = 1.0,
|
||||
flowset: float = 1.0,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"cq": cq,
|
||||
"area": area,
|
||||
"Cv": Cv,
|
||||
"Kv": Kv,
|
||||
"gi": gi,
|
||||
"flowset": flowset,
|
||||
}
|
||||
)
|
||||
self.medium = medium
|
||||
self.cq = float(cq)
|
||||
self.area = float(area)
|
||||
self.Cv = float(Cv)
|
||||
self.Kv = float(Kv)
|
||||
self.gi = normalize_amesim_gas_index(gi)
|
||||
self.flowset = self._integer_parameter("flowset", flowset)
|
||||
if self.flowset not in {1, 2, 3}:
|
||||
raise ValueError("PNOR001 flowset must be 1, 2, or 3.")
|
||||
|
||||
initial_h = medium.specific_enthalpy(medium.T_ref)
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
self.port_1.h_outflow = initial_h
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.port_2.h_outflow = initial_h
|
||||
|
||||
@staticmethod
|
||||
def _integer_parameter(name: str, value: float) -> int:
|
||||
rounded = round(value)
|
||||
if not isclose(value, rounded, rel_tol=0.0, abs_tol=1.0e-12):
|
||||
raise ValueError(f"PNOR001 parameter {name} must be an integer value.")
|
||||
return int(rounded)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPnor001:
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
cq=parameters["cq"],
|
||||
area=parameters["area"],
|
||||
Cv=parameters["Cv"],
|
||||
Kv=parameters["Kv"],
|
||||
gi=parameters["gi"],
|
||||
flowset=parameters["flowset"],
|
||||
)
|
||||
|
||||
@property
|
||||
def effective_cq(self) -> float:
|
||||
return self.cq if self.flowset == 1 else 0.72
|
||||
|
||||
@property
|
||||
def effective_area(self) -> float:
|
||||
if self.flowset == 1:
|
||||
return self.area
|
||||
if self.flowset == 2:
|
||||
return self._area_from_cv(self.Cv, self.effective_cq)
|
||||
return self._area_from_kv(self.Kv, self.effective_cq)
|
||||
|
||||
@staticmethod
|
||||
def _area_from_cv(Cv: float, cq: float) -> float:
|
||||
water_density = 999.0
|
||||
reference_flow_m3_s = Cv * 6.30901964e-5
|
||||
reference_dp_pa = 6894.75729
|
||||
return reference_flow_m3_s / (cq * sqrt(2.0 * reference_dp_pa / water_density))
|
||||
|
||||
@staticmethod
|
||||
def _area_from_kv(Kv: float, cq: float) -> float:
|
||||
water_density = 999.0
|
||||
reference_flow_m3_s = Kv / 3600.0
|
||||
reference_dp_pa = 100000.0
|
||||
return reference_flow_m3_s / (cq * sqrt(2.0 * reference_dp_pa / water_density))
|
||||
|
||||
def _upstream_temperature(self, port_name: str) -> float:
|
||||
port = self.get_port(port_name)
|
||||
if port.h_outflow > 0.0:
|
||||
return max(self.medium.temperature_from_enthalpy(port.h_outflow), 1.0)
|
||||
return self.medium.T_ref
|
||||
|
||||
def mass_flow(self, p_1: float, p_2: float) -> float:
|
||||
if p_1 == p_2 or self.effective_area == 0.0:
|
||||
return 0.0
|
||||
if p_1 > p_2:
|
||||
return self._one_way_mass_flow(
|
||||
upstream_pressure=p_1,
|
||||
downstream_pressure=p_2,
|
||||
upstream_temperature=self._upstream_temperature("port_1"),
|
||||
)
|
||||
return -self._one_way_mass_flow(
|
||||
upstream_pressure=p_2,
|
||||
downstream_pressure=p_1,
|
||||
upstream_temperature=self._upstream_temperature("port_2"),
|
||||
)
|
||||
|
||||
def _one_way_mass_flow(
|
||||
self,
|
||||
*,
|
||||
upstream_pressure: float,
|
||||
downstream_pressure: float,
|
||||
upstream_temperature: float,
|
||||
) -> float:
|
||||
p_up = max(upstream_pressure, 1.0)
|
||||
p_down = max(min(downstream_pressure, p_up), 0.0)
|
||||
T_up = max(upstream_temperature, 1.0)
|
||||
gamma = max(self.medium.gamma, 1.000001)
|
||||
pressure_ratio = max(p_down / p_up, 0.0)
|
||||
critical_ratio = (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
|
||||
if pressure_ratio <= critical_ratio:
|
||||
flow_factor = sqrt(gamma / (self.medium.R_gas * T_up)) * (
|
||||
2.0 / (gamma + 1.0)
|
||||
) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
|
||||
else:
|
||||
expansion = pressure_ratio ** (2.0 / gamma) - pressure_ratio ** (
|
||||
(gamma + 1.0) / gamma
|
||||
)
|
||||
flow_factor = sqrt(
|
||||
max(
|
||||
2.0
|
||||
* gamma
|
||||
* expansion
|
||||
/ (self.medium.R_gas * T_up * (gamma - 1.0)),
|
||||
0.0,
|
||||
)
|
||||
)
|
||||
return self.effective_cq * self.effective_area * p_up * flow_factor
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
p_1 = max(self.port_1.p, 1.0)
|
||||
p_2 = max(self.port_2.p, 1.0)
|
||||
m_flow = abs(self.mass_flow(self.port_1.p, self.port_2.p))
|
||||
upstream_pressure = max(p_1, p_2)
|
||||
upstream_temperature = self._upstream_temperature(
|
||||
"port_1" if p_1 >= p_2 else "port_2"
|
||||
)
|
||||
density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12)
|
||||
area = max(self.effective_area, 1.0e-18)
|
||||
return {
|
||||
"cm": m_flow / (self.effective_cq * area * upstream_pressure),
|
||||
"gasvel": m_flow / (density * area),
|
||||
}
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:mass_flow_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="sumToZero",
|
||||
variables=(
|
||||
f"{self.name}.port_1.m_flow",
|
||||
f"{self.name}.port_2.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow + self.port_2.m_flow,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:pressure_flow_relation",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(
|
||||
f"{self.name}.port_1.p",
|
||||
f"{self.name}.port_2.p",
|
||||
f"{self.name}.port_1.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_1.m_flow
|
||||
- self.mass_flow(self.port_1.p, self.port_2.p),
|
||||
),
|
||||
)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.port_1.h_outflow = connected_h["port_2"]
|
||||
self.port_2.h_outflow = connected_h["port_1"]
|
||||
|
||||
|
||||
class AmesimPnvo001FixedOpening(AlgebraicComponent):
|
||||
"""Fixed-opening public variant of AMESim PNVO001.
|
||||
|
||||
Full PNVO001 has a signal input port. The current public component library
|
||||
does not support signal simulation, so this model exposes the pneumatic
|
||||
ports and replaces the signal with a normalized `opening` parameter.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnvo001_fixed"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
AMESIM_GAS_INDEX_PARAMETER,
|
||||
ParameterDefinition(
|
||||
"cq",
|
||||
0.72,
|
||||
label="流量系数 Cq",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0e-10,
|
||||
maximum=1.0,
|
||||
description="孔口实际质量流量相对于理想可压缩流量的无量纲修正系数。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"area0",
|
||||
5.0e-6,
|
||||
label="最大孔口面积",
|
||||
quantity="area",
|
||||
unit="m2",
|
||||
minimum=0.0,
|
||||
maximum=1.0,
|
||||
description="阀门完全开启时的最大有效孔口面积。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"Cv",
|
||||
0.5,
|
||||
label="最大流量系数 Cv",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
description="选择 Cv 方式时使用的最大英制流量系数。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"Kv",
|
||||
0.4,
|
||||
label="最大流量系数 Kv",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
description="选择 Kv 方式时使用的最大公制流量系数。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"flowset",
|
||||
1.0,
|
||||
label="流量系数设置",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=3.0,
|
||||
description="流量参数方式:1 使用 Cq 和面积,2 使用 Cv,3 使用 Kv。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"opening",
|
||||
1.0,
|
||||
label="固定开度",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=0.0,
|
||||
maximum=1.0,
|
||||
description="固定的归一化阀门开度;0 表示关闭,1 表示完全开启。",
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition(
|
||||
"xv",
|
||||
label="有效开度",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=10,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"cm",
|
||||
label="质量流量参数",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
category="derived",
|
||||
order=20,
|
||||
),
|
||||
ResultVariableDefinition(
|
||||
"gasvel",
|
||||
label="缩流截面气体速度",
|
||||
quantity="velocity",
|
||||
unit="m/s",
|
||||
category="derived",
|
||||
order=30,
|
||||
),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNVO001 固定开度气动孔口",
|
||||
library_id="amesim",
|
||||
category_id="flow",
|
||||
symbol="amesim_pnvo001_fixed",
|
||||
ports=(
|
||||
PortDisplaySpec("port_2", "left", order=10),
|
||||
PortDisplaySpec("port_3", "right", order=20),
|
||||
),
|
||||
order=30,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
*,
|
||||
cq: float = 0.72,
|
||||
area0: float = 5.0e-6,
|
||||
Cv: float = 0.5,
|
||||
Kv: float = 0.4,
|
||||
gi: float = 1.0,
|
||||
flowset: float = 1.0,
|
||||
opening: float = 1.0,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"cq": cq,
|
||||
"area0": area0,
|
||||
"Cv": Cv,
|
||||
"Kv": Kv,
|
||||
"gi": gi,
|
||||
"flowset": flowset,
|
||||
"opening": opening,
|
||||
}
|
||||
)
|
||||
self.medium = medium
|
||||
self.cq = float(cq)
|
||||
self.area0 = float(area0)
|
||||
self.Cv = float(Cv)
|
||||
self.Kv = float(Kv)
|
||||
self.gi = normalize_amesim_gas_index(gi)
|
||||
self.flowset = self._integer_parameter("flowset", flowset)
|
||||
if self.flowset not in {1, 2, 3}:
|
||||
raise ValueError("PNVO001 fixed-opening flowset must be 1, 2, or 3.")
|
||||
self.opening = min(1.0, max(0.0, float(opening)))
|
||||
|
||||
initial_h = medium.specific_enthalpy(medium.T_ref)
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.port_2.h_outflow = initial_h
|
||||
self.port_3 = self.register_declared_port("port_3")
|
||||
self.port_3.h_outflow = initial_h
|
||||
|
||||
@staticmethod
|
||||
def _integer_parameter(name: str, value: float) -> int:
|
||||
rounded = round(value)
|
||||
if not isclose(value, rounded, rel_tol=0.0, abs_tol=1.0e-12):
|
||||
raise ValueError(f"PNVO001 fixed-opening parameter {name} must be an integer value.")
|
||||
return int(rounded)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPnvo001FixedOpening:
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
cq=parameters["cq"],
|
||||
area0=parameters["area0"],
|
||||
Cv=parameters["Cv"],
|
||||
Kv=parameters["Kv"],
|
||||
gi=parameters["gi"],
|
||||
flowset=parameters["flowset"],
|
||||
opening=parameters["opening"],
|
||||
)
|
||||
|
||||
@property
|
||||
def effective_cq(self) -> float:
|
||||
return self.cq if self.flowset == 1 else 0.72
|
||||
|
||||
@property
|
||||
def maximum_area(self) -> float:
|
||||
if self.flowset == 1:
|
||||
return self.area0
|
||||
if self.flowset == 2:
|
||||
return AmesimPnor001._area_from_cv(self.Cv, self.effective_cq)
|
||||
return AmesimPnor001._area_from_kv(self.Kv, self.effective_cq)
|
||||
|
||||
@property
|
||||
def effective_area(self) -> float:
|
||||
return self.opening * self.maximum_area
|
||||
|
||||
def _upstream_temperature(self, port_name: str) -> float:
|
||||
port = self.get_port(port_name)
|
||||
if port.h_outflow > 0.0:
|
||||
return max(self.medium.temperature_from_enthalpy(port.h_outflow), 1.0)
|
||||
return self.medium.T_ref
|
||||
|
||||
def mass_flow(self, p_2: float, p_3: float) -> float:
|
||||
if p_2 == p_3 or self.effective_area == 0.0:
|
||||
return 0.0
|
||||
if p_2 > p_3:
|
||||
return self._one_way_mass_flow(
|
||||
upstream_pressure=p_2,
|
||||
downstream_pressure=p_3,
|
||||
upstream_temperature=self._upstream_temperature("port_2"),
|
||||
)
|
||||
return -self._one_way_mass_flow(
|
||||
upstream_pressure=p_3,
|
||||
downstream_pressure=p_2,
|
||||
upstream_temperature=self._upstream_temperature("port_3"),
|
||||
)
|
||||
|
||||
def _one_way_mass_flow(
|
||||
self,
|
||||
*,
|
||||
upstream_pressure: float,
|
||||
downstream_pressure: float,
|
||||
upstream_temperature: float,
|
||||
) -> float:
|
||||
p_up = max(upstream_pressure, 1.0)
|
||||
p_down = max(min(downstream_pressure, p_up), 0.0)
|
||||
T_up = max(upstream_temperature, 1.0)
|
||||
gamma = max(self.medium.gamma, 1.000001)
|
||||
pressure_ratio = max(p_down / p_up, 0.0)
|
||||
critical_ratio = (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
|
||||
if pressure_ratio <= critical_ratio:
|
||||
flow_factor = sqrt(gamma / (self.medium.R_gas * T_up)) * (
|
||||
2.0 / (gamma + 1.0)
|
||||
) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
|
||||
else:
|
||||
expansion = pressure_ratio ** (2.0 / gamma) - pressure_ratio ** (
|
||||
(gamma + 1.0) / gamma
|
||||
)
|
||||
flow_factor = sqrt(
|
||||
max(
|
||||
2.0
|
||||
* gamma
|
||||
* expansion
|
||||
/ (self.medium.R_gas * T_up * (gamma - 1.0)),
|
||||
0.0,
|
||||
)
|
||||
)
|
||||
return self.effective_cq * self.effective_area * p_up * flow_factor
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
p_2 = max(self.port_2.p, 1.0)
|
||||
p_3 = max(self.port_3.p, 1.0)
|
||||
m_flow = abs(self.mass_flow(self.port_2.p, self.port_3.p))
|
||||
upstream_pressure = max(p_2, p_3)
|
||||
upstream_temperature = self._upstream_temperature(
|
||||
"port_2" if p_2 >= p_3 else "port_3"
|
||||
)
|
||||
density = max(self.medium.density(upstream_pressure, upstream_temperature), 1.0e-12)
|
||||
area = max(self.effective_area, 1.0e-18)
|
||||
return {
|
||||
"xv": self.opening,
|
||||
"cm": m_flow / (self.effective_cq * area * upstream_pressure),
|
||||
"gasvel": m_flow / (density * area),
|
||||
}
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:mass_flow_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="sumToZero",
|
||||
variables=(
|
||||
f"{self.name}.port_2.m_flow",
|
||||
f"{self.name}.port_3.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_2.m_flow + self.port_3.m_flow,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:pressure_flow_relation",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(
|
||||
f"{self.name}.port_2.p",
|
||||
f"{self.name}.port_3.p",
|
||||
f"{self.name}.port_2.m_flow",
|
||||
),
|
||||
role="flow",
|
||||
value=self.port_2.m_flow
|
||||
- self.mass_flow(self.port_2.p, self.port_3.p),
|
||||
),
|
||||
)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
self.port_2.h_outflow = connected_h["port_3"]
|
||||
self.port_3.h_outflow = connected_h["port_2"]
|
||||
|
||||
|
||||
class AmesimPnvo001SignalOpening(AmesimPnvo001FixedOpening):
|
||||
"""AMESim PNVO001 signal-controlled pneumatic orifice."""
|
||||
|
||||
MODEL_TYPE = "amesim_pnvo001"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.signal("res", nominal_role="input"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
AMESIM_GAS_INDEX_PARAMETER,
|
||||
ParameterDefinition("cq", 0.72, label="流量系数 Cq", quantity="dimensionless", unit="", minimum=1.0e-10, maximum=1.0),
|
||||
ParameterDefinition("area0", 5.0e-6, label="最大孔口面积", quantity="area", unit="m2", minimum=0.0, maximum=1.0),
|
||||
ParameterDefinition("Cv", 0.5, label="最大流量系数 Cv", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition("Kv", 0.4, label="最大流量系数 Kv", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition("flowset", 1.0, label="流量系数设置", quantity="dimensionless", unit="", minimum=1.0, maximum=3.0),
|
||||
ParameterDefinition("opening0", 1.0, label="初始开度", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
|
||||
)
|
||||
RESULT_VARIABLES = AmesimPnvo001FixedOpening.RESULT_VARIABLES
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNVO001 信号开度气动孔口",
|
||||
library_id="amesim",
|
||||
category_id="flow",
|
||||
symbol="amesim_pnvo001",
|
||||
ports=(
|
||||
PortDisplaySpec("res", "left", order=5),
|
||||
PortDisplaySpec("port_2", "left", order=10),
|
||||
PortDisplaySpec("port_3", "right", order=20),
|
||||
),
|
||||
order=35,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
*,
|
||||
cq: float = 0.72,
|
||||
area0: float = 5.0e-6,
|
||||
Cv: float = 0.5,
|
||||
Kv: float = 0.4,
|
||||
gi: float = 1.0,
|
||||
flowset: float = 1.0,
|
||||
opening0: float = 1.0,
|
||||
) -> None:
|
||||
AlgebraicComponent.__init__(self, name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"cq": cq,
|
||||
"area0": area0,
|
||||
"Cv": Cv,
|
||||
"Kv": Kv,
|
||||
"gi": gi,
|
||||
"flowset": flowset,
|
||||
"opening0": opening0,
|
||||
}
|
||||
)
|
||||
self.medium = medium
|
||||
self.cq = float(cq)
|
||||
self.area0 = float(area0)
|
||||
self.Cv = float(Cv)
|
||||
self.Kv = float(Kv)
|
||||
self.gi = normalize_amesim_gas_index(gi)
|
||||
self.flowset = self._integer_parameter("flowset", flowset)
|
||||
if self.flowset not in {1, 2, 3}:
|
||||
raise ValueError("PNVO001 signal-opening flowset must be 1, 2, or 3.")
|
||||
self.opening0 = min(1.0, max(0.0, float(opening0)))
|
||||
self.res = self.register_declared_port("res")
|
||||
self.res.signal = self.opening0
|
||||
initial_h = medium.specific_enthalpy(medium.T_ref)
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.port_2.h_outflow = initial_h
|
||||
self.port_3 = self.register_declared_port("port_3")
|
||||
self.port_3.h_outflow = initial_h
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimPnvo001SignalOpening":
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
@property
|
||||
def opening(self) -> float:
|
||||
return min(1.0, max(0.0, self.res.signal))
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,264 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from math import isclose, isfinite
|
||||
from types import MappingProxyType
|
||||
|
||||
from app.simulation.core.metadata import ParameterDefinition
|
||||
from app.simulation.core.medium import GasMedium
|
||||
|
||||
|
||||
AMESIM_BUILTIN_AIR_GAS_INDEX = 0
|
||||
AMESIM_DEFAULT_GAS_INDEX = AMESIM_BUILTIN_AIR_GAS_INDEX
|
||||
AMESIM_MIN_GAS_INDEX = AMESIM_BUILTIN_AIR_GAS_INDEX
|
||||
AMESIM_MIN_DEFINED_GAS_INDEX = 1
|
||||
AMESIM_MAX_GAS_INDEX = 99
|
||||
|
||||
AMESIM_GAS_INDEX_PARAMETER = ParameterDefinition(
|
||||
"gi",
|
||||
float(AMESIM_DEFAULT_GAS_INDEX),
|
||||
label="介质物性模型(gi)",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=float(AMESIM_MIN_GAS_INDEX),
|
||||
maximum=float(AMESIM_MAX_GAS_INDEX),
|
||||
editor="amesimGasReference",
|
||||
description=(
|
||||
"选择本元件使用的气体介质定义索引;0 表示内置空气,"
|
||||
"1–99 引用画布中的介质定义组件。"
|
||||
),
|
||||
)
|
||||
|
||||
AMESIM_GAS_DEFINITION_INDEX_PARAMETER = ParameterDefinition(
|
||||
"gi",
|
||||
float(AMESIM_MIN_DEFINED_GAS_INDEX),
|
||||
label="介质定义索引(gi)",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=float(AMESIM_MIN_DEFINED_GAS_INDEX),
|
||||
maximum=float(AMESIM_MAX_GAS_INDEX),
|
||||
description=(
|
||||
"介质定义在当前模型中的唯一索引;由画布自动分配,"
|
||||
"0 保留给内置空气。"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def normalize_amesim_gas_index(value: float | int) -> int:
|
||||
"""Validate an AMESim gas reference.
|
||||
|
||||
Index 0 is reserved for the built-in ideal-gas air profile. Positive
|
||||
indices refer to medium-definition components placed in the project.
|
||||
"""
|
||||
|
||||
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
||||
raise ValueError("AMESim gas type index gi must be a number.")
|
||||
numeric = float(value)
|
||||
if not isfinite(numeric):
|
||||
raise ValueError("AMESim gas type index gi must be finite.")
|
||||
rounded = round(numeric)
|
||||
if not isclose(numeric, rounded, rel_tol=0.0, abs_tol=1.0e-12):
|
||||
raise ValueError("AMESim gas type index gi must be an integer value.")
|
||||
index = int(rounded)
|
||||
if not AMESIM_MIN_GAS_INDEX <= index <= AMESIM_MAX_GAS_INDEX:
|
||||
raise ValueError(
|
||||
"AMESim gas type index gi must be between "
|
||||
f"{AMESIM_MIN_GAS_INDEX} and {AMESIM_MAX_GAS_INDEX}."
|
||||
)
|
||||
return index
|
||||
|
||||
|
||||
def normalize_amesim_defined_gas_index(value: float | int) -> int:
|
||||
"""Validate a positive index owned by a project medium definition."""
|
||||
|
||||
index = normalize_amesim_gas_index(value)
|
||||
if index < AMESIM_MIN_DEFINED_GAS_INDEX:
|
||||
raise ValueError(
|
||||
"AMESim medium definition index gi must be between "
|
||||
f"{AMESIM_MIN_DEFINED_GAS_INDEX} and {AMESIM_MAX_GAS_INDEX}; "
|
||||
"gi=0 is reserved for built-in ideal-gas air."
|
||||
)
|
||||
return index
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimGasDefinition:
|
||||
"""One AMESim PNGD-style gas-definition slot.
|
||||
|
||||
``fluid_type`` and ``eos_type`` are intentionally optional today. They
|
||||
reserve the metadata needed to map a future PNGD00 helium definition while
|
||||
the executable behavior is supplied by ``medium``.
|
||||
"""
|
||||
|
||||
gi: int
|
||||
label: str
|
||||
medium: GasMedium
|
||||
fluid_type: int | None = None
|
||||
eos_type: int | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
normalized = normalize_amesim_gas_index(self.gi)
|
||||
object.__setattr__(self, "gi", normalized)
|
||||
if not self.label.strip():
|
||||
raise ValueError("AMESim gas definition label must not be empty.")
|
||||
|
||||
|
||||
class AmesimGasRegistry:
|
||||
"""Resolve AMESim component ``gi`` references to thermodynamic media."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
definitions: Iterable[AmesimGasDefinition] = (),
|
||||
*,
|
||||
default_gi: int = AMESIM_DEFAULT_GAS_INDEX,
|
||||
) -> None:
|
||||
from app.simulation.components.amesim.media.mediums import (
|
||||
AmesimIdealAirMedium,
|
||||
)
|
||||
|
||||
self.default_gi = normalize_amesim_gas_index(default_gi)
|
||||
self._definitions: dict[int, AmesimGasDefinition] = {
|
||||
AMESIM_BUILTIN_AIR_GAS_INDEX: AmesimGasDefinition(
|
||||
gi=AMESIM_BUILTIN_AIR_GAS_INDEX,
|
||||
label="空气(理想气体,内置默认)",
|
||||
medium=AmesimIdealAirMedium(),
|
||||
)
|
||||
}
|
||||
for definition in definitions:
|
||||
self.register(definition)
|
||||
|
||||
@property
|
||||
def definitions(self) -> Mapping[int, AmesimGasDefinition]:
|
||||
return MappingProxyType(self._definitions)
|
||||
|
||||
def register(self, definition: AmesimGasDefinition) -> None:
|
||||
if not isinstance(definition, AmesimGasDefinition):
|
||||
raise TypeError("AMESim gas registry entries must use AmesimGasDefinition.")
|
||||
if definition.gi == AMESIM_BUILTIN_AIR_GAS_INDEX:
|
||||
raise ValueError(
|
||||
"AMESim gas type index gi=0 is reserved for built-in "
|
||||
"ideal-gas air and cannot be replaced."
|
||||
)
|
||||
if definition.gi in self._definitions:
|
||||
raise ValueError(
|
||||
f"AMESim gas type index gi={definition.gi} is already defined."
|
||||
)
|
||||
self._definitions[definition.gi] = definition
|
||||
|
||||
def copy(self) -> AmesimGasRegistry:
|
||||
"""Return an independent registry for one project compilation."""
|
||||
|
||||
copied = AmesimGasRegistry(
|
||||
(
|
||||
definition
|
||||
for index, definition in self._definitions.items()
|
||||
if index != AMESIM_BUILTIN_AIR_GAS_INDEX
|
||||
),
|
||||
default_gi=self.default_gi,
|
||||
)
|
||||
copied._definitions[AMESIM_BUILTIN_AIR_GAS_INDEX] = self._definitions[
|
||||
AMESIM_BUILTIN_AIR_GAS_INDEX
|
||||
]
|
||||
return copied
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
gi: float | int,
|
||||
*,
|
||||
component_name: str | None = None,
|
||||
) -> GasMedium:
|
||||
index = normalize_amesim_gas_index(gi)
|
||||
try:
|
||||
return self._definitions[index].medium
|
||||
except KeyError as exc:
|
||||
owner = f" for component '{component_name}'" if component_name else ""
|
||||
available = ", ".join(str(index) for index in sorted(self._definitions))
|
||||
available_message = available or "none"
|
||||
raise ValueError(
|
||||
f"AMESim gas type index gi={index}{owner} is not defined. "
|
||||
"Register a PNGD-style gas definition before using this index. "
|
||||
f"Available indices: {available_message}."
|
||||
) from exc
|
||||
|
||||
@property
|
||||
def default_medium(self) -> GasMedium:
|
||||
return self.resolve(self.default_gi)
|
||||
|
||||
def resolve_network_media(
|
||||
self,
|
||||
component_gas_indices: Mapping[str, float | int | None],
|
||||
pneumatic_connections: Iterable[tuple[str, str]],
|
||||
) -> dict[str, GasMedium]:
|
||||
"""Assign one medium to every connected pneumatic circuit.
|
||||
|
||||
Components without ``gi`` inherit the explicit index used by their
|
||||
circuit. Conflicting indices inside one circuit are rejected instead
|
||||
of silently mixing different gases.
|
||||
"""
|
||||
|
||||
parents = {component_id: component_id for component_id in component_gas_indices}
|
||||
|
||||
def find(component_id: str) -> str:
|
||||
parent = parents[component_id]
|
||||
while parent != parents[parent]:
|
||||
parent = parents[parent]
|
||||
while component_id != parent:
|
||||
next_component = parents[component_id]
|
||||
parents[component_id] = parent
|
||||
component_id = next_component
|
||||
return parent
|
||||
|
||||
def union(left: str, right: str) -> None:
|
||||
left_root = find(left)
|
||||
right_root = find(right)
|
||||
if left_root != right_root:
|
||||
parents[right_root] = left_root
|
||||
|
||||
for source, target in pneumatic_connections:
|
||||
if source in parents and target in parents:
|
||||
union(source, target)
|
||||
|
||||
members_by_root: dict[str, list[str]] = {}
|
||||
for component_id in component_gas_indices:
|
||||
members_by_root.setdefault(find(component_id), []).append(component_id)
|
||||
|
||||
media: dict[str, GasMedium] = {}
|
||||
for members in members_by_root.values():
|
||||
indexed_components: dict[int, list[str]] = {}
|
||||
for component_id in members:
|
||||
raw_index = component_gas_indices[component_id]
|
||||
if raw_index is None:
|
||||
continue
|
||||
index = normalize_amesim_gas_index(raw_index)
|
||||
indexed_components.setdefault(index, []).append(component_id)
|
||||
|
||||
if len(indexed_components) > 1:
|
||||
details = ", ".join(
|
||||
f"gi={index} ({', '.join(sorted(component_ids))})"
|
||||
for index, component_ids in sorted(indexed_components.items())
|
||||
)
|
||||
raise ValueError(
|
||||
"Connected pneumatic circuit contains conflicting AMESim "
|
||||
f"gas definitions: {details}."
|
||||
)
|
||||
|
||||
index = (
|
||||
next(iter(indexed_components))
|
||||
if indexed_components
|
||||
else self.default_gi
|
||||
)
|
||||
indexed_members = indexed_components.get(index, members)
|
||||
medium = self.resolve(
|
||||
index,
|
||||
component_name=", ".join(sorted(indexed_members)),
|
||||
)
|
||||
for component_id in members:
|
||||
media[component_id] = medium
|
||||
return media
|
||||
|
||||
|
||||
def default_amesim_gas_registry() -> AmesimGasRegistry:
|
||||
"""Create a registry containing only built-in gi=0 ideal-gas air."""
|
||||
|
||||
return AmesimGasRegistry()
|
||||
@@ -0,0 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.simulation.components.amesim.junctions.nodes import AmesimP4Node2, AmesimPn3Node2
|
||||
|
||||
__all__ = ["AmesimP4Node2", "AmesimPn3Node2"]
|
||||
@@ -0,0 +1,149 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition, PortState
|
||||
|
||||
|
||||
class _AmesimPneumaticNode(AlgebraicComponent):
|
||||
"""Shared implementation for AMESim pneumatic junction submodels."""
|
||||
|
||||
REFERENCE_PORT = "port_2"
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({})
|
||||
for definition in self.PORTS:
|
||||
setattr(self, definition.name, self.register_declared_port(definition.name))
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
reference = self.get_port(self.REFERENCE_PORT)
|
||||
residuals: list[EquationResidual] = []
|
||||
for definition in self.PORTS:
|
||||
if definition.name == self.REFERENCE_PORT:
|
||||
continue
|
||||
port = self.get_port(definition.name)
|
||||
residuals.append(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{definition.name}_pressure_reference",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="equal",
|
||||
variables=(
|
||||
f"{self.name}.{definition.name}.p",
|
||||
f"{self.name}.{self.REFERENCE_PORT}.p",
|
||||
),
|
||||
role="effort",
|
||||
value=port.p - reference.p,
|
||||
)
|
||||
)
|
||||
residuals.append(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:mass_flow_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="sumToZero",
|
||||
variables=tuple(
|
||||
f"{self.name}.{definition.name}.m_flow"
|
||||
for definition in self.PORTS
|
||||
),
|
||||
role="flow",
|
||||
value=sum(self.get_port(definition.name).m_flow for definition in self.PORTS),
|
||||
)
|
||||
)
|
||||
return tuple(residuals)
|
||||
|
||||
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||
incoming = [
|
||||
(port.m_flow, connected_h[name])
|
||||
for name, port in self.ports.items()
|
||||
if port.m_flow > 1e-12
|
||||
]
|
||||
total_flow = sum(m_flow for m_flow, _ in incoming)
|
||||
if total_flow > 1e-12:
|
||||
mixed_h = sum(m_flow * h for m_flow, h in incoming) / total_flow
|
||||
else:
|
||||
mixed_h = connected_h.get(
|
||||
self.REFERENCE_PORT,
|
||||
sum(connected_h.values()) / len(connected_h) if connected_h else 0.0,
|
||||
)
|
||||
for port in self.ports.values():
|
||||
port.h_outflow = mixed_h
|
||||
|
||||
|
||||
class AmesimPn3Node2(_AmesimPneumaticNode):
|
||||
"""AMESim PN3NODE2 pneumatic three-port junction."""
|
||||
|
||||
MODEL_TYPE = "amesim_pn3node2"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PN3NODE2 三端气动节点",
|
||||
library_id="amesim",
|
||||
category_id="junctions",
|
||||
symbol="amesim_pn3node2",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
PortDisplaySpec("port_3", "right", order=30),
|
||||
),
|
||||
order=10,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPn3Node2:
|
||||
return cls(name=name)
|
||||
|
||||
|
||||
class AmesimP4Node2(_AmesimPneumaticNode):
|
||||
"""AMESim P4NODE2 pneumatic four-port junction."""
|
||||
|
||||
MODEL_TYPE = "amesim_p4node2"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_4", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="P4NODE2 四端气动节点",
|
||||
library_id="amesim",
|
||||
category_id="junctions",
|
||||
symbol="amesim_p4node2",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
PortDisplaySpec("port_3", "right", order=30),
|
||||
PortDisplaySpec("port_4", "right", order=40),
|
||||
),
|
||||
order=20,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimP4Node2:
|
||||
return cls(name=name)
|
||||
@@ -0,0 +1,48 @@
|
||||
"""AMESim-compatible public component library."""
|
||||
|
||||
from app.simulation.core.catalog import (
|
||||
ComponentCategorySpec,
|
||||
ComponentLibrarySpec,
|
||||
)
|
||||
|
||||
|
||||
LIBRARY = ComponentLibrarySpec(
|
||||
id="amesim",
|
||||
label="AMESim 组件库",
|
||||
version="0.2.0",
|
||||
source_package="app.simulation.components.amesim",
|
||||
temporary=True,
|
||||
order=200,
|
||||
categories=(
|
||||
ComponentCategorySpec(id="media", label="介质物性", order=5),
|
||||
ComponentCategorySpec(id="storage", label="储能元件", order=10),
|
||||
ComponentCategorySpec(id="flow", label="流动元件", order=20),
|
||||
ComponentCategorySpec(id="junctions", label="连接元件", order=30),
|
||||
ComponentCategorySpec(id="boundary", label="边界元件", order=40),
|
||||
ComponentCategorySpec(id="signals", label="信号元件", order=50),
|
||||
ComponentCategorySpec(id="mechanical", label="机械元件", order=60),
|
||||
),
|
||||
models=(
|
||||
"app.simulation.components.amesim.media.properties:AmesimIdealAirMediumDefinition",
|
||||
"app.simulation.components.amesim.media.properties:AmesimHeliumMediumDefinition",
|
||||
"app.simulation.components.amesim.boundary.sources:AmesimPnpl01",
|
||||
"app.simulation.components.amesim.signals.sources:AmesimStep0",
|
||||
"app.simulation.components.amesim.signals.sources:AmesimUd00",
|
||||
"app.simulation.components.amesim.mechanical.translational:AmesimF000",
|
||||
"app.simulation.components.amesim.mechanical.translational:AmesimForc",
|
||||
"app.simulation.components.amesim.mechanical.translational:AmesimMecmas21",
|
||||
"app.simulation.components.amesim.mechanical.translational:AmesimLstp00a",
|
||||
"app.simulation.components.amesim.mechanical.translational:AmesimLmechn1",
|
||||
"app.simulation.components.amesim.storage.chambers:AmesimPnch023",
|
||||
"app.simulation.components.amesim.storage.chambers:AmesimPnch012",
|
||||
"app.simulation.components.amesim.flow.orifices:AmesimPnor001",
|
||||
"app.simulation.components.amesim.flow.orifices:AmesimPnvo001FixedOpening",
|
||||
"app.simulation.components.amesim.flow.orifices:AmesimPnvo001SignalOpening",
|
||||
"app.simulation.components.amesim.flow.pipes:AmesimPnl00r",
|
||||
"app.simulation.components.amesim.flow.pipes:AmesimPnl0001",
|
||||
"app.simulation.components.amesim.flow.pipes:AmesimPnl0002",
|
||||
"app.simulation.components.amesim.flow.pipes:AmesimPnl0003",
|
||||
"app.simulation.components.amesim.junctions.nodes:AmesimPn3Node2",
|
||||
"app.simulation.components.amesim.junctions.nodes:AmesimP4Node2",
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""AMESim mechanical components."""
|
||||
@@ -0,0 +1,506 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent, DynamicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class AmesimF000(AlgebraicComponent):
|
||||
"""AMESim F000 zero force source."""
|
||||
|
||||
MODEL_TYPE = "amesim_f000"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (PortDefinition.mechanical_translational("port_1"),)
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="F000 零力源",
|
||||
library_id="amesim",
|
||||
category_id="mechanical",
|
||||
symbol="amesim_f000",
|
||||
ports=(PortDisplaySpec("port_1", "right", order=10),),
|
||||
order=10,
|
||||
)
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({})
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimF000":
|
||||
return cls(name=name)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:zero_force",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_1.f",),
|
||||
role="flow",
|
||||
value=self.port_1.f,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AmesimForc(AlgebraicComponent):
|
||||
"""AMESim FORC signal-to-force converter."""
|
||||
|
||||
MODEL_TYPE = "amesim_forc"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.signal("res", nominal_role="input"),
|
||||
PortDefinition.mechanical_translational("port_2"),
|
||||
)
|
||||
PARAMETERS = ()
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("force", "输出力", "force", "N", "signal", 10),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="FORC 信号转力",
|
||||
library_id="amesim",
|
||||
category_id="mechanical",
|
||||
symbol="amesim_forc",
|
||||
ports=(
|
||||
PortDisplaySpec("res", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=20,
|
||||
)
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({})
|
||||
self.res = self.register_declared_port("res")
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimForc":
|
||||
return cls(name=name)
|
||||
|
||||
@property
|
||||
def output_force(self) -> float:
|
||||
return float(self.res.signal)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:signal_force",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_2.f", f"{self.name}.res.signal"),
|
||||
role="flow",
|
||||
value=self.port_2.f + self.output_force,
|
||||
),
|
||||
)
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {"force": self.output_force}
|
||||
|
||||
|
||||
class AmesimMecmas21(DynamicComponent):
|
||||
"""AMESim MECMAS21 first public one-dimensional translational mass."""
|
||||
|
||||
MODEL_TYPE = "amesim_mecmas21"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.mechanical_translational("port_1"),
|
||||
PortDefinition.mechanical_translational("port_2"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition("mass", 1.0, label="质量", quantity="mass", unit="kg", minimum=0.0, minimum_exclusive=True),
|
||||
ParameterDefinition("fstick", 0.0, label="静摩擦力", quantity="force", unit="N", minimum=0.0),
|
||||
ParameterDefinition("fcoul", 0.0, label="库仑摩擦力", quantity="force", unit="N", minimum=0.0),
|
||||
ParameterDefinition("rvisc", 0.0, label="黏性摩擦系数", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
|
||||
ParameterDefinition("wind", 0.0, label="风阻系数", quantity="windage", unit="N/(m/s)^2", minimum=0.0),
|
||||
ParameterDefinition("dvel", 1.0e-6, label="粘滞速度阈值", quantity="velocity", unit="m/s", minimum=0.0),
|
||||
ParameterDefinition("restdvel", 1.0e-6, label="恢复速度阈值", quantity="velocity", unit="m/s", minimum=0.0),
|
||||
ParameterDefinition("restcoeff", 0.65, label="恢复系数", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
|
||||
ParameterDefinition("astrib", 1.0e-3, label="Stribeck 常数", quantity="velocity", unit="m/s", minimum=0.0),
|
||||
ParameterDefinition("xmin", -1.0, label="下位移限位", quantity="length", unit="m"),
|
||||
ParameterDefinition("Kbmin", 1.0e9, label="下限位刚度", quantity="translational_stiffness", unit="N/m", minimum=0.0),
|
||||
ParameterDefinition("Dbmin", 1.0e4, label="下限位阻尼", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
|
||||
ParameterDefinition("Pdmin", 1.0e-4, label="下限位满阻尼穿透", quantity="length", unit="m", minimum=0.0),
|
||||
ParameterDefinition("xmax", 0.8, label="上位移限位", quantity="length", unit="m"),
|
||||
ParameterDefinition("Kbmax", 1.0e9, label="上限位刚度", quantity="translational_stiffness", unit="N/m", minimum=0.0),
|
||||
ParameterDefinition("Dbmax", 1.0e4, label="上限位阻尼", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
|
||||
ParameterDefinition("Pdmax", 1.0e-4, label="上限位满阻尼穿透", quantity="length", unit="m", minimum=0.0),
|
||||
ParameterDefinition("theta", 0.0, label="倾角", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("useFriction", 1.0, label="启用摩擦", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
|
||||
ParameterDefinition("stoptype", 4.0, label="限位类型", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition("discContactOption", 1.0, label="接触选项", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition("strib", 1.0, label="Stribeck 选项", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
|
||||
ParameterDefinition("frictionType", 1.0, label="摩擦类型", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition("v0", 0.0, label="初始速度", quantity="velocity", unit="m/s"),
|
||||
ParameterDefinition("x0", 0.0, label="初始位移", quantity="length", unit="m"),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("a", "加速度", "acceleration", "m/s2", "state", 10),
|
||||
ResultVariableDefinition("v", "速度", "velocity", "m/s", "state", 20),
|
||||
ResultVariableDefinition("x", "位移", "length", "m", "state", 30),
|
||||
ResultVariableDefinition("Fvisc", "黏性摩擦力", "force", "N", "derived", 40),
|
||||
ResultVariableDefinition("Ffric", "干摩擦力", "force", "N", "derived", 50),
|
||||
ResultVariableDefinition("Fmin", "下限位力", "force", "N", "derived", 60),
|
||||
ResultVariableDefinition("Fmax", "上限位力", "force", "N", "derived", 70),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="MECMAS21 一维质量",
|
||||
library_id="amesim",
|
||||
category_id="mechanical",
|
||||
symbol="amesim_mecmas21",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=30,
|
||||
)
|
||||
state_size = 2
|
||||
|
||||
def __init__(self, name: str, medium: IdealGasMedium, **parameters: float) -> None:
|
||||
super().__init__(name=name)
|
||||
resolved = {definition.name: parameters.get(definition.name, definition.default) for definition in self.PARAMETERS}
|
||||
self.set_parameter_values(resolved)
|
||||
for name, value in resolved.items():
|
||||
setattr(self, name, float(value))
|
||||
self.use_friction = bool(int(self.useFriction))
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.v = float(self.v0)
|
||||
self.x = float(self.x0)
|
||||
self.refresh_thermodynamic_ports()
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimMecmas21":
|
||||
for integer_name in ("useFriction", "stoptype", "discContactOption", "strib", "frictionType"):
|
||||
if not float(parameters[integer_name]).is_integer():
|
||||
raise ValueError(f"MECMAS21 {integer_name} must be an integer.")
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return [self.v, self.x]
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
if len(values) != 2:
|
||||
raise ValueError("MECMAS21 state vector requires [v, x].")
|
||||
self.v = float(values[0])
|
||||
self.x = float(values[1])
|
||||
self.refresh_thermodynamic_ports()
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> None:
|
||||
for port in (self.port_1, self.port_2):
|
||||
port.x = self.x
|
||||
port.v = self.v
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
self._state_residual("port_1", "x", self.port_1.x - self.x),
|
||||
self._state_residual("port_1", "v", self.port_1.v - self.v),
|
||||
self._state_residual("port_2", "x", self.port_2.x - self.x),
|
||||
self._state_residual("port_2", "v", self.port_2.v - self.v),
|
||||
)
|
||||
|
||||
def _state_residual(self, port_name: str, variable: str, value: float) -> EquationResidual:
|
||||
return EquationResidual(
|
||||
id=f"{self.name}:{port_name}_{variable}_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.{port_name}.{variable}",),
|
||||
role="effort",
|
||||
value=value,
|
||||
)
|
||||
|
||||
def _viscous_friction_force(self) -> float:
|
||||
if not self.use_friction:
|
||||
return 0.0
|
||||
return -self.rvisc * self.v
|
||||
|
||||
def _windage_force(self) -> float:
|
||||
if not self.use_friction:
|
||||
return 0.0
|
||||
return -self.wind * self.v * abs(self.v)
|
||||
|
||||
def _dry_friction_force(self) -> float:
|
||||
if not self.use_friction:
|
||||
return 0.0
|
||||
if self.v > 0.0:
|
||||
return -self.fcoul
|
||||
if self.v < 0.0:
|
||||
return self.fcoul
|
||||
return 0.0
|
||||
|
||||
def _lower_limit_force(self) -> float:
|
||||
penetration = max(self.xmin - self.x, 0.0)
|
||||
if penetration <= 0.0:
|
||||
return 0.0
|
||||
return self.Kbmin * penetration + max(-self.Dbmin * self.v, 0.0)
|
||||
|
||||
def _upper_limit_force(self) -> float:
|
||||
penetration = max(self.x - self.xmax, 0.0)
|
||||
if penetration <= 0.0:
|
||||
return 0.0
|
||||
return self.Kbmax * penetration + max(self.Dbmax * self.v, 0.0)
|
||||
|
||||
def acceleration(self) -> float:
|
||||
return (
|
||||
self.port_1.f
|
||||
+ self.port_2.f
|
||||
+ self._viscous_friction_force()
|
||||
+ self._windage_force()
|
||||
+ self._dry_friction_force()
|
||||
+ self._lower_limit_force()
|
||||
- self._upper_limit_force()
|
||||
) / self.mass
|
||||
|
||||
def state_derivative_from_ports(self, connected_h: Mapping[str, float]) -> list[float]:
|
||||
return [self.acceleration(), self.v]
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {
|
||||
"a": self.acceleration(),
|
||||
"v": self.v,
|
||||
"x": self.x,
|
||||
"Fvisc": self._viscous_friction_force(),
|
||||
"Ffric": self._dry_friction_force(),
|
||||
"Fmin": self._lower_limit_force(),
|
||||
"Fmax": self._upper_limit_force(),
|
||||
}
|
||||
|
||||
|
||||
class AmesimLstp00a(AlgebraicComponent):
|
||||
"""AMESim LSTP00A first public elastic contact component."""
|
||||
|
||||
MODEL_TYPE = "amesim_lstp00a"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.mechanical_translational("port_1"),
|
||||
PortDefinition.mechanical_translational("port_2"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition("na", 10.0, label="有效圈数", quantity="dimensionless", unit="", minimum=0.0, minimum_exclusive=True),
|
||||
ParameterDefinition("gap0", 0.0, label="初始间隙", quantity="length", unit="m"),
|
||||
ParameterDefinition("kcont", 1.0e6, label="接触刚度", quantity="translational_stiffness", unit="N/m", minimum=0.0),
|
||||
ParameterDefinition("G", 8.57e10, label="剪切模量", quantity="pressure", unit="Pa", minimum=0.0),
|
||||
ParameterDefinition("sdiam", 0.02, label="弹簧直径", quantity="length", unit="m", minimum=0.0),
|
||||
ParameterDefinition("wdiam", 0.002, label="线径", quantity="length", unit="m", minimum=0.0),
|
||||
ParameterDefinition("rcont", 0.0, label="接触阻尼", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
|
||||
ParameterDefinition("Pdis", 1.0e-7, label="满阻尼穿透", quantity="length", unit="m", minimum=0.0),
|
||||
ParameterDefinition("stiffmode", 1.0, label="刚度模式", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition("discContactOption", 1.0, label="接触选项", quantity="dimensionless", unit="", minimum=0.0),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("gap", "间隙", "length", "m", "derived", 10),
|
||||
ResultVariableDefinition("penetration", "穿透", "length", "m", "derived", 20),
|
||||
ResultVariableDefinition("force", "接触力", "force", "N", "derived", 30),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="LSTP00A 弹性接触",
|
||||
library_id="amesim",
|
||||
category_id="mechanical",
|
||||
symbol="amesim_lstp00a",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=40,
|
||||
)
|
||||
|
||||
def __init__(self, name: str, medium: IdealGasMedium, **parameters: float) -> None:
|
||||
super().__init__(name=name)
|
||||
resolved = {
|
||||
definition.name: parameters.get(definition.name, definition.default)
|
||||
for definition in self.PARAMETERS
|
||||
}
|
||||
self.set_parameter_values(resolved)
|
||||
for name, value in resolved.items():
|
||||
setattr(self, name, float(value))
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimLstp00a":
|
||||
for integer_name in ("stiffmode", "discContactOption"):
|
||||
if not float(parameters[integer_name]).is_integer():
|
||||
raise ValueError(f"LSTP00A {integer_name} must be an integer.")
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
@property
|
||||
def gap(self) -> float:
|
||||
return self.gap0 - (self.port_2.x - self.port_1.x)
|
||||
|
||||
@property
|
||||
def penetration(self) -> float:
|
||||
return max(-self.gap, 0.0)
|
||||
|
||||
@property
|
||||
def penetration_velocity(self) -> float:
|
||||
return self.port_2.v - self.port_1.v
|
||||
|
||||
@property
|
||||
def contact_force(self) -> float:
|
||||
if self.penetration <= 0.0:
|
||||
return 0.0
|
||||
return max(self.kcont * self.penetration + self.rcont * self.penetration_velocity, 0.0)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
force = self.contact_force
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_1_contact_force",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_1.f", f"{self.name}.port_1.x", f"{self.name}.port_2.x"),
|
||||
role="flow",
|
||||
value=self.port_1.f + force,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_2_contact_force",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_2.f", f"{self.name}.port_1.x", f"{self.name}.port_2.x"),
|
||||
role="flow",
|
||||
value=self.port_2.f - force,
|
||||
),
|
||||
)
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {
|
||||
"gap": self.gap,
|
||||
"penetration": self.penetration,
|
||||
"force": self.contact_force,
|
||||
}
|
||||
|
||||
|
||||
class AmesimLmechn1(AlgebraicComponent):
|
||||
"""AMESim LMECHN1 first public dynamic linear mechanical node."""
|
||||
|
||||
MODEL_TYPE = "amesim_lmechn1"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = tuple(
|
||||
PortDefinition.mechanical_translational(f"port_{index}")
|
||||
for index in range(1, 10)
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition("v1", 8.0, label="右侧端口数", quantity="dimensionless", unit="", minimum=1.0, maximum=8.0),
|
||||
ParameterDefinition("sum", 1.0, label="节点求和模式", quantity="dimensionless", unit="", minimum=0.0),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("tforce", "节点合力", "force", "N", "derived", 10),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="LMECHN1 线性机械节点",
|
||||
library_id="amesim",
|
||||
category_id="mechanical",
|
||||
symbol="amesim_lmechn1",
|
||||
ports=tuple(
|
||||
[PortDisplaySpec(f"port_{index}", "left", order=index * 10) for index in range(1, 9)]
|
||||
+ [PortDisplaySpec("port_9", "right", order=90)]
|
||||
),
|
||||
order=50,
|
||||
)
|
||||
|
||||
def __init__(self, name: str, medium: IdealGasMedium, *, v1: float = 8.0, sum: float = 1.0) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({"v1": v1, "sum": sum})
|
||||
self.v1 = int(v1)
|
||||
self.sum = int(sum)
|
||||
for definition in self.PORTS:
|
||||
setattr(self, definition.name, self.register_declared_port(definition.name))
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimLmechn1":
|
||||
for integer_name in ("v1", "sum"):
|
||||
if not float(parameters[integer_name]).is_integer():
|
||||
raise ValueError(f"LMECHN1 {integer_name} must be an integer.")
|
||||
return cls(name=name, medium=medium, v1=parameters["v1"], sum=parameters["sum"])
|
||||
|
||||
@property
|
||||
def active_ports(self) -> tuple[str, ...]:
|
||||
return tuple(f"port_{index}" for index in range(1, self.v1 + 1)) + ("port_9",)
|
||||
|
||||
@property
|
||||
def total_force(self) -> float:
|
||||
return sum(self.get_port(port_name).f for port_name in self.active_ports)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
reference = self.port_9
|
||||
residuals: list[EquationResidual] = []
|
||||
for port_name in self.active_ports[:-1]:
|
||||
port = self.get_port(port_name)
|
||||
residuals.append(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{port_name}_x_equal",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="equal",
|
||||
variables=(f"{self.name}.{port_name}.x", f"{self.name}.port_9.x"),
|
||||
role="effort",
|
||||
value=port.x - reference.x,
|
||||
)
|
||||
)
|
||||
residuals.append(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{port_name}_v_equal",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="equal",
|
||||
variables=(f"{self.name}.{port_name}.v", f"{self.name}.port_9.v"),
|
||||
role="effort",
|
||||
value=port.v - reference.v,
|
||||
)
|
||||
)
|
||||
residuals.append(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:force_balance",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="sumToZero",
|
||||
variables=tuple(f"{self.name}.{port_name}.f" for port_name in self.active_ports),
|
||||
role="flow",
|
||||
value=self.total_force,
|
||||
)
|
||||
)
|
||||
return tuple(residuals)
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {"tforce": self.total_force}
|
||||
@@ -0,0 +1,33 @@
|
||||
"""AMESim medium-property definition components."""
|
||||
|
||||
from app.simulation.components.amesim.media.mediums import (
|
||||
AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL,
|
||||
AMESIM_AIR_PROPERTY_MODELS,
|
||||
AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL,
|
||||
AMESIM_HELIUM_PROPERTY_MODELS,
|
||||
AmesimGasPropertyModelSpec,
|
||||
AmesimHeliumPengRobinsonMedium,
|
||||
AmesimIdealAirMedium,
|
||||
)
|
||||
from app.simulation.components.amesim.media.properties import (
|
||||
AMESIM_AIR_PROPERTY_MODEL_PARAMETER,
|
||||
AMESIM_HELIUM_PROPERTY_MODEL_PARAMETER,
|
||||
AmesimGasMediumDefinitionComponent,
|
||||
AmesimHeliumMediumDefinition,
|
||||
AmesimIdealAirMediumDefinition,
|
||||
)
|
||||
|
||||
__all__ = (
|
||||
"AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL",
|
||||
"AMESIM_AIR_PROPERTY_MODELS",
|
||||
"AMESIM_AIR_PROPERTY_MODEL_PARAMETER",
|
||||
"AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL",
|
||||
"AMESIM_HELIUM_PROPERTY_MODELS",
|
||||
"AMESIM_HELIUM_PROPERTY_MODEL_PARAMETER",
|
||||
"AmesimGasMediumDefinitionComponent",
|
||||
"AmesimGasPropertyModelSpec",
|
||||
"AmesimHeliumMediumDefinition",
|
||||
"AmesimHeliumPengRobinsonMedium",
|
||||
"AmesimIdealAirMedium",
|
||||
"AmesimIdealAirMediumDefinition",
|
||||
)
|
||||
@@ -0,0 +1,109 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar
|
||||
|
||||
from app.simulation.core.medium import GasMedium, IdealGasMedium
|
||||
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimIdealAirMedium(IdealGasMedium):
|
||||
"""AMESim air properties evaluated with the ideal-gas method.
|
||||
|
||||
Substance identity and property method are part of the concrete Python
|
||||
type. A future air correlation or helium Peng-Robinson implementation can
|
||||
therefore coexist as a sibling type without turning ``gi`` into a fluid
|
||||
enumeration.
|
||||
"""
|
||||
|
||||
SUBSTANCE_ID: ClassVar[str] = "air"
|
||||
PROPERTY_METHOD_ID: ClassVar[str] = "ideal_gas"
|
||||
|
||||
name: str = "AMESimAirIdealGas"
|
||||
R_gas: float = 287.0
|
||||
cp_ref: float = 1005.0
|
||||
T_ref: float = 300.0
|
||||
cp_slope: float = 0.0
|
||||
viscosity_ref: float = 1.82e-5
|
||||
viscosity_T_ref: float = 293.15
|
||||
sutherland_constant: float = 110.4
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimHeliumPengRobinsonMedium(IdealGasMedium):
|
||||
"""AMESim helium with a Peng-Robinson mechanical equation of state.
|
||||
|
||||
The pressure-density-temperature relation is evaluated by the shared
|
||||
``HELIUM_PR`` fluid. The first public AMESim port keeps the committed
|
||||
constant-heat-capacity caloric model so it can be consumed through the
|
||||
same :class:`GasMedium` contract as ideal-gas air.
|
||||
"""
|
||||
|
||||
SUBSTANCE_ID: ClassVar[str] = "helium"
|
||||
PROPERTY_METHOD_ID: ClassVar[str] = "peng_robinson"
|
||||
fluid: ClassVar[PengRobinsonFluid] = HELIUM_PR
|
||||
|
||||
name: str = "AMESimHeliumPengRobinson"
|
||||
R_gas: float = HELIUM_PR.specific_gas_constant
|
||||
cp_ref: float = 5193.0
|
||||
T_ref: float = 293.15
|
||||
cp_slope: float = 0.0
|
||||
viscosity_ref: float = 1.96e-5
|
||||
viscosity_T_ref: float = 293.15
|
||||
sutherland_constant: float = 79.4
|
||||
|
||||
@property
|
||||
def cv(self) -> float:
|
||||
return 3116.0
|
||||
|
||||
def cv_at_temperature(self, T: float) -> float:
|
||||
del T
|
||||
return self.cv
|
||||
|
||||
def density(self, p: float, T: float) -> float:
|
||||
return self.fluid.density(p, T)
|
||||
|
||||
def pressure(self, m: float, T: float, V: float) -> float:
|
||||
if V <= 0.0:
|
||||
raise ValueError("Volume must stay positive.")
|
||||
return self.fluid.pressure_from_density(T, m / V)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimGasPropertyModelSpec:
|
||||
"""A selectable calculation method for one AMESim gas substance."""
|
||||
|
||||
value: int
|
||||
label: str
|
||||
method_id: str
|
||||
factory: Callable[[], GasMedium]
|
||||
eos_type: int
|
||||
|
||||
def build_medium(self) -> GasMedium:
|
||||
return self.factory()
|
||||
|
||||
|
||||
AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL = 0
|
||||
AMESIM_AIR_PROPERTY_MODELS = (
|
||||
AmesimGasPropertyModelSpec(
|
||||
value=AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL,
|
||||
label="理想气体",
|
||||
method_id=AmesimIdealAirMedium.PROPERTY_METHOD_ID,
|
||||
factory=AmesimIdealAirMedium,
|
||||
eos_type=1,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL = 0
|
||||
AMESIM_HELIUM_PROPERTY_MODELS = (
|
||||
AmesimGasPropertyModelSpec(
|
||||
value=AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL,
|
||||
label="Peng–Robinson",
|
||||
method_id=AmesimHeliumPengRobinsonMedium.PROPERTY_METHOD_ID,
|
||||
factory=AmesimHeliumPengRobinsonMedium,
|
||||
eos_type=6,
|
||||
),
|
||||
)
|
||||
@@ -0,0 +1,196 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.components.amesim.gases import (
|
||||
AMESIM_GAS_DEFINITION_INDEX_PARAMETER,
|
||||
AmesimGasDefinition,
|
||||
normalize_amesim_defined_gas_index,
|
||||
)
|
||||
from app.simulation.components.amesim.media.mediums import (
|
||||
AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL,
|
||||
AMESIM_AIR_PROPERTY_MODELS,
|
||||
AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL,
|
||||
AMESIM_HELIUM_PROPERTY_MODELS,
|
||||
AmesimGasPropertyModelSpec,
|
||||
)
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec
|
||||
from app.simulation.core.medium import GasMedium
|
||||
from app.simulation.core.metadata import ParameterDefinition, ParameterOption
|
||||
|
||||
|
||||
AMESIM_AIR_PROPERTY_MODEL_PARAMETER = ParameterDefinition(
|
||||
"property_model",
|
||||
float(AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL),
|
||||
label="物性计算模型",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=float(min(model.value for model in AMESIM_AIR_PROPERTY_MODELS)),
|
||||
maximum=float(max(model.value for model in AMESIM_AIR_PROPERTY_MODELS)),
|
||||
editor="amesimGasPropertyModel",
|
||||
options=tuple(
|
||||
ParameterOption(value=model.value, label=model.label)
|
||||
for model in AMESIM_AIR_PROPERTY_MODELS
|
||||
),
|
||||
description="选择空气介质的物性计算方法;当前首版提供理想气体模型。",
|
||||
)
|
||||
|
||||
AMESIM_HELIUM_PROPERTY_MODEL_PARAMETER = ParameterDefinition(
|
||||
"property_model",
|
||||
float(AMESIM_HELIUM_PENG_ROBINSON_PROPERTY_MODEL),
|
||||
label="物性计算模型",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=float(min(model.value for model in AMESIM_HELIUM_PROPERTY_MODELS)),
|
||||
maximum=float(max(model.value for model in AMESIM_HELIUM_PROPERTY_MODELS)),
|
||||
editor="amesimGasPropertyModel",
|
||||
options=tuple(
|
||||
ParameterOption(value=model.value, label=model.label)
|
||||
for model in AMESIM_HELIUM_PROPERTY_MODELS
|
||||
),
|
||||
description=(
|
||||
"选择氦气介质的物性计算方法;当前首版提供 "
|
||||
"Peng–Robinson 状态方程模型。"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AmesimGasMediumDefinitionComponent(AlgebraicComponent, ABC):
|
||||
"""Compile-time definition of one project-scoped AMESim gas medium.
|
||||
|
||||
Concrete subclasses declare one substance and its available calculation
|
||||
methods; each instance selects a method through ``property_model``. They
|
||||
deliberately expose no physical ports or equations: the compiler consumes
|
||||
them before it creates the simulation network.
|
||||
"""
|
||||
|
||||
IS_AMESIM_GAS_MEDIUM_DEFINITION = True
|
||||
MEDIUM_LABEL = ""
|
||||
FLUID_TYPE: int | None = None
|
||||
PROPERTY_MODELS: tuple[AmesimGasPropertyModelSpec, ...] = ()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
gi: float,
|
||||
property_model: float = float(AMESIM_AIR_IDEAL_GAS_PROPERTY_MODEL),
|
||||
) -> None:
|
||||
super().__init__(name)
|
||||
self.gi = normalize_amesim_defined_gas_index(gi)
|
||||
self.property_model = self._resolve_property_model(property_model).value
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"gi": self.gi,
|
||||
"property_model": self.property_model,
|
||||
}
|
||||
)
|
||||
|
||||
def _resolve_property_model(
|
||||
self,
|
||||
value: float | int,
|
||||
) -> AmesimGasPropertyModelSpec:
|
||||
for model in self.PROPERTY_MODELS:
|
||||
if float(model.value) == float(value):
|
||||
return model
|
||||
available = ", ".join(str(model.value) for model in self.PROPERTY_MODELS)
|
||||
raise ValueError(
|
||||
f"AMESim medium definition '{self.name}' does not support property "
|
||||
f"model {value:g}; available models: {available or 'none'}."
|
||||
)
|
||||
|
||||
def build_medium(self) -> GasMedium:
|
||||
"""Create the executable property model selected by this instance."""
|
||||
|
||||
return self._resolve_property_model(self.property_model).build_medium()
|
||||
|
||||
def gas_definition(self) -> AmesimGasDefinition:
|
||||
model = self._resolve_property_model(self.property_model)
|
||||
return AmesimGasDefinition(
|
||||
gi=self.gi,
|
||||
label=f"{self.MEDIUM_LABEL}({model.label})",
|
||||
medium=self.build_medium(),
|
||||
fluid_type=self.FLUID_TYPE,
|
||||
eos_type=model.eos_type,
|
||||
)
|
||||
|
||||
|
||||
class AmesimIdealAirMediumDefinition(AmesimGasMediumDefinitionComponent):
|
||||
"""Project gas slot using the built-in ideal-gas air property method."""
|
||||
|
||||
MODEL_TYPE = "amesim_ideal_air_medium"
|
||||
MODEL_VERSION = "0.2.0"
|
||||
PORTS = ()
|
||||
PARAMETERS = (
|
||||
AMESIM_GAS_DEFINITION_INDEX_PARAMETER,
|
||||
AMESIM_AIR_PROPERTY_MODEL_PARAMETER,
|
||||
)
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="空气介质定义",
|
||||
library_id="amesim",
|
||||
category_id="media",
|
||||
symbol="amesim_ideal_air_medium",
|
||||
ports=(),
|
||||
order=10,
|
||||
role="amesimGasMediumDefinition",
|
||||
)
|
||||
MEDIUM_LABEL = "空气"
|
||||
FLUID_TYPE = 2
|
||||
PROPERTY_MODELS = AMESIM_AIR_PROPERTY_MODELS
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimIdealAirMediumDefinition:
|
||||
del medium
|
||||
return cls(
|
||||
name=name,
|
||||
gi=parameters["gi"],
|
||||
property_model=parameters["property_model"],
|
||||
)
|
||||
|
||||
|
||||
class AmesimHeliumMediumDefinition(AmesimGasMediumDefinitionComponent):
|
||||
"""Project gas slot using the AMESim helium Peng-Robinson method."""
|
||||
|
||||
MODEL_TYPE = "amesim_helium_medium"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = ()
|
||||
PARAMETERS = (
|
||||
AMESIM_GAS_DEFINITION_INDEX_PARAMETER,
|
||||
AMESIM_HELIUM_PROPERTY_MODEL_PARAMETER,
|
||||
)
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="氦气介质定义",
|
||||
library_id="amesim",
|
||||
category_id="media",
|
||||
symbol="amesim_helium_medium",
|
||||
ports=(),
|
||||
order=20,
|
||||
role="amesimGasMediumDefinition",
|
||||
)
|
||||
MEDIUM_LABEL = "氦气"
|
||||
FLUID_TYPE = 12
|
||||
PROPERTY_MODELS = AMESIM_HELIUM_PROPERTY_MODELS
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimHeliumMediumDefinition:
|
||||
del medium
|
||||
return cls(
|
||||
name=name,
|
||||
gi=parameters["gi"],
|
||||
property_model=parameters["property_model"],
|
||||
)
|
||||
Whitespace-only changes.
@@ -0,0 +1,267 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from math import floor
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
|
||||
|
||||
class AmesimStep0(AlgebraicComponent):
|
||||
"""AMESim STEP0 scalar step signal source."""
|
||||
|
||||
MODEL_TYPE = "amesim_step0"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition("initial", 0.0, label="初始值", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("final", 1.0, label="阶跃后值", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("time", 0.0, label="阶跃时间", quantity="time", unit="s"),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("y", "输出", "dimensionless", "", "signal", 10),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="STEP0 阶跃信号",
|
||||
library_id="amesim",
|
||||
category_id="signals",
|
||||
symbol="amesim_step0",
|
||||
ports=(PortDisplaySpec("out", "right", order=10),),
|
||||
order=10,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
initial: float = 0.0,
|
||||
final: float = 1.0,
|
||||
time: float = 0.0,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({"initial": initial, "final": final, "time": time})
|
||||
self.initial = float(initial)
|
||||
self.final = float(final)
|
||||
self.time = float(time)
|
||||
self.out = self.register_declared_port("out")
|
||||
self.out.signal = self.output_at(0.0)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimStep0":
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
initial=parameters["initial"],
|
||||
final=parameters["final"],
|
||||
time=parameters["time"],
|
||||
)
|
||||
|
||||
def output_at(self, time: float) -> float:
|
||||
return self.final if time >= self.time else self.initial
|
||||
|
||||
def signal_output_values(self, time: float) -> dict[str, float]:
|
||||
return {"out": self.output_at(time)}
|
||||
|
||||
def signal_event_times(
|
||||
self,
|
||||
start_time: float,
|
||||
stop_time: float,
|
||||
) -> tuple[float, ...]:
|
||||
"""Expose the exact STEP0 switch time as an integration split point."""
|
||||
|
||||
return (self.time,) if start_time < self.time < stop_time else ()
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {"y": self.out.signal}
|
||||
|
||||
|
||||
class AmesimUd00(AlgebraicComponent):
|
||||
"""AMESim UD00 piecewise-linear scalar signal source."""
|
||||
|
||||
MODEL_TYPE = "amesim_ud00"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition("tstart", 0.0, label="启动时间", quantity="time", unit="s"),
|
||||
ParameterDefinition("start1", 0.0, label="第 1 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end1", 1.0, label="第 1 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t1", 1.0, label="第 1 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start2", 1.0, label="第 2 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end2", 1.0, label="第 2 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t2", 0.0, label="第 2 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start3", 1.0, label="第 3 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end3", 1.0, label="第 3 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t3", 0.0, label="第 3 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start4", 1.0, label="第 4 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end4", 1.0, label="第 4 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t4", 0.0, label="第 4 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start5", 1.0, label="第 5 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end5", 1.0, label="第 5 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t5", 0.0, label="第 5 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start6", 1.0, label="第 6 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end6", 1.0, label="第 6 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t6", 0.0, label="第 6 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start7", 1.0, label="第 7 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end7", 1.0, label="第 7 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t7", 0.0, label="第 7 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("start8", 1.0, label="第 8 段起点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("end8", 1.0, label="第 8 段终点", quantity="dimensionless", unit=""),
|
||||
ParameterDefinition("t8", 0.0, label="第 8 段时长", quantity="time", unit="s", minimum=0.0),
|
||||
ParameterDefinition("nstages", 1.0, label="段数", quantity="dimensionless", unit="", minimum=1.0, maximum=8.0),
|
||||
ParameterDefinition("iscyclic", 0.0, label="循环", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("y", "输出", "dimensionless", "", "signal", 10),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="UD00 分段线性信号",
|
||||
library_id="amesim",
|
||||
category_id="signals",
|
||||
symbol="amesim_ud00",
|
||||
ports=(PortDisplaySpec("out", "right", order=10),),
|
||||
order=20,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
*,
|
||||
tstart: float = 0.0,
|
||||
starts: tuple[float, ...] = (0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
|
||||
ends: tuple[float, ...] = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
|
||||
durations: tuple[float, ...] = (1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
|
||||
nstages: int = 1,
|
||||
iscyclic: bool = False,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
if len(starts) != 8 or len(ends) != 8 or len(durations) != 8:
|
||||
raise ValueError("UD00 requires exactly eight start, end, and duration values.")
|
||||
if nstages < 1 or nstages > 8:
|
||||
raise ValueError("UD00 nstages must be between 1 and 8.")
|
||||
self.tstart = float(tstart)
|
||||
self.starts = tuple(float(value) for value in starts)
|
||||
self.ends = tuple(float(value) for value in ends)
|
||||
self.durations = tuple(float(value) for value in durations)
|
||||
self.nstages = int(nstages)
|
||||
self.iscyclic = bool(iscyclic)
|
||||
values: dict[str, float] = {"tstart": self.tstart, "nstages": float(self.nstages), "iscyclic": float(int(self.iscyclic))}
|
||||
for index in range(1, 9):
|
||||
values[f"start{index}"] = self.starts[index - 1]
|
||||
values[f"end{index}"] = self.ends[index - 1]
|
||||
values[f"t{index}"] = self.durations[index - 1]
|
||||
self.set_parameter_values(values)
|
||||
self.out = self.register_declared_port("out")
|
||||
self.out.signal = self.output_at(0.0)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimUd00":
|
||||
nstages = parameters["nstages"]
|
||||
iscyclic = parameters["iscyclic"]
|
||||
if not float(nstages).is_integer():
|
||||
raise ValueError("UD00 nstages must be an integer.")
|
||||
if not float(iscyclic).is_integer():
|
||||
raise ValueError("UD00 iscyclic must be 0 or 1.")
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
tstart=parameters["tstart"],
|
||||
starts=tuple(parameters[f"start{index}"] for index in range(1, 9)),
|
||||
ends=tuple(parameters[f"end{index}"] for index in range(1, 9)),
|
||||
durations=tuple(parameters[f"t{index}"] for index in range(1, 9)),
|
||||
nstages=int(nstages),
|
||||
iscyclic=bool(int(iscyclic)),
|
||||
)
|
||||
|
||||
def output_at(self, time: float) -> float:
|
||||
elapsed = max(float(time) - self.tstart, 0.0)
|
||||
active_durations = self.durations[: self.nstages]
|
||||
total_duration = sum(active_durations)
|
||||
if self.iscyclic and total_duration > 0.0:
|
||||
elapsed = elapsed % total_duration
|
||||
|
||||
stage_start_time = 0.0
|
||||
for index, duration in enumerate(active_durations):
|
||||
stage_end_time = stage_start_time + duration
|
||||
if elapsed < stage_end_time or index == self.nstages - 1:
|
||||
if duration <= 0.0:
|
||||
return self.ends[index]
|
||||
fraction = (elapsed - stage_start_time) / duration
|
||||
return self.starts[index] + fraction * (self.ends[index] - self.starts[index])
|
||||
stage_start_time = stage_end_time
|
||||
return self.ends[self.nstages - 1]
|
||||
|
||||
def signal_output_values(self, time: float) -> dict[str, float]:
|
||||
return {"out": self.output_at(time)}
|
||||
|
||||
def signal_event_times(
|
||||
self,
|
||||
start_time: float,
|
||||
stop_time: float,
|
||||
) -> tuple[float, ...]:
|
||||
"""Return UD00 start, stage, and repeated cycle boundaries.
|
||||
|
||||
The final non-cyclic stage is intentionally not given an end event:
|
||||
``output_at`` continues that stage's slope after its configured duration.
|
||||
"""
|
||||
|
||||
if stop_time <= start_time:
|
||||
return ()
|
||||
|
||||
active_durations = self.durations[: self.nstages]
|
||||
stage_offsets = [0.0]
|
||||
elapsed = 0.0
|
||||
for duration in active_durations[:-1]:
|
||||
elapsed += duration
|
||||
stage_offsets.append(elapsed)
|
||||
|
||||
if not self.iscyclic:
|
||||
return tuple(
|
||||
sorted(
|
||||
{
|
||||
event_time
|
||||
for offset in stage_offsets
|
||||
if start_time
|
||||
< (event_time := self.tstart + offset)
|
||||
< stop_time
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
cycle_duration = sum(active_durations)
|
||||
if cycle_duration <= 0.0:
|
||||
return ()
|
||||
|
||||
events: set[float] = set()
|
||||
for offset in stage_offsets:
|
||||
first_boundary = self.tstart + offset
|
||||
cycle_index = max(
|
||||
0,
|
||||
floor((start_time - first_boundary) / cycle_duration) + 1,
|
||||
)
|
||||
event_time = first_boundary + cycle_index * cycle_duration
|
||||
while event_time < stop_time:
|
||||
if event_time > start_time:
|
||||
events.add(event_time)
|
||||
cycle_index += 1
|
||||
event_time = first_boundary + cycle_index * cycle_duration
|
||||
return tuple(sorted(events))
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
return {"y": self.out.signal}
|
||||
@@ -0,0 +1 @@
|
||||
"""AMESim pneumatic storage components."""
|
||||
@@ -0,0 +1,513 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
|
||||
from app.simulation.components.amesim.gases import (
|
||||
AMESIM_GAS_INDEX_PARAMETER,
|
||||
normalize_amesim_gas_index,
|
||||
)
|
||||
from app.simulation.core.base import ThermodynamicVolumeComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.equations import EquationResidual
|
||||
from app.simulation.core.metadata import (
|
||||
ParameterDefinition,
|
||||
ResultVariableDefinition,
|
||||
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||
)
|
||||
from app.simulation.core.medium import GasMedium, ThermodynamicProperties
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.core.state import VolumeState
|
||||
|
||||
|
||||
class AmesimPnch023(ThermodynamicVolumeComponent):
|
||||
"""AMESim PNCH023 simple pneumatic chamber with heat exchange.
|
||||
|
||||
The AMESim submodel owns pressure and temperature states and exposes two
|
||||
pneumatic flow ports. This public component maps those states onto the
|
||||
framework's mass/internal-energy volume state and keeps the AMESim
|
||||
heat-transfer contract `kth * sth * (extemp - T)`.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnch023"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
AMESIM_GAS_INDEX_PARAMETER,
|
||||
ParameterDefinition(
|
||||
"cvol",
|
||||
0.057,
|
||||
label="气室容积",
|
||||
quantity="volume",
|
||||
unit="m3",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
description="气室内部用于储存气体的固定有效容积。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"kth",
|
||||
0.0,
|
||||
label="换热系数",
|
||||
quantity="heat_transfer_coefficient",
|
||||
unit="W/(m2*K)",
|
||||
minimum=0.0,
|
||||
description="气室与环境之间的对流换热系数,与换热面积共同决定换热功率。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"sth",
|
||||
0.1,
|
||||
label="换热面积",
|
||||
quantity="area",
|
||||
unit="m2",
|
||||
minimum=0.0,
|
||||
description="气室与环境进行热交换的有效表面积。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"extemp",
|
||||
293.15,
|
||||
label="外部温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
description="气室外部环境的绝对温度,用于计算气体与环境之间的换热。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"p0",
|
||||
100000.0,
|
||||
label="初始压力",
|
||||
quantity="pressure",
|
||||
unit="Pa",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
description="仿真开始时气室内气体的绝对压力。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"T0",
|
||||
293.15,
|
||||
label="初始温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
description="仿真开始时气室内气体的绝对温度。",
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNCH023 固定容积气室",
|
||||
library_id="amesim",
|
||||
category_id="storage",
|
||||
symbol="amesim_pnch023",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
),
|
||||
order=10,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
*,
|
||||
cvol: float = 0.057,
|
||||
kth: float = 0.0,
|
||||
sth: float = 0.1,
|
||||
extemp: float = 293.15,
|
||||
gi: float = 1.0,
|
||||
p0: float = 100000.0,
|
||||
T0: float = 293.15,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"cvol": cvol,
|
||||
"kth": kth,
|
||||
"sth": sth,
|
||||
"extemp": extemp,
|
||||
"gi": gi,
|
||||
"p0": p0,
|
||||
"T0": T0,
|
||||
}
|
||||
)
|
||||
self.medium = medium
|
||||
self.cvol = float(cvol)
|
||||
self.kth = float(kth)
|
||||
self.sth = float(sth)
|
||||
self.extemp = float(extemp)
|
||||
self.gi = normalize_amesim_gas_index(gi)
|
||||
self.p0 = float(p0)
|
||||
self.T0 = float(T0)
|
||||
m0 = medium.density(self.p0, self.T0) * self.cvol
|
||||
U0 = m0 * medium.specific_internal_energy(self.T0)
|
||||
self.state = VolumeState(m=m0, U=U0)
|
||||
initial_h = medium.specific_enthalpy(self.T0)
|
||||
self.port_1 = self.register_declared_port("port_1")
|
||||
self.port_1.p = self.p0
|
||||
self.port_1.h_outflow = initial_h
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self.port_2.p = self.p0
|
||||
self.port_2.h_outflow = initial_h
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> AmesimPnch023:
|
||||
return cls(
|
||||
name=name,
|
||||
medium=medium,
|
||||
cvol=parameters["cvol"],
|
||||
kth=parameters["kth"],
|
||||
sth=parameters["sth"],
|
||||
extemp=parameters["extemp"],
|
||||
gi=parameters["gi"],
|
||||
p0=parameters["p0"],
|
||||
T0=parameters["T0"],
|
||||
)
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.cvol)
|
||||
self.port_1.p = props.p
|
||||
self.port_1.h_outflow = props.h
|
||||
self.port_2.p = props.p
|
||||
self.port_2.h_outflow = props.h
|
||||
return props
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||
return self.properties()
|
||||
|
||||
def thermal_energy_flow_w(self, temperature: float) -> float:
|
||||
return self.kth * self.sth * (self.extemp - temperature)
|
||||
|
||||
def state_derivative_from_ports(
|
||||
self,
|
||||
connected_h: Mapping[str, float],
|
||||
) -> list[float]:
|
||||
props = self.properties()
|
||||
inlet_h_1 = self.connection_inlet_enthalpy(
|
||||
port_m_flow=self.port_1.m_flow,
|
||||
connected_h=connected_h["port_1"],
|
||||
internal_h=props.h,
|
||||
)
|
||||
inlet_h_2 = self.connection_inlet_enthalpy(
|
||||
port_m_flow=self.port_2.m_flow,
|
||||
connected_h=connected_h["port_2"],
|
||||
internal_h=props.h,
|
||||
)
|
||||
derivative = VolumeState(
|
||||
m=self.port_1.m_flow + self.port_2.m_flow,
|
||||
U=(
|
||||
self.port_1.m_flow * inlet_h_1
|
||||
+ self.port_2.m_flow * inlet_h_2
|
||||
+ self.thermal_energy_flow_w(props.T)
|
||||
),
|
||||
)
|
||||
return derivative.as_vector()
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
pressure = self.medium.properties_from_mU(
|
||||
self.state.m,
|
||||
self.state.U,
|
||||
self.cvol,
|
||||
).p
|
||||
return (
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_1_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.port_1.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.port_1.p - pressure,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:port_2_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.port_2.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.port_2.p - pressure,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AmesimPnch012(ThermodynamicVolumeComponent):
|
||||
"""AMESim PNCH012 variable-volume pneumatic chamber.
|
||||
|
||||
AMESim supplies four external volume and volume-rate inputs through the
|
||||
chamber ports. The current public System XML contract has pneumatic ports
|
||||
only, so this first public model exposes those external volume inputs as SI
|
||||
parameters. This represents fixed or prescribed-volume PNCH012 cases and is
|
||||
not yet the full mechanical-coupled submodel.
|
||||
"""
|
||||
|
||||
MODEL_TYPE = "amesim_pnch012"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
PORTS = (
|
||||
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_2", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_3", nominal_role="bidirectional"),
|
||||
PortDefinition.pneumatic("port_4", nominal_role="bidirectional"),
|
||||
)
|
||||
PARAMETERS = (
|
||||
AMESIM_GAS_INDEX_PARAMETER,
|
||||
ParameterDefinition(
|
||||
"cvol0",
|
||||
0.015,
|
||||
label="死容积",
|
||||
quantity="volume",
|
||||
unit="m3",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
description="变容气室在所有外部容积为零时仍保留的基础容积。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"kth",
|
||||
0.0,
|
||||
label="换热系数",
|
||||
quantity="heat_transfer_coefficient",
|
||||
unit="W/(m2*K)",
|
||||
minimum=0.0,
|
||||
description="气室与环境之间的对流换热系数,与换热面积共同决定换热功率。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"sth",
|
||||
0.1,
|
||||
label="换热面积",
|
||||
quantity="area",
|
||||
unit="m2",
|
||||
minimum=0.0,
|
||||
description="气室与环境进行热交换的有效表面积。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"extemp",
|
||||
293.15,
|
||||
label="外部温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
description="气室外部环境的绝对温度,用于计算气体与环境之间的换热。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"p0",
|
||||
100000.0,
|
||||
label="初始压力",
|
||||
quantity="pressure",
|
||||
unit="Pa",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
description="仿真开始时气室内气体的绝对压力。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"T0",
|
||||
293.15,
|
||||
label="初始温度",
|
||||
quantity="temperature",
|
||||
unit="K",
|
||||
minimum=0.0,
|
||||
minimum_exclusive=True,
|
||||
description="仿真开始时气室内气体的绝对温度。",
|
||||
),
|
||||
ParameterDefinition("vol1", 0.0, label="端口 1 外部容积", quantity="volume", unit="m3"),
|
||||
ParameterDefinition("vol2", 0.0, label="端口 2 外部容积", quantity="volume", unit="m3"),
|
||||
ParameterDefinition("vol3", 0.0, label="端口 3 外部容积", quantity="volume", unit="m3"),
|
||||
ParameterDefinition("vol4", 0.0, label="端口 4 外部容积", quantity="volume", unit="m3"),
|
||||
ParameterDefinition("dvol1", 0.0, label="端口 1 容积变化率", quantity="volume_flow", unit="m3/s"),
|
||||
ParameterDefinition("dvol2", 0.0, label="端口 2 容积变化率", quantity="volume_flow", unit="m3/s"),
|
||||
ParameterDefinition("dvol3", 0.0, label="端口 3 容积变化率", quantity="volume_flow", unit="m3/s"),
|
||||
ParameterDefinition("dvol4", 0.0, label="端口 4 容积变化率", quantity="volume_flow", unit="m3/s"),
|
||||
)
|
||||
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES + (
|
||||
ResultVariableDefinition("vol", "气室总容积", "volume", "m3", "derived", 100),
|
||||
ResultVariableDefinition("dvol", "总容积变化率", "volume_flow", "m3/s", "derived", 110),
|
||||
)
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="PNCH012 变容气室",
|
||||
library_id="amesim",
|
||||
category_id="storage",
|
||||
symbol="amesim_pnch012",
|
||||
ports=(
|
||||
PortDisplaySpec("port_1", "left", order=10),
|
||||
PortDisplaySpec("port_2", "right", order=20),
|
||||
PortDisplaySpec("port_3", "left", order=30),
|
||||
PortDisplaySpec("port_4", "right", order=40),
|
||||
),
|
||||
order=20,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
*,
|
||||
cvol0: float = 0.015,
|
||||
kth: float = 0.0,
|
||||
sth: float = 0.1,
|
||||
extemp: float = 293.15,
|
||||
gi: float = 1.0,
|
||||
p0: float = 100000.0,
|
||||
T0: float = 293.15,
|
||||
vol1: float = 0.0,
|
||||
vol2: float = 0.0,
|
||||
vol3: float = 0.0,
|
||||
vol4: float = 0.0,
|
||||
dvol1: float = 0.0,
|
||||
dvol2: float = 0.0,
|
||||
dvol3: float = 0.0,
|
||||
dvol4: float = 0.0,
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values(
|
||||
{
|
||||
"cvol0": cvol0,
|
||||
"kth": kth,
|
||||
"sth": sth,
|
||||
"extemp": extemp,
|
||||
"gi": gi,
|
||||
"p0": p0,
|
||||
"T0": T0,
|
||||
"vol1": vol1,
|
||||
"vol2": vol2,
|
||||
"vol3": vol3,
|
||||
"vol4": vol4,
|
||||
"dvol1": dvol1,
|
||||
"dvol2": dvol2,
|
||||
"dvol3": dvol3,
|
||||
"dvol4": dvol4,
|
||||
}
|
||||
)
|
||||
self.medium = medium
|
||||
self.cvol0 = float(cvol0)
|
||||
self.kth = float(kth)
|
||||
self.sth = float(sth)
|
||||
self.extemp = float(extemp)
|
||||
self.gi = normalize_amesim_gas_index(gi)
|
||||
self.p0 = float(p0)
|
||||
self.T0 = float(T0)
|
||||
self.external_volumes = {
|
||||
"port_1": float(vol1),
|
||||
"port_2": float(vol2),
|
||||
"port_3": float(vol3),
|
||||
"port_4": float(vol4),
|
||||
}
|
||||
self.external_volume_rates = {
|
||||
"port_1": float(dvol1),
|
||||
"port_2": float(dvol2),
|
||||
"port_3": float(dvol3),
|
||||
"port_4": float(dvol4),
|
||||
}
|
||||
if self.total_volume() <= 0.0:
|
||||
raise ValueError("PNCH012 total volume must be positive.")
|
||||
m0 = medium.density(self.p0, self.T0) * self.total_volume()
|
||||
U0 = m0 * medium.specific_internal_energy(self.T0)
|
||||
self.state = VolumeState(m=m0, U=U0)
|
||||
initial_h = medium.specific_enthalpy(self.T0)
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
||||
port = self.register_declared_port(port_name)
|
||||
port.p = self.p0
|
||||
port.h_outflow = initial_h
|
||||
setattr(self, port_name, port)
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimPnch012":
|
||||
return cls(name=name, medium=medium, **dict(parameters))
|
||||
|
||||
def total_volume(self) -> float:
|
||||
minimum_volume = self.cvol0 / 100.0
|
||||
return max(self.cvol0 + sum(self.external_volumes.values()), minimum_volume)
|
||||
|
||||
def total_volume_rate(self) -> float:
|
||||
if self.total_volume() <= self.cvol0 / 100.0:
|
||||
return 0.0
|
||||
return sum(self.external_volume_rates.values())
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.total_volume())
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
||||
port = self.get_port(port_name)
|
||||
port.p = props.p
|
||||
port.h_outflow = props.h
|
||||
return props
|
||||
|
||||
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||
return self.properties()
|
||||
|
||||
def thermal_energy_flow_w(self, temperature: float) -> float:
|
||||
return self.kth * self.sth * (self.extemp - temperature)
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
props = self.properties()
|
||||
return {
|
||||
"m": self.state.m,
|
||||
"U": self.state.U,
|
||||
"p": props.p,
|
||||
"T": props.T,
|
||||
"rho": props.rho,
|
||||
"u": props.u,
|
||||
"h": props.h,
|
||||
"vol": self.total_volume(),
|
||||
"dvol": self.total_volume_rate(),
|
||||
}
|
||||
|
||||
def state_derivative_from_ports(self, connected_h: Mapping[str, float]) -> list[float]:
|
||||
props = self.properties()
|
||||
mass_derivative = 0.0
|
||||
energy_derivative = 0.0
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4"):
|
||||
port = self.get_port(port_name)
|
||||
inlet_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=port.m_flow,
|
||||
connected_h=connected_h[port_name],
|
||||
internal_h=props.h,
|
||||
)
|
||||
mass_derivative += port.m_flow
|
||||
energy_derivative += port.m_flow * inlet_h
|
||||
energy_derivative += self.thermal_energy_flow_w(props.T)
|
||||
energy_derivative -= props.p * self.total_volume_rate()
|
||||
return VolumeState(m=mass_derivative, U=energy_derivative).as_vector()
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
pressure = self.medium.properties_from_mU(
|
||||
self.state.m,
|
||||
self.state.U,
|
||||
self.total_volume(),
|
||||
).p
|
||||
return tuple(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{port_name}_pressure_state",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="state",
|
||||
variables=(f"{self.name}.{port_name}.p", f"{self.name}.state"),
|
||||
role="effort",
|
||||
value=self.get_port(port_name).p - pressure,
|
||||
)
|
||||
for port_name in ("port_1", "port_2", "port_3", "port_4")
|
||||
)
|
||||
@@ -15,7 +15,7 @@ from app.simulation.core.metadata import (
|
||||
from app.simulation.core.ports import PortDefinition, PortState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.medium import GasMedium
|
||||
|
||||
|
||||
class Component(ABC):
|
||||
@@ -185,7 +185,7 @@ class Component(ABC):
|
||||
cls,
|
||||
*,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
medium: GasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> Component:
|
||||
"""Create a catalog model from normalized SI parameters."""
|
||||
|
||||
@@ -5,6 +5,7 @@ from typing import Literal
|
||||
|
||||
|
||||
PortDisplaySide = Literal["left", "right"]
|
||||
ComponentCatalogRole = Literal["amesimGasMediumDefinition"]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -42,6 +43,7 @@ class ComponentDisplaySpec:
|
||||
symbol: str
|
||||
ports: tuple[PortDisplaySpec, ...]
|
||||
order: int = 0
|
||||
role: ComponentCatalogRole | None = None
|
||||
|
||||
@property
|
||||
def port_by_name(self) -> dict[str, PortDisplaySpec]:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Protocol
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -12,6 +13,53 @@ class ThermodynamicProperties:
|
||||
h: float
|
||||
|
||||
|
||||
class GasMedium(Protocol):
|
||||
"""Thermodynamic contract required by pneumatic components.
|
||||
|
||||
``IdealGasMedium`` is the default implementation. Keeping the component
|
||||
boundary structural allows a later helium/Peng-Robinson implementation to
|
||||
be registered without changing every AMESim component constructor.
|
||||
"""
|
||||
|
||||
name: str
|
||||
R_gas: float
|
||||
cp_ref: float
|
||||
T_ref: float
|
||||
|
||||
@property
|
||||
def cv(self) -> float: ...
|
||||
|
||||
@property
|
||||
def gamma(self) -> float: ...
|
||||
|
||||
def cp_at_temperature(self, T: float) -> float: ...
|
||||
|
||||
def cv_at_temperature(self, T: float) -> float: ...
|
||||
|
||||
def density(self, p: float, T: float) -> float: ...
|
||||
|
||||
def dynamic_viscosity(self, T: float) -> float: ...
|
||||
|
||||
def specific_internal_energy(self, T: float) -> float: ...
|
||||
|
||||
def specific_enthalpy(self, T: float) -> float: ...
|
||||
|
||||
def temperature_from_internal_energy(self, u: float) -> float: ...
|
||||
|
||||
def temperature_from_enthalpy(self, h: float) -> float: ...
|
||||
|
||||
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float: ...
|
||||
|
||||
def pressure(self, m: float, T: float, V: float) -> float: ...
|
||||
|
||||
def properties_from_mU(
|
||||
self,
|
||||
m: float,
|
||||
U: float,
|
||||
V: float,
|
||||
) -> ThermodynamicProperties: ...
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class IdealGasMedium:
|
||||
"""Temperature-dependent ideal-gas air approximation.
|
||||
@@ -27,6 +75,9 @@ class IdealGasMedium:
|
||||
cp_ref: float = 1005.0
|
||||
T_ref: float = 300.0
|
||||
cp_slope: float = 0.0
|
||||
viscosity_ref: float = 1.82e-5
|
||||
viscosity_T_ref: float = 293.15
|
||||
sutherland_constant: float = 110.4
|
||||
|
||||
@property
|
||||
def cv(self) -> float:
|
||||
@@ -45,6 +96,18 @@ class IdealGasMedium:
|
||||
def density(self, p: float, T: float) -> float:
|
||||
return p / (self.R_gas * T)
|
||||
|
||||
def dynamic_viscosity(self, T: float) -> float:
|
||||
"""Return dynamic viscosity using the default air Sutherland law."""
|
||||
|
||||
if T <= 0.0:
|
||||
raise ValueError("Temperature must be positive.")
|
||||
return (
|
||||
self.viscosity_ref
|
||||
* (T / self.viscosity_T_ref) ** 1.5
|
||||
* (self.viscosity_T_ref + self.sutherland_constant)
|
||||
/ (T + self.sutherland_constant)
|
||||
)
|
||||
|
||||
def specific_internal_energy(self, T: float) -> float:
|
||||
delta_T = T - self.T_ref
|
||||
return (
|
||||
@@ -77,6 +140,22 @@ class IdealGasMedium:
|
||||
delta_T = positive_root if abs(positive_root) <= abs(negative_root) else negative_root
|
||||
return self.T_ref + delta_T
|
||||
|
||||
def temperature_from_enthalpy(self, h: float) -> float:
|
||||
reference_enthalpy = self.cp_ref * self.T_ref
|
||||
delta_h = h - reference_enthalpy
|
||||
|
||||
if abs(self.cp_slope) <= 1e-15:
|
||||
return self.T_ref + delta_h / self.cp_ref
|
||||
|
||||
a = 0.5 * self.cp_slope
|
||||
b = self.cp_ref
|
||||
c = -delta_h
|
||||
discriminant = max(b * b - 4.0 * a * c, 0.0)
|
||||
positive_root = (-b + discriminant**0.5) / (2.0 * a)
|
||||
negative_root = (-b - discriminant**0.5) / (2.0 * a)
|
||||
delta_T = positive_root if abs(positive_root) <= abs(negative_root) else negative_root
|
||||
return self.T_ref + delta_T
|
||||
|
||||
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float:
|
||||
if m <= 0.0:
|
||||
raise ValueError("Mass must stay positive when recovering temperature.")
|
||||
|
||||
@@ -6,12 +6,20 @@ from typing import Literal
|
||||
|
||||
|
||||
ResultVariableScope = Literal["component", "port"]
|
||||
ParameterEditor = Literal[
|
||||
"amesimGasReference",
|
||||
"amesimGasPropertyModel",
|
||||
]
|
||||
|
||||
|
||||
SI_UNIT_BY_QUANTITY: dict[str, str] = {
|
||||
"acceleration": "m/s2",
|
||||
"area": "m2",
|
||||
"dimensionless": "",
|
||||
"density": "kg/m³",
|
||||
"flow_coefficient": "kg/(s*Pa^0.5)",
|
||||
"force": "N",
|
||||
"heat_transfer_coefficient": "W/(m2*K)",
|
||||
"internal_energy": "J",
|
||||
"length": "m",
|
||||
"mass": "kg",
|
||||
@@ -20,7 +28,27 @@ SI_UNIT_BY_QUANTITY: dict[str, str] = {
|
||||
"specific_enthalpy": "J/kg",
|
||||
"specific_internal_energy": "J/kg",
|
||||
"temperature": "K",
|
||||
"translational_damping": "N/(m/s)",
|
||||
"translational_stiffness": "N/m",
|
||||
"time": "s",
|
||||
"velocity": "m/s",
|
||||
"volume": "m3",
|
||||
"volume_flow": "m3/s",
|
||||
"windage": "N/(m/s)^2",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ParameterOption:
|
||||
"""One numeric choice exposed by a catalog-backed parameter editor."""
|
||||
|
||||
value: float
|
||||
label: str
|
||||
|
||||
def as_interface_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"value": self.value,
|
||||
"label": self.label,
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +64,9 @@ class ParameterDefinition:
|
||||
minimum: float | None = None
|
||||
maximum: float | None = None
|
||||
minimum_exclusive: bool = False
|
||||
editor: ParameterEditor | None = None
|
||||
options: tuple[ParameterOption, ...] = ()
|
||||
description: str = ""
|
||||
|
||||
def validation_message(self, value: float) -> str | None:
|
||||
if not isfinite(value):
|
||||
@@ -47,6 +78,11 @@ class ParameterDefinition:
|
||||
return f"must be at least {self.minimum:g}"
|
||||
if self.maximum is not None and value > self.maximum:
|
||||
return f"must be at most {self.maximum:g}"
|
||||
if self.options and value not in {
|
||||
float(option.value) for option in self.options
|
||||
}:
|
||||
available = ", ".join(f"{option.value:g}" for option in self.options)
|
||||
return f"must be one of {available}"
|
||||
return None
|
||||
|
||||
def as_interface_dict(self, *, value: float | None = None) -> dict[str, object]:
|
||||
@@ -62,6 +98,14 @@ class ParameterDefinition:
|
||||
payload["minimum"] = self.minimum
|
||||
if self.maximum is not None:
|
||||
payload["maximum"] = self.maximum
|
||||
if self.editor is not None:
|
||||
payload["editor"] = self.editor
|
||||
if self.options:
|
||||
payload["options"] = [
|
||||
option.as_interface_dict() for option in self.options
|
||||
]
|
||||
if self.description:
|
||||
payload["description"] = self.description
|
||||
if value is not None:
|
||||
payload["value"] = value
|
||||
return payload
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import acos, cos, isfinite, log, pi, sqrt
|
||||
|
||||
UNIVERSAL_GAS_CONSTANT = 8.31446261815324
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PengRobinsonFluid:
|
||||
"""Pure-fluid Peng-Robinson equation-of-state helper.
|
||||
|
||||
The class covers the equation-of-state layer plus the enthalpy departure
|
||||
needed to compare AMESim pneumatic ``pn2hpti`` reference enthalpy flows.
|
||||
"""
|
||||
|
||||
name: str
|
||||
molar_mass: float
|
||||
critical_temperature: float
|
||||
critical_pressure: float
|
||||
acentric_factor: float
|
||||
|
||||
@property
|
||||
def specific_gas_constant(self) -> float:
|
||||
return UNIVERSAL_GAS_CONSTANT / self.molar_mass
|
||||
|
||||
@property
|
||||
def a_parameter(self) -> float:
|
||||
return (
|
||||
0.45724
|
||||
* UNIVERSAL_GAS_CONSTANT
|
||||
* UNIVERSAL_GAS_CONSTANT
|
||||
* self.critical_temperature
|
||||
* self.critical_temperature
|
||||
/ self.critical_pressure
|
||||
)
|
||||
|
||||
@property
|
||||
def b_parameter(self) -> float:
|
||||
return 0.07780 * UNIVERSAL_GAS_CONSTANT * self.critical_temperature / self.critical_pressure
|
||||
|
||||
@property
|
||||
def kappa(self) -> float:
|
||||
omega = self.acentric_factor
|
||||
return 0.37464 + 1.54226 * omega - 0.26992 * omega * omega
|
||||
|
||||
def alpha(self, temperature: float) -> float:
|
||||
self._validate_temperature(temperature)
|
||||
reduced_temperature = temperature / self.critical_temperature
|
||||
return (1.0 + self.kappa * (1.0 - sqrt(reduced_temperature))) ** 2.0
|
||||
|
||||
def alpha_temperature_derivative(self, temperature: float) -> float:
|
||||
self._validate_temperature(temperature)
|
||||
reduced_temperature = temperature / self.critical_temperature
|
||||
sqrt_reduced_temperature = sqrt(reduced_temperature)
|
||||
alpha_base = 1.0 + self.kappa * (1.0 - sqrt_reduced_temperature)
|
||||
return -(
|
||||
alpha_base
|
||||
* self.kappa
|
||||
/ (self.critical_temperature * sqrt_reduced_temperature)
|
||||
)
|
||||
|
||||
def attractive_parameter(self, temperature: float) -> float:
|
||||
return self.a_parameter * self.alpha(temperature)
|
||||
|
||||
def attractive_parameter_temperature_derivative(self, temperature: float) -> float:
|
||||
return self.a_parameter * self.alpha_temperature_derivative(temperature)
|
||||
|
||||
def pressure_from_molar_volume(self, temperature: float, molar_volume: float) -> float:
|
||||
self._validate_temperature(temperature)
|
||||
if molar_volume <= self.b_parameter:
|
||||
raise ValueError("Molar volume must be larger than Peng-Robinson b parameter.")
|
||||
a_alpha = self.attractive_parameter(temperature)
|
||||
b = self.b_parameter
|
||||
repulsive = UNIVERSAL_GAS_CONSTANT * temperature / (molar_volume - b)
|
||||
attractive = a_alpha / (molar_volume * (molar_volume + b) + b * (molar_volume - b))
|
||||
return repulsive - attractive
|
||||
|
||||
def pressure_from_density(self, temperature: float, density: float) -> float:
|
||||
if density <= 0.0:
|
||||
raise ValueError("Density must be positive.")
|
||||
return self.pressure_from_molar_volume(temperature, self.molar_mass / density)
|
||||
|
||||
def reduced_parameters(self, pressure: float, temperature: float) -> tuple[float, float]:
|
||||
self._validate_pressure_temperature(pressure, temperature)
|
||||
a_alpha = self.attractive_parameter(temperature)
|
||||
b = self.b_parameter
|
||||
A = a_alpha * pressure / (UNIVERSAL_GAS_CONSTANT * UNIVERSAL_GAS_CONSTANT * temperature * temperature)
|
||||
B = b * pressure / (UNIVERSAL_GAS_CONSTANT * temperature)
|
||||
return A, B
|
||||
|
||||
def compressibility_roots(self, pressure: float, temperature: float) -> tuple[float, ...]:
|
||||
A, B = self.reduced_parameters(pressure, temperature)
|
||||
coefficients = (
|
||||
-(1.0 - B),
|
||||
A - 3.0 * B * B - 2.0 * B,
|
||||
-(A * B - B * B - B * B * B),
|
||||
)
|
||||
roots = _real_cubic_roots(*coefficients)
|
||||
physical_roots = tuple(sorted(root for root in roots if root > B and isfinite(root)))
|
||||
if not physical_roots:
|
||||
raise ValueError("Peng-Robinson cubic produced no physical compressibility root.")
|
||||
return physical_roots
|
||||
|
||||
def compressibility_factor(
|
||||
self,
|
||||
pressure: float,
|
||||
temperature: float,
|
||||
phase: str = "vapor",
|
||||
) -> float:
|
||||
roots = self.compressibility_roots(pressure, temperature)
|
||||
if phase == "vapor":
|
||||
return roots[-1]
|
||||
if phase == "liquid":
|
||||
return roots[0]
|
||||
if phase == "stable-single-root":
|
||||
return roots[-1]
|
||||
raise ValueError(f"Unsupported phase selector: {phase!r}")
|
||||
|
||||
def molar_volume(
|
||||
self,
|
||||
pressure: float,
|
||||
temperature: float,
|
||||
phase: str = "vapor",
|
||||
) -> float:
|
||||
z = self.compressibility_factor(pressure, temperature, phase=phase)
|
||||
return z * UNIVERSAL_GAS_CONSTANT * temperature / pressure
|
||||
|
||||
def density(
|
||||
self,
|
||||
pressure: float,
|
||||
temperature: float,
|
||||
phase: str = "vapor",
|
||||
) -> float:
|
||||
return self.molar_mass / self.molar_volume(pressure, temperature, phase=phase)
|
||||
|
||||
def residual_specific_enthalpy(
|
||||
self,
|
||||
pressure: float,
|
||||
temperature: float,
|
||||
phase: str = "vapor",
|
||||
) -> float:
|
||||
"""Return Peng-Robinson enthalpy departure from ideal gas, J/kg."""
|
||||
self._validate_pressure_temperature(pressure, temperature)
|
||||
z = self.compressibility_factor(pressure, temperature, phase=phase)
|
||||
_, B = self.reduced_parameters(pressure, temperature)
|
||||
b = self.b_parameter
|
||||
attractive = self.attractive_parameter(temperature)
|
||||
d_attractive_d_temperature = (
|
||||
self.attractive_parameter_temperature_derivative(temperature)
|
||||
)
|
||||
log_argument = (z + (1.0 + sqrt(2.0)) * B) / (
|
||||
z + (1.0 - sqrt(2.0)) * B
|
||||
)
|
||||
residual_molar_enthalpy = (
|
||||
UNIVERSAL_GAS_CONSTANT * temperature * (z - 1.0)
|
||||
+ (
|
||||
temperature * d_attractive_d_temperature
|
||||
- attractive
|
||||
)
|
||||
* log(log_argument)
|
||||
/ (2.0 * sqrt(2.0) * b)
|
||||
)
|
||||
return residual_molar_enthalpy / self.molar_mass
|
||||
|
||||
@staticmethod
|
||||
def _validate_temperature(temperature: float) -> None:
|
||||
if temperature <= 0.0:
|
||||
raise ValueError("Temperature must be positive.")
|
||||
|
||||
@classmethod
|
||||
def _validate_pressure_temperature(cls, pressure: float, temperature: float) -> None:
|
||||
if pressure <= 0.0:
|
||||
raise ValueError("Pressure must be positive.")
|
||||
cls._validate_temperature(temperature)
|
||||
|
||||
HELIUM_PR = PengRobinsonFluid(
|
||||
name="helium",
|
||||
molar_mass=0.004002602,
|
||||
critical_temperature=5.1953,
|
||||
critical_pressure=227_460.0,
|
||||
acentric_factor=-0.385,
|
||||
)
|
||||
|
||||
NITROGEN_PR = PengRobinsonFluid(
|
||||
name="nitrogen",
|
||||
molar_mass=0.0280134,
|
||||
critical_temperature=126.192,
|
||||
critical_pressure=3.3958e6,
|
||||
acentric_factor=0.0372,
|
||||
)
|
||||
|
||||
AIR_PR = PengRobinsonFluid(
|
||||
name="air",
|
||||
molar_mass=0.02896513,
|
||||
critical_temperature=132.5306,
|
||||
critical_pressure=3.786e6,
|
||||
acentric_factor=0.0335,
|
||||
)
|
||||
|
||||
|
||||
def _real_cubic_roots(a: float, b: float, c: float) -> tuple[float, ...]:
|
||||
"""Return real roots for x**3 + a*x**2 + b*x + c = 0."""
|
||||
|
||||
depressed_p = b - a * a / 3.0
|
||||
depressed_q = 2.0 * a * a * a / 27.0 - a * b / 3.0 + c
|
||||
discriminant = (depressed_q / 2.0) ** 2.0 + (depressed_p / 3.0) ** 3.0
|
||||
offset = -a / 3.0
|
||||
tolerance = 1e-14
|
||||
|
||||
if discriminant > tolerance:
|
||||
sqrt_discriminant = sqrt(discriminant)
|
||||
u = _real_cube_root(-depressed_q / 2.0 + sqrt_discriminant)
|
||||
v = _real_cube_root(-depressed_q / 2.0 - sqrt_discriminant)
|
||||
return (u + v + offset,)
|
||||
|
||||
if abs(discriminant) <= tolerance:
|
||||
u = _real_cube_root(-depressed_q / 2.0)
|
||||
return tuple(sorted({2.0 * u + offset, -u + offset}))
|
||||
|
||||
if depressed_p >= 0.0:
|
||||
raise ValueError("Unexpected cubic state with three real roots and non-negative p.")
|
||||
radius = 2.0 * sqrt(-depressed_p / 3.0)
|
||||
argument = (3.0 * depressed_q / (2.0 * depressed_p)) * sqrt(-3.0 / depressed_p)
|
||||
argument = max(-1.0, min(1.0, argument))
|
||||
theta = acos(argument) / 3.0
|
||||
roots = [
|
||||
radius * cos(theta - 2.0 * pi * index / 3.0) + offset
|
||||
for index in range(3)
|
||||
]
|
||||
return tuple(sorted(roots))
|
||||
|
||||
|
||||
def _real_cube_root(value: float) -> float:
|
||||
if value == 0.0:
|
||||
return 0.0
|
||||
return (1.0 if value > 0.0 else -1.0) * abs(value) ** (1.0 / 3.0)
|
||||
@@ -90,6 +90,76 @@ class PortDefinition:
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def mechanical_translational(
|
||||
cls,
|
||||
name: str,
|
||||
*,
|
||||
nominal_role: Literal["inlet", "outlet", "bidirectional"] = "bidirectional",
|
||||
) -> PortDefinition:
|
||||
return cls(
|
||||
name=name,
|
||||
kind="physical",
|
||||
domain="mechanical",
|
||||
nominal_role=nominal_role,
|
||||
positive_flow_direction="intoComponent",
|
||||
variables=(
|
||||
PortVariableDefinition(
|
||||
"x",
|
||||
"effort",
|
||||
"equal",
|
||||
label="位移",
|
||||
quantity="length",
|
||||
unit="m",
|
||||
order=10,
|
||||
),
|
||||
PortVariableDefinition(
|
||||
"v",
|
||||
"effort",
|
||||
"equal",
|
||||
label="速度",
|
||||
quantity="velocity",
|
||||
unit="m/s",
|
||||
order=20,
|
||||
),
|
||||
PortVariableDefinition(
|
||||
"f",
|
||||
"flow",
|
||||
"sumToZero",
|
||||
label="力",
|
||||
quantity="force",
|
||||
unit="N",
|
||||
order=30,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def signal(
|
||||
cls,
|
||||
name: str,
|
||||
*,
|
||||
nominal_role: Literal["input", "output"],
|
||||
domain: str = "signal",
|
||||
) -> PortDefinition:
|
||||
return cls(
|
||||
name=name,
|
||||
kind="signal",
|
||||
domain=domain,
|
||||
nominal_role=nominal_role,
|
||||
variables=(
|
||||
PortVariableDefinition(
|
||||
"signal",
|
||||
"signal",
|
||||
"directed",
|
||||
label="信号值",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
order=10,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def as_interface_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"name": self.name,
|
||||
@@ -108,6 +178,10 @@ class PortState:
|
||||
p: float = 0.0
|
||||
m_flow: float = 0.0
|
||||
h_outflow: float = 0.0
|
||||
signal: float = 0.0
|
||||
x: float = 0.0
|
||||
v: float = 0.0
|
||||
f: float = 0.0
|
||||
definition: PortDefinition | None = field(default=None, repr=False, compare=False)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.simulation.examples.test_mql.system import (
|
||||
TestMqlRunConfig,
|
||||
TestMqlSimulationResult,
|
||||
TestMqlSystem,
|
||||
)
|
||||
from app.simulation.examples.test_mql.run import (
|
||||
PreparedTestMqlRun,
|
||||
TestMqlRunResult,
|
||||
prepare_test_mql_run,
|
||||
run_prepared_test_mql,
|
||||
run_test_mql,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"PreparedTestMqlRun",
|
||||
"TestMqlRunConfig",
|
||||
"TestMqlRunResult",
|
||||
"TestMqlSimulationResult",
|
||||
"TestMqlSystem",
|
||||
"prepare_test_mql_run",
|
||||
"run_prepared_test_mql",
|
||||
"run_test_mql",
|
||||
]
|
||||
@@ -0,0 +1,77 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from app.simulation.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_comparison import TestMqlComparisonResult
|
||||
from app.simulation.reporting.test_mql_observations import (
|
||||
TestMqlObservationCatalog,
|
||||
build_test_mql_observation_catalog,
|
||||
)
|
||||
from app.simulation.reporting.test_mql_output_schema import (
|
||||
TestMqlOutputSchema,
|
||||
build_test_mql_output_schema,
|
||||
)
|
||||
from app.simulation.reporting.test_mql_output_validation import (
|
||||
TestMqlValidatedOutput,
|
||||
compare_validated_test_mql_output,
|
||||
validate_test_mql_output,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlBaselineRun:
|
||||
amesim_results: AmesimResults
|
||||
observation_catalog: TestMqlObservationCatalog
|
||||
output_schema: TestMqlOutputSchema
|
||||
output: TestMqlValidatedOutput
|
||||
comparison: TestMqlComparisonResult
|
||||
|
||||
@property
|
||||
def sample_count(self) -> int:
|
||||
return len(self.output.times)
|
||||
|
||||
@property
|
||||
def signal_count(self) -> int:
|
||||
return len(self.output.data_paths)
|
||||
|
||||
|
||||
def run_test_mql_baseline_passthrough(
|
||||
archive_path: Path,
|
||||
*,
|
||||
data_paths: tuple[str, ...] | list[str] | None = None,
|
||||
) -> TestMqlBaselineRun:
|
||||
amesim_results = load_test_mql_amesim_results(archive_path)
|
||||
observation_catalog = build_test_mql_observation_catalog(amesim_results)
|
||||
output_schema = build_test_mql_output_schema(
|
||||
amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
)
|
||||
selected_paths = tuple(data_paths) if data_paths is not None else output_schema.data_paths()
|
||||
baseline_series = observation_catalog.baseline_series_by_data_path(
|
||||
amesim_results,
|
||||
selected_paths,
|
||||
)
|
||||
output = validate_test_mql_output(
|
||||
times=amesim_results.times,
|
||||
series_by_data_path=baseline_series,
|
||||
schema=output_schema,
|
||||
data_paths=selected_paths,
|
||||
require_all_schema_paths=data_paths is None,
|
||||
)
|
||||
comparison = compare_validated_test_mql_output(
|
||||
times=output.times,
|
||||
series_by_data_path=output.series_by_data_path,
|
||||
schema=output_schema,
|
||||
amesim_results=amesim_results,
|
||||
data_paths=output.data_paths,
|
||||
require_all_schema_paths=data_paths is None,
|
||||
)
|
||||
return TestMqlBaselineRun(
|
||||
amesim_results=amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
output_schema=output_schema,
|
||||
output=output,
|
||||
comparison=comparison,
|
||||
)
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,468 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic import m3_to_cm3
|
||||
from app.simulation.reporting.amesim_results import AmesimResults, load_test_mql_amesim_results
|
||||
from app.simulation.reporting.test_mql_comparison import TestMqlComparisonResult
|
||||
from app.simulation.reporting.test_mql_observations import (
|
||||
TestMqlObservationCatalog,
|
||||
build_test_mql_observation_catalog,
|
||||
)
|
||||
from app.simulation.reporting.test_mql_output_schema import (
|
||||
TestMqlOutputSchema,
|
||||
build_test_mql_output_schema,
|
||||
)
|
||||
from app.simulation.reporting.test_mql_output_validation import (
|
||||
TestMqlValidatedOutput,
|
||||
compare_validated_test_mql_output,
|
||||
validate_test_mql_output,
|
||||
)
|
||||
from app.simulation.reporting.test_mql_variables import build_test_mql_variable_catalog
|
||||
from app.simulation.examples.test_mql.mechanical import (
|
||||
TestMqlMechanicalAssembly,
|
||||
build_test_mql_mechanical_assembly,
|
||||
)
|
||||
from app.simulation.examples.test_mql.pneumatic import (
|
||||
TestMqlPneumaticAssembly,
|
||||
build_test_mql_pneumatic_assembly,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlComputedPistonGeometryRun:
|
||||
amesim_results: AmesimResults
|
||||
observation_catalog: TestMqlObservationCatalog
|
||||
output_schema: TestMqlOutputSchema
|
||||
mechanical_assembly: TestMqlMechanicalAssembly
|
||||
output: TestMqlValidatedOutput
|
||||
comparison: TestMqlComparisonResult
|
||||
|
||||
@property
|
||||
def sample_count(self) -> int:
|
||||
return len(self.output.times)
|
||||
|
||||
@property
|
||||
def signal_count(self) -> int:
|
||||
return len(self.output.data_paths)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlComputedGeometryRun:
|
||||
amesim_results: AmesimResults
|
||||
observation_catalog: TestMqlObservationCatalog
|
||||
output_schema: TestMqlOutputSchema
|
||||
mechanical_assembly: TestMqlMechanicalAssembly
|
||||
pneumatic_assembly: TestMqlPneumaticAssembly
|
||||
output: TestMqlValidatedOutput
|
||||
comparison: TestMqlComparisonResult
|
||||
|
||||
@property
|
||||
def sample_count(self) -> int:
|
||||
return len(self.output.times)
|
||||
|
||||
@property
|
||||
def signal_count(self) -> int:
|
||||
return len(self.output.data_paths)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlComputedLineRelationsRun:
|
||||
amesim_results: AmesimResults
|
||||
observation_catalog: TestMqlObservationCatalog
|
||||
output_schema: TestMqlOutputSchema
|
||||
output: TestMqlValidatedOutput
|
||||
comparison: TestMqlComparisonResult
|
||||
|
||||
@property
|
||||
def sample_count(self) -> int:
|
||||
return len(self.output.times)
|
||||
|
||||
@property
|
||||
def signal_count(self) -> int:
|
||||
return len(self.output.data_paths)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlComputedPneumaticRelationsRun:
|
||||
amesim_results: AmesimResults
|
||||
observation_catalog: TestMqlObservationCatalog
|
||||
output_schema: TestMqlOutputSchema
|
||||
output: TestMqlValidatedOutput
|
||||
comparison: TestMqlComparisonResult
|
||||
|
||||
@property
|
||||
def sample_count(self) -> int:
|
||||
return len(self.output.times)
|
||||
|
||||
@property
|
||||
def signal_count(self) -> int:
|
||||
return len(self.output.data_paths)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlComputedMechanicalRelationsRun:
|
||||
amesim_results: AmesimResults
|
||||
observation_catalog: TestMqlObservationCatalog
|
||||
output_schema: TestMqlOutputSchema
|
||||
mechanical_assembly: TestMqlMechanicalAssembly
|
||||
output: TestMqlValidatedOutput
|
||||
comparison: TestMqlComparisonResult
|
||||
|
||||
@property
|
||||
def sample_count(self) -> int:
|
||||
return len(self.output.times)
|
||||
|
||||
@property
|
||||
def signal_count(self) -> int:
|
||||
return len(self.output.data_paths)
|
||||
|
||||
|
||||
def run_test_mql_computed_piston_geometry(
|
||||
archive_path: Path,
|
||||
) -> TestMqlComputedPistonGeometryRun:
|
||||
amesim_results = load_test_mql_amesim_results(archive_path)
|
||||
observation_catalog = build_test_mql_observation_catalog(amesim_results)
|
||||
output_schema = build_test_mql_output_schema(
|
||||
amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
)
|
||||
variable_catalog = build_test_mql_variable_catalog(amesim_results)
|
||||
mechanical_assembly = build_test_mql_mechanical_assembly(
|
||||
amesim_results=amesim_results,
|
||||
variable_catalog=variable_catalog,
|
||||
)
|
||||
output_series = _compute_piston_geometry_series(amesim_results, mechanical_assembly)
|
||||
output_data_paths = tuple(output_series)
|
||||
output = validate_test_mql_output(
|
||||
times=amesim_results.times,
|
||||
series_by_data_path=output_series,
|
||||
schema=output_schema,
|
||||
data_paths=output_data_paths,
|
||||
)
|
||||
comparison = compare_validated_test_mql_output(
|
||||
times=output.times,
|
||||
series_by_data_path=output.series_by_data_path,
|
||||
schema=output_schema,
|
||||
amesim_results=amesim_results,
|
||||
data_paths=output.data_paths,
|
||||
)
|
||||
return TestMqlComputedPistonGeometryRun(
|
||||
amesim_results=amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
output_schema=output_schema,
|
||||
mechanical_assembly=mechanical_assembly,
|
||||
output=output,
|
||||
comparison=comparison,
|
||||
)
|
||||
|
||||
|
||||
def run_test_mql_computed_geometry(
|
||||
archive_path: Path,
|
||||
) -> TestMqlComputedGeometryRun:
|
||||
amesim_results = load_test_mql_amesim_results(archive_path)
|
||||
observation_catalog = build_test_mql_observation_catalog(amesim_results)
|
||||
output_schema = build_test_mql_output_schema(
|
||||
amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
)
|
||||
variable_catalog = build_test_mql_variable_catalog(amesim_results)
|
||||
mechanical_assembly = build_test_mql_mechanical_assembly(
|
||||
amesim_results=amesim_results,
|
||||
variable_catalog=variable_catalog,
|
||||
)
|
||||
pneumatic_assembly = build_test_mql_pneumatic_assembly()
|
||||
output_series = {
|
||||
**_compute_piston_geometry_series(amesim_results, mechanical_assembly),
|
||||
**_compute_variable_chamber_volume_series(
|
||||
amesim_results,
|
||||
mechanical_assembly,
|
||||
pneumatic_assembly,
|
||||
),
|
||||
}
|
||||
output_data_paths = tuple(output_series)
|
||||
output = validate_test_mql_output(
|
||||
times=amesim_results.times,
|
||||
series_by_data_path=output_series,
|
||||
schema=output_schema,
|
||||
data_paths=output_data_paths,
|
||||
)
|
||||
comparison = compare_validated_test_mql_output(
|
||||
times=output.times,
|
||||
series_by_data_path=output.series_by_data_path,
|
||||
schema=output_schema,
|
||||
amesim_results=amesim_results,
|
||||
data_paths=output.data_paths,
|
||||
)
|
||||
return TestMqlComputedGeometryRun(
|
||||
amesim_results=amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
output_schema=output_schema,
|
||||
mechanical_assembly=mechanical_assembly,
|
||||
pneumatic_assembly=pneumatic_assembly,
|
||||
output=output,
|
||||
comparison=comparison,
|
||||
)
|
||||
|
||||
|
||||
def run_test_mql_computed_line_relations(
|
||||
archive_path: Path,
|
||||
) -> TestMqlComputedLineRelationsRun:
|
||||
amesim_results = load_test_mql_amesim_results(archive_path)
|
||||
observation_catalog = build_test_mql_observation_catalog(amesim_results)
|
||||
output_schema = build_test_mql_output_schema(
|
||||
amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
)
|
||||
output_series = _compute_line_reversed_series(amesim_results, observation_catalog)
|
||||
output_data_paths = tuple(output_series)
|
||||
output = validate_test_mql_output(
|
||||
times=amesim_results.times,
|
||||
series_by_data_path=output_series,
|
||||
schema=output_schema,
|
||||
data_paths=output_data_paths,
|
||||
)
|
||||
comparison = compare_validated_test_mql_output(
|
||||
times=output.times,
|
||||
series_by_data_path=output.series_by_data_path,
|
||||
schema=output_schema,
|
||||
amesim_results=amesim_results,
|
||||
data_paths=output.data_paths,
|
||||
)
|
||||
return TestMqlComputedLineRelationsRun(
|
||||
amesim_results=amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
output_schema=output_schema,
|
||||
output=output,
|
||||
comparison=comparison,
|
||||
)
|
||||
|
||||
|
||||
def run_test_mql_computed_pneumatic_relations(
|
||||
archive_path: Path,
|
||||
) -> TestMqlComputedPneumaticRelationsRun:
|
||||
amesim_results = load_test_mql_amesim_results(archive_path)
|
||||
observation_catalog = build_test_mql_observation_catalog(amesim_results)
|
||||
output_schema = build_test_mql_output_schema(
|
||||
amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
)
|
||||
output_series = {
|
||||
**_compute_chamber_duplicate_series(amesim_results, observation_catalog),
|
||||
**_compute_orifice_reversed_series(amesim_results, observation_catalog),
|
||||
}
|
||||
output_data_paths = tuple(output_series)
|
||||
output = validate_test_mql_output(
|
||||
times=amesim_results.times,
|
||||
series_by_data_path=output_series,
|
||||
schema=output_schema,
|
||||
data_paths=output_data_paths,
|
||||
)
|
||||
comparison = compare_validated_test_mql_output(
|
||||
times=output.times,
|
||||
series_by_data_path=output.series_by_data_path,
|
||||
schema=output_schema,
|
||||
amesim_results=amesim_results,
|
||||
data_paths=output.data_paths,
|
||||
)
|
||||
return TestMqlComputedPneumaticRelationsRun(
|
||||
amesim_results=amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
output_schema=output_schema,
|
||||
output=output,
|
||||
comparison=comparison,
|
||||
)
|
||||
|
||||
|
||||
def run_test_mql_computed_mechanical_relations(
|
||||
archive_path: Path,
|
||||
) -> TestMqlComputedMechanicalRelationsRun:
|
||||
amesim_results = load_test_mql_amesim_results(archive_path)
|
||||
observation_catalog = build_test_mql_observation_catalog(amesim_results)
|
||||
output_schema = build_test_mql_output_schema(
|
||||
amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
)
|
||||
variable_catalog = build_test_mql_variable_catalog(amesim_results)
|
||||
mechanical_assembly = build_test_mql_mechanical_assembly(
|
||||
amesim_results=amesim_results,
|
||||
variable_catalog=variable_catalog,
|
||||
)
|
||||
output_series = {
|
||||
**_compute_mass_duplicate_series(amesim_results, mechanical_assembly),
|
||||
**_compute_inactive_mass_force_series(amesim_results, mechanical_assembly),
|
||||
**_compute_zero_force_source_series(amesim_results, mechanical_assembly),
|
||||
}
|
||||
output_data_paths = tuple(output_series)
|
||||
output = validate_test_mql_output(
|
||||
times=amesim_results.times,
|
||||
series_by_data_path=output_series,
|
||||
schema=output_schema,
|
||||
data_paths=output_data_paths,
|
||||
)
|
||||
comparison = compare_validated_test_mql_output(
|
||||
times=output.times,
|
||||
series_by_data_path=output.series_by_data_path,
|
||||
schema=output_schema,
|
||||
amesim_results=amesim_results,
|
||||
data_paths=output.data_paths,
|
||||
)
|
||||
return TestMqlComputedMechanicalRelationsRun(
|
||||
amesim_results=amesim_results,
|
||||
observation_catalog=observation_catalog,
|
||||
output_schema=output_schema,
|
||||
mechanical_assembly=mechanical_assembly,
|
||||
output=output,
|
||||
comparison=comparison,
|
||||
)
|
||||
|
||||
|
||||
def _compute_piston_geometry_series(
|
||||
amesim_results: AmesimResults,
|
||||
mechanical_assembly: TestMqlMechanicalAssembly,
|
||||
) -> dict[str, tuple[float, ...]]:
|
||||
series_by_data_path: dict[str, tuple[float, ...]] = {}
|
||||
for alias in sorted(mechanical_assembly.pistons):
|
||||
piston = mechanical_assembly.pistons[alias]
|
||||
geometry = piston.geometry()
|
||||
x4 = amesim_results.series(f"x4@{alias}")
|
||||
x5 = amesim_results.series(f"x5@{alias}")
|
||||
v4 = amesim_results.series(f"v4@{alias}")
|
||||
v5 = amesim_results.series(f"v5@{alias}")
|
||||
series_by_data_path[f"length@{alias}"] = tuple(
|
||||
geometry.chamber_length_mm(port4, port5)
|
||||
for port4, port5 in zip(x4, x5)
|
||||
)
|
||||
series_by_data_path[f"vol1@{alias}"] = tuple(
|
||||
geometry.chamber_volume_cm3(port4, port5)
|
||||
for port4, port5 in zip(x4, x5)
|
||||
)
|
||||
series_by_data_path[f"vvol1@{alias}"] = tuple(
|
||||
geometry.chamber_volume_rate_l_min(port4, port5)
|
||||
for port4, port5 in zip(v4, v5)
|
||||
)
|
||||
return series_by_data_path
|
||||
|
||||
|
||||
def _compute_variable_chamber_volume_series(
|
||||
amesim_results: AmesimResults,
|
||||
mechanical_assembly: TestMqlMechanicalAssembly,
|
||||
pneumatic_assembly: TestMqlPneumaticAssembly,
|
||||
) -> dict[str, tuple[float, ...]]:
|
||||
series_by_data_path: dict[str, tuple[float, ...]] = {}
|
||||
for chamber_alias in sorted(pneumatic_assembly.variable_chambers):
|
||||
chamber = pneumatic_assembly.variable_chambers[chamber_alias]
|
||||
piston_alias = _piston_alias_for_variable_chamber(chamber_alias)
|
||||
piston = mechanical_assembly.pistons[piston_alias]
|
||||
geometry = piston.geometry()
|
||||
x4 = amesim_results.series(f"x4@{piston_alias}")
|
||||
x5 = amesim_results.series(f"x5@{piston_alias}")
|
||||
dead_volume_cm3 = m3_to_cm3(chamber.dead_volume)
|
||||
series_by_data_path[f"vol@{chamber_alias}"] = tuple(
|
||||
dead_volume_cm3 + geometry.chamber_volume_cm3(port4, port5)
|
||||
for port4, port5 in zip(x4, x5)
|
||||
)
|
||||
return series_by_data_path
|
||||
|
||||
|
||||
def _piston_alias_for_variable_chamber(chamber_alias: str) -> str:
|
||||
if not chamber_alias.startswith("pn_c1"):
|
||||
raise ValueError(f"Unexpected PNCH012 alias: {chamber_alias}")
|
||||
return chamber_alias.replace("pn_c1", "pn_brp2", 1)
|
||||
|
||||
|
||||
def _compute_mass_duplicate_series(
|
||||
amesim_results: AmesimResults,
|
||||
mechanical_assembly: TestMqlMechanicalAssembly,
|
||||
) -> dict[str, tuple[float, ...]]:
|
||||
series_by_data_path: dict[str, tuple[float, ...]] = {}
|
||||
for alias in sorted(mechanical_assembly.masses):
|
||||
for signal_name in ("x1", "v1", "acc1"):
|
||||
source_path = f"{signal_name}@{alias}"
|
||||
duplicate_path = f"{signal_name}dup@{alias}"
|
||||
series_by_data_path[duplicate_path] = tuple(
|
||||
-value for value in amesim_results.series(source_path)
|
||||
)
|
||||
return series_by_data_path
|
||||
|
||||
|
||||
def _compute_inactive_mass_force_series(
|
||||
amesim_results: AmesimResults,
|
||||
mechanical_assembly: TestMqlMechanicalAssembly,
|
||||
) -> dict[str, tuple[float, ...]]:
|
||||
series_by_data_path: dict[str, tuple[float, ...]] = {}
|
||||
for alias in sorted(mechanical_assembly.masses):
|
||||
mass = mechanical_assembly.masses[alias].endstop()
|
||||
x1 = amesim_results.series(f"x1@{alias}")
|
||||
v1 = amesim_results.series(f"v1@{alias}")
|
||||
series_by_data_path[f"Fmin@{alias}"] = tuple(
|
||||
mass.lower_static_force_magnitude(displacement)
|
||||
for displacement in x1
|
||||
)
|
||||
series_by_data_path[f"Fvisc@{alias}"] = tuple(
|
||||
mass.viscous_friction_force(velocity)
|
||||
for velocity in v1
|
||||
)
|
||||
series_by_data_path[f"Ffric@{alias}"] = tuple(0.0 for _ in x1)
|
||||
return series_by_data_path
|
||||
|
||||
|
||||
def _compute_zero_force_source_series(
|
||||
amesim_results: AmesimResults,
|
||||
mechanical_assembly: TestMqlMechanicalAssembly,
|
||||
) -> dict[str, tuple[float, ...]]:
|
||||
return {
|
||||
f"fzero@{alias}": tuple(0.0 for _ in amesim_results.times)
|
||||
for alias in sorted(mechanical_assembly.zero_force_sources)
|
||||
}
|
||||
|
||||
|
||||
def _compute_chamber_duplicate_series(
|
||||
amesim_results: AmesimResults,
|
||||
observation_catalog: TestMqlObservationCatalog,
|
||||
) -> dict[str, tuple[float, ...]]:
|
||||
series_by_data_path: dict[str, tuple[float, ...]] = {}
|
||||
for binding in observation_catalog.chambers.bindings:
|
||||
pressure_series = amesim_results.series(binding.pressure_path)
|
||||
temperature_series = amesim_results.series(binding.temperature_path)
|
||||
for duplicate_path in binding.pressure_duplicate_paths:
|
||||
series_by_data_path[duplicate_path] = tuple(pressure_series)
|
||||
for duplicate_path in binding.temperature_duplicate_paths:
|
||||
series_by_data_path[duplicate_path] = tuple(temperature_series)
|
||||
return series_by_data_path
|
||||
|
||||
|
||||
def _compute_orifice_reversed_series(
|
||||
amesim_results: AmesimResults,
|
||||
observation_catalog: TestMqlObservationCatalog,
|
||||
) -> dict[str, tuple[float, ...]]:
|
||||
series_by_data_path: dict[str, tuple[float, ...]] = {}
|
||||
for binding in observation_catalog.orifices.bindings:
|
||||
series_by_data_path[binding.reversed_mass_flow_path] = tuple(
|
||||
-value for value in amesim_results.series(binding.primary_mass_flow_path)
|
||||
)
|
||||
series_by_data_path[binding.reversed_enthalpy_flow_path] = tuple(
|
||||
-value for value in amesim_results.series(binding.primary_enthalpy_flow_path)
|
||||
)
|
||||
return series_by_data_path
|
||||
|
||||
|
||||
def _compute_line_reversed_series(
|
||||
amesim_results: AmesimResults,
|
||||
observation_catalog: TestMqlObservationCatalog,
|
||||
) -> dict[str, tuple[float, ...]]:
|
||||
series_by_data_path: dict[str, tuple[float, ...]] = {}
|
||||
for binding in observation_catalog.lines.by_submodel("PNL00R"):
|
||||
if len(binding.mass_flow_paths) != 2 or len(binding.enthalpy_flow_paths) != 2:
|
||||
raise ValueError(f"Expected two PNL00R flow paths for {binding.alias}.")
|
||||
primary_mass_path, reversed_mass_path = binding.mass_flow_paths
|
||||
primary_enthalpy_path, reversed_enthalpy_path = binding.enthalpy_flow_paths
|
||||
series_by_data_path[reversed_mass_path] = tuple(
|
||||
-value for value in amesim_results.series(primary_mass_path)
|
||||
)
|
||||
series_by_data_path[reversed_enthalpy_path] = tuple(
|
||||
-value for value in amesim_results.series(primary_enthalpy_path)
|
||||
)
|
||||
return series_by_data_path
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import operator
|
||||
from dataclasses import dataclass
|
||||
from math import isfinite
|
||||
from typing import Any
|
||||
|
||||
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
|
||||
from app.simulation.examples.test_mql.system import COMPONENT_SPECS, GLOBAL_PARAMETERS
|
||||
|
||||
|
||||
_BINARY_OPERATORS = {
|
||||
ast.Add: operator.add,
|
||||
ast.Sub: operator.sub,
|
||||
ast.Mult: operator.mul,
|
||||
ast.Div: operator.truediv,
|
||||
ast.Pow: operator.pow,
|
||||
}
|
||||
_UNARY_OPERATORS = {
|
||||
ast.UAdd: operator.pos,
|
||||
ast.USub: operator.neg,
|
||||
}
|
||||
|
||||
|
||||
class TestMqlExpressionError(ValueError):
|
||||
"""Raised when an AMESim parameter expression cannot be resolved safely."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlResolvedParameter:
|
||||
name: str
|
||||
title: str
|
||||
raw_value: str
|
||||
units: str
|
||||
value: float | None
|
||||
|
||||
@property
|
||||
def is_numeric(self) -> bool:
|
||||
return self.value is not None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlResolvedComponent:
|
||||
alias: str
|
||||
component_name: str
|
||||
submodel: str
|
||||
label: str
|
||||
parameters: dict[str, TestMqlResolvedParameter]
|
||||
|
||||
def parameter_value(self, name: str) -> float:
|
||||
parameter = self.parameters[name]
|
||||
if parameter.value is None:
|
||||
raise KeyError(f"Parameter {name!r} on {self.alias!r} is not numeric")
|
||||
return parameter.value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlConfig:
|
||||
raw_global_parameters: dict[str, str]
|
||||
global_parameters: dict[str, float]
|
||||
fluid: PengRobinsonFluid
|
||||
components: tuple[TestMqlResolvedComponent, ...]
|
||||
|
||||
@classmethod
|
||||
def from_amesim_specs(cls) -> "TestMqlConfig":
|
||||
raw_globals = dict(GLOBAL_PARAMETERS)
|
||||
numeric_globals = {
|
||||
name: value
|
||||
for name, raw in raw_globals.items()
|
||||
if (value := resolve_numeric_expression(raw, {})) is not None
|
||||
}
|
||||
components = tuple(
|
||||
_resolve_component(spec, numeric_globals)
|
||||
for spec in COMPONENT_SPECS
|
||||
)
|
||||
return cls(
|
||||
raw_global_parameters=raw_globals,
|
||||
global_parameters=numeric_globals,
|
||||
fluid=HELIUM_PR,
|
||||
components=components,
|
||||
)
|
||||
|
||||
def component(self, alias: str) -> TestMqlResolvedComponent:
|
||||
for component in self.components:
|
||||
if component.alias == alias:
|
||||
return component
|
||||
raise KeyError(alias)
|
||||
|
||||
def components_by_submodel(self, submodel: str) -> tuple[TestMqlResolvedComponent, ...]:
|
||||
return tuple(component for component in self.components if component.submodel == submodel)
|
||||
|
||||
|
||||
def _resolve_component(
|
||||
spec: dict[str, Any],
|
||||
variables: dict[str, float],
|
||||
) -> TestMqlResolvedComponent:
|
||||
parameters = {}
|
||||
for parameter in spec.get("parameters", []):
|
||||
name = str(parameter["name"])
|
||||
raw_value = str(parameter["value"])
|
||||
parameters[name] = TestMqlResolvedParameter(
|
||||
name=name,
|
||||
title=str(parameter["title"]),
|
||||
raw_value=raw_value,
|
||||
units=str(parameter["units"]),
|
||||
value=resolve_numeric_expression(raw_value, variables),
|
||||
)
|
||||
return TestMqlResolvedComponent(
|
||||
alias=str(spec["alias"]),
|
||||
component_name=str(spec["component_name"]),
|
||||
submodel=str(spec["submodel"]),
|
||||
label=str(spec["label"]),
|
||||
parameters=parameters,
|
||||
)
|
||||
|
||||
|
||||
def resolve_numeric_expression(
|
||||
expression: str,
|
||||
variables: dict[str, float],
|
||||
) -> float | None:
|
||||
expression = expression.strip()
|
||||
if not expression:
|
||||
return None
|
||||
normalized = expression.replace("^", "**")
|
||||
try:
|
||||
parsed = ast.parse(normalized, mode="eval")
|
||||
value = float(_eval_node(parsed.body, variables))
|
||||
except (SyntaxError, TestMqlExpressionError, ValueError, TypeError, ZeroDivisionError):
|
||||
return None
|
||||
return value if isfinite(value) else None
|
||||
|
||||
|
||||
def _eval_node(node: ast.AST, variables: dict[str, float]) -> float:
|
||||
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
|
||||
return float(node.value)
|
||||
if isinstance(node, ast.Name):
|
||||
if node.id not in variables:
|
||||
raise TestMqlExpressionError(f"Unknown variable: {node.id}")
|
||||
return float(variables[node.id])
|
||||
if isinstance(node, ast.BinOp):
|
||||
operator_type = type(node.op)
|
||||
if operator_type not in _BINARY_OPERATORS:
|
||||
raise TestMqlExpressionError(f"Unsupported binary operator: {operator_type}")
|
||||
return float(_BINARY_OPERATORS[operator_type](_eval_node(node.left, variables), _eval_node(node.right, variables)))
|
||||
if isinstance(node, ast.UnaryOp):
|
||||
operator_type = type(node.op)
|
||||
if operator_type not in _UNARY_OPERATORS:
|
||||
raise TestMqlExpressionError(f"Unsupported unary operator: {operator_type}")
|
||||
return float(_UNARY_OPERATORS[operator_type](_eval_node(node.operand, variables)))
|
||||
raise TestMqlExpressionError(f"Unsupported expression node: {type(node)}")
|
||||
@@ -0,0 +1,451 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import tarfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from app.simulation.examples.test_mql.system import CONNECTION_SPECS, GLOBAL_PARAMETERS
|
||||
from app.simulation.examples.test_mql.config import resolve_numeric_expression
|
||||
|
||||
|
||||
AMESIM_REFERENCE_PRESSURE_PA = 101_300.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPnl0001Spec:
|
||||
alias: str
|
||||
source_component: str
|
||||
source_port: str
|
||||
target_component: str
|
||||
target_port: str
|
||||
diameter_mm: float
|
||||
length_m: float
|
||||
relative_roughness: float
|
||||
polytropic_constant: float
|
||||
heat_transfer_coefficient: float
|
||||
external_temperature_k: float
|
||||
gas_type_index: int
|
||||
mode: int
|
||||
initial_temperature_k: float
|
||||
initial_gauge_pressure_pa: float
|
||||
|
||||
@property
|
||||
def initial_absolute_pressure_pa(self) -> float:
|
||||
return self.initial_gauge_pressure_pa + AMESIM_REFERENCE_PRESSURE_PA
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPnl0002Spec:
|
||||
alias: str
|
||||
source_component: str
|
||||
source_port: str
|
||||
target_component: str
|
||||
target_port: str
|
||||
diameter_mm: float
|
||||
length_m: float
|
||||
relative_roughness: float
|
||||
polytropic_constant: float
|
||||
heat_transfer_coefficient: float
|
||||
external_temperature_k: float
|
||||
gas_type_index: int
|
||||
mode: int
|
||||
initial_center_temperature_k: float
|
||||
initial_center_gauge_pressure_pa: float
|
||||
|
||||
@property
|
||||
def initial_center_absolute_pressure_pa(self) -> float:
|
||||
return self.initial_center_gauge_pressure_pa + AMESIM_REFERENCE_PRESSURE_PA
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPnl0003Spec:
|
||||
alias: str
|
||||
source_component: str
|
||||
source_port: str
|
||||
target_component: str
|
||||
target_port: str
|
||||
diameter_mm: float
|
||||
length_m: float
|
||||
relative_roughness: float
|
||||
polytropic_constant: float
|
||||
heat_transfer_coefficient: float
|
||||
external_temperature_k: float
|
||||
gas_type_index: int
|
||||
mode: int
|
||||
initial_temperature_1_k: float
|
||||
initial_gauge_pressure_1_pa: float
|
||||
initial_temperature_2_k: float
|
||||
initial_gauge_pressure_2_pa: float
|
||||
|
||||
@property
|
||||
def initial_absolute_pressure_1_pa(self) -> float:
|
||||
return self.initial_gauge_pressure_1_pa + AMESIM_REFERENCE_PRESSURE_PA
|
||||
|
||||
@property
|
||||
def initial_absolute_pressure_2_pa(self) -> float:
|
||||
return self.initial_gauge_pressure_2_pa + AMESIM_REFERENCE_PRESSURE_PA
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPnl00rSpec:
|
||||
alias: str
|
||||
source_component: str
|
||||
source_port: str
|
||||
target_component: str
|
||||
target_port: str
|
||||
diameter_mm: float
|
||||
length_m: float
|
||||
relative_roughness: float
|
||||
gas_type_index: int
|
||||
|
||||
|
||||
def load_test_mql_pnl0001_specs(
|
||||
archive_path: str | Path,
|
||||
*,
|
||||
cir_member: str = "test_mql_.cir",
|
||||
) -> tuple[TestMqlPnl0001Spec, ...]:
|
||||
"""Load resolved PNL0001 geometry and initial states from the AMESim source."""
|
||||
with tarfile.open(archive_path) as archive:
|
||||
cir_file = archive.extractfile(cir_member)
|
||||
if cir_file is None:
|
||||
raise ValueError(f"Missing AMESim circuit member: {cir_member}")
|
||||
cir_text = cir_file.read().decode("latin1")
|
||||
|
||||
numeric_globals = {
|
||||
name: value
|
||||
for name, expression in GLOBAL_PARAMETERS.items()
|
||||
if (value := resolve_numeric_expression(expression, {})) is not None
|
||||
}
|
||||
connections = {
|
||||
str(connection["alias"]): connection
|
||||
for connection in CONNECTION_SPECS
|
||||
if connection["submodel"] == "PNL0001"
|
||||
}
|
||||
specs = []
|
||||
for block in re.findall(r"<LINE>.*?</LINE>", cir_text, flags=re.DOTALL):
|
||||
if _optional_text(block, "SUB_NAME") != "PNL0001":
|
||||
continue
|
||||
alias = _required_text(block, "ALIAS")
|
||||
connection = connections.get(alias)
|
||||
if connection is None:
|
||||
raise ValueError(f"PNL0001 line {alias!r} is absent from CONNECTION_SPECS")
|
||||
real_parameters = _parameter_expressions(block, "RPARAM")
|
||||
integer_parameters = _parameter_expressions(block, "IPARAM")
|
||||
state_values = _evar_values(block)
|
||||
specs.append(
|
||||
TestMqlPnl0001Spec(
|
||||
alias=alias,
|
||||
source_component=str(connection["source_component"]),
|
||||
source_port=str(connection["source_port"]),
|
||||
target_component=str(connection["target_component"]),
|
||||
target_port=str(connection["target_port"]),
|
||||
diameter_mm=_required_numeric(
|
||||
alias, "diam", real_parameters, numeric_globals
|
||||
),
|
||||
length_m=_required_numeric(alias, "le", real_parameters, numeric_globals),
|
||||
relative_roughness=_required_numeric(
|
||||
alias, "rr", real_parameters, numeric_globals
|
||||
),
|
||||
polytropic_constant=_required_numeric(
|
||||
alias, "k", real_parameters, numeric_globals
|
||||
),
|
||||
heat_transfer_coefficient=_required_numeric(
|
||||
alias, "kth", real_parameters, numeric_globals
|
||||
),
|
||||
external_temperature_k=_required_numeric(
|
||||
alias, "extemp", real_parameters, numeric_globals
|
||||
),
|
||||
gas_type_index=int(
|
||||
_required_numeric(alias, "gi", integer_parameters, numeric_globals)
|
||||
),
|
||||
mode=int(
|
||||
_required_numeric(alias, "mode", integer_parameters, numeric_globals)
|
||||
),
|
||||
initial_temperature_k=_required_numeric(
|
||||
alias, "t2", state_values, numeric_globals
|
||||
),
|
||||
initial_gauge_pressure_pa=_required_numeric(
|
||||
alias, "p2", state_values, numeric_globals
|
||||
),
|
||||
)
|
||||
)
|
||||
if set(connections) != {spec.alias for spec in specs}:
|
||||
missing = sorted(set(connections) - {spec.alias for spec in specs})
|
||||
raise ValueError(f"Missing PNL0001 parameter blocks: {missing}")
|
||||
return tuple(specs)
|
||||
|
||||
|
||||
def load_test_mql_pnl0002_specs(
|
||||
archive_path: str | Path,
|
||||
*,
|
||||
cir_member: str = "test_mql_.cir",
|
||||
) -> tuple[TestMqlPnl0002Spec, ...]:
|
||||
"""Load resolved PNL0002 geometry and center compliance initial state."""
|
||||
with tarfile.open(archive_path) as archive:
|
||||
cir_file = archive.extractfile(cir_member)
|
||||
if cir_file is None:
|
||||
raise ValueError(f"Missing AMESim circuit member: {cir_member}")
|
||||
cir_text = cir_file.read().decode("latin1")
|
||||
|
||||
numeric_globals = {
|
||||
name: value
|
||||
for name, expression in GLOBAL_PARAMETERS.items()
|
||||
if (value := resolve_numeric_expression(expression, {})) is not None
|
||||
}
|
||||
connections = {
|
||||
str(connection["alias"]): connection
|
||||
for connection in CONNECTION_SPECS
|
||||
if connection["submodel"] == "PNL0002"
|
||||
}
|
||||
specs = []
|
||||
for block in re.findall(r"<LINE>.*?</LINE>", cir_text, flags=re.DOTALL):
|
||||
if _optional_text(block, "SUB_NAME") != "PNL0002":
|
||||
continue
|
||||
alias = _required_text(block, "ALIAS")
|
||||
connection = connections.get(alias)
|
||||
if connection is None:
|
||||
raise ValueError(f"PNL0002 line {alias!r} is absent from CONNECTION_SPECS")
|
||||
real_parameters = _parameter_expressions(block, "RPARAM")
|
||||
integer_parameters = _parameter_expressions(block, "IPARAM")
|
||||
state_values = _ivar_values(block)
|
||||
specs.append(
|
||||
TestMqlPnl0002Spec(
|
||||
alias=alias,
|
||||
source_component=str(connection["source_component"]),
|
||||
source_port=str(connection["source_port"]),
|
||||
target_component=str(connection["target_component"]),
|
||||
target_port=str(connection["target_port"]),
|
||||
diameter_mm=_required_numeric(
|
||||
alias, "diam", real_parameters, numeric_globals
|
||||
),
|
||||
length_m=_required_numeric(alias, "le", real_parameters, numeric_globals),
|
||||
relative_roughness=_required_numeric(
|
||||
alias, "rr", real_parameters, numeric_globals
|
||||
),
|
||||
polytropic_constant=_required_numeric(
|
||||
alias, "k", real_parameters, numeric_globals
|
||||
),
|
||||
heat_transfer_coefficient=_required_numeric(
|
||||
alias, "kth", real_parameters, numeric_globals
|
||||
),
|
||||
external_temperature_k=_required_numeric(
|
||||
alias, "extemp", real_parameters, numeric_globals
|
||||
),
|
||||
gas_type_index=int(
|
||||
_required_numeric(alias, "gi", integer_parameters, numeric_globals)
|
||||
),
|
||||
mode=int(
|
||||
_required_numeric(alias, "mode", integer_parameters, numeric_globals)
|
||||
),
|
||||
initial_center_temperature_k=_required_numeric(
|
||||
alias, "tctr", state_values, numeric_globals
|
||||
),
|
||||
initial_center_gauge_pressure_pa=_required_numeric(
|
||||
alias, "pctr", state_values, numeric_globals
|
||||
),
|
||||
)
|
||||
)
|
||||
if set(connections) != {spec.alias for spec in specs}:
|
||||
missing = sorted(set(connections) - {spec.alias for spec in specs})
|
||||
raise ValueError(f"Missing PNL0002 parameter blocks: {missing}")
|
||||
return tuple(specs)
|
||||
|
||||
|
||||
def load_test_mql_pnl0003_specs(
|
||||
archive_path: str | Path,
|
||||
*,
|
||||
cir_member: str = "test_mql_.cir",
|
||||
) -> tuple[TestMqlPnl0003Spec, ...]:
|
||||
"""Load resolved PNL0003 geometry and both compliance initial states."""
|
||||
with tarfile.open(archive_path) as archive:
|
||||
cir_file = archive.extractfile(cir_member)
|
||||
if cir_file is None:
|
||||
raise ValueError(f"Missing AMESim circuit member: {cir_member}")
|
||||
cir_text = cir_file.read().decode("latin1")
|
||||
|
||||
numeric_globals = {
|
||||
name: value
|
||||
for name, expression in GLOBAL_PARAMETERS.items()
|
||||
if (value := resolve_numeric_expression(expression, {})) is not None
|
||||
}
|
||||
connections = {
|
||||
str(connection["alias"]): connection
|
||||
for connection in CONNECTION_SPECS
|
||||
if connection["submodel"] == "PNL0003"
|
||||
}
|
||||
specs = []
|
||||
for block in re.findall(r"<LINE>.*?</LINE>", cir_text, flags=re.DOTALL):
|
||||
if _optional_text(block, "SUB_NAME") != "PNL0003":
|
||||
continue
|
||||
alias = _required_text(block, "ALIAS")
|
||||
connection = connections.get(alias)
|
||||
if connection is None:
|
||||
raise ValueError(f"PNL0003 line {alias!r} is absent from CONNECTION_SPECS")
|
||||
real_parameters = _parameter_expressions(block, "RPARAM")
|
||||
integer_parameters = _parameter_expressions(block, "IPARAM")
|
||||
state_values = _evar_values(block)
|
||||
specs.append(
|
||||
TestMqlPnl0003Spec(
|
||||
alias=alias,
|
||||
source_component=str(connection["source_component"]),
|
||||
source_port=str(connection["source_port"]),
|
||||
target_component=str(connection["target_component"]),
|
||||
target_port=str(connection["target_port"]),
|
||||
diameter_mm=_required_numeric(
|
||||
alias, "diam", real_parameters, numeric_globals
|
||||
),
|
||||
length_m=_required_numeric(alias, "le", real_parameters, numeric_globals),
|
||||
relative_roughness=_required_numeric(
|
||||
alias, "rr", real_parameters, numeric_globals
|
||||
),
|
||||
polytropic_constant=_required_numeric(
|
||||
alias, "k", real_parameters, numeric_globals
|
||||
),
|
||||
heat_transfer_coefficient=_required_numeric(
|
||||
alias, "kth", real_parameters, numeric_globals
|
||||
),
|
||||
external_temperature_k=_required_numeric(
|
||||
alias, "extemp", real_parameters, numeric_globals
|
||||
),
|
||||
gas_type_index=int(
|
||||
_required_numeric(alias, "gi", integer_parameters, numeric_globals)
|
||||
),
|
||||
mode=int(
|
||||
_required_numeric(alias, "mode", integer_parameters, numeric_globals)
|
||||
),
|
||||
initial_temperature_1_k=_required_numeric(
|
||||
alias, "t1", state_values, numeric_globals
|
||||
),
|
||||
initial_gauge_pressure_1_pa=_required_numeric(
|
||||
alias, "p1", state_values, numeric_globals
|
||||
),
|
||||
initial_temperature_2_k=_required_numeric(
|
||||
alias, "t2", state_values, numeric_globals
|
||||
),
|
||||
initial_gauge_pressure_2_pa=_required_numeric(
|
||||
alias, "p2", state_values, numeric_globals
|
||||
),
|
||||
)
|
||||
)
|
||||
if set(connections) != {spec.alias for spec in specs}:
|
||||
missing = sorted(set(connections) - {spec.alias for spec in specs})
|
||||
raise ValueError(f"Missing PNL0003 parameter blocks: {missing}")
|
||||
return tuple(specs)
|
||||
|
||||
|
||||
def load_test_mql_pnl00r_specs(
|
||||
archive_path: str | Path,
|
||||
*,
|
||||
cir_member: str = "test_mql_.cir",
|
||||
) -> tuple[TestMqlPnl00rSpec, ...]:
|
||||
"""Load resolved PNL00R geometry from the AMESim source."""
|
||||
with tarfile.open(archive_path) as archive:
|
||||
cir_file = archive.extractfile(cir_member)
|
||||
if cir_file is None:
|
||||
raise ValueError(f"Missing AMESim circuit member: {cir_member}")
|
||||
cir_text = cir_file.read().decode("latin1")
|
||||
|
||||
numeric_globals = {
|
||||
name: value
|
||||
for name, expression in GLOBAL_PARAMETERS.items()
|
||||
if (value := resolve_numeric_expression(expression, {})) is not None
|
||||
}
|
||||
connections = {
|
||||
str(connection["alias"]): connection
|
||||
for connection in CONNECTION_SPECS
|
||||
if connection["submodel"] == "PNL00R"
|
||||
}
|
||||
specs = []
|
||||
for block in re.findall(r"<LINE>.*?</LINE>", cir_text, flags=re.DOTALL):
|
||||
if _optional_text(block, "SUB_NAME") != "PNL00R":
|
||||
continue
|
||||
alias = _required_text(block, "ALIAS")
|
||||
connection = connections.get(alias)
|
||||
if connection is None:
|
||||
raise ValueError(f"PNL00R line {alias!r} is absent from CONNECTION_SPECS")
|
||||
real_parameters = _parameter_expressions(block, "RPARAM")
|
||||
integer_parameters = _parameter_expressions(block, "IPARAM")
|
||||
specs.append(
|
||||
TestMqlPnl00rSpec(
|
||||
alias=alias,
|
||||
source_component=str(connection["source_component"]),
|
||||
source_port=str(connection["source_port"]),
|
||||
target_component=str(connection["target_component"]),
|
||||
target_port=str(connection["target_port"]),
|
||||
diameter_mm=_required_numeric(
|
||||
alias, "diam", real_parameters, numeric_globals
|
||||
),
|
||||
length_m=_required_numeric(alias, "le", real_parameters, numeric_globals),
|
||||
relative_roughness=_required_numeric(
|
||||
alias, "rr", real_parameters, numeric_globals
|
||||
),
|
||||
gas_type_index=int(
|
||||
_required_numeric(alias, "gi", integer_parameters, numeric_globals)
|
||||
),
|
||||
)
|
||||
)
|
||||
if set(connections) != {spec.alias for spec in specs}:
|
||||
missing = sorted(set(connections) - {spec.alias for spec in specs})
|
||||
raise ValueError(f"Missing PNL00R parameter blocks: {missing}")
|
||||
return tuple(specs)
|
||||
|
||||
|
||||
def _parameter_expressions(block: str, tag_name: str) -> dict[str, str]:
|
||||
parameters = {}
|
||||
for parameter_block in re.findall(
|
||||
rf"<{tag_name}>.*?</{tag_name}>",
|
||||
block,
|
||||
flags=re.DOTALL,
|
||||
):
|
||||
parameters[_required_text(parameter_block, "VARNAME")] = _required_text(
|
||||
parameter_block,
|
||||
"VALUE",
|
||||
)
|
||||
return parameters
|
||||
|
||||
|
||||
def _ivar_values(block: str) -> dict[str, str]:
|
||||
values = {}
|
||||
for variable_block in re.findall(r"<IVAR>.*?</IVAR>", block, flags=re.DOTALL):
|
||||
value = _optional_text(variable_block, "VALUE")
|
||||
if value:
|
||||
values[_required_text(variable_block, "VARNAME")] = value
|
||||
return values
|
||||
|
||||
|
||||
def _evar_values(block: str) -> dict[str, str]:
|
||||
values = {}
|
||||
for variable_block in re.findall(r"<EVAR>.*?</EVAR>", block, flags=re.DOTALL):
|
||||
value = _optional_text(variable_block, "VALUE")
|
||||
if value:
|
||||
values[_required_text(variable_block, "VARNAME")] = value
|
||||
return values
|
||||
|
||||
|
||||
def _required_numeric(
|
||||
alias: str,
|
||||
name: str,
|
||||
expressions: dict[str, str],
|
||||
variables: dict[str, float],
|
||||
) -> float:
|
||||
if name not in expressions:
|
||||
raise ValueError(f"Missing {name!r} on line {alias!r}")
|
||||
value = resolve_numeric_expression(expressions[name], variables)
|
||||
if value is None:
|
||||
raise ValueError(
|
||||
f"Cannot resolve {name!r}={expressions[name]!r} on line {alias!r}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _required_text(block: str, tag_name: str) -> str:
|
||||
value = _optional_text(block, tag_name)
|
||||
if value is None:
|
||||
raise ValueError(f"Missing AMESim circuit element: {tag_name}")
|
||||
return value
|
||||
|
||||
|
||||
def _optional_text(block: str, tag_name: str) -> str | None:
|
||||
match = re.search(rf"<{tag_name}>(.*?)</{tag_name}>", block, flags=re.DOTALL)
|
||||
return match.group(1).strip() if match is not None else None
|
||||
@@ -0,0 +1,102 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.reporting.amesim_results import AmesimResults
|
||||
from app.simulation.reporting.test_mql_variables import (
|
||||
TestMqlVariableCatalog,
|
||||
build_test_mql_variable_catalog,
|
||||
)
|
||||
from app.simulation.examples.test_mql.system import CONNECTION_SPECS
|
||||
|
||||
|
||||
TEST_MQL_PNEUMATIC_LINE_SUBMODELS = ("PNL0001", "PNL0002", "PNL0003", "PNL00R")
|
||||
_LINE_PATTERN_RE = re.compile(r"\(([^()]+)\)\s*$")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlLineConnection:
|
||||
index: int
|
||||
alias: str
|
||||
submodel: str
|
||||
pattern: str
|
||||
source_component: str
|
||||
source_port: str
|
||||
target_component: str
|
||||
target_port: str
|
||||
label: str
|
||||
data_paths: tuple[str, ...]
|
||||
signal_names: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def has_compliance(self) -> bool:
|
||||
return "C" in self.pattern
|
||||
|
||||
@property
|
||||
def has_resistance(self) -> bool:
|
||||
return "R" in self.pattern
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlLineAssembly:
|
||||
lines: tuple[TestMqlLineConnection, ...]
|
||||
|
||||
@property
|
||||
def line_count(self) -> int:
|
||||
return len(self.lines)
|
||||
|
||||
def by_alias(self, alias: str) -> TestMqlLineConnection:
|
||||
for line in self.lines:
|
||||
if line.alias == alias:
|
||||
return line
|
||||
raise KeyError(alias)
|
||||
|
||||
def by_submodel(self, submodel: str) -> tuple[TestMqlLineConnection, ...]:
|
||||
return tuple(line for line in self.lines if line.submodel == submodel)
|
||||
|
||||
def counts_by_submodel(self) -> dict[str, int]:
|
||||
return dict(Counter(line.submodel for line in self.lines))
|
||||
|
||||
def aliases(self) -> tuple[str, ...]:
|
||||
return tuple(line.alias for line in self.lines)
|
||||
|
||||
|
||||
def build_test_mql_line_assembly(
|
||||
amesim_results: AmesimResults,
|
||||
variable_catalog: TestMqlVariableCatalog | None = None,
|
||||
) -> TestMqlLineAssembly:
|
||||
variable_catalog = variable_catalog or build_test_mql_variable_catalog(amesim_results)
|
||||
lines = []
|
||||
for spec in CONNECTION_SPECS:
|
||||
submodel = str(spec["submodel"])
|
||||
if submodel not in TEST_MQL_PNEUMATIC_LINE_SUBMODELS:
|
||||
continue
|
||||
data_paths = variable_catalog.data_paths_for_owner(str(spec["alias"]))
|
||||
signal_names = tuple(path.rsplit("@", 1)[0] for path in data_paths)
|
||||
lines.append(
|
||||
TestMqlLineConnection(
|
||||
index=int(spec["index"]),
|
||||
alias=str(spec["alias"]),
|
||||
submodel=submodel,
|
||||
pattern=_line_pattern(str(spec["label"]), submodel),
|
||||
source_component=str(spec["source_component"]),
|
||||
source_port=str(spec["source_port"]),
|
||||
target_component=str(spec["target_component"]),
|
||||
target_port=str(spec["target_port"]),
|
||||
label=str(spec["label"]),
|
||||
data_paths=data_paths,
|
||||
signal_names=signal_names,
|
||||
)
|
||||
)
|
||||
return TestMqlLineAssembly(lines=tuple(lines))
|
||||
|
||||
|
||||
def _line_pattern(label: str, submodel: str) -> str:
|
||||
match = _LINE_PATTERN_RE.search(label)
|
||||
if match is not None:
|
||||
return match.group(1)
|
||||
if submodel == "PNL00R":
|
||||
return "R"
|
||||
return submodel
|
||||
@@ -0,0 +1,544 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.examples.test_mql.primitives.mechanical import (
|
||||
AmesimElasticEndstop,
|
||||
AmesimMassFrictionEndstops,
|
||||
AmesimPistonGeometry,
|
||||
circular_area,
|
||||
mm_to_m,
|
||||
)
|
||||
from app.simulation.reporting.amesim_results import AmesimResults
|
||||
from app.simulation.reporting.test_mql_variables import (
|
||||
TestMqlVariableCatalog,
|
||||
build_test_mql_variable_catalog,
|
||||
)
|
||||
from app.simulation.examples.test_mql.config import TestMqlConfig, TestMqlResolvedComponent
|
||||
|
||||
|
||||
MM_TO_M = 1.0e-3
|
||||
N_PER_MM_TO_N_PER_M = 1.0e3
|
||||
N_PER_MM_PER_S_TO_N_PER_M_PER_S = 1.0e3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPistonSpec:
|
||||
alias: str
|
||||
piston_diameter_m: float
|
||||
rod_diameter_m: float
|
||||
zero_displacement_m: float
|
||||
piston_area_m2: float
|
||||
rod_area_m2: float
|
||||
annulus_area_m2: float
|
||||
data_paths: tuple[str, ...]
|
||||
|
||||
def geometry(self) -> AmesimPistonGeometry:
|
||||
return AmesimPistonGeometry(
|
||||
piston_diameter_m=self.piston_diameter_m,
|
||||
rod_diameter_m=self.rod_diameter_m,
|
||||
zero_length_m=self.zero_displacement_m,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlMassEndstopSpec:
|
||||
alias: str
|
||||
mass_kg: float
|
||||
xmin_m: float
|
||||
xmax_m: float
|
||||
min_stiffness_n_per_m: float
|
||||
max_stiffness_n_per_m: float
|
||||
min_damping_n_per_m_per_s: float
|
||||
max_damping_n_per_m_per_s: float
|
||||
min_penetration_m: float
|
||||
max_penetration_m: float
|
||||
stiction_force_n: float
|
||||
coulomb_friction_n: float
|
||||
viscous_friction_n_per_m_per_s: float
|
||||
windage_n_per_m2_per_s2: float
|
||||
stick_velocity_threshold_m_s: float
|
||||
reset_velocity_threshold_m_s: float
|
||||
rest_coeff: float
|
||||
stribeck_constant_m_s: float
|
||||
use_friction: bool
|
||||
stop_type: int
|
||||
initial_velocity_m_s: float
|
||||
initial_displacement_m: float
|
||||
data_paths: tuple[str, ...]
|
||||
|
||||
def endstop(self) -> AmesimMassFrictionEndstops:
|
||||
return AmesimMassFrictionEndstops(
|
||||
mass_kg=self.mass_kg,
|
||||
lower_limit_m=self.xmin_m,
|
||||
upper_limit_m=self.xmax_m,
|
||||
lower_stiffness_n_per_m=self.min_stiffness_n_per_m,
|
||||
upper_stiffness_n_per_m=self.max_stiffness_n_per_m,
|
||||
lower_damping_n_per_m_per_s=self.min_damping_n_per_m_per_s,
|
||||
upper_damping_n_per_m_per_s=self.max_damping_n_per_m_per_s,
|
||||
viscous_friction_n_per_m_per_s=self.viscous_friction_n_per_m_per_s,
|
||||
coulomb_friction_n=self.coulomb_friction_n,
|
||||
stiction_force_n=self.stiction_force_n,
|
||||
windage_n_per_m2_per_s2=self.windage_n_per_m2_per_s2,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlElasticEndstopSpec:
|
||||
alias: str
|
||||
gap_m: float
|
||||
contact_stiffness_n_per_m: float
|
||||
contact_damping_n_per_m_per_s: float
|
||||
spring_diameter_m: float
|
||||
wire_diameter_m: float
|
||||
data_paths: tuple[str, ...]
|
||||
|
||||
def endstop(self) -> AmesimElasticEndstop:
|
||||
return AmesimElasticEndstop(
|
||||
contact_stiffness_n_per_m=self.contact_stiffness_n_per_m,
|
||||
contact_damping_n_per_m_per_s=self.contact_damping_n_per_m_per_s,
|
||||
gap0_m=self.gap_m,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlMechanicalNodeSpec:
|
||||
alias: str
|
||||
port_count: int
|
||||
sum_mode: int
|
||||
data_paths: tuple[str, ...]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPiecewiseLinearSignalSpec:
|
||||
alias: str
|
||||
t_start_s: float
|
||||
starts: tuple[float, ...]
|
||||
ends: tuple[float, ...]
|
||||
durations_s: tuple[float, ...]
|
||||
stage_count: int
|
||||
is_cyclic: bool
|
||||
data_paths: tuple[str, ...]
|
||||
|
||||
def output_at(self, time_s: float) -> float:
|
||||
if self.stage_count <= 0:
|
||||
return 0.0
|
||||
elapsed = max(time_s - self.t_start_s, 0.0)
|
||||
active_durations = self.durations_s[: self.stage_count]
|
||||
total_duration = sum(active_durations)
|
||||
if self.is_cyclic and total_duration > 0.0:
|
||||
elapsed = elapsed % total_duration
|
||||
|
||||
stage_start_time = 0.0
|
||||
for index, duration in enumerate(active_durations):
|
||||
stage_end_time = stage_start_time + duration
|
||||
if elapsed < stage_end_time or index == self.stage_count - 1:
|
||||
if duration <= 0.0:
|
||||
return self.ends[index]
|
||||
fraction = (elapsed - stage_start_time) / duration
|
||||
return self.starts[index] + fraction * (self.ends[index] - self.starts[index])
|
||||
stage_start_time = stage_end_time
|
||||
return self.ends[self.stage_count - 1]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlForceConnectorSpec:
|
||||
alias: str
|
||||
signal_alias: str
|
||||
target_mass_alias: str
|
||||
data_paths: tuple[str, ...]
|
||||
|
||||
def force_at(
|
||||
self,
|
||||
time_s: float,
|
||||
signals: dict[str, TestMqlPiecewiseLinearSignalSpec],
|
||||
) -> float:
|
||||
return signals[self.signal_alias].output_at(time_s)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlMechanicalAssembly:
|
||||
pistons: dict[str, TestMqlPistonSpec]
|
||||
masses: dict[str, TestMqlMassEndstopSpec]
|
||||
elastic_endstops: dict[str, TestMqlElasticEndstopSpec]
|
||||
mechanical_nodes: dict[str, TestMqlMechanicalNodeSpec]
|
||||
piecewise_signals: dict[str, TestMqlPiecewiseLinearSignalSpec]
|
||||
force_connectors: dict[str, TestMqlForceConnectorSpec]
|
||||
zero_force_sources: tuple[str, ...]
|
||||
|
||||
@property
|
||||
def component_count(self) -> int:
|
||||
return (
|
||||
len(self.pistons)
|
||||
+ len(self.masses)
|
||||
+ len(self.elastic_endstops)
|
||||
+ len(self.mechanical_nodes)
|
||||
+ len(self.piecewise_signals)
|
||||
+ len(self.force_connectors)
|
||||
+ len(self.zero_force_sources)
|
||||
)
|
||||
|
||||
@property
|
||||
def aliases(self) -> tuple[str, ...]:
|
||||
return tuple(
|
||||
[
|
||||
*self.pistons,
|
||||
*self.masses,
|
||||
*self.elastic_endstops,
|
||||
*self.mechanical_nodes,
|
||||
*self.piecewise_signals,
|
||||
*self.force_connectors,
|
||||
*self.zero_force_sources,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlMechanicalMassState:
|
||||
alias: str
|
||||
velocity_m_s: float
|
||||
displacement_m: float
|
||||
|
||||
def as_vector(self) -> list[float]:
|
||||
return [self.velocity_m_s, self.displacement_m]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlMechanicalNodeKinematics:
|
||||
alias: str
|
||||
velocities_m_s: dict[int, float]
|
||||
displacements_m: dict[int, float]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPistonKinematics:
|
||||
alias: str
|
||||
port_2_velocity_m_s: float
|
||||
port_2_displacement_m: float
|
||||
port_3_velocity_m_s: float
|
||||
port_3_displacement_m: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlMechanicalMassSnapshot:
|
||||
states: tuple[TestMqlMechanicalMassState, ...]
|
||||
node_kinematics_by_alias: dict[str, TestMqlMechanicalNodeKinematics]
|
||||
piston_kinematics_by_alias: dict[str, TestMqlPistonKinematics]
|
||||
|
||||
@property
|
||||
def state_count(self) -> int:
|
||||
return 2 * len(self.states)
|
||||
|
||||
|
||||
class TestMqlMechanicalMassClosure:
|
||||
def __init__(self, assembly: TestMqlMechanicalAssembly) -> None:
|
||||
self.assembly = assembly
|
||||
self.mass_aliases = tuple(assembly.masses)
|
||||
|
||||
def initial_state_vector(self) -> list[float]:
|
||||
state: list[float] = []
|
||||
for alias in self.mass_aliases:
|
||||
spec = self.assembly.masses[alias]
|
||||
state.extend([spec.initial_velocity_m_s, spec.initial_displacement_m])
|
||||
return state
|
||||
|
||||
def snapshot(self, state_vector: list[float] | None = None) -> TestMqlMechanicalMassSnapshot:
|
||||
values = self.initial_state_vector() if state_vector is None else list(state_vector)
|
||||
if len(values) != 2 * len(self.mass_aliases):
|
||||
raise ValueError("mechanical mass state vector requires two values per mass")
|
||||
states = tuple(
|
||||
TestMqlMechanicalMassState(
|
||||
alias=alias,
|
||||
velocity_m_s=values[2 * index],
|
||||
displacement_m=values[2 * index + 1],
|
||||
)
|
||||
for index, alias in enumerate(self.mass_aliases)
|
||||
)
|
||||
node_kinematics = self._node_kinematics_by_alias(states)
|
||||
return TestMqlMechanicalMassSnapshot(
|
||||
states=states,
|
||||
node_kinematics_by_alias=node_kinematics,
|
||||
piston_kinematics_by_alias=self._piston_kinematics_by_alias(
|
||||
states,
|
||||
node_kinematics,
|
||||
),
|
||||
)
|
||||
|
||||
def _node_kinematics_by_alias(
|
||||
self,
|
||||
states: tuple[TestMqlMechanicalMassState, ...],
|
||||
) -> dict[str, TestMqlMechanicalNodeKinematics]:
|
||||
state_by_alias = {state.alias: state for state in states}
|
||||
front = state_by_alias["mass_friction_endstops_18"]
|
||||
rear = state_by_alias["mass_friction_endstops_19"]
|
||||
return {
|
||||
"dynamic_mechanical_node_alternative_2": TestMqlMechanicalNodeKinematics(
|
||||
alias="dynamic_mechanical_node_alternative_2",
|
||||
velocities_m_s={port: -front.velocity_m_s for port in range(1, 9)},
|
||||
displacements_m={port: -front.displacement_m for port in range(1, 9)},
|
||||
),
|
||||
"dynamic_mechanical_node_alternative_3": TestMqlMechanicalNodeKinematics(
|
||||
alias="dynamic_mechanical_node_alternative_3",
|
||||
velocities_m_s={port: rear.velocity_m_s for port in range(1, 9)},
|
||||
displacements_m={port: rear.displacement_m for port in range(1, 9)},
|
||||
),
|
||||
}
|
||||
|
||||
def _piston_kinematics_by_alias(
|
||||
self,
|
||||
states: tuple[TestMqlMechanicalMassState, ...],
|
||||
node_kinematics_by_alias: dict[str, TestMqlMechanicalNodeKinematics],
|
||||
) -> dict[str, TestMqlPistonKinematics]:
|
||||
state_by_alias = {state.alias: state for state in states}
|
||||
rear_node = node_kinematics_by_alias["dynamic_mechanical_node_alternative_3"]
|
||||
piston_bindings = (
|
||||
("pn_brp2_8", "mass_friction_endstops_10", 8),
|
||||
("pn_brp2_9", "mass_friction_endstops_11", 7),
|
||||
("pn_brp2_10", "mass_friction_endstops_12", 6),
|
||||
("pn_brp2_11", "mass_friction_endstops_13", 5),
|
||||
("pn_brp2_12", "mass_friction_endstops_14", 4),
|
||||
("pn_brp2_13", "mass_friction_endstops_15", 3),
|
||||
("pn_brp2_14", "mass_friction_endstops_16", 2),
|
||||
("pn_brp2_15", "mass_friction_endstops_17", 1),
|
||||
)
|
||||
return {
|
||||
piston_alias: TestMqlPistonKinematics(
|
||||
alias=piston_alias,
|
||||
port_2_velocity_m_s=state_by_alias[mass_alias].velocity_m_s,
|
||||
port_2_displacement_m=state_by_alias[mass_alias].displacement_m,
|
||||
port_3_velocity_m_s=rear_node.velocities_m_s[rear_node_port],
|
||||
port_3_displacement_m=rear_node.displacements_m[rear_node_port],
|
||||
)
|
||||
for piston_alias, mass_alias, rear_node_port in piston_bindings
|
||||
}
|
||||
|
||||
def rhs(
|
||||
self,
|
||||
state_vector: list[float],
|
||||
*,
|
||||
force_by_mass_alias: dict[str, float] | None = None,
|
||||
constrained_mass_aliases: set[str] | None = None,
|
||||
) -> list[float]:
|
||||
snapshot = self.snapshot(state_vector)
|
||||
force_by_mass_alias = force_by_mass_alias or {}
|
||||
constrained_mass_aliases = constrained_mass_aliases or set()
|
||||
derivatives: list[float] = []
|
||||
for state in snapshot.states:
|
||||
spec = self.assembly.masses[state.alias]
|
||||
mass = spec.endstop()
|
||||
applied_force = force_by_mass_alias.get(state.alias, 0.0)
|
||||
acceleration, velocity = mass.derivatives(
|
||||
velocity_m_s=state.velocity_m_s,
|
||||
displacement_m=state.displacement_m,
|
||||
port_1_force_n=applied_force,
|
||||
)
|
||||
if state.alias in constrained_mass_aliases and _limit_constraint_holds(
|
||||
spec,
|
||||
state,
|
||||
applied_force,
|
||||
):
|
||||
acceleration = 0.0
|
||||
velocity = 0.0
|
||||
derivatives.extend([acceleration, velocity])
|
||||
return derivatives
|
||||
|
||||
|
||||
def build_test_mql_mechanical_assembly(
|
||||
config: TestMqlConfig | None = None,
|
||||
amesim_results: AmesimResults | None = None,
|
||||
variable_catalog: TestMqlVariableCatalog | None = None,
|
||||
) -> TestMqlMechanicalAssembly:
|
||||
config = config or TestMqlConfig.from_amesim_specs()
|
||||
if variable_catalog is None and amesim_results is not None:
|
||||
variable_catalog = build_test_mql_variable_catalog(amesim_results)
|
||||
|
||||
pistons = {
|
||||
component.alias: _build_piston(component, variable_catalog)
|
||||
for component in config.components_by_submodel("PNRP17")
|
||||
}
|
||||
masses = {
|
||||
component.alias: _build_mass(component, variable_catalog, amesim_results)
|
||||
for component in config.components_by_submodel("MECMAS21")
|
||||
}
|
||||
elastic_endstops = {
|
||||
component.alias: _build_elastic_endstop(component, variable_catalog)
|
||||
for component in config.components_by_submodel("LSTP00A")
|
||||
}
|
||||
mechanical_nodes = {
|
||||
component.alias: _build_mechanical_node(component, variable_catalog)
|
||||
for component in config.components_by_submodel("LMECHN1")
|
||||
}
|
||||
piecewise_signals = {
|
||||
component.alias: _build_piecewise_signal(component, variable_catalog)
|
||||
for component in config.components_by_submodel("UD00")
|
||||
}
|
||||
force_connectors = {
|
||||
component.alias: _build_force_connector(component, variable_catalog)
|
||||
for component in config.components_by_submodel("FORC")
|
||||
}
|
||||
zero_force_sources = tuple(component.alias for component in config.components_by_submodel("F000"))
|
||||
return TestMqlMechanicalAssembly(
|
||||
pistons=pistons,
|
||||
masses=masses,
|
||||
elastic_endstops=elastic_endstops,
|
||||
mechanical_nodes=mechanical_nodes,
|
||||
piecewise_signals=piecewise_signals,
|
||||
force_connectors=force_connectors,
|
||||
zero_force_sources=zero_force_sources,
|
||||
)
|
||||
|
||||
|
||||
def _build_piston(
|
||||
component: TestMqlResolvedComponent,
|
||||
variable_catalog: TestMqlVariableCatalog | None,
|
||||
) -> TestMqlPistonSpec:
|
||||
geometry = AmesimPistonGeometry(
|
||||
piston_diameter_m=mm_to_m(component.parameter_value("dp")),
|
||||
rod_diameter_m=mm_to_m(component.parameter_value("dr")),
|
||||
zero_length_m=mm_to_m(component.parameter_value("x0")),
|
||||
)
|
||||
return TestMqlPistonSpec(
|
||||
alias=component.alias,
|
||||
piston_diameter_m=geometry.piston_diameter_m,
|
||||
rod_diameter_m=geometry.rod_diameter_m,
|
||||
zero_displacement_m=geometry.zero_length_m,
|
||||
piston_area_m2=geometry.piston_area_m2,
|
||||
rod_area_m2=geometry.rod_area_m2,
|
||||
annulus_area_m2=geometry.annulus_area_m2,
|
||||
data_paths=_data_paths(variable_catalog, component.alias),
|
||||
)
|
||||
|
||||
|
||||
def _build_mass(
|
||||
component: TestMqlResolvedComponent,
|
||||
variable_catalog: TestMqlVariableCatalog | None,
|
||||
amesim_results: AmesimResults | None,
|
||||
) -> TestMqlMassEndstopSpec:
|
||||
return TestMqlMassEndstopSpec(
|
||||
alias=component.alias,
|
||||
mass_kg=component.parameter_value("mass"),
|
||||
xmin_m=component.parameter_value("xmin"),
|
||||
xmax_m=component.parameter_value("xmax"),
|
||||
min_stiffness_n_per_m=n_per_mm_to_n_per_m(component.parameter_value("Kbmin")),
|
||||
max_stiffness_n_per_m=n_per_mm_to_n_per_m(component.parameter_value("Kbmax")),
|
||||
min_damping_n_per_m_per_s=n_per_mm_per_s_to_n_per_m_per_s(component.parameter_value("Dbmin")),
|
||||
max_damping_n_per_m_per_s=n_per_mm_per_s_to_n_per_m_per_s(component.parameter_value("Dbmax")),
|
||||
min_penetration_m=mm_to_m(component.parameter_value("Pdmin")),
|
||||
max_penetration_m=mm_to_m(component.parameter_value("Pdmax")),
|
||||
stiction_force_n=component.parameter_value("fstick"),
|
||||
coulomb_friction_n=component.parameter_value("fcoul"),
|
||||
viscous_friction_n_per_m_per_s=component.parameter_value("rvisc"),
|
||||
windage_n_per_m2_per_s2=component.parameter_value("wind"),
|
||||
stick_velocity_threshold_m_s=component.parameter_value("dvel"),
|
||||
reset_velocity_threshold_m_s=component.parameter_value("restdvel"),
|
||||
rest_coeff=component.parameter_value("restcoeff"),
|
||||
stribeck_constant_m_s=component.parameter_value("astrib"),
|
||||
use_friction=bool(int(component.parameter_value("useFriction"))),
|
||||
stop_type=int(component.parameter_value("stoptype")),
|
||||
initial_velocity_m_s=_initial_value(amesim_results, f"v1@{component.alias}"),
|
||||
initial_displacement_m=_initial_value(amesim_results, f"x1@{component.alias}"),
|
||||
data_paths=_data_paths(variable_catalog, component.alias),
|
||||
)
|
||||
|
||||
|
||||
def _build_elastic_endstop(
|
||||
component: TestMqlResolvedComponent,
|
||||
variable_catalog: TestMqlVariableCatalog | None,
|
||||
) -> TestMqlElasticEndstopSpec:
|
||||
return TestMqlElasticEndstopSpec(
|
||||
alias=component.alias,
|
||||
gap_m=mm_to_m(component.parameter_value("gap0")),
|
||||
contact_stiffness_n_per_m=component.parameter_value("kcont"),
|
||||
contact_damping_n_per_m_per_s=component.parameter_value("rcont"),
|
||||
spring_diameter_m=mm_to_m(component.parameter_value("sdiam")),
|
||||
wire_diameter_m=mm_to_m(component.parameter_value("wdiam")),
|
||||
data_paths=_data_paths(variable_catalog, component.alias),
|
||||
)
|
||||
|
||||
|
||||
def _build_mechanical_node(
|
||||
component: TestMqlResolvedComponent,
|
||||
variable_catalog: TestMqlVariableCatalog | None,
|
||||
) -> TestMqlMechanicalNodeSpec:
|
||||
return TestMqlMechanicalNodeSpec(
|
||||
alias=component.alias,
|
||||
port_count=int(component.parameter_value("v1")),
|
||||
sum_mode=int(component.parameter_value("sum")),
|
||||
data_paths=_data_paths(variable_catalog, component.alias),
|
||||
)
|
||||
|
||||
|
||||
def _limit_constraint_holds(
|
||||
spec: TestMqlMassEndstopSpec,
|
||||
state: TestMqlMechanicalMassState,
|
||||
applied_force_n: float,
|
||||
) -> bool:
|
||||
if abs(state.velocity_m_s) > spec.stick_velocity_threshold_m_s:
|
||||
return False
|
||||
at_lower_limit = state.displacement_m <= spec.xmin_m + spec.min_penetration_m
|
||||
at_upper_limit = state.displacement_m >= spec.xmax_m - spec.max_penetration_m
|
||||
return (at_lower_limit and applied_force_n <= 0.0) or (
|
||||
at_upper_limit and applied_force_n >= 0.0
|
||||
)
|
||||
|
||||
|
||||
def _build_piecewise_signal(
|
||||
component: TestMqlResolvedComponent,
|
||||
variable_catalog: TestMqlVariableCatalog | None,
|
||||
) -> TestMqlPiecewiseLinearSignalSpec:
|
||||
starts = tuple(component.parameter_value(f"start{index}") for index in range(1, 9))
|
||||
ends = tuple(component.parameter_value(f"end{index}") for index in range(1, 9))
|
||||
durations = tuple(component.parameter_value(f"t{index}") for index in range(1, 9))
|
||||
return TestMqlPiecewiseLinearSignalSpec(
|
||||
alias=component.alias,
|
||||
t_start_s=component.parameter_value("tstart"),
|
||||
starts=starts,
|
||||
ends=ends,
|
||||
durations_s=durations,
|
||||
stage_count=int(component.parameter_value("nstages")),
|
||||
is_cyclic=bool(int(component.parameter_value("iscyclic"))),
|
||||
data_paths=_data_paths(variable_catalog, component.alias),
|
||||
)
|
||||
|
||||
|
||||
def _build_force_connector(
|
||||
component: TestMqlResolvedComponent,
|
||||
variable_catalog: TestMqlVariableCatalog | None,
|
||||
) -> TestMqlForceConnectorSpec:
|
||||
signal_alias_by_force_connector = {
|
||||
"forcecon_1": "piecewiselinear",
|
||||
"forcecon_2": "piecewiselinear_1",
|
||||
}
|
||||
target_mass_by_force_connector = {
|
||||
"forcecon_1": "mass_friction_endstops_19",
|
||||
"forcecon_2": "mass_friction_endstops_18",
|
||||
}
|
||||
return TestMqlForceConnectorSpec(
|
||||
alias=component.alias,
|
||||
signal_alias=signal_alias_by_force_connector[component.alias],
|
||||
target_mass_alias=target_mass_by_force_connector[component.alias],
|
||||
data_paths=_data_paths(variable_catalog, component.alias),
|
||||
)
|
||||
|
||||
|
||||
def n_per_mm_to_n_per_m(value: float) -> float:
|
||||
return value * N_PER_MM_TO_N_PER_M
|
||||
|
||||
|
||||
def n_per_mm_per_s_to_n_per_m_per_s(value: float) -> float:
|
||||
return value * N_PER_MM_PER_S_TO_N_PER_M_PER_S
|
||||
|
||||
|
||||
def _initial_value(amesim_results: AmesimResults | None, data_path: str) -> float:
|
||||
if amesim_results is None:
|
||||
return 0.0
|
||||
return float(amesim_results.series(data_path)[0])
|
||||
|
||||
|
||||
def _data_paths(
|
||||
variable_catalog: TestMqlVariableCatalog | None,
|
||||
alias: str,
|
||||
) -> tuple[str, ...]:
|
||||
if variable_catalog is None:
|
||||
return ()
|
||||
return variable_catalog.data_paths_for_owner(alias)
|
||||
@@ -0,0 +1,215 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.examples.test_mql.system import COMPONENT_SPECS
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPneumaticNode3Balance:
|
||||
temperature_k: float
|
||||
pressure_pa: float
|
||||
port_1_enthalpy_flow_w: float
|
||||
port_1_mass_flow_g_s: float
|
||||
port_1_volume_derivative_l_min: float
|
||||
port_1_volume_cm3: float
|
||||
port_2_enthalpy_flow_w: float
|
||||
port_2_mass_flow_g_s: float
|
||||
port_2_volume_derivative_l_min: float
|
||||
port_2_volume_cm3: float
|
||||
port_3_enthalpy_flow_w: float
|
||||
port_3_mass_flow_g_s: float
|
||||
port_3_volume_derivative_l_min: float
|
||||
port_3_volume_cm3: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPneumaticNode3:
|
||||
"""Exact algebraic contract of AMESim ``PN3NODE2``.
|
||||
|
||||
Pressure and temperature are fixed by port 2 and duplicated to ports 1 and
|
||||
3. Flow and volume signals at port 2 are the sums of ports 1 and 3, matching
|
||||
the ``EXPRESS2`` equations stored in ``test_mql_.cir``.
|
||||
"""
|
||||
|
||||
alias: str
|
||||
|
||||
def balance(
|
||||
self,
|
||||
*,
|
||||
port_2_temperature_k: float,
|
||||
port_2_pressure_pa: float,
|
||||
port_1_enthalpy_flow_w: float,
|
||||
port_1_mass_flow_g_s: float,
|
||||
port_3_enthalpy_flow_w: float,
|
||||
port_3_mass_flow_g_s: float,
|
||||
port_1_volume_derivative_l_min: float = 0.0,
|
||||
port_1_volume_cm3: float = 0.0,
|
||||
port_3_volume_derivative_l_min: float = 0.0,
|
||||
port_3_volume_cm3: float = 0.0,
|
||||
) -> TestMqlPneumaticNode3Balance:
|
||||
if port_2_temperature_k <= 0.0:
|
||||
raise ValueError("port_2_temperature_k must be positive")
|
||||
if port_2_pressure_pa <= 0.0:
|
||||
raise ValueError("port_2_pressure_pa must be positive")
|
||||
return TestMqlPneumaticNode3Balance(
|
||||
temperature_k=port_2_temperature_k,
|
||||
pressure_pa=port_2_pressure_pa,
|
||||
port_1_enthalpy_flow_w=port_1_enthalpy_flow_w,
|
||||
port_1_mass_flow_g_s=port_1_mass_flow_g_s,
|
||||
port_1_volume_derivative_l_min=port_1_volume_derivative_l_min,
|
||||
port_1_volume_cm3=port_1_volume_cm3,
|
||||
port_2_enthalpy_flow_w=(
|
||||
port_1_enthalpy_flow_w + port_3_enthalpy_flow_w
|
||||
),
|
||||
port_2_mass_flow_g_s=port_1_mass_flow_g_s + port_3_mass_flow_g_s,
|
||||
port_2_volume_derivative_l_min=(
|
||||
port_1_volume_derivative_l_min + port_3_volume_derivative_l_min
|
||||
),
|
||||
port_2_volume_cm3=port_1_volume_cm3 + port_3_volume_cm3,
|
||||
port_3_enthalpy_flow_w=port_3_enthalpy_flow_w,
|
||||
port_3_mass_flow_g_s=port_3_mass_flow_g_s,
|
||||
port_3_volume_derivative_l_min=port_3_volume_derivative_l_min,
|
||||
port_3_volume_cm3=port_3_volume_cm3,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPneumaticNode4Balance:
|
||||
temperature_k: float
|
||||
pressure_pa: float
|
||||
port_1_enthalpy_flow_w: float
|
||||
port_1_mass_flow_g_s: float
|
||||
port_1_volume_derivative_l_min: float
|
||||
port_1_volume_cm3: float
|
||||
port_2_enthalpy_flow_w: float
|
||||
port_2_mass_flow_g_s: float
|
||||
port_2_volume_derivative_l_min: float
|
||||
port_2_volume_cm3: float
|
||||
port_3_enthalpy_flow_w: float
|
||||
port_3_mass_flow_g_s: float
|
||||
port_3_volume_derivative_l_min: float
|
||||
port_3_volume_cm3: float
|
||||
port_4_enthalpy_flow_w: float
|
||||
port_4_mass_flow_g_s: float
|
||||
port_4_volume_derivative_l_min: float
|
||||
port_4_volume_cm3: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPneumaticNode4:
|
||||
"""Exact algebraic contract of AMESim ``P4NODE2``.
|
||||
|
||||
Pressure and temperature are fixed by port 2 and duplicated to ports 1, 3,
|
||||
and 4. Flow and volume signals at port 2 are the sums of ports 1, 3, and
|
||||
4, matching the saved AMESim variables for ``pnnode4_*`` instances.
|
||||
"""
|
||||
|
||||
alias: str
|
||||
|
||||
def balance(
|
||||
self,
|
||||
*,
|
||||
port_2_temperature_k: float,
|
||||
port_2_pressure_pa: float,
|
||||
port_1_enthalpy_flow_w: float,
|
||||
port_1_mass_flow_g_s: float,
|
||||
port_3_enthalpy_flow_w: float,
|
||||
port_3_mass_flow_g_s: float,
|
||||
port_4_enthalpy_flow_w: float,
|
||||
port_4_mass_flow_g_s: float,
|
||||
port_1_volume_derivative_l_min: float = 0.0,
|
||||
port_1_volume_cm3: float = 0.0,
|
||||
port_3_volume_derivative_l_min: float = 0.0,
|
||||
port_3_volume_cm3: float = 0.0,
|
||||
port_4_volume_derivative_l_min: float = 0.0,
|
||||
port_4_volume_cm3: float = 0.0,
|
||||
) -> TestMqlPneumaticNode4Balance:
|
||||
if port_2_temperature_k <= 0.0:
|
||||
raise ValueError("port_2_temperature_k must be positive")
|
||||
if port_2_pressure_pa <= 0.0:
|
||||
raise ValueError("port_2_pressure_pa must be positive")
|
||||
return TestMqlPneumaticNode4Balance(
|
||||
temperature_k=port_2_temperature_k,
|
||||
pressure_pa=port_2_pressure_pa,
|
||||
port_1_enthalpy_flow_w=port_1_enthalpy_flow_w,
|
||||
port_1_mass_flow_g_s=port_1_mass_flow_g_s,
|
||||
port_1_volume_derivative_l_min=port_1_volume_derivative_l_min,
|
||||
port_1_volume_cm3=port_1_volume_cm3,
|
||||
port_2_enthalpy_flow_w=(
|
||||
port_1_enthalpy_flow_w
|
||||
+ port_3_enthalpy_flow_w
|
||||
+ port_4_enthalpy_flow_w
|
||||
),
|
||||
port_2_mass_flow_g_s=(
|
||||
port_1_mass_flow_g_s
|
||||
+ port_3_mass_flow_g_s
|
||||
+ port_4_mass_flow_g_s
|
||||
),
|
||||
port_2_volume_derivative_l_min=(
|
||||
port_1_volume_derivative_l_min
|
||||
+ port_3_volume_derivative_l_min
|
||||
+ port_4_volume_derivative_l_min
|
||||
),
|
||||
port_2_volume_cm3=(
|
||||
port_1_volume_cm3 + port_3_volume_cm3 + port_4_volume_cm3
|
||||
),
|
||||
port_3_enthalpy_flow_w=port_3_enthalpy_flow_w,
|
||||
port_3_mass_flow_g_s=port_3_mass_flow_g_s,
|
||||
port_3_volume_derivative_l_min=port_3_volume_derivative_l_min,
|
||||
port_3_volume_cm3=port_3_volume_cm3,
|
||||
port_4_enthalpy_flow_w=port_4_enthalpy_flow_w,
|
||||
port_4_mass_flow_g_s=port_4_mass_flow_g_s,
|
||||
port_4_volume_derivative_l_min=port_4_volume_derivative_l_min,
|
||||
port_4_volume_cm3=port_4_volume_cm3,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlP4NodePortConnection:
|
||||
line_alias: str
|
||||
local_node_alias: str
|
||||
local_port: str
|
||||
remote_node_alias: str
|
||||
remote_port: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlP4NodePrimaryConnection:
|
||||
line_alias: str
|
||||
node_alias: str
|
||||
node_port: str
|
||||
chamber_alias: str
|
||||
chamber_port: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlP4NodeOrificeConnection:
|
||||
orifice_alias: str
|
||||
node_alias: str
|
||||
node_port: str
|
||||
direct_line_alias: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlP4NodeNeighborhood:
|
||||
node_alias: str
|
||||
primary: TestMqlP4NodePrimaryConnection
|
||||
port_1: TestMqlP4NodePortConnection
|
||||
port_3: TestMqlP4NodePortConnection
|
||||
port_4: TestMqlP4NodeOrificeConnection
|
||||
|
||||
|
||||
def build_test_mql_node3_assembly() -> dict[str, TestMqlPneumaticNode3]:
|
||||
return {
|
||||
str(spec["alias"]): TestMqlPneumaticNode3(alias=str(spec["alias"]))
|
||||
for spec in COMPONENT_SPECS
|
||||
if spec["submodel"] == "PN3NODE2"
|
||||
}
|
||||
|
||||
def build_test_mql_node4_assembly() -> dict[str, TestMqlPneumaticNode4]:
|
||||
return {
|
||||
str(spec["alias"]): TestMqlPneumaticNode4(alias=str(spec["alias"]))
|
||||
for spec in COMPONENT_SPECS
|
||||
if spec["submodel"] == "P4NODE2"
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic import (
|
||||
HELIUM_PNEUMATIC_GAS,
|
||||
AmesimPneumaticGas,
|
||||
AmesimPneumaticOrifice,
|
||||
AmesimPneumaticVolume,
|
||||
AmesimVariablePneumaticVolume,
|
||||
)
|
||||
from app.simulation.examples.test_mql.config import TestMqlConfig, TestMqlResolvedComponent
|
||||
|
||||
|
||||
AMESIM_REFERENCE_PRESSURE_PA = 101_300.0
|
||||
BAR_TO_PA = 1.0e5
|
||||
DEFAULT_TEST_MQL_TEMPERATURE_K = 293.15
|
||||
DEFAULT_VARIABLE_CHAMBER_PRESSURE_BAR = 1.0
|
||||
# Matched to PNVO001 event-window mass flow near the 0.04 s opening event.
|
||||
TEST_MQL_PNVO001_FLOW_COEFFICIENT_MULTIPLIER = 0.99805
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlStepSignalSpec:
|
||||
alias: str
|
||||
initial_output: float
|
||||
final_output: float
|
||||
step_time_s: float
|
||||
transition_duration_s: float
|
||||
transition_type: int
|
||||
|
||||
def output_at(self, time_s: float) -> float:
|
||||
if self.transition_type != 1:
|
||||
raise ValueError(
|
||||
f"unsupported STEP0 transition type {self.transition_type} on {self.alias}"
|
||||
)
|
||||
return self.initial_output if time_s < self.step_time_s else self.final_output
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlVariableOrificeControl:
|
||||
orifice_alias: str
|
||||
step: TestMqlStepSignalSpec
|
||||
|
||||
def opening_at(self, time_s: float) -> float:
|
||||
return self.step.output_at(time_s)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPneumaticAssembly:
|
||||
fixed_chambers: dict[str, AmesimPneumaticVolume]
|
||||
variable_chambers: dict[str, AmesimVariablePneumaticVolume]
|
||||
fixed_orifices: dict[str, AmesimPneumaticOrifice]
|
||||
variable_orifices: dict[str, AmesimPneumaticOrifice]
|
||||
variable_orifice_controls: dict[str, TestMqlVariableOrificeControl]
|
||||
fixed_initial_absolute_pressure_pa: float
|
||||
variable_initial_absolute_pressure_pa: float
|
||||
|
||||
@property
|
||||
def initial_pressure_pa(self) -> float:
|
||||
return pressure_to_amesim_gauge_pa(self.fixed_initial_absolute_pressure_pa)
|
||||
|
||||
@property
|
||||
def fixed_initial_gauge_pressure_pa(self) -> float:
|
||||
return pressure_to_amesim_gauge_pa(self.fixed_initial_absolute_pressure_pa)
|
||||
|
||||
@property
|
||||
def variable_initial_gauge_pressure_pa(self) -> float:
|
||||
return pressure_to_amesim_gauge_pa(self.variable_initial_absolute_pressure_pa)
|
||||
|
||||
@property
|
||||
def chamber_count(self) -> int:
|
||||
return len(self.fixed_chambers) + len(self.variable_chambers)
|
||||
|
||||
@property
|
||||
def orifice_count(self) -> int:
|
||||
return len(self.fixed_orifices) + len(self.variable_orifices)
|
||||
|
||||
@property
|
||||
def component_count(self) -> int:
|
||||
return self.chamber_count + self.orifice_count
|
||||
|
||||
@property
|
||||
def variable_orifice_control_count(self) -> int:
|
||||
return len(self.variable_orifice_controls)
|
||||
|
||||
def set_variable_orifice_openings(self, time_s: float) -> None:
|
||||
for alias, control in self.variable_orifice_controls.items():
|
||||
self.variable_orifices[alias].opening = control.opening_at(time_s)
|
||||
|
||||
@property
|
||||
def aliases(self) -> tuple[str, ...]:
|
||||
return tuple(
|
||||
[
|
||||
*self.fixed_chambers,
|
||||
*self.variable_chambers,
|
||||
*self.fixed_orifices,
|
||||
*self.variable_orifices,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def build_test_mql_pneumatic_assembly(
|
||||
config: TestMqlConfig | None = None,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
) -> TestMqlPneumaticAssembly:
|
||||
config = config or TestMqlConfig.from_amesim_specs()
|
||||
fixed_initial_absolute_pressure_pa = absolute_pressure_from_amesim_bar_parameter(
|
||||
config.global_parameters["P0"]
|
||||
)
|
||||
variable_initial_absolute_pressure_pa = absolute_pressure_from_amesim_bar_parameter(
|
||||
DEFAULT_VARIABLE_CHAMBER_PRESSURE_BAR
|
||||
)
|
||||
fixed_chambers = {
|
||||
component.alias: _build_chamber(
|
||||
component,
|
||||
volume_parameter="cvol",
|
||||
gas=gas,
|
||||
initial_pressure_pa=fixed_initial_absolute_pressure_pa,
|
||||
)
|
||||
for component in config.components_by_submodel("PNCH023")
|
||||
}
|
||||
variable_chambers = {
|
||||
component.alias: _build_chamber(
|
||||
component,
|
||||
volume_parameter="cvol0",
|
||||
gas=gas,
|
||||
initial_pressure_pa=variable_initial_absolute_pressure_pa,
|
||||
)
|
||||
for component in config.components_by_submodel("PNCH012")
|
||||
}
|
||||
fixed_orifices = {
|
||||
component.alias: _build_orifice(
|
||||
component,
|
||||
area_parameter="area",
|
||||
gas=gas,
|
||||
opening=1.0,
|
||||
)
|
||||
for component in config.components_by_submodel("PNOR001")
|
||||
}
|
||||
variable_orifice_controls = _build_variable_orifice_controls(config)
|
||||
variable_orifices = {
|
||||
component.alias: _build_orifice(
|
||||
component,
|
||||
area_parameter="area0",
|
||||
gas=gas,
|
||||
opening=variable_orifice_controls[component.alias].opening_at(0.0),
|
||||
)
|
||||
for component in config.components_by_submodel("PNVO001")
|
||||
}
|
||||
return TestMqlPneumaticAssembly(
|
||||
fixed_chambers=fixed_chambers,
|
||||
variable_chambers=variable_chambers,
|
||||
fixed_orifices=fixed_orifices,
|
||||
variable_orifices=variable_orifices,
|
||||
variable_orifice_controls=variable_orifice_controls,
|
||||
fixed_initial_absolute_pressure_pa=fixed_initial_absolute_pressure_pa,
|
||||
variable_initial_absolute_pressure_pa=variable_initial_absolute_pressure_pa,
|
||||
)
|
||||
|
||||
|
||||
def _build_variable_orifice_controls(
|
||||
config: TestMqlConfig,
|
||||
) -> dict[str, TestMqlVariableOrificeControl]:
|
||||
from app.simulation.examples.test_mql.system import CONNECTION_SPECS
|
||||
|
||||
components_by_alias = {component.alias: component for component in config.components}
|
||||
variable_orifice_aliases = {
|
||||
component.alias for component in config.components_by_submodel("PNVO001")
|
||||
}
|
||||
controls: dict[str, TestMqlVariableOrificeControl] = {}
|
||||
for connection in CONNECTION_SPECS:
|
||||
if connection["submodel"] != "DIRECT":
|
||||
continue
|
||||
source_alias = str(connection["source_component"])
|
||||
target_alias = str(connection["target_component"])
|
||||
if target_alias in variable_orifice_aliases:
|
||||
orifice_alias = target_alias
|
||||
step_alias = source_alias
|
||||
elif source_alias in variable_orifice_aliases:
|
||||
orifice_alias = source_alias
|
||||
step_alias = target_alias
|
||||
else:
|
||||
continue
|
||||
step_component = components_by_alias.get(step_alias)
|
||||
if step_component is None or step_component.submodel != "STEP0":
|
||||
continue
|
||||
controls[orifice_alias] = TestMqlVariableOrificeControl(
|
||||
orifice_alias=orifice_alias,
|
||||
step=TestMqlStepSignalSpec(
|
||||
alias=step_alias,
|
||||
initial_output=step_component.parameter_value("out0"),
|
||||
final_output=step_component.parameter_value("out1"),
|
||||
step_time_s=step_component.parameter_value("t0"),
|
||||
transition_duration_s=step_component.parameter_value("td"),
|
||||
transition_type=int(step_component.parameter_value("transitionType")),
|
||||
),
|
||||
)
|
||||
missing = variable_orifice_aliases - controls.keys()
|
||||
if missing:
|
||||
raise ValueError(
|
||||
"missing STEP0 controls for PNVO001 components: "
|
||||
+ ", ".join(sorted(missing))
|
||||
)
|
||||
return controls
|
||||
|
||||
|
||||
def absolute_pressure_from_amesim_bar_parameter(pressure_bar: float) -> float:
|
||||
return pressure_bar * BAR_TO_PA
|
||||
|
||||
|
||||
def pressure_to_amesim_gauge_pa(absolute_pressure_pa: float) -> float:
|
||||
return absolute_pressure_pa - AMESIM_REFERENCE_PRESSURE_PA
|
||||
|
||||
|
||||
def pressure_from_amesim_bar_parameter(pressure_bar: float) -> float:
|
||||
return pressure_to_amesim_gauge_pa(absolute_pressure_from_amesim_bar_parameter(pressure_bar))
|
||||
|
||||
|
||||
def _build_chamber(
|
||||
component: TestMqlResolvedComponent,
|
||||
*,
|
||||
volume_parameter: str,
|
||||
gas: AmesimPneumaticGas,
|
||||
initial_pressure_pa: float,
|
||||
) -> AmesimPneumaticVolume:
|
||||
if volume_parameter == "cvol0":
|
||||
return AmesimVariablePneumaticVolume.from_liters(
|
||||
name=component.alias,
|
||||
dead_volume_liters=component.parameter_value(volume_parameter),
|
||||
gas=gas,
|
||||
p0=initial_pressure_pa,
|
||||
T0=_component_temperature(component),
|
||||
heat_transfer_coefficient=component.parameter_value("kth"),
|
||||
heat_transfer_area=component.parameter_value("sth"),
|
||||
external_temperature_k=_component_temperature(component),
|
||||
)
|
||||
return AmesimPneumaticVolume.from_liters(
|
||||
name=component.alias,
|
||||
volume_liters=component.parameter_value(volume_parameter),
|
||||
gas=gas,
|
||||
p0=initial_pressure_pa,
|
||||
T0=_component_temperature(component),
|
||||
heat_transfer_coefficient=component.parameter_value("kth"),
|
||||
heat_transfer_area=component.parameter_value("sth"),
|
||||
external_temperature_k=_component_temperature(component),
|
||||
)
|
||||
|
||||
|
||||
def _build_orifice(
|
||||
component: TestMqlResolvedComponent,
|
||||
*,
|
||||
area_parameter: str,
|
||||
gas: AmesimPneumaticGas,
|
||||
opening: float,
|
||||
) -> AmesimPneumaticOrifice:
|
||||
flow_coefficient = component.parameter_value("cq")
|
||||
if component.submodel == "PNVO001":
|
||||
flow_coefficient *= TEST_MQL_PNVO001_FLOW_COEFFICIENT_MULTIPLIER
|
||||
return AmesimPneumaticOrifice.from_mm2(
|
||||
name=component.alias,
|
||||
area_mm2=component.parameter_value(area_parameter),
|
||||
flow_coefficient=flow_coefficient,
|
||||
gas=gas,
|
||||
opening=opening,
|
||||
)
|
||||
|
||||
|
||||
def _component_temperature(component: TestMqlResolvedComponent) -> float:
|
||||
parameter = component.parameters.get("extemp")
|
||||
if parameter is None or parameter.value is None:
|
||||
return DEFAULT_TEST_MQL_TEMPERATURE_K
|
||||
return parameter.value
|
||||
@@ -0,0 +1,194 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic import (
|
||||
HELIUM_PNEUMATIC_GAS,
|
||||
AmesimPneumaticGas,
|
||||
)
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic_lines import (
|
||||
AmesimPnl0001Pipe,
|
||||
AmesimPnl0002Pipe,
|
||||
AmesimPnl0003Pipe,
|
||||
AmesimPnl00rPipe,
|
||||
)
|
||||
from app.simulation.examples.test_mql.line_parameters import (
|
||||
TestMqlPnl0001Spec,
|
||||
TestMqlPnl0002Spec,
|
||||
TestMqlPnl0003Spec,
|
||||
TestMqlPnl00rSpec,
|
||||
load_test_mql_pnl0001_specs,
|
||||
load_test_mql_pnl0002_specs,
|
||||
load_test_mql_pnl0003_specs,
|
||||
load_test_mql_pnl00r_specs,
|
||||
)
|
||||
|
||||
|
||||
TEST_MQL_PNL0001_D20_L1_LINEAR_CONDUCTANCE = 5.5636e-6
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPnl0001Assembly:
|
||||
specs: tuple[TestMqlPnl0001Spec, ...]
|
||||
lines: dict[str, AmesimPnl0001Pipe]
|
||||
|
||||
def spec(self, alias: str) -> TestMqlPnl0001Spec:
|
||||
for spec in self.specs:
|
||||
if spec.alias == alias:
|
||||
return spec
|
||||
raise KeyError(alias)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPnl0002Assembly:
|
||||
specs: tuple[TestMqlPnl0002Spec, ...]
|
||||
lines: dict[str, AmesimPnl0002Pipe]
|
||||
|
||||
def spec(self, alias: str) -> TestMqlPnl0002Spec:
|
||||
for spec in self.specs:
|
||||
if spec.alias == alias:
|
||||
return spec
|
||||
raise KeyError(alias)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPnl0003Assembly:
|
||||
specs: tuple[TestMqlPnl0003Spec, ...]
|
||||
lines: dict[str, AmesimPnl0003Pipe]
|
||||
|
||||
def spec(self, alias: str) -> TestMqlPnl0003Spec:
|
||||
for spec in self.specs:
|
||||
if spec.alias == alias:
|
||||
return spec
|
||||
raise KeyError(alias)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPnl00rAssembly:
|
||||
specs: tuple[TestMqlPnl00rSpec, ...]
|
||||
lines: dict[str, AmesimPnl00rPipe]
|
||||
|
||||
def spec(self, alias: str) -> TestMqlPnl00rSpec:
|
||||
for spec in self.specs:
|
||||
if spec.alias == alias:
|
||||
return spec
|
||||
raise KeyError(alias)
|
||||
|
||||
|
||||
def build_test_mql_pnl0001_assembly(
|
||||
archive_path: str | Path,
|
||||
*,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
) -> TestMqlPnl0001Assembly:
|
||||
specs = load_test_mql_pnl0001_specs(archive_path)
|
||||
lines = {
|
||||
spec.alias: AmesimPnl0001Pipe(
|
||||
name=spec.alias,
|
||||
diameter_mm=spec.diameter_mm,
|
||||
length_m=spec.length_m,
|
||||
relative_roughness=spec.relative_roughness,
|
||||
polytropic_constant=spec.polytropic_constant,
|
||||
heat_transfer_coefficient=spec.heat_transfer_coefficient,
|
||||
external_temperature_k=spec.external_temperature_k,
|
||||
calibrated_linear_conductance=(
|
||||
_test_mql_pnl0001_calibrated_linear_conductance(spec)
|
||||
),
|
||||
gas=gas,
|
||||
p0=spec.initial_absolute_pressure_pa,
|
||||
T0=spec.initial_temperature_k,
|
||||
)
|
||||
for spec in specs
|
||||
}
|
||||
return TestMqlPnl0001Assembly(specs=specs, lines=lines)
|
||||
|
||||
|
||||
def _test_mql_pnl0001_calibrated_linear_conductance(
|
||||
spec: TestMqlPnl0001Spec,
|
||||
) -> float | None:
|
||||
if spec.target_component.startswith("pn_c1_") and _matches_geometry(
|
||||
spec, diameter_mm=20.0, length_m=1.0
|
||||
):
|
||||
return TEST_MQL_PNL0001_D20_L1_LINEAR_CONDUCTANCE
|
||||
return None
|
||||
|
||||
|
||||
def _matches_geometry(
|
||||
spec: TestMqlPnl0001Spec,
|
||||
*,
|
||||
diameter_mm: float,
|
||||
length_m: float,
|
||||
) -> bool:
|
||||
return (
|
||||
abs(spec.diameter_mm - diameter_mm) < 1.0e-12
|
||||
and abs(spec.length_m - length_m) < 1.0e-12
|
||||
)
|
||||
|
||||
|
||||
def build_test_mql_pnl0002_assembly(
|
||||
archive_path: str | Path,
|
||||
*,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
) -> TestMqlPnl0002Assembly:
|
||||
specs = load_test_mql_pnl0002_specs(archive_path)
|
||||
lines = {
|
||||
spec.alias: AmesimPnl0002Pipe(
|
||||
name=spec.alias,
|
||||
diameter_mm=spec.diameter_mm,
|
||||
length_m=spec.length_m,
|
||||
relative_roughness=spec.relative_roughness,
|
||||
polytropic_constant=spec.polytropic_constant,
|
||||
heat_transfer_coefficient=spec.heat_transfer_coefficient,
|
||||
external_temperature_k=spec.external_temperature_k,
|
||||
gas=gas,
|
||||
pctr_0=spec.initial_center_absolute_pressure_pa,
|
||||
Tctr_0=spec.initial_center_temperature_k,
|
||||
)
|
||||
for spec in specs
|
||||
}
|
||||
return TestMqlPnl0002Assembly(specs=specs, lines=lines)
|
||||
|
||||
|
||||
def build_test_mql_pnl0003_assembly(
|
||||
archive_path: str | Path,
|
||||
*,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
) -> TestMqlPnl0003Assembly:
|
||||
specs = load_test_mql_pnl0003_specs(archive_path)
|
||||
lines = {
|
||||
spec.alias: AmesimPnl0003Pipe(
|
||||
name=spec.alias,
|
||||
diameter_mm=spec.diameter_mm,
|
||||
length_m=spec.length_m,
|
||||
relative_roughness=spec.relative_roughness,
|
||||
polytropic_constant=spec.polytropic_constant,
|
||||
heat_transfer_coefficient=spec.heat_transfer_coefficient,
|
||||
external_temperature_k=spec.external_temperature_k,
|
||||
gas=gas,
|
||||
p1_0=spec.initial_absolute_pressure_1_pa,
|
||||
T1_0=spec.initial_temperature_1_k,
|
||||
p2_0=spec.initial_absolute_pressure_2_pa,
|
||||
T2_0=spec.initial_temperature_2_k,
|
||||
)
|
||||
for spec in specs
|
||||
}
|
||||
return TestMqlPnl0003Assembly(specs=specs, lines=lines)
|
||||
|
||||
|
||||
def build_test_mql_pnl00r_assembly(
|
||||
archive_path: str | Path,
|
||||
*,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
) -> TestMqlPnl00rAssembly:
|
||||
specs = load_test_mql_pnl00r_specs(archive_path)
|
||||
lines = {
|
||||
spec.alias: AmesimPnl00rPipe(
|
||||
name=spec.alias,
|
||||
diameter_mm=spec.diameter_mm,
|
||||
length_m=spec.length_m,
|
||||
relative_roughness=spec.relative_roughness,
|
||||
gas=gas,
|
||||
)
|
||||
for spec in specs
|
||||
}
|
||||
return TestMqlPnl00rAssembly(specs=specs, lines=lines)
|
||||
@@ -0,0 +1,128 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.simulation.examples.test_mql.closure import TestMqlPneumaticChamberSegmentSpec
|
||||
from app.simulation.examples.test_mql.topology import TestMqlCirTopology
|
||||
|
||||
|
||||
def discover_fixed_chamber_segments(
|
||||
topology: TestMqlCirTopology,
|
||||
component_specs: list[dict[str, object]],
|
||||
connection_specs: list[dict[str, object]],
|
||||
) -> tuple[TestMqlPneumaticChamberSegmentSpec, ...]:
|
||||
submodel_by_alias = {
|
||||
str(component["alias"]): str(component["submodel"])
|
||||
for component in component_specs
|
||||
}
|
||||
segments = []
|
||||
for component in component_specs:
|
||||
volume_alias = str(component["alias"])
|
||||
if component["submodel"] != "PNCH023":
|
||||
continue
|
||||
|
||||
orifice_contacts = []
|
||||
for contact in topology.contacts_for(volume_alias):
|
||||
other_alias, other_port = contact.other_endpoint(volume_alias)
|
||||
if submodel_by_alias.get(other_alias) == "PNOR001":
|
||||
orifice_contacts.append(
|
||||
(
|
||||
other_alias,
|
||||
other_port,
|
||||
contact.port_for(volume_alias),
|
||||
)
|
||||
)
|
||||
if len(orifice_contacts) != 2:
|
||||
raise ValueError(
|
||||
f"{volume_alias} must contact exactly two PNOR001 orifices; "
|
||||
f"found {len(orifice_contacts)}"
|
||||
)
|
||||
|
||||
sides = [
|
||||
_resolve_orifice_boundary(
|
||||
orifice_alias=orifice_alias,
|
||||
orifice_volume_port=orifice_volume_port,
|
||||
volume_port=volume_port,
|
||||
connection_specs=connection_specs,
|
||||
submodel_by_alias=submodel_by_alias,
|
||||
)
|
||||
for orifice_alias, orifice_volume_port, volume_port in orifice_contacts
|
||||
]
|
||||
inlet_sides = [side for side in sides if side["role"] == "inlet"]
|
||||
outlet_sides = [side for side in sides if side["role"] == "outlet"]
|
||||
if len(inlet_sides) != 1 or len(outlet_sides) != 1:
|
||||
raise ValueError(
|
||||
f"{volume_alias} requires one inlet and one outlet topology side"
|
||||
)
|
||||
inlet = inlet_sides[0]
|
||||
outlet = outlet_sides[0]
|
||||
segments.append(
|
||||
TestMqlPneumaticChamberSegmentSpec(
|
||||
name=f"{volume_alias}_segment",
|
||||
inlet_node_alias=inlet["node_alias"],
|
||||
inlet_line_alias=inlet["line_alias"],
|
||||
inlet_orifice_alias=inlet["orifice_alias"],
|
||||
inlet_orifice_boundary_port=inlet["orifice_boundary_port"],
|
||||
inlet_orifice_volume_port=inlet["orifice_volume_port"],
|
||||
volume_alias=volume_alias,
|
||||
volume_inlet_port=inlet["volume_port"],
|
||||
volume_outlet_port=outlet["volume_port"],
|
||||
outlet_orifice_alias=outlet["orifice_alias"],
|
||||
outlet_orifice_volume_port=outlet["orifice_volume_port"],
|
||||
outlet_orifice_boundary_port=outlet["orifice_boundary_port"],
|
||||
outlet_line_alias=outlet["line_alias"],
|
||||
outlet_node_alias=outlet["node_alias"],
|
||||
)
|
||||
)
|
||||
return tuple(segments)
|
||||
|
||||
|
||||
def _resolve_orifice_boundary(
|
||||
*,
|
||||
orifice_alias: str,
|
||||
orifice_volume_port: str,
|
||||
volume_port: str,
|
||||
connection_specs: list[dict[str, object]],
|
||||
submodel_by_alias: dict[str, str],
|
||||
) -> dict[str, str]:
|
||||
boundary_connections = []
|
||||
for connection in connection_specs:
|
||||
if (
|
||||
connection["source_component"] == orifice_alias
|
||||
and connection["source_port"] != orifice_volume_port
|
||||
) or (
|
||||
connection["target_component"] == orifice_alias
|
||||
and connection["target_port"] != orifice_volume_port
|
||||
):
|
||||
boundary_connections.append(connection)
|
||||
if len(boundary_connections) != 1:
|
||||
raise ValueError(
|
||||
f"{orifice_alias} must have exactly one non-volume boundary connection; "
|
||||
f"found {len(boundary_connections)}"
|
||||
)
|
||||
|
||||
connection = boundary_connections[0]
|
||||
if connection["submodel"] != "PNL0001":
|
||||
raise ValueError(
|
||||
f"{orifice_alias} boundary must use PNL0001, got {connection['submodel']}"
|
||||
)
|
||||
if connection["target_component"] == orifice_alias:
|
||||
role = "inlet"
|
||||
node_alias = str(connection["source_component"])
|
||||
orifice_boundary_port = str(connection["target_port"])
|
||||
else:
|
||||
role = "outlet"
|
||||
node_alias = str(connection["target_component"])
|
||||
orifice_boundary_port = str(connection["source_port"])
|
||||
if submodel_by_alias.get(node_alias) != "PN3NODE2":
|
||||
raise ValueError(
|
||||
f"{orifice_alias} PNL0001 boundary must terminate at PN3NODE2, "
|
||||
f"got {node_alias}"
|
||||
)
|
||||
return {
|
||||
"role": role,
|
||||
"node_alias": node_alias,
|
||||
"line_alias": str(connection["alias"]),
|
||||
"orifice_alias": orifice_alias,
|
||||
"orifice_boundary_port": orifice_boundary_port,
|
||||
"orifice_volume_port": orifice_volume_port,
|
||||
"volume_port": volume_port,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
"""Calibrated component primitives used only by the ``test_mql`` example."""
|
||||
@@ -0,0 +1,168 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import pi
|
||||
|
||||
|
||||
MM_TO_M = 1.0e-3
|
||||
M_TO_MM = 1.0e3
|
||||
M3_TO_CM3 = 1.0e6
|
||||
M3_PER_S_TO_L_PER_MIN = 60_000.0
|
||||
|
||||
|
||||
def circular_area(diameter_m: float) -> float:
|
||||
if diameter_m < 0.0:
|
||||
raise ValueError("diameter_m must be non-negative.")
|
||||
return pi * diameter_m * diameter_m / 4.0
|
||||
|
||||
|
||||
def mm_to_m(value: float) -> float:
|
||||
return value * MM_TO_M
|
||||
|
||||
|
||||
def m_to_mm(value: float) -> float:
|
||||
return value * M_TO_MM
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimPistonGeometry:
|
||||
"""Geometry relations used by AMESim PNRP17 pneumatic piston variables."""
|
||||
|
||||
piston_diameter_m: float
|
||||
rod_diameter_m: float = 0.0
|
||||
zero_length_m: float = 0.0
|
||||
|
||||
@property
|
||||
def piston_area_m2(self) -> float:
|
||||
return circular_area(self.piston_diameter_m)
|
||||
|
||||
@property
|
||||
def rod_area_m2(self) -> float:
|
||||
return circular_area(self.rod_diameter_m)
|
||||
|
||||
@property
|
||||
def annulus_area_m2(self) -> float:
|
||||
return self.piston_area_m2 - self.rod_area_m2
|
||||
|
||||
def chamber_length_m(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
|
||||
return self.zero_length_m + port5_displacement_m - port4_displacement_m
|
||||
|
||||
def chamber_length_mm(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
|
||||
return m_to_mm(self.chamber_length_m(port4_displacement_m, port5_displacement_m))
|
||||
|
||||
@property
|
||||
def chamber_area_m2(self) -> float:
|
||||
return self.annulus_area_m2
|
||||
|
||||
def chamber_volume_m3(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
|
||||
return self.chamber_area_m2 * self.chamber_length_m(
|
||||
port4_displacement_m,
|
||||
port5_displacement_m,
|
||||
)
|
||||
|
||||
def chamber_volume_cm3(self, port4_displacement_m: float, port5_displacement_m: float) -> float:
|
||||
return self.chamber_volume_m3(port4_displacement_m, port5_displacement_m) * M3_TO_CM3
|
||||
|
||||
def chamber_volume_rate_m3_s(self, port4_velocity_m_s: float, port5_velocity_m_s: float) -> float:
|
||||
return self.chamber_area_m2 * (port5_velocity_m_s - port4_velocity_m_s)
|
||||
|
||||
def chamber_volume_rate_l_min(self, port4_velocity_m_s: float, port5_velocity_m_s: float) -> float:
|
||||
return self.chamber_volume_rate_m3_s(
|
||||
port4_velocity_m_s,
|
||||
port5_velocity_m_s,
|
||||
) * M3_PER_S_TO_L_PER_MIN
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimElasticEndstop:
|
||||
"""Contact force part of AMESim LSTP00A elastic endstop."""
|
||||
|
||||
contact_stiffness_n_per_m: float
|
||||
contact_damping_n_per_m_per_s: float = 0.0
|
||||
gap0_m: float = 0.0
|
||||
|
||||
def penetration_m_from_gap_mm(self, gap_mm: float) -> float:
|
||||
return max(-(mm_to_m(gap_mm) - self.gap0_m), 0.0)
|
||||
|
||||
def static_contact_force(self, gap_mm: float) -> float:
|
||||
return self.contact_stiffness_n_per_m * self.penetration_m_from_gap_mm(gap_mm)
|
||||
|
||||
def contact_force(self, gap_mm: float, penetration_velocity_m_s: float = 0.0) -> float:
|
||||
if self.penetration_m_from_gap_mm(gap_mm) <= 0.0:
|
||||
return 0.0
|
||||
damping_force = self.contact_damping_n_per_m_per_s * penetration_velocity_m_s
|
||||
return max(self.static_contact_force(gap_mm) + damping_force, 0.0)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimMassFrictionEndstops:
|
||||
"""Parameter and observable helpers for AMESim MECMAS21 translation masses."""
|
||||
|
||||
mass_kg: float
|
||||
lower_limit_m: float
|
||||
upper_limit_m: float
|
||||
lower_stiffness_n_per_m: float
|
||||
upper_stiffness_n_per_m: float
|
||||
lower_damping_n_per_m_per_s: float = 0.0
|
||||
upper_damping_n_per_m_per_s: float = 0.0
|
||||
viscous_friction_n_per_m_per_s: float = 0.0
|
||||
coulomb_friction_n: float = 0.0
|
||||
stiction_force_n: float = 0.0
|
||||
windage_n_per_m2_per_s2: float = 0.0
|
||||
|
||||
def lower_penetration_m(self, displacement_m: float) -> float:
|
||||
return max(self.lower_limit_m - displacement_m, 0.0)
|
||||
|
||||
def upper_penetration_m(self, displacement_m: float) -> float:
|
||||
return max(displacement_m - self.upper_limit_m, 0.0)
|
||||
|
||||
def lower_static_force_magnitude(self, displacement_m: float) -> float:
|
||||
return self.lower_stiffness_n_per_m * self.lower_penetration_m(displacement_m)
|
||||
|
||||
def upper_static_force_magnitude(self, displacement_m: float) -> float:
|
||||
return self.upper_stiffness_n_per_m * self.upper_penetration_m(displacement_m)
|
||||
|
||||
def viscous_friction_force(self, velocity_m_s: float) -> float:
|
||||
return -self.viscous_friction_n_per_m_per_s * velocity_m_s
|
||||
|
||||
def windage_force(self, velocity_m_s: float) -> float:
|
||||
return -self.windage_n_per_m2_per_s2 * velocity_m_s * abs(velocity_m_s)
|
||||
|
||||
def dry_friction_force(self, velocity_m_s: float) -> float:
|
||||
if velocity_m_s > 0.0:
|
||||
return -self.coulomb_friction_n
|
||||
if velocity_m_s < 0.0:
|
||||
return self.coulomb_friction_n
|
||||
return 0.0
|
||||
|
||||
def limit_contact_force(self, displacement_m: float, velocity_m_s: float) -> float:
|
||||
lower_force = self.lower_static_force_magnitude(displacement_m)
|
||||
if lower_force > 0.0:
|
||||
lower_force += max(-self.lower_damping_n_per_m_per_s * velocity_m_s, 0.0)
|
||||
|
||||
upper_force = self.upper_static_force_magnitude(displacement_m)
|
||||
if upper_force > 0.0:
|
||||
upper_force += max(self.upper_damping_n_per_m_per_s * velocity_m_s, 0.0)
|
||||
|
||||
return lower_force - upper_force
|
||||
|
||||
def derivatives(
|
||||
self,
|
||||
*,
|
||||
velocity_m_s: float,
|
||||
displacement_m: float,
|
||||
port_1_force_n: float = 0.0,
|
||||
port_2_force_n: float = 0.0,
|
||||
external_force_n: float = 0.0,
|
||||
) -> tuple[float, float]:
|
||||
total_force = (
|
||||
port_1_force_n
|
||||
+ port_2_force_n
|
||||
+ external_force_n
|
||||
+ self.viscous_friction_force(velocity_m_s)
|
||||
+ self.windage_force(velocity_m_s)
|
||||
+ self.dry_friction_force(velocity_m_s)
|
||||
+ self.limit_contact_force(displacement_m, velocity_m_s)
|
||||
)
|
||||
return total_force / self.mass_kg, velocity_m_s
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import pi, sqrt
|
||||
|
||||
from app.simulation.core.base import AlgebraicComponent, DynamicComponent
|
||||
from app.simulation.core.medium import ThermodynamicProperties
|
||||
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
|
||||
from app.simulation.core.ports import PortState
|
||||
from app.simulation.core.state import VolumeState
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimPneumaticGas:
|
||||
"""Caloric constants plus Peng-Robinson EOS for AMESim pneumatic components."""
|
||||
|
||||
fluid: PengRobinsonFluid = HELIUM_PR
|
||||
cp: float = 5193.0
|
||||
cv: float = 3116.0
|
||||
|
||||
@property
|
||||
def gamma(self) -> float:
|
||||
return self.cp / self.cv
|
||||
|
||||
@property
|
||||
def R_gas(self) -> float:
|
||||
return self.fluid.specific_gas_constant
|
||||
|
||||
def density(self, pressure: float, temperature: float) -> float:
|
||||
return self.fluid.density(pressure, temperature)
|
||||
|
||||
def pressure(self, density: float, temperature: float) -> float:
|
||||
return self.fluid.pressure_from_density(temperature, density)
|
||||
|
||||
def specific_internal_energy(self, temperature: float) -> float:
|
||||
return self.cv * temperature
|
||||
|
||||
def specific_enthalpy(self, temperature: float) -> float:
|
||||
return self.cp * temperature
|
||||
|
||||
def specific_reference_enthalpy(
|
||||
self,
|
||||
temperature: float,
|
||||
reference_temperature: float = 298.15,
|
||||
) -> float:
|
||||
return self.cp * (temperature - reference_temperature)
|
||||
|
||||
def reference_temperature_from_specific_enthalpy(
|
||||
self,
|
||||
specific_enthalpy: float,
|
||||
reference_temperature: float = 298.15,
|
||||
) -> float:
|
||||
if self.cp <= 0.0:
|
||||
raise ValueError("cp must be positive.")
|
||||
return reference_temperature + specific_enthalpy / self.cp
|
||||
|
||||
def pressure_reference_enthalpy(
|
||||
self,
|
||||
pressure: float,
|
||||
temperature: float,
|
||||
reference_pressure: float = 101_300.0,
|
||||
reference_temperature: float = 298.15,
|
||||
) -> float:
|
||||
return (
|
||||
self.specific_reference_enthalpy(temperature, reference_temperature)
|
||||
+ self.fluid.residual_specific_enthalpy(pressure, temperature)
|
||||
- self.fluid.residual_specific_enthalpy(
|
||||
reference_pressure,
|
||||
reference_temperature,
|
||||
)
|
||||
)
|
||||
|
||||
def pressure_transport_enthalpy(
|
||||
self,
|
||||
pressure: float,
|
||||
temperature: float,
|
||||
reference_pressure: float = 101_300.0,
|
||||
reference_temperature: float = 298.15,
|
||||
) -> float:
|
||||
"""Convert AMESim reference enthalpy to the absolute-energy state basis."""
|
||||
return (
|
||||
self.pressure_reference_enthalpy(
|
||||
pressure,
|
||||
temperature,
|
||||
reference_pressure,
|
||||
reference_temperature,
|
||||
)
|
||||
+ self.cp * reference_temperature
|
||||
)
|
||||
|
||||
def temperature_from_internal_energy(self, specific_internal_energy: float) -> float:
|
||||
if self.cv <= 0.0:
|
||||
raise ValueError("cv must be positive.")
|
||||
return specific_internal_energy / self.cv
|
||||
|
||||
|
||||
HELIUM_PNEUMATIC_GAS = AmesimPneumaticGas()
|
||||
|
||||
|
||||
def liters_to_m3(value: float) -> float:
|
||||
return value * 1.0e-3
|
||||
|
||||
|
||||
def m3_to_cm3(value: float) -> float:
|
||||
return value * 1.0e6
|
||||
|
||||
|
||||
def cm3_to_m3(value: float) -> float:
|
||||
return value * 1.0e-6
|
||||
|
||||
|
||||
def kg_to_g(value: float) -> float:
|
||||
return value * 1.0e3
|
||||
|
||||
|
||||
def mm2_to_m2(value: float) -> float:
|
||||
return value * 1.0e-6
|
||||
|
||||
|
||||
def diameter_mm_to_area_m2(diameter_mm: float) -> float:
|
||||
diameter_m = diameter_mm * 1.0e-3
|
||||
return pi * diameter_m * diameter_m / 4.0
|
||||
|
||||
|
||||
class AmesimPneumaticVolume(DynamicComponent):
|
||||
"""First-pass AMESim pneumatic control volume using helium PR pressure closure."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
volume: float,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
p0: float = 101_325.0,
|
||||
T0: float = 293.15,
|
||||
heat_transfer_coefficient: float = 0.0,
|
||||
heat_transfer_area: float = 0.0,
|
||||
external_temperature_k: float = 293.15,
|
||||
) -> None:
|
||||
if volume <= 0.0:
|
||||
raise ValueError("volume must be positive.")
|
||||
if heat_transfer_coefficient < 0.0:
|
||||
raise ValueError("heat_transfer_coefficient must be non-negative.")
|
||||
if heat_transfer_area < 0.0:
|
||||
raise ValueError("heat_transfer_area must be non-negative.")
|
||||
if external_temperature_k <= 0.0:
|
||||
raise ValueError("external_temperature_k must be positive.")
|
||||
super().__init__(name=name)
|
||||
self.volume = volume
|
||||
self.gas = gas
|
||||
self.heat_transfer_coefficient = heat_transfer_coefficient
|
||||
self.heat_transfer_area = heat_transfer_area
|
||||
self.external_temperature = external_temperature_k
|
||||
rho0 = gas.density(p0, T0)
|
||||
m0 = rho0 * volume
|
||||
U0 = m0 * gas.specific_internal_energy(T0)
|
||||
self.state = VolumeState(m=m0, U=U0)
|
||||
self.port_a = PortState()
|
||||
self.port_b = PortState()
|
||||
|
||||
@classmethod
|
||||
def from_liters(
|
||||
cls,
|
||||
name: str,
|
||||
volume_liters: float,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
p0: float = 101_325.0,
|
||||
T0: float = 293.15,
|
||||
heat_transfer_coefficient: float = 0.0,
|
||||
heat_transfer_area: float = 0.0,
|
||||
external_temperature_k: float = 293.15,
|
||||
) -> "AmesimPneumaticVolume":
|
||||
return cls(
|
||||
name=name,
|
||||
volume=liters_to_m3(volume_liters),
|
||||
gas=gas,
|
||||
p0=p0,
|
||||
T0=T0,
|
||||
heat_transfer_coefficient=heat_transfer_coefficient,
|
||||
heat_transfer_area=heat_transfer_area,
|
||||
external_temperature_k=external_temperature_k,
|
||||
)
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def volume_cm3(self) -> float:
|
||||
return m3_to_cm3(self.volume)
|
||||
|
||||
def volume_rate_m3_s(self) -> float:
|
||||
return 0.0
|
||||
|
||||
def thermal_energy_flow_w(self, temperature_k: float | None = None) -> float:
|
||||
temperature = self.properties().T if temperature_k is None else temperature_k
|
||||
return (
|
||||
self.heat_transfer_coefficient
|
||||
* self.heat_transfer_area
|
||||
* (self.external_temperature - temperature)
|
||||
)
|
||||
|
||||
def gas_mass_g(self) -> float:
|
||||
return kg_to_g(self.state.m)
|
||||
|
||||
def pressure_gauge_pa(self, reference_pressure_pa: float = 101_300.0) -> float:
|
||||
return self.properties().p - reference_pressure_pa
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
if self.state.m <= 0.0:
|
||||
raise ValueError("volume mass must stay positive.")
|
||||
T = self.gas.temperature_from_internal_energy(self.state.U / self.state.m)
|
||||
rho = self.state.m / self.volume
|
||||
p = self.gas.pressure(rho, T)
|
||||
u = self.state.U / self.state.m
|
||||
h = self.gas.specific_enthalpy(T)
|
||||
self.port_a.p = p
|
||||
self.port_a.h_outflow = h
|
||||
self.port_b.p = p
|
||||
self.port_b.h_outflow = h
|
||||
return ThermodynamicProperties(p=p, T=T, rho=rho, u=u, h=h)
|
||||
|
||||
def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState:
|
||||
return VolumeState(
|
||||
m=m_flow,
|
||||
U=m_flow * inlet_h + self.thermal_energy_flow_w(),
|
||||
)
|
||||
|
||||
def derivatives_from_two_connections(
|
||||
self,
|
||||
*,
|
||||
port_a_m_flow: float,
|
||||
connected_h_a: float,
|
||||
port_b_m_flow: float,
|
||||
connected_h_b: float,
|
||||
internal_h: float,
|
||||
volume_rate_m3_s: float | None = None,
|
||||
) -> VolumeState:
|
||||
properties = self.properties()
|
||||
inlet_h_a = self.connection_inlet_enthalpy(
|
||||
port_m_flow=port_a_m_flow,
|
||||
connected_h=connected_h_a,
|
||||
internal_h=internal_h,
|
||||
)
|
||||
inlet_h_b = self.connection_inlet_enthalpy(
|
||||
port_m_flow=port_b_m_flow,
|
||||
connected_h=connected_h_b,
|
||||
internal_h=internal_h,
|
||||
)
|
||||
return VolumeState(
|
||||
m=port_a_m_flow + port_b_m_flow,
|
||||
U=(
|
||||
port_a_m_flow * inlet_h_a
|
||||
+ port_b_m_flow * inlet_h_b
|
||||
+ self.thermal_energy_flow_w(properties.T)
|
||||
- properties.p * (
|
||||
self.volume_rate_m3_s()
|
||||
if volume_rate_m3_s is None
|
||||
else volume_rate_m3_s
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AmesimVariablePneumaticVolume(AmesimPneumaticVolume):
|
||||
"""PNCH012-style volume with a dead volume plus an external moving volume."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
dead_volume: float,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
p0: float = 101_325.0,
|
||||
T0: float = 293.15,
|
||||
external_volume: float = 0.0,
|
||||
heat_transfer_coefficient: float = 0.0,
|
||||
heat_transfer_area: float = 0.0,
|
||||
external_temperature_k: float = 293.15,
|
||||
) -> None:
|
||||
if dead_volume <= 0.0:
|
||||
raise ValueError("dead_volume must be positive.")
|
||||
if dead_volume + external_volume <= 0.0:
|
||||
raise ValueError("total volume must be positive.")
|
||||
self.dead_volume = dead_volume
|
||||
self.external_volume = external_volume
|
||||
self.external_volume_rate = 0.0
|
||||
super().__init__(
|
||||
name=name,
|
||||
volume=dead_volume + external_volume,
|
||||
gas=gas,
|
||||
p0=p0,
|
||||
T0=T0,
|
||||
heat_transfer_coefficient=heat_transfer_coefficient,
|
||||
heat_transfer_area=heat_transfer_area,
|
||||
external_temperature_k=external_temperature_k,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_liters(
|
||||
cls,
|
||||
name: str,
|
||||
dead_volume_liters: float,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
p0: float = 101_325.0,
|
||||
T0: float = 293.15,
|
||||
external_volume_liters: float = 0.0,
|
||||
heat_transfer_coefficient: float = 0.0,
|
||||
heat_transfer_area: float = 0.0,
|
||||
external_temperature_k: float = 293.15,
|
||||
) -> "AmesimVariablePneumaticVolume":
|
||||
return cls(
|
||||
name=name,
|
||||
dead_volume=liters_to_m3(dead_volume_liters),
|
||||
gas=gas,
|
||||
p0=p0,
|
||||
T0=T0,
|
||||
external_volume=liters_to_m3(external_volume_liters),
|
||||
heat_transfer_coefficient=heat_transfer_coefficient,
|
||||
heat_transfer_area=heat_transfer_area,
|
||||
external_temperature_k=external_temperature_k,
|
||||
)
|
||||
|
||||
def volume_rate_m3_s(self) -> float:
|
||||
return self.external_volume_rate
|
||||
|
||||
def set_external_volume_m3(
|
||||
self,
|
||||
external_volume: float,
|
||||
external_volume_rate_m3_s: float = 0.0,
|
||||
) -> None:
|
||||
if self.dead_volume + external_volume <= 0.0:
|
||||
raise ValueError("total volume must be positive.")
|
||||
self.external_volume = external_volume
|
||||
self.external_volume_rate = external_volume_rate_m3_s
|
||||
self.volume = self.dead_volume + self.external_volume
|
||||
|
||||
|
||||
class AmesimPneumaticOrifice(AlgebraicComponent):
|
||||
"""First-pass PNOR001/PNVO001-style compressible helium orifice.
|
||||
|
||||
This is a calibrated placeholder boundary for the Python port. It preserves
|
||||
AMESim-style area and coefficient inputs, but final parity must be checked
|
||||
against AMESim CSV results before treating it as numerically equivalent.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
area: float,
|
||||
flow_coefficient: float = 1.0,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
opening: float = 1.0,
|
||||
) -> None:
|
||||
if area < 0.0:
|
||||
raise ValueError("area must be non-negative.")
|
||||
if flow_coefficient < 0.0:
|
||||
raise ValueError("flow_coefficient must be non-negative.")
|
||||
super().__init__(name=name)
|
||||
self.area = area
|
||||
self.flow_coefficient = flow_coefficient
|
||||
self.gas = gas
|
||||
self.opening = opening
|
||||
self.port_a = PortState()
|
||||
self.port_b = PortState()
|
||||
|
||||
@classmethod
|
||||
def from_mm2(
|
||||
cls,
|
||||
name: str,
|
||||
area_mm2: float,
|
||||
flow_coefficient: float = 1.0,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
opening: float = 1.0,
|
||||
) -> "AmesimPneumaticOrifice":
|
||||
return cls(
|
||||
name=name,
|
||||
area=mm2_to_m2(area_mm2),
|
||||
flow_coefficient=flow_coefficient,
|
||||
gas=gas,
|
||||
opening=opening,
|
||||
)
|
||||
|
||||
@property
|
||||
def effective_area(self) -> float:
|
||||
opening = min(max(self.opening, 0.0), 1.0)
|
||||
return self.area * opening
|
||||
|
||||
def mass_flow(self, p_a: float, p_b: float, upstream_temperature: float) -> float:
|
||||
if p_a == p_b or self.effective_area == 0.0 or self.flow_coefficient == 0.0:
|
||||
return 0.0
|
||||
if p_a > p_b:
|
||||
return compressible_orifice_mass_flow(
|
||||
upstream_pressure=p_a,
|
||||
downstream_pressure=p_b,
|
||||
upstream_temperature=upstream_temperature,
|
||||
area=self.effective_area,
|
||||
flow_coefficient=self.flow_coefficient,
|
||||
gas=self.gas,
|
||||
)
|
||||
return -compressible_orifice_mass_flow(
|
||||
upstream_pressure=p_b,
|
||||
downstream_pressure=p_a,
|
||||
upstream_temperature=upstream_temperature,
|
||||
area=self.effective_area,
|
||||
flow_coefficient=self.flow_coefficient,
|
||||
gas=self.gas,
|
||||
)
|
||||
|
||||
|
||||
def compressible_orifice_mass_flow(
|
||||
*,
|
||||
upstream_pressure: float,
|
||||
downstream_pressure: float,
|
||||
upstream_temperature: float,
|
||||
area: float,
|
||||
flow_coefficient: float,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
) -> float:
|
||||
if upstream_pressure <= 0.0 or downstream_pressure < 0.0:
|
||||
raise ValueError("pressures must be non-negative and upstream pressure must be positive.")
|
||||
if upstream_temperature <= 0.0:
|
||||
raise ValueError("upstream_temperature must be positive.")
|
||||
if area < 0.0 or flow_coefficient < 0.0:
|
||||
raise ValueError("area and flow_coefficient must be non-negative.")
|
||||
if downstream_pressure >= upstream_pressure or area == 0.0 or flow_coefficient == 0.0:
|
||||
return 0.0
|
||||
|
||||
gamma = gas.gamma
|
||||
pressure_ratio = max(downstream_pressure / upstream_pressure, 0.0)
|
||||
critical_ratio = (2.0 / (gamma + 1.0)) ** (gamma / (gamma - 1.0))
|
||||
coefficient = flow_coefficient * area * upstream_pressure / sqrt(gas.R_gas * upstream_temperature)
|
||||
if pressure_ratio <= critical_ratio:
|
||||
flow_function = sqrt(gamma) * (2.0 / (gamma + 1.0)) ** ((gamma + 1.0) / (2.0 * (gamma - 1.0)))
|
||||
else:
|
||||
term = pressure_ratio ** (2.0 / gamma) - pressure_ratio ** ((gamma + 1.0) / gamma)
|
||||
flow_function = sqrt((2.0 * gamma / (gamma - 1.0)) * max(term, 0.0))
|
||||
return coefficient * flow_function
|
||||
@@ -0,0 +1,881 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import log10, pi, sqrt
|
||||
|
||||
from app.simulation.examples.test_mql.primitives.pneumatic import (
|
||||
HELIUM_PNEUMATIC_GAS,
|
||||
AmesimPneumaticGas,
|
||||
compressible_orifice_mass_flow,
|
||||
diameter_mm_to_area_m2,
|
||||
)
|
||||
from app.simulation.core.base import AlgebraicComponent, DynamicComponent
|
||||
from app.simulation.core.medium import ThermodynamicProperties
|
||||
from app.simulation.core.ports import PortState
|
||||
from app.simulation.core.state import VolumeState
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimPnl0001Diagnostics:
|
||||
mass_flow_kg_s: float
|
||||
reynolds_number: float
|
||||
gas_velocity_m_s: float
|
||||
friction_factor: float
|
||||
pressure_drop_pa: float
|
||||
|
||||
|
||||
class _DarcyPipeResistanceMixin:
|
||||
diameter: float
|
||||
length: float
|
||||
relative_roughness: float
|
||||
area: float
|
||||
|
||||
def _mass_flow_for_pressure_drop(
|
||||
self,
|
||||
pressure_drop_pa: float,
|
||||
*,
|
||||
density: float,
|
||||
temperature: float,
|
||||
) -> float:
|
||||
if pressure_drop_pa <= 0.0:
|
||||
return 0.0
|
||||
upper = 1.0e-9
|
||||
while self._darcy_pressure_drop(
|
||||
upper,
|
||||
density=density,
|
||||
temperature=temperature,
|
||||
) < pressure_drop_pa:
|
||||
upper *= 10.0
|
||||
if upper > 1.0e3:
|
||||
raise ValueError("unable to bracket pneumatic pipe resistance flow")
|
||||
lower = 0.0
|
||||
for _ in range(48):
|
||||
middle = 0.5 * (lower + upper)
|
||||
if self._darcy_pressure_drop(
|
||||
middle,
|
||||
density=density,
|
||||
temperature=temperature,
|
||||
) < pressure_drop_pa:
|
||||
lower = middle
|
||||
else:
|
||||
upper = middle
|
||||
return 0.5 * (lower + upper)
|
||||
|
||||
def pn2pipefr_mass_flow(
|
||||
self,
|
||||
*,
|
||||
port_1_pressure_pa: float,
|
||||
port_1_temperature_k: float,
|
||||
port_2_pressure_pa: float,
|
||||
port_2_temperature_k: float,
|
||||
length: float | None = None,
|
||||
) -> float:
|
||||
pressure_difference = port_1_pressure_pa - port_2_pressure_pa
|
||||
if pressure_difference == 0.0:
|
||||
return 0.0
|
||||
upstream_pressure = max(port_1_pressure_pa, port_2_pressure_pa)
|
||||
downstream_pressure = min(port_1_pressure_pa, port_2_pressure_pa)
|
||||
upstream_temperature = (
|
||||
port_1_temperature_k
|
||||
if pressure_difference > 0.0
|
||||
else port_2_temperature_k
|
||||
)
|
||||
resistance_length = self.length if length is None else length
|
||||
if resistance_length <= 0.0:
|
||||
raise ValueError("length must be positive")
|
||||
|
||||
def target_flow(mass_flow_kg_s: float) -> float:
|
||||
reynolds = self._reynolds_number(mass_flow_kg_s, upstream_temperature)
|
||||
friction_factor = self._friction_factor(reynolds)
|
||||
flow_coefficient = sqrt(
|
||||
self.diameter / (resistance_length * friction_factor)
|
||||
)
|
||||
return compressible_orifice_mass_flow(
|
||||
upstream_pressure=upstream_pressure,
|
||||
downstream_pressure=downstream_pressure,
|
||||
upstream_temperature=upstream_temperature,
|
||||
area=self.area,
|
||||
flow_coefficient=flow_coefficient,
|
||||
gas=self.gas,
|
||||
)
|
||||
|
||||
flow_coefficient = sqrt(self.diameter / (resistance_length * 0.02))
|
||||
magnitude = compressible_orifice_mass_flow(
|
||||
upstream_pressure=upstream_pressure,
|
||||
downstream_pressure=downstream_pressure,
|
||||
upstream_temperature=upstream_temperature,
|
||||
area=self.area,
|
||||
flow_coefficient=flow_coefficient,
|
||||
gas=self.gas,
|
||||
)
|
||||
for _ in range(12):
|
||||
next_magnitude = target_flow(magnitude)
|
||||
if abs(next_magnitude - magnitude) <= max(1.0e-12, abs(magnitude) * 1.0e-9):
|
||||
magnitude = next_magnitude
|
||||
break
|
||||
magnitude = 0.5 * (magnitude + next_magnitude)
|
||||
return magnitude if pressure_difference > 0.0 else -magnitude
|
||||
|
||||
def _darcy_pressure_drop(
|
||||
self,
|
||||
mass_flow_kg_s: float,
|
||||
*,
|
||||
density: float,
|
||||
temperature: float,
|
||||
) -> float:
|
||||
if mass_flow_kg_s == 0.0:
|
||||
return 0.0
|
||||
reynolds = self._reynolds_number(mass_flow_kg_s, temperature)
|
||||
friction_factor = self._friction_factor(reynolds)
|
||||
velocity = mass_flow_kg_s / (density * self.area)
|
||||
magnitude = (
|
||||
friction_factor
|
||||
* (self.length / self.diameter)
|
||||
* density
|
||||
* velocity
|
||||
* velocity
|
||||
/ 2.0
|
||||
)
|
||||
return magnitude if mass_flow_kg_s > 0.0 else -magnitude
|
||||
|
||||
def _reynolds_number(self, mass_flow_kg_s: float, temperature: float) -> float:
|
||||
viscosity = helium_dynamic_viscosity(temperature)
|
||||
return 4.0 * abs(mass_flow_kg_s) / (pi * self.diameter * viscosity)
|
||||
|
||||
def _friction_factor(self, reynolds_number: float) -> float:
|
||||
if reynolds_number <= 0.0:
|
||||
return 64_000_000.0
|
||||
laminar = 64.0 / reynolds_number
|
||||
if reynolds_number <= 2_300.0:
|
||||
return laminar
|
||||
turbulent = 1.0 / (
|
||||
-1.8
|
||||
* log10(
|
||||
(self.relative_roughness / 3.7) ** 1.11
|
||||
+ 6.9 / reynolds_number
|
||||
)
|
||||
) ** 2
|
||||
if reynolds_number >= 4_000.0:
|
||||
return turbulent
|
||||
fraction = (reynolds_number - 2_300.0) / 1_700.0
|
||||
return laminar + fraction * (turbulent - laminar)
|
||||
|
||||
|
||||
class AmesimPnl0001Pipe(_DarcyPipeResistanceMixin, DynamicComponent):
|
||||
"""Physical first-pass implementation of AMESim ``PNL0001`` (C-R).
|
||||
|
||||
Port 2 owns the lumped gas storage. Port 1 is connected through a Darcy
|
||||
resistance. Both connection mass flows use the simulation convention:
|
||||
positive values enter the pipe storage.
|
||||
|
||||
AMESim's proprietary ``pn2pipefr`` utility is represented by an
|
||||
optional calibrated linear conductance when a model-specific baseline
|
||||
supports it; otherwise the component falls back to an auditable
|
||||
Darcy-Weisbach law. Both paths preserve the real geometry, state count,
|
||||
mass/energy balance, heat-transfer parameter, and observable diagnostics.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
diameter_mm: float,
|
||||
length_m: float,
|
||||
relative_roughness: float,
|
||||
polytropic_constant: float = 1.35,
|
||||
heat_transfer_coefficient: float = 0.0,
|
||||
external_temperature_k: float = 293.15,
|
||||
calibrated_linear_conductance: float | None = None,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
p0: float = 101_325.0,
|
||||
T0: float = 293.15,
|
||||
) -> None:
|
||||
if diameter_mm <= 0.0:
|
||||
raise ValueError("diameter_mm must be positive")
|
||||
if length_m <= 0.0:
|
||||
raise ValueError("length_m must be positive")
|
||||
if relative_roughness < 0.0:
|
||||
raise ValueError("relative_roughness must be non-negative")
|
||||
if polytropic_constant <= 0.0:
|
||||
raise ValueError("polytropic_constant must be positive")
|
||||
if heat_transfer_coefficient < 0.0:
|
||||
raise ValueError("heat_transfer_coefficient must be non-negative")
|
||||
if external_temperature_k <= 0.0:
|
||||
raise ValueError("external_temperature_k must be positive")
|
||||
if (
|
||||
calibrated_linear_conductance is not None
|
||||
and calibrated_linear_conductance <= 0.0
|
||||
):
|
||||
raise ValueError("calibrated_linear_conductance must be positive")
|
||||
|
||||
super().__init__(name=name)
|
||||
self.diameter = diameter_mm * 1.0e-3
|
||||
self.length = length_m
|
||||
self.relative_roughness = relative_roughness
|
||||
self.polytropic_constant = polytropic_constant
|
||||
self.heat_transfer_coefficient = heat_transfer_coefficient
|
||||
self.external_temperature = external_temperature_k
|
||||
self.calibrated_linear_conductance = calibrated_linear_conductance
|
||||
self.gas = gas
|
||||
self.area = diameter_mm_to_area_m2(diameter_mm)
|
||||
self.volume = self.area * self.length
|
||||
self.heat_transfer_area = pi * self.diameter * self.length
|
||||
|
||||
rho0 = gas.density(p0, T0)
|
||||
mass0 = rho0 * self.volume
|
||||
self.state = VolumeState(
|
||||
m=mass0,
|
||||
U=mass0 * gas.specific_internal_energy(T0),
|
||||
)
|
||||
self.port_1 = PortState()
|
||||
self.port_2 = PortState()
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
if self.state.m <= 0.0:
|
||||
raise ValueError("pipe mass must stay positive")
|
||||
temperature = self.gas.temperature_from_internal_energy(
|
||||
self.state.U / self.state.m
|
||||
)
|
||||
density = self.state.m / self.volume
|
||||
pressure = self.gas.pressure(density, temperature)
|
||||
properties = ThermodynamicProperties(
|
||||
p=pressure,
|
||||
T=temperature,
|
||||
rho=density,
|
||||
u=self.state.U / self.state.m,
|
||||
h=self.gas.specific_enthalpy(temperature),
|
||||
)
|
||||
self.port_2.p = pressure
|
||||
self.port_2.h_outflow = properties.h
|
||||
return properties
|
||||
|
||||
def gas_mass_g(self) -> float:
|
||||
return self.state.m * 1.0e3
|
||||
|
||||
def resistance_mass_flow(
|
||||
self,
|
||||
*,
|
||||
port_1_pressure_pa: float,
|
||||
port_1_temperature_k: float,
|
||||
) -> float:
|
||||
"""Return mass flow from port 1 into the port-2 storage in kg/s."""
|
||||
if port_1_pressure_pa <= 0.0:
|
||||
raise ValueError("port_1_pressure_pa must be positive")
|
||||
if port_1_temperature_k <= 0.0:
|
||||
raise ValueError("port_1_temperature_k must be positive")
|
||||
|
||||
internal = self.properties()
|
||||
pressure_difference = port_1_pressure_pa - internal.p
|
||||
if pressure_difference == 0.0:
|
||||
return 0.0
|
||||
if self.calibrated_linear_conductance is not None:
|
||||
return (
|
||||
self.calibrated_linear_conductance
|
||||
* pressure_difference
|
||||
/ sqrt(internal.T)
|
||||
)
|
||||
upstream_pressure = max(port_1_pressure_pa, internal.p)
|
||||
upstream_temperature = (
|
||||
port_1_temperature_k if pressure_difference > 0.0 else internal.T
|
||||
)
|
||||
density = self.gas.density(upstream_pressure, upstream_temperature)
|
||||
magnitude = self._mass_flow_for_pressure_drop(
|
||||
abs(pressure_difference),
|
||||
density=density,
|
||||
temperature=upstream_temperature,
|
||||
)
|
||||
return magnitude if pressure_difference > 0.0 else -magnitude
|
||||
|
||||
def diagnostics(
|
||||
self,
|
||||
*,
|
||||
mass_flow_kg_s: float,
|
||||
temperature_k: float | None = None,
|
||||
) -> AmesimPnl0001Diagnostics:
|
||||
properties = self.properties()
|
||||
temperature = temperature_k or properties.T
|
||||
reynolds = self._reynolds_number(mass_flow_kg_s, temperature)
|
||||
friction_factor = self._friction_factor(reynolds)
|
||||
velocity = mass_flow_kg_s / (properties.rho * self.area)
|
||||
pressure_drop = self._darcy_pressure_drop(
|
||||
mass_flow_kg_s,
|
||||
density=properties.rho,
|
||||
temperature=temperature,
|
||||
)
|
||||
return AmesimPnl0001Diagnostics(
|
||||
mass_flow_kg_s=mass_flow_kg_s,
|
||||
reynolds_number=reynolds,
|
||||
gas_velocity_m_s=velocity,
|
||||
friction_factor=friction_factor,
|
||||
pressure_drop_pa=pressure_drop,
|
||||
)
|
||||
|
||||
def darcy_pressure_drop_for_state(
|
||||
self,
|
||||
*,
|
||||
mass_flow_kg_s: float,
|
||||
pressure_pa: float,
|
||||
temperature_k: float,
|
||||
) -> float:
|
||||
if pressure_pa <= 0.0:
|
||||
raise ValueError("pressure_pa must be positive")
|
||||
if temperature_k <= 0.0:
|
||||
raise ValueError("temperature_k must be positive")
|
||||
density = self.gas.density(pressure_pa, temperature_k)
|
||||
return self._darcy_pressure_drop(
|
||||
mass_flow_kg_s,
|
||||
density=density,
|
||||
temperature=temperature_k,
|
||||
)
|
||||
|
||||
def derivatives_from_connections(
|
||||
self,
|
||||
*,
|
||||
port_1_m_flow: float,
|
||||
connected_h_1: float,
|
||||
port_2_m_flow: float,
|
||||
connected_h_2: float,
|
||||
) -> VolumeState:
|
||||
internal = self.properties()
|
||||
# Default first-pass PNL0001 behavior uses the historical internal-energy
|
||||
# approximation. AMESim-specific transport-enthalpy corrections are kept
|
||||
# behind derivatives_from_transport_enthalpy_connections so they can be
|
||||
# applied only where validated against baseline data.
|
||||
inlet_u_1 = (
|
||||
connected_h_1 / self.gas.gamma
|
||||
if port_1_m_flow > 0.0
|
||||
else internal.u
|
||||
)
|
||||
inlet_u_2 = (
|
||||
connected_h_2 / self.gas.gamma
|
||||
if port_2_m_flow > 0.0
|
||||
else internal.u
|
||||
)
|
||||
heat_flow = (
|
||||
self.heat_transfer_coefficient
|
||||
* self.heat_transfer_area
|
||||
* (self.external_temperature - internal.T)
|
||||
)
|
||||
return VolumeState(
|
||||
m=port_1_m_flow + port_2_m_flow,
|
||||
U=port_1_m_flow * inlet_u_1 + port_2_m_flow * inlet_u_2 + heat_flow,
|
||||
)
|
||||
|
||||
def derivatives_from_transport_enthalpy_connections(
|
||||
self,
|
||||
*,
|
||||
port_1_m_flow: float,
|
||||
connected_h_1: float,
|
||||
port_2_m_flow: float,
|
||||
connected_h_2: float,
|
||||
) -> VolumeState:
|
||||
internal = self.properties()
|
||||
inlet_h_1 = connected_h_1 if port_1_m_flow > 0.0 else internal.h
|
||||
inlet_h_2 = connected_h_2 if port_2_m_flow > 0.0 else internal.h
|
||||
heat_flow = (
|
||||
self.heat_transfer_coefficient
|
||||
* self.heat_transfer_area
|
||||
* (self.external_temperature - internal.T)
|
||||
)
|
||||
return VolumeState(
|
||||
m=port_1_m_flow + port_2_m_flow,
|
||||
U=port_1_m_flow * inlet_h_1 + port_2_m_flow * inlet_h_2 + heat_flow,
|
||||
)
|
||||
|
||||
|
||||
class AmesimPnl0003Pipe(_DarcyPipeResistanceMixin, DynamicComponent):
|
||||
"""First-pass AMESim ``PNL0003`` (C-R-C) pipe.
|
||||
|
||||
The two pipe-end compliances are represented as equal half-volume gas
|
||||
stores connected by the same auditable Darcy resistance used for PNL0001.
|
||||
Center flow is positive from port 1 storage to port 2 storage.
|
||||
"""
|
||||
|
||||
state_size = 4
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
diameter_mm: float,
|
||||
length_m: float,
|
||||
relative_roughness: float,
|
||||
polytropic_constant: float = 1.35,
|
||||
heat_transfer_coefficient: float = 0.0,
|
||||
external_temperature_k: float = 293.15,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
p1_0: float = 101_325.0,
|
||||
T1_0: float = 293.15,
|
||||
p2_0: float = 101_325.0,
|
||||
T2_0: float = 293.15,
|
||||
) -> None:
|
||||
if diameter_mm <= 0.0:
|
||||
raise ValueError("diameter_mm must be positive")
|
||||
if length_m <= 0.0:
|
||||
raise ValueError("length_m must be positive")
|
||||
if relative_roughness < 0.0:
|
||||
raise ValueError("relative_roughness must be non-negative")
|
||||
if polytropic_constant <= 0.0:
|
||||
raise ValueError("polytropic_constant must be positive")
|
||||
if heat_transfer_coefficient < 0.0:
|
||||
raise ValueError("heat_transfer_coefficient must be non-negative")
|
||||
if external_temperature_k <= 0.0:
|
||||
raise ValueError("external_temperature_k must be positive")
|
||||
|
||||
super().__init__(name=name)
|
||||
self.diameter = diameter_mm * 1.0e-3
|
||||
self.length = length_m
|
||||
self.relative_roughness = relative_roughness
|
||||
self.polytropic_constant = polytropic_constant
|
||||
self.heat_transfer_coefficient = heat_transfer_coefficient
|
||||
self.external_temperature = external_temperature_k
|
||||
self.gas = gas
|
||||
self.area = diameter_mm_to_area_m2(diameter_mm)
|
||||
self.volume = self.area * self.length
|
||||
self.compliance_volume = self.volume / 2.0
|
||||
self.heat_transfer_area = pi * self.diameter * self.length
|
||||
|
||||
self.state_1 = self._initial_state(p1_0, T1_0)
|
||||
self.state_2 = self._initial_state(p2_0, T2_0)
|
||||
self.port_1 = PortState()
|
||||
self.port_2 = PortState()
|
||||
|
||||
def _initial_state(self, pressure: float, temperature: float) -> VolumeState:
|
||||
rho = self.gas.density(pressure, temperature)
|
||||
mass = rho * self.compliance_volume
|
||||
return VolumeState(
|
||||
m=mass,
|
||||
U=mass * self.gas.specific_internal_energy(temperature),
|
||||
)
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return [*self.state_1.as_vector(), *self.state_2.as_vector()]
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
if len(values) != 4:
|
||||
raise ValueError("PNL0003 state vector requires four values")
|
||||
self.state_1 = VolumeState.from_vector(values[:2])
|
||||
self.state_2 = VolumeState.from_vector(values[2:])
|
||||
|
||||
def properties_1(self) -> ThermodynamicProperties:
|
||||
properties = self._properties(self.state_1)
|
||||
self.port_1.p = properties.p
|
||||
self.port_1.h_outflow = properties.h
|
||||
return properties
|
||||
|
||||
def properties_2(self) -> ThermodynamicProperties:
|
||||
properties = self._properties(self.state_2)
|
||||
self.port_2.p = properties.p
|
||||
self.port_2.h_outflow = properties.h
|
||||
return properties
|
||||
|
||||
def _properties(self, state: VolumeState) -> ThermodynamicProperties:
|
||||
if state.m <= 0.0:
|
||||
raise ValueError("pipe mass must stay positive")
|
||||
temperature = self.gas.temperature_from_internal_energy(state.U / state.m)
|
||||
density = state.m / self.compliance_volume
|
||||
pressure = self.gas.pressure(density, temperature)
|
||||
return ThermodynamicProperties(
|
||||
p=pressure,
|
||||
T=temperature,
|
||||
rho=density,
|
||||
u=state.U / state.m,
|
||||
h=self.gas.specific_enthalpy(temperature),
|
||||
)
|
||||
|
||||
def gas_mass_g(self) -> float:
|
||||
return (self.state_1.m + self.state_2.m) * 1.0e3
|
||||
|
||||
def resistance_mass_flow(self) -> float:
|
||||
"""Return center mass flow from port 1 storage to port 2 storage."""
|
||||
port_1 = self.properties_1()
|
||||
port_2 = self.properties_2()
|
||||
pressure_difference = port_1.p - port_2.p
|
||||
if pressure_difference == 0.0:
|
||||
return 0.0
|
||||
upstream = port_1 if pressure_difference > 0.0 else port_2
|
||||
magnitude = self._mass_flow_for_pressure_drop(
|
||||
abs(pressure_difference),
|
||||
density=upstream.rho,
|
||||
temperature=upstream.T,
|
||||
)
|
||||
return magnitude if pressure_difference > 0.0 else -magnitude
|
||||
|
||||
def diagnostics(
|
||||
self,
|
||||
*,
|
||||
mass_flow_kg_s: float,
|
||||
temperature_k: float | None = None,
|
||||
) -> AmesimPnl0001Diagnostics:
|
||||
port_1 = self.properties_1()
|
||||
port_2 = self.properties_2()
|
||||
temperature = temperature_k or (port_1.T if mass_flow_kg_s >= 0.0 else port_2.T)
|
||||
density = port_1.rho if mass_flow_kg_s >= 0.0 else port_2.rho
|
||||
reynolds = self._reynolds_number(mass_flow_kg_s, temperature)
|
||||
friction_factor = self._friction_factor(reynolds)
|
||||
velocity = mass_flow_kg_s / (density * self.area)
|
||||
pressure_drop = self._darcy_pressure_drop(
|
||||
mass_flow_kg_s,
|
||||
density=density,
|
||||
temperature=temperature,
|
||||
)
|
||||
return AmesimPnl0001Diagnostics(
|
||||
mass_flow_kg_s=mass_flow_kg_s,
|
||||
reynolds_number=reynolds,
|
||||
gas_velocity_m_s=velocity,
|
||||
friction_factor=friction_factor,
|
||||
pressure_drop_pa=pressure_drop,
|
||||
)
|
||||
|
||||
def derivatives_from_connections(
|
||||
self,
|
||||
*,
|
||||
port_1_m_flow: float,
|
||||
connected_h_1: float,
|
||||
port_2_m_flow: float,
|
||||
connected_h_2: float,
|
||||
) -> tuple[VolumeState, VolumeState]:
|
||||
port_1 = self.properties_1()
|
||||
port_2 = self.properties_2()
|
||||
center_flow = self.resistance_mass_flow()
|
||||
heat_flow_each = (
|
||||
self.heat_transfer_coefficient
|
||||
* self.heat_transfer_area
|
||||
* (self.external_temperature - 0.5 * (port_1.T + port_2.T))
|
||||
/ 2.0
|
||||
)
|
||||
port_1_external_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=port_1_m_flow,
|
||||
connected_h=connected_h_1,
|
||||
internal_h=port_1.h,
|
||||
)
|
||||
port_2_external_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=port_2_m_flow,
|
||||
connected_h=connected_h_2,
|
||||
internal_h=port_2.h,
|
||||
)
|
||||
port_1_center_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=-center_flow,
|
||||
connected_h=port_2.h,
|
||||
internal_h=port_1.h,
|
||||
)
|
||||
port_2_center_h = self.connection_inlet_enthalpy(
|
||||
port_m_flow=center_flow,
|
||||
connected_h=port_1.h,
|
||||
internal_h=port_2.h,
|
||||
)
|
||||
return (
|
||||
VolumeState(
|
||||
m=port_1_m_flow - center_flow,
|
||||
U=(
|
||||
port_1_m_flow * port_1_external_h
|
||||
- center_flow * port_1_center_h
|
||||
+ heat_flow_each
|
||||
),
|
||||
),
|
||||
VolumeState(
|
||||
m=port_2_m_flow + center_flow,
|
||||
U=(
|
||||
port_2_m_flow * port_2_external_h
|
||||
+ center_flow * port_2_center_h
|
||||
+ heat_flow_each
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class AmesimPnl0002Pipe(_DarcyPipeResistanceMixin, DynamicComponent):
|
||||
"""First-pass AMESim ``PNL0002`` (R-C-R) pipe.
|
||||
|
||||
The center compliance owns the gas state. Positive connection mass flows
|
||||
enter that center storage from each external port.
|
||||
"""
|
||||
|
||||
state_size = 2
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
diameter_mm: float,
|
||||
length_m: float,
|
||||
relative_roughness: float,
|
||||
polytropic_constant: float = 1.35,
|
||||
heat_transfer_coefficient: float = 0.0,
|
||||
external_temperature_k: float = 293.15,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
pctr_0: float = 101_325.0,
|
||||
Tctr_0: float = 293.15,
|
||||
) -> None:
|
||||
if diameter_mm <= 0.0:
|
||||
raise ValueError("diameter_mm must be positive")
|
||||
if length_m <= 0.0:
|
||||
raise ValueError("length_m must be positive")
|
||||
if relative_roughness < 0.0:
|
||||
raise ValueError("relative_roughness must be non-negative")
|
||||
if polytropic_constant <= 0.0:
|
||||
raise ValueError("polytropic_constant must be positive")
|
||||
if heat_transfer_coefficient < 0.0:
|
||||
raise ValueError("heat_transfer_coefficient must be non-negative")
|
||||
if external_temperature_k <= 0.0:
|
||||
raise ValueError("external_temperature_k must be positive")
|
||||
|
||||
super().__init__(name=name)
|
||||
self.diameter = diameter_mm * 1.0e-3
|
||||
self.length = length_m
|
||||
self.relative_roughness = relative_roughness
|
||||
self.polytropic_constant = polytropic_constant
|
||||
self.heat_transfer_coefficient = heat_transfer_coefficient
|
||||
self.external_temperature = external_temperature_k
|
||||
self.gas = gas
|
||||
self.area = diameter_mm_to_area_m2(diameter_mm)
|
||||
self.volume = self.area * self.length
|
||||
self.heat_transfer_area = pi * self.diameter * self.length
|
||||
self._resistance_length = self.length / 2.0
|
||||
|
||||
rho0 = gas.density(pctr_0, Tctr_0)
|
||||
mass0 = rho0 * self.volume
|
||||
self.state = VolumeState(
|
||||
m=mass0,
|
||||
U=mass0 * gas.specific_internal_energy(Tctr_0),
|
||||
)
|
||||
self.port_1 = PortState()
|
||||
self.port_2 = PortState()
|
||||
|
||||
def get_state_vector(self) -> list[float]:
|
||||
return self.state.as_vector()
|
||||
|
||||
def set_state_vector(self, values: list[float]) -> None:
|
||||
self.state = VolumeState.from_vector(values)
|
||||
|
||||
def properties(self) -> ThermodynamicProperties:
|
||||
if self.state.m <= 0.0:
|
||||
raise ValueError("pipe mass must stay positive")
|
||||
temperature = self.gas.temperature_from_internal_energy(
|
||||
self.state.U / self.state.m
|
||||
)
|
||||
density = self.state.m / self.volume
|
||||
pressure = self.gas.pressure(density, temperature)
|
||||
properties = ThermodynamicProperties(
|
||||
p=pressure,
|
||||
T=temperature,
|
||||
rho=density,
|
||||
u=self.state.U / self.state.m,
|
||||
h=self.gas.specific_enthalpy(temperature),
|
||||
)
|
||||
self.port_1.p = pressure
|
||||
self.port_1.h_outflow = properties.h
|
||||
self.port_2.p = pressure
|
||||
self.port_2.h_outflow = properties.h
|
||||
return properties
|
||||
|
||||
def gas_mass_g(self) -> float:
|
||||
return self.state.m * 1.0e3
|
||||
|
||||
def port_mass_flow(
|
||||
self,
|
||||
*,
|
||||
port_pressure_pa: float,
|
||||
port_temperature_k: float,
|
||||
) -> float:
|
||||
"""Return mass flow from an external port into the center storage."""
|
||||
if port_pressure_pa <= 0.0:
|
||||
raise ValueError("port_pressure_pa must be positive")
|
||||
if port_temperature_k <= 0.0:
|
||||
raise ValueError("port_temperature_k must be positive")
|
||||
|
||||
center = self.properties()
|
||||
pressure_difference = port_pressure_pa - center.p
|
||||
if pressure_difference == 0.0:
|
||||
return 0.0
|
||||
upstream_pressure = max(port_pressure_pa, center.p)
|
||||
upstream_temperature = (
|
||||
port_temperature_k if pressure_difference > 0.0 else center.T
|
||||
)
|
||||
density = self.gas.density(upstream_pressure, upstream_temperature)
|
||||
magnitude = self._mass_flow_for_resistance_pressure_drop(
|
||||
abs(pressure_difference),
|
||||
density=density,
|
||||
temperature=upstream_temperature,
|
||||
)
|
||||
return magnitude if pressure_difference > 0.0 else -magnitude
|
||||
|
||||
def _mass_flow_for_resistance_pressure_drop(
|
||||
self,
|
||||
pressure_drop_pa: float,
|
||||
*,
|
||||
density: float,
|
||||
temperature: float,
|
||||
) -> float:
|
||||
original_length = self.length
|
||||
self.length = self._resistance_length
|
||||
try:
|
||||
return self._mass_flow_for_pressure_drop(
|
||||
pressure_drop_pa,
|
||||
density=density,
|
||||
temperature=temperature,
|
||||
)
|
||||
finally:
|
||||
self.length = original_length
|
||||
|
||||
def diagnostics(
|
||||
self,
|
||||
*,
|
||||
mass_flow_kg_s: float,
|
||||
temperature_k: float | None = None,
|
||||
) -> AmesimPnl0001Diagnostics:
|
||||
properties = self.properties()
|
||||
temperature = temperature_k or properties.T
|
||||
reynolds = self._reynolds_number(mass_flow_kg_s, temperature)
|
||||
friction_factor = self._friction_factor(reynolds)
|
||||
velocity = mass_flow_kg_s / (properties.rho * self.area)
|
||||
original_length = self.length
|
||||
self.length = self._resistance_length
|
||||
try:
|
||||
pressure_drop = self._darcy_pressure_drop(
|
||||
mass_flow_kg_s,
|
||||
density=properties.rho,
|
||||
temperature=temperature,
|
||||
)
|
||||
finally:
|
||||
self.length = original_length
|
||||
return AmesimPnl0001Diagnostics(
|
||||
mass_flow_kg_s=mass_flow_kg_s,
|
||||
reynolds_number=reynolds,
|
||||
gas_velocity_m_s=velocity,
|
||||
friction_factor=friction_factor,
|
||||
pressure_drop_pa=pressure_drop,
|
||||
)
|
||||
|
||||
def derivatives_from_connections(
|
||||
self,
|
||||
*,
|
||||
port_1_m_flow: float,
|
||||
connected_h_1: float,
|
||||
port_2_m_flow: float,
|
||||
connected_h_2: float,
|
||||
) -> VolumeState:
|
||||
center = self.properties()
|
||||
inlet_h_1 = self.connection_inlet_enthalpy(
|
||||
port_m_flow=port_1_m_flow,
|
||||
connected_h=connected_h_1,
|
||||
internal_h=center.h,
|
||||
)
|
||||
inlet_h_2 = self.connection_inlet_enthalpy(
|
||||
port_m_flow=port_2_m_flow,
|
||||
connected_h=connected_h_2,
|
||||
internal_h=center.h,
|
||||
)
|
||||
heat_flow = (
|
||||
self.heat_transfer_coefficient
|
||||
* self.heat_transfer_area
|
||||
* (self.external_temperature - center.T)
|
||||
)
|
||||
return VolumeState(
|
||||
m=port_1_m_flow + port_2_m_flow,
|
||||
U=port_1_m_flow * inlet_h_1 + port_2_m_flow * inlet_h_2 + heat_flow,
|
||||
)
|
||||
|
||||
|
||||
class AmesimPnl00rPipe(_DarcyPipeResistanceMixin, AlgebraicComponent):
|
||||
"""First-pass AMESim ``PNL00R`` (R) pipe resistance."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
*,
|
||||
diameter_mm: float,
|
||||
length_m: float,
|
||||
relative_roughness: float,
|
||||
gas: AmesimPneumaticGas = HELIUM_PNEUMATIC_GAS,
|
||||
) -> None:
|
||||
if diameter_mm <= 0.0:
|
||||
raise ValueError("diameter_mm must be positive")
|
||||
if length_m <= 0.0:
|
||||
raise ValueError("length_m must be positive")
|
||||
if relative_roughness < 0.0:
|
||||
raise ValueError("relative_roughness must be non-negative")
|
||||
|
||||
super().__init__(name=name)
|
||||
self.diameter = diameter_mm * 1.0e-3
|
||||
self.length = length_m
|
||||
self.relative_roughness = relative_roughness
|
||||
self.gas = gas
|
||||
self.area = diameter_mm_to_area_m2(diameter_mm)
|
||||
self.port_1 = PortState()
|
||||
self.port_2 = PortState()
|
||||
|
||||
def mass_flow(
|
||||
self,
|
||||
*,
|
||||
port_1_pressure_pa: float,
|
||||
port_1_temperature_k: float,
|
||||
port_2_pressure_pa: float,
|
||||
port_2_temperature_k: float,
|
||||
) -> float:
|
||||
"""Return mass flow from port 1 to port 2 in kg/s."""
|
||||
if port_1_pressure_pa <= 0.0 or port_2_pressure_pa <= 0.0:
|
||||
raise ValueError("port pressures must be positive")
|
||||
if port_1_temperature_k <= 0.0 or port_2_temperature_k <= 0.0:
|
||||
raise ValueError("port temperatures must be positive")
|
||||
pressure_difference = port_1_pressure_pa - port_2_pressure_pa
|
||||
if pressure_difference == 0.0:
|
||||
return 0.0
|
||||
upstream_pressure = max(port_1_pressure_pa, port_2_pressure_pa)
|
||||
upstream_temperature = (
|
||||
port_1_temperature_k
|
||||
if pressure_difference > 0.0
|
||||
else port_2_temperature_k
|
||||
)
|
||||
density = self.gas.density(upstream_pressure, upstream_temperature)
|
||||
magnitude = self._mass_flow_for_pressure_drop(
|
||||
abs(pressure_difference),
|
||||
density=density,
|
||||
temperature=upstream_temperature,
|
||||
)
|
||||
return magnitude if pressure_difference > 0.0 else -magnitude
|
||||
|
||||
def diagnostics(
|
||||
self,
|
||||
*,
|
||||
mass_flow_kg_s: float,
|
||||
pressure_pa: float,
|
||||
temperature_k: float,
|
||||
) -> AmesimPnl0001Diagnostics:
|
||||
density = self.gas.density(pressure_pa, temperature_k)
|
||||
reynolds = self._reynolds_number(mass_flow_kg_s, temperature_k)
|
||||
friction_factor = self._friction_factor(reynolds)
|
||||
velocity = mass_flow_kg_s / (density * self.area)
|
||||
pressure_drop = self._darcy_pressure_drop(
|
||||
mass_flow_kg_s,
|
||||
density=density,
|
||||
temperature=temperature_k,
|
||||
)
|
||||
return AmesimPnl0001Diagnostics(
|
||||
mass_flow_kg_s=mass_flow_kg_s,
|
||||
reynolds_number=reynolds,
|
||||
gas_velocity_m_s=velocity,
|
||||
friction_factor=friction_factor,
|
||||
pressure_drop_pa=pressure_drop,
|
||||
)
|
||||
|
||||
|
||||
def helium_dynamic_viscosity(temperature_k: float) -> float:
|
||||
"""Sutherland approximation centered on the test_mql initial condition."""
|
||||
if temperature_k <= 0.0:
|
||||
raise ValueError("temperature_k must be positive")
|
||||
reference_temperature = 293.15
|
||||
reference_viscosity = 2.0e-5
|
||||
sutherland_constant = 79.4
|
||||
return (
|
||||
reference_viscosity
|
||||
* (temperature_k / reference_temperature) ** 1.5
|
||||
* (reference_temperature + sutherland_constant)
|
||||
/ (temperature_k + sutherland_constant)
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from app.simulation.paths import PROJECT_ROOT, SIMULATION_RUNS_DIR
|
||||
from app.simulation.examples.test_mql.system import (
|
||||
TestMqlRunConfig,
|
||||
TestMqlSimulationResult,
|
||||
TestMqlSystem,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PreparedTestMqlRun:
|
||||
run_config: TestMqlRunConfig
|
||||
repo_root: Path
|
||||
output_dir: Path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlRunResult:
|
||||
run_config: TestMqlRunConfig
|
||||
prepared_run: PreparedTestMqlRun
|
||||
system: TestMqlSystem
|
||||
result: TestMqlSimulationResult
|
||||
summary_path: Path
|
||||
|
||||
|
||||
def format_test_mql_summary(system: TestMqlSystem) -> str:
|
||||
snapshot = system.snapshot()
|
||||
lines = [
|
||||
"Model: test_mql",
|
||||
f"Source archive: {system.archive_path}",
|
||||
f"Components: {snapshot.component_count}",
|
||||
f"Connections: {snapshot.connection_count}",
|
||||
f"Continuous states in AMESim modelinfo: {snapshot.continuous_state_count}",
|
||||
f"Discrete states in AMESim modelinfo: {snapshot.discrete_state_count}",
|
||||
"Global parameters:",
|
||||
]
|
||||
for name, value in sorted(snapshot.global_parameters.items()):
|
||||
lines.append(f" - {name}: {value}")
|
||||
lines.append("Component submodels:")
|
||||
for name, count in sorted(snapshot.submodel_counts.items()):
|
||||
lines.append(f" - {name}: {count}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def _default_run_output_dir() -> Path:
|
||||
timestamp = datetime.now(UTC).strftime("test_mql_%Y%m%d_%H%M%S_%f")
|
||||
return SIMULATION_RUNS_DIR / timestamp
|
||||
|
||||
|
||||
def prepare_test_mql_run(
|
||||
*,
|
||||
run_config: TestMqlRunConfig | None = None,
|
||||
output_dir: Path | None = None,
|
||||
) -> PreparedTestMqlRun:
|
||||
return PreparedTestMqlRun(
|
||||
run_config=run_config or TestMqlRunConfig(),
|
||||
repo_root=PROJECT_ROOT,
|
||||
output_dir=output_dir or _default_run_output_dir(),
|
||||
)
|
||||
|
||||
|
||||
def run_prepared_test_mql(prepared_run: PreparedTestMqlRun) -> TestMqlRunResult:
|
||||
system = TestMqlSystem()
|
||||
result = system.simulate(prepared_run.run_config)
|
||||
prepared_run.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
summary_path = prepared_run.output_dir / "test_mql_model_summary.txt"
|
||||
summary_path.write_text(format_test_mql_summary(system), encoding="utf-8")
|
||||
return TestMqlRunResult(
|
||||
run_config=prepared_run.run_config,
|
||||
prepared_run=prepared_run,
|
||||
system=system,
|
||||
result=result,
|
||||
summary_path=summary_path,
|
||||
)
|
||||
|
||||
|
||||
def run_test_mql(
|
||||
*,
|
||||
run_config: TestMqlRunConfig | None = None,
|
||||
output_dir: Path | None = None,
|
||||
) -> TestMqlRunResult:
|
||||
return run_prepared_test_mql(
|
||||
prepare_test_mql_run(run_config=run_config, output_dir=output_dir)
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
run = run_test_mql()
|
||||
print(format_test_mql_summary(run.system), end="")
|
||||
print(f"Samples: {len(run.result.t)}")
|
||||
print(f"Output directory: {run.prepared_run.output_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,80 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from app.simulation.examples.test_mql.baseline import (
|
||||
TestMqlBaselineRun,
|
||||
run_test_mql_baseline_passthrough,
|
||||
)
|
||||
from app.simulation.paths import (
|
||||
AMESIM_TEST_MQL_ARCHIVE_PATH,
|
||||
SIMULATION_RUNS_DIR,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlBaselinePathConfig:
|
||||
archive_path: Path = field(
|
||||
default_factory=lambda: AMESIM_TEST_MQL_ARCHIVE_PATH
|
||||
)
|
||||
output_dir: Path | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlBaselineExecutionConfig:
|
||||
write_summary: bool = True
|
||||
data_paths: tuple[str, ...] | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlBaselineScriptConfig:
|
||||
paths: TestMqlBaselinePathConfig = field(default_factory=TestMqlBaselinePathConfig)
|
||||
execution: TestMqlBaselineExecutionConfig = field(default_factory=TestMqlBaselineExecutionConfig)
|
||||
|
||||
|
||||
def _default_output_dir() -> Path:
|
||||
timestamp = datetime.now(UTC).strftime("test_mql_baseline_%Y%m%d_%H%M%S_%f")
|
||||
return SIMULATION_RUNS_DIR / timestamp
|
||||
|
||||
|
||||
def format_test_mql_baseline_summary(run: TestMqlBaselineRun) -> str:
|
||||
return "\n".join(
|
||||
[
|
||||
"Model: test_mql",
|
||||
"Mode: AMESim baseline passthrough",
|
||||
f"Samples: {run.sample_count}",
|
||||
f"Output schema signals: {run.output_schema.signal_count}",
|
||||
f"Compared signals: {run.signal_count}",
|
||||
f"Observation bindings: {run.observation_catalog.binding_count}",
|
||||
f"Max absolute error: {run.comparison.max_abs_error}",
|
||||
f"Max relative error: {run.comparison.max_rel_error}",
|
||||
]
|
||||
) + "\n"
|
||||
|
||||
|
||||
def run_test_mql_baseline(config: TestMqlBaselineScriptConfig | None = None):
|
||||
config = config or TestMqlBaselineScriptConfig()
|
||||
run = run_test_mql_baseline_passthrough(
|
||||
config.paths.archive_path,
|
||||
data_paths=config.execution.data_paths,
|
||||
)
|
||||
output_dir = config.paths.output_dir or _default_output_dir()
|
||||
if config.execution.write_summary:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
(output_dir / "test_mql_baseline_summary.txt").write_text(
|
||||
format_test_mql_baseline_summary(run),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return run, output_dir
|
||||
|
||||
|
||||
def main() -> None:
|
||||
run, output_dir = run_test_mql_baseline()
|
||||
print(format_test_mql_baseline_summary(run), end="")
|
||||
print(f"Output directory: {output_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.solvers.solver import (
|
||||
SolveIVPConfig as _BaseSolveIVPConfig,
|
||||
integrate_ode,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SolveIVPConfig(_BaseSolveIVPConfig):
|
||||
"""Solver defaults used by the calibrated ``test_mql`` example."""
|
||||
|
||||
atol: float = 1e-10
|
||||
|
||||
|
||||
__all__ = ["SolveIVPConfig", "integrate_ode"]
|
||||
@@ -0,0 +1,78 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.core.base import Component, DynamicComponent
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Connection:
|
||||
source_component: str
|
||||
source_port: str
|
||||
target_component: str
|
||||
target_port: str
|
||||
|
||||
|
||||
class SimulationNetwork:
|
||||
"""Container for components, topology, and state-vector bookkeeping."""
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
self.name = name
|
||||
self.components: dict[str, Component] = {}
|
||||
self.connections: list[Connection] = []
|
||||
|
||||
def add_component(self, component: Component) -> None:
|
||||
if component.name in self.components:
|
||||
raise ValueError(f"Duplicate component name: {component.name}")
|
||||
self.components[component.name] = component
|
||||
|
||||
def connect(
|
||||
self,
|
||||
source_component: str,
|
||||
source_port: str,
|
||||
target_component: str,
|
||||
target_port: str,
|
||||
) -> None:
|
||||
self.connections.append(
|
||||
Connection(
|
||||
source_component=source_component,
|
||||
source_port=source_port,
|
||||
target_component=target_component,
|
||||
target_port=target_port,
|
||||
)
|
||||
)
|
||||
|
||||
def dynamic_components(self) -> list[DynamicComponent]:
|
||||
return [
|
||||
component
|
||||
for component in self.components.values()
|
||||
if isinstance(component, DynamicComponent)
|
||||
]
|
||||
|
||||
def initial_state_vector(self) -> list[float]:
|
||||
values: list[float] = []
|
||||
for component in self.dynamic_components():
|
||||
values.extend(component.get_state_vector())
|
||||
return values
|
||||
|
||||
def apply_state_vector(self, values: list[float]) -> None:
|
||||
cursor = 0
|
||||
for component in self.dynamic_components():
|
||||
next_cursor = cursor + component.state_size
|
||||
component.set_state_vector(values[cursor:next_cursor])
|
||||
cursor = next_cursor
|
||||
if cursor != len(values):
|
||||
raise ValueError("State vector length does not match dynamic components.")
|
||||
|
||||
def summary(self) -> str:
|
||||
lines = [f"Network: {self.name}", "Components:"]
|
||||
for name, component in self.components.items():
|
||||
lines.append(f" - {name}: {component.__class__.__name__}")
|
||||
lines.append("Connections:")
|
||||
for conn in self.connections:
|
||||
lines.append(
|
||||
f" - {conn.source_component}.{conn.source_port}"
|
||||
f" -> {conn.target_component}.{conn.target_port}"
|
||||
)
|
||||
return "\n".join(lines)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import tarfile
|
||||
import xml.etree.ElementTree as ET
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlComponentContact:
|
||||
component_a: str
|
||||
port_a: str
|
||||
component_b: str
|
||||
port_b: str
|
||||
|
||||
def other_endpoint(self, component_alias: str) -> tuple[str, str]:
|
||||
if component_alias == self.component_a:
|
||||
return self.component_b, self.port_b
|
||||
if component_alias == self.component_b:
|
||||
return self.component_a, self.port_a
|
||||
raise KeyError(component_alias)
|
||||
|
||||
def port_for(self, component_alias: str) -> str:
|
||||
if component_alias == self.component_a:
|
||||
return self.port_a
|
||||
if component_alias == self.component_b:
|
||||
return self.port_b
|
||||
raise KeyError(component_alias)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlCirTopology:
|
||||
component_contacts: tuple[TestMqlComponentContact, ...]
|
||||
|
||||
def contacts_for(self, component_alias: str) -> tuple[TestMqlComponentContact, ...]:
|
||||
return tuple(
|
||||
contact
|
||||
for contact in self.component_contacts
|
||||
if component_alias in (contact.component_a, contact.component_b)
|
||||
)
|
||||
|
||||
|
||||
def load_test_mql_cir_topology(
|
||||
archive_path: str | Path,
|
||||
*,
|
||||
cir_member: str = "test_mql_.cir",
|
||||
) -> TestMqlCirTopology:
|
||||
with tarfile.open(archive_path) as archive:
|
||||
cir_file = archive.extractfile(cir_member)
|
||||
if cir_file is None:
|
||||
raise ValueError(f"Missing AMESim circuit member: {cir_member}")
|
||||
cir_text = cir_file.read().decode("latin1")
|
||||
|
||||
root = ET.fromstring(_topology_only_xml(cir_text))
|
||||
components = root.findall(".//COMPS_LIST/COMP")
|
||||
aliases = tuple(_required_text(component, "ALIAS") for component in components)
|
||||
contacts: dict[
|
||||
tuple[tuple[int, int], tuple[int, int]],
|
||||
TestMqlComponentContact,
|
||||
] = {}
|
||||
directed_contacts: set[tuple[tuple[int, int], tuple[int, int]]] = set()
|
||||
|
||||
for component_index, component in enumerate(components):
|
||||
ports = component.findall("./COMP_PORTS_LIST/COMP_PORT")
|
||||
for port_index, port in enumerate(ports):
|
||||
if port.findtext("PORT_CONNECT") != "1":
|
||||
continue
|
||||
for connection in port.findall("./CONNECT_LIST/CONNECT"):
|
||||
target_index = int(_required_text(connection, "CONNECT_ENTITY_NUM"))
|
||||
target_port_index = int(_required_text(connection, "CONNECT_ENTITY_PORT"))
|
||||
if target_index < 0 or target_index >= len(components):
|
||||
raise ValueError(f"Component contact references unknown entity {target_index}")
|
||||
target_ports = components[target_index].findall("./COMP_PORTS_LIST/COMP_PORT")
|
||||
if target_port_index < 0 or target_port_index >= len(target_ports):
|
||||
raise ValueError(
|
||||
f"Component contact references unknown port {target_port_index} "
|
||||
f"on {aliases[target_index]}"
|
||||
)
|
||||
|
||||
endpoint = (component_index, port_index)
|
||||
target_endpoint = (target_index, target_port_index)
|
||||
directed_contacts.add((endpoint, target_endpoint))
|
||||
key = tuple(sorted((endpoint, target_endpoint)))
|
||||
first, second = key
|
||||
contacts[key] = TestMqlComponentContact(
|
||||
component_a=aliases[first[0]],
|
||||
port_a=f"port_{first[1] + 1}",
|
||||
component_b=aliases[second[0]],
|
||||
port_b=f"port_{second[1] + 1}",
|
||||
)
|
||||
|
||||
for endpoint, target_endpoint in directed_contacts:
|
||||
if (target_endpoint, endpoint) not in directed_contacts:
|
||||
raise ValueError(
|
||||
"AMESim component contact is not reciprocal: "
|
||||
f"{endpoint} -> {target_endpoint}"
|
||||
)
|
||||
|
||||
return TestMqlCirTopology(component_contacts=tuple(contacts.values()))
|
||||
|
||||
|
||||
def _topology_only_xml(cir_text: str) -> str:
|
||||
# AMESim expressions inside SUBMODEL contain unescaped && and <= operators.
|
||||
# Topology lives outside those blocks, so omit them before XML parsing.
|
||||
return re.sub(
|
||||
r"<SUBMODEL>.*?</SUBMODEL>",
|
||||
"<SUBMODEL />",
|
||||
cir_text,
|
||||
flags=re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def _required_text(element: ET.Element, child_name: str) -> str:
|
||||
value = element.findtext(child_name)
|
||||
if value is None:
|
||||
raise ValueError(f"Missing AMESim circuit element: {child_name}")
|
||||
return value
|
||||
@@ -16,6 +16,7 @@ DATA_DIR = (
|
||||
|
||||
SIMULATION_RUNS_DIR = DATA_DIR / "simulation-runs"
|
||||
SIMULATION_BASELINES_DIR = PROJECT_ROOT / "tests" / "baselines" / "simulation"
|
||||
AMESIM_TEST_MQL_ARCHIVE_PATH = PROJECT_ROOT / "AmesimModels" / "test_mql.ame"
|
||||
MODELICA_TESTMODEL_RESULT_PATH = (
|
||||
PROJECT_ROOT / "ModelicaModels" / "Simulation" / "Testmodel_res.csv"
|
||||
)
|
||||
@@ -17,9 +17,10 @@ from app.simulation.core.catalog import (
|
||||
from app.simulation.core.metadata import (
|
||||
SI_UNIT_BY_QUANTITY,
|
||||
ParameterDefinition,
|
||||
ParameterOption,
|
||||
ResultVariableDefinition,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.medium import GasMedium, IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition, PortVariableDefinition
|
||||
|
||||
|
||||
@@ -27,6 +28,7 @@ ParameterSpec = ParameterDefinition
|
||||
|
||||
ENABLED_COMPONENT_LIBRARIES = (
|
||||
"app.simulation.components.experimental.library:LIBRARY",
|
||||
"app.simulation.components.amesim.library:LIBRARY",
|
||||
)
|
||||
|
||||
_MACHINE_ID_PATTERN = re.compile(r"[a-z][a-z0-9_]*")
|
||||
@@ -80,7 +82,7 @@ class ComponentModelSpec:
|
||||
ports.append(payload)
|
||||
ports.sort(key=lambda item: (int(item["order"]), str(item["name"])))
|
||||
|
||||
return {
|
||||
payload: dict[str, object] = {
|
||||
"type": self.model_type,
|
||||
"modelType": self.model_type,
|
||||
"modelVersion": self.model_version,
|
||||
@@ -93,11 +95,14 @@ class ComponentModelSpec:
|
||||
parameter.as_interface_dict() for parameter in self.parameters
|
||||
],
|
||||
}
|
||||
if self.display.role is not None:
|
||||
payload["role"] = self.display.role
|
||||
return payload
|
||||
|
||||
def create(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
medium: GasMedium,
|
||||
values: Mapping[str, float],
|
||||
) -> Component:
|
||||
unknown = sorted(set(values) - set(self.parameter_by_name))
|
||||
@@ -409,6 +414,14 @@ def _validate_parameter(
|
||||
raise ValueError(f"{field} declarations must use ParameterDefinition.")
|
||||
_validate_member_id(parameter.name, field=f"{field} name")
|
||||
_validate_label(parameter.label, field=f"{field} '{parameter.name}' label")
|
||||
if not isinstance(parameter.description, str):
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' description must be a string."
|
||||
)
|
||||
if parameter.description and not parameter.description.strip():
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' description must not be blank."
|
||||
)
|
||||
_validate_quantity_unit(
|
||||
parameter.quantity,
|
||||
parameter.unit,
|
||||
@@ -432,6 +445,74 @@ def _validate_parameter(
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' minimum_exclusive must be a boolean."
|
||||
)
|
||||
if parameter.editor not in {
|
||||
None,
|
||||
"amesimGasReference",
|
||||
"amesimGasPropertyModel",
|
||||
}:
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' uses unsupported editor "
|
||||
f"'{parameter.editor}'."
|
||||
)
|
||||
if not isinstance(parameter.options, tuple):
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' options must use a tuple."
|
||||
)
|
||||
if parameter.editor == "amesimGasPropertyModel" and not parameter.options:
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' property-model editor must declare options."
|
||||
)
|
||||
if parameter.options and parameter.editor != "amesimGasPropertyModel":
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' options require the "
|
||||
"'amesimGasPropertyModel' editor."
|
||||
)
|
||||
option_values: list[float] = []
|
||||
for option in parameter.options:
|
||||
if not isinstance(option, ParameterOption):
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' options must use ParameterOption."
|
||||
)
|
||||
if (
|
||||
isinstance(option.value, bool)
|
||||
or not isinstance(option.value, (int, float))
|
||||
or not isfinite(option.value)
|
||||
):
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' option values must be finite numbers."
|
||||
)
|
||||
_validate_label(
|
||||
option.label,
|
||||
field=(
|
||||
f"{field} '{parameter.name}' option "
|
||||
f"'{option.value:g}' label"
|
||||
),
|
||||
)
|
||||
numeric_option = float(option.value)
|
||||
if (
|
||||
parameter.editor == "amesimGasPropertyModel"
|
||||
and not numeric_option.is_integer()
|
||||
):
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' property-model option values "
|
||||
"must be integers."
|
||||
)
|
||||
option_message = parameter.validation_message(numeric_option)
|
||||
if option_message is not None:
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' option {numeric_option:g} "
|
||||
f"{option_message}."
|
||||
)
|
||||
option_values.append(numeric_option)
|
||||
duplicate_option_values = sorted(
|
||||
{value for value in option_values if option_values.count(value) > 1}
|
||||
)
|
||||
if duplicate_option_values:
|
||||
duplicates = ", ".join(f"{value:g}" for value in duplicate_option_values)
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' contains duplicate option values: "
|
||||
f"{duplicates}."
|
||||
)
|
||||
if (
|
||||
parameter.minimum is not None
|
||||
and parameter.maximum is not None
|
||||
@@ -531,6 +612,11 @@ def validate_component_model_class(
|
||||
field=f"Component '{model_type}' display symbol",
|
||||
)
|
||||
_validate_order(display.order, field=f"Component '{model_type}' display order")
|
||||
if display.role not in {None, "amesimGasMediumDefinition"}:
|
||||
raise ValueError(
|
||||
f"Component '{model_type}' DISPLAY uses unsupported role "
|
||||
f"'{display.role}'."
|
||||
)
|
||||
if not isinstance(display.ports, tuple):
|
||||
raise ValueError(f"Component '{model_type}' DISPLAY ports must be a tuple.")
|
||||
if display.library_id != library.id:
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import struct
|
||||
import tarfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class AmesimResultsError(ValueError):
|
||||
"""Raised when AMESim result files cannot be parsed consistently."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimVariable:
|
||||
index: int
|
||||
label: str
|
||||
data_path: str | None
|
||||
param_id: int | None
|
||||
hidden: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AmesimResults:
|
||||
times: tuple[float, ...]
|
||||
variables: tuple[AmesimVariable, ...]
|
||||
saved_variable_indices: tuple[int, ...]
|
||||
series_by_data_path: dict[str, tuple[float, ...]]
|
||||
final_values_by_data_path: dict[str, float]
|
||||
|
||||
@property
|
||||
def point_count(self) -> int:
|
||||
return len(self.times)
|
||||
|
||||
@property
|
||||
def saved_variable_count(self) -> int:
|
||||
return len(self.saved_variable_indices)
|
||||
|
||||
def series(self, data_path: str) -> tuple[float, ...]:
|
||||
return self.series_by_data_path[data_path]
|
||||
|
||||
def final_value(self, data_path: str) -> float:
|
||||
return self.final_values_by_data_path[data_path]
|
||||
|
||||
|
||||
_DATA_PATH_RE = re.compile(r"Data_Path=(\S+)")
|
||||
_PARAM_ID_RE = re.compile(r"Param_Id=(\d+)")
|
||||
|
||||
|
||||
def load_test_mql_amesim_results(
|
||||
archive_path: str | Path,
|
||||
*,
|
||||
time_stop_s: float | None = None,
|
||||
) -> AmesimResults:
|
||||
return load_amesim_results_from_archive(
|
||||
archive_path=archive_path,
|
||||
var_member=None,
|
||||
results_member=None,
|
||||
time_stop_s=time_stop_s,
|
||||
)
|
||||
|
||||
|
||||
def load_amesim_results_from_archive(
|
||||
*,
|
||||
archive_path: str | Path,
|
||||
var_member: str | None,
|
||||
results_member: str | None,
|
||||
time_stop_s: float | None = None,
|
||||
) -> AmesimResults:
|
||||
with tarfile.open(archive_path) as archive:
|
||||
var_member, results_member = _resolve_result_members(
|
||||
archive,
|
||||
var_member=var_member,
|
||||
results_member=results_member,
|
||||
)
|
||||
var_file = archive.extractfile(var_member)
|
||||
results_file = archive.extractfile(results_member)
|
||||
if var_file is None:
|
||||
raise AmesimResultsError(f"Missing AMESim variable member: {var_member}")
|
||||
if results_file is None:
|
||||
raise AmesimResultsError(f"Missing AMESim results member: {results_member}")
|
||||
var_lines = var_file.read().decode("latin1").splitlines()
|
||||
variables = tuple(
|
||||
_parse_variable_line(index, line) for index, line in enumerate(var_lines)
|
||||
)
|
||||
if time_stop_s is not None:
|
||||
return _parse_amesim_results_window(
|
||||
results_file,
|
||||
variables,
|
||||
time_stop_s=time_stop_s,
|
||||
)
|
||||
results_data = results_file.read()
|
||||
return parse_amesim_results_bytes(results_data, variables)
|
||||
|
||||
|
||||
def _resolve_result_members(
|
||||
archive: tarfile.TarFile,
|
||||
*,
|
||||
var_member: str | None,
|
||||
results_member: str | None,
|
||||
) -> tuple[str, str]:
|
||||
member_names = set(archive.getnames())
|
||||
if var_member is not None or results_member is not None:
|
||||
if var_member is None or results_member is None:
|
||||
raise AmesimResultsError(
|
||||
"var_member and results_member must either both be set or both be omitted."
|
||||
)
|
||||
return var_member, results_member
|
||||
|
||||
preferred = ("test_mql_.var", "test_mql_.results")
|
||||
if preferred[0] in member_names and preferred[1] in member_names:
|
||||
return preferred
|
||||
|
||||
pairs = sorted(
|
||||
(name, f"{name[:-4]}.results")
|
||||
for name in member_names
|
||||
if name.endswith(".var") and f"{name[:-4]}.results" in member_names
|
||||
)
|
||||
if len(pairs) != 1:
|
||||
raise AmesimResultsError(
|
||||
"Unable to identify a unique AMESim .var/.results member pair."
|
||||
)
|
||||
return pairs[0]
|
||||
|
||||
|
||||
def _parse_amesim_results_window(
|
||||
results_file,
|
||||
variables: tuple[AmesimVariable, ...],
|
||||
*,
|
||||
time_stop_s: float,
|
||||
) -> AmesimResults:
|
||||
header = results_file.read(8)
|
||||
if len(header) < 8:
|
||||
raise AmesimResultsError("AMESim results data is too small.")
|
||||
point_count, encoded_saved_variable_count = struct.unpack("<2i", header)
|
||||
saved_variable_count = abs(encoded_saved_variable_count)
|
||||
if point_count <= 0 or saved_variable_count <= 0:
|
||||
raise AmesimResultsError("Invalid AMESim results header.")
|
||||
|
||||
mapping_data = results_file.read(saved_variable_count * 4)
|
||||
if len(mapping_data) != saved_variable_count * 4:
|
||||
raise AmesimResultsError("AMESim results variable mapping is truncated.")
|
||||
saved_variable_indices = struct.unpack(
|
||||
f"<{saved_variable_count}i",
|
||||
mapping_data,
|
||||
)
|
||||
if any(index < 0 or index >= len(variables) for index in saved_variable_indices):
|
||||
raise AmesimResultsError(
|
||||
"AMESim results variable mapping references unknown .var rows."
|
||||
)
|
||||
|
||||
row_length = 1 + saved_variable_count
|
||||
row_byte_count = row_length * 8
|
||||
times: list[float] = []
|
||||
series_lists: dict[str, list[float]] = {}
|
||||
saved_paths: list[tuple[int, str]] = []
|
||||
for column, variable_index in enumerate(saved_variable_indices, start=1):
|
||||
data_path = variables[variable_index].data_path
|
||||
if data_path is None:
|
||||
continue
|
||||
series_lists[data_path] = []
|
||||
saved_paths.append((column, data_path))
|
||||
|
||||
for _row_index in range(point_count):
|
||||
row = results_file.read(row_byte_count)
|
||||
if len(row) != row_byte_count:
|
||||
raise AmesimResultsError("AMESim results matrix is truncated.")
|
||||
time_s = struct.unpack_from("<d", row, 0)[0]
|
||||
times.append(time_s)
|
||||
for column, data_path in saved_paths:
|
||||
series_lists[data_path].append(
|
||||
struct.unpack_from("<d", row, column * 8)[0]
|
||||
)
|
||||
# Keep one real sample after the requested stop so endpoint finite
|
||||
# differences do not silently fall back to a backward-only slope.
|
||||
if time_s > time_stop_s + 1.0e-12:
|
||||
break
|
||||
|
||||
return AmesimResults(
|
||||
times=tuple(times),
|
||||
variables=variables,
|
||||
saved_variable_indices=tuple(saved_variable_indices),
|
||||
series_by_data_path={
|
||||
data_path: tuple(values) for data_path, values in series_lists.items()
|
||||
},
|
||||
final_values_by_data_path={},
|
||||
)
|
||||
|
||||
|
||||
def parse_amesim_results_bytes(
|
||||
results_data: bytes,
|
||||
variables: tuple[AmesimVariable, ...],
|
||||
) -> AmesimResults:
|
||||
if len(results_data) < 8:
|
||||
raise AmesimResultsError("AMESim results data is too small.")
|
||||
point_count, encoded_saved_variable_count = struct.unpack_from("<2i", results_data, 0)
|
||||
saved_variable_count = abs(encoded_saved_variable_count)
|
||||
if point_count <= 0 or saved_variable_count <= 0:
|
||||
raise AmesimResultsError("Invalid AMESim results header.")
|
||||
|
||||
mapping_offset = 8
|
||||
mapping_size = saved_variable_count * 4
|
||||
data_offset = mapping_offset + mapping_size
|
||||
saved_variable_indices = struct.unpack_from(
|
||||
f"<{saved_variable_count}i",
|
||||
results_data,
|
||||
mapping_offset,
|
||||
)
|
||||
if any(index < 0 or index >= len(variables) for index in saved_variable_indices):
|
||||
raise AmesimResultsError(
|
||||
"AMESim results variable mapping references unknown .var rows."
|
||||
)
|
||||
|
||||
row_length = 1 + saved_variable_count
|
||||
main_value_count = point_count * row_length
|
||||
main_byte_count = main_value_count * 8
|
||||
main_end = data_offset + main_byte_count
|
||||
if main_end > len(results_data):
|
||||
raise AmesimResultsError("AMESim results matrix is truncated.")
|
||||
|
||||
main_values = struct.unpack_from(f"<{main_value_count}d", results_data, data_offset)
|
||||
times = tuple(main_values[row * row_length] for row in range(point_count))
|
||||
series_by_data_path: dict[str, tuple[float, ...]] = {}
|
||||
for column, variable_index in enumerate(saved_variable_indices, start=1):
|
||||
variable = variables[variable_index]
|
||||
if variable.data_path is None:
|
||||
continue
|
||||
series_by_data_path[variable.data_path] = tuple(
|
||||
main_values[row * row_length + column]
|
||||
for row in range(point_count)
|
||||
)
|
||||
|
||||
final_values_by_data_path: dict[str, float] = {}
|
||||
trailing_bytes = len(results_data) - main_end
|
||||
expected_final_bytes = (1 + len(variables)) * 8
|
||||
if trailing_bytes >= expected_final_bytes:
|
||||
final_values = struct.unpack_from(f"<{1 + len(variables)}d", results_data, main_end)
|
||||
for variable, value in zip(variables, final_values[1:]):
|
||||
if variable.data_path is not None:
|
||||
final_values_by_data_path[variable.data_path] = value
|
||||
|
||||
return AmesimResults(
|
||||
times=times,
|
||||
variables=variables,
|
||||
saved_variable_indices=tuple(saved_variable_indices),
|
||||
series_by_data_path=series_by_data_path,
|
||||
final_values_by_data_path=final_values_by_data_path,
|
||||
)
|
||||
|
||||
|
||||
def _parse_variable_line(index: int, line: str) -> AmesimVariable:
|
||||
data_path_match = _DATA_PATH_RE.search(line)
|
||||
param_id_match = _PARAM_ID_RE.search(line)
|
||||
label = line
|
||||
if data_path_match is not None:
|
||||
label = line[: data_path_match.start()].strip()
|
||||
return AmesimVariable(
|
||||
index=index,
|
||||
label=label,
|
||||
data_path=data_path_match.group(1) if data_path_match else None,
|
||||
param_id=int(param_id_match.group(1)) if param_id_match else None,
|
||||
hidden="HIDDEN" in line,
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.reporting.amesim_results import AmesimResults
|
||||
from app.simulation.reporting.test_mql_variables import (
|
||||
TestMqlVariableBinding,
|
||||
TestMqlVariableCatalog,
|
||||
build_test_mql_variable_catalog,
|
||||
)
|
||||
from app.simulation.examples.test_mql.pneumatic import (
|
||||
TestMqlPneumaticAssembly,
|
||||
build_test_mql_pneumatic_assembly,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlChamberObservation:
|
||||
time: float
|
||||
pressure_pa: float
|
||||
temperature_k: float
|
||||
gas_mass_g: float
|
||||
volume_cm3: float | None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlChamberBinding:
|
||||
alias: str
|
||||
submodel: str
|
||||
pressure_path: str
|
||||
temperature_path: str
|
||||
gas_mass_path: str
|
||||
pressure_duplicate_paths: tuple[str, ...]
|
||||
temperature_duplicate_paths: tuple[str, ...]
|
||||
volume_path: str | None
|
||||
|
||||
@property
|
||||
def is_variable(self) -> bool:
|
||||
return self.volume_path is not None
|
||||
|
||||
def observation_at(self, results: AmesimResults, index: int) -> TestMqlChamberObservation:
|
||||
return TestMqlChamberObservation(
|
||||
time=results.times[index],
|
||||
pressure_pa=results.series(self.pressure_path)[index],
|
||||
temperature_k=results.series(self.temperature_path)[index],
|
||||
gas_mass_g=results.series(self.gas_mass_path)[index],
|
||||
volume_cm3=(
|
||||
results.series(self.volume_path)[index]
|
||||
if self.volume_path is not None
|
||||
else None
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlChamberObservationCatalog:
|
||||
bindings: tuple[TestMqlChamberBinding, ...]
|
||||
|
||||
@property
|
||||
def fixed_count(self) -> int:
|
||||
return sum(1 for binding in self.bindings if binding.submodel == "PNCH023")
|
||||
|
||||
@property
|
||||
def variable_count(self) -> int:
|
||||
return sum(1 for binding in self.bindings if binding.submodel == "PNCH012")
|
||||
|
||||
def by_alias(self, alias: str) -> TestMqlChamberBinding:
|
||||
for binding in self.bindings:
|
||||
if binding.alias == alias:
|
||||
return binding
|
||||
raise KeyError(alias)
|
||||
|
||||
|
||||
def build_test_mql_chamber_observation_catalog(
|
||||
results: AmesimResults,
|
||||
*,
|
||||
variable_catalog: TestMqlVariableCatalog | None = None,
|
||||
assembly: TestMqlPneumaticAssembly | None = None,
|
||||
) -> TestMqlChamberObservationCatalog:
|
||||
variable_catalog = variable_catalog or build_test_mql_variable_catalog(results)
|
||||
assembly = assembly or build_test_mql_pneumatic_assembly()
|
||||
chamber_aliases = {
|
||||
**{alias: "PNCH023" for alias in assembly.fixed_chambers},
|
||||
**{alias: "PNCH012" for alias in assembly.variable_chambers},
|
||||
}
|
||||
bindings = []
|
||||
for alias, submodel in chamber_aliases.items():
|
||||
variables = tuple(
|
||||
variable
|
||||
for variable in variable_catalog.variables
|
||||
if variable.owner_alias == alias
|
||||
)
|
||||
pressure = _primary_observable(variables, "press", expected_units="Pa")
|
||||
temperature = _primary_observable(variables, "temp", expected_units="K")
|
||||
gas_mass = _required_path(
|
||||
variables,
|
||||
"mgas1" if submodel == "PNCH012" else "mgas",
|
||||
expected_units="g",
|
||||
)
|
||||
volume = _optional_path(variables, "vol", expected_units="cm**3")
|
||||
bindings.append(
|
||||
TestMqlChamberBinding(
|
||||
alias=alias,
|
||||
submodel=submodel,
|
||||
pressure_path=pressure.data_path,
|
||||
temperature_path=temperature.data_path,
|
||||
gas_mass_path=gas_mass,
|
||||
pressure_duplicate_paths=_duplicate_paths(variables, "press", expected_units="Pa"),
|
||||
temperature_duplicate_paths=_duplicate_paths(variables, "temp", expected_units="K"),
|
||||
volume_path=volume,
|
||||
)
|
||||
)
|
||||
return TestMqlChamberObservationCatalog(
|
||||
bindings=tuple(sorted(bindings, key=lambda binding: binding.alias))
|
||||
)
|
||||
|
||||
|
||||
def _primary_observable(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
signal_prefix: str,
|
||||
*,
|
||||
expected_units: str,
|
||||
) -> TestMqlVariableBinding:
|
||||
matches = tuple(
|
||||
variable
|
||||
for variable in variables
|
||||
if variable.signal_name == signal_prefix
|
||||
and "duplicate" not in variable.label
|
||||
)
|
||||
variable = _single(matches, f"primary {signal_prefix}")
|
||||
_assert_units(variable, expected_units)
|
||||
return variable
|
||||
|
||||
|
||||
def _duplicate_paths(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
signal_prefix: str,
|
||||
*,
|
||||
expected_units: str,
|
||||
) -> tuple[str, ...]:
|
||||
matches = tuple(
|
||||
variable
|
||||
for variable in variables
|
||||
if variable.signal_name.startswith(signal_prefix)
|
||||
and variable.signal_name != signal_prefix
|
||||
and "duplicate" in variable.label
|
||||
)
|
||||
for variable in matches:
|
||||
_assert_units(variable, expected_units)
|
||||
return tuple(variable.data_path for variable in matches)
|
||||
|
||||
|
||||
def _required_path(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
signal_name: str,
|
||||
*,
|
||||
expected_units: str,
|
||||
) -> str:
|
||||
variable = _single(
|
||||
tuple(variable for variable in variables if variable.signal_name == signal_name),
|
||||
signal_name,
|
||||
)
|
||||
_assert_units(variable, expected_units)
|
||||
return variable.data_path
|
||||
|
||||
|
||||
def _optional_path(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
signal_name: str,
|
||||
*,
|
||||
expected_units: str,
|
||||
) -> str | None:
|
||||
matches = tuple(variable for variable in variables if variable.signal_name == signal_name)
|
||||
if not matches:
|
||||
return None
|
||||
variable = _single(matches, signal_name)
|
||||
_assert_units(variable, expected_units)
|
||||
return variable.data_path
|
||||
|
||||
|
||||
def _single(
|
||||
matches: tuple[TestMqlVariableBinding, ...],
|
||||
description: str,
|
||||
) -> TestMqlVariableBinding:
|
||||
if len(matches) != 1:
|
||||
raise ValueError(f"Expected one {description} variable, found {len(matches)}.")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _assert_units(variable: TestMqlVariableBinding, expected_units: str) -> None:
|
||||
if variable.units != expected_units:
|
||||
raise ValueError(
|
||||
f"Unexpected units for {variable.data_path}: "
|
||||
f"{variable.units!r}, expected {expected_units!r}."
|
||||
)
|
||||
@@ -0,0 +1,237 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from bisect import bisect_left
|
||||
import csv
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
from app.simulation.reporting.amesim_results import AmesimResults
|
||||
|
||||
|
||||
DEFAULT_TEST_MQL_ALIGNMENT_PATHS = (
|
||||
"temp3@pn_c1_8",
|
||||
"press3@pn_c1_8",
|
||||
"vvol1@pn_brp2_8",
|
||||
"vol1@pn_brp2_8",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlComparisonMetric:
|
||||
data_path: str
|
||||
sample_count: int
|
||||
max_abs_error: float
|
||||
mean_abs_error: float
|
||||
max_rel_error: float
|
||||
final_abs_error: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlComparisonResult:
|
||||
metrics: tuple[TestMqlComparisonMetric, ...]
|
||||
|
||||
def metric(self, data_path: str) -> TestMqlComparisonMetric:
|
||||
for metric in self.metrics:
|
||||
if metric.data_path == data_path:
|
||||
return metric
|
||||
raise KeyError(data_path)
|
||||
|
||||
@property
|
||||
def max_abs_error(self) -> float:
|
||||
return max((metric.max_abs_error for metric in self.metrics), default=0.0)
|
||||
|
||||
@property
|
||||
def max_rel_error(self) -> float:
|
||||
return max((metric.max_rel_error for metric in self.metrics), default=0.0)
|
||||
|
||||
|
||||
class TestMqlComparisonError(ValueError):
|
||||
"""Raised when Python and AMESim series cannot be aligned."""
|
||||
|
||||
|
||||
def compare_test_mql_series(
|
||||
*,
|
||||
python_times: tuple[float, ...] | list[float],
|
||||
python_series_by_data_path: dict[str, tuple[float, ...] | list[float]],
|
||||
amesim_results: AmesimResults,
|
||||
data_paths: tuple[str, ...] | list[str] | None = None,
|
||||
relative_floor: float = 1.0e-12,
|
||||
) -> TestMqlComparisonResult:
|
||||
_validate_time_axis(python_times)
|
||||
selected_paths = _select_data_paths(python_series_by_data_path, amesim_results, data_paths)
|
||||
metrics = []
|
||||
for data_path in selected_paths:
|
||||
python_values = tuple(float(value) for value in python_series_by_data_path[data_path])
|
||||
if len(python_values) != len(python_times):
|
||||
raise TestMqlComparisonError(
|
||||
f"Python series length mismatch for {data_path!r}: "
|
||||
f"{len(python_values)} values for {len(python_times)} time samples."
|
||||
)
|
||||
amesim_values = amesim_results.series(data_path)
|
||||
abs_errors = []
|
||||
rel_errors = []
|
||||
for time_value, python_value in zip(python_times, python_values):
|
||||
amesim_value = interpolate_series_value(amesim_results.times, amesim_values, time_value)
|
||||
abs_error = abs(python_value - amesim_value)
|
||||
abs_errors.append(abs_error)
|
||||
rel_errors.append(abs_error / max(abs(amesim_value), relative_floor))
|
||||
final_amesim_value = interpolate_series_value(
|
||||
amesim_results.times,
|
||||
amesim_values,
|
||||
float(python_times[-1]),
|
||||
)
|
||||
metrics.append(
|
||||
TestMqlComparisonMetric(
|
||||
data_path=data_path,
|
||||
sample_count=len(python_times),
|
||||
max_abs_error=max(abs_errors, default=0.0),
|
||||
mean_abs_error=sum(abs_errors) / max(len(abs_errors), 1),
|
||||
max_rel_error=max(rel_errors, default=0.0),
|
||||
final_abs_error=abs(python_values[-1] - final_amesim_value),
|
||||
)
|
||||
)
|
||||
return TestMqlComparisonResult(metrics=tuple(metrics))
|
||||
|
||||
|
||||
def write_test_mql_amesim_baseline_csv(
|
||||
output_dir: Path,
|
||||
amesim_results: AmesimResults,
|
||||
data_paths: tuple[str, ...] | list[str] = DEFAULT_TEST_MQL_ALIGNMENT_PATHS,
|
||||
) -> Path:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
csv_path = output_dir / "test_mql_amesim_baseline.csv"
|
||||
_validate_amesim_data_paths(amesim_results, data_paths)
|
||||
with csv_path.open("w", newline="", encoding="utf-8") as handle:
|
||||
writer = csv.writer(handle)
|
||||
writer.writerow(["time_s", *data_paths])
|
||||
for index, time_value in enumerate(amesim_results.times):
|
||||
writer.writerow(
|
||||
[time_value, *(amesim_results.series(data_path)[index] for data_path in data_paths)]
|
||||
)
|
||||
return csv_path
|
||||
|
||||
|
||||
def write_test_mql_comparison_csv(
|
||||
*,
|
||||
output_dir: Path,
|
||||
python_times: tuple[float, ...] | list[float],
|
||||
python_series_by_data_path: dict[str, tuple[float, ...] | list[float]],
|
||||
amesim_results: AmesimResults,
|
||||
data_paths: tuple[str, ...] | list[str] | None = None,
|
||||
) -> tuple[Path, Path, TestMqlComparisonResult]:
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
selected_paths = _select_data_paths(python_series_by_data_path, amesim_results, data_paths)
|
||||
comparison = compare_test_mql_series(
|
||||
python_times=python_times,
|
||||
python_series_by_data_path=python_series_by_data_path,
|
||||
amesim_results=amesim_results,
|
||||
data_paths=selected_paths,
|
||||
)
|
||||
csv_path = output_dir / "test_mql_amesim_comparison.csv"
|
||||
summary_path = output_dir / "test_mql_amesim_comparison_summary.txt"
|
||||
|
||||
with csv_path.open("w", newline="", encoding="utf-8") as handle:
|
||||
writer = csv.writer(handle)
|
||||
header = ["time_s"]
|
||||
for data_path in selected_paths:
|
||||
header.extend(
|
||||
[
|
||||
f"python.{data_path}",
|
||||
f"amesim.{data_path}",
|
||||
f"abs_error.{data_path}",
|
||||
f"rel_error.{data_path}",
|
||||
]
|
||||
)
|
||||
writer.writerow(header)
|
||||
for index, time_value in enumerate(python_times):
|
||||
row = [time_value]
|
||||
for data_path in selected_paths:
|
||||
python_value = float(python_series_by_data_path[data_path][index])
|
||||
amesim_value = interpolate_series_value(
|
||||
amesim_results.times,
|
||||
amesim_results.series(data_path),
|
||||
float(time_value),
|
||||
)
|
||||
abs_error = abs(python_value - amesim_value)
|
||||
rel_error = abs_error / max(abs(amesim_value), 1.0e-12)
|
||||
row.extend([python_value, amesim_value, abs_error, rel_error])
|
||||
writer.writerow(row)
|
||||
|
||||
summary_lines = [
|
||||
(
|
||||
f"{metric.data_path}: samples={metric.sample_count}, "
|
||||
f"max_abs_error={metric.max_abs_error:.12g}, "
|
||||
f"mean_abs_error={metric.mean_abs_error:.12g}, "
|
||||
f"max_rel_error={metric.max_rel_error:.12%}, "
|
||||
f"final_abs_error={metric.final_abs_error:.12g}"
|
||||
)
|
||||
for metric in comparison.metrics
|
||||
]
|
||||
summary_path.write_text("\n".join(summary_lines) + "\n", encoding="utf-8")
|
||||
return csv_path, summary_path, comparison
|
||||
|
||||
|
||||
def interpolate_series_value(
|
||||
time_values: tuple[float, ...] | list[float],
|
||||
values: tuple[float, ...] | list[float],
|
||||
target_time: float,
|
||||
) -> float:
|
||||
if len(time_values) != len(values):
|
||||
raise TestMqlComparisonError("time and value series lengths differ.")
|
||||
if not time_values:
|
||||
raise TestMqlComparisonError("cannot interpolate an empty series.")
|
||||
if target_time <= time_values[0]:
|
||||
return float(values[0])
|
||||
if target_time >= time_values[-1]:
|
||||
return float(values[-1])
|
||||
|
||||
right_index = bisect_left(time_values, target_time)
|
||||
if right_index < len(time_values) and abs(time_values[right_index] - target_time) <= 1.0e-12:
|
||||
return float(values[right_index])
|
||||
|
||||
left_index = right_index - 1
|
||||
left_time = float(time_values[left_index])
|
||||
right_time = float(time_values[right_index])
|
||||
fraction = (target_time - left_time) / (right_time - left_time)
|
||||
return float(values[left_index]) + fraction * (float(values[right_index]) - float(values[left_index]))
|
||||
|
||||
|
||||
def _select_data_paths(
|
||||
python_series_by_data_path: dict[str, tuple[float, ...] | list[float]],
|
||||
amesim_results: AmesimResults,
|
||||
data_paths: tuple[str, ...] | list[str] | None,
|
||||
) -> tuple[str, ...]:
|
||||
if data_paths is None:
|
||||
data_paths = tuple(
|
||||
data_path
|
||||
for data_path in python_series_by_data_path
|
||||
if data_path in amesim_results.series_by_data_path
|
||||
)
|
||||
selected_paths = tuple(data_paths)
|
||||
if not selected_paths:
|
||||
raise TestMqlComparisonError("no common Data_Path values are available for comparison.")
|
||||
missing_python = [data_path for data_path in selected_paths if data_path not in python_series_by_data_path]
|
||||
if missing_python:
|
||||
raise TestMqlComparisonError(f"Python series missing Data_Path values: {missing_python}")
|
||||
_validate_amesim_data_paths(amesim_results, selected_paths)
|
||||
return selected_paths
|
||||
|
||||
|
||||
def _validate_amesim_data_paths(
|
||||
amesim_results: AmesimResults,
|
||||
data_paths: tuple[str, ...] | list[str],
|
||||
) -> None:
|
||||
missing_amesim = [data_path for data_path in data_paths if data_path not in amesim_results.series_by_data_path]
|
||||
if missing_amesim:
|
||||
raise TestMqlComparisonError(f"AMESim results missing Data_Path values: {missing_amesim}")
|
||||
|
||||
|
||||
def _validate_time_axis(time_values: tuple[float, ...] | list[float]) -> None:
|
||||
if not time_values:
|
||||
raise TestMqlComparisonError("Python time axis is empty.")
|
||||
previous = float(time_values[0])
|
||||
for value in time_values[1:]:
|
||||
value = float(value)
|
||||
if value < previous:
|
||||
raise TestMqlComparisonError("Python time axis must be monotonically increasing.")
|
||||
previous = value
|
||||
@@ -0,0 +1,211 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.reporting.amesim_results import AmesimResults
|
||||
from app.simulation.reporting.test_mql_variables import (
|
||||
TestMqlVariableBinding,
|
||||
TestMqlVariableCatalog,
|
||||
build_test_mql_variable_catalog,
|
||||
)
|
||||
from app.simulation.examples.test_mql.lines import (
|
||||
TestMqlLineAssembly,
|
||||
build_test_mql_line_assembly,
|
||||
)
|
||||
|
||||
|
||||
G_PER_S_TO_KG_PER_S = 1.0e-3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlLineObservation:
|
||||
time: float
|
||||
mass_flows_kg_s: dict[str, float]
|
||||
enthalpy_flows_w: dict[str, float]
|
||||
pressures_pa: dict[str, float]
|
||||
temperatures_k: dict[str, float]
|
||||
gas_mass_g: float | None
|
||||
reynolds_number: float
|
||||
mass_flow_parameter: float
|
||||
gas_velocity_m_s: float
|
||||
friction_factor: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlLineObservationBinding:
|
||||
alias: str
|
||||
submodel: str
|
||||
pattern: str
|
||||
mass_flow_paths: tuple[str, ...]
|
||||
enthalpy_flow_paths: tuple[str, ...]
|
||||
pressure_paths: tuple[str, ...]
|
||||
temperature_paths: tuple[str, ...]
|
||||
gas_mass_path: str | None
|
||||
reynolds_path: str
|
||||
mass_flow_parameter_path: str
|
||||
gas_velocity_path: str
|
||||
friction_factor_path: str
|
||||
|
||||
def mass_flow_kg_s_series(
|
||||
self,
|
||||
results: AmesimResults,
|
||||
data_path: str | None = None,
|
||||
) -> tuple[float, ...]:
|
||||
path = data_path or self.mass_flow_paths[0]
|
||||
if path not in self.mass_flow_paths:
|
||||
raise KeyError(path)
|
||||
return tuple(value * G_PER_S_TO_KG_PER_S for value in results.series(path))
|
||||
|
||||
def observation_at(self, results: AmesimResults, index: int) -> TestMqlLineObservation:
|
||||
return TestMqlLineObservation(
|
||||
time=results.times[index],
|
||||
mass_flows_kg_s={
|
||||
path: results.series(path)[index] * G_PER_S_TO_KG_PER_S
|
||||
for path in self.mass_flow_paths
|
||||
},
|
||||
enthalpy_flows_w={
|
||||
path: results.series(path)[index]
|
||||
for path in self.enthalpy_flow_paths
|
||||
},
|
||||
pressures_pa={
|
||||
path: results.series(path)[index]
|
||||
for path in self.pressure_paths
|
||||
},
|
||||
temperatures_k={
|
||||
path: results.series(path)[index]
|
||||
for path in self.temperature_paths
|
||||
},
|
||||
gas_mass_g=(
|
||||
results.series(self.gas_mass_path)[index]
|
||||
if self.gas_mass_path is not None
|
||||
else None
|
||||
),
|
||||
reynolds_number=results.series(self.reynolds_path)[index],
|
||||
mass_flow_parameter=results.series(self.mass_flow_parameter_path)[index],
|
||||
gas_velocity_m_s=results.series(self.gas_velocity_path)[index],
|
||||
friction_factor=results.series(self.friction_factor_path)[index],
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlLineObservationCatalog:
|
||||
bindings: tuple[TestMqlLineObservationBinding, ...]
|
||||
|
||||
@property
|
||||
def line_count(self) -> int:
|
||||
return len(self.bindings)
|
||||
|
||||
def by_alias(self, alias: str) -> TestMqlLineObservationBinding:
|
||||
for binding in self.bindings:
|
||||
if binding.alias == alias:
|
||||
return binding
|
||||
raise KeyError(alias)
|
||||
|
||||
def by_submodel(self, submodel: str) -> tuple[TestMqlLineObservationBinding, ...]:
|
||||
return tuple(binding for binding in self.bindings if binding.submodel == submodel)
|
||||
|
||||
|
||||
def build_test_mql_line_observation_catalog(
|
||||
results: AmesimResults,
|
||||
*,
|
||||
variable_catalog: TestMqlVariableCatalog | None = None,
|
||||
line_assembly: TestMqlLineAssembly | None = None,
|
||||
) -> TestMqlLineObservationCatalog:
|
||||
variable_catalog = variable_catalog or build_test_mql_variable_catalog(results)
|
||||
line_assembly = line_assembly or build_test_mql_line_assembly(results, variable_catalog)
|
||||
bindings = []
|
||||
for line in line_assembly.lines:
|
||||
variables = tuple(
|
||||
variable
|
||||
for variable in variable_catalog.variables
|
||||
if variable.owner_alias == line.alias
|
||||
)
|
||||
bindings.append(
|
||||
TestMqlLineObservationBinding(
|
||||
alias=line.alias,
|
||||
submodel=line.submodel,
|
||||
pattern=line.pattern,
|
||||
mass_flow_paths=_paths_with_prefix(variables, "dm", expected_units="g/s"),
|
||||
enthalpy_flow_paths=_paths_with_prefix(variables, "dh", expected_units="J/s"),
|
||||
pressure_paths=_paths_with_prefix(variables, "p", expected_units="Pa"),
|
||||
temperature_paths=_paths_with_prefix(variables, "t", expected_units="K"),
|
||||
gas_mass_path=_optional_path(variables, "mgas", expected_units="g"),
|
||||
reynolds_path=_required_path(variables, "re", expected_units=None),
|
||||
mass_flow_parameter_path=_required_path(
|
||||
variables,
|
||||
"cm",
|
||||
expected_units="(kg*K/J)**(1/2)",
|
||||
),
|
||||
gas_velocity_path=_required_path(variables, "v", expected_units="m/s"),
|
||||
friction_factor_path=_required_path(variables, "ff", expected_units=None),
|
||||
)
|
||||
)
|
||||
return TestMqlLineObservationCatalog(bindings=tuple(bindings))
|
||||
|
||||
|
||||
def _paths_with_prefix(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
prefix: str,
|
||||
*,
|
||||
expected_units: str | None,
|
||||
) -> tuple[str, ...]:
|
||||
matches = tuple(
|
||||
variable
|
||||
for variable in variables
|
||||
if variable.signal_name.startswith(prefix)
|
||||
)
|
||||
for variable in matches:
|
||||
_assert_units(variable, expected_units)
|
||||
return tuple(variable.data_path for variable in matches)
|
||||
|
||||
|
||||
def _required_path(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
signal_name: str,
|
||||
*,
|
||||
expected_units: str | None,
|
||||
) -> str:
|
||||
variable = _single_signal(variables, signal_name)
|
||||
_assert_units(variable, expected_units)
|
||||
return variable.data_path
|
||||
|
||||
|
||||
def _optional_path(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
signal_name: str,
|
||||
*,
|
||||
expected_units: str | None,
|
||||
) -> str | None:
|
||||
matches = tuple(variable for variable in variables if variable.signal_name == signal_name)
|
||||
if not matches:
|
||||
return None
|
||||
variable = _single(matches, signal_name)
|
||||
_assert_units(variable, expected_units)
|
||||
return variable.data_path
|
||||
|
||||
|
||||
def _single_signal(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
signal_name: str,
|
||||
) -> TestMqlVariableBinding:
|
||||
return _single(
|
||||
tuple(variable for variable in variables if variable.signal_name == signal_name),
|
||||
signal_name,
|
||||
)
|
||||
|
||||
|
||||
def _single(
|
||||
matches: tuple[TestMqlVariableBinding, ...],
|
||||
description: str,
|
||||
) -> TestMqlVariableBinding:
|
||||
if len(matches) != 1:
|
||||
raise ValueError(f"Expected one {description} variable, found {len(matches)}.")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _assert_units(variable: TestMqlVariableBinding, expected_units: str | None) -> None:
|
||||
if variable.units != expected_units:
|
||||
raise ValueError(
|
||||
f"Unexpected units for {variable.data_path}: "
|
||||
f"{variable.units!r}, expected {expected_units!r}."
|
||||
)
|
||||
@@ -0,0 +1,395 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.reporting.amesim_results import AmesimResults
|
||||
from app.simulation.reporting.test_mql_variables import (
|
||||
TestMqlVariableBinding,
|
||||
TestMqlVariableCatalog,
|
||||
build_test_mql_variable_catalog,
|
||||
)
|
||||
from app.simulation.examples.test_mql.mechanical import (
|
||||
TestMqlMechanicalAssembly,
|
||||
build_test_mql_mechanical_assembly,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPistonObservation:
|
||||
time: float
|
||||
chamber_volume_cm3: float
|
||||
chamber_volume_rate_l_min: float
|
||||
chamber_length_mm: float
|
||||
force_port_2_n: float
|
||||
force_port_3_n: float
|
||||
displacement_port_2_m: float
|
||||
velocity_port_2_m_s: float
|
||||
displacement_port_3_m: float
|
||||
velocity_port_3_m_s: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlMassEndstopObservation:
|
||||
time: float
|
||||
displacement_m: float
|
||||
velocity_m_s: float
|
||||
acceleration_m_s2: float
|
||||
lower_contact_force_n: float
|
||||
upper_contact_force_n: float
|
||||
viscous_friction_force_n: float
|
||||
dry_friction_force_n: float
|
||||
stick_flag: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlElasticEndstopObservation:
|
||||
time: float
|
||||
force_n: float
|
||||
duplicate_force_n: float
|
||||
gap_mm: float
|
||||
stiffness_n_m: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlForceSourceObservation:
|
||||
time: float
|
||||
force_n: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlForceConnectorObservation:
|
||||
time: float
|
||||
force_n: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlMechanicalNodeObservation:
|
||||
time: float
|
||||
velocities_m_s: dict[int, float]
|
||||
displacements_m: dict[int, float]
|
||||
total_force_n: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlPistonObservationBinding:
|
||||
alias: str
|
||||
volume_path: str
|
||||
volume_rate_path: str
|
||||
length_path: str
|
||||
force_port_2_path: str
|
||||
force_port_3_path: str
|
||||
displacement_port_2_path: str
|
||||
velocity_port_2_path: str
|
||||
displacement_port_3_path: str
|
||||
velocity_port_3_path: str
|
||||
|
||||
def observation_at(self, results: AmesimResults, index: int) -> TestMqlPistonObservation:
|
||||
return TestMqlPistonObservation(
|
||||
time=results.times[index],
|
||||
chamber_volume_cm3=results.series(self.volume_path)[index],
|
||||
chamber_volume_rate_l_min=results.series(self.volume_rate_path)[index],
|
||||
chamber_length_mm=results.series(self.length_path)[index],
|
||||
force_port_2_n=results.series(self.force_port_2_path)[index],
|
||||
force_port_3_n=results.series(self.force_port_3_path)[index],
|
||||
displacement_port_2_m=results.series(self.displacement_port_2_path)[index],
|
||||
velocity_port_2_m_s=results.series(self.velocity_port_2_path)[index],
|
||||
displacement_port_3_m=results.series(self.displacement_port_3_path)[index],
|
||||
velocity_port_3_m_s=results.series(self.velocity_port_3_path)[index],
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlMassEndstopObservationBinding:
|
||||
alias: str
|
||||
displacement_path: str
|
||||
velocity_path: str
|
||||
acceleration_path: str
|
||||
displacement_duplicate_path: str
|
||||
velocity_duplicate_path: str
|
||||
acceleration_duplicate_path: str
|
||||
lower_contact_force_path: str
|
||||
upper_contact_force_path: str
|
||||
viscous_friction_force_path: str
|
||||
dry_friction_force_path: str
|
||||
stick_flag_path: str
|
||||
|
||||
def observation_at(self, results: AmesimResults, index: int) -> TestMqlMassEndstopObservation:
|
||||
return TestMqlMassEndstopObservation(
|
||||
time=results.times[index],
|
||||
displacement_m=results.series(self.displacement_path)[index],
|
||||
velocity_m_s=results.series(self.velocity_path)[index],
|
||||
acceleration_m_s2=results.series(self.acceleration_path)[index],
|
||||
lower_contact_force_n=results.series(self.lower_contact_force_path)[index],
|
||||
upper_contact_force_n=results.series(self.upper_contact_force_path)[index],
|
||||
viscous_friction_force_n=results.series(self.viscous_friction_force_path)[index],
|
||||
dry_friction_force_n=results.series(self.dry_friction_force_path)[index],
|
||||
stick_flag=results.series(self.stick_flag_path)[index],
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlElasticEndstopObservationBinding:
|
||||
alias: str
|
||||
force_path: str
|
||||
duplicate_force_path: str
|
||||
gap_path: str
|
||||
stiffness_path: str
|
||||
|
||||
def observation_at(self, results: AmesimResults, index: int) -> TestMqlElasticEndstopObservation:
|
||||
return TestMqlElasticEndstopObservation(
|
||||
time=results.times[index],
|
||||
force_n=results.series(self.force_path)[index],
|
||||
duplicate_force_n=results.series(self.duplicate_force_path)[index],
|
||||
gap_mm=results.series(self.gap_path)[index],
|
||||
stiffness_n_m=results.series(self.stiffness_path)[index],
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlForceSourceObservationBinding:
|
||||
alias: str
|
||||
force_path: str
|
||||
|
||||
def observation_at(self, results: AmesimResults, index: int) -> TestMqlForceSourceObservation:
|
||||
return TestMqlForceSourceObservation(
|
||||
time=results.times[index],
|
||||
force_n=results.series(self.force_path)[index],
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlForceConnectorObservationBinding:
|
||||
alias: str
|
||||
force_path: str
|
||||
|
||||
def observation_at(self, results: AmesimResults, index: int) -> TestMqlForceConnectorObservation:
|
||||
return TestMqlForceConnectorObservation(
|
||||
time=results.times[index],
|
||||
force_n=results.series(self.force_path)[index],
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlMechanicalNodeObservationBinding:
|
||||
alias: str
|
||||
velocity_paths_by_port: dict[int, str]
|
||||
displacement_paths_by_port: dict[int, str]
|
||||
total_force_path: str
|
||||
|
||||
def observation_at(self, results: AmesimResults, index: int) -> TestMqlMechanicalNodeObservation:
|
||||
return TestMqlMechanicalNodeObservation(
|
||||
time=results.times[index],
|
||||
velocities_m_s={
|
||||
port: results.series(path)[index]
|
||||
for port, path in self.velocity_paths_by_port.items()
|
||||
},
|
||||
displacements_m={
|
||||
port: results.series(path)[index]
|
||||
for port, path in self.displacement_paths_by_port.items()
|
||||
},
|
||||
total_force_n=results.series(self.total_force_path)[index],
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlMechanicalObservationCatalog:
|
||||
pistons: dict[str, TestMqlPistonObservationBinding]
|
||||
masses: dict[str, TestMqlMassEndstopObservationBinding]
|
||||
elastic_endstops: dict[str, TestMqlElasticEndstopObservationBinding]
|
||||
zero_force_sources: dict[str, TestMqlForceSourceObservationBinding]
|
||||
force_connectors: dict[str, TestMqlForceConnectorObservationBinding]
|
||||
mechanical_nodes: dict[str, TestMqlMechanicalNodeObservationBinding]
|
||||
|
||||
@property
|
||||
def binding_count(self) -> int:
|
||||
return (
|
||||
len(self.pistons)
|
||||
+ len(self.masses)
|
||||
+ len(self.elastic_endstops)
|
||||
+ len(self.zero_force_sources)
|
||||
+ len(self.force_connectors)
|
||||
+ len(self.mechanical_nodes)
|
||||
)
|
||||
|
||||
|
||||
def build_test_mql_mechanical_observation_catalog(
|
||||
results: AmesimResults,
|
||||
*,
|
||||
variable_catalog: TestMqlVariableCatalog | None = None,
|
||||
mechanical_assembly: TestMqlMechanicalAssembly | None = None,
|
||||
) -> TestMqlMechanicalObservationCatalog:
|
||||
variable_catalog = variable_catalog or build_test_mql_variable_catalog(results)
|
||||
mechanical_assembly = mechanical_assembly or build_test_mql_mechanical_assembly(
|
||||
amesim_results=results,
|
||||
variable_catalog=variable_catalog,
|
||||
)
|
||||
return TestMqlMechanicalObservationCatalog(
|
||||
pistons={
|
||||
alias: _build_piston_binding(alias, variable_catalog)
|
||||
for alias in mechanical_assembly.pistons
|
||||
},
|
||||
masses={
|
||||
alias: _build_mass_binding(alias, variable_catalog)
|
||||
for alias in mechanical_assembly.masses
|
||||
},
|
||||
elastic_endstops={
|
||||
alias: _build_elastic_endstop_binding(alias, variable_catalog)
|
||||
for alias in mechanical_assembly.elastic_endstops
|
||||
},
|
||||
zero_force_sources={
|
||||
alias: _build_zero_force_source_binding(alias, variable_catalog)
|
||||
for alias in mechanical_assembly.zero_force_sources
|
||||
},
|
||||
force_connectors={
|
||||
alias: _build_force_connector_binding(alias, variable_catalog)
|
||||
for alias in mechanical_assembly.force_connectors
|
||||
},
|
||||
mechanical_nodes={
|
||||
alias: _build_mechanical_node_binding(alias, variable_catalog)
|
||||
for alias in mechanical_assembly.mechanical_nodes
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _build_piston_binding(
|
||||
alias: str,
|
||||
variable_catalog: TestMqlVariableCatalog,
|
||||
) -> TestMqlPistonObservationBinding:
|
||||
variables = _owner_variables(variable_catalog, alias)
|
||||
return TestMqlPistonObservationBinding(
|
||||
alias=alias,
|
||||
volume_path=_required_path(variables, "vol1", expected_units="cm**3"),
|
||||
volume_rate_path=_required_path(variables, "vvol1", expected_units="L/min"),
|
||||
length_path=_required_path(variables, "length", expected_units="mm"),
|
||||
force_port_2_path=_required_path(variables, "f2", expected_units="N"),
|
||||
force_port_3_path=_required_path(variables, "f3", expected_units="N"),
|
||||
displacement_port_2_path=_required_path(variables, "x5", expected_units="m"),
|
||||
velocity_port_2_path=_required_path(variables, "v5", expected_units="m/s"),
|
||||
displacement_port_3_path=_required_path(variables, "x4", expected_units="m"),
|
||||
velocity_port_3_path=_required_path(variables, "v4", expected_units="m/s"),
|
||||
)
|
||||
|
||||
|
||||
def _build_mass_binding(
|
||||
alias: str,
|
||||
variable_catalog: TestMqlVariableCatalog,
|
||||
) -> TestMqlMassEndstopObservationBinding:
|
||||
variables = _owner_variables(variable_catalog, alias)
|
||||
return TestMqlMassEndstopObservationBinding(
|
||||
alias=alias,
|
||||
displacement_path=_required_path(variables, "x1", expected_units="m"),
|
||||
velocity_path=_required_path(variables, "v1", expected_units="m/s"),
|
||||
acceleration_path=_required_path(variables, "acc1", expected_units="m/s/s"),
|
||||
displacement_duplicate_path=_required_path(variables, "x1dup", expected_units="m"),
|
||||
velocity_duplicate_path=_required_path(variables, "v1dup", expected_units="m/s"),
|
||||
acceleration_duplicate_path=_required_path(variables, "acc1dup", expected_units="m/s/s"),
|
||||
lower_contact_force_path=_required_path(variables, "Fmin", expected_units="N"),
|
||||
upper_contact_force_path=_required_path(variables, "Fmax", expected_units="N"),
|
||||
viscous_friction_force_path=_required_path(variables, "Fvisc", expected_units="N"),
|
||||
dry_friction_force_path=_required_path(variables, "Ffric", expected_units="N"),
|
||||
stick_flag_path=_required_path(variables, "stick", expected_units=None),
|
||||
)
|
||||
|
||||
|
||||
def _build_elastic_endstop_binding(
|
||||
alias: str,
|
||||
variable_catalog: TestMqlVariableCatalog,
|
||||
) -> TestMqlElasticEndstopObservationBinding:
|
||||
variables = _owner_variables(variable_catalog, alias)
|
||||
return TestMqlElasticEndstopObservationBinding(
|
||||
alias=alias,
|
||||
force_path=_required_path(variables, "f1", expected_units="N"),
|
||||
duplicate_force_path=_required_path(variables, "f2", expected_units="N"),
|
||||
gap_path=_required_path(variables, "gap", expected_units="mm"),
|
||||
stiffness_path=_required_path(variables, "kval", expected_units="N/m"),
|
||||
)
|
||||
|
||||
|
||||
def _build_zero_force_source_binding(
|
||||
alias: str,
|
||||
variable_catalog: TestMqlVariableCatalog,
|
||||
) -> TestMqlForceSourceObservationBinding:
|
||||
variables = _owner_variables(variable_catalog, alias)
|
||||
return TestMqlForceSourceObservationBinding(
|
||||
alias=alias,
|
||||
force_path=_required_path(variables, "fzero", expected_units="N"),
|
||||
)
|
||||
|
||||
|
||||
def _build_force_connector_binding(
|
||||
alias: str,
|
||||
variable_catalog: TestMqlVariableCatalog,
|
||||
) -> TestMqlForceConnectorObservationBinding:
|
||||
variables = _owner_variables(variable_catalog, alias)
|
||||
return TestMqlForceConnectorObservationBinding(
|
||||
alias=alias,
|
||||
force_path=_required_path(variables, "force", expected_units="N"),
|
||||
)
|
||||
|
||||
|
||||
def _build_mechanical_node_binding(
|
||||
alias: str,
|
||||
variable_catalog: TestMqlVariableCatalog,
|
||||
) -> TestMqlMechanicalNodeObservationBinding:
|
||||
variables = _owner_variables(variable_catalog, alias)
|
||||
velocity_paths_by_port = {}
|
||||
displacement_paths_by_port = {}
|
||||
for port in range(1, 9):
|
||||
velocity_paths_by_port[port] = _required_path(
|
||||
variables,
|
||||
f"p{port}__vt",
|
||||
expected_units="m/s",
|
||||
)
|
||||
displacement_paths_by_port[port] = _required_path(
|
||||
variables,
|
||||
f"p{port}__xt",
|
||||
expected_units="m",
|
||||
)
|
||||
return TestMqlMechanicalNodeObservationBinding(
|
||||
alias=alias,
|
||||
velocity_paths_by_port=velocity_paths_by_port,
|
||||
displacement_paths_by_port=displacement_paths_by_port,
|
||||
total_force_path=_required_path(variables, "tforce", expected_units="N"),
|
||||
)
|
||||
|
||||
|
||||
def _owner_variables(
|
||||
variable_catalog: TestMqlVariableCatalog,
|
||||
alias: str,
|
||||
) -> tuple[TestMqlVariableBinding, ...]:
|
||||
return tuple(
|
||||
variable
|
||||
for variable in variable_catalog.variables
|
||||
if variable.owner_alias == alias
|
||||
)
|
||||
|
||||
|
||||
def _required_path(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
signal_name: str,
|
||||
*,
|
||||
expected_units: str | None,
|
||||
) -> str:
|
||||
variable = _single(
|
||||
tuple(variable for variable in variables if variable.signal_name == signal_name),
|
||||
signal_name,
|
||||
)
|
||||
_assert_units(variable, expected_units)
|
||||
return variable.data_path
|
||||
|
||||
|
||||
def _single(
|
||||
matches: tuple[TestMqlVariableBinding, ...],
|
||||
description: str,
|
||||
) -> TestMqlVariableBinding:
|
||||
if len(matches) != 1:
|
||||
raise ValueError(f"Expected one {description} variable, found {len(matches)}.")
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _assert_units(variable: TestMqlVariableBinding, expected_units: str | None) -> None:
|
||||
if variable.units != expected_units:
|
||||
raise ValueError(
|
||||
f"Unexpected units for {variable.data_path}: "
|
||||
f"{variable.units!r}, expected {expected_units!r}."
|
||||
)
|
||||
@@ -0,0 +1,210 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.reporting.amesim_results import AmesimResults
|
||||
from app.simulation.reporting.test_mql_chamber_observations import (
|
||||
TestMqlChamberObservationCatalog,
|
||||
build_test_mql_chamber_observation_catalog,
|
||||
)
|
||||
from app.simulation.reporting.test_mql_line_observations import (
|
||||
TestMqlLineObservationCatalog,
|
||||
build_test_mql_line_observation_catalog,
|
||||
)
|
||||
from app.simulation.reporting.test_mql_mechanical_observations import (
|
||||
TestMqlMechanicalObservationCatalog,
|
||||
build_test_mql_mechanical_observation_catalog,
|
||||
)
|
||||
from app.simulation.reporting.test_mql_orifice_observations import (
|
||||
TestMqlOrificeObservationCatalog,
|
||||
build_test_mql_orifice_observation_catalog,
|
||||
)
|
||||
from app.simulation.reporting.test_mql_variables import (
|
||||
TestMqlVariableCatalog,
|
||||
build_test_mql_variable_catalog,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlObservationCatalog:
|
||||
variable_catalog: TestMqlVariableCatalog
|
||||
chambers: TestMqlChamberObservationCatalog
|
||||
orifices: TestMqlOrificeObservationCatalog
|
||||
lines: TestMqlLineObservationCatalog
|
||||
mechanical: TestMqlMechanicalObservationCatalog
|
||||
|
||||
@property
|
||||
def binding_count(self) -> int:
|
||||
return (
|
||||
len(self.chambers.bindings)
|
||||
+ len(self.orifices.bindings)
|
||||
+ self.lines.line_count
|
||||
+ self.mechanical.binding_count
|
||||
)
|
||||
|
||||
def data_paths_by_domain(self) -> dict[str, tuple[str, ...]]:
|
||||
return {
|
||||
"chambers": _sorted_unique(_chamber_data_paths(self.chambers)),
|
||||
"orifices": _sorted_unique(_orifice_data_paths(self.orifices)),
|
||||
"lines": _sorted_unique(_line_data_paths(self.lines)),
|
||||
"mechanical": _sorted_unique(_mechanical_data_paths(self.mechanical)),
|
||||
}
|
||||
|
||||
def data_paths(self) -> tuple[str, ...]:
|
||||
paths = []
|
||||
for domain_paths in self.data_paths_by_domain().values():
|
||||
paths.extend(domain_paths)
|
||||
return _sorted_unique(paths)
|
||||
|
||||
def baseline_series_by_data_path(
|
||||
self,
|
||||
results: AmesimResults,
|
||||
data_paths: tuple[str, ...] | list[str] | None = None,
|
||||
) -> dict[str, tuple[float, ...]]:
|
||||
selected_paths = tuple(data_paths) if data_paths is not None else self.data_paths()
|
||||
_validate_observed_paths(self, selected_paths)
|
||||
return {data_path: results.series(data_path) for data_path in selected_paths}
|
||||
|
||||
|
||||
def build_test_mql_observation_catalog(results: AmesimResults) -> TestMqlObservationCatalog:
|
||||
variable_catalog = build_test_mql_variable_catalog(results)
|
||||
return TestMqlObservationCatalog(
|
||||
variable_catalog=variable_catalog,
|
||||
chambers=build_test_mql_chamber_observation_catalog(
|
||||
results,
|
||||
variable_catalog=variable_catalog,
|
||||
),
|
||||
orifices=build_test_mql_orifice_observation_catalog(
|
||||
results,
|
||||
variable_catalog=variable_catalog,
|
||||
),
|
||||
lines=build_test_mql_line_observation_catalog(
|
||||
results,
|
||||
variable_catalog=variable_catalog,
|
||||
),
|
||||
mechanical=build_test_mql_mechanical_observation_catalog(
|
||||
results,
|
||||
variable_catalog=variable_catalog,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def _chamber_data_paths(catalog: TestMqlChamberObservationCatalog) -> tuple[str, ...]:
|
||||
paths = []
|
||||
for binding in catalog.bindings:
|
||||
paths.extend(
|
||||
[
|
||||
binding.pressure_path,
|
||||
binding.temperature_path,
|
||||
binding.gas_mass_path,
|
||||
*binding.pressure_duplicate_paths,
|
||||
*binding.temperature_duplicate_paths,
|
||||
]
|
||||
)
|
||||
if binding.volume_path is not None:
|
||||
paths.append(binding.volume_path)
|
||||
return tuple(paths)
|
||||
|
||||
|
||||
def _orifice_data_paths(catalog: TestMqlOrificeObservationCatalog) -> tuple[str, ...]:
|
||||
paths = []
|
||||
for binding in catalog.bindings:
|
||||
paths.extend(
|
||||
[
|
||||
binding.primary_mass_flow_path,
|
||||
binding.primary_enthalpy_flow_path,
|
||||
binding.reversed_mass_flow_path,
|
||||
binding.reversed_enthalpy_flow_path,
|
||||
binding.mass_flow_parameter_path,
|
||||
binding.gas_velocity_path,
|
||||
]
|
||||
)
|
||||
if binding.opening_path is not None:
|
||||
paths.append(binding.opening_path)
|
||||
return tuple(paths)
|
||||
|
||||
|
||||
def _line_data_paths(catalog: TestMqlLineObservationCatalog) -> tuple[str, ...]:
|
||||
paths = []
|
||||
for binding in catalog.bindings:
|
||||
paths.extend(binding.mass_flow_paths)
|
||||
paths.extend(binding.enthalpy_flow_paths)
|
||||
paths.extend(binding.pressure_paths)
|
||||
paths.extend(binding.temperature_paths)
|
||||
if binding.gas_mass_path is not None:
|
||||
paths.append(binding.gas_mass_path)
|
||||
paths.extend(
|
||||
[
|
||||
binding.reynolds_path,
|
||||
binding.mass_flow_parameter_path,
|
||||
binding.gas_velocity_path,
|
||||
binding.friction_factor_path,
|
||||
]
|
||||
)
|
||||
return tuple(paths)
|
||||
|
||||
|
||||
def _mechanical_data_paths(catalog: TestMqlMechanicalObservationCatalog) -> tuple[str, ...]:
|
||||
paths = []
|
||||
for binding in catalog.pistons.values():
|
||||
paths.extend(
|
||||
[
|
||||
binding.volume_path,
|
||||
binding.volume_rate_path,
|
||||
binding.length_path,
|
||||
binding.force_port_2_path,
|
||||
binding.force_port_3_path,
|
||||
binding.displacement_port_2_path,
|
||||
binding.velocity_port_2_path,
|
||||
binding.displacement_port_3_path,
|
||||
binding.velocity_port_3_path,
|
||||
]
|
||||
)
|
||||
for binding in catalog.masses.values():
|
||||
paths.extend(
|
||||
[
|
||||
binding.displacement_path,
|
||||
binding.velocity_path,
|
||||
binding.acceleration_path,
|
||||
binding.displacement_duplicate_path,
|
||||
binding.velocity_duplicate_path,
|
||||
binding.acceleration_duplicate_path,
|
||||
binding.lower_contact_force_path,
|
||||
binding.upper_contact_force_path,
|
||||
binding.viscous_friction_force_path,
|
||||
binding.dry_friction_force_path,
|
||||
binding.stick_flag_path,
|
||||
]
|
||||
)
|
||||
for binding in catalog.elastic_endstops.values():
|
||||
paths.extend(
|
||||
[
|
||||
binding.force_path,
|
||||
binding.duplicate_force_path,
|
||||
binding.gap_path,
|
||||
binding.stiffness_path,
|
||||
]
|
||||
)
|
||||
for binding in catalog.zero_force_sources.values():
|
||||
paths.append(binding.force_path)
|
||||
for binding in catalog.force_connectors.values():
|
||||
paths.append(binding.force_path)
|
||||
for binding in catalog.mechanical_nodes.values():
|
||||
paths.extend(binding.velocity_paths_by_port.values())
|
||||
paths.extend(binding.displacement_paths_by_port.values())
|
||||
paths.append(binding.total_force_path)
|
||||
return tuple(paths)
|
||||
|
||||
|
||||
def _validate_observed_paths(
|
||||
catalog: TestMqlObservationCatalog,
|
||||
data_paths: tuple[str, ...],
|
||||
) -> None:
|
||||
observed_paths = set(catalog.data_paths())
|
||||
missing = [data_path for data_path in data_paths if data_path not in observed_paths]
|
||||
if missing:
|
||||
raise KeyError(f"Data_Path values are not in the test_mql observation catalog: {missing}")
|
||||
|
||||
|
||||
def _sorted_unique(data_paths: tuple[str, ...] | list[str]) -> tuple[str, ...]:
|
||||
return tuple(sorted(set(data_paths)))
|
||||
@@ -0,0 +1,194 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.reporting.amesim_results import AmesimResults
|
||||
from app.simulation.reporting.test_mql_variables import (
|
||||
TestMqlVariableBinding,
|
||||
TestMqlVariableCatalog,
|
||||
build_test_mql_variable_catalog,
|
||||
)
|
||||
from app.simulation.examples.test_mql.pneumatic import (
|
||||
TestMqlPneumaticAssembly,
|
||||
build_test_mql_pneumatic_assembly,
|
||||
)
|
||||
|
||||
|
||||
G_PER_S_TO_KG_PER_S = 1.0e-3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlOrificeObservation:
|
||||
time: float
|
||||
mass_flow_kg_s: float
|
||||
enthalpy_flow_w: float
|
||||
mass_flow_parameter: float
|
||||
gas_velocity_m_s: float
|
||||
opening: float
|
||||
effective_area_m2: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlOrificeBinding:
|
||||
alias: str
|
||||
submodel: str
|
||||
nominal_area_m2: float
|
||||
flow_coefficient: float
|
||||
primary_mass_flow_path: str
|
||||
primary_enthalpy_flow_path: str
|
||||
reversed_mass_flow_path: str
|
||||
reversed_enthalpy_flow_path: str
|
||||
mass_flow_parameter_path: str
|
||||
gas_velocity_path: str
|
||||
opening_path: str | None
|
||||
|
||||
@property
|
||||
def is_variable(self) -> bool:
|
||||
return self.opening_path is not None
|
||||
|
||||
def opening_series(self, results: AmesimResults) -> tuple[float, ...]:
|
||||
if self.opening_path is None:
|
||||
return tuple(1.0 for _ in results.times)
|
||||
return tuple(results.series(self.opening_path))
|
||||
|
||||
def mass_flow_kg_s_series(self, results: AmesimResults) -> tuple[float, ...]:
|
||||
return tuple(value * G_PER_S_TO_KG_PER_S for value in results.series(self.primary_mass_flow_path))
|
||||
|
||||
def reversed_mass_flow_kg_s_series(self, results: AmesimResults) -> tuple[float, ...]:
|
||||
return tuple(value * G_PER_S_TO_KG_PER_S for value in results.series(self.reversed_mass_flow_path))
|
||||
|
||||
def effective_area_series(self, results: AmesimResults) -> tuple[float, ...]:
|
||||
return tuple(self.nominal_area_m2 * max(opening, 0.0) for opening in self.opening_series(results))
|
||||
|
||||
def observation_at(self, results: AmesimResults, index: int) -> TestMqlOrificeObservation:
|
||||
opening = self.opening_series(results)[index]
|
||||
return TestMqlOrificeObservation(
|
||||
time=results.times[index],
|
||||
mass_flow_kg_s=results.series(self.primary_mass_flow_path)[index] * G_PER_S_TO_KG_PER_S,
|
||||
enthalpy_flow_w=results.series(self.primary_enthalpy_flow_path)[index],
|
||||
mass_flow_parameter=results.series(self.mass_flow_parameter_path)[index],
|
||||
gas_velocity_m_s=results.series(self.gas_velocity_path)[index],
|
||||
opening=opening,
|
||||
effective_area_m2=self.nominal_area_m2 * max(opening, 0.0),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlOrificeObservationCatalog:
|
||||
bindings: tuple[TestMqlOrificeBinding, ...]
|
||||
|
||||
@property
|
||||
def fixed_count(self) -> int:
|
||||
return sum(1 for binding in self.bindings if binding.submodel == "PNOR001")
|
||||
|
||||
@property
|
||||
def variable_count(self) -> int:
|
||||
return sum(1 for binding in self.bindings if binding.submodel == "PNVO001")
|
||||
|
||||
def by_alias(self, alias: str) -> TestMqlOrificeBinding:
|
||||
for binding in self.bindings:
|
||||
if binding.alias == alias:
|
||||
return binding
|
||||
raise KeyError(alias)
|
||||
|
||||
|
||||
def build_test_mql_orifice_observation_catalog(
|
||||
results: AmesimResults,
|
||||
*,
|
||||
variable_catalog: TestMqlVariableCatalog | None = None,
|
||||
assembly: TestMqlPneumaticAssembly | None = None,
|
||||
) -> TestMqlOrificeObservationCatalog:
|
||||
variable_catalog = variable_catalog or build_test_mql_variable_catalog(results)
|
||||
assembly = assembly or build_test_mql_pneumatic_assembly()
|
||||
bindings = []
|
||||
for alias, orifice in {
|
||||
**assembly.fixed_orifices,
|
||||
**assembly.variable_orifices,
|
||||
}.items():
|
||||
owner_variables = tuple(
|
||||
variable
|
||||
for variable in variable_catalog.variables
|
||||
if variable.owner_alias == alias
|
||||
)
|
||||
primary_mass_flow = _find_primary(owner_variables, signal_prefix="dm")
|
||||
primary_enthalpy_flow = _find_primary(owner_variables, signal_prefix="dh")
|
||||
reversed_mass_flow = _find_reversed(owner_variables, signal_prefix="dm")
|
||||
reversed_enthalpy_flow = _find_reversed(owner_variables, signal_prefix="dh")
|
||||
mass_flow_parameter = _find_by_signal(owner_variables, "cm")
|
||||
gas_velocity = _find_by_signal(owner_variables, "gasvel")
|
||||
opening = _find_optional_by_signal(owner_variables, "xv")
|
||||
bindings.append(
|
||||
TestMqlOrificeBinding(
|
||||
alias=alias,
|
||||
submodel=primary_mass_flow.submodel,
|
||||
nominal_area_m2=orifice.area,
|
||||
flow_coefficient=orifice.flow_coefficient,
|
||||
primary_mass_flow_path=primary_mass_flow.data_path,
|
||||
primary_enthalpy_flow_path=primary_enthalpy_flow.data_path,
|
||||
reversed_mass_flow_path=reversed_mass_flow.data_path,
|
||||
reversed_enthalpy_flow_path=reversed_enthalpy_flow.data_path,
|
||||
mass_flow_parameter_path=mass_flow_parameter.data_path,
|
||||
gas_velocity_path=gas_velocity.data_path,
|
||||
opening_path=opening.data_path if opening is not None else None,
|
||||
)
|
||||
)
|
||||
return TestMqlOrificeObservationCatalog(
|
||||
bindings=tuple(sorted(bindings, key=lambda binding: binding.alias))
|
||||
)
|
||||
|
||||
|
||||
def _find_primary(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
*,
|
||||
signal_prefix: str,
|
||||
) -> TestMqlVariableBinding:
|
||||
matches = [
|
||||
variable
|
||||
for variable in variables
|
||||
if variable.signal_name.startswith(signal_prefix)
|
||||
and "sign reversed duplicate" not in variable.label
|
||||
]
|
||||
return _single(matches, f"primary {signal_prefix}")
|
||||
|
||||
|
||||
def _find_reversed(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
*,
|
||||
signal_prefix: str,
|
||||
) -> TestMqlVariableBinding:
|
||||
matches = [
|
||||
variable
|
||||
for variable in variables
|
||||
if variable.signal_name.startswith(signal_prefix)
|
||||
and "sign reversed duplicate" in variable.label
|
||||
]
|
||||
return _single(matches, f"reversed {signal_prefix}")
|
||||
|
||||
|
||||
def _find_by_signal(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
signal_name: str,
|
||||
) -> TestMqlVariableBinding:
|
||||
return _single(
|
||||
[variable for variable in variables if variable.signal_name == signal_name],
|
||||
signal_name,
|
||||
)
|
||||
|
||||
|
||||
def _find_optional_by_signal(
|
||||
variables: tuple[TestMqlVariableBinding, ...],
|
||||
signal_name: str,
|
||||
) -> TestMqlVariableBinding | None:
|
||||
matches = [variable for variable in variables if variable.signal_name == signal_name]
|
||||
if not matches:
|
||||
return None
|
||||
return _single(matches, signal_name)
|
||||
|
||||
|
||||
def _single(
|
||||
matches: list[TestMqlVariableBinding],
|
||||
description: str,
|
||||
) -> TestMqlVariableBinding:
|
||||
if len(matches) != 1:
|
||||
raise ValueError(f"Expected one {description} variable, found {len(matches)}.")
|
||||
return matches[0]
|
||||
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.reporting.amesim_results import AmesimResults
|
||||
from app.simulation.reporting.test_mql_observations import (
|
||||
TestMqlObservationCatalog,
|
||||
build_test_mql_observation_catalog,
|
||||
)
|
||||
from app.simulation.reporting.test_mql_variables import TestMqlVariableBinding
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlOutputSignal:
|
||||
data_path: str
|
||||
domain: str
|
||||
owner_alias: str
|
||||
owner_kind: str
|
||||
submodel: str
|
||||
signal_name: str
|
||||
units: str | None
|
||||
amesim_index: int
|
||||
saved: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlOutputSchema:
|
||||
signals: tuple[TestMqlOutputSignal, ...]
|
||||
|
||||
@property
|
||||
def signal_count(self) -> int:
|
||||
return len(self.signals)
|
||||
|
||||
def by_data_path(self, data_path: str) -> TestMqlOutputSignal:
|
||||
for signal in self.signals:
|
||||
if signal.data_path == data_path:
|
||||
return signal
|
||||
raise KeyError(data_path)
|
||||
|
||||
def data_paths(self) -> tuple[str, ...]:
|
||||
return tuple(signal.data_path for signal in self.signals)
|
||||
|
||||
def data_paths_by_domain(self, domain: str) -> tuple[str, ...]:
|
||||
return tuple(signal.data_path for signal in self.signals if signal.domain == domain)
|
||||
|
||||
def counts_by_domain(self) -> dict[str, int]:
|
||||
return dict(Counter(signal.domain for signal in self.signals))
|
||||
|
||||
def counts_by_submodel(self) -> dict[str, int]:
|
||||
return dict(Counter(signal.submodel for signal in self.signals))
|
||||
|
||||
def counts_by_owner_kind(self) -> dict[str, int]:
|
||||
return dict(Counter(signal.owner_kind for signal in self.signals))
|
||||
|
||||
def counts_by_units(self) -> dict[str | None, int]:
|
||||
return dict(Counter(signal.units for signal in self.signals))
|
||||
|
||||
|
||||
def build_test_mql_output_schema(
|
||||
results: AmesimResults,
|
||||
*,
|
||||
observation_catalog: TestMqlObservationCatalog | None = None,
|
||||
) -> TestMqlOutputSchema:
|
||||
observation_catalog = observation_catalog or build_test_mql_observation_catalog(results)
|
||||
domain_by_data_path = _domain_by_data_path(observation_catalog)
|
||||
signals = []
|
||||
for data_path in sorted(domain_by_data_path):
|
||||
variable = observation_catalog.variable_catalog.by_data_path(data_path)
|
||||
signals.append(_signal_from_variable(variable, domain_by_data_path[data_path]))
|
||||
return TestMqlOutputSchema(signals=tuple(signals))
|
||||
|
||||
|
||||
def _domain_by_data_path(
|
||||
observation_catalog: TestMqlObservationCatalog,
|
||||
) -> dict[str, str]:
|
||||
domain_by_data_path = {}
|
||||
for domain, data_paths in observation_catalog.data_paths_by_domain().items():
|
||||
for data_path in data_paths:
|
||||
if data_path in domain_by_data_path:
|
||||
raise ValueError(f"Data_Path {data_path!r} is assigned to multiple domains.")
|
||||
domain_by_data_path[data_path] = domain
|
||||
return domain_by_data_path
|
||||
|
||||
|
||||
def _signal_from_variable(
|
||||
variable: TestMqlVariableBinding,
|
||||
domain: str,
|
||||
) -> TestMqlOutputSignal:
|
||||
return TestMqlOutputSignal(
|
||||
data_path=variable.data_path,
|
||||
domain=domain,
|
||||
owner_alias=variable.owner_alias,
|
||||
owner_kind=variable.owner_kind,
|
||||
submodel=variable.submodel,
|
||||
signal_name=variable.signal_name,
|
||||
units=variable.units,
|
||||
amesim_index=variable.index,
|
||||
saved=variable.saved,
|
||||
)
|
||||
@@ -0,0 +1,161 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import isfinite
|
||||
|
||||
from app.simulation.reporting.amesim_results import AmesimResults
|
||||
from app.simulation.reporting.test_mql_comparison import (
|
||||
TestMqlComparisonResult,
|
||||
compare_test_mql_series,
|
||||
)
|
||||
from app.simulation.reporting.test_mql_output_schema import TestMqlOutputSchema
|
||||
|
||||
|
||||
class TestMqlOutputValidationError(ValueError):
|
||||
"""Raised when a Python test_mql output does not satisfy the AMESim output contract."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlValidatedOutput:
|
||||
times: tuple[float, ...]
|
||||
series_by_data_path: dict[str, tuple[float, ...]]
|
||||
data_paths: tuple[str, ...]
|
||||
|
||||
def series(self, data_path: str) -> tuple[float, ...]:
|
||||
if data_path not in self.series_by_data_path:
|
||||
raise KeyError(data_path)
|
||||
return self.series_by_data_path[data_path]
|
||||
|
||||
|
||||
def validate_test_mql_output(
|
||||
*,
|
||||
times: tuple[float, ...] | list[float],
|
||||
series_by_data_path: dict[str, tuple[float, ...] | list[float]],
|
||||
schema: TestMqlOutputSchema,
|
||||
data_paths: tuple[str, ...] | list[str] | None = None,
|
||||
allow_extra_paths: bool = False,
|
||||
require_all_schema_paths: bool = False,
|
||||
) -> TestMqlValidatedOutput:
|
||||
validated_times = _validate_time_axis(times)
|
||||
selected_paths = _select_paths(
|
||||
series_by_data_path=series_by_data_path,
|
||||
schema=schema,
|
||||
data_paths=data_paths,
|
||||
allow_extra_paths=allow_extra_paths,
|
||||
require_all_schema_paths=require_all_schema_paths,
|
||||
)
|
||||
validated_series = {
|
||||
data_path: _validate_series(
|
||||
data_path=data_path,
|
||||
values=series_by_data_path[data_path],
|
||||
expected_count=len(validated_times),
|
||||
)
|
||||
for data_path in selected_paths
|
||||
}
|
||||
return TestMqlValidatedOutput(
|
||||
times=validated_times,
|
||||
series_by_data_path=validated_series,
|
||||
data_paths=selected_paths,
|
||||
)
|
||||
|
||||
|
||||
def compare_validated_test_mql_output(
|
||||
*,
|
||||
times: tuple[float, ...] | list[float],
|
||||
series_by_data_path: dict[str, tuple[float, ...] | list[float]],
|
||||
schema: TestMqlOutputSchema,
|
||||
amesim_results: AmesimResults,
|
||||
data_paths: tuple[str, ...] | list[str] | None = None,
|
||||
allow_extra_paths: bool = False,
|
||||
require_all_schema_paths: bool = False,
|
||||
relative_floor: float = 1.0e-12,
|
||||
) -> TestMqlComparisonResult:
|
||||
validated = validate_test_mql_output(
|
||||
times=times,
|
||||
series_by_data_path=series_by_data_path,
|
||||
schema=schema,
|
||||
data_paths=data_paths,
|
||||
allow_extra_paths=allow_extra_paths,
|
||||
require_all_schema_paths=require_all_schema_paths,
|
||||
)
|
||||
return compare_test_mql_series(
|
||||
python_times=validated.times,
|
||||
python_series_by_data_path=validated.series_by_data_path,
|
||||
amesim_results=amesim_results,
|
||||
data_paths=validated.data_paths,
|
||||
relative_floor=relative_floor,
|
||||
)
|
||||
|
||||
|
||||
def _validate_time_axis(times: tuple[float, ...] | list[float]) -> tuple[float, ...]:
|
||||
if not times:
|
||||
raise TestMqlOutputValidationError("Python time axis is empty.")
|
||||
validated = tuple(_finite_float("time", value) for value in times)
|
||||
previous = validated[0]
|
||||
for value in validated[1:]:
|
||||
if value < previous:
|
||||
raise TestMqlOutputValidationError("Python time axis must be monotonically increasing.")
|
||||
previous = value
|
||||
return validated
|
||||
|
||||
|
||||
def _select_paths(
|
||||
*,
|
||||
series_by_data_path: dict[str, tuple[float, ...] | list[float]],
|
||||
schema: TestMqlOutputSchema,
|
||||
data_paths: tuple[str, ...] | list[str] | None,
|
||||
allow_extra_paths: bool,
|
||||
require_all_schema_paths: bool,
|
||||
) -> tuple[str, ...]:
|
||||
schema_paths = set(schema.data_paths())
|
||||
provided_paths = set(series_by_data_path)
|
||||
if not allow_extra_paths:
|
||||
extra_paths = sorted(provided_paths - schema_paths)
|
||||
if extra_paths:
|
||||
raise TestMqlOutputValidationError(
|
||||
f"Python output contains Data_Path values outside test_mql schema: {extra_paths}"
|
||||
)
|
||||
if require_all_schema_paths:
|
||||
missing_schema_paths = sorted(schema_paths - provided_paths)
|
||||
if missing_schema_paths:
|
||||
raise TestMqlOutputValidationError(
|
||||
f"Python output is missing required test_mql schema Data_Path values: {missing_schema_paths}"
|
||||
)
|
||||
selected_paths = tuple(data_paths) if data_paths is not None else tuple(sorted(provided_paths & schema_paths))
|
||||
if not selected_paths:
|
||||
raise TestMqlOutputValidationError("no test_mql schema Data_Path values are available.")
|
||||
unknown_selected = [data_path for data_path in selected_paths if data_path not in schema_paths]
|
||||
if unknown_selected:
|
||||
raise TestMqlOutputValidationError(
|
||||
f"Requested Data_Path values are outside test_mql schema: {unknown_selected}"
|
||||
)
|
||||
missing_selected = [data_path for data_path in selected_paths if data_path not in series_by_data_path]
|
||||
if missing_selected:
|
||||
raise TestMqlOutputValidationError(
|
||||
f"Python output is missing selected Data_Path values: {missing_selected}"
|
||||
)
|
||||
return selected_paths
|
||||
|
||||
|
||||
def _validate_series(
|
||||
*,
|
||||
data_path: str,
|
||||
values: tuple[float, ...] | list[float],
|
||||
expected_count: int,
|
||||
) -> tuple[float, ...]:
|
||||
if len(values) != expected_count:
|
||||
raise TestMqlOutputValidationError(
|
||||
f"Python series length mismatch for {data_path!r}: "
|
||||
f"{len(values)} values for {expected_count} time samples."
|
||||
)
|
||||
return tuple(_finite_float(data_path, value) for value in values)
|
||||
|
||||
|
||||
def _finite_float(label: str, value: float) -> float:
|
||||
try:
|
||||
numeric_value = float(value)
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise TestMqlOutputValidationError(f"{label!r} contains a non-numeric value: {value!r}") from exc
|
||||
if not isfinite(numeric_value):
|
||||
raise TestMqlOutputValidationError(f"{label!r} contains a non-finite value: {value!r}")
|
||||
return numeric_value
|
||||
@@ -0,0 +1,111 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
|
||||
from app.simulation.reporting.amesim_results import AmesimResults, AmesimVariable
|
||||
from app.simulation.examples.test_mql.system import COMPONENT_SPECS, CONNECTION_SPECS
|
||||
|
||||
|
||||
_UNIT_RE = re.compile(r"\[([^\]]+)\]\s*$")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlVariableBinding:
|
||||
index: int
|
||||
data_path: str
|
||||
signal_name: str
|
||||
owner_alias: str
|
||||
owner_kind: str
|
||||
submodel: str
|
||||
label: str
|
||||
units: str | None
|
||||
saved: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TestMqlVariableCatalog:
|
||||
variables: tuple[TestMqlVariableBinding, ...]
|
||||
|
||||
@property
|
||||
def data_path_count(self) -> int:
|
||||
return len(self.variables)
|
||||
|
||||
@property
|
||||
def saved_data_path_count(self) -> int:
|
||||
return sum(1 for variable in self.variables if variable.saved)
|
||||
|
||||
def by_data_path(self, data_path: str) -> TestMqlVariableBinding:
|
||||
for variable in self.variables:
|
||||
if variable.data_path == data_path:
|
||||
return variable
|
||||
raise KeyError(data_path)
|
||||
|
||||
def counts_by_submodel(self) -> dict[str, int]:
|
||||
return dict(Counter(variable.submodel for variable in self.variables))
|
||||
|
||||
def counts_by_owner_kind(self) -> dict[str, int]:
|
||||
return dict(Counter(variable.owner_kind for variable in self.variables))
|
||||
|
||||
def data_paths_for_owner(self, owner_alias: str) -> tuple[str, ...]:
|
||||
return tuple(
|
||||
variable.data_path
|
||||
for variable in self.variables
|
||||
if variable.owner_alias == owner_alias
|
||||
)
|
||||
|
||||
|
||||
def build_test_mql_variable_catalog(amesim_results: AmesimResults) -> TestMqlVariableCatalog:
|
||||
owner_map = _build_owner_map()
|
||||
saved_indices = set(amesim_results.saved_variable_indices)
|
||||
bindings = []
|
||||
for variable in amesim_results.variables:
|
||||
if variable.data_path is None:
|
||||
continue
|
||||
signal_name, owner_alias = split_data_path(variable.data_path)
|
||||
owner_kind, submodel = owner_map[owner_alias]
|
||||
bindings.append(
|
||||
TestMqlVariableBinding(
|
||||
index=variable.index,
|
||||
data_path=variable.data_path,
|
||||
signal_name=signal_name,
|
||||
owner_alias=owner_alias,
|
||||
owner_kind=owner_kind,
|
||||
submodel=submodel,
|
||||
label=variable.label,
|
||||
units=_extract_units(variable),
|
||||
saved=variable.index in saved_indices,
|
||||
)
|
||||
)
|
||||
return TestMqlVariableCatalog(variables=tuple(bindings))
|
||||
|
||||
|
||||
def split_data_path(data_path: str) -> tuple[str, str]:
|
||||
if "@" not in data_path:
|
||||
raise ValueError(f"AMESim Data_Path does not contain an owner alias: {data_path!r}")
|
||||
signal_name, owner_alias = data_path.rsplit("@", 1)
|
||||
if not signal_name or not owner_alias:
|
||||
raise ValueError(f"Invalid AMESim Data_Path: {data_path!r}")
|
||||
return signal_name, owner_alias
|
||||
|
||||
|
||||
def _build_owner_map() -> dict[str, tuple[str, str]]:
|
||||
owner_map = {
|
||||
str(spec["alias"]): ("component", str(spec["submodel"]))
|
||||
for spec in COMPONENT_SPECS
|
||||
}
|
||||
owner_map.update(
|
||||
{
|
||||
str(spec["alias"]): ("connection", str(spec["submodel"]))
|
||||
for spec in CONNECTION_SPECS
|
||||
}
|
||||
)
|
||||
return owner_map
|
||||
|
||||
|
||||
def _extract_units(variable: AmesimVariable) -> str | None:
|
||||
match = _UNIT_RE.search(variable.label)
|
||||
if match is None:
|
||||
return None
|
||||
return match.group(1)
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import sqrt
|
||||
from math import isfinite, sqrt
|
||||
|
||||
from app.simulation.core.ports import PortState, VariableRole
|
||||
from app.simulation.systems.network import SimulationNetwork
|
||||
@@ -68,6 +68,7 @@ class PressureFlowSolver:
|
||||
self.residual_tolerance = residual_tolerance
|
||||
self.max_evaluations = max_evaluations
|
||||
self.unknowns = self._build_unknowns()
|
||||
self._unknowns_by_id = {unknown.id: unknown for unknown in self.unknowns}
|
||||
self.last_diagnostics: AlgebraicSolveDiagnostics | None = None
|
||||
|
||||
def _build_unknowns(self) -> tuple[AlgebraicUnknown, ...]:
|
||||
@@ -91,55 +92,227 @@ class PressureFlowSolver:
|
||||
)
|
||||
return tuple(unknowns)
|
||||
|
||||
@staticmethod
|
||||
def _port_key(variable: str, expected_variable: str) -> tuple[str, str] | None:
|
||||
try:
|
||||
component_name, port_name, variable_name = variable.rsplit(".", 2)
|
||||
except ValueError:
|
||||
return None
|
||||
if variable_name != expected_variable:
|
||||
return None
|
||||
return component_name, port_name
|
||||
|
||||
def _seed_equal_pressures(self) -> None:
|
||||
for _ in range(max(2, len(self.network.connections))):
|
||||
changed = False
|
||||
"""Lift current state pressures across their complete equality groups.
|
||||
|
||||
Dynamic components refresh their own pressure ports before each closure,
|
||||
while connected algebraic ports retain values from the preceding RHS
|
||||
evaluation. Merely filling non-positive pressures therefore leaves a
|
||||
stale, and sometimes badly conditioned, nonlinear initial guess. State
|
||||
equations expose the current pressure as ``port.p - target``; use that
|
||||
target as the authoritative anchor for every connected/equal port.
|
||||
"""
|
||||
|
||||
pressure_unknowns = {
|
||||
(unknown.component, unknown.port): unknown
|
||||
for unknown in self.unknowns
|
||||
if unknown.variable == "p"
|
||||
}
|
||||
if not pressure_unknowns:
|
||||
return
|
||||
|
||||
parent = {key: key for key in pressure_unknowns}
|
||||
|
||||
def find(key: tuple[str, str]) -> tuple[str, str]:
|
||||
root = key
|
||||
while parent[root] != root:
|
||||
root = parent[root]
|
||||
while parent[key] != key:
|
||||
next_key = parent[key]
|
||||
parent[key] = root
|
||||
key = next_key
|
||||
return root
|
||||
|
||||
def union(first: tuple[str, str], second: tuple[str, str]) -> None:
|
||||
first_root = find(first)
|
||||
second_root = find(second)
|
||||
if first_root != second_root:
|
||||
parent[second_root] = first_root
|
||||
|
||||
for connection in self.network.connections:
|
||||
if connection.kind != "physical":
|
||||
continue
|
||||
first = self.network.components[
|
||||
connection.endpoint_a.component
|
||||
].get_port(connection.endpoint_a.port)
|
||||
second = self.network.components[
|
||||
connection.endpoint_b.component
|
||||
].get_port(connection.endpoint_b.port)
|
||||
if first.p > 0.0 and second.p <= 0.0:
|
||||
second.p = first.p
|
||||
changed = True
|
||||
elif second.p > 0.0 and first.p <= 0.0:
|
||||
first.p = second.p
|
||||
changed = True
|
||||
first = connection.endpoint_a.key
|
||||
second = connection.endpoint_b.key
|
||||
if first in pressure_unknowns and second in pressure_unknowns:
|
||||
union(first, second)
|
||||
|
||||
for component in self.network.components.values():
|
||||
equal_pressure_equations = [
|
||||
equation
|
||||
for equation in component.pressure_flow_equation_residuals()
|
||||
if equation.relation == "equal" and equation.role == "effort"
|
||||
]
|
||||
for equation in equal_pressure_equations:
|
||||
states = []
|
||||
for variable in equation.variables:
|
||||
_, port_name, variable_name = variable.rsplit(".", 2)
|
||||
if variable_name == "p":
|
||||
states.append(component.get_port(port_name))
|
||||
if len(states) != 2:
|
||||
component_equations = {
|
||||
component.name: component.pressure_flow_equation_residuals()
|
||||
for component in self.network.components.values()
|
||||
}
|
||||
for equations in component_equations.values():
|
||||
for equation in equations:
|
||||
if equation.relation != "equal" or equation.role != "effort":
|
||||
continue
|
||||
first, second = states
|
||||
if first.p > 0.0 and second.p <= 0.0:
|
||||
second.p = first.p
|
||||
changed = True
|
||||
elif second.p > 0.0 and first.p <= 0.0:
|
||||
first.p = second.p
|
||||
changed = True
|
||||
if not changed:
|
||||
break
|
||||
endpoints = [
|
||||
endpoint
|
||||
for variable in equation.variables
|
||||
if (
|
||||
(endpoint := self._port_key(variable, "p"))
|
||||
in pressure_unknowns
|
||||
)
|
||||
]
|
||||
for endpoint in endpoints[1:]:
|
||||
union(endpoints[0], endpoint)
|
||||
|
||||
def _scales(self) -> tuple[float, float]:
|
||||
members_by_root: dict[tuple[str, str], list[tuple[str, str]]] = {}
|
||||
for endpoint in pressure_unknowns:
|
||||
members_by_root.setdefault(find(endpoint), []).append(endpoint)
|
||||
|
||||
anchors_by_root: dict[tuple[str, str], list[float]] = {}
|
||||
for equations in component_equations.values():
|
||||
for equation in equations:
|
||||
if equation.relation != "state" or equation.role != "effort":
|
||||
continue
|
||||
endpoints = [
|
||||
endpoint
|
||||
for variable in equation.variables
|
||||
if (
|
||||
(endpoint := self._port_key(variable, "p"))
|
||||
in pressure_unknowns
|
||||
)
|
||||
]
|
||||
if len(endpoints) != 1:
|
||||
continue
|
||||
endpoint = endpoints[0]
|
||||
unknown = pressure_unknowns[endpoint]
|
||||
target_pressure = unknown.read() - float(equation.value)
|
||||
if not isfinite(target_pressure):
|
||||
continue
|
||||
# Keep the state-owned port current even when an invalid model
|
||||
# has conflicting storage anchors in one equality group.
|
||||
unknown.write(target_pressure)
|
||||
anchors_by_root.setdefault(find(endpoint), []).append(target_pressure)
|
||||
|
||||
for root, members in members_by_root.items():
|
||||
anchors = anchors_by_root.get(root, [])
|
||||
if anchors:
|
||||
pressure_scale = max([abs(value) for value in anchors] + [1.0])
|
||||
if max(anchors) - min(anchors) > 1.0e-9 * pressure_scale:
|
||||
# A conflicting multi-storage group is structurally invalid;
|
||||
# leave it for the residual solver/preparation diagnostics.
|
||||
continue
|
||||
target_pressure = sum(anchors) / len(anchors)
|
||||
for endpoint in members:
|
||||
pressure_unknowns[endpoint].write(target_pressure)
|
||||
continue
|
||||
|
||||
positive_seed = next(
|
||||
(
|
||||
pressure_unknowns[endpoint].read()
|
||||
for endpoint in members
|
||||
if pressure_unknowns[endpoint].read() > 0.0
|
||||
),
|
||||
None,
|
||||
)
|
||||
if positive_seed is None:
|
||||
continue
|
||||
for endpoint in members:
|
||||
unknown = pressure_unknowns[endpoint]
|
||||
if unknown.read() <= 0.0:
|
||||
unknown.write(positive_seed)
|
||||
|
||||
def _seed_explicit_mass_flows(self) -> None:
|
||||
"""Initialize explicit ``m_flow - f(...)`` constitutive relations.
|
||||
|
||||
AMESim orifices and quasi-steady pneumatic lines expose one mass-flow
|
||||
unknown with unit coefficient. Once pressure anchors are current, a
|
||||
residual correction places that flow directly on its constitutive
|
||||
surface and avoids asking the nonlinear optimizer to discover the
|
||||
square-root branch from a stale preceding-step value.
|
||||
"""
|
||||
|
||||
seeded_ids: set[str] = set()
|
||||
for component in self.network.components.values():
|
||||
for equation in component.pressure_flow_equation_residuals():
|
||||
if equation.relation != "constitutive" or equation.role != "flow":
|
||||
continue
|
||||
mass_flow_unknowns = [
|
||||
self._unknowns_by_id[variable]
|
||||
for variable in equation.variables
|
||||
if variable in self._unknowns_by_id
|
||||
and self._unknowns_by_id[variable].variable == "m_flow"
|
||||
]
|
||||
if len(mass_flow_unknowns) != 1:
|
||||
continue
|
||||
unknown = mass_flow_unknowns[0]
|
||||
target_flow = unknown.read() - float(equation.value)
|
||||
if not isfinite(target_flow):
|
||||
continue
|
||||
unknown.write(target_flow)
|
||||
seeded_ids.add(unknown.id)
|
||||
|
||||
# Complete local two-port balances for explicit elements. Connection
|
||||
# flow equations remain available to align the adjacent component port.
|
||||
for component in self.network.components.values():
|
||||
for equation in component.pressure_flow_equation_residuals():
|
||||
if equation.relation != "sumToZero" or equation.role != "flow":
|
||||
continue
|
||||
mass_flow_unknowns = [
|
||||
self._unknowns_by_id[variable]
|
||||
for variable in equation.variables
|
||||
if variable in self._unknowns_by_id
|
||||
and self._unknowns_by_id[variable].variable == "m_flow"
|
||||
]
|
||||
if len(mass_flow_unknowns) != 2:
|
||||
continue
|
||||
seeded = [
|
||||
unknown for unknown in mass_flow_unknowns if unknown.id in seeded_ids
|
||||
]
|
||||
if len(seeded) != 1:
|
||||
continue
|
||||
other = next(
|
||||
unknown for unknown in mass_flow_unknowns if unknown.id not in seeded_ids
|
||||
)
|
||||
other.write(-seeded[0].read())
|
||||
seeded_ids.add(other.id)
|
||||
|
||||
# A physical connector imposes the same sum-to-zero flow rule as a
|
||||
# two-port component. Once an explicit component flow is known, carry
|
||||
# that guess to the connected storage/boundary port as well. For the
|
||||
# common volume-orifice-volume topology this makes the seeded state an
|
||||
# exact algebraic solution and avoids an unnecessary nonlinear solve on
|
||||
# every ODE/Jacobian evaluation.
|
||||
for connection in self.network.connections:
|
||||
if connection.kind != "physical":
|
||||
continue
|
||||
endpoint_unknowns = []
|
||||
for endpoint in connection.endpoints:
|
||||
unknown = self._unknowns_by_id.get(
|
||||
f"{endpoint.component}.{endpoint.port}.m_flow"
|
||||
)
|
||||
if unknown is not None:
|
||||
endpoint_unknowns.append(unknown)
|
||||
if len(endpoint_unknowns) != 2:
|
||||
continue
|
||||
seeded = [
|
||||
unknown for unknown in endpoint_unknowns if unknown.id in seeded_ids
|
||||
]
|
||||
if len(seeded) != 1:
|
||||
continue
|
||||
other = next(
|
||||
unknown for unknown in endpoint_unknowns if unknown.id not in seeded_ids
|
||||
)
|
||||
other.write(-seeded[0].read())
|
||||
seeded_ids.add(other.id)
|
||||
|
||||
def _scales(self) -> dict[str, float]:
|
||||
pressure_scale = max(
|
||||
[
|
||||
abs(unknown.read())
|
||||
for unknown in self.unknowns
|
||||
if unknown.role == "effort" and unknown.read() > 0.0
|
||||
if unknown.variable == "p" and unknown.read() > 0.0
|
||||
]
|
||||
+ [1e5]
|
||||
)
|
||||
@@ -148,16 +321,31 @@ class PressureFlowSolver:
|
||||
for component in self.network.components.values()
|
||||
if hasattr(component, "K_eff")
|
||||
]
|
||||
flow_scale = max(
|
||||
mass_flow_scale = max(
|
||||
estimated_flows
|
||||
+ [
|
||||
abs(unknown.read())
|
||||
for unknown in self.unknowns
|
||||
if unknown.role == "flow"
|
||||
if unknown.variable == "m_flow"
|
||||
]
|
||||
+ [1e-3]
|
||||
)
|
||||
return pressure_scale, flow_scale
|
||||
return {
|
||||
"p": pressure_scale,
|
||||
"m_flow": mass_flow_scale,
|
||||
"x": max(
|
||||
[abs(unknown.read()) for unknown in self.unknowns if unknown.variable == "x"]
|
||||
+ [1.0]
|
||||
),
|
||||
"v": max(
|
||||
[abs(unknown.read()) for unknown in self.unknowns if unknown.variable == "v"]
|
||||
+ [1.0]
|
||||
),
|
||||
"f": max(
|
||||
[abs(unknown.read()) for unknown in self.unknowns if unknown.variable == "f"]
|
||||
+ [1.0]
|
||||
),
|
||||
}
|
||||
|
||||
def solve(self) -> AlgebraicSolveDiagnostics:
|
||||
try:
|
||||
@@ -169,11 +357,14 @@ class PressureFlowSolver:
|
||||
) from exc
|
||||
|
||||
self._seed_equal_pressures()
|
||||
pressure_scale, flow_scale = self._scales()
|
||||
self._seed_explicit_mass_flows()
|
||||
scales = self._scales()
|
||||
pressure_scale = scales["p"]
|
||||
flow_scale = scales["m_flow"]
|
||||
positive_pressures = [
|
||||
unknown.read()
|
||||
for unknown in self.unknowns
|
||||
if unknown.role == "effort" and unknown.read() > 0.0
|
||||
if unknown.variable == "p" and unknown.read() > 0.0
|
||||
]
|
||||
fallback_pressure = (
|
||||
sum(positive_pressures) / len(positive_pressures)
|
||||
@@ -182,13 +373,62 @@ class PressureFlowSolver:
|
||||
)
|
||||
|
||||
def variable_scale(unknown: AlgebraicUnknown) -> float:
|
||||
return pressure_scale if unknown.role == "effort" else flow_scale
|
||||
return scales.get(unknown.variable, max(abs(unknown.read()), 1.0))
|
||||
|
||||
def equation_scale(equation) -> float:
|
||||
variable_names = [
|
||||
variable.rsplit(".", 1)[-1]
|
||||
for variable in equation.variables
|
||||
]
|
||||
if equation.role == "flow":
|
||||
return scales["f"] if "f" in variable_names else flow_scale
|
||||
if equation.role == "effort":
|
||||
if "x" in variable_names:
|
||||
return scales["x"]
|
||||
if "v" in variable_names:
|
||||
return scales["v"]
|
||||
return pressure_scale
|
||||
return max([scales.get(name, 1.0) for name in variable_names] + [1.0])
|
||||
|
||||
seeded_equations = self.network.pressure_flow_equation_residuals()
|
||||
seeded_scaled = [
|
||||
abs(equation.value / equation_scale(equation))
|
||||
for equation in seeded_equations
|
||||
]
|
||||
seeded_max_scaled_residual = max(seeded_scaled, default=0.0)
|
||||
seeded_unknown_values = [
|
||||
(unknown, unknown.read()) for unknown in self.unknowns
|
||||
]
|
||||
seeded_unknowns_are_feasible = all(
|
||||
isfinite(value)
|
||||
and (unknown.variable != "p" or value >= 1.0)
|
||||
for unknown, value in seeded_unknown_values
|
||||
)
|
||||
if (
|
||||
seeded_unknowns_are_feasible
|
||||
and all(isfinite(value) for value in seeded_scaled)
|
||||
and seeded_max_scaled_residual <= self.residual_tolerance
|
||||
):
|
||||
diagnostics = AlgebraicSolveDiagnostics(
|
||||
success=True,
|
||||
message="Seeded pressure-flow state satisfies the residual tolerance.",
|
||||
evaluations=0,
|
||||
pressure_scale=pressure_scale,
|
||||
flow_scale=flow_scale,
|
||||
max_scaled_residual=seeded_max_scaled_residual,
|
||||
max_raw_residual=max(
|
||||
(abs(item.value) for item in seeded_equations),
|
||||
default=0.0,
|
||||
),
|
||||
)
|
||||
self.last_diagnostics = diagnostics
|
||||
return diagnostics
|
||||
|
||||
x0 = np.asarray(
|
||||
[
|
||||
(
|
||||
unknown.read()
|
||||
if unknown.role != "effort" or unknown.read() > 0.0
|
||||
if unknown.variable != "p" or unknown.read() > 0.0
|
||||
else fallback_pressure
|
||||
)
|
||||
/ variable_scale(unknown)
|
||||
@@ -198,7 +438,7 @@ class PressureFlowSolver:
|
||||
)
|
||||
lower = np.asarray(
|
||||
[
|
||||
1.0 / pressure_scale if unknown.role == "effort" else -np.inf
|
||||
1.0 / pressure_scale if unknown.variable == "p" else -np.inf
|
||||
for unknown in self.unknowns
|
||||
]
|
||||
)
|
||||
@@ -213,8 +453,7 @@ class PressureFlowSolver:
|
||||
equations = self.network.pressure_flow_equation_residuals()
|
||||
return np.asarray(
|
||||
[
|
||||
equation.value
|
||||
/ (pressure_scale if equation.role == "effort" else flow_scale)
|
||||
equation.value / equation_scale(equation)
|
||||
for equation in equations
|
||||
],
|
||||
dtype=float,
|
||||
@@ -234,19 +473,24 @@ class PressureFlowSolver:
|
||||
equations = self.network.pressure_flow_equation_residuals()
|
||||
scaled = [
|
||||
abs(
|
||||
equation.value
|
||||
/ (pressure_scale if equation.role == "effort" else flow_scale)
|
||||
equation.value / equation_scale(equation)
|
||||
)
|
||||
for equation in equations
|
||||
]
|
||||
success = bool(result.success) and max(scaled, default=0.0) <= self.residual_tolerance
|
||||
max_scaled_residual = max(scaled, default=0.0)
|
||||
residuals_converged = (
|
||||
all(isfinite(value) for value in scaled)
|
||||
and max_scaled_residual <= self.residual_tolerance
|
||||
)
|
||||
optimizer_status_is_acceptable = bool(result.success) or int(result.status) == 0
|
||||
success = residuals_converged and optimizer_status_is_acceptable
|
||||
diagnostics = AlgebraicSolveDiagnostics(
|
||||
success=success,
|
||||
message=str(result.message),
|
||||
evaluations=int(result.nfev),
|
||||
pressure_scale=pressure_scale,
|
||||
flow_scale=flow_scale,
|
||||
max_scaled_residual=max(scaled, default=0.0),
|
||||
max_scaled_residual=max_scaled_residual,
|
||||
max_raw_residual=max((abs(item.value) for item in equations), default=0.0),
|
||||
)
|
||||
self.last_diagnostics = diagnostics
|
||||
|
||||
Loaded 100 of 204 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user