WitZuk 0.1.0__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,3 @@
1
+ No license has been selected yet.
2
+ Before public release, choose and add a LICENSE file and corresponding metadata in pyproject.toml.
3
+ Without a license, copyright remains with the author and reuse rights are not granted automatically.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE*
3
+ recursive-include src/witzuk py.typed
witzuk-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: WitZuk
3
+ Version: 0.1.0
4
+ Summary: Scientific and engineering simulation functions by Witold Zukowski
5
+ Author: Witold Zukowski
6
+ Keywords: chemical-engineering,fluidized-bed,Kato-Wen,formic-acid,HCOOH
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: Intended Audience :: Education
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE_NOT_SELECTED.txt
20
+ Requires-Dist: numpy>=1.24
21
+ Requires-Dist: scipy>=1.10
22
+ Requires-Dist: pandas>=2.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: build>=1.2; extra == "dev"
25
+ Requires-Dist: twine>=5.1; extra == "dev"
26
+ Requires-Dist: pytest>=8.0; extra == "dev"
27
+ Requires-Dist: ruff>=0.6; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # WitZuk
31
+
32
+ Biblioteka funkcji naukowych i inżynierskich autorstwa Witolda Żukowskiego.
33
+
34
+ ## Moduł `hcooh_fbr`
35
+
36
+ Model płuczki z kwasem mrówkowym i reaktora fluidyzacyjnego Kato-Wena.
37
+
38
+ ### Instalacja
39
+
40
+ ```bash
41
+ python -m pip install WitZuk
42
+ ```
43
+
44
+ ### Minimalny przykład z płuczką
45
+
46
+ ```python
47
+ from witzuk.hcooh_fbr import simulate_scrubber, simulate_fluidized_bed
48
+
49
+ feed = simulate_scrubber()
50
+ result = simulate_fluidized_bed(feed, return_profile=False)
51
+ print(result["conversion"]["HCOOH"])
52
+ ```
53
+
54
+ ### Minimalny przykład bez płuczki
55
+
56
+ ```python
57
+ from witzuk.hcooh_fbr import stream_from_feed, simulate_fluidized_bed
58
+
59
+ feed = stream_from_feed(
60
+ molar_fractions={"HCOOH": 0.02, "N2": 0.98},
61
+ total_flow_mol_s=5.0e-4,
62
+ )
63
+ result = simulate_fluidized_bed(feed, return_profile=False)
64
+ ```
65
+
66
+ ### Bilans cieplny i pierwiastkowy
67
+
68
+ ```python
69
+ from witzuk.hcooh_fbr import calculate_reactor_heat_effect, check_element_balance
70
+
71
+ heat = calculate_reactor_heat_effect(feed, result["outlet_stream"])
72
+ balance = check_element_balance(feed, result["outlet_stream"])
73
+ ```
74
+
75
+ ## Status
76
+
77
+ Wersja 0.1.0 (alpha). Przed publikacją wybierz licencję i uzupełnij metadane projektu.
witzuk-0.1.0/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # WitZuk
2
+
3
+ Biblioteka funkcji naukowych i inżynierskich autorstwa Witolda Żukowskiego.
4
+
5
+ ## Moduł `hcooh_fbr`
6
+
7
+ Model płuczki z kwasem mrówkowym i reaktora fluidyzacyjnego Kato-Wena.
8
+
9
+ ### Instalacja
10
+
11
+ ```bash
12
+ python -m pip install WitZuk
13
+ ```
14
+
15
+ ### Minimalny przykład z płuczką
16
+
17
+ ```python
18
+ from witzuk.hcooh_fbr import simulate_scrubber, simulate_fluidized_bed
19
+
20
+ feed = simulate_scrubber()
21
+ result = simulate_fluidized_bed(feed, return_profile=False)
22
+ print(result["conversion"]["HCOOH"])
23
+ ```
24
+
25
+ ### Minimalny przykład bez płuczki
26
+
27
+ ```python
28
+ from witzuk.hcooh_fbr import stream_from_feed, simulate_fluidized_bed
29
+
30
+ feed = stream_from_feed(
31
+ molar_fractions={"HCOOH": 0.02, "N2": 0.98},
32
+ total_flow_mol_s=5.0e-4,
33
+ )
34
+ result = simulate_fluidized_bed(feed, return_profile=False)
35
+ ```
36
+
37
+ ### Bilans cieplny i pierwiastkowy
38
+
39
+ ```python
40
+ from witzuk.hcooh_fbr import calculate_reactor_heat_effect, check_element_balance
41
+
42
+ heat = calculate_reactor_heat_effect(feed, result["outlet_stream"])
43
+ balance = check_element_balance(feed, result["outlet_stream"])
44
+ ```
45
+
46
+ ## Status
47
+
48
+ Wersja 0.1.0 (alpha). Przed publikacją wybierz licencję i uzupełnij metadane projektu.
@@ -0,0 +1,48 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "WitZuk"
7
+ version = "0.1.0"
8
+ description = "Scientific and engineering simulation functions by Witold Zukowski"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{ name = "Witold Zukowski" }]
12
+ dependencies = [
13
+ "numpy>=1.24",
14
+ "scipy>=1.10",
15
+ "pandas>=2.0",
16
+ ]
17
+ classifiers = [
18
+ "Development Status :: 3 - Alpha",
19
+ "Intended Audience :: Science/Research",
20
+ "Intended Audience :: Education",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3 :: Only",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Topic :: Scientific/Engineering :: Chemistry",
28
+ ]
29
+ keywords = ["chemical-engineering", "fluidized-bed", "Kato-Wen", "formic-acid", "HCOOH"]
30
+
31
+ [project.optional-dependencies]
32
+ dev = [
33
+ "build>=1.2",
34
+ "twine>=5.1",
35
+ "pytest>=8.0",
36
+ "ruff>=0.6",
37
+ ]
38
+
39
+ [tool.setuptools.packages.find]
40
+ where = ["src"]
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
44
+ addopts = "-ra"
45
+
46
+ [tool.ruff]
47
+ line-length = 100
48
+ target-version = "py310"
witzuk-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,77 @@
1
+ Metadata-Version: 2.4
2
+ Name: WitZuk
3
+ Version: 0.1.0
4
+ Summary: Scientific and engineering simulation functions by Witold Zukowski
5
+ Author: Witold Zukowski
6
+ Keywords: chemical-engineering,fluidized-bed,Kato-Wen,formic-acid,HCOOH
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: Intended Audience :: Education
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE_NOT_SELECTED.txt
20
+ Requires-Dist: numpy>=1.24
21
+ Requires-Dist: scipy>=1.10
22
+ Requires-Dist: pandas>=2.0
23
+ Provides-Extra: dev
24
+ Requires-Dist: build>=1.2; extra == "dev"
25
+ Requires-Dist: twine>=5.1; extra == "dev"
26
+ Requires-Dist: pytest>=8.0; extra == "dev"
27
+ Requires-Dist: ruff>=0.6; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # WitZuk
31
+
32
+ Biblioteka funkcji naukowych i inżynierskich autorstwa Witolda Żukowskiego.
33
+
34
+ ## Moduł `hcooh_fbr`
35
+
36
+ Model płuczki z kwasem mrówkowym i reaktora fluidyzacyjnego Kato-Wena.
37
+
38
+ ### Instalacja
39
+
40
+ ```bash
41
+ python -m pip install WitZuk
42
+ ```
43
+
44
+ ### Minimalny przykład z płuczką
45
+
46
+ ```python
47
+ from witzuk.hcooh_fbr import simulate_scrubber, simulate_fluidized_bed
48
+
49
+ feed = simulate_scrubber()
50
+ result = simulate_fluidized_bed(feed, return_profile=False)
51
+ print(result["conversion"]["HCOOH"])
52
+ ```
53
+
54
+ ### Minimalny przykład bez płuczki
55
+
56
+ ```python
57
+ from witzuk.hcooh_fbr import stream_from_feed, simulate_fluidized_bed
58
+
59
+ feed = stream_from_feed(
60
+ molar_fractions={"HCOOH": 0.02, "N2": 0.98},
61
+ total_flow_mol_s=5.0e-4,
62
+ )
63
+ result = simulate_fluidized_bed(feed, return_profile=False)
64
+ ```
65
+
66
+ ### Bilans cieplny i pierwiastkowy
67
+
68
+ ```python
69
+ from witzuk.hcooh_fbr import calculate_reactor_heat_effect, check_element_balance
70
+
71
+ heat = calculate_reactor_heat_effect(feed, result["outlet_stream"])
72
+ balance = check_element_balance(feed, result["outlet_stream"])
73
+ ```
74
+
75
+ ## Status
76
+
77
+ Wersja 0.1.0 (alpha). Przed publikacją wybierz licencję i uzupełnij metadane projektu.
@@ -0,0 +1,21 @@
1
+ LICENSE_NOT_SELECTED.txt
2
+ MANIFEST.in
3
+ README.md
4
+ pyproject.toml
5
+ src/WitZuk.egg-info/PKG-INFO
6
+ src/WitZuk.egg-info/SOURCES.txt
7
+ src/WitZuk.egg-info/dependency_links.txt
8
+ src/WitZuk.egg-info/requires.txt
9
+ src/WitZuk.egg-info/top_level.txt
10
+ src/witzuk/__init__.py
11
+ src/witzuk/_version.py
12
+ src/witzuk/py.typed
13
+ src/witzuk/hcooh_fbr/__init__.py
14
+ src/witzuk/hcooh_fbr/element_balance.py
15
+ src/witzuk/hcooh_fbr/fluidized_bed.py
16
+ src/witzuk/hcooh_fbr/heat_effect.py
17
+ src/witzuk/hcooh_fbr/py.typed
18
+ src/witzuk/hcooh_fbr/scrubber.py
19
+ src/witzuk/hcooh_fbr/stream_from_feed.py
20
+ tests/test_imports.py
21
+ tests/test_public_api.py
@@ -0,0 +1,9 @@
1
+ numpy>=1.24
2
+ scipy>=1.10
3
+ pandas>=2.0
4
+
5
+ [dev]
6
+ build>=1.2
7
+ twine>=5.1
8
+ pytest>=8.0
9
+ ruff>=0.6
@@ -0,0 +1 @@
1
+ witzuk
@@ -0,0 +1,5 @@
1
+ """WitZuk scientific and engineering library."""
2
+
3
+ from ._version import __version__
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,19 @@
1
+ """HCOOH scrubber and fluidized-bed reactor models."""
2
+
3
+ from .element_balance import check_element_balance, elemental_molar_flows
4
+ from .fluidized_bed import Params, simulate_fluidized_bed
5
+ from .heat_effect import calculate_reactor_heat_effect, stream_enthalpy_flow_W
6
+ from .scrubber import simulate_scrubber
7
+ from .stream_from_feed import reactor_feed_from_stream, stream_from_feed
8
+
9
+ __all__ = [
10
+ "Params",
11
+ "calculate_reactor_heat_effect",
12
+ "check_element_balance",
13
+ "elemental_molar_flows",
14
+ "reactor_feed_from_stream",
15
+ "simulate_fluidized_bed",
16
+ "simulate_scrubber",
17
+ "stream_enthalpy_flow_W",
18
+ "stream_from_feed",
19
+ ]
@@ -0,0 +1,47 @@
1
+ """Bilans molowy pierwiastkow dla uniwersalnych strumieni gazowych."""
2
+
3
+ import math
4
+
5
+ ATOMS = {
6
+ "HCOOH": {"C": 1, "O": 2, "H": 2},
7
+ "CO": {"C": 1, "O": 1, "H": 0},
8
+ "H2O": {"C": 0, "O": 1, "H": 2},
9
+ "CO2": {"C": 1, "O": 2, "H": 0},
10
+ "H2": {"C": 0, "O": 0, "H": 2},
11
+ "CH4": {"C": 1, "O": 0, "H": 4},
12
+ "N2": {"C": 0, "O": 0, "H": 0},
13
+ }
14
+
15
+
16
+ def elemental_molar_flows(stream):
17
+ """Zwraca strumienie molowe atomow C, O i H w mol_atom/s."""
18
+ flows = stream["molar_flows"]
19
+ return {
20
+ element: sum(
21
+ float(flows.get(species, 0.0)) * composition[element]
22
+ for species, composition in ATOMS.items()
23
+ )
24
+ for element in ("C", "O", "H")
25
+ }
26
+
27
+
28
+ def check_element_balance(inlet_stream, outlet_stream, rtol=1e-6, atol=1e-12):
29
+ """Sprawdza bilans C, O i H pomiedzy wlotem i wylotem reaktora."""
30
+ inlet = elemental_molar_flows(inlet_stream)
31
+ outlet = elemental_molar_flows(outlet_stream)
32
+ result = {}
33
+
34
+ for element in ("C", "O", "H"):
35
+ difference = outlet[element] - inlet[element]
36
+ relative_error = difference / inlet[element] if abs(inlet[element]) > atol else 0.0
37
+ result[element] = {
38
+ "in_mol_atom_s": inlet[element],
39
+ "out_mol_atom_s": outlet[element],
40
+ "difference_mol_atom_s": difference,
41
+ "relative_error": relative_error,
42
+ "relative_error_percent": 100.0 * relative_error,
43
+ "balanced": math.isclose(outlet[element], inlet[element], rel_tol=rtol, abs_tol=atol),
44
+ }
45
+
46
+ result["all_balanced"] = all(result[e]["balanced"] for e in ("C", "O", "H"))
47
+ return result
@@ -0,0 +1,131 @@
1
+ """Niezależny model reaktora fluidyzacyjnego Kato-Wena dla HCOOH/N2."""
2
+
3
+ import math
4
+ from dataclasses import dataclass, fields
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+ from scipy.integrate import solve_ivp, trapezoid
9
+
10
+ from .stream_from_feed import SPECIES, R, reactor_feed_from_stream, stream_from_feed
11
+
12
+ G, P0, T0 = 9.81, 1e5, 298.15
13
+ IDX = {s: i for i, s in enumerate(SPECIES)}
14
+ NS = len(SPECIES)
15
+ NU = np.array([[-1.,-1.,0.],[1.,0.,-1.],[1.,0.,1.],[0.,1.,0.],
16
+ [0.,1.,-3.],[0.,0.,1.],[0.,0.,0.]])
17
+ DNU = NU.sum(axis=0)
18
+ MW = np.array([46.0254,28.0101,18.01528,44.0095,2.01588,16.0425,28.0134])/1000
19
+ FULLER_V = np.array([41.,18.,13.1,26.9,6.12,25.15,18.5])
20
+ SIGMA = np.array([4.2,3.69,2.64,3.996,2.92,3.78,3.798])
21
+ EPSK = np.array([470.,91.7,809.1,190.,59.7,148.6,71.4])
22
+
23
+ @dataclass
24
+ class Params:
25
+ T_reactor: float=573.15; p: float=101325.; D_col: float=0.034
26
+ H: float=0.096; dp: float=0.00011; rho_s: float=1235.; eps_mf: float=0.43
27
+ k1_dehydr: float=1.5; k2_dehyd: float=1.; k3_meth: float=1e6
28
+ r3_reversible: bool=True; hole_density: float=125000.; d_hole: float=0.0006
29
+ Dbmax: float=0.020; Db0min: float=0.001; Db0max: float=0.010; alphaD: float=0.30
30
+ n_points: int=151; variable_viscosity: bool=True; add_equilibrium_diag: bool=True
31
+
32
+ def _omega(t): return 1.16145*t**-0.14874+0.52487*np.exp(-0.77320*t)+2.16178*np.exp(-2.43787*t)
33
+ def _pure_mu(T,M,s,e): return 2.6693e-6*math.sqrt(M*T)/(s*s*_omega(T/e))
34
+ def _gas_mu(p,y):
35
+ if not p.variable_viscosity: return 2e-5
36
+ mu=np.array([_pure_mu(p.T_reactor,MW[i]*1000,SIGMA[i],EPSK[i]) for i in range(NS)])
37
+ y=np.maximum(np.asarray(y),0); y=y/y.sum(); phi=np.ones((NS,NS))
38
+ for i in range(NS):
39
+ for j in range(NS):
40
+ phi[i,j]=(1+math.sqrt(mu[i]/mu[j])*(MW[j]/MW[i])**0.25)**2/math.sqrt(8*(1+MW[i]/MW[j]))
41
+ return float(sum(y[i]*mu[i]/sum(y[j]*phi[i,j] for j in range(NS)) for i in range(NS)))
42
+ def _gas_rho(p,y): return p.p*float(np.dot(y,MW))/(R*p.T_reactor)
43
+ def _umf(p,rho,mu):
44
+ Ar=max(p.dp**3*rho*(p.rho_s-rho)*G/mu**2,0); Re=math.sqrt(33.7**2+0.0408*Ar)-33.7
45
+ return Re*mu/(rho*p.dp),Ar,Re
46
+
47
+ def _fuller(T,p,Mi,Mj,Vi,Vj):
48
+ d=1e-3*T**1.75*math.sqrt(1/(Mi*1000)+1/(Mj*1000))/((p/101325)*(Vi**(1/3)+Vj**(1/3))**2)
49
+ return d*1e-4
50
+ def _dbin(p):
51
+ D=np.zeros((NS,NS))
52
+ for i in range(NS):
53
+ for j in range(NS): D[i,j]=np.nan if i==j else _fuller(p.T_reactor,p.p,MW[i],MW[j],FULLER_V[i],FULLER_V[j])
54
+ return D
55
+ def _dmix(i,y,D): return (1-y[i])/max(sum(y[j]/D[i,j] for j in range(NS) if j!=i),1e-30)
56
+
57
+ # Wartości Kp jak w aplikacji v7.1.
58
+ SH={'CO':[25.56759,6.096130,4.054656,-2.671301,.131021,-118.0089,227.3665,-110.5271],
59
+ 'CO2':[24.99735,55.18696,-33.69137,7.948387,-.136638,-403.6075,228.2431,-393.5224],
60
+ 'H2':[33.066178,-11.363417,11.432816,-2.772874,-.158558,-9.980797,172.707974,0],
61
+ 'H2O':[30.092,6.832514,6.793435,-2.53448,.082139,-250.881,223.3967,-241.8264],
62
+ 'CH4':[-.703029,108.4773,-42.52157,5.862788,.678565,-76.84376,158.7163,-74.8731]}
63
+ HF={'CO':-110.5271,'CO2':-393.5224,'H2':0.,'H2O':-241.8264,'CH4':-74.8731,'HCOOH':-378.6}
64
+ def _sh(T,c):
65
+ A,B,C,D,E,F,S,H=c; t=T/1000
66
+ return A*t+B*t*t/2+C*t**3/3+D*t**4/4-E/t+F-H, A*math.log(t)+B*t+C*t*t/2+D*t**3/3-E/(2*t*t)+S
67
+ def _hcooh(T):
68
+ k=1.380649e-23; h=6.62607015e-34; c=299792458.; Na=6.02214076e23; m=46.0254e-3/Na
69
+ rot=[2.58548,.40211,.34745]; vib=[3570,2943,1770,1387,1229,1105,625,1033,638]
70
+ St=R*(math.log(((2*math.pi*m*k*T)/(h*h))**1.5*(k*T/P0))+2.5)
71
+ th=[h*c*(b*100)/k for b in rot]; Sr=R*(math.log(math.sqrt(math.pi)*T**1.5/math.sqrt(np.prod(th)))+1.5)
72
+ def sv(x): return sum(R*((q:=h*c*(v*100)/(k*x))/(math.exp(q)-1)-math.log(1-math.exp(-q))) for v in vib)
73
+ def hv(x): return sum(R*(h*c*(v*100)/k)/(math.exp((h*c*(v*100)/k)/x)-1) for v in vib)/1000
74
+ return 4*R*(T-T0)/1000+hv(T)-hv(T0),St+Sr+sv(T)
75
+ def _kp(T):
76
+ gf={}
77
+ for s,c in SH.items(): H,S=_sh(T,c); gf[s]=HF[s]+H-T*S/1000
78
+ H,S=_hcooh(T); gf['HCOOH']=HF['HCOOH']+H-T*S/1000
79
+ rx=[{'CO':1,'H2O':1,'HCOOH':-1},{'CO2':1,'H2':1,'HCOOH':-1},{'CH4':1,'H2O':1,'CO':-1,'H2':-3}]
80
+ return [math.exp(-sum(n*gf[s] for s,n in r.items())*1000/(R*T)) for r in rx]
81
+ def _qp3(y,p):
82
+ co,h2,ch4,h2o=y[IDX['CO']],y[IDX['H2']],y[IDX['CH4']],y[IDX['H2O']]
83
+ if co<=1e-30 or h2<=1e-30: return 0. if ch4*h2o<=1e-30 else 1e300
84
+ return max(ch4,0)*max(h2o,0)/(co*h2**3)*(p/P0)**-2
85
+
86
+ def _reconstruct(x):
87
+ y=np.zeros(NS); y[:-1]=x; y[-1]=1-y[:-1].sum(); y=np.maximum(y,0)
88
+ return y/y.sum()
89
+ def _db0(p,a,Q): return min(max(1.38*((Q/a['Nh'] if a['Nh']>0 else Q)**2/G)**.2,p.Db0min),p.Db0max)
90
+ def _hydro(p,a,z,FTb,yb,ye):
91
+ FT=FTb+a['FTe']; yt=(FTb*yb+a['FTe']*ye)/FT; u0=FT/(a['A']*a['CT']); Ub=FTb/(a['A']*a['CT'])
92
+ dbmax=min(p.Dbmax,.6*p.D_col); d0=_db0(p,a,u0*a['A']); db=min(max(dbmax-(dbmax-d0)*math.exp(-(p.alphaD/p.D_col)*z),p.Db0min),dbmax)
93
+ ubr=.711*math.sqrt(G*db); up=max(Ub+ubr,1e-12); delta=min(max(Ub/up,1e-8),.95); ab=6*delta/db
94
+ rho=_gas_rho(p,yt); mu=_gas_mu(p,yt); umf,Ar,Re=_umf(p,rho,mu)
95
+ return dict(FT=FT,yt=yt,u0=u0,Ub=Ub,Ue=a['umf0'],Db=db,ubr=ubr,up=up,delta=delta,ab=ab,rho_g=rho,mu_g=mu,umf_diag=umf,Ar=Ar,Re=Re,Nf=u0/umf)
96
+ def _rates(p,a,z,FTb,yb,ye):
97
+ h=_hydro(p,a,z,FTb,yb,ye); eps=(1-h['delta'])*p.eps_mf; C=a['CT']*ye; yh2=max(ye[IDX['H2']],0)
98
+ r1=eps*p.k1_dehydr*C[IDX['HCOOH']]; r2=eps*p.k2_dehyd*C[IDX['HCOOH']]
99
+ rf=eps*p.k3_meth*C[IDX['CO']]*yh2**3; q=_qp3(ye,p.p); drive=1-q/a['Kp'][2] if p.r3_reversible else 1.; r3=rf*drive
100
+ rv=np.array([r1,r2,r3]); Rsp=NU@rv; Rtot=float(DNU@rv); MT=-Rtot
101
+ ym=(yb+ye)/2; ym/=ym.sum(); Dm=np.array([_dmix(i,ym,a['Dbin']) for i in range(NS)]); K=4*Dm/h['Db']+.5*a['umf0']
102
+ M=K*h['ab']*a['CT']*(yb-ye)+(ye if MT<0 else yb)*MT
103
+ h.update(r1=r1,r2=r2,r3=r3,r3_forward=rf,r3_reverse_equiv=rf*q/a['Kp'][2] if p.r3_reversible else 0.,r3_drive=drive,Qp3_e_local=q,Kp3_thermo=a['Kp'][2],Rsp=Rsp,Rtot=Rtot,MT=MT,M=M,K=K,Dm=Dm)
104
+ return h
105
+
106
+ def simulate_fluidized_bed(inlet_stream, return_profile=True, **kwargs):
107
+ """Symuluje reaktor. Niepodane argumenty przyjmują wartości z aplikacji v7.1."""
108
+ valid={f.name for f in fields(Params)}; unknown=set(kwargs)-valid
109
+ if unknown: raise TypeError(f"Nieznane parametry: {sorted(unknown)}")
110
+ p=Params(**kwargs); fd=reactor_feed_from_stream(inlet_stream,p); y0=fd['y0']; F0=fd['F0']
111
+ rho=_gas_rho(p,y0); mu=_gas_mu(p,y0); umf,Ar,Re=_umf(p,rho,mu); FTe=fd['A']*fd['CT']*umf
112
+ Nh=p.hole_density*fd['A']; Ah=math.pi*p.d_hole**2/4
113
+ a={**fd,'rho0':rho,'mu0':mu,'umf0':umf,'Ar0':Ar,'Re0':Re,'FTe':FTe,'Nh':Nh,'Ah':Ah,'Aop':Nh*Ah,'Dbin':_dbin(p),'Kp':_kp(p.T_reactor)}
114
+ if FTe>=.98*F0: raise ValueError(f"FTe={FTe:.3e} mol/s jest zbyt duże względem F0={F0:.3e} mol/s.")
115
+ FTb0=F0-FTe; Y0=np.r_[FTb0,y0[:-1],y0[:-1]]
116
+ def ode(z,Y):
117
+ FTb=max(float(Y[0]),1e-20); yb=_reconstruct(Y[1:NS]); ye=_reconstruct(Y[NS:2*NS-1]); h=_rates(p,a,z,FTb,yb,ye)
118
+ dF=fd['A']*h['Rtot']; return np.r_[dF,(-fd['A']*h['M'][:-1]-yb[:-1]*dF)/FTb,fd['A']*(h['M'][:-1]+h['Rsp'][:-1])/FTe]
119
+ sol=solve_ivp(ode,(0,p.H),Y0,t_eval=np.linspace(0,p.H,int(p.n_points)),method='BDF',rtol=1e-6,atol=1e-9)
120
+ if not sol.success: raise RuntimeError(sol.message)
121
+ rows=[]; FH0=F0*y0[IDX['HCOOH']]
122
+ for k,z in enumerate(sol.t):
123
+ FTb=max(float(sol.y[0,k]),1e-20); yb=_reconstruct(sol.y[1:NS,k]); ye=_reconstruct(sol.y[NS:2*NS-1,k]); h=_rates(p,a,float(z),FTb,yb,ye)
124
+ F=FTb*yb+FTe*ye; FT=float(F.sum()); row={'z_m':float(z),'FT_mol_s':FT,'FTb_mol_s':FTb,'FTe_mol_s':FTe,'X_HCOOH':(FH0-F[IDX['HCOOH']])/FH0 if FH0>0 else np.nan,'u0_m_s':h['u0'],'Db_m':h['Db'],'delta_b':h['delta'],'r1_dehydr_mol_m3_s':h['r1'],'r2_dehyd_mol_m3_s':h['r2'],'r3_net_mol_m3_s':h['r3']}
125
+ for i,s in enumerate(SPECIES): row[f'F_{s}_mol_s']=F[i]; row[f'y_{s}_total']=F[i]/FT; row[f'C_{s}_total_mol_m3']=fd['CT']*F[i]/FT
126
+ rows.append(row)
127
+ profile=pd.DataFrame(rows); out=profile.iloc[-1]
128
+ outlet=stream_from_feed(molar_flows={s:float(out[f'F_{s}_mol_s']) for s in SPECIES},temperature_K=p.T_reactor,pressure_Pa=p.p)
129
+ outlet['concentrations_mol_m3']={s:float(out[f'C_{s}_total_mol_m3']) for s in SPECIES}
130
+ summary={'X_HCOOH':float(out['X_HCOOH']),'solid_mass_kg':float(math.pi*p.D_col**2/4*trapezoid((1-profile['delta_b'])*(1-p.eps_mf)*p.rho_s, profile['z_m'])),'Kp':a['Kp']}
131
+ return {'outlet_stream':outlet,'conversion':{'HCOOH':float(out['X_HCOOH'])},'outlet':out.to_dict(),'profile':profile if return_profile else None,'summary':summary,'parameters':{f.name:getattr(p,f.name) for f in fields(p)}}
@@ -0,0 +1,106 @@
1
+ """Niezalezny bilans entalpii strumieni gazowych reaktora.
2
+
3
+ Konwencja znaku:
4
+ heat_duty_W > 0 - cieplo nalezy doprowadzic do reaktora,
5
+ heat_duty_W < 0 - cieplo nalezy odprowadzic z reaktora.
6
+ """
7
+
8
+ import math
9
+
10
+ R = 8.314462618
11
+ T0 = 298.15
12
+ P0 = 1.0e5
13
+ SPECIES = ("HCOOH", "CO", "H2O", "CO2", "H2", "CH4", "N2")
14
+
15
+ # Standardowe entalpie tworzenia gazow w 298.15 K [kJ/mol].
16
+ HF298_KJ_MOL = {
17
+ "HCOOH": -378.6,
18
+ "CO": -110.5271,
19
+ "H2O": -241.8264,
20
+ "CO2": -393.5224,
21
+ "H2": 0.0,
22
+ "CH4": -74.87310,
23
+ "N2": 0.0,
24
+ }
25
+
26
+ # Wspolczynniki Shomate: A, B, C, D, E, F, G, H.
27
+ SHOMATE = {
28
+ "CO": [25.56759, 6.096130, 4.054656, -2.671301, 0.131021, -118.0089, 227.3665, -110.5271],
29
+ "CO2": [24.99735, 55.18696, -33.69137, 7.948387, -0.136638, -403.6075, 228.2431, -393.5224],
30
+ "H2": [33.066178, -11.363417, 11.432816, -2.772874, -0.158558, -9.980797, 172.707974, 0.0],
31
+ "H2O": [30.09200, 6.832514, 6.793435, -2.534480, 0.082139, -250.8810, 223.3967, -241.8264],
32
+ "CH4": [-0.703029, 108.4773, -42.52157, 5.862788, 0.678565, -76.84376, 158.7163, -74.87310],
33
+ "N2": [28.98641, 1.853978, -9.647459, 16.63537, 0.000117, -8.671914, 226.4168, 0.0],
34
+ }
35
+
36
+
37
+ def _shomate_h_increment_kj_mol(T_K, coeff):
38
+ A, B, C, D, E, F, _G, H = coeff
39
+ t = T_K / 1000.0
40
+ return A*t + B*t*t/2.0 + C*t**3/3.0 + D*t**4/4.0 - E/t + F - H
41
+
42
+
43
+ def _hcooh_h_increment_kj_mol(T_K):
44
+ """Przyrost entalpii HCOOH(g) zgodny z modelem termodynamicznym v7.1."""
45
+ kB = 1.380649e-23
46
+ h = 6.62607015e-34
47
+ c = 299792458.0
48
+ vib_cm = (3570, 2943, 1770, 1387, 1229, 1105, 625, 1033, 638)
49
+
50
+ def hv(T):
51
+ return sum(
52
+ R * (h*c*(nu*100.0)/kB) /
53
+ (math.exp((h*c*(nu*100.0)/kB)/T) - 1.0)
54
+ for nu in vib_cm
55
+ ) / 1000.0
56
+
57
+ return 4.0*R*(T_K - T0)/1000.0 + hv(T_K) - hv(T0)
58
+
59
+
60
+ def species_enthalpy_J_mol(species, temperature_K):
61
+ """Entalpia molowa gazu obejmujaca tworzenie i czesc jawna [J/mol]."""
62
+ if species not in SPECIES:
63
+ raise ValueError(f"Nieznany skladnik: {species}")
64
+ T = float(temperature_K)
65
+ if not math.isfinite(T) or T <= 0.0:
66
+ raise ValueError("temperature_K musi byc dodatnia i skonczona.")
67
+ dh = (_hcooh_h_increment_kj_mol(T) if species == "HCOOH"
68
+ else _shomate_h_increment_kj_mol(T, SHOMATE[species]))
69
+ return 1000.0 * (HF298_KJ_MOL[species] + dh)
70
+
71
+
72
+ def stream_enthalpy_flow_W(stream):
73
+ """Zwraca strumien entalpii chemicznej i jawnej [W = J/s]."""
74
+ if not isinstance(stream, dict) or "molar_flows" not in stream:
75
+ raise TypeError("stream musi zawierac slownik molar_flows.")
76
+ T = float(stream["temperature_K"])
77
+ return sum(
78
+ float(stream["molar_flows"].get(sp, 0.0)) * species_enthalpy_J_mol(sp, T)
79
+ for sp in SPECIES
80
+ )
81
+
82
+
83
+ def calculate_reactor_heat_effect(inlet_stream, outlet_stream, heat_loss_W=0.0, **kwargs):
84
+ """Oblicza efekt cieplny reaktora z bilansu entalpii wlot-wylot.
85
+
86
+ Wynik dodatni oznacza wymagane doprowadzenie ciepla. Parametr
87
+ ``heat_loss_W`` jest dodatni dla strat ciepla z reaktora do otoczenia.
88
+ """
89
+ H_in = stream_enthalpy_flow_W(inlet_stream)
90
+ H_out = stream_enthalpy_flow_W(outlet_stream)
91
+ loss = float(heat_loss_W)
92
+ duty = H_out - H_in + loss
93
+ F_hcooh_in = float(inlet_stream["molar_flows"].get("HCOOH", 0.0))
94
+ conversion = None
95
+ if F_hcooh_in > 0.0:
96
+ conversion = 1.0 - float(outlet_stream["molar_flows"].get("HCOOH", 0.0)) / F_hcooh_in
97
+ return {
98
+ "enthalpy_in_W": H_in,
99
+ "enthalpy_out_W": H_out,
100
+ "heat_loss_W": loss,
101
+ "heat_duty_W": duty,
102
+ "heat_duty_kW": duty / 1000.0,
103
+ "thermal_character": "endothermic" if duty > 0 else "exothermic" if duty < 0 else "neutral",
104
+ "HCOOH_conversion": conversion,
105
+ "specific_heat_duty_kJ_per_mol_HCOOH_in": duty / F_hcooh_in / 1000.0 if F_hcooh_in > 0 else None,
106
+ }
File without changes
@@ -0,0 +1,46 @@
1
+ """Model nasycania azotu parami kwasu mrówkowego w płuczce."""
2
+
3
+ from .stream_from_feed import stream_from_feed
4
+
5
+ R = 8.314462618
6
+
7
+
8
+ def antoine_formic_acid_bar(T_K):
9
+ A, B, C = 2.00121, 515.0, -139.408
10
+ return 10 ** (A - B / (T_K + C))
11
+
12
+
13
+ def simulate_scrubber(Q_N2_L_min=0.5, T_flow_ref=298.15,
14
+ p_flow_ref=101325.0, T_bubbler_C=15.0,
15
+ acid_activity=1.0, saturation_factor=0.650,
16
+ pressure_Pa=101325.0, temperature_K=573.15,
17
+ HCOOH_in_override=None, **kwargs):
18
+ """Symuluje płuczkę; niepodane parametry mają wartości z aplikacji v7.1."""
19
+ Q_N2 = float(Q_N2_L_min) * 1e-3 / 60.0
20
+ F_N2 = float(p_flow_ref) * Q_N2 / (R * float(T_flow_ref))
21
+ psat = antoine_formic_acid_bar(float(T_bubbler_C) + 273.15) * 1e5
22
+
23
+ if HCOOH_in_override is None:
24
+ p_hcooh = min(float(saturation_factor) * float(acid_activity) * psat,
25
+ 0.95 * float(pressure_Pa))
26
+ F_HCOOH = F_N2 * p_hcooh / max(float(pressure_Pa) - p_hcooh, 1e-30)
27
+ else:
28
+ y = float(HCOOH_in_override)
29
+ if not 0.0 <= y < 1.0:
30
+ raise ValueError("HCOOH_in_override musi należeć do [0, 1).")
31
+ p_hcooh = y * float(pressure_Pa)
32
+ F_HCOOH = F_N2 * y / max(1.0 - y, 1e-30)
33
+
34
+ stream = stream_from_feed(
35
+ molar_flows={"HCOOH": F_HCOOH, "N2": F_N2},
36
+ temperature_K=temperature_K,
37
+ pressure_Pa=pressure_Pa,
38
+ )
39
+ stream["scrubber"] = {
40
+ "T_bubbler_C": float(T_bubbler_C),
41
+ "psat_HCOOH_Pa": psat,
42
+ "pHCOOH_feed_Pa": p_hcooh,
43
+ "acid_activity": float(acid_activity),
44
+ "saturation_factor": float(saturation_factor),
45
+ }
46
+ return stream
@@ -0,0 +1,89 @@
1
+ """Tworzenie uniwersalnego strumienia gazowego z podanego zasilania."""
2
+
3
+ import math
4
+
5
+ import numpy as np
6
+
7
+ R = 8.314462618
8
+ SPECIES = ("HCOOH", "CO", "H2O", "CO2", "H2", "CH4", "N2")
9
+
10
+
11
+ def stream_from_feed(*, molar_flows=None, molar_fractions=None,
12
+ total_flow_mol_s=None, temperature_K=573.15,
13
+ pressure_Pa=101325.0, **kwargs):
14
+ """Zwraca ujednolicony strumien gazowy.
15
+
16
+ Podaj albo ``molar_flows``, albo parę ``molar_fractions`` i
17
+ ``total_flow_mol_s``. Brakujące składniki są przyjmowane jako zero.
18
+ Dodatkowe argumenty nazwane są ignorowane, co ułatwia wspólne API.
19
+ """
20
+ temperature_K = float(temperature_K)
21
+ pressure_Pa = float(pressure_Pa)
22
+ if not math.isfinite(temperature_K) or temperature_K <= 0:
23
+ raise ValueError("temperature_K musi być dodatnie.")
24
+ if not math.isfinite(pressure_Pa) or pressure_Pa <= 0:
25
+ raise ValueError("pressure_Pa musi być dodatnie.")
26
+
27
+ if molar_flows is not None:
28
+ if not isinstance(molar_flows, dict):
29
+ raise TypeError("molar_flows musi być słownikiem.")
30
+ unknown = set(molar_flows) - set(SPECIES)
31
+ if unknown:
32
+ raise ValueError(f"Nieznane składniki: {sorted(unknown)}")
33
+ flows = {s: float(molar_flows.get(s, 0.0)) for s in SPECIES}
34
+ elif molar_fractions is not None and total_flow_mol_s is not None:
35
+ if not isinstance(molar_fractions, dict):
36
+ raise TypeError("molar_fractions musi być słownikiem.")
37
+ unknown = set(molar_fractions) - set(SPECIES)
38
+ if unknown:
39
+ raise ValueError(f"Nieznane składniki: {sorted(unknown)}")
40
+ fractions = {s: float(molar_fractions.get(s, 0.0)) for s in SPECIES}
41
+ sy = sum(fractions.values())
42
+ if not math.isfinite(sy) or sy <= 0:
43
+ raise ValueError("Suma ułamków molowych musi być dodatnia.")
44
+ fractions = {s: y / sy for s, y in fractions.items()}
45
+ total = float(total_flow_mol_s)
46
+ flows = {s: total * fractions[s] for s in SPECIES}
47
+ else:
48
+ raise ValueError("Podaj molar_flows albo molar_fractions i total_flow_mol_s.")
49
+
50
+ if any((not math.isfinite(v) or v < 0) for v in flows.values()):
51
+ raise ValueError("Strumienie molowe muszą być nieujemne i skończone.")
52
+ total = sum(flows.values())
53
+ if total <= 0:
54
+ raise ValueError("Całkowity strumień molowy musi być dodatni.")
55
+ fractions = {s: flows[s] / total for s in SPECIES}
56
+ return {
57
+ "type": "gas_stream",
58
+ "temperature_K": temperature_K,
59
+ "pressure_Pa": pressure_Pa,
60
+ "total_flow_mol_s": total,
61
+ "molar_flows": flows,
62
+ "molar_fractions": fractions,
63
+ }
64
+
65
+
66
+ def reactor_feed_from_stream(inlet_stream, params):
67
+ """Konwertuje uniwersalny strumień na dane wewnętrzne reaktora."""
68
+ if "molar_flows" in inlet_stream:
69
+ stream = stream_from_feed(
70
+ molar_flows=inlet_stream["molar_flows"],
71
+ temperature_K=inlet_stream.get("temperature_K", params.T_reactor),
72
+ pressure_Pa=inlet_stream.get("pressure_Pa", params.p),
73
+ )
74
+ else:
75
+ stream = stream_from_feed(
76
+ molar_fractions=inlet_stream["molar_fractions"],
77
+ total_flow_mol_s=inlet_stream["total_flow_mol_s"],
78
+ temperature_K=inlet_stream.get("temperature_K", params.T_reactor),
79
+ pressure_Pa=inlet_stream.get("pressure_Pa", params.p),
80
+ )
81
+ flows = np.array([stream["molar_flows"][s] for s in SPECIES], dtype=float)
82
+ F0 = float(flows.sum())
83
+ return {
84
+ "A": math.pi * params.D_col**2 / 4.0,
85
+ "CT": params.p / (R * params.T_reactor),
86
+ "F0": F0,
87
+ "y0": flows / F0,
88
+ "species_flows": flows,
89
+ }
File without changes
@@ -0,0 +1,4 @@
1
+ def test_public_imports():
2
+ import witzuk
3
+ import witzuk.hcooh_fbr
4
+ assert witzuk.__version__
@@ -0,0 +1,27 @@
1
+ import math
2
+
3
+ from witzuk.hcooh_fbr import (
4
+ calculate_reactor_heat_effect,
5
+ check_element_balance,
6
+ simulate_fluidized_bed,
7
+ simulate_scrubber,
8
+ stream_from_feed,
9
+ )
10
+
11
+
12
+ def test_stream_from_feed_normalizes():
13
+ stream = stream_from_feed(
14
+ molar_fractions={"HCOOH": 2.0, "N2": 98.0},
15
+ total_flow_mol_s=5e-4,
16
+ )
17
+ assert math.isclose(sum(stream["molar_fractions"].values()), 1.0)
18
+
19
+
20
+ def test_default_scrubber_and_reactor():
21
+ feed = simulate_scrubber()
22
+ result = simulate_fluidized_bed(feed, return_profile=False)
23
+ outlet = result["outlet_stream"]
24
+ assert 0.0 <= result["conversion"]["HCOOH"] <= 1.0
25
+ assert math.isclose(sum(outlet["molar_fractions"].values()), 1.0, rel_tol=1e-8)
26
+ assert check_element_balance(feed, outlet)["all_balanced"]
27
+ assert math.isfinite(calculate_reactor_heat_effect(feed, outlet)["heat_duty_W"])