Compare commits
176
Commits
3217048a9d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
408b4ecb22 | ||
|
|
e18399c022 | ||
|
|
c19cf77aee | ||
|
|
eb6ea70e19 | ||
|
|
27f9f4add8 | ||
|
|
60b743dd81 | ||
|
|
a8c733883c | ||
|
|
53f8601fec | ||
|
|
f725f038b6 | ||
|
|
684d28752a | ||
|
|
b435daecf2 | ||
|
|
143e8dd309 | ||
|
|
0fa166c8e5 | ||
|
|
16a7eb2d6c | ||
|
|
6bb0591d32 | ||
|
|
cca9d1e883 | ||
|
|
22b35b2945 | ||
|
|
f09dfcf542 | ||
|
|
5332a788f3 | ||
|
|
57b459bc72 | ||
|
|
4e0b9fd8cc | ||
|
|
6a064892e2 | ||
|
|
6572defaa4 | ||
|
|
456c29b3b6 | ||
|
|
caca32a513 | ||
|
|
0f73d5b568 | ||
|
|
6abcc220de | ||
|
|
7671418582 | ||
|
|
40c72422ff | ||
|
|
e9fc855a1c | ||
|
|
09778972a6 | ||
|
|
df1d676131 | ||
|
|
be54070855 | ||
|
|
32f21f08ff | ||
|
|
046aa49814 | ||
|
|
18d9802f03 | ||
|
|
971e8f2336 | ||
|
|
de265cdde6 | ||
|
|
cf249d6c0d | ||
|
|
11093613c5 | ||
|
|
1f4027573f | ||
|
|
503394f3ed | ||
|
|
410ef535e8 | ||
|
|
e7177ab03e | ||
|
|
3e4f466114 | ||
|
|
127ec36a55 | ||
|
|
e87df789b2 | ||
|
|
2ddaaeb56a | ||
|
|
3f51a124d6 | ||
|
|
db4bdb4b70 | ||
|
|
420bafeb4e | ||
|
|
e17a952bee | ||
|
|
a8a68f6dd4 | ||
|
|
71da73ef70 | ||
|
|
cd2fa800b9 | ||
|
|
532fdb4e6b | ||
|
|
bbc88a6bbb | ||
|
|
bd7c8c99e9 | ||
|
|
bf4dc0af3d | ||
|
|
200d4593e8 | ||
|
|
890bf1d483 | ||
|
|
137a4cf7d6 | ||
|
|
920ee336e9 | ||
|
|
fc46c4272a | ||
|
|
8b9f229b80 | ||
|
|
918952f130 | ||
|
|
d67fb251d2 | ||
|
|
e473162d4c | ||
|
|
8a2d2ae190 | ||
|
|
7182f929e8 | ||
|
|
021ea63a2b | ||
|
|
f8af99ace1 | ||
|
|
01b606df30 | ||
|
|
331435e78a | ||
|
|
a219cddc0b | ||
|
|
f7f1078911 | ||
|
|
1602e0dc51 | ||
|
|
84d1675292 | ||
|
|
f0f8f40c7a | ||
|
|
2120901909 | ||
|
|
a0855018d5 | ||
|
|
7c68caed27 | ||
|
|
f1256a121d | ||
|
|
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 | ||
|
|
104d41d294 | ||
|
|
c52266ba11 |
No files matched your search
@@ -0,0 +1,3 @@
|
|||||||
|
*.bat text eol=crlf
|
||||||
|
*.cmd text eol=crlf
|
||||||
|
*.sh text eol=lf
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
name: Solver regression
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- "app/simulation/**"
|
||||||
|
- "tests/**"
|
||||||
|
- "requirements.txt"
|
||||||
|
- "constraints/**"
|
||||||
|
- ".python-version"
|
||||||
|
- "README.md"
|
||||||
|
- ".github/workflows/solver-regression.yml"
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- "app/simulation/**"
|
||||||
|
- "tests/**"
|
||||||
|
- "requirements.txt"
|
||||||
|
- "constraints/**"
|
||||||
|
- ".python-version"
|
||||||
|
- "README.md"
|
||||||
|
- ".github/workflows/solver-regression.yml"
|
||||||
|
schedule:
|
||||||
|
- cron: "17 3 * * 1-6"
|
||||||
|
- cron: "17 3 * * 0"
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
suite:
|
||||||
|
description: Regression tier
|
||||||
|
required: true
|
||||||
|
default: quick
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- quick
|
||||||
|
- historical
|
||||||
|
- main-long
|
||||||
|
case:
|
||||||
|
description: Longest main-model horizon (predecessors run first)
|
||||||
|
required: true
|
||||||
|
default: 0.2s
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- 0.2s
|
||||||
|
- 1s
|
||||||
|
- 5s
|
||||||
|
- 10s
|
||||||
|
lane:
|
||||||
|
description: Output sampling lane
|
||||||
|
required: true
|
||||||
|
default: production
|
||||||
|
type: choice
|
||||||
|
options:
|
||||||
|
- solver-only
|
||||||
|
- production
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: solver-regression-${{ github.ref }}-${{ github.event_name }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quick:
|
||||||
|
if: >-
|
||||||
|
github.event_name == 'push' ||
|
||||||
|
github.event_name == 'pull_request' ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.suite == 'quick')
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version-file: .python-version
|
||||||
|
cache: pip
|
||||||
|
cache-dependency-path: |
|
||||||
|
requirements.txt
|
||||||
|
constraints/python312-direct.txt
|
||||||
|
constraints/python312-linux-x86_64.lock
|
||||||
|
- name: Install hashed Linux release lock
|
||||||
|
run: |
|
||||||
|
python -m pip install \
|
||||||
|
--force-reinstall \
|
||||||
|
-r constraints/python312-linux-x86_64.lock
|
||||||
|
python -m pip check
|
||||||
|
- name: Run solver foundation tests
|
||||||
|
env:
|
||||||
|
SYSTEM_SIMULATION_VERIFY_LOCKED_ENV: "1"
|
||||||
|
run: |
|
||||||
|
python -W error::ResourceWarning -m unittest \
|
||||||
|
tests.test_dependency_constraints \
|
||||||
|
tests.test_benchmark_regression \
|
||||||
|
tests.test_physical_state_v21 \
|
||||||
|
tests.test_test_mql_ame_contract \
|
||||||
|
tests.test_test_mql_8_regression \
|
||||||
|
tests.test_mql_full_branches_regression \
|
||||||
|
tests.test_pressure_flow_causal_execution \
|
||||||
|
tests.test_stream_pressure_block_solver \
|
||||||
|
tests.test_core_solver \
|
||||||
|
tests.test_supported_piston_tangent \
|
||||||
|
tests.test_three_piston_tangent \
|
||||||
|
tests.test_sparse_secant_jacobian \
|
||||||
|
tests.test_generic_jacobian_sparsity \
|
||||||
|
tests.test_generic_system_xml_simulation
|
||||||
|
|
||||||
|
historical-nightly:
|
||||||
|
if: >-
|
||||||
|
(github.event_name == 'schedule' && github.event.schedule == '17 3 * * 1-6') ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.suite == 'historical')
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version-file: .python-version
|
||||||
|
cache: pip
|
||||||
|
cache-dependency-path: |
|
||||||
|
requirements.txt
|
||||||
|
constraints/python312-direct.txt
|
||||||
|
constraints/python312-linux-x86_64.lock
|
||||||
|
- name: Install hashed Linux release lock
|
||||||
|
run: |
|
||||||
|
python -m pip install \
|
||||||
|
--force-reinstall \
|
||||||
|
-r constraints/python312-linux-x86_64.lock
|
||||||
|
python -m pip check
|
||||||
|
- name: Run 0.81 and 2.10 second historical regression
|
||||||
|
run: |
|
||||||
|
mkdir -p artifacts
|
||||||
|
python -m app.simulation.benchmark_regression \
|
||||||
|
--manifest tests/baselines/simulation/test_mql_full_branches/manifest.json \
|
||||||
|
--lane production \
|
||||||
|
--output artifacts/test-mql-full-branches.json
|
||||||
|
- if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: historical-solver-regression
|
||||||
|
path: artifacts/*.json
|
||||||
|
if-no-files-found: warn
|
||||||
|
|
||||||
|
main-periodic:
|
||||||
|
if: >-
|
||||||
|
(github.event_name == 'schedule' && github.event.schedule == '17 3 * * 0') ||
|
||||||
|
(github.event_name == 'workflow_dispatch' && inputs.suite == 'main-long')
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 180
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version-file: .python-version
|
||||||
|
cache: pip
|
||||||
|
cache-dependency-path: |
|
||||||
|
requirements.txt
|
||||||
|
constraints/python312-direct.txt
|
||||||
|
constraints/python312-linux-x86_64.lock
|
||||||
|
- name: Install hashed Linux release lock
|
||||||
|
run: |
|
||||||
|
python -m pip install \
|
||||||
|
--force-reinstall \
|
||||||
|
-r constraints/python312-linux-x86_64.lock
|
||||||
|
python -m pip check
|
||||||
|
- name: Run bounded progressive main-model regression
|
||||||
|
env:
|
||||||
|
REQUESTED_CASE: ${{ github.event_name == 'workflow_dispatch' && inputs.case || '10s' }}
|
||||||
|
REQUESTED_LANE: ${{ github.event_name == 'workflow_dispatch' && inputs.lane || 'production' }}
|
||||||
|
run: |
|
||||||
|
mkdir -p artifacts
|
||||||
|
python -m app.simulation.benchmark_regression \
|
||||||
|
--manifest tests/baselines/simulation/test_mql_8/manifest.json \
|
||||||
|
--lane "$REQUESTED_LANE" \
|
||||||
|
--case "$REQUESTED_CASE" \
|
||||||
|
--output artifacts/test-mql-8-progressive.json
|
||||||
|
- if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: main-model-progressive-regression
|
||||||
|
path: artifacts/*.json
|
||||||
|
if-no-files-found: warn
|
||||||
+7
-1
@@ -12,11 +12,17 @@ htmlcov/
|
|||||||
# Local virtual environments
|
# Local virtual environments
|
||||||
.venv/
|
.venv/
|
||||||
.venv-win/
|
.venv-win/
|
||||||
PythonModels/runs/
|
|
||||||
|
# Local Linux toolchain (downloaded for the startup scripts)
|
||||||
|
.tools/node-*-linux-x64/
|
||||||
|
|
||||||
app/data/
|
app/data/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
frontend/.vite/
|
frontend/.vite/
|
||||||
|
frontend/test-results/
|
||||||
|
frontend/playwright-report/
|
||||||
|
frontend/blob-report/
|
||||||
venv/
|
venv/
|
||||||
env/
|
env/
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
3.12.3
|
||||||
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 完全一致”之类结论。
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
"""Python port scaffold for the Modelica-based pressurization system."""
|
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from PythonModels.core.base import DynamicComponent
|
|
||||||
from PythonModels.core.medium import IdealGasMedium, ThermodynamicProperties
|
|
||||||
from PythonModels.core.ports import PortState
|
|
||||||
from PythonModels.core.state import VolumeState
|
|
||||||
|
|
||||||
|
|
||||||
class Cylinder(DynamicComponent):
|
|
||||||
"""Python port of ModelicaModels.Mycylinder."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
medium: IdealGasMedium,
|
|
||||||
V: float = 0.01,
|
|
||||||
p0: float = 35e6,
|
|
||||||
T0: float = 300.0,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(name=name)
|
|
||||||
self.medium = medium
|
|
||||||
self.V = V
|
|
||||||
m0 = p0 * V / (medium.R_gas * T0)
|
|
||||||
U0 = m0 * medium.specific_internal_energy(T0)
|
|
||||||
self.state = VolumeState(m=m0, U=U0)
|
|
||||||
self.port_b = 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:
|
|
||||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.V)
|
|
||||||
self.port_b.p = props.p
|
|
||||||
self.port_b.h_outflow = props.h
|
|
||||||
return props
|
|
||||||
|
|
||||||
def derivatives_from_connection(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
connected_h: float,
|
|
||||||
port_m_flow: float,
|
|
||||||
internal_h: float,
|
|
||||||
) -> VolumeState:
|
|
||||||
inlet_h = self.connection_inlet_enthalpy(
|
|
||||||
port_m_flow=port_m_flow,
|
|
||||||
connected_h=connected_h,
|
|
||||||
internal_h=internal_h,
|
|
||||||
)
|
|
||||||
return self.derivatives(inlet_h, port_m_flow)
|
|
||||||
|
|
||||||
def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState:
|
|
||||||
return VolumeState(m=m_flow, U=m_flow * inlet_h)
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from math import sqrt
|
|
||||||
|
|
||||||
from PythonModels.core.base import AlgebraicComponent
|
|
||||||
from PythonModels.core.ports import PortState
|
|
||||||
|
|
||||||
|
|
||||||
class Orifice(AlgebraicComponent):
|
|
||||||
"""Python port of ModelicaModels.Myorifice."""
|
|
||||||
|
|
||||||
def __init__(self, name: str, opening: float = 1.0, K: float = 1e-7) -> None:
|
|
||||||
super().__init__(name=name)
|
|
||||||
self.opening = opening
|
|
||||||
self.K = K
|
|
||||||
self.port_a = PortState()
|
|
||||||
self.port_b = PortState()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def K_eff(self) -> float:
|
|
||||||
return self.K * max(self.opening, 0.001)
|
|
||||||
|
|
||||||
def mass_flow(self, p_a: float, p_b: float) -> float:
|
|
||||||
dp = p_a - p_b
|
|
||||||
if dp == 0.0:
|
|
||||||
return 0.0
|
|
||||||
return self.K_eff * sqrt(abs(dp)) * (1.0 if dp > 0.0 else -1.0)
|
|
||||||
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from PythonModels.core.base import DynamicComponent
|
|
||||||
from PythonModels.core.medium import IdealGasMedium, ThermodynamicProperties
|
|
||||||
from PythonModels.core.ports import PortState
|
|
||||||
from PythonModels.core.state import VolumeState
|
|
||||||
|
|
||||||
|
|
||||||
class Pipe(DynamicComponent):
|
|
||||||
"""Python port of ModelicaModels.Mypipe."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
medium: IdealGasMedium,
|
|
||||||
L: float = 5.0,
|
|
||||||
D: float = 0.02,
|
|
||||||
lambda_darcy: float = 0.02,
|
|
||||||
p0: float = 1e5,
|
|
||||||
T0: float = 300.0,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(name=name)
|
|
||||||
self.medium = medium
|
|
||||||
self.L = L
|
|
||||||
self.D = D
|
|
||||||
self.lambda_darcy = lambda_darcy
|
|
||||||
self.area = 3.141592653589793 * D * D / 4.0
|
|
||||||
self.V = self.area * L
|
|
||||||
m0 = p0 * self.V / (medium.R_gas * T0)
|
|
||||||
U0 = m0 * medium.specific_internal_energy(T0)
|
|
||||||
self.state = VolumeState(m=m0, U=U0)
|
|
||||||
self.port_a = PortState()
|
|
||||||
self.port_b = 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:
|
|
||||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.V)
|
|
||||||
self.port_b.p = props.p
|
|
||||||
self.port_a.h_outflow = props.h
|
|
||||||
self.port_b.h_outflow = props.h
|
|
||||||
return props
|
|
||||||
|
|
||||||
def inlet_pressure(self, m_flow_a: float, rho: float, core_pressure: float) -> float:
|
|
||||||
resistance = self.lambda_darcy * (self.L / self.D)
|
|
||||||
dynamic_term = m_flow_a * abs(m_flow_a) / (2.0 * rho * self.area * self.area)
|
|
||||||
return core_pressure + resistance * dynamic_term
|
|
||||||
|
|
||||||
def port_a_inlet_enthalpy(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
port_a_m_flow: float,
|
|
||||||
connected_h: float,
|
|
||||||
internal_h: float,
|
|
||||||
) -> float:
|
|
||||||
return self.connection_inlet_enthalpy(
|
|
||||||
port_m_flow=port_a_m_flow,
|
|
||||||
connected_h=connected_h,
|
|
||||||
internal_h=internal_h,
|
|
||||||
)
|
|
||||||
|
|
||||||
def port_b_inlet_enthalpy(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
port_b_m_flow: float,
|
|
||||||
connected_h: float,
|
|
||||||
internal_h: float,
|
|
||||||
) -> float:
|
|
||||||
return self.connection_inlet_enthalpy(
|
|
||||||
port_m_flow=port_b_m_flow,
|
|
||||||
connected_h=connected_h,
|
|
||||||
internal_h=internal_h,
|
|
||||||
)
|
|
||||||
|
|
||||||
def connection_inlet_enthalpies(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
port_a_m_flow: float,
|
|
||||||
connected_h_a: float,
|
|
||||||
port_b_m_flow: float,
|
|
||||||
connected_h_b: float,
|
|
||||||
internal_h: float,
|
|
||||||
) -> tuple[float, float]:
|
|
||||||
return (
|
|
||||||
self.port_a_inlet_enthalpy(
|
|
||||||
port_a_m_flow=port_a_m_flow,
|
|
||||||
connected_h=connected_h_a,
|
|
||||||
internal_h=internal_h,
|
|
||||||
),
|
|
||||||
self.port_b_inlet_enthalpy(
|
|
||||||
port_b_m_flow=port_b_m_flow,
|
|
||||||
connected_h=connected_h_b,
|
|
||||||
internal_h=internal_h,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def derivatives_from_connections(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
port_a_m_flow: float,
|
|
||||||
connected_h_a: float,
|
|
||||||
port_b_m_flow: float,
|
|
||||||
connected_h_b: float,
|
|
||||||
internal_h: float,
|
|
||||||
) -> VolumeState:
|
|
||||||
inlet_h_a, inlet_h_b = self.connection_inlet_enthalpies(
|
|
||||||
port_a_m_flow=port_a_m_flow,
|
|
||||||
connected_h_a=connected_h_a,
|
|
||||||
port_b_m_flow=port_b_m_flow,
|
|
||||||
connected_h_b=connected_h_b,
|
|
||||||
internal_h=internal_h,
|
|
||||||
)
|
|
||||||
return self.derivatives(
|
|
||||||
inlet_h_a=inlet_h_a,
|
|
||||||
inlet_h_b=inlet_h_b,
|
|
||||||
m_flow_a=port_a_m_flow,
|
|
||||||
m_flow_b=port_b_m_flow,
|
|
||||||
)
|
|
||||||
|
|
||||||
def derivatives(
|
|
||||||
self,
|
|
||||||
inlet_h_a: float,
|
|
||||||
inlet_h_b: float,
|
|
||||||
m_flow_a: float,
|
|
||||||
m_flow_b: float,
|
|
||||||
) -> VolumeState:
|
|
||||||
dm_dt = m_flow_a + m_flow_b
|
|
||||||
dU_dt = m_flow_a * inlet_h_a + m_flow_b * inlet_h_b
|
|
||||||
return VolumeState(m=dm_dt, U=dU_dt)
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from PythonModels.core.base import DynamicComponent
|
|
||||||
from PythonModels.core.medium import IdealGasMedium, ThermodynamicProperties
|
|
||||||
from PythonModels.core.ports import PortState
|
|
||||||
from PythonModels.core.state import VolumeState
|
|
||||||
|
|
||||||
|
|
||||||
class Tank(DynamicComponent):
|
|
||||||
"""Python port of ModelicaModels.Mytank."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
name: str,
|
|
||||||
medium: IdealGasMedium,
|
|
||||||
V: float = 0.1,
|
|
||||||
p0: float = 1e5,
|
|
||||||
T0: float = 300.0,
|
|
||||||
) -> None:
|
|
||||||
super().__init__(name=name)
|
|
||||||
self.medium = medium
|
|
||||||
self.V = V
|
|
||||||
m0 = p0 * V / (medium.R_gas * T0)
|
|
||||||
U0 = m0 * medium.specific_internal_energy(T0)
|
|
||||||
self.state = VolumeState(m=m0, U=U0)
|
|
||||||
self.port_a = 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:
|
|
||||||
props = self.medium.properties_from_mU(self.state.m, self.state.U, self.V)
|
|
||||||
self.port_a.p = props.p
|
|
||||||
self.port_a.h_outflow = props.h
|
|
||||||
return props
|
|
||||||
|
|
||||||
def derivatives_from_connection(
|
|
||||||
self,
|
|
||||||
*,
|
|
||||||
connected_h: float,
|
|
||||||
port_m_flow: float,
|
|
||||||
internal_h: float,
|
|
||||||
) -> VolumeState:
|
|
||||||
inlet_h = self.connection_inlet_enthalpy(
|
|
||||||
port_m_flow=port_m_flow,
|
|
||||||
connected_h=connected_h,
|
|
||||||
internal_h=internal_h,
|
|
||||||
)
|
|
||||||
return self.derivatives(inlet_h, port_m_flow)
|
|
||||||
|
|
||||||
def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState:
|
|
||||||
return VolumeState(m=m_flow, U=m_flow * inlet_h)
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
|
||||||
class Component(ABC):
|
|
||||||
def __init__(self, name: str) -> None:
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
|
|
||||||
class DynamicComponent(Component):
|
|
||||||
state_size = 2
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def actual_stream_enthalpy(
|
|
||||||
port_m_flow: float,
|
|
||||||
connected_h: float,
|
|
||||||
internal_h: float,
|
|
||||||
) -> float:
|
|
||||||
"""Approximate `actualStream(port.h_outflow)` for a mixed control volume port."""
|
|
||||||
|
|
||||||
return connected_h if port_m_flow > 0.0 else internal_h
|
|
||||||
|
|
||||||
def connection_inlet_enthalpy(
|
|
||||||
self,
|
|
||||||
port_m_flow: float,
|
|
||||||
connected_h: float,
|
|
||||||
internal_h: float,
|
|
||||||
) -> float:
|
|
||||||
"""Resolve the enthalpy convected into this control volume through one port."""
|
|
||||||
|
|
||||||
return self.actual_stream_enthalpy(
|
|
||||||
port_m_flow=port_m_flow,
|
|
||||||
connected_h=connected_h,
|
|
||||||
internal_h=internal_h,
|
|
||||||
)
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_state_vector(self) -> list[float]:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def set_state_vector(self, values: list[float]) -> None:
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
|
|
||||||
class AlgebraicComponent(Component):
|
|
||||||
"""Stateless element described by algebraic constraints only."""
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ThermodynamicProperties:
|
|
||||||
p: float
|
|
||||||
T: float
|
|
||||||
rho: float
|
|
||||||
u: float
|
|
||||||
h: float
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class IdealGasMedium:
|
|
||||||
"""Temperature-dependent ideal-gas air approximation.
|
|
||||||
|
|
||||||
This is still not a strict clone of `Modelica.Media.Air.SimpleAir`.
|
|
||||||
The small linear `cp(T)` term is kept configurable for calibration, but the
|
|
||||||
current default is calibrated against the committed Testmodel baseline and
|
|
||||||
therefore falls back to the constant-heat-capacity limit.
|
|
||||||
"""
|
|
||||||
|
|
||||||
name: str = "SimpleAirApprox"
|
|
||||||
R_gas: float = 287.0
|
|
||||||
cp_ref: float = 1005.0
|
|
||||||
T_ref: float = 300.0
|
|
||||||
cp_slope: float = 0.0
|
|
||||||
|
|
||||||
@property
|
|
||||||
def cv(self) -> float:
|
|
||||||
return self.cv_at_temperature(self.T_ref)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def gamma(self) -> float:
|
|
||||||
return self.cp_at_temperature(self.T_ref) / self.cv
|
|
||||||
|
|
||||||
def cp_at_temperature(self, T: float) -> float:
|
|
||||||
return self.cp_ref + self.cp_slope * (T - self.T_ref)
|
|
||||||
|
|
||||||
def cv_at_temperature(self, T: float) -> float:
|
|
||||||
return self.cp_at_temperature(T) - self.R_gas
|
|
||||||
|
|
||||||
def density(self, p: float, T: float) -> float:
|
|
||||||
return p / (self.R_gas * T)
|
|
||||||
|
|
||||||
def specific_internal_energy(self, T: float) -> float:
|
|
||||||
delta_T = T - self.T_ref
|
|
||||||
return (
|
|
||||||
self.cv * self.T_ref
|
|
||||||
+ self.cv * delta_T
|
|
||||||
+ 0.5 * self.cp_slope * delta_T * delta_T
|
|
||||||
)
|
|
||||||
|
|
||||||
def specific_enthalpy(self, T: float) -> float:
|
|
||||||
delta_T = T - self.T_ref
|
|
||||||
return (
|
|
||||||
self.cp_ref * self.T_ref
|
|
||||||
+ self.cp_ref * delta_T
|
|
||||||
+ 0.5 * self.cp_slope * delta_T * delta_T
|
|
||||||
)
|
|
||||||
|
|
||||||
def temperature_from_internal_energy(self, u: float) -> float:
|
|
||||||
reference_internal_energy = self.cv * self.T_ref
|
|
||||||
delta_u = u - reference_internal_energy
|
|
||||||
|
|
||||||
if abs(self.cp_slope) <= 1e-15:
|
|
||||||
return self.T_ref + delta_u / self.cv
|
|
||||||
|
|
||||||
a = 0.5 * self.cp_slope
|
|
||||||
b = self.cv
|
|
||||||
c = -delta_u
|
|
||||||
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.")
|
|
||||||
return self.temperature_from_internal_energy(U / m)
|
|
||||||
|
|
||||||
def pressure(self, m: float, T: float, V: float) -> float:
|
|
||||||
if V <= 0.0:
|
|
||||||
raise ValueError("Volume must stay positive.")
|
|
||||||
return m * self.R_gas * T / V
|
|
||||||
|
|
||||||
def properties_from_mU(self, m: float, U: float, V: float) -> ThermodynamicProperties:
|
|
||||||
T = self.temperature_from_mass_internal_energy(m, U)
|
|
||||||
p = self.pressure(m, T, V)
|
|
||||||
rho = m / V
|
|
||||||
u = U / m
|
|
||||||
h = self.specific_enthalpy(T)
|
|
||||||
return ThermodynamicProperties(p=p, T=T, rho=rho, u=u, h=h)
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PortState:
|
|
||||||
"""Python-side analogue of a Modelica fluid port."""
|
|
||||||
|
|
||||||
p: float = 0.0
|
|
||||||
m_flow: float = 0.0
|
|
||||||
h_outflow: float = 0.0
|
|
||||||
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Callable
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class SolveIVPConfig:
|
|
||||||
t_start: float = 0.0
|
|
||||||
t_stop: float = 20.0
|
|
||||||
method: str = "BDF"
|
|
||||||
rtol: float = 1e-6
|
|
||||||
atol: float = 1e-8
|
|
||||||
max_step: float = 1e-3
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ODESolution:
|
|
||||||
t: list[float]
|
|
||||||
y: list[list[float]]
|
|
||||||
success: bool
|
|
||||||
message: str
|
|
||||||
|
|
||||||
|
|
||||||
def _vector_add(a: list[float], b: list[float], scale: float = 1.0) -> list[float]:
|
|
||||||
return [x + scale * y for x, y in zip(a, b)]
|
|
||||||
|
|
||||||
|
|
||||||
def _runge_kutta_4(
|
|
||||||
rhs: Callable[[float, list[float]], list[float]],
|
|
||||||
initial_state: list[float],
|
|
||||||
config: SolveIVPConfig,
|
|
||||||
t_eval: list[float] | None,
|
|
||||||
) -> ODESolution:
|
|
||||||
if t_eval is None:
|
|
||||||
point_count = max(
|
|
||||||
2,
|
|
||||||
int((config.t_stop - config.t_start) / max(config.max_step, 1e-6)) + 1,
|
|
||||||
)
|
|
||||||
step = (config.t_stop - config.t_start) / (point_count - 1)
|
|
||||||
t_eval = [config.t_start + index * step for index in range(point_count)]
|
|
||||||
|
|
||||||
state = list(initial_state)
|
|
||||||
states = [[value] for value in state]
|
|
||||||
times = [float(t_eval[0])]
|
|
||||||
current_time = float(t_eval[0])
|
|
||||||
|
|
||||||
for target_time in t_eval[1:]:
|
|
||||||
while current_time < target_time - 1e-15:
|
|
||||||
dt = min(config.max_step, target_time - current_time)
|
|
||||||
k1 = rhs(current_time, state)
|
|
||||||
k2 = rhs(current_time + 0.5 * dt, _vector_add(state, k1, 0.5 * dt))
|
|
||||||
k3 = rhs(current_time + 0.5 * dt, _vector_add(state, k2, 0.5 * dt))
|
|
||||||
k4 = rhs(current_time + dt, _vector_add(state, k3, dt))
|
|
||||||
state = [
|
|
||||||
value + (dt / 6.0) * (a + 2.0 * b + 2.0 * c + d)
|
|
||||||
for value, a, b, c, d in zip(state, k1, k2, k3, k4)
|
|
||||||
]
|
|
||||||
current_time += dt
|
|
||||||
|
|
||||||
times.append(float(target_time))
|
|
||||||
for index, value in enumerate(state):
|
|
||||||
states[index].append(value)
|
|
||||||
|
|
||||||
return ODESolution(
|
|
||||||
t=times,
|
|
||||||
y=states,
|
|
||||||
success=True,
|
|
||||||
message="Integrated with built-in RK4 fallback because SciPy is unavailable.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def integrate_ode(
|
|
||||||
rhs: Callable[[float, list[float]], list[float]],
|
|
||||||
initial_state: list[float],
|
|
||||||
config: SolveIVPConfig,
|
|
||||||
t_eval: list[float] | None = None,
|
|
||||||
):
|
|
||||||
"""Thin wrapper around scipy.integrate.solve_ivp with a pure-Python fallback."""
|
|
||||||
|
|
||||||
if abs(config.t_stop - config.t_start) <= 1e-15:
|
|
||||||
return ODESolution(
|
|
||||||
t=[float(config.t_start)],
|
|
||||||
y=[[value] for value in initial_state],
|
|
||||||
success=True,
|
|
||||||
message="Skipped integration because t_start equals t_stop.",
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
from scipy.integrate import solve_ivp
|
|
||||||
except ImportError:
|
|
||||||
return _runge_kutta_4(rhs, initial_state, config, t_eval)
|
|
||||||
|
|
||||||
return solve_ivp(
|
|
||||||
fun=rhs,
|
|
||||||
t_span=(config.t_start, config.t_stop),
|
|
||||||
y0=initial_state,
|
|
||||||
method=config.method,
|
|
||||||
rtol=config.rtol,
|
|
||||||
atol=config.atol,
|
|
||||||
t_eval=t_eval,
|
|
||||||
)
|
|
||||||
@@ -1,2 +1,114 @@
|
|||||||
# SystemSimulationApp
|
# SystemSimulationApp
|
||||||
|
|
||||||
|
ReactFlow 系统建模与 `app.simulation` 仿真后端。
|
||||||
|
|
||||||
|
## 开发环境准备
|
||||||
|
|
||||||
|
后端统一使用 Python 3.12;仓库根目录的 `.python-version` 记录本轮参考补丁版本
|
||||||
|
`3.12.3`。`requirements.txt` 保留支持范围,
|
||||||
|
`constraints/python312-direct.txt` 固定跨平台开发环境的直接依赖参考版本;
|
||||||
|
`constraints/python312-linux-x86_64.lock` 则完整固定发布与 CI 所用的 Linux x86_64
|
||||||
|
wheel、全部传递依赖及其 SHA-256。
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
py -3.12 -m venv .venv-win
|
||||||
|
.\.venv-win\Scripts\python.exe -m pip install `
|
||||||
|
-r requirements.txt `
|
||||||
|
-c constraints/python312-direct.txt
|
||||||
|
.\.venv-win\Scripts\python.exe -m pip check
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3.12 -m venv .venv
|
||||||
|
./.venv/bin/python -m pip install \
|
||||||
|
-r constraints/python312-linux-x86_64.lock
|
||||||
|
./.venv/bin/python -m pip check
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux 发布锁仅适用于兼容 manylinux_2_28 的 Linux x86_64 和 CPython 3.12。它启用
|
||||||
|
`--only-binary=:all:` 与 `--require-hashes`,因此不会静默改用源码包或未审计 wheel;
|
||||||
|
CI 和正式性能复测必须直接以 `-r` 安装该文件。Windows 或其他平台的开发环境继续
|
||||||
|
使用 `requirements.txt` 加 `constraints/python312-direct.txt`。若要测试
|
||||||
|
`requirements.txt` 声明的兼容范围,可显式省略约束,但这类结果不应与锁定环境的
|
||||||
|
性能数据直接比较。
|
||||||
|
|
||||||
|
升级参考版本时,应在干净的 Python 3.12 Linux x86_64 虚拟环境中解析范围文件,
|
||||||
|
仅下载兼容 wheel,逐个记录 wheel 的 SHA-256,再从空环境安装发布锁并运行
|
||||||
|
`pip check`、依赖契约测试与后端测试。不能只复制 `pip freeze`,因为它既不证明
|
||||||
|
依赖来源,也不校验安装产物。
|
||||||
|
|
||||||
|
前端使用 Vite 8,需要 Node.js `20.19+` 或 `22.12+`。首次启动前安装前端依赖。
|
||||||
|
|
||||||
|
Windows(PowerShell,使用仓库内的便携 Node.js):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$nodeDir = Get-ChildItem .tools -Directory -Filter "node-*-win-x64" |
|
||||||
|
Where-Object { (Test-Path "$($_.FullName)\node.exe") -and (Test-Path "$($_.FullName)\npm.cmd") } |
|
||||||
|
Select-Object -First 1
|
||||||
|
& "$($nodeDir.FullName)\npm.cmd" --prefix frontend ci
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
Windows 启动脚本会自动使用 `.tools/node-*-win-x64` 下兼容的便携 Node.js;Linux 启动脚本优先使用 `.tools/node-*-linux-x64` 下兼容的运行时(如果存在),否则使用 `PATH` 中的 `node` 和 `npm`。`start-all.sh` 需要 Bash 4.3 或更高版本。
|
||||||
|
|
||||||
|
## 启动项目
|
||||||
|
|
||||||
|
脚本统一存放在 `bat/` 目录。三个入口分别用于同时启动、只启动后端、只启动前端。
|
||||||
|
|
||||||
|
Windows:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
bat\start-all.bat
|
||||||
|
bat\start-backend.bat
|
||||||
|
bat\start-reactflow.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./bat/start-all.sh
|
||||||
|
./bat/start-backend.sh
|
||||||
|
./bat/start-reactflow.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
后端地址为 `http://127.0.0.1:8000`,前端地址为 `http://127.0.0.1:5173`。Windows 的 `start-all.bat` 会分别打开两个命令行窗口;Linux 的 `start-all.sh` 会在同一终端管理两个进程,按 `Ctrl+C` 会同时停止它们。
|
||||||
|
|
||||||
|
## 后端接口
|
||||||
|
|
||||||
|
- `GET /api/components/catalog`:返回组件库与模型版本、分类、图标键、端口布局和参数契约,供 ReactFlow 启动时自动加载。
|
||||||
|
- `POST /api/reactflow/system-xml`:导出精简的 System XML v3。
|
||||||
|
- `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 v3,返回 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/simulation-results/csv`:校验结构化结果快照并导出 UTF-8 CSV 文件。
|
||||||
|
|
||||||
|
气动端口的后端契约采用 `p` 势变量相等、`m_flow` 流变量代数和为零、`h_outflow` 按 stream 规则混合。所有组件统一规定 `m_flow > 0` 表示流入组件,物理连接的端点顺序不表示流向。
|
||||||
|
|
||||||
|
当前网络层可按端口域处理气动压力-流量残差与 stream 焓、标量信号传播,以及一维机械 `x/v` 等值和 `f` 平衡,并使用 SciPy 完成非线性代数闭合和时间积分。XML 通用仿真当前采用半显式 ODE/代数 MVP:气瓶和贮箱作为储能元件,孔板及 XML 管段作为阻性元件,三通作为等压零结点,同时支持已登记的信号和机械基础件。它不是完整 DAE 或事件求解器,也不等价于严格 Modelica.Fluid 实现。
|
||||||
|
|
||||||
|
XML 解析依赖 `lxml` 执行本地 XSD 校验,该依赖已包含在 `requirements.txt` 中。
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
- [开发文档索引](docs/README.md)
|
||||||
|
- [后端接口版本与定义规范 v1](docs/standard/backend-interface-version-spec-v1.md)
|
||||||
|
- [组件模型建模规范 v1](docs/standard/component-model-authoring-spec-v1.md)
|
||||||
|
- [组件库分类、发现与读取规范 v1](docs/standard/component-library-spec-v1.md)
|
||||||
|
- [组件目录 JSON Schema v1](schemas/component-catalog-v1.schema.json)
|
||||||
|
- [System XML v3 协议(当前规范)](docs/standard/system-xml-v3.md)
|
||||||
|
- [System XML v3 XSD(当前 Schema)](schemas/system-simulation-v3.xsd)
|
||||||
+1358
-46
File diff suppressed because it is too large.
Load diff
@@ -1,41 +1,94 @@
|
|||||||
# PythonModels
|
# 仿真后端
|
||||||
|
|
||||||
`PythonModels` 用于承接 `ModelicaModels` 的 Python 平台移植。
|
`app.simulation` 是 SystemSimulationApp 的仿真子包,用于承接模型定义、系统装配、数值求解和结果导出。
|
||||||
|
|
||||||
目标不是把 `.mo` 文件逐行翻译成 Python,而是建立一个可运行、可对比、可逐步逼近 `OpenModelica` 行为的 Python 仿真框架。
|
目标不是逐行翻译源模型,而是建立可运行、可测试、可导出,并能与 OpenModelica 或 AMESim baseline 对比的 Python 仿真框架。
|
||||||
|
|
||||||
当前状态不是“只有骨架”,而是“`Testmodel` 已有一版可运行的 ODE 近似实现,并具备基础结果导出与对比能力”。
|
当前包含两条模型线:`Testmodel` 已有可运行的 ODE 近似和 OpenModelica 对比能力;`test_mql` 已形成 132 状态气动机械总闭包,正在按 AMESim baseline 做数值校准。
|
||||||
|
|
||||||
## 当前目录
|
## 当前目录
|
||||||
|
|
||||||
- `core/`: 通用基础设施
|
- `core/`: 元件基类、端口、状态、介质、方程和元数据协议。
|
||||||
包含组件基类、状态与端口数据结构、介质模型、网络装配、积分入口。
|
- `solvers/`: ODE、压力流量代数方程和 stream 求解。
|
||||||
- `components/`: 元件级 Python 实现
|
- `components/experimental/`: 用于验证元件开发规范的临时组件库。
|
||||||
目前有 `Cylinder`、`Tank`、`Pipe`、`Orifice`、`Tee` 五类元件。
|
- `components/experimental/storage/`: 气瓶和贮箱等储能元件。
|
||||||
- `systems/`: 系统级装配与闭合
|
- `components/experimental/flow/`: 对外注册的阻性管道和孔板等流动元件。
|
||||||
当前只有 `TestModelSystem`,对应 `ModelicaModels/Testmodel.mo`。
|
- `components/experimental/junctions/`: 三通等连接节点。
|
||||||
- `reporting/`: 结果导出与对比
|
- `components/amesim/`: AMESim 气动、信号和机械组件原语。
|
||||||
当前承接主变量 CSV、温度 CSV/SVG、Python 对 OpenModelica 的对比表与误差摘要导出。
|
- `systems/`: 通用仿真网络与 XML 驱动系统装配。
|
||||||
- `scripts/`: 运行脚本
|
- `examples/testmodel/`: 固定 TestModel、专用闭合逻辑和基线运行入口。
|
||||||
当前入口是 `run_testmodel.py`。
|
- `examples/test_mql/`: AMESim `test_mql` 的系统装配、校准原语、诊断和运行入口。
|
||||||
- `baselines/`: 提交进仓库的稳定基线
|
- `reporting/`: CSV、SVG、运行报告、Modelica 对比结果、AMESim 结果读取和诊断报告导出。
|
||||||
当前承接 Python 主变量基线和 Python 对 Modelica 的误差摘要基线。
|
- `registry.py`: 从已启用库清单受控发现、校验和实例化组件。
|
||||||
- `runs/`: 每次实际运行的默认输出目录
|
- `paths.py`: 项目、运行产物、基准和 Modelica 参考结果路径。
|
||||||
当前脚本默认会在这里创建带时间戳的子目录,用来放这次运行生成的产物。
|
|
||||||
|
稳定基准存放在 `tests/baselines/simulation/`,实际运行产物默认写入被 Git 忽略的
|
||||||
|
`app/data/simulation-runs/`。新增或修改元件时,先阅读 `components/example.md`。
|
||||||
|
需要把运行产物写到仓库外时,可以设置 `SIMULATIONAPP_DATA_DIR` 环境变量。
|
||||||
|
|
||||||
|
FastAPI 的 `GET /api/components/catalog` 会把注册表转换成前端组件目录。ReactFlow
|
||||||
|
启动时自动读取该接口;接口暂时不可用时使用内置的同结构兜底定义。
|
||||||
|
|
||||||
|
临时组件库的声明入口是 `components/experimental/library.py`,AMESim 第一版
|
||||||
|
公开临时库入口是 `components/amesim/library.py`。公开模型必须在
|
||||||
|
模型类中声明 `MODEL_TYPE / MODEL_VERSION / PORTS / PARAMETERS /
|
||||||
|
RESULT_VARIABLES / DISPLAY / create()`,再把类路径加入库清单。完整规范参见
|
||||||
|
[`组件模型建模规范 v1`](../../docs/standard/component-model-authoring-spec-v1.md)和
|
||||||
|
[`组件库分类、发现与读取规范 v1`](../../docs/standard/component-library-spec-v1.md)。
|
||||||
|
|
||||||
当前关键文件:
|
当前关键文件:
|
||||||
|
|
||||||
- `core/medium.py`: 理想气体近似介质 `IdealGasMedium`
|
- `core/medium.py`: 气体介质协议 `GasMedium` 与通用理想气体实现 `IdealGasMedium`
|
||||||
- `core/medium.py`: 温度相关的空气近似介质 `IdealGasMedium`
|
- `components/amesim/media/`: AMESim 零端口介质物性定义元件;具体类型确定介质,`property_model` 下拉参数选择计算方法,当前提供空气理想气体和氦气 Peng-Robinson
|
||||||
- `core/network.py`: `SimulationNetwork`,负责组件注册、连接拓扑和状态向量拼装
|
- `components/amesim/gases.py`: AMESim `gi` 介质物性实例注册表;`gi=0` 固定为空气(理想气体,内置默认),`gi=1..99` 引用画布中的显式介质定义
|
||||||
- `core/solver.py`: `integrate_ode()`,优先走 `SciPy solve_ivp`,缺依赖时回退到内置 RK4,并支持 `t_start == t_stop` 的零时长返回
|
- `core/peng_robinson.py`: `test_mql` 与公开氦气介质共用的 Peng-Robinson 状态方程
|
||||||
- `components/pipe.py`: 单阻容管道近似,入口压降 + 出口直连内容腔
|
- `performance.py`: 默认关闭、按单次仿真隔离的阶段与物性性能埋点
|
||||||
- `components/tee.py`: 三通的最小 stream 混合 helper
|
- `benchmark_performance.py`: System XML 主求解路径的可重复命令行基准工具
|
||||||
- `systems/testmodel.py`: `Testmodel` 的系统装配壳与外部运行入口
|
- `systems/network.py`: `SimulationNetwork`,负责组件注册、连接拓扑和状态向量拼装
|
||||||
- `systems/testmodel_closure.py`: `Testmodel` 当前专用的闭合、初始化投影、分支求解与端口回写
|
- `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/对比摘要导出
|
- `reporting/testmodel_outputs.py`: `Testmodel` 的 CSV/SVG/对比摘要导出
|
||||||
- `scripts/run_testmodel.py`: 基线运行与程序化执行入口
|
- `reporting/amesim_results.py`: AMESim 结果读取入口
|
||||||
- `tests/test_pythonmodels_regression.py`: 当前 Python 基线回归测试
|
- `examples/test_mql/run_full_state_comparison.py`: `test_mql` 短时域 AMESim comparison 和诊断入口
|
||||||
|
- `examples/test_mql/run.py`: `test_mql` 结构运行与程序化执行入口
|
||||||
|
- `tests/`: 当前组件契约、XML、通用系统、AMESim 迁移和结果导出测试
|
||||||
|
|
||||||
|
## 可选性能诊断
|
||||||
|
|
||||||
|
`SIMULATIONAPP_PROFILE` 支持 `off`(默认)、`standard` 和 `audit`。`standard`
|
||||||
|
只统计低频的大阶段;`audit` 才展开 RHS、代数闭合、stream 和物性调用,开销也
|
||||||
|
明显更高。最终优化收益必须在 `off` 下复测。
|
||||||
|
|
||||||
|
Peng–Robinson 氦气的高开销物性默认使用一次仿真内独立的精确 LRU 缓存;不同
|
||||||
|
仿真任务不会共享条目,仿真结束后自动释放。可在启动进程前设置
|
||||||
|
`SIMULATIONAPP_PROPERTY_CACHE=off` 做数值和性能 A/B,正常运行保持默认 `on`。
|
||||||
|
缓存只复用完全相同的输入,不做四舍五入或容差匹配。
|
||||||
|
|
||||||
|
FastAPI worker 默认在 lifespan 启动阶段预热 SciPy 积分、非线性求解、稀疏
|
||||||
|
Jacobian 和 System XML XSD,完成后才开始接收请求。它不会运行业务模型,也不
|
||||||
|
写入文件;如需诊断冷启动,可设置 `SIMULATIONAPP_WARMUP=off`。每个 worker 都会
|
||||||
|
独立暖机一次。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.venv-win\Scripts\python.exe -m app.simulation.benchmark_performance `
|
||||||
|
--mode audit --warmups 1 --runs 3 `
|
||||||
|
--factory "helium_step=tests.test_amesim_pnvo001_signal_xml:high_pressure_helium_step_project" `
|
||||||
|
--output app/data/performance-evaluations/helium-step.json
|
||||||
|
```
|
||||||
|
|
||||||
|
缓存关闭对照可在同一命令中增加 `--disable-property-cache`。缓存容量、命中、
|
||||||
|
未命中和驱逐数会在 audit 响应的
|
||||||
|
`diagnostics.performance.propertyCache` 中返回。
|
||||||
|
|
||||||
|
基准原始 JSON 默认放到已忽略的 `app/data/` 下。指标字段、实测结果和使用边界见
|
||||||
|
[`仿真性能评估 2026-08-15`](../../docs/other/仿真性能评估-2026-08-15.md)。
|
||||||
|
|
||||||
## 当前阶段进度
|
## 当前阶段进度
|
||||||
|
|
||||||
@@ -73,7 +126,7 @@
|
|||||||
本次推送已经把上一轮建议里的 `M2-M5` 推进到下面这个状态:
|
本次推送已经把上一轮建议里的 `M2-M5` 推进到下面这个状态:
|
||||||
|
|
||||||
1. `M2`:已完成当前阶段首版
|
1. `M2`:已完成当前阶段首版
|
||||||
- 已把 `Testmodel` 的专用闭合、初始化投影、分支入口流量求解、下游支路出口流量闭合、端口状态回写,从 `systems/testmodel.py` 拆到新的 `systems/testmodel_closure.py`
|
- 已把 `Testmodel` 的专用闭合、初始化投影、分支入口流量求解、下游支路出口流量闭合、端口状态回写,从 `examples/testmodel/system.py` 拆到 `examples/testmodel/closure.py`
|
||||||
- `TestModelSystem` 现在主要承担组件装配、网络注册和对闭合器的委托,不再继续堆积系统级手写细节
|
- `TestModelSystem` 现在主要承担组件装配、网络注册和对闭合器的委托,不再继续堆积系统级手写细节
|
||||||
|
|
||||||
2. `M3`:已完成当前阶段首版
|
2. `M3`:已完成当前阶段首版
|
||||||
@@ -165,31 +218,31 @@
|
|||||||
`testmodel_tank_temperature.svg`
|
`testmodel_tank_temperature.svg`
|
||||||
11. 基于 `ModelicaModels/Simulation/Testmodel_res.csv` 的逐时刻对比与误差摘要导出。
|
11. 基于 `ModelicaModels/Simulation/Testmodel_res.csv` 的逐时刻对比与误差摘要导出。
|
||||||
12. 基于 `unittest` 的自动回归测试,当前已覆盖初始化守恒、主变量基线、运行接口、内部闭合诊断、通用分支兼容层、通用结果键与旧键别名一致性,以及部分中间闭合过程行为。
|
12. 基于 `unittest` 的自动回归测试,当前已覆盖初始化守恒、主变量基线、运行接口、内部闭合诊断、通用分支兼容层、通用结果键与旧键别名一致性,以及部分中间闭合过程行为。
|
||||||
|
13. 面向 System XML v3 的拓扑驱动仿真 MVP:压力-流量非线性闭合、stream 焓传播、动态状态自动拼装和端口结果序列。
|
||||||
|
|
||||||
当前没有实现:
|
当前没有实现:
|
||||||
|
|
||||||
- 通用 DAE 初始化器
|
- 通用 DAE 初始化器
|
||||||
- `Modelica.Media.Air.SimpleAir` 的严格复刻
|
- `Modelica.Media.Air.SimpleAir` 的严格复刻
|
||||||
- 面向任意拓扑的通用 connector/stream 求解器
|
- 一般高指数 DAE、事件和严格 Modelica `inStream/actualStream` 求解器
|
||||||
|
|
||||||
## 当前怎么运行
|
## 当前怎么运行
|
||||||
|
|
||||||
最小运行方式:
|
最小运行方式:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python3 -m PythonModels.scripts.run_testmodel
|
python -m app.simulation.examples.testmodel.run
|
||||||
```
|
```
|
||||||
|
|
||||||
如果要改模型参数或运行参数,建议直接改配置对象,而不是改源码里的默认值。例如:
|
如果要改模型参数或运行参数,建议直接改配置对象,而不是改源码里的默认值。例如:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from PythonModels.core.solver import SolveIVPConfig
|
from app.simulation.examples.testmodel.run import (
|
||||||
from PythonModels.scripts.run_testmodel import (
|
|
||||||
TestModelRunConfig,
|
TestModelRunConfig,
|
||||||
TestModelSamplingConfig,
|
TestModelSamplingConfig,
|
||||||
run_testmodel,
|
run_testmodel,
|
||||||
)
|
)
|
||||||
from PythonModels.systems.testmodel import (
|
from app.simulation.examples.testmodel.system import (
|
||||||
BranchConfig,
|
BranchConfig,
|
||||||
CylinderConfig,
|
CylinderConfig,
|
||||||
OrificeConfig,
|
OrificeConfig,
|
||||||
@@ -197,6 +250,7 @@ from PythonModels.systems.testmodel import (
|
|||||||
TankConfig,
|
TankConfig,
|
||||||
TestModelConfig,
|
TestModelConfig,
|
||||||
)
|
)
|
||||||
|
from app.simulation.solvers.solver import SolveIVPConfig
|
||||||
|
|
||||||
run_config = TestModelRunConfig(
|
run_config = TestModelRunConfig(
|
||||||
model=TestModelConfig(
|
model=TestModelConfig(
|
||||||
@@ -217,7 +271,7 @@ result = run_testmodel(run_config=run_config)
|
|||||||
如果调用方想先确认“这次运行最后到底会用哪些路径、哪些采样点”,可以先准备请求,再执行:
|
如果调用方想先确认“这次运行最后到底会用哪些路径、哪些采样点”,可以先准备请求,再执行:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from PythonModels.scripts.run_testmodel import (
|
from app.simulation.examples.testmodel.run import (
|
||||||
prepare_testmodel_run,
|
prepare_testmodel_run,
|
||||||
run_prepared_testmodel,
|
run_prepared_testmodel,
|
||||||
TestModelRunConfig,
|
TestModelRunConfig,
|
||||||
@@ -237,12 +291,13 @@ print(result.used_modelica_reference)
|
|||||||
1. 构建 `TestModelSystem`
|
1. 构建 `TestModelSystem`
|
||||||
2. 打印原始初值向量与约束一致后的初值向量
|
2. 打印原始初值向量与约束一致后的初值向量
|
||||||
3. 运行 `0 s -> 20 s` 的仿真,默认采样间隔 `0.1 s`
|
3. 运行 `0 s -> 20 s` 的仿真,默认采样间隔 `0.1 s`
|
||||||
4. 将结果写入 `PythonModels/runs/` 下本次运行专属的时间戳目录
|
4. 将结果写入 `app/data/simulation-runs/` 下本次运行专属的时间戳目录
|
||||||
5. 若存在 `ModelicaModels/Simulation/Testmodel_res.csv`,自动生成 Python 与 OpenModelica 对比结果
|
5. 若存在 `ModelicaModels/Simulation/Testmodel_res.csv`,自动生成 Python 与 OpenModelica 对比结果
|
||||||
|
|
||||||
当前脚本默认不会再把运行结果直接写到提交基线目录,而是会在 `PythonModels/runs/` 下创建一个带时间戳的子目录,例如:
|
当前脚本默认不会把运行结果直接写到提交基线目录,而是会在
|
||||||
|
`app/data/simulation-runs/` 下创建一个带时间戳的子目录,例如:
|
||||||
|
|
||||||
- `PythonModels/runs/testmodel_20260512_103000_123456/`
|
- `app/data/simulation-runs/testmodel_20260512_103000_123456/`
|
||||||
|
|
||||||
该目录里通常会包含:
|
该目录里通常会包含:
|
||||||
|
|
||||||
@@ -256,18 +311,30 @@ print(result.used_modelica_reference)
|
|||||||
## 基线结果
|
## 基线结果
|
||||||
|
|
||||||
当前基线对比摘要来自:
|
当前基线对比摘要来自:
|
||||||
[testmodel_modelica_comparison_summary.txt](/home/lujz/projects/pressurization-transfer-system/PythonModels/baselines/testmodel/testmodel_modelica_comparison_summary.txt)
|
[`testmodel_modelica_comparison_summary.txt`](../../tests/data/testmodel/testmodel_modelica_comparison_summary.txt)
|
||||||
|
|
||||||
当前四个主变量的最大误差为:
|
当前四个主变量的最大误差为:
|
||||||
|
|
||||||
- `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%`
|
- `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.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%`
|
- `mycylinder.T`: `max_abs_error = 0.009069 K`, `max_rel_error = 0.003870%`
|
||||||
|
|
||||||
这说明在当前基线工况下,Python 版主变量已经能较好贴近 OpenModelica 结果。
|
这说明在当前基线工况下,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 当前架构判断
|
||||||
|
|
||||||
如果按“组件正确 -> 网络闭合 -> 积分可跑 -> 结果对齐 -> 去近似”来看,当前大致处于:
|
如果按“组件正确 -> 网络闭合 -> 积分可跑 -> 结果对齐 -> 去近似”来看,当前大致处于:
|
||||||
|
|
||||||
@@ -281,14 +348,14 @@ print(result.used_modelica_reference)
|
|||||||
|
|
||||||
`Testmodel` 已有一版可运行、可导出、可对比的 Python 近似实现。
|
`Testmodel` 已有一版可运行、可导出、可对比的 Python 近似实现。
|
||||||
|
|
||||||
## 已知限制
|
## Testmodel 已知限制
|
||||||
|
|
||||||
当前最主要的限制可以直接理解成下面几条:
|
当前最主要的限制可以直接理解成下面几条:
|
||||||
|
|
||||||
- 介质模型已从常 `cp/cv` 推进到温度相关空气近似,但仍不是 `Modelica.Media.Air.SimpleAir` 的严格复刻。
|
- 介质模型已从常 `cp/cv` 推进到温度相关空气近似,但仍不是 `Modelica.Media.Air.SimpleAir` 的严格复刻。
|
||||||
- 系统整体仍是 ODE 化近似,不是原始 Modelica DAE 的直接复现。
|
- 系统整体仍是 ODE 化近似,不是原始 Modelica DAE 的直接复现。
|
||||||
- `mytee1 -> mytank` 这一段虽然已经去掉早期的“虚拟出口导通系数”,改成了基于压力一致性的下游能量闭合,但本质上仍是工程近似。
|
- `mytee1 -> mytank` 这一段虽然已经去掉早期的“虚拟出口导通系数”,改成了基于压力一致性的下游能量闭合,但本质上仍是工程近似。
|
||||||
- 当前 `Tee` 的 stream 语义只覆盖了当前 `Testmodel` 需要的最小集合,还不是通用的 `inStream/actualStream` 框架。
|
- 通用 XML 求解链路已经支持按实际流向传播和三通混合 stream 焓,但仍是正则化 MVP,不是严格的 Modelica `inStream/actualStream` 框架。
|
||||||
- 当前一致初值仍是 ODE 入口处的约束投影,不等同于真正的 DAE 初始化求解。
|
- 当前一致初值仍是 ODE 入口处的约束投影,不等同于真正的 DAE 初始化求解。
|
||||||
- 当前自动校验主要锁的是 Python 提交基线,还不是稳定的 Modelica 阈值回归。
|
- 当前自动校验主要锁的是 Python 提交基线,还不是稳定的 Modelica 阈值回归。
|
||||||
- 当前闭合器、系统层和 reporting 层虽然已经开始做“双支路结构化”,但对外结果序列、报告字段和部分导出命名仍然保留 `Testmodel` 专名兼容层,还没有完全转成通用表达。
|
- 当前闭合器、系统层和 reporting 层虽然已经开始做“双支路结构化”,但对外结果序列、报告字段和部分导出命名仍然保留 `Testmodel` 专名兼容层,还没有完全转成通用表达。
|
||||||
@@ -307,7 +374,7 @@ print(result.used_modelica_reference)
|
|||||||
- 作为最终工程结论的唯一依据
|
- 作为最终工程结论的唯一依据
|
||||||
- 直接扩展到更复杂拓扑而不补通用连接器语义
|
- 直接扩展到更复杂拓扑而不补通用连接器语义
|
||||||
|
|
||||||
## 文件级现状
|
## Testmodel 文件级现状
|
||||||
|
|
||||||
按代码现状逐项看:
|
按代码现状逐项看:
|
||||||
|
|
||||||
@@ -317,24 +384,24 @@ print(result.used_modelica_reference)
|
|||||||
`PortState` 目前只保留 `p`、`m_flow`、`h_outflow` 三个必要字段。
|
`PortState` 目前只保留 `p`、`m_flow`、`h_outflow` 三个必要字段。
|
||||||
- `core/state.py`: 正常
|
- `core/state.py`: 正常
|
||||||
`VolumeState` 只负责 `[m, U]` 状态打包。
|
`VolumeState` 只负责 `[m, U]` 状态打包。
|
||||||
- `core/network.py`: 正常
|
- `systems/network.py`: 正常
|
||||||
负责状态向量拼装和连接摘要,不参与物理求解。
|
负责状态向量拼装和连接摘要,不参与物理求解。
|
||||||
- `core/solver.py`: 正常
|
- `solvers/solver.py`: 正常
|
||||||
已支持 SciPy、RK4 回退和零时长仿真。
|
已支持 SciPy、RK4 回退和零时长仿真。
|
||||||
- `components/*.py`: 正常
|
- `components/experimental/**/*.py`: 正常
|
||||||
都是当前一版近似模型,没有发现与 README 明显冲突的“未记录能力”。
|
都是当前一版近似模型,没有发现与 README 明显冲突的“未记录能力”。
|
||||||
- `systems/testmodel.py`: 是当前最重要的技术债集中区
|
- `examples/testmodel/system.py`: 是当前最重要的技术债集中区
|
||||||
这里承载了下游流向切换、焓混合、压力投影等近似逻辑,后续演进应主要落在这里。
|
这里承载了下游流向切换、焓混合、压力投影等近似逻辑,后续演进应主要落在这里。
|
||||||
- `scripts/run_testmodel.py`: 正常
|
- `examples/testmodel/run.py`: 正常
|
||||||
已不是“最小打印脚本”,而是当前结果导出和对比入口。
|
已不是“最小打印脚本”,而是当前结果导出和对比入口。
|
||||||
- `baselines/`: 是当前稳定基线,不应该随着日常运行频繁改动。
|
- `tests/baselines/simulation/`: 是当前稳定基线,不应该随着日常运行频繁改动。
|
||||||
- `runs/`: 是当前默认运行产物目录,不是手写源代码,也不应该当作提交基线使用。
|
- `app/data/simulation-runs/`: 是默认运行产物目录,不是手写源代码,也不应该提交。
|
||||||
|
|
||||||
## 当前主技术债
|
## Testmodel 当前主技术债
|
||||||
|
|
||||||
目前最主要的技术债,可以直接理解成下面 4 件事:
|
目前最主要的技术债,可以直接理解成下面 4 件事:
|
||||||
|
|
||||||
1. 当前初始化虽然已经引入迭代诊断,但本质上仍是 ODE 入口近似,不是真正的 DAE 初始化器。
|
1. 当前初始化虽然已经引入迭代诊断,但本质上仍是 ODE 入口近似,不是真正的 DAE 初始化器。
|
||||||
2. `systems/testmodel.py` 还是承载了太多系统级闭合和初始化逻辑,只是主要端口的手写 stream 方向判断已经搬到组件 helper 里了,装配参数本身已经基本收口到配置对象。
|
2. `examples/testmodel/system.py` 还是承载了太多系统级闭合和初始化逻辑,只是主要端口的手写 stream 方向判断已经搬到组件 helper 里了,装配参数本身已经基本收口到配置对象。
|
||||||
3. 自动校验现在主要锁的是 Python 这一版自己的基线,还不是稳定的 Modelica 阈值回归。
|
3. 自动校验现在主要锁的是 Python 这一版自己的基线,还不是稳定的 Modelica 阈值回归。
|
||||||
4. 当前空气物性已经完成首轮基线校准,但还不是 `SimpleAir` 的严格复刻。以后如果换工况,或者拿到更多 Modelica 原始结果,参数大概率还要继续调。
|
4. 当前空气物性已经完成首轮基线校准,但还不是 `SimpleAir` 的严格复刻。以后如果换工况,或者拿到更多 Modelica 原始结果,参数大概率还要继续调。
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
"""Simulation domain models, solvers, system assembly, and result tools."""
|
||||||
|
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import hashlib
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import statistics
|
||||||
|
import sys
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from math import ceil
|
||||||
|
from pathlib import Path
|
||||||
|
from time import perf_counter_ns, process_time_ns
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def _named_value(value: str, *, option: str) -> tuple[str, str]:
|
||||||
|
name, separator, target = value.partition("=")
|
||||||
|
if not separator or not name.strip() or not target.strip():
|
||||||
|
raise ValueError(
|
||||||
|
f"{option} must use NAME=VALUE syntax, received {value!r}."
|
||||||
|
)
|
||||||
|
return name.strip(), target.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _percentile(values: list[float], percentile: float) -> float:
|
||||||
|
ordered = sorted(values)
|
||||||
|
index = max(0, min(len(ordered) - 1, ceil(percentile * len(ordered)) - 1))
|
||||||
|
return ordered[index]
|
||||||
|
|
||||||
|
|
||||||
|
def _duration_summary(values: list[float]) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"samplesMs": values,
|
||||||
|
"minimumMs": min(values),
|
||||||
|
"medianMs": statistics.median(values),
|
||||||
|
"p95Ms": _percentile(values, 0.95),
|
||||||
|
"maximumMs": max(values),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_factory_xml(specification: str) -> bytes:
|
||||||
|
module_name, separator, member_name = specification.partition(":")
|
||||||
|
if not separator or not module_name or not member_name:
|
||||||
|
raise ValueError(
|
||||||
|
"Factory specifications must use module.path:callable syntax."
|
||||||
|
)
|
||||||
|
factory = getattr(importlib.import_module(module_name), member_name)
|
||||||
|
value = factory()
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str):
|
||||||
|
return value.encode("utf-8")
|
||||||
|
|
||||||
|
from app.main import build_reactflow_system_xml
|
||||||
|
|
||||||
|
return build_reactflow_system_xml(value)
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_result_event(result: dict[str, object]) -> bytes:
|
||||||
|
"""Render the final NDJSON payload shape used by the streaming endpoint."""
|
||||||
|
|
||||||
|
status = str(result.get("status", "completed"))
|
||||||
|
event = {
|
||||||
|
"event": "result",
|
||||||
|
"progress": 100 if status == "completed" else 0,
|
||||||
|
"phase": status,
|
||||||
|
"message": "仿真完成" if status == "completed" else "仿真任务结束",
|
||||||
|
"simulatedTime": result.get("simulatedUntil"),
|
||||||
|
"totalTime": result.get("requestedStopTime"),
|
||||||
|
"result": result,
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
json.dumps(event, ensure_ascii=False, separators=(",", ":")) + "\n"
|
||||||
|
).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_case(
|
||||||
|
name: str,
|
||||||
|
xml_bytes: bytes,
|
||||||
|
*,
|
||||||
|
warmups: int,
|
||||||
|
runs: int,
|
||||||
|
cancellable_path: bool,
|
||||||
|
allow_failures: bool,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
from app.main import run_system_xml_simulation
|
||||||
|
|
||||||
|
cancel_check = (lambda: False) if cancellable_path else None
|
||||||
|
for _ in range(warmups):
|
||||||
|
result = run_system_xml_simulation(xml_bytes, cancel_check=cancel_check)
|
||||||
|
if not bool(result.get("success")) and not allow_failures:
|
||||||
|
raise RuntimeError(f"Warmup for {name!r} failed: {result.get('message')}")
|
||||||
|
|
||||||
|
wall_samples_ms: list[float] = []
|
||||||
|
cpu_samples_ms: list[float] = []
|
||||||
|
serialization_samples_ms: list[float] = []
|
||||||
|
serialized_sizes: list[int] = []
|
||||||
|
profiles: list[dict[str, object]] = []
|
||||||
|
final_result: dict[str, object] | None = None
|
||||||
|
for _ in range(runs):
|
||||||
|
wall_start = perf_counter_ns()
|
||||||
|
cpu_start = process_time_ns()
|
||||||
|
result = run_system_xml_simulation(xml_bytes, cancel_check=cancel_check)
|
||||||
|
cpu_samples_ms.append((process_time_ns() - cpu_start) / 1_000_000.0)
|
||||||
|
wall_samples_ms.append((perf_counter_ns() - wall_start) / 1_000_000.0)
|
||||||
|
if not bool(result.get("success")) and not allow_failures:
|
||||||
|
raise RuntimeError(f"Benchmark for {name!r} failed: {result.get('message')}")
|
||||||
|
diagnostics = result.get("diagnostics")
|
||||||
|
if isinstance(diagnostics, dict):
|
||||||
|
performance = diagnostics.get("performance")
|
||||||
|
if isinstance(performance, dict):
|
||||||
|
profiles.append(performance)
|
||||||
|
serialization_start = perf_counter_ns()
|
||||||
|
serialized_event = _serialize_result_event(result)
|
||||||
|
serialization_samples_ms.append(
|
||||||
|
(perf_counter_ns() - serialization_start) / 1_000_000.0
|
||||||
|
)
|
||||||
|
serialized_sizes.append(len(serialized_event))
|
||||||
|
final_result = result
|
||||||
|
|
||||||
|
assert final_result is not None
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"success": bool(final_result.get("success")),
|
||||||
|
"message": final_result.get("message"),
|
||||||
|
"inputBytes": len(xml_bytes),
|
||||||
|
"inputSha256": hashlib.sha256(xml_bytes).hexdigest(),
|
||||||
|
"status": final_result.get("status"),
|
||||||
|
"simulatedUntil": final_result.get("simulatedUntil"),
|
||||||
|
"requestedStopTime": final_result.get("requestedStopTime"),
|
||||||
|
"wall": _duration_summary(wall_samples_ms),
|
||||||
|
"cpu": _duration_summary(cpu_samples_ms),
|
||||||
|
"resultSerialization": _duration_summary(serialization_samples_ms),
|
||||||
|
"resultEventBytes": serialized_sizes,
|
||||||
|
"performanceRuns": profiles,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_arguments(argv: list[str] | None = None) -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Benchmark the real System XML simulation path with optional profiling."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--mode",
|
||||||
|
choices=("off", "standard", "audit"),
|
||||||
|
default="audit",
|
||||||
|
help="Instrumentation depth selected before importing the simulation modules.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--warmups", type=int, default=1)
|
||||||
|
parser.add_argument("--runs", type=int, default=5)
|
||||||
|
parser.add_argument(
|
||||||
|
"--xml",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
metavar="NAME=PATH",
|
||||||
|
help="Add an XML file benchmark case.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--factory",
|
||||||
|
action="append",
|
||||||
|
default=[],
|
||||||
|
metavar="NAME=MODULE:CALLABLE",
|
||||||
|
help="Add a zero-argument factory returning XML or ReactFlowProjectPayload.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--direct-path",
|
||||||
|
action="store_true",
|
||||||
|
help="Do not pass a cancel callback; use the one-shot SciPy path when eligible.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--disable-property-cache",
|
||||||
|
action="store_true",
|
||||||
|
help="Disable the run-local exact property cache for an A/B comparison.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--allow-failures",
|
||||||
|
action="store_true",
|
||||||
|
help="Record failed simulation runs instead of aborting the benchmark.",
|
||||||
|
)
|
||||||
|
parser.add_argument("--output", type=Path)
|
||||||
|
arguments = parser.parse_args(argv)
|
||||||
|
if arguments.warmups < 0:
|
||||||
|
parser.error("--warmups must not be negative.")
|
||||||
|
if arguments.runs <= 0:
|
||||||
|
parser.error("--runs must be positive.")
|
||||||
|
if not arguments.xml and not arguments.factory:
|
||||||
|
parser.error("At least one --xml or --factory case is required.")
|
||||||
|
return arguments
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
arguments = _parse_arguments(argv)
|
||||||
|
os.environ["SIMULATIONAPP_PROFILE"] = arguments.mode
|
||||||
|
os.environ["SIMULATIONAPP_PROPERTY_CACHE"] = (
|
||||||
|
"off" if arguments.disable_property_cache else "on"
|
||||||
|
)
|
||||||
|
|
||||||
|
cases: list[tuple[str, bytes]] = []
|
||||||
|
for raw_case in arguments.xml:
|
||||||
|
name, raw_path = _named_value(raw_case, option="--xml")
|
||||||
|
cases.append((name, Path(raw_path).read_bytes()))
|
||||||
|
for raw_case in arguments.factory:
|
||||||
|
name, specification = _named_value(raw_case, option="--factory")
|
||||||
|
cases.append((name, _load_factory_xml(specification)))
|
||||||
|
|
||||||
|
report: dict[str, Any] = {
|
||||||
|
"generatedAt": datetime.now(UTC).isoformat(),
|
||||||
|
"profileMode": arguments.mode,
|
||||||
|
"cancellableSolverPath": not arguments.direct_path,
|
||||||
|
"propertyCacheEnabled": not arguments.disable_property_cache,
|
||||||
|
"allowFailures": bool(arguments.allow_failures),
|
||||||
|
"warmups": arguments.warmups,
|
||||||
|
"runs": arguments.runs,
|
||||||
|
"runtime": {
|
||||||
|
"python": sys.version,
|
||||||
|
"platform": platform.platform(),
|
||||||
|
"processor": platform.processor(),
|
||||||
|
},
|
||||||
|
"cases": [
|
||||||
|
_run_case(
|
||||||
|
name,
|
||||||
|
xml_bytes,
|
||||||
|
warmups=arguments.warmups,
|
||||||
|
runs=arguments.runs,
|
||||||
|
cancellable_path=not arguments.direct_path,
|
||||||
|
allow_failures=arguments.allow_failures,
|
||||||
|
)
|
||||||
|
for name, xml_bytes in cases
|
||||||
|
],
|
||||||
|
}
|
||||||
|
text = json.dumps(report, ensure_ascii=False, indent=2)
|
||||||
|
if arguments.output is not None:
|
||||||
|
arguments.output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
arguments.output.write_text(text + "\n", encoding="utf-8")
|
||||||
|
print(f"Performance report written to {arguments.output.resolve()}")
|
||||||
|
else:
|
||||||
|
print(text)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
File diff suppressed because it is too large.
Load diff
File renamed without changes.
@@ -0,0 +1,3 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
__all__: list[str] = []
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""AMESim pneumatic boundary components."""
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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"
|
||||||
|
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||||
|
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_values(self) -> tuple[float, ...]:
|
||||||
|
return (self.port_1.m_flow,)
|
||||||
|
|
||||||
|
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."""
|
||||||
File diff suppressed because it is too large.
Load diff
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,239 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
_REFERENCE_OUTFLOW_REGULARIZATION_RATIO = 0.05
|
||||||
|
|
||||||
|
|
||||||
|
def _regularized_inverse_outflow(flow: float, transition_flow: float) -> float:
|
||||||
|
"""Return a C1 inverse that tends to zero as a negative flow vanishes."""
|
||||||
|
|
||||||
|
if flow >= 0.0:
|
||||||
|
return 0.0
|
||||||
|
transition_flow = max(float(transition_flow), 1.0e-12)
|
||||||
|
if -flow >= transition_flow:
|
||||||
|
return 1.0 / flow
|
||||||
|
return (
|
||||||
|
flow
|
||||||
|
* (2.0 * transition_flow * transition_flow - flow * flow)
|
||||||
|
/ transition_flow**4
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _AmesimPneumaticNode(AlgebraicComponent):
|
||||||
|
"""Shared implementation for AMESim pneumatic junction submodels.
|
||||||
|
|
||||||
|
PN3NODE2/P4NODE2 use port 2 as their pressure and temperature reference.
|
||||||
|
Non-reference outlet ports use that reference temperature. When port 2 is
|
||||||
|
an outlet, its enthalpy is the residual that closes the junction energy
|
||||||
|
balance, matching the AMESim dh2 causality.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||||
|
REFERENCE_PORT = "port_2"
|
||||||
|
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
super().__init__(name=name)
|
||||||
|
self.set_parameter_values({})
|
||||||
|
self.temperature_reference_h = 0.0
|
||||||
|
for definition in self.PORTS:
|
||||||
|
setattr(self, definition.name, self.register_declared_port(definition.name))
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
reference = self.get_port(self.REFERENCE_PORT)
|
||||||
|
return tuple(
|
||||||
|
self.get_port(definition.name).p - reference.p
|
||||||
|
for definition in self.PORTS
|
||||||
|
if definition.name != self.REFERENCE_PORT
|
||||||
|
) + (
|
||||||
|
sum(
|
||||||
|
self.get_port(definition.name).m_flow
|
||||||
|
for definition in self.PORTS
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
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:
|
||||||
|
self.temperature_reference_h = connected_h.get(
|
||||||
|
self.REFERENCE_PORT,
|
||||||
|
sum(connected_h.values()) / len(connected_h) if connected_h else 0.0,
|
||||||
|
)
|
||||||
|
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 = self.temperature_reference_h
|
||||||
|
|
||||||
|
reference_port = self.get_port(self.REFERENCE_PORT)
|
||||||
|
for name, port in self.ports.items():
|
||||||
|
port.h_outflow = (
|
||||||
|
mixed_h
|
||||||
|
if name == self.REFERENCE_PORT
|
||||||
|
else self.temperature_reference_h
|
||||||
|
)
|
||||||
|
|
||||||
|
if reference_port.m_flow < 0.0:
|
||||||
|
energy_without_reference = sum(
|
||||||
|
port.m_flow
|
||||||
|
* (
|
||||||
|
connected_h[name]
|
||||||
|
if port.m_flow > 1e-12
|
||||||
|
else self.temperature_reference_h
|
||||||
|
)
|
||||||
|
for name, port in self.ports.items()
|
||||||
|
if name != self.REFERENCE_PORT
|
||||||
|
)
|
||||||
|
non_reference_flow_scale = sum(
|
||||||
|
abs(port.m_flow)
|
||||||
|
for name, port in self.ports.items()
|
||||||
|
if name != self.REFERENCE_PORT
|
||||||
|
)
|
||||||
|
transition_flow = (
|
||||||
|
_REFERENCE_OUTFLOW_REGULARIZATION_RATIO
|
||||||
|
* non_reference_flow_scale
|
||||||
|
)
|
||||||
|
# Port 2 carries AMESim's residual-energy causality. Exact
|
||||||
|
# division is singular when its outflow reverses through zero, so
|
||||||
|
# use a C1 band that matches the exact balance at its boundary and
|
||||||
|
# tends to the mixed enthalpy at zero flow.
|
||||||
|
inverse_flow = _regularized_inverse_outflow(
|
||||||
|
reference_port.m_flow,
|
||||||
|
transition_flow,
|
||||||
|
)
|
||||||
|
energy_residual_at_mixed_h = (
|
||||||
|
energy_without_reference
|
||||||
|
+ reference_port.m_flow * mixed_h
|
||||||
|
)
|
||||||
|
reference_port.h_outflow = (
|
||||||
|
mixed_h - energy_residual_at_mixed_h * inverse_flow
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class AmesimPn3Node2(_AmesimPneumaticNode):
|
||||||
|
"""AMESim PN3NODE2 pneumatic three-port junction."""
|
||||||
|
|
||||||
|
MODEL_TYPE = "amesim_pn3node2"
|
||||||
|
MODEL_VERSION = "0.3.0"
|
||||||
|
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||||
|
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||||||
|
("mass_flow_balance",)
|
||||||
|
)
|
||||||
|
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.3.0"
|
||||||
|
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||||
|
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||||||
|
("mass_flow_balance",)
|
||||||
|
)
|
||||||
|
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,49 @@
|
|||||||
|
"""AMESim-compatible public component library."""
|
||||||
|
|
||||||
|
from app.simulation.core.catalog import (
|
||||||
|
ComponentCategorySpec,
|
||||||
|
ComponentLibrarySpec,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
LIBRARY = ComponentLibrarySpec(
|
||||||
|
id="amesim",
|
||||||
|
label="AMESim 组件库",
|
||||||
|
version="0.3.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.mechanical.pistons:AmesimPnrp17",
|
||||||
|
"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,305 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from math import isfinite, pi
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
AMESIM_REFERENCE_PRESSURE_PA = 101300.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Pnrp17Linearization:
|
||||||
|
volume: float
|
||||||
|
volume_flow: float
|
||||||
|
pressure_force: float
|
||||||
|
volume_tangent: tuple[float, ...]
|
||||||
|
volume_flow_tangent: tuple[float, ...]
|
||||||
|
pressure_force_tangent: tuple[float, ...]
|
||||||
|
valid: bool = True
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class AmesimPnrp17(AlgebraicComponent):
|
||||||
|
"""AMESim PNRP17 pneumatic piston with two mechanical faces.
|
||||||
|
|
||||||
|
Mechanical ports 2/5 share the piston-side motion and ports 3/4 share the
|
||||||
|
cylinder-side motion. The pneumatic port contributes its swept volume and
|
||||||
|
volume rate to the connected variable-volume chamber.
|
||||||
|
"""
|
||||||
|
|
||||||
|
MODEL_TYPE = "amesim_pnrp17"
|
||||||
|
MODEL_VERSION = "0.1.0"
|
||||||
|
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||||
|
PORTS = (
|
||||||
|
PortDefinition.pneumatic("port_1", nominal_role="bidirectional"),
|
||||||
|
PortDefinition.mechanical_translational("port_2"),
|
||||||
|
PortDefinition.mechanical_translational("port_3"),
|
||||||
|
PortDefinition.mechanical_translational("port_4"),
|
||||||
|
PortDefinition.mechanical_translational("port_5"),
|
||||||
|
)
|
||||||
|
PARAMETERS = (
|
||||||
|
AMESIM_GAS_INDEX_PARAMETER,
|
||||||
|
ParameterDefinition(
|
||||||
|
"dp",
|
||||||
|
0.2,
|
||||||
|
label="活塞直径",
|
||||||
|
quantity="length",
|
||||||
|
unit="m",
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
description="活塞外径;与活塞杆直径共同确定有效受压面积。",
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"dr",
|
||||||
|
0.001,
|
||||||
|
label="活塞杆直径",
|
||||||
|
quantity="length",
|
||||||
|
unit="m",
|
||||||
|
minimum=0.0,
|
||||||
|
description="穿过气室一侧的活塞杆直径,必须不大于活塞直径。",
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"x0",
|
||||||
|
0.0,
|
||||||
|
label="初始腔长",
|
||||||
|
quantity="length",
|
||||||
|
unit="m",
|
||||||
|
description="机械端位移均为零时的气动腔长度。",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
RESULT_VARIABLES = (
|
||||||
|
ResultVariableDefinition("volume", "扫掠容积", "volume", "m3", "derived", 10),
|
||||||
|
ResultVariableDefinition(
|
||||||
|
"volume_flow",
|
||||||
|
"扫掠容积变化率",
|
||||||
|
"volume_flow",
|
||||||
|
"m3/s",
|
||||||
|
"derived",
|
||||||
|
20,
|
||||||
|
),
|
||||||
|
ResultVariableDefinition("length", "气动腔长度", "length", "m", "derived", 30),
|
||||||
|
ResultVariableDefinition(
|
||||||
|
"pressure_force",
|
||||||
|
"气压力",
|
||||||
|
"force",
|
||||||
|
"N",
|
||||||
|
"derived",
|
||||||
|
40,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
DISPLAY = ComponentDisplaySpec(
|
||||||
|
label="PNRP17 气动活塞",
|
||||||
|
library_id="amesim",
|
||||||
|
category_id="mechanical",
|
||||||
|
symbol="amesim_pnrp17",
|
||||||
|
ports=(
|
||||||
|
PortDisplaySpec("port_1", "left", order=10),
|
||||||
|
PortDisplaySpec("port_3", "left", order=20),
|
||||||
|
PortDisplaySpec("port_2", "left", order=30),
|
||||||
|
PortDisplaySpec("port_4", "right", order=40),
|
||||||
|
PortDisplaySpec("port_5", "right", order=50),
|
||||||
|
),
|
||||||
|
order=60,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
medium: GasMedium,
|
||||||
|
*,
|
||||||
|
gi: float = 0.0,
|
||||||
|
dp: float = 0.2,
|
||||||
|
dr: float = 0.001,
|
||||||
|
x0: float = 0.0,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(name=name)
|
||||||
|
self.set_parameter_values({"gi": gi, "dp": dp, "dr": dr, "x0": x0})
|
||||||
|
self.medium = medium
|
||||||
|
self.gi = normalize_amesim_gas_index(gi)
|
||||||
|
self.dp = float(dp)
|
||||||
|
self.dr = float(dr)
|
||||||
|
self.x0 = float(x0)
|
||||||
|
if self.dr > self.dp:
|
||||||
|
raise ValueError("PNRP17 rod diameter dr must not exceed piston diameter dp.")
|
||||||
|
for definition in self.PORTS:
|
||||||
|
port = self.register_declared_port(definition.name)
|
||||||
|
setattr(self, definition.name, port)
|
||||||
|
self.port_1.h_outflow = medium.specific_enthalpy(medium.T_ref)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
medium: GasMedium,
|
||||||
|
parameters: Mapping[str, float],
|
||||||
|
) -> "AmesimPnrp17":
|
||||||
|
return cls(name=name, medium=medium, **dict(parameters))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def effective_area(self) -> float:
|
||||||
|
return pi * (self.dp * self.dp - self.dr * self.dr) / 4.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chamber_length(self) -> float:
|
||||||
|
return self.x0 + self.port_5.x - self.port_4.x
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chamber_volume(self) -> float:
|
||||||
|
return self.effective_area * self.chamber_length
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chamber_volume_flow(self) -> float:
|
||||||
|
return self.effective_area * (self.port_5.v - self.port_4.v)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pressure_force(self) -> float:
|
||||||
|
return (self.port_1.p - AMESIM_REFERENCE_PRESSURE_PA) * self.effective_area
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
values = [self.port_1.m_flow]
|
||||||
|
effort_pairs = (("port_2", "port_5"), ("port_3", "port_4"))
|
||||||
|
for first_name, second_name in effort_pairs:
|
||||||
|
first = self.get_port(first_name)
|
||||||
|
second = self.get_port(second_name)
|
||||||
|
values.extend((first.x - second.x, first.v - second.v))
|
||||||
|
force = self.pressure_force
|
||||||
|
values.extend(
|
||||||
|
(
|
||||||
|
self.port_2.f + self.port_5.f + force,
|
||||||
|
self.port_3.f + self.port_4.f - force,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tuple(values)
|
||||||
|
|
||||||
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
|
effort_pairs = (("port_2", "port_5"), ("port_3", "port_4"))
|
||||||
|
residuals: list[EquationResidual] = [
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:pneumatic_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,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
for first_name, second_name in effort_pairs:
|
||||||
|
first = self.get_port(first_name)
|
||||||
|
second = self.get_port(second_name)
|
||||||
|
for variable in ("x", "v"):
|
||||||
|
residuals.append(
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:{first_name}_{second_name}_{variable}_equal",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="equal",
|
||||||
|
variables=(
|
||||||
|
f"{self.name}.{first_name}.{variable}",
|
||||||
|
f"{self.name}.{second_name}.{variable}",
|
||||||
|
),
|
||||||
|
role="effort",
|
||||||
|
value=getattr(first, variable) - getattr(second, variable),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
force = self.pressure_force
|
||||||
|
residuals.extend(
|
||||||
|
(
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:piston_side_force_balance",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="constitutive",
|
||||||
|
variables=(f"{self.name}.port_2.f", f"{self.name}.port_5.f", f"{self.name}.port_1.p"),
|
||||||
|
role="flow",
|
||||||
|
value=self.port_2.f + self.port_5.f + force,
|
||||||
|
),
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:cylinder_side_force_balance",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="constitutive",
|
||||||
|
variables=(f"{self.name}.port_3.f", f"{self.name}.port_4.f", f"{self.name}.port_1.p"),
|
||||||
|
role="flow",
|
||||||
|
value=self.port_3.f + self.port_4.f - force,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tuple(residuals)
|
||||||
|
|
||||||
|
def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]:
|
||||||
|
return {"port_1": (self.chamber_volume, self.chamber_volume_flow)}
|
||||||
|
|
||||||
|
def linearize_geometry_and_force(
|
||||||
|
self,
|
||||||
|
port_4_x_tangent: Sequence[float],
|
||||||
|
port_5_x_tangent: Sequence[float],
|
||||||
|
port_4_v_tangent: Sequence[float],
|
||||||
|
port_5_v_tangent: Sequence[float],
|
||||||
|
port_1_pressure_tangent: Sequence[float],
|
||||||
|
) -> Pnrp17Linearization:
|
||||||
|
"""Return exact piston geometry and pressure-force tangents."""
|
||||||
|
|
||||||
|
vectors = tuple(
|
||||||
|
tuple(float(value) for value in values)
|
||||||
|
for values in (
|
||||||
|
port_4_x_tangent,
|
||||||
|
port_5_x_tangent,
|
||||||
|
port_4_v_tangent,
|
||||||
|
port_5_v_tangent,
|
||||||
|
port_1_pressure_tangent,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
widths = {len(values) for values in vectors}
|
||||||
|
if len(widths) != 1:
|
||||||
|
raise ValueError("PNRP17 tangent vectors must have equal lengths.")
|
||||||
|
valid = all(isfinite(value) for values in vectors for value in values)
|
||||||
|
area = self.effective_area
|
||||||
|
volume_tangent = tuple(
|
||||||
|
area * (right - left)
|
||||||
|
for left, right in zip(vectors[0], vectors[1], strict=True)
|
||||||
|
)
|
||||||
|
volume_flow_tangent = tuple(
|
||||||
|
area * (right - left)
|
||||||
|
for left, right in zip(vectors[2], vectors[3], strict=True)
|
||||||
|
)
|
||||||
|
pressure_force_tangent = tuple(
|
||||||
|
area * value for value in vectors[4]
|
||||||
|
)
|
||||||
|
return Pnrp17Linearization(
|
||||||
|
volume=self.chamber_volume,
|
||||||
|
volume_flow=self.chamber_volume_flow,
|
||||||
|
pressure_force=self.pressure_force,
|
||||||
|
volume_tangent=volume_tangent,
|
||||||
|
volume_flow_tangent=volume_flow_tangent,
|
||||||
|
pressure_force_tangent=pressure_force_tangent,
|
||||||
|
valid=valid,
|
||||||
|
reason=None if valid else "non_finite_tangent_input",
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||||
|
self.port_1.h_outflow = connected_h.get(
|
||||||
|
"port_1",
|
||||||
|
self.medium.specific_enthalpy(self.medium.T_ref),
|
||||||
|
)
|
||||||
|
|
||||||
|
def component_result_values(self) -> Mapping[str, float]:
|
||||||
|
return {
|
||||||
|
"volume": self.chamber_volume,
|
||||||
|
"volume_flow": self.chamber_volume_flow,
|
||||||
|
"length": self.chamber_length,
|
||||||
|
"pressure_force": self.pressure_force,
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -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,517 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from math import exp, isfinite, log
|
||||||
|
from typing import ClassVar
|
||||||
|
|
||||||
|
from app.simulation.core.errors import RecoverableTrialStateError
|
||||||
|
from app.simulation.core.medium import (
|
||||||
|
GasMedium,
|
||||||
|
IdealGasMedium,
|
||||||
|
ThermodynamicProperties,
|
||||||
|
ThermodynamicPropertiesLinearization,
|
||||||
|
ThermodynamicPropertyTangents,
|
||||||
|
)
|
||||||
|
from app.simulation.core.peng_robinson import HELIUM_PR, PengRobinsonFluid
|
||||||
|
from app.simulation.performance import profile_property, record_property_iterations
|
||||||
|
from app.simulation.property_cache import cache_property_calculation
|
||||||
|
|
||||||
|
|
||||||
|
@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 caloric reference follows the constant NASA
|
||||||
|
polynomial from Simcenter Amesim 2404 ``helium_cp_h_s.data``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
SUBSTANCE_ID: ClassVar[str] = "helium"
|
||||||
|
PROPERTY_METHOD_ID: ClassVar[str] = "peng_robinson"
|
||||||
|
fluid: ClassVar[PengRobinsonFluid] = HELIUM_PR
|
||||||
|
nasa_cp_over_R: ClassVar[float] = 2.5
|
||||||
|
nasa_enthalpy_constant_K: ClassVar[float] = -745.375
|
||||||
|
nasa_viscosity_coefficients: ClassVar[tuple[float, float, float, float]] = (
|
||||||
|
0.7501594,
|
||||||
|
35.76324,
|
||||||
|
-2212.129,
|
||||||
|
0.9212635,
|
||||||
|
)
|
||||||
|
|
||||||
|
name: str = "AMESimHeliumPengRobinson"
|
||||||
|
R_gas: float = HELIUM_PR.specific_gas_constant
|
||||||
|
cp_ref: float = nasa_cp_over_R * HELIUM_PR.specific_gas_constant
|
||||||
|
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 (self.nasa_cp_over_R - 1.0) * self.R_gas
|
||||||
|
|
||||||
|
def cv_at_temperature(self, T: float) -> float:
|
||||||
|
del T
|
||||||
|
return self.cv
|
||||||
|
|
||||||
|
def diagnostic_dynamic_viscosity(self, T: float) -> float:
|
||||||
|
"""Return the AMESim NASA-table viscosity used by pipe diagnostics.
|
||||||
|
|
||||||
|
pn2pipefr reports Reynolds number with sagum viscosity. Keep this
|
||||||
|
separate from dynamic_viscosity so matching that diagnostic cannot
|
||||||
|
alter the already-validated pipe flow or friction dynamics.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if T <= 0.0:
|
||||||
|
raise ValueError("Temperature must be positive.")
|
||||||
|
a, b, c, d = self.nasa_viscosity_coefficients
|
||||||
|
return 1.0e-7 * exp(a * log(T) + b / T + c / (T * T) + d)
|
||||||
|
|
||||||
|
@profile_property("density")
|
||||||
|
@cache_property_calculation("density")
|
||||||
|
def density(self, p: float, T: float) -> float:
|
||||||
|
return self.fluid.density(p, T)
|
||||||
|
|
||||||
|
def _real_heat_capacities(
|
||||||
|
self,
|
||||||
|
p: float,
|
||||||
|
T: float,
|
||||||
|
) -> tuple[float, float, float, float, float]:
|
||||||
|
density = self.density(p, T)
|
||||||
|
pressure_density_derivative = (
|
||||||
|
self.fluid.pressure_density_derivative_at_temperature(
|
||||||
|
T,
|
||||||
|
density,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pressure_temperature_derivative = (
|
||||||
|
self.fluid.pressure_temperature_derivative_at_density(
|
||||||
|
T,
|
||||||
|
density,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cv = (
|
||||||
|
self.cv_at_temperature(T)
|
||||||
|
+ self.fluid.residual_isochoric_heat_capacity_at_density(T, density)
|
||||||
|
)
|
||||||
|
cp = (
|
||||||
|
cv
|
||||||
|
+ T
|
||||||
|
* pressure_temperature_derivative
|
||||||
|
* pressure_temperature_derivative
|
||||||
|
/ (density * density * pressure_density_derivative)
|
||||||
|
)
|
||||||
|
if cp <= 0.0 or cv <= 0.0:
|
||||||
|
raise ValueError("Real-gas heat capacities must be positive.")
|
||||||
|
return (
|
||||||
|
cp,
|
||||||
|
cv,
|
||||||
|
density,
|
||||||
|
pressure_density_derivative,
|
||||||
|
pressure_temperature_derivative,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _local_isentropic_density_pressure_factor(
|
||||||
|
self,
|
||||||
|
p: float,
|
||||||
|
T: float,
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
cp, cv, density, pressure_density_derivative, pressure_temperature_derivative = (
|
||||||
|
self._real_heat_capacities(p, T)
|
||||||
|
)
|
||||||
|
heat_capacity_ratio = cp / cv
|
||||||
|
factor = p / (
|
||||||
|
density * pressure_density_derivative * heat_capacity_ratio
|
||||||
|
)
|
||||||
|
exponent = (
|
||||||
|
p
|
||||||
|
* (heat_capacity_ratio - 1.0)
|
||||||
|
/ (
|
||||||
|
heat_capacity_ratio
|
||||||
|
* T
|
||||||
|
* pressure_temperature_derivative
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return factor, exponent
|
||||||
|
|
||||||
|
@profile_property("isentropic_density_pressure_factor")
|
||||||
|
@cache_property_calculation("isentropic_density_pressure_factor")
|
||||||
|
def isentropic_density_pressure_factor(
|
||||||
|
self,
|
||||||
|
p: float,
|
||||||
|
T: float,
|
||||||
|
downstream_pressure: float | None = None,
|
||||||
|
) -> float:
|
||||||
|
upstream_factor, isentropic_temperature_exponent = (
|
||||||
|
self._local_isentropic_density_pressure_factor(p, T)
|
||||||
|
)
|
||||||
|
if downstream_pressure is None or downstream_pressure >= p:
|
||||||
|
return upstream_factor
|
||||||
|
|
||||||
|
pressure_ratio = max(downstream_pressure / p, 1.0e-12)
|
||||||
|
isentropic_temperature = max(
|
||||||
|
T * pressure_ratio**isentropic_temperature_exponent,
|
||||||
|
2.2,
|
||||||
|
)
|
||||||
|
downstream_factor, _unused_exponent = (
|
||||||
|
self._local_isentropic_density_pressure_factor(
|
||||||
|
max(downstream_pressure, 1.0),
|
||||||
|
isentropic_temperature,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# AMESim 2404 saggs_ evaluates the local factor at the upstream
|
||||||
|
# state and at an approximate isentropic downstream state.
|
||||||
|
return 0.5 * (upstream_factor + downstream_factor)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@profile_property("specific_internal_energy")
|
||||||
|
def specific_internal_energy(self, T: float) -> float:
|
||||||
|
return self.R_gas * (
|
||||||
|
(self.nasa_cp_over_R - 1.0) * T
|
||||||
|
+ self.nasa_enthalpy_constant_K
|
||||||
|
)
|
||||||
|
|
||||||
|
@profile_property("specific_internal_energy_at_pressure")
|
||||||
|
def specific_internal_energy_at_pressure(self, p: float, T: float) -> float:
|
||||||
|
density = self.density(p, T)
|
||||||
|
return (
|
||||||
|
self.specific_internal_energy(T)
|
||||||
|
+ self.fluid.residual_specific_internal_energy_at_density(T, density)
|
||||||
|
)
|
||||||
|
|
||||||
|
@profile_property("specific_enthalpy")
|
||||||
|
def specific_enthalpy(self, T: float) -> float:
|
||||||
|
return self.R_gas * (
|
||||||
|
self.nasa_cp_over_R * T
|
||||||
|
+ self.nasa_enthalpy_constant_K
|
||||||
|
)
|
||||||
|
|
||||||
|
@profile_property("specific_enthalpy_at_pressure")
|
||||||
|
def specific_enthalpy_at_pressure(self, p: float, T: float) -> float:
|
||||||
|
return self.specific_enthalpy(T) + self.fluid.residual_specific_enthalpy(p, T)
|
||||||
|
|
||||||
|
def temperature_from_internal_energy(self, u: float) -> float:
|
||||||
|
return (
|
||||||
|
u / self.R_gas - self.nasa_enthalpy_constant_K
|
||||||
|
) / (self.nasa_cp_over_R - 1.0)
|
||||||
|
|
||||||
|
def temperature_from_enthalpy(self, h: float) -> float:
|
||||||
|
return (
|
||||||
|
h / self.R_gas - self.nasa_enthalpy_constant_K
|
||||||
|
) / self.nasa_cp_over_R
|
||||||
|
|
||||||
|
@profile_property("temperature_from_pressure_enthalpy")
|
||||||
|
@cache_property_calculation("temperature_from_pressure_enthalpy")
|
||||||
|
def temperature_from_pressure_enthalpy(self, p: float, h: float) -> float:
|
||||||
|
temperature = max(self.temperature_from_enthalpy(h), 2.2)
|
||||||
|
for _iteration in range(16):
|
||||||
|
residual_enthalpy = self.fluid.residual_specific_enthalpy(p, temperature)
|
||||||
|
next_temperature = max(
|
||||||
|
self.temperature_from_enthalpy(h - residual_enthalpy),
|
||||||
|
2.2,
|
||||||
|
)
|
||||||
|
if abs(next_temperature - temperature) <= 1.0e-10 * max(
|
||||||
|
temperature,
|
||||||
|
1.0,
|
||||||
|
):
|
||||||
|
record_property_iterations(
|
||||||
|
"temperature_from_pressure_enthalpy",
|
||||||
|
_iteration + 1,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
return next_temperature
|
||||||
|
temperature = next_temperature
|
||||||
|
record_property_iterations(
|
||||||
|
"temperature_from_pressure_enthalpy",
|
||||||
|
16,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
return temperature
|
||||||
|
|
||||||
|
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float:
|
||||||
|
if m <= 0.0:
|
||||||
|
raise RecoverableTrialStateError(
|
||||||
|
"Mass must stay positive when recovering temperature."
|
||||||
|
)
|
||||||
|
return self.temperature_from_internal_energy(U / m)
|
||||||
|
|
||||||
|
@profile_property("properties_from_mU")
|
||||||
|
@cache_property_calculation("properties_from_mU")
|
||||||
|
def properties_from_mU(
|
||||||
|
self,
|
||||||
|
m: float,
|
||||||
|
U: float,
|
||||||
|
V: float,
|
||||||
|
) -> ThermodynamicProperties:
|
||||||
|
"""Recover a real-gas state, reusing exact repeated evaluations.
|
||||||
|
|
||||||
|
Implicit integration asks several component interfaces for the same
|
||||||
|
``(m, U, V)`` state while closing one RHS evaluation and while building
|
||||||
|
finite-difference Jacobians. The calculation is pure and its result is
|
||||||
|
immutable, so an exact-key bounded cache avoids repeating the
|
||||||
|
Peng-Robinson temperature iteration without changing model semantics.
|
||||||
|
"""
|
||||||
|
if m <= 0.0:
|
||||||
|
raise RecoverableTrialStateError(
|
||||||
|
"Mass must stay positive when recovering temperature."
|
||||||
|
)
|
||||||
|
if V <= 0.0:
|
||||||
|
raise ValueError("Volume must stay positive.")
|
||||||
|
density = m / V
|
||||||
|
target_internal_energy = U / m
|
||||||
|
temperature = max(
|
||||||
|
self.temperature_from_internal_energy(target_internal_energy),
|
||||||
|
2.2,
|
||||||
|
)
|
||||||
|
converged = False
|
||||||
|
for _iteration in range(16):
|
||||||
|
residual_internal_energy = (
|
||||||
|
self.fluid.residual_specific_internal_energy_at_density(
|
||||||
|
temperature,
|
||||||
|
density,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
next_temperature = max(
|
||||||
|
self.temperature_from_internal_energy(
|
||||||
|
target_internal_energy - residual_internal_energy
|
||||||
|
),
|
||||||
|
2.2,
|
||||||
|
)
|
||||||
|
if abs(next_temperature - temperature) <= 1.0e-10 * max(
|
||||||
|
temperature,
|
||||||
|
1.0,
|
||||||
|
):
|
||||||
|
temperature = next_temperature
|
||||||
|
converged = True
|
||||||
|
break
|
||||||
|
temperature = next_temperature
|
||||||
|
record_property_iterations(
|
||||||
|
"properties_from_mU",
|
||||||
|
_iteration + 1,
|
||||||
|
converged,
|
||||||
|
)
|
||||||
|
pressure = self.fluid.pressure_from_density(temperature, density)
|
||||||
|
return ThermodynamicProperties(
|
||||||
|
p=pressure,
|
||||||
|
T=temperature,
|
||||||
|
rho=density,
|
||||||
|
u=target_internal_energy,
|
||||||
|
h=self.specific_enthalpy_at_pressure(
|
||||||
|
pressure,
|
||||||
|
temperature,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def linearize_properties_from_mU(
|
||||||
|
self,
|
||||||
|
m: float,
|
||||||
|
U: float,
|
||||||
|
V: float,
|
||||||
|
dm: Sequence[float],
|
||||||
|
dU: Sequence[float],
|
||||||
|
dV: Sequence[float],
|
||||||
|
*,
|
||||||
|
properties: ThermodynamicProperties | None = None,
|
||||||
|
) -> ThermodynamicPropertiesLinearization:
|
||||||
|
"""Implicitly differentiate the Peng-Robinson m/U/V recovery."""
|
||||||
|
|
||||||
|
dm_values = tuple(float(value) for value in dm)
|
||||||
|
dU_values = tuple(float(value) for value in dU)
|
||||||
|
dV_values = tuple(float(value) for value in dV)
|
||||||
|
if not (len(dm_values) == len(dU_values) == len(dV_values)):
|
||||||
|
raise ValueError("Thermodynamic tangent vectors must have equal lengths.")
|
||||||
|
props = properties or self.properties_from_mU(m, U, V)
|
||||||
|
width = len(dm_values)
|
||||||
|
|
||||||
|
def invalid(reason: str) -> ThermodynamicPropertiesLinearization:
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents.zeros(width),
|
||||||
|
valid=False,
|
||||||
|
reason=reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
expected_density = m / V
|
||||||
|
expected_internal_energy = U / m
|
||||||
|
if (
|
||||||
|
abs(props.rho - expected_density)
|
||||||
|
> 1.0e-12 * max(abs(expected_density), 1.0)
|
||||||
|
or abs(props.u - expected_internal_energy)
|
||||||
|
> 1.0e-12 * max(abs(expected_internal_energy), 1.0)
|
||||||
|
):
|
||||||
|
return invalid("properties_primal_mismatch")
|
||||||
|
if not all(
|
||||||
|
isfinite(value)
|
||||||
|
for values in (dm_values, dU_values, dV_values)
|
||||||
|
for value in values
|
||||||
|
):
|
||||||
|
return invalid("non_finite_tangent_input")
|
||||||
|
if props.T <= 2.2 * (1.0 + 1.0e-10):
|
||||||
|
return invalid("temperature_floor_boundary")
|
||||||
|
|
||||||
|
pressure_temperature_derivative = (
|
||||||
|
self.fluid.pressure_temperature_derivative_at_density(
|
||||||
|
props.T,
|
||||||
|
props.rho,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pressure_density_derivative = (
|
||||||
|
self.fluid.pressure_density_derivative_at_temperature(
|
||||||
|
props.T,
|
||||||
|
props.rho,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cv = (
|
||||||
|
self.cv_at_temperature(props.T)
|
||||||
|
+ self.fluid.residual_isochoric_heat_capacity_at_density(
|
||||||
|
props.T,
|
||||||
|
props.rho,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
recovered_internal_energy = (
|
||||||
|
self.specific_internal_energy(props.T)
|
||||||
|
+ self.fluid.residual_specific_internal_energy_at_density(
|
||||||
|
props.T,
|
||||||
|
props.rho,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
recovery_scale = max(
|
||||||
|
abs(props.u),
|
||||||
|
abs(cv * props.T) if isfinite(cv) else 0.0,
|
||||||
|
1.0,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
not all(
|
||||||
|
isfinite(value)
|
||||||
|
for value in (
|
||||||
|
pressure_temperature_derivative,
|
||||||
|
pressure_density_derivative,
|
||||||
|
cv,
|
||||||
|
recovered_internal_energy,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
or cv <= 0.0
|
||||||
|
):
|
||||||
|
return invalid("invalid_peng_robinson_derivative")
|
||||||
|
if abs(recovered_internal_energy - props.u) > 1.0e-8 * recovery_scale:
|
||||||
|
return invalid("properties_recovery_not_converged")
|
||||||
|
|
||||||
|
internal_energy_density_derivative = (
|
||||||
|
props.p - props.T * pressure_temperature_derivative
|
||||||
|
) / (props.rho * props.rho)
|
||||||
|
drho: list[float] = []
|
||||||
|
du: list[float] = []
|
||||||
|
dT: list[float] = []
|
||||||
|
dp: list[float] = []
|
||||||
|
dh: list[float] = []
|
||||||
|
for mass_tangent, energy_tangent, volume_tangent in zip(
|
||||||
|
dm_values,
|
||||||
|
dU_values,
|
||||||
|
dV_values,
|
||||||
|
strict=True,
|
||||||
|
):
|
||||||
|
density_tangent = (
|
||||||
|
mass_tangent / V - m * volume_tangent / (V * V)
|
||||||
|
)
|
||||||
|
internal_energy_tangent = (
|
||||||
|
energy_tangent / m - U * mass_tangent / (m * m)
|
||||||
|
)
|
||||||
|
temperature_tangent = (
|
||||||
|
internal_energy_tangent
|
||||||
|
- internal_energy_density_derivative * density_tangent
|
||||||
|
) / cv
|
||||||
|
pressure_tangent = (
|
||||||
|
pressure_temperature_derivative * temperature_tangent
|
||||||
|
+ pressure_density_derivative * density_tangent
|
||||||
|
)
|
||||||
|
enthalpy_tangent = (
|
||||||
|
internal_energy_tangent
|
||||||
|
+ pressure_tangent / props.rho
|
||||||
|
- props.p * density_tangent / (props.rho * props.rho)
|
||||||
|
)
|
||||||
|
drho.append(density_tangent)
|
||||||
|
du.append(internal_energy_tangent)
|
||||||
|
dT.append(temperature_tangent)
|
||||||
|
dp.append(pressure_tangent)
|
||||||
|
dh.append(enthalpy_tangent)
|
||||||
|
|
||||||
|
tangent_values = (*drho, *du, *dT, *dp, *dh)
|
||||||
|
if not all(isfinite(value) for value in tangent_values):
|
||||||
|
return invalid("non_finite_property_tangent")
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents(
|
||||||
|
p=tuple(dp),
|
||||||
|
T=tuple(dT),
|
||||||
|
rho=tuple(drho),
|
||||||
|
u=tuple(du),
|
||||||
|
h=tuple(dh),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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,355 @@
|
|||||||
|
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,
|
||||||
|
ParameterGroupDisplaySpec,
|
||||||
|
PortDisplaySpec,
|
||||||
|
)
|
||||||
|
from app.simulation.core.metadata import (
|
||||||
|
ParameterCondition,
|
||||||
|
ParameterDefinition,
|
||||||
|
ParameterOption,
|
||||||
|
ResultVariableDefinition,
|
||||||
|
)
|
||||||
|
from app.simulation.core.medium import IdealGasMedium
|
||||||
|
from app.simulation.core.ports import PortDefinition
|
||||||
|
|
||||||
|
|
||||||
|
def _ud00_stage_parameters(index: int) -> tuple[ParameterDefinition, ...]:
|
||||||
|
visible_when = (
|
||||||
|
()
|
||||||
|
if index == 1
|
||||||
|
else (
|
||||||
|
ParameterCondition(
|
||||||
|
"nstages",
|
||||||
|
tuple(float(stage_count) for stage_count in range(index, 9)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
ParameterDefinition(
|
||||||
|
f"start{index}",
|
||||||
|
0.0 if index == 1 else 1.0,
|
||||||
|
label=f"第 {index} 段起点",
|
||||||
|
quantity="dimensionless",
|
||||||
|
unit="",
|
||||||
|
description=f"第 {index} 段开始时的输出值。",
|
||||||
|
visible_when=visible_when,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
f"end{index}",
|
||||||
|
1.0,
|
||||||
|
label=f"第 {index} 段终点",
|
||||||
|
quantity="dimensionless",
|
||||||
|
unit="",
|
||||||
|
description=f"第 {index} 段结束时的输出值。",
|
||||||
|
visible_when=visible_when,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
f"t{index}",
|
||||||
|
1.0 if index == 1 else 0.0,
|
||||||
|
label=f"第 {index} 段时长",
|
||||||
|
quantity="time",
|
||||||
|
unit="s",
|
||||||
|
minimum=0.0,
|
||||||
|
description=f"第 {index} 段的持续时间。",
|
||||||
|
visible_when=visible_when,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_UD00_STAGE_PARAMETERS = tuple(
|
||||||
|
parameter
|
||||||
|
for stage_index in range(1, 9)
|
||||||
|
for parameter in _ud00_stage_parameters(stage_index)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
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.2.0"
|
||||||
|
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
|
||||||
|
PARAMETERS = (
|
||||||
|
ParameterDefinition(
|
||||||
|
"tstart",
|
||||||
|
0.0,
|
||||||
|
label="启动时间",
|
||||||
|
quantity="time",
|
||||||
|
unit="s",
|
||||||
|
description="分段信号开始输出第一段之前的等待时间。",
|
||||||
|
),
|
||||||
|
*_UD00_STAGE_PARAMETERS,
|
||||||
|
ParameterDefinition(
|
||||||
|
"nstages",
|
||||||
|
1.0,
|
||||||
|
label="段数",
|
||||||
|
quantity="dimensionless",
|
||||||
|
unit="",
|
||||||
|
minimum=1.0,
|
||||||
|
maximum=8.0,
|
||||||
|
editor="choice",
|
||||||
|
options=tuple(
|
||||||
|
ParameterOption(float(stage_count), str(stage_count))
|
||||||
|
for stage_count in range(1, 9)
|
||||||
|
),
|
||||||
|
description="参与输出计算的有效线性分段数量。",
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"iscyclic",
|
||||||
|
0.0,
|
||||||
|
label="循环",
|
||||||
|
quantity="dimensionless",
|
||||||
|
unit="",
|
||||||
|
minimum=0.0,
|
||||||
|
maximum=1.0,
|
||||||
|
editor="choice",
|
||||||
|
options=(
|
||||||
|
ParameterOption(0.0, "否"),
|
||||||
|
ParameterOption(1.0, "是"),
|
||||||
|
),
|
||||||
|
description="当前公共协议编码:0 表示单次输出,1 表示循环输出。",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
parameter_groups=(
|
||||||
|
ParameterGroupDisplaySpec(
|
||||||
|
id="stages",
|
||||||
|
label="分段参数",
|
||||||
|
parameters=tuple(
|
||||||
|
parameter.name for parameter in _UD00_STAGE_PARAMETERS
|
||||||
|
),
|
||||||
|
order=10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
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"]
|
||||||
|
definitions = {definition.name: definition for definition in cls.PARAMETERS}
|
||||||
|
for parameter_name, value in (
|
||||||
|
("nstages", nstages),
|
||||||
|
("iscyclic", iscyclic),
|
||||||
|
):
|
||||||
|
numeric_value = float(value)
|
||||||
|
if not numeric_value.is_integer():
|
||||||
|
raise ValueError(f"UD00 {parameter_name} must be an integer.")
|
||||||
|
message = definitions[parameter_name].validation_message(numeric_value)
|
||||||
|
if message is not None:
|
||||||
|
raise ValueError(f"UD00 {parameter_name} {message}.")
|
||||||
|
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,690 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping, Sequence
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from math import isfinite
|
||||||
|
|
||||||
|
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,
|
||||||
|
ThermodynamicPropertiesLinearization,
|
||||||
|
)
|
||||||
|
from app.simulation.core.ports import PortDefinition
|
||||||
|
from app.simulation.core.state import VolumeState
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Pnch012DerivativeLinearization:
|
||||||
|
derivative: tuple[float, float]
|
||||||
|
tangents: tuple[tuple[float, ...], tuple[float, ...]]
|
||||||
|
properties: ThermodynamicPropertiesLinearization
|
||||||
|
valid: bool = True
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
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_at_pressure(self.p0, self.T0)
|
||||||
|
self.state = VolumeState(m=m0, U=U0)
|
||||||
|
initial_h = medium.specific_enthalpy_at_pressure(self.p0, 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_values(self) -> tuple[float, ...]:
|
||||||
|
pressure = self.medium.properties_from_mU(
|
||||||
|
self.state.m,
|
||||||
|
self.state.U,
|
||||||
|
self.cvol,
|
||||||
|
).p
|
||||||
|
return (
|
||||||
|
self.port_1.p - pressure,
|
||||||
|
self.port_2.p - pressure,
|
||||||
|
)
|
||||||
|
|
||||||
|
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. Fixed/prescribed contributions remain available as SI
|
||||||
|
parameters, while connected moving-boundary components can now add live
|
||||||
|
volume and volume-rate values through the pneumatic connector contract.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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_at_pressure(self.p0, self.T0)
|
||||||
|
self.state = VolumeState(m=m0, U=U0)
|
||||||
|
initial_h = medium.specific_enthalpy_at_pressure(self.p0, 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 connected_external_volume(self) -> float:
|
||||||
|
return sum(
|
||||||
|
getattr(getattr(self, port_name, None), "volume", 0.0)
|
||||||
|
for port_name in self.external_volumes
|
||||||
|
)
|
||||||
|
|
||||||
|
def connected_external_volume_rate(self) -> float:
|
||||||
|
return sum(
|
||||||
|
getattr(getattr(self, port_name, None), "volume_flow", 0.0)
|
||||||
|
for port_name in self.external_volume_rates
|
||||||
|
)
|
||||||
|
|
||||||
|
def total_volume(self) -> float:
|
||||||
|
minimum_volume = self.cvol0 / 100.0
|
||||||
|
return max(
|
||||||
|
self.cvol0 + sum(self.external_volumes.values()) + self.connected_external_volume(),
|
||||||
|
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()) + self.connected_external_volume_rate()
|
||||||
|
|
||||||
|
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 linearize_state_derivative(
|
||||||
|
self,
|
||||||
|
connected_h: Mapping[str, float],
|
||||||
|
*,
|
||||||
|
state_mass_tangent: Sequence[float],
|
||||||
|
state_energy_tangent: Sequence[float],
|
||||||
|
external_volume_tangent: Sequence[float],
|
||||||
|
external_volume_rate_tangent: Sequence[float],
|
||||||
|
port_mass_flow_tangents: Mapping[str, Sequence[float]],
|
||||||
|
connected_h_tangents: Mapping[str, Sequence[float]],
|
||||||
|
property_linearization: ThermodynamicPropertiesLinearization | None = None,
|
||||||
|
flow_boundary_tolerance: float = 1.0e-12,
|
||||||
|
) -> Pnch012DerivativeLinearization:
|
||||||
|
"""Linearize the chamber balance while keeping stream modes fixed."""
|
||||||
|
|
||||||
|
port_names = ("port_1", "port_2", "port_3", "port_4")
|
||||||
|
vectors = {
|
||||||
|
"state_mass": tuple(float(value) for value in state_mass_tangent),
|
||||||
|
"state_energy": tuple(float(value) for value in state_energy_tangent),
|
||||||
|
"volume": tuple(float(value) for value in external_volume_tangent),
|
||||||
|
"volume_rate": tuple(
|
||||||
|
float(value) for value in external_volume_rate_tangent
|
||||||
|
),
|
||||||
|
}
|
||||||
|
for port_name in port_names:
|
||||||
|
vectors[f"flow:{port_name}"] = tuple(
|
||||||
|
float(value) for value in port_mass_flow_tangents[port_name]
|
||||||
|
)
|
||||||
|
vectors[f"enthalpy:{port_name}"] = tuple(
|
||||||
|
float(value) for value in connected_h_tangents[port_name]
|
||||||
|
)
|
||||||
|
widths = {len(values) for values in vectors.values()}
|
||||||
|
if len(widths) != 1:
|
||||||
|
raise ValueError("PNCH012 tangent vectors must have equal lengths.")
|
||||||
|
width = len(vectors["state_mass"])
|
||||||
|
invalid_reason: str | None = None
|
||||||
|
if not all(isfinite(value) for values in vectors.values() for value in values):
|
||||||
|
invalid_reason = "non_finite_tangent_input"
|
||||||
|
|
||||||
|
raw_volume = (
|
||||||
|
self.cvol0
|
||||||
|
+ sum(self.external_volumes.values())
|
||||||
|
+ self.connected_external_volume()
|
||||||
|
)
|
||||||
|
minimum_volume = self.cvol0 / 100.0
|
||||||
|
volume_scale = max(abs(raw_volume), abs(minimum_volume), 1.0e-18)
|
||||||
|
on_volume_boundary = (
|
||||||
|
abs(raw_volume - minimum_volume) <= 1.0e-12 * volume_scale
|
||||||
|
)
|
||||||
|
supplied_volume_tangent = vectors["volume"]
|
||||||
|
if raw_volume < minimum_volume or on_volume_boundary:
|
||||||
|
used_volume_tangent = (0.0,) * width
|
||||||
|
used_volume_rate_tangent = (0.0,) * width
|
||||||
|
if on_volume_boundary and any(
|
||||||
|
value != 0.0
|
||||||
|
for value in (
|
||||||
|
*supplied_volume_tangent,
|
||||||
|
*vectors["volume_rate"],
|
||||||
|
)
|
||||||
|
):
|
||||||
|
invalid_reason = invalid_reason or "volume_floor_boundary"
|
||||||
|
else:
|
||||||
|
used_volume_tangent = supplied_volume_tangent
|
||||||
|
used_volume_rate_tangent = vectors["volume_rate"]
|
||||||
|
|
||||||
|
properties = property_linearization or self.medium.linearize_properties_from_mU(
|
||||||
|
self.state.m,
|
||||||
|
self.state.U,
|
||||||
|
self.total_volume(),
|
||||||
|
vectors["state_mass"],
|
||||||
|
vectors["state_energy"],
|
||||||
|
used_volume_tangent,
|
||||||
|
)
|
||||||
|
if properties.tangents.width != width:
|
||||||
|
raise ValueError(
|
||||||
|
"PNCH012 property tangent width must match balance tangents."
|
||||||
|
)
|
||||||
|
props = properties.properties
|
||||||
|
if not properties.valid:
|
||||||
|
invalid_reason = invalid_reason or properties.reason
|
||||||
|
|
||||||
|
mass_derivative = sum(
|
||||||
|
self.get_port(port_name).m_flow for port_name in port_names
|
||||||
|
)
|
||||||
|
volume_rate = self.total_volume_rate()
|
||||||
|
energy_derivative = self.thermal_energy_flow_w(props.T) - props.p * volume_rate
|
||||||
|
mass_tangent = [0.0] * width
|
||||||
|
energy_tangent = [
|
||||||
|
-self.kth * self.sth * properties.tangents.T[index]
|
||||||
|
- volume_rate * properties.tangents.p[index]
|
||||||
|
- props.p * used_volume_rate_tangent[index]
|
||||||
|
for index in range(width)
|
||||||
|
]
|
||||||
|
|
||||||
|
for port_name in port_names:
|
||||||
|
port = self.get_port(port_name)
|
||||||
|
flow_tangent = vectors[f"flow:{port_name}"]
|
||||||
|
if (
|
||||||
|
abs(port.m_flow) <= flow_boundary_tolerance
|
||||||
|
and any(value != 0.0 for value in flow_tangent)
|
||||||
|
):
|
||||||
|
invalid_reason = invalid_reason or (
|
||||||
|
f"flow_direction_boundary:{port_name}"
|
||||||
|
)
|
||||||
|
if port.m_flow > 0.0:
|
||||||
|
inlet_h = connected_h[port_name]
|
||||||
|
inlet_h_tangent = vectors[f"enthalpy:{port_name}"]
|
||||||
|
else:
|
||||||
|
inlet_h = props.h
|
||||||
|
inlet_h_tangent = properties.tangents.h
|
||||||
|
energy_derivative += port.m_flow * inlet_h
|
||||||
|
for index in range(width):
|
||||||
|
mass_tangent[index] += flow_tangent[index]
|
||||||
|
energy_tangent[index] += (
|
||||||
|
inlet_h * flow_tangent[index]
|
||||||
|
+ port.m_flow * inlet_h_tangent[index]
|
||||||
|
)
|
||||||
|
|
||||||
|
return Pnch012DerivativeLinearization(
|
||||||
|
derivative=(mass_derivative, energy_derivative),
|
||||||
|
tangents=(tuple(mass_tangent), tuple(energy_tangent)),
|
||||||
|
properties=properties,
|
||||||
|
valid=invalid_reason is None,
|
||||||
|
reason=invalid_reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
pressure = self.medium.properties_from_mU(
|
||||||
|
self.state.m,
|
||||||
|
self.state.U,
|
||||||
|
self.total_volume(),
|
||||||
|
).p
|
||||||
|
return tuple(
|
||||||
|
self.get_port(port_name).p - pressure
|
||||||
|
for port_name in ("port_1", "port_2", "port_3", "port_4")
|
||||||
|
)
|
||||||
|
|
||||||
|
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")
|
||||||
|
)
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
# 元件建模规范与示例
|
||||||
|
|
||||||
|
规范的权威版本位于
|
||||||
|
[`docs/standard/component-model-authoring-spec-v1.md`](../../../docs/standard/component-model-authoring-spec-v1.md)。
|
||||||
|
本文档保留在组件目录中,作为离模型源码最近的完整示例;若两者不一致,应在同一次
|
||||||
|
修改中同步,不能让示例形成另一套规则。
|
||||||
|
|
||||||
|
本文档是 `app/simulation/components` 下新增元件的最小开发规范。当前
|
||||||
|
`experimental` 是用于验证规范的临时组件库;后续正式模型应建立独立组件库,
|
||||||
|
不要继续堆放在 `experimental` 中。
|
||||||
|
|
||||||
|
目标是让元件的端口、输入参数和可展示结果都由元件类显式声明,避免 XML
|
||||||
|
校验、求解器和前端分别维护同一份含义。
|
||||||
|
|
||||||
|
## 一、元件类必须声明的内容
|
||||||
|
|
||||||
|
每个对外注册的元件类至少需要声明以下六个类属性:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MODEL_TYPE = "example_component"
|
||||||
|
MODEL_VERSION = "1.0.0"
|
||||||
|
PORTS = (...)
|
||||||
|
PARAMETERS = (...)
|
||||||
|
RESULT_VARIABLES = (...)
|
||||||
|
DISPLAY = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
- `MODEL_TYPE`:稳定的模型类型标识,对应 System XML 中的 `Component/@type`。发布后不要随意改名。
|
||||||
|
- `MODEL_VERSION`:模型契约版本,采用 `主版本.次版本.修订版本`。
|
||||||
|
- `PORTS`:端口契约,包括端口名、物理域、变量和正流量方向。
|
||||||
|
- `PARAMETERS`:用户可配置的输入参数,包括默认值、物理量、SI 单位和取值范围。
|
||||||
|
- `RESULT_VARIABLES`:允许写入仿真结果并显示在结果页的组件级变量。端口结果由 `PORTS` 中的端口变量定义自动生成。
|
||||||
|
- `DISPLAY`:组件库名称、分类、图标、排序和端口画布位置,不参与物理求解。
|
||||||
|
|
||||||
|
元件构造函数还必须:
|
||||||
|
|
||||||
|
1. 调用 `super().__init__(name)`。
|
||||||
|
2. 使用 `set_parameter_values()` 保存规范化后的输入参数。
|
||||||
|
3. 使用 `register_declared_port()` 创建已声明端口。
|
||||||
|
4. 若声明了组件结果变量,实现 `component_result_values()` 并返回对应数值;标准热力学容腔可以直接继承 `ThermodynamicVolumeComponent` 的实现。
|
||||||
|
5. 实现统一的类方法 `create()`,接收规范化后的 SI 参数。
|
||||||
|
|
||||||
|
## 二、输入参数与结果变量
|
||||||
|
|
||||||
|
输入参数和仿真结果必须分开声明:
|
||||||
|
|
||||||
|
- 输入参数描述一次仿真开始前由用户配置的量,例如 `volume`、`p0`、`T0`。
|
||||||
|
- 结果变量描述随时间变化、允许绘图的量,例如 `p`、`T`、`m`、`m_flow`。
|
||||||
|
- 求解器缓存、中间残差和调试字段不得自动暴露为结果变量。
|
||||||
|
- 参数名和结果变量名使用稳定的英文机器标识;`label` 专门用于界面显示。
|
||||||
|
|
||||||
|
参数定义示例:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ParameterDefinition(
|
||||||
|
name="volume",
|
||||||
|
label="容积",
|
||||||
|
quantity="volume",
|
||||||
|
unit="m3",
|
||||||
|
default=0.1,
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
结果变量定义示例:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ResultVariableDefinition(
|
||||||
|
name="p",
|
||||||
|
label="压力",
|
||||||
|
quantity="pressure",
|
||||||
|
unit="Pa",
|
||||||
|
category="thermodynamic",
|
||||||
|
order=30,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 三、命名和单位约定
|
||||||
|
|
||||||
|
- 模型类型、参数、端口和变量名使用 `snake_case`,已有热力学惯例 `T`、`U` 可以保留。
|
||||||
|
- 输入参数保存和计算统一使用 SI 基准值;界面单位换算不能改变后端存储值。
|
||||||
|
- 无量纲参数的 `unit` 使用空字符串。
|
||||||
|
- `quantity` 表示稳定的物理量类型,例如 `pressure`、`temperature`、`mass_flow`,不能使用界面文案代替。
|
||||||
|
- 正质量流量统一定义为流入元件,即 `positiveFlowDirection="intoComponent"`。
|
||||||
|
- 端口变量 `p`、`m_flow`、`h_outflow` 的连接规则由 `PortDefinition.pneumatic()` 统一提供。
|
||||||
|
|
||||||
|
## 四、完整示例:单端口储气容腔
|
||||||
|
|
||||||
|
下面的示例展示一个可直接接入当前框架的动态元件。真实新增元件时应放入独立的 `.py` 文件,并补充对应测试。
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
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,
|
||||||
|
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||||
|
)
|
||||||
|
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
|
||||||
|
from app.simulation.core.ports import PortDefinition
|
||||||
|
from app.simulation.core.state import VolumeState
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleVolume(ThermodynamicVolumeComponent):
|
||||||
|
MODEL_TYPE = "example_volume"
|
||||||
|
MODEL_VERSION = "1.0.0"
|
||||||
|
PORTS = (
|
||||||
|
PortDefinition.pneumatic("port_a", nominal_role="bidirectional"),
|
||||||
|
)
|
||||||
|
PARAMETERS = (
|
||||||
|
ParameterDefinition(
|
||||||
|
name="volume",
|
||||||
|
label="容积",
|
||||||
|
quantity="volume",
|
||||||
|
unit="m3",
|
||||||
|
default=0.1,
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
name="p0",
|
||||||
|
label="初始压力",
|
||||||
|
quantity="pressure",
|
||||||
|
unit="Pa",
|
||||||
|
default=100000.0,
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
name="T0",
|
||||||
|
label="初始温度",
|
||||||
|
quantity="temperature",
|
||||||
|
unit="K",
|
||||||
|
default=300.0,
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||||
|
DISPLAY = ComponentDisplaySpec(
|
||||||
|
label="示例容腔",
|
||||||
|
library_id="experimental",
|
||||||
|
category_id="storage",
|
||||||
|
symbol="generic",
|
||||||
|
ports=(PortDisplaySpec("port_a", "left"),),
|
||||||
|
order=90,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
medium: IdealGasMedium,
|
||||||
|
volume: float = 0.1,
|
||||||
|
p0: float = 100000.0,
|
||||||
|
T0: float = 300.0,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(name)
|
||||||
|
self.set_parameter_values(
|
||||||
|
{"volume": volume, "p0": p0, "T0": T0}
|
||||||
|
)
|
||||||
|
self.medium = medium
|
||||||
|
self.V = volume
|
||||||
|
initial_mass = p0 * volume / (medium.R_gas * T0)
|
||||||
|
initial_energy = initial_mass * medium.specific_internal_energy(T0)
|
||||||
|
self.state = VolumeState(m=initial_mass, U=initial_energy)
|
||||||
|
self.port_a = self.register_declared_port("port_a")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
medium: IdealGasMedium,
|
||||||
|
parameters: Mapping[str, float],
|
||||||
|
) -> ExampleVolume:
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
medium=medium,
|
||||||
|
volume=parameters["volume"],
|
||||||
|
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 refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||||
|
properties = self.medium.properties_from_mU(
|
||||||
|
self.state.m, self.state.U, self.V
|
||||||
|
)
|
||||||
|
self.port_a.p = properties.p
|
||||||
|
self.port_a.h_outflow = properties.h
|
||||||
|
return properties
|
||||||
|
|
||||||
|
def state_derivative_from_ports(
|
||||||
|
self,
|
||||||
|
connected_h: Mapping[str, float],
|
||||||
|
) -> list[float]:
|
||||||
|
properties = self.refresh_thermodynamic_ports()
|
||||||
|
inlet_h = self.connection_inlet_enthalpy(
|
||||||
|
port_m_flow=self.port_a.m_flow,
|
||||||
|
connected_h=connected_h["port_a"],
|
||||||
|
internal_h=properties.h,
|
||||||
|
)
|
||||||
|
return [self.port_a.m_flow, self.port_a.m_flow * inlet_h]
|
||||||
|
|
||||||
|
def pressure_flow_equation_residuals(
|
||||||
|
self,
|
||||||
|
) -> tuple[EquationResidual, ...]:
|
||||||
|
pressure = self.medium.properties_from_mU(
|
||||||
|
self.state.m, self.state.U, self.V
|
||||||
|
).p
|
||||||
|
return (
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:port_a_pressure_state",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="state",
|
||||||
|
variables=(f"{self.name}.port_a.p", f"{self.name}.state"),
|
||||||
|
role="effort",
|
||||||
|
value=self.port_a.p - pressure,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
模型文件不再直接修改全局注册表。完成模型类后,只把类路径加入所属库
|
||||||
|
`library.py` 的 `models` 清单:
|
||||||
|
|
||||||
|
```python
|
||||||
|
models=(
|
||||||
|
# ...已有模型
|
||||||
|
"app.simulation.components.experimental.storage.example_volume:ExampleVolume",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
后端会受控导入清单中的类,校验版本、分类、端口、参数、单位、显示信息和默认实例,
|
||||||
|
再自动建立注册表。校验通过后,`GET /api/components/catalog` 会输出该元件,
|
||||||
|
前端刷新时即可加载。
|
||||||
|
当前 `experimental` 仅用于规范验证;正式模型应先建立新的库声明,再把
|
||||||
|
`library_id` 指向正式库。
|
||||||
|
|
||||||
|
完成仿真后,每个已声明结果都会得到一条结构化元数据。前端应按字段筛选,不能再拆解 `key` 猜测含义:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"key": "example_volume_1.port_a.m_flow",
|
||||||
|
"componentId": "example_volume_1",
|
||||||
|
"componentType": "example_volume",
|
||||||
|
"scope": "port",
|
||||||
|
"portName": "port_a",
|
||||||
|
"name": "m_flow",
|
||||||
|
"label": "质量流量",
|
||||||
|
"quantity": "mass_flow",
|
||||||
|
"unit": "kg/s",
|
||||||
|
"category": "flow",
|
||||||
|
"order": 20
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、新增元件检查清单
|
||||||
|
|
||||||
|
1. `MODEL_TYPE` 是否唯一,并与 XML 的模型类型一致。
|
||||||
|
2. 所有构造参数是否在 `PARAMETERS` 中声明并保存。
|
||||||
|
3. 所有端口是否在 `PORTS` 中声明并通过 `register_declared_port()` 创建。
|
||||||
|
4. `RESULT_VARIABLES` 与 `component_result_values()` 的键是否完全一致。
|
||||||
|
5. 结果变量是否包含明确的 `quantity`、`label`、`unit` 和显示顺序。
|
||||||
|
6. 是否只暴露有工程意义的结果,而非内部计算变量。
|
||||||
|
7. `MODEL_VERSION` 和 `DISPLAY` 是否完整,显示端口是否与物理端口完全一致。
|
||||||
|
8. 是否实现统一的 `create()`,并能用默认参数创建模型。
|
||||||
|
9. 模型类路径是否只加入所属库的 `library.py` 清单。
|
||||||
|
10. 是否补充参数边界、端口契约、目录输出、结果元数据和最小仿真的自动测试。
|
||||||
|
|
||||||
|
组件库、分类和自动发现的完整规则参见
|
||||||
|
[`组件库分类、发现与读取规范 v1`](../../../docs/standard/component-library-spec-v1.md)。
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""Temporary component library used to validate the model authoring contract."""
|
||||||
|
|
||||||
|
from app.simulation.components.experimental.library import LIBRARY
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Flow-path and resistance components."""
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from math import sqrt
|
||||||
|
|
||||||
|
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
|
||||||
|
from app.simulation.core.medium import IdealGasMedium
|
||||||
|
from app.simulation.core.ports import PortDefinition
|
||||||
|
|
||||||
|
|
||||||
|
class Orifice(AlgebraicComponent):
|
||||||
|
"""Python port of ModelicaModels.Myorifice."""
|
||||||
|
|
||||||
|
MODEL_TYPE = "orifice"
|
||||||
|
MODEL_VERSION = "1.0.0"
|
||||||
|
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||||
|
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||||||
|
("mass_flow_balance",)
|
||||||
|
)
|
||||||
|
PORTS = (
|
||||||
|
PortDefinition.pneumatic("port_a", nominal_role="inlet"),
|
||||||
|
PortDefinition.pneumatic("port_b", nominal_role="outlet"),
|
||||||
|
)
|
||||||
|
PARAMETERS = (
|
||||||
|
ParameterDefinition(
|
||||||
|
"K",
|
||||||
|
1e-5,
|
||||||
|
label="流量系数",
|
||||||
|
quantity="flow_coefficient",
|
||||||
|
unit="kg/(s*Pa^0.5)",
|
||||||
|
minimum=0.0,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"opening",
|
||||||
|
1.0,
|
||||||
|
label="开度",
|
||||||
|
minimum=0.0,
|
||||||
|
maximum=1.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
RESULT_VARIABLES = ()
|
||||||
|
DISPLAY = ComponentDisplaySpec(
|
||||||
|
label="孔板/阀门",
|
||||||
|
library_id="experimental",
|
||||||
|
category_id="flow",
|
||||||
|
symbol="orifice",
|
||||||
|
ports=(
|
||||||
|
PortDisplaySpec("port_a", "left", order=10),
|
||||||
|
PortDisplaySpec("port_b", "right", order=20),
|
||||||
|
),
|
||||||
|
order=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, name: str, opening: float = 1.0, K: float = 1e-5) -> None:
|
||||||
|
super().__init__(name=name)
|
||||||
|
self.set_parameter_values({"K": K, "opening": opening})
|
||||||
|
self.opening = opening
|
||||||
|
self.K = K
|
||||||
|
self.port_a = self.register_declared_port("port_a")
|
||||||
|
self.port_b = self.register_declared_port("port_b")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
medium: IdealGasMedium,
|
||||||
|
parameters: Mapping[str, float],
|
||||||
|
) -> Orifice:
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
opening=parameters["opening"],
|
||||||
|
K=parameters["K"],
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def K_eff(self) -> float:
|
||||||
|
return self.K * max(self.opening, 0.001)
|
||||||
|
|
||||||
|
def mass_flow(self, p_a: float, p_b: float) -> float:
|
||||||
|
dp = p_a - p_b
|
||||||
|
if dp == 0.0:
|
||||||
|
return 0.0
|
||||||
|
return self.K_eff * sqrt(abs(dp)) * (1.0 if dp > 0.0 else -1.0)
|
||||||
|
|
||||||
|
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_a.m_flow",
|
||||||
|
f"{self.name}.port_b.m_flow",
|
||||||
|
),
|
||||||
|
role="flow",
|
||||||
|
value=self.port_a.m_flow + self.port_b.m_flow,
|
||||||
|
),
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:pressure_flow_relation",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="constitutive",
|
||||||
|
variables=(
|
||||||
|
f"{self.name}.port_a.p",
|
||||||
|
f"{self.name}.port_b.p",
|
||||||
|
f"{self.name}.port_a.m_flow",
|
||||||
|
),
|
||||||
|
role="flow",
|
||||||
|
value=self.port_a.m_flow
|
||||||
|
- self.mass_flow(self.port_a.p, self.port_b.p),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||||
|
self.port_a.h_outflow = connected_h["port_b"]
|
||||||
|
self.port_b.h_outflow = connected_h["port_a"]
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
"""Compatibility import for the TestModel-only dynamic pipe.
|
||||||
|
|
||||||
|
The public ``pipe`` catalog model is ``ResistivePipe``. New code should import
|
||||||
|
this legacy dynamic model from ``app.simulation.examples.testmodel.dynamic_pipe``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from app.simulation.examples.testmodel.dynamic_pipe import Pipe
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ("Pipe",)
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
from math import pi
|
||||||
|
|
||||||
|
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
|
||||||
|
from app.simulation.core.medium import IdealGasMedium
|
||||||
|
from app.simulation.core.ports import PortDefinition
|
||||||
|
|
||||||
|
|
||||||
|
class ResistivePipe(AlgebraicComponent):
|
||||||
|
"""Quasi-steady Darcy resistance used by topology-driven simulation."""
|
||||||
|
|
||||||
|
MODEL_TYPE = "pipe"
|
||||||
|
MODEL_VERSION = "1.0.0"
|
||||||
|
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||||
|
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||||||
|
("mass_flow_balance",)
|
||||||
|
)
|
||||||
|
PORTS = (
|
||||||
|
PortDefinition.pneumatic("port_a", nominal_role="inlet"),
|
||||||
|
PortDefinition.pneumatic("port_b", nominal_role="outlet"),
|
||||||
|
)
|
||||||
|
PARAMETERS = (
|
||||||
|
ParameterDefinition(
|
||||||
|
"length",
|
||||||
|
5.0,
|
||||||
|
label="长度",
|
||||||
|
quantity="length",
|
||||||
|
unit="m",
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"diameter",
|
||||||
|
0.02,
|
||||||
|
label="直径",
|
||||||
|
quantity="length",
|
||||||
|
unit="m",
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"lambda_darcy",
|
||||||
|
0.02,
|
||||||
|
label="摩阻系数",
|
||||||
|
minimum=0.0,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"p0",
|
||||||
|
1e5,
|
||||||
|
label="初始压力",
|
||||||
|
quantity="pressure",
|
||||||
|
unit="Pa",
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"T0",
|
||||||
|
300.0,
|
||||||
|
label="初始温度",
|
||||||
|
quantity="temperature",
|
||||||
|
unit="K",
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
RESULT_VARIABLES = ()
|
||||||
|
DISPLAY = ComponentDisplaySpec(
|
||||||
|
label="管段",
|
||||||
|
library_id="experimental",
|
||||||
|
category_id="flow",
|
||||||
|
symbol="pipe",
|
||||||
|
ports=(
|
||||||
|
PortDisplaySpec("port_a", "left", order=10),
|
||||||
|
PortDisplaySpec("port_b", "right", order=20),
|
||||||
|
),
|
||||||
|
order=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
medium: IdealGasMedium,
|
||||||
|
L: float = 5.0,
|
||||||
|
D: float = 0.02,
|
||||||
|
lambda_darcy: float = 0.02,
|
||||||
|
p0: float = 1e5,
|
||||||
|
T0: float = 300.0,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(name=name)
|
||||||
|
self.set_parameter_values(
|
||||||
|
{
|
||||||
|
"length": L,
|
||||||
|
"diameter": D,
|
||||||
|
"lambda_darcy": lambda_darcy,
|
||||||
|
"p0": p0,
|
||||||
|
"T0": T0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.medium = medium
|
||||||
|
self.L = L
|
||||||
|
self.D = D
|
||||||
|
self.lambda_darcy = lambda_darcy
|
||||||
|
self.p0 = p0
|
||||||
|
self.T0 = T0
|
||||||
|
self.area = pi * D * D / 4.0
|
||||||
|
initial_h = medium.specific_enthalpy(T0)
|
||||||
|
|
||||||
|
self.port_a = self.register_declared_port("port_a")
|
||||||
|
self.port_a.p = p0
|
||||||
|
self.port_a.h_outflow = initial_h
|
||||||
|
|
||||||
|
self.port_b = self.register_declared_port("port_b")
|
||||||
|
self.port_b.p = p0
|
||||||
|
self.port_b.h_outflow = initial_h
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
medium: IdealGasMedium,
|
||||||
|
parameters: Mapping[str, float],
|
||||||
|
) -> ResistivePipe:
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
medium=medium,
|
||||||
|
L=parameters["length"],
|
||||||
|
D=parameters["diameter"],
|
||||||
|
lambda_darcy=parameters["lambda_darcy"],
|
||||||
|
p0=parameters["p0"],
|
||||||
|
T0=parameters["T0"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def pressure_drop(self, m_flow_a: float, p_a: float, p_b: float) -> float:
|
||||||
|
average_pressure = max(0.5 * (p_a + p_b), 1.0)
|
||||||
|
density = max(self.medium.density(average_pressure, self.T0), 1e-12)
|
||||||
|
resistance = self.lambda_darcy * (self.L / self.D)
|
||||||
|
return (
|
||||||
|
resistance
|
||||||
|
* m_flow_a
|
||||||
|
* abs(m_flow_a)
|
||||||
|
/ (2.0 * density * self.area * self.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_a.m_flow",
|
||||||
|
f"{self.name}.port_b.m_flow",
|
||||||
|
),
|
||||||
|
role="flow",
|
||||||
|
value=self.port_a.m_flow + self.port_b.m_flow,
|
||||||
|
),
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:darcy_pressure_loss",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="constitutive",
|
||||||
|
variables=(
|
||||||
|
f"{self.name}.port_a.p",
|
||||||
|
f"{self.name}.port_b.p",
|
||||||
|
f"{self.name}.port_a.m_flow",
|
||||||
|
),
|
||||||
|
role="effort",
|
||||||
|
value=(
|
||||||
|
self.port_a.p
|
||||||
|
- self.port_b.p
|
||||||
|
- self.pressure_drop(
|
||||||
|
self.port_a.m_flow,
|
||||||
|
self.port_a.p,
|
||||||
|
self.port_b.p,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||||
|
self.port_a.h_outflow = connected_h["port_b"]
|
||||||
|
self.port_b.h_outflow = connected_h["port_a"]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Flow junction components."""
|
||||||
+103
-5
@@ -1,17 +1,115 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from PythonModels.core.base import AlgebraicComponent
|
from collections.abc import Mapping
|
||||||
from PythonModels.core.ports import PortState
|
|
||||||
|
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 Tee(AlgebraicComponent):
|
class Tee(AlgebraicComponent):
|
||||||
"""Python port of ModelicaModels.Mytee."""
|
"""Python port of ModelicaModels.Mytee."""
|
||||||
|
|
||||||
|
MODEL_TYPE = "tee"
|
||||||
|
MODEL_VERSION = "1.0.0"
|
||||||
|
PRESSURE_FLOW_DEPENDS_ON_STREAM = False
|
||||||
|
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES = frozenset(
|
||||||
|
("mass_flow_balance",)
|
||||||
|
)
|
||||||
|
PORTS = (
|
||||||
|
PortDefinition.pneumatic("port_in", nominal_role="bidirectional"),
|
||||||
|
PortDefinition.pneumatic("port_out1", nominal_role="bidirectional"),
|
||||||
|
PortDefinition.pneumatic("port_out2", nominal_role="bidirectional"),
|
||||||
|
)
|
||||||
|
PARAMETERS = ()
|
||||||
|
RESULT_VARIABLES = ()
|
||||||
|
DISPLAY = ComponentDisplaySpec(
|
||||||
|
label="三通",
|
||||||
|
library_id="experimental",
|
||||||
|
category_id="junctions",
|
||||||
|
symbol="tee",
|
||||||
|
ports=(
|
||||||
|
PortDisplaySpec("port_in", "left", order=10),
|
||||||
|
PortDisplaySpec("port_out1", "right", order=20),
|
||||||
|
PortDisplaySpec("port_out2", "right", order=30),
|
||||||
|
),
|
||||||
|
order=50,
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, name: str) -> None:
|
def __init__(self, name: str) -> None:
|
||||||
super().__init__(name=name)
|
super().__init__(name=name)
|
||||||
self.port_in = PortState()
|
self.set_parameter_values({})
|
||||||
self.port_out1 = PortState()
|
self.port_in = self.register_declared_port("port_in")
|
||||||
self.port_out2 = PortState()
|
self.port_out1 = self.register_declared_port("port_out1")
|
||||||
|
self.port_out2 = self.register_declared_port("port_out2")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
medium: IdealGasMedium,
|
||||||
|
parameters: Mapping[str, float],
|
||||||
|
) -> Tee:
|
||||||
|
return cls(name=name)
|
||||||
|
|
||||||
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
|
return (
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:common_pressure_out1",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="equal",
|
||||||
|
variables=(f"{self.name}.port_in.p", f"{self.name}.port_out1.p"),
|
||||||
|
role="effort",
|
||||||
|
value=self.port_in.p - self.port_out1.p,
|
||||||
|
),
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:common_pressure_out2",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="equal",
|
||||||
|
variables=(f"{self.name}.port_in.p", f"{self.name}.port_out2.p"),
|
||||||
|
role="effort",
|
||||||
|
value=self.port_in.p - self.port_out2.p,
|
||||||
|
),
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:mass_flow_balance",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="sumToZero",
|
||||||
|
variables=(
|
||||||
|
f"{self.name}.port_in.m_flow",
|
||||||
|
f"{self.name}.port_out1.m_flow",
|
||||||
|
f"{self.name}.port_out2.m_flow",
|
||||||
|
),
|
||||||
|
role="flow",
|
||||||
|
value=(
|
||||||
|
self.port_in.m_flow
|
||||||
|
+ self.port_out1.m_flow
|
||||||
|
+ self.port_out2.m_flow
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
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 * enthalpy for m_flow, enthalpy in incoming
|
||||||
|
) / total_flow
|
||||||
|
else:
|
||||||
|
values = list(connected_h.values())
|
||||||
|
mixed_h = sum(values) / len(values) if values else 0.0
|
||||||
|
for port in self.ports.values():
|
||||||
|
port.h_outflow = mixed_h
|
||||||
|
|
||||||
def mixed_inlet_enthalpy(
|
def mixed_inlet_enthalpy(
|
||||||
self,
|
self,
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Manifest for the temporary library used to validate component authoring."""
|
||||||
|
|
||||||
|
from app.simulation.core.catalog import (
|
||||||
|
ComponentCategorySpec,
|
||||||
|
ComponentLibrarySpec,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
LIBRARY = ComponentLibrarySpec(
|
||||||
|
id="experimental",
|
||||||
|
label="临时测试组件库",
|
||||||
|
version="0.1.0",
|
||||||
|
source_package="app.simulation.components.experimental",
|
||||||
|
temporary=True,
|
||||||
|
order=100,
|
||||||
|
categories=(
|
||||||
|
ComponentCategorySpec(id="storage", label="储能元件", order=10),
|
||||||
|
ComponentCategorySpec(id="flow", label="流动元件", order=20),
|
||||||
|
ComponentCategorySpec(id="junctions", label="连接元件", order=30),
|
||||||
|
),
|
||||||
|
models=(
|
||||||
|
"app.simulation.components.experimental.storage.cylinder:Cylinder",
|
||||||
|
"app.simulation.components.experimental.storage.tank:Tank",
|
||||||
|
"app.simulation.components.experimental.flow.resistive_pipe:ResistivePipe",
|
||||||
|
"app.simulation.components.experimental.flow.orifice:Orifice",
|
||||||
|
"app.simulation.components.experimental.junctions.tee:Tee",
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Storage and thermodynamic volume components."""
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
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,
|
||||||
|
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||||
|
)
|
||||||
|
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
|
||||||
|
from app.simulation.core.ports import PortDefinition
|
||||||
|
from app.simulation.core.state import VolumeState
|
||||||
|
|
||||||
|
|
||||||
|
class Cylinder(ThermodynamicVolumeComponent):
|
||||||
|
"""Python port of ModelicaModels.Mycylinder."""
|
||||||
|
|
||||||
|
MODEL_TYPE = "cylinder"
|
||||||
|
MODEL_VERSION = "1.0.0"
|
||||||
|
PORTS = (PortDefinition.pneumatic("port_b", nominal_role="outlet"),)
|
||||||
|
PARAMETERS = (
|
||||||
|
ParameterDefinition(
|
||||||
|
"volume",
|
||||||
|
0.01,
|
||||||
|
label="容积",
|
||||||
|
quantity="volume",
|
||||||
|
unit="m3",
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"p0",
|
||||||
|
35e6,
|
||||||
|
label="初始压力",
|
||||||
|
quantity="pressure",
|
||||||
|
unit="Pa",
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"T0",
|
||||||
|
300.0,
|
||||||
|
label="初始温度",
|
||||||
|
quantity="temperature",
|
||||||
|
unit="K",
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||||
|
DISPLAY = ComponentDisplaySpec(
|
||||||
|
label="气瓶",
|
||||||
|
library_id="experimental",
|
||||||
|
category_id="storage",
|
||||||
|
symbol="cylinder",
|
||||||
|
ports=(PortDisplaySpec("port_b", "right"),),
|
||||||
|
order=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
medium: IdealGasMedium,
|
||||||
|
V: float = 0.01,
|
||||||
|
p0: float = 35e6,
|
||||||
|
T0: float = 300.0,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(name=name)
|
||||||
|
self.set_parameter_values({"volume": V, "p0": p0, "T0": T0})
|
||||||
|
self.medium = medium
|
||||||
|
self.V = V
|
||||||
|
m0 = p0 * V / (medium.R_gas * T0)
|
||||||
|
U0 = m0 * medium.specific_internal_energy(T0)
|
||||||
|
self.state = VolumeState(m=m0, U=U0)
|
||||||
|
self.port_b = self.register_declared_port("port_b")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
medium: IdealGasMedium,
|
||||||
|
parameters: Mapping[str, float],
|
||||||
|
) -> Cylinder:
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
medium=medium,
|
||||||
|
V=parameters["volume"],
|
||||||
|
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.V)
|
||||||
|
self.port_b.p = props.p
|
||||||
|
self.port_b.h_outflow = props.h
|
||||||
|
return props
|
||||||
|
|
||||||
|
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||||
|
return self.properties()
|
||||||
|
|
||||||
|
def state_derivative_from_ports(
|
||||||
|
self,
|
||||||
|
connected_h: Mapping[str, float],
|
||||||
|
) -> list[float]:
|
||||||
|
properties = self.properties()
|
||||||
|
derivative = self.derivatives_from_connection(
|
||||||
|
connected_h=connected_h["port_b"],
|
||||||
|
port_m_flow=self.port_b.m_flow,
|
||||||
|
internal_h=properties.h,
|
||||||
|
)
|
||||||
|
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.V,
|
||||||
|
).p
|
||||||
|
return (
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:port_b_pressure_state",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="state",
|
||||||
|
variables=(f"{self.name}.port_b.p", f"{self.name}.state"),
|
||||||
|
role="effort",
|
||||||
|
value=self.port_b.p - pressure,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def derivatives_from_connection(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
connected_h: float,
|
||||||
|
port_m_flow: float,
|
||||||
|
internal_h: float,
|
||||||
|
) -> VolumeState:
|
||||||
|
inlet_h = self.connection_inlet_enthalpy(
|
||||||
|
port_m_flow=port_m_flow,
|
||||||
|
connected_h=connected_h,
|
||||||
|
internal_h=internal_h,
|
||||||
|
)
|
||||||
|
return self.derivatives(inlet_h, port_m_flow)
|
||||||
|
|
||||||
|
def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState:
|
||||||
|
return VolumeState(m=m_flow, U=m_flow * inlet_h)
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
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,
|
||||||
|
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||||
|
)
|
||||||
|
from app.simulation.core.medium import IdealGasMedium, ThermodynamicProperties
|
||||||
|
from app.simulation.core.ports import PortDefinition
|
||||||
|
from app.simulation.core.state import VolumeState
|
||||||
|
|
||||||
|
|
||||||
|
class Tank(ThermodynamicVolumeComponent):
|
||||||
|
"""Python port of ModelicaModels.Mytank."""
|
||||||
|
|
||||||
|
MODEL_TYPE = "tank"
|
||||||
|
MODEL_VERSION = "1.0.0"
|
||||||
|
PORTS = (PortDefinition.pneumatic("port_a", nominal_role="inlet"),)
|
||||||
|
PARAMETERS = (
|
||||||
|
ParameterDefinition(
|
||||||
|
"volume",
|
||||||
|
0.1,
|
||||||
|
label="容积",
|
||||||
|
quantity="volume",
|
||||||
|
unit="m3",
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"p0",
|
||||||
|
1e5,
|
||||||
|
label="初始压力",
|
||||||
|
quantity="pressure",
|
||||||
|
unit="Pa",
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
ParameterDefinition(
|
||||||
|
"T0",
|
||||||
|
300.0,
|
||||||
|
label="初始温度",
|
||||||
|
quantity="temperature",
|
||||||
|
unit="K",
|
||||||
|
minimum=0.0,
|
||||||
|
minimum_exclusive=True,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||||
|
DISPLAY = ComponentDisplaySpec(
|
||||||
|
label="贮箱",
|
||||||
|
library_id="experimental",
|
||||||
|
category_id="storage",
|
||||||
|
symbol="tank",
|
||||||
|
ports=(PortDisplaySpec("port_a", "left"),),
|
||||||
|
order=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
medium: IdealGasMedium,
|
||||||
|
V: float = 0.1,
|
||||||
|
p0: float = 1e5,
|
||||||
|
T0: float = 300.0,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(name=name)
|
||||||
|
self.set_parameter_values({"volume": V, "p0": p0, "T0": T0})
|
||||||
|
self.medium = medium
|
||||||
|
self.V = V
|
||||||
|
m0 = p0 * V / (medium.R_gas * T0)
|
||||||
|
U0 = m0 * medium.specific_internal_energy(T0)
|
||||||
|
self.state = VolumeState(m=m0, U=U0)
|
||||||
|
self.port_a = self.register_declared_port("port_a")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
medium: IdealGasMedium,
|
||||||
|
parameters: Mapping[str, float],
|
||||||
|
) -> Tank:
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
medium=medium,
|
||||||
|
V=parameters["volume"],
|
||||||
|
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.V)
|
||||||
|
self.port_a.p = props.p
|
||||||
|
self.port_a.h_outflow = props.h
|
||||||
|
return props
|
||||||
|
|
||||||
|
def refresh_thermodynamic_ports(self) -> ThermodynamicProperties:
|
||||||
|
return self.properties()
|
||||||
|
|
||||||
|
def state_derivative_from_ports(
|
||||||
|
self,
|
||||||
|
connected_h: Mapping[str, float],
|
||||||
|
) -> list[float]:
|
||||||
|
properties = self.properties()
|
||||||
|
derivative = self.derivatives_from_connection(
|
||||||
|
connected_h=connected_h["port_a"],
|
||||||
|
port_m_flow=self.port_a.m_flow,
|
||||||
|
internal_h=properties.h,
|
||||||
|
)
|
||||||
|
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.V,
|
||||||
|
).p
|
||||||
|
return (
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:port_a_pressure_state",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="state",
|
||||||
|
variables=(f"{self.name}.port_a.p", f"{self.name}.state"),
|
||||||
|
role="effort",
|
||||||
|
value=self.port_a.p - pressure,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def derivatives_from_connection(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
connected_h: float,
|
||||||
|
port_m_flow: float,
|
||||||
|
internal_h: float,
|
||||||
|
) -> VolumeState:
|
||||||
|
inlet_h = self.connection_inlet_enthalpy(
|
||||||
|
port_m_flow=port_m_flow,
|
||||||
|
connected_h=connected_h,
|
||||||
|
internal_h=internal_h,
|
||||||
|
)
|
||||||
|
return self.derivatives(inlet_h, port_m_flow)
|
||||||
|
|
||||||
|
def derivatives(self, inlet_h: float, m_flow: float) -> VolumeState:
|
||||||
|
return VolumeState(m=m_flow, U=m_flow * inlet_h)
|
||||||
File renamed without changes.
@@ -0,0 +1,365 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from collections.abc import Callable, Mapping
|
||||||
|
from typing import TYPE_CHECKING, Any, ClassVar
|
||||||
|
|
||||||
|
from app.simulation.core.catalog import ComponentDisplaySpec
|
||||||
|
from app.simulation.core.equations import EquationResidual
|
||||||
|
from app.simulation.core.metadata import (
|
||||||
|
ParameterDefinition,
|
||||||
|
ResultVariableDefinition,
|
||||||
|
ResultVariableMetadata,
|
||||||
|
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
|
||||||
|
)
|
||||||
|
from app.simulation.core.ports import PortDefinition, PortState
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.simulation.core.medium import GasMedium
|
||||||
|
|
||||||
|
|
||||||
|
class Component(ABC):
|
||||||
|
MODEL_TYPE: ClassVar[str | None] = None
|
||||||
|
MODEL_VERSION: ClassVar[str | None] = None
|
||||||
|
# ``True`` means that pressure/flow residuals read values written by
|
||||||
|
# ``update_stream_outflows`` or ``update_flow_temperature_references``.
|
||||||
|
# ``False`` is an explicit promise that those residuals are independent of
|
||||||
|
# stream propagation. ``None`` keeps custom components conservative: when
|
||||||
|
# they override either stream hook, the closure planner retains the legacy
|
||||||
|
# full-network thermofluid fixed point.
|
||||||
|
PRESSURE_FLOW_DEPENDS_ON_STREAM: ClassVar[bool | None] = None
|
||||||
|
# Exact residual suffixes whose declared variables are summed, in order,
|
||||||
|
# to form a ``sumToZero`` flow equation. The causal solver deliberately
|
||||||
|
# reads this capability from the concrete class ``__dict__``: subclasses
|
||||||
|
# must repeat the promise after changing any equation semantics.
|
||||||
|
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES: ClassVar[
|
||||||
|
frozenset[str]
|
||||||
|
] = frozenset()
|
||||||
|
PORTS: ClassVar[tuple[PortDefinition, ...]] = ()
|
||||||
|
PARAMETERS: ClassVar[tuple[ParameterDefinition, ...]] = ()
|
||||||
|
RESULT_VARIABLES: ClassVar[tuple[ResultVariableDefinition, ...]] = ()
|
||||||
|
DISPLAY: ClassVar[ComponentDisplaySpec | None] = None
|
||||||
|
|
||||||
|
def __init__(self, name: str) -> None:
|
||||||
|
self.name = name
|
||||||
|
self.model_type = self.MODEL_TYPE or self.__class__.__name__.lower()
|
||||||
|
self._ports: dict[str, PortState] = {}
|
||||||
|
self._parameter_values: dict[str, float] = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ports(self) -> dict[str, PortState]:
|
||||||
|
return dict(self._ports)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def port_definitions(self) -> tuple[PortDefinition, ...]:
|
||||||
|
return tuple(
|
||||||
|
port.definition
|
||||||
|
for port in self._ports.values()
|
||||||
|
if port.definition is not None
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def active_port_definitions_for_parameters(
|
||||||
|
cls,
|
||||||
|
parameters: Mapping[str, float],
|
||||||
|
) -> tuple[PortDefinition, ...]:
|
||||||
|
"""Declared ports enabled by one normalized parameter set."""
|
||||||
|
|
||||||
|
return cls.PORTS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_port_definitions(self) -> tuple[PortDefinition, ...]:
|
||||||
|
"""Instance ports that participate in execution and result reporting."""
|
||||||
|
|
||||||
|
return self.port_definitions
|
||||||
|
|
||||||
|
@property
|
||||||
|
def required_connection_ports(self) -> tuple[str, ...]:
|
||||||
|
"""Physical ports that must have an external connection before simulation."""
|
||||||
|
|
||||||
|
return tuple(
|
||||||
|
definition.name
|
||||||
|
for definition in self.active_port_definitions
|
||||||
|
if definition.kind == "physical"
|
||||||
|
)
|
||||||
|
|
||||||
|
def register_port(self, port: PortState) -> PortState:
|
||||||
|
definition = port.definition
|
||||||
|
if definition is None:
|
||||||
|
raise ValueError(f"Component {self.name} cannot register an undefined port.")
|
||||||
|
if definition.name in self._ports:
|
||||||
|
raise ValueError(f"Duplicate port {self.name}.{definition.name}.")
|
||||||
|
self._ports[definition.name] = port
|
||||||
|
return port
|
||||||
|
|
||||||
|
def register_declared_port(self, name: str) -> PortState:
|
||||||
|
try:
|
||||||
|
definition = next(item for item in self.PORTS if item.name == name)
|
||||||
|
except StopIteration as exc:
|
||||||
|
raise ValueError(
|
||||||
|
f"Component model {self.model_type} does not declare port {name}."
|
||||||
|
) from exc
|
||||||
|
return self.register_port(PortState(definition=definition))
|
||||||
|
|
||||||
|
def set_parameter_values(self, values: Mapping[str, float]) -> None:
|
||||||
|
definitions = {definition.name: definition for definition in self.PARAMETERS}
|
||||||
|
unknown = sorted(set(values) - set(definitions))
|
||||||
|
if unknown:
|
||||||
|
raise ValueError(
|
||||||
|
f"Component {self.name} contains unsupported parameters: "
|
||||||
|
+ ", ".join(unknown)
|
||||||
|
+ "."
|
||||||
|
)
|
||||||
|
missing = sorted(set(definitions) - set(values))
|
||||||
|
if missing:
|
||||||
|
raise ValueError(
|
||||||
|
f"Component {self.name} is missing parameters: "
|
||||||
|
+ ", ".join(missing)
|
||||||
|
+ "."
|
||||||
|
)
|
||||||
|
|
||||||
|
resolved: dict[str, float] = {}
|
||||||
|
for name, definition in definitions.items():
|
||||||
|
value = float(values[name])
|
||||||
|
message = definition.validation_message(value)
|
||||||
|
if message is not None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Parameter '{name}' on component '{self.name}' {message}."
|
||||||
|
)
|
||||||
|
resolved[name] = value
|
||||||
|
self._parameter_values = resolved
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parameter_values(self) -> dict[str, float]:
|
||||||
|
return dict(self._parameter_values)
|
||||||
|
|
||||||
|
def get_port(self, name: str) -> PortState:
|
||||||
|
try:
|
||||||
|
return self._ports[name]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise ValueError(f"Component {self.name} has no port named {name}.") from exc
|
||||||
|
|
||||||
|
def component_result_values(self) -> Mapping[str, float]:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def result_values(self) -> dict[str, float]:
|
||||||
|
component_values = dict(self.component_result_values())
|
||||||
|
declared = {definition.name: definition for definition in self.RESULT_VARIABLES}
|
||||||
|
unknown = sorted(set(component_values) - set(declared))
|
||||||
|
if unknown:
|
||||||
|
raise ValueError(
|
||||||
|
f"Component {self.name} returned undeclared result variables: "
|
||||||
|
+ ", ".join(unknown)
|
||||||
|
+ "."
|
||||||
|
)
|
||||||
|
|
||||||
|
values: dict[str, float] = {}
|
||||||
|
for name, definition in declared.items():
|
||||||
|
if not definition.visible:
|
||||||
|
continue
|
||||||
|
if name not in component_values:
|
||||||
|
raise ValueError(
|
||||||
|
f"Component {self.name} did not provide declared result variable {name}."
|
||||||
|
)
|
||||||
|
values[name] = float(component_values[name])
|
||||||
|
|
||||||
|
for port_definition in self.active_port_definitions:
|
||||||
|
port = self.get_port(port_definition.name)
|
||||||
|
for variable in port_definition.variables:
|
||||||
|
if not variable.result_visible:
|
||||||
|
continue
|
||||||
|
values[f"{port_definition.name}.{variable.name}"] = float(
|
||||||
|
getattr(port, variable.name)
|
||||||
|
)
|
||||||
|
return values
|
||||||
|
|
||||||
|
def result_variable_metadata(self) -> tuple[ResultVariableMetadata, ...]:
|
||||||
|
metadata = [
|
||||||
|
ResultVariableMetadata(
|
||||||
|
key=f"{self.name}.{definition.name}",
|
||||||
|
component_id=self.name,
|
||||||
|
component_type=self.model_type,
|
||||||
|
scope="component",
|
||||||
|
name=definition.name,
|
||||||
|
label=definition.label,
|
||||||
|
quantity=definition.quantity,
|
||||||
|
unit=definition.unit,
|
||||||
|
category=definition.category,
|
||||||
|
order=definition.order,
|
||||||
|
)
|
||||||
|
for definition in self.RESULT_VARIABLES
|
||||||
|
if definition.visible
|
||||||
|
]
|
||||||
|
for port_definition in self.active_port_definitions:
|
||||||
|
for variable in port_definition.variables:
|
||||||
|
if not variable.result_visible:
|
||||||
|
continue
|
||||||
|
metadata.append(
|
||||||
|
ResultVariableMetadata(
|
||||||
|
key=f"{self.name}.{port_definition.name}.{variable.name}",
|
||||||
|
component_id=self.name,
|
||||||
|
component_type=self.model_type,
|
||||||
|
scope="port",
|
||||||
|
port_name=port_definition.name,
|
||||||
|
name=variable.name,
|
||||||
|
label=variable.label or variable.name,
|
||||||
|
quantity=variable.quantity or variable.name,
|
||||||
|
unit=variable.unit,
|
||||||
|
category=variable.role,
|
||||||
|
order=variable.order,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tuple(metadata)
|
||||||
|
|
||||||
|
def parameter_interface_dicts(self) -> list[dict[str, object]]:
|
||||||
|
return [
|
||||||
|
definition.as_interface_dict(
|
||||||
|
value=self._parameter_values.get(definition.name)
|
||||||
|
)
|
||||||
|
for definition in self.PARAMETERS
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(
|
||||||
|
cls,
|
||||||
|
*,
|
||||||
|
name: str,
|
||||||
|
medium: GasMedium,
|
||||||
|
parameters: Mapping[str, float],
|
||||||
|
) -> Component:
|
||||||
|
"""Create a catalog model from normalized SI parameters."""
|
||||||
|
|
||||||
|
raise NotImplementedError(
|
||||||
|
f"Component model {cls.__name__} must implement create()."
|
||||||
|
)
|
||||||
|
|
||||||
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
|
"""Return algebraic residuals after the network assigns port states."""
|
||||||
|
|
||||||
|
return ()
|
||||||
|
|
||||||
|
def pressure_flow_equation_values(self) -> tuple[float, ...]:
|
||||||
|
"""Return live residual values in the declared equation order.
|
||||||
|
|
||||||
|
Components with frequently evaluated equations can override this
|
||||||
|
method to avoid rebuilding immutable equation metadata during closure.
|
||||||
|
The default keeps third-party components compatible with the public
|
||||||
|
residual API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return tuple(
|
||||||
|
float(equation.value)
|
||||||
|
for equation in self.pressure_flow_equation_residuals()
|
||||||
|
)
|
||||||
|
|
||||||
|
def pressure_flow_equation_value_readers(
|
||||||
|
self,
|
||||||
|
) -> Mapping[str, Callable[[], float]]:
|
||||||
|
"""Return explicitly separable scalar residual readers.
|
||||||
|
|
||||||
|
The solver consumes this optional capability only when the concrete
|
||||||
|
component class declares the method itself. Subclasses therefore
|
||||||
|
cannot accidentally inherit an equation-purity promise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
|
||||||
|
"""Update connector outflow properties from current flow directions."""
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_flow_temperature_references(
|
||||||
|
self,
|
||||||
|
connected_h: Mapping[str, float],
|
||||||
|
) -> None:
|
||||||
|
"""Update enthalpy references used only by pressure-flow laws.
|
||||||
|
|
||||||
|
Most components use the normal stream enthalpy for both energy
|
||||||
|
transport and upstream-property evaluation. AMESim node submodels can
|
||||||
|
expose a distinct temperature reference, so the default is a no-op.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]:
|
||||||
|
"""Return directed ``volume``/``volume_flow`` values by pneumatic port.
|
||||||
|
|
||||||
|
Most pneumatic components contribute no external chamber volume. Moving
|
||||||
|
boundaries such as PNRP17 override this hook; the network resolver then
|
||||||
|
propagates the pair to the component connected at the same physical port.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicComponent(Component):
|
||||||
|
state_size = 2
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def actual_stream_enthalpy(
|
||||||
|
port_m_flow: float,
|
||||||
|
connected_h: float,
|
||||||
|
internal_h: float,
|
||||||
|
) -> float:
|
||||||
|
"""Approximate `actualStream(port.h_outflow)` for a mixed control volume port."""
|
||||||
|
|
||||||
|
return connected_h if port_m_flow > 0.0 else internal_h
|
||||||
|
|
||||||
|
def connection_inlet_enthalpy(
|
||||||
|
self,
|
||||||
|
port_m_flow: float,
|
||||||
|
connected_h: float,
|
||||||
|
internal_h: float,
|
||||||
|
) -> float:
|
||||||
|
"""Resolve the enthalpy convected into this control volume through one port."""
|
||||||
|
|
||||||
|
return self.actual_stream_enthalpy(
|
||||||
|
port_m_flow=port_m_flow,
|
||||||
|
connected_h=connected_h,
|
||||||
|
internal_h=internal_h,
|
||||||
|
)
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_state_vector(self) -> list[float]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def set_state_vector(self, values: list[float]) -> None:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def refresh_thermodynamic_ports(self) -> Any:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def state_derivative_from_ports(
|
||||||
|
self,
|
||||||
|
connected_h: Mapping[str, float],
|
||||||
|
) -> list[float]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class ThermodynamicVolumeComponent(DynamicComponent):
|
||||||
|
"""Two-state gas volume exposing the shared thermodynamic result contract."""
|
||||||
|
|
||||||
|
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
|
||||||
|
|
||||||
|
def component_result_values(self) -> Mapping[str, float]:
|
||||||
|
state = self.get_state_vector()
|
||||||
|
if len(state) < 2:
|
||||||
|
raise ValueError(
|
||||||
|
f"Thermodynamic component {self.name} must expose mass and energy states."
|
||||||
|
)
|
||||||
|
properties = self.refresh_thermodynamic_ports()
|
||||||
|
return {
|
||||||
|
"m": float(state[0]),
|
||||||
|
"U": float(state[1]),
|
||||||
|
"p": float(properties.p),
|
||||||
|
"T": float(properties.T),
|
||||||
|
"rho": float(properties.rho),
|
||||||
|
"u": float(properties.u),
|
||||||
|
"h": float(properties.h),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AlgebraicComponent(Component):
|
||||||
|
"""Stateless element described by algebraic constraints only."""
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
PortDisplaySide = Literal["left", "right"]
|
||||||
|
ComponentCatalogRole = Literal["amesimGasMediumDefinition"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ComponentCategorySpec:
|
||||||
|
"""A presentation-only category declared by one component library."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
label: str
|
||||||
|
order: int = 0
|
||||||
|
|
||||||
|
def as_catalog_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"label": self.label,
|
||||||
|
"order": self.order,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PortDisplaySpec:
|
||||||
|
"""Canvas placement for one port without changing its physical contract."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
side: PortDisplaySide
|
||||||
|
order: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ParameterGroupDisplaySpec:
|
||||||
|
"""Ordered, collapsible presentation group for component parameters."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
label: str
|
||||||
|
parameters: tuple[str, ...]
|
||||||
|
order: int = 0
|
||||||
|
default_expanded: bool = False
|
||||||
|
|
||||||
|
def as_catalog_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"label": self.label,
|
||||||
|
"parameters": list(self.parameters),
|
||||||
|
"order": self.order,
|
||||||
|
"defaultExpanded": self.default_expanded,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ComponentDisplaySpec:
|
||||||
|
"""Frontend metadata co-located with a component implementation."""
|
||||||
|
|
||||||
|
label: str
|
||||||
|
library_id: str
|
||||||
|
category_id: str
|
||||||
|
symbol: str
|
||||||
|
ports: tuple[PortDisplaySpec, ...]
|
||||||
|
order: int = 0
|
||||||
|
role: ComponentCatalogRole | None = None
|
||||||
|
parameter_groups: tuple[ParameterGroupDisplaySpec, ...] = ()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def port_by_name(self) -> dict[str, PortDisplaySpec]:
|
||||||
|
return {port.name: port for port in self.ports}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ComponentLibrarySpec:
|
||||||
|
"""Manifest for one explicitly enabled component library."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
label: str
|
||||||
|
version: str
|
||||||
|
source_package: str
|
||||||
|
categories: tuple[ComponentCategorySpec, ...]
|
||||||
|
models: tuple[str, ...]
|
||||||
|
temporary: bool = False
|
||||||
|
order: int = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def category_by_id(self) -> dict[str, ComponentCategorySpec]:
|
||||||
|
return {category.id: category for category in self.categories}
|
||||||
|
|
||||||
|
def as_catalog_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"label": self.label,
|
||||||
|
"version": self.version,
|
||||||
|
"sourcePackage": self.source_package,
|
||||||
|
"temporary": self.temporary,
|
||||||
|
"order": self.order,
|
||||||
|
"categories": [
|
||||||
|
category.as_catalog_dict()
|
||||||
|
for category in sorted(
|
||||||
|
self.categories,
|
||||||
|
key=lambda item: (item.order, item.id),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from app.simulation.core.ports import VariableRole
|
||||||
|
|
||||||
|
|
||||||
|
EquationOwner = Literal["connection", "component"]
|
||||||
|
EquationRelation = Literal["equal", "sumToZero", "constitutive", "state"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class EquationResidual:
|
||||||
|
"""One executable scalar equation in the pressure-flow subsystem."""
|
||||||
|
|
||||||
|
id: str
|
||||||
|
owner: EquationOwner
|
||||||
|
owner_id: str
|
||||||
|
relation: EquationRelation
|
||||||
|
variables: tuple[str, ...]
|
||||||
|
value: float
|
||||||
|
role: VariableRole | None = None
|
||||||
|
|
||||||
|
def as_definition_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"id": self.id,
|
||||||
|
"owner": self.owner,
|
||||||
|
"ownerId": self.owner_id,
|
||||||
|
"relation": self.relation,
|
||||||
|
"variables": list(self.variables),
|
||||||
|
"role": self.role,
|
||||||
|
}
|
||||||
|
|
||||||
|
def as_interface_dict(self) -> dict[str, object]:
|
||||||
|
return {**self.as_definition_dict(), "residual": self.value}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class RecoverableTrialStateError(ValueError):
|
||||||
|
"""A physical-domain failure caused by an integrator trial state."""
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from math import isfinite
|
||||||
|
from typing import Protocol, Sequence
|
||||||
|
|
||||||
|
from app.simulation.core.errors import RecoverableTrialStateError
|
||||||
|
from app.simulation.performance import profile_property
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThermodynamicProperties:
|
||||||
|
p: float
|
||||||
|
T: float
|
||||||
|
rho: float
|
||||||
|
u: float
|
||||||
|
h: float
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThermodynamicPropertyTangents:
|
||||||
|
"""Directional derivatives of a recovered thermodynamic state."""
|
||||||
|
|
||||||
|
p: tuple[float, ...]
|
||||||
|
T: tuple[float, ...]
|
||||||
|
rho: tuple[float, ...]
|
||||||
|
u: tuple[float, ...]
|
||||||
|
h: tuple[float, ...]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def width(self) -> int:
|
||||||
|
return len(self.p)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def zeros(cls, width: int) -> "ThermodynamicPropertyTangents":
|
||||||
|
values = (0.0,) * width
|
||||||
|
return cls(p=values, T=values, rho=values, u=values, h=values)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ThermodynamicPropertiesLinearization:
|
||||||
|
"""Primal properties and a validity-checked directional linearization."""
|
||||||
|
|
||||||
|
properties: ThermodynamicProperties
|
||||||
|
tangents: ThermodynamicPropertyTangents
|
||||||
|
valid: bool = True
|
||||||
|
reason: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
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 isentropic_density_pressure_factor(
|
||||||
|
self,
|
||||||
|
p: float,
|
||||||
|
T: float,
|
||||||
|
downstream_pressure: float | None = None,
|
||||||
|
) -> float: ...
|
||||||
|
|
||||||
|
def dynamic_viscosity(self, T: float) -> float: ...
|
||||||
|
|
||||||
|
def diagnostic_dynamic_viscosity(self, T: float) -> float: ...
|
||||||
|
|
||||||
|
def specific_internal_energy(self, T: float) -> float: ...
|
||||||
|
|
||||||
|
def specific_internal_energy_at_pressure(self, p: float, T: float) -> float: ...
|
||||||
|
|
||||||
|
def specific_enthalpy(self, T: float) -> float: ...
|
||||||
|
|
||||||
|
def specific_enthalpy_at_pressure(self, p: float, T: float) -> float: ...
|
||||||
|
|
||||||
|
def temperature_from_internal_energy(self, u: float) -> float: ...
|
||||||
|
|
||||||
|
def temperature_from_enthalpy(self, h: float) -> float: ...
|
||||||
|
|
||||||
|
def temperature_from_pressure_enthalpy(self, p: float, 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: ...
|
||||||
|
|
||||||
|
def linearize_properties_from_mU(
|
||||||
|
self,
|
||||||
|
m: float,
|
||||||
|
U: float,
|
||||||
|
V: float,
|
||||||
|
dm: Sequence[float],
|
||||||
|
dU: Sequence[float],
|
||||||
|
dV: Sequence[float],
|
||||||
|
*,
|
||||||
|
properties: ThermodynamicProperties | None = None,
|
||||||
|
) -> ThermodynamicPropertiesLinearization: ...
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class IdealGasMedium:
|
||||||
|
"""Temperature-dependent ideal-gas air approximation.
|
||||||
|
|
||||||
|
This is still not a strict clone of `Modelica.Media.Air.SimpleAir`.
|
||||||
|
The small linear `cp(T)` term is kept configurable for calibration, but the
|
||||||
|
current default is calibrated against the committed Testmodel baseline and
|
||||||
|
therefore falls back to the constant-heat-capacity limit.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str = "SimpleAirApprox"
|
||||||
|
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
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cv(self) -> float:
|
||||||
|
return self.cv_at_temperature(self.T_ref)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gamma(self) -> float:
|
||||||
|
return self.cp_at_temperature(self.T_ref) / self.cv
|
||||||
|
|
||||||
|
def cp_at_temperature(self, T: float) -> float:
|
||||||
|
return self.cp_ref + self.cp_slope * (T - self.T_ref)
|
||||||
|
|
||||||
|
def cv_at_temperature(self, T: float) -> float:
|
||||||
|
return self.cp_at_temperature(T) - self.R_gas
|
||||||
|
|
||||||
|
@profile_property("density")
|
||||||
|
def density(self, p: float, T: float) -> float:
|
||||||
|
return p / (self.R_gas * T)
|
||||||
|
|
||||||
|
@profile_property("isentropic_density_pressure_factor")
|
||||||
|
def isentropic_density_pressure_factor(
|
||||||
|
self,
|
||||||
|
p: float,
|
||||||
|
T: float,
|
||||||
|
downstream_pressure: float | None = None,
|
||||||
|
) -> float:
|
||||||
|
del p
|
||||||
|
del downstream_pressure
|
||||||
|
cp = self.cp_at_temperature(T)
|
||||||
|
cv = self.cv_at_temperature(T)
|
||||||
|
return cv / cp
|
||||||
|
|
||||||
|
@profile_property("dynamic_viscosity")
|
||||||
|
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 diagnostic_dynamic_viscosity(self, T: float) -> float:
|
||||||
|
"""Return the viscosity convention used by derived diagnostics.
|
||||||
|
|
||||||
|
Most media use the same transport property for dynamics and reported
|
||||||
|
diagnostics. Reference-library media may override this without
|
||||||
|
changing a calibrated constitutive flow relation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.dynamic_viscosity(T)
|
||||||
|
|
||||||
|
@profile_property("specific_internal_energy")
|
||||||
|
def specific_internal_energy(self, T: float) -> float:
|
||||||
|
delta_T = T - self.T_ref
|
||||||
|
return (
|
||||||
|
self.cv * self.T_ref
|
||||||
|
+ self.cv * delta_T
|
||||||
|
+ 0.5 * self.cp_slope * delta_T * delta_T
|
||||||
|
)
|
||||||
|
|
||||||
|
@profile_property("specific_internal_energy_at_pressure")
|
||||||
|
def specific_internal_energy_at_pressure(self, p: float, T: float) -> float:
|
||||||
|
del p
|
||||||
|
return self.specific_internal_energy(T)
|
||||||
|
|
||||||
|
@profile_property("specific_enthalpy")
|
||||||
|
def specific_enthalpy(self, T: float) -> float:
|
||||||
|
delta_T = T - self.T_ref
|
||||||
|
return (
|
||||||
|
self.cp_ref * self.T_ref
|
||||||
|
+ self.cp_ref * delta_T
|
||||||
|
+ 0.5 * self.cp_slope * delta_T * delta_T
|
||||||
|
)
|
||||||
|
|
||||||
|
@profile_property("specific_enthalpy_at_pressure")
|
||||||
|
def specific_enthalpy_at_pressure(self, p: float, T: float) -> float:
|
||||||
|
del p
|
||||||
|
return self.specific_enthalpy(T)
|
||||||
|
|
||||||
|
def temperature_from_internal_energy(self, u: float) -> float:
|
||||||
|
reference_internal_energy = self.cv * self.T_ref
|
||||||
|
delta_u = u - reference_internal_energy
|
||||||
|
|
||||||
|
if abs(self.cp_slope) <= 1e-15:
|
||||||
|
return self.T_ref + delta_u / self.cv
|
||||||
|
|
||||||
|
a = 0.5 * self.cp_slope
|
||||||
|
b = self.cv
|
||||||
|
c = -delta_u
|
||||||
|
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_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
|
||||||
|
|
||||||
|
@profile_property("temperature_from_pressure_enthalpy")
|
||||||
|
def temperature_from_pressure_enthalpy(self, p: float, h: float) -> float:
|
||||||
|
del p
|
||||||
|
return self.temperature_from_enthalpy(h)
|
||||||
|
|
||||||
|
def temperature_from_mass_internal_energy(self, m: float, U: float) -> float:
|
||||||
|
if m <= 0.0:
|
||||||
|
raise RecoverableTrialStateError(
|
||||||
|
"Mass must stay positive when recovering temperature."
|
||||||
|
)
|
||||||
|
return self.temperature_from_internal_energy(U / m)
|
||||||
|
|
||||||
|
def pressure(self, m: float, T: float, V: float) -> float:
|
||||||
|
if V <= 0.0:
|
||||||
|
raise ValueError("Volume must stay positive.")
|
||||||
|
return m * self.R_gas * T / V
|
||||||
|
|
||||||
|
@profile_property("properties_from_mU")
|
||||||
|
def properties_from_mU(self, m: float, U: float, V: float) -> ThermodynamicProperties:
|
||||||
|
T = self.temperature_from_mass_internal_energy(m, U)
|
||||||
|
p = self.pressure(m, T, V)
|
||||||
|
rho = m / V
|
||||||
|
u = U / m
|
||||||
|
h = self.specific_enthalpy(T)
|
||||||
|
return ThermodynamicProperties(p=p, T=T, rho=rho, u=u, h=h)
|
||||||
|
|
||||||
|
def linearize_properties_from_mU(
|
||||||
|
self,
|
||||||
|
m: float,
|
||||||
|
U: float,
|
||||||
|
V: float,
|
||||||
|
dm: Sequence[float],
|
||||||
|
dU: Sequence[float],
|
||||||
|
dV: Sequence[float],
|
||||||
|
*,
|
||||||
|
properties: ThermodynamicProperties | None = None,
|
||||||
|
) -> ThermodynamicPropertiesLinearization:
|
||||||
|
"""Linearize properties_from_mU for several seed directions."""
|
||||||
|
|
||||||
|
dm_values = tuple(float(value) for value in dm)
|
||||||
|
dU_values = tuple(float(value) for value in dU)
|
||||||
|
dV_values = tuple(float(value) for value in dV)
|
||||||
|
if not (len(dm_values) == len(dU_values) == len(dV_values)):
|
||||||
|
raise ValueError("Thermodynamic tangent vectors must have equal lengths.")
|
||||||
|
props = properties or self.properties_from_mU(m, U, V)
|
||||||
|
width = len(dm_values)
|
||||||
|
expected_density = m / V
|
||||||
|
expected_internal_energy = U / m
|
||||||
|
if (
|
||||||
|
abs(props.rho - expected_density)
|
||||||
|
> 1.0e-12 * max(abs(expected_density), 1.0)
|
||||||
|
or abs(props.u - expected_internal_energy)
|
||||||
|
> 1.0e-12 * max(abs(expected_internal_energy), 1.0)
|
||||||
|
):
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents.zeros(width),
|
||||||
|
valid=False,
|
||||||
|
reason="properties_primal_mismatch",
|
||||||
|
)
|
||||||
|
if not all(
|
||||||
|
isfinite(value)
|
||||||
|
for values in (dm_values, dU_values, dV_values)
|
||||||
|
for value in values
|
||||||
|
):
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents.zeros(width),
|
||||||
|
valid=False,
|
||||||
|
reason="non_finite_tangent_input",
|
||||||
|
)
|
||||||
|
|
||||||
|
cv = self.cv_at_temperature(props.T)
|
||||||
|
cp = self.cp_at_temperature(props.T)
|
||||||
|
if not isfinite(cv) or not isfinite(cp) or cv <= 0.0 or cp <= 0.0:
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents.zeros(width),
|
||||||
|
valid=False,
|
||||||
|
reason="non_positive_heat_capacity",
|
||||||
|
)
|
||||||
|
|
||||||
|
drho: list[float] = []
|
||||||
|
du: list[float] = []
|
||||||
|
dT: list[float] = []
|
||||||
|
dp: list[float] = []
|
||||||
|
dh: list[float] = []
|
||||||
|
for mass_tangent, energy_tangent, volume_tangent in zip(
|
||||||
|
dm_values,
|
||||||
|
dU_values,
|
||||||
|
dV_values,
|
||||||
|
strict=True,
|
||||||
|
):
|
||||||
|
density_tangent = mass_tangent / V - m * volume_tangent / (V * V)
|
||||||
|
internal_energy_tangent = (
|
||||||
|
energy_tangent / m - U * mass_tangent / (m * m)
|
||||||
|
)
|
||||||
|
temperature_tangent = internal_energy_tangent / cv
|
||||||
|
pressure_tangent = self.R_gas * (
|
||||||
|
props.T * density_tangent + props.rho * temperature_tangent
|
||||||
|
)
|
||||||
|
enthalpy_tangent = cp * temperature_tangent
|
||||||
|
drho.append(density_tangent)
|
||||||
|
du.append(internal_energy_tangent)
|
||||||
|
dT.append(temperature_tangent)
|
||||||
|
dp.append(pressure_tangent)
|
||||||
|
dh.append(enthalpy_tangent)
|
||||||
|
|
||||||
|
tangent_values = (*drho, *du, *dT, *dp, *dh)
|
||||||
|
valid = all(isfinite(value) for value in tangent_values)
|
||||||
|
return ThermodynamicPropertiesLinearization(
|
||||||
|
properties=props,
|
||||||
|
tangents=ThermodynamicPropertyTangents(
|
||||||
|
p=tuple(dp),
|
||||||
|
T=tuple(dT),
|
||||||
|
rho=tuple(drho),
|
||||||
|
u=tuple(du),
|
||||||
|
h=tuple(dh),
|
||||||
|
),
|
||||||
|
valid=valid,
|
||||||
|
reason=None if valid else "non_finite_property_tangent",
|
||||||
|
)
|
||||||
@@ -0,0 +1,240 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from math import isfinite
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
ResultVariableScope = Literal["component", "port"]
|
||||||
|
ParameterEditor = Literal[
|
||||||
|
"amesimGasReference",
|
||||||
|
"amesimGasPropertyModel",
|
||||||
|
"choice",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
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",
|
||||||
|
"mass_flow": "kg/s",
|
||||||
|
"pressure": "Pa",
|
||||||
|
"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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ParameterCondition:
|
||||||
|
"""Match when the controlling parameter equals any declared value."""
|
||||||
|
|
||||||
|
parameter: str
|
||||||
|
values: tuple[float, ...]
|
||||||
|
|
||||||
|
def as_interface_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"parameter": self.parameter,
|
||||||
|
"values": list(self.values),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ParameterDefinition:
|
||||||
|
"""User-configurable model input expressed in the backend SI contract.
|
||||||
|
|
||||||
|
Every ``visible_when`` condition must match for the catalog parameter to
|
||||||
|
be visible; each individual condition matches any one of its ``values``.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
default: float
|
||||||
|
label: str = ""
|
||||||
|
quantity: str = "dimensionless"
|
||||||
|
unit: str = ""
|
||||||
|
minimum: float | None = None
|
||||||
|
maximum: float | None = None
|
||||||
|
minimum_exclusive: bool = False
|
||||||
|
editor: ParameterEditor | None = None
|
||||||
|
options: tuple[ParameterOption, ...] = ()
|
||||||
|
description: str = ""
|
||||||
|
visible_when: tuple[ParameterCondition, ...] = ()
|
||||||
|
|
||||||
|
def validation_message(self, value: float) -> str | None:
|
||||||
|
if not isfinite(value):
|
||||||
|
return "must be finite"
|
||||||
|
if self.minimum is not None:
|
||||||
|
if self.minimum_exclusive and value <= self.minimum:
|
||||||
|
return f"must be greater than {self.minimum:g}"
|
||||||
|
if not self.minimum_exclusive and value < self.minimum:
|
||||||
|
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]:
|
||||||
|
payload: dict[str, object] = {
|
||||||
|
"name": self.name,
|
||||||
|
"label": self.label or self.name,
|
||||||
|
"quantity": self.quantity,
|
||||||
|
"unit": self.unit,
|
||||||
|
"default": self.default,
|
||||||
|
"minimumExclusive": self.minimum_exclusive,
|
||||||
|
}
|
||||||
|
if self.minimum is not None:
|
||||||
|
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 self.visible_when:
|
||||||
|
payload["visibleWhen"] = [
|
||||||
|
condition.as_interface_dict() for condition in self.visible_when
|
||||||
|
]
|
||||||
|
if value is not None:
|
||||||
|
payload["value"] = value
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ResultVariableDefinition:
|
||||||
|
"""Component-relative declaration of a user-visible simulation result."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
label: str
|
||||||
|
quantity: str
|
||||||
|
unit: str = ""
|
||||||
|
category: str = "derived"
|
||||||
|
order: int = 0
|
||||||
|
visible: bool = True
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ResultVariableMetadata:
|
||||||
|
"""A result declaration bound to one concrete component instance."""
|
||||||
|
|
||||||
|
key: str
|
||||||
|
component_id: str
|
||||||
|
component_type: str
|
||||||
|
scope: ResultVariableScope
|
||||||
|
name: str
|
||||||
|
label: str
|
||||||
|
quantity: str
|
||||||
|
unit: str
|
||||||
|
category: str
|
||||||
|
order: int
|
||||||
|
port_name: str | None = None
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"key": self.key,
|
||||||
|
"componentId": self.component_id,
|
||||||
|
"componentType": self.component_type,
|
||||||
|
"scope": self.scope,
|
||||||
|
"portName": self.port_name,
|
||||||
|
"name": self.name,
|
||||||
|
"label": self.label,
|
||||||
|
"quantity": self.quantity,
|
||||||
|
"unit": self.unit,
|
||||||
|
"category": self.category,
|
||||||
|
"order": self.order,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
THERMODYNAMIC_VOLUME_RESULT_VARIABLES = (
|
||||||
|
ResultVariableDefinition(
|
||||||
|
name="m",
|
||||||
|
label="质量",
|
||||||
|
quantity="mass",
|
||||||
|
unit="kg",
|
||||||
|
category="state",
|
||||||
|
order=10,
|
||||||
|
),
|
||||||
|
ResultVariableDefinition(
|
||||||
|
name="U",
|
||||||
|
label="内能",
|
||||||
|
quantity="internal_energy",
|
||||||
|
unit="J",
|
||||||
|
category="state",
|
||||||
|
order=20,
|
||||||
|
),
|
||||||
|
ResultVariableDefinition(
|
||||||
|
name="p",
|
||||||
|
label="压力",
|
||||||
|
quantity="pressure",
|
||||||
|
unit="Pa",
|
||||||
|
category="thermodynamic",
|
||||||
|
order=30,
|
||||||
|
),
|
||||||
|
ResultVariableDefinition(
|
||||||
|
name="T",
|
||||||
|
label="温度",
|
||||||
|
quantity="temperature",
|
||||||
|
unit="K",
|
||||||
|
category="thermodynamic",
|
||||||
|
order=40,
|
||||||
|
),
|
||||||
|
ResultVariableDefinition(
|
||||||
|
name="rho",
|
||||||
|
label="密度",
|
||||||
|
quantity="density",
|
||||||
|
unit="kg/m³",
|
||||||
|
category="thermodynamic",
|
||||||
|
order=50,
|
||||||
|
),
|
||||||
|
ResultVariableDefinition(
|
||||||
|
name="u",
|
||||||
|
label="比内能",
|
||||||
|
quantity="specific_internal_energy",
|
||||||
|
unit="J/kg",
|
||||||
|
category="thermodynamic",
|
||||||
|
order=60,
|
||||||
|
),
|
||||||
|
ResultVariableDefinition(
|
||||||
|
name="h",
|
||||||
|
label="比焓",
|
||||||
|
quantity="specific_enthalpy",
|
||||||
|
unit="J/kg",
|
||||||
|
category="thermodynamic",
|
||||||
|
order=70,
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from app.simulation.core.errors import RecoverableTrialStateError
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from math import acos, cos, isfinite, log, pi, sqrt
|
||||||
|
|
||||||
|
from app.simulation.performance import profile_property
|
||||||
|
|
||||||
|
UNIVERSAL_GAS_CONSTANT = 8.31446261815324
|
||||||
|
# Simcenter Amesim 2404 ``sag_reinit_eos_`` keeps more digits than the
|
||||||
|
# commonly printed Peng-Robinson constants 0.45724 and 0.07780.
|
||||||
|
PENG_ROBINSON_A_COEFFICIENT = 0.457235583
|
||||||
|
PENG_ROBINSON_B_COEFFICIENT = 0.07779607
|
||||||
|
|
||||||
|
|
||||||
|
@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 (
|
||||||
|
PENG_ROBINSON_A_COEFFICIENT
|
||||||
|
* UNIVERSAL_GAS_CONSTANT
|
||||||
|
* UNIVERSAL_GAS_CONSTANT
|
||||||
|
* self.critical_temperature
|
||||||
|
* self.critical_temperature
|
||||||
|
/ self.critical_pressure
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def b_parameter(self) -> float:
|
||||||
|
return (
|
||||||
|
PENG_ROBINSON_B_COEFFICIENT
|
||||||
|
* 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 alpha_temperature_second_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 (
|
||||||
|
self.kappa
|
||||||
|
/ (2.0 * self.critical_temperature * self.critical_temperature)
|
||||||
|
* (
|
||||||
|
self.kappa / reduced_temperature
|
||||||
|
+ alpha_base / (reduced_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 attractive_parameter_temperature_second_derivative(
|
||||||
|
self,
|
||||||
|
temperature: float,
|
||||||
|
) -> float:
|
||||||
|
return self.a_parameter * self.alpha_temperature_second_derivative(temperature)
|
||||||
|
|
||||||
|
@profile_property(
|
||||||
|
"pressure_from_molar_volume",
|
||||||
|
layer="kernel",
|
||||||
|
minimum_mode="audit",
|
||||||
|
)
|
||||||
|
def pressure_from_molar_volume(self, temperature: float, molar_volume: float) -> float:
|
||||||
|
self._validate_temperature(temperature)
|
||||||
|
if molar_volume <= self.b_parameter:
|
||||||
|
raise RecoverableTrialStateError("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
|
||||||
|
|
||||||
|
@profile_property(
|
||||||
|
"pressure_from_density",
|
||||||
|
layer="kernel",
|
||||||
|
minimum_mode="audit",
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
|
||||||
|
@profile_property(
|
||||||
|
"pressure_temperature_derivative_at_density",
|
||||||
|
layer="kernel",
|
||||||
|
minimum_mode="audit",
|
||||||
|
)
|
||||||
|
def pressure_temperature_derivative_at_density(
|
||||||
|
self,
|
||||||
|
temperature: float,
|
||||||
|
density: float,
|
||||||
|
) -> float:
|
||||||
|
self._validate_temperature(temperature)
|
||||||
|
if density <= 0.0:
|
||||||
|
raise ValueError("Density must be positive.")
|
||||||
|
molar_volume = self.molar_mass / density
|
||||||
|
if molar_volume <= self.b_parameter:
|
||||||
|
raise RecoverableTrialStateError(
|
||||||
|
"Molar volume must be larger than Peng-Robinson b parameter."
|
||||||
|
)
|
||||||
|
b = self.b_parameter
|
||||||
|
denominator = molar_volume * (molar_volume + b) + b * (molar_volume - b)
|
||||||
|
return (
|
||||||
|
UNIVERSAL_GAS_CONSTANT / (molar_volume - b)
|
||||||
|
- self.attractive_parameter_temperature_derivative(temperature) / denominator
|
||||||
|
)
|
||||||
|
|
||||||
|
@profile_property(
|
||||||
|
"pressure_density_derivative_at_temperature",
|
||||||
|
layer="kernel",
|
||||||
|
minimum_mode="audit",
|
||||||
|
)
|
||||||
|
def pressure_density_derivative_at_temperature(
|
||||||
|
self,
|
||||||
|
temperature: float,
|
||||||
|
density: float,
|
||||||
|
) -> float:
|
||||||
|
self._validate_temperature(temperature)
|
||||||
|
if density <= 0.0:
|
||||||
|
raise ValueError("Density must be positive.")
|
||||||
|
molar_volume = self.molar_mass / density
|
||||||
|
if molar_volume <= self.b_parameter:
|
||||||
|
raise RecoverableTrialStateError(
|
||||||
|
"Molar volume must be larger than Peng-Robinson b parameter."
|
||||||
|
)
|
||||||
|
b = self.b_parameter
|
||||||
|
denominator = molar_volume * (molar_volume + b) + b * (molar_volume - b)
|
||||||
|
pressure_molar_volume_derivative = (
|
||||||
|
-UNIVERSAL_GAS_CONSTANT * temperature / (molar_volume - b) ** 2
|
||||||
|
+ self.attractive_parameter(temperature)
|
||||||
|
* 2.0
|
||||||
|
* (molar_volume + b)
|
||||||
|
/ denominator**2
|
||||||
|
)
|
||||||
|
molar_volume_density_derivative = -self.molar_mass / (density * density)
|
||||||
|
return pressure_molar_volume_derivative * molar_volume_density_derivative
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
@profile_property(
|
||||||
|
"compressibility_roots",
|
||||||
|
layer="kernel",
|
||||||
|
minimum_mode="audit",
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
@profile_property(
|
||||||
|
"compressibility_factor",
|
||||||
|
layer="kernel",
|
||||||
|
minimum_mode="audit",
|
||||||
|
)
|
||||||
|
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}")
|
||||||
|
|
||||||
|
@profile_property(
|
||||||
|
"molar_volume",
|
||||||
|
layer="kernel",
|
||||||
|
minimum_mode="audit",
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
@profile_property("density", layer="kernel", minimum_mode="audit")
|
||||||
|
def density(
|
||||||
|
self,
|
||||||
|
pressure: float,
|
||||||
|
temperature: float,
|
||||||
|
phase: str = "vapor",
|
||||||
|
) -> float:
|
||||||
|
return self.molar_mass / self.molar_volume(pressure, temperature, phase=phase)
|
||||||
|
|
||||||
|
@profile_property(
|
||||||
|
"residual_specific_enthalpy",
|
||||||
|
layer="kernel",
|
||||||
|
minimum_mode="audit",
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
@profile_property(
|
||||||
|
"residual_specific_internal_energy_at_density",
|
||||||
|
layer="kernel",
|
||||||
|
minimum_mode="audit",
|
||||||
|
)
|
||||||
|
def residual_specific_internal_energy_at_density(
|
||||||
|
self,
|
||||||
|
temperature: float,
|
||||||
|
density: float,
|
||||||
|
) -> float:
|
||||||
|
"""Return Peng-Robinson internal-energy departure, J/kg."""
|
||||||
|
self._validate_temperature(temperature)
|
||||||
|
if density <= 0.0:
|
||||||
|
raise ValueError("Density must be positive.")
|
||||||
|
molar_volume = self.molar_mass / density
|
||||||
|
b = self.b_parameter
|
||||||
|
if molar_volume <= b:
|
||||||
|
raise RecoverableTrialStateError(
|
||||||
|
"Molar volume must be larger than Peng-Robinson b parameter."
|
||||||
|
)
|
||||||
|
attractive = self.attractive_parameter(temperature)
|
||||||
|
d_attractive_d_temperature = (
|
||||||
|
self.attractive_parameter_temperature_derivative(temperature)
|
||||||
|
)
|
||||||
|
log_argument = (
|
||||||
|
molar_volume + (1.0 + sqrt(2.0)) * b
|
||||||
|
) / (
|
||||||
|
molar_volume + (1.0 - sqrt(2.0)) * b
|
||||||
|
)
|
||||||
|
residual_molar_internal_energy = (
|
||||||
|
temperature * d_attractive_d_temperature - attractive
|
||||||
|
) * log(log_argument) / (2.0 * sqrt(2.0) * b)
|
||||||
|
return residual_molar_internal_energy / self.molar_mass
|
||||||
|
|
||||||
|
@profile_property(
|
||||||
|
"residual_isochoric_heat_capacity_at_density",
|
||||||
|
layer="kernel",
|
||||||
|
minimum_mode="audit",
|
||||||
|
)
|
||||||
|
def residual_isochoric_heat_capacity_at_density(
|
||||||
|
self,
|
||||||
|
temperature: float,
|
||||||
|
density: float,
|
||||||
|
) -> float:
|
||||||
|
"""Return the constant-volume heat-capacity departure, J/kg/K."""
|
||||||
|
self._validate_temperature(temperature)
|
||||||
|
if density <= 0.0:
|
||||||
|
raise ValueError("Density must be positive.")
|
||||||
|
molar_volume = self.molar_mass / density
|
||||||
|
b = self.b_parameter
|
||||||
|
if molar_volume <= b:
|
||||||
|
raise RecoverableTrialStateError(
|
||||||
|
"Molar volume must be larger than Peng-Robinson b parameter."
|
||||||
|
)
|
||||||
|
log_argument = (
|
||||||
|
molar_volume + (1.0 + sqrt(2.0)) * b
|
||||||
|
) / (
|
||||||
|
molar_volume + (1.0 - sqrt(2.0)) * b
|
||||||
|
)
|
||||||
|
residual_molar_cv = (
|
||||||
|
temperature
|
||||||
|
* self.attractive_parameter_temperature_second_derivative(temperature)
|
||||||
|
* log(log_argument)
|
||||||
|
/ (2.0 * sqrt(2.0) * b)
|
||||||
|
)
|
||||||
|
return residual_molar_cv / self.molar_mass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _validate_temperature(temperature: float) -> None:
|
||||||
|
if temperature <= 0.0:
|
||||||
|
raise RecoverableTrialStateError("Temperature must be positive.")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _validate_pressure_temperature(cls, pressure: float, temperature: float) -> None:
|
||||||
|
if pressure <= 0.0:
|
||||||
|
raise RecoverableTrialStateError("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,
|
||||||
|
# Simcenter Amesim 2404 helium_eos.data.
|
||||||
|
acentric_factor=-0.382,
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
|
||||||
|
PortKind = Literal["physical", "signal"]
|
||||||
|
PortNominalRole = Literal["inlet", "outlet", "bidirectional", "input", "output"]
|
||||||
|
ActualFlowDirection = Literal["in", "out", "stagnant"]
|
||||||
|
VariableRole = Literal["effort", "flow", "stream", "signal"]
|
||||||
|
ConnectionRule = Literal["equal", "sumToZero", "streamMix", "directed"]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PortVariableDefinition:
|
||||||
|
name: str
|
||||||
|
role: VariableRole
|
||||||
|
connection_rule: ConnectionRule
|
||||||
|
label: str = field(default="", compare=False)
|
||||||
|
quantity: str = field(default="", compare=False)
|
||||||
|
unit: str = field(default="", compare=False)
|
||||||
|
result_visible: bool = field(default=True, compare=False)
|
||||||
|
order: int = field(default=0, compare=False)
|
||||||
|
|
||||||
|
def as_interface_dict(self) -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"role": self.role,
|
||||||
|
"connectionRule": self.connection_rule,
|
||||||
|
"label": self.label or self.name,
|
||||||
|
"quantity": self.quantity or self.name,
|
||||||
|
"unit": self.unit,
|
||||||
|
"resultVisible": self.result_visible,
|
||||||
|
"order": self.order,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class PortDefinition:
|
||||||
|
"""Stable connector contract shared by components, XML, and the compiler."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
kind: PortKind
|
||||||
|
domain: str
|
||||||
|
nominal_role: PortNominalRole
|
||||||
|
positive_flow_direction: Literal["intoComponent"] | None = None
|
||||||
|
variables: tuple[PortVariableDefinition, ...] = ()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def pneumatic(
|
||||||
|
cls,
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
nominal_role: Literal["inlet", "outlet", "bidirectional"] = "bidirectional",
|
||||||
|
) -> PortDefinition:
|
||||||
|
return cls(
|
||||||
|
name=name,
|
||||||
|
kind="physical",
|
||||||
|
domain="pneumatic",
|
||||||
|
nominal_role=nominal_role,
|
||||||
|
positive_flow_direction="intoComponent",
|
||||||
|
variables=(
|
||||||
|
PortVariableDefinition(
|
||||||
|
"p",
|
||||||
|
"effort",
|
||||||
|
"equal",
|
||||||
|
label="压力",
|
||||||
|
quantity="pressure",
|
||||||
|
unit="Pa",
|
||||||
|
order=10,
|
||||||
|
),
|
||||||
|
PortVariableDefinition(
|
||||||
|
"m_flow",
|
||||||
|
"flow",
|
||||||
|
"sumToZero",
|
||||||
|
label="质量流量",
|
||||||
|
quantity="mass_flow",
|
||||||
|
unit="kg/s",
|
||||||
|
order=20,
|
||||||
|
),
|
||||||
|
PortVariableDefinition(
|
||||||
|
"h_outflow",
|
||||||
|
"stream",
|
||||||
|
"streamMix",
|
||||||
|
label="流出比焓",
|
||||||
|
quantity="specific_enthalpy",
|
||||||
|
unit="J/kg",
|
||||||
|
order=30,
|
||||||
|
),
|
||||||
|
PortVariableDefinition(
|
||||||
|
"volume",
|
||||||
|
"signal",
|
||||||
|
"directed",
|
||||||
|
label="外部容积",
|
||||||
|
quantity="volume",
|
||||||
|
unit="m3",
|
||||||
|
result_visible=False,
|
||||||
|
order=40,
|
||||||
|
),
|
||||||
|
PortVariableDefinition(
|
||||||
|
"volume_flow",
|
||||||
|
"signal",
|
||||||
|
"directed",
|
||||||
|
label="外部容积变化率",
|
||||||
|
quantity="volume_flow",
|
||||||
|
unit="m3/s",
|
||||||
|
result_visible=False,
|
||||||
|
order=50,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@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,
|
||||||
|
"kind": self.kind,
|
||||||
|
"domain": self.domain,
|
||||||
|
"nominalRole": self.nominal_role,
|
||||||
|
"positiveFlowDirection": self.positive_flow_direction,
|
||||||
|
"variables": [variable.as_interface_dict() for variable in self.variables],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PortState:
|
||||||
|
"""Python-side analogue of a Modelica fluid port."""
|
||||||
|
|
||||||
|
p: float = 0.0
|
||||||
|
m_flow: float = 0.0
|
||||||
|
h_outflow: float = 0.0
|
||||||
|
volume: float = 0.0
|
||||||
|
volume_flow: 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
|
||||||
|
def pneumatic(
|
||||||
|
cls,
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
nominal_role: Literal["inlet", "outlet", "bidirectional"] = "bidirectional",
|
||||||
|
) -> PortState:
|
||||||
|
return cls(definition=PortDefinition.pneumatic(name, nominal_role=nominal_role))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def inflow_rate(self) -> float:
|
||||||
|
return max(self.m_flow, 0.0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def outflow_rate(self) -> float:
|
||||||
|
return max(-self.m_flow, 0.0)
|
||||||
|
|
||||||
|
def actual_direction(self, tolerance: float = 1e-12) -> ActualFlowDirection:
|
||||||
|
if self.m_flow > tolerance:
|
||||||
|
return "in"
|
||||||
|
if self.m_flow < -tolerance:
|
||||||
|
return "out"
|
||||||
|
return "stagnant"
|
||||||
|
|
||||||
File renamed without changes.
@@ -0,0 +1 @@
|
|||||||
|
"""Reference systems and regression examples."""
|
||||||
Loaded 100 of 393 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user