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

identification of coordinate systems #115

Closed
jbfaden opened this issue Mar 17, 2021 · 46 comments
Closed

identification of coordinate systems #115

jbfaden opened this issue Mar 17, 2021 · 46 comments
Assignees
Labels
NovHackathon to be resolved during Nov 2021 session priority-medium
Milestone

Comments

@jbfaden
Copy link
Contributor

jbfaden commented Mar 17, 2021

Like with the units, there is no standard way of identifying coordinate systems for vector quantities. For example,

vap+hapi:https://cdaweb.gsfc.nasa.gov/hapi?id=RBSP-A_MAGNETOMETER_4SEC-GEO_EMFISIS-L3&parameters=Mag&timerange=2019-10-14

is known by the human operator to be in GEO at RBSP-A, but there is no mechanism to let the computer know that.

So we propose that a coordinate system identifier be introduced, and schemas to provide a mechanism to interpret the identifier.

@jbfaden
Copy link
Contributor Author

jbfaden commented Mar 17, 2021

Jim Lewis mentioned this problem in his talk at IDHEA in 2020.

@jvandegriff
Copy link
Collaborator

SPEDAS uses a list of allowed coordinate system / frame names.

@jvandegriff
Copy link
Collaborator

possibly use some of NAIF conventions; also CDF has lots of examples;

see also review paper on coordinate frames;

@jvandegriff
Copy link
Collaborator

Related to the semantics of identifying a variable as an actual spatial vector quantity.

If you want to identify something as a vector in space, you need to provide it's representation (x,y,z OR r, theta, phi, etc) and also provide a coordiante from

Also -- don't break existing servers that don't identify vectors as such

@jvandegriff jvandegriff added this to the Version 3.1 milestone May 26, 2021
@jvandegriff
Copy link
Collaborator

jvandegriff commented Jun 2, 2021

The goal here is to describe what the data provider has indicted for coordinate frame names.

SPASE uses a generally agreed upon list of coordinate names that many datasets use. Some frames, like GSM, are hard to re-create exactly numerically, so different implementations will not agree (beyond 5 places).

HAPI is not really able to solve the ambiguities in the coordinate definitions.

Documentation about coordinate frames is outside the scope of HAPI. The best approach for HAPI is to just provide a way to expose the info present in the data, and leave it to users to find out any details about precise definitions used. This is likely to work for a handful of well-known frames, and then there will be a lot of additional mission-specific frames that are dataset specific.

SPASE has a CoordinateSystem object with a CoordinateRepresentation (constrained to be cartesian, cylindrical, sphereical), and then a CoordinateSystemName, also constrained to a lists of specific frame names.
This is effectively a schema, so we can do the same thing here as with units.

Two proposals are

vectorObject (present for vector quantity)
   coordianteFrameName (reqd)
   coordianteRepresentation

At top level:

coordinateSystemSchema = "SPASE-2.3" // Applies to all coordinateFameNames in parameters

At the parameter level:

coordianteFrameName (e.g., GSE, GSM; optional, even for vector quantity; no default)
coordianteRepresentation (Cartesian, Cylindrical, Spherical; optional, even for vector quantity; default is Cartesian)

@jvandegriff
Copy link
Collaborator

Notes from telecon on 2021-08-02:

Need to check with SPEDAS team (Eric and Jim) about their source of coordinate frame names. Jeremy to set up meeting.

Possible sources of existing coordinate system definitions and names:

FITS WCS:
https://fits.gsfc.nasa.gov/fits_wcs.html
http://tdc-www.harvard.edu/software/wcstools/wcstools.wcs.html

(Expert is Bill Thompson - could he give an overview? Bobby to ask for 20 minute session at future HAPI telecon.)

Solar Orbiter has data in FITS and CDF, so they are following ISTP guidelines and FITS stanadards:
https://issues.cosmos.esa.int/solarorbiterwiki/download/attachments/6558892/SOL-SGS-TN-0009-MetadataStandard-2.4.pdf?api=v2

Consistency with coordinate definition schemes within SpacePy, as well as SunPy and AstroPy

(Note: SunPy and AstroPy do support FITS WCS)

For Planetary - no definitive set of coordinate frame names - different for each mission and even for instruments within a mission.

Still broad agreement that specifying a coordinate schema would be useful. Need to understand how complete existing systems are, and how they use names, add names, resolve conflicting views of frames, etc. Note: schema is for the full dataset.

Other aspect of this is to be able to indicate which parameters are vectors, and then which frame they are in, as well as which coordinate representation (cartesian, cylindrical, spherical, other).

SpacePy has CType (cartesian, cylindrical, etc) and CSys (frame name like GSM, GSE, etc)

@ericthewizard
Copy link

Need to check with SPEDAS team (Eric and Jim) about their source of coordinate frame names. Jeremy to set up meeting.

We get the coordinate systems from the CDF metadata - specifically, the 'COORDINATE_SYSTEM' variable attribute. These are set by the individual instrument teams (so sometimes it's simply 'GSE' or 'GSM', and sometimes it's mission/instrument specific, e.g., 'DBCS' for MMS/FPI's 'near-GSE' coordinate system). I think our transformation routines (at least the ones I'm familiar with) assume 3D cartesian coordinates (x, y, z).

@jbfaden
Copy link
Contributor Author

jbfaden commented Aug 5, 2021

Bobby, Eric, Jon and I (Jeremy) met to talk about this issue. Eric is using CDF metadata in SPEDAS that identifies coordinate systems for CDF variables. The case we looked at was https://cdaweb.gsfc.nasa.gov/hapi/info?id=MMS1_FGM_SRVY_L2@1 or in Autoplot vap+cdaweb:ds=MMS1_FGM_SRVY_L2&id=mms1_fgm_r_gsm_srvy_l2 . For this one, the metadata contains COORDINATE_SYSTEM=GSM and REPRESENTATION_1=represent_vec_tot. This data is mms1_fgm_r_gsm_srvy_l2[n,4], with X,Y,Z, and magnitude of the 3-vector. We debated about whether this 4-component notation should be supported in HAPI, or if the server maintainer should be encouraged to split them into two separate parameters (r_vec and r_mag). Otherwise, the CDF has a model and we should probably just support this.

So here we'd have something like:

coordinateSystemSchema='CDAWeb'  (global for the entire dataset)
coordinateFrameName='GSM'
coordinateRepresentation='CartesianPlusMagnitude'

