├── pyflowsheet ├── backends │ ├── __init__.py │ ├── foreignObject.py │ └── bitmapcontext.py ├── annotations │ ├── __init__.py │ ├── textelement.py │ ├── figure.py │ └── table.py ├── core │ ├── __init__.py │ ├── enums.py │ ├── port.py │ ├── stream.py │ ├── pathfinder.py │ ├── unitoperation.py │ └── flowsheet.py ├── internals │ ├── baseinternal.py │ ├── __init__.py │ ├── dividingWall.py │ ├── catalystBed.py │ ├── reciprocating.py │ ├── liquidRing.py │ ├── trays.py │ ├── baffles.py │ ├── tubes.py │ ├── randomPacking.py │ ├── discdonutbaffles.py │ ├── jacket.py │ └── stirrer.py ├── unitoperations │ ├── __init__.py │ ├── blackbox.py │ ├── valve.py │ ├── pump.py │ ├── streamflag.py │ ├── mixer.py │ ├── splitter.py │ ├── compressor.py │ ├── heatexchanger.py │ ├── platehex.py │ ├── vessel.py │ └── distillation.py └── __init__.py ├── todo.md ├── rebuild.bat ├── LICENSE ├── setup.py ├── .gitignore ├── changelog.md ├── img ├── styling_example.svg ├── fermenter_example.svg ├── simple_column.svg ├── styling_colouring.svg ├── blockflowprocess.svg ├── small_process.svg └── externalized_column_with_preheater.svg ├── docs ├── pyflowsheet │ ├── annotations │ │ ├── index.html │ │ └── textelement.html │ ├── backends │ │ └── index.html │ ├── core │ │ └── index.html │ ├── internals │ │ ├── baseinternal.html │ │ └── dividingWall.html │ └── unitoperations │ │ └── index.html ├── index.html └── textelement.html └── examples └── fermenter.ipynb /pyflowsheet/backends/__init__.py: -------------------------------------------------------------------------------- 1 | from .svgcontext import SvgContext -------------------------------------------------------------------------------- /pyflowsheet/annotations/__init__.py: -------------------------------------------------------------------------------- 1 | from .textelement import TextElement 2 | -------------------------------------------------------------------------------- /todo.md: -------------------------------------------------------------------------------- 1 | # ToDo List & Feature Ideas 2 | 3 | * Tags (with UOM) 4 | * Plate Heat Exchanger 5 | * Multiple Passes on Vessel -------------------------------------------------------------------------------- /pyflowsheet/core/__init__.py: -------------------------------------------------------------------------------- 1 | from .unitoperation import UnitOperation 2 | from .flowsheet import Flowsheet 3 | from .pathfinder import Pathfinder 4 | from .port import Port 5 | from .stream import Stream -------------------------------------------------------------------------------- /pyflowsheet/internals/baseinternal.py: -------------------------------------------------------------------------------- 1 | class BaseInternal(object): 2 | def __init__(self, parent): 3 | self.parent = parent 4 | return 5 | 6 | def draw(self, ctx): 7 | 8 | return -------------------------------------------------------------------------------- /rebuild.bat: -------------------------------------------------------------------------------- 1 | echo "Rebuilding documentation" 2 | pdoc --html --force --output-dir docs pyflowsheet 3 | move docs\pyflowsheet\*.* docs\ 4 | rmdir docs\pyflowsheet\ 5 | 6 | 7 | 8 | echo "Rebuilding dist" 9 | del dist\*.* 10 | python setup.py sdist bdist_wheel -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/__init__.py: -------------------------------------------------------------------------------- 1 | from .blackbox import BlackBox 2 | from .distillation import Distillation 3 | from .heatexchanger import HeatExchanger 4 | from .mixer import Mixer 5 | from .splitter import Splitter 6 | from .pump import Pump 7 | from .vessel import Vessel 8 | from .valve import Valve 9 | from .streamflag import StreamFlag 10 | from .compressor import Compressor 11 | from .platehex import PlateHex -------------------------------------------------------------------------------- /pyflowsheet/core/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, auto 2 | 3 | 4 | class HorizontalLabelAlignment(Enum): 5 | LeftOuter = auto() 6 | Left = auto() 7 | Center = auto() 8 | Right = auto() 9 | RightOuter = auto() 10 | 11 | 12 | class VerticalLabelAlignment(Enum): 13 | Top = auto() 14 | Center = auto() 15 | Bottom = auto() 16 | 17 | 18 | class FlowPattern(Enum): 19 | CoCurrent = auto() 20 | CounterCurrent = auto() -------------------------------------------------------------------------------- /pyflowsheet/internals/__init__.py: -------------------------------------------------------------------------------- 1 | from .tubes import Tubes 2 | from .catalystBed import CatalystBed 3 | from .baffles import Baffles 4 | from .discdonutbaffles import DiscDonutBaffles 5 | 6 | from .trays import Trays 7 | from .dividingWall import DividingWall 8 | from .randomPacking import RandomPacking 9 | 10 | from .stirrer import Stirrer 11 | from .stirrer import StirrerType 12 | from .jacket import Jacket 13 | 14 | from .reciprocating import Reciprocating 15 | from .liquidRing import LiquidRing -------------------------------------------------------------------------------- /pyflowsheet/internals/dividingWall.py: -------------------------------------------------------------------------------- 1 | from .baseinternal import BaseInternal 2 | 3 | 4 | class DividingWall(BaseInternal): 5 | def __init__(self): 6 | return 7 | 8 | def draw(self, ctx): 9 | if self.parent is None: 10 | Warning("Internal has no parent set!") 11 | return 12 | 13 | unit = self.parent 14 | 15 | ctx.line( 16 | (unit.position[0] + unit.size[0] / 2, unit.position[1] + unit.size[0]), 17 | ( 18 | unit.position[0] + unit.size[0] / 2, 19 | unit.position[1] + unit.size[1] - unit.size[0], 20 | ), 21 | unit.lineColor, 22 | unit.lineSize, 23 | ) 24 | 25 | return -------------------------------------------------------------------------------- /pyflowsheet/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import Flowsheet 2 | from .core import UnitOperation 3 | from .core import Stream 4 | from .core import Port 5 | 6 | from .core.enums import VerticalLabelAlignment, HorizontalLabelAlignment 7 | 8 | 9 | from .unitoperations import Distillation 10 | from .unitoperations import Vessel 11 | from .unitoperations import BlackBox 12 | from .unitoperations import Pump 13 | from .unitoperations import Valve 14 | from .unitoperations import StreamFlag 15 | from .unitoperations import HeatExchanger 16 | from .unitoperations import Mixer 17 | from .unitoperations import Splitter 18 | from .unitoperations import Compressor 19 | from .unitoperations import PlateHex 20 | 21 | from .annotations import TextElement 22 | from .backends import SvgContext 23 | -------------------------------------------------------------------------------- /pyflowsheet/annotations/textelement.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core.enums import HorizontalLabelAlignment, VerticalLabelAlignment 3 | 4 | 5 | class TextElement(UnitOperation): 6 | def __init__(self, text, position=(0, 0)): 7 | super().__init__("text", "text", position=position, size=(20, 20)) 8 | self.text = text 9 | self.drawBoundingBox = False 10 | self.showTitle = False 11 | self.fontFamily = "Consolas" 12 | return 13 | 14 | def draw(self, ctx): 15 | # html = self.data.to_html() 16 | # ctx.html(html, self.position, self.size) 17 | 18 | ctx.text( 19 | self.position, 20 | text=self.text, 21 | fontFamily=self.fontFamily, 22 | textColor=self.textColor, 23 | textAnchor="start", 24 | ) 25 | # super().draw(ctx) 26 | 27 | return -------------------------------------------------------------------------------- /pyflowsheet/backends/foreignObject.py: -------------------------------------------------------------------------------- 1 | import svgwrite 2 | 3 | 4 | class ForeignObject( 5 | svgwrite.base.BaseElement, 6 | svgwrite.mixins.Transform, 7 | svgwrite.container.Presentation, 8 | ): 9 | """Create an instance of the ForeignObject class. This class describes an add-on to svgwrite and allows arbitrary HTML code to be embedded in SVG drawings. 10 | 11 | Args: 12 | svgwrite ([type]): [description] 13 | svgwrite ([type]): [description] 14 | svgwrite ([type]): [description] 15 | 16 | Returns: 17 | [type]: [description] 18 | """ 19 | 20 | elementname = "foreignObject" 21 | 22 | def __init__(self, obj, **extra): 23 | super().__init__(**extra) 24 | self.obj = obj 25 | 26 | def get_xml(self): 27 | xml = super().get_xml() 28 | xml.append(svgwrite.etree.etree.fromstring(self.obj)) 29 | return xml 30 | -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/blackbox.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | 5 | class BlackBox(UnitOperation): 6 | def __init__( 7 | self, id: str, name: str, position=(0, 0), size=(20, 20), description: str = "" 8 | ): 9 | super().__init__(id, name, position=position, size=size) 10 | self.updatePorts() 11 | 12 | def updatePorts(self): 13 | self.ports = {} 14 | self.ports["In"] = Port("In", self, (0, 0.5), (-1, 0)) 15 | self.ports["Out"] = Port("Out", self, (1, 0.5), (1, 0), intent="out") 16 | return 17 | 18 | def draw(self, ctx): 19 | 20 | ctx.rectangle( 21 | [ 22 | self.position, 23 | (self.position[0] + self.size[0], self.position[1] + self.size[1]), 24 | ], 25 | self.fillColor, 26 | self.lineColor, 27 | self.lineSize, 28 | ) 29 | 30 | super().draw(ctx) 31 | 32 | return -------------------------------------------------------------------------------- /pyflowsheet/internals/catalystBed.py: -------------------------------------------------------------------------------- 1 | from .baseinternal import BaseInternal 2 | 3 | 4 | class CatalystBed(BaseInternal): 5 | def __init__(self): 6 | return 7 | 8 | def draw(self, ctx): 9 | if self.parent is None: 10 | Warning("Internal has no parent set!") 11 | return 12 | 13 | unit = self.parent 14 | 15 | if unit.capLength == None: 16 | capLength = unit.size[0] / 2 17 | else: 18 | capLength = unit.capLength 19 | 20 | start = (unit.position[0], unit.position[1] + capLength) 21 | end = ( 22 | unit.position[0] + unit.size[0], 23 | unit.position[1] + unit.size[1] - capLength, 24 | ) 25 | ctx.line(start, end, unit.lineColor, unit.lineSize) 26 | 27 | start = (unit.position[0] + unit.size[0], unit.position[1] + capLength) 28 | end = (unit.position[0], unit.position[1] + unit.size[1] - capLength) 29 | ctx.line(start, end, unit.lineColor, unit.lineSize) 30 | 31 | return -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jochen Steimel 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. 22 | -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/valve.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | 5 | class Valve(UnitOperation): 6 | def __init__( 7 | self, id: str, name: str, position=(0, 0), size=(40, 20), description: str = "" 8 | ): 9 | super().__init__(id, name, position=position, size=size) 10 | self.updatePorts() 11 | 12 | def updatePorts(self): 13 | self.ports = {} 14 | self.ports["In"] = Port("In", self, (0, 0.5), (-1, 0)) 15 | self.ports["Out"] = Port("Out", self, (1, 0.5), (1, 0), intent="out") 16 | return 17 | 18 | def draw(self, ctx): 19 | points = [] 20 | 21 | points.append((self.position[0], self.position[1])) 22 | points.append( 23 | (self.position[0] + self.size[0], self.position[1] + self.size[1]) 24 | ) 25 | points.append((self.position[0] + self.size[0], self.position[1])) 26 | points.append((self.position[0], self.position[1] + self.size[1])) 27 | 28 | ctx.path(points, self.fillColor, self.lineColor, self.lineSize, close=True) 29 | 30 | super().draw(ctx) 31 | 32 | return -------------------------------------------------------------------------------- /pyflowsheet/internals/reciprocating.py: -------------------------------------------------------------------------------- 1 | from .baseinternal import BaseInternal 2 | 3 | 4 | class Reciprocating(BaseInternal): 5 | def __init__(self): 6 | return 7 | 8 | def draw(self, ctx): 9 | if self.parent is None: 10 | Warning("Internal has no parent set!") 11 | return 12 | 13 | unit = self.parent 14 | 15 | ctx.line( 16 | ( 17 | unit.position[0] + unit.size[0] * 0.4, 18 | unit.position[1] + unit.size[1] / 2, 19 | ), 20 | ( 21 | unit.position[0] + unit.size[0] * 0.8, 22 | unit.position[1] + unit.size[1] / 2, 23 | ), 24 | unit.lineColor, 25 | unit.lineSize, 26 | ) 27 | ctx.line( 28 | ( 29 | unit.position[0] + unit.size[0] * 0.4, 30 | unit.position[1] + unit.size[1] * 0.3, 31 | ), 32 | ( 33 | unit.position[0] + unit.size[0] * 0.4, 34 | unit.position[1] + unit.size[1] * 0.7, 35 | ), 36 | unit.lineColor, 37 | unit.lineSize, 38 | ) 39 | 40 | return -------------------------------------------------------------------------------- /pyflowsheet/internals/liquidRing.py: -------------------------------------------------------------------------------- 1 | from .baseinternal import BaseInternal 2 | from math import sin, cos, radians, sqrt 3 | 4 | 5 | class LiquidRing(BaseInternal): 6 | def __init__(self): 7 | return 8 | 9 | def draw(self, ctx): 10 | if self.parent is None: 11 | Warning("Internal has no parent set!") 12 | return 13 | 14 | unit = self.parent 15 | 16 | lines = 4 17 | bladeLength = unit.size[0] / 2 * 0.5 18 | 19 | for i in range(lines): 20 | angle = 180 / lines * i 21 | angleInRadians = radians(angle) 22 | dxs = bladeLength * cos(angleInRadians) 23 | dys = bladeLength * sin(angleInRadians) 24 | 25 | ctx.line( 26 | ( 27 | unit.position[0] + unit.size[0] / 2 + dxs, 28 | unit.position[1] + unit.size[1] / 2 + dys, 29 | ), 30 | ( 31 | unit.position[0] + unit.size[0] / 2 - dxs, 32 | unit.position[1] + unit.size[1] / 2 - dys, 33 | ), 34 | unit.lineColor, 35 | unit.lineSize / 2, 36 | ) 37 | 38 | return -------------------------------------------------------------------------------- /pyflowsheet/core/port.py: -------------------------------------------------------------------------------- 1 | class Port(object): 2 | def __init__(self, name, parent, rel_pos, normal, intent="in"): 3 | self.name = name 4 | self.relativePosition = rel_pos 5 | self.normal = normal 6 | self.size = (6, 6) 7 | self.fillColor = None 8 | self.lineColor = (0, 0, 255, 255) 9 | self.lineSize = 1 10 | self.parent = parent 11 | self.intent = intent 12 | 13 | def get_position(self): 14 | 15 | base_x = ( 16 | self.parent.position[0] + self.relativePosition[0] * self.parent.size[0] 17 | ) 18 | base_y = ( 19 | self.parent.position[1] + self.relativePosition[1] * self.parent.size[1] 20 | ) 21 | return (base_x, base_y) 22 | 23 | def draw(self, ctx): 24 | 25 | base_x, base_y = self.get_position() 26 | 27 | ctx.circle( 28 | [ 29 | (base_x - self.size[0] / 2, base_y - self.size[1] / 2), 30 | (base_x + self.size[0] / 2, base_y + self.size[1] / 2), 31 | ], 32 | self.fillColor, 33 | self.lineColor if self.intent == "in" else (255, 0, 0, 255), 34 | self.lineSize, 35 | ) 36 | -------------------------------------------------------------------------------- /pyflowsheet/backends/bitmapcontext.py: -------------------------------------------------------------------------------- 1 | import importlib.util 2 | 3 | package_name = "pillow" 4 | spec = importlib.util.find_spec(package_name) 5 | 6 | if spec is None: 7 | Warning("Pillow is not installed. You cannot render to the bitmap context!") 8 | PYFLOWSHEET_PILLOW_MISSING = True 9 | else: 10 | from PIL import Image, ImageDraw 11 | 12 | PYFLOWSHEET_PILLOW_MISSING = False 13 | 14 | 15 | class BitmapContext(object): 16 | def __init__(self, size): 17 | 18 | if PYFLOWSHEET_PILLOW_MISSING: 19 | Warning("Pillow is not installed. You cannot render to the bitmap context!") 20 | self.img = Image.new("RGBA", size, (255, 255, 255, 255)) 21 | self.draw = ImageDraw.Draw(self.img) 22 | 23 | def rectangle(self, rect, fillColor, lineColor, lineSize): 24 | self.draw.rectangle(rect, fillColor, lineColor, lineSize) 25 | return 26 | 27 | def chord(self, rect, start, end, fillColor, lineColor, lineSize): 28 | self.draw.chord(rect, start, end, fillColor, lineColor, width=lineSize) 29 | return 30 | 31 | def circle(self, rect, fillColor, lineColor, lineSize): 32 | return 33 | 34 | def text(self, x, y, text, fontFamily, textColor): 35 | return 36 | 37 | def render(self): 38 | return self.img -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/pump.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | 5 | class Pump(UnitOperation): 6 | def __init__( 7 | self, 8 | id: str, 9 | name: str, 10 | position=(0, 0), 11 | size=(40, 40), 12 | description: str = "", 13 | internals=[], 14 | ): 15 | super().__init__(id, name, position=position, size=size, internals=internals) 16 | self.updatePorts() 17 | 18 | def updatePorts(self): 19 | self.ports = {} 20 | self.ports["In"] = Port("In", self, (0, 0.5), (-1, 0)) 21 | self.ports["Out"] = Port("Out", self, (1, 0.5), (1, 0), intent="out") 22 | return 23 | 24 | def draw(self, ctx): 25 | 26 | ctx.circle( 27 | [ 28 | self.position, 29 | (self.position[0] + self.size[0], self.position[1] + self.size[1]), 30 | ], 31 | self.fillColor, 32 | self.lineColor, 33 | self.lineSize, 34 | ) 35 | 36 | start_up = (self.position[0] + self.size[0] / 2, self.position[1]) 37 | start_lo = ( 38 | self.position[0] + self.size[0] / 2, 39 | self.position[1] + self.size[1], 40 | ) 41 | end = (self.position[0] + self.size[0], self.position[1] + self.size[1] / 2) 42 | 43 | ctx.line(start_up, end, self.lineColor, self.lineSize) 44 | ctx.line(start_lo, end, self.lineColor, self.lineSize) 45 | 46 | super().draw(ctx) 47 | 48 | return -------------------------------------------------------------------------------- /pyflowsheet/internals/trays.py: -------------------------------------------------------------------------------- 1 | from .baseinternal import BaseInternal 2 | 3 | 4 | class Trays(BaseInternal): 5 | def __init__(self, start=0, end=1, numberOfTrays=11): 6 | self.start = start 7 | self.end = end 8 | self.numberOfTrays = numberOfTrays 9 | return 10 | 11 | def draw(self, ctx): 12 | if self.parent is None: 13 | Warning("Internal has no parent set!") 14 | return 15 | 16 | unit = self.parent 17 | 18 | availableHeight = (unit.size[1] - unit.size[0]) * (self.end - self.start) 19 | 20 | for i in range(self.numberOfTrays): 21 | if i % 2 == 0: 22 | xs = unit.position[0] 23 | xe = unit.position[0] + unit.size[0] * 0.8 24 | else: 25 | xs = unit.position[0] + unit.size[0] * 0.2 26 | xe = unit.position[0] + unit.size[0] 27 | 28 | ctx.line( 29 | ( 30 | xs, 31 | unit.position[1] 32 | + unit.size[0] / 2 33 | + availableHeight * self.start 34 | + availableHeight / self.numberOfTrays * i, 35 | ), 36 | ( 37 | xe, 38 | unit.position[1] 39 | + unit.size[0] / 2 40 | + availableHeight * self.start 41 | + availableHeight / self.numberOfTrays * i, 42 | ), 43 | unit.lineColor, 44 | unit.lineSize, 45 | ) 46 | 47 | return -------------------------------------------------------------------------------- /pyflowsheet/internals/baffles.py: -------------------------------------------------------------------------------- 1 | from .baseinternal import BaseInternal 2 | 3 | 4 | class Baffles(BaseInternal): 5 | def __init__(self, start=0, end=1, numberOfBaffles=11): 6 | self.start = start 7 | self.end = end 8 | self.numberOfBaffles = numberOfBaffles 9 | return 10 | 11 | def draw(self, ctx): 12 | if self.parent is None: 13 | Warning("Internal has no parent set!") 14 | return 15 | 16 | unit = self.parent 17 | if unit.capLength == None: 18 | capLength = unit.size[0] / 2 19 | else: 20 | capLength = unit.capLength 21 | 22 | availableHeight = (unit.size[1] - 2 * capLength) * (self.end - self.start) 23 | 24 | for i in range(self.numberOfBaffles): 25 | if i % 2 == 0: 26 | xs = unit.position[0] 27 | xe = unit.position[0] + unit.size[0] * 0.8 28 | else: 29 | xs = unit.position[0] + unit.size[0] * 0.2 30 | xe = unit.position[0] + unit.size[0] 31 | 32 | ctx.line( 33 | ( 34 | xs, 35 | unit.position[1] 36 | + capLength 37 | + availableHeight * self.start 38 | + availableHeight / self.numberOfBaffles * i, 39 | ), 40 | ( 41 | xe, 42 | unit.position[1] 43 | + capLength 44 | + availableHeight * self.start 45 | + availableHeight / self.numberOfBaffles * i, 46 | ), 47 | unit.lineColor, 48 | unit.lineSize, 49 | ) 50 | 51 | return -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/streamflag.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | 5 | class StreamFlag(UnitOperation): 6 | def __init__( 7 | self, id: str, name: str, position=(0, 0), size=(40, 40), description: str = "" 8 | ): 9 | super().__init__(id, name, position=position, size=size) 10 | self.updatePorts() 11 | 12 | def updatePorts(self): 13 | self.ports = {} 14 | self.ports["In"] = Port("In", self, (0, 0.5), (-1, 0)) 15 | self.ports["Out"] = Port("Out", self, (1, 0.5), (1, 0), intent="out") 16 | return 17 | 18 | def draw(self, ctx): 19 | 20 | vfrac = 1 / 4 21 | hfrac = 1 / 2 22 | 23 | points = [] 24 | 25 | points.append((self.position[0], self.position[1] + self.size[1] * vfrac)) 26 | points.append( 27 | ( 28 | self.position[0] + self.size[0] * hfrac, 29 | self.position[1] + self.size[1] * vfrac, 30 | ) 31 | ) 32 | points.append((self.position[0] + self.size[0] * hfrac, self.position[1])) 33 | points.append( 34 | (self.position[0] + self.size[0], self.position[1] + self.size[1] / 2) 35 | ) 36 | points.append( 37 | (self.position[0] + self.size[0] * hfrac, self.position[1] + self.size[1]) 38 | ) 39 | points.append( 40 | ( 41 | self.position[0] + self.size[0] * hfrac, 42 | self.position[1] + self.size[1] * (1 - vfrac), 43 | ) 44 | ) 45 | points.append((self.position[0], self.position[1] + self.size[1] * (1 - vfrac))) 46 | 47 | ctx.path(points, self.fillColor, self.lineColor, self.lineSize, close=True) 48 | 49 | super().draw(ctx) 50 | 51 | return -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/mixer.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | 5 | class Mixer(UnitOperation): 6 | def __init__( 7 | self, id: str, name: str, position=(0, 0), size=(20, 20), description: str = "" 8 | ): 9 | super().__init__(id, name, position=position, size=size) 10 | self.fillColor = (0, 0, 0, 255) 11 | self.lineColor = (0, 0, 0, 255) 12 | self.textOffset = (0, 10) 13 | self.updatePorts() 14 | 15 | def updatePorts(self): 16 | self.ports = {} 17 | self.ports["In1"] = Port("In1", self, (0.2, 0.5), (-1, 0)) 18 | self.ports["In2"] = Port("In2", self, (0.5, 0.2), (0, -1)) 19 | self.ports["In3"] = Port("In3", self, (0.5, 0.8), (0, 1)) 20 | self.ports["Out"] = Port("Out", self, (0.8, 0.5), (1, 0), intent="out") 21 | return 22 | 23 | def draw(self, ctx): 24 | 25 | ctx.rectangle( 26 | [ 27 | (self.position[0] + 5, self.position[1] + 5), 28 | ( 29 | self.position[0] + self.size[0] - 5, 30 | self.position[1] + self.size[1] - 5, 31 | ), 32 | ], 33 | self.fillColor, 34 | self.lineColor, 35 | self.lineSize, 36 | ) 37 | # ctx.line( 38 | # (self.position[0], self.position[1] + self.size[1] / 2), 39 | # (self.position[0] + self.size[0], self.position[1] + self.size[1] / 2), 40 | # self.lineColor, 41 | # self.lineSize, 42 | # ) 43 | # ctx.line( 44 | # (self.position[0] + self.size[0] / 2, self.position[1]), 45 | # (self.position[0] + self.size[0] / 2, self.position[1] + self.size[1]), 46 | # self.lineColor, 47 | # self.lineSize, 48 | # ) 49 | 50 | super().draw(ctx) 51 | 52 | return -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/splitter.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | 5 | class Splitter(UnitOperation): 6 | def __init__( 7 | self, id: str, name: str, position=(0, 0), size=(20, 20), description: str = "" 8 | ): 9 | super().__init__(id, name, position=position, size=size) 10 | self.fillColor = (0, 0, 0, 255) 11 | self.lineColor = (0, 0, 0, 255) 12 | self.textOffset = (0, 10) 13 | self.updatePorts() 14 | 15 | def updatePorts(self): 16 | self.ports = {} 17 | self.ports["In"] = Port("In", self, (0.2, 0.5), (-2, 0)) 18 | self.ports["Out2"] = Port("Out2", self, (0.5, 0.2), (0, -2), intent="out") 19 | self.ports["Out3"] = Port("Out3", self, (0.5, 0.8), (0, 2), intent="out") 20 | self.ports["Out1"] = Port("Out1", self, (0.8, 0.5), (2, 0), intent="out") 21 | return 22 | 23 | def draw(self, ctx): 24 | 25 | ctx.rectangle( 26 | [ 27 | (self.position[0] + 5, self.position[1] + 5), 28 | ( 29 | self.position[0] + self.size[0] - 5, 30 | self.position[1] + self.size[1] - 5, 31 | ), 32 | ], 33 | self.fillColor, 34 | self.lineColor, 35 | self.lineSize, 36 | ) 37 | 38 | # ctx.line( 39 | # (self.position[0], self.position[1] + self.size[1] / 2), 40 | # (self.position[0] + self.size[0], self.position[1] + self.size[1] / 2), 41 | # self.lineColor, 42 | # self.lineSize, 43 | # ) 44 | # ctx.line( 45 | # (self.position[0] + self.size[0] / 2, self.position[1]), 46 | # (self.position[0] + self.size[0] / 2, self.position[1] + self.size[1]), 47 | # self.lineColor, 48 | # self.lineSize, 49 | # ) 50 | 51 | super().draw(ctx) 52 | 53 | return -------------------------------------------------------------------------------- /pyflowsheet/internals/tubes.py: -------------------------------------------------------------------------------- 1 | from .baseinternal import BaseInternal 2 | 3 | 4 | class Tubes(BaseInternal): 5 | def __init__(self, numberOfTubes=5, numberOfPasses=1): 6 | self.numberOfPasses = numberOfPasses 7 | self.numberOfTubes = numberOfTubes 8 | return 9 | 10 | def draw(self, ctx): 11 | if self.parent is None: 12 | Warning("Internal has no parent set!") 13 | return 14 | 15 | unit = self.parent 16 | 17 | if unit.capLength == None: 18 | capLength = unit.size[0] / 2 19 | else: 20 | capLength = unit.capLength 21 | 22 | for i in range(1, self.numberOfTubes): 23 | x = unit.position[0] + unit.size[0] / self.numberOfTubes * i 24 | start = (x, unit.position[1] + capLength) 25 | end = (x, unit.position[1] + unit.size[1] - capLength) 26 | ctx.line(start, end, unit.lineColor, unit.lineSize) 27 | 28 | for p in range(1, self.numberOfPasses): 29 | x = unit.position[0] + unit.size[0] - p * unit.size[0] / self.numberOfPasses 30 | if p % 2 == 1: 31 | start = (x, unit.position[1] + capLength) 32 | end = (x, unit.position[1] + capLength / 2) 33 | ctx.line(start, end, unit.lineColor, unit.lineSize) 34 | 35 | start = (x, unit.position[1] + unit.size[1]) 36 | end = (x, unit.position[1] + unit.size[1] - capLength) 37 | ctx.line(start, end, unit.lineColor, unit.lineSize) 38 | if p % 2 == 0: 39 | start = (x, unit.position[1]) 40 | end = (x, unit.position[1] + capLength) 41 | ctx.line(start, end, unit.lineColor, unit.lineSize) 42 | 43 | start = (x, unit.position[1] + unit.size[1] - capLength) 44 | end = (x, unit.position[1] + unit.size[1] - capLength / 2) 45 | ctx.line(start, end, unit.lineColor, unit.lineSize) 46 | 47 | return -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # License: MIT License 3 | # Copyright (C) 2020 Jochen Steimel 4 | import os 5 | from setuptools import setup, find_packages 6 | 7 | AUTHOR_NAME = "Jochen Steimel" 8 | AUTHOR_EMAIL = "jochen.steimel@googlemail.com" 9 | 10 | 11 | def read(fname): 12 | try: 13 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 14 | except IOError: 15 | return "File '%s' not found.\n" % fname 16 | 17 | 18 | setup( 19 | name="pyflowsheet", 20 | version="0.2.1", 21 | description="A Python library for creating process flow diagrams (PFD) for process engineering using SVG drawings.", 22 | author=AUTHOR_NAME, 23 | email=AUTHOR_EMAIL, 24 | url="https://github.com/nukleon84/pyflowsheet", 25 | python_requires=">=3.7", 26 | packages=find_packages(), 27 | provides=["pyflowsheet"], 28 | long_description=read("readme.md") + read("changelog.md"), 29 | long_description_content_type="text/markdown", 30 | platforms="OS Independent", 31 | license="MIT License", 32 | install_requires=["svgwrite", "pathfinding"], 33 | extras_require={"plots": ["matplotlib"]}, 34 | classifiers=[ 35 | "Development Status :: 3 - Alpha", 36 | "License :: OSI Approved :: MIT License", 37 | "Operating System :: OS Independent", 38 | "Programming Language :: Python :: 3", 39 | "Programming Language :: Python :: 3.7", 40 | "Programming Language :: Python :: 3.8", 41 | "Programming Language :: Python :: 3.9", 42 | "Programming Language :: Python :: Implementation :: CPython", 43 | "Programming Language :: Python :: Implementation :: PyPy", 44 | "Intended Audience :: Developers", 45 | "Topic :: Multimedia :: Graphics", 46 | "Topic :: Software Development :: Libraries :: Python Modules", 47 | "Topic :: Scientific/Engineering :: Chemistry", 48 | ], 49 | project_urls={ 50 | "Bug Reports": "http://github.com/nukleon84/pyflowsheet/issues", 51 | "Source": "http://github.com/nukleon84/pyflowsheet/", 52 | }, 53 | ) -------------------------------------------------------------------------------- /pyflowsheet/annotations/figure.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | import importlib.util 5 | 6 | package_name = "matplotlib" 7 | spec = importlib.util.find_spec(package_name) 8 | 9 | if spec is None: 10 | Warning( 11 | "Matplotlib is not installed. You cannot render tables. Please install matplotlib first!" 12 | ) 13 | PYFLOWSHEET_MATPLOTLIB_MISSING = True 14 | else: 15 | import matplotlib.pyplot as plt 16 | import base64 17 | from io import BytesIO 18 | 19 | plt.ioff() 20 | PYFLOWSHEET_MATPLOTLIB_MISSING = False 21 | 22 | 23 | class Figure(UnitOperation): 24 | def __init__( 25 | self, 26 | id: str, 27 | name: str, 28 | fig, 29 | position=(0, 0), 30 | size=(40, 20), 31 | description: str = "", 32 | ): 33 | super().__init__(id, name, position=position, size=size) 34 | self.fig = fig 35 | self.drawBoundingBox = False 36 | 37 | return 38 | 39 | def draw(self, ctx): 40 | if not PYFLOWSHEET_MATPLOTLIB_MISSING: 41 | tmpfile = BytesIO() 42 | self.fig.savefig(tmpfile, format="png") 43 | encoded = base64.b64encode(tmpfile.getvalue()).decode("utf-8") 44 | data = "data:image/png;base64,{}".format(encoded) 45 | ctx.image(data, self.position, self.size) 46 | else: 47 | start = (self.position[0], self.position[1]) 48 | end = ( 49 | self.position[0] + self.size[0], 50 | self.position[1] + self.size[1], 51 | ) 52 | ctx.line(start, end, (255, 0, 0), self.lineSize) 53 | 54 | start = (self.position[0] + self.size[0], self.position[1]) 55 | end = (self.position[0], self.position[1] + self.size[1]) 56 | ctx.line(start, end, (255, 0, 0), self.lineSize) 57 | 58 | ctx.rectangle( 59 | [ 60 | self.position, 61 | (self.position[0] + self.size[0], self.position[1] + self.size[1]), 62 | ], 63 | None, 64 | self.lineColor, 65 | self.lineSize, 66 | ) 67 | 68 | super().draw(ctx) 69 | 70 | return -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/compressor.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | from math import sin, cos, radians, sqrt 4 | 5 | 6 | class Compressor(UnitOperation): 7 | def __init__( 8 | self, 9 | id: str, 10 | name: str, 11 | position=(0, 0), 12 | size=(40, 40), 13 | description: str = "", 14 | internals=[], 15 | ): 16 | super().__init__(id, name, position=position, size=size, internals=internals) 17 | self.updatePorts() 18 | 19 | def updatePorts(self): 20 | self.ports = {} 21 | self.ports["In"] = Port("In", self, (0, 0.5), (-1, 0)) 22 | self.ports["Out"] = Port("Out", self, (1, 0.5), (1, 0), intent="out") 23 | return 24 | 25 | def draw(self, ctx): 26 | 27 | ctx.circle( 28 | [ 29 | self.position, 30 | (self.position[0] + self.size[0], self.position[1] + self.size[1]), 31 | ], 32 | self.fillColor, 33 | self.lineColor, 34 | self.lineSize, 35 | ) 36 | 37 | radiansOfStartingAngle = radians(120) 38 | dxs = self.size[0] / 2 * cos(radiansOfStartingAngle) 39 | dys = self.size[0] / 2 * sin(radiansOfStartingAngle) 40 | 41 | start_up = ( 42 | self.position[0] + self.size[0] / 2 + dxs, 43 | self.position[1] + self.size[1] / 2 - dys, 44 | ) 45 | start_low = ( 46 | self.position[0] + self.size[0] / 2 + dxs, 47 | self.position[1] + self.size[1] / 2 + dys, 48 | ) 49 | 50 | radiansOfEndingAngle = radians(10) 51 | dx = self.size[0] / 2 * cos(radiansOfEndingAngle) 52 | dy = self.size[0] / 2 * sin(radiansOfEndingAngle) 53 | 54 | end_up = ( 55 | self.position[0] + self.size[0] / 2 + dx, 56 | self.position[1] + self.size[1] / 2 - dy, 57 | ) 58 | end_low = ( 59 | self.position[0] + self.size[0] / 2 + dx, 60 | self.position[1] + self.size[1] / 2 + dy, 61 | ) 62 | 63 | ctx.line(start_up, end_up, self.lineColor, self.lineSize) 64 | ctx.line(start_low, end_low, self.lineColor, self.lineSize) 65 | 66 | super().draw(ctx) 67 | 68 | return -------------------------------------------------------------------------------- /pyflowsheet/internals/randomPacking.py: -------------------------------------------------------------------------------- 1 | from .baseinternal import BaseInternal 2 | 3 | 4 | class RandomPacking(BaseInternal): 5 | def __init__(self, start=0, end=1): 6 | self.start = start 7 | self.end = end 8 | return 9 | 10 | def draw(self, ctx): 11 | if self.parent is None: 12 | Warning("Internal has no parent set!") 13 | return 14 | 15 | unit = self.parent 16 | 17 | availableHeight = unit.size[1] - unit.size[0] 18 | 19 | ctx.line( 20 | ( 21 | unit.position[0], 22 | unit.position[1] + unit.size[0] / 2 + availableHeight * self.start, 23 | ), 24 | ( 25 | unit.position[0] + unit.size[0], 26 | unit.position[1] + unit.size[0] / 2 + availableHeight * self.end, 27 | ), 28 | unit.lineColor, 29 | unit.lineSize, 30 | ) 31 | 32 | ctx.line( 33 | ( 34 | unit.position[0] + unit.size[0], 35 | unit.position[1] + unit.size[0] / 2 + availableHeight * self.start, 36 | ), 37 | ( 38 | unit.position[0], 39 | unit.position[1] + unit.size[0] / 2 + availableHeight * self.end, 40 | ), 41 | unit.lineColor, 42 | unit.lineSize, 43 | ) 44 | 45 | # horizontal line top 46 | ctx.line( 47 | ( 48 | unit.position[0], 49 | unit.position[1] + unit.size[0] / 2 + availableHeight * self.start, 50 | ), 51 | ( 52 | unit.position[0] + unit.size[0], 53 | unit.position[1] + unit.size[0] / 2 + availableHeight * self.start, 54 | ), 55 | unit.lineColor, 56 | unit.lineSize, 57 | ) 58 | # horizontal line bottom 59 | ctx.line( 60 | ( 61 | unit.position[0], 62 | unit.position[1] + unit.size[0] / 2 + availableHeight * self.end, 63 | ), 64 | ( 65 | unit.position[0] + unit.size[0], 66 | unit.position[1] + unit.size[0] / 2 + availableHeight * self.end, 67 | ), 68 | unit.lineColor, 69 | unit.lineSize, 70 | ) 71 | 72 | return -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/heatexchanger.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | 5 | class HeatExchanger(UnitOperation): 6 | def __init__( 7 | self, id: str, name: str, position=(0, 0), size=(40, 40), description: str = "" 8 | ): 9 | super().__init__(id, name, position=position, size=size) 10 | self.updatePorts() 11 | 12 | def updatePorts(self): 13 | self.ports = {} 14 | self.ports["TIn"] = Port("TIn", self, (0, 0.5), (-1, 0)) 15 | self.ports["TOut"] = Port("TOut", self, (1, 0.5), (1, 0), intent="out") 16 | 17 | self.ports["SIn"] = Port("SIn", self, (0.5, 0), (0, -1)) 18 | self.ports["SOut"] = Port("SOut", self, (0.5, 1), (0, 1), intent="out") 19 | 20 | return 21 | 22 | def draw(self, ctx): 23 | 24 | ctx.circle( 25 | [ 26 | self.position, 27 | (self.position[0] + self.size[0], self.position[1] + self.size[1]), 28 | ], 29 | self.fillColor, 30 | self.lineColor, 31 | self.lineSize, 32 | ) 33 | 34 | vfrac = 1 / 4 35 | hfrac = 1 / 3 36 | hfrac2 = 1 / 6 37 | 38 | points = [] 39 | 40 | points.append((self.position[0], self.position[1] + self.size[1] * 0.5)) 41 | points.append( 42 | ( 43 | self.position[0] + self.size[0] * hfrac2, 44 | self.position[1] + self.size[1] * 0.5, 45 | ) 46 | ) 47 | points.append( 48 | ( 49 | self.position[0] + self.size[0] * hfrac, 50 | self.position[1] + self.size[1] * (0.5 - vfrac), 51 | ) 52 | ) 53 | points.append( 54 | ( 55 | self.position[0] + self.size[0] * 2 * hfrac, 56 | self.position[1] + self.size[1] * (0.5 + vfrac), 57 | ) 58 | ) 59 | points.append( 60 | ( 61 | self.position[0] + self.size[0] * (1 - hfrac2), 62 | self.position[1] + self.size[1] * 0.5, 63 | ) 64 | ) 65 | points.append( 66 | (self.position[0] + self.size[0], self.position[1] + self.size[1] * 0.5) 67 | ) 68 | 69 | ctx.path(points, None, self.lineColor, self.lineSize, close=False) 70 | 71 | super().draw(ctx) 72 | 73 | return -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Local VScode 132 | .vscode/ 133 | mystats 134 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Versioning 2 | 3 | The versioning scheme using in this project is based on Semantic Versioning, but adopts a different approach to handling pre-releases and build metadata. 4 | 5 | The essence of semantic versioning is a 3-part MAJOR.MINOR.MAINTENANCE numbering scheme, where the project author increments: 6 | 7 | * MAJOR version when they make incompatible API changes, 8 | 9 | * MINOR version when they add functionality in a backwards-compatible manner, and 10 | 11 | * MAINTENANCE version when they make backwards-compatible bug fixes. 12 | 13 | 14 | # History 15 | 16 | ## Version 0.2.1 (04-01-2021) 17 | 18 | * Version 0.2.0 was not running after restructuring of source files. Restored package by adding all submodules correctly in setup.py 19 | 20 | ## Version 0.2.0 (03-01-2021) 21 | 22 | **New Features** 23 | * Added an "Internals" System. You can now add any instance derived from BaseInternal to the internals list of a unit operation. These internals can represent tubes in a heat exchanger, trays or packing in a column, or special realisations of pumps and compressors. 24 | 25 | **Breaking Change**: It is not possible to define internals by a string anymore. 26 | ```python 27 | U3=pfd.unit(Vessel("Fermenter","Fermenter", position=(200,190), capLength=20, showCapLines=False, size=(80,140),internals=[Stirrer(type=StirrerType.Anchor), Jacket()] )) 28 | ``` 29 | * Changed how vertical/horizontal vessels work: Now every vessel is vertical by default, but can be rotated either with the rotate(angle) method or by passing the angle as parameter into the constructor. 30 | ```python 31 | BA11=Vessel("DS10-BA11","Horizontal Vessel", angle=90, position=(560,400), size=(40,100), capLength=20,internals=[CatalystBed()] ) 32 | ``` 33 | * Compressor unit operation 34 | 35 | **Bugfixes** 36 | * Multiple calls to .rotate(angle) do not rotate the ports anymore while keeping the unit itself at the same angle. 37 | * Rotating a unit now influences the area that is blocked for pathfinding. 38 | 39 | 40 | 41 | ## Version 0.1.1 (27-12-2020) 42 | * Alpha version 43 | * Basic drawing functions implemented 44 | * [svgwrite](https://github.com/mozman/svgwrite) backend for vector output 45 | * Limited library of unit operations 46 | * Mixer / Splitter 47 | * Heat Exchanger 48 | * Vessel (vertical/horizontal) 49 | * Distillation Column (optional reboiler and condenser, some internals) 50 | * Pump 51 | * Stream flag / Feed / Product 52 | * Valve 53 | * BlackBox Unit Operation (catch all for non-implemented icons) 54 | * Arbitary Rotation 55 | * Horizontal Flipping 56 | * Stream routing via Dykstra algorithm (using the [python-pathfinding package](https://github.com/brean/python-pathfinding) ) 57 | 58 | -------------------------------------------------------------------------------- /pyflowsheet/annotations/table.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | import importlib.util 5 | 6 | package_name = "matplotlib" 7 | spec = importlib.util.find_spec(package_name) 8 | 9 | if spec is None: 10 | Warning( 11 | "Matplotlib is not installed. You cannot render tables. Please install matplotlib first!" 12 | ) 13 | PYFLOWSHEET_MATPLOTLIB_MISSING = True 14 | else: 15 | import matplotlib.pyplot as plt 16 | import base64 17 | from io import BytesIO 18 | 19 | plt.ioff() 20 | PYFLOWSHEET_MATPLOTLIB_MISSING = False 21 | 22 | 23 | class Table(UnitOperation): 24 | def __init__( 25 | self, 26 | id: str, 27 | name: str, 28 | data, 29 | position=(0, 0), 30 | size=(40, 20), 31 | figsize=(5, 5), 32 | description: str = "", 33 | ): 34 | super().__init__(id, name, position=position, size=size) 35 | self.data = data 36 | self.drawBoundingBox = False 37 | self.figsize = figsize 38 | return 39 | 40 | def draw(self, ctx): 41 | cell_text = [] 42 | if not PYFLOWSHEET_MATPLOTLIB_MISSING: 43 | for row in range(len(self.data)): 44 | cell_text.append(self.data.iloc[row]) 45 | plt.figure(figsize=self.figsize) 46 | 47 | plt.table( 48 | cellText=cell_text, 49 | colLabels=self.data.columns, 50 | loc="center", 51 | bbox=[0, 0, 1, 1], 52 | ) 53 | plt.axis("off") 54 | 55 | fig = plt.gcf() 56 | fig.tight_layout() 57 | tmpfile = BytesIO() 58 | fig.savefig(tmpfile, format="png") 59 | encoded = base64.b64encode(tmpfile.getvalue()).decode("utf-8") 60 | 61 | htmlString = "data:image/png;base64,{}".format(encoded) 62 | ctx.image(htmlString, self.position, self.size) 63 | else: 64 | start = (self.position[0], self.position[1]) 65 | end = ( 66 | self.position[0] + self.size[0], 67 | self.position[1] + self.size[1], 68 | ) 69 | ctx.line(start, end, (255, 0, 0), self.lineSize) 70 | 71 | start = (self.position[0] + self.size[0], self.position[1]) 72 | end = (self.position[0], self.position[1] + self.size[1]) 73 | ctx.line(start, end, (255, 0, 0), self.lineSize) 74 | 75 | ctx.rectangle( 76 | [ 77 | self.position, 78 | (self.position[0] + self.size[0], self.position[1] + self.size[1]), 79 | ], 80 | None, 81 | self.lineColor, 82 | self.lineSize, 83 | ) 84 | super().draw(ctx) 85 | 86 | return -------------------------------------------------------------------------------- /img/styling_example.svg: -------------------------------------------------------------------------------- 1 | 2 | S1S2U1U2U3 -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/platehex.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | from ..core.enums import FlowPattern 5 | 6 | 7 | class PlateHex(UnitOperation): 8 | def __init__( 9 | self, 10 | id: str, 11 | name: str, 12 | position=(0, 0), 13 | size=(40, 80), 14 | description: str = "", 15 | pattern=FlowPattern.CounterCurrent, 16 | ): 17 | super().__init__(id, name, position=position, size=size) 18 | self.pattern = pattern 19 | self.updatePorts() 20 | 21 | def updatePorts(self): 22 | self.ports = {} 23 | if self.pattern == FlowPattern.CounterCurrent: 24 | self.addPort(Port("In1", self, (0, 0.875), (-1, 0))) 25 | self.addPort(Port("In2", self, (1, 0.875), (1, 0))) 26 | self.addPort(Port("Out1", self, (1, 0.125), (1, 0), intent="out")) 27 | self.addPort(Port("Out2", self, (0, 0.125), (-1, 0), intent="out")) 28 | else: 29 | self.addPort(Port("In1", self, (0, 0.875), (-1, 0))) 30 | self.addPort(Port("In2", self, (0, 0.125), (-1, 0))) 31 | self.addPort(Port("Out1", self, (1, 0.125), (1, 0), intent="out")) 32 | self.addPort(Port("Out2", self, (1, 0.875), (1, 0), intent="out")) 33 | return 34 | 35 | def draw(self, ctx): 36 | 37 | ctx.rectangle( 38 | [ 39 | self.position, 40 | (self.position[0] + self.size[0], self.position[1] + self.size[1]), 41 | ], 42 | self.fillColor, 43 | self.lineColor, 44 | self.lineSize, 45 | ) 46 | 47 | ctx.line( 48 | (self.position[0], self.position[1] + 0.875 * self.size[1]), 49 | ( 50 | self.position[0] + self.size[0], 51 | self.position[1] + 0.125 * self.size[1], 52 | ), 53 | self.lineColor, 54 | self.lineSize, 55 | ) 56 | 57 | ctx.line( 58 | (self.position[0], self.position[1] + 0.125 * self.size[1]), 59 | ( 60 | self.position[0] + self.size[0], 61 | self.position[1] + 0.875 * self.size[1], 62 | ), 63 | self.lineColor, 64 | self.lineSize, 65 | ) 66 | 67 | availableHeight = self.size[1] * (0.7 - 0.3) 68 | 69 | for i in range(3): 70 | y = self.position[1] + self.size[1] * 0.3 + availableHeight / 2 * i 71 | ctx.line( 72 | (self.position[0], y), 73 | ( 74 | self.position[0] + self.size[0], 75 | y, 76 | ), 77 | self.lineColor, 78 | self.lineSize, 79 | ) 80 | 81 | super().draw(ctx) 82 | 83 | return -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/vessel.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | 5 | class Vessel(UnitOperation): 6 | def __init__( 7 | self, 8 | id: str, 9 | name: str, 10 | position=(0, 0), 11 | size=(40, 100), 12 | description: str = "", 13 | capLength=None, 14 | internals=[], 15 | angle=0, 16 | showCapLines=True, 17 | ): 18 | 19 | super().__init__(id, name, position=position, size=size, internals=internals) 20 | 21 | self.capLength = capLength 22 | self.showCapLines = showCapLines 23 | self.updatePorts() 24 | self.rotate(angle) 25 | 26 | def updatePorts(self): 27 | self.ports = {} 28 | 29 | self.ports["In"] = Port("In", self, (0.5, 1), (0, 1)) 30 | self.ports["Out"] = Port("Out", self, (0.5, 0), (0, -1), intent="out") 31 | 32 | def _drawBasicShape(self, ctx): 33 | if self.capLength == None: 34 | capLength = self.size[0] / 2 35 | else: 36 | capLength = self.capLength 37 | 38 | ctx.rectangle( 39 | [ 40 | (self.position[0], self.position[1] + capLength), 41 | ( 42 | self.position[0] + self.size[0], 43 | self.position[1] + self.size[1] - capLength, 44 | ), 45 | ], 46 | self.fillColor, 47 | self.fillColor, 48 | self.lineSize, 49 | ) 50 | 51 | ctx.line( 52 | (self.position[0], self.position[1] + capLength), 53 | ( 54 | self.position[0], 55 | self.position[1] + self.size[1] - capLength, 56 | ), 57 | self.lineColor, 58 | self.lineSize, 59 | ) 60 | ctx.line( 61 | (self.position[0] + self.size[0], self.position[1] + capLength), 62 | ( 63 | self.position[0] + self.size[0], 64 | self.position[1] + self.size[1] - capLength, 65 | ), 66 | self.lineColor, 67 | self.lineSize, 68 | ) 69 | 70 | ctx.chord( 71 | [ 72 | (self.position[0], self.position[1]), 73 | (self.position[0] + self.size[0], self.position[1] + 2 * capLength), 74 | ], 75 | 180, 76 | 360, 77 | self.fillColor, 78 | self.lineColor, 79 | self.lineSize, 80 | closePath=self.showCapLines, 81 | ) 82 | 83 | ctx.chord( 84 | [ 85 | (self.position[0], self.position[1] + self.size[1] - 2 * capLength), 86 | (self.position[0] + self.size[0], self.position[1] + self.size[1]), 87 | ], 88 | 0, 89 | 180, 90 | self.fillColor, 91 | self.lineColor, 92 | self.lineSize, 93 | closePath=self.showCapLines, 94 | ) 95 | 96 | return 97 | 98 | def draw(self, ctx): 99 | 100 | self._drawBasicShape(ctx) 101 | 102 | super().draw(ctx) 103 | return -------------------------------------------------------------------------------- /pyflowsheet/internals/discdonutbaffles.py: -------------------------------------------------------------------------------- 1 | from .baseinternal import BaseInternal 2 | 3 | 4 | class DiscDonutBaffles(BaseInternal): 5 | def __init__(self, start=0, end=1, numberOfBaffles=11): 6 | self.start = start 7 | self.end = end 8 | self.numberOfBaffles = numberOfBaffles 9 | return 10 | 11 | def draw(self, ctx): 12 | if self.parent is None: 13 | Warning("Internal has no parent set!") 14 | return 15 | 16 | unit = self.parent 17 | if unit.capLength == None: 18 | capLength = unit.size[0] / 2 19 | else: 20 | capLength = unit.capLength 21 | 22 | availableHeight = (unit.size[1] - 2 * capLength) * (self.end - self.start) 23 | 24 | for i in range(self.numberOfBaffles): 25 | if i % 2 == 0: 26 | xs = unit.position[0] + unit.size[0] * 0.2 27 | xe = unit.position[0] + unit.size[0] * 0.8 28 | ctx.line( 29 | ( 30 | xs, 31 | unit.position[1] 32 | + capLength 33 | + availableHeight * self.start 34 | + availableHeight / self.numberOfBaffles * i, 35 | ), 36 | ( 37 | xe, 38 | unit.position[1] 39 | + capLength 40 | + availableHeight * self.start 41 | + availableHeight / self.numberOfBaffles * i, 42 | ), 43 | unit.lineColor, 44 | unit.lineSize, 45 | ) 46 | else: 47 | xs = unit.position[0] + unit.size[0] * 0.3 48 | xe = unit.position[0] + unit.size[0] * 0.7 49 | ctx.line( 50 | ( 51 | unit.position[0], 52 | unit.position[1] 53 | + capLength 54 | + availableHeight * self.start 55 | + availableHeight / self.numberOfBaffles * i, 56 | ), 57 | ( 58 | xs, 59 | unit.position[1] 60 | + capLength 61 | + availableHeight * self.start 62 | + availableHeight / self.numberOfBaffles * i, 63 | ), 64 | unit.lineColor, 65 | unit.lineSize, 66 | ) 67 | ctx.line( 68 | ( 69 | xe, 70 | unit.position[1] 71 | + capLength 72 | + availableHeight * self.start 73 | + availableHeight / self.numberOfBaffles * i, 74 | ), 75 | ( 76 | unit.position[0] + unit.size[0], 77 | unit.position[1] 78 | + capLength 79 | + availableHeight * self.start 80 | + availableHeight / self.numberOfBaffles * i, 81 | ), 82 | unit.lineColor, 83 | unit.lineSize, 84 | ) 85 | 86 | return -------------------------------------------------------------------------------- /pyflowsheet/internals/jacket.py: -------------------------------------------------------------------------------- 1 | from .baseinternal import BaseInternal 2 | 3 | 4 | class Jacket(BaseInternal): 5 | def __init__(self, thickness=5, startYFraction=0.5): 6 | self.thickness = thickness 7 | self.startYFraction = startYFraction 8 | return 9 | 10 | def draw(self, ctx): 11 | if self.parent is None: 12 | Warning("Internal has no parent set!") 13 | return 14 | 15 | unit = self.parent 16 | 17 | if unit.capLength == None: 18 | capLength = unit.size[0] / 2 19 | else: 20 | capLength = unit.capLength 21 | 22 | bevelLength = self.thickness * 2 23 | 24 | ctx.path( 25 | [ 26 | ( 27 | unit.position[0], 28 | unit.position[1] + unit.size[1] * self.startYFraction + bevelLength, 29 | ), 30 | ( 31 | unit.position[0] - self.thickness, 32 | unit.position[1] 33 | + unit.size[1] * self.startYFraction 34 | + 2 * bevelLength, 35 | ), 36 | ( 37 | unit.position[0] - self.thickness, 38 | unit.position[1] + unit.size[1] - capLength, 39 | ), 40 | ], 41 | None, 42 | unit.lineColor, 43 | unit.lineSize, 44 | ) 45 | 46 | ctx.path( 47 | [ 48 | ( 49 | unit.position[0] + unit.size[0], 50 | unit.position[1] + unit.size[1] * self.startYFraction + bevelLength, 51 | ), 52 | ( 53 | unit.position[0] + unit.size[0] + self.thickness, 54 | unit.position[1] 55 | + unit.size[1] * self.startYFraction 56 | + 2 * bevelLength, 57 | ), 58 | ( 59 | unit.position[0] + unit.size[0] + self.thickness, 60 | unit.position[1] + unit.size[1] - capLength, 61 | ), 62 | ], 63 | None, 64 | unit.lineColor, 65 | unit.lineSize, 66 | ) 67 | 68 | ctx.chord( 69 | [ 70 | ( 71 | unit.position[0] - self.thickness, 72 | unit.position[1] + unit.size[1] - 2 * capLength - self.thickness, 73 | ), 74 | ( 75 | unit.position[0] + unit.size[0] + self.thickness, 76 | unit.position[1] + unit.size[1] + self.thickness, 77 | ), 78 | ], 79 | 0, 80 | 180, 81 | None, 82 | unit.lineColor, 83 | unit.lineSize, 84 | closePath=False, 85 | ) 86 | # ctx.chord( 87 | # [ 88 | # ( 89 | # unit.position[0] - self.thickness, 90 | # unit.position[1] + unit.size[1] - 2 * capLength - self.thickness, 91 | # ), 92 | # ( 93 | # unit.position[0] + unit.size[0] + self.thickness, 94 | # unit.position[1] + unit.size[1] + self.thickness, 95 | # ), 96 | # ], 97 | # 100, 98 | # 180, 99 | # None, 100 | # unit.lineColor, 101 | # unit.lineSize, 102 | # closePath=False, 103 | # ) 104 | 105 | # ctx.path( 106 | # [ 107 | # ( 108 | # unit.position[0] + unit.size[0] / 2 - 9, 109 | # unit.position[1] + unit.size[1] + self.thickness - 1, 110 | # ), 111 | # ( 112 | # unit.position[0] + unit.size[0] / 2, 113 | # unit.position[1] + unit.size[1], 114 | # ), 115 | # ( 116 | # unit.position[0] + unit.size[0] / 2 + 9, 117 | # unit.position[1] + unit.size[1] + self.thickness - 1, 118 | # ), 119 | # ], 120 | # None, 121 | # unit.lineColor, 122 | # unit.lineSize, 123 | # ) 124 | 125 | return -------------------------------------------------------------------------------- /img/fermenter_example.svg: -------------------------------------------------------------------------------- 1 | 2 | S1S2S3S4S5AirSubstrateFermenterOff-GasProductValve -------------------------------------------------------------------------------- /img/simple_column.svg: -------------------------------------------------------------------------------- 1 | 2 | S01S02S03S04S05F100DS10-WA10DS10-KA10P1P2 -------------------------------------------------------------------------------- /pyflowsheet/internals/stirrer.py: -------------------------------------------------------------------------------- 1 | from .baseinternal import BaseInternal 2 | from enum import Enum, auto 3 | 4 | 5 | class StirrerType(Enum): 6 | Propeller = auto() 7 | Anchor = auto() 8 | Helical = auto() 9 | 10 | 11 | class Stirrer(BaseInternal): 12 | def __init__(self, type=StirrerType.Propeller): 13 | self.type = type 14 | return 15 | 16 | def draw(self, ctx): 17 | if self.parent is None: 18 | Warning("Internal has no parent set!") 19 | return 20 | 21 | unit = self.parent 22 | 23 | ctx.line( 24 | (unit.position[0] + unit.size[0] / 2, unit.position[1]), 25 | ( 26 | unit.position[0] + unit.size[0] / 2, 27 | unit.position[1] + unit.size[1] - unit.size[0] / 2, 28 | ), 29 | unit.lineColor, 30 | unit.lineSize, 31 | ) 32 | 33 | bladeLength = unit.size[0] / 4 34 | bladeHeight = unit.size[1] / 10 35 | 36 | if self.type == StirrerType.Anchor: 37 | ctx.path( 38 | [ 39 | ( 40 | unit.position[0] + unit.size[0] / 2 - bladeLength, 41 | unit.position[1] 42 | + unit.size[1] 43 | - unit.size[0] / 2 44 | - bladeHeight, 45 | ), 46 | ( 47 | unit.position[0] + unit.size[0] / 2 - bladeLength, 48 | unit.position[1] + unit.size[1] - unit.size[0] / 2, 49 | ), 50 | ( 51 | unit.position[0] + unit.size[0] / 2 + bladeLength, 52 | unit.position[1] + unit.size[1] - unit.size[0] / 2, 53 | ), 54 | ( 55 | unit.position[0] + unit.size[0] / 2 + bladeLength, 56 | unit.position[1] 57 | + unit.size[1] 58 | - unit.size[0] / 2 59 | - bladeHeight, 60 | ), 61 | ], 62 | None, 63 | unit.lineColor, 64 | unit.lineSize, 65 | ) 66 | 67 | if self.type == StirrerType.Propeller: 68 | ctx.line( 69 | ( 70 | unit.position[0] + unit.size[0] / 2 - bladeLength, 71 | unit.position[1] + unit.size[1] - unit.size[0] / 2, 72 | ), 73 | ( 74 | unit.position[0] + unit.size[0] / 2 + bladeLength, 75 | unit.position[1] + unit.size[1] - unit.size[0] / 2, 76 | ), 77 | unit.lineColor, 78 | unit.lineSize, 79 | ) 80 | ctx.rectangle( 81 | [ 82 | ( 83 | unit.position[0] + unit.size[0] / 2 - bladeLength, 84 | unit.position[1] 85 | + unit.size[1] 86 | - unit.size[0] / 2 87 | - bladeHeight, 88 | ), 89 | ( 90 | unit.position[0] + unit.size[0] / 2 - 0.5 * bladeLength, 91 | unit.position[1] 92 | + unit.size[1] 93 | - unit.size[0] / 2 94 | + bladeHeight, 95 | ), 96 | ], 97 | unit.lineColor, 98 | unit.lineColor, 99 | unit.lineSize, 100 | ) 101 | ctx.rectangle( 102 | [ 103 | ( 104 | unit.position[0] + unit.size[0] / 2 + 0.5 * bladeLength, 105 | unit.position[1] 106 | + unit.size[1] 107 | - unit.size[0] / 2 108 | - bladeHeight, 109 | ), 110 | ( 111 | unit.position[0] + unit.size[0] / 2 + bladeLength, 112 | unit.position[1] 113 | + unit.size[1] 114 | - unit.size[0] / 2 115 | + bladeHeight, 116 | ), 117 | ], 118 | unit.lineColor, 119 | unit.lineColor, 120 | unit.lineSize, 121 | ) 122 | 123 | return -------------------------------------------------------------------------------- /pyflowsheet/core/stream.py: -------------------------------------------------------------------------------- 1 | from .pathfinder import Pathfinder, rectifyPath, compressPath 2 | 3 | 4 | class Stream(object): 5 | def __init__(self, id, fromPort, toPort): 6 | self.id = id 7 | self.lineColor = (0, 0, 0, 255) 8 | self.textColor = (0, 0, 0, 255) 9 | self.lineSize = 2 10 | self.fromPort = fromPort 11 | self.toPort = toPort 12 | self.showTitle = True 13 | self.fontFamily = "Arial" 14 | self.dashArray = None 15 | self.manualRouting = [] 16 | self.showPoints = False 17 | self.labelOffset = (0, 10) 18 | 19 | def draw(self, ctx, grid, minx, miny): 20 | 21 | if len(self.manualRouting) == 0: 22 | points, startAnchor = self._calculateAutoRoute(minx, miny, grid) 23 | else: 24 | points = [] 25 | points.append(self.fromPort.get_position()) 26 | 27 | for step in self.manualRouting: 28 | p = (points[-1][0] + step[0], points[-1][1] + step[1]) 29 | points.append(p) 30 | 31 | points.append(self.toPort.get_position()) 32 | startAnchor = points[0] 33 | 34 | ctx.path( 35 | points, None, self.lineColor, self.lineSize, False, self.dashArray, True 36 | ) 37 | 38 | if self.showPoints: 39 | for p in points: 40 | ctx.circle( 41 | [(p[0] - 2, p[1] - 2), (p[0] + 2, p[1] + 2)], 42 | None, 43 | (64, 64, 64, 255), 44 | 1, 45 | ) 46 | 47 | textAnchor = ( 48 | startAnchor[0] + self.labelOffset[0], 49 | startAnchor[1] + self.labelOffset[1], 50 | ) 51 | ctx.text( 52 | textAnchor, 53 | text=self.id, 54 | fontFamily=self.fontFamily, 55 | textColor=self.textColor, 56 | fontSize="10", 57 | ) 58 | 59 | grid.cleanup() 60 | 61 | return 62 | 63 | def _calculateAutoRoute(self, minx, miny, grid): 64 | normalLength = 10 65 | points = [] 66 | gridsize = 10 67 | startAnchor = ( 68 | self.fromPort.get_position()[0] + self.fromPort.normal[0] * normalLength, 69 | self.fromPort.get_position()[1] + self.fromPort.normal[1] * normalLength, 70 | ) 71 | endAnchor = ( 72 | self.toPort.get_position()[0] + self.toPort.normal[0] * normalLength, 73 | self.toPort.get_position()[1] + self.toPort.normal[1] * normalLength, 74 | ) 75 | 76 | startAnchor2 = ( 77 | self.fromPort.get_position()[0], 78 | self.fromPort.get_position()[1], 79 | ) 80 | endAnchor2 = ( 81 | self.toPort.get_position()[0], 82 | self.toPort.get_position()[1], 83 | ) 84 | 85 | startAnchor = ( 86 | round(startAnchor[0] / gridsize) * gridsize, 87 | round(startAnchor[1] / gridsize) * gridsize, 88 | ) 89 | endAnchor = ( 90 | round(endAnchor[0] / gridsize) * gridsize, 91 | round(endAnchor[1] / gridsize) * gridsize, 92 | ) 93 | 94 | startAnchor2 = ( 95 | round(startAnchor2[0] / gridsize) * gridsize, 96 | round(startAnchor2[1] / gridsize) * gridsize, 97 | ) 98 | endAnchor2 = ( 99 | round(endAnchor2[0] / gridsize) * gridsize, 100 | round(endAnchor2[1] / gridsize) * gridsize, 101 | ) 102 | sx = round((startAnchor[0] - minx) / gridsize) 103 | sy = round((startAnchor[1] - miny) / gridsize) 104 | 105 | ex = round((endAnchor[0] - minx) / gridsize) 106 | ey = round((endAnchor[1] - miny) / gridsize) 107 | 108 | scx = round((startAnchor2[0] - minx) / gridsize) 109 | scy = round((startAnchor2[1] - miny) / gridsize) 110 | 111 | ecx = round((endAnchor2[0] - minx) / gridsize) 112 | ecy = round((endAnchor2[1] - miny) / gridsize) 113 | 114 | start = grid.node(scx, scy) 115 | end = grid.node(ecx, ecy) 116 | 117 | grid.node(sx, sy).walkable = True 118 | grid.node(ex, ey).walkable = True 119 | grid.node(scx, scy).walkable = True 120 | grid.node(ecx, ecy).walkable = True 121 | 122 | finder = Pathfinder() 123 | path, _ = finder.find_path(start, end, grid) 124 | 125 | w = 10 126 | 127 | for step in path: 128 | grid.node(step[0], step[1]).weight += w 129 | 130 | path = compressPath(path) 131 | 132 | points.append(self.fromPort.get_position()) 133 | for step in path: 134 | p = (step[0] * gridsize + minx, step[1] * gridsize + miny) 135 | points.append(p) 136 | 137 | realendpoint = self.toPort.get_position() 138 | if realendpoint[0] != points[-1][0] or realendpoint[1] != points[-1][1]: 139 | points.append(realendpoint) 140 | return points, startAnchor 141 | -------------------------------------------------------------------------------- /img/styling_colouring.svg: -------------------------------------------------------------------------------- 1 | 2 | S01S02S03S04S05S06S07F100F200MX10DS10-WA10SP10P2P1P3 -------------------------------------------------------------------------------- /pyflowsheet/core/pathfinder.py: -------------------------------------------------------------------------------- 1 | from pathfinding.core.diagonal_movement import DiagonalMovement 2 | from pathfinding.core.grid import Grid 3 | from pathfinding.finder.dijkstra import DijkstraFinder 4 | from pathfinding.finder.a_star import AStarFinder 5 | from pathfinding.finder.breadth_first import BreadthFirstFinder 6 | from pathfinding.core.heuristic import chebyshev, null, manhatten 7 | from pathfinding.core.util import SQRT2 8 | from math import pow 9 | from pathfinding.core.util import backtrace, bi_backtrace 10 | import heapq 11 | 12 | 13 | def distance(a, b): 14 | return pow(a[0] - b[0], 2) + pow(a[1] - b[1], 2) 15 | 16 | 17 | def compressPath(path): 18 | if len(path) < 2: 19 | return path 20 | deltaPath = [] 21 | newPath = [] 22 | 23 | newPath.append(path[0]) 24 | 25 | for i, n in enumerate(path[1:]): 26 | deltaPath.append((n[0] - path[i][0], n[1] - path[i][1])) 27 | 28 | for i, delta in enumerate(deltaPath[1:]): 29 | lastDelta = deltaPath[i] 30 | if delta[0] != lastDelta[0] or delta[1] != lastDelta[1]: 31 | newPath.append(path[i + 1]) 32 | newPath.append(path[-1]) 33 | return newPath 34 | 35 | 36 | def rectifyPath(path, grid, end): 37 | 38 | if len(path) < 2: 39 | return path 40 | 41 | newPath = [] 42 | deltaPath = [] 43 | last = path[0] 44 | goal = (end.x, end.y) 45 | 46 | containsBends = True 47 | 48 | while containsBends: 49 | i = 0 50 | containsBends = False 51 | while i < len(path) - 2: 52 | if ( 53 | path[i][0] == path[i + 1][0] 54 | and path[i][1] > path[i + 2][1] 55 | and path[i + 1][0] > path[i + 2][0] 56 | ): 57 | print("Up/Left-Bend detected") 58 | containsBends = True 59 | 60 | newNode = (path[i + 2][0], path[i][1]) 61 | path.remove(path[i]) 62 | path.remove(path[i]) 63 | path.remove(path[i]) 64 | path.insert(i, newNode) 65 | 66 | i += 1 67 | for n in path: 68 | newPath.append(n) 69 | # newPath.append(path[-2]) 70 | # newPath.append(path[-1]) 71 | # newPath.append(path[]) 72 | 73 | return newPath 74 | 75 | 76 | class Pathfinder(AStarFinder): 77 | def __init__(self, turnPenalty=150): 78 | super(Pathfinder, self).__init__( 79 | diagonal_movement=DiagonalMovement.never, heuristic=null 80 | ) 81 | 82 | self.turnPenalty = turnPenalty 83 | return 84 | 85 | def process_node(self, node, parent, end, open_list, open_value=True): 86 | """ 87 | we check if the given node is path of the path by calculating its 88 | cost and add or remove it from our path 89 | :param node: the node we like to test 90 | (the neighbor in A* or jump-node in JumpPointSearch) 91 | :param parent: the parent node (the current node we like to test) 92 | :param end: the end point to calculate the cost of the path 93 | :param open_list: the list that keeps track of our current path 94 | :param open_value: needed if we like to set the open list to something 95 | else than True (used for bi-directional algorithms) 96 | """ 97 | # calculate cost from current node (parent) to the next node (neighbor) 98 | ng = self.calc_cost(parent, node) 99 | 100 | lastDirection = ( 101 | None 102 | if parent.parent == None 103 | else (parent.x - parent.parent.x, parent.y - parent.parent.y) 104 | ) 105 | turned = ( 106 | 0 107 | if lastDirection == None 108 | else ( 109 | lastDirection[0] != parent.x - node.x 110 | or lastDirection[1] != parent.y - node.y 111 | ) 112 | ) 113 | 114 | ng += self.turnPenalty * turned 115 | 116 | if not node.opened or ng < node.g: 117 | node.g = ng 118 | node.h = node.h or self.apply_heuristic(node, end) * self.weight 119 | # f is the estimated total cost from start to goal 120 | node.f = node.g + node.h 121 | node.parent = parent 122 | 123 | if not node.opened: 124 | heapq.heappush(open_list, node) 125 | node.opened = open_value 126 | else: 127 | # the node can be reached with smaller cost. 128 | # Since its f value has been updated, we have to 129 | # update its position in the open list 130 | open_list.remove(node) 131 | heapq.heappush(open_list, node) 132 | 133 | def calc_cost(self, node_a, node_b): 134 | """ 135 | get the distance between current node and the neighbor (cost) 136 | """ 137 | if node_b.x - node_a.x == 0 or node_b.y - node_a.y == 0: 138 | # direct neighbor - distance is 1 139 | ng = 1 140 | else: 141 | # not a direct neighbor - diagonal movement 142 | ng = SQRT2 143 | 144 | # weight for weighted algorithms 145 | if self.weighted: 146 | ng *= node_b.weight 147 | 148 | return node_a.g + ng 149 | -------------------------------------------------------------------------------- /img/blockflowprocess.svg: -------------------------------------------------------------------------------- 1 | 2 | S01S02S03S04S05S06S07S08S09S10FeedProductWasteSideProductPretreatmentReactionGas-RecoverySeparationPurification -------------------------------------------------------------------------------- /pyflowsheet/unitoperations/distillation.py: -------------------------------------------------------------------------------- 1 | from ..core import UnitOperation 2 | from ..core import Port 3 | 4 | 5 | class Distillation(UnitOperation): 6 | def __init__( 7 | self, 8 | id: str, 9 | name: str, 10 | hasCondenser=True, 11 | hasReboiler=True, 12 | position=(0, 0), 13 | size=(40, 200), 14 | description: str = "", 15 | internals=[], 16 | ): 17 | 18 | super().__init__(id, name, position=position, size=size, internals=internals) 19 | 20 | self.hasReboiler = hasReboiler 21 | self.hasCondenser = hasCondenser 22 | 23 | if self.hasReboiler: 24 | self.textOffset = (0, self.size[0]) 25 | else: 26 | self.textOffset = (0, 20) 27 | self.updatePorts() 28 | 29 | def updatePorts(self): 30 | self.ports = {} 31 | self.ports["Feed"] = Port("Feed", self, (0, 0.5), (-1, 0)) 32 | 33 | if self.hasCondenser: 34 | self.ports["Top"] = Port( 35 | "Top", 36 | self, 37 | (2.0, self.size[0] / 2 / self.size[1]), 38 | (1, 0), 39 | intent="out", 40 | ) 41 | else: 42 | self.ports["VOut"] = Port("VOut", self, (0.5, 0), (0, -1), intent="out") 43 | self.ports["RIn"] = Port( 44 | "RIn", self, (1.0, self.size[0] / 2 / self.size[1]), (1, 0) 45 | ) 46 | 47 | if self.hasReboiler: 48 | self.ports["Bottom"] = Port( 49 | "Bottom", 50 | self, 51 | (2.0, 1 + self.size[0] / 2 / self.size[1]), 52 | (1, 0), 53 | intent="out", 54 | ) 55 | else: 56 | self.ports["LOut"] = Port("LOut", self, (0.5, 1), (0, 1), intent="out") 57 | self.ports["VIn"] = Port( 58 | "VIn", self, (1.0, 1 - self.size[0] / 2 / self.size[1]), (1, 0) 59 | ) 60 | 61 | return 62 | 63 | def intersectsPoint(self, point): 64 | minx = self.position[0] 65 | miny = self.position[1] 66 | maxx = self.position[0] + self.size[0] 67 | maxy = self.position[1] + self.size[1] 68 | 69 | if self.hasCondenser: 70 | miny -= self.size[0] 71 | maxx = self.position[0] + self.size[0] + self.size[0] 72 | if self.hasReboiler: 73 | maxy += self.size[0] 74 | maxx = self.position[0] + self.size[0] + self.size[0] 75 | 76 | test_x = point[0] >= minx and point[0] <= maxx 77 | test_y = point[1] >= miny and point[1] <= maxy 78 | return test_x and test_y 79 | 80 | def draw(self, ctx): 81 | 82 | if self.hasCondenser == True: 83 | self._drawCondenser(ctx) 84 | 85 | if self.hasReboiler == True: 86 | self._drawReboiler(ctx) 87 | 88 | self._drawBasicShape(ctx) 89 | 90 | super().draw(ctx) 91 | 92 | return 93 | 94 | def _drawBasicShape(self, ctx): 95 | ctx.chord( 96 | [ 97 | (self.position[0], self.position[1]), 98 | (self.position[0] + self.size[0], self.position[1] + self.size[0]), 99 | ], 100 | 180, 101 | 360, 102 | self.fillColor, 103 | self.lineColor, 104 | self.lineSize, 105 | ) 106 | 107 | ctx.rectangle( 108 | [ 109 | (self.position[0], self.position[1] + self.size[0] / 2), 110 | ( 111 | self.position[0] + self.size[0], 112 | self.position[1] + self.size[1] - self.size[0] / 2, 113 | ), 114 | ], 115 | self.fillColor, 116 | self.lineColor, 117 | self.lineSize, 118 | ) 119 | 120 | ctx.chord( 121 | [ 122 | (self.position[0], self.position[1] + self.size[1] - self.size[0]), 123 | (self.position[0] + self.size[0], self.position[1] + self.size[1]), 124 | ], 125 | 0, 126 | 180, 127 | self.fillColor, 128 | self.lineColor, 129 | self.lineSize, 130 | ) 131 | 132 | def _drawReboiler(self, ctx): 133 | ctx.rectangle( 134 | [ 135 | ( 136 | self.position[0] + self.size[0] / 2, 137 | self.position[1] + self.size[1] - self.size[0] / 2, 138 | ), 139 | ( 140 | self.position[0] + 3 * self.size[0] / 2, 141 | self.position[1] + self.size[1] + self.size[0] / 2, 142 | ), 143 | ], 144 | None, 145 | self.lineColor, 146 | self.lineSize, 147 | ) 148 | ctx.circle( 149 | [ 150 | (self.position[0] + self.size[0], self.position[1] + self.size[1]), 151 | ( 152 | self.position[0] + 2 * self.size[0], 153 | self.position[1] + self.size[1] + self.size[0], 154 | ), 155 | ], 156 | self.fillColor, 157 | self.lineColor, 158 | self.lineSize, 159 | ) 160 | 161 | def _drawCondenser(self, ctx): 162 | ctx.rectangle( 163 | [ 164 | ( 165 | self.position[0] + self.size[0] / 2, 166 | self.position[1] - self.size[0] / 2, 167 | ), 168 | ( 169 | self.position[0] + 3 * self.size[0] / 2, 170 | self.position[1] + self.size[0] / 2, 171 | ), 172 | ], 173 | None, 174 | self.lineColor, 175 | self.lineSize, 176 | ) 177 | ctx.circle( 178 | [ 179 | (self.position[0] + self.size[0], self.position[1] - self.size[0]), 180 | (self.position[0] + 2 * self.size[0], self.position[1]), 181 | ], 182 | self.fillColor, 183 | self.lineColor, 184 | self.lineSize, 185 | ) 186 | ctx.line( 187 | ( 188 | self.position[0] + 3 * self.size[0] / 2, 189 | self.position[1] + self.size[0] / 2, 190 | ), 191 | ( 192 | self.position[0] + 2 * self.size[0], 193 | self.position[1] + self.size[0] / 2, 194 | ), 195 | self.lineColor, 196 | self.lineSize, 197 | ) 198 | -------------------------------------------------------------------------------- /pyflowsheet/core/unitoperation.py: -------------------------------------------------------------------------------- 1 | from .enums import HorizontalLabelAlignment, VerticalLabelAlignment 2 | from math import sin, cos, radians, sqrt 3 | 4 | 5 | class UnitOperation(object): 6 | def __init__( 7 | self, 8 | id: str, 9 | name: str, 10 | position=(0, 0), 11 | size=(20, 20), 12 | description: str = "", 13 | internals=[], 14 | ): 15 | self.id = id 16 | self.name = name 17 | self.description = description 18 | self.lineColor = (0, 0, 0, 255) 19 | self.fillColor = (255, 255, 255, 255) 20 | self.textColor = (0, 0, 0, 255) 21 | self.size = size 22 | self.position = position 23 | self.lineSize = 2 24 | self.drawBoundingBox = False 25 | self.verticalLabelAlignment = VerticalLabelAlignment.Bottom 26 | self.horizontalLabelAlignment = HorizontalLabelAlignment.Center 27 | self.showTitle = True 28 | self.fontFamily = "Arial" 29 | self.fontSize = 12 30 | self.ports = {} 31 | self.isFlippedHorizontal = False 32 | self.isFlippedVertical = False 33 | self.rotation = 0 34 | self.textOffset = (0, 20) 35 | self.internals = [] 36 | if internals is not None: 37 | self.addInternals(internals) 38 | 39 | def addInternals(self, internals): 40 | for internal in internals: 41 | internal.parent = self 42 | self.internals.append(internal) 43 | 44 | def addPort(self, port): 45 | self.ports[port.name] = port 46 | return 47 | 48 | def updatePorts(self): 49 | self.ports = {} 50 | return 51 | 52 | def __getitem__(self, key): 53 | if key in self.ports: 54 | return self.ports[key] 55 | else: 56 | raise KeyError(f"UnitOperation {self.id} does not have a port named {key}") 57 | 58 | def flip(self, axis="horizontal"): 59 | if axis.lower() == "horizontal": 60 | self.flipHorizontal() 61 | if axis.lower() == "vertical": 62 | self.flipVertical() 63 | return 64 | 65 | def flipVertical(self): 66 | self.isFlippedVertical = True 67 | for p in self.ports.values(): 68 | p.relativePosition = ( 69 | (p.relativePosition[0]), 70 | 1 - p.relativePosition[1], 71 | ) 72 | p.normal = (p.normal[0], p.normal[1] * -1) 73 | return 74 | 75 | def flipHorizontal(self): 76 | self.isFlippedHorizontal = True 77 | for p in self.ports.values(): 78 | p.relativePosition = ( 79 | 1 - (p.relativePosition[0]), 80 | p.relativePosition[1], 81 | ) 82 | p.normal = (p.normal[0] * -1, p.normal[1]) 83 | return 84 | 85 | def getNormalLength(self): 86 | a = radians(self.rotation) 87 | return 20 * sin(a) 88 | 89 | def rotate(self, angle): 90 | 91 | deltaAngle = angle - self.rotation 92 | 93 | self.rotation = angle 94 | for p in self.ports.values(): 95 | x = p.relativePosition[0] * self.size[0] 96 | y = p.relativePosition[1] * self.size[1] 97 | nx = p.normal[0] 98 | ny = p.normal[1] 99 | 100 | cx = 0.5 * self.size[0] 101 | cy = 0.5 * self.size[1] 102 | a = radians(deltaAngle) 103 | p.relativePosition = ( 104 | (x * cos(a) - y * sin(a) - cx * cos(a) + cy * sin(a) + cx) 105 | / self.size[0], 106 | (x * sin(a) + y * cos(a) - cx * sin(a) - cy * cos(a) + cy) 107 | / self.size[1], 108 | ) 109 | p.normal = (nx * cos(a) - ny * sin(a), nx * sin(a) + ny * cos(a)) 110 | 111 | def intersectsPoint(self, point): 112 | 113 | cx = self.position[0] + 0.5 * self.size[0] 114 | cy = self.position[1] + 0.5 * self.size[1] 115 | a = radians(-self.rotation) 116 | x, y = point 117 | 118 | rotatedPoint = ( 119 | (x * cos(a) - y * sin(a) - cx * cos(a) + cy * sin(a) + cx), 120 | (x * sin(a) + y * cos(a) - cx * sin(a) - cy * cos(a) + cy), 121 | ) 122 | 123 | test_x = ( 124 | rotatedPoint[0] >= self.position[0] 125 | and rotatedPoint[0] <= self.position[0] + self.size[0] 126 | ) 127 | test_y = ( 128 | rotatedPoint[1] >= self.position[1] 129 | and rotatedPoint[1] <= self.position[1] + self.size[1] 130 | ) 131 | return test_x and test_y 132 | 133 | def draw(self, ctx): 134 | 135 | for i in self.internals: 136 | i.draw(ctx) 137 | 138 | if self.drawBoundingBox: 139 | ctx.rectangle( 140 | [ 141 | (self.position[0], self.position[1]), 142 | (self.position[0] + self.size[0], self.position[1] + self.size[1]), 143 | ], 144 | fillColor=None, 145 | lineColor=(255, 0, 0, 255), 146 | lineSize=1, 147 | ) 148 | 149 | return 150 | 151 | def setTextAnchor(self, horizontal, vertical, offset=None): 152 | 153 | if offset != None: 154 | self.textOffset = offset 155 | self.horizontalLabelAlignment = horizontal 156 | self.verticalLabelAlignment = vertical 157 | return 158 | 159 | def getTextAnchor(self): 160 | 161 | x = 0 162 | y = 0 163 | align = "middle" 164 | if self.horizontalLabelAlignment == HorizontalLabelAlignment.Center: 165 | x = self.position[0] + self.size[0] / 2 166 | if self.horizontalLabelAlignment == HorizontalLabelAlignment.LeftOuter: 167 | x = self.position[0] 168 | align = "end" 169 | if self.horizontalLabelAlignment == HorizontalLabelAlignment.Left: 170 | x = self.position[0] 171 | align = "start" 172 | if self.horizontalLabelAlignment == HorizontalLabelAlignment.RightOuter: 173 | x = self.position[0] + self.size[0] 174 | align = "start" 175 | if self.horizontalLabelAlignment == HorizontalLabelAlignment.Right: 176 | x = self.position[0] + self.size[0] 177 | align = "end" 178 | 179 | if self.verticalLabelAlignment == VerticalLabelAlignment.Center: 180 | y = self.position[1] + self.size[1] / 2 181 | elif self.verticalLabelAlignment == VerticalLabelAlignment.Bottom: 182 | y = self.position[1] + self.size[1] 183 | elif self.verticalLabelAlignment == VerticalLabelAlignment.Top: 184 | y = self.position[1] 185 | 186 | anchor = (x + self.textOffset[0], y + self.textOffset[1]) 187 | 188 | return anchor, align 189 | 190 | def drawTextLayer(self, ctx, showPorts=False): 191 | if showPorts: 192 | for p in self.ports.values(): 193 | p.draw(ctx) 194 | 195 | if self.showTitle: 196 | insert, align = self.getTextAnchor() 197 | ctx.text( 198 | insert, 199 | text=self.id, 200 | fontFamily=self.fontFamily, 201 | textColor=self.textColor, 202 | textAnchor=align, 203 | fontSize=self.fontSize, 204 | ) 205 | return -------------------------------------------------------------------------------- /docs/pyflowsheet/annotations/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pyflowsheet.annotations API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pyflowsheet.annotations

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from .textelement import TextElement
30 |
31 |
32 |
33 |

