processpi 0.2.0.1__tar.gz → 0.2.0.3__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.
- {processpi-0.2.0.1 → processpi-0.2.0.3}/PKG-INFO +1 -1
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/__init__.py +4 -1
- processpi-0.2.0.3/processpi/equipment/__init__.py +21 -0
- processpi-0.2.0.3/processpi/equipment/absorber.py +67 -0
- processpi-0.2.0.3/processpi/equipment/base.py +57 -0
- processpi-0.2.0.3/processpi/equipment/batch.py +39 -0
- processpi-0.2.0.3/processpi/equipment/compressor.py +80 -0
- processpi-0.2.0.3/processpi/equipment/cstr.py +41 -0
- processpi-0.2.0.3/processpi/equipment/decanter.py +61 -0
- processpi-0.2.0.3/processpi/equipment/distillation.py +82 -0
- processpi-0.2.0.3/processpi/equipment/flash.py +84 -0
- processpi-0.2.0.3/processpi/equipment/heater_cooler.py +103 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/__init__.py +7 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/base.py +129 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/examples.py +116 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/mechanical/__init__.py +53 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/mechanical/basic_params.py +83 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/mechanical/bell_method.py +101 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/mechanical/classification.py +66 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/mechanical/condenser.py +262 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/mechanical/double_pipe.py +633 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/mechanical/evaporator.py +263 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/mechanical/kern_method.py +112 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/mechanical/reboiler.py +288 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/mechanical/shell_tube.py +686 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/mechanical.py +20 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/simulation.py +186 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/standards.py +118 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers/utils.py +1 -0
- processpi-0.2.0.3/processpi/equipment/heatexchangers_.py +155 -0
- processpi-0.2.0.3/processpi/equipment/mixer.py +54 -0
- processpi-0.2.0.3/processpi/equipment/pfr.py +44 -0
- processpi-0.2.0.3/processpi/equipment/pumps.py +92 -0
- processpi-0.2.0.3/processpi/equipment/splitter.py +41 -0
- processpi-0.2.0.3/processpi/equipment/stripper.py +67 -0
- processpi-0.2.0.3/processpi/equipment/turbine.py +81 -0
- processpi-0.2.0.3/processpi/equipment/valves.py +112 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/streams/__init__.py +1 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi.egg-info/PKG-INFO +1 -1
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi.egg-info/SOURCES.txt +35 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/pyproject.toml +3 -1
- {processpi-0.2.0.1 → processpi-0.2.0.3}/LICENSE +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/README.md +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/__init__.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/base.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/engine.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/fluids/__init__.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/fluids/flow_type.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/fluids/friction_factor_colebrookwhite.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/fluids/optimium_pipe_dia.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/fluids/pressure_drop_darcy.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/fluids/pressure_drop_fanning.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/fluids/pressure_drop_hazen_williams.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/fluids/pump_power.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/fluids/reynolds_number.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/fluids/velocity.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/__init__.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/biot.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/boiling_chen.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/combined_modes.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/condensation.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/condensation_dropwise.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/condensation_nusselt.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/conduction_heat_loss.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/convection_heat_loss.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/crossflow_tube.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/fourier.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/fourierlaw.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/grashof.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/heat_exchanger_area.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/lmtd.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/newton_cooling.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/ntu.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/nusselt.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/overall_u.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/peclet.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/prandtl.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/radial_cylinder.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/radiation_blackbody.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/radiation_exchange.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/radiation_greybody.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/radiation_viewfactor.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/resistance.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/reyleigh.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/rohsenow_boiling.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/heat_transfer/stefan_boltzmann.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/mass_transfer/__init__.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/mass_transfer/distillation_stage_count.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/mass_transfer/drying_rate.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/reaction_engineering/__init__.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/reaction_engineering/catalyst_activity.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/reaction_engineering/reaction_rate.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/reaction_engineering/residence_time.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/thermodynamics/__init__.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/thermodynamics/enthalpy_change.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/thermodynamics/entropy_change.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/calculations/thermodynamics/heat_of_vaporization.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/__init__.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/aceticacid.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/acetone.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/acrylicacid.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/air.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/ammonia.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/base.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/benzene.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/benzoicacid.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/bromine.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/butane.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/carbondioxide.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/carbonmonoxide.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/carbontetrachloride.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/chlorine.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/chlorobenzene.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/chloroform.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/chloromethane.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/constants.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/cyanogen.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/cyclohexene.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/ethane.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/ethanol.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/ethylacetate.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/ethylene.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/fluorine.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/fluorobenzene.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/formicacid.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/gas.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/inorganic_liquid.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/methanol.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/oil.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/organic_liquid.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/steam.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/toluene.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/vapor.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/components/water.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/constants.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/__init__.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/base.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/design_rules.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/engine.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/equipment.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/fittings.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/insulation.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/materials.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/network.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/nozzle.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/pipelineresults.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/pipes.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/piping_costs.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/pumps.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/selection.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/standards.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/pipelines/vessel.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/streams/energy.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/streams/material.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/__init__.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/area.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/base.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/density.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/diameter.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/dimensionless.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/heat_flow.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/heat_flux.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/heat_of_vaporization.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/heat_transfer_coefficient.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/length.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/mass.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/mass_flowrate.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/molar_flowrate.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/power.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/pressure.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/specific_heat.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/strings.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/temperature.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/thermal_conductivity.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/thermal_resistance.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/velocity.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/viscosity.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/volume.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi/units/volumetric_flowrate.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi.egg-info/dependency_links.txt +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi.egg-info/requires.txt +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/processpi.egg-info/top_level.txt +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/setup.cfg +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/setup.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/tests/test_engine_network.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/tests/test_network.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/tests/test_pipeline_network.py +0 -0
- {processpi-0.2.0.1 → processpi-0.2.0.3}/tests/test_variables.py +0 -0
|
@@ -29,8 +29,11 @@ from . import calculations
|
|
|
29
29
|
from . import pipelines
|
|
30
30
|
from . import units
|
|
31
31
|
from . import components
|
|
32
|
+
from . import equipment
|
|
33
|
+
from . import integration
|
|
34
|
+
from . import streams
|
|
32
35
|
|
|
33
|
-
__all__ = ["calculations", "pipelines", "units", "components"]
|
|
36
|
+
__all__ = ["calculations", "pipelines", "units", "components","equipment","integration","streams"]
|
|
34
37
|
|
|
35
38
|
# -------------------------------------------------------------------
|
|
36
39
|
# Loading animation (only for interactive sessions)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Equipment models and utilities for ProcessPI."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"absorber",
|
|
5
|
+
"base",
|
|
6
|
+
"batch",
|
|
7
|
+
"compressor",
|
|
8
|
+
"cstr",
|
|
9
|
+
"decanter",
|
|
10
|
+
"distillation",
|
|
11
|
+
"flash",
|
|
12
|
+
"heater_cooler",
|
|
13
|
+
"heatexchangers",
|
|
14
|
+
"mixer",
|
|
15
|
+
"pfr",
|
|
16
|
+
"pumps",
|
|
17
|
+
"splitter",
|
|
18
|
+
"stripper",
|
|
19
|
+
"turbine",
|
|
20
|
+
"valves",
|
|
21
|
+
]
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# processpi/equipment/absorber.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Optional, Dict
|
|
5
|
+
from .base import Equipment
|
|
6
|
+
from ..streams import MaterialStream
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Absorber(Equipment):
|
|
10
|
+
"""
|
|
11
|
+
Absorber Column.
|
|
12
|
+
|
|
13
|
+
Removes solute from gas stream into a liquid solvent.
|
|
14
|
+
|
|
15
|
+
Assumptions (initial version):
|
|
16
|
+
- Steady state
|
|
17
|
+
- Counter-current contact
|
|
18
|
+
- Uses overall K-removal efficiency (fraction of solute absorbed)
|
|
19
|
+
- Single major solute for simplicity (extendable to multicomponent)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, name: str, stages: int, removal_efficiency: float):
|
|
23
|
+
super().__init__(name)
|
|
24
|
+
self.stages = stages
|
|
25
|
+
self.removal_efficiency = removal_efficiency # 0–1 fraction
|
|
26
|
+
|
|
27
|
+
# Streams
|
|
28
|
+
self.gas_in: Optional[MaterialStream] = None
|
|
29
|
+
self.gas_out: Optional[MaterialStream] = None
|
|
30
|
+
self.liquid_in: Optional[MaterialStream] = None
|
|
31
|
+
self.liquid_out: Optional[MaterialStream] = None
|
|
32
|
+
|
|
33
|
+
def simulate(self) -> Dict:
|
|
34
|
+
if (self.gas_in is None or self.gas_out is None or
|
|
35
|
+
self.liquid_in is None or self.liquid_out is None):
|
|
36
|
+
raise ValueError(f"Absorber {self.name} must have gas_in, gas_out, liquid_in, liquid_out streams.")
|
|
37
|
+
|
|
38
|
+
F_gas = self.gas_in.flow_rate
|
|
39
|
+
F_liq = self.liquid_in.flow_rate
|
|
40
|
+
|
|
41
|
+
z_gas = self.gas_in.components.copy()
|
|
42
|
+
z_liq = self.liquid_in.components.copy()
|
|
43
|
+
|
|
44
|
+
# Pick solute (for demo: assume first component in gas is solute)
|
|
45
|
+
solute = list(z_gas.keys())[0]
|
|
46
|
+
|
|
47
|
+
# Apply removal efficiency
|
|
48
|
+
solute_in_gas = z_gas[solute] * F_gas
|
|
49
|
+
absorbed = solute_in_gas * self.removal_efficiency
|
|
50
|
+
solute_out_gas = solute_in_gas - absorbed
|
|
51
|
+
|
|
52
|
+
# Update outlet streams
|
|
53
|
+
self.gas_out.copy(self.gas_in)
|
|
54
|
+
self.liquid_out.copy(self.liquid_in)
|
|
55
|
+
|
|
56
|
+
self.gas_out.flow_rate = F_gas
|
|
57
|
+
self.liquid_out.flow_rate = F_liq + absorbed # solute added to solvent
|
|
58
|
+
|
|
59
|
+
# Adjust compositions
|
|
60
|
+
self.gas_out.components[solute] = solute_out_gas / F_gas
|
|
61
|
+
self.liquid_out.components[solute] = absorbed / self.liquid_out.flow_rate
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
"absorbed": absorbed,
|
|
65
|
+
"gas_out_comp": self.gas_out.components,
|
|
66
|
+
"liq_out_comp": self.liquid_out.components,
|
|
67
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# processpi/equipment/base.py
|
|
2
|
+
from typing import List, Optional, Literal
|
|
3
|
+
from ..streams import MaterialStream
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Equipment:
|
|
7
|
+
"""
|
|
8
|
+
Base class for all equipment units.
|
|
9
|
+
Manages inlet/outlet MaterialStreams and ensures consistent graph connections.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, name: str, inlet_ports: int = 0, outlet_ports: int = 0):
|
|
13
|
+
self.name = name
|
|
14
|
+
self.inlets: List[Optional[MaterialStream]] = [None] * inlet_ports
|
|
15
|
+
self.outlets: List[Optional[MaterialStream]] = [None] * outlet_ports
|
|
16
|
+
|
|
17
|
+
def attach_stream(self, stream: MaterialStream, port: Literal["inlet", "outlet"], index: int = 0):
|
|
18
|
+
"""
|
|
19
|
+
Attach a MaterialStream to an inlet or outlet port.
|
|
20
|
+
Enforces port registration and ensures stream ↔ equipment consistency.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
stream : MaterialStream
|
|
25
|
+
The stream to connect
|
|
26
|
+
port : "inlet" or "outlet"
|
|
27
|
+
Whether this is an inlet or outlet connection
|
|
28
|
+
index : int
|
|
29
|
+
Port index (for multi-port equipment like mixers/splitters)
|
|
30
|
+
"""
|
|
31
|
+
if not isinstance(stream, MaterialStream):
|
|
32
|
+
raise TypeError(f"Expected MaterialStream, got {type(stream).__name__}")
|
|
33
|
+
|
|
34
|
+
if port == "inlet":
|
|
35
|
+
if index >= len(self.inlets) or index < 0:
|
|
36
|
+
raise IndexError(f"Inlet port {index} out of range (max={len(self.inlets)-1})")
|
|
37
|
+
if self.inlets[index] is not None:
|
|
38
|
+
raise ValueError(f"Inlet port {index} of {self.name} already occupied")
|
|
39
|
+
self.inlets[index] = stream
|
|
40
|
+
#stream.connect_outlet(self) # stream → equipment consistency
|
|
41
|
+
|
|
42
|
+
elif port == "outlet":
|
|
43
|
+
if index >= len(self.outlets) or index < 0:
|
|
44
|
+
raise IndexError(f"Outlet port {index} out of range (max={len(self.outlets)-1})")
|
|
45
|
+
if self.outlets[index] is not None:
|
|
46
|
+
raise ValueError(f"Outlet port {index} of {self.name} already occupied")
|
|
47
|
+
self.outlets[index] = stream
|
|
48
|
+
#stream.connect_inlet(self) # stream ← equipment consistency
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
raise ValueError("port must be 'inlet' or 'outlet'")
|
|
52
|
+
|
|
53
|
+
def __repr__(self) -> str:
|
|
54
|
+
return (
|
|
55
|
+
f"<Equipment {self.name}, "
|
|
56
|
+
f"inlets={len(self.inlets)}, outlets={len(self.outlets)}>"
|
|
57
|
+
)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
from .base import Equipment
|
|
4
|
+
from ..streams import MaterialStream
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BatchReactor(Equipment):
|
|
8
|
+
"""
|
|
9
|
+
Batch Reactor.
|
|
10
|
+
|
|
11
|
+
Assumptions:
|
|
12
|
+
- Closed system (no inlet/outlet during reaction).
|
|
13
|
+
- Time-dependent simulation.
|
|
14
|
+
- Reaction integrated over given time.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, name: str, volume: float, rate_fn: Optional[callable] = None):
|
|
18
|
+
super().__init__(name)
|
|
19
|
+
self.volume = volume
|
|
20
|
+
self.rate_fn = rate_fn
|
|
21
|
+
|
|
22
|
+
def simulate(self, time: float = 1.0, steps: int = 10) -> Dict[str, Any]:
|
|
23
|
+
stream: MaterialStream = self.inlet # batch uses "inlet" as initial contents
|
|
24
|
+
|
|
25
|
+
if stream is None:
|
|
26
|
+
raise ValueError(f"BatchReactor {self.name} requires a feed MaterialStream as initial contents.")
|
|
27
|
+
|
|
28
|
+
if self.rate_fn:
|
|
29
|
+
dt = time / steps
|
|
30
|
+
for _ in range(steps):
|
|
31
|
+
rates = self.rate_fn(stream, stream.temperature, stream.pressure)
|
|
32
|
+
stream.apply_reaction(rates, residence_time=dt)
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
"type": "BatchReactor",
|
|
36
|
+
"final_T": stream.temperature,
|
|
37
|
+
"final_F": stream.mass_flow,
|
|
38
|
+
"time": time
|
|
39
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# processpi/equipment/compressor.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from .base import Equipment
|
|
6
|
+
from ..streams import MaterialStream
|
|
7
|
+
from ..units import Power
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Compressor(Equipment):
|
|
11
|
+
"""
|
|
12
|
+
Gas-phase compressor.
|
|
13
|
+
|
|
14
|
+
Assumptions:
|
|
15
|
+
- Single stage.
|
|
16
|
+
- Isentropic relation with efficiency correction.
|
|
17
|
+
- No heat exchange (adiabatic).
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, name: str,
|
|
21
|
+
isentropic_efficiency: float = 0.75,
|
|
22
|
+
outlet_pressure: Optional[float] = None,
|
|
23
|
+
pressure_ratio: Optional[float] = None):
|
|
24
|
+
super().__init__(name, inlet_ports=1, outlet_ports=1)
|
|
25
|
+
self.eta = isentropic_efficiency
|
|
26
|
+
self.outlet_pressure = outlet_pressure
|
|
27
|
+
self.pressure_ratio = pressure_ratio
|
|
28
|
+
|
|
29
|
+
def simulate(self) -> Dict[str, Any]:
|
|
30
|
+
inlet: MaterialStream = self.inlets[0]
|
|
31
|
+
outlet: MaterialStream = self.outlets[0]
|
|
32
|
+
|
|
33
|
+
if inlet is None or outlet is None:
|
|
34
|
+
raise ValueError(f"{self.name}: Must have inlet and outlet streams connected.")
|
|
35
|
+
|
|
36
|
+
if inlet.phase != "vapor":
|
|
37
|
+
raise ValueError(f"{self.name}: Compressor requires vapor phase at inlet.")
|
|
38
|
+
|
|
39
|
+
P1 = inlet.pressure.to("Pa").value
|
|
40
|
+
T1 = inlet.temperature.to("K").value
|
|
41
|
+
cp = inlet.get_property("cp") # J/kg-K
|
|
42
|
+
gamma = inlet.get_property("gamma") # cp/cv
|
|
43
|
+
m_dot = inlet.mass_flow().to("kg/s").value
|
|
44
|
+
|
|
45
|
+
# Determine outlet pressure
|
|
46
|
+
if self.outlet_pressure is not None:
|
|
47
|
+
P2 = self.outlet_pressure.to("Pa").value
|
|
48
|
+
elif self.pressure_ratio is not None:
|
|
49
|
+
P2 = P1 * self.pressure_ratio
|
|
50
|
+
else:
|
|
51
|
+
raise ValueError("Must specify outlet_pressure or pressure_ratio.")
|
|
52
|
+
|
|
53
|
+
if P2 <= P1:
|
|
54
|
+
raise ValueError("Compressor must increase pressure (P2 > P1).")
|
|
55
|
+
|
|
56
|
+
# Ideal (isentropic) outlet temperature
|
|
57
|
+
T2s = T1 * (P2 / P1) ** ((gamma - 1) / gamma)
|
|
58
|
+
|
|
59
|
+
# Actual outlet temperature with efficiency
|
|
60
|
+
T2 = T1 + (T2s - T1) / self.eta
|
|
61
|
+
|
|
62
|
+
# Work requirement (W = m_dot * cp * ΔT)
|
|
63
|
+
w_dot = m_dot * cp * (T2 - T1)
|
|
64
|
+
power = Power(w_dot, "W")
|
|
65
|
+
|
|
66
|
+
# Update outlet stream
|
|
67
|
+
outlet.copy_from(inlet)
|
|
68
|
+
outlet.pressure = P2
|
|
69
|
+
outlet.temperature.value = T2
|
|
70
|
+
# Phase remains vapor
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
"inlet_P": P1,
|
|
74
|
+
"outlet_P": P2,
|
|
75
|
+
"Tin": T1,
|
|
76
|
+
"Tout": T2,
|
|
77
|
+
"m_dot": m_dot,
|
|
78
|
+
"power": power,
|
|
79
|
+
"efficiency": self.eta
|
|
80
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
from .base import Equipment
|
|
4
|
+
from ..streams import MaterialStream
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CSTR(Equipment):
|
|
8
|
+
"""
|
|
9
|
+
Continuous Stirred Tank Reactor (CSTR).
|
|
10
|
+
|
|
11
|
+
Assumptions:
|
|
12
|
+
- Perfect mixing (outlet composition = reactor composition).
|
|
13
|
+
- Steady-state operation.
|
|
14
|
+
- Reaction kinetics placeholder (rate function can be injected).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, name: str, volume: float, rate_fn: Optional[callable] = None):
|
|
18
|
+
super().__init__(name)
|
|
19
|
+
self.volume = volume
|
|
20
|
+
self.rate_fn = rate_fn
|
|
21
|
+
|
|
22
|
+
def simulate(self) -> Dict[str, Any]:
|
|
23
|
+
inlet: MaterialStream = self.inlet
|
|
24
|
+
outlet: MaterialStream = self.outlet
|
|
25
|
+
|
|
26
|
+
if inlet is None or outlet is None:
|
|
27
|
+
raise ValueError(f"CSTR {self.name} must have inlet and outlet streams.")
|
|
28
|
+
|
|
29
|
+
outlet.copy_from(inlet)
|
|
30
|
+
|
|
31
|
+
if self.rate_fn:
|
|
32
|
+
rates = self.rate_fn(outlet, outlet.temperature, outlet.pressure)
|
|
33
|
+
outlet.apply_reaction(rates, residence_time=self.volume / inlet.volumetric_flow)
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
"type": "CSTR",
|
|
37
|
+
"inlet_T": inlet.temperature,
|
|
38
|
+
"outlet_T": outlet.temperature,
|
|
39
|
+
"inlet_F": inlet.mass_flow,
|
|
40
|
+
"outlet_F": outlet.mass_flow
|
|
41
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# processpi/equipment/decanter.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Optional, Dict
|
|
5
|
+
from .base import Equipment
|
|
6
|
+
from ..streams import MaterialStream
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Decanter(Equipment):
|
|
10
|
+
"""
|
|
11
|
+
Decanter (liquid–liquid phase separator).
|
|
12
|
+
|
|
13
|
+
Assumptions:
|
|
14
|
+
- Two immiscible liquid phases (e.g., water-rich and organic-rich).
|
|
15
|
+
- Separation based on density: heavy phase → bottom, light phase → top.
|
|
16
|
+
- User specifies split fractions OR component affinity (partitioning).
|
|
17
|
+
- For now: simple split fraction for light/heavy phase.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, name: str, light_fraction: float = 0.5):
|
|
21
|
+
super().__init__(name, inlet_ports=1, outlet_ports=2)
|
|
22
|
+
self.light_fraction = light_fraction # fraction of flow to light phase (0–1)
|
|
23
|
+
|
|
24
|
+
def simulate(self) -> Dict:
|
|
25
|
+
inlet: MaterialStream = self.inlets[0]
|
|
26
|
+
if inlet is None:
|
|
27
|
+
raise ValueError(f"Decanter {self.name} must have an inlet stream.")
|
|
28
|
+
|
|
29
|
+
outlet_light: MaterialStream = self.outlets[0]
|
|
30
|
+
outlet_heavy: MaterialStream = self.outlets[1]
|
|
31
|
+
|
|
32
|
+
if outlet_light is None or outlet_heavy is None:
|
|
33
|
+
raise ValueError(f"Decanter {self.name} must have two outlet streams attached.")
|
|
34
|
+
|
|
35
|
+
F_total = inlet.flow
|
|
36
|
+
z = inlet.composition.copy()
|
|
37
|
+
|
|
38
|
+
# Flow split
|
|
39
|
+
F_light = F_total * self.light_fraction
|
|
40
|
+
F_heavy = F_total - F_light
|
|
41
|
+
|
|
42
|
+
# For now: assume components split proportional to flow
|
|
43
|
+
# (extend later with partition coefficients)
|
|
44
|
+
outlet_light.copy_from(inlet)
|
|
45
|
+
outlet_heavy.copy_from(inlet)
|
|
46
|
+
|
|
47
|
+
outlet_light.flow = F_light
|
|
48
|
+
outlet_heavy.flow = F_heavy
|
|
49
|
+
|
|
50
|
+
# Normalize compositions (still same, just different flow basis)
|
|
51
|
+
outlet_light.composition = {k: v for k, v in z.items()}
|
|
52
|
+
outlet_heavy.composition = {k: v for k, v in z.items()}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
"F_in": F_total,
|
|
56
|
+
"F_light": F_light,
|
|
57
|
+
"F_heavy": F_heavy,
|
|
58
|
+
"z_in": z,
|
|
59
|
+
"z_light": outlet_light.composition,
|
|
60
|
+
"z_heavy": outlet_heavy.composition,
|
|
61
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# processpi/equipment/distillation.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Optional, Dict
|
|
5
|
+
from .base import Equipment
|
|
6
|
+
from ..streams import MaterialStream
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DistillationColumn(Equipment):
|
|
10
|
+
"""
|
|
11
|
+
Simplified Distillation Column (binary or multicomponent).
|
|
12
|
+
|
|
13
|
+
Assumptions (initial version):
|
|
14
|
+
- Steady-state.
|
|
15
|
+
- Total condenser (distillate is liquid).
|
|
16
|
+
- Partial reboiler (bottoms is liquid).
|
|
17
|
+
- Uses constant relative volatility shortcut (Fenske-Underwood-Gilliland).
|
|
18
|
+
- Feed at specified stage.
|
|
19
|
+
- No rigorous stage-by-stage solving (yet).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, name: str, n_stages: int, feed_stage: int,
|
|
23
|
+
reflux_ratio: Optional[float] = None,
|
|
24
|
+
distillate_rate: Optional[float] = None):
|
|
25
|
+
super().__init__(name)
|
|
26
|
+
self.n_stages = n_stages
|
|
27
|
+
self.feed_stage = feed_stage
|
|
28
|
+
self.reflux_ratio = reflux_ratio
|
|
29
|
+
self.distillate_rate = distillate_rate
|
|
30
|
+
|
|
31
|
+
# Streams
|
|
32
|
+
self.feed: Optional[MaterialStream] = None
|
|
33
|
+
self.distillate: Optional[MaterialStream] = None
|
|
34
|
+
self.bottoms: Optional[MaterialStream] = None
|
|
35
|
+
|
|
36
|
+
def simulate(self) -> Dict:
|
|
37
|
+
if self.feed is None or self.distillate is None or self.bottoms is None:
|
|
38
|
+
raise ValueError(f"DistillationColumn {self.name} must have feed, distillate, and bottoms streams attached.")
|
|
39
|
+
|
|
40
|
+
# --- Step 1: Material balance ---
|
|
41
|
+
F = self.feed.flow
|
|
42
|
+
if self.distillate_rate is not None:
|
|
43
|
+
D = self.distillate_rate
|
|
44
|
+
B = F - D
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError("distillate_rate must be specified (initial version).")
|
|
47
|
+
|
|
48
|
+
# --- Step 2: Split compositions using shortcut method ---
|
|
49
|
+
z_feed = self.feed.composition # dict: {component: mole fraction}
|
|
50
|
+
xD, xB = {}, {}
|
|
51
|
+
|
|
52
|
+
for comp, z in z_feed.items():
|
|
53
|
+
# Simple split rule: more volatile → enriched in distillate
|
|
54
|
+
if comp.relative_volatility >= 1.5: # crude threshold
|
|
55
|
+
xD[comp] = min(1.0, z * 1.5)
|
|
56
|
+
xB[comp] = max(0.0, z - (xD[comp] * D / F))
|
|
57
|
+
else:
|
|
58
|
+
xB[comp] = min(1.0, z * 1.5)
|
|
59
|
+
xD[comp] = max(0.0, z - (xB[comp] * B / F))
|
|
60
|
+
|
|
61
|
+
# Normalize
|
|
62
|
+
sum_xD = sum(xD.values())
|
|
63
|
+
sum_xB = sum(xB.values())
|
|
64
|
+
for comp in xD: xD[comp] /= sum_xD
|
|
65
|
+
for comp in xB: xB[comp] /= sum_xB
|
|
66
|
+
|
|
67
|
+
# --- Step 3: Copy to outlet streams ---
|
|
68
|
+
self.distillate.copy_from(self.feed)
|
|
69
|
+
self.distillate.flow = D
|
|
70
|
+
self.distillate.composition = xD
|
|
71
|
+
|
|
72
|
+
self.bottoms.copy_from(self.feed)
|
|
73
|
+
self.bottoms.flow = B
|
|
74
|
+
self.bottoms.composition = xB
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"feed_flow": F,
|
|
78
|
+
"distillate_flow": D,
|
|
79
|
+
"bottoms_flow": B,
|
|
80
|
+
"distillate_composition": xD,
|
|
81
|
+
"bottoms_composition": xB
|
|
82
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# processpi/equipment/flash.py
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
from typing import Optional, Dict, Any
|
|
5
|
+
from .base import Equipment
|
|
6
|
+
from ..streams import MaterialStream
|
|
7
|
+
from ..thermo.vle import flash_equilibrium # placeholder VLE calc function
|
|
8
|
+
|
|
9
|
+
class FlashDrum(Equipment):
|
|
10
|
+
"""
|
|
11
|
+
Flash Drum for vapor-liquid separation.
|
|
12
|
+
|
|
13
|
+
Modes:
|
|
14
|
+
- T, P specified (isothermal flash)
|
|
15
|
+
- P, Q specified (adiabatic flash with duty)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, name: str):
|
|
19
|
+
super().__init__(name, inlet_ports=1, outlet_ports=2)
|
|
20
|
+
self.vapor_out: Optional[MaterialStream] = None
|
|
21
|
+
self.liquid_out: Optional[MaterialStream] = None
|
|
22
|
+
|
|
23
|
+
def attach_stream(self, stream: MaterialStream, port: str):
|
|
24
|
+
if port == "in":
|
|
25
|
+
self.inlets[0] = stream
|
|
26
|
+
stream.connect_outlet(self)
|
|
27
|
+
elif port == "vapor":
|
|
28
|
+
self.vapor_out = stream
|
|
29
|
+
stream.connect_inlet(self)
|
|
30
|
+
elif port == "liquid":
|
|
31
|
+
self.liquid_out = stream
|
|
32
|
+
stream.connect_inlet(self)
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError(f"Invalid port: {port}")
|
|
35
|
+
|
|
36
|
+
def simulate(self,
|
|
37
|
+
pressure: Optional[float] = None,
|
|
38
|
+
temperature: Optional[float] = None,
|
|
39
|
+
duty: Optional[float] = None) -> Dict[str, Any]:
|
|
40
|
+
"""
|
|
41
|
+
Perform flash separation.
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
- pressure: operating P [Pa]
|
|
45
|
+
- temperature: operating T [K]
|
|
46
|
+
- duty: optional heat duty (W)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
- dict with vapor_fraction, liquid_fraction, compositions, enthalpies
|
|
50
|
+
"""
|
|
51
|
+
feed = self.inlets[0]
|
|
52
|
+
if feed is None or self.vapor_out is None or self.liquid_out is None:
|
|
53
|
+
raise ValueError(f"{self.name}: must attach feed, vapor, and liquid streams")
|
|
54
|
+
|
|
55
|
+
# Use feed enthalpy/composition
|
|
56
|
+
z = feed.components
|
|
57
|
+
P = pressure or feed.pressure.value
|
|
58
|
+
T = temperature or feed.temperature.value
|
|
59
|
+
|
|
60
|
+
# Call thermo package (placeholder)
|
|
61
|
+
flash_results = flash_equilibrium(z, T, P, duty=duty)
|
|
62
|
+
|
|
63
|
+
Vfrac = flash_results["vapor_fraction"]
|
|
64
|
+
Lfrac = 1 - Vfrac
|
|
65
|
+
|
|
66
|
+
# Update outlet streams
|
|
67
|
+
self.vapor_out.set_composition(flash_results["y"], basis="mole")
|
|
68
|
+
self.vapor_out.pressure = feed.pressure
|
|
69
|
+
self.vapor_out.temperature = feed.temperature
|
|
70
|
+
self.vapor_out.phase = "vapor"
|
|
71
|
+
|
|
72
|
+
self.liquid_out.set_composition(flash_results["x"], basis="mole")
|
|
73
|
+
self.liquid_out.pressure = feed.pressure
|
|
74
|
+
self.liquid_out.temperature = feed.temperature
|
|
75
|
+
self.liquid_out.phase = "liquid"
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
"Vfrac": Vfrac,
|
|
79
|
+
"Lfrac": Lfrac,
|
|
80
|
+
"y": flash_results["y"],
|
|
81
|
+
"x": flash_results["x"],
|
|
82
|
+
"T": T,
|
|
83
|
+
"P": P
|
|
84
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
from ..streams import MaterialStream, EnergyStream
|
|
3
|
+
from .base import Equipment
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HeatTransferUnit(Equipment):
|
|
7
|
+
"""
|
|
8
|
+
Base class for heater/cooler type units.
|
|
9
|
+
Supports sensible (+ latent, via MaterialStream auto phase handling).
|
|
10
|
+
Energy balance:
|
|
11
|
+
Q = m * cp * ΔT [+/- m * Hvap if phase change occurs]
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, name: str, mode: str = "heater", energy_stream: Optional[EnergyStream] = None):
|
|
15
|
+
super().__init__(name, inlet_ports=1, outlet_ports=1)
|
|
16
|
+
self.mode = mode # "heater" or "cooler"
|
|
17
|
+
self.energy_stream = energy_stream
|
|
18
|
+
|
|
19
|
+
# Auto-bind energy stream if provided
|
|
20
|
+
if self.energy_stream:
|
|
21
|
+
self.energy_stream.bind_equipment(self)
|
|
22
|
+
|
|
23
|
+
def simulate(
|
|
24
|
+
self,
|
|
25
|
+
heat_duty: Optional[float] = None,
|
|
26
|
+
outlet_temperature: Optional[float] = None
|
|
27
|
+
) -> Dict[str, Any]:
|
|
28
|
+
"""
|
|
29
|
+
Simulate heater/cooler.
|
|
30
|
+
|
|
31
|
+
Parameters:
|
|
32
|
+
- heat_duty (W) [optional]
|
|
33
|
+
- outlet_temperature (float) [optional]
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
- dict with results: Q (W), Tin, Tout, phase_in, phase_out
|
|
37
|
+
"""
|
|
38
|
+
inlet: MaterialStream = self.inlets[0]
|
|
39
|
+
outlet: MaterialStream = self.outlets[0]
|
|
40
|
+
|
|
41
|
+
if inlet is None or outlet is None:
|
|
42
|
+
raise ValueError(f"{self.name}: Must have both inlet and outlet connected.")
|
|
43
|
+
|
|
44
|
+
m_dot = inlet.mass_flow().to("kg/s").value
|
|
45
|
+
cp = inlet.get_property("cp") # J/kg-K
|
|
46
|
+
|
|
47
|
+
Tin = inlet.temperature.value
|
|
48
|
+
Tout = outlet.temperature.value if outlet.temperature else None
|
|
49
|
+
Qin = 0.0
|
|
50
|
+
|
|
51
|
+
# -------------------------
|
|
52
|
+
# Case 1: Duty specified
|
|
53
|
+
# -------------------------
|
|
54
|
+
if heat_duty is not None:
|
|
55
|
+
dT = heat_duty / (m_dot * cp)
|
|
56
|
+
Tout = Tin + dT
|
|
57
|
+
Qin = heat_duty
|
|
58
|
+
|
|
59
|
+
# -------------------------
|
|
60
|
+
# Case 2: Outlet T specified
|
|
61
|
+
# -------------------------
|
|
62
|
+
elif outlet_temperature is not None:
|
|
63
|
+
Tout = outlet_temperature
|
|
64
|
+
Qin = m_dot * cp * (Tout - Tin)
|
|
65
|
+
|
|
66
|
+
else:
|
|
67
|
+
raise ValueError("Must specify either heat_duty or outlet_temperature.")
|
|
68
|
+
|
|
69
|
+
# Update outlet stream — phase auto-updates inside MaterialStream
|
|
70
|
+
outlet.temperature.value = Tout
|
|
71
|
+
outlet.P = inlet.P
|
|
72
|
+
outlet.flow_rate = inlet.flow_rate
|
|
73
|
+
|
|
74
|
+
# Flip sign for cooler
|
|
75
|
+
if self.mode == "cooler" and Qin > 0:
|
|
76
|
+
Qin *= -1
|
|
77
|
+
|
|
78
|
+
# -------------------------
|
|
79
|
+
# EnergyStream logging
|
|
80
|
+
# -------------------------
|
|
81
|
+
if self.energy_stream:
|
|
82
|
+
tag = "Q_in" if Qin > 0 else "Q_out"
|
|
83
|
+
self.energy_stream.record(abs(Qin), tag=tag, equipment=self.name)
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
"Q": Qin,
|
|
87
|
+
"Tin": Tin,
|
|
88
|
+
"Tout": Tout,
|
|
89
|
+
"phase_in": inlet.phase,
|
|
90
|
+
"phase_out": outlet.phase, # comes directly from MaterialStream auto-detection
|
|
91
|
+
"cp": cp,
|
|
92
|
+
"m_dot": m_dot,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Heater(HeatTransferUnit):
|
|
97
|
+
def __init__(self, name: str, energy_stream: Optional[EnergyStream] = None):
|
|
98
|
+
super().__init__(name, mode="heater", energy_stream=energy_stream)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Cooler(HeatTransferUnit):
|
|
102
|
+
def __init__(self, name: str, energy_stream: Optional[EnergyStream] = None):
|
|
103
|
+
super().__init__(name, mode="cooler", energy_stream=energy_stream)
|