So here "CartesianPlusMagnitude" asserts that the data is [n,4] and the 4 channels are X, Y, Z, and magnitude.

We also looked at ndcube. Jon captured the chat which had a bunch of interesting links, and will be posting it here.

@jvandegriff
Copy link
Collaborator

Here's the Zoom chat finally posted from Aug 5:

These links and info were all from Bobby Candey:

https://docs.sunpy.org/projects/ndcube/en/stable/ndcube.html#coordinate-transformations
Cluster dictionary http://www.sp.ph.ic.ac.uk/csc-web/DOCS/DS-QMW-TN-0010.pdf
https://caa.esac.esa.int/documents/CAA-MDD-0001_v35.pdf

see page 53 of Solar Orbiter metadata:
https://issues.cosmos.esa.int/solarorbiterwiki/download/attachments/6558892/SOL-SGS-TN-0009-MetadataStandard-2.4.pdf?api=v2

CDF metadata example:

	"CATDESC"     CDF_CHAR     { "Definitive Position in GSM " -
	                                 "coordinates, 30 second" }
	    "DEPEND_0"    CDF_CHAR     { "Epoch_state" }
	    "DISPLAY_TYPE"
	                  CDF_CHAR     { "time_series" }
	    "FIELDNAM"    CDF_CHAR     { "mms3_fgm_r_gsm_srvy_l2" }
	    "FILLVAL"     CDF_REAL4    { -1.0e+31 }
	    "FORMAT"      CDF_CHAR     { "E12.2" }
	    "LABL_PTR_1"  CDF_CHAR     { "label_r_gsm" }
	    "UNITS"       CDF_CHAR     { "km" }
	    "VALIDMIN"    CDF_REAL4    { -1.0e+06 }
	    "VALIDMAX"    CDF_REAL4    { 1.0e+06 }
	    "VAR_TYPE"    CDF_CHAR     { "data" }
	    "SCALETYP"    CDF_CHAR     { "linear" }
	    "COORDINATE_SYSTEM"
	                  CDF_CHAR     { "GSM" }
	    "SI_CONVERSION"
	                  CDF_CHAR     { "1.0e3>m" }
	    "TENSOR_ORDER"
	                  CDF_INT4     { 1 }
	    "REPRESENTATION_1"
	                  CDF_CHAR     { "represent_vec_tot" } 

Two more examples:

	"LABLAXIS"    CDF_CHAR     { "label_r_gsm" }
	    "VAR_TYPE"    CDF_CHAR     { "metadata" } .
	
	  ! NRV values follow...
	
	    [1] = { "X GSM     " }
	    [2] = { "Y GSM     " }
	    [3] = { "Z GSM     " }
	    [4] = { "R         " }
	"FIELDNAM"    CDF_CHAR     { "representation for vector plus total" }
	    "FORMAT"      CDF_CHAR     { "A2" }
	    "LABLAXIS"    CDF_CHAR     { "represent_vec_tot" }
	    "VAR_TYPE"    CDF_CHAR     { "metadata" } .
	
	  ! NRV values follow...
	
	    [1] = { "x" }
	    [2] = { "y" }
	    [3] = { "z" }
	    [4] = { "r" }

@jvandegriff jvandegriff added the NovHackathon to be resolved during Nov 2021 session label Nov 1, 2021
@jvandegriff
Copy link
Collaborator

I'm going to create a very simple schema for identifying a coordinate system, and then we can use that for now, but with the idea that a larger effort should really expand on our toy model.

