diff --git a/TODO.md b/TODO.md index 95c3878..8264622 100644 --- a/TODO.md +++ b/TODO.md @@ -43,7 +43,7 @@ ### GUI: - [ ] Better help in configuration. A foldable dedicated text browser widget to show info? -- [ ] Implement UserTally options/definition in GUI +- [X] Implement UserTally options/definition in GUI - [X] Getting Started - [X] About @@ -99,8 +99,8 @@ Tally: ### User - GUI: - - [ ] Add a database of pre-defined materials with full definition: composition, density, Ed, etc - - [ ] Pressing add material presents to the user a selection/search function to discover & select + - [X] Add a database of pre-defined materials with full definition: composition, density, Ed, etc + - [X] Pressing add material presents to the user a selection/search function to discover & select - CLI: - [X] Block Ctrl-C signal so that data is saved before the program is aborted diff --git a/source/gui/CMakeLists.txt b/source/gui/CMakeLists.txt index fb80061..f333eab 100644 --- a/source/gui/CMakeLists.txt +++ b/source/gui/CMakeLists.txt @@ -9,6 +9,8 @@ set(GUI_HEADERS simulationoptionsview.h periodictablewidget.h materialsdefview.h + materialdatabasedialog.h + usertallyview.h optionsmodel.h floatlineedit.h mydatawidgetmapper.h @@ -31,6 +33,8 @@ set(GUI_SOURCES simulationoptionsview.cpp periodictablewidget.cpp materialsdefview.cpp + materialdatabasedialog.cpp + usertallyview.cpp optionsmodel.cpp floatlineedit.cpp mydatawidgetmapper.cpp diff --git a/source/gui/assets/data/materials_database.json b/source/gui/assets/data/materials_database.json new file mode 100644 index 0000000..301f749 --- /dev/null +++ b/source/gui/assets/data/materials_database.json @@ -0,0 +1,503 @@ +[ + { + "id": "Fe", + "description": "Iron (BCC, standard SRIM values)", + "density": 7.874, + "composition": [ + { "symbol": "Fe", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 4.28, "Er": 40.0, "Rc": 0.946 } + ] + }, + { + "id": "Fe-low", + "description": "Iron (low-damage model)", + "density": 7.874, + "composition": [ + { "symbol": "Fe", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 4.28, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "FeO", + "description": "Iron oxide", + "density": 5.745, + "composition": [ + { "symbol": "Fe", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 4.28, "Er": 40.0, "Rc": 0.946 }, + { "symbol": "O", "X": 1.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "Fe2O3", + "description": "Hematite", + "density": 5.24, + "composition": [ + { "symbol": "Fe", "X": 2.0, "Ed": 40.0, "El": 3.0, "Es": 4.28, "Er": 40.0, "Rc": 0.946 }, + { "symbol": "O", "X": 3.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "Fe3O4", + "description": "Magnetite", + "density": 5.17, + "composition": [ + { "symbol": "Fe", "X": 3.0, "Ed": 40.0, "El": 3.0, "Es": 4.28, "Er": 40.0, "Rc": 0.946 }, + { "symbol": "O", "X": 4.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "Si", + "description": "Silicon", + "density": 2.329, + "composition": [ + { "symbol": "Si", "X": 1.0, "Ed": 15.0, "El": 2.0, "Es": 4.7, "Er": 15.0, "Rc": 0.946 } + ] + }, + { + "id": "SiO2", + "description": "Silicon dioxide (amorphous)", + "density": 2.2, + "composition": [ + { "symbol": "Si", "X": 1.0, "Ed": 15.0, "El": 2.0, "Es": 4.7, "Er": 15.0, "Rc": 0.946 }, + { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "SiC", + "description": "Silicon carbide", + "density": 3.21, + "composition": [ + { "symbol": "Si", "X": 1.0, "Ed": 20.0, "El": 3.0, "Es": 4.7, "Er": 20.0, "Rc": 0.946 }, + { "symbol": "C", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 7.4, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "C", + "description": "Carbon (graphite)", + "density": 2.267, + "composition": [ + { "symbol": "C", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 7.4, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "C-diamond", + "description": "Carbon (diamond)", + "density": 3.51, + "composition": [ + { "symbol": "C", "X": 1.0, "Ed": 43.0, "El": 3.0, "Es": 7.4, "Er": 43.0, "Rc": 0.946 } + ] + }, + { + "id": "Al", + "description": "Aluminum", + "density": 2.70, + "composition": [ + { "symbol": "Al", "X": 1.0, "Ed": 25.0, "El": 3.36, "Es": 3.36, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "Al2O3", + "description": "Alumina", + "density": 3.97, + "composition": [ + { "symbol": "Al", "X": 2.0, "Ed": 25.0, "El": 3.36, "Es": 3.36, "Er": 25.0, "Rc": 0.946 }, + { "symbol": "O", "X": 3.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "AlN", + "description": "Aluminum nitride", + "density": 3.26, + "composition": [ + { "symbol": "Al", "X": 1.0, "Ed": 25.0, "El": 3.36, "Es": 3.36, "Er": 25.0, "Rc": 0.946 }, + { "symbol": "N", "X": 1.0, "Ed": 10.0, "El": 2.0, "Es": 2.0, "Er": 10.0, "Rc": 0.946 } + ] + }, + { + "id": "Cu", + "description": "Copper", + "density": 8.96, + "composition": [ + { "symbol": "Cu", "X": 1.0, "Ed": 30.0, "El": 3.49, "Es": 3.49, "Er": 30.0, "Rc": 0.946 } + ] + }, + { + "id": "Ni", + "description": "Nickel", + "density": 8.90, + "composition": [ + { "symbol": "Ni", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 4.5, "Er": 40.0, "Rc": 0.946 } + ] + }, + { + "id": "Cr", + "description": "Chromium", + "density": 7.19, + "composition": [ + { "symbol": "Cr", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 4.0, "Er": 40.0, "Rc": 0.946 } + ] + }, + { + "id": "NiCr", + "description": "Nickel-chromium alloy", + "density": 8.4, + "composition": [ + { "symbol": "Ni", "X": 0.8, "Ed": 40.0, "El": 3.0, "Es": 4.5, "Er": 40.0, "Rc": 0.946 }, + { "symbol": "Cr", "X": 0.2, "Ed": 40.0, "El": 3.0, "Es": 4.0, "Er": 40.0, "Rc": 0.946 } + ] + }, + { + "id": "SS304", + "description": "Stainless steel 304", + "density": 8.0, + "composition": [ + { "symbol": "Fe", "X": 0.70, "Ed": 40.0, "El": 3.0, "Es": 4.28, "Er": 40.0, "Rc": 0.946 }, + { "symbol": "Cr", "X": 0.19, "Ed": 40.0, "El": 3.0, "Es": 4.0, "Er": 40.0, "Rc": 0.946 }, + { "symbol": "Ni", "X": 0.11, "Ed": 40.0, "El": 3.0, "Es": 4.5, "Er": 40.0, "Rc": 0.946 } + ] + }, + { + "id": "W", + "description": "Tungsten", + "density": 19.25, + "composition": [ + { "symbol": "W", "X": 1.0, "Ed": 90.0, "El": 3.0, "Es": 8.79, "Er": 90.0, "Rc": 0.946 } + ] + }, + { + "id": "WC", + "description": "Tungsten carbide", + "density": 15.63, + "composition": [ + { "symbol": "W", "X": 1.0, "Ed": 90.0, "El": 3.0, "Es": 8.79, "Er": 90.0, "Rc": 0.946 }, + { "symbol": "C", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 7.4, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "Au", + "description": "Gold", + "density": 19.32, + "composition": [ + { "symbol": "Au", "X": 1.0, "Ed": 25.0, "El": 3.81, "Es": 3.81, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "Ag", + "description": "Silver", + "density": 10.49, + "composition": [ + { "symbol": "Ag", "X": 1.0, "Ed": 25.0, "El": 2.95, "Es": 2.95, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "Pt", + "description": "Platinum", + "density": 21.45, + "composition": [ + { "symbol": "Pt", "X": 1.0, "Ed": 30.0, "El": 3.5, "Es": 3.5, "Er": 30.0, "Rc": 0.946 } + ] + }, + { + "id": "Pd", + "description": "Palladium", + "density": 12.02, + "composition": [ + { "symbol": "Pd", "X": 1.0, "Ed": 30.0, "El": 3.0, "Es": 3.0, "Er": 30.0, "Rc": 0.946 } + ] + }, + { + "id": "Pb", + "description": "Lead", + "density": 11.34, + "composition": [ + { "symbol": "Pb", "X": 1.0, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "PbWO4", + "description": "Lead tungstate", + "density": 8.28, + "composition": [ + { "symbol": "Pb", "X": 1.0, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 }, + { "symbol": "W", "X": 1.0, "Ed": 90.0, "El": 3.0, "Es": 8.79, "Er": 90.0, "Rc": 0.946 }, + { "symbol": "O", "X": 4.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "Ti", + "description": "Titanium", + "density": 4.506, + "composition": [ + { "symbol": "Ti", "X": 1.0, "Ed": 30.0, "El": 3.0, "Es": 4.85, "Er": 30.0, "Rc": 0.946 } + ] + }, + { + "id": "TiO2", + "description": "Titanium dioxide", + "density": 4.23, + "composition": [ + { "symbol": "Ti", "X": 1.0, "Ed": 30.0, "El": 3.0, "Es": 4.85, "Er": 30.0, "Rc": 0.946 }, + { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "Zr", + "description": "Zirconium", + "density": 6.52, + "composition": [ + { "symbol": "Zr", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 6.25, "Er": 40.0, "Rc": 0.946 } + ] + }, + { + "id": "ZrO2", + "description": "Zirconia", + "density": 5.68, + "composition": [ + { "symbol": "Zr", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 6.25, "Er": 40.0, "Rc": 0.946 }, + { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "Hf", + "description": "Hafnium", + "density": 13.31, + "composition": [ + { "symbol": "Hf", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 6.0, "Er": 40.0, "Rc": 0.946 } + ] + }, + { + "id": "HfO2", + "description": "Hafnia", + "density": 9.68, + "composition": [ + { "symbol": "Hf", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 6.0, "Er": 40.0, "Rc": 0.946 }, + { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "Y2O3", + "description": "Yttria", + "density": 5.01, + "composition": [ + { "symbol": "Y", "X": 2.0, "Ed": 25.0, "El": 3.0, "Es": 3.0, "Er": 25.0, "Rc": 0.946 }, + { "symbol": "O", "X": 3.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "CeO2", + "description": "Ceria", + "density": 7.22, + "composition": [ + { "symbol": "Ce", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 3.0, "Er": 25.0, "Rc": 0.946 }, + { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "UO2", + "description": "Uranium dioxide", + "density": 10.97, + "composition": [ + { "symbol": "U", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 6.0, "Er": 40.0, "Rc": 0.946 }, + { "symbol": "O", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "GaAs", + "description": "Gallium arsenide", + "density": 5.32, + "composition": [ + { "symbol": "Ga", "X": 1.0, "Ed": 10.0, "El": 3.0, "Es": 3.0, "Er": 10.0, "Rc": 0.946 }, + { "symbol": "As", "X": 1.0, "Ed": 10.0, "El": 3.0, "Es": 3.0, "Er": 10.0, "Rc": 0.946 } + ] + }, + { + "id": "GaN", + "description": "Gallium nitride", + "density": 6.15, + "composition": [ + { "symbol": "Ga", "X": 1.0, "Ed": 10.0, "El": 3.0, "Es": 3.0, "Er": 10.0, "Rc": 0.946 }, + { "symbol": "N", "X": 1.0, "Ed": 10.0, "El": 2.0, "Es": 2.0, "Er": 10.0, "Rc": 0.946 } + ] + }, + { + "id": "InP", + "description": "Indium phosphide", + "density": 4.81, + "composition": [ + { "symbol": "In", "X": 1.0, "Ed": 10.0, "El": 3.0, "Es": 3.0, "Er": 10.0, "Rc": 0.946 }, + { "symbol": "P", "X": 1.0, "Ed": 10.0, "El": 3.0, "Es": 3.0, "Er": 10.0, "Rc": 0.946 } + ] + }, + { + "id": "Ge", + "description": "Germanium", + "density": 5.32, + "composition": [ + { "symbol": "Ge", "X": 1.0, "Ed": 15.0, "El": 3.0, "Es": 3.0, "Er": 15.0, "Rc": 0.946 } + ] + }, + { + "id": "Li", + "description": "Lithium", + "density": 0.534, + "composition": [ + { "symbol": "Li", "X": 1.0, "Ed": 10.0, "El": 2.0, "Es": 2.0, "Er": 10.0, "Rc": 0.946 } + ] + }, + { + "id": "Be", + "description": "Beryllium", + "density": 1.85, + "composition": [ + { "symbol": "Be", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 3.0, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "B", + "description": "Boron", + "density": 2.34, + "composition": [ + { "symbol": "B", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 3.0, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "BN", + "description": "Boron nitride", + "density": 2.1, + "composition": [ + { "symbol": "B", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 3.0, "Er": 25.0, "Rc": 0.946 }, + { "symbol": "N", "X": 1.0, "Ed": 10.0, "El": 2.0, "Es": 2.0, "Er": 10.0, "Rc": 0.946 } + ] + }, + { + "id": "Mg", + "description": "Magnesium", + "density": 1.74, + "composition": [ + { "symbol": "Mg", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 2.9, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "MgO", + "description": "Magnesium oxide", + "density": 3.58, + "composition": [ + { "symbol": "Mg", "X": 1.0, "Ed": 25.0, "El": 3.0, "Es": 2.9, "Er": 25.0, "Rc": 0.946 }, + { "symbol": "O", "X": 1.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "Ca", + "description": "Calcium", + "density": 1.55, + "composition": [ + { "symbol": "Ca", "X": 1.0, "Ed": 20.0, "El": 2.0, "Es": 2.0, "Er": 20.0, "Rc": 0.946 } + ] + }, + { + "id": "CaO", + "description": "Calcium oxide", + "density": 3.34, + "composition": [ + { "symbol": "Ca", "X": 1.0, "Ed": 20.0, "El": 2.0, "Es": 2.0, "Er": 20.0, "Rc": 0.946 }, + { "symbol": "O", "X": 1.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "Co", + "description": "Cobalt", + "density": 8.90, + "composition": [ + { "symbol": "Co", "X": 1.0, "Ed": 35.0, "El": 3.0, "Es": 3.0, "Er": 35.0, "Rc": 0.946 } + ] + }, + { + "id": "Mn", + "description": "Manganese", + "density": 7.21, + "composition": [ + { "symbol": "Mn", "X": 1.0, "Ed": 35.0, "El": 3.0, "Es": 3.0, "Er": 35.0, "Rc": 0.946 } + ] + }, + { + "id": "V", + "description": "Vanadium", + "density": 6.11, + "composition": [ + { "symbol": "V", "X": 1.0, "Ed": 35.0, "El": 3.0, "Es": 3.0, "Er": 35.0, "Rc": 0.946 } + ] + }, + { + "id": "Nb", + "description": "Niobium", + "density": 8.57, + "composition": [ + { "symbol": "Nb", "X": 1.0, "Ed": 40.0, "El": 3.0, "Es": 3.0, "Er": 40.0, "Rc": 0.946 } + ] + }, + { + "id": "Mo", + "description": "Molybdenum", + "density": 10.28, + "composition": [ + { "symbol": "Mo", "X": 1.0, "Ed": 60.0, "El": 3.0, "Es": 5.0, "Er": 60.0, "Rc": 0.946 } + ] + }, + { + "id": "Ta", + "description": "Tantalum", + "density": 16.65, + "composition": [ + { "symbol": "Ta", "X": 1.0, "Ed": 90.0, "El": 3.0, "Es": 8.0, "Er": 90.0, "Rc": 0.946 } + ] + }, + { + "id": "Sn", + "description": "Tin", + "density": 7.31, + "composition": [ + { "symbol": "Sn", "X": 1.0, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "Zn", + "description": "Zinc", + "density": 7.14, + "composition": [ + { "symbol": "Zn", "X": 1.0, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "CuZn", + "description": "Brass (Cu-Zn)", + "density": 8.5, + "composition": [ + { "symbol": "Cu", "X": 0.7, "Ed": 30.0, "El": 3.49, "Es": 3.49, "Er": 30.0, "Rc": 0.946 }, + { "symbol": "Zn", "X": 0.3, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "CuSn", + "description": "Bronze (Cu-Sn)", + "density": 8.8, + "composition": [ + { "symbol": "Cu", "X": 0.9, "Ed": 30.0, "El": 3.49, "Es": 3.49, "Er": 30.0, "Rc": 0.946 }, + { "symbol": "Sn", "X": 0.1, "Ed": 25.0, "El": 2.0, "Es": 2.0, "Er": 25.0, "Rc": 0.946 } + ] + }, + { + "id": "H2O", + "description": "Water", + "density": 1.0, + "composition": [ + { "symbol": "H", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }, + { "symbol": "O", "X": 1.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + }, + { + "id": "Ice", + "description": "Water ice", + "density": 0.92, + "composition": [ + { "symbol": "H", "X": 2.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 }, + { "symbol": "O", "X": 1.0, "Ed": 2.0, "El": 2.0, "Es": 2.0, "Er": 2.0, "Rc": 0.946 } + ] + } +] diff --git a/source/gui/assets/data/tally_templates.json b/source/gui/assets/data/tally_templates.json new file mode 100644 index 0000000..5e85e93 --- /dev/null +++ b/source/gui/assets/data/tally_templates.json @@ -0,0 +1,77 @@ +[ + { + "id": "DepthProfile", + "description": "Ion implantation depth distribution along x-axis (100 bins, 0-100 nm)", + "event": "IonStop", + "bins": { "x": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, + 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, + 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, + 30.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, + 40.0, 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0, + 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, + 60.0, 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, + 70.0, 71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0, + 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, + 90.0, 91.0, 92.0, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, 100.0] } + }, + { + "id": "AngularDistribution", + "description": "Angular distribution of exiting ions (direction cosines nx, ny)", + "event": "IonExit", + "bins": { + "nx": [-1.0, -0.9, -0.8, -0.6, -0.4, -0.2, 0.0, 0.2, 0.4, 0.6, 0.8, 0.9, 1.0], + "ny": [-1.0, -0.8, -0.6, -0.4, -0.2, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0] + } + }, + { + "id": "EnergySpectrum", + "description": "Energy spectrum of stopped ions (logarithmic bins, 1 eV - 1 MeV)", + "event": "IonStop", + "bins": { "E": [1.0, 2.0, 5.0, 10.0, 20.0, 50.0, 100.0, 200.0, 500.0, + 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0, 100000.0, + 200000.0, 500000.0, 1000000.0] } + }, + { + "id": "ImplantationMap", + "description": "2D implantation position map (x-depth vs y-lateral, 50x20 bins)", + "event": "IonStop", + "bins": { + "x": [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0, + 20.0, 22.0, 24.0, 26.0, 28.0, 30.0, 32.0, 34.0, 36.0, 38.0, + 40.0, 42.0, 44.0, 46.0, 48.0, 50.0, 52.0, 54.0, 56.0, 58.0, + 60.0, 62.0, 64.0, 66.0, 68.0, 70.0, 72.0, 74.0, 76.0, 78.0, + 80.0, 82.0, 84.0, 86.0, 88.0, 90.0, 92.0, 94.0, 96.0, 98.0, 100.0], + "y": [-50.0, -45.0, -40.0, -35.0, -30.0, -25.0, -20.0, -15.0, -10.0, -5.0, + 0.0, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0] + } + }, + { + "id": "VacancyDepthProfile", + "description": "Vacancy depth distribution along x-axis (100 bins, 0-100 nm)", + "event": "Vacancy", + "bins": { "x": [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, + 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, + 20.0, 21.0, 22.0, 23.0, 24.0, 25.0, 26.0, 27.0, 28.0, 29.0, + 30.0, 31.0, 32.0, 33.0, 34.0, 35.0, 36.0, 37.0, 38.0, 39.0, + 40.0, 41.0, 42.0, 43.0, 44.0, 45.0, 46.0, 47.0, 48.0, 49.0, + 50.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 57.0, 58.0, 59.0, + 60.0, 61.0, 62.0, 63.0, 64.0, 65.0, 66.0, 67.0, 68.0, 69.0, + 70.0, 71.0, 72.0, 73.0, 74.0, 75.0, 76.0, 77.0, 78.0, 79.0, + 80.0, 81.0, 82.0, 83.0, 84.0, 85.0, 86.0, 87.0, 88.0, 89.0, + 90.0, 91.0, 92.0, 93.0, 94.0, 95.0, 96.0, 97.0, 98.0, 99.0, 100.0] } + }, + { + "id": "LateralSpread", + "description": "Cylindrical radius r vs depth x (lateral spread map, 20x50 bins)", + "event": "IonStop", + "bins": { + "r": [0.0, 2.5, 5.0, 7.5, 10.0, 12.5, 15.0, 17.5, 20.0, 22.5, + 25.0, 27.5, 30.0, 32.5, 35.0, 37.5, 40.0, 42.5, 45.0, 47.5, 50.0], + "x": [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0, + 20.0, 22.0, 24.0, 26.0, 28.0, 30.0, 32.0, 34.0, 36.0, 38.0, + 40.0, 42.0, 44.0, 46.0, 48.0, 50.0, 52.0, 54.0, 56.0, 58.0, + 60.0, 62.0, 64.0, 66.0, 68.0, 70.0, 72.0, 74.0, 76.0, 78.0, + 80.0, 82.0, 84.0, 86.0, 88.0, 90.0, 92.0, 94.0, 96.0, 98.0, 100.0] + } + } +] diff --git a/source/gui/assets/ionicons/copy-outline.svg b/source/gui/assets/ionicons/copy-outline.svg new file mode 100644 index 0000000..ef3cb11 --- /dev/null +++ b/source/gui/assets/ionicons/copy-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/gui/assets/lucide/database-search.svg b/source/gui/assets/lucide/database-search.svg new file mode 100644 index 0000000..8ced514 --- /dev/null +++ b/source/gui/assets/lucide/database-search.svg @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/source/gui/materialdatabasedialog.cpp b/source/gui/materialdatabasedialog.cpp new file mode 100644 index 0000000..3b346df --- /dev/null +++ b/source/gui/materialdatabasedialog.cpp @@ -0,0 +1,256 @@ +#include "materialdatabasedialog.h" + +#include "json_defs_p.h" +#include "periodic_table.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +const char *kDbPath = ":/assets/data/materials_database.json"; + +bool containsCaseInsensitive(const QString &haystack, const QString &needle) +{ + if (needle.trimmed().isEmpty()) + return true; + return haystack.contains(needle, Qt::CaseInsensitive); +} + +QString joinMaterialElements(const material::material_desc_t &mat) +{ + QStringList symbols; + for (const auto &a : mat.composition) + symbols << QString::fromStdString(a.element.symbol); + symbols.removeDuplicates(); + return symbols.join(", "); +} + +} // namespace + +MaterialDatabaseDialog::MaterialDatabaseDialog(QWidget *parent) : QDialog(parent) +{ + setWindowTitle("Material Database"); + resize(900, 520); + + QVBoxLayout *root = new QVBoxLayout(this); + + QHBoxLayout *filters = new QHBoxLayout; + filters->addWidget(new QLabel("Name filter:")); + nameFilter_ = new QLineEdit; + nameFilter_->setPlaceholderText("iron"); + filters->addWidget(nameFilter_, 1); + filters->addSpacing(10); + filters->addWidget(new QLabel("Element filter:")); + elementFilter_ = new QLineEdit; + elementFilter_->setPlaceholderText("Fe"); + filters->addWidget(elementFilter_); + root->addLayout(filters); + + QHBoxLayout *content = new QHBoxLayout; + listWidget_ = new QListWidget; + listWidget_->setMinimumWidth(320); + content->addWidget(listWidget_, 1); + + QVBoxLayout *preview = new QVBoxLayout; + previewTitle_ = new QLabel("Preview"); + densityLabel_ = new QLabel("Density:"); + elementsLabel_ = new QLabel("Elements:"); + + preview->addWidget(previewTitle_); + preview->addWidget(densityLabel_); + preview->addWidget(elementsLabel_); + + compositionTable_ = new QTableWidget; + compositionTable_->setColumnCount(7); + compositionTable_->setHorizontalHeaderLabels( + { "Symbol", "X", "Ed", "El", "Es", "Er", "Rc" }); + compositionTable_->verticalHeader()->setVisible(false); + compositionTable_->setEditTriggers(QAbstractItemView::NoEditTriggers); + compositionTable_->setSelectionMode(QAbstractItemView::NoSelection); + compositionTable_->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); + preview->addWidget(compositionTable_, 1); + + content->addLayout(preview, 2); + root->addLayout(content, 1); + + QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Cancel); + acceptButton_ = box->addButton("Add to simulation", QDialogButtonBox::AcceptRole); + acceptButton_->setEnabled(false); + connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); + root->addWidget(box); + + connect(nameFilter_, &QLineEdit::textChanged, this, &MaterialDatabaseDialog::applyFilters); + connect(elementFilter_, &QLineEdit::textChanged, this, &MaterialDatabaseDialog::applyFilters); + connect(listWidget_, &QListWidget::currentRowChanged, this, + &MaterialDatabaseDialog::onCurrentMaterialChanged); + + if (!loadDatabase()) { + QMessageBox::warning(this, "Material database", + "Failed to load embedded materials database."); + } + rebuildList(); +} + +material::material_desc_t MaterialDatabaseDialog::selectedMaterial() const +{ + int row = listWidget_->currentRow(); + if (row < 0 || row >= static_cast(visibleRows_.size())) + return material::material_desc_t(); + + return allEntries_[visibleRows_[row]].mat; +} + +bool MaterialDatabaseDialog::loadDatabase() +{ + allEntries_.clear(); + + QFile f(kDbPath); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) + return false; + + ojson j; + try { + j = ojson::parse(f.readAll().toStdString(), nullptr, true, true); + } catch (...) { + return false; + } + + if (!j.is_array()) + return false; + + for (const auto &node : j) { + if (!node.is_object() || !node.contains("id") || !node.contains("composition")) + continue; + + DbEntry e; + e.mat.id = node.value("id", std::string("Material")); + e.mat.density = node.value("density", 1.0f); + e.mat.color = node.value("color", std::string("#55aaff")); + + const std::string description = node.value("description", std::string()); + e.description = QString::fromStdString(description); + + const auto &comp = node["composition"]; + if (comp.is_array()) { + for (const auto &atomNode : comp) { + atom::parameters a; + std::string symbol = atomNode.value("symbol", std::string("H")); + a.element.symbol = symbol; + const auto &pt = periodic_table::at(symbol); + if (pt.is_valid()) { + a.element.atomic_number = pt.Z; + a.element.atomic_mass = static_cast(pt.mass); + } + + a.X = atomNode.value("X", 1.0f); + a.Ed = atomNode.value("Ed", 40.0f); + a.El = atomNode.value("El", 3.0f); + a.Es = atomNode.value("Es", 10.0f); + a.Er = atomNode.value("Er", 40.0f); + a.Rc = atomNode.value("Rc", 0.946f); + e.mat.composition.push_back(a); + } + } + + e.displayText = QString::fromStdString(e.mat.id); + if (!e.description.isEmpty()) + e.displayText += " - " + e.description; + + allEntries_.push_back(e); + } + + return !allEntries_.empty(); +} + +void MaterialDatabaseDialog::rebuildList() +{ + listWidget_->clear(); + visibleRows_.clear(); + + const QString nameNeedle = nameFilter_->text().trimmed(); + const QString elementNeedle = elementFilter_->text().trimmed(); + + for (int i = 0; i < static_cast(allEntries_.size()); ++i) { + const auto &entry = allEntries_[i]; + + QString nameText = entry.displayText + " " + QString::fromStdString(entry.mat.id); + if (!containsCaseInsensitive(nameText, nameNeedle)) + continue; + + if (!elementNeedle.isEmpty()) { + bool elementMatch = false; + for (const auto &a : entry.mat.composition) { + const QString symbol = QString::fromStdString(a.element.symbol); + if (symbol.startsWith(elementNeedle, Qt::CaseInsensitive)) { + elementMatch = true; + break; + } + } + if (!elementMatch) + continue; + } + + visibleRows_.push_back(i); + listWidget_->addItem(entry.displayText); + } + + if (listWidget_->count() > 0) + listWidget_->setCurrentRow(0); + else + updatePreview(-1); +} + +void MaterialDatabaseDialog::updatePreview(int row) +{ + compositionTable_->setRowCount(0); + + if (row < 0 || row >= static_cast(visibleRows_.size())) { + previewTitle_->setText("Preview"); + densityLabel_->setText("Density:"); + elementsLabel_->setText("Elements:"); + acceptButton_->setEnabled(false); + return; + } + + const auto &entry = allEntries_[visibleRows_[row]]; + const auto &mat = entry.mat; + + previewTitle_->setText(QString("Preview: %1").arg(mat.id.c_str())); + densityLabel_->setText(QString("Density: %1 g/cm^3").arg(mat.density)); + elementsLabel_->setText(QString("Elements: %1").arg(joinMaterialElements(mat))); + + compositionTable_->setRowCount(static_cast(mat.composition.size())); + for (int i = 0; i < static_cast(mat.composition.size()); ++i) { + const auto &a = mat.composition[i]; + compositionTable_->setItem(i, 0, new QTableWidgetItem(QString::fromStdString(a.element.symbol))); + compositionTable_->setItem(i, 1, new QTableWidgetItem(QString::number(a.X))); + compositionTable_->setItem(i, 2, new QTableWidgetItem(QString::number(a.Ed))); + compositionTable_->setItem(i, 3, new QTableWidgetItem(QString::number(a.El))); + compositionTable_->setItem(i, 4, new QTableWidgetItem(QString::number(a.Es))); + compositionTable_->setItem(i, 5, new QTableWidgetItem(QString::number(a.Er))); + compositionTable_->setItem(i, 6, new QTableWidgetItem(QString::number(a.Rc))); + } + + acceptButton_->setEnabled(true); +} + +void MaterialDatabaseDialog::applyFilters() +{ + rebuildList(); +} + +void MaterialDatabaseDialog::onCurrentMaterialChanged(int row) +{ + updatePreview(row); +} diff --git a/source/gui/materialdatabasedialog.h b/source/gui/materialdatabasedialog.h new file mode 100644 index 0000000..d6d4de5 --- /dev/null +++ b/source/gui/materialdatabasedialog.h @@ -0,0 +1,54 @@ +#ifndef MATERIALDATABASEDIALOG_H +#define MATERIALDATABASEDIALOG_H + +#include +#include + +#include + +#include "target.h" + +class QListWidget; +class QLabel; +class QLineEdit; +class QTableWidget; +class QPushButton; + +class MaterialDatabaseDialog : public QDialog +{ + Q_OBJECT + +public: + explicit MaterialDatabaseDialog(QWidget *parent = nullptr); + + material::material_desc_t selectedMaterial() const; + +private slots: + void applyFilters(); + void onCurrentMaterialChanged(int row); + +private: + struct DbEntry { + material::material_desc_t mat; + QString description; + QString displayText; + }; + + bool loadDatabase(); + void rebuildList(); + void updatePreview(int row); + + QLineEdit *nameFilter_; + QLineEdit *elementFilter_; + QListWidget *listWidget_; + QLabel *previewTitle_; + QLabel *densityLabel_; + QLabel *elementsLabel_; + QTableWidget *compositionTable_; + QPushButton *acceptButton_; + + std::vector allEntries_; + std::vector visibleRows_; +}; + +#endif // MATERIALDATABASEDIALOG_H diff --git a/source/gui/materialsdefview.cpp b/source/gui/materialsdefview.cpp index b223715..35ea04d 100644 --- a/source/gui/materialsdefview.cpp +++ b/source/gui/materialsdefview.cpp @@ -3,6 +3,7 @@ #include "periodic_table.h" #include "periodictablewidget.h" #include "floatlineedit.h" +#include "materialdatabasedialog.h" #include "optionsmodel.h" #include @@ -20,6 +21,7 @@ #include #include #include +#include void MaterialsDefView::setBtMatColor(const QColor &clr) { @@ -44,27 +46,35 @@ MaterialsDefView::MaterialsDefView(OptionsModel *m, QWidget *parent) : QWidget{ QFormLayout *flayout = new QFormLayout; cbMaterialID = new MyComboBox; - cbMaterialID->setPlaceholderText("Material id"); + cbMaterialID->setPlaceholderText("Add material or search database..."); cbMaterialID->setMinimumContentsLength(15); + cbMaterialID->setEnabled(false); + cbMaterialID->setToolTip( + "Select material. If empty, use 'Add material from database' or 'Add new material'."); connect(cbMaterialID, &MyComboBox::doubleClicked, this, &MaterialsDefView::editMaterialName); - connect(cbMaterialID, &MyComboBox::currentTextChanged, this, + connect(cbMaterialID, QOverload::of(&QComboBox::currentIndexChanged), this, &MaterialsDefView::updateSelectedMaterial); + btDbMaterial = new QToolButton; + btDbMaterial->setIcon(QIcon(":/assets/lucide/database-search.svg")); + btDbMaterial->setToolTip("Add material from database"); + btAddMaterial = new QToolButton; btAddMaterial->setIcon(QIcon(":/assets/ionicons/add-outline.svg")); - btAddMaterial->setToolTip("Add Material"); + btAddMaterial->setToolTip("Add new material"); btDelMaterial = new QToolButton; btDelMaterial->setIcon(QIcon(":/assets/ionicons/remove-outline.svg")); - btDelMaterial->setToolTip("Remove Material"); + btDelMaterial->setToolTip("Remove material"); btDelMaterial->setEnabled(false); btEdtMaterial = new QToolButton; btEdtMaterial->setIcon(QIcon(":/assets/ionicons/create-outline.svg")); - btEdtMaterial->setToolTip("Edit Material's Name"); + btEdtMaterial->setToolTip("Rename material"); btEdtMaterial->setEnabled(false); + connect(btDbMaterial, &QToolButton::clicked, this, &MaterialsDefView::addFromDatabase); connect(btAddMaterial, &QToolButton::clicked, this, &MaterialsDefView::addMaterial); connect(btDelMaterial, &QToolButton::clicked, this, &MaterialsDefView::removeMaterial); connect(btEdtMaterial, &QToolButton::clicked, this, &MaterialsDefView::editMaterialName); @@ -73,6 +83,7 @@ MaterialsDefView::MaterialsDefView(OptionsModel *m, QWidget *parent) : QWidget{ sbDensity->setMinimum(0.001); sbDensity->setMaximum(100.0); sbDensity->setDecimals(4); + sbDensity->setEnabled(false); connect(sbDensity, SIGNAL(valueChanged(double)), this, SLOT(setDensity(double))); btMatColor = new QToolButton; @@ -80,9 +91,11 @@ MaterialsDefView::MaterialsDefView(OptionsModel *m, QWidget *parent) : QWidget{ btMatColor->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); setBtMatColor(Qt::darkBlue); btMatColor->setToolTip("Select display color for this material"); + btMatColor->setEnabled(false); connect(btMatColor, &QToolButton::pressed, this, &MaterialsDefView::selectColor); hbox->addWidget(cbMaterialID); + hbox->addWidget(btDbMaterial); hbox->addWidget(btAddMaterial); hbox->addWidget(btDelMaterial); hbox->addWidget(btEdtMaterial); @@ -105,6 +118,26 @@ MaterialsDefView::MaterialsDefView(OptionsModel *m, QWidget *parent) : QWidget{ setLayout(vbox); } +void MaterialsDefView::addFromDatabase() +{ + MaterialDatabaseDialog dlg(this); + if (dlg.exec() != QDialog::Accepted) + return; + + material::material_desc_t mat = dlg.selectedMaterial(); + if (mat.id.empty()) + return; + + auto &materials = model_->options()->Target.materials; + materials.push_back(mat); + setWidgetData(); + cbMaterialID->setCurrentIndex(cbMaterialID->count() - 1); + updateSelectedMaterial(); + model_->notifyDataChanged(materialsIndex_); + + emit materialsChanged(); +} + void MaterialsDefView::addMaterial() { bool ok; @@ -117,10 +150,9 @@ void MaterialsDefView::addMaterial() newMaterial.id = id.toStdString(); materials.push_back(newMaterial); setWidgetData(); // widgets updated - cbMaterialID->setCurrentText(id); - // fake setData just to let model_ know that - // underlying data changed - model_->setData(materialsIndex_, QVariant()); + cbMaterialID->setCurrentIndex(cbMaterialID->count() - 1); + updateSelectedMaterial(); + model_->notifyDataChanged(materialsIndex_); emit materialsChanged(); } @@ -140,9 +172,7 @@ void MaterialsDefView::editMaterialName() cbMaterialID->setItemText(i, id); material::material_desc_t &m = model_->options()->Target.materials[i]; m.id = id.toStdString(); - // fake setData just to let model_ know that - // underlying data changed - model_->setData(materialsIndex_, QVariant()); + model_->notifyDataChanged(materialsIndex_); } setValueData(); // update material name } @@ -160,10 +190,7 @@ void MaterialsDefView::removeMaterial() auto &materials = model_->options()->Target.materials; materials.erase(materials.begin() + i); setWidgetData(); // widgets updated - // fake setData just to let model_ know that - // underlying data changed - model_->setData(materialsIndex_, QVariant()); - + model_->notifyDataChanged(materialsIndex_); emit materialsChanged(); } } @@ -172,7 +199,7 @@ void MaterialsDefView::setWidgetData() { auto &materials = model_->options()->Target.materials; - int i = cbMaterialID->currentIndex(); + int prevIdx = cbMaterialID->currentIndex(); cbMaterialID->blockSignals(true); cbMaterialID->clear(); @@ -180,29 +207,39 @@ void MaterialsDefView::setWidgetData() materialsView->setMaterialIdx(); if (materials.empty()) { + cbMaterialID->setPlaceholderText("No materials - use + or DB button"); + cbMaterialID->setEnabled(false); + cbMaterialID->setCurrentIndex(-1); + sbDensity->setEnabled(false); + btMatColor->setEnabled(false); + btDelMaterial->setEnabled(false); + btEdtMaterial->setEnabled(false); cbMaterialID->blockSignals(false); return; } - // copy materials to combo box + // Copy materials to combo box and disambiguate duplicate IDs in display text. int n = materials.size(); + QHash idCount; for (int k = 0; k < n; ++k) { - cbMaterialID->addItem(QString::fromStdString(materials[k].id)); + QString id = QString::fromStdString(materials[k].id).trimmed(); + if (id.isEmpty()) + id = QString("Material #%1").arg(k + 1); + int occ = ++idCount[id]; + QString displayId = (occ > 1) ? QString("%1 (%2)").arg(id).arg(occ) : id; + cbMaterialID->addItem(displayId); } - // update selection if out of bounds - if (n) { - if (i < 0) - i = 0; - else if (i >= n) - i = n - 1; - } else - i = -1; - - // set data to selected material + // Always select the first material if previously empty, else keep previous index if valid + int i = (prevIdx < 0 || prevIdx >= n) ? 0 : prevIdx; cbMaterialID->setCurrentIndex(i); + + cbMaterialID->setEnabled(true); + sbDensity->setEnabled(true); + btMatColor->setEnabled(true); btDelMaterial->setEnabled(i >= 0); btEdtMaterial->setEnabled(i >= 0); + sbDensity->blockSignals(true); sbDensity->setValue(materials[i].density); sbDensity->blockSignals(false); @@ -215,15 +252,31 @@ void MaterialsDefView::setValueData() { } void MaterialsDefView::updateSelectedMaterial() { + auto &materials = model_->options()->Target.materials; + if (materials.empty()) { + sbDensity->clear(); + sbDensity->setEnabled(false); + btMatColor->setEnabled(false); + materialsView->setMaterialIdx(); + btDelMaterial->setEnabled(false); + btEdtMaterial->setEnabled(false); + return; + } + int i = cbMaterialID->currentIndex(); - if (i < 0) { // no selection + if (i < 0 || i >= materials.size()) { // no valid selection + cbMaterialID->setCurrentIndex(-1); sbDensity->clear(); + sbDensity->setEnabled(false); + btMatColor->setEnabled(false); materialsView->setMaterialIdx(); } else { - const material::material_desc_t &mat = model_->options()->Target.materials[i]; + const material::material_desc_t &mat = materials[i]; sbDensity->blockSignals(true); sbDensity->setValue(mat.density); sbDensity->blockSignals(false); + sbDensity->setEnabled(true); + btMatColor->setEnabled(true); setBtMatColor(QColor(mat.color.c_str())); materialsView->setMaterialIdx(i); } @@ -245,9 +298,7 @@ void MaterialsDefView::setDensity(double v) QString matid = cbMaterialID->currentText(); material::material_desc_t &mat = materials[i]; mat.density = v; - // fake setData just to let model_ know that - // underlying data changed - model_->setData(materialsIndex_, QVariant()); + model_->notifyDataChanged(materialsIndex_); } void MaterialsDefView::selectColor() @@ -269,9 +320,7 @@ void MaterialsDefView::selectColor() if (clr.isValid()) { mat.color = clr.name(QColor::HexArgb).toStdString(); setBtMatColor(clr); - // fake setData just to let model_ know that - // underlying data changed - model_->setData(materialsIndex_, QVariant()); + model_->notifyDataChanged(materialsIndex_); emit materialsChanged(); } @@ -431,9 +480,7 @@ bool MaterialCompositionModel::setData(const QModelIndex &index, const QVariant default:; } - // fake setData just to let model_ know that - // underlying data changed - model_->setData(materialsIndex_, QVariant()); + model_->notifyDataChanged(materialsIndex_); return true; } @@ -451,9 +498,7 @@ bool MaterialCompositionModel::insertRows(int position, int rows, const QModelIn mat->composition.push_back(atom::parameters()); endInsertRows(); - // fake setData just to let model_ know that - // underlying data changed - model_->setData(materialsIndex_, QVariant()); + model_->notifyDataChanged(materialsIndex_); return true; } @@ -473,9 +518,7 @@ bool MaterialCompositionModel::removeRows(int position, int rows, const QModelIn mat->composition.erase(mat->composition.begin() + position); endRemoveRows(); - // fake setData just to let model_ know that - // underlying data changed - model_->setData(materialsIndex_, QVariant()); + model_->notifyDataChanged(materialsIndex_); return true; } diff --git a/source/gui/materialsdefview.h b/source/gui/materialsdefview.h index 6e3c888..b5668f9 100644 --- a/source/gui/materialsdefview.h +++ b/source/gui/materialsdefview.h @@ -23,6 +23,7 @@ class MaterialsDefView : public QWidget Q_OBJECT MyComboBox *cbMaterialID; + QToolButton *btDbMaterial; QToolButton *btAddMaterial; QToolButton *btDelMaterial; QToolButton *btEdtMaterial; @@ -42,6 +43,7 @@ class MaterialsDefView : public QWidget void materialsChanged(); public slots: + void addFromDatabase(); void addMaterial(); void removeMaterial(); void editMaterialName(); diff --git a/source/gui/opentrim.qrc b/source/gui/opentrim.qrc index bd0ff0d..d056227 100644 --- a/source/gui/opentrim.qrc +++ b/source/gui/opentrim.qrc @@ -26,6 +26,9 @@ assets/ionicons/list-outline.png assets/ionicons/bar-chart-outline.png assets/lucide/chart-line.svg + assets/lucide/database-search.svg + assets/data/materials_database.json + assets/data/tally_templates.json md/quick_start.md md/images/intro.png md/images/intro22.png @@ -37,6 +40,7 @@ assets/ionicons/open-outline.svg assets/3d/cubeFlat.obj assets/ionicons/create-outline.svg + assets/ionicons/copy-outline.svg examples/270keV_He_on_C.json diff --git a/source/gui/optionsmodel.cpp b/source/gui/optionsmodel.cpp index 9a5ef4a..746a032 100644 --- a/source/gui/optionsmodel.cpp +++ b/source/gui/optionsmodel.cpp @@ -67,9 +67,19 @@ QVariant OptionsItem::value() const } bool OptionsItem::setValue(const QVariant &v) { + QString vnew = v.toString(); + if (vnew.trimmed().isEmpty()) { + const QString k = key().trimmed(); + const QString jp = QString::fromStdString(jpath_); + if (k == "materials" || k == "regions" || k == "UserTally" + || jp.endsWith("/materials") || jp.endsWith("/regions") || jp.endsWith("/UserTally")) { + vnew = "[]"; + } + } + QVariant v0 = value(); - if (v0.toString() != v.toString()) { - set_(v.toString()); + if (v0.toString() != vnew) { + set_(vnew); return true; } return false; @@ -112,6 +122,21 @@ bool OptionsItem::get_(QString &qs) const bool OptionsItem::set_(const QString &qs) { std::string s = qs.toStdString(); + if (qs.trimmed().isEmpty()) { + const QString jp = QString::fromStdString(jpath_); + if (jp.endsWith("/materials") || jp.endsWith("/regions") || jp.endsWith("/UserTally")) { + s = "[]"; + } + + QString current; + if (s.empty() && get_(current)) { + const QString trimmed = current.trimmed(); + if (trimmed.startsWith('[')) + s = "[]"; + else if (trimmed.startsWith('{')) + s = "{}"; + } + } std::ostringstream os; bool ret = options_->set(jpath_, s, &os); if (!ret) { @@ -450,17 +475,25 @@ QVariant OptionsModel::data(const QModelIndex &index, int role) const } bool OptionsModel::setData(const QModelIndex &index, const QVariant &value, int role) { - // int col = index.column(); if (Qt::EditRole == role) { - // if (col == 1) { + if (!index.isValid() || index.column() != 1 || !value.isValid()) + return false; + OptionsItem *item = static_cast(index.internalPointer()); + // Base OptionsItem represents struct/array containers; these should never be edited directly. + if (typeid(*item) == typeid(OptionsItem)) + return false; + if (item->setValue(value)) emit dataChanged(index, index, { Qt::EditRole }); return true; - //} } return false; } +void OptionsModel::notifyDataChanged(const QModelIndex &index) +{ + emit dataChanged(index, index, { Qt::EditRole }); +} QVariant OptionsModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) @@ -552,10 +585,16 @@ int OptionsModel::columnCount(const QModelIndex &parent) const Qt::ItemFlags OptionsModel::flags(const QModelIndex &index) const { - if (index.column() == 1) - return Qt::ItemIsEditable | QAbstractItemModel::flags(index); - else - return QAbstractItemModel::flags(index); + Qt::ItemFlags baseFlags = QAbstractItemModel::flags(index); + if (!index.isValid() || index.column() != 1) + return baseFlags; + + OptionsItem *item = static_cast(index.internalPointer()); + // Only typed option nodes are editable. Container nodes (struct/array) are read-only. + if (item && typeid(*item) != typeid(OptionsItem)) + return baseFlags | Qt::ItemIsEditable; + + return baseFlags; } OptionsItem *OptionsModel::getItem(const QModelIndex &index) const diff --git a/source/gui/optionsmodel.h b/source/gui/optionsmodel.h index 41f4c86..a3dd628 100644 --- a/source/gui/optionsmodel.h +++ b/source/gui/optionsmodel.h @@ -206,6 +206,7 @@ class OptionsModel : public QAbstractItemModel Qt::ItemFlags flags(const QModelIndex &index) const override; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + void notifyDataChanged(const QModelIndex &index); OptionsItem *getItem(const QModelIndex &index) const; diff --git a/source/gui/regionsview.cpp b/source/gui/regionsview.cpp index d9fae3c..a234a66 100644 --- a/source/gui/regionsview.cpp +++ b/source/gui/regionsview.cpp @@ -137,9 +137,7 @@ bool RegionsModel::setData(const QModelIndex &index, const QVariant &value, int break; } - // fake setData just to let model_ know that - // underlying data changed - model_->setData(regionsIndex_, QVariant()); + model_->notifyDataChanged(regionsIndex_); emit dataChanged(index, index); @@ -161,9 +159,7 @@ bool RegionsModel::insertRows(int position, int rows, const QModelIndex &parent) opt->Target.regions.push_back(reg); endInsertRows(); - // fake setData just to let model_ know that - // underlying data changed - model_->setData(regionsIndex_, QVariant()); + model_->notifyDataChanged(regionsIndex_); return true; } @@ -181,9 +177,7 @@ bool RegionsModel::removeRows(int position, int rows, const QModelIndex &parent) regions.erase(it); endRemoveRows(); - // fake setData just to let model_ know that - // underlying data changed - model_->setData(regionsIndex_, QVariant()); + model_->notifyDataChanged(regionsIndex_); return true; } @@ -215,9 +209,7 @@ bool RegionsModel::moveRow(int from, int to) regions.insert(regions.begin() + to, reg); endInsertRows(); - // fake setData just to let model_ know that - // underlying data changed - model_->setData(regionsIndex_, QVariant()); + model_->notifyDataChanged(regionsIndex_); return true; } diff --git a/source/gui/simulationoptionsview.cpp b/source/gui/simulationoptionsview.cpp index 3e14ba9..4195730 100644 --- a/source/gui/simulationoptionsview.cpp +++ b/source/gui/simulationoptionsview.cpp @@ -3,6 +3,7 @@ #include "periodic_table.h" #include "periodictablewidget.h" #include "materialsdefview.h" +#include "usertallyview.h" #include "regionsview.h" #include "optionsmodel.h" #include "mydatawidgetmapper.h" @@ -72,7 +73,8 @@ SimulationOptionsView::SimulationOptionsView(MainUI *iui, QWidget *parent) else if (category == "Target") widget = createTargetTab(idx); else if (category == "UserTally") { - // TODO !! + userTallyView_ = new UserTallyView(model); + widget = userTallyView_; } else widget = createTab(idx); @@ -193,6 +195,8 @@ void SimulationOptionsView::revert() mapper->model()->setOptions(opt); mapper->revert(); materialsView->setWidgetData(); + if (userTallyView_) + userTallyView_->setWidgetData(); regionsView->revert(); jsonView->setPlainText(QString::fromStdString(ionsui->driverObj()->json())); @@ -443,6 +447,8 @@ void SimulationOptionsView::onDriverStatusChanged() mapper->setEnabled(isreset); btSelectIon->setEnabled(isreset); materialsView->setEnabled(isreset); + if (userTallyView_) + userTallyView_->setEnabled(isreset); regionsView->setEnabled(isreset); if (isreset) applyRules(); diff --git a/source/gui/simulationoptionsview.h b/source/gui/simulationoptionsview.h index a470cb3..99e8062 100644 --- a/source/gui/simulationoptionsview.h +++ b/source/gui/simulationoptionsview.h @@ -13,6 +13,7 @@ class QLineEdit; class MyDataWidgetMapper; class MaterialsDefView; +class UserTallyView; class RegionsView; class SimBoxView; class OptionsModel; @@ -74,6 +75,7 @@ public slots: QLineEdit *simTitle; MyDataWidgetMapper *mapper; MaterialsDefView *materialsView; + UserTallyView *userTallyView_{ nullptr }; RegionsView *regionsView; SimBoxView *simBoxView; QDialogButtonBox *buttonBox; diff --git a/source/gui/usertallyview.cpp b/source/gui/usertallyview.cpp new file mode 100644 index 0000000..a8a99a5 --- /dev/null +++ b/source/gui/usertallyview.cpp @@ -0,0 +1,1126 @@ +#include "usertallyview.h" + +#include "json_defs_p.h" +#include "optionsmodel.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { +const char *kTemplatePath = ":/assets/data/tally_templates.json"; + +struct VariableInfo { + const char *name; + const char *description; +}; + +const VariableInfo kVars[] = { + { "x", "Position x" }, { "y", "Position y" }, + { "z", "Position z" }, { "r", "Radius" }, + { "rho", "Cylindrical rho" }, { "cosTheta", "Direction cosine theta" }, + { "nx", "Direction x" }, { "ny", "Direction y" }, + { "nz", "Direction z" }, { "E", "Ion energy" }, + { "Tdam", "Damage energy" }, { "V", "Vacancies" }, + { "atom_id", "Atomic species" }, { "recoil_id", "Recoil generation" } +}; + +Event parseEvent(const QString &name) +{ + if (name == "IonExit") + return Event::IonExit; + if (name == "Vacancy") + return Event::Vacancy; + if (name == "Replacement") + return Event::Replacement; + if (name == "CascadeComplete") + return Event::CascadeComplete; + if (name == "BoundaryCrossing") + return Event::BoundaryCrossing; + return Event::IonStop; +} + +QString eventToString(Event ev) +{ + return event_name(ev); +} + +void setSpin3(QDoubleSpinBox *sb[3], const vector3 &v) +{ + sb[0]->setValue(v.x()); + sb[1]->setValue(v.y()); + sb[2]->setValue(v.z()); +} + +vector3 spin3Value(QDoubleSpinBox *sb[3]) +{ + return vector3(sb[0]->value(), sb[1]->value(), sb[2]->value()); +} + +bool isLabFrame(const coord_sys &cs) +{ + const vector3 O0(0.f, 0.f, 0.f); + const vector3 Z0(0.f, 0.f, 1.f); + const vector3 XZ0(1.f, 0.f, 1.f); + return cs.origin == O0 && cs.zaxis == Z0 && cs.xzvector == XZ0; +} + +std::vector binsToRows(const user_tally::bin_var_t &bins) +{ + std::vector rows; + auto pushIf = [&rows](const char *name, const std::vector &v) { + if (v.size() >= 2) + rows.push_back({ name, BinVariableModel::edgesToString(v) }); + }; + + pushIf("x", bins.x); + pushIf("y", bins.y); + pushIf("z", bins.z); + pushIf("r", bins.r); + pushIf("rho", bins.rho); + pushIf("cosTheta", bins.cosTheta); + pushIf("nx", bins.nx); + pushIf("ny", bins.ny); + pushIf("nz", bins.nz); + pushIf("E", bins.E); + pushIf("Tdam", bins.Tdam); + pushIf("V", bins.V); + pushIf("atom_id", bins.atom_id); + pushIf("recoil_id", bins.recoil_id); + + return rows; +} + +void rowsToBins(const std::vector &rows, user_tally::bin_var_t &bins) +{ + bins = user_tally::bin_var_t(); + + for (const auto &r : rows) { + std::vector edges = BinVariableModel::parseEdges(r.edges); + if (edges.size() < 2) + continue; + + if (r.variable == "x") + bins.x = edges; + else if (r.variable == "y") + bins.y = edges; + else if (r.variable == "z") + bins.z = edges; + else if (r.variable == "r") + bins.r = edges; + else if (r.variable == "rho") + bins.rho = edges; + else if (r.variable == "cosTheta") + bins.cosTheta = edges; + else if (r.variable == "nx") + bins.nx = edges; + else if (r.variable == "ny") + bins.ny = edges; + else if (r.variable == "nz") + bins.nz = edges; + else if (r.variable == "E") + bins.E = edges; + else if (r.variable == "Tdam") + bins.Tdam = edges; + else if (r.variable == "V") + bins.V = edges; + else if (r.variable == "atom_id") + bins.atom_id = edges; + else if (r.variable == "recoil_id") + bins.recoil_id = edges; + } +} + +class VariableComboDelegate : public QStyledItemDelegate +{ +public: + QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &, const QModelIndex &) const override + { + QComboBox *cb = new QComboBox(parent); + const QStringList vars = BinVariableModel::variableNames(); + for (const QString &v : vars) + cb->addItem(v + " - " + BinVariableModel::variableDescription(v), v); + return cb; + } + + void setEditorData(QWidget *editor, const QModelIndex &index) const override + { + QComboBox *cb = static_cast(editor); + const QString var = index.model()->data(index, Qt::EditRole).toString(); + int i = cb->findData(var); + if (i >= 0) + cb->setCurrentIndex(i); + } + + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override + { + QComboBox *cb = static_cast(editor); + model->setData(index, cb->currentData(), Qt::EditRole); + } +}; + +} // namespace + +BinVariableModel::BinVariableModel(QObject *parent) : QAbstractTableModel(parent) { } + +int BinVariableModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + return static_cast(rows_.size()); +} + +int BinVariableModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 5; +} + +QVariant BinVariableModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) + return QVariant(); + + const Row &r = rows_[index.row()]; + + if (role == Qt::BackgroundRole && index.column() == 1) { + const std::vector edges = parseEdges(r.edges); + if (!isStrictlyMonotonic(edges)) + return QColor(255, 220, 220); + } + + if (role != Qt::DisplayRole && role != Qt::EditRole) + return QVariant(); + + switch (index.column()) { + case 0: + return r.variable; + case 1: + return r.edges; + case 2: { + const std::vector edges = parseEdges(r.edges); + if (edges.size() < 2) + return 0; + return static_cast(edges.size() - 1); + } + case 3: + return "~"; + case 4: + return "x"; + default: + break; + } + + return QVariant(); +} + +QVariant BinVariableModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole || orientation != Qt::Horizontal) + return QVariant(); + + switch (section) { + case 0: + return "Variable"; + case 1: + return "Bin edges"; + case 2: + return "N bins"; + case 3: + return "[~]"; + case 4: + return "[x]"; + default: + return QVariant(); + } +} + +Qt::ItemFlags BinVariableModel::flags(const QModelIndex &index) const +{ + if (!index.isValid()) + return Qt::NoItemFlags; + + Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + if (index.column() == 0 || index.column() == 1) + f |= Qt::ItemIsEditable; + return f; +} + +bool BinVariableModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (!index.isValid() || role != Qt::EditRole) + return false; + + Row &r = rows_[index.row()]; + if (index.column() == 0) + r.variable = value.toString(); + else if (index.column() == 1) + r.edges = value.toString(); + else + return false; + + emit dataChanged(index, index); + emit dataChanged(this->index(index.row(), 2), this->index(index.row(), 2)); + emit rowsChanged(); + return true; +} + +bool BinVariableModel::insertRows(int row, int count, const QModelIndex &parent) +{ + if (count <= 0) + return false; + + beginInsertRows(parent, row, row + count - 1); + for (int i = 0; i < count; ++i) + rows_.insert(rows_.begin() + row, { "x", "0 1" }); + endInsertRows(); + emit rowsChanged(); + return true; +} + +bool BinVariableModel::removeRows(int row, int count, const QModelIndex &parent) +{ + if (count <= 0 || row < 0 || row + count > rowCount()) + return false; + + beginRemoveRows(parent, row, row + count - 1); + rows_.erase(rows_.begin() + row, rows_.begin() + row + count); + endRemoveRows(); + emit rowsChanged(); + return true; +} + +void BinVariableModel::setRows(const std::vector &rows) +{ + beginResetModel(); + rows_ = rows; + endResetModel(); + emit rowsChanged(); +} + +std::vector BinVariableModel::rows() const +{ + return rows_; +} + +void BinVariableModel::clear() +{ + beginResetModel(); + rows_.clear(); + endResetModel(); + emit rowsChanged(); +} + +QStringList BinVariableModel::variableNames() +{ + QStringList vars; + for (const auto &v : kVars) + vars << v.name; + return vars; +} + +QString BinVariableModel::variableDescription(const QString &name) +{ + for (const auto &v : kVars) { + if (name == v.name) + return v.description; + } + return QString(); +} + +std::vector BinVariableModel::parseEdges(const QString &text) +{ + std::vector out; + const QStringList parts = text.split(QRegularExpression("[\\s,;]+"), Qt::SkipEmptyParts); + for (const QString &p : parts) { + bool ok = false; + float v = p.toFloat(&ok); + if (ok) + out.push_back(v); + } + return out; +} + +QString BinVariableModel::edgesToString(const std::vector &edges) +{ + QStringList s; + for (float v : edges) + s << QString::number(v, 'g', 8); + return s.join(' '); +} + +bool BinVariableModel::isStrictlyMonotonic(const std::vector &edges) +{ + if (edges.size() < 2) + return false; + for (size_t i = 1; i < edges.size(); ++i) { + if (!(edges[i] > edges[i - 1])) + return false; + } + return true; +} + +LinspaceDialog::LinspaceDialog(QWidget *parent) : QDialog(parent) +{ + setWindowTitle("Generate bin edges"); + QVBoxLayout *v = new QVBoxLayout(this); + + QFormLayout *f = new QFormLayout; + min_ = new QDoubleSpinBox; + max_ = new QDoubleSpinBox; + bins_ = new QSpinBox; + + min_->setRange(-1e9, 1e9); + max_->setRange(-1e9, 1e9); + min_->setDecimals(6); + max_->setDecimals(6); + bins_->setRange(1, 1000000); + + min_->setValue(0.0); + max_->setValue(1.0); + bins_->setValue(10); + + f->addRow("Min value:", min_); + f->addRow("Max value:", max_); + f->addRow("N bins:", bins_); + v->addLayout(f); + + preview_ = new QLabel; + v->addWidget(preview_); + + QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); + v->addWidget(box); + + connect(min_, QOverload::of(&QDoubleSpinBox::valueChanged), this, + &LinspaceDialog::updatePreview); + connect(max_, QOverload::of(&QDoubleSpinBox::valueChanged), this, + &LinspaceDialog::updatePreview); + connect(bins_, QOverload::of(&QSpinBox::valueChanged), this, + &LinspaceDialog::updatePreview); + updatePreview(); +} + +std::vector LinspaceDialog::edges() const +{ + const int n = bins_->value(); + const double a = min_->value(); + const double b = max_->value(); + std::vector e; + e.reserve(static_cast(n) + 1); + + if (n <= 0) + return e; + + const double step = (b - a) / static_cast(n); + for (int i = 0; i <= n; ++i) + e.push_back(static_cast(a + step * i)); + + return e; +} + +void LinspaceDialog::updatePreview() +{ + const std::vector e = edges(); + if (e.size() < 2) { + preview_->setText("No edges generated"); + return; + } + + QString txt = QString("%1 edges, %2 bins\n").arg(e.size()).arg(e.size() - 1); + txt += QString("%1, %2, ... , %3, %4") + .arg(e.front(), 0, 'g', 6) + .arg(e[1], 0, 'g', 6) + .arg(e[e.size() - 2], 0, 'g', 6) + .arg(e.back(), 0, 'g', 6); + preview_->setText(txt); +} + +TallyTemplateDialog::TallyTemplateDialog(QWidget *parent) : QDialog(parent) +{ + setWindowTitle("Tally Templates"); + resize(760, 460); + + QVBoxLayout *root = new QVBoxLayout(this); + QHBoxLayout *body = new QHBoxLayout; + + list_ = new QListWidget; + preview_ = new QTextBrowser; + preview_->setReadOnly(true); + + body->addWidget(list_, 1); + body->addWidget(preview_, 2); + root->addLayout(body, 1); + + QDialogButtonBox *box = new QDialogButtonBox(QDialogButtonBox::Cancel); + accept_ = box->addButton("Load into current tally", QDialogButtonBox::AcceptRole); + accept_->setEnabled(false); + connect(box, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(box, &QDialogButtonBox::rejected, this, &QDialog::reject); + root->addWidget(box); + + connect(list_, &QListWidget::currentRowChanged, this, &TallyTemplateDialog::onCurrentTemplateChanged); + + const bool ok = loadTemplates(); + if (ok && list_->count() > 0) + list_->setCurrentRow(0); + else { + preview_->setPlainText("No templates available. Check resource file tally_templates.json."); + accept_->setEnabled(false); + } +} + +bool TallyTemplateDialog::loadTemplates() +{ + entries_.clear(); + + static bool cacheInitialized = false; + static ojson cache; + + if (!cacheInitialized) { + QFile f(kTemplatePath); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) + return false; + + try { + cache = ojson::parse(f.readAll().toStdString(), nullptr, true, true); + cacheInitialized = true; + } catch (...) { + return false; + } + } + + const ojson &j = cache; + + if (!j.is_array()) + return false; + + for (const auto &node : j) { + if (!node.is_object()) + continue; + + TemplateEntry e; + e.parameters.id = node.value("id", std::string("Template")); + e.parameters.description = node.value("description", std::string()); + e.parameters.event = parseEvent(QString::fromStdString(node.value("event", std::string("IonStop")))); + + if (node.contains("bins") && node["bins"].is_object()) { + for (auto it = node["bins"].begin(); it != node["bins"].end(); ++it) { + std::vector edges; + if (it.value().is_array()) + edges = it.value().get>(); + + const std::string k = it.key(); + if (k == "x") + e.parameters.bins.x = edges; + else if (k == "y") + e.parameters.bins.y = edges; + else if (k == "z") + e.parameters.bins.z = edges; + else if (k == "r") + e.parameters.bins.r = edges; + else if (k == "rho") + e.parameters.bins.rho = edges; + else if (k == "cosTheta") + e.parameters.bins.cosTheta = edges; + else if (k == "nx") + e.parameters.bins.nx = edges; + else if (k == "ny") + e.parameters.bins.ny = edges; + else if (k == "nz") + e.parameters.bins.nz = edges; + else if (k == "E") + e.parameters.bins.E = edges; + else if (k == "Tdam") + e.parameters.bins.Tdam = edges; + else if (k == "V") + e.parameters.bins.V = edges; + else if (k == "atom_id") + e.parameters.bins.atom_id = edges; + else if (k == "recoil_id") + e.parameters.bins.recoil_id = edges; + } + } + + entries_.push_back(e); + list_->addItem(QString::fromStdString(e.parameters.id)); + } + + return !entries_.empty(); +} + +user_tally::parameters TallyTemplateDialog::selectedTemplate() const +{ + int i = list_->currentRow(); + if (i < 0 || i >= static_cast(entries_.size())) + return user_tally::parameters(); + return entries_[i].parameters; +} + +void TallyTemplateDialog::refreshPreview(int row) +{ + if (row < 0 || row >= static_cast(entries_.size())) { + preview_->clear(); + accept_->setEnabled(false); + return; + } + + const auto &p = entries_[row].parameters; + std::vector rows = binsToRows(p.bins); + + QString html; + html += QString("Event: %1
").arg(eventToString(p.event)); + html += QString("Description: %1

