-
Notifications
You must be signed in to change notification settings - Fork 0
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
Generating plots via plots
attribute
#16
Conversation
…ed to initialize gui. FitGUI.layout is now lazy loaded as a consequence.
Btw. I use python black as a formatter -- so not every change has an actual meaning... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be more user-friendly if we removed the wrapper (or at least made it optional). See the comment regarding content.py:get_figures
for a workaround.
example/lsqfit_basic_example.py
Outdated
|
||
#@lsqfitgui.plot | ||
@wrap_plot_function(kind="band", opacity=0.5, name="fit") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The eff_mass
plot doesn't seem to work without this wrapper, but it would probably be better if the wrapper were optional -- by default, the function should be plotted with a band, eg.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I probably should have provided more description here.
The general notion is that plot functions take a fit
object as inputs to create a plot. This is inline with the remaining API for, e.g., the callbacks previous plot functions. I believe, that this is the most flexible way to pass in all the dynamic (as in before and after the fit) information and simplifies the creation of complex plots as mentioned in #15.
This wrapper just simplifies the API such that it extracts the x
and p
values from the fit.
I guess, one way to make it work is provide a different key to the plots
dictionary to say that the input is rather x
and p
than fit
; but to me, this is more a documentation issue.
example/lsqfit_basic_example.py
Outdated
{ | ||
"name": "Effective mass wf", | ||
"fcn": eff_wf, | ||
"x-data": x, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excluding x-data
from prevents y-data from appearing -- should we default to using fit.x
? On the other hand, if someone decides to use meta_config
to vary x
, this default could lead to issues, so maybe it's okay as is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we should probably, in the long run, think about more possibilities here. At best, when populating the plots
-attribute of the GUI, we should also have runtime checks which warn about possible issues.
It certainly needs documentation.
I am more a fan of raising explicit warnings than doing a lot inference on how a plot should be created.
I.e., if a user provides a static y-data
array, so by my understanding of plots, also x-data
must be static (modulo meta configs which may change both). In this sense the API should rather warn: "you have added y-data
without x-data
. Do something!" instead of trying to see if fit.x
may also resolve this issue. As you point out, such scenarios may cause it to work for one case but maybe not another wich makes it hard to fix.
|
||
|
||
def plot_fit(fit, fig: Optional[Figure] = None, fcn: Optional[Callable] = None, y = None): # add type hint | ||
def plot_fit(fit, fig: Optional[Figure] = None): # add type hint |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this method is necessary, and it only gets referenced once (well, technically twice, if you include DEFAULT_PLOTS
). I think it would be better to replace plot_fit(fit)
with plot_gvar(fit.x, fit.fcn(fit.x, fit.p))
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Arguably, that is almost the entire content of this function 😅
As it calls plot, plot_gvar
twice (once for the fit, another time for the data), I just found it more convenient to have it in one function instead of one lambda
. It might be possible to do a
{"name": "Fit", "fcn": wrap_plot_fcn(kind="band")(fit.fcn), "x-data": fit.x, "y-data": fit.y}
However, if we were to change the x and y data of a dynamic fit = generate_fit(**meta_config)
, the wrapped function approach would need some custom hacking while the fit
based approach can already deal with it (see also my above comment #16 (comment)).
fcn = data.get("fcn") | ||
x, y = data.get("x-data"), data.get("y-data") | ||
|
||
fig = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a modification that uses the additional plot functions directly without applying the wrappers; also this removes the need for plot_fit
.
fcn = data.get("fcn")
x, y = data.get("x-data"), data.get("y-data")
if data.get("name") == "Fit":
fcn = fit.fcn
x, y, = fit.x, fit.y
fig = None
if data.get("name") == "Residuals":
fig = plot_residuals(fit)
elif fcn is not None:
x_min, x_max = nanmin(fit.x), nanmax(fit.x)
fig = plot_gvar(
linspace(x_min, x_max),
fcn(linspace(x_min, x_max), fit.p),
kind='band',
name=data.get("name"),
**kwargs
)
# ... code for plotting x, y data (below) unchanged
Additionally, using linspace
here makes the bands finer (like they were before).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Answering this and the next comment in this comment.
I am not a big fan of having internal behavior depend on too many if statements.
The reason why I like the approach with the functions taking fit
s as arguments is that it is the same for all of them. I.e., you specify their name and the function that does the work. I.e., the plot panel gets a "standard".
Someone who wants to figure out how to add new plots looks at the DEFAULT_PLOTS
, sees there needs to be a name
and a function. Can look up the body of the function and customize it to get an individual plot.
If we remove this API and make it an internal logic based on names, it is tough to see how it generally works and may one has to understand more lines of code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand your point, though I'd probably address it the other way -- rather than force the additional functions to take the fit as an input, I'd have plot_fit
and plot_residuals
take x, p, fcn
as an input (maybe an optional fit
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I guess that is also another valid approach pointing towards #10 .
For now, I'd prefer to take fit objects, as the other API takes them as well. But since plots need well defined x
and y
values, it is probably valid and makes less assumptions. Let me take a look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, the residuals method needs all four values for x
, y
, p
and fcn
. I guess it is possible to hack the y
into the kwargs
of the plots
dicts. But since it may be dynamic (i.e., t-range plots), I believe it is best to have these functions still take the maximal amount of information as inputs; i.e., fits.
Once we come up with a different object, we should revisit this.
@@ -88,23 +89,47 @@ def toggle_function_source_collapse(n, is_open): | |||
[State("collapse-function-source", "is_open")], | |||
) | |||
|
|||
DEFAULT_PLOTS = [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can replace these with
DEFAULT_PLOTS = [
{"name": "Fit"},
{"name": "Residuals"},
]
if we use change get_figures
as suggested in my other comment.
lsqfitgui/frontend/content.py
Outdated
if fcn is not None: | ||
fig = fcn(fit, **kwargs) | ||
if x is not None and y is not None: | ||
fig = plot_gvar(x, y, name=data.get("name"), fig=fig) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better to have name="data"
(or possibly disable the label entirely). The tab heading already has the name of the function, so this seems a bit redundant. (I'd recommend similarly for the bands.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. The reason I kept it was that it is used for the labels / legend inside the plots. But that is probably to some degree redundant as well, as the tab already knows the name. Probably better to just use the kwargs as if actually needed for some reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I just checked, I need a unique name for the bands to properly behave under the plotly legend style (as the mean is a separate plot which should also interact with clicks on the legend)
@@ -33,51 +33,40 @@ class FitGUI: | |||
def __init__( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can remove FitGUI.plot_fcns
(line 31), too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I missed this one.
|
||
from lsqfitgui.plot.util import LOG_MENU | ||
|
||
|
There was a problem hiding this 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 probably just eliminate this wrapper, as it makes using lsqfitgui
less user-friendly. It would probably be better to just use the kwargs
in plots
to adjust opacity etc.
Thanks again for your comments and suggestions. I generally believe we should keep the wrapper as I would advocate that the default syntax should be using However I do see that there is some inconsistency about what are plot arguments and what are computation arguments in both the Let me try to push an edit here. |
I have pushed a few changes which slightly modify the keywords passed such that things are a bit more inline. I.e., this wrapper wraps def plot_gvar(
x,
y,
fig: Optional[go.Figure] = None,
kind: str = "band",
add_log_menu: bool = False,
scatter_kwargs: Optional[Dict] = None, # passed to plotly
): In more detail @wrap_plot_gvar(kind="band", scatter_kwargs={"opacity": 0.5, "name": "Fit"})
def eff_wf(x, p):
return np.exp(np.log(fcn(x, p) / fcn(x + 1, p)) * x) * fcn(x, p)
# The above call does effectively the below code
def plot_eff_wf(fit):
ff = fcn(fit.x, fit.p)
return plot_gvar(
fit.x,
np.exp(np.log(ff / fcn(fit.x + 1, fit.p)) * fit.x) * ff,
kind="band",
scatter_kwargs={"opacity": 0.5, "name": "Fit"},
) Effectively are valid functions passed to the plots attribute additional_plots.append({
"name": "Effective WF",
# "fcn": eff_wf, # both would do the same thing
"fcn": plot_eff_wf,
"kwargs": {}, # passed to eff_mass function directly
"static_plot_gvar": { # calls plot_gvar on the same figure
"x": x,
"y": y_eff_wf,
"kind": "errorbars",
"scatter_kwargs": {"name": "Data"},
},
}) See also the basic example for more details. |
I like the Regardless, I think we've added a bunch of good functionality here. We should go ahead and merge this branch with main (assuming it's stable). |
Ahh, sorry, I believe I missed/misunderstood this argument before. Indeed, if not properly explained, this can lead to confusions. The only direction I see here would be not including wrappers at all. Regarding the "coppy" bands. Yes, I've already re-introduced it in the wrapper (but not |
This was a bit more API changing than expected. But I believe it is much cleaner now.
The FitGUI inside
run_server
is now set up as followsI.e., only variables needed on initialization are present on
__init__
and things which can be updated on runtime (i.e., public methods) can be added as attributes.A
run_server
script now has the formIn principle, the
plot["fcn"]
should be of the formand the
-data
kwargs in the additional plot list are added on top of thefcn
plot (corresponding to your transformed data).Inspired by your decorator approach, I have added the script
wrap_plot_function
which allows the following API(The wrapper kwargs now address plotting details).
Let me know if this works for your case, update things, and as soon as this is fine, we can merge both PRs.