It's not part of the HAPI spec, so I'll put something here:
https://github.com/hapi-server/data-specification/wiki/simple-helio-coord-frame-schema
(this will evolve over time - it's still just being created)

@jvandegriff
Copy link
Collaborator

jvandegriff commented Feb 28, 2022

Based on discussion on 2022-02-21:

This examples adds a coordinateSystemSchema element for the entire dataset, and the a vector object to indicate that a parameter represents a vector in a specific coordinate frame.

{  "HAPI": "3.0",
   "status": { "code": 1200, "message": "OK"},
   "startDate": "1998-001Z",  "stopDate" : "2017-100Z",
   "coordinateSystemSchema" : "hapi_coords_v1",
   "parameters": [
       { "name": "Time", "type": "isotime", units": "UTC", "fill": null, "length": 24 },
       { "name": "mag_GSE",
         "description": "hourly average magnetic field",
         "type": "double", "units": "nT", "fill": "-1e31",
         "size" : [3],
         “units” : [“nT”, “rad”, “rad” ],
         "label": ["Bmag", "theta", "phi"],
         # Could just use single label
         "label": "B field",
         "vector" : {
            "coordinateSystemName": "GSE",
            "vectorRepresentation" : "cartesian"
            "vectorRepresentation" : "spherical"  # only have one vectorRepresentation,
            "vectorRepresentation" : "cylindrical" # but the potential options are shown here.

            "vectorRepresentation" : "polar"  # 2D with r, phi
            "vectorRepresentation" : "geographic"
         }
    ]
}

The vectorRepresentation describes what the elements in the array are in terms of vector components. If not specified, the assumed default is a Cartesian 3-vector with x,y,z.

You can label a vector with a short-cut label if the elements follow a common convention. If none of the common conventions match, then you can specify what vector components you have, and in what order.

Note: need to discuss “size” : [1] case – you could have this for a 1-D vector (i.e., x dimension only)
Some languages might not like this is. The verifier issues a warning if it sees an array parameter with a length of 1.

These are only right-handed coordinate systems.

component names:
“magnitude”
“x”
“y”
”z”
“rho” – cylindrical distance from Z

For angles:
theta is an angle relative to the Z=0 plane, specified either as

  1. theta-latitude: the angle up or down from the plane (-90 to 90)
  2. theta-colatitude: the angle down from the +Z axis (0 to 180)

Phi is the azimuthal angle:

  1. phi-360: angle relative to +X axis in the range 0 to 360 (or equivalent in radians)
  2. phi-180: angle relative to +X axis in the range -180 to 180 (or equivalent in radians)

For data with geographic coordinates, there might have only 2 elements (latitude and longitude).
latitude | colatitude
east-longitude-180 | east-longitude-360 | west-longitude-180 | west-longitude-360

For ground magnetometer data, common vector component labels are:
H,D,Z,F
H = magnitude of horizontal field
D = declination; angle w.r.t. geomagnetic, which varies, or geographic north; positive Eastwards
Z = Z component, with positive downwards
F = magnitude

“coordinateSystemName” : “Legacy Ground Mag Frame”
To map this to standard spherical components:
H is rho
D is phi-180
Z is z
(+X is positive northward)

Sometimes, also the inclination angle I is specified.
http://www.nerc-bas.ac.uk/public/uasd/instrums/magnet/hdz.html
This is the angle between the mag vector and the horizontal plane.

For spherical polar coordinates:
F = magnitude
D = phi-180
I = theta-lattitude

Examples:

"vectorRepresentation" : "cartesian"
"vectorRepresentation" : "cartesian2"   # for just 2 components, like latitude and longitude (maybe this is not needed?)
"vectorRepresentation" : "polar2"  # similar - just a 2D coordinate system

"vectorRepresentation" : { "cartesian" : [ "x", "y", "z"] }

"vectorRepresentation" : { "cartesian" : [ "x", "y"] }

"vectorRepresentation" : { "cartesian" : [ "z", "y", "x"] }

"vectorRepresentation" : { "cartesian" : [ "x", "y", "z", “magnitude”] }

"vectorRepresentation" : { "cartesian" : [ "magnitude", "x", "y", "z", “theta- latitude”] }

"vectorRepresentation" : { "spherical" : { [ "magnitude", "theta-polar", "phi" ] }

"vectorRepresentation" : { "spherical" : [ "magnitude", "theta-elevation", "phi" ] }

"vectorRepresentation" : { "cylindrical" : [ "rho", "azimuth", "z" ] }

@jvandegriff
Copy link
Collaborator

Note that this approach only works if the vector quantities are present in the data as an array, with each element of the array being one of the vector components.

There are plenty of datasets where this is not true, i.e., the vector quantities are split out as separate scalars.

But aggregating these scalars into what would effectively be a new variable starts getting into a kind of semantic overlay on top of HAPI data. That is outside the scope of this kind of change.

Jeremy and i have thought about how to convert what is present in HAPI (or other structured data, like CDF) to a more semantically uniform data structures that have science meaning. This kind of Science Data Interface would be a way of viewing the data as known semantic entities, and those entities would need to be assembled from whatever information and structures are present in the data. This will be entered in another ticket.

@jvandegriff
Copy link
Collaborator

For a list of coordinate frame names, the SPASE schema actually specifies a fairly lengthy list already, so we do not need to define our own schema!

See this page for a list of frame names:
https://spase-group.org/data/model/spase-2.4.0/index.html
(and search for CoordinateSystem)

The SPASE list has the eight coordinates that are in SSCWeb:
https://sscweb.gsfc.nasa.gov/users_guide/Appendix_C.shtml

And the three that are mentioned in COHOWeb:
https://omniweb.gsfc.nasa.gov/coho/helios/heli.html

@jvandegriff
Copy link
Collaborator

One other potential vectorRepresentation is MLT (magnetic local time). Essentially, this treats longitude like a clock angle: 00 to 24.

Also, there should probably be an unknown vector representation in case there is something really strange that just can't be described.

@rweigel
Copy link
Contributor

rweigel commented Jul 11, 2022

To avoid the issue of vector and allow for generalization to higher dimensions, we could allow elements of labels to be objects:

coordSys = "GSE"
labels = [X_GSE, Y_GSE,Z_GSE,"Magnitude"]
vectorInfo = {
         "X_GSE": [GSE, cartesian, x]
}
coordSys = "GSE"
labels = [
    {"X_GSE": {coorSys="GSE", "comp='x'}},
    {"Y_GSE": {comp='y'}},
    {"Z_GSE": {comp='z'}},
    "Magnitude"
]

@jbfaden
Copy link
Contributor Author

jbfaden commented Jul 12, 2022

So would we be adding a new element "labels"? I like this idea. "comp" should be a list of somewhat reserved, so that we don't get "magnitude" and "mag". Also I think tag names should never be abbreviated, so:

"label" : "Spacecraft Position",
"coordinateSystem" : "GSE",
"labels" : [
    { "X_GSE" : { "component":"x", "label":"S/C Position X GSE" } },
    { "Y_GSE" : { "component":"y", "label":"S/C Position Y GSE" } },
    { "Z_GSE" : { "component":"z", "label":"S/C Position Z GSE" } },
    { "Magn" : { "component":"magnitude", "label":"S/C Position Radius"  } }
]

If a label contains a tag "coordinateSystem" then this overrides for just that component, allowing multiple coordinateSystems to be used.

@rweigel
Copy link
Contributor

rweigel commented Jul 12, 2022

What I wrote was incomplete notes from my discussion with Jon. It gets around the problem of having stuff that is not a vector described under a vector key as in Jon's proposal.

So I suggested something like

coordSys = "GSE"
labels = [
    {"X_GSE": {"component='x'}}, // No need for label because X_GSE is the label.
    {"Y_GSE": {component='y'}},
    {"Z_GSE": {component='z'}},
    "Mag" // No component b/c not a component of a vector.
]

This has two problems. (1) It does not indicate that "Magnitude" is the vector magnitude (was a used case we wanted to address) and (2) It is somewhat awkward to put this sort of information inside something called labels. So an alternative that addresses (2) is

labels = [X_GSE, Y_GSE,Z_GSE, Mag]

vectors = {
         "X_GSE": [GSE, cartesian, x]
         "Y_GSE": [GSE, cartesian, y]
         "Z_GSE": [GSE, cartesian, z]
}

To address (1), we could add scalars:

scalars = {
    "Mag": [magnitude(x,y,z)]
}

The reason for adding x,y,z as an argument to magnitude is for the case of ground magnetometer data where H (horizontal magnitude), D (declination), Z (vertical) is sometimes reported

labels = [H, D, Z]

vectors {
   Z: [GEO, cartesian, x] // do we need "cartesian?". Is implied by "x".
   "D": [GEO, spherical, theta] 
}

scalars = {
   "H": [magnitude(x,y)] // awkward b/c y is not given.
}

@jbfaden
Copy link
Contributor Author

jbfaden commented Jul 12, 2022

Why not "components" instead of "vectors" or "labels" ? All we need is something to more precisely describe what each component is, and it seems like with a set of allowed values (x,y,z,magnitude,theta,phi,rho,other) the client could easily figure out how to use the vector.

We should keep this simple. The more complex it is, the more documentation we'll need, and the less likely it is that people will actually use it.

@rweigel
Copy link
Contributor

rweigel commented Jul 12, 2022

Would that work for HDZ?

@jbfaden
Copy link
Contributor Author

jbfaden commented Jul 12, 2022

We could qualify H, D, and Z, like allocate "magnetometer_H", "magnetometer_D", and "magnetomer_Z"

@jbfaden
Copy link
Contributor Author

jbfaden commented Jul 18, 2022

Here's an example of where the components are not identified:

image

@jvandegriff
Copy link
Collaborator

In this case (with the CDAWeb AC_H3_MFI data), it's pretty clear what these three components are. They are inside an array of length 3 that is representing magnetic field, so ideally there would be very little more HAPI metadata needed for a computer to recognize that this is a vector in a specific frame.

To indicate that this is a Cartesian vector in a specific frame, just add this:
"vector" : {
"coordinateSystemName": "RTN"
}

The defaults representation is Cartesian, and the default component ordering is x,y,z.

I still think having a separate vector block is the simplest. The presence of the vector keyword do not mean that the variable is a vector, but that you can make one from it, and the vecgtor block tells how to interpret the contents of the variable. If a variable contains higher dimensionality data like matrices, then those would need to be distinct, i.e., we would need a matrix block. This seems fine, since matrices have their own setting that woudl complicate a vector block.

(This kind of RTN frame is commonly used, and it is present int he SPASE list of standardized names. Note that SPASE is at 2.4.1 now:
https://spase-group.org/data/model/spase-2.4.1/index.html
It is not too useful without spacecraft ephemeris and pointing info, unless everything else you have in the the SC frame too.)

@jbfaden
Copy link
Contributor Author

jbfaden commented Jul 25, 2022

Yes, that would be sufficient to let me know what I can do with it. We still have the case of the n x 4 block, and it would be nice if you could specify the components explicitly (R,T,N,magnitude).

@jvandegriff
Copy link
Collaborator

Here is what we concluded at the end of yesterday’s telecon, which is basically to add an optional keywords.

At the dataset level (this is consistent with earlier discussion):
coordinateSystemSchema - points to a computer readable XML or JSON, etc with a list of coordinate frame definitions

At the parameter level:
coordinateSystemName must be in the schema (if a schema was provided)
vectorElements (an array of values, one for each array element in the parameter, indicating what vector component or vector element is in the array. It could actually also be used for scalar parameters; the enumerated options are:

  • x Cartesian X component
  • y Cartesian X component
  • z Cartesian X component
  • r magnitude of vector
  • rho Cylindrical coordinate representing SQRT (x^2 + y^2)
  • latitude Polar angle -90 to 90, or -Pi to Pi (depends on units)
  • colatitude Polar angle down from +Z axis, 0 to 180, or 0 to Pi (Depends on units)
  • azimuth longitudinal angle, -180 to 180, or -Pi to Pi
  • azimuth360 longitudinal angle, 0 to 360, or 0 to 2 Pi

(The azimuth360 name is not ideal, since it could also be radians. WE need something better for that.)

@jbfaden
Copy link
Contributor Author

jbfaden commented Jul 26, 2022

@jvandegriff
Copy link
Collaborator

I talked with a math person about this, and he favored descriptive names, like latitude and colatitude.

For cases where the branch cut is at a different place, he suggested we use these names:
azimuth for -180 to 180, or -Pi to Pi
azimuthBranch0 branch cut at 0 degree and also 0 radians, so 0 to 360 or 0 to 2Pi
(could just use azimuth0)

We could also use these:
azimuth (-180 to 180)
rightAscension (0 to 360) (astronomers think of it like a clock, so the units would be important)

For angles starting at the minus Z axis:
inverseColatitude
Not sure if we need this:
inverseLatitude

For west longitude:
inverseAzimuth
inverseAzimuth0
These kind of west longitudes are sometimes needed when giving angles from an instrument perspective (looking out of the instrument inverts the direction sense of longitude.)

@rweigel
Copy link
Contributor

rweigel commented Jul 29, 2022

Looks good. I'd change azimuthBranch0 to azimuth0 so we don't need to explain a branch cut, which for me is only ever used in the context of complex analysis. It is also more consistent with inverseAzimuth0 which omits "branch".

@jvandegriff
Copy link
Collaborator

jvandegriff commented Aug 1, 2022

Here is the collected set of what we've come up with so far.

At the dataset level:
coordinateSystemSchema - points to a computer readable XML or JSON, etc with a list of coordinate frame definitions

At the parameter level:
coordinateSystemName must be in the schema (if a schema was provided)
coordinates (an array of values, one for each array element in the parameter, indicating what coordinate component or value is in each element of the array. The only allowed options are from this enumeration of strings:

  • x Cartesian X component of vector
  • y Cartesian Y component of vector
  • z Cartesian Z component of vector
  • r magnitude of vector
  • rho magnitude of radial component in a Cylindrical coordinate representation
  • latitude Polar angle -90 to 90, or -Pi to Pi (positive as you go from Z=0 plane to +Z)
  • colatitude Polar angle down from +Z axis, 0 to 180, or 0 to Pi (positive as you go down from +Z)
  • inverseLatitude Polar angle away from Z=0 plane (positive as you go from Z=0 to -Z, and negative above the Z=0 plane)
  • inverseColatitude angle from -Z axis, 0 to 180, or 0 to Pi (positive as you go away from -Z towards +Z)
  • azimuth longitudinal angle, -180 to 180, or -Pi to Pi (this is East longitude, i.e., positive as you got from +X to +Y)
  • azimuth0 longitudinal angle, 0 to 360, or 0 to 2 Pi (also East longitude, , i.e., positive as you got from +X to +Y)
  • inverseAzimuth West longitudinal angle, -180 to 180, or -Pi to Pi (positive as you go from +X to -Y)
  • inverseAzimuth0 West longitudinal angle, 0 to 360, or 0 to 2 Pi (positive as you go from +X to -Y)
  • other any value that cannot be represented by something in this list

Note that the separate units array is essential for properly interpreting the coordinates (radians versus degrees for the angles, for example).

These can also be used on scalar parameters too.

Note: use unicode Pi and degrees

@jvandegriff
Copy link
Collaborator

jvandegriff commented Aug 1, 2022

For referencing the SPASE list of coordinate systems, we can for now just use the URL to the SPASE schema, since that has been stable as a way to reference the list of known systems there.

https://spase-group.org/data/schema/spase-2.4.1.xsd
(but this is hard to interpret - lots of non-coordinate system items!)
And a more readable version is this particular part of the SPASE as HTML:
https://spase-group.org/data/model/spase-2.4.1/spase-2_4_1_xsd.html#CoordinateSystemName

FYI, here is Bobby's code for getting enumerated items out of SPASE (using jq to extract enumerated values For type = Enumeration) in case we wanted to extract is separately:

jq -r '.dictionary  | .[] | select(.type == "Enumeration")| [ ( [ .term, .definition]| join(": ") ), ( select(.allowedValues |length > 0)| ( " Allowed Values:", ( .allowedValues | join(", ") ) )  ) ] | join(" ")' spase-2.4.0.json

For now, we won't extract - we will just reference the XSD and ask for a DOI (still).

@jvandegriff
Copy link
Collaborator

Bob and I reviewed the coordinate frame and vector component descriptions today, and we realized there is too much invented terminology involved in some of the named items for vector components. Plus we still don't cover all the potential weird cases, and it will be hard to expand our nomenclature consistently if we wanted to include them later.

Standard vector components like x,y,z, r, rho, latitude, longitude, and colatitude are clear, and things like azimuth and azimuth0 are common enough that they can be justified (could be point of discussion), but all the "inverse" quantities (inverseLatitude, inverseAzimuth, inverseColatitude) take quite a bit of mental wrangling to wrap your head around, and these don't cover all the corner cases. The "inverse" terminology is something we invented, so this will be confusing and (more importantly) open to misinterpretation, especially when seen apart from the detailed descriptions.

For example, we currently have inverseLatitude which is the angle relative to the X-Y plane, but positive is the angle for below the plane so that the -Z axis is at +90 degrees.

We also have inverseColatitude, which is the angle away from the -Z axis, such that the -Z axis itself is at 0 degrees of inverseColatitude, and +Z is 180 degrees.

There are multiple other combinatorics that you could envision that we don't have named items for. They are going to be rare, but some data really does come in weird coordinate frames.

We think it's better to have just the simple components as named items, and then allow for a way to indicate angles that are relative to any plane or axis with any sense of direction (i.e., which way is positive for this angle).

angleDefinition = [ <plane defined by cross product of two axes>, <angle_range> ]
angleDefinition = [ <single axis from which angle is to relative to>, <angle_range> ]
angleDefinition = [ <rotation axis and starting point for rotation>, <angle_range> ]

A plane is defined with two vectors, and the order is important because then the cross product tells you the direction of positive angles. So if you want to indicate what we now have as inverseLatitude, this would be:

angleDefinition = [ "+Y cross +X", [-90,90] ]

If you wanted inverseColatitude, that is relative to -Z, so it's just:

angleDefinition = [ "-Z", [0,180] ]

Longitudinal type angles are clock angles around an axis, so you need to specify the rotation axis and the axis that defines the zero point (and also the angle range). Regular longitude would be this:

angleDefinition = [ "+Z,+X", [-180,180] ]

azimuth0 then is a shorthand for

angleDefinition = [ "+Z,+X", [0,360] ]

If you hand an angle that is West Longitude (starts at +X, but is positive towards -Y), you would have this:

angleDefinition = [ "-Z,+X", [0,360] ]

(Notice the -Z axis, so that the rotation goes the other way).

This can then accommodate any possible angle from any of the Cartesian axes with any sense of direction.

These angleDefinition items could be placed in the header so you can refer to them, or they are short enough you can put them in

I suggest that all ranges for these definition be given in degrees, regardless if the quantities themselves are in degrees or radians. I could be convinced, but radians means having users enter values for Pi, which is awkward.

@jvandegriff
Copy link
Collaborator

One other item worth adding: the ability to associate scalar parameters with each other so they could be linked together as a single vector quantity.

If a dataset has
B_GSE_X
B_GSE_Y
B_GSE_Z
as separate scalars, the HAPI info could link them as components belonging to a single, named vector.

{  "name": "B_GSE_X",
         "type": "double",
         "units": "nT",
         "fill": "-1e31",
         "description": "hourly average Cartesian magnetic field in nT in GSE",
         "label": "Bx in GSE",
         "coordinateSystem": "GSE",
         "vectorComponents": "x",
          "vectorName": "B_GSE"
},
{  "name": "B_GSE_Y",
         "type": "double",
         "units": "nT",
         "fill": "-1e31",
         "description": "hourly average Cartesian magnetic field in nT in GSE",
         "label": "By in GSE",
         "coordinateSystem": "GSE",
         "vectorComponents": "y",
          "vectorName": "B_GSE"
},
{  "name": "B_GSE_Z",
         "type": "double",
         "units": "nT",
         "fill": "-1e31",
         "description": "hourly average Cartesian magnetic field in nT in GSE",
         "label": "Bz in GSE",
         "coordinateSystem": "GSE",
         "vectorComponents": "z",
          "vectorName": "B_GSE"
}






@jvandegriff
Copy link
Collaborator

Provide aq table to map our names to commonly used names in other fields (RA, DEC), and (latitude, longitude), etc.

Include HDZ for ground magnetometers.

Then have some full metadata examples using the vector components.

@jvandegriff
Copy link
Collaborator

jvandegriff commented Sep 27, 2022

Here's a way to express custom or non-standard angles. Since the custom angles may appear in more than one parameter, they can be defined less compactly outside the parameter list, and then when listing vectorComponents, you just refer to the name of the custom angle you already defined.

Here is draft text we could put in the spec:

If the data you are presenting has an angle not represented in the table of standard vectorComponents, you can create an custom angle definition to describe it.

There are three types of angles that can appear in data:
azimuth - angle about an axis; like like longitude or right ascension or a clock angle
elevation - angle above or below a plane; like latitude or declination
polar - the angle away from a specified axis, like colatitude

It takes different criteria to define each of these types of angles:
azimuth - requires two axes to define; first is axis rotation axis, with positive direction following the right handed rule (grab axis with right hand so thumb points along axis, then fingers rotate in positive direction); second axis is the zero point reference for the rotation angle (and it must not be parallel to the rotation axis)
elevation - requires two axes to define the plane; order is important since cross product of these two (first cross second) points in the direction of positive angles
polar - requires one axis to define, since the value is just the shortest angle to this axis

Note that the units of the angle are already specified within the units keyword for the parameter.

"customAngles" : [
    {
      "angleName": "userDefinedName",
      "angleType": "azimuth|elevation|polar",
      "relevantAxes": [ one or more axis names used to defined the angle]
      "angleRange" : [minDegrees, maxDegrees]
    },
    {
      ...other custom angle...
    }
]

You could also specify customAngles with a has instead of a list, like this: (we need to decide which one of these to put in the spec!)

"customAngles" : {
  "userDefinedAngleName" : {
      "angleType": "elevation|azimuth|polar",
      "relevantAxes": [ one or more axis names used to defined the angle]
      "angleRange" : [minDegrees, maxDegrees]
  },
  "otherCustomAngle" : { ... }
}

Allowed values in the "relevantAxes" are +x, -x, +y, -y, +z, -z

Examples follow for

  1. left handed longitude (often called west longitude)
  2. angle from the -z axis (a kind of inverse colatitude)
  3. magnetic local time
  4. the "D" angle in HDZ geomagnetic coordinates
  5. the "I" angle from the HDZ frame
    For reference, HDZ is defined below.
"customAngles" : [
  { "angleName": "WestLongitude",
    "angleType": "azimuth",
    "relevantAxes": ["-z", "+x"]
    "angleRange" : [-180, 180]
  },
  { "angleName": "InverseColat",
    "angleType": "polar",
    "relevantAxes": ["-z"]
    "angleRange" : [0, 180]
  },
  { "angleName": "MLT",
    "angleType": "azimuth",
    "relevantAxes": ["+z", "+x"]
    "angleRange" : [0,24]
  },
  { "angleName": "geomag_D_angle",
    "angleType": "azimuth",
    "relevantAxes": ["-z", "+y"]
    "angleRange" : [-180,180]
  },
  { "angleName": "geomag_I_angle",
    "angleType": "elevation",
    "relevantAxes": ["+y", "+x"]
    "angleRange" : [-90,90]
  },

Then to use these in a parameter definition, you just refer to the angle name:

"vectorComponents" : ["r", "latitude", "MLT"]

Description of HDZ from
http://www.nerc-bas.ac.uk/public/uasd/instrums/magnet/hdz.html
[My notes: the text below is copied here for preservation; the figures mentioned in the text are are not available at the site; also, the definitions here are not good; D is actually ambiguous here since it says is is the angle that the horizontal component makes with the eastward direction, but then also says is refers to the angle between geographic and magnetic north, which would seem to be the angle relative to north. In my JSON definition above, I use the the former definition (angle between H and east, where east is the +y axis;
The Cartesian HDZ system has: H is +x and points north; D is +y and points east; Z is +z and points down into the planet). The cylindrical HDZ values are then H is like rho (just in x-y plane); D is the angle from east (it doesn't say which way is positive?) and Z is the same (distance from x-y plane with positive into the earth, which is the +z direction)

From the URL:

Figure 1a shows a local Cartesian system in which the magnetic field vector, F, has components, H, D, and Z. H is defined as positive northwards, D as positive eastwards, and Z as positive downwards. Here "north" can mean one of two things: Either, "north" can mean geographic north. This is defined as the direction of the great circle path to the geographic north pole. Alternatively, "north" can mean geomagnetic north. This is generally not the same as geographic north for reasons which will be apparent later. Geomagnetic north is usually defined as the direction that the local field points in the horizontal plane, either on average or on a "quiet" day when there is little short time scale (< 1 day) variation.

Figure 1b shows a local cylindrical polar coordinate system in which the magnetic field vector, F, has components, H, D, and Z. Here Z is the vertical component defined as positive downwards, as before, H is the component of the magnetic field in the horizontal plane, and D is the angle of the horizontal field component, H, from the eastward direction. D is referred to as the declination. (It is often to be seen on maps, indicating the angle between geographic and geomagnetic north.) Another angle that may be quoted is also shown in the figure. It is the inclination angle, I, between the vector, F, and the horizontal plane. We can see that (F, /2 -I , D) define a spherical polar coordinate system.

@rweigel
Copy link
Contributor

rweigel commented Sep 28, 2022

This looks close. It captures the three types of angles:

  1. Angle relative to an axis (angles always in range [0, 180]) (your "polar")
  2. Angle relative to a plane (angles always in range [-90, 90]) (your "elevation")
  3. Angle in one of three planes (your "azimuth")

WhenangleType=azimuth and relevantAxis=["-z", "+x"], does the last element define the zero angle?

The only issue I see is that relevantAxes is awkward, but I see why. For the three cases above, it

1., defines a zero angle axis and no rotation direction (b/c not needed). Name is releventAxes but what is provided is an axis, so this needs to be addressed;
2. defines a zero plane (from two letters put together) and positive rotation direction (from cross product); and
3. defines a zero angle axis (from cross product) and a positive rotation direction (from cross product)

@jvandegriff
Copy link
Collaborator

Yes, the last element for azimuthal angles is the zero angle (i.e., this: angleType=azimuth and relevantAxis=["-z", "+x"])

The types 1 and 2 above do tend to have fixed angle ranges, (0 to 180 for rel. to an axis) and then -90 to 90 for rel. to a plane. Can they ever be anything else? If so, we can drop that part. The angle in a plane can be either 0 to 360 or -180 to 180.

I agree that relevantAxes is awkward. It lets us reuse the same keyword three times, but it has a different meaning each time. We could instead use three different keywords, so that a specific keyword is required for each angle type:

"customAngles" : [
    {
      "angleName": "myAzimuthalAngle",
      "angleType": "azimuth",
      "rotationAndZeroAxes" : [ "-z", "+x"],
      "angleRange" : [0,180]
    },
    {
      "angleName": "myPolarAngle",
      "angleType": "polar",
       "polarAxis": "+y",         # NOTE: this is just a scalar now (not an array)
      "angleRange" : [minDegrees, maxDegrees]
    },
    {
      "angleName": "myElevationAngle",
      "angleType": "elevation",
      "axesDefiningPlane": ["+x", "+y"]
      "angleRange" : [minDegrees, maxDegrees]
    }
 ]
}

Hmm.. If the defining axes are all different, then do we even need the angleType? Maybe those could be combined so that there's just one indicator of the angle type, and it also specifies the unique info for defining that angle type.

@jvandegriff
Copy link
Collaborator

Maybe have a keyword that is:

"typeAndDefiningAxes": ["polar | azimuth | elevation", axis1, axis2]

and then those axes have different meanings for each type. We could say that axis2 must be null for polar angles, and then verification becomes much easier since its the same JSON structure for all types.

@rweigel
Copy link
Contributor

rweigel commented Sep 28, 2022

I would require polar to always be 0-180 or 0 to pi. Otherwise, one can specify the same thing using an elevation angle. I'd also require elevation always to be -90 to 90 or -pi to pi. (I made a correction in my list for 2. I said "always 0-180".)

Or, we could drop elevation and say the labeling starts at the axis. So latitude is polar = "z" with angles [90,-90] means label z=90, xy plane = 0, -z = 90 and colatitude is polar = "z" with angles [0, 180] so z=0, xy plane = 90, -z = 180.

@jvandegriff
Copy link
Collaborator

I agree with the angle range requirements. I favor keeping the polar designation and having that always be 0 to 180, and the elevation always be -90 to 90.

Note that range designations are always in degrees, regardless of units of the data itself, since they are just specifying the intent of the vector component.

This leads to a quick comment about 0 to 24 for MLT that Iused in an above example. This should not be there since it confuses units with angle range. Angle range should always be in degrees (regardless of the units of the actual data).

This means that the angle range is only relevant for azimuthal items. And also, the angle range is really just a flag (is it 0 to 360, or -180 to 180). So rather than require a numeric range, we should just make that a flag. The flag indicates the location of the branch cut (i.e., where is the discontinuity: at 0 or at 180)?

@rweigel
Copy link
Contributor

rweigel commented Sep 28, 2022

I think (and would like) the MLT to still be captured. If the units are "hours" and it is identified as an angle, then to get an angle in degrees, use a factor of 24. We do a similar thing with pi if the units are "radians". So we would allow angle units to be degrees, radians, and fractional hours.

But then we'd have to answer the follow-up of how to handle the angle specified by integers "hours, minutes" and fractional seconds.

@jvandegriff
Copy link
Collaborator

MLT is still captured. You can represent a floating point MLT in hours without any need for a customAngle.
definitions.

{ "parameters": [
       "name" : "spacecraftLocation",
       "description": "position in R, MLT and magnetic latitude",
       "size": [3],
       "units": ["Re", "hours", "degrees" ],
       "coordinateSystemName": "GSM",
       "vectorComponents": ["r", "longitude", "latitude"]
}

Example values would be:

[6.5, 20.45, 11.01]
[6.8, 23.47, 10.09]
[7.4, 1.99, 8.85]

There is a confusing point here is that the way you specify the angleRange for a customAngle is independent of the units of the actual variable. This is why we should get rid of the angleRange and just have a flag to indicate where the branch cut is (and this is just needed for the longitude-like angles).

@jvandegriff
Copy link
Collaborator

You could also define MLT to be a custom angle if you wanted to. It would really just become a synonym for longitude.

"customAngles": [
   { "angleName": "MLT",
     "angleType": "azimuth",
     "relevantAxes": ["+z", "+x"],
      "azimuthDiscontinuityValue": 0
  }
]

(not sure `azimuthDiscontinuityValue` is the right verbiage yet)

Then the parameter could use `MLT` as an angle name in the `vectorComponents` definition.

{ "parameters": [
       "name" : "spacecraftLocation",
       "description": "position in R, MLT and magnetic latitude",
       "size": [3],
       "units": ["Re", "hours", "degrees" ],
       "coordinateSystemName": "GSM",
       "vectorComponents": ["r", "MLT", "latitude"]
}

@jvandegriff
Copy link
Collaborator

jvandegriff commented Sep 30, 2022

Here then are the allowed vectorComponents:

  • x Cartesian X component of vector
  • y Cartesian Y component of vector
  • z Cartesian Z component of vector
  • r magnitude of vector
  • rho magnitude of radial component in a Cylindrical coordinate representation
  • latitude Polar angle -90 to 90, or -Pi to Pi (positive as you go from Z=0 plane to +Z)
  • colatitude Polar angle down from +Z axis, 0 to 180, or 0 to Pi (positive as you go down from +Z)
  • longitude longitudinal angle, -180 to 180, or -Pi to Pi (this is East longitude, i.e., positive as you got from +X to +Y)
  • longitude0 longitudinal angle, 0 to 360, or 0 to 2 Pi (also East longitude, , i.e., positive as you got from +X to +Y)
  • other any value that cannot be represented by something in this list

If the data you are presenting has an angle not represented in the table of standard vectorComponents, you can create an custom angle definition to describe it.

There are three types of angles that can appear in data:
azimuth or azimuth0 - angle about an axis of the projection of a vector into a plane; like like longitude or right ascension or a clock angle; the discontinuity can be either at 0 degrees for azimuth0 (angles 0 to 360 or 0 to 2 Pi) or at 180 degrees for azimuth (-180 to 180, or -Pi to Pi).
elevation - angle above or below a plane; like latitude or declination; always ranges from -90 to 90 or -Pi/2 to Pi/2
polar - the angle away from a specified axis, like colatitude; always varies from 0 to 180 or 0 to Pi

It takes different criteria to define each of these types of angles:
azimuth - requires two axes to define; first is axis rotation axis, with positive direction following the right handed rule (grab axis with right hand so thumb points along axis, then fingers rotate in positive direction); second axis is the zero point reference for the rotation angle (and it must not be parallel to the rotation axis)
elevation - requires two axes to define the plane; order is important since cross product of these two (first cross second) points in the direction of positive angles
polar - requires one axis to define, since the value is just the shortest angle to this axis

If your azimuthal angle has the discontinuity at zero (as in 0 to 360 or 0 to 2 PI), then use azimuth0 as the angle type.

Note that the units of the angle are already specified within the units keyword for the parameter.

"customAngles" : [
    {
      "angleName": "userDefinedName",
      "angleDefinition": [ "azimuth|azimuth0|elevation|polar", axis1, axis2]
    },
    {
      ...other custom angle...
    }
]

Allowed values in the "relevantAxes" are +x, -x, +y, -y, +z, -z

Examples follow for

  1. left handed longitude (often called west longitude)
  2. angle from the -z axis (a kind of inverse colatitude)
  3. magnetic local time
  4. the "D" angle in HDZ geomagnetic coordinates
  5. the "I" angle from the HDZ frame
    For reference, HDZ is defined below.
"customAngles" : [
  { "angleName": "WestLongitude_0_to_360",
    "angleDefinition": ["azimuth0", "-z", "+x" ]
  },
  { "angleName": "InverseColat",
    "angleDefinition": ["polar", "-z", null ]
  },
  { "angleName": "MLT",
    "angleDefinition": ["azimuth", "+z", "+x" ]
  },
  { "angleName": "geomag_D_angle",
    "angleDefinition": [ "azimuth", "-z", "+y" ]
  },
  { "angleName": "geomag_I_angle",
    "angleDefinition": ["elevation", "+y", "+x" ]
  }
]

Then to use these in a parameter definition, you just refer to the angle name:

"vectorComponents" : ["r", "latitude", "MLT"]

@jvandegriff
Copy link
Collaborator

At today's telecon, we decided to move forward with the standard list of vectorComponents, and delay inclusion of customAngles pending further external review.

It should be easy to add that on later, and getting it wrong now means having to support non-optimal stuff for a potentially long time.

@jvandegriff
Copy link
Collaborator

Here are the parts that I took out of the draft spec, but want to save for next time.

From the info response table for a dataset:

| `customAngles` | list of objects| **Optional**  A way to describe any non-standard angular values that appear as a `vectorComponent`. Custom angles that have no standard keyword can be given a name that can then be used in a list of `vectorComponents` for any `parameter` in the dataset.  [See below for details and examples](#368-specifying-vectorcomponents) of `vectorComponents` and how to define custom angles. |

From the parameters object:

| `vectorComponents` | string or array of strings| **Optional**  The name or list of names of the vector components present in a directional or positional quanitity. For a scalar `parameter`, only a single string indicating the component type is allowed.  For an array `parameter`, an array of corresponding component names is expected.  If not provided, the default value for `vectorComponents` is `["x","y","z"]`, which assumes the `parameter` is an array of length 3. There is an enumeration of allowed names for common vector components and a way to specify other non-standard coordinate angles. [See below for details](#368-specifying-vectorcomponents) on describing `vectorComponents`. |

Description of customAngles:

If a vector component in the data is an angular quantity that is not in the
enumerated list above, it can be described using the following generic
representation. This representation captures both elevation-like angles
and azimuth-like angles.

Once you have defined a custom angle and given it a name, you can use that name as you
would any of the official enumerated vectorComponent items.

There are three types of angles that can appear in data:
azimuth or azimuth0 - angle about an axis of the projection of a vector into a plane; like longitude or right ascension or a clock angle; the discontinuity can be either at 0 degrees for azimuth0 (angles 0 to 360 or 0 to 2 Pi) or at 180 degrees for azimuth (-180 to 180, or -Pi to Pi).
elevation - angle above or below a plane; like latitude or declination; always ranges from -90 to 90 or -Pi/2 to Pi/2
polar - the angle away from a specified axis, like colatitude; always varies from 0 to 180 or 0 to Pi

It takes different criteria to define each of these types of angles:
azimuth or azimuth0 - requires two axes to define; first is axis rotation axis, with positive direction following the right handed rule (grab axis with right hand so thumb points along axis, then fingers rotate in positive direction); second axis is the zero point reference for the rotation angle (and it must not be parallel to the rotation axis)
elevation - requires two axes to define the plane; order is important since cross product of these two (first cross second) points in the direction of positive angles
polar - requires one axis to define, since the value is just the shortest angle to this axis

If your azimuthal angle has the discontinuity at zero (as in 0 to 360 or 0 to 2 PI), then use azimuth0 as the angle type.

Note that the units of the angle are already specified within the units keyword for the parameter.

All custome angle definitions are given in a list of objects with the customAngles keyword.

"customAngles" : [
    {
      "angleName": "userDefinedName",
      "angleDefinition": [ "azimuth|azimuth0|elevation|polar", axis1, axis2]
    },
    {
      ...other custom angle...
    }
]

Allowed values for axis1 and axis2 are: +x, -x, +y, -y, +z, -z. The meaning of these axes in the custom angle definintion depends on which type of custom angle you are defining. Note that a polar angle only requires one axis, so axis2 should be null when defining a polar angle.

Examples follow for

  1. left handed longitude (often called west longitude)
  2. angle from the -z axis (a kind of inverse colatitude)
  3. magnetic local time
  4. the "D" angle in HDZ geomagnetic coordinates
  5. the "I" angle from the HDZ frame
    For reference, HDZ is defined below.
"customAngles" : [
  { "angleName": "WestLongitude_0_to_360",
    "angleDefinition": ["azimuth0", "-z", "+x" ]
  },
  { "angleName": "InverseColat",
    "angleDefinition": ["polar", "-z", null ]
  },
  { "angleName": "MLT",
    "angleDefinition": ["azimuth", "+z", "+x" ]
  },
  { "angleName": "geomag_D_angle",
    "angleDefinition": [ "azimuth", "-z", "+y" ]
  },
  { "angleName": "geomag_I_angle",
    "angleDefinition": ["elevation", "+y", "+x" ]
  }
]

Then to use these in a parameter definition, you just refer to the angle name:

"vectorComponents" : ["r", "WestLongitude_0_to_360", "latitude"]
"vectorComponents" : ["r", "inverseColatitude", "longitude"]
"vectorComponents" : ["r", "latitude", "MLT"]
"vectorComponents" : ["r", "latitude", "geomag_D_angle"]
"vectorComponents" : ["r", "geomag_I_angle", "geomag_D_angle"]

jvandegriff added a commit that referenced this issue Oct 31, 2022
@rweigel
Copy link
Contributor

rweigel commented Dec 8, 2022

While installing astropy, I noticed a package named "asdf". The page https://docs.astropy.org/en/stable/io/asdf-schemas.html#coordinates-representation-1-0-0 has a description of representations of angles. The links to the schema are broken, but I did find this https://asdf-coordinates-schemas.readthedocs.io/en/latest/generated/schemas/representation-1.0.0.html, which does not provide definitions of terms.

@jvandegriff
Copy link
Collaborator

closed by #132

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NovHackathon to be resolved during Nov 2021 session priority-medium
Projects
None yet
Development

No branches or pull requests

5 participants