This repository has been archived by the owner on Sep 6, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 8
/
DesignSPHysics.py
4927 lines (4195 loc) · 227 KB
/
DesignSPHysics.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
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
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
"""
Initializes a complete interface with DualSPHysics suite related operations.
It allows an user to create DualSPHysics compatible cases, automating a bunch of things needed to use them.
More info in http://design.sphysics.org/
"""
import FreeCAD
import FreeCADGui
import Draft
import glob
import os
import sys
import time
import pickle
import threading
import traceback
import subprocess
import shutil
import uuid
from PySide import QtGui, QtCore
reload(sys)
sys.setdefaultencoding('utf-8')
# Fix FreeCAD not searching in the user-set macro folder.
try:
from ds_modules.properties import *
from ds_modules import utils, guiutils, xmlimporter, dsphwidgets
from ds_modules.utils import __
except:
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from ds_modules.properties import *
from ds_modules import utils, guiutils, xmlimporter, dsphwidgets
from ds_modules.utils import __
# Copyright (C) 2017 - Andrés Vieira ([email protected])
# EPHYSLAB Environmental Physics Laboratory, Universidade de Vigo
# EPHYTECH Environmental Physics Technologies
#
# This file is part of DesignSPHysics.
#
# DesignSPHysics is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# DesignSPHysics is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with DesignSPHysics. If not, see <http://www.gnu.org/licenses/>.
__author__ = "Andrés Vieira"
__copyright__ = "Copyright 2016-2017, DualSHPysics Team"
__credits__ = ["Andrés Vieira",
"Alejandro Jacobo Cabrera Crespo", "Orlando García Feal"]
__license__ = "GPL"
__version__ = utils.VERSION
__maintainer__ = "Andrés Vieira"
__email__ = "[email protected]"
__status__ = "Development"
# Print license at macro start
try:
utils.print_license()
except EnvironmentError:
guiutils.warning_dialog(
__("LICENSE file could not be found. Are you sure you didn't delete it?")
)
# Version check. This script is only compatible with FreeCAD 0.16 or higher
is_compatible = utils.is_compatible_version()
if not is_compatible:
guiutils.error_dialog(
__("This FreeCAD version is not compatible. Please update FreeCAD to version 0.16 or higher.")
)
raise EnvironmentError(
__("This FreeCAD version is not compatible. Please update FreeCAD to version 0.16 or higher.")
)
# Set QT to UTF-8 encoding
QtCore.QTextCodec.setCodecForCStrings(QtCore.QTextCodec.codecForName('UTF-8'))
# Main data structure
# TODO: Big Change: Data structures should be an instance of a class like CaseData() and TempCaseData(), not a dict()
data = dict() # Used to save on disk case parameters and related data
temp_data = dict() # Used to store temporal useful items (like processes)
# Used to store widgets that will be disabled/enabled, so they are centralized
widget_state_elements = dict()
# Establishing references for the different elements that the script will use later.
fc_main_window = FreeCADGui.getMainWindow() # FreeCAD main window
# TODO: Big Change: This should definetly be a custom class like DesignSPHysicsDock(QtGui.QDockWidget)
dsph_main_dock = QtGui.QDockWidget() # DSPH main dock
# Scaffolding widget, only useful to apply to the dsph_dock widget
dsph_main_dock_scaff_widget = QtGui.QWidget()
# Executes the default data function the first time and merges results with current data structure.
default_data, default_temp_data = utils.get_default_data()
data.update(default_data)
temp_data.update(default_temp_data)
# The script needs only one document open, called DSPH_Case.
# This section tries to close all the current documents.
if utils.document_count() > 0:
success = utils.prompt_close_all_documents()
if not success:
quit()
# If the script is executed even when a previous DSPH Dock is created it makes sure that it's deleted before.
previous_dock = fc_main_window.findChild(QtGui.QDockWidget, __("DSPH Widget"))
if previous_dock:
previous_dock.setParent(None)
previous_dock = None
# Creation of the DSPH Widget.
# Creates a widget with a series of layouts added, to apply to the DSPH dock at the end.
dsph_main_dock.setObjectName("DSPH Widget")
dsph_main_dock.setWindowTitle("{} {}".format(utils.APP_NAME, str(__version__)))
main_layout = QtGui.QVBoxLayout() # Main Widget layout. Vertical ordering
# Component layouts definition
logo_layout = QtGui.QHBoxLayout()
logo_layout.setSpacing(0)
logo_layout.setContentsMargins(0, 0, 0, 0)
intro_layout = QtGui.QVBoxLayout()
# DSPH dock first section.
# Includes constant definition, help, etc.
constants_label = QtGui.QLabel("<b>{}</b>".format(__("Configuration")))
constants_label.setWordWrap(True)
constants_button = QtGui.QPushButton(__("Define\nConstants"))
constants_button.setToolTip(__("Use this button to define case constants,\nsuch as gravity or fluid reference density."))
# Opens constant definition window on button click
constants_button.clicked.connect(lambda: guiutils.def_constants_window(data))
widget_state_elements['constants_button'] = constants_button
# Help button that opens a help URL for DesignSPHysics
help_button = QtGui.QPushButton("Help")
help_button.setToolTip(__("Push this button to open a browser with help\non how to use this tool."))
help_button.clicked.connect(utils.open_help)
# Setup window button.
setup_button = QtGui.QPushButton(__("Setup\nPlugin"))
setup_button.setToolTip(__("Setup of the simulator executables"))
setup_button.clicked.connect(lambda: guiutils.def_setup_window(data))
# Execution parameters button.
execparams_button = QtGui.QPushButton(__("Execution\nParameters"))
execparams_button.setToolTip(__("Change execution parameters, such as\ntime of simulation, viscosity, etc."))
execparams_button.clicked.connect(lambda: guiutils.def_execparams_window(data))
widget_state_elements['execparams_button'] = execparams_button
# Logo. Made from a label. Labels can have image as well as text.
logo_label = QtGui.QLabel()
logo_label.setPixmap(guiutils.get_icon(file_name="logo.png", return_only_path=True))
def on_dp_changed():
""" DP Introduction.
Changes the dp at the moment the user changes the text. """
data['dp'] = float(dp_input.text())
# DP Introduction layout
dp_layout = QtGui.QHBoxLayout()
dp_label = QtGui.QLabel(__("Inter-particle distance: "))
dp_label.setToolTip(__("Lower DP to have more particles in the case."))
dp_input = QtGui.QLineEdit()
dp_input.setToolTip(__("Lower DP to have more particles in the case."))
dp_label2 = QtGui.QLabel(" meters")
dp_input.setMaxLength(10)
dp_input.setText(str(data['dp']))
dp_input.textChanged.connect(on_dp_changed)
widget_state_elements['dp_input'] = dp_input
dp_validator = QtGui.QDoubleValidator(0.0, 100, 8, dp_input)
dp_input.setValidator(dp_validator)
dp_layout.addWidget(dp_label)
dp_layout.addWidget(dp_input)
dp_layout.addWidget(dp_label2)
# Case control definition.
# Includes things like New Case, Save, etc...
cc_layout = QtGui.QVBoxLayout()
cclabel_layout = QtGui.QHBoxLayout()
ccfilebuttons_layout = QtGui.QHBoxLayout()
ccsecondrow_layout = QtGui.QHBoxLayout()
ccthirdrow_layout = QtGui.QHBoxLayout()
ccfourthrow_layout = QtGui.QHBoxLayout()
casecontrols_label = QtGui.QLabel("<b>{}</b>".format(__("Pre-processing")))
# New Case button
casecontrols_bt_newdoc = QtGui.QToolButton()
casecontrols_bt_newdoc.setPopupMode(QtGui.QToolButton.MenuButtonPopup)
casecontrols_bt_newdoc.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
casecontrols_bt_newdoc.setText(" {}".format(__("New\n Case")))
casecontrols_bt_newdoc.setToolTip(__("Creates a new case. \nThe opened documents will be closed."))
casecontrols_bt_newdoc.setIcon(guiutils.get_icon("new.png"))
casecontrols_bt_newdoc.setIconSize(QtCore.QSize(28, 28))
casecontrols_menu_newdoc = QtGui.QMenu()
casecontrols_menu_newdoc.addAction(guiutils.get_icon("new.png"), __("New"))
casecontrols_menu_newdoc.addAction(guiutils.get_icon("new.png"), __("Import FreeCAD Document"))
casecontrols_bt_newdoc.setMenu(casecontrols_menu_newdoc)
# Save Case button and dropdown
casecontrols_bt_savedoc = QtGui.QToolButton()
casecontrols_bt_savedoc.setPopupMode(QtGui.QToolButton.MenuButtonPopup)
casecontrols_bt_savedoc.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
casecontrols_bt_savedoc.setText(" {}".format(__("Save\n Case")))
casecontrols_bt_savedoc.setToolTip(__("Saves the case."))
casecontrols_bt_savedoc.setIcon(guiutils.get_icon("save.png"))
casecontrols_bt_savedoc.setIconSize(QtCore.QSize(28, 28))
casecontrols_menu_savemenu = QtGui.QMenu()
casecontrols_menu_savemenu.addAction(guiutils.get_icon("save.png"), __("Save and run GenCase"))
casecontrols_menu_savemenu.addAction(guiutils.get_icon("save.png"), __("Save as..."))
casecontrols_bt_savedoc.setMenu(casecontrols_menu_savemenu)
widget_state_elements['casecontrols_bt_savedoc'] = casecontrols_bt_savedoc
# Load Case button
casecontrols_bt_loaddoc = QtGui.QToolButton()
casecontrols_bt_loaddoc.setToolButtonStyle(QtCore.Qt.ToolButtonTextBesideIcon)
casecontrols_bt_loaddoc.setText(" {}".format(__("Load\n Case")))
casecontrols_bt_loaddoc.setToolTip(__("Loads a case from disk. All the current documents\nwill be closed."))
casecontrols_bt_loaddoc.setIcon(guiutils.get_icon("load.png"))
casecontrols_bt_loaddoc.setIconSize(QtCore.QSize(28, 28))
# Add fillbox button
casecontrols_bt_addfillbox = QtGui.QPushButton(__("Add fillbox"))
casecontrols_bt_addfillbox.setToolTip(__("Adds a FillBox. A FillBox is able to fill an empty space\nwithin "
"limits of geometry and a maximum bounding\nbox placed by the user."))
casecontrols_bt_addfillbox.setEnabled(False)
widget_state_elements['casecontrols_bt_addfillbox'] = casecontrols_bt_addfillbox
# Import STL button
casecontrols_bt_addstl = QtGui.QPushButton("Import STL")
casecontrols_bt_addstl.setToolTip(__("Imports a STL with postprocessing. "
"This way you can set the scale of the imported object."))
casecontrols_bt_addstl.setEnabled(False)
widget_state_elements['casecontrols_bt_addstl'] = casecontrols_bt_addstl
# Import XML button
casecontrols_bt_importxml = QtGui.QPushButton(__("Import XML"))
casecontrols_bt_importxml.setToolTip(__("Imports an already created XML case from disk."))
casecontrols_bt_importxml.setEnabled(True)
# Case summary button
summary_bt = QtGui.QPushButton(__("Case summary"))
summary_bt.setToolTip(__("Shows a complete case summary with objects, "
"configurations and settings in a brief view."))
widget_state_elements['summary_bt'] = summary_bt
summary_bt.setEnabled(False)
# Toggle 3D/2D button
toggle3dbutton = QtGui.QPushButton(__("Change 3D/2D"))
toggle3dbutton.setToolTip(__("Changes the case mode between 2D and 3D mode, switching the Case Limits between a plane or a cube"))
widget_state_elements['toggle3dbutton'] = toggle3dbutton
# Damping add button
casecontrols_bt_special = QtGui.QPushButton(__("Special"))
casecontrols_bt_special.setToolTip(__("Special actions for the case."))
widget_state_elements['dampingbutton'] = casecontrols_bt_special
def on_new_case(prompt=True):
""" Defines what happens when new case is clicked. Closes all documents
if possible and creates a FreeCAD document with Case Limits object. """
# Closes all documents as there can only be one open.
if utils.document_count() > 0:
new_case_success = utils.prompt_close_all_documents(prompt)
if not new_case_success:
return
# Creates a new document and merges default data to the current data structure.
new_case_default_data, new_case_temp_data = utils.get_default_data()
data.update(new_case_default_data)
temp_data.update(new_case_temp_data)
utils.create_dsph_document()
guiutils.widget_state_config(widget_state_elements, "new case")
data['simobjects']['Case_Limits'] = ["mkspecial", "typespecial", "fillspecial"]
dp_input.setText(str(data["dp"]))
# Forces call to item selection change function so all changes are taken into account
on_tree_item_selection_change()
def on_new_from_freecad_document(prompt=True):
""" Creates a new case based on an existing FreeCAD document.
This is specially useful for CAD users that want to use existing geometry for DesignSPHysics. """
file_name, _ = QtGui.QFileDialog().getOpenFileName(guiutils.get_fc_main_window(), "Select document to import", QtCore.QDir.homePath())
if utils.document_count() > 0:
new_case_success = utils.prompt_close_all_documents(prompt)
if not new_case_success:
return
# Creates a new document and merges default data to the current data structure.
new_case_default_data, new_case_temp_data = utils.get_default_data()
data.update(new_case_default_data)
temp_data.update(new_case_temp_data)
utils.create_dsph_document_from_fcstd(file_name)
guiutils.widget_state_config(widget_state_elements, "new case")
data['simobjects']['Case_Limits'] = ["mkspecial", "typespecial", "fillspecial"]
dp_input.setText(str(data["dp"]))
# Forces call to item selection change function so all changes are taken into account
on_tree_item_selection_change()
def on_save_case(save_as=None):
""" Defines what happens when save case button is clicked.
Saves a freecad scene definition, and a dump of dsph data for the case."""
# Watch if save path is available. Prompt the user if not.
if (data['project_path'] == "" and data['project_name'] == "") or save_as:
# noinspection PyArgumentList
save_name, _ = QtGui.QFileDialog.getSaveFileName(dsph_main_dock, __("Save Case"), QtCore.QDir.homePath())
else:
save_name = data['project_path']
# Check if there is any path, a blank one meant the user cancelled the save file dialog
if save_name != '':
project_name = save_name.split('/')[-1]
# Watch if folder already exists or create it
if not os.path.exists(save_name):
os.makedirs(save_name)
data['project_path'] = save_name
data['project_name'] = project_name
# Create out folder for the case
if not os.path.exists("{}/{}_out".format(save_name, project_name)):
os.makedirs("{}/{}_out".format(save_name, project_name))
# Copy files from movements and change its paths to be inside the project.
for key, value in data["motion_mks"].iteritems():
for movement in value:
if isinstance(movement, SpecialMovement):
if isinstance(movement.generator, FileGen) or isinstance(movement.generator, RotationFileGen):
filename = movement.generator.filename
utils.debug("Copying {} to {}".format(filename, save_name))
# Change directory to de case one, so if file path is already relative it copies it to the
# out folder
os.chdir(save_name)
try:
# Copy to project root
shutil.copy2(filename, save_name)
except IOError:
utils.error("Unable to copy {} into {}".format(filename, save_name))
except shutil.Error:
# Probably already copied the file.
pass
try:
# Copy to project out folder
shutil.copy2(filename, save_name + "/" + project_name + "_out")
movement.generator.filename = "{}".format(filename.split("/")[-1])
except IOError:
utils.error("Unable to copy {} into {}".format(filename, save_name))
except shutil.Error:
# Probably already copied the file.
pass
# Copy files from Acceleration input and change paths to be inside the project folder.
for aid in data['accinput'].acclist:
filename = aid.datafile
utils.debug("Copying {} to {}".format(filename, save_name + "/" + project_name + "_out"))
# Change directory to de case one, so if file path is already relative it copies it to the
# out folder
os.chdir(save_name)
try:
# Copy to project root
shutil.copy2(filename, save_name)
except IOError:
utils.error("Unable to copy {} into {}".format(filename, save_name))
except shutil.Error:
# Probably already copied the file.
pass
try:
# Copy to project out folder
shutil.copy2(filename, save_name + "/" + project_name + "_out")
aid.datafile = filename.split("/")[-1]
except IOError:
utils.error("Unable to copy {} into {}".format(filename, save_name))
except shutil.Error:
# Probably already copied the file.
pass
# Copy files from pistons and change paths to be inside the project folder.
for key, piston in data["mlayerpistons"].iteritems():
if isinstance(piston, MLPiston1D):
filename = piston.filevelx
utils.debug("Copying {} to {}".format(filename, save_name + "/" + project_name + "_out"))
# Change directory to de case one, so if file path is already relative it copies it to the
# out folder
os.chdir(save_name)
try:
# Copy to project root
shutil.copy2(filename, save_name)
except IOError:
utils.error("Unable to copy {} into {}".format(filename, save_name))
except shutil.Error:
# Probably already copied the file.
pass
try:
# Copy to project out folder
shutil.copy2(filename, save_name + "/" + project_name + "_out")
piston.filevelx = filename.split("/")[-1]
except IOError:
utils.error("Unable to copy {} into {}".format(filename, save_name))
except shutil.Error:
# Probably already copied the file.
pass
if isinstance(piston, MLPiston2D):
veldata = piston.veldata
for v in veldata:
filename = v.filevelx
utils.debug("Copying {} to {}".format(filename, save_name + "/" + project_name + "_out"))
# Change directory to de case one, so if file path is already relative it copies it to the
# out folder
os.chdir(save_name)
try:
# Copy to project root
shutil.copy2(filename, save_name)
except IOError:
utils.error("Unable to copy {} into {}".format(filename, save_name))
except shutil.Error:
# Probably already copied the file.
pass
try:
# Copy to project out folder
shutil.copy2(filename, save_name + "/" + project_name + "_out")
v.filevelx = filename.split("/")[-1]
except IOError:
utils.error("Unable to copy {} into {}".format(filename, save_name))
except shutil.Error:
# Probably already copied the file.
pass
# Copies files needed for RelaxationZones into the project folder and changes data paths to relative ones.
if isinstance(data['relaxationzone'], RelaxationZoneFile):
# Need to copy the abc_x*_y*.csv file series to the out folder
filename = data['relaxationzone'].filesvel
# Change directory to de case one, so if file path is already relative it copies it to the
# out folder
os.chdir(save_name)
for f in glob.glob("{}*".format(filename)):
utils.debug("Copying {} to {}".format(filename, save_name + "/" + project_name + "_out"))
try:
# Copy to project root
shutil.copy2(f, save_name)
except IOError:
utils.error("Unable to copy {} into {}".format(filename, save_name))
except shutil.Error:
# Probably already copied the file.
pass
try:
# Copy to project out folder
shutil.copy2(f, save_name + "/" + project_name + "_out")
data['relaxationzone'].filesvel = filename.split("/")[-1]
except IOError:
utils.error("Unable to copy {} into {}".format(filename, save_name))
except shutil.Error:
# Probably already copied the file.
pass
# Dumps all the case data to an XML file.
utils.dump_to_xml(data, save_name)
# Generate batch file in disk
# TODO: Batch file generation needs to be updated.
# Batch files need to have all the data they can have into it. Also, users should have options to include
# different post-processing tools in the batch files. It is disabled for now because it's not really useful.
# if (data['gencase_path'] == "") or (data['dsphysics_path'] == "") or (data['partvtk4_path'] == ""):
# utils.warning(
# __("Can't create executable bat file! One or more of the paths in plugin setup is not set"))
# else:
# # Export batch files
# utils.batch_generator(
# full_path=save_name,
# case_name=project_name,
# gcpath=data['gencase_path'],
# dsphpath=data['dsphysics_path'],
# pvtkpath=data['partvtk4_path'],
# exec_params="-{} {}".format(
# str(ex_selector_combo.currentText()).lower(), data['additional_parameters']),
# lib_path='/'.join(data['gencase_path'].split('/')[:-1]))
# Save data array on disk. It is saved as a binary file with Pickle.
try:
with open(save_name + "/casedata.dsphdata", 'wb') as picklefile:
pickle.dump(data, picklefile, utils.PICKLE_PROTOCOL)
except Exception as e:
traceback.print_exc()
guiutils.error_dialog(__("There was a problem saving the DSPH information file (casedata.dsphdata)."))
utils.refocus_cwd()
else:
utils.log(__("Saving cancelled."))
def on_save_with_gencase():
""" Saves data into disk and uses GenCase to generate the case files."""
# Save Case as usual so all the data needed for GenCase is on disk
on_save_case()
# Ensure the current working directory is the DesignSPHysics directory
utils.refocus_cwd()
# Use gencase if possible to generate the case final definition
data['gencase_done'] = False
if data['gencase_path'] != "":
# Tries to spawn a process with GenCase to generate the case
process = QtCore.QProcess(fc_main_window)
gencase_full_path = os.getcwd() + "/" + data['gencase_path']
process.setWorkingDirectory(data['project_path'])
process.start(gencase_full_path, [
data['project_path'] + '/' + data['project_name'] + '_Def', data['project_path'] +
'/' + data['project_name'] + '_out/' + data['project_name'],
'-save:+all'
])
process.waitForFinished()
output = str(process.readAllStandardOutput())
error_in_gen_case = False
# If GenCase was successful, check for internal errors
# This is done because a "clean" exit (return 0) of GenCase does not mean that all went correct.
if str(process.exitCode()) == "0":
try:
total_particles_text = output[output.index("Total particles: "):output.index(" (bound=")]
total_particles = int(total_particles_text[total_particles_text.index(": ") + 2:])
data['total_particles'] = total_particles
utils.log(__("Total number of particles exported: ") + str(total_particles))
if total_particles < 300:
utils.warning(__("Are you sure all the parameters are set right? The number of particles is very low ({}). "
"Lower the DP to increase number of particles").format(str(total_particles)))
elif total_particles > 200000:
utils.warning(__("Number of particles is pretty high ({}) "
"and it could take a lot of time to simulate.").format(str(total_particles)))
data['gencase_done'] = True
guiutils.widget_state_config(widget_state_elements, "gencase done")
data["last_number_particles"] = int(total_particles)
guiutils.gencase_completed_dialog(particle_count=total_particles,
detail_text=output.split("================================")[1],
data=data, temp_data=temp_data)
except ValueError:
# Not an expected result. GenCase had a not handled error
error_in_gen_case = True
# If for some reason GenCase failed
if str(process.exitCode()) != "0" or error_in_gen_case:
# Multiple possible causes. Let the user know
gencase_out_file = open(data['project_path'] + '/' + data['project_name'] + '_out/' + data['project_name'] + ".out", "rb")
gencase_failed_dialog = QtGui.QMessageBox()
gencase_failed_dialog.setText(__("Error executing GenCase. Did you add objects to the case?. "
"Another reason could be memory issues. View details for more info."))
gencase_failed_dialog.setDetailedText(gencase_out_file.read().split("================================")[1])
gencase_failed_dialog.setIcon(QtGui.QMessageBox.Critical)
gencase_out_file.close()
gencase_failed_dialog.exec_()
utils.warning(__("GenCase Failed."))
# Save results again so all the data is updated if something changes.
on_save_case()
def on_newdoc_menu(action):
""" Handles the new document button and its dropdown items. """
if __("New") in action.text():
on_new_case()
if __("Import FreeCAD Document") in action.text():
on_new_from_freecad_document()
def on_save_menu(action):
""" Handles the save button and its dropdown items. """
if __("Save and run GenCase") in action.text():
on_save_with_gencase()
if __("Save as...") in action.text():
on_save_case(save_as=True)
def on_load_button():
""" Defines load case button behaviour. This is made so errors can be detected and handled. """
try:
on_load_case()
except ImportError:
guiutils.error_dialog(__("There was an error loading the case"),
__("The case you are trying to load has some data that DesignSPHysics could not"
" load.\n\nDid you make the case in a previous version?"))
on_new_case(prompt=False)
def on_load_case():
"""Defines loading case mechanism.
Load points to a dsphdata custom file, that stores all the relevant info.
If FCStd file is not found the project is considered corrupt."""
# noinspection PyArgumentList
load_name, _ = QtGui.QFileDialog.getOpenFileName(dsph_main_dock, __("Load Case"), QtCore.QDir.homePath(), "casedata.dsphdata")
if load_name == "":
# User pressed cancel. No path is selected.
return
# Check if FCStd file is in there.
load_path_project_folder = "/".join(load_name.split("/")[:-1])
if not os.path.isfile(load_path_project_folder + "/DSPH_Case.FCStd"):
guiutils.error_dialog(__("DSPH_Case.FCStd file not found! Corrupt or moved project. Aborting."))
utils.error(__("DSPH_Case.FCStd file not found! Corrupt or moved project. Aborting."))
return
# Tries to close all documents
if utils.document_count() > 0:
load_success = utils.prompt_close_all_documents()
if not load_success:
return
# Opens the case freecad document
FreeCAD.open(load_path_project_folder + "/DSPH_Case.FCStd")
# Loads own file and sets data and button behaviour
global data
global dp_input
try:
# Previous versions of DesignSPHysics saved the data on disk in an ASCII way (pickle protocol 0), so sometimes
# due to OS changes files would be corrupted. Now it is saved in binary mode so that wouldn't happen. This bit
# of code is to open files which have an error (or corrupted binary files!).
with open(load_name, 'rb') as load_picklefile:
try:
load_disk_data = pickle.load(load_picklefile)
except AttributeError:
guiutils.error_dialog(__("There was an error trying to load the case. This can be due to the project being "
"from another version or an error while saving the case."))
on_new_case(prompt=False)
return
# Remove exec paths from loaded data if user have already correct ones.
_, already_correct = utils.check_executables(data)
if already_correct:
[load_disk_data.pop(x, None) for x in
['gencase_path', 'dsphysics_path', 'partvtk4_path', 'floatinginfo_path', 'computeforces_path',
'measuretool_path', 'isosurface_path', 'boundaryvtk_path']]
# Update data structure with disk loaded one
data.update(load_disk_data)
except (EOFError, ValueError):
guiutils.error_dialog(
__("There was an error importing the case properties. You probably need to set them again."
"\n\n"
"This could be caused due to file corruption, "
"caused by operating system based line endings or ends-of-file, or other related aspects."))
# Fill some data
dp_input.setText(str(data['dp']))
data['project_path'] = load_path_project_folder
data['project_name'] = load_path_project_folder.split("/")[-1]
# Compatibility code. Transform content from previous version to this one.
# Make FloatProperty compatible
data['floating_mks'] = utils.float_list_to_float_property(data['floating_mks'])
data['initials_mks'] = utils.initials_list_to_initials_property(data['initials_mks'])
# Adapt widget state to case info
guiutils.widget_state_config(widget_state_elements, "load base")
if data['gencase_done']:
guiutils.widget_state_config(widget_state_elements, "gencase done")
else:
guiutils.widget_state_config(widget_state_elements, "gencase not done")
if data['simulation_done']:
guiutils.widget_state_config(widget_state_elements, "simulation done")
else:
guiutils.widget_state_config(widget_state_elements, "simulation not done")
# Check executable paths
utils.refocus_cwd()
data, correct_execs = utils.check_executables(data)
if not correct_execs:
guiutils.widget_state_config(widget_state_elements, "execs not correct")
# Update FreeCAD case state
on_tree_item_selection_change()
def on_add_fillbox():
""" Add fillbox group. It consists
in a group with 2 objects inside: a point and a box.
The point represents the fill seed and the box sets
the bounds for the filling"""
fillbox_gp = FreeCAD.getDocument("DSPH_Case").addObject("App::DocumentObjectGroup", "FillBox")
fillbox_point = FreeCAD.ActiveDocument.addObject("Part::Sphere", "FillPoint")
fillbox_limits = FreeCAD.ActiveDocument.addObject("Part::Box", "FillLimit")
fillbox_limits.Length = '1000 mm'
fillbox_limits.Width = '1000 mm'
fillbox_limits.Height = '1000 mm'
fillbox_limits.ViewObject.DisplayMode = "Wireframe"
fillbox_limits.ViewObject.LineColor = (0.00, 0.78, 1.00)
fillbox_point.Radius.Value = 10
fillbox_point.Placement.Base = FreeCAD.Vector(500, 500, 500)
fillbox_point.ViewObject.ShapeColor = (0.00, 0.00, 0.00)
fillbox_gp.addObject(fillbox_limits)
fillbox_gp.addObject(fillbox_point)
FreeCAD.ActiveDocument.recompute()
FreeCADGui.SendMsgToActiveView("ViewFit")
def on_add_damping_zone():
""" Adds a damping zone into the case. It consist on a solid line that rempresents the damping vector
and a dashed line representing the overlimit. It can be adjusted in the damping property window
or changing the lines itselves."""
damping_group = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "DampingZone")
# Limits line
points = [FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(1000, 1000, 1000)]
limits = Draft.makeWire(points, closed=False, face=False, support=None)
Draft.autogroup(limits)
limits.Label = "Limits"
limitsv = FreeCADGui.ActiveDocument.getObject(limits.Name)
limitsv.ShapeColor = (0.32, 1.00, 0.00)
limitsv.LineColor = (0.32, 1.00, 0.00)
limitsv.PointColor = (0.32, 1.00, 0.00)
limitsv.EndArrow = True
limitsv.ArrowSize = "10 mm"
limitsv.ArrowType = "Dot"
# Overlimit line
points = [FreeCAD.Vector(*limits.End), FreeCAD.Vector(1580, 1577.35, 1577.35)]
overlimit = Draft.makeWire(points, closed=False, face=False, support=None)
Draft.autogroup(overlimit)
overlimit.Label = "Overlimit"
overlimitv = FreeCADGui.ActiveDocument.getObject(overlimit.Name)
overlimitv.DrawStyle = "Dotted"
overlimitv.ShapeColor = (0.32, 1.00, 0.00)
overlimitv.LineColor = (0.32, 1.00, 0.00)
overlimitv.PointColor = (0.32, 1.00, 0.00)
overlimitv.EndArrow = True
overlimitv.ArrowSize = "10 mm"
overlimitv.ArrowType = "Dot"
# Add the two lines to the group
damping_group.addObject(limits)
damping_group.addObject(overlimit)
FreeCAD.ActiveDocument.recompute()
FreeCADGui.SendMsgToActiveView("ViewFit")
# Save damping in the main data structure.
data["damping"][damping_group.Name] = Damping()
# Opens damping configuration window to tweak the added damping zone.
guiutils.damping_config_window(data, damping_group.Name)
def on_add_stl():
""" Add STL file. Opens a file opener and allows
the user to set parameters for the import process"""
# TODO: Low priority: This Dialog should be implemented and designed as a class like AddSTLDialog(QtGui.QDialog)
filedialog = QtGui.QFileDialog()
# noinspection PyArgumentList
file_name, _ = filedialog.getOpenFileName(fc_main_window, __("Select STL to import"), QtCore.QDir.homePath(), "STL Files (*.stl)")
if len(file_name) <= 1:
# User didn't select any files
return
# Defines import stl dialog
stl_dialog = QtGui.QDialog()
stl_dialog.setModal(True)
stl_dialog.setWindowTitle(__("Import STL"))
stl_dialog_layout = QtGui.QVBoxLayout()
stl_group = QtGui.QGroupBox(__("Import STL options"))
stl_group_layout = QtGui.QVBoxLayout()
# STL File selection
stl_file_layout = QtGui.QHBoxLayout()
stl_file_label = QtGui.QLabel(__("STL File: "))
stl_file_path = QtGui.QLineEdit()
stl_file_path.setText(file_name)
stl_file_browse = QtGui.QPushButton(__("Browse"))
[stl_file_layout.addWidget(x) for x in [stl_file_label, stl_file_path, stl_file_browse]]
# END STL File selection
# Scaling factor
stl_scaling_layout = QtGui.QHBoxLayout()
stl_scaling_label = QtGui.QLabel(__("Scaling factor: "))
stl_scaling_x_l = QtGui.QLabel("X: ")
stl_scaling_x_e = QtGui.QLineEdit("1")
stl_scaling_y_l = QtGui.QLabel("Y: ")
stl_scaling_y_e = QtGui.QLineEdit("1")
stl_scaling_z_l = QtGui.QLabel("Z: ")
stl_scaling_z_e = QtGui.QLineEdit("1")
[stl_scaling_layout.addWidget(x) for x in [
stl_scaling_label,
stl_scaling_x_l,
stl_scaling_x_e,
stl_scaling_y_l,
stl_scaling_y_e,
stl_scaling_z_l,
stl_scaling_z_e,
]]
# END Scaling factor
# Import object name
stl_objname_layout = QtGui.QHBoxLayout()
stl_objname_label = QtGui.QLabel(__("Import object name: "))
stl_objname_text = QtGui.QLineEdit("ImportedSTL")
[stl_objname_layout.addWidget(x) for x in [stl_objname_label, stl_objname_text]]
# End object name
# Add component layouts to group layout
[stl_group_layout.addLayout(x) for x in [stl_file_layout, stl_scaling_layout, stl_objname_layout]]
stl_group_layout.addStretch(1)
stl_group.setLayout(stl_group_layout)
# Create button layout
stl_button_layout = QtGui.QHBoxLayout()
stl_button_ok = QtGui.QPushButton(__("Import"))
stl_button_cancel = QtGui.QPushButton(__("Cancel"))
stl_button_cancel.clicked.connect(lambda: stl_dialog.reject())
stl_button_layout.addStretch(1)
stl_button_layout.addWidget(stl_button_cancel)
stl_button_layout.addWidget(stl_button_ok)
# Compose main window layout
stl_dialog_layout.addWidget(stl_group)
stl_dialog_layout.addStretch(1)
stl_dialog_layout.addLayout(stl_button_layout)
stl_dialog.setLayout(stl_dialog_layout)
# STL Dialog function definition and connections
def stl_ok_clicked():
""" Defines ok button behaviour"""
[stl_scaling_edit.setText(stl_scaling_edit.text().replace(",", ".")) for stl_scaling_edit in [
stl_scaling_x_e,
stl_scaling_y_e,
stl_scaling_z_e
]]
try:
utils.import_stl(
filename=str(stl_file_path.text()),
scale_x=float(stl_scaling_x_e.text()),
scale_y=float(stl_scaling_y_e.text()),
scale_z=float(stl_scaling_z_e.text()),
name=str(stl_objname_text.text()))
stl_dialog.accept()
except ValueError:
utils.error(__("There was an error. Are you sure you wrote correct float values in the scaling factor?"))
guiutils.error_dialog(__("There was an error. Are you sure you wrote correct float values in the sacaling factor?"))
def stl_dialog_browse():
""" Defines the browse button behaviour."""
# noinspection PyArgumentList
file_name_temp, _ = filedialog.getOpenFileName(fc_main_window, __("Select STL to import"), QtCore.QDir.homePath(), "STL Files (*.stl)")
stl_file_path.setText(file_name_temp)
stl_dialog.raise_()
stl_dialog.activateWindow()
stl_button_cancel.clicked.connect(lambda: stl_dialog.reject())
stl_button_ok.clicked.connect(stl_ok_clicked)
stl_file_browse.clicked.connect(stl_dialog_browse)
stl_dialog.exec_()
def on_import_xml():
""" Imports an already created GenCase/DSPH compatible
file and loads it in the scene. """
guiutils.warning_dialog(__("This feature is experimental. It's meant to help to build a case importing bits from"
"previous, non DesignSPHysics code. This is not intended neither to import all objects "
"nor its properties."))
# noinspection PyArgumentList
import_name, _ = QtGui.QFileDialog.getOpenFileName(dsph_main_dock, __("Import XML"), QtCore.QDir.homePath(), "XML Files (*.xml)")
if import_name == "":
# User pressed cancel. No path is selected.
return
else:
if utils.get_number_of_documents() > 0:
if utils.prompt_close_all_documents():
on_new_case()
else:
return
else:
on_new_case()
config, objects = xmlimporter.import_xml_file(import_name)
# Set Config
dp_input.setText(str(config['dp']))
limits_point_min = config['limits_min']
limits_point_max = config['limits_max']
# noinspection PyArgumentList
FreeCAD.ActiveDocument.getObject('Case_Limits').Placement = FreeCAD.Placement(
FreeCAD.Vector(limits_point_min[0] * 1000, limits_point_min[1] * 1000, limits_point_min[2] * 1000),
FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0))
FreeCAD.ActiveDocument.getObject("Case_Limits").Length = str(limits_point_max[0] - limits_point_min[0]) + ' m'
FreeCAD.ActiveDocument.getObject("Case_Limits").Width = str(limits_point_max[1] - limits_point_min[1]) + ' m'
FreeCAD.ActiveDocument.getObject("Case_Limits").Height = str(limits_point_max[2] - limits_point_min[2]) + ' m'
# Merges and updates current data with the imported one.
data.update(config)
# Add results to DSPH objects
for key, value in objects.iteritems():
add_object_to_sim(key)
data['simobjects'][key] = value
# Change visual properties based on fill mode and type
target_object = FreeCADGui.ActiveDocument.getObject(key)
if "bound" in value[1]:
if "full" in value[2]:
target_object.ShapeColor = (0.80, 0.80, 0.80)
target_object.Transparency = 0
elif "solid" in value[2]:
target_object.ShapeColor = (0.80, 0.80, 0.80)
target_object.Transparency = 0
elif "face" in value[2]:
target_object.ShapeColor = (0.80, 0.80, 0.80)
target_object.Transparency = 80
elif "wire" in value[2]:
target_object.ShapeColor = (0.80, 0.80, 0.80)
target_object.Transparency = 85
if "fluid" in value[1]:
if "full" in value[2]:
target_object.ShapeColor = (0.00, 0.45, 1.00)
target_object.Transparency = 30
elif "solid" in value[2]:
target_object.ShapeColor = (0.00, 0.45, 1.00)
target_object.Transparency = 30
elif "face" in value[2]:
target_object.ShapeColor = (0.00, 0.45, 1.00)
target_object.Transparency = 80
elif "wire" in value[2]:
target_object.ShapeColor = (0.00, 0.45, 1.00)
target_object.Transparency = 85
# Notify change to refresh UI elements related.
on_tree_item_selection_change()
guiutils.info_dialog(__("Importing successful. Note that some objects may not be automatically added to the case, "
"and other may not have its properties correctly applied."))
def on_summary():
""" Handles Case Summary button """
guiutils.case_summary(data)
def on_2d_toggle():
""" Handles Toggle 3D/2D Button. Changes the Case Limits object accordingly. """
if utils.valid_document_environment():
if data['3dmode']:
# Change to 2D
# TODO: Low-priority: This dialog should be implemented as a class like 2DModeConfig(QtGui.QDialog)
y_pos_2d_window = QtGui.QDialog()
y_pos_2d_window.setWindowTitle(__("Set Y position"))
ok_button = QtGui.QPushButton(__("Ok"))
cancel_button = QtGui.QPushButton(__("Cancel"))
# Ok Button handler
def on_ok():
temp_data['3d_width'] = utils.get_fc_object('Case_Limits').Width.Value
try:
utils.get_fc_object('Case_Limits').Placement.Base.y = float(y2_pos_input.text())
except ValueError:
guiutils.error_dialog(__("The Y position that was inserted is not valid."))
utils.get_fc_object('Case_Limits').Width.Value = utils.WIDTH_2D
guiutils.get_fc_view_object('Case_Limits').DisplayMode = 'Flat Lines'