Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Qmini spectrometer GUI #1314

Merged
merged 8 commits into from
Dec 20, 2024
Merged

Conversation

aberges-SLAC
Copy link
Contributor

Description

Gave a face-lift to the Qmini embedded & (new) detailed screen(s) to better suit the needs of SRD in MODS.

Motivation and Context

Closes #1313

Adds a shiny new save-spectrum feature so that scientists aren't tempted to disconnect the spectrometers and plug it into their laptops ;]

How Has This Been Tested?

I've tested this in the pcds-5.9.1 environment and pretty extensively:
https://github.com/user-attachments/assets/fb2ba094-2165-40a2-9e9b-bd6075e3bb4e

Where Has This Been Documented?

In this PR and in the code!

Pre-merge checklist

  • Code works interactively
  • Code contains descriptive docstrings, including context and API
  • New/changed functions and methods are covered in the test suite where possible
  • Test suite passes locally
  • Test suite passes on GitHub Actions
  • Ran docs/pre-release-notes.sh and created a pre-release documentation page
  • Pre-release docs include context, functional descriptions, and contributors as appropriate

@aberges-SLAC aberges-SLAC requested a review from ZLLentz December 19, 2024 23:14
Copy link
Member

@ZLLentz ZLLentz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like what you've done here in the python file and I mostly have nitpicks and questions
I'll peek at the ui files next

@@ -83,6 +87,47 @@ class QminiSpectrometer(Device):
fit_stdev = Cpt(EpicsSignalRO, ':STDEV', kind='config')
fit_chisq = Cpt(EpicsSignalRO, ':CHISQ', kind='config')

# Save spectra functions
def save_data(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick, can ignore:
Is there any utility in making this more useful in non-gui contexts?

def save_data(self, filename: str = ""):
    if not filename:
        filename = self.file_dest.get().strip()

Copy link
Contributor Author

@aberges-SLAC aberges-SLAC Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What did you have in mind?

In a non-gui context you could just do the shell script method of:

# save-spectrum.sh or whatever we wanna call it
file_dest=#2
prefix=#1

caget -t $prefix:WAVELENGTHS $prefix:SPECTRUM >> file_dest.txt

With a call like save-spectrum SOME:BASE:PV:SPECTROMETER save_me_to_some_file.txt

However there is this weird bug with the caget -t call were the first element of the array is actually the array size, so you always get 2018 as the first element in both x & y. 😕

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm imagining me having a qmini object in hutch-python for some reason
Then, I want to save the data
I call

qmini.save_data()

But then since I don't have the GUI open it's not obvious that I'm supposed to put to a string signal.

So maybe it would be nice if I could also do:

qmini.save_data("some/path/of/mine")

"""
if len(self.file_dest.get().strip()) == 0:
# set a default destination for the file if you're lazy or give it whitespace
_file = os.getcwd() + '/' + self.name + time.strftime("_%Y-%m-%d_%H%M%S") + '.txt'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: I noticed that you prepend a lot of function-scoped variables with a leading underscore. I'd previously only seen this as a convention for semi-private class members and function names that are intended not to be exported via import as per https://peps.python.org/pep-0008/.

Are there other reasons to name variables like this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm doing for (paranoid) privacy reasons on the calls. I'm probably being overly cautious, honestly, but it's a habit I've kept. I don't think there's going to be any conflicts (aside from the signal attribute name shenanigans that need it — as I found out the hard way)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you expand a bit on privacy reasons? My question is mostly to learn

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, strictly speaking, _var is functionally a stylistic convention for saying "this var is private-ish, but I'm not gonna enforce it. Remember that you need to be janky to get here" vs. __var getting the built-in string mangling.

Mostly useful for me in contexts where there are pretty commonly recycled var names (i.e. result , data, output, etc.) and I want to be explicit about which one I am messing with. That way I can have a parent function return a result dependent on an inner function call with output _result without getting too weird. Idk maybe I inherited some weird jank convention from C that I haven't unlearned.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, I do the same but only for data attributes e.g. self._var. I can see how it'd also be nice to indicate which variables are meant to persist through the function and which are e.g. loop-only. Thanks for humoring me.

# set a default destination for the file if you're lazy or give it whitespace
_file = os.getcwd() + '/' + self.name + time.strftime("_%Y-%m-%d_%H%M%S") + '.txt'
else:
_file = self.file_dest.get()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few lines above you strip the text contents from this signal. Maybe you should strip here too to help users avoid having leading/trailing spaces in their filenames.

'exposure (us)': str(self.exposure.get()),
'averages': str(self.exposures_to_average.get()),
# Lets do some sneaky conversion to bool from int
'settings': {f"{sig}": str(eval(f"{self}.{sig}.get() > 0")).lower()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: maybe simpler without an eval so the intent is clearer and the next maintainer doesn't get confused.
I probably botched this because I also got confused. I left it as an int because ints are allowed in json.

Suggested change
'settings': {f"{sig}": str(eval(f"{self}.{sig}.get() > 0")).lower()
'settings': {f"{sig}": int(getattr(self, sig).get())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I neglect getattr a little too often for my own good, that's much cleaner. int are allowed but I preferred true and false outputs for the states to match their respective PVs as bo records.

This is maybe just convenience for the consumer of the dump, since it saves them a comparison when checking settings in the object.

I might just be enforcing my own preferences, tbh, so it isn't strictly necessary. Maybe I'll ask the scientists what they prefer 🤷

Copy link
Member

@ZLLentz ZLLentz Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha, I guess since the point is for the end-user to read there could a lot of ways to do it!

You can also put boolean literals into json and it looks like true or false without the quotes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I could've just cast to bool anyway, right? Not sure why I compared to get there in the first place lol. Very good point, I like that much better and will do that

'normalize_exposure', 'adjust_offset', 'subtract_dark',
'remove_bad_pixels', 'remove_temp_bad_pixels']
_data = {'timestamp': time.strftime("%Y-%m-%d %H:%M:%S"),
'exposure (us)': str(self.exposure.get()),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand why these need to be cast to strings.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.dumps was being weird for a second and I think I cast everything to a str when sorting out the problem and left that in there. These can probably live as the ints that they are

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, but the wavelength & spectra must be cast to str since Float32 is not json compat, but that's on the consumer to handle turning back into a number.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UI files look good!

Note that there's one small discrepancy: the embedded has a button with the text "Start Single Exposure" and the detailed has the same button with the text "Force Single Exposure". I don't have any preference but maybe the text should be the same if they do the same thing!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oooooh very good catch! I meant for these to both be "Force" to be consistent with the typhos default detailed screen that the folks are used to.

@ZLLentz
Copy link
Member

ZLLentz commented Dec 20, 2024

I think this is mergeable with consideration or rejection of my comments + a pre-release docs page

@aberges-SLAC aberges-SLAC requested a review from ZLLentz December 20, 2024 02:27
Copy link
Member

@ZLLentz ZLLentz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should bring this in!

@ZLLentz
Copy link
Member

ZLLentz commented Dec 20, 2024

I'm not sure if you're online Adam, so I'm going to merge this myself to include it in the imminent tag!

@ZLLentz ZLLentz merged commit bf311ec into pcdshub:master Dec 20, 2024
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve QminiSpectrometer.Embedded.ui
2 participants