Coverage for backend/django/flowsheetInternals/unitops/models/Port.py: 100%
73 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2026-07-22 05:22 +0000
1from django.db import models
2from core.auxiliary.enums import ConType, AnchorSide
3from typing import TYPE_CHECKING
5from core.managers import AccessControlManager, AllFlowsheetStatesManager
6from core.auxiliary.models.PropertyValue import PropertyValue
7from core.auxiliary.models.IndexedItem import IndexedItem
8from core.auxiliary.models.FlowsheetHistoryModel import FlowsheetHistoryModel
9from flowsheetInternals.graphicData.models.graphicObjectModel import GraphicObject
10from flowsheetInternals.unitops.config import get_object_schema
12if TYPE_CHECKING:
13 from core.auxiliary.models.Flowsheet import Flowsheet
14 from flowsheetInternals.unitops.models.SimulationObject import SimulationObject
15 from flowsheetInternals.graphicData.models.graphicObjectModel import GraphicObject
18class Port(FlowsheetHistoryModel, models.Model):
19 flowsheet_state = models.ForeignKey(
20 "core_auxiliary.FlowsheetState", on_delete=models.CASCADE, related_name="Ports"
21 )
22 displayName = models.CharField(max_length=64)
23 direction = models.CharField(choices=ConType.choices)
24 key = models.CharField(max_length=64) # key to identify this port in the schema
25 index = models.IntegerField(default=0) # index of the port
26 anchor_side = models.CharField(
27 choices=AnchorSide.choices, max_length=16, null=True, blank=True
28 )
29 anchor_index = models.IntegerField(null=True, blank=True)
30 unitOp = models.ForeignKey(
31 "SimulationObject", on_delete=models.CASCADE, related_name="ports", null=True
32 )
33 stream = models.ForeignKey(
34 "SimulationObject",
35 on_delete=models.SET_NULL,
36 null=True,
37 related_name="connectedPorts",
38 )
40 created_at = models.DateTimeField(auto_now_add=True)
41 objects = AccessControlManager()
42 all_states = AllFlowsheetStatesManager()
44 # runtime-accessed relations
45 flowsheet: "Flowsheet"
46 unitOp: "SimulationObject | None"
47 stream: "SimulationObject | None"
49 class Meta:
50 ordering = ["index", "created_at"]
52 def default_stream_position(
53 self,
54 unitop: "flowsheetInternals.unitops.SimulationObject",
55 unitop_graphic_object: GraphicObject,
56 port_index: int,
57 num_ports: int,
58 ) -> dict[str, float]:
59 """Calculate the default position for a stream connected to this port"""
60 stream_offset = get_object_schema(unitop).ports[self.key].streamOffset
61 rotation = unitop_graphic_object.rotation
62 flipped = unitop_graphic_object.flipped
64 if self.direction == ConType.Inlet:
65 xOffset = -stream_offset
66 else:
67 xOffset = 1 + stream_offset
69 # Calculate y offset based on port index and total number of ports
70 yOffset = (port_index + 0.5) / num_ports
72 # Re-orient the offsets based on the rotation and flip of the unit operation.
73 transforms = {
74 (0, False): lambda x, y: (x, y),
75 (0, True): lambda x, y: (1 - x, y),
76 (90, False): lambda x, y: (1 - y, x),
77 (90, True): lambda x, y: (1 - y, 1 - x),
78 (180, False): lambda x, y: (1 - x, 1 - y),
79 (180, True): lambda x, y: (1 + x, 1 - y),
80 (270, False): lambda x, y: (y, 1 - x),
81 (270, True): lambda x, y: (y, x),
82 }
83 xOffset, yOffset = transforms[(rotation, bool(flipped))](xOffset, yOffset)
85 return {
86 "x": unitop_graphic_object.x + xOffset * unitop_graphic_object.width,
87 "y": unitop_graphic_object.y + yOffset * unitop_graphic_object.height,
88 }
90 def default_stream_name(
91 self, unit_op: "flowsheetInternals.unitops.SimulationObject"
92 ) -> str:
93 return get_object_schema(unit_op).ports[self.key].streamName
95 def reindex_port_on_delete(self):
96 subsequent_ports = Port.objects.filter(
97 unitOp=self.unitOp, key=self.key, index__gt=self.index
98 ).order_by("index")
100 unit_op_schema = get_object_schema(self.unitOp)
101 display_name = unit_op_schema.ports[self.key].displayName
102 index_before_update = self.index
103 for p in subsequent_ports:
104 p.index -= 1
105 p.displayName = f"{display_name} {p.index + 1}"
106 p.save()
108 property_set = self.unitOp.properties
109 property_infos = property_set.containedProperties.filter(
110 index__gte=self.index
111 ).order_by("index")
113 unit = self.unitOp
115 value = False
116 if self.direction == ConType.Outlet:
117 split_property = unit.properties.ContainedProperties.filter(
118 key__in=("split_fraction", "priorities", "split_flow")
119 ).first()
121 # Get the index of the outlet to be deleted.
122 indexed_item = IndexedItem.objects.filter(
123 owner=unit, key="outlet_" + f"{index_before_update + 1}"
124 ).first()
125 # Delete the outlet and its property values
126 if split_property is not None and indexed_item is not None:
127 PropertyValue.objects.filter(
128 property=split_property,
129 indexedItems=indexed_item,
130 ).delete()
131 if indexed_item is not None:
132 indexed_item.delete()
134 # Re-index the display names of the input fields for the remaining outlets.
135 indexed_items = IndexedItem.objects.filter(
136 owner=unit, type="splitter_fraction"
137 )
138 # For the remaining outlets,
139 num_outlets = indexed_items.count()
140 for i in range(indexed_items.count()):
141 # Update the index
142 indexed_item = indexed_items[i]
143 indexed_item.key = "outlet_" + f"{i + 1}"
144 # Use the updated index to change the name displayed on the frontend
145 indexed_item.displayName = (
146 unit.schema.splitter_fraction_name + f" {i + 1}"
147 )
148 indexed_item.save()
150 self.delete()
151 unit.update_height()
152 unit.refresh_from_db()
153 unit.reevaluate_properties_enabled()
154 else:
155 self.delete()
156 unit.update_height()