├── 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 | S1 S2 U1 U2 U3
--------------------------------------------------------------------------------
/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 | S1 S2 S3 S4 S5 Air Substrate Fermenter Off-Gas Product Valve
--------------------------------------------------------------------------------
/img/simple_column.svg:
--------------------------------------------------------------------------------
1 |
2 | S01 S02 S03 S04 S05 F100 DS10-WA10 DS10-KA10 P1 P2
--------------------------------------------------------------------------------
/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 | S01 S02 S03 S04 S05 S06 S07 F100 F200 MX10 DS10-WA10 SP10 P2 P1 P3
--------------------------------------------------------------------------------
/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 | S01 S02 S03 S04 S05 S06 S07 S08 S09 S10 Feed Product Waste SideProduct Pretreatment Reaction Gas-Recovery Separation Purification
--------------------------------------------------------------------------------
/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 |
49 |
51 |
53 |
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 |
49 |
51 |
53 |
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": "S1 S2 S3 S4 S5 Air Substrate Fermenter Off-Gas Product Valve "
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 | S01 S02 S03 S07 S04 S05 S06 S08 F100 DS10-PA10 DS10-WA10 DS10-VA10 DS10-KA10 DS10-BA11 P1 P2 DS10-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 |
65 |
67 |
69 |
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 |
77 |
79 |
81 |
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 |
41 |
43 |
45 |
46 |
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 |
58 |
60 |
62 |
63 |
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 | S01 S02 S04 S05 S06 S08 S09 S10 S11 S12 S13 S14 S15 S07 Feed Preheater Tower MX1 SP1 Reboiler Condenser SP2 P1 P2 P3
--------------------------------------------------------------------------------
/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 |
60 |
62 |
64 |
65 |
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 |
60 |
62 |
64 |
65 |
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 |
86 |
88 |
90 |
92 |
93 |
120 |
121 |
124 |
125 |
--------------------------------------------------------------------------------