") + .arg(QString::fromStdString(p.description).toHtmlEscaped()); + html += "Bins:
"; + for (const auto &r : rows) { + const std::vector edges = BinVariableModel::parseEdges(r.edges); + if (edges.size() >= 2) { + html += QString("%1 : %2 → %3   (%4 bins)
") + .arg(r.variable.toHtmlEscaped()) + .arg(edges.front(), 0, 'g', 4) + .arg(edges.back(), 0, 'g', 4) + .arg(edges.size() - 1); + } else { + html += QString("%1 : %2
") + .arg(r.variable.toHtmlEscaped()) + .arg(r.edges.toHtmlEscaped()); + } + } + + preview_->setHtml(html); + accept_->setEnabled(true); +} + +void TallyTemplateDialog::onCurrentTemplateChanged(int row) +{ + refreshPreview(row); +} + +UserTallyView::UserTallyView(OptionsModel *m, QWidget *parent) + : QWidget(parent), + model_(m), + binModel_(new BinVariableModel(this)) +{ + tallyIndex_ = model_->index("UserTally"); + + QVBoxLayout *root = new QVBoxLayout(this); + + QHBoxLayout *top = new QHBoxLayout; + top->addWidget(new QLabel("Tally:")); + cbTallyID_ = new QComboBox; + cbTallyID_->setMinimumContentsLength(16); + top->addWidget(cbTallyID_, 1); + + btDbTally_ = new QToolButton; + btDbTally_->setIcon(QIcon(":/assets/lucide/database-search.svg")); + btDbTally_->setToolTip("Load tally template"); + top->addWidget(btDbTally_); + + btAddTally_ = new QToolButton; + btAddTally_->setIcon(QIcon(":/assets/ionicons/add-outline.svg")); + btAddTally_->setToolTip("Add new tally"); + top->addWidget(btAddTally_); + + btDupTally_ = new QToolButton; + btDupTally_->setIcon(QIcon(":/assets/ionicons/copy-outline.svg")); + btDupTally_->setToolTip("Duplicate tally"); + top->addWidget(btDupTally_); + + btDelTally_ = new QToolButton; + btDelTally_->setIcon(QIcon(":/assets/ionicons/remove-outline.svg")); + btDelTally_->setToolTip("Remove tally"); + top->addWidget(btDelTally_); + + btEdtTally_ = new QToolButton; + btEdtTally_->setIcon(QIcon(":/assets/ionicons/create-outline.svg")); + btEdtTally_->setToolTip("Rename tally"); + top->addWidget(btEdtTally_); + + root->addLayout(top); + + summaryLabel_ = new QLabel("No bins defined - tally is inactive"); + memoryLabel_ = new QLabel("Memory: 0 B per tally flush"); + warningLabel_ = new QLabel; + warningLabel_->setStyleSheet("color: #b22222;"); + root->addWidget(summaryLabel_); + root->addWidget(memoryLabel_); + root->addWidget(warningLabel_); + + QFormLayout *form = new QFormLayout; + leDescription_ = new QLineEdit; + cbEvent_ = new QComboBox; + cbEvent_->addItems({ "IonStop", "IonExit", "Vacancy", "Replacement", "CascadeComplete", "BoundaryCrossing" }); + form->addRow("Description:", leDescription_); + form->addRow("Event:", cbEvent_); + root->addLayout(form); + + QGroupBox *csBox = new QGroupBox("Coordinate system"); + QVBoxLayout *csV = new QVBoxLayout(csBox); + cbUseLabFrame_ = new QCheckBox("Use lab frame"); + csV->addWidget(cbUseLabFrame_); + + coordWidget_ = new QWidget; + QGridLayout *cg = new QGridLayout(coordWidget_); + + for (int i = 0; i < 3; ++i) { + sbOrigin_[i] = new QDoubleSpinBox; + sbZaxis_[i] = new QDoubleSpinBox; + sbXZvec_[i] = new QDoubleSpinBox; + + sbOrigin_[i]->setRange(-1e6, 1e6); + sbZaxis_[i]->setRange(-1e3, 1e3); + sbXZvec_[i]->setRange(-1e3, 1e3); + + sbOrigin_[i]->setDecimals(6); + sbZaxis_[i]->setDecimals(6); + sbXZvec_[i]->setDecimals(6); + } + + cg->addWidget(new QLabel("Origin:"), 0, 0); + cg->addWidget(sbOrigin_[0], 0, 1); + cg->addWidget(sbOrigin_[1], 0, 2); + cg->addWidget(sbOrigin_[2], 0, 3); + + cg->addWidget(new QLabel("Z-axis:"), 1, 0); + cg->addWidget(sbZaxis_[0], 1, 1); + cg->addWidget(sbZaxis_[1], 1, 2); + cg->addWidget(sbZaxis_[2], 1, 3); + + cg->addWidget(new QLabel("XZ-plane vec:"), 2, 0); + cg->addWidget(sbXZvec_[0], 2, 1); + cg->addWidget(sbXZvec_[1], 2, 2); + cg->addWidget(sbXZvec_[2], 2, 3); + + csV->addWidget(coordWidget_); + root->addWidget(csBox); + + QHBoxLayout *binsHdr = new QHBoxLayout; + binsHdr->addWidget(new QLabel("Bin variables")); + binsHdr->addStretch(); + btAddBin_ = new QToolButton; + btAddBin_->setIcon(QIcon(":/assets/ionicons/add-outline.svg")); + btAddBin_->setToolTip("Add variable"); + binsHdr->addWidget(btAddBin_); + root->addLayout(binsHdr); + + binTableView_ = new QTableView; + binTableView_->setModel(binModel_); + binTableView_->setItemDelegateForColumn(0, new VariableComboDelegate); + binTableView_->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + binTableView_->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + binTableView_->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + binTableView_->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + binTableView_->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents); + root->addWidget(binTableView_, 1); + + connect(btAddTally_, &QToolButton::clicked, this, &UserTallyView::addTally); + connect(btDupTally_, &QToolButton::clicked, this, &UserTallyView::duplicateTally); + connect(btDelTally_, &QToolButton::clicked, this, &UserTallyView::removeTally); + connect(btEdtTally_, &QToolButton::clicked, this, &UserTallyView::renameTally); + connect(btDbTally_, &QToolButton::clicked, this, &UserTallyView::loadFromTemplate); + connect(btAddBin_, &QToolButton::clicked, this, &UserTallyView::addBinVariable); + + connect(cbTallyID_, QOverload::of(&QComboBox::currentIndexChanged), this, + &UserTallyView::updateSelectedTally); + connect(leDescription_, &QLineEdit::editingFinished, this, &UserTallyView::setValueData); + connect(cbEvent_, QOverload::of(&QComboBox::currentIndexChanged), this, + &UserTallyView::setValueData); + connect(cbUseLabFrame_, &QCheckBox::stateChanged, this, &UserTallyView::onUseLabFrameChanged); + connect(binModel_, &BinVariableModel::rowsChanged, this, &UserTallyView::setValueData); + connect(binModel_, &BinVariableModel::rowsChanged, this, &UserTallyView::updateSummaryLabel); + connect(binTableView_, &QTableView::clicked, this, &UserTallyView::onBinTableClicked); + + for (int i = 0; i < 3; ++i) { + connect(sbOrigin_[i], QOverload::of(&QDoubleSpinBox::valueChanged), this, + &UserTallyView::setValueData); + connect(sbZaxis_[i], QOverload::of(&QDoubleSpinBox::valueChanged), this, + &UserTallyView::setValueData); + connect(sbXZvec_[i], QOverload::of(&QDoubleSpinBox::valueChanged), this, + &UserTallyView::setValueData); + } + + setWidgetData(); +} + +void UserTallyView::setWidgetData() +{ + cbTallyID_->blockSignals(true); + cbTallyID_->clear(); + + auto &ut = model_->options()->UserTally; + for (const auto &t : ut) + cbTallyID_->addItem(QString::fromStdString(t.id)); + + cbTallyID_->blockSignals(false); + + if (!ut.empty()) { + cbTallyID_->setCurrentIndex(0); + updateSelectedTally(); + } else { + leDescription_->clear(); + cbEvent_->setCurrentIndex(0); + cbUseLabFrame_->setChecked(true); + onUseLabFrameChanged(); + binModel_->clear(); + warningLabel_->clear(); + updateSummaryLabel(); + setTallyEditorEnabled(false); + } + + const bool has = !ut.empty(); + cbTallyID_->setEnabled(has); + btDupTally_->setEnabled(has); + btDelTally_->setEnabled(has); + btEdtTally_->setEnabled(has); +} + +void UserTallyView::setTallyEditorEnabled(bool enabled) +{ + leDescription_->setEnabled(enabled); + cbEvent_->setEnabled(enabled); + cbUseLabFrame_->setEnabled(enabled); + coordWidget_->setEnabled(enabled && !cbUseLabFrame_->isChecked()); + btAddBin_->setEnabled(enabled); + binTableView_->setEnabled(enabled); +} + +void UserTallyView::setValueData() +{ + if (syncingUi_) + return; + + user_tally::parameters *t = currentTally(); + if (!t) + return; + + t->description = leDescription_->text().toStdString(); + t->event = currentEvent(); + + if (cbUseLabFrame_->isChecked()) { + t->coordinate_system.reset(); + } else { + t->coordinate_system.origin = spin3Value(sbOrigin_); + t->coordinate_system.zaxis = spin3Value(sbZaxis_); + t->coordinate_system.xzvector = spin3Value(sbXZvec_); + } + + rowsToBins(binModel_->rows(), t->bins); + + model_->notifyDataChanged(tallyIndex_); + updateSummaryLabel(); +} + +void UserTallyView::addTally() +{ + user_tally::parameters p; + p.id = QString("Tally%1").arg(model_->options()->UserTally.size() + 1).toStdString(); + p.description = "User tally"; + p.event = Event::IonStop; + + model_->options()->UserTally.push_back(p); + model_->notifyDataChanged(tallyIndex_); + + setWidgetData(); + cbTallyID_->setCurrentIndex(cbTallyID_->count() - 1); +} + +void UserTallyView::removeTally() +{ + int i = cbTallyID_->currentIndex(); + auto &ut = model_->options()->UserTally; + if (i < 0 || i >= static_cast(ut.size())) + return; + + QMessageBox::StandardButton ret = QMessageBox::warning( + this, "Remove tally", + QString("%1 is being removed.\nClick OK to proceed.").arg(cbTallyID_->currentText()), + QMessageBox::Ok | QMessageBox::Cancel); + if (ret != QMessageBox::Ok) + return; + + ut.erase(ut.begin() + i); + model_->notifyDataChanged(tallyIndex_); + setWidgetData(); +} + +void UserTallyView::duplicateTally() +{ + int i = cbTallyID_->currentIndex(); + auto &ut = model_->options()->UserTally; + if (i < 0 || i >= static_cast(ut.size())) + return; + + user_tally::parameters copy = ut[i]; + QString baseId = QString::fromStdString(copy.id); + if (baseId.isEmpty()) + baseId = "Tally"; + + QString newId = baseId + "_copy"; + int suffix = 2; + auto exists = [&ut](const QString &id) { + for (const auto &t : ut) { + if (QString::fromStdString(t.id) == id) + return true; + } + return false; + }; + while (exists(newId)) { + newId = QString("%1_copy%2").arg(baseId).arg(suffix++); + } + + copy.id = newId.toStdString(); + ut.push_back(copy); + model_->notifyDataChanged(tallyIndex_); + + setWidgetData(); + cbTallyID_->setCurrentText(newId); +} + +void UserTallyView::renameTally() +{ + int i = cbTallyID_->currentIndex(); + auto &ut = model_->options()->UserTally; + if (i < 0 || i >= static_cast(ut.size())) + return; + + bool ok = false; + QString s = QInputDialog::getText(this, "Rename tally", "New tally id", QLineEdit::Normal, + cbTallyID_->currentText(), &ok); + if (!ok || s.trimmed().isEmpty()) + return; + + ut[i].id = s.toStdString(); + model_->notifyDataChanged(tallyIndex_); + + setWidgetData(); + cbTallyID_->setCurrentText(s); +} + +void UserTallyView::addBinVariable() +{ + binModel_->insertRows(binModel_->rowCount(), 1); +} + +void UserTallyView::loadFromTemplate() +{ + user_tally::parameters *t = currentTally(); + if (!t) { + addTally(); + t = currentTally(); + if (!t) + return; + } + + TallyTemplateDialog dlg(this); + + if (dlg.exec() != QDialog::Accepted) + return; + + user_tally::parameters p = dlg.selectedTemplate(); + t->description = p.description; + t->event = p.event; + t->bins = p.bins; + + updateSelectedTally(); + model_->notifyDataChanged(tallyIndex_); +} + +void UserTallyView::updateSelectedTally() +{ + const user_tally::parameters *t = currentTally(); + if (!t) { + setTallyEditorEnabled(false); + return; + } + + syncingUi_ = true; + + leDescription_->blockSignals(true); + cbEvent_->blockSignals(true); + + leDescription_->setText(QString::fromStdString(t->description)); + setCurrentEvent(t->event); + + setSpin3(sbOrigin_, t->coordinate_system.origin); + setSpin3(sbZaxis_, t->coordinate_system.zaxis); + setSpin3(sbXZvec_, t->coordinate_system.xzvector); + + cbUseLabFrame_->setChecked(isLabFrame(t->coordinate_system)); + onUseLabFrameChanged(); + + binModel_->setRows(binsToRows(t->bins)); + + leDescription_->blockSignals(false); + cbEvent_->blockSignals(false); + syncingUi_ = false; + + setTallyEditorEnabled(true); + + btDelTally_->setEnabled(true); + btEdtTally_->setEnabled(true); + btDupTally_->setEnabled(true); + + updateSummaryLabel(); +} + +void UserTallyView::updateSummaryLabel() +{ + const std::vector rows = binModel_->rows(); + + QStringList names; + QStringList dims; + long long totalBins = 1; + bool hasBins = false; + bool hasInvalid = false; + + for (const auto &r : rows) { + const std::vector edges = BinVariableModel::parseEdges(r.edges); + if (edges.size() < 2) { + hasInvalid = true; + continue; + } + if (!BinVariableModel::isStrictlyMonotonic(edges)) { + hasInvalid = true; + continue; + } + + const int n = static_cast(edges.size() - 1); + names << r.variable; + dims << QString::number(n); + totalBins *= n; + hasBins = true; + } + + if (!hasBins) { + summaryLabel_->setText("No bins defined - tally is inactive"); + summaryLabel_->setStyleSheet(""); + memoryLabel_->setText("Memory: 0 B per tally flush"); + } else if (names.size() == 1) { + summaryLabel_->setText( + QString("1D tally: %1 (%2 bins)").arg(names.join(" ")).arg(totalBins)); + } else { + summaryLabel_->setText(QString("%1D tally: %2 (%3 = %4 bins)") + .arg(names.size()) + .arg(names.join(" x ")) + .arg(dims.join(" x ")) + .arg(totalBins)); + } + + if (hasBins) { + if (totalBins >= 200000) + summaryLabel_->setStyleSheet("color: #b22222; font-weight: bold;"); + else if (totalBins >= 10000) + summaryLabel_->setStyleSheet("color: #e07000; font-weight: bold;"); + else + summaryLabel_->setStyleSheet(""); + } + + const double bytes = static_cast(totalBins) * sizeof(double); + QString memStr; + if (bytes < 1024.0) + memStr = QString("%1 B").arg(bytes, 0, 'f', 0); + else if (bytes < 1024.0 * 1024.0) + memStr = QString("%1 KB").arg(bytes / 1024.0, 0, 'f', 1); + else + memStr = QString("%1 MB").arg(bytes / (1024.0 * 1024.0), 0, 'f', 2); + memoryLabel_->setText(QString("Memory: ~%1 per tally flush").arg(memStr)); + + QStringList warnings; + if (hasInvalid) + warnings << "Bin edges must be strictly monotonic for all rows."; + if (totalBins > 500000) + warnings << "Large tally warning: total bins exceed 500000."; + warningLabel_->setText(warnings.join(" ")); +} + +void UserTallyView::applyLinspaceToCurrentRow() +{ + const QModelIndex idx = binTableView_->currentIndex(); + if (!idx.isValid()) + return; + + LinspaceDialog dlg(this); + if (dlg.exec() != QDialog::Accepted) + return; + + const std::vector edges = dlg.edges(); + if (edges.size() < 2) + return; + + binModel_->setData(binModel_->index(idx.row(), 1), BinVariableModel::edgesToString(edges)); +} + +void UserTallyView::onBinTableClicked(const QModelIndex &index) +{ + if (!index.isValid()) + return; + + if (index.column() == 3) { + binTableView_->setCurrentIndex(index); + applyLinspaceToCurrentRow(); + } else if (index.column() == 4) { + binModel_->removeRows(index.row(), 1); + } +} + +void UserTallyView::onUseLabFrameChanged(int) +{ + const bool useLab = cbUseLabFrame_->isChecked(); + coordWidget_->setVisible(!useLab); + setValueData(); +} + +Event UserTallyView::currentEvent() const +{ + return parseEvent(cbEvent_->currentText()); +} + +void UserTallyView::setCurrentEvent(Event ev) +{ + int i = cbEvent_->findText(eventToString(ev)); + if (i < 0) + i = 0; + cbEvent_->setCurrentIndex(i); +} + +user_tally::parameters *UserTallyView::currentTally() +{ + int i = cbTallyID_->currentIndex(); + auto &ut = model_->options()->UserTally; + if (i < 0 || i >= static_cast(ut.size())) + return nullptr; + return &ut[i]; +} + +const user_tally::parameters *UserTallyView::currentTally() const +{ + int i = cbTallyID_->currentIndex(); + const auto &ut = model_->options()->UserTally; + if (i < 0 || i >= static_cast(ut.size())) + return nullptr; + return &ut[i]; +} diff --git a/source/gui/usertallyview.h b/source/gui/usertallyview.h new file mode 100644 index 0000000..6a7e297 --- /dev/null +++ b/source/gui/usertallyview.h @@ -0,0 +1,169 @@ +#ifndef USERTALLYVIEW_H +#define USERTALLYVIEW_H + +#include +#include +#include +#include + +#include "mcdriver.h" + +class QCheckBox; +class QComboBox; +class QDoubleSpinBox; +class QLabel; +class QLineEdit; +class QPushButton; +class QSpinBox; +class QTableView; +class QToolButton; +class QModelIndex; +class QStringList; +class QListWidget; +class QTextBrowser; + +class OptionsModel; + +class BinVariableModel : public QAbstractTableModel +{ + Q_OBJECT + +public: + struct Row { + QString variable; + QString edges; + }; + + explicit BinVariableModel(QObject *parent = nullptr); + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override; + bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; + + void setRows(const std::vector &rows); + std::vector rows() const; + void clear(); + + static QStringList variableNames(); + static QString variableDescription(const QString &name); + static std::vector parseEdges(const QString &text); + static QString edgesToString(const std::vector &edges); + static bool isStrictlyMonotonic(const std::vector &edges); + +signals: + void rowsChanged(); + +private: + std::vector rows_; +}; + +class LinspaceDialog : public QDialog +{ + Q_OBJECT + +public: + explicit LinspaceDialog(QWidget *parent = nullptr); + std::vector edges() const; + +private slots: + void updatePreview(); + +private: + QDoubleSpinBox *min_; + QDoubleSpinBox *max_; + QSpinBox *bins_; + QLabel *preview_; +}; + +class TallyTemplateDialog : public QDialog +{ + Q_OBJECT + +public: + explicit TallyTemplateDialog(QWidget *parent = nullptr); + user_tally::parameters selectedTemplate() const; + +private slots: + void onCurrentTemplateChanged(int row); + +private: + struct TemplateEntry { + user_tally::parameters parameters; + }; + + bool loadTemplates(); + void refreshPreview(int row); + + QListWidget *list_; + QTextBrowser *preview_; + QPushButton *accept_; + std::vector entries_; +}; + +class UserTallyView : public QWidget +{ + Q_OBJECT + +public: + explicit UserTallyView(OptionsModel *m, QWidget *parent = nullptr); + +public slots: + void setWidgetData(); + void setValueData(); + void addTally(); + void removeTally(); + void renameTally(); + void duplicateTally(); + void addBinVariable(); + void loadFromTemplate(); + void updateSelectedTally(); + void updateSummaryLabel(); + +private slots: + void applyLinspaceToCurrentRow(); + void onBinTableClicked(const QModelIndex &index); + void onUseLabFrameChanged(int state = 0); + +private: + Event currentEvent() const; + void setCurrentEvent(Event ev); + void setTallyEditorEnabled(bool enabled); + user_tally::parameters *currentTally(); + const user_tally::parameters *currentTally() const; + + QComboBox *cbTallyID_; + QToolButton *btDbTally_; + QToolButton *btAddTally_; + QToolButton *btDupTally_; + QToolButton *btDelTally_; + QToolButton *btEdtTally_; + QToolButton *btAddBin_; + + QLabel *summaryLabel_; + QLabel *memoryLabel_; + QLabel *warningLabel_; + + QLineEdit *leDescription_; + QComboBox *cbEvent_; + + QCheckBox *cbUseLabFrame_; + QWidget *coordWidget_; + QDoubleSpinBox *sbOrigin_[3]; + QDoubleSpinBox *sbZaxis_[3]; + QDoubleSpinBox *sbXZvec_[3]; + + BinVariableModel *binModel_; + QTableView *binTableView_; + bool syncingUi_ = false; + + OptionsModel *model_; + QPersistentModelIndex tallyIndex_; +}; + +#endif // USERTALLYVIEW_H diff --git a/source/lib/parse_json.cpp b/source/lib/parse_json.cpp index 853ecde..14c1aa8 100644 --- a/source/lib/parse_json.cpp +++ b/source/lib/parse_json.cpp @@ -428,7 +428,12 @@ void mcconfig::set_impl_(const std::string &path, const std::string &json_str) { ojson j(*this); ojson::json_pointer ptr(path.c_str()); - ojson v = ojson::parse(json_str); + std::string normalized = json_str; + if (normalized.empty()) { + // Defensive guard: empty input is invalid JSON. Preserve existing value instead. + normalized = j.at(ptr).dump(); + } + ojson v = ojson::parse(normalized); j.at(ptr) = v; *this = j; validate(true);