-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathsimulator.py
278 lines (234 loc) · 8.78 KB
/
simulator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
from dataclasses import dataclass
from threading import Thread
import time
import mixbox
# constants
TANK_VOLUME = 100 # liters
TANK_OUTFLOW = 2 # liter / s
BASIN_VOLUME = 500 # liters
BASIN_OUTFLOW = 5 # liter / s
@dataclass
class PaintMixture:
"""
Represents a paint mixture consisting of several basic colors
"""
cyan: int = 0
magenta: int = 0
yellow: int = 0
black: int = 0
white: int = 0
@property
def volume(self):
"""
get the volume of the paint mixture
"""
return self.cyan + self.magenta + self.yellow + self.black + self.white
def __add__(self, b):
"""
add the volume of two paint mixtures
:param b: other instance
:return: PaintMixture instance that represents the sum of self + b
"""
return PaintMixture(self.cyan + b.cyan, self.magenta + b.magenta, self.yellow + b.yellow, self.black + b.black,
self.white + b.white)
def __sub__(self, b):
"""
subtract another volume from this paint mixture
:param b: other instance
:return: PaintMixture instance that represents the self - b
"""
return PaintMixture(self.cyan - b.cyan, self.magenta - b.magenta, self.yellow - b.yellow, self.black - b.black,
self.white - b.white)
def __mul__(self, b):
"""
multiply the volume of this paint mixture by a factor
:param b: multiplication factor
:return: PaintMixture instance that represents self*b
"""
return PaintMixture(self.cyan * b, self.magenta * b, self.yellow * b, self.black * b,
self.white * b)
def CMYKToRGB(c, m, y, k):
"""
convert from RGB to CMYK colors
"""
r = (255 * (1 - c) * (1 - k))
g = (255 * (1 - m) * (1 - k))
b = (255 * (1 - y) * (1 - k))
return r, g, b
# RGB colors
CYAN_RGB = CMYKToRGB(1, 0, 0, 0)
MAGENTA_RGB = CMYKToRGB(0, 1, 0, 0)
YELLOW_RGB = CMYKToRGB(0, 0, 1, 0)
BLACK_RGB = (0, 0, 0)
WHITE_RGB = (255, 255, 255)
# mixbox colors
CYAN = mixbox.rgb_to_latent(CYAN_RGB)
MAGENTA = mixbox.rgb_to_latent(MAGENTA_RGB)
YELLOW = mixbox.rgb_to_latent(YELLOW_RGB)
BLACK = mixbox.rgb_to_latent(BLACK_RGB)
WHITE = mixbox.rgb_to_latent(WHITE_RGB)
class PaintTank:
"""
Class represents a paint tank
"""
def __init__(self, name, volume, outflow_rate, paint: PaintMixture, connected_to=None):
"""
Initializes the paint tank with the give parameters
:param name: given human-friendly name of the tank, e.g. "cyan"
:param volume: total volume of the tank
:param outflow_rate: maximum outgoing flow rate when the valve is fully open
:param paint: initial paint mixture in the tank
:param level: initial fill level
"""
self.name = name
self.tank_volume = volume
self.outflow_rate = outflow_rate
self.initial_paint = paint
self.connected_to = connected_to
self.paint = self.initial_paint
self.valve_ratio = 0 # valve closed
self.outflow = 0
def add(self, inflow):
"""
Add paint to the tank
:param inflow: paint to add
"""
self.paint += inflow
def fill(self, level=1.0):
"""
fill up the tank based on the specified initial paint mixture
"""
self.paint = self.initial_paint * (level * self.tank_volume / self.initial_paint.volume)
def flush(self):
"""
flush the tank
"""
self.paint = PaintMixture()
def get_level(self):
"""
get the current level of the tank measured from the bottom
range: 0.0 (empty) - 1.0 (full)
"""
return self.paint.volume / self.tank_volume
def get_valve(self):
"""
get the current valve setting:
range: 0.0 (fully closed) - 1.0 (fully opened)
"""
return self.valve_ratio
def set_valve(self, ratio):
"""
set the valve, enforces values between 0 and 1
"""
self.valve_ratio = min(1, max(0, ratio))
def get_outflow(self):
"""
get volume of the paint mixture flowing out of the tank
"""
return self.outflow
def get_color_rgb(self):
"""
get the color of the paint mixture in hex format #rrggbb
"""
volume = self.paint.volume
if volume == 0:
return "#000000"
# https://github.com/scrtwpns/mixbox/blob/master/python/mixbox.py
z_mix = [0] * mixbox.LATENT_SIZE
for i in range(len(z_mix)):
z_mix[i] = (self.paint.cyan / volume * CYAN[i] +
self.paint.magenta / volume * MAGENTA[i] +
self.paint.yellow / volume * YELLOW[i] +
self.paint.black / volume * BLACK[i] +
self.paint.white / volume * WHITE[i]
)
rgb = mixbox.latent_to_rgb(z_mix)
return "#%02x%02x%02x" % (rgb[0], rgb[1], rgb[2])
def simulate_timestep(self, interval):
"""
update the simulation based on the specified time interval
"""
# calculate the volume of the paint flowing out in the current time interval
outgoing_volume = self.valve_ratio * self.outflow_rate * interval
if outgoing_volume >= self.paint.volume:
# tank will be empty within the current time interval
out = self.paint
self.paint = PaintMixture() # empty
else:
# tank will not be empty
out = self.paint * (outgoing_volume / self.paint.volume)
self.paint -= out
# set outgoing paint volume
self.outflow = out.volume
if self.connected_to is not None:
# add outgoing paint into the connected tank
self.connected_to.add(out)
# check if tank has overflown
if self.paint.volume > self.tank_volume:
# keep it at the maximum fill level
self.paint *= self.tank_volume / self.paint.volume
# return outgoing paint mixture
return out
class Simulator(Thread):
"""
simulation of a paint mixing plant
"""
def __init__(self):
Thread.__init__(self)
self.stopRequested = False
self.sim_time = 0
# set up the mixing tank, initially empty
self.mixer = PaintTank("mixer", BASIN_VOLUME, BASIN_OUTFLOW, PaintMixture())
# set up the paint storage tanks and connect them to the mixing tank
self.tanks = [
PaintTank("cyan", TANK_VOLUME, TANK_OUTFLOW, PaintMixture(TANK_VOLUME, 0, 0, 0, 0),
connected_to=self.mixer), # cyan
PaintTank("magenta", TANK_VOLUME, TANK_OUTFLOW, PaintMixture(0, TANK_VOLUME, 0, 0, 0),
connected_to=self.mixer), # magenta
PaintTank("yellow", TANK_VOLUME, TANK_OUTFLOW, PaintMixture(0, 0, TANK_VOLUME, 0, 0),
connected_to=self.mixer), # yellow
PaintTank("black", TANK_VOLUME, TANK_OUTFLOW, PaintMixture(0, 0, 0, TANK_VOLUME, 0),
connected_to=self.mixer), # black
PaintTank("white", TANK_VOLUME, TANK_OUTFLOW, PaintMixture(0, 0, 0, 0, TANK_VOLUME),
connected_to=self.mixer), # white
self.mixer # mixing basin
]
def get_paint_tank_by_name(self, name):
"""
Helper method to get a reference to the PaintTank instance with the given name.
Returns None if not found.
"""
return next((tank for tank in self.tanks if tank.name == name), None)
def simulate(self, interval: float):
"""
advance simulation for a simulated duration of the specified time interval
"""
for tank in self.tanks:
tank.simulate_timestep(interval)
# increase simulation time
self.sim_time += interval
def stop(self):
"""
Request the simulation thread to stop.
"""
self.stopRequested = True
def run(self) -> None:
"""
main function for the simulation thread
"""
interval = 1.0 # 1 second
while not self.stopRequested:
self.simulate(interval=interval)
time.sleep(interval)
if __name__ == "__main__":
# create the simulator
simulator = Simulator()
# set initial conditions, open valve of first tank by 50%
simulator.tanks[0].set_valve(50)
# run the simulation for the specified time step and print some information
for i in range(10):
simulator.simulate(1.0)
print("============================================")
for tank in simulator.tanks:
print("Name: %s Volume: %.2f/%.2f" % (tank.name, tank.paint.volume, tank.tank_volume),
"paint: %s" % tank.paint)