-
Notifications
You must be signed in to change notification settings - Fork 224
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
What's the most Pythonic way for long GMT arguments? #1082
Comments
This has come up again and again in different forms, so I've started a project at https://github.com/GenericMappingTools/pygmt/projects/6 to track the various issues and PRs tackling this. Let's make this issue the central point to gather all the ideas. Here's my attempt at summarize the various candidates (feel free to edit this comment anyone, or suggest alternatives) DictionaryPass the arguments into the parameter as a Python dictionary. E.g. at #262 (comment), #1082 (comment) fig.logo(position={location: "jTR", offset: [0.3, 0.6], width: "3c"}) Pros: Appears easy to implement, sort of intuitive for new Python users FunctionsExpand the PyGMT module's keyword argument list to specify every possible parameter. E.g. at #1173 (comment) fig.logo(position_location="jTR", position_offset=[0.3, 0.6], position_width="3c") Pros: Can tab-complete inside PyGMT module, more discoverable by new users ClassesSet up a dedicated PyGMT Python class to handle the parameter/arguments. E.g. at #356 (comment), #262 (comment). position = pygmt.param.Position(location="jTR", offset=[0.3, 0.6], width="3c")
fig.logo(position=position) Pros: Can be tab-completed, only need to implement once and be used throughout all PyGMT modules, can plug into existing workflow Yes, my preference is for the 'Classes' option, so I'll elaborate on it a bit more. The main idea is to have a
Implementation (of pygmt.param.ClassName)It's been mentioned at #249 (comment) and attempted for e.g. at #379, but I'll summarize the idea again. A class like pen = pygmt.param.Pen(width="1p", color="blue", style="-")
print(pen)
# 1p,blue,- Since PyGMT has adopted NEP29 #1074 and is Python 3.7+ now, we can use Python dataclasses to implement this (see https://realpython.com/python-data-classes), and this means less boilerplate when coding things up (compared to traditional Python classes). TLDR: Let's start small-ish, e.g. @willschlitzer's issue on pen (-W) at #1173 will be a great start, and we can work out the implementation details along the way. |
The "Classes" option sounds the best way, but we need to think about a general way to implement classes for all options. |
I agree that the classes option is good. I have a couple further opinions about the implementation of long GMT arguments:
|
Yep, we should definitely have standardized aliases across all GMT wrappers. There was a lot of discussion around this in 2019/2020 on GitHub and the GMT forum (e.g. https://forum.generic-mapping-tools.org/t/standardized-human-readable-gmt-parameters-for-pygmt-matlab-julia-pyshtools-etc/77) but the conversation stalled for some reason, so thanks for picking this up again and starting the project to keep track!
This can be arranged. We can have both a Edit: Actually, Python 3.7 data classes already implements a pen = pygmt.param.Pen(style=".-.-", color="120-1-1", width="0.5c")
str(pen) #'0.5c,120-1-1,.-.-'
print(pen) # 0.5c,120-1-1,.-.-
repr(pen) # "Pen(width='0.5c', color='120-1-1', style='.-.-')"
pen # Pen(width='0.5c', color='120-1-1', style='.-.-') For the last one, simply putting |
Cool, this is on the agenda to discuss at the next GMT community meeting. Of course, we could also discuss at the next PyGMT meeting in case anyone is unavailable May 6.
Yes, but I could be convinced elsewise if others feel strongly that |
Sorry, jumping in here a bit late here. A quick impression is that something like the dictionary approach would be the easiest way to implement but not necessarily the best way to use the library. Mainly because the dictionary arguments won't be tab-completed. Same thing goes for the classes. It would be nice as a developer but I really wouldn't want to have to create separate instances just to pass them as arguments to a function. This is something I feel is very easy to forget when making software: good APIs optimize usability. So if we forget about the implementation details: How would you want to use PyGMT? Not as a GMT guru but as a Python user who wants to make some nice maps. As a user, I feel like the easiest thing to use would be just flat function arguments. Then I can type So if we take something like this: import pygmt
# Load sample grid and point datasets
grid = pygmt.datasets.load_earth_relief()
points = pygmt.datasets.load_ocean_ridge_points()
# Sample the bathymetry along the world's ocean ridges at specified track points
track = pygmt.grdtrack(points=points, grid=grid, newcolname="bathymetry")
fig = pygmt.Figure()
# Plot the earth relief grid on Cylindrical Stereographic projection, masking land areas
fig.basemap(region="g", projection="Cyl_stere/150/-20/15c", frame=True)
pygmt.makecpt(cmap="gray", series=[0, 800])
fig.grdimage(grid=grid, cmap=True)
fig.coast(land="#666666")
# Plot the sampled bathymetry points using circles (c) of 0.15 cm
# Points are colored using elevation values (normalized for visual purposes)
fig.plot(
x=track.longitude,
y=track.latitude,
style="c0.15c",
cmap="terra",
color=(track.bathymetry - track.bathymetry.mean()) / track.bathymetry.std(),
)
fig.show() As a user, I would want to write something like this instead: import pygmt
# These lines are fine
grid = pygmt.datasets.load_earth_relief()
points = pygmt.datasets.load_ocean_ridge_points()
track = pygmt.grdtrack(points=points, grid=grid, newcolname="bathymetry")
fig = pygmt.Figure()
fig.basemap(
region="g",
projection=pygmt.CylindricalStereographic(central_longitude=150, central_latitude=-20), # long names are tab-completed
width="15c", # Makes no sense to have this as part of the project. Could also have a default value
frame=True, # Should probably be True by default
)
# makecpt is called internally. It's not a name anyone new to GMT would think to use.
fig.grdimage(
grid=grid,
cmap=pygmt.cmap.gray(vmin=0, vmax=800), # Can tab complete the cmap name and keyword arguments
# alternatively
# cmap=pygmt.cmap("gray", vmin=0, vmax=800), # not as good since can't tab-complete the name but still nice
)
fig.coast(land="#666666")
fig.plot(
x=track.longitude,
y=track.latitude,
style="c", # It's really confusing to have c0.15c (what are all the cs?)
size="0.15c",
cmap="terra", # This is also OK if you don't need to edit CPT properties
color=(track.bathymetry - track.bathymetry.mean()) / track.bathymetry.std(),
)
fig.show() And going back to the logo example: fig.logo(
location="TR",
offset=[0.3, 0.6],
width="3c",
justify="default", # or "mirror" for J
)
# if you want to plot the timestamp
fig.timestamp(location=..., offset=..., ...) To me, every time multiple options have the same modifier that is an indication that this function is trying to do too many things. The mapping of "GMT module = PyGMT function/method" is not great for usability. We can always internally combine the options into something GMT wants and call the appropriate module. Decoupling the GMT I/O from the API is something that would make this a lot easier on our side but that is a huge amount of work. You all have been putting in the time here so of course you get to decide and I'm perfectly happy with whatever the group settles on. You have been an awesome group and I wish I had more time to be interact more with you 🙂 |
The |
Been thinking about this some more. The classes option is not a bad idea but there are some things to watch out for:
As a coder, I would definitely prefer implementing the classes since they would make our job easier. But as a user, I would probably end up passing the crazy GMT strings instead since creating all these different instances would be a bit tedious. |
Now it's possible to tab-complete dict keys, see #2793. It works in VSCode and other IDEs, but I can't make it work in Jupyter notebooks.
The parameter names are also too long and it means a lot of work for maintainers, as we have to convert
I feel we can have both classes and dictionary. We can use classes for common options (e.g., |
For each GMT option, PyGMT usually provides a long-form parameter. For example, GMT CLI option -B is equivalent to frame in PyGMT. This is done by the alias system (the
use_alias
decorator), which automatically converts long-form PyGMT parameters to short-form GMT options. The alias system works well but has known limitations and issues (e.g., #256, #262).Sometimes, GMT arguments are still specified using long unreadable strings. For example,
projection="S0/90/12c"
: related to Create Projection classes instead of strings #356, Implement Projection classes to encapsulate arguments #379frame=["WSen", "xaf+lXLABEL", "yaf+lYLABEL"]
: related to Better way to configure axis (instead of B/frame) #249As these common options are used a lot, we may take some time to find and implement a more Pythonic way to specify
projection
,frame
et al.However, there are still a lot of other long GMT arguments. Here is a simple example showing how to plot a GMT logo on maps.
The above Python script is equivalent to the following GMT CLI script:
The long argument for position and box are really difficult to remember and write. There are definitely some better and more Pythonic ways to specify these arguments. As GMT has too many options, we must provide a universal way to specify these long arguments.
Perhaps the simplest way is to pass these long arguments as dict, i.e.,
Are there any better ways?
The text was updated successfully, but these errors were encountered: