openflume 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ # Dependencies
2
+ node_modules/
3
+ .pnpm-store/
4
+
5
+ # Build output
6
+ dist/
7
+ *.tsbuildinfo
8
+
9
+ # Test & coverage
10
+ test-results/
11
+ playwright-report/
12
+ coverage/
13
+ *.lcov
14
+
15
+ # Logs
16
+ *.log
17
+ npm-debug.log*
18
+
19
+ # OS
20
+ .DS_Store
21
+ Thumbs.db
22
+
23
+ # IDE
24
+ .vscode/
25
+ .idea/
26
+ *.swp
27
+ *.swo
28
+
29
+ # Local env
30
+ .env
31
+
32
+ # Python venv for scripts/build-cea-tables.py (offline table generator only —
33
+ # never imported at runtime; see docs/combustion.md)
34
+ .venv-cea/
35
+ __pycache__/
36
+ .env.local
37
+
38
+ # Misc
39
+ *.tgz
40
+ debug_*.ts
41
+ debug-*.ts
42
+ check_*.ts
43
+ check-*.ts
44
+ diag_*.ts
45
+ diag-*.ts
46
+ scratch_*.ts
47
+ scratch-*.ts
48
+ tmp_*.ts
49
+ tmp-*.ts
50
+ # Root-level scratch scripts named with suffixes (e.g. nh3_debug.ts, nh3_probe.mjs)
51
+ /*_debug*.ts
52
+ /*_debug*.mjs
53
+ /*_probe*.ts
54
+ /*_probe*.mjs
55
+ # Preferred home for local scratch work
56
+ /scratch/
57
+
58
+ # Vite cache
59
+ .vite/
60
+
61
+ # Validation source documents — re-fetchable via URLs/DOIs in the CATALOG files
62
+ validation/**/*.pdf
63
+
64
+ # Cloned upstream repo (nested .git) — re-clonable:
65
+ # git clone github.com/fateh-btpfl/FlowBoilingHTC && git checkout f2e6d83
66
+ # (see validation/references/CATALOG_TIERS.md, tier-5 entry #31)
67
+ validation/references/tier5/FlowBoilingHTC/
68
+
69
+ # QC render PNGs (~31 MB, regenerable via validation/qc_review/build_gallery.py) —
70
+ # the gallery scripts/HTML under qc_review/ ARE committed
71
+ validation/qc_review/pdf_pages/
72
+ validation/qc_review/plots/
73
+
74
+ # Python bytecode from validation extraction scripts
75
+ __pycache__/
76
+
77
+ # Local digitization scratch workspaces (untracked, regenerable): the ~97 MB
78
+ # nbs9264_verify replot/screenshot workspace and the clicker analysis overlays.
79
+ # Canonical digitized CSVs, calibrations, and annotations remain tracked.
80
+ validation/data/digitized/chilldown/nbs9264_verify/
81
+ validation/data/digitized/chilldown/nbs9264_clicker/analysis/
82
+
83
+ # Regenerable digitization workspace outputs (retain source points/logs/docs)
84
+ validation/data/digitized/chilldown/*/auto/cache/
85
+ validation/data/digitized/chilldown/*/auto/overlays/
86
+ validation/data/digitized/chilldown/*/analysis/previews/
87
+ validation/data/digitized/chilldown/*/analysis/overlays/
88
+ validation/data/digitized/chilldown/*/analysis/*.png
89
+ validation/data/digitized/chilldown/*/analysis/rejected_*/
90
+ validation/data/digitized/chilldown/*/previews/
91
+ validation/data/digitized/chilldown/*/shots/
92
+ validation/data/digitized/chilldown/*/rejected_*/
93
+ validation/data/digitized/chilldown/*/fast_extract/**/cache.npz
94
+ validation/data/digitized/chilldown/*/fast_extract/**/.preprocess_time.json
95
+ validation/data/digitized/chilldown/*/fast_extract/**/diagnostics.png
96
+ validation/data/digitized/chilldown/*/fast_extract/**/overlay.png
97
+
98
+ # Python package (python/): bundled JS core + license copy are build
99
+ # artifacts of `npm run build:python`; wheels/sdists of `python -m build`.
100
+ python/src/openflume/_js/
101
+ python/LICENSE
102
+ python/dist/
103
+ python/build/
104
+ .venv/
105
+ *.egg-info/
106
+ .pytest_cache/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenFLUME contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.5
2
+ Name: openflume
3
+ Version: 0.3.1
4
+ Summary: Python API for the OpenFLUME thermal-fluid network solver (steady and transient, real fluids via CoolProp).
5
+ Project-URL: Homepage, https://github.com/OpenFLUME/OpenFLUME
6
+ Project-URL: Documentation, https://github.com/OpenFLUME/OpenFLUME/blob/main/docs/python.md
7
+ Project-URL: Issues, https://github.com/OpenFLUME/OpenFLUME/issues
8
+ Project-URL: Changelog, https://github.com/OpenFLUME/OpenFLUME/blob/main/CHANGELOG.md
9
+ Author: OpenFLUME contributors
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: CoolProp,GFSSP,aerospace,cryogenics,fluid network,propulsion,solver,thermal-fluid
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Scientific/Engineering :: Physics
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: nodejs-wheel-binaries>=22.13
28
+ Provides-Extra: numpy
29
+ Requires-Dist: numpy>=1.24; extra == 'numpy'
30
+ Provides-Extra: pandas
31
+ Requires-Dist: pandas>=2.0; extra == 'pandas'
32
+ Provides-Extra: test
33
+ Requires-Dist: pytest>=8; extra == 'test'
34
+ Description-Content-Type: text/markdown
35
+
36
+ # openflume (Python)
37
+
38
+ Python API for [OpenFLUME](https://github.com/OpenFLUME/OpenFLUME), an open-source
39
+ thermal-fluid network solver for steady and transient analysis of propulsion feed
40
+ systems, cryogenic lines, and tank pressurization, with real-fluid properties from
41
+ CoolProp.
42
+
43
+ ```bash
44
+ pip install openflume # add [numpy,pandas] for array/DataFrame output
45
+ ```
46
+
47
+ No other installation is needed. The solver core is the same TypeScript code as the
48
+ web app, bundled into the package and run on a Node.js runtime that pip installs as
49
+ a dependency (`nodejs-wheel-binaries`). It is the same solver, with the same
50
+ verification record, not a port.
51
+
52
+ ## Quick start
53
+
54
+ ```python
55
+ import openflume as of
56
+
57
+ net = of.load("feed_system.fn") # .fn from the web app, or .json
58
+ print(net.validate()) # [] means runnable
59
+
60
+ result = net.solve() # steady or transient per settings.mode
61
+ print(result.converged, result.node("tank")["pressure"]) # SI units: Pa
62
+ ```
63
+
64
+ Start from a bundled example and edit the model as a plain dict:
65
+
66
+ ```python
67
+ net = of.example("Tank blowdown")
68
+ net.settings["endTime"] = 2.0
69
+ net.branch("orifice")["component"]["cd"] = 0.65
70
+
71
+ tr = net.solve(progress=lambda p: print(f"t = {p['time']:.3f} s", end="\r"))
72
+ t, p = tr.times, tr.series("tank", "pressure") # numpy arrays if numpy is installed
73
+ df = tr.frame() # pandas: index time, columns (id, quantity)
74
+ ```
75
+
76
+ Parameter sweeps are ordinary Python:
77
+
78
+ ```python
79
+ base = of.example("Sanity: orifice hand-calc")
80
+ for cd in (0.55, 0.60, 0.65):
81
+ net = base.copy()
82
+ net.branch("o")["component"]["cd"] = cd
83
+ print(cd, net.solve().branch("o")["mdot"])
84
+ ```
85
+
86
+ Fluid properties use the same EOS models as the solver:
87
+
88
+ ```python
89
+ lox = of.Fluid.real("Oxygen")
90
+ lox.density(2e6, 95.0) # kg/m³
91
+ lox.saturation_temperature([1e5, 5e5, 1e6])
92
+ of.fluids(curated=True) # CoolProp HEOS catalogue
93
+ of.combustion_gas("lox-rp1", pc=7e6, of=2.6) # NASA CEA equilibrium tables
94
+ ```
95
+
96
+ Command line:
97
+
98
+ ```bash
99
+ openflume validate model.fn
100
+ openflume solve model.fn -o result.json
101
+ openflume info
102
+ ```
103
+
104
+ ## Notes
105
+
106
+ - **Units** are SI throughout: Pa, K, kg/s, m, m², J/kg.
107
+ - **Always check `result.converged`.** The solver returns its best state either way.
108
+ - **Model format.** `Network.config` is the `NetworkConfig` dict (schema v2) defined in
109
+ `src/core/schema.ts` and documented in the user manual. `.fn` files round-trip
110
+ losslessly with the web app.
111
+ - **User code.** Models can embed JavaScript user components (`componentLibrary`). That
112
+ code runs in the Node process with your user's privileges, so only solve models you
113
+ trust.
114
+ - **Runtime.** One Node process per `Session` is started on first use and stopped at
115
+ exit. Set `OPENFLUME_NODE` to use a specific Node.js 22+ executable. For parallel
116
+ sweeps, use one process (and therefore one session) per worker, e.g. with
117
+ `concurrent.futures.ProcessPoolExecutor`. Ctrl-C interrupts a running solve.
118
+
119
+ Full documentation: [docs/python.md](https://github.com/OpenFLUME/OpenFLUME/blob/main/docs/python.md).
120
+ License: MIT.
@@ -0,0 +1,85 @@
1
+ # openflume (Python)
2
+
3
+ Python API for [OpenFLUME](https://github.com/OpenFLUME/OpenFLUME), an open-source
4
+ thermal-fluid network solver for steady and transient analysis of propulsion feed
5
+ systems, cryogenic lines, and tank pressurization, with real-fluid properties from
6
+ CoolProp.
7
+
8
+ ```bash
9
+ pip install openflume # add [numpy,pandas] for array/DataFrame output
10
+ ```
11
+
12
+ No other installation is needed. The solver core is the same TypeScript code as the
13
+ web app, bundled into the package and run on a Node.js runtime that pip installs as
14
+ a dependency (`nodejs-wheel-binaries`). It is the same solver, with the same
15
+ verification record, not a port.
16
+
17
+ ## Quick start
18
+
19
+ ```python
20
+ import openflume as of
21
+
22
+ net = of.load("feed_system.fn") # .fn from the web app, or .json
23
+ print(net.validate()) # [] means runnable
24
+
25
+ result = net.solve() # steady or transient per settings.mode
26
+ print(result.converged, result.node("tank")["pressure"]) # SI units: Pa
27
+ ```
28
+
29
+ Start from a bundled example and edit the model as a plain dict:
30
+
31
+ ```python
32
+ net = of.example("Tank blowdown")
33
+ net.settings["endTime"] = 2.0
34
+ net.branch("orifice")["component"]["cd"] = 0.65
35
+
36
+ tr = net.solve(progress=lambda p: print(f"t = {p['time']:.3f} s", end="\r"))
37
+ t, p = tr.times, tr.series("tank", "pressure") # numpy arrays if numpy is installed
38
+ df = tr.frame() # pandas: index time, columns (id, quantity)
39
+ ```
40
+
41
+ Parameter sweeps are ordinary Python:
42
+
43
+ ```python
44
+ base = of.example("Sanity: orifice hand-calc")
45
+ for cd in (0.55, 0.60, 0.65):
46
+ net = base.copy()
47
+ net.branch("o")["component"]["cd"] = cd
48
+ print(cd, net.solve().branch("o")["mdot"])
49
+ ```
50
+
51
+ Fluid properties use the same EOS models as the solver:
52
+
53
+ ```python
54
+ lox = of.Fluid.real("Oxygen")
55
+ lox.density(2e6, 95.0) # kg/m³
56
+ lox.saturation_temperature([1e5, 5e5, 1e6])
57
+ of.fluids(curated=True) # CoolProp HEOS catalogue
58
+ of.combustion_gas("lox-rp1", pc=7e6, of=2.6) # NASA CEA equilibrium tables
59
+ ```
60
+
61
+ Command line:
62
+
63
+ ```bash
64
+ openflume validate model.fn
65
+ openflume solve model.fn -o result.json
66
+ openflume info
67
+ ```
68
+
69
+ ## Notes
70
+
71
+ - **Units** are SI throughout: Pa, K, kg/s, m, m², J/kg.
72
+ - **Always check `result.converged`.** The solver returns its best state either way.
73
+ - **Model format.** `Network.config` is the `NetworkConfig` dict (schema v2) defined in
74
+ `src/core/schema.ts` and documented in the user manual. `.fn` files round-trip
75
+ losslessly with the web app.
76
+ - **User code.** Models can embed JavaScript user components (`componentLibrary`). That
77
+ code runs in the Node process with your user's privileges, so only solve models you
78
+ trust.
79
+ - **Runtime.** One Node process per `Session` is started on first use and stopped at
80
+ exit. Set `OPENFLUME_NODE` to use a specific Node.js 22+ executable. For parallel
81
+ sweeps, use one process (and therefore one session) per worker, e.g. with
82
+ `concurrent.futures.ProcessPoolExecutor`. Ctrl-C interrupts a running solve.
83
+
84
+ Full documentation: [docs/python.md](https://github.com/OpenFLUME/OpenFLUME/blob/main/docs/python.md).
85
+ License: MIT.
@@ -0,0 +1,30 @@
1
+ """Build hook: refuse to produce a wheel/sdist without the bundled JS core.
2
+
3
+ The Node bridge and coolprop.wasm in src/openflume/_js/ are generated by
4
+ `npm run build:python` at the repository root. Building from an sdist works
5
+ without Node because the sdist already carries them.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
13
+
14
+ REQUIRED = ("openflume-bridge.mjs", "coolprop.wasm")
15
+
16
+
17
+ class CustomBuildHook(BuildHookInterface):
18
+ def initialize(self, version: str, build_data: dict) -> None:
19
+ root = Path(self.root)
20
+ js_dir = root / "src" / "openflume" / "_js"
21
+ missing = [name for name in REQUIRED if not (js_dir / name).is_file()]
22
+ if not (root / "LICENSE").is_file():
23
+ missing.append("../LICENSE copy")
24
+ if missing:
25
+ raise RuntimeError(
26
+ "openflume: bundled JS core not found ("
27
+ + ", ".join(missing)
28
+ + "). Run `npm ci && npm run build:python` at the repository "
29
+ "root before building the Python package."
30
+ )
@@ -0,0 +1,94 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "openflume"
7
+ # Kept equal to ../package.json "version" — tests/test_packaging.py enforces it.
8
+ version = "0.3.1"
9
+ description = "Python API for the OpenFLUME thermal-fluid network solver (steady and transient, real fluids via CoolProp)."
10
+ readme = "README.md"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ requires-python = ">=3.10"
14
+ authors = [{ name = "OpenFLUME contributors" }]
15
+ keywords = [
16
+ "fluid network",
17
+ "thermal-fluid",
18
+ "solver",
19
+ "aerospace",
20
+ "propulsion",
21
+ "cryogenics",
22
+ "CoolProp",
23
+ "GFSSP",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 3 - Alpha",
27
+ "Intended Audience :: Science/Research",
28
+ "Intended Audience :: Developers",
29
+ "Operating System :: OS Independent",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3 :: Only",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
36
+ "Programming Language :: Python :: 3.14",
37
+ "Topic :: Scientific/Engineering :: Physics",
38
+ "Typing :: Typed",
39
+ ]
40
+ dependencies = [
41
+ # Supplies a Node.js runtime as a regular wheel, so `pip install openflume`
42
+ # needs nothing else on the machine. OPENFLUME_NODE overrides it.
43
+ "nodejs-wheel-binaries>=22.13",
44
+ ]
45
+
46
+ [project.optional-dependencies]
47
+ numpy = ["numpy>=1.24"]
48
+ pandas = ["pandas>=2.0"]
49
+ test = ["pytest>=8"]
50
+
51
+ [project.scripts]
52
+ openflume = "openflume.__main__:main"
53
+
54
+ [project.urls]
55
+ Homepage = "https://github.com/OpenFLUME/OpenFLUME"
56
+ Documentation = "https://github.com/OpenFLUME/OpenFLUME/blob/main/docs/python.md"
57
+ Issues = "https://github.com/OpenFLUME/OpenFLUME/issues"
58
+ Changelog = "https://github.com/OpenFLUME/OpenFLUME/blob/main/CHANGELOG.md"
59
+
60
+ # The bundled JS core (src/openflume/_js/) is a gitignored build artifact
61
+ # produced by `npm run build:python` at the repository root; `artifacts`
62
+ # re-includes it despite .gitignore, and hatch_build.py refuses to build a
63
+ # package without it.
64
+ [tool.hatch.build.hooks.custom]
65
+ path = "hatch_build.py"
66
+
67
+ [tool.hatch.build.targets.wheel]
68
+ packages = ["src/openflume"]
69
+ artifacts = ["src/openflume/_js/*"]
70
+
71
+ [tool.hatch.build.targets.sdist]
72
+ include = [
73
+ "src/openflume",
74
+ "tests",
75
+ "hatch_build.py",
76
+ "README.md",
77
+ "LICENSE",
78
+ ]
79
+ artifacts = ["src/openflume/_js/*", "LICENSE"]
80
+
81
+ [tool.pytest.ini_options]
82
+ testpaths = ["tests"]
83
+ addopts = "-ra"
84
+
85
+ [tool.ruff]
86
+ line-length = 120
87
+ target-version = "py310"
88
+
89
+ [tool.ruff.lint]
90
+ select = ["E", "F", "W", "I", "UP", "B", "SIM", "RUF"]
91
+ ignore = [
92
+ "RUF001", "RUF002", "RUF003", # ρ, μ, ² etc. in docstrings are intentional
93
+ "RUF022", # __all__ is grouped by topic, not sorted
94
+ ]
@@ -0,0 +1,79 @@
1
+ """OpenFLUME — thermal-fluid network solver, from Python.
2
+
3
+ Quick start::
4
+
5
+ import openflume as of
6
+
7
+ net = of.load("feed_system.fn") # or of.example("Tank blowdown")
8
+ errors = net.validate() # [] means runnable
9
+ result = net.solve() # steady or transient per settings.mode
10
+ print(result.converged, result.node("tank")["pressure"])
11
+
12
+ n2 = of.Fluid.real("Nitrogen")
13
+ n2.density(101325, 300.0) # kg/m³
14
+
15
+ Everything is SI (Pa, K, kg/s, m, J/kg). The solver core is the same
16
+ TypeScript code that runs the web app, executed in a bundled Node.js runtime
17
+ (see :class:`Session`). Models that embed JavaScript user components run that
18
+ code with the privileges of your Python process — only solve models you trust.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from importlib.metadata import PackageNotFoundError
24
+ from importlib.metadata import version as _dist_version
25
+
26
+ from ._bridge import Session, default_session, find_node, shutdown
27
+ from .errors import (
28
+ BridgeError,
29
+ ConfigDecodeError,
30
+ CoreError,
31
+ InvalidRequestError,
32
+ OpenFLUMEError,
33
+ ParseError,
34
+ UnknownExampleError,
35
+ UnsupportedPropertyError,
36
+ ValidationError,
37
+ )
38
+ from .fluids import Fluid, combustion_gas, combustion_propellants, fluids
39
+ from .network import Network, example, examples, load, solve, validate
40
+ from .results import SteadyResult, TransientResult
41
+
42
+ try:
43
+ __version__ = _dist_version("openflume")
44
+ except PackageNotFoundError: # running from a source tree without install
45
+ __version__ = "0+unknown"
46
+
47
+ __all__ = [
48
+ "__version__",
49
+ # models
50
+ "Network",
51
+ "load",
52
+ "example",
53
+ "examples",
54
+ "solve",
55
+ "validate",
56
+ # results
57
+ "SteadyResult",
58
+ "TransientResult",
59
+ # fluids
60
+ "Fluid",
61
+ "fluids",
62
+ "combustion_gas",
63
+ "combustion_propellants",
64
+ # runtime
65
+ "Session",
66
+ "default_session",
67
+ "shutdown",
68
+ "find_node",
69
+ # errors
70
+ "OpenFLUMEError",
71
+ "BridgeError",
72
+ "ConfigDecodeError",
73
+ "ValidationError",
74
+ "ParseError",
75
+ "CoreError",
76
+ "UnknownExampleError",
77
+ "InvalidRequestError",
78
+ "UnsupportedPropertyError",
79
+ ]
@@ -0,0 +1,90 @@
1
+ """Command line: ``openflume info | validate MODEL | solve MODEL [-o OUT]``.
2
+
3
+ Also runnable as ``python -m openflume``. Results are written as JSON (the
4
+ core's raw result object); exit status is 0 on success, 1 on validation
5
+ failure or a non-converged solve, 2 on usage/IO errors.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ from . import __version__
16
+ from ._bridge import _encode_nonfinite, default_session, find_node
17
+ from .errors import OpenFLUMEError
18
+ from .network import Network
19
+
20
+
21
+ def _cmd_info(_: argparse.Namespace) -> int:
22
+ info = default_session().start().info
23
+ print(f"openflume {__version__}")
24
+ print(f"core {info.get('version')} (config schema v{info.get('configVersion')})")
25
+ print(f"node {info.get('node')} ({find_node()})")
26
+ return 0
27
+
28
+
29
+ def _cmd_validate(args: argparse.Namespace) -> int:
30
+ errors = Network.load(args.model).validate()
31
+ for e in errors:
32
+ print(e)
33
+ if not errors:
34
+ print("valid")
35
+ return 1 if errors else 0
36
+
37
+
38
+ def _cmd_solve(args: argparse.Namespace) -> int:
39
+ net = Network.load(args.model)
40
+
41
+ def progress(p: dict) -> None:
42
+ if p.get("kind") == "transient":
43
+ print(f"\r t = {p['time']:.6g} / {p['endTime']:.6g} s", end="", file=sys.stderr)
44
+ else:
45
+ print(f"\r iter {p['iteration']} residual {p['residual']:.3e}", end="", file=sys.stderr)
46
+
47
+ result = net.solve(args.mode, progress=None if args.quiet else progress)
48
+ if not args.quiet:
49
+ print(file=sys.stderr)
50
+ print(repr(result), file=sys.stderr)
51
+ text = json.dumps(_encode_nonfinite(result.raw), indent=None if args.compact else 2)
52
+ if args.output:
53
+ Path(args.output).write_text(text + "\n", encoding="utf-8")
54
+ else:
55
+ print(text)
56
+ return 0 if result.converged else 1
57
+
58
+
59
+ def main(argv: list[str] | None = None) -> int:
60
+ parser = argparse.ArgumentParser(prog="openflume", description="OpenFLUME thermal-fluid network solver")
61
+ parser.add_argument("--version", action="version", version=f"openflume {__version__}")
62
+ sub = parser.add_subparsers(dest="command", required=True)
63
+
64
+ sub.add_parser("info", help="show package, core and Node.js versions").set_defaults(fn=_cmd_info)
65
+
66
+ p_val = sub.add_parser("validate", help="validate a .fn/.json model")
67
+ p_val.add_argument("model")
68
+ p_val.set_defaults(fn=_cmd_validate)
69
+
70
+ p_solve = sub.add_parser("solve", help="solve a .fn/.json model and print the JSON result")
71
+ p_solve.add_argument("model")
72
+ p_solve.add_argument("-o", "--output", help="write the JSON result here instead of stdout")
73
+ p_solve.add_argument("--mode", choices=["steady", "transient"], help="override settings.mode")
74
+ p_solve.add_argument("-q", "--quiet", action="store_true", help="no progress on stderr")
75
+ p_solve.add_argument("--compact", action="store_true", help="single-line JSON")
76
+ p_solve.set_defaults(fn=_cmd_solve)
77
+
78
+ args = parser.parse_args(argv)
79
+ try:
80
+ return int(args.fn(args))
81
+ except OpenFLUMEError as exc:
82
+ print(f"error: {exc}", file=sys.stderr)
83
+ return 1
84
+ except OSError as exc:
85
+ print(f"error: {exc}", file=sys.stderr)
86
+ return 2
87
+
88
+
89
+ if __name__ == "__main__":
90
+ sys.exit(main())