Sub-modules

34 |
35 |
pyflowsheet.annotations.figure
36 |
37 |
38 |
39 |
pyflowsheet.annotations.table
40 |
41 |
42 |
43 |
pyflowsheet.annotations.textelement
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 76 |
77 | 80 | 81 | -------------------------------------------------------------------------------- /docs/pyflowsheet/backends/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pyflowsheet.backends API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pyflowsheet.backends

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from .svgcontext import SvgContext
30 |
31 |
32 |
33 |

Sub-modules

34 |
35 |
pyflowsheet.backends.bitmapcontext
36 |
37 |
38 |
39 |
pyflowsheet.backends.foreignObject
40 |
41 |
42 |
43 |
pyflowsheet.backends.svgcontext
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 76 |
77 | 80 | 81 | -------------------------------------------------------------------------------- /examples/fermenter.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": { 3 | "language_info": { 4 | "codemirror_mode": { 5 | "name": "ipython", 6 | "version": 3 7 | }, 8 | "file_extension": ".py", 9 | "mimetype": "text/x-python", 10 | "name": "python", 11 | "nbconvert_exporter": "python", 12 | "pygments_lexer": "ipython3", 13 | "version": "3.7.6-final" 14 | }, 15 | "orig_nbformat": 2, 16 | "kernelspec": { 17 | "name": "python3", 18 | "display_name": "Python 3", 19 | "language": "python" 20 | } 21 | }, 22 | "nbformat": 4, 23 | "nbformat_minor": 2, 24 | "cells": [ 25 | { 26 | "cell_type": "code", 27 | "execution_count": 1, 28 | "metadata": {}, 29 | "outputs": [ 30 | { 31 | "output_type": "execute_result", 32 | "data": { 33 | "text/plain": [ 34 | "" 35 | ], 36 | "image/svg+xml": "S1S2S3S4S5AirSubstrateFermenterOff-GasProductValve" 37 | }, 38 | "metadata": {}, 39 | "execution_count": 1 40 | } 41 | ], 42 | "source": [ 43 | "from pyflowsheet import Flowsheet, UnitOperation, Distillation, Vessel, BlackBox, Pump, Stream, StreamFlag, Valve,HeatExchanger,Mixer, Splitter, Port, SvgContext\n", 44 | "from pyflowsheet import VerticalLabelAlignment, HorizontalLabelAlignment\n", 45 | "from pyflowsheet.internals import Tubes, Stirrer, StirrerType, Jacket\n", 46 | "from IPython.core.display import SVG, HTML\n", 47 | "\n", 48 | "pfd= Flowsheet(\"Demo\",\"Simple Distillation\", \"Demo Flowsheet for showing different styling options\")\n", 49 | "\n", 50 | "U1=pfd.unit(StreamFlag(\"Air\",\"Off-Page Connector\", position=(00,400)))\n", 51 | "U2=pfd.unit(StreamFlag(\"Substrate\",\"Off-Page Connector\", position=(00,240)))\n", 52 | "U3=pfd.unit(Vessel(\"Fermenter\",\"Fermenter\", position=(200,190), capLength=20, showCapLines=False, size=(80,140),internals=[Stirrer(type=StirrerType.Anchor), Jacket()] ))\n", 53 | "U4=pfd.unit(StreamFlag(\"Off-Gas\",\"Off-Page Connector\", position=(240,000)))\n", 54 | "U5=pfd.unit(StreamFlag(\"Product\",\"Off-Page Connector\", position=(400,240)))\n", 55 | "U6=pfd.unit(Valve(\"Valve\",\"Relief Valve\", position=(240,120)))\n", 56 | "\n", 57 | "U4.rotate(-90)\n", 58 | "U6.rotate(-90)\n", 59 | "U3.ports[\"In2\"] = Port(\"In2\", U3, (0, 0.5), (-1, 0))\n", 60 | "U3.ports[\"Out2\"] = Port(\"Out2\", U3, (1, 0.5), (1, 0))\n", 61 | "U3.ports[\"Out\"].relativePosition=(60/80,0.02)\n", 62 | "\n", 63 | "pfd.connect(\"S1\", U1[\"Out\"],U3[\"In\"] )\n", 64 | "pfd.connect(\"S2\", U2[\"Out\"],U3[\"In2\"] )\n", 65 | "pfd.connect(\"S3\", U3[\"Out2\"],U5[\"In\"] )\n", 66 | "pfd.connect(\"S4\", U3[\"Out\"],U6[\"In\"] )\n", 67 | "pfd.connect(\"S5\", U6[\"Out\"],U4[\"In\"] )\n", 68 | "\n", 69 | "#pfd.showGrid=True\n", 70 | "ctx= SvgContext(\"../img/fermenter_example.svg\")\n", 71 | "img = pfd.draw(ctx)\n", 72 | "SVG(img.render(scale=1.5))\n" 73 | ] 74 | }, 75 | { 76 | "cell_type": "code", 77 | "execution_count": null, 78 | "metadata": {}, 79 | "outputs": [], 80 | "source": [] 81 | } 82 | ] 83 | } -------------------------------------------------------------------------------- /pyflowsheet/core/flowsheet.py: -------------------------------------------------------------------------------- 1 | from .stream import Stream 2 | from ..annotations import TextElement 3 | from pathfinding.core.grid import Grid 4 | 5 | 6 | class Flowsheet(object): 7 | def __init__(self, id: str, name: str, description: str = ""): 8 | """Generates a new Flowsheet Object. The Flowsheet object represent a Process Flow Diagram (PFD). A Flowsheet 9 | is made up of unit operations, streams and annotations. 10 | 11 | Args: 12 | id (str): Short identifier of the flowsheet 13 | name (str): A human readable, longer name 14 | description (str, optional): A text that describes the process task of the flowsheet. Defaults to "". 15 | """ 16 | self.id = id 17 | self.name = name 18 | self.description = description 19 | self.lineColor = (64, 64, 64, 255) 20 | self.fillColor = (255, 255, 255, 255) 21 | self.textColor = (0, 0, 0, 255) 22 | self.size = (512, 512) 23 | self.position = (0, 0) 24 | self.lineSize = 2 25 | self.unitOperations = {} 26 | self.annotations = [] 27 | self.streams = {} 28 | self.showGrid = False 29 | self.showPorts = False 30 | 31 | def addAnnotations(self, elements): 32 | for e in elements: 33 | self.annotations.append(e) 34 | return 35 | 36 | def addUnits(self, units): 37 | """Add a list of units to the flowsheet in one go. 38 | 39 | Args: 40 | units (List[UnitOperation]): A list of UnitOperation objects 41 | """ 42 | for u in units: 43 | self.unitOperations[u.id] = u 44 | return 45 | 46 | def unit(self, unitoperation): 47 | """Add a new unit operation to the flowsheet and return a reference 48 | 49 | Raises: 50 | ValueError: If None is passed this function will raise a ValueError. 51 | If the id of the UnitOperation is already present in the flowsheet a value error will be raised. 52 | 53 | Returns: 54 | UnitOperation: The UnitOperation object passed into the function as an argument. 55 | """ 56 | if unitoperation is None: 57 | raise ValueError( 58 | "unitoperation must be an object derived from the UnitOperation class!" 59 | ) 60 | if unitoperation.id in self.unitOperations: 61 | raise ValueError( 62 | "The id of unitoperation is already used within the flowsheet. Please provide a unique id. If you want to override a specific unit operation, access it directly with the flowsheet.unitoperations[] accessor." 63 | ) 64 | self.unitOperations[unitoperation.id] = unitoperation 65 | return unitoperation 66 | 67 | def connect(self, name, fromPort, toPort): 68 | """Connect two ports of two unit operations with a stream. 69 | 70 | Args: 71 | name (string): The identifier/name of the stream. 72 | fromPort (Port): The source port from which to route the stream 73 | toPort (Port): The destination port to which to route the stream 74 | """ 75 | if name in self.streams: 76 | raise ValueError( 77 | "The id of the stream is already used within the flowsheet. Please provide a unique id. If you want to override a specific stream, access it directly with the flowsheet.streams[] accessor." 78 | ) 79 | 80 | self.streams[name] = Stream(name, fromPort, toPort) 81 | return 82 | 83 | def _calcGrid(self): 84 | """Private helper function to rasterize the canvas and generate a course grid for pathfinding. This functions scans 85 | the entire canvas area and tests if a unit intersects the grid point. If any unit does so, the point is marked as 86 | "impassable" for the pathfinding algorithm. 87 | 88 | Returns: 89 | [2d-list]: The reachability matrix of the canvas area 90 | [int] : The minimum x coordinate of the canvas (upper-left) 91 | [int] : The minimum y coordinate of the canvas (upper-left) 92 | """ 93 | minx = min([u.position[0] for u in self.unitOperations.values()]) 94 | maxx = max([u.position[0] + u.size[0] for u in self.unitOperations.values()]) 95 | miny = min([u.position[1] for u in self.unitOperations.values()]) 96 | maxy = max([u.position[1] + u.size[1] for u in self.unitOperations.values()]) 97 | 98 | gridsize = 10 99 | 100 | minx = int(minx / gridsize - 8) * gridsize 101 | miny = int(miny / gridsize - 8) * gridsize 102 | maxx = int(maxx / gridsize + 8) * gridsize 103 | maxy = int(maxy / gridsize + 8) * gridsize 104 | 105 | grid = [] 106 | 107 | for y in range(miny, maxy, gridsize): 108 | row = [] 109 | for x in range(minx, maxx, gridsize): 110 | intersectionFound = False 111 | 112 | for u in self.unitOperations.values(): 113 | if u.intersectsPoint((x, y)): 114 | intersectionFound = True 115 | 116 | if intersectionFound: 117 | row.append(0) 118 | else: 119 | row.append(1) 120 | grid.append(row) 121 | 122 | return grid, minx, miny 123 | 124 | def _drawGrid(self, grid, ctx, minx, miny): 125 | ctx.startGroup("RoutingGrid") 126 | 127 | for y in range(grid.height): 128 | for x in range(grid.width): 129 | sx = minx + x * 10 130 | sy = miny + y * 10 131 | if not grid.node(x, y).walkable: 132 | ctx.circle( 133 | [(sx - 5, sy - 5), (sx + 5, sy + 5)], 134 | (0, 0, 0, 255), 135 | (0, 0, 0, 255), 136 | 1, 137 | ) 138 | else: 139 | w = 255 - 10 * grid.node(x, y).weight 140 | w = max(w, 0) 141 | ctx.circle( 142 | [(sx - 5, sy - 5), (sx + 5, sy + 5)], 143 | (w, w, w, 255), 144 | (0, 0, 0, 255), 145 | 1, 146 | ) 147 | 148 | ctx.endGroup() 149 | return 150 | 151 | def callout(self, text, position): 152 | text = TextElement(text, position) 153 | self.annotations.append(text) 154 | return 155 | 156 | def draw(self, ctx): 157 | """Draws the process flow diagram with the help of the context passed as an argument. 158 | This function has 3 stages. In the first stage, the reachability map of the diagram is calculated, which is used in 159 | the second stage to route the streams using Dykstra's algorithm. In the third stage, the unit operations are drawn. 160 | 161 | The unit operation draw loop has two stages. In the first stage the icon is drawn with transformations applied. 162 | In the second stage the text layer is drawn without any transformations (i.e. rotation) applied. 163 | 164 | Args: 165 | ctx ([type]): A drawing context that provides an abstraction for the primitive drawing functions. 166 | 167 | Returns: 168 | [type]: The same context as was passed in 169 | """ 170 | 171 | matrix, minx, miny = self._calcGrid() 172 | grid = Grid(matrix=matrix) 173 | 174 | for s in self.streams.values(): 175 | ctx.startGroup(s.id) 176 | s.draw(ctx, grid, minx, miny) 177 | ctx.endGroup() 178 | 179 | if self.showGrid: 180 | self._drawGrid(grid, ctx, minx, miny) 181 | 182 | # print(grid.grid_str(show_weight=True)) 183 | for u in self.unitOperations.values(): 184 | ctx.startGroup(u.id) 185 | ctx.startTransformedGroup(u) 186 | u.draw(ctx) 187 | ctx.endGroup() 188 | u.drawTextLayer(ctx, self.showPorts) 189 | ctx.endGroup() 190 | 191 | for e in self.annotations: 192 | ctx.startGroup(e.id) 193 | e.draw(ctx) 194 | e.drawTextLayer(ctx) 195 | ctx.endGroup() 196 | 197 | return ctx -------------------------------------------------------------------------------- /img/small_process.svg: -------------------------------------------------------------------------------- 1 | 2 | S01S02S03S07S04S05S06S08F100DS10-PA10DS10-WA10DS10-VA10DS10-KA10DS10-BA11P1P2DS10-PA11 -------------------------------------------------------------------------------- /docs/pyflowsheet/core/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pyflowsheet.core API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pyflowsheet.core

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from .unitoperation import UnitOperation
 30 | from .flowsheet import Flowsheet
 31 | from .pathfinder import Pathfinder
 32 | from .port import Port
 33 | from .stream import Stream
