-
Notifications
You must be signed in to change notification settings - Fork 8
/
AtomAgents.py
3015 lines (2244 loc) · 117 KB
/
AtomAgents.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 python
# coding: utf-8
######################################################LLMs#########################################################
import autogen
filter_dict_4o = {"model": ["gpt-4o"]}
filter_dict_4turbo = {"model": ["gpt-4-turbo"]}
config_list_4o = autogen.config_list_from_json(env_or_file="config_list", filter_dict=filter_dict_4o)
config_list_4_turbo = autogen.config_list_from_json(env_or_file="config_list", filter_dict=filter_dict_4turbo)
##################################################################################################################
from typing import Annotated
import re
import numpy as np
import pandas as pd
from functools import reduce
import math
from ase.build import bulk
from ase.io import lammpsdata
from ase.lattice.cubic import BodyCenteredCubic
from ase.lattice.cubic import FaceCenteredCubic
import subprocess
import matplotlib.pyplot as plt
import atomman as am
import os
import json
from IPython.display import display, Markdown
import markdown2
import pdfkit
from datetime import datetime
from autogen import AssistantAgent
from autogen.agentchat.contrib.retrieve_assistant_agent import RetrieveAssistantAgent
from autogen.agentchat.contrib.retrieve_user_proxy_agent import RetrieveUserProxyAgent
from chromadb.utils import embedding_functions
from autogen.agentchat.contrib.multimodal_conversable_agent import MultimodalConversableAgent
from autogen.agentchat.contrib.img_utils import get_pil_image, pil_to_data_uri
from autogen import register_function
from autogen import ConversableAgent
from typing import Dict, List
from autogen import Agent
gpt4o_config = {"config_list": config_list_4o}
gpt4_turbo_config = {"config_list": config_list_4_turbo}
openai_ef = embedding_functions.OpenAIEmbeddingFunction(
api_key=config_list_4o[0]['api_key'],
model_name="text-embedding-3-small"
)
scientist = AssistantAgent(
name="scientist",
system_message='''You are a helpful AI scientist. You are an expert in materials science with a focus on alloys.
Your expertise allows you to propose suggestion about possible correlations between different materials characterisitcs.
Note that your hypotheses should be verifiable using the tools you have access to.
''',
llm_config=gpt4o_config,
description='You follow and execute the plan.'
)
admin_core = autogen.UserProxyAgent(
name="admin_core",
is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("TERMINATE_ALL"),
human_input_mode="ALWAYS",
system_message="admin_core. You pose the task.",
code_execution_config=False,
llm_config=False,
)
engineer_core = AssistantAgent(
name="engineer_core",
system_message='''You are the engineer_core, the central agent in a multi-agent system tasked with solving complex problems.
Task Initiation: When given a task, your first step is to use the "plan_task" tool to generate a comprehensive plan.
Execution: Do not start executing any functions until you receive a plan from the "plan_task". Follow the plan meticulously to ensure all steps are completed as outlined.
Error Handling: If an error occurs due to a human mistake, such as the wrong name of a potential, immediately ask the user for the correct information.
Data Integrity: When calling a function, if you discover that a critical input parameter is missing, prompt the user to provide the necessary data. Do not make assumptions or use your own interpretations to fill in missing information.
By adhering to these guidelines, you ensure that your operations are precise, user-driven, and efficient, contributing effectively to the multi-agent system's goal of solving complex tasks.
''',
llm_config=gpt4o_config,
description='You will solve a problem with the help of a set of tools.'
)
# # Coding agents
# In[81]:
coder_user = autogen.UserProxyAgent(
name="coder_user",
is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("TERMINATE_CODER"),
human_input_mode="NEVER",
system_message="code_user. execute python code.",
llm_config=gpt4o_config,
code_execution_config= {"work_dir": "code_dir",
"use_docker": False},
)
coder = AssistantAgent(
name="coder",
system_message='''coder. Given a task, pleas write python codes to accomplish the task. Write codes in a python block.
The task may involve saving some data as a plot or as a csv file. In this case, ensure the files will be saved to the local computer.
The code needs to be executed by the "coder_user" AI agent, so avoid saying "save the code and execute it in your computer." ''',
llm_config=gpt4o_config,
)
# # Planning agents
# In[82]:
planner = AssistantAgent(
name="planner",
system_message='''You are the Planner Agent.
Suggest a plan for the given task.
Do not write code.
Make sure your plan includes the necessary tools for each step.
Your plan will be reviewed by "critic."
Use only the tools required to accomplish the task, avoiding unnecessary computations and analyses.
Return "TERMINATE_PLAN" when the plan is approved.
''',
llm_config=gpt4_turbo_config,
description='You develop a plan.'
)
critic = AssistantAgent(
name="critic",
system_message=''' You are the Critic Agent.
Review the planner's plan for completeness and accuracy.
Ensure the plan does not include unnecessary functions.
Return "TERMINATE_PLAN" when the plan is approved.
Do not execute any functions.''',
llm_config=gpt4_turbo_config,
description='You review a plan from planner.'
)
admin_plan = autogen.UserProxyAgent(
name="admin_plan",
is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("TERMINATE_PLAN"),
human_input_mode="NEVER",
system_message="admin_plan. You pose the task.",
code_execution_config=False,
llm_config=False,
)
groupchat_plan = autogen.GroupChat(
agents=[planner, admin_plan, critic,#sequence_retriever,
], messages=[], max_round=200, send_introductions=True,
#allowed_or_disallowed_speaker_transitions=allowed_transitions_1, speaker_transitions_type='allowed',
speaker_selection_method='auto',
)
manager_plan = autogen.GroupChatManager(groupchat=groupchat_plan, llm_config=gpt4_turbo_config,
system_message='You dynamically select a speaker based on the current and previous conversations.')
def _reset_agents_glob():
planner.reset()
critic.reset()
def _clear_history_glob():
planner.clear_history()
critic.clear_history()
_reset_agents_glob()
_clear_history_glob()
# # Plot analyzing agents
# In[83]:
admin = autogen.UserProxyAgent(
name="admin",
is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("TERMINATE"),
human_input_mode="NEVER",
system_message="admin. You pose the task. Return 'TERMINATE' in the end when everything is over.",
llm_config=gpt4o_config,
code_execution_config=False,
)
multi_model_agent = MultimodalConversableAgent(name="multi_model_agent",
system_message='''multi_model_agent.
You extract important information from a plot.
''',
llm_config={"config_list": config_list_4o, "temperature": 0.0},
description='Extract important information from the plots.'
)
# # Rag agents
# In[84]:
assistant = RetrieveAssistantAgent(
name="assistant",
system_message="assistant. You are a helpful assistant. You retrieve knowledge from a text. You should pay attention to all the details, specially quantitative data.",
llm_config=gpt4o_config,
)
reviewer = RetrieveAssistantAgent(
name="reviewer",
system_message='''reviewer. double-check the response from the assistant for correctness.
Return 'TERMINATE' in the end when the task is over.''',
llm_config=gpt4o_config,
)
ragproxyagent = RetrieveUserProxyAgent(
human_input_mode="NEVER",
name="ragproxyagent",
retrieve_config={
"task": "qa",
"docs_path": "./code_dir/Mishin_Al_Ni.pdf",
"embedding_function": openai_ef,
"model": "gpt-4o",
"overwrite": True,
"get_or_create": True,
},
code_execution_config=False,
)
groupchat_rag = autogen.GroupChat(
agents=[assistant, reviewer, ragproxyagent, #sequence_retriever,
], messages=[], max_round=20,
speaker_selection_method='auto',
)
manager_rag = autogen.GroupChatManager(groupchat=groupchat_rag, llm_config=gpt4o_config,
system_message='You dynamically select a speaker.')
# # Computation agents
# In[85]:
admin = autogen.UserProxyAgent(
name="admin",
is_termination_msg=lambda x: x.get("content", "") and x.get("content", "").rstrip().endswith("TERMINATE"),
human_input_mode="NEVER",
system_message="admin. You pose the task. Return 'TERMINATE' in the end when everything is over.",
llm_config=gpt4o_config,
code_execution_config=False,
)
engineer = autogen.AssistantAgent(
name="engineer",
system_message = """
engineer. You are a helpful AI assistant. Suggest a plan to solve the problem.
The plan should include the functions and required input parameters.
In the end avoid saying "If you need any further analysis or additional tasks, please let me know!" or similar expressions. Return "TERMINATE" instead.
""",
llm_config=gpt4o_config,
description='You call the tools.',
)
# # Functions executed by core agents
# In[86]:
@admin_core.register_for_execution()
@engineer_core.register_for_llm()
@critic.register_for_llm()
@planner.register_for_llm(description='''Use this function to analyze the differential displacement map and determine the screw dislocation core structure.
Use this function ONLY IF the task requires analyzing screw dislocation core.
Use this function ONLY after the screw dislocation is created using "create_screw_dislocation".''')
def analyze_screw_core(plot_path: Annotated[str, 'path to the differential displacement map plot.'],) -> str:
res = admin.initiate_chat(
multi_model_agent,
clear_history=True,
silent=False,
max_turns=1,
message=f''' This first plot is the dislocation displacement map template showing polarized and unpolarized core structures.\n <img ./0_codes/screw_cores.png>\n\n
Based on this template, determine the screw dislocation core structure for the following plot.\n <img {plot_path}>
''',
)
return res.chat_history[-1]['content']
@admin_core.register_for_execution()
@engineer_core.register_for_llm()
@critic.register_for_llm()
@planner.register_for_llm(description='''Use this function for plot analysis and draw conclusions from the results. To use this function, you should first use use "save_plot_task" to save the plot of the results.''')
def analyze_plot(plot_name: Annotated[str, 'plot name that was saved'], message: Annotated[str, 'Clearly elaborate your request. Start with what the data represent. E.g. "The data shows the variation of .... Is there any correlation between ... and ...?" or "can you draw conclusions from the results." ']) -> str:
res = admin.initiate_chat(
multi_model_agent,
clear_history=True,
silent=False,
max_turns=1,
message=f'''{message}
<img ./code_dir/{plot_name}>
''',
)
return res.chat_history[-1]['content']
@admin_core.register_for_execution()
@engineer_core.register_for_llm()
@critic.register_for_llm()
@scientist.register_for_llm()
@planner.register_for_llm(description='''This function returns the surface energy of the material along a given plane. The plane is usually the crack plane which should be known a priori.
Ensure to include all task details in the "message" as the function does not inherently know them.
Note that for crack problems, the surface_dir must be the crack plane direction.
''')
def computation_task_surface_energy(iter_num: Annotated[int, 'The iteration number indicating how many times the function has been called.'],
message: Annotated[str, 'the query regarding the task that includes all the details.'],
working_directory: Annotated[str, 'a proper name for the computation at each iteration'],
surface_dir: Annotated[list, 'the surface plane that the surface energy is computed for.'],
conc1: Annotated[int, 'concentration of the first element in %'],
conc2: Annotated[int, 'concentration of the second element in %']) -> str:
res = admin.initiate_chat(
engineer,
clear_history=True,
silent=False,
message=f'''{message}\nworking directory: {working_directory}\nsurface plane: {surface_dir}\nconcentration_1: {conc1}\nconcentration_2: {conc2}''',
#summary_method="reflection_with_llm",
#summary_args={"summary_prompt": f"{summary_msg}"}
)
return res.chat_history[-1]['content']
@admin_core.register_for_execution()
@engineer_core.register_for_llm()
@critic.register_for_llm()
@scientist.register_for_llm()
@planner.register_for_llm(description='''Use this function for generating a relaxed structure with a screw dislocation. The function also plots the differential displacement map.
It returns a) the name of the generated structure and b) the path to the associated differential displacement map.
Ensure to include all task details in the "message" as the function does not inherently know them.''')
def computation_task_screw_dislocation(iter_num: Annotated[int, 'The iteration number indicating how many times the function has been called.'],
message: Annotated[str, 'the query regarding the task that includes all the details. Since this is a computation task, start with "Compute XXX" and also indicate "after the computations return YYY'],
working_directory: Annotated[str, 'a proper name for the computation at each iteration'],
conc1: Annotated[int, 'concentration of the first element in %'],
conc2: Annotated[int, 'concentration of the second element in %']) -> str:
#admin.clear_history()
#engineer.clear_history()
#manager.clear_history()
res = admin.initiate_chat(
engineer,
clear_history=True,
silent=False,
message=f'''{message}\nworking directory: {working_directory}\nconcentration_1: {conc1}\nconcentration_2: {conc2}''',
#summary_method="reflection_with_llm",
#summary_args={"summary_prompt": f"{summary_msg}"}
)
return res.chat_history[-1]['content']
@admin_core.register_for_execution()
@engineer_core.register_for_llm()
@critic.register_for_llm()
@scientist.register_for_llm()
@planner.register_for_llm(description='''Use this function for computing the energy barrier against dislocations in random alloys.
It returns the mean and standard deviation of the Peierls barriers distribution.
Additionally, it calculates the mean and standard deviation of the energy changes when a dislocation moves between minima in the alloy.
Ensure to include all task details in the "message" as the function does not inherently know them.''')
def computation_task_NEB(iter_num: Annotated[int, 'The iteration number indicating how many times the function has been called.'],
message: Annotated[str, 'the query regarding the task that includes all the details. Since this is a computation task, start with "Compute XXX" and also indicate "after the computations return YYY'],
working_directory: Annotated[str, 'a proper name for the computation at each iteration'],
conc1: Annotated[int, 'concentration of the first element in %'],
conc2: Annotated[int, 'concentration of the second element in %']) -> str:
#admin.clear_history()
#engineer.clear_history()
#manager.clear_history()
res = admin.initiate_chat(
engineer,
clear_history=True,
silent=False,
message=f'''{message}\nworking directory: {working_directory}\nconcentration_1: {conc1}\nconcentration_2: {conc2}''',
#summary_method="reflection_with_llm",
#summary_args={"summary_prompt": f"{summary_msg}"}
)
return res.chat_history[-1]['content']
@admin_core.register_for_execution()
@engineer_core.register_for_llm(description='''Use this function to develop an implementation plan for a task. Ensure the material type, the interatomic potential, and other essential inputs are provided by the user before calling this function.''')
def plan_task(query: Annotated[str, 'the query regarding the task including all the details.']) -> str:
message = f''' Develop detailed plans for the following task.
{query}
Remember, your responsibility is limited to planning only. You cannot execute any tasks or implement any functions.
'''
res = admin_plan.initiate_chat(
manager_plan,
clear_history=True,
silent=False,
message=message,
summary_method="reflection_with_llm",
summary_args={"summary_prompt": "Return the final revision of the plan with all the details."}
)
return res.summary
@admin_core.register_for_execution()
@engineer_core.register_for_llm()
@critic.register_for_llm()
@scientist.register_for_llm()
@planner.register_for_llm(description='''Use this function for computational tasks that involve performing simulations to compute a material property.
The function should be used for a specific material with a specific composition, not for general purposes.
The task should include all the details about the simulations such as orientations and concentrations. Avoid any abbreviation or abstraction.''')
def computation_task(iter_num: Annotated[int, 'The iteration number indicating how many times the function has been called.'],
message: Annotated[str, 'the query regarding the task. Here, you should include every details.'],
summary_msg: Annotated[str, 'The specific outcome of the computational task.'],
working_directory: Annotated[str, 'a proper and descriptive name for the computation at each iteration'],
conc1: Annotated[int, 'concentration of the first element in %'],
conc2: Annotated[int, 'concentration of the second element in %'],) -> str:
res = admin.initiate_chat(
engineer,
clear_history=True,
silent=False,
message=f'''{message}\nworking directory: {working_directory}\nconcentration_1: {conc1}\nconcentration_2: {conc2}''',
summary_method="reflection_with_llm",
summary_args={"summary_prompt": f"{summary_msg}"}
)
return res.summary
@admin_core.register_for_execution()
@engineer_core.register_for_llm()
@critic.register_for_llm()
@scientist.register_for_llm()
@planner.register_for_llm(description='''"save_image_data". Use this function to plot the results. Use this function if (a) the user specifically asks to plot the data, and (b) if the results need to be deeply analyzed for instance to find specific correlations.
This function is useful for examining trends over the overall data.
The function takes a message describing the data type and a request to save data as an image (.png format), a data_dictionary comprising all the data, and a proper name for the file that will be saved.
The saved plt can be analyzed by "analyze_plot" function.''')
def save_image_data(message: Annotated[str, 'The message should provide a full description of the request regarding saving the data. For instance, "plot the data and save the plot as image"'],
data_dictionary: Annotated[str, 'A dictionary comprising all the data that needs to be saved either as an image. The full data should be provided as a dictionary with proper name, format, and units'],
name: Annotated[str, 'a proper name for the file with a proper format (.png for image) to be saved.']) -> str:
res = coder_user.initiate_chat(
coder,
clear_history=True,
silent=False,
max_turns=2,
message=f'task: {message}\nname of file to be saved: {name}\ndata:{data_dictionary}',
summary_method="reflection_with_llm",
summary_args={"summary_prompt":"Return the name of the file"}
)
return res.summary
@admin_core.register_for_execution()
@engineer_core.register_for_llm()
@critic.register_for_llm()
@scientist.register_for_llm()
@planner.register_for_llm(description='''"save_csv_data". This function saves the tabular results as a csv file.
The function takes a message describing the data type and a request to save data as a csv file (.csv format), a data_dictionary comprising all the data, and a proper name for the file that will be saved.
Only use this function if specifically asked to save the data as csv file.''')
def save_csv_data(message: Annotated[str, 'The message should provide a full description of the request regarding saving the data. For instance, "save the data as a csv file"'],
data_dictionary: Annotated[str, 'A dictionary comprising all the data that needs to be saved either as a csv file. The full data should be provided as a dictionary with proper name, format, and units'],
name: Annotated[str, 'a proper name for the file with a proper format (.csv for table) to be saved.']) -> str:
res = coder_user.initiate_chat(
coder,
clear_history=True,
silent=False,
max_turns=2,
message=f'task: {message}\nname of file to be saved: {name}\ndata:{data_dictionary}',
summary_method="reflection_with_llm",
summary_args={"summary_prompt":"Return the name of the file"}
)
return res.summary
@admin_core.register_for_execution()
@scientist.register_for_llm(description='''Use this function to retrieve knowledge such as material properties from an external source.
Each time, you should ask about a specific element and about a single material property.
Only use this function when you were specifically asked to retrieve knowledge.''')
def retrieve_knowledge(msg: Annotated[str, "the question for the retrieval task. The question should only contain a single element, for instance Al or Ni."]) -> str:
res = ragproxyagent.initiate_chat(
manager_rag,
message=ragproxyagent.message_generator,
problem = msg,
clear_history=True,
silent=False,
summary_method="reflection_with_llm",
summary_args={"summary_prompt":"Return the retrieved knowledge."}
)
return res.summary
@admin_core.register_for_execution()
@engineer_core.register_for_llm()
@scientist.register_for_llm()
@critic.register_for_llm()
@planner.register_for_llm(description='''critical_stress_intensity_cleavage.
This function computes "critical stress intensity for cleavage (KIc).
Important parameters are the surface energy of the crack plane and the elastic constants (C11, C12, C44) which should be computed by simulations.
The crack plane is thus needed for the material of interest.
The function returns critical stress intensity for cleavage in unis of MPa*m^0.5''')
def critical_stress_intensity_cleavage(orient_x: Annotated[list, 'crack propagation direction.'],
orient_y: Annotated[list, 'crack normal plane direction.'],
orient_z: Annotated[list, 'crack front direction'],
c11: Annotated[float, 'C11: material elastic constant (GPa)'],
c12: Annotated[float, 'C12: material elastic constant (GPa)'],
c44: Annotated[float, 'C44: material elastic constant (GPa)'],
gamma_s: Annotated[float, 'surface energy (in units of J/m^2) along crack plane direction obtained from simulations.']) -> str:
# base crystal orientation
e1 = [1, 0, 0]; e2 = [0., 1., 0.]; e3 = [0, 0, 1]
m1 = orient_x
m2 = orient_y
m3 = orient_z
m1 /= np.linalg.norm(m1); m2 /= np.linalg.norm(m2); m3 /= np.linalg.norm(m3)
Q = np.array([[np.matmul(m1,e1), np.matmul(m1,e2), np.matmul(m1,e3)], [np.matmul(m2,e1), np.matmul(m2,e2), np.matmul(m2,e3)], [np.matmul(m3,e1), np.matmul(m3,e2), np.matmul(m3,e3)]])
K1 = np.array([[Q[0,0]**2, Q[0,1]**2, Q[0,2]**2], [Q[1,0]**2, Q[1,1]**2, Q[1,2]**2], [Q[2,0]**2, Q[2,1]**2, Q[2,2]**2]])
K2 = np.array([[Q[0,1]*Q[0,2], Q[0,2]*Q[0,0], Q[0,0]*Q[0,1]], [Q[1,1]*Q[1,2], Q[1,2]*Q[1,0], Q[1,0]*Q[1,1]], [Q[2,1]*Q[2,2], Q[2,2]*Q[2,0], Q[2,0]*Q[2,1]]])
K3 = np.array([[Q[1,0]*Q[2,0], Q[1,1]*Q[2,1], Q[1,2]*Q[2,2]], [Q[2,0]*Q[0,0], Q[2,1]*Q[0,1], Q[2,2]*Q[0,2]], [Q[0,0]*Q[1,0], Q[0,1]*Q[1,1], Q[0,2]*Q[1,2]]])
K4 = np.array([[Q[1,1]*Q[2,2]+Q[1,2]*Q[2,1], Q[1,2]*Q[2,0]+Q[1,0]*Q[2,2], Q[1,0]*Q[2,1]+Q[1,1]*Q[2,0]], [Q[2,1]*Q[0,2]+Q[2,2]*Q[0,1], Q[2,2]*Q[0,0]+Q[2,0]*Q[0,2], Q[2,0]*Q[0,1]+Q[2,1]*Q[0,0]],
[Q[0,1]*Q[1,2]+Q[0,2]*Q[1,1], Q[0,2]*Q[1,0]+Q[0,0]*Q[1,2], Q[0,0]*Q[1,1]+Q[0,1]*Q[1,0]]])
KK1 = np.concatenate((K1, 2*K2), axis=1)
KK2 = np.concatenate((K3, K4), axis=1)
KK = np.concatenate((KK1, KK2), axis=0)
# Material stiffness tensor
C_base = np.array([[c11, c12, c12, 0, 0, 0],
[c12, c11, c12, 0, 0, 0],
[c12, c12, c11, 0, 0, 0],
[0, 0, 0, c44, 0, 0],
[0, 0, 0, 0, c44, 0],
[0, 0, 0, 0, 0, c44]])
C = np.dot(np.dot(KK, C_base), np.transpose(KK)) # Rotation of the material stiffness tensor
A1 = np.linalg.inv(C) #Compliance tensor
b11 = (A1[0,0]*A1[2,2]-A1[0,2]**2)/A1[2,2]
b22 = (A1[1,1]*A1[2,2]-A1[1,2]**2)/A1[2,2]
b12 = (A1[0,1]*A1[2,2]-A1[0,2]*A1[1,2])/A1[2,2]
b66 = (A1[5,5]*A1[2,2]-A1[1,5]**2)/A1[2,2]
Estar = ((b11*b22/2)*(np.sqrt(b22/b11)+(2*b12+b66)/(2*b11)))**(-0.5)
QQ = np.array([[C[0,0], C[0,5], C[0,4]], [C[0,5], C[5,5], C[4,5]], [C[0,4], C[4,5], C[4,4]]])
R = np.array([[C[0,5], C[0,1], C[0,3]], [C[5,5], C[1,5], C[3,5]], [C[4,5], C[1,4], C[3,4]]])
T = np.array([[C[5,5], C[1,5], C[3,5]], [C[1,5], C[1,1], C[1,3]], [C[3,5], C[1,3], C[3,3]]])
N1 = -1 * np.dot(np.linalg.inv(T),np.transpose(R))
N2 = np.linalg.inv(T)
N3 = np.dot(np.dot(R, np.linalg.inv(T)), np.transpose(R)) - QQ
NN1 = np.concatenate((N1, N2), axis=1)
NN2 = np.concatenate((N3, np.transpose(N1)), axis=1)
N = np.concatenate((NN1, NN2), axis=0)
#--- finding eigenvector and eigen values, ...
[v, u] = np.linalg.eig(N) # v - eigenvalues, v - eigenvectors
a1 = [[u[0,0]], [u[1,0]], [u[2,0]]]
pp1 = v[0]
b1 = np.dot(np.transpose(R)+np.dot(pp1, T),a1)
a2 = [[u[0,2]], [u[1,2]], [u[2,2]]]
pp2 = v[2]
b2 = np.dot(np.transpose(R)+np.dot(pp2, T),a2)
a3 = [[u[0,4]], [u[1,4]], [u[2,4]]]
pp3 = v[4]
b3 = np.dot(np.transpose(R)+np.dot(pp3, T),a3)
AA = np.concatenate((a1, a2, a3), axis=1)
BB = np.concatenate((b1, b2, b3), axis=1)
p = np.array([pp1, pp2, pp3])
L = 0.5 * np.real(1j*np.dot(AA,np.linalg.inv(BB)))
lambda_coeff = np.linalg.inv(L)[1,1]
K_GG = np.sqrt(2*gamma_s*lambda_coeff*10**9)*10**(-6)
output_dict = {'Critical fracture toughness (MPa*m^1/2)': f'{K_GG:.3f}',
}
return json.dumps(output_dict)
# # Functions executed by computation agents
# In[ ]:
@admin.register_for_execution()
@engineer.register_for_llm(description='''Use this function to determine crystal orientations based on given orientation(s).''')
def suggest_orientation(orientation: Annotated[list, 'known crystal orientation(s)'],) -> str:
res = coder_user.initiate_chat(
coder,
clear_history=True,
silent=False,
max_turns=2,
message=f'''Suggest a right-handed crystal orientation for the given orientation(s) {orientation}.
Instructions:
1- First choose the simplest vector that is normal to the given vector.
2- Compute the cross-product between the two vectors.
3- Divide the numbers in each vector by their GCD.
''',
summary_method="reflection_with_llm",
summary_args={"summary_prompt":"Return final crystal orientations without explanation."}
)
return res.summary
@admin.register_for_execution()
@engineer.register_for_llm(description='''"create_crystal" function.
Use this function to create crystal structures
An important input parameter is "lat_const" which is the lattice constant and should be computed by "lattice_constant_simulation".''')
def create_crystal(working_folder: Annotated[str, 'working directory for the project'],
output_name: Annotated[str, 'name of the output pristine crystal structure that is generated. Use ".lmp" as the format of the structure.'],
lat_type: Annotated[str, 'lattice structure, fcc or bcc'],
lat_const: Annotated[float, 'lattice constant of the crystal. Should be computed by "lattice_constant_simulation".'],
conc_1: Annotated[int, 'solute concentration of first element in %.'],
conc_2: Annotated[int, 'solute concentration of second element in %. 0 if the material is unary and the potential is not MTP.'],
mtp_pot: Annotated[bool, 'True for MTP potential, False otherwise'],
orient_x: Annotated[list, '''crystal orientation along x, such as [1, -1, 1]'''],
orient_y: Annotated[list, '''crystal orientation along y, such as [0, 1, 0]'''],
orient_z: Annotated[list, '''crystal orientation along z, such as [1, -1, 2]'''],
size_x: Annotated[int, 'size of crystal in lattice units along x'],
size_y: Annotated[int, 'size of crystal in lattice units along y'],
size_z: Annotated[int, 'size of crystal in lattice units along z']) -> str:
try:
os.mkdir(working_folder)
except:
pass
if np.dot(orient_x, orient_y)==0 and (np.dot(orient_x, orient_z)!=0 or np.dot(orient_y, orient_z)!=0):
orient_3 = list(np.cross(orient_x, orient_y))
gcd = gcd_of_vector(orient_3)
if gcd != 0:
orient_3 = [int(xxx / gcd) for xxx in orient_3]
raise ValueError(f"the orientations are not orthogonal. Choose orient_z as {orient_3}")
elif np.dot(orient_x, orient_z)==0 and (np.dot(orient_x, orient_y)!=0 or np.dot(orient_z, orient_y)!=0):
orient_3 = list(np.cross(orient_z, orient_x))
gcd = gcd_of_vector(orient_3)
if gcd != 0:
orient_3 = [int(xxx / gcd) for xxx in orient_3]
raise ValueError(f"the orientations are not orthogonal. Choose orient_y as {orient_3}")
elif np.dot(orient_y, orient_z)==0 and (np.dot(orient_y, orient_x)!=0 or np.dot(orient_z, orient_x)!=0):
orient_3 = list(np.cross(orient_y, orient_z))
gcd = gcd_of_vector(orient_3)
if gcd != 0:
orient_3 = [int(xxx / gcd) for xxx in orient_3]
raise ValueError(f"the orientations are not orthogonal. Choose orient_x as {orient_3}")
assert conc_1+conc_2==100, 'the sum of concentrations should be 100'
#orient_x = ''.join(f"{num}" if num < 0 else f"+{num}" for num in orient_x).replace('+', '')
#orient_y = ''.join(f"{num}" if num < 0 else f"+{num}" for num in orient_y).replace('+', '')
#orient_z = ''.join(f"{num}" if num < 0 else f"+{num}" for num in orient_z).replace('+', '')
file_output_name = f'./{working_folder}/{output_name}'
try:
os.remove(file_output_name)
except FileNotFoundError:
pass
if conc_1==100:
if lat_type=='bcc':
atoms = BodyCenteredCubic(directions=[orient_x, orient_y, orient_z],
size=(size_x, size_y, size_z),
symbol='X',
latticeconstant=lat_const
)
elif lat_type=='fcc':
atoms = FaceCenteredCubic(directions=[orient_x, orient_y, orient_z],
size=(size_x, size_y, size_z),
symbol='X',
latticeconstant=lat_const
)
lammpsdata.write_lammps_data(file_output_name, atoms)
with open(file_output_name) as file:
lines=file.readlines()
new_lines = []
for line in lines:
if re.search(r'Atoms # atomic', line):
# Use a regular expression to split the line while preserving spaces
new_lines.append('Masses\n\n')
new_lines.append('1 1\n\n')
# Identify the item to be changed and modify it
# Assuming the item to change is the second item (index 3 because split includes spaces)
# Reassemble the line
new_lines.append(line)
with open(file_output_name, 'w') as file:
file.writelines(new_lines)
if conc_2==100:
assert mtp_pot==True, 'It sounds you have MTP potential. If not set conc_1=100 and conc_2=0'
if lat_type=='bcc':
atoms = BodyCenteredCubic(directions=[orient_x, orient_y, orient_z],
size=(size_x, size_y, size_z),
symbol='X',
latticeconstant=lat_const
)
elif lat_type=='fcc':
atoms = FaceCenteredCubic(directions=[orient_x, orient_y, orient_z],
size=(size_x, size_y, size_z),
symbol='X',
latticeconstant=lat_const
)
atoms.symbols[0]='Nb'
lammpsdata.write_lammps_data(file_output_name, atoms)
with open(file_output_name) as file:
lines=file.readlines()
new_lines = []
for line in lines:
if re.search(r' 1 1', line):
# Use a regular expression to split the line while preserving spaces
parts = re.split(r'(\s+)', line)
# Identify the item to be changed and modify it
# Assuming the item to change is the second item (index 3 because split includes spaces)
parts[4] = '2'
# Reassemble the line
line = ''.join(parts)
if re.search(r'Atoms # atomic', line):
# Use a regular expression to split the line while preserving spaces
new_lines.append('Masses\n\n')
new_lines.append('1 1\n')
new_lines.append('2 1\n\n')
# Identify the item to be changed and modify it
# Assuming the item to change is the second item (index 3 because split includes spaces)
# Reassemble the line
new_lines.append(line)
with open(file_output_name, 'w') as file:
file.writelines(new_lines)
if conc_1!=100 and conc_1!=0:
if lat_type=='bcc':
atoms = BodyCenteredCubic(directions=[orient_x, orient_y, orient_z],
size=(size_x, size_y, size_z),
symbol='X',
latticeconstant=lat_const
)
elif lat_type=='fcc':
atoms = FaceCenteredCubic(directions=[orient_x, orient_y, orient_z],
size=(size_x, size_y, size_z),
symbol='X',
latticeconstant=lat_const
)
atom_indices = np.random.permutation(len(atoms))
atoms.symbols[atom_indices[:int(len(atoms)*conc_1/100)]] = 'X'
atoms.symbols[atom_indices[int(len(atoms)*conc_1/100):len(atoms)]] = 'Y'
lammpsdata.write_lammps_data(file_output_name, atoms)
with open(file_output_name) as file:
lines=file.readlines()
new_lines = []
for line in lines:
if re.search(r'Atoms # atomic', line):
# Use a regular expression to split the line while preserving spaces
new_lines.append('Masses\n\n')
new_lines.append('1 1\n')
new_lines.append('2 1\n\n')
# Identify the item to be changed and modify it
# Assuming the item to change is the second item (index 3 because split includes spaces)
# Reassemble the line
new_lines.append(line)
with open(file_output_name, 'w') as file:
file.writelines(new_lines)
return output_name
@admin.register_for_execution()
@engineer.register_for_llm(description='''This function creates the potential file necessary for the atomistic simulations.
The function takes the name of working_folder, the pair_style, and the pair_coeff. You should construct write pair_coeff using the potential names given by the user.
The potentials are stored in "../potential_repository", so always use "../potential_repository/" before the potential name.
The examples of the pair_style are eam/alloy for eam potential, meam for meam potential, and mlip {mtp file name} for MTP potential.
Note that for MTP potential, the potential name goes in pair_stype not pair_coeff.''')
def create_potential_file(working_folder: Annotated[str, 'working directory for the project'],
pair_style_text: Annotated[str, 'interatomic potential pair style, e.g. eam/alloy for EAM potential and mlip {potential name for MTP file. Use "../potential_repository/" before MTP potential name}'],
#atom_chemical_name: Annotated[str, 'chemical name of atom'],
pair_coeff_text: Annotated[str, 'pair coefficient for the lammps simulations. Use "../potential_repository/" before the potential name. e.g. "* * {potential name} {atom chemical name}", "* * library.meam {chemical name} atom.meam {chemical name}", and "* *" for MTP potential (no argument needed). ']) -> str:
try:
os.mkdir(working_folder)
except:
pass
content = f'pair_style {pair_style_text} \npair_coeff {pair_coeff_text}'
print(content)
assert re.search('../potential_repository/', content), "../potential_repository/ not defined"
assert re.search('\* \*', content), "pair_coeff must have * *"
if re.search('pair_coeff pair_coeff', content):
raise TypeError('''The function returned an extra 'pair_coeff' string. Only one 'pair_coeff * *' is expected."! Please provide "pair_coeff_text" without "pair_coeff''')
if re.search('pair_style pair_style', content):
raise TypeError("pair_style is repeated!")
filename = f'./{working_folder}/potential.inp'
with open(filename, 'w') as file:
file.write(content)
return filename
@admin.register_for_execution()
@engineer.register_for_llm(description='''Use this function to create a working folder for the project where the data will be stored.
Choose a proper name that best describes the project.''')
def create_working_folder(working_folder: Annotated[str, "a proper name for the project's working directory. Choose a name that best describes the project."],)-> str:
try:
os.mkdir(working_folder)
except:
pass
return f"Working folder: {working_folder}"
@admin.register_for_execution()
@engineer.register_for_llm(description='''This function computes the lattice constant of the material crucial for atomistic simulations.
The lattice constant is computed by energy minimization and pressure relaxation.
To use this function, the potential file should be created by "create_potential" function, first.''')
def lattice_constant_simulation(working_folder: Annotated[str, 'name of working folder at which the simulation results will be saved. This name should be identical for all functions for the same task.'],
lat_type: Annotated[str, 'lattice structure, fcc or bcc'],
lat_const_guess: Annotated[float, 'An approximate guess for the lattice constant'],
conc_1: Annotated[int, 'solute concentration of first element in %.'],
conc_2: Annotated[int, 'solute concentration of second element in %. 0 if material is unary and the potential is not MTP.'],
mtp_pot: Annotated[bool, 'True for MTP potential, False otherwise'],
num_cpus: Annotated[int, 'number of CPUs (slots) available for the simulations']=4,
thermo_time: Annotated[int, 'output thermodynamics every this timesteps']=100,
dump_time: Annotated[int, 'dump on timesteps which are multiples of this']=1000) -> dict:
try:
os.mkdir(working_folder)
except:
pass
assert conc_1+conc_2==100, 'the concentrations should sum up to 100.'
if conc_1!=0 and conc_1!=100:
total_samples = 3
else:
total_samples = 1
nx = 10
ny = 10
nz = 10
lat_all = []
for num_samples in range(total_samples):
if num_samples==0:
input_data_file_name = create_crystal(working_folder,
'lattice_constant_alloy.lmp',
lat_type, lat_const_guess,
conc_1, conc_2, mtp_pot,
[1, 0 ,0], [0, 1, 0], [0, 0, 1],
nx, ny, nz)
else:
input_data_file_name = create_crystal(working_folder, 'lattice_constant_alloy.lmp',
lat_type, np.mean(lat_all),
conc_1, conc_2, mtp_pot,
[1, 0 ,0], [0, 1, 0], [0, 0, 1],
nx, ny, nz)
bash_script = f'''
clear
units metal
dimension 3
boundary p p p
atom_style atomic
shell cd ./{working_folder}
variable data_file string "{input_data_file_name}"
variable potential_file string "potential.inp"
variable thermo_time equal {thermo_time}
variable dump_time equal {dump_time}
read_data ${{data_file}}
include ${{potential_file}}
compute peratom all pe/atom
compute pe all pe
thermo_style custom step temp pe vol lx ly lz press pxx pyy pzz fnorm
thermo_modify format float %5.5g
# print thermo every N times
thermo ${{thermo_time}}
shell mkdir dump_lattice_constant
reset_timestep 0
dump first_dump all custom 1 dump.lattice.initial.{num_samples} id type x y z c_peratom
run 0
undump first_dump
variable dump_id equal 1
variable N equal 100 #dump on timesteps which are multiples of N
variable dump_out_name string "./dump_lattice_constant/dump.out"
dump ${{dump_id}} all custom ${{dump_time}} ${{dump_out_name}}.* id type x y z c_peratom
variable p1 equal "step"
variable p2 equal "count(all)"
variable p3 equal "lx"
variable p4 equal "ly"
variable p5 equal "lz"
variable p6 equal "pe"
variable p7 equal "vol"
fix 1 all box/relax aniso 0
min_style cg
minimize 0 1e-5 2000 20000
unfix 1
min_style cg
minimize 0 1e-5 20000 20000
fix 1 all box/relax aniso 0
min_style cg
minimize 0 1e-5 2000 20000
unfix 1
min_style cg
minimize 0 1e-5 20000 20000
fix 1 all box/relax aniso 0
min_style cg
minimize 0 1e-5 2000 20000
unfix 1
min_style cg
minimize 0 1e-5 20000 20000
fix extra all print 100 "${{p1}} ${{p2}} ${{p3}} ${{p4}} ${{p5}} ${{p6}} ${{p7}}" file "relaxed_outputs.csv" title "#step numb_atoms a_x a_y a_z pe vol" screen no
reset_timestep 0
dump last_dump all custom 1 dump.lattice.final.{num_samples} id type x y z c_peratom
run 0
unfix extra
undump last_dump
quit
'''
lammps_script = f'{working_folder}/lmp_lattice_constant_script.in'
with open(lammps_script, 'w') as f:
f.write(bash_script)
text = f'''from lammps import lammps
lmp = lammps(cmdargs=['-log', f'./{working_folder}/log.lammps.lattice.constant.alloy'])
lmp.file(f'{working_folder}/lmp_lattice_constant_script.in')
lmp.command('quit')
lmp.close()'''
with open('run_lammps_lattice_constant.py', 'w') as f:
f.write(text)
command = f'mpirun -np {num_cpus} python run_lammps_lattice_constant.py'