-
-
Notifications
You must be signed in to change notification settings - Fork 277
Expand file tree
/
Copy pathtank.py
More file actions
1399 lines (1230 loc) · 48.5 KB
/
Copy pathtank.py
File metadata and controls
1399 lines (1230 loc) · 48.5 KB
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
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from abc import ABC, abstractmethod
import numpy as np
from ..mathutils.function import Function, funcify_method
from ..plots.tank_plots import _TankPlots
from ..prints.tank_prints import _TankPrints
from ..tools import tuple_handler
class Tank(ABC):
"""Abstract Tank class that defines a tank object for a rocket motor, so
that it evaluates useful properties of the tank and its fluids, such as
mass, volume, fluid flow rate, center of mass, etc.
See Also
--------
:ref:`tanks_usage`
Attributes
----------
Tank.name : str
Name of the tank.
Tank.geometry : TankGeometry
Geometry of the tank.
Tank.flux_time : float, tuple of float, optional
Tank flux time in seconds.
Tank.liquid : Fluid
Liquid inside the tank as a Fluid object.
Tank.gas : Fluid
Gas inside the tank as a Fluid object.
Tank.discretize : int, optional
Number of points to discretize fluid inputs.
Tank.fluid_mass : Function
Total mass of liquid and gases in kg inside the tank as a function
of time.
Tank.net_mass_flow_rate : Function
Net mass flow rate of the tank in kg/s as a function of time, also
understood as time derivative of the fluids mass.
Tank.liquid_volume : Function
Volume of the liquid inside the Tank in m^3 as a function of time.
Tank.gas_volume : Function
Volume of the gas inside the Tank in m^3 as a function of time.
Tank.liquid_height : Function
Height of the liquid inside the Tank in m as a function of time.
The zero level reference is the same as set in Tank.geometry.
Tank.gas_height : Function
Height of the gas inside the Tank in m as a function of time.
The zero level reference is the same as set in Tank.geometry.
Tank.liquid_mass : Function
Mass of the liquid inside the Tank in kg as a function of time.
Tank.gas_mass : Function
Mass of the gas inside the Tank in kg as a function of time.
Tank.liquid_center_of_mass : Function
Center of mass of the liquid inside the Tank in m as a function of
time. The zero level reference is the same as set in Tank.geometry.
Tank.gas_center_of_mass : Function
Center of mass of the gas inside the Tank in m as a function of
time. The zero level reference is the same as set in Tank.geometry.
Tank.center_of_mass : Function
Center of mass of liquid and gas (i.e. propellant) inside the Tank
in m as a function of time. The zero level reference is the same as
set in Tank.geometry.
Tank.liquid_inertia : Function
The inertia of the liquid inside the Tank in kg*m^2 as a function
of time around a perpendicular axis to the Tank symmetry axis. The
reference point is the Tank center of mass.
Tank.gas_inertia : Function
The inertia of the gas inside the Tank in kg*m^2 as a function of
time around a perpendicular axis to the Tank symmetry axis. The
reference point is the Tank center of mass.
Tank.inertia : Function
The inertia of the liquid and gas (i.e. propellant) inside the Tank
in kg*m^2 as a function of time around a perpendicular axis to the
Tank symmetry axis. The reference point is the Tank center of mass.
"""
def __init__(self, name, geometry, flux_time, liquid, gas, discretize=100):
"""Initialize Tank class.
Parameters
----------
name : str
Name of the tank.
geometry : TankGeometry
Geometry of the tank.
flux_time : float, tuple of float, optional
Tank flux time in seconds. It is the time range in which the tank
flux is being analyzed. In general, during this time, the tank is
being filled or emptied.If a float is given, the flux time is
assumed to be between 0 and the given float, in seconds. If a tuple
of float is given, the flux time is assumed to be between the first
and second elements of the tuple.
gas : Fluid
Gas inside the tank as a Fluid object.
liquid : Fluid
Liquid inside the tank as a Fluid object.
discretize : int, optional
Number of points to discretize fluid inputs. If the input
already has a appropriate discretization, this parameter
must be set to None. The default is 100.
"""
self.name = name
self.geometry = geometry
self.flux_time = flux_time
self.gas = gas
self.liquid = liquid
self.discretize = discretize
# Initialize plots and prints object
self.prints = _TankPrints(self)
self.plots = _TankPlots(self)
return None
@property
def flux_time(self):
"""Returns the start and final times of the tank flux.
Returns
-------
tuple
Tuple containing start and final times of the tank flux.
"""
return self._flux_time
@flux_time.setter
def flux_time(self, flux_time):
"""Sets the start and final times of the tank flux.
Parameters
----------
flux_time : tuple
Tuple containing start and final times of the tank flux.
"""
self._flux_time = tuple_handler(flux_time)
@property
@abstractmethod
def fluid_mass(self):
"""
Returns the total mass of liquid and gases inside the tank as a
function of time.
Returns
-------
Function
Mass of the tank as a function of time. Units in kg.
"""
pass
@property
@abstractmethod
def net_mass_flow_rate(self):
"""
Returns the net mass flow rate of the tank as a function of time.
Net mass flow rate is the mass flow rate entering the tank minus the
mass flow rate exiting the tank, including liquids and gases.
Returns
-------
Function
Net mass flow rate of the tank as a function of time.
"""
pass
@property
@abstractmethod
def fluid_volume(self):
"""
Returns the volume total fluid volume inside the tank as a
function of time. This volume is the sum of the liquid and gas
volumes.
Returns
-------
Function
Volume of the fluid as a function of time.
"""
pass
@property
@abstractmethod
def liquid_volume(self):
"""
Returns the volume of the liquid as a function of time.
Returns
-------
Function
Volume of the liquid as a function of time.
"""
pass
@property
@abstractmethod
def gas_volume(self):
"""
Returns the volume of the gas as a function of time.
Returns
-------
Function
Volume of the gas as a function of time.
"""
pass
@property
@abstractmethod
def liquid_height(self):
"""
Returns the liquid level as a function of time. This
height is measured from the zero level of the tank
geometry.
Returns
-------
Function
Height of the ullage as a function of time.
"""
pass
@property
@abstractmethod
def gas_height(self):
"""
Returns the gas level as a function of time. This
height is measured from the zero level of the tank
geometry.
Returns
-------
Function
Height of the ullage as a function of time.
"""
pass
@property
@abstractmethod
def liquid_mass(self):
"""
Returns the mass of the liquid as a function of time.
Returns
-------
Function
Mass of the liquid as a function of time.
"""
pass
@property
@abstractmethod
def gas_mass(self):
"""
Returns the mass of the gas as a function of time.
Returns
-------
Function
Mass of the gas as a function of time.
"""
pass
@funcify_method("Time (s)", "Center of mass of liquid (m)")
def liquid_center_of_mass(self):
"""
Returns the center of mass of the liquid portion of the tank
as a function of time. This height is measured from the zero
level of the tank geometry.
Returns
-------
Function
Center of mass of the liquid portion of the tank as a
function of time.
"""
moment = self.geometry.volume_moment(
self.geometry.bottom, self.liquid_height.max
)
liquid_moment = moment @ self.liquid_height
centroid = liquid_moment / self.liquid_volume
# Check for zero liquid volume
bound_volume = self.liquid_volume < 1e-4 * self.geometry.total_volume
if bound_volume.any():
# TODO: pending Function setter impl.
centroid.y_array[bound_volume] = self.geometry.bottom
centroid.set_interpolation()
centroid.set_extrapolation()
return centroid
@funcify_method("Time (s)", "Center of mass of gas (m)")
def gas_center_of_mass(self):
"""
Returns the center of mass of the gas portion of the tank
as a function of time. This height is measured from the zero
level of the tank geometry.
Returns
-------
Function
Center of mass of the gas portion of the tank as a
function of time.
"""
moment = self.geometry.volume_moment(self.geometry.bottom, self.gas_height.max)
upper_moment = moment @ self.gas_height
lower_moment = moment @ self.liquid_height
centroid = (upper_moment - lower_moment) / self.gas_volume
# Check for zero gas volume
bound_volume = self.gas_volume < 1e-4 * self.geometry.total_volume
if bound_volume.any():
# TODO: pending Function setter impl.
centroid.y_array[bound_volume] = self.liquid_height.y_array[bound_volume]
centroid.set_interpolation()
centroid.set_extrapolation()
return centroid
@funcify_method("Time (s)", "Center of mass of Fluid (m)")
def center_of_mass(self):
"""Returns the center of mass of the tank's fluids as a function of
time. This height is measured from the zero level of the tank
geometry.
Returns
-------
Function
Center of mass of the tank's fluids as a function of time.
"""
center_of_mass = (
self.liquid_center_of_mass * self.liquid_mass
+ self.gas_center_of_mass * self.gas_mass
) / (self.fluid_mass)
# Check for zero mass
bound_mass = (
self.fluid_mass < 0.001 * self.geometry.total_volume * self.gas.density
)
if bound_mass.any():
# TODO: pending Function setter impl.
center_of_mass.y_array[bound_mass] = self.geometry.bottom
center_of_mass.set_interpolation()
center_of_mass.set_extrapolation()
return center_of_mass
@funcify_method("Time (s)", "Inertia tensor of liquid (kg*m²)")
def liquid_inertia(self):
"""
Returns the inertia tensor of the liquid portion of the tank
as a function of time. The reference point is the center of
mass of the tank.
Returns
-------
Function
Inertia tensor of the liquid portion of the tank as a
function of time.
"""
Ix_volume = self.geometry.Ix_volume(
self.geometry.bottom, self.liquid_height.max
)
Ix_volume = Ix_volume @ self.liquid_height
# Steiner theorem to account for center of mass
Ix_volume -= self.liquid_volume * self.liquid_center_of_mass**2
Ix_volume += (
self.liquid_volume * (self.liquid_center_of_mass - self.center_of_mass) ** 2
)
return self.liquid.density * Ix_volume
@funcify_method("Time (s)", "inertia tensor of gas (kg*m^2)")
def gas_inertia(self):
"""
Returns the inertia tensor of the gas portion of the tank
as a function of time. The reference point is the center of
mass of the tank.
Returns
-------
Function
Inertia tensor of the gas portion of the tank as a
function of time.
"""
Ix_volume = self.geometry.Ix_volume(self.geometry.bottom, self.gas_height.max)
lower_inertia_volume = Ix_volume @ self.liquid_height
upper_inertia_volume = Ix_volume @ self.gas_height
inertia_volume = upper_inertia_volume - lower_inertia_volume
# Steiner theorem to account for center of mass
inertia_volume -= self.gas_volume * self.gas_center_of_mass**2
inertia_volume += (
self.gas_volume * (self.gas_center_of_mass - self.center_of_mass) ** 2
)
return self.gas.density * inertia_volume
@funcify_method("Time (s)", "inertia tensor (kg*m^2)")
def inertia(self):
"""
Returns the inertia tensor of the tank's fluids as a function of
time. The reference point is the center of mass of the tank.
Returns
-------
Function
Inertia tensor of the tank's fluids as a function of time.
"""
return self.liquid_inertia + self.gas_inertia
def draw(self):
"""Draws the tank geometry."""
self.plots.draw()
class MassFlowRateBasedTank(Tank):
"""Class to define a tank based on mass flow rates inputs. This class
inherits from the Tank class. See the Tank class for more information
on its attributes and methods.
See Also
--------
:ref:`tanks_usage`
"""
def __init__(
self,
name,
geometry,
flux_time,
liquid,
gas,
initial_liquid_mass,
initial_gas_mass,
liquid_mass_flow_rate_in,
gas_mass_flow_rate_in,
liquid_mass_flow_rate_out,
gas_mass_flow_rate_out,
discretize=100,
):
"""Initializes the MassFlowRateBasedTank class.
Parameters
----------
name : str
Name of the tank.
geometry : TankGeometry
Geometry of the tank.
flux_time : float, tuple of float, optional
Tank flux time in seconds. It is the time range in which the tank
flux is being analyzed. In general, during this time, the tank is
being filled or emptied.
If a float is given, the flux time is assumed to be between 0 and
the given float, in seconds. If a tuple of float is given, the flux
time is assumed to be between the first and second elements of the
tuple.
liquid : Fluid
Liquid inside the tank as a Fluid object.
gas : Fluid
Gas inside the tank as a Fluid object.
initial_liquid_mass : float
Initial liquid mass in the tank in kg.
initial_gas_mass : float
Initial gas mass in the tank in kg.
liquid_mass_flow_rate_in : int, float, callable, string, array, Function
Liquid mass flow rate into the tank in kg/s. Always positive.
It must be a valid :class:`Function` source.
If a callable is given, it must be a function of time.
If a ``.csv`` file is given, it must have two columns, the first
one being time in seconds and the second one being the mass flow
rate in kg/s.
gas_mass_flow_rate_in : int, float, callable, string, array, Function
Gas mass flow rate into the tank in kg/s. Always positive.
It must be a valid :class:`Function` source.
If a callable is given, it must be a function of time.
If a ``.csv`` file is given, it must have two columns, the first
one being time in seconds and the second one being the mass flow
rate in kg/s.
liquid_mass_flow_rate_out : int, float, callable, string, array, Function
Liquid mass flow rate out of the tank in kg/s. Always positive.
It must be a valid :class:`Function` source.
If a callable is given, it must be a function of time.
If a ``.csv`` file is given, it must have two columns, the first
one being time in seconds and the second one being the mass flow
rate in kg/s.
gas_mass_flow_rate_out : int, float, callable, string, array, Function
Gas mass flow rate out of the tank in kg/s. Always positive.
It must be a valid :class:`Function` source.
If a callable is given, it must be a function of time.
If a ``.csv`` file is given, it must have two columns, the first
one being time in seconds and the second one being the mass flow
rate in kg/s.
discretize : int, optional
Number of points to discretize fluid inputs. If the mass flow
rate inputs are uniformly discretized (have the same time steps)
this parameter may be set to None. Otherwise, an uniform
discretization will be applied based on the discretize value.
The default is 100.
"""
super().__init__(name, geometry, flux_time, liquid, gas, discretize)
self.initial_liquid_mass = initial_liquid_mass
self.initial_gas_mass = initial_gas_mass
# Define flow rates
self.liquid_mass_flow_rate_in = Function(
liquid_mass_flow_rate_in,
inputs="Time (s)",
outputs="Mass Flow Rate (kg/s)",
interpolation="linear",
extrapolation="zero",
)
self.gas_mass_flow_rate_in = Function(
gas_mass_flow_rate_in,
inputs="Time (s)",
outputs="Mass Flow Rate (kg/s)",
interpolation="linear",
extrapolation="zero",
)
self.liquid_mass_flow_rate_out = Function(
liquid_mass_flow_rate_out,
inputs="Time (s)",
outputs="Mass Flow Rate (kg/s)",
interpolation="linear",
extrapolation="zero",
)
self.gas_mass_flow_rate_out = Function(
gas_mass_flow_rate_out,
inputs="Time (s)",
outputs="Mass Flow Rate (kg/s)",
interpolation="linear",
extrapolation="zero",
)
# Discretize input flow if needed
self.discretize_flow() if discretize else None
return None
@funcify_method("Time (s)", "Mass (kg)")
def fluid_mass(self):
"""
Returns the total mass of liquid and gases inside the tank as a
function of time.
Returns
-------
Function
Mass of the tank as a function of time. Units in kg.
"""
return self.liquid_mass + self.gas_mass
@funcify_method("Time (s)", "Mass (kg)")
def liquid_mass(self):
"""
Returns the mass of the liquid as a function of time by integrating
the liquid mass flow rate.
Returns
-------
Function
Mass of the liquid as a function of time.
"""
liquid_flow = self.net_liquid_flow_rate.integral_function()
liquid_mass = self.initial_liquid_mass + liquid_flow
if (liquid_mass < 0).any():
raise ValueError(
f"The tank {self.name} is underfilled. "
+ "The liquid mass is negative given the mass flow rates.\n\t\t"
+ "Try increasing the initial liquid mass, or reducing the mass"
+ "flow rates.\n\t\t"
+ f"The liquid mass is {np.min(liquid_mass.y_array):.3f} kg at "
+ f"{liquid_mass.x_array[np.argmin(liquid_mass.y_array)]} s."
)
return liquid_mass
@funcify_method("Time (s)", "Mass (kg)")
def gas_mass(self):
"""
Returns the mass of the gas as a function of time by integrating
the gas mass flow rate.
Returns
-------
Function
Mass of the gas as a function of time.
"""
gas_flow = self.net_gas_flow_rate.integral_function()
gas_mass = self.initial_gas_mass + gas_flow
if (gas_mass < -1e-6).any(): # -1e-6 is to avoid numerical errors
raise ValueError(
f"The tank {self.name} is underfilled. The gas mass is negative"
+ " given the mass flow rates.\n\t\t"
+ "Try increasing the initial gas mass, or reducing the mass"
+ " flow rates.\n\t\t"
+ f"The gas mass is {np.min(gas_mass.y_array):.3f} kg at "
+ f"{gas_mass.x_array[np.argmin(gas_mass.y_array)]} s."
)
return gas_mass
@funcify_method("Time (s)", "liquid mass flow rate (kg/s)", extrapolation="zero")
def net_liquid_flow_rate(self):
"""
Returns the net mass flow rate of liquid as a function of time.
It is computed as the liquid mass flow rate entering the tank
minus the liquid mass flow rate exiting the tank.
Returns
-------
Function
Net liquid mass flow rate of the tank as a function of time.
"""
return self.liquid_mass_flow_rate_in - self.liquid_mass_flow_rate_out
@funcify_method("Time (s)", "gas mass flow rate (kg/s)", extrapolation="zero")
def net_gas_flow_rate(self):
"""
Returns the net mass flow rate of gas as a function of time.
It is computed as the gas mass flow rate entering the tank
minus the gas mass flow rate exiting the tank.
Returns
-------
Function
Net gas mass flow rate of the tank as a function of time.
"""
return self.gas_mass_flow_rate_in - self.gas_mass_flow_rate_out
@funcify_method("Time (s)", "mass flow rate (kg/s)", extrapolation="zero")
def net_mass_flow_rate(self):
"""
Returns the net mass flow rate of the tank as a function of time.
Net mass flow rate is the mass flow rate entering the tank minus the
mass flow rate exiting the tank, including liquids and gases.
Returns
-------
Function
Net mass flow rate of the tank as a function of time.
"""
return self.net_liquid_flow_rate + self.net_gas_flow_rate
@funcify_method("Time (s)", "Volume (m³)")
def fluid_volume(self):
"""
Returns the volume total fluid volume inside the tank as a
function of time. This volume is the sum of the liquid and gas
volumes.
Returns
-------
Function
Volume of the fluid as a function of time.
"""
return self.liquid_volume + self.gas_volume
@funcify_method("Time (s)", "Volume (m³)")
def liquid_volume(self):
"""
Returns the volume of the liquid as a function of time.
Returns
-------
Function
Volume of the liquid as a function of time.
"""
return self.liquid_mass / self.liquid.density
@funcify_method("Time (s)", "Volume (m³)")
def gas_volume(self):
"""
Returns the volume of the gas as a function of time.
Returns
-------
Function
Volume of the gas as a function of time.
"""
return self.gas_mass / self.gas.density
@funcify_method("Time (s)", "Height (m)")
def liquid_height(self):
"""
Returns the liquid level as a function of time. This
height is measured from the zero level of the tank
geometry.
Returns
-------
Function
Height of the ullage as a function of time.
"""
liquid_height = self.geometry.inverse_volume.compose(self.liquid_volume)
diff_bt = liquid_height - self.geometry.bottom
diff_up = liquid_height - self.geometry.top
if (diff_bt < 0).any():
raise ValueError(
f"The tank '{self.name}' is underfilled. The liquid height is "
+ "below the tank bottom.\n\t\t"
+ "Try increasing the initial liquid mass, or reducing the mass"
+ " flow rates.\n\t\t"
+ f"The liquid height is {np.min(diff_bt.y_array):.3f} m below "
+ f"the tank bottom at {diff_bt.x_array[np.argmin(diff_bt.y_array)]:.3f} s."
)
if (diff_up > 0).any():
raise ValueError(
f"The tank '{self.name}' is overfilled. The liquid height is "
+ "above the tank top.\n\t\t"
+ "Try increasing the tank height, or reducing the initial liquid"
+ " mass, or reducing the mass flow rates.\n\t\t"
+ f"The liquid height is {np.max(diff_up.y_array):.3f} m above "
+ f"the tank top at {diff_up.x_array[np.argmax(diff_up.y_array)]:.3f} s."
)
return liquid_height
@funcify_method("Time (s)", "Height (m)")
def gas_height(self):
"""
Returns the gas level as a function of time. This
height is measured from the zero level of the tank
geometry.
Returns
-------
Function
Height of the ullage as a function of time.
"""
fluid_volume = self.gas_volume + self.liquid_volume
gas_height = self.geometry.inverse_volume.compose(fluid_volume)
diff = gas_height - self.geometry.top
if (diff > 0).any():
raise ValueError(
f"The tank '{self.name}' is overfilled. "
+ "The gas height is above the tank top.\n\t\t"
+ "Try increasing the tank height, or reducing fluids' mass,"
+ " or double check the mass flow rates.\n\t\t"
+ f"The gas height is {np.max(diff.y_array):.3f} m above "
+ f"the tank top at {diff.x_array[np.argmax(diff.y_array)]} s."
)
return gas_height
def discretize_flow(self):
"""Discretizes the mass flow rate inputs according to the flux time and
the discretize parameter.
"""
self.liquid_mass_flow_rate_in.set_discrete(*self.flux_time, self.discretize)
self.gas_mass_flow_rate_in.set_discrete(*self.flux_time, self.discretize)
self.liquid_mass_flow_rate_out.set_discrete(*self.flux_time, self.discretize)
self.gas_mass_flow_rate_out.set_discrete(*self.flux_time, self.discretize)
class UllageBasedTank(Tank):
"""Class to define a tank whose flow is described by ullage volume, i.e.,
the volume of the tank that is not occupied by the liquid. It assumes that
the ullage volume is uniformly filled by the gas. This class inherits from
the Tank class. See the Tank class for more information on its attributes
and methods.
See Also
--------
:ref:`tanks_usage`
"""
def __init__(
self,
name,
geometry,
flux_time,
liquid,
gas,
ullage,
discretize=100,
):
"""
Parameters
----------
name : str
Name of the tank.
geometry : TankGeometry
Geometry of the tank.
flux_time : float, tuple of float, optional
Tank flux time in seconds. It is the time range in which the tank
flux is being analyzed. In general, during this time, the tank is
being filled or emptied. If a float is given, the flux time is
assumed to be between 0 and the given float, in seconds. If a tuple
of float is given, the flux time is assumed to be between the first
and second elements of the tuple.
liquid : Fluid
Liquid inside the tank as a Fluid object.
gas : Fluid
Gas inside the tank as a Fluid object.
ullage : int, float, callable, string, array, Function
Ullage volume as a function of time in m^3. Also understood as the
volume of the Tank that is not occupied by liquid. Must be a valid
:class:`Function` source.
If a callable is given, it must be a function of time in seconds.
If a ``.csv`` file is given, the first column must be the time in
seconds and the second column must be the ullage volume in m^3.
discretize : int, optional
Number of points to discretize fluid inputs. If the ullage input is
already discretized this parameter may be set to None. Otherwise,
an uniform discretization will be applied based on the discretize
value.
The default is 100.
"""
super().__init__(name, geometry, flux_time, liquid, gas, discretize)
# Define ullage
self.ullage = Function(ullage, "Time (s)", "Volume (m³)", "linear")
# Discretize input if needed
self.discretize_ullage() if discretize else None
# Check if the ullage is within bounds
if (self.ullage > self.geometry.total_volume).any():
raise ValueError(
"The ullage volume is out of bounds. It is greater than the "
+ "total volume of the tank."
)
if (self.ullage < 0).any():
raise ValueError("The ullage volume is out of bounds. It is negative.")
return None
@funcify_method("Time (s)", "Mass (kg)")
def fluid_mass(self):
"""
Returns the total mass of liquid and gases inside the tank as a
function of time.
Returns
-------
Function
Mass of the tank as a function of time. Units in kg.
"""
return self.liquid_mass + self.gas_mass
@funcify_method("Time (s)", "Mass flow rate (kg/s)")
def net_mass_flow_rate(self):
"""
Returns the net mass flow rate of the tank as a function of time by
taking the derivative of the mass function.
Returns
-------
Function
Net mass flow rate of the tank as a function of time.
"""
return self.fluid_mass.derivative_function()
@funcify_method("Time (s)", "Volume (m³)")
def fluid_volume(self):
"""
Returns the volume total fluid volume inside the tank as a
function of time. This volume is the sum of the liquid and gas
volumes.
Returns
-------
Function
Volume of the fluid as a function of time.
"""
return self.geometry.total_volume
@funcify_method("Time (s)", "Volume (m³)")
def liquid_volume(self):
"""
Returns the volume of the liquid as a function of time. The
volume is computed by subtracting the ullage volume from the
total volume of the tank.
Returns
-------
Function
Volume of the liquid as a function of time.
"""
return -(self.ullage - self.geometry.total_volume)
@funcify_method("Time (s)", "Volume (m³)")
def gas_volume(self):
"""
Returns the volume of the gas as a function of time. From the
Tank assumptions the gas volume is equal to the ullage volume.
Returns
-------
Function
Volume of the gas as a function of time.
"""
return self.ullage
@funcify_method("Time (s)", "Mass (kg)")
def gas_mass(self):
"""
Returns the mass of the gas as a function of time.
Returns
-------
Function
Mass of the gas as a function of time.
"""
return self.gas_volume * self.gas.density
@funcify_method("Time (s)", "Mass (kg)")
def liquid_mass(self):
"""
Returns the mass of the liquid as a function of time.
Returns
-------
Function
Mass of the liquid as a function of time.
"""
return self.liquid_volume * self.liquid.density
@funcify_method("Time (s)", "Height (m)")
def liquid_height(self):
"""
Returns the liquid level as a function of time. This
height is measured from the zero level of the tank
geometry.
Returns
-------
Function
Height of the ullage as a function of time.
"""
return self.geometry.inverse_volume.compose(self.liquid_volume)
@funcify_method("Time (s)", "Height (m)", "linear")
def gas_height(self):
"""
Returns the gas level as a function of time. This height is measured
from the zero level of the tank geometry. Since the gas is assumed to
be uniformly distributed in the ullage, the gas height is constant
and equal to the top of the tank geometry.
Returns
-------
Function
Height of the ullage as a function of time.
"""
return Function(self.geometry.top).set_discrete_based_on_model(self.gas_volume)
def discretize_ullage(self):
"""Discretizes the ullage input according to the flux time and the
discretize parameter."""
self.ullage.set_discrete(*self.flux_time, self.discretize)
class LevelBasedTank(Tank):
"""Class to define a tank whose flow is described by liquid level, i.e.,
the height of the liquid inside the tank. It assumes that the volume
above the liquid level is uniformly occupied by gas. This class inherits
from the Tank class. See the Tank class for more information on its
attributes and methods.
See Also
--------
:ref:`tanks_usage`
"""
def __init__(
self,
name,
geometry,
flux_time,
liquid,
gas,
liquid_height,
discretize=100,
):
"""
Parameters
----------
name : str
Name of the tank.
geometry : TankGeometry
Geometry of the tank.
flux_time : float, tuple of float, optional
Tank flux time in seconds. It is the time range in which the tank
flux is being analyzed. In general, during this time, the tank is
being filled or emptied. If a float is given, the flux time is
assumed to be between 0 and the given float, in seconds. If a tuple
of float is given, the flux time is assumed to be between the first
and second elements of the tuple.
liquid : Fluid
Liquid inside the tank as a Fluid object.
gas : Fluid
Gas inside the tank as a Fluid object.
liquid_height : int, float, callable, string, array, Function
Liquid height as a function of time in m. Must be a valid
:class:`Function` source. The liquid height zero level
reference is assumed to be the same as the Tank geometry.
If a callable is given, it must be a function of time in seconds
If a ``.csv`` file is given, the first column is assumed to be the
time and the second column the liquid height in meters.
discretize : int, optional