34 |
35 |
36 |
37 |

Sub-modules

38 |
39 |
pyflowsheet.core.enums
40 |
41 |
42 |
43 |
pyflowsheet.core.flowsheet
44 |
45 |
46 |
47 |
pyflowsheet.core.pathfinder
48 |
49 |
50 |
51 |
pyflowsheet.core.port
52 |
53 |
54 |
55 |
pyflowsheet.core.stream
56 |
57 |
58 |
59 |
pyflowsheet.core.unitoperation
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | 95 |
96 | 99 | 100 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pyflowsheet API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Package pyflowsheet

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from .core import Flowsheet
 30 | from .core import UnitOperation
 31 | from .core import Stream
 32 | from .core import Port
 33 | 
 34 | from .core.enums import VerticalLabelAlignment, HorizontalLabelAlignment
 35 | 
 36 | 
 37 | from .unitoperations import Distillation
 38 | from .unitoperations import Vessel
 39 | from .unitoperations import BlackBox
 40 | from .unitoperations import Pump
 41 | from .unitoperations import Valve
 42 | from .unitoperations import StreamFlag
 43 | from .unitoperations import HeatExchanger
 44 | from .unitoperations import Mixer
 45 | from .unitoperations import Splitter
 46 | from .unitoperations import Compressor
 47 | 
 48 | from .annotations import TextElement
 49 | from .backends import SvgContext
