convenience methods for reading specific dyes on plate readers
front end methods
benefit is providing excitation/emission wavelengths right in PLR, no need to ask chat
what do you think?
convenience methods for reading specific dyes on plate readers
front end methods
benefit is providing excitation/emission wavelengths right in PLR, no need to ask chat
what do you think?
I have some initial thoughts!
We should probably have a Fluorophore class. The PlateReader backends can make a best attempt at creating an optimized optical configuration within the constraints of your particular instrument configuration in the absence of a device specific optimized parameter set.
Because the absorbtion and emission spectra do not fit nicely into once single model, we can do this discretely since it would only take a couple hundred bytes per spectra.
This provided some inspiration here
class Fluorophore:
"""
Canonical fluorophore information.
"""
name: str
ex: Spectrum
em: Spectrum
# do we include absorption spectra or approximate it with excitation?
class Spectrum:
"""
Dense spectral data. Given the limited range, integers should provide
sufficient precision.
"""
start_nm: int
step_nm: int
values: np.ndarray # of sorts
def end(self) -> int:
""" end of known spectral data, assumed to be zero beyond here """
return self.start_nm + (len(self.values) - 1) * self.step_nm
def peak(self) -> int:
""" for lazy optimization implementations :P """
return self.values.max()
oh i like that!
looking at the code i am assuming the value[i] matches start_nm + i * step_nm? why not use a dictionary nm -> 'value'?
how do we make this more explicit in the api? “smart” behavior can oftentimes also be ambiguous or confusing
i thought there is always one optimal setting (emission, excitation wavelength tuple) per flurophore (correct me if i am wrong, im not a biologist, hence the rfc) so we could have a parameter allow_best_guess = False
similar to the existing labware library, it might be useful for users to introduce a stain library referencing vendor part numbers
different stains with the same fluorophore can perform differently in a way that default autoexposure doesn’t completely solve for
this would eliminate ambiguous behavior at risk of becoming too specific
are the stains sufficiently different between vendors? is the difference significant compared to the difference in how dyes are prepared? in my experience some people vibe some dye into a bottle until density matches experience when preparing solutions (since these measurements are relative, how much % is removed when warshing?)
Can really be either but I figure the access patterns will either be peak only or accessing the entire spectrum sequentially.
I think the allow_best_guess or similar you suggested below this is probably the best way to go about it?
For a single fluorophore the solution is usually simple, however the instrument config will usually dictate a minimum distance you want to keep between you ex and em filters. For example, when I do qubit on the CLARIOstar, my filter doesn’t even include the peak of the (one of the… I forget which) spectrum.
With the spectral data you could even generate the configs for multi fluorophore assays! Or take an absorbance spectra prior to adding fluorophores and use that spectrum to optimize the subsequent assay.
The spectra are not always monotonic so the problem of maximizing all SNRs is NP-hard. Since the wavelengths we operate within is pretty narrow, and we can use pretty low precission (1nm), we can probably crunch it in a few ms.
hm, i wonder if this is enough for modeling the spectrum.
in any case if we go with a couple of ‘calibration points’* I think a dictionary would be much better than a list and two values start+step size, so it’s more explicit.
*as opposed to a function
Hi everyone,
Entering the chat a bit late but have been looking forward to getting PLR started with developing a full fluorescence acquisition management system for a long time ![]()
But to start and stay on the more singular topic of this thread:
Objective: What is the purpose of this plate_reader.read_dye() method?
From the PR it seems all it is doing is map a fluorophore name to a set of arguments (excitation_wavelength, emission_wavelength, focal_height]
Method name: The name of the method appears too restrictive imo:
“Dyes” is a very broad term. An oversimplified but I believe useful ontology tree:
**Luminescent Molecule**
│
├── **Fluorophores** *(emit light after optical excitation)*
│ │
│ ├── **Fluorescent dyes** *(typically small-molecule fluorophores)*
│ │ ├── **Free fluorophores / reporter dyes** *(fluoresce without binding)*
│ │ │ ├── Fluorescein
│ │ │ ├── Rhodamine B
│ │ │ └── Coumarin, Nile Red
│ │ │
│ │ └── **Stains (direct binding)** *(dyes with inherent affinity for biomolecules/structures)*
│ │ │ ├── DNA stains (e.g., SYBR Green, DAPI)
│ │ │ ├── Protein stains (e.g., Coomassie derivatives, NHS esters)
│ │ │ └── Organelle stains (e.g., MitoTracker, LysoTracker, Cresyl Violet)
│ │
│ ├── **Fluorescent proteins** *(biomacromolecules with intrinsic chromophores)*
│ │ ├── GFP family (GFP, YFP, CFP, mCherry, mKate, etc.)
│ │ ├── Phycobiliproteins (phycoerythrin, allophycocyanin)
│ │ └── Engineered variants (photoactivatable, pH-sensitive, FRET pairs)
│ │
│ ├── **Fluorescent nanomaterials**
│ │ ├── Quantum dots (semiconductor nanocrystals)
│ │ └── Carbon dots / graphene quantum dots
│ │
│ └── **Autofluorescent biomolecules**
│ ├── Aromatic amino acids (tryptophan, tyrosine)
│ ├── Structural proteins (collagen, elastin)
│ └── Cofactors (flavins, porphyrins, NADH, FAD)
│
└── **Chemiluminescent / Bioluminescent Molecules** *(emit light via chemical or enzymatic reactions)*
├── **Chemiluminescent** (e.g., luminol, acridinium esters)
└── **Bioluminescent** (e.g., luciferin–luciferase systems, coelenterazine)
…I’ve added Chemi- and Bio-luminescence, next to fluorescence for clarity.
What this shows is that the term “dye” is actually talking about a subset of what people can be measuring in fluorescence measurements.
→ I’d therefore recommend to rename plate_reader.read_fluorophore() instead.
(if this method is really the way forward
)
Because these settings can apply to any measurement of a material that fluoresces, including fluorescent small molecules (i.e. dyes), fluorescent proteins and quantum dots - all captured in one relatively widely accepted word already: fluorophore.
I highly discourage this:
For single responsibility reasons the decision of a specific channel_configuration (i.e. which light_source + excitation_filter + dichroic + emission_filter) should be handled by a separate tool, not the frontend.
The frontends responsiblity, in my opinion, is to provide us access to these machines configurations, not choose them for us.
I would recommend the creation of a separate thread for the discussion of such a channel_configuration management/decision making tool.
(I’ve got quite a bit of experience in that area and am excited to get into it in PLR
)
“benefit is providing excitation/emission wavelengths right in PLR”
i believe these parameters are a function of “dye”/fluorophore, so no need to copy these numbers over and over again. also it makes the code more readable.
the alternative to having this in PLR is everyone redefining what wavelengths and such to use when reading fluorophores in their own scripts, everyone would create the same function. we might as well have a very nice and shared one in PLR.
if these parameters are not a function of ‘dye’, but of ‘dye’ + machine we can figure out a way to encode this on the backend. this increases reproducibility of methods since the scientific intent is expressed on the plr-user level (pr.read_fluorophore) rather than a specific machine instruction.
await pr.read_fluorescence(
excitation_wavelength=546,
emission_wavelength=567,
focal_height=7,
)
is less readable (and potentially less reproducible, if machine specific) than
await pr.read_fluorophore("rhodamine_b", focal_height=7)
nice, let’s use this
see point earlier in my comment. the front ends are the user-level interface and should express scientific intent (in addition to machine instructions) so the code is more readable and reproducible
a specialized tool is possible, it might be a function living outside the front end if that is nicer implementation-wise, but i do believe this is fundamentally a front end task
I don’t think these parameters are a function of “dye/fluorophore”.
They are a function dye + machine_configuration: importantly, what we are talking about here in regards to measuring fluorescence with plate readers, also applies to any fluorescence measurement-taking machine, i.e. the same goes for flow cytometers and microscopes.
All three, plate readers, flow cytometers, microscopes have modular light path configurations.
That means the same machine in two different labs is likely to be very different.
(I actually have never seen two machines that are identical… though the CLARIOstar (Plus) come pretty close, but that is because they have quite special light path super-powers
).
This means, yes, even on the same plate reader in two different labs, people might have to use very different settings to measure the same fluorophore.
Regarding specialised tool: If we encode the tool / decision making into the frontend (or even backend), will this not have to be copied again into every plate reader, flow cytometer and microscope we will integrate into PLR?
I think this is important and this is very big!
We should build a full measurement toolkit (and I will
) and I think @hofcake’s suggestion of building a class structure for it (including Fluorophore, Spectrum, Dichroic, …) is the only way this can be solved in the PLR-way, i.e. with abstraction, re-usability, physical correctness and longevity in mind.
A taste of what I am talking about:
this stuff is quite complex - perfect for PLR ![]()
I do really recommend discussing this in a separate “big picture” thread though and keeping this thread focused on the more immediate convenience method discussion ![]()
is the final fluorophore + machine_configuration encoded into a wavelength in the end?
since this is the parameter that is exposed in every PLR-supported plate reader on the firmware level
No - to enable the modularity that plate readers, flow cytometers and microscopes are built on, we have to understand how a specific channel_configuration is a) supported by the machine_configuration and b) is encoded into the firmware.
This means 2 plate readers so far if I’m correct:
But here is my thought-chain of how they might work:
For a modular machine it doesn’t make sense to encode modular components at the firmware level.
Instead, let’s say the Cytation 5 has an emission filter turret with 10 emission filters in it.
Each filter can be swapped out to any other filter by simply opening up the machine and manually taking them out.
From the firmware I would “expect” that it encodes the turret positions, not the specific filter in each turret position.
That way each filter movement is just a mechanical action and agnostic to what filter is in it.
Then, the higher level OEM software (or PLR) can easily keep a record of what filters are in which position, and when a channel_configuration is chosen sends the fimware command for the corresponding turret position.
If I understand you correctly though, that is not the case for the Cytation 5?
Instead of turret positions the firmware keeps the record of the filter in that turret position?
Making it incredibly easy to mess up the firmware-to-physical_reality relationship by simply swapping the filters manually?
Could it be that the EEPROM of the machine keeps a record of the filter to turret positions, and then performs the mapping of firmware commands to positions?
That would still make sense to me: anytime you change your filters you just have to update the EEPROM and accordingly the firmware bytestrings one can send.
Otherwise how would one ever use a new filter set on their plate reader, flow cytometer or microscope (e.g. when their assay changes)?
As I am aware that there are a lot of concepts involved in this which might not be everyone’s cup of tea (physics/optics, fluorophore science, machine configurations, …), I will try to make some simple infographics this week to help us approach this new PLR frontier ![]()
E.g. I don’t know whether everyone is familiar with the difference between a sequential vs parallel light path configuration … But that is the difference between a CLARIOstar and a PHERAstar and has major implications on fluorophore + filter and dichroic choice AND measurement speed(!), and over time I want to help integrate both into PLR.
I don’t know about the internals of spectrophotometry, but for imaging yes this is how it works. You specify “go to filter index i” and the filter->index mapping is only client side, not on the machine.
For spectrophotometry I made gen5.exe send some commands and figured out how the wavelengths and a bunch of other parameters are encoded. I don’t know if it has different filters inside or how it chooses them. The wavelength is an integer with a fixed range (e.g. 250 <= excitation_wavelength <= 700). I haven’t found a filter index here.
In normal operation (not automated) how would people decide what wavelengths to use when reading a specific fluorophore like rhodamine b? The programs typically take wavelength parameters. (What I did is simply look up the excitation and emission peaks and use those.)
If you’re not doing anything bespoke, there will probably be an instrument specific setting.
For example, my CLARIOstar has the EX: 483-15 Dichroic: 502.8 EM: 530-30
which is notably off peak in the excitation spectra, but the dichroic shown in their diagram has slope of ~5 so you can’t put the excitation spectrum on the peak.
If you’re doing something more complex then you probably need to factor in any absorbance spectra that may overlap the excitation spectra of other fluorophores.
edit: this is for PicoGreen, lol
Spectrophotometry usually means “absorbance” measurements (though technically it can be fluorescence too but I’ve never met someone talking about fluorescence as spectrophotometry).
Fluorescence measurements are normally a different light path and measurement mode, and we need to know what fluorescence-specific components are needed for a specific fluorophore.
Please let me know whether this makes sense and is useful:
Note: the dichroic is the foundational technology behind fluorescence measurements - yet nobody ever talks about them ![]()
Note note: in theory all one needs is [light source] → [dichroic] → [detector] but in practice the filters are usually needed to tune the light path (most light sources are not a single wavelength but rather spectra of their own, i.e. “blobby”)
(Please say if you find any mistakes or disagree with any of the terms; people often use the same terms in different ways)
I’d say the standard way is:
The goal is to maximise the overlap between
…while adhering to the dichroic’s reflection/transmission rule (machine)
…plus: capturing a maximal overlap between
This is why the measurement of fluorophore’s is a function of the fluorophore + machine_configuration.
This is the first principle approach
and therefore what is needed for writing an algorithm for a decision making tool or agent ![]()
(…but yeah, I’d say most people do vibe the decision making process)
Are you sure here we are not talking about absorbance measurements?
In absorbance measurement we are only interested in the wavelength that is being sent through the sample (transmission), or more precisely the amount that is absorbed and therefore not transmitted.
thank you camillo for the nice infographics and explanations!
to summarize what you said: to enable “expressing scientific intent” for fluorescence, we need the following
both machine + fluorophore information (detailed as above) are the inputs.
→ you then choose the wavelengths that have the largest overlap as outputs. these are emission and excitation wavelength as a float 2-tuple (?)
[with the inputs I mean stuff you have (machine, fluorophore), the outputs is what is computed. All information about the outputs is present in the inputs (+ the algorithm).]
once we confirm this, we can start to think about encoding this information in to PLR and how to use this data to go from inputs to outputs.
all I said in the post above is I see plate readers (cytation1&5, byonoy, CLARIOstar) take wavelengths as parameters instead of other values and that those wavelengths are constrained in range.
if I understand correctly from above, these two wavelengths are ultimately the value that is sent per reading.