A FastAPI-based process simulation engine for sugar factory operations. This engine calculates material and energy balances across all major sugar processing units.
Version: 0.7.0 (18 January 2026)
Status: Enhanced Solver with Equipment Constraints, Thermodynamic Properties, and Topology Visualization
✅ Shared Wegstein Core - Unified acceleration for sugar & generic solvers
✅ Equipment Constraints - Parameter bounds & defaults in API responses
✅ Stream Thermo Properties - Optional comprehensive property calculation
✅ Auto-Recycle Detection - Automatic tear stream identification
✅ Sugar Plant Topology - Visual flowsheet with recycle convergence status
The calculation engine comprises two parallel systems:
Block-based calculations for sugar factory operations organized into 10 blocks, each handling a specific calculation domain:
┌─────────────────────────────────────────────────────────────────────────┐
│ PLANT MODE FLOW │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ Block 1 Block 2 Block 3 Block 4 Block 5 │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Input │ ──▶ │Compo-│ ──▶ │ Mill │ ──▶ │Heater│ ──▶ │Clari-│ │
│ │Valid │ │sition│ │Tandem│ │ │ │fier │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
│ Block 6 Block 7 Block 8 Block 9 Block 10 │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │Evapo-│ ──▶ │Cryst-│ ──▶ │Centri│ ──▶ │Plant │ ──▶ │Recycle│ │
│ │rator │ │allize│ │fuge │ │Integ │ │Streams│ │
│ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Physics-based component library (core/) for flexible flowsheet simulation:
- Thermodynamics (
thermodynamics/): Ideal, NRTL, Hybrid property packages - Streams (
streams/): MaterialStream - universal data carrier - Equipment (
equipment/): Distillation, flash, decanter, stripper/absorber, heat exchangers, multi-effect evaporator - Flowsheet (
flowsheet/): NetworkX-based with Wegstein recycle convergence - Dynamic Simulation (
dynamic/): ODE-based transient simulation - Adsorption & PSA (
adsorption/): Complete CO2 capture & PSA system (214 tests)- Isotherms: Langmuir, Freundlich, Dual-site for equilibrium
- Kinetics: LDF mass transfer model with film/pore resistance
- Hydraulics: Ergun equation for pressure drop
- Database: Pre-configured adsorbents (zeolites, activated carbon, MOFs)
- Dynamic breakthrough: Single & multi-component with competitive adsorption
- PSA scheduling: 2-bed cycles with 180° phase shift
- Multi-Effect Evaporator (
equipment/simple/): Complete MEE system (18 tests)- Design mode: Calculate areas from target concentration
- Rating mode: Find achievable concentration from given areas
- Steam economy: Actual vs benchmark with efficiency rating
- Effects: 1-7 effects with pressure distribution
- Utilities: Live steam, cooling water sizing
📖 See ADSORPTION_API_GUIDE.md for complete adsorption documentation 📖 See MEE_API_EXAMPLES.md for multi-effect evaporator documentation
| Block | Name | Purpose |
|---|---|---|
| 1 | Input Validation | Validate all plant/unit parameters against physical limits |
| 2 | Composition | Calculate cane composition (sugar, fiber, water, ash) |
| 3 | Mill Tandem | Multi-stage extraction with crusher + mills |
| 4 | Heater | Juice heating with steam consumption calculation |
| 5 | Clarifier | Mud separation, settling, clear juice output |
| 6 | Evaporator | Multi-effect concentration (1-7 effects) |
| 7 | Crystallizer | Vacuum pan crystallization (Complete) |
| 8 | Centrifuge | Sugar/molasses separation (Complete) |
| 9 | Plant Integration | Chain all blocks for full plant simulation (Pending) |
| 10 | Recycle Streams | Molasses, magma, wash water recycles (Pending) |
| Block | Status | Tests | Description |
|---|---|---|---|
| Block 1 | ✅ Complete | 48 | Input validation with Pydantic models |
| Block 2 | ✅ Complete | 35 | Cane composition calculations |
| Block 3 | ✅ Complete | 139 | Mill tandem extraction (crusher + mills) |
| Block 4 | ✅ Complete | 69 | Heater train (ideal/realistic modes) |
| Block 5 | ✅ Complete | 68 | Clarifier (settling, mud, purity gain) |
| Block 6 | ✅ Complete | 67 | Evaporator (multi-effect, Robert/Falling-Film) |
| Block 7 | ✅ Complete | 82 | Crystallizer (vacuum pan, A/B/C strikes) |
| Block 8 | ✅ Complete | 77 | Centrifuge (multi-stage, G-force separation) |
| Block 9 | 🔜 Pending | - | Plant mode integration |
| Block 10 | 🔜 Pending | - | Recycle stream convergence |
Total Tests: 649 (all passing)
- Python 3.10 or higher
- pip (Python package manager)
macOS:
# Using Homebrew
brew install python@3.11
# Verify installation
python3 --versionUbuntu/Debian:
sudo apt update
sudo apt install python3.11 python3.11-venv python3-pipWindows:
# Download from https://www.python.org/downloads/
# Or use winget
winget install Python.Python.3.11git clone <repository-url>
cd CalculationEngineV2# Create virtual environment
python3 -m venv .venv
# Activate virtual environment
# macOS/Linux (bash/zsh):
source .venv/bin/activate
# macOS/Linux (fish):
source .venv/bin/activate.fish
# Windows (PowerShell):
.venv\Scripts\Activate.ps1
# Windows (CMD):
.venv\Scripts\activate.batpip install -r requirements.txtThermodynamic parameters and entrainer recommendations are stored in-memory
with automatic file persistence at data/thermo_store.json. Seed data is
loaded automatically on first startup. No external database required.
# Copy example environment file
cp .env.example .env
# Edit with your settings (optional - defaults work for local development)
# Required for admin endpoints: Set ADMIN_API_KEY# Development mode with auto-reload
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
# Production mode
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
GET /healthResponse:
{
"status": "healthy",
"version": "0.2.0",
"industry": "sugar"
}GET /defaults/sugarReturns all default parameter values for plant and unit configurations.
GET /limits/sugarReturns min/max/default for all configurable parameters.
The Parameter Management API allows you to store and retrieve binary interaction parameters and entrainer recommendations.
GET /thermo/parametersGET /thermo/parameters/{compound1}/{compound2}?model=nrtlcurl -X POST http://localhost:8000/thermo/parameters \
-H "Authorization: Bearer YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{
"compound1": "ethylene glycol",
"compound2": "ethanol",
"model": "nrtl",
"parameters": {"A12": -1.03, "A21": 0.74, "B12": 385.1, "B21": -125.6, "alpha": 0.3},
"source": "literature",
"confidence": "high"
}'curl -X POST http://localhost:8000/thermo/parameters/seed \
-H "Authorization: Bearer YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d @data/entrainer_parameters.json# List all entrainers
GET /thermo/entrainers
# Get entrainers for an azeotrope (IPA-water: 87.7% azeotrope)
GET /thermo/entrainers/isopropanol/water
# Get thermodynamic properties
POST /thermo/properties
# Body: {"property_package": "nrtl", "compounds": ["ethanol", "water"]}
# Get default entrainer
GET /thermo/entrainers/ethanol/water/defaultRoute distillation column outputs to downstream equipment:
# Distillate → Cooler, Bottoms → Tank
curl -X POST http://localhost:8000/simulate/dynamic \
-H "Content-Type: application/json" \
-d '{
"compounds": ["benzene", "toluene"],
"property_package": "ideal",
"feed_streams": [{"stream_id": "feed", "target_equipment": "column_1", "flow_rate": 100, "temperature_K": 370, "composition": {"benzene": 0.5, "toluene": 0.5}}],
"equipment": [
{"id": "column_1", "type": "distillation_column", "parameters": {"num_stages": 15, "reflux_ratio": 3}},
{"id": "cooler", "type": "cooler", "parameters": {"outlet_temperature_K": 310}},
{"id": "tank", "type": "mixer", "parameters": {}}
],
"edges": [
{"source": "column_1", "target": "cooler", "source_port": "distillate"},
{"source": "column_1", "target": "tank", "source_port": "bottoms"}
]
}'Available Column Ports: feed, feed_2, distillate, bottoms
POST /simulate/sugarRequest Payload:
{
"capacity": 2500,
"cane_brix": 13.5,
"fiber_pct": 13.0,
"ash_pct": 2.0,
"cane_purity": 82.0,
"mill": {
"num_mills": 4,
"rotation_speed": 5.0,
"roller_pressure": 150,
"imbibition_pct": 25.0
},
"heater": {
"inlet_temp": 30.0,
"outlet_temp": 105.0,
"steam_pressure": 2.5,
"num_heaters": 3
},
"clarifier": {
"retention_time": 2.0,
"flocculant_dose": 3.0,
"mud_pct": 10.0,
"operating_temp": 103.0
},
"evaporator": {
"num_effects": 4,
"vacuum_pressure": 0.2,
"target_brix": 60.0,
"first_effect_pressure": 1.5
},
"crystallizer": {
"seed_pct": 5.0,
"cooling_rate": 2.0,
"retention_time": 24.0,
"final_temp": 50.0,
"target_crystal_size": 0.6
},
"centrifuge": {
"spin_speed": 1100,
"cycle_time": 3.0,
"wash_water_pct": 2.0,
"num_centrifuges": 4
}
}Response:
{
"status": "success",
"mode": "plant",
"unit": null,
"blocks_completed": ["block1_inputs", "block2_composition"],
"blocks_pending": ["block3_mill", "block4_heater", "..."],
"inputs": { "...validated inputs..." },
"computed": {
"water_pct": 71.5,
"cane_flow_kg_hr": 104166.67,
"cane_composition": {
"cane_flow_kg_hr": 104166.67,
"sugar_kg_hr": 14062.5,
"sucrose_kg_hr": 11531.25,
"non_sucrose_kg_hr": 2531.25,
"fiber_kg_hr": 13541.67,
"ash_kg_hr": 2083.33,
"water_kg_hr": 74479.17
},
"juice_estimate": {
"estimated_juice_flow_kg_hr": 83776.04,
"estimated_brix": 15.95,
"estimated_purity": 82.0,
"imbibition_water_kg_hr": 3385.42,
"extraction_efficiency": 0.95
}
},
"outputs": null,
"kpis": null,
"warnings": [],
"execution_time_ms": 0.79
}POST /simulate/sugar/millRequest Payload (Minimal):
{
"inlet_stream": {
"flow_kg_hr": 100000,
"brix": 15.0,
"fiber_pct": 13.0
}
}Request Payload (With Per-Stage Configuration):
{
"inlet_stream": {
"flow_kg_hr": 100000,
"brix": 15.0,
"fiber_pct": 13.0,
"ash_pct": 0.5,
"temperature": 30.0,
"purity": 82.0
},
"operating_params": {
"num_mills": 4,
"rotation_speed": 5.0,
"roller_pressure": 150,
"imbibition_pct": 25.0,
"stage_configs": [
{
"stage_number": 1,
"rotation_speed": 4.0,
"roller_pressure": 120,
"imbibition_pct": 0
},
{
"stage_number": 2,
"rotation_speed": 5.0,
"roller_pressure": 150,
"imbibition_pct": 33.3
},
{
"stage_number": 3,
"rotation_speed": 5.0,
"roller_pressure": 160,
"imbibition_pct": 33.3
},
{
"stage_number": 4,
"rotation_speed": 5.0,
"roller_pressure": 160,
"imbibition_pct": 33.4
}
]
}
}Response:
{
"status": "success",
"mode": "standalone",
"unit": "mill",
"blocks_completed": ["block1_inputs", "block2_composition", "block3_mill"],
"blocks_pending": [],
"inputs": { "...validated inputs with stage_configs..." },
"computed": {
"cane_composition": { "..." },
"juice_estimate": { "..." }
},
"outputs": {
"mixed_juice": {
"flow_kg_hr": 71801.44,
"sugar_kg_hr": 14801.44,
"brix": 20.61
},
"final_bagasse": {
"flow_kg_hr": 26198.56,
"moisture_pct": 49.62,
"fiber_pct": 49.62,
"residual_sugar_kg_hr": 198.56,
"pol_pct": 0.76
},
"stages": [
{
"stage_number": 1,
"stage_type": "crusher",
"rotation_speed_rpm": 4.0,
"roller_pressure_bar": 120.0,
"efficiency_factor": 0.5633,
"sugar_extracted_kg_hr": 8449.27,
"juice_flow_kg_hr": 58395.67,
"bagasse_moisture_pct": 50.63
},
{
"stage_number": 2,
"stage_type": "mill",
"rotation_speed_rpm": 5.0,
"roller_pressure_bar": 150.0,
"efficiency_factor": 0.7432,
"sugar_extracted_kg_hr": 4868.83,
"juice_flow_kg_hr": 11922.43,
"bagasse_moisture_pct": 46.96
},
"..."
]
},
"kpis": {
"overall_extraction_pct": 98.68,
"total_sugar_extracted_kg_hr": 14801.44,
"sugar_loss_in_bagasse_kg_hr": 198.56
},
"warnings": [],
"execution_time_ms": 0.64
}POST /validate/sugarValidates plant inputs without running simulation. Returns validated inputs or validation errors.
POST /validate/sugar/{unit}Where {unit} is one of: mill, heater, clarifier, evaporator, crystallizer, centrifuge
┌────────────────────────────────────────────────────────────────────────┐
│ BLOCK 2: Composition Calculation │
├────────────────────────────────────────────────────────────────────────┤
│ Input: │
│ - cane_flow_kg_hr (from capacity: TCD × 1000 / 24) │
│ - cane_brix (sugar %) │
│ - fiber_pct, ash_pct, cane_purity │
│ │
│ Output: │
│ - cane_composition: {sugar, fiber, water, ash in kg/hr} │
│ - juice_estimate: {flow, brix, purity, imbibition water} │
└────────────────────────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────────────────────────┐
│ BLOCK 3: Mill Tandem Extraction │
├────────────────────────────────────────────────────────────────────────┤
│ Input: │
│ - cane_composition (from Block 2) │
│ - mill_input: {num_mills, rpm, pressure, imbibition, stage_configs} │
│ │
│ Physics: │
│ - Stage 1 (Crusher): cell_rupture × squeeze efficiency │
│ - Stages 2+ (Mills): squeeze only (fiber already ruptured) │
│ - RPM modifier: η = 1.0 - 0.02 × (rpm - 5.0)² │
│ - Pressure modifier: η = 1.0 + 0.0003 × (pressure - 150) │
│ │
│ Output: │
│ - mixed_juice: {flow, sugar, brix} │
│ - final_bagasse: {flow, moisture, fiber, residual_sugar} │
│ - stages[]: per-stage extraction details │
│ - kpis: {overall_extraction_pct, sugar_loss} │
└────────────────────────────────────────────────────────────────────────┘
The engine enforces physically realistic behavior:
- Sugar extraction decreases per stage (diminishing returns)
- Juice flow decreases per stage (less extractable liquid)
- Bagasse moisture stays within 48-52% range
- Mass balance is maintained throughout
- Efficiency limits are clamped to physical bounds
# Run all tests with verbose output
python -m pytest tests/ -v
# Run specific block tests
python -m pytest tests/test_block1_inputs.py -v
python -m pytest tests/test_block2_composition.py -v
python -m pytest tests/test_block3_mill.py -v
# Run with coverage
python -m pytest tests/ --cov=modules --cov-report=html| Test File | Count | Coverage |
|---|---|---|
| test_block1_inputs.py | 48 | Input validation |
| test_block2_composition.py | 35 | Composition math |
| test_block3_mill.py | 139 | Mill extraction |
| test_block4_heater.py | 69 | Heater train physics |
| test_block5_clarifier.py | 68 | Clarifier settling |
| test_block6_evaporator.py | 67 | Evaporator multi-effect |
| test_block7_crystallizer.py | 82 | Crystallization |
| test_block8_centrifuge.py | 77 | Centrifugal separation |
| Sugar Subtotal | 585 |
| Test File | Count | Coverage |
|---|---|---|
| test_thermodynamic_properties.py | 62 | Property packages, flash |
| test_streams.py | 47 | Material streams |
| test_distillation_column.py | 62 | Distillation, McCabe-Thiele |
| test_decanter.py | 13 | VLLE decanter |
| test_stripper_equipment.py | 13 | Stripper/absorber columns |
| test_htu_ntu.py | 23 | HTU/NTU packed column sizing |
| test_stage_solver.py | 18 | McCabe-Thiele tray columns |
| test_mass_transfer.py | 32 | Onda/Billet-Schultes MT |
| test_recycle.py | 36 | Recycle convergence |
| testdynamic*.py | 73 | Dynamic simulation, ODE |
| test_controllers.py | 31 | PID, valve control |
| Adsorption Module | 214 | Complete PSA system |
| - test_adsorption_isotherms.py | 38 | Langmuir, Freundlich, Dual |
| - test_adsorption_database.py | 38 | Adsorbent properties |
| - test_adsorption_kinetics.py | 29 | LDF mass transfer |
| - test_adsorption_hydraulics.py | 33 | Ergun equation, pressure drop |
| - test_adsorption_discretization.py | 42 | Spatial discretization |
| - test_adsorption_column.py | 17 | Equipment class |
| - test_breakthrough_simulation.py | 8 | Breakthrough curves (1 skip) |
| - test_psa_cycles.py | 24 | PSA scheduling |
| - test_literature_validation.py | 9 | Literature validation |
| Generic Subtotal | 538 |
| Test File | Count | Coverage |
|---|---|---|
| test_api.py | 29 | API endpoints |
| test_topology_validation.py | 24 | Flowsheet validation |
| Other integration tests | 105 | Full system tests |
| Integration Subtotal | 158 |
Total: 1281 tests passing, 2 skipped
CalculationEngineV2/
├── main.py # FastAPI application & endpoints
├── requirements.txt # Python dependencies
├── README.md # This file
├── CONTEXT.md # Detailed development context
├── API_CURL_COMMANDS.md # cURL examples for all endpoints
├── physics_check.py # Physics validation script
├── .gitignore # Git ignore rules
│
├── core/
│ ├── constants.py # All limits, defaults, physics constants
│ └── utils.py # Shared utility functions
│
├── modules/
│ └── sugar/
│ ├── block1_inputs.py # Input models & validation
│ ├── block2_composition.py # Composition calculations
│ ├── block2_outputs.py # Block 2 output models
│ ├── block3_mill.py # Mill tandem extraction
│ ├── block4_heater.py # Heater train calculation
│ ├── block5_clarifier.py # Clarifier calculation
│ ├── block6_evaporator.py # Evaporator calculation
│ ├── block7_crystallizer.py # Crystallizer calculation
│ ├── streams.py # Stream models (Cane, Juice, etc.)
│ ├── standalone_inputs.py # Standalone mode input models
│ └── recycle.py # Recycle stream models
│
└── tests/
├── test_block1_inputs.py
├── test_block2_composition.py
├── test_block3_mill.py
├── test_block4_heater.py
├── test_block5_clarifier.py
├── test_block6_evaporator.py
└── test_block7_crystallizer.py
curl http://localhost:8000/healthcurl -X POST http://localhost:8000/simulate/sugar/mill \
-H "Content-Type: application/json" \
-d '{"inlet_stream": {"flow_kg_hr": 100000, "brix": 15.0, "fiber_pct": 13.0}}'curl -X POST http://localhost:8000/simulate/sugar/mill \
-H "Content-Type: application/json" \
-d '{
"inlet_stream": {
"flow_kg_hr": 100000,
"brix": 15.0,
"fiber_pct": 13.0
},
"operating_params": {
"num_mills": 4,
"imbibition_pct": 25.0,
"stage_configs": [
{"stage_number": 1, "rotation_speed": 4.0, "roller_pressure": 120},
{"stage_number": 2, "rotation_speed": 5.0, "roller_pressure": 150},
{"stage_number": 3, "rotation_speed": 5.0, "roller_pressure": 160},
{"stage_number": 4, "rotation_speed": 5.0, "roller_pressure": 160}
]
}
}'curl -X POST http://localhost:8000/simulate/sugar \
-H "Content-Type: application/json" \
-d '{
"capacity": 2500,
"cane_brix": 13.5,
"fiber_pct": 13.0,
"ash_pct": 2.0,
"cane_purity": 82.0
}'curl -X POST http://localhost:8000/simulate/sugar/crystallizer \
-H "Content-Type: application/json" \
-d '{
"inlet_stream": {
"flow_kg_hr": 25000,
"brix": 78.0,
"purity": 85.0,
"temperature": 75.0
},
"operating_params": {
"strike_type": "A",
"vacuum_pressure_bar": 0.18,
"steam_pressure_bar": 2.5,
"seed_loading_kg": 200,
"target_crystal_content": 50.0
}
}'curl -X POST http://localhost:8000/simulate/sugar/centrifuge \
-H "Content-Type: application/json" \
-d '{
"inlet_stream": {
"flow_kg_hr": 30000,
"brix": 92.0,
"crystal_content": 45.0,
"temperature": 60.0,
"purity": 78.0
},
"operating_params": {
"spin_speed": 1400,
"cycle_time": 3.5,
"wash_water_pct": 2.0,
"num_centrifuges": 4,
"num_stages": 1,
"operating_mode": "realistic"
}
}'Expected Response (Verified - 0.0% Mass Balance Error):
{
"status": "success",
"outputs": {
"raw_sugar": {
"flow_kg_hr": 13573.2,
"sucrose_kg_hr": 13278.13,
"purity": 99.0,
"moisture_pct": 1.2
},
"molasses": {
"flow_kg_hr": 16672.44,
"brix": 85.1,
"purity": 58.15
},
"mass_balance": {
"total_inlet_kg_hr": 30268.24,
"total_outlet_kg_hr": 30268.24,
"balance_error_pct": 0.0,
"is_valid": true
}
},
"kpis": {
"crystal_recovery_pct": 99.35,
"sucrose_partition_to_sugar_pct": 61.678,
"moisture_content_pct": 1.2
}
}Proprietary - ARKEN AI
For questions or support, contact the ARKEN AI development team.