50 |
51 |
52 |
53 |

Sub-modules

54 |
55 |
pyflowsheet.annotations
56 |
57 |
58 |
59 |
pyflowsheet.backends
60 |
61 |
62 |
63 |
pyflowsheet.core
64 |
65 |
66 |
67 |
pyflowsheet.internals
68 |
69 |
70 |
71 |
pyflowsheet.unitoperations
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | 101 |
102 | 105 | 106 | -------------------------------------------------------------------------------- /docs/pyflowsheet/internals/baseinternal.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pyflowsheet.internals.baseinternal API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pyflowsheet.internals.baseinternal

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
class BaseInternal(object):
 30 |     def __init__(self, parent):
 31 |         self.parent = parent
 32 |         return
 33 | 
 34 |     def draw(self, ctx):
 35 | 
 36 |         return
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |

Classes

47 |
48 |
49 | class BaseInternal 50 | (parent) 51 |
52 |
53 |
54 |
55 | 56 | Expand source code 57 | 58 |
class BaseInternal(object):
 59 |     def __init__(self, parent):
 60 |         self.parent = parent
 61 |         return
 62 | 
 63 |     def draw(self, ctx):
 64 | 
 65 |         return
66 |
67 |

Subclasses

68 | 81 |

Methods

82 |
83 |
84 | def draw(self, ctx) 85 |
86 |
87 |
88 |
89 | 90 | Expand source code 91 | 92 |
def draw(self, ctx):
 93 | 
 94 |     return
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 | 125 |
126 | 129 | 130 | -------------------------------------------------------------------------------- /docs/pyflowsheet/internals/dividingWall.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pyflowsheet.internals.dividingWall API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pyflowsheet.internals.dividingWall

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from .baseinternal import BaseInternal
 30 | 
 31 | 
 32 | class DividingWall(BaseInternal):
 33 |     def __init__(self):
 34 |         return
 35 | 
 36 |     def draw(self, ctx):
 37 |         if self.parent is None:
 38 |             Warning("Internal has no parent set!")
 39 |             return
 40 | 
 41 |         unit = self.parent
 42 | 
 43 |         ctx.line(
 44 |             (unit.position[0] + unit.size[0] / 2, unit.position[1] + unit.size[0]),
 45 |             (
 46 |                 unit.position[0] + unit.size[0] / 2,
 47 |                 unit.position[1] + unit.size[1] - unit.size[0],
 48 |             ),
 49 |             unit.lineColor,
 50 |             unit.lineSize,
 51 |         )
 52 | 
 53 |         return
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |

