diff --git a/.gitattributes b/.gitattributes index 00293ac..52b6e20 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ + * text=auto *.cs text *.sln text eol=crlf @@ -5,3 +6,68 @@ *.vcproj eol=crlf *.csproj eol=crlf *.vdproj eol=crlf + +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain + diff --git a/.gitignore b/.gitignore index 94420dc..1af395c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,16 @@ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. + + +# User-specific files + +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + # User-specific files +*.rsuser + *.suo *.user *.userosscache @@ -10,6 +19,11 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs +# Mono auto generated files +mono_crash.* + + + # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -17,28 +31,55 @@ [Rr]eleases/ x64/ x86/ + + bld/ [Bb]in/ [Oo]bj/ # Visual Studio 2015 cache/options directory + + +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory + .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot +# Uncomment if you have tasks that create the project"s static files in wwwroot #wwwroot/ +# Visual Studio 2017 auto generated files +Generated\ Files/ + # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* + + # NUNIT *.VisualState.xml TestResult.xml +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c + # DNX project.lock.json artifacts/ @@ -51,6 +92,33 @@ artifacts/ *.obj *.pch *.pdb + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb + *.pgc *.pgd *.rsp @@ -60,6 +128,10 @@ artifacts/ *.tlh *.tmp *.tmp_proj + + +*_wpftmp.csproj + *.log *.vspscc *.vssscc @@ -80,12 +152,22 @@ ipch/ *.sdf *.cachefile + + +*.VC.db +*.VC.VC.opendb + + # Visual Studio profiler *.psess *.vsp *.vspx *.sap +# Visual Studio Trace Files +*.e2e + + # TFS 2012 Local Workspace $tf/ @@ -97,6 +179,7 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user + # JustCode is a .NET coding add-in .JustCode @@ -106,6 +189,20 @@ _TeamCity* # DotCover is a Code Coverage Tool *.dotCover +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + + # NCrunch _NCrunch_* .*crunch*.local.xml @@ -137,11 +234,18 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml + + # TODO: Comment the next line if you want to checkin your web deploy settings + +# Note: Comment the next line if you want to checkin your web deploy settings, + # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj + + # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore @@ -151,6 +255,25 @@ publish/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config + +# NuGet v3"s project.json files produces more ignorable files + *.nuget.props *.nuget.targets @@ -162,6 +285,8 @@ csx/ ecf/ rcf/ + + # Microsoft Azure ApplicationInsights config file ApplicationInsights.config @@ -169,23 +294,48 @@ ApplicationInsights.config AppPackages/ BundleArtifacts/ +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache + !*.[Cc]ache/ +!?*.[Cc]ache/ + + # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview + +*.jfm *.pfx *.publishsettings node_modules/ orleans.codegen.cs + + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + # RIA/Silverlight projects Generated_Code/ @@ -197,15 +347,28 @@ Backup*/ UpgradeLog*.XML UpgradeLog*.htm + +ServiceFabricBackup/ +*.rptproj.bak + + # SQL Server files *.mdf *.ldf +*.ndf + + # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + # Microsoft Fakes FakesAssemblies/ @@ -215,12 +378,22 @@ FakesAssemblies/ # Node.js Tools for Visual Studio .ntvs_analysis.dat + +node_modules/ + + # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt + + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + + # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -232,5 +405,69 @@ _Pvt_Extensions # Paket dependency manager .paket/paket.exe + + # FAKE - F# Make .fake/ + +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik"s JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +/build/app +/dist/app +/executable diff --git a/Python_SANS_Script_GUI/App.py b/Python_SANS_Script_GUI/App.py new file mode 100644 index 0000000..41ee35e --- /dev/null +++ b/Python_SANS_Script_GUI/App.py @@ -0,0 +1,22 @@ +import tkinter as tk +from MainWindow import MainWindow + +class App: + def __init__(self): + self.root = tk.Tk() + self.main_window = MainWindow(self.root) + self.main_window.grid(sticky="nsew") + self.root.grid_rowconfigure(0, weight=1) + self.root.grid_columnconfigure(0, weight=1) + self.root.protocol("WM_DELETE_WINDOW", self.on_close) + + + def run(self): + self.root.mainloop() + + def on_close(self): + self.main_window.on_close() + +if __name__ == "__main__": + app = App() + app.run() \ No newline at end of file diff --git a/Python_SANS_Script_GUI/Dialogs/PreviewDialog.py b/Python_SANS_Script_GUI/Dialogs/PreviewDialog.py new file mode 100644 index 0000000..5cf56cc --- /dev/null +++ b/Python_SANS_Script_GUI/Dialogs/PreviewDialog.py @@ -0,0 +1,71 @@ +import tkinter as tk +from tkinter import scrolledtext + + + +class PreviewDialog(tk.Toplevel): + def __init__(self, parent, script=None): + super().__init__(parent) + self.parent = parent # Store reference to the parent (main window) + + self.title("PreviewDialog") + #self.geometry("600x500") + self.resizable(True, True) + self.columnconfigure(0,weight=1) + self.rowconfigure(0,weight=1) + + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(0, weight=1) + self.rowconfigure(0, weight=1) + + # Create a frame for the text area with a border + border_frame = tk.Frame(self, borderwidth=1, relief="solid") + border_frame.grid(row=0, column=0, sticky="nsew",padx=5, pady=5) + + # Create a scrollable text area + self.txt_script = scrolledtext.ScrolledText(border_frame, wrap=tk.WORD) + self.txt_script.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) + border_frame.grid_rowconfigure(0, weight=1) + border_frame.grid_columnconfigure(0, weight=1) + + self.txt_script.insert(tk.END, script) + self.txt_script.config(state=tk.DISABLED) + # Create a frame for the buttons + button_frame = tk.Frame(self) + button_frame.grid(row=1, column=0, sticky="e",padx=5, pady=5) + + # Create the Print Script button (initially hidden) + self.btn_print = tk.Button(button_frame, text="Print Script", command=self.print_script) + self.btn_print.grid(row=0, column=0, padx=5, pady=5) + self.btn_print.pack_forget() # Hide the button initially + + # Create the Close button + btn_close = tk.Button(button_frame, text="Close", command=self.close) + btn_close.grid(row=0, column=1, padx=5, pady=5) + + def set_script(self, script): + self.txt_script.delete(1.0, tk.END) + self.txt_script.insert(tk.END, script) + + def print_script(self): + # Call the pnl_settings_on_write_clicked method from the main window + self.parent.pnl_settings_on_write_clicked() + + def show(self): + self.grab_set() + self.wait_window() + + def close(self): + self.destroy() + +# Example usage +if __name__ == "__main__": + root = tk.Tk() + root.withdraw() # Hide the root window + + dialog = PreviewDialog(root) + + dialog.set_script("This is a preview of the script.") + + #dialog.txt_script.insert(tk.END, "This is a preview of the script.") + dialog.mainloop() \ No newline at end of file diff --git a/Python_SANS_Script_GUI/EstimationPanel.py b/Python_SANS_Script_GUI/EstimationPanel.py new file mode 100644 index 0000000..07c6d24 --- /dev/null +++ b/Python_SANS_Script_GUI/EstimationPanel.py @@ -0,0 +1,86 @@ +import tkinter as tk +from tkinter import ttk +from Models.Estimation import Estimation +from Settings import Settings + +class EstimationPanel(ttk.Frame): + def __init__(self, parent, estimation = None, settings = None): + super().__init__(parent) + self.on_calculate_clicked = None + self.settings = settings if settings is not None else Settings() + self.estimation = estimation if estimation is not None else Estimation(settings=self.settings) + + self.create_widgets() + + def create_widgets(self): + + #self.pack(fill=tk.BOTH, expand=True) + self.grid(sticky="nsew") + + label = ttk.Label(self, text="Estimated Script Time", font=("Helvetica", 16, "bold")) + label.grid(row=0, column=0, columnspan=3,padx=5, pady=5) + + grid_frame = ttk.Frame(self) + grid_frame.grid(row=1, column=0, columnspan=3, sticky="nsew",padx=5, pady=5) + + + # Est. count rate + ttk.Label(grid_frame, text="Est. count rate:").grid(row=0, column=0, sticky=tk.E,padx=5, pady=5) + count_rate_frame = ttk.Frame(grid_frame) + count_rate_frame.grid(row=0, column=1, columnspan=2,padx=5, pady=5) + self.txt_count_rate = ttk.Entry(count_rate_frame, width=10) + self.txt_count_rate.insert(0, self.settings.count_rate) # Default value + self.txt_count_rate.grid(row=0, column=0,padx=5, pady=5) + ttk.Label(count_rate_frame, text="µA/hr").grid(row=0, column=1,padx=5, pady=5) + self.txt_count_rate.bind("", self.update_count_rate) + + # Est. time between moves + ttk.Label(grid_frame, text="Est. time between moves:").grid(row=1, column=0, sticky=tk.E,padx=5, pady=5) + move_time_frame = ttk.Frame(grid_frame) + move_time_frame.grid(row=1, column=1, columnspan=2,padx=5, pady=5) + self.txt_move_time = ttk.Entry(move_time_frame, width=10) + self.txt_move_time.insert(0, self.settings.move_time) # Default value + self.txt_move_time.grid(row=0, column=0,padx=5, pady=5) + ttk.Label(move_time_frame, text="seconds").grid(row=0, column=1,padx=5, pady=5) + self.txt_move_time.bind("", self.update_move_time) + + + + # Est. script time + ttk.Label(grid_frame, text="Est. script time:").grid(row=2, column=0, sticky=tk.E,padx=5, pady=5) + script_time_frame = ttk.Frame(grid_frame) + script_time_frame.grid(row=2, column=1, columnspan=3,padx=5, pady=5) + self.txt_estimation = ttk.Entry(script_time_frame, width=15) + self.txt_estimation.insert(0, self.estimation.script_time) + self.txt_estimation.config(state="readonly") + self.txt_estimation.grid(row=0, column=0,padx=5, pady=5) + + # Calculate button + self.btn_calculate = ttk.Button(grid_frame, text="Calculate", command=self.btn_calculate_click) + self.btn_calculate.grid(row=2, column=3, padx=5, pady=5) + + + # Define the Settings update methods + def update_count_rate(self, event): + self.settings.count_rate = self.txt_count_rate.get() + + def update_move_time(self, event): + self.settings.move_time = self.txt_move_time.get() + + + + + def btn_calculate_click(self): + if self.on_calculate_clicked: + self.on_calculate_clicked() + + + +if __name__ == "__main__": + root = tk.Tk() + estimation = Estimation() + panel = EstimationPanel(root, estimation) + panel.grid(sticky="nsew") + root.grid_rowconfigure(0, weight=1) + root.grid_columnconfigure(0, weight=1) + root.mainloop() \ No newline at end of file diff --git a/Python_SANS_Script_GUI/IO/CsvExporter.py b/Python_SANS_Script_GUI/IO/CsvExporter.py new file mode 100644 index 0000000..4c86197 --- /dev/null +++ b/Python_SANS_Script_GUI/IO/CsvExporter.py @@ -0,0 +1,39 @@ +import csv +from Models.Experiment import Experiment + +class CsvExporter: + @staticmethod + def export(file, runs): + with open(file, mode="w", newline="") as csvfile: + writer = csv.writer(csvfile) + for exp in runs: + row = [ + exp.position, + str(exp.trans), + exp.trans_wait, + str(exp.sans), + exp.sans_wait, + exp.period, + exp.sample, + exp.background, + exp.thickness, + exp.temperature1, + exp.temperature2, + exp.field, + exp.shear_rate1, + exp.shear_rate2, + exp.shear_angle1, + exp.shear_angle2, + exp.pre_command, + exp.post_command, + exp.rb_number + ] + writer.writerow(row) + + + +if __name__ == "__main__": + runs = [ + Experiment(position="pos1", trans=10, trans_wait="Minutes", sans=20, sans_wait="Minutes", period="1", sample="Sample1", background="Background1", thickness="1.0", temperature1="25", temperature2="30", field="1.5T", shear_rate1="100", shear_rate2="200", shear_angle1="45", shear_angle2="90", pre_command="Pre Command", post_command="Post Command", rb_number="RB12345") + ] + CsvExporter.export("output.csv", runs) \ No newline at end of file diff --git a/Python_SANS_Script_GUI/IO/ExcelIO.py b/Python_SANS_Script_GUI/IO/ExcelIO.py new file mode 100644 index 0000000..89b9a40 --- /dev/null +++ b/Python_SANS_Script_GUI/IO/ExcelIO.py @@ -0,0 +1,85 @@ +import pandas as pd +from tkinter import messagebox +from Models.Experiment import Experiment + +class ExcelIO: + columns = [ + "POSITION", "TRANS", "TRANS_WAIT_FOR", "SANS", "SANS_WAIT_FOR", "PERIOD", "SAMPLE_IDENTIFICATION_STRING", "BACKGROUND", + "THICKNESS", "TEMPERATURE", "TEMPERATURE2", "FIELD", "SHEAR_RATE_1", "SHEAR_RATE_2", "SHEAR_ANGLE_1", "SHEAR_ANGLE_2", + "PRE_COMMAND", "POST_COMMAND", "RB_NUMBER" + ] + + + + def __init__(self): + self.dataset = None + + def open_excel_workbook(self, filename): + try: + self.dataset = pd.read_excel(filename, sheet_name=None) + + except Exception as ex: + messagebox.showerror("SANS Script - Error", str(ex)) + raise IOError("Could not open Excel spreadsheet") + + + def extract_experiments(self): + experiments = [] + if self.dataset is not None: + for sheet_name, df in self.dataset.items(): + for _, row in df.iterrows(): + exp = Experiment( + position=row.get("POSITION", ""), + trans=self.cast_to_double(row.get("TRANS", 0)), + trans_wait=row.get("TRANS_WAIT_FOR", ""), + sans=self.cast_to_double(row.get("SANS", 0)), + sans_wait=row.get("SANS_WAIT_FOR", ""), + period=row.get("PERIOD", ""), + sample=row.get("SAMPLE_IDENTIFICATION_STRING", ""), + background=row.get("BACKGROUND", ""), + thickness=row.get("THICKNESS", ""), + temperature1=row.get("TEMPERATURE", ""), + temperature2=row.get("TEMPERATURE2", ""), + field=row.get("FIELD", ""), + shear_rate1=row.get("SHEAR_RATE_1", ""), + shear_rate2=row.get("SHEAR_RATE_2", ""), + shear_angle1=row.get("SHEAR_ANGLE_1", ""), + shear_angle2=row.get("SHEAR_ANGLE_2", ""), + pre_command=row.get("PRE_COMMAND", ""), + post_command=row.get("POST_COMMAND", ""), + rb_number=row.get("RB_NUMBER", "") + ) + experiments.append(exp) + return experiments + + @staticmethod + def cast_to_double(val): + try: + return float(val) + except (ValueError, TypeError): + return 0.0 + + + def export_data_to_excel(self, runs, filename): + data = [] + for exp in runs: + data.append([ + exp.position, exp.trans, exp.trans_wait, exp.sans, exp.sans_wait, + exp.period, exp.sample, exp.background, exp.thickness, exp.temperature1, exp.temperature2, exp.field, + exp.shear_rate1, exp.shear_rate2, exp.shear_angle1, exp.shear_angle2, exp.pre_command, + exp.post_command, exp.rb_number + ]) + df = pd.DataFrame(data, columns=self.columns) + df.to_excel(filename, index=False) + + +if __name__ == "__main__": + runs = [ + Experiment(position="pos1", trans=10, trans_wait="Minutes", sans=20, sans_wait="Minutes", period="1", sample="Sample1", background="Background1", thickness="1.0", temperature1="25", temperature2="30", field="1.5T", shear_rate1="100", shear_rate2="200", shear_angle1="45", shear_angle2="90", pre_command="Pre Command", post_command="Post Command", rb_number="RB12345") + ] + excel_io = ExcelIO() + excel_io.export_data_to_excel(runs, "output.xlsx") + excel_io.open_excel_workbook("output.xlsx") + experiments = excel_io.extract_experiments() + for exp in experiments: + print(exp.__dict__) \ No newline at end of file diff --git a/Python_SANS_Script_GUI/MainWindow.py b/Python_SANS_Script_GUI/MainWindow.py new file mode 100644 index 0000000..98a5f15 --- /dev/null +++ b/Python_SANS_Script_GUI/MainWindow.py @@ -0,0 +1,220 @@ +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +from tkinter.scrolledtext import ScrolledText +from Models.DataCollection import DataCollection +from Models.InstrumentInfo import InstrumentInfo +from Models.Estimation import Estimation +from IO.CsvExporter import CsvExporter +from Dialogs.PreviewDialog import PreviewDialog +from SettingsPanel import SettingsPanel +from EstimationPanel import EstimationPanel +from TableWidget import TableWidget +from Settings import Settings +from Scripting.ScriptGenerator import ScriptGenerator +from Models.ExperimentSettings import ExperimentSettings +from TableDataConstructor import TableDataConstructor + +TableEntries = "POSITION, TRANS, TRANS_WAIT, SANS, SANS_WAIT, PERIOD, SAMPLE_IDENTIFICATION_STRING, BACKGROUND, THICKNESS, TEMPERATURE, TEMPERATURE2, FIELD, SHEAR_RATE_1, SHEAR_RATE_2, SHEAR_ANGLE_1, SHEAR_ANGLE_2, PRE_COMMAND, POST_COMMAND, RB_NUMBER" + + +class MainWindow(tk.Frame): + def __init__(self,parent): + super().__init__(parent) + self.parent = parent + self.parent.title("SANS Script GUI") + #self.parent.geometry("1200x600") + self.parent.resizable(True, True) + + + self.settings = Settings() + self.estimation = Estimation(settings=self.settings) + self.experiment = ExperimentSettings(settings=self.settings, estimation=self.estimation) + self.inst_info = InstrumentInfo(settings=self.settings,experiment=self.experiment) + + self.create_widgets() + self.load_last_instrument() + + + def create_widgets(self): + #self.data_grid = ttk.Treeview(self) + + # Configure grid weights for the parent container + + #self.parent.rowconfigure(0,weight=1) + self.parent.columnconfigure(0,weight=1) + #self.master.rowconfigure(0,weight=1) + self.master.columnconfigure(0,weight=1) + # Configure grid weights for the frame itself + #self.grid_rowconfigure(0, weight=1) + #self.grid_columnconfigure(0, weight=1) + #self.rowconfigure(0, weight=1) + #self.columnconfigure(0, weight=1) + + + # Data Grid with horizontal and vertical scrollbars + + + self.data_frame = TableWidget(self) + self.data_frame.grid(row=0, column=1, rowspan=3, sticky="nsew") + + #self.data_frame.config(width=800) + + + self.columnconfigure(1, weight=1) + + """ + for col in self.data_grid["columns"]: + self.data_grid.heading(col, text=col.replace("_", " ")) + """ + + + # Settings Panel + self.pnl_settings = SettingsPanel(self, inst_info=self.inst_info,settings=self.settings) + self.pnl_settings.grid(row=0, column=0, sticky="ew",padx=5, pady=5) + self.pnl_settings.on_preview_clicked = self.pnl_settings_on_preview_clicked + self.pnl_settings.on_write_clicked = self.pnl_settings_on_write_clicked + # Estimation Panel + self.pnl_estimation = EstimationPanel(self, estimation=self.estimation,settings=self.settings) + self.pnl_estimation.grid(row=1, column=0, sticky="ew",padx=5, pady=5) + self.pnl_estimation.on_calculate_clicked = self.pnl_estimate_on_calculate_clicked + # Buttons + button_frame = ttk.Frame(self) + button_frame.grid(row=3, column=0, sticky="w",padx=5, pady=5) + self.btn_create_new_table = ttk.Button(button_frame, text="Create New Table", command=self.btn_create_new_table_click) + self.btn_create_new_table.grid(row=0, column=0 ,padx=5, pady=5) + + self.btn_open_cvs = ttk.Button(button_frame, text="Import Table", command=self.btn_open_csv_click) + self.btn_open_cvs.grid(row=0, column=1,padx=5, pady=5) + self.btn_save_csv = ttk.Button(button_frame, text="Export Table", command=self.mnu_save_csv_click) + self.btn_save_csv.grid(row=0, column=2,padx=5, pady=5) + + self.mnu_main = tk.Menu(self.parent) + self.parent.config(menu=self.mnu_main) + + file_menu = tk.Menu(self.mnu_main, tearoff=0) + self.mnu_main.add_cascade(label="File", menu=file_menu) + #file_menu.add_command(label="Export to Excel", command=self.mnu_export_to_excel_click) + file_menu.add_command(label="Import Table", command=self.btn_open_csv_click) + file_menu.add_command(label="Export Table", command=self.mnu_save_csv_click) + file_menu.add_separator() + file_menu.add_command(label="Exit", command=self.mnu_exit_click) + + instrument_menu = tk.Menu(self.mnu_main, tearoff=0) + self.mnu_main.add_cascade(label="Instrument", menu=instrument_menu) + + instrument_menu.add_radiobutton(label="LARMOR", variable=self.experiment.instrument, value="LARMOR", command=self.update_panel) + instrument_menu.add_radiobutton(label="LOQ", variable=self.experiment.instrument, value="LOQ", command=self.update_panel) + instrument_menu.add_radiobutton(label="SANS2D", variable=self.experiment.instrument, value="SANS2D", command=self.update_panel) + instrument_menu.add_radiobutton(label="ZOOM", variable=self.experiment.instrument, value="ZOOM", command=self.update_panel) + + + """ + def onFrameConfigure(self, event): + self.canvas.configure(scrollregion=self.canvas.bbox("all")) + """ + + def update_settings_panel(self): + self.pnl_settings.destroy() + self.inst_info = InstrumentInfo(settings=self.settings) + self.pnl_settings = SettingsPanel(self, inst_info=self.inst_info, settings=self.settings) + self.pnl_settings.grid(row=0, column=0, sticky="ew",padx=5, pady=5) + self.pnl_settings.on_preview_clicked = self.pnl_settings_on_preview_clicked + self.pnl_settings.on_write_clicked = self.pnl_settings_on_write_clicked + + def update_estimation_panel(self): + self.pnl_estimation.destroy() + self.inst_info = InstrumentInfo(settings=self.settings) + self.pnl_estimation = EstimationPanel(self, estimation=self.estimation,settings=self.settings) + self.pnl_estimation.grid(row=1, column=0, sticky="ew",padx=5, pady=5) + self.pnl_estimation.on_preview_clicked = self.pnl_settings_on_preview_clicked + self.pnl_estimation.on_write_clicked = self.pnl_settings_on_write_clicked + self.pnl_estimation.on_calculate_clicked = self.pnl_estimate_on_calculate_clicked + + def update_panel(self): + self.update_settings_panel() + self.update_estimation_panel() + + + def load_last_instrument(self): + inst = self.settings.last_instrument # Replace with actual settings retrieval + self.experiment.is_instrument = inst + + + + + def pnl_settings_on_write_clicked(self): + filetypes = [("genie_python Files", "*.py")] if self.experiment.is_instrument in ["LARMOR", "LOQ", "ZOOM", "SANS2D"] else [("Open Genie Files", "*.gcl")] + filepath = filedialog.asksaveasfilename(defaultextension=filetypes[0][1], filetypes=filetypes) + constructor = TableDataConstructor(TableEntries) + + + self.data = constructor.construct_data(self.data_frame.table.model.getAllCells()) + self.experiment_data=constructor.create_experiments(self.data.values()) + + if filepath: + scriptname = filepath.split("/")[-1].replace(" ", "_").rsplit(".", 1)[0] + script_generator= ScriptGenerator(self.settings) + script = script_generator.generate_script(self.experiment_data, scriptname) + with open(filepath, "w") as file: + file.write(script) + + def pnl_settings_on_preview_clicked(self): + self.script_generator= ScriptGenerator(self.settings) + constructor = TableDataConstructor(TableEntries) + + self.data = constructor.construct_data(self.data_frame.table.model.getAllCells()) + self.experiment_data=constructor.create_experiments(self.data.values()) + self.script = self.script_generator.generate_script(self.experiment_data, "my_script") + print(self.script) + preview_dialog = PreviewDialog(self, self.script) + preview_dialog.show() + + def pnl_estimate_on_calculate_clicked(self): + self.script_generator= ScriptGenerator(self.settings) + constructor = TableDataConstructor(TableEntries) + self.data = constructor.construct_data(self.data_frame.table.model.getAllCells()) + self.experiment_data=constructor.create_experiments(self.data.values()) + self.estimation.script_time = self.script_generator.estimate_run_time(self.experiment_data, self.estimation) + self.update_estimation_panel() + + def btn_create_new_table_click(self): + self.data =None + self.data_frame.reset_table() + + + def mnu_exit_click(self): + self.on_close() + + + def mnu_save_csv_click(self): + + try: + self.data_frame.table.exportTable() + except Exception as err: + messagebox.showerror("SANS Script - Error", "Could not save as CSV") + + def btn_open_csv_click(self): + + try: + self.data_frame.table.importTable() + except Exception as err: + messagebox.showerror("SANS Script - Error", f"Could not open CS") + + def on_close(self): + # Save last_instrument to settings + self.settings.save() + self.parent.destroy() + + + + + + +if __name__ == "__main__": + root = tk.Tk() + app = MainWindow(root) + app.grid(sticky="nsew") + #root.grid_rowconfigure(0, weight=1) + #root.grid_columnconfigure(0, weight=1) + app.protocol("WM_DELETE_WINDOW", app.on_close) + app.mainloop() \ No newline at end of file diff --git a/Python_SANS_Script_GUI/Models/A2Setting.py b/Python_SANS_Script_GUI/Models/A2Setting.py new file mode 100644 index 0000000..740b756 --- /dev/null +++ b/Python_SANS_Script_GUI/Models/A2Setting.py @@ -0,0 +1,52 @@ +class A2Setting: + X_LARGE = "XLarge (20x18 mm)" + LARGE = "Large (17 mm)" + MEDIUM = "Medium (12 mm)" + SMALL = "Small (5/1 mm)" + TRANS = "Trans (1 mm)" + LARMOR_LARGE = "Large (25, 16 mm)" + LARMOR_MEDIUM = "Medium (20, 14 mm)" + LARMOR_SMALL = "Small (15, 10 mm)" + + + LARMOR_choices = [ + LARMOR_LARGE, + LARMOR_MEDIUM, + LARMOR_SMALL + ] + + LOQ_choices = [ + LARGE, MEDIUM, SMALL + ] + + SANS2D_choices = [ + X_LARGE, LARGE, MEDIUM, SMALL + ] + + ZOOM_choices = [ + LARGE, MEDIUM, SMALL + ] + + @staticmethod + def get_LARMOR_choices(): + return A2Setting.LARMOR_choices + + @staticmethod + def get_LOQ_choices(): + return A2Setting.LOQ_choices + + @staticmethod + def get_SANS2D_choices(): + return A2Setting.SANS2D_choices + + @staticmethod + def get_ZOOM_choices(): + return A2Setting.ZOOM_choices + +# Example usage +if __name__ == "__main__": + print(A2Setting.get_LOQ_choices()[0]) + + + + diff --git a/Python_SANS_Script_GUI/Models/DataCollection.py b/Python_SANS_Script_GUI/Models/DataCollection.py new file mode 100644 index 0000000..27b81b5 --- /dev/null +++ b/Python_SANS_Script_GUI/Models/DataCollection.py @@ -0,0 +1,45 @@ +class DataCollection: + HISTOGRAM = "Histogram" + EVENTS = "Events" + SESANS = "SESANS" + TRANSSANS_EVENTS = "TRANSSANS_Events" + TRANSSANS_HISTOGRAM = "TRANSSANS_Histogram" + + LOQ_choices = [ + HISTOGRAM, + TRANSSANS_HISTOGRAM, + EVENTS, + TRANSSANS_EVENTS + ] + + SANS2D_choices = [ + EVENTS + ] + + ZOOM_choices = [ + EVENTS + ] + + LARMOR_choices = [ + EVENTS, SESANS + ] + + @staticmethod + def get_LARMOR_choices(): + return DataCollection.LARMOR_choices + + @staticmethod + def get_LOQ_choices(): + return DataCollection.LOQ_choices + + @staticmethod + def get_SANS2D_choices(): + return DataCollection.SANS2D_choices + + @staticmethod + def get_ZOOM_choices(): + return DataCollection.ZOOM_choices + +# Example usage +if __name__ == "__main__": + print(DataCollection.get_ZOOM_choices()) \ No newline at end of file diff --git a/Python_SANS_Script_GUI/Models/Estimation.py b/Python_SANS_Script_GUI/Models/Estimation.py new file mode 100644 index 0000000..40b571e --- /dev/null +++ b/Python_SANS_Script_GUI/Models/Estimation.py @@ -0,0 +1,106 @@ +import tkinter as tk +from Settings import Settings + + + +class Estimation: + def __init__(self, settings=None): + self.settings = settings if settings is not None else Settings() + self._count_rate = tk.DoubleVar(value=self.settings.count_rate) + self._move_time = tk.DoubleVar(value=self.settings.move_time) + self._script_time = tk.StringVar(value="") + + self._is_valid = tk.BooleanVar(value=True) + + + self.property_changed = [] + + self._count_rate.trace_add("write", self.validate) + self._move_time.trace_add("write", self.validate) + self._script_time.trace_add("write", self.validate) + + self._count_rate.trace_add("write", self.on_property_changed) + self._move_time.trace_add("write", self.on_property_changed) + + self._count_rate.trace_add("write", self.save_changes) + self._move_time.trace_add("write", self.save_changes) + + + + + @property + def count_rate(self): + return self._count_rate.get() + + @count_rate.setter + def count_rate(self, value): + self._count_rate.set(value) + self.on_property_changed("count_rate") + + @property + def move_time(self): + return self._move_time.get() + + @move_time.setter + def move_time(self, value): + self._move_time.set(value) + self.on_property_changed("move_time") + + @property + def script_time(self): + return self._script_time.get() + + @script_time.setter + def script_time(self, value): + self._script_time.set(value) + self.on_property_changed("script_time") + + @property + def is_valid(self): + return self._is_valid.get() + + @is_valid.setter + def is_valid(self, value): + self._is_valid.set(value) + self.on_property_changed("is_valid") + + def on_property_changed(self, *args): + for callback in self.property_changed: + callback(args[0]) + + def save_changes(self, *args): + self.settings.count_rate = self.count_rate + self.settings.move_time = self.move_time + + def validate(self, *args): + error_message = "" + if self.count_rate < 0: + error_message = "Count rate cannot be negative!" + elif self.move_time < 0: + error_message = "Move time cannot be negative!" + if error_message: + self.is_valid = False + else: + self.is_valid = True + + return error_message + + def update_count_rate(self): + self._count_rate.set(self.settings.count_rate) + + def update_move_time(self): + self._move_time.set(self.settings.move_time) + +# Example usage +if __name__ == "__main__": + def on_property_changed(name): + print(f"Property {name} changed") + + estimation = Estimation() + estimation.property_changed.append(on_property_changed) + + print(estimation.count_rate) + estimation.count_rate = -10 # This should trigger validation and set is_valid to False + print(estimation.is_valid) + estimation.count_rate = 50 # This should trigger validation and set is_valid to True + print(estimation.is_valid) \ No newline at end of file diff --git a/Python_SANS_Script_GUI/Models/Experiment.py b/Python_SANS_Script_GUI/Models/Experiment.py new file mode 100644 index 0000000..cc33835 --- /dev/null +++ b/Python_SANS_Script_GUI/Models/Experiment.py @@ -0,0 +1,226 @@ +import tkinter as tk + +class Experiment: + + def __init__(self, position="", trans=0, trans_wait="", sans=0, sans_wait="", period="", background="", sample="", thickness=0, temperature1="", temperature2="", field="", shear_rate1="", shear_rate2="", shear_angle1="", shear_angle2="", pre_command="", post_command="", rb_number=""): + self._position = tk.StringVar(value=position) + self._trans = tk.DoubleVar(value=trans) + self._trans_wait = tk.StringVar(value=trans_wait) + self._sans = tk.DoubleVar(value=sans) + self._sans_wait = tk.StringVar(value=sans_wait) + self._period = tk.StringVar(value=period) + self._background = tk.StringVar(value=background) + self._sample = tk.StringVar(value=sample) + self._thickness = tk.DoubleVar(value=thickness) + self._temperature1 = tk.StringVar(value=temperature1) + self._temperature2 = tk.StringVar(value=temperature2) + self._field = tk.StringVar(value=field) + self._shear_rate1 = tk.StringVar(value=shear_rate1) + self._shear_rate2 = tk.StringVar(value=shear_rate2) + self._shear_angle1 = tk.StringVar(value=shear_angle1) + self._shear_angle2 = tk.StringVar(value=shear_angle2) + self._pre_command = tk.StringVar(value=pre_command) + self._post_command = tk.StringVar(value=post_command) + self._rb_number = tk.StringVar(value=rb_number) + self.property_changed = [] + + self._position.trace_add("write", self.on_property_changed) + self._trans.trace_add("write", self.on_property_changed) + self._trans_wait.trace_add("write", self.on_property_changed) + self._sans.trace_add("write", self.on_property_changed) + self._sans_wait.trace_add("write", self.on_property_changed) + self._period.trace_add("write", self.on_property_changed) + self._background.trace_add("write", self.on_property_changed) + self._sample.trace_add("write", self.on_property_changed) + self._thickness.trace_add("write", self.on_property_changed) + self._temperature1.trace_add("write", self.on_property_changed) + self._temperature2.trace_add("write", self.on_property_changed) + self._field.trace_add("write", self.on_property_changed) + self._shear_rate1.trace_add("write", self.on_property_changed) + self._shear_rate2.trace_add("write", self.on_property_changed) + self._shear_angle1.trace_add("write", self.on_property_changed) + self._shear_angle2.trace_add("write", self.on_property_changed) + self._pre_command.trace_add("write", self.on_property_changed) + self._post_command.trace_add("write", self.on_property_changed) + self._rb_number.trace_add("write", self.on_property_changed) + + def on_property_changed(self, *args): + for callback in self.property_changed: + callback(args[0]) + + @property + def position(self): + return self._position.get() + + @position.setter + def position(self, value): + self._position.set(value) + + @property + def trans(self): + return self._trans.get() + + @trans.setter + def trans(self, value): + self._trans.set(value) + + @property + def trans_wait(self): + return self._trans_wait.get() + + @trans_wait.setter + def trans_wait(self, value): + self._trans_wait.set(value) + + @property + def sans(self): + return self._sans.get() + + @sans.setter + def sans(self, value): + self._sans.set(value) + + @property + def sans_wait(self): + return self._sans_wait.get() + + @sans_wait.setter + def sans_wait(self, value): + self._sans_wait.set(value) + + @property + def period(self): + return self._period.get() + + @period.setter + def period(self, value): + self._period.set(value) + + @property + def background(self): + return self._background.get() + + @background.setter + def background(self, value): + self._background.set(value) + + @property + def sample(self): + return self._sample.get() + + @sample.setter + def sample(self, value): + self._sample.set(value) + + @property + def thickness(self): + value = self._thickness.get() + if value == "": + raise ValueError("Thickness cannot be an empty string") + return float(value) + + @thickness.setter + def thickness(self, value): + self._thickness.set(value) + + @property + def temperature1(self): + return self._temperature1.get() + + @temperature1.setter + def temperature1(self, value): + self._temperature1.set(value) + + @property + def temperature2(self): + return self._temperature2.get() + + @temperature2.setter + def temperature2(self, value): + self._temperature2.set(value) + + @property + def field(self): + return self._field.get() + + @field.setter + def field(self, value): + self._field.set(value) + + @property + def shear_rate1(self): + return self._shear_rate1.get() + + @shear_rate1.setter + def shear_rate1(self, value): + self._shear_rate1.set(value) + + @property + def shear_rate2(self): + return self._shear_rate2.get() + + @shear_rate2.setter + def shear_rate2(self, value): + self._shear_rate2.set(value) + + @property + def shear_angle1(self): + return self._shear_angle1.get() + + @shear_angle1.setter + def shear_angle1(self, value): + self._shear_angle1.set(value) + + @property + def shear_angle2(self): + return self._shear_angle2.get() + + @shear_angle2.setter + def shear_angle2(self, value): + self._shear_angle2.set(value) + + @property + def pre_command(self): + return self._pre_command.get() + + @pre_command.setter + def pre_command(self, value): + self._pre_command.set(value) + + @property + def post_command(self): + return self._post_command.get() + + @post_command.setter + def post_command(self, value): + self._post_command.set(value) + + @property + def rb_number(self): + return self._rb_number.get() + + @rb_number.setter + def rb_number(self, value): + self._rb_number.set(value) + +# Example usage +if __name__ == "__main__": + def on_property_changed(name): + print(f"Property {name} changed") + root = tk.Tk() + experiment = Experiment() + experiment.property_changed.append(on_property_changed) + + experiment.position = "New Position" + experiment.trans = 10.5 + experiment.sans = 20.0 + experiment.temperature1 = "25" + experiment.temperature2 = "30" + experiment.field = "1.5T" + experiment.shear_rate1 = "100" + experiment.shear_rate2 = "200" + experiment.shear_angle1 = "45" + experiment.shear_angle2 = "90" + experiment.pre_command = "Pre Command" + experiment.post_command = "Post Command" + experiment.rb_number = "RB12345" \ No newline at end of file diff --git a/Python_SANS_Script_GUI/Models/ExperimentSettings.py b/Python_SANS_Script_GUI/Models/ExperimentSettings.py new file mode 100644 index 0000000..aab40ae --- /dev/null +++ b/Python_SANS_Script_GUI/Models/ExperimentSettings.py @@ -0,0 +1,300 @@ +import tkinter as tk +from Settings import Settings +from Models.A2Setting import A2Setting +from Models.DataCollection import DataCollection +from Models.Estimation import Estimation +from Settings import Settings +from Models.RunOrder import RunOrder +from Models.Geometry import Geometry + +class ExperimentSettings: + def __init__(self, settings=None, estimation=None): + self.settings = settings if settings is not None else Settings() + self.estimation = estimation if estimation is not None else Estimation(settings=self.settings) + + + + self._run_order = tk.StringVar(value=RunOrder.get_choice_string(self.settings.run_order)) + self._num_sans = tk.IntVar(value=self.settings.num_sans) + self._num_trans = tk.IntVar(value=self.settings.num_trans) + self._loop_per_run = tk.BooleanVar(value=self.settings.loop_per_run) + self._a2_setting_sans = tk.StringVar(value=self.settings.a2_setting_sans) + self._a2_setting_trans = tk.StringVar(value=self.settings.a2_setting_trans) + self._sample_geometry = tk.StringVar(value=Geometry.get_choice_string(self.settings.sample_geometry)) + self._sample_height = tk.DoubleVar(value=self.settings.sample_height) + self._sample_width = tk.DoubleVar(value=self.settings.sample_width) + self._sample_width_enabled = tk.BooleanVar(value=True) + + self._collection_mode = tk.StringVar(value=self.settings.collection_mode) + self._is_valid = tk.BooleanVar(value=True) + self.property_changed = [] + + self.instrument = tk.StringVar(value=self.settings.last_instrument) + self.instrument.trace_add("write", self.on_property_changed) + self.instrument.trace_add("write", self.update_instrument) + + self._run_order.trace_add("write", self.on_property_changed) + self._num_sans.trace_add("write", self.on_property_changed) + self._num_trans.trace_add("write", self.on_property_changed) + self._loop_per_run.trace_add("write", self.on_property_changed) + self._a2_setting_sans.trace_add("write", self.on_property_changed) + self._a2_setting_trans.trace_add("write", self.on_property_changed) + self._sample_geometry.trace_add("write", self.on_property_changed) + self._sample_height.trace_add("write", self.on_property_changed) + self._sample_width.trace_add("write", self.on_property_changed) + self._collection_mode.trace_add("write", self.on_property_changed) + + self._num_sans.trace_add("write", self.validate) + self._num_trans.trace_add("write", self.validate) + self._sample_height.trace_add("write", self.validate) + self._sample_width.trace_add("write", self.validate) + self._loop_per_run.trace_add("write", self.validate) + + + self._run_order.trace_add("write", self.save_changes) + self._num_sans.trace_add("write", self.save_changes) + self._num_trans.trace_add("write", self.save_changes) + self._loop_per_run.trace_add("write", self.save_changes) + self._a2_setting_sans.trace_add("write", self.save_changes) + self._a2_setting_trans.trace_add("write", self.save_changes) + self._sample_geometry.trace_add("write", self.save_changes) + self._sample_height.trace_add("write", self.save_changes) + self._sample_width.trace_add("write", self.save_changes) + self._collection_mode.trace_add("write", self.save_changes) + + self.larmor_aperture_sizes=A2Setting.get_LARMOR_choices() + self.loq_aperture_sizes=A2Setting.get_LOQ_choices() + self.sans2d_aperture_sizes=A2Setting.get_SANS2D_choices() + self.zoom_aperture_sizes=A2Setting.get_ZOOM_choices() + + self.larmor_data_collection=DataCollection.get_LARMOR_choices() + self.loq_data_collection=DataCollection.get_LOQ_choices() + self.sans2d_data_collection=DataCollection.get_SANS2D_choices() + self.zoom_data_collection=DataCollection.get_ZOOM_choices() + + self.update_instrument() + + ''' + def __call__(self, order=None, num_sans=None, num_trans=None, loop_per_run=None, a2_setting_sans=None, a2_setting_trans=None, sample_geometry=None, sample_height=None, sample_width=None, collection_mode=None, is_instrument=None, is_valid=None): + if order is not None: + self.order = order + if num_sans is not None: + self.num_sans = num_sans + if num_trans is not None: + self.num_trans = num_trans + if loop_per_run is not None: + self.loop_per_run = loop_per_run + if a2_setting_sans is not None: + self.a2_setting_sans = a2_setting_sans + if a2_setting_trans is not None: + self.a2_setting_trans = a2_setting_trans + if sample_geometry is not None: + self.sample_geometry = sample_geometry + if sample_height is not None: + self.sample_height = sample_height + if sample_width is not None: + self.sample_width = sample_width + if collection_mode is not None: + self.collection_mode = collection_mode + if is_valid is not None: + self.is_valid = is_valid + if is_instrument is not None: + self.is_instrument = is_instrument + ''' + + def on_property_changed(self, *args): + for callback in self.property_changed: + callback(args[0]) + + + + def update_instrument(self, *args): + if self.is_instrument == "LARMOR": + self.set_instrument(self.is_instrument, "A1, S1 Setting:", self.larmor_aperture_sizes,"Data Acquisition:",self.larmor_data_collection, 40) + elif self.is_instrument == "LOQ": + self.set_instrument(self.is_instrument, "A2 Setting:", self.loq_aperture_sizes,"Data Acquisition:",self.loq_data_collection, 170) + elif self.is_instrument == "SANS2D": + self.set_instrument(self.is_instrument, "A6 Setting:", self.sans2d_aperture_sizes,"Data Acquisition:",self.sans2d_data_collection, 40) + elif self.is_instrument == "ZOOM": + self.set_instrument(self.is_instrument, "A6 Setting:", self.zoom_aperture_sizes,"Data Acquisition:",self.zoom_data_collection, 40) + + + def set_instrument(self, instrument, a2_label, a2_choices, data_collection_label, data_collection_choices, count_rate): + + self.settings.last_instrument = instrument + self.estimation.count_rate = count_rate + self.count_rate = count_rate + self.settings.count_rate = count_rate + + + self.a2_label = a2_label + self.a2_choices = a2_choices + self.data_collection_label = data_collection_label + self.data_collection_choices = data_collection_choices + self.a2_trans_enabled = True + self.collection_mode_enabled = True + + # set to default 1st entry in choices + self.settings.a2_setting_sans = a2_choices[0] + self.settings.a2_setting_trans = a2_choices[0] + self.settings.collection_mode = 'Events' + + + + def save_changes(self, *args): + self.settings.run_order = self.run_order + self.settings.num_sans = self.num_sans + self.settings.num_trans = self.num_trans + self.settings.loop_per_run = self.loop_per_run + self.settings.a2_setting_sans = self.a2_setting_sans + self.settings.a2_setting_trans = self.a2_setting_trans + self.settings.sample_geometry = self.sample_geometry + self.settings.sample_height = self.sample_height + self.settings.sample_width = self.sample_width + self.settings.collection_mode = self.collection_mode + + + @property + def run_order(self): + return self._order.get() + + @run_order.setter + def run_order(self, value): + self._run_order.set(str(value)) + + @property + def num_sans(self): + return self._num_sans.get() + + @num_sans.setter + def num_sans(self, value): + self._num_sans.set(int(value)) + + @property + def num_trans(self): + return self._num_trans.get() + + @num_trans.setter + def num_trans(self, value): + self._num_trans.set(int(value)) + + @property + def loop_per_run(self): + return self._loop_per_run.get() + + @loop_per_run.setter + def loop_per_run(self, value): + self._loop_per_run.set(str(value)) + + @property + def a2_setting_sans(self): + return self._a2_setting_sans.get() + + @a2_setting_sans.setter + def a2_setting_sans(self, value): + self._a2_setting_sans.set(str(value)) + + @property + def a2_setting_trans(self): + return self._a2_setting_trans.get() + + @a2_setting_trans.setter + def a2_setting_trans(self, value): + self._a2_setting_trans.set(str(value)) + + @property + def sample_geometry(self): + return self._sample_geometry.get() + + @sample_geometry.setter + def sample_geometry(self, value): + self._sample_geometry.set(str(value)) + self.sample_width_enabled = value != "Disc" + + @property + def sample_height(self): + return self._sample_height.get() + + @sample_height.setter + def sample_height(self, value): + self._sample_height.set(float(value)) + if not self.sample_width_enabled: + self.sample_width = float(value) + + @property + def sample_width(self): + return self._sample_width.get() + + @sample_width.setter + def sample_width(self, value): + self._sample_width.set(float(value)) + + @property + def sample_width_enabled(self): + return self._sample_width_enabled.get() + + @sample_width_enabled.setter + def sample_width_enabled(self, value): + self._sample_width_enabled.set(float(value)) + if not value: + self.sample_width = self.sample_height + + @property + def collection_mode(self): + return self._collection_mode.get() + + @collection_mode.setter + def collection_mode(self, value): + self._collection_mode.set(str(value)) + + @property + def is_instrument(self): + return self.instrument.get() + + @is_instrument.setter + def is_instrument(self, value): + self.instrument.set(str(value)) + + @property + def is_valid(self): + return self._is_valid.get() + + @is_valid.setter + def is_valid(self, value): + self._is_valid.set(value) + + def validate(self, *args): + error_message = "" + if self.loop_per_run < 0: + error_message = "Number of Loops per runs cannot be negative!" + if self.num_sans < 0: + error_message = "Number of SANS runs cannot be negative!" + elif self.num_trans < 0: + error_message = "Number of TRANS runs cannot be negative!" + elif self.sample_height <= 0 or self.sample_height > 20: + error_message = "Sample height must be between 0 and 20" + elif self.sample_width <= 0 or self.sample_width > 20: + error_message = "Sample width must be between 0 and 20" + + if error_message: + self.is_valid = False + else: + self.is_valid = True + + return error_message + +# Example usage +if __name__ == "__main__": + def on_property_changed(name): + print(f"Property {name} changed") + + settings = ExperimentSettings() + settings.property_changed.append(on_property_changed) + + settings.order = "SansFirst" + settings.num_sans = 2 + settings.num_trans = 3 + settings.sample_height = 15 + settings.sample_width = 10 + settings.collection_mode = "Histogram" + print(settings.validate()) \ No newline at end of file diff --git a/Python_SANS_Script_GUI/Models/Geometry.py b/Python_SANS_Script_GUI/Models/Geometry.py new file mode 100644 index 0000000..966004a --- /dev/null +++ b/Python_SANS_Script_GUI/Models/Geometry.py @@ -0,0 +1,43 @@ +class Geometry: + DISC = [1, "Disc"] + CYLINDRICAL = [2, "Cylindrical"] + FLAT_PLATE = [3, "Flat Plate"] + SINGLE_CRYSTAL = [4, "Single Crystal"] + + + choices = [ + DISC[1], + CYLINDRICAL[1], + FLAT_PLATE[1], + SINGLE_CRYSTAL[1] + ] + + list_order = ( + DISC, + CYLINDRICAL, + FLAT_PLATE, + SINGLE_CRYSTAL + ) + + @staticmethod + def get_choices(): + return Geometry.choices + + @staticmethod + def get_choice_string(ordinal): + for choice in Geometry.list_order: + if choice[0] == float(ordinal): + return choice[1] + return None + + @staticmethod + def get_ordinal_from_string(choice_string): + for choice in Geometry.list_order: + if choice[1] == choice_string: + return choice[0] + return None + +# Example usage +if __name__ == "__main__": + print(Geometry.get_choices()) + print(Geometry.get_choice_string(1)) \ No newline at end of file diff --git a/Python_SANS_Script_GUI/Models/InstrumentInfo.py b/Python_SANS_Script_GUI/Models/InstrumentInfo.py new file mode 100644 index 0000000..c3f3942 --- /dev/null +++ b/Python_SANS_Script_GUI/Models/InstrumentInfo.py @@ -0,0 +1,118 @@ +import tkinter as tk +from tkinter import StringVar, BooleanVar +from typing import List +#from Settings import Settings +from Models.ExperimentSettings import ExperimentSettings + +class InstrumentInfo: + property_changed = [] + static_property_changed = [] + script_generator = None + a2_label = "" + a2_choices = [] + data_collection_label = "" + data_collection_choices = [] + a2_trans_enabled = False + collection_mode_enabled = True + + def __init__(self,settings=None, experiment=None): + #self.settings = settings if settings is not None else Settings() + self.experiment = experiment if experiment is not None else ExperimentSettings() + self._a2_label = StringVar(value=self.experiment.a2_label) + self._a2_choices = tk.Variable(value=self.experiment.a2_choices) + self._data_collection_label = StringVar(value=self.experiment.data_collection_label) + self._data_collection_choices = tk.Variable(value=self.experiment.data_collection_choices) + self._a2_trans_enabled = BooleanVar(value=self.experiment.a2_trans_enabled) + self._collection_mode_enabled = BooleanVar(value=self.experiment.collection_mode_enabled) + self._script_generator = None + + + self._a2_label.trace_add("write", self.on_property_changed) + self._a2_choices.trace_add("write", self.on_property_changed) + self._data_collection_label.trace_add("write", self.on_property_changed) + self._data_collection_choices.trace_add("write", self.on_property_changed) + self._a2_trans_enabled.trace_add("write", self.on_property_changed) + self._collection_mode_enabled.trace_add("write", self.on_property_changed) + + + def on_property_changed(self, *args): + for callback in self.property_changed: + callback(args[0]) + + @staticmethod + def on_static_property_changed(property_name): + for callback in InstrumentInfo.static_property_changed: + callback(property_name) + + @property + def a2_label(self): + return self._a2_label.get() + + @a2_label.setter + def a2_label(self, value): + self._a2_label.set(value) + self.on_static_property_changed("a2_label") + + @property + def a2_choices(self): + return self._a2_choices.get() + + @a2_choices.setter + def a2_choices(self, value: List[str]): + self._a2_choices.set(value) + self.on_static_property_changed("a2_choices") + + @property + def data_collection_label(self): + return self._data_collection_label.get() + + @data_collection_label.setter + def data_collection_label(self, value): + self._data_collection_label.set(value) + self.on_static_property_changed("data_collection_label") + + @property + def data_collection_choices(self): + return self._data_collection_choices.get() + + @data_collection_choices.setter + def data_collection_choices(self, value: List[str]): + self._data_collection_choices.set(value) + self.on_static_property_changed("data_collection_choices") + + @property + def a2_trans_enabled(self): + return self._a2_trans_enabled.get() + + @a2_trans_enabled.setter + def a2_trans_enabled(self, value): + self._a2_trans_enabled.set(value) + self.on_static_property_changed("a2_trans_enabled") + + @property + def collection_mode_enabled(self): + return self._collection_mode_enabled.get() + + @collection_mode_enabled.setter + def collection_mode_enabled(self, value): + self._collection_mode_enabled.set(value) + self.on_static_property_changed("collection_mode_enabled") + + +# Example usage +if __name__ == "__main__": + root = tk.Tk() + def on_property_changed(name): + print(f"Property {name} changed") + + def on_static_property_changed(name): + print(f"Static property {name} changed") + + instrument_info = InstrumentInfo() + instrument_info.property_changed.append(on_property_changed) + InstrumentInfo.static_property_changed.append(on_static_property_changed) + + instrument_info.a2_label = "New A2 Setting:" + instrument_info.a2_choices = ["XLarge", "Large", "Medium", "Small"] + instrument_info.a2_trans_enabled = False + instrument_info.collection_mode_enabled = True \ No newline at end of file diff --git a/Python_SANS_Script_GUI/Models/RunOrder.py b/Python_SANS_Script_GUI/Models/RunOrder.py new file mode 100644 index 0000000..a05cd41 --- /dev/null +++ b/Python_SANS_Script_GUI/Models/RunOrder.py @@ -0,0 +1,43 @@ +class RunOrder: + ALL_TRANS = [1, "1: All TRANS first"] + TRANS_FIRST = [2, "2: Alternate - TRANS first"] + ALL_SANS = [3, "3: SANS first"] + SANS_FIRST = [4, "4: Alternate - SANS first"] + + choices = ( + ALL_TRANS[1], + TRANS_FIRST[1], + ALL_SANS[1], + SANS_FIRST[1] + ) + + list_order = ( + ALL_TRANS, + TRANS_FIRST, + ALL_SANS, + SANS_FIRST + ) + + @staticmethod + def get_choices(): + return RunOrder.choices + + @staticmethod + def get_choice_string(ordinal): + for choice in RunOrder.list_order: + if choice[0] == float(ordinal): + return choice[1] + return None + + @staticmethod + def get_ordinal_from_string(choice_string): + for choice in RunOrder.list_order: + if choice[1] == choice_string: + return choice[0] + return None + +# Example usage +if __name__ == "__main__": + print(RunOrder.get_choices()) + print(RunOrder.get_choice_string(1)) + print(RunOrder.get_ordinal_from_string("1: All TRANS first")) \ No newline at end of file diff --git a/Python_SANS_Script_GUI/Models/WaitForUnits.py b/Python_SANS_Script_GUI/Models/WaitForUnits.py new file mode 100644 index 0000000..e43cecf --- /dev/null +++ b/Python_SANS_Script_GUI/Models/WaitForUnits.py @@ -0,0 +1,20 @@ +class WaitForUnits: + MICROAMPS = "uamps" + MINUTES = "Minutes" + SECONDS = "Seconds" + FRAMES = "Frames" + + choices = [ + MICROAMPS, + MINUTES, + SECONDS, + FRAMES + ] + + @staticmethod + def get_choices(): + return WaitForUnits.choices + +# Example usage +if __name__ == "__main__": + print(WaitForUnits.get_choices()) \ No newline at end of file diff --git a/Python_SANS_Script_GUI/Properties/AssemblyInfo.py b/Python_SANS_Script_GUI/Properties/AssemblyInfo.py new file mode 100644 index 0000000..e4c5d67 --- /dev/null +++ b/Python_SANS_Script_GUI/Properties/AssemblyInfo.py @@ -0,0 +1,23 @@ +from setuptools import setup, find_packages + +setup( + name="LOQ_Script_Gui", + version="1.0.0", + description="", + author="", + author_email="", + url="", + packages=find_packages(), + include_package_data=True, + install_requires=[ + # Add your dependencies here + ], + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires=">=3.6", +) + +# Additional metadata for localization and resource management can be added as needed. \ No newline at end of file diff --git a/Python_SANS_Script_GUI/SANS_Icon.ico b/Python_SANS_Script_GUI/SANS_Icon.ico new file mode 100644 index 0000000..514a7d8 Binary files /dev/null and b/Python_SANS_Script_GUI/SANS_Icon.ico differ diff --git a/Python_SANS_Script_GUI/Scripting/ScriptGenerator.py b/Python_SANS_Script_GUI/Scripting/ScriptGenerator.py new file mode 100644 index 0000000..02e998e --- /dev/null +++ b/Python_SANS_Script_GUI/Scripting/ScriptGenerator.py @@ -0,0 +1,293 @@ +import tkinter as tk + +from datetime import datetime, timedelta +from Models.Estimation import Estimation +from Models.Experiment import Experiment +from Settings import Settings +from Models.ExperimentSettings import ExperimentSettings +from Models.A2Setting import A2Setting +from Models.RunOrder import RunOrder +from Models.DataCollection import DataCollection + + +class ScriptGenerator: + def __init__(self, settings=None): + self.settings = settings if settings is not None else Settings() + #self.experiment = experiment if experiment is not None else ExperimentSettings() + self.indent = " " + self.instrument = self.settings.last_instrument + + + def generate_script(self, runs, scriptname): + script = f"# Script created by SANSScript at {datetime.now()}\n\n" + script += self.generate_header(scriptname) + script += self.generate_setup() + script += self.generate_runs(runs) + script += "\n" + return script + + def get_indent_string(self, n): + return self.indent * n + + def generate_header(self, scriptname): + script = "import sys\n" + script += "from genie_python import genie as g, BLOCK_NAMES as b\n" + script += "sys.path.append(r\"c:\\instrument\\scripts\")\n" + script += self.import_instrument_module() + script += f"\ndef {scriptname}():\n" + return script + + def import_instrument_module(self): + instrument_modules = { + "LARMOR": "from instrument.larmor import * # pylint: disable=wildcard-import, unused-wildcard-import\n", + "LOQ": "from instrument.loq import * # pylint: disable=wildcard-import, unused-wildcard-import\n", + "ZOOM": "from instrument.zoom import * # pylint: disable=wildcard-import, unused-wildcard-import\n", + "SANS2D": "from instrument.sans2d import * # pylint: disable=wildcard-import, unused-wildcard-import\n" + } + return instrument_modules.get(self.instrument, "") + + def generate_setup(self): + script = "" + script += self.change_sample_parameter('Width', self.settings.sample_width) + script += self.change_sample_parameter('height', self.settings.sample_height) + script += self.change_sample_parameter('Geometry', self.settings.sample_geometry) + return script + + def change_sample_parameter(self, param, value): + return self.get_indent_string(1) + f"g.change_sample_par('{param}', '{value}')\n" + + + def check_positions(self, runs): + script = "" + + position_list = ",".join([exp.position for exp in runs if exp.position]) + if position_list: + script += self.get_indent_string(1) + f"if check_move_pos(['{position_list}']) != True:\n" + script += self.get_indent_string(2) + "print('Invalid positions - script aborted')\n" + script += self.get_indent_string(2) + "return\n\n" + return script + + def generate_runs(self, runs): + script = self.check_positions(runs) + if not runs: + return script + + if float(self.settings.run_order) == RunOrder.ALL_TRANS[0]: + script += self.generate_all_trans_runs(runs) + elif float(self.settings.run_order) == RunOrder.ALL_SANS[0]: + script += self.generate_all_sans_runs(runs) + else: + script += self.generate_mixed_runs(runs) + return script + + def generate_all_trans_runs(self, runs): + script = self.get_indent_string(1) + f"for i in range({self.settings.num_trans}):\n" + for exp in runs: + if exp.trans: + script = self.do_trans_run(script, exp, 2) + script += self.get_indent_string(1) + f"for i in range({self.settings.num_sans}):\n" + for exp in runs: + if exp.sans: + script = self.do_sans_run(script, exp, 2) + return script + + def generate_all_sans_runs(self, runs): + script = self.get_indent_string(1) + f"for i in range({self.settings.num_sans}):\n" + for exp in runs: + if exp.sans: + script = self.do_sans_run(script, exp, 2) + if self.settings.num_trans and any(exp.trans for exp in runs): + script += self.get_indent_string(1) + f"for i in range({self.settings.num_trans}):\n" + for exp in runs: + if exp.trans: + script = self.do_trans_run(script, exp, 2) + return script + + def generate_mixed_runs(self, runs): + script = ( + self.get_indent_string(1) + f"num_sans = {self.settings.num_sans}\n" + + self.get_indent_string(1) + f"num_trans = {self.settings.num_trans}\n" + + self.get_indent_string(1) + "count = 0\n" + + self.get_indent_string(1) + "while True:\n" + ) + indent = 2 + for exp in runs: + if float(self.settings.run_order) == RunOrder.TRANS_FIRST[0]: + script = self.add_trans_first_runs(script, exp, indent) + else: + script = self.add_sans_first_runs(script, exp, indent) + script += self.get_indent_string(indent) + "count += 1\n" + script += self.get_indent_string(indent) + "if count >= num_trans and count >= num_sans: break\n" + return script + + def add_trans_first_runs(self, script, exp, indent): + if exp.trans: + script += self.get_indent_string(indent) + "if count < num_trans:\n" + script = self.do_trans_run(script, exp, indent + 1) + if exp.sans: + script += self.get_indent_string(indent) + "if count < num_sans:\n" + script = self.do_sans_run(script, exp, indent + 1) + return script + + def add_sans_first_runs(self, script, exp, indent): + if exp.sans: + script += self.get_indent_string(indent) + "if count < num_sans:\n" + script = self.do_sans_run(script, exp, indent + 1) + if exp.trans: + script += self.get_indent_string(indent) + "if count < num_trans:\n" + script = self.do_trans_run(script, exp, indent + 1) + return script + + def do_trans_run(self, script, exp, indent): + script += self.do_sample_environment(exp, indent) + waitfor, rtype, fulltitle = self.get_run_parameters(exp, "trans") + script += self.get_trans_run_command(exp, indent, waitfor, fulltitle) + script += self.get_indent_string(indent) + exp.post_command + "\n" + return script + + def do_sans_run(self, script, exp, indent): + script += self.do_sample_environment(exp, indent) + waitfor, rtype, fulltitle = self.get_run_parameters(exp, "sans") + script += self.get_sans_run_command(exp, indent, waitfor, fulltitle) + script += self.get_indent_string(indent) + exp.post_command + "\n" + return script + + def get_run_parameters(self, exp, run_type): + waitfor = "uamps" + rtype = "histogram" + fulltitle = f"{{{exp.sample}}}" + if run_type == "trans": + if exp.trans_wait == "Frames": + waitfor = "frames" + elif exp.trans_wait == "Seconds": + waitfor = "seconds" + elif exp.trans_wait == "Minutes": + waitfor = "minutes" + else: + if exp.background: + fulltitle += f"_{{{exp.background}}}" + if self.settings.collection_mode in ["Events", "TRANSSANS_Events"]: + rtype = "event" + elif self.settings.collection_mode == "SESANS": + rtype = "sesans" + if self.settings.collection_mode in ["TRANSSANS_Events", "TRANSSANS_Histogram"]: + fulltitle += "_TRANS" + if exp.sans_wait == "Frames": + waitfor = "frames" + elif exp.sans_wait == "Seconds": + waitfor = "seconds" + elif exp.sans_wait == "Minutes": + waitfor = "minutes" + return waitfor, rtype, fulltitle + + def get_trans_run_command(self, exp, indent, waitfor, fulltitle): + if self.instrument in ["LARMOR", "ZOOM"]: + return self.get_indent_string(indent) + f"do_trans(title='{fulltitle}', pos='{exp.position}', {waitfor}={exp.trans}, thickness={exp.thickness})\n" + elif self.instrument == "LOQ": + return self.get_indent_string(indent) + f"do_trans(title='{fulltitle}', pos='{exp.position}', {waitfor}={exp.trans}, thickness={exp.thickness}, aperture='small')\n" + else: + return self.get_indent_string(indent) + f"do_trans(title='{fulltitle}', pos='{exp.position}', {waitfor}={exp.trans}, thickness={exp.thickness}, dae='transmission', aperture='{self.aperture_string(self.settings.a2_setting_trans)}')\n" + + def get_sans_run_command(self, exp, indent, waitfor, fulltitle): + if self.instrument in ["LARMOR", "ZOOM"]: + return self.get_indent_string(indent) + f"do_sans(title='{fulltitle}', pos='{exp.position}', {waitfor}={exp.sans}, thickness={exp.thickness}, dae='histogram')\n" + else: + return self.get_indent_string(indent) + f"do_sans(title='{fulltitle}', pos='{exp.position}', {waitfor}={exp.sans}, thickness={exp.thickness}, dae='histogram', aperture='{self.aperture_string(self.settings.a2_setting_sans)}')\n" + + + def aperture_string(self, size): + aperture_sizes = { + A2Setting.LARGE: "large", + A2Setting.LARMOR_LARGE: "large", + A2Setting.MEDIUM: "medium", + A2Setting.LARMOR_MEDIUM: "medium", + A2Setting.SMALL: "small", + A2Setting.LARMOR_SMALL: "small", + A2Setting.X_LARGE: "xlarge" + } + return aperture_sizes.get(size, "") + + def do_sample_environment(self, exp, indent): + script = "" + script += self.set_temperature(exp.temperature1, "JULABO1", indent) + script += self.set_temperature(exp.temperature2, "JULABO2", indent) + if exp.pre_command: + script += self.get_indent_string(indent) + exp.pre_command + "\n" + if exp.field: + script += self.get_indent_string(indent) + f"g.cset(FIELD={exp.field})\n" + if exp.shear_rate1: + script += self.get_indent_string(indent) + f"g.cset(SHEAR_RATE_1={exp.shear_rate1})\n" + if exp.shear_rate2: + script += self.get_indent_string(indent) + f"g.cset(SHEAR_RATE_2={exp.shear_rate2})\n" + if exp.shear_angle1: + script += self.get_indent_string(indent) + f"g.cset(SHEAR_ANGLE_1={exp.shear_angle1})\n" + if exp.shear_angle2: + script += self.get_indent_string(indent) + f"g.cset(SHEAR_ANGLE_2={exp.shear_angle2})\n" + return script + + def set_temperature(self, temperature, julabo, indent): + if temperature: + temp = float(temperature) + temp_low = temp - 1.0 + temp_high = temp + 1.0 + if self.is_loq: + return ( + self.get_indent_string(indent) + f"g.cset(internal_setpoint_{julabo[-1]}={temperature})\n" + + self.get_indent_string(indent) + f"g.cset(\"Julabo_{julabo[-1]}_ext\", lowlimit={temp_low}, highlimit={temp_high}, runcontrol=True)\n" + ) + else: + return ( + self.get_indent_string(indent) + f"g.cset({julabo}_SP={temperature})\n" + + self.get_indent_string(indent) + f"g.cset(\"{julabo}_TEMP\", lowlimit={temp_low}, highlimit={temp_high}, runcontrol=True)\n" + ) + return "" + + def estimate_run_time(self, runs, estimation): + minutes = 0 + if self.settings.num_sans > 0: + minutes += self.calculate_time(runs, True, estimation.count_rate, estimation.move_time) * self.settings.num_sans + if self.settings.num_trans > 0: + minutes += self.calculate_time(runs, False, estimation.count_rate, estimation.move_time) * self.settings.num_trans + time = datetime(1, 1, 1, 0, 0, 0) + timedelta(minutes=minutes) + return time.strftime("%H:%M:%S") + + @staticmethod + def calculate_time(runs, is_sans, count_rate, seconds_between): + minutes = 0 + for exp in runs: + if exp.position: + if is_sans: + minutes += ScriptGenerator.get_minutes(exp.sans_wait, exp.sans, count_rate) + else: + minutes += ScriptGenerator.get_minutes(exp.trans_wait, exp.trans, count_rate) + minutes += seconds_between / 60 + return minutes + + @staticmethod + def get_minutes(wait, length, count_rate): + if wait == "Minutes": + return length + elif wait == "Seconds": + return length / 60.0 + elif wait == "Frames": + return length / (25.0 * 60.0) + else: + return length / (count_rate / 60.0) + + + + +if __name__ == "__main__": + root = tk.Tk() + experiment=Experiment() + runs = [experiment(position="pos1", trans=10, sans=20, trans_wait="Minutes", sans_wait="Minutes", sample="Sample1", thickness=1.0)] + experiment_settings =ExperimentSettings() + settings = experiment_settings(sample_width="10", sample_height="10", sample_geometry="Flat", order="AllTrans", num_trans=1, num_sans=1, collection_mode="Events", a2_setting_trans="Large", a2_setting_sans="Large") + estimation=Estimation() + estimate = estimation(settings=settings) + instrument = "LARMOR" + generator = ScriptGenerator(instrument) + script = generator.generate_script(runs, settings, "my_script") + print(script) + estimated_time = generator.estimate_run_time(runs, settings, estimate) + print(f"Estimated run time: {estimated_time}") \ No newline at end of file diff --git a/Python_SANS_Script_GUI/Settings.ini b/Python_SANS_Script_GUI/Settings.ini new file mode 100644 index 0000000..a6c0b47 --- /dev/null +++ b/Python_SANS_Script_GUI/Settings.ini @@ -0,0 +1,16 @@ +[DEFAULT] +lastinstrument = LOQ +sampleheight = 10 +samplewidth = 2 +geometry = 4 +runorder = 2 +numtrans = 3 +numsans = 1 +loopperrun = True +a2settingsans = Large (17 mm) +a2settingtrans = Large (17 mm) +collectionmode = Events +tableentries = "POSITION, TRANS, TRANS_WAIT, SANS, SANS_WAIT, PERIOD, SAMPLE_IDENTIFICATION_STRING, BACKGROUND, THICKNESS, TEMPERATURE, TEMPERATURE2, FIELD, SHEAR_RATE_1, SHEAR_RATE_2, SHEAR_ANGLE_1, SHEAR_ANGLE_2, PRE_COMMAND, POST_COMMAND, RB_NUMBER" +countrate = 170 +movetime = 60.0 + diff --git a/Python_SANS_Script_GUI/Settings.py b/Python_SANS_Script_GUI/Settings.py new file mode 100644 index 0000000..ba06326 --- /dev/null +++ b/Python_SANS_Script_GUI/Settings.py @@ -0,0 +1,139 @@ +import configparser +import os + +class Settings: + def __init__(self, filename="Settings.ini"): + self.dict_settings = configparser.ConfigParser() + directory=os.path.dirname(os.path.abspath(__file__)) + + self.setting_ini_path = os.path.join(directory,filename) + print(self.setting_ini_path) + self.dict_settings.read(self.setting_ini_path) + + @property + def last_instrument(self): + return str(self.dict_settings.get("DEFAULT","LastInstrument")) + + @last_instrument.setter + def last_instrument(self, value): + self.dict_settings.set("DEFAULT","LastInstrument", str(value)) + self.save() + + @property + def sample_height(self): + return float(self.dict_settings.get("DEFAULT","SampleHeight")) + + @sample_height.setter + def sample_height(self, value): + self.dict_settings.set("DEFAULT","SampleHeight", str(value)) + self.save() + + @property + def sample_width(self): + return float(self.dict_settings.get("DEFAULT","SampleWidth")) + + @sample_width.setter + def sample_width(self, value): + self.dict_settings.set("DEFAULT","SampleWidth", str(value)) + self.save() + + @property + def sample_geometry(self): + return str(self.dict_settings.get("DEFAULT","Geometry")) + + @sample_geometry.setter + def sample_geometry(self, value): + self.dict_settings.set("DEFAULT","Geometry", str(value)) + self.save() + + @property + def run_order(self): + return str(self.dict_settings.get("DEFAULT","RunOrder")) + + @run_order.setter + def run_order(self, value): + self.dict_settings.set("DEFAULT","RunOrder", str(value)) + self.save() + + @property + def num_trans(self): + return int(self.dict_settings.get("DEFAULT","NumTrans")) + + @num_trans.setter + def num_trans(self, value): + self.dict_settings.set("DEFAULT","NumTrans", str(value)) + self.save() + + @property + def num_sans(self): + return int(self.dict_settings.get("DEFAULT","NumSans")) + + @num_sans.setter + def num_sans(self, value): + self.dict_settings.set("DEFAULT","NumSans", str(value)) + self.save() + + @property + def loop_per_run(self): + return str(self.dict_settings.get("DEFAULT","LoopPerRun")) + + @loop_per_run.setter + def loop_per_run(self, value): + self.dict_settings.set("DEFAULT","LoopPerRun", str(value)) + self.save() + + @property + def a2_setting_sans(self): + return str(self.dict_settings.get("DEFAULT","A2SettingSans")) + + @a2_setting_sans.setter + def a2_setting_sans(self, value): + self.dict_settings.set("DEFAULT","A2SettingSans", str(value)) + self.save() + + @property + def a2_setting_trans(self): + return str(self.dict_settings.get("DEFAULT","A2SettingTrans")) + + @a2_setting_trans.setter + def a2_setting_trans(self, value): + self.dict_settings.set("DEFAULT","A2SettingTrans", str(value)) + self.save() + + @property + def collection_mode(self): + return str(self.dict_settings.get("DEFAULT","CollectionMode")) + + @collection_mode.setter + def collection_mode(self, value): + self.dict_settings.set("DEFAULT","CollectionMode", str(value)) + self.save() + + @property + def count_rate(self): + return float(self.dict_settings.get("DEFAULT","CountRate")) + + @count_rate.setter + def count_rate(self, value): + self.dict_settings.set("DEFAULT","CountRate", str(value)) + self.save() + + @property + def move_time(self): + return float(self.dict_settings.get("DEFAULT","MoveTime")) + + @move_time.setter + def move_time(self, value): + self.dict_settings.set("DEFAULT","MoveTime", str(value)) + self.save() + + def save(self): + with open(self.setting_ini_path, "w") as configfile: + self.dict_settings.write(configfile) + +# Example usage +if __name__ == "__main__": + settings = Settings() + print(settings.last_instrument) + settings.last_instrument = "LOQ" + print(settings.last_instrument) \ No newline at end of file diff --git a/Python_SANS_Script_GUI/SettingsPanel.py b/Python_SANS_Script_GUI/SettingsPanel.py new file mode 100644 index 0000000..4368376 --- /dev/null +++ b/Python_SANS_Script_GUI/SettingsPanel.py @@ -0,0 +1,229 @@ +import tkinter as tk +from tkinter import ttk +from Settings import Settings +from Models.RunOrder import RunOrder +from Models.Geometry import Geometry +from Models.ExperimentSettings import ExperimentSettings +from Models.InstrumentInfo import InstrumentInfo + +class SettingsPanel(ttk.Frame): + #def __init__(self, parent, experiment=None, inst_info=None): + def __init__(self, parent,inst_info=None, settings=None): + super().__init__(parent) + self.parent = parent + self.settings = settings if settings is not None else Settings() + self.inst_info = inst_info if inst_info is not None else InstrumentInfo() + self.on_preview_clicked = None + self.on_write_clicked = None + self.property_changed = [] + self.order_choices = RunOrder.get_choices() + self.geometry_choices = Geometry.get_choices() + + #self._experiment = experiment if experiment is not None else ExperimentSettings() + #self._inst_info = inst_info if inst_info is not None else InstrumentInfo() + + self.create_widgets() + + def create_widgets(self): + #self.preview_button = ttk.Button(self, text="Preview Script", command=self.on_preview_clicked) + #self.preview_button.pack(side=tk.LEFT, padx=5, pady=5) + #self.write_button = ttk.Button(self, text="Write Script", command=self.on_write_clicked) + #self.write_button.pack(side=tk.LEFT, padx=5, pady=5) + + + label = ttk.Label(self, text="Settings", font=("Helvetica", 16, "bold")) + label.grid(row=0, column=0, columnspan=2, padx=5, pady=5) + + grid_frame = ttk.Frame(self) + grid_frame.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="nsew") + + + # Order + ttk.Label(grid_frame, text="Order:").grid(row=0, column=0, sticky=tk.E,padx=5, pady=5) + + self.com_order = ttk.Combobox(grid_frame, values=self.order_choices) + self.com_order["values"]=self.order_choices + self.com_order.grid(row=0, column=1, columnspan=2, padx=5, pady=5) + self.com_order.set(RunOrder.get_choice_string(self.settings.run_order)) # Set default value + self.com_order.bind("", self.update_order) + + + # Do SANS + ttk.Label(grid_frame, text="Do SANS:").grid(row=1, column=0, sticky=tk.E) + sans_frame = ttk.Frame(grid_frame) + sans_frame.grid(row=1, column=1, columnspan=2,padx=5, pady=5) + self.txt_times_sans = ttk.Entry(sans_frame, width=5) + self.txt_times_sans.insert(0, self.settings.num_sans) # Default value + self.txt_times_sans.grid(row=0, column=0, padx=5, pady=5) + ttk.Label(sans_frame, text="time(s)").grid(row=0, column=1) + self.txt_times_sans.bind("", self.update_num_sans) + + # Do TRANS + ttk.Label(grid_frame, text="Do TRANS:").grid(row=2, column=0, sticky=tk.E,padx=5, pady=5) + trans_frame = ttk.Frame(grid_frame) + trans_frame.grid(row=2, column=1, columnspan=2,padx=5, pady=5) + self.txt_times_trans = ttk.Entry(trans_frame, width=5) + self.txt_times_trans.insert(0, self.settings.num_trans) # Default value + self.txt_times_trans.grid(row=0, column=0, padx=5, pady=5) + ttk.Label(trans_frame, text="time(s)").grid(row=0, column=1) + self.txt_times_trans.bind("", self.update_num_trans) + + # Loop over each run + self.loop_run_var = tk.BooleanVar(value=self.settings.loop_per_run) # Create BooleanVar and set default value + self.chk_loop_run = ttk.Checkbutton(grid_frame, text="Loop over each run?", variable=self.loop_run_var) + self.chk_loop_run.grid(row=3, column=0, columnspan=3, padx=5, pady=5) + self.chk_loop_run.bind("", self.update_loop_per_run) + + # Collection Mode + tk.Label(grid_frame, text=self.inst_info.data_collection_label).grid(row=4, column=0, sticky=tk.E) + self.com_collection_mode = ttk.Combobox(grid_frame, values=self.inst_info.data_collection_choices) + self.com_collection_mode.grid(row=4, column=1, columnspan=2, padx=5, pady=5) + self.com_collection_mode.set(self.settings.collection_mode) # Set default value + self.com_collection_mode.bind("", self.update_collection_mode) + + + # A2 SANS + tk.Label(grid_frame, text="SANS "+self.inst_info.a2_label).grid(row=5, column=0, sticky=tk.E,padx=5, pady=5) + self.com_a2_sans = ttk.Combobox(grid_frame, values=self.inst_info.a2_choices ) + self.com_a2_sans.grid(row=5, column=1, columnspan=2, padx=5, pady=5) + self.com_a2_sans.set(self.settings.a2_setting_sans) # Set default value + self.com_a2_sans.bind("", self.update_a2_sans) + + # A2 TRANS + tk.Label(grid_frame, text="TRANS "+self.inst_info.a2_label).grid(row=6, column=0, sticky=tk.E,padx=5, pady=5) + self.com_a2_trans = ttk.Combobox(grid_frame, values=self.inst_info.a2_choices) + self.com_a2_trans.grid(row=6, column=1, columnspan=2, padx=5, pady=5) + self.com_a2_trans.set(self.settings.a2_setting_trans) # Set default value + self.com_a2_trans.bind("", self.update_a2_trans) + + # Sample Geometry + ttk.Label(grid_frame, text="Sample Geometry:").grid(row=7, column=0, sticky=tk.E,padx=5, pady=5) + self.com_sample_geo = ttk.Combobox(grid_frame, values=self.geometry_choices) + self.com_sample_geo.grid(row=7, column=1, columnspan=2, padx=5, pady=5) + self.com_sample_geo.set(Geometry.get_choice_string(self.settings.sample_geometry)) # Set default value + self.com_sample_geo.bind("", self.update_sample_geometry) + + # Sample Height + ttk.Label(grid_frame, text="Sample Height:").grid(row=8, column=0, sticky=tk.E,padx=5, pady=5) + sample_height_frame = ttk.Frame(grid_frame) + sample_height_frame.grid(row=8, column=1, columnspan=2) + self.txt_sample_height = ttk.Entry(sample_height_frame, width=5) + self.txt_sample_height.insert(0, self.settings.sample_height) # Default value + self.txt_sample_height.grid(row=0, column=0, padx=5, pady=5) + ttk.Label(sample_height_frame, text="mm").grid(row=0, column=1) + self.txt_sample_height.bind("", self.update_sample_height) + + # Sample Width + ttk.Label(grid_frame, text="Sample Width:").grid(row=9, column=0, sticky=tk.E,padx=5, pady=5) + sample_width_frame = ttk.Frame(grid_frame) + sample_width_frame.grid(row=9, column=1, columnspan=2) + self.txt_sample_width = ttk.Entry(sample_width_frame, width=5) + self.txt_sample_width.insert(0, self.settings.sample_width) # Default value + self.txt_sample_width.grid(row=0, column=0, padx=5, pady=5) + ttk.Label(sample_width_frame, text="mm").grid(row=0, column=1) + self.txt_sample_width.bind("", self.update_sample_width) + + # Buttons + button_frame = ttk.Frame(self) + button_frame.grid(row=10, column=0, columnspan=2,padx=5, pady=5) + self.btn_preview_script = ttk.Button(button_frame, text="Preview Script", command=self.btn_preview_script_click) + self.btn_preview_script.grid(row=0, column=0, padx=5, pady=5) + self.btn_write_script = ttk.Button(button_frame, text="Write Script", command=self.btn_write_script_click) + self.btn_write_script.grid(row=0, column=1,padx=5, pady=5) + + # Define the Settings update methods + def update_order(self, event): + self.settings.run_order = str(RunOrder.get_ordinal_from_string(self.com_order.get())) + + def update_num_sans(self, event): + try: + self.settings.num_sans = int(self.txt_times_sans.get()) + except ValueError: + # Handle the error, e.g., show a message box or log the error + print("Invalid input for num_sans. Please enter an integer.") + + def update_num_trans(self, event): + try: + self.settings.num_trans = int(self.txt_times_trans.get()) + except ValueError: + # Handle the error, e.g., show a message box or log the error + print("Invalid input for num_trans. Please enter an integer.") + + def update_loop_per_run(self, event): + self.settings.loop_per_run = self.loop_run_var.get() + + def update_collection_mode(self, event): + self.settings.collection_mode = str(self.com_collection_mode.get()) + + def update_a2_sans(self, event): + self.settings.a2_setting_sans = str(self.com_a2_sans.get()) + + def update_a2_trans(self, event): + self.settings.a2_setting_trans = str(self.com_a2_trans.get()) + + def update_sample_geometry(self, event): + self.settings.sample_geometry = str(Geometry.get_ordinal_from_string(self.com_sample_geo.get())) + + def update_sample_height(self, event): + try: + self.settings.sample_height = float(self.txt_sample_height.get()) + except ValueError: + # Handle the error, e.g., show a message box or log the error + print("Invalid input for sample_height. Please enter a number.") + + def update_sample_width(self, event): + try: + self.settings.sample_width = float(self.txt_sample_width.get()) + except ValueError: + # Handle the error, e.g., show a message box or log the error + print("Invalid input for sample_height. Please enter a number.") + + def btn_preview_script_click(self): + if self.on_preview_clicked: + self.on_preview_clicked() + + + + def btn_write_script_click(self): + if self.on_write_clicked: + self.on_write_clicked() + + def on_property_changed(self, name): + for callback in self.property_changed: + callback(name) + + ''' + @property + def experiment(self): + return self._experiment + + @experiment.setter + def experiment(self, value): + self._experiment = value + self.on_property_changed("experiment") + + @property + def inst_info(self): + return self._inst_info + + @inst_info.setter + def inst_info(self, value): + self._inst_info = value + self.on_property_changed("inst_info") + ''' + @staticmethod + def order_choices(): + return RunOrder.get_choices() + + @staticmethod + def geometry_choices(): + return Geometry.get_choices() + + +if __name__ == "__main__": + root = tk.Tk() + panel = SettingsPanel(root) + panel.grid(sticky="nsew") + root.grid_rowconfigure(0, weight=1) + root.grid_columnconfigure(0, weight=1) + root.mainloop() \ No newline at end of file diff --git a/Python_SANS_Script_GUI/TableDataConstructor.py b/Python_SANS_Script_GUI/TableDataConstructor.py new file mode 100644 index 0000000..4ff5b58 --- /dev/null +++ b/Python_SANS_Script_GUI/TableDataConstructor.py @@ -0,0 +1,99 @@ +import tkinter as tk +from Models.Experiment import Experiment + +class TableDataConstructor: + def __init__(self, table_entries): + self.table_entries = table_entries.split(", ") + + def construct_empty_data(self): + empty_data = { + 1: {entry: "" for entry in self.table_entries} + } + return empty_data + + def construct_data(self, data_list): + data = {} + for i, row in enumerate(data_list.values(), start=1): + data[i] = {entry: value for entry, value in zip(self.table_entries, row)} + return data + + def deconstruct_data(self, data_dict): + data_list = [] + for key in sorted(data_dict.keys()): + row = [data_dict[key][entry] for entry in self.table_entries] + data_list.append(row) + return data_list + + def create_experiments(self, data_list): + experiments = [] + for row in data_list: + experiment = Experiment( + position=row.get("POSITION", ""), + trans=row.get("TRANS", 0), + trans_wait=row.get("TRANS_WAIT", ""), + sans=row.get("SANS", 0), + sans_wait=row.get("SANS_WAIT", ""), + period=row.get("PERIOD", ""), + background=row.get("BACKGROUND", ""), + sample=row.get("SAMPLE_IDENTIFICATION_STRING", ""), + thickness=row.get("THICKNESS", 0), + temperature1=row.get("TEMPERATURE", ""), + temperature2=row.get("TEMPERATURE2", ""), + field=row.get("FIELD", ""), + shear_rate1=row.get("SHEAR_RATE_1", ""), + shear_rate2=row.get("SHEAR_RATE_2", ""), + shear_angle1=row.get("SHEAR_ANGLE_1", ""), + shear_angle2=row.get("SHEAR_ANGLE_2", ""), + pre_command=row.get("PRE_COMMAND", ""), + post_command=row.get("POST_COMMAND", ""), + rb_number=row.get("RB_NUMBER", "") + ) + experiments.append(experiment) + return experiments + +if __name__ == "__main__": + root = tk.Tk() + # Example usage + TableEntries = "POSITION, TRANS, TRANS_WAIT, SANS, SANS_WAIT, PERIOD, SAMPLE_IDENTIFICATION_STRING, BACKGROUND, THICKNESS, TEMPERATURE, TEMPERATURE2, FIELD, SHEAR_RATE_1, SHEAR_RATE_2, SHEAR_ANGLE_1, SHEAR_ANGLE_2, PRE_COMMAND, POST_COMMAND, RB_NUMBER" + constructor = TableDataConstructor(TableEntries) + empty_data = constructor.construct_empty_data() + print(empty_data) + ''' + # Example usage + TableEntries = "col1, col2, label, col3, col4, label2, col5, col6, label3" + constructor = TableDataConstructor(TableEntries) + + # Construct data from a list of lists + data_list = [ + [99.88, 108.79, "rec1", 99.88, 108.79, "rec1", 99.88, 108.79, "rec1"], + [99.88, 108.79, "rec2", 99.88, 108.79, "rec2", 99.88, 108.79, "rec2"], + [99.88, 108.79, "rec3", 99.88, 108.79, "rec3", 99.88, 108.79, "rec3"]] + # Add more rows as needed + + data = constructor.construct_data(data_list) + print("Data:", data) + + # Deconstruct data back to list of lists + deconstructed_data_list = constructor.deconstruct_data(data) + print("Deconstructed Data List:", deconstructed_data_list) + + # Example usage + TableEntries = "POSITION, TRANS, TRANS_WAIT, SANS, SANS_WAIT, PERIOD, SAMPLE_IDENTIFICATION_STRING, BACKGROUND, THICKNESS, TEMPERATURE, TEMPERATURE2, FIELD, SHEAR_RATE_1, SHEAR_RATE_2, SHEAR_ANGLE_1, SHEAR_ANGLE_2, PRE_COMMAND, POST_COMMAND, RB_NUMBER" + constructor = TableDataConstructor(TableEntries) + + # Construct data from a list of lists + data_list = [ + ["Pos1", 10.5, "Wait1", 20.0, "Wait2", "Period1", "Sample1", "Background1", 1.0, "25", "30", "1.5T", "100", "200", "45", "90", "Pre Command1", "Post Command1", "RB12345"], + ["Pos2", 15.0, "Wait3", 25.0, "Wait4", "Period2", "Sample2", "Background2", 2.0, "35", "40", "2.0T", "150", "250", "50", "95", "Pre Command2", "Post Command2", "RB67890"] + ] + + data = constructor.construct_data(data_list) + print("Data:", data) + ''' + # Create experiments from the constructed data + data_list= {0: ['AB', '10', '', '10', '', '', '', '', '1', '', '', '', '', '', '', '', '', '', ''], 1: ['CD', '3', '', '2', '', '', '', '', '2', '', '', '', '', '', '', '', '', '', ''], 2: ['DF', '2', '', '4', '', '', '', '', '5', '', '', '', '', '', '', '', '', '', '']} + data = constructor.construct_data(data_list) + print("Data:", data) + experiments = constructor.create_experiments(data.values()) + for exp in experiments: + print(exp.position, exp.trans, exp.sans, exp.temperature1, exp.rb_number) diff --git a/Python_SANS_Script_GUI/TableTest.py b/Python_SANS_Script_GUI/TableTest.py new file mode 100644 index 0000000..ad7ec38 --- /dev/null +++ b/Python_SANS_Script_GUI/TableTest.py @@ -0,0 +1,111 @@ + +""" +import tkinter as tk + +class SimpleTableInput(tk.Frame): + def __init__(self, parent, rows, columns): + tk.Frame.__init__(self, parent) + + self._entry = {} + self.rows = rows + self.columns = columns + + # register a command to use for validation + vcmd = (self.register(self._validate), "%P") + + # create the table of widgets + for row in range(self.rows): + for column in range(self.columns): + index = (row, column) + e = tk.Entry(self, validate="key", validatecommand=vcmd) + e.grid(row=row, column=column, stick="nsew") + self._entry[index] = e + # adjust column weights so they all expand equally + for column in range(self.columns): + self.grid_columnconfigure(column, weight=1) + # designate a final, empty row to fill up any extra space + self.grid_rowconfigure(rows, weight=1) + + def get(self): + """Return a list of lists, containing the data in the table""" + result = [] + for row in range(self.rows): + current_row = [] + for column in range(self.columns): + index = (row, column) + current_row.append(self._entry[index].get()) + result.append(current_row) + return result + + def _validate(self, P): + """Perform input validation. + + Allow only an empty value, or a value that can be converted to a float + """ + if P.strip() == "": + return True + + try: + f = float(P) + except ValueError: + self.bell() + return False + return True + +class Example(tk.Frame): + def __init__(self, parent): + tk.Frame.__init__(self, parent) + self.table = SimpleTableInput(self, 3, 4) + self.submit = tk.Button(self, text="Submit", command=self.on_submit) + self.table.pack(side="top", fill="both", expand=True) + self.submit.pack(side="bottom") + + def on_submit(self): + print(self.table.get()) + + +root = tk.Tk() +Example(root).pack(side="top", fill="both", expand=True) +root.mainloop() +""" + + +""" +move to next column and row with enter and tab +import tkinter as tk + +class Example(): + def __init__(self): + self.root = tk.Tk() + self.table = tk.Frame(self.root) + self.table.pack(fill="both", expand=True) + + self.rows = [] + for row in range(10): + row_entries = [] + self.rows.append(row_entries) + for column in range(4): + entry = tk.Entry(self.table) + entry.grid(row=row, column=column) + row_entries.append(entry) + + entry.bind("", self.handle_enter) + + def handle_enter(self, event): + # get the row and column of the entry that got the event + entry = event.widget + row = int(entry.grid_info()["row"]) + column = int(entry.grid_info()["column"]) + + # compute the new row; either the next row or circle + # back around to the first row + new_row = row+1 if row+1 < len(self.rows) else 0 + + # get the entry for the new row, and set focus to it + entry = self.rows[new_row][column] + entry.focus_set() + +example = Example() +tk.mainloop() +""" + diff --git a/Python_SANS_Script_GUI/TableWidget.py b/Python_SANS_Script_GUI/TableWidget.py new file mode 100644 index 0000000..5dffda5 --- /dev/null +++ b/Python_SANS_Script_GUI/TableWidget.py @@ -0,0 +1,551 @@ + +from tkintertable import * #TableCanvas, TableModel, ColumnHeader, RowHeader + +from tkinter import * +from openpyxl import load_workbook +import random +from collections import OrderedDict +from TableDataConstructor import TableDataConstructor +from Models.WaitForUnits import WaitForUnits + +TableEntries = "POSITION, TRANS, TRANS_WAIT, SANS, SANS_WAIT, PERIOD, SAMPLE_IDENTIFICATION_STRING, BACKGROUND, THICKNESS, TEMPERATURE, TEMPERATURE2, FIELD, SHEAR_RATE_1, SHEAR_RATE_2, SHEAR_ANGLE_1, SHEAR_ANGLE_2, PRE_COMMAND, POST_COMMAND, RB_NUMBER" +#Get position of entries +entries = TableEntries.split(", ") +TRANS_WAIT_POS = entries.index("TRANS_WAIT") +SANS_WAIT_POS = entries.index("SANS_WAIT") + +BACK_POS = entries.index("BACKGROUND") +SAMPLE_POS = entries.index("SAMPLE_IDENTIFICATION_STRING") + +''' +Check if this allows to Identify the column indices for "back" and "sample" +back_col = self.model.columnNames.index("back") +sample_col = self.model.columnNames.index("sample") +''' + +data=None + + +class CustomRowHeader(RowHeader): + def customCommand(self): + """Define your custom command here""" + print("Custom command executed") + + +class CustomColumnHeader(ColumnHeader): + def popupMenu(self, event): + """Customize the popup menu for the column header""" + + colname = self.model.columnNames[self.table.currentcol] + collabel = self.model.columnlabels[colname] + currcol = self.table.currentcol + popupmenu = Menu(self, tearoff=0) + + def popupFocusOut(event): + popupmenu.unpost() + + # Default actions from the original class + popupmenu.add_command(label="Sort by " + collabel, command=lambda: self.table.sortTable(currcol)) + popupmenu.add_command(label="Sort by " + collabel + " (descending)", command=lambda: self.table.sortTable(currcol, reverse=1)) + #popupmenu.add_command(label="Delete This Column", command=self.table.deleteColumn) + + # Adding a custom command + #popupmenu.add_command(label="Custom Command", command=self.customCommand) + + popupmenu.bind("", popupFocusOut) + popupmenu.focus_set() + popupmenu.post(event.x_root, event.y_root) + return popupmenu + + def customCommand(self): + """Define your custom command here""" + print("Custom command executed") + +class TableImporter: + """Provides import utility methods for the Table and Table Model classes""" + + def __init__(self): + """Setup globals""" + self.separator_list = [",", " ", "\t", ":"] + self.var_sep = StringVar() + self.var_sep.set(",") + return + + @staticmethod + def ImportTableData(table, sep=None): + """Import table data from a comma separated file and populate the table model""" + + parent = table.parentframe + filename = filedialog.askopenfilename(parent=parent, defaultextension=".csv", + filetypes=[("CSV files", "*.csv")]) + if not filename: + return + + if sep is None: + sep = "," + + if not os.path.isfile(filename): + return None + + dictdata = {} + try: + with open(filename, "r") as csvfile: + reader = csv.DictReader(csvfile, delimiter=sep) + for count, rec in enumerate(reader): + dictdata[count] = rec + except Exception as e: + messagebox.showerror("Import Error", f"Failed to import data: {str(e)}") + return None + + model = table.getModel() + #lift focus from cell and row for clean delete + table.clearSelected() + model.deleteRows() + model.importDict(dictdata) + #reset focus of selected row + table.drawCellEntry(1, table.currentcol) + table.setSelectedRow(1) + table.drawSelectedRect(1, table.currentcol) + table.drawSelectedRow() + table.redrawTable() + return dictdata + + +class CustomTableCanvas(TableCanvas): + + def show(self, callback=None): + """Adds column header and scrollbars and combines them with + the current table adding all to the master frame provided in constructor. + Table is then redrawn.""" + + #Add the table and header to the frame + self.tablerowheader = CustomRowHeader(self.parentframe, self, width=self.rowheaderwidth) + self.tablecolheader = CustomColumnHeader(self.parentframe, self) + self.Yscrollbar = AutoScrollbar(self.parentframe,orient=VERTICAL,command=self.set_yviews) + self.Yscrollbar.grid(row=1,column=2,rowspan=1,sticky="news",pady=0,ipady=0) + self.Xscrollbar = AutoScrollbar(self.parentframe,orient=HORIZONTAL,command=self.set_xviews) + self.Xscrollbar.grid(row=2,column=1,columnspan=1,sticky="news") + self["xscrollcommand"] = self.Xscrollbar.set + self["yscrollcommand"] = self.Yscrollbar.set + self.tablecolheader["xscrollcommand"] = self.Xscrollbar.set + self.tablerowheader["yscrollcommand"] = self.Yscrollbar.set + self.parentframe.rowconfigure(1,weight=1) + self.parentframe.columnconfigure(1,weight=1) + + # Configure grid layout + + self.tablecolheader.grid(row=0,column=1,rowspan=1,sticky='news',pady=0,ipady=0) + self.tablerowheader.grid(row=1,column=0,rowspan=1,sticky='news',pady=0,ipady=0) + #self.tablecolheader.grid(row=0, column=1, sticky="ew") + #self.tablerowheader.grid(row=1, column=0, sticky="ns") + self.grid(row=1, column=1, sticky="news",pady=0,ipady=0) + #self.Yscrollbar.grid(row=1, column=2, sticky="ns") + #self.Xscrollbar.grid(row=2, column=1, sticky="ew") + + #self.grid(row=1,column=1,rowspan=1,sticky="news",pady=0,ipady=0) + + self.adjustColumnWidths() + self.redrawTable(callback=callback) + self.parentframe.bind("", self.redrawVisible) + self.tablecolheader.xview("moveto", 0) + self.xview("moveto", 0) + return + + + def deleteCells(self, rows=None, cols=None): + """Clear the cell contents""" + + n = messagebox.askyesno("Clear Confirm", + "Clear this data?", + parent=self.parentframe) + if not n: + return + for col in cols: + for row in rows: + #absrow = self.get_AbsoluteRow(row) + self.model.deleteCellRecord(row, col) + self.redrawCell(row,col) + return + + + def clearData(self, evt=None): + """Delete all cells in the table.""" + n = messagebox.askyesno("Clear Confirm", "Clear all data?", parent=self.parentframe) + if not n: + return + + # Get all rows and columns + rows = range(self.model.getRowCount()) + cols = range(self.model.getColumnCount()) + + # Delete all cells + for col in cols: + for row in rows: + self.model.deleteCellRecord(row, col) + + self.redrawTable() + return + + def new(self, evt=None): + """Clears all the data and makes a new table""" + constructor = TableDataConstructor(TableEntries) + EMPTY_DATA = constructor.construct_empty_data() + #clear focus of selected row + self.clearSelected() + self.model.deleteRows() + + self.model.importDict(EMPTY_DATA) + #reset focus of selected row + self.drawCellEntry(0, self.currentcol) + self.setSelectedRow(0) + self.drawSelectedRect(0, self.currentcol) + self.drawSelectedRow() + self.redrawTable() + + + + def fillDown(self, rowlist, collist): + """Fill down a column, or multiple columns""" + model = self.model + #absrow = self.get_AbsoluteRow(rowlist[0]) + #remove first element as we don"t want to overwrite it + #rowlist.remove(rowlist[0]) + return + + def load_excel_file(self): + global workbook + filepath = filedialog.askopenfilename(filetypes=[("Excel Files", "*.xlsx")]) + if filepath: + self.workbook = load_workbook(filepath) + sheet_names = self.workbook.sheetnames + self.display_sheet_data(sheet_names[0]) + + def display_sheet_data(self,name=None): + sheet_name = name + if sheet_name and self.workbook: + sheet = self.workbook[sheet_name] + #TableEntries = "col1, col2, label, col3, col4, label2, col5, col6, label3" + + self.model.importDict(data) + self.redraw() + + def importTable(self): + #self.load_excel_file() + self.importCSV() + + + def importCSV(self, filename=None, sep=","): + """Import from csv file""" + + importer = TableImporter() + importer.ImportTableData(self) + return + + def do_bindings(self): + """Bind keys and mouse clicks, this can be overriden""" + self.bind("",self.handle_left_click) + self.bind("",self.handle_double_click) + self.bind("", self.handle_left_ctrl_click) + self.bind("", self.handle_left_shift_click) + + self.bind("", self.handle_left_release) + if self.ostyp=="mac": + #For mac we bind Shift, left-click to right click + self.bind("", self.handle_right_click) + self.bind("",self.handle_right_click) + else: + self.bind("", self.handle_right_click) + + self.bind("", self.handle_mouse_drag) + self.bind("", self.handle_motion) + + self.bind_all("", self.deleteRow) + self.bind_all("", self.addRow) + self.bind_all("", self.clearData) + self.bind_all("", self.paste) + + #if not hasattr(self,"parentapp"): + # self.parentapp = self.parentframe + + self.parentframe.master.bind_all("", self.handle_arrow_keys) + self.parentframe.master.bind_all("", self.handle_arrow_keys) + self.parentframe.master.bind_all("", self.handle_arrow_keys) + self.parentframe.master.bind_all("", self.handle_arrow_keys) + self.parentframe.master.bind_all("", self.handle_arrow_keys) + #self.parentframe.master.bind_all("", self.handle_arrow_keys) + self.parentframe.master.bind_all("", self.handle_arrow_keys) + self.parentframe.master.bind_all("", self.move_to_next_row) + + #if "windows" in self.platform: + self.bind("", self.mouse_wheel) + self.bind("", self.mouse_wheel) + self.bind("", self.mouse_wheel) + self.focus_set() + return + + + def move_to_next_row(self, event): + """Move to the next row when enter is pressed and create a new row if it doesn"t exist""" + current_row = self.getSelectedRow() + if current_row >= self.rows - 1: + self.addRow() + self.setSelectedRow(current_row + 1) + self.drawSelectedRect(self.currentrow, self.currentcol) + self.drawSelectedRow() + coltype = self.model.getColumnType(self.currentcol) + if coltype == "text" or coltype == "number": + if self.currentrow < self.rows: + self.drawCellEntry(self.currentrow, self.currentcol) + else: + self.drawCellEntry(self.rows-1, self.currentcol) + self.setSelectedRow(self.rows-1) + self.drawSelectedRect(self.currentrow, self.currentcol) + self.drawSelectedRow() + + def drawCellEntry(self, row, col, text=None): + if self.read_only: + return + h = self.rowheight + model = self.getModel() + cellvalue = model.getCellRecord(row, col) + #create dropdown list for Tr + if col == TRANS_WAIT_POS or col == SANS_WAIT_POS: + values = WaitForUnits.choices + x1, y1, x2, y2 = self.getCellCoords(row, col) + w = x2 - x1 + h = y2 - y1 + + def callback(value): + model.setValueAt(value, row, col) + self.drawText(row, col, value, 'black', align=self.align) + self.delete('entry') + + self.cellentry = Frame(self.parentframe, width=w, height=h) + self.cellentry.place(x=x1, y=y1) + var = StringVar(value=cellvalue) + option_menu = OptionMenu(self.cellentry, var, *values, command=callback) + option_menu.grid(row=0, column=0, sticky="nsew") + self.cellentry.bind('', lambda e: self.cellentry.destroy()) + self.cellentry.focus_set() + elif col == BACK_POS: + # Collect unique, non-empty strings from the "sample" column + values = set() + for r in range(self.model.getRowCount()): + value = self.model.getValueAt(r, SAMPLE_POS) + if value: + values.add(value) + values = list(values) + if not values: + return + + x1, y1, x2, y2 = self.getCellCoords(row, col) + w = x2 - x1 + h = y2 - y1 + + def callback(value): + model.setValueAt(value, row, col) + self.drawText(row, col, value, 'black', align=self.align) + self.delete('entry') + + self.cellentry = Frame(self.parentframe, width=w, height=h) + self.cellentry.place(x=x1, y=y1) + var = StringVar(value=cellvalue) + option_menu = OptionMenu(self.cellentry, var, *values, command=callback) + option_menu.grid(row=0, column=0, sticky="nsew") + self.cellentry.bind('', lambda e: self.cellentry.destroy()) + self.cellentry.focus_set() + else: + # Existing entry handling code + if Formula.isFormula(cellvalue): + return + else: + text = self.model.getValueAt(row, col) + x1,y1,x2,y2 = self.getCellCoords(row,col) + w=x2-x1 + #Draw an entry window + txtvar = StringVar() + txtvar.set(text) + def callback(e): + value = txtvar.get() + if value == '=': + #do a dialog that gets the formula into a text area + #then they can click on the cells they want + #when done the user presses ok and its entered into the cell + self.cellentry.destroy() + #its all done here.. + self.formula_Dialog(row, col) + return + + coltype = self.model.getColumnType(col) + if coltype == 'number': + sta = self.checkDataEntry(e) + if sta == 1: + model.setValueAt(value,row,col) + elif coltype == 'text': + model.setValueAt(value,row,col) + + color = self.model.getColorAt(row,col,'fg') + self.drawText(row, col, value, color, align=self.align) + if e.keysym=='Return': + self.delete('entry') + #self.drawRect(row, col) + #self.gotonextCell(e) + return + + self.cellentry=Entry(self.parentframe,width=20, + textvariable=txtvar, + #bg=self.entrybackgr, + #relief=FLAT, + takefocus=1, + font=self.thefont) + self.cellentry.icursor(END) + self.cellentry.bind('', callback) + self.cellentry.bind('', callback) + self.cellentry.focus_set() + self.entrywin=self.create_window(x1+self.inset,y1+self.inset, + width=w-self.inset*2,height=h-self.inset*2, + window=self.cellentry,anchor='nw', + tag='entry') + + return + + def popupMenu(self, event, rows=None, cols=None, outside=None): + """Add left and right click behaviour for canvas, should not have to override + this function, it will take its values from defined dicts in constructor""" + + # Default actions from the original class + defaultactions = { + #"Set Fill Color": lambda: self.setcellColor(rows, cols, key="bg"), + #"Set Text Color": lambda: self.setcellColor(rows, cols, key="fg"), + "Copy": lambda: self.copyCell(rows, cols), + "Paste": lambda: self.pasteCell(rows, cols), + "Fill Down": lambda: self.fillDown(rows, cols), + "Add Row(s)": lambda: self.addRows(), + "Delete Row(s)": lambda: self.deleteRow(), + "Clear Data": lambda: self.clearData(), + "Select All": self.select_All, + "Create New Table": lambda: self.new(), + "Import Table": self.importTable, + "Export Table": self.exportTable, + "Export Reductionbatch": self.exportTable, + "Preferences": self.showtablePrefs, + } + + # Adding a custom command + #defaultactions["Custom Command"] = self.customCommand + + main = [ "Copy", "Paste", "Fill Down", "Clear Data"] + general = ["Select All", "Add Row(s)", "Delete Row(s)", "Preferences","New", "Import Table", "Export Table", "Export Reductionbatch"] + + + def createSubMenu(parent, label, commands): + menu = Menu(parent, tearoff=0) + popupmenu.add_cascade(label=label, menu=menu) + for action in commands: + menu.add_command(label=action, command=defaultactions[action]) + return menu + + + + def add_commands(fieldtype): + """Add commands to popup menu for column type and specific cell""" + functions = self.columnactions[fieldtype] + for f in functions.keys(): + func = getattr(self, functions[f]) + popupmenu.add_command(label=f, command=lambda: func(row, col)) + return + + popupmenu = Menu(self, tearoff=0) + def popupFocusOut(event): + popupmenu.unpost() + + if outside == None: + # if outside table, just show general items + row = self.get_row_clicked(event) + col = self.get_col_clicked(event) + coltype = self.model.getColumnType(col) + def add_defaultcommands(): + """now add general actions for all cells""" + for action in main: + if action == "Fill Down" and (rows == None or len(rows) <= 1): + continue + else: + popupmenu.add_command(label=action, command=defaultactions[action]) + return + + if coltype in self.columnactions: + add_commands(coltype) + add_defaultcommands() + + for action in general: + if action in defaultactions: + popupmenu.add_command(label=action, command=defaultactions[action]) + else: + print(f"Warning: No default action defined for '{action}'") + + popupmenu.add_separator() + popupmenu.bind("", popupFocusOut) + popupmenu.focus_set() + popupmenu.post(event.x_root, event.y_root) + return popupmenu + +# def customCommand(self): +# """Define your custom command here""" +# print("Custom command executed") + +class TableWidget(Frame): + """Frame for the table""" + #TableEntries = "col1, col2, label, col3, col4, label2, col5, col6, label3" + + constructor = TableDataConstructor(TableEntries) + EMPTY_DATA = constructor.construct_empty_data() + + + def __init__(self, parent=None, data=None): + Frame.__init__(self, parent) + + self.parent = parent + if data is None: + data = TableWidget.EMPTY_DATA + + self.table = CustomTableCanvas(self, data=data ) + #table.importCSV("test.csv") + print (self.table.model.columnNames) + #table.model.data[1]["a"] = "XX" + #table.model.setValueAt("YY",0,2) + self.table.show() + self.grid(row=0, column=0, sticky="nsew") + #self.parent.grid_rowconfigure(0, weight=1) + #self.parent.grid_columnconfigure(0, weight=1) + + # Bind the resize event to redraw the table + + #self.parent.bind("", self.on_resize) + + #def on_resize(self, event): + # self.table.update_table_size() + + def customOption(self): + print("Custom option selected") + return + + def reset_table(self): + """Reset the table to its initial empty state.""" + self.table.new() + + + + +if __name__ == "__main__": + + root = Tk() + root.geometry("800x500+200+100") + root.title("Test") + if data==None: + data = TableWidget.EMPTY_DATA + app = TableWidget(root, data=data) + app.pack(fill=BOTH, expand=1) + root.mainloop() + + + diff --git a/Python_SANS_Script_GUI/TextFile2.py b/Python_SANS_Script_GUI/TextFile2.py new file mode 100644 index 0000000..1265f3a --- /dev/null +++ b/Python_SANS_Script_GUI/TextFile2.py @@ -0,0 +1,38 @@ +import tkinter as tk +from tkinter import ttk, filedialog, messagebox +from tkinter.scrolledtext import ScrolledText +import pandas as pd +from Models.DataCollection import DataCollection +from Models.InstrumentInfo import InstrumentInfo +from Models.Estimation import Estimation +from Models.ExperimentSettings import ExperimentSettings +from IO.ExcelIO import ExcelIO +from IO.CsvExporter import CsvExporter +from Dialogs.PreviewDialog import PreviewDialog +from SettingsPanel import SettingsPanel +from EstimationPanel import EstimationPanel +from TableWidget import TableWidget +from Settings import Settings +from Scripting.ScriptGenerator import ScriptGenerator + +from datetime import datetime, timedelta +from Models.Estimation import Estimation +from Models.Experiment import Experiment +from Models.ExperimentSettings import ExperimentSettings +from Models.A2Setting import A2Setting + +if __name__ == "__main__": + root = tk.Tk() + runs = [Experiment(position="pos1", trans=10, sans=20, trans_wait="Minutes", sans_wait="Minutes", sample="Sample1", thickness=1.0)] + settings = ExperimentSettings() + settings(sample_width=10, sample_height=10, sample_geometry="Flat ", order="AllTrans", num_trans=1, num_sans=1, collection_mode="Events", a2_setting_trans="Large", a2_setting_sans="Large") + + estimation = Estimation(settings=settings) + instrument = "LARMOR" + generator = ScriptGenerator(instrument) + script = generator.generate_script(runs, settings, "my_script") + print(script) + estimated_time = generator.estimate_run_time(runs, settings, estimation) + print(f"Estimated run time: {estimated_time}") + + diff --git a/Python_SANS_Script_GUI/app.spec b/Python_SANS_Script_GUI/app.spec new file mode 100644 index 0000000..3c5a556 --- /dev/null +++ b/Python_SANS_Script_GUI/app.spec @@ -0,0 +1,45 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['app.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + [], + exclude_binaries=True, + name='SANS_ScriptWriter', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + console=False, + icon='SANS_Icon.ico', + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=True, + upx_exclude=[], + name='app', +) diff --git a/README.md b/README.md index 2593c16..2e7b322 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ + # SANS_Script The script generator designed for use on LARMOR, SANS2D and LOQ. + +The folder /SANS_Script_GUI is the original version written in C# +The folder /Python_SANS_script_GUI is a rewrite in Python. It should allow easier maintainability and faster implementation of new features as Python is a more familiar language. +The python version uses the Python packages Tkintertable, openpyxl. + + +## Usage + +1) Clone the repository: +git clone https://github.com/dehoni/SANS_Script_GUI.git + +cd Python_SANS_Script_GUI + + +2) Start the script writer: +python app.py + +3) to build the app: + pyinstaller app.spec + + The executable can be found in the "dist" folder + + add settings.ini to the "_internal" folder + run "SANS script Writer.exe" +