Classes

64 |
65 |
66 | class DividingWall 67 |
68 |
69 |
70 |
71 | 72 | Expand source code 73 | 74 |
class DividingWall(BaseInternal):
 75 |     def __init__(self):
 76 |         return
 77 | 
 78 |     def draw(self, ctx):
 79 |         if self.parent is None:
 80 |             Warning("Internal has no parent set!")
 81 |             return
 82 | 
 83 |         unit = self.parent
 84 | 
 85 |         ctx.line(
 86 |             (unit.position[0] + unit.size[0] / 2, unit.position[1] + unit.size[0]),
 87 |             (
 88 |                 unit.position[0] + unit.size[0] / 2,
 89 |                 unit.position[1] + unit.size[1] - unit.size[0],
 90 |             ),
 91 |             unit.lineColor,
 92 |             unit.lineSize,
 93 |         )
 94 | 
 95 |         return
96 |
97 |

Ancestors

98 | 101 |

Methods

102 |
103 |
104 | def draw(self, ctx) 105 |
106 |
107 |
108 |
109 | 110 | Expand source code 111 | 112 |
def draw(self, ctx):
113 |     if self.parent is None:
114 |         Warning("Internal has no parent set!")
115 |         return
116 | 
117 |     unit = self.parent
118 | 
119 |     ctx.line(
120 |         (unit.position[0] + unit.size[0] / 2, unit.position[1] + unit.size[0]),
121 |         (
122 |             unit.position[0] + unit.size[0] / 2,
123 |             unit.position[1] + unit.size[1] - unit.size[0],
124 |         ),
125 |         unit.lineColor,
126 |         unit.lineSize,
127 |     )
128 | 
129 |     return
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | 160 |
161 | 164 | 165 | -------------------------------------------------------------------------------- /img/externalized_column_with_preheater.svg: -------------------------------------------------------------------------------- 1 | 2 | S01S02S04S05S06S08S09S10S11S12S13S14S15S07FeedPreheaterTowerMX1SP1ReboilerCondenserSP2P1P2P3 -------------------------------------------------------------------------------- /docs/textelement.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pyflowsheet.textelement API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pyflowsheet.textelement

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from .unitoperation import UnitOperation
 30 | from .enums import HorizontalLabelAlignment, VerticalLabelAlignment
 31 | 
 32 | 
 33 | class TextElement(UnitOperation):
 34 |     def __init__(self, text, position=(0, 0)):
 35 |         super().__init__("text", "text", position=position, size=(20, 20))
 36 |         self.text = text
 37 |         self.drawBoundingBox = False
 38 |         self.showTitle = False
 39 |         self.fontFamily = "Consolas"
 40 |         return
 41 | 
 42 |     def draw(self, ctx):
 43 |         # html = self.data.to_html()
 44 |         # ctx.html(html, self.position, self.size)
 45 | 
 46 |         ctx.text(
 47 |             self.position,
 48 |             text=self.text,
 49 |             fontFamily=self.fontFamily,
 50 |             textColor=self.textColor,
 51 |             textAnchor="start",
 52 |         )
 53 |         # super().draw(ctx)
 54 | 
 55 |         return
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |

Classes

66 |
67 |
68 | class TextElement 69 | (text, position=(0, 0)) 70 |
71 |
72 |
73 |
74 | 75 | Expand source code 76 | 77 |
class TextElement(UnitOperation):
 78 |     def __init__(self, text, position=(0, 0)):
 79 |         super().__init__("text", "text", position=position, size=(20, 20))
 80 |         self.text = text
 81 |         self.drawBoundingBox = False
 82 |         self.showTitle = False
 83 |         self.fontFamily = "Consolas"
 84 |         return
 85 | 
 86 |     def draw(self, ctx):
 87 |         # html = self.data.to_html()
 88 |         # ctx.html(html, self.position, self.size)
 89 | 
 90 |         ctx.text(
 91 |             self.position,
 92 |             text=self.text,
 93 |             fontFamily=self.fontFamily,
 94 |             textColor=self.textColor,
 95 |             textAnchor="start",
 96 |         )
 97 |         # super().draw(ctx)
 98 | 
 99 |         return
100 |
101 |

Ancestors

102 | 105 |

Methods

106 |
107 |
108 | def draw(self, ctx) 109 |
110 |
111 |
112 |
113 | 114 | Expand source code 115 | 116 |
def draw(self, ctx):
117 |     # html = self.data.to_html()
118 |     # ctx.html(html, self.position, self.size)
119 | 
120 |     ctx.text(
121 |         self.position,
122 |         text=self.text,
123 |         fontFamily=self.fontFamily,
124 |         textColor=self.textColor,
125 |         textAnchor="start",
126 |     )
127 |     # super().draw(ctx)
128 | 
129 |     return
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | 160 |
161 | 164 | 165 | -------------------------------------------------------------------------------- /docs/pyflowsheet/annotations/textelement.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pyflowsheet.annotations.textelement API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pyflowsheet.annotations.textelement

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from ..core import UnitOperation
 30 | from ..core.enums import HorizontalLabelAlignment, VerticalLabelAlignment
 31 | 
 32 | 
 33 | class TextElement(UnitOperation):
 34 |     def __init__(self, text, position=(0, 0)):
 35 |         super().__init__("text", "text", position=position, size=(20, 20))
 36 |         self.text = text
 37 |         self.drawBoundingBox = False
 38 |         self.showTitle = False
 39 |         self.fontFamily = "Consolas"
 40 |         return
 41 | 
 42 |     def draw(self, ctx):
 43 |         # html = self.data.to_html()
 44 |         # ctx.html(html, self.position, self.size)
 45 | 
 46 |         ctx.text(
 47 |             self.position,
 48 |             text=self.text,
 49 |             fontFamily=self.fontFamily,
 50 |             textColor=self.textColor,
 51 |             textAnchor="start",
 52 |         )
 53 |         # super().draw(ctx)
 54 | 
 55 |         return
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |

Classes

66 |
67 |
68 | class TextElement 69 | (text, position=(0, 0)) 70 |
71 |
72 |
73 |
74 | 75 | Expand source code 76 | 77 |
class TextElement(UnitOperation):
 78 |     def __init__(self, text, position=(0, 0)):
 79 |         super().__init__("text", "text", position=position, size=(20, 20))
 80 |         self.text = text
 81 |         self.drawBoundingBox = False
 82 |         self.showTitle = False
 83 |         self.fontFamily = "Consolas"
 84 |         return
 85 | 
 86 |     def draw(self, ctx):
 87 |         # html = self.data.to_html()
 88 |         # ctx.html(html, self.position, self.size)
 89 | 
 90 |         ctx.text(
 91 |             self.position,
 92 |             text=self.text,
 93 |             fontFamily=self.fontFamily,
 94 |             textColor=self.textColor,
 95 |             textAnchor="start",
 96 |         )
 97 |         # super().draw(ctx)
 98 | 
 99 |         return
100 |
101 |

Ancestors

102 | 105 |

Methods

106 |
107 |
108 | def draw(self, ctx) 109 |
110 |
111 |
112 |
113 | 114 | Expand source code 115 | 116 |
def draw(self, ctx):
117 |     # html = self.data.to_html()
118 |     # ctx.html(html, self.position, self.size)
119 | 
120 |     ctx.text(
121 |         self.position,
122 |         text=self.text,
123 |         fontFamily=self.fontFamily,
124 |         textColor=self.textColor,
125 |         textAnchor="start",
126 |     )
127 |     # super().draw(ctx)
128 | 
129 |     return
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 | 160 |
161 | 164 | 165 | -------------------------------------------------------------------------------- /docs/pyflowsheet/unitoperations/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | pyflowsheet.unitoperations API documentation 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 |

Module pyflowsheet.unitoperations

23 |
24 |
25 |
26 | 27 | Expand source code 28 | 29 |
from .blackbox import BlackBox
 30 | from .distillation import Distillation
 31 | from .heatexchanger import HeatExchanger
 32 | from .mixer import Mixer
 33 | from .splitter import Splitter
 34 | from .pump import Pump
 35 | from .vessel import Vessel
 36 | from .valve import Valve
 37 | from .streamflag import StreamFlag
 38 | from .compressor import Compressor
39 |
40 |
41 |
42 |

Sub-modules

43 |
44 |
pyflowsheet.unitoperations.blackbox
45 |
46 |
47 |
48 |
pyflowsheet.unitoperations.compressor
49 |
50 |
51 |
52 |
pyflowsheet.unitoperations.distillation
53 |
54 |
55 |
56 |
pyflowsheet.unitoperations.heatexchanger
57 |
58 |
59 |
60 |
pyflowsheet.unitoperations.mixer
61 |
62 |
63 |
64 |
pyflowsheet.unitoperations.pump
65 |
66 |
67 |
68 |
pyflowsheet.unitoperations.splitter
69 |
70 |
71 |
72 |
pyflowsheet.unitoperations.streamflag
73 |
74 |
75 |
76 |
pyflowsheet.unitoperations.valve
77 |
78 |
79 |
80 |
pyflowsheet.unitoperations.vessel
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | 120 |
121 | 124 | 125 | --------------------------------------------------------------------------------