a2p2 0.2.15__py3-none-any.whl → 0.7.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- a2p2/__main__.py +39 -0
- a2p2/chara/facility.py +51 -3
- a2p2/chara/gui.py +29 -5
- a2p2/client.py +144 -34
- a2p2/facility.py +11 -1
- a2p2/gui.py +32 -1
- a2p2/jmmc/__init__.py +7 -0
- a2p2/jmmc/catalogs.py +129 -0
- a2p2/jmmc/generated_models.py +191 -0
- a2p2/jmmc/models.py +104 -0
- a2p2/jmmc/services.py +16 -0
- a2p2/jmmc/utils.py +130 -0
- a2p2/jmmc/webservices.py +48 -0
- a2p2/ob.py +95 -9
- a2p2/samp.py +17 -0
- a2p2/version.py +205 -137
- a2p2/vlti/conf/GRAVITY_ditTable.json +21 -19
- a2p2/vlti/conf/GRAVITY_rangeTable.json +200 -28
- a2p2/vlti/conf/MATISSE_rangeTable.json +58 -22
- a2p2/vlti/conf/PIONIER_ditTable.json +1 -1
- a2p2/vlti/conf/PIONIER_rangeTable.json +16 -18
- a2p2/vlti/facility.py +156 -118
- a2p2/vlti/gravity.py +243 -311
- a2p2/vlti/gui.py +162 -40
- a2p2/vlti/instrument.py +264 -49
- a2p2/vlti/matisse.py +61 -147
- a2p2/vlti/pionier.py +34 -157
- {a2p2-0.2.15.dist-info → a2p2-0.7.4.dist-info}/METADATA +34 -20
- a2p2-0.7.4.dist-info/RECORD +39 -0
- {a2p2-0.2.15.dist-info → a2p2-0.7.4.dist-info}/WHEEL +1 -1
- {a2p2-0.2.15.dist-info → a2p2-0.7.4.dist-info}/entry_points.txt +0 -1
- a2p2/vlti/confP104/GRAVITY_ditTable.json +0 -122
- a2p2/vlti/confP104/GRAVITY_rangeTable.json +0 -202
- a2p2/vlti/confP104/MATISSE_ditTable.json +0 -2
- a2p2/vlti/confP104/MATISSE_rangeTable.json +0 -202
- a2p2/vlti/confP104/PIONIER_ditTable.json +0 -77
- a2p2/vlti/confP104/PIONIER_rangeTable.json +0 -118
- a2p2/vlti/confP105/GRAVITY_ditTable.json +0 -37
- a2p2/vlti/confP105/GRAVITY_rangeTable.json +0 -42
- a2p2/vlti/confP105/MATISSE_ditTable.json +0 -2
- a2p2/vlti/confP105/MATISSE_rangeTable.json +0 -44
- a2p2/vlti/confP105/PIONIER_ditTable.json +0 -25
- a2p2/vlti/confP105/PIONIER_rangeTable.json +0 -38
- a2p2-0.2.15.dist-info/RECORD +0 -44
- {a2p2-0.2.15.dist-info → a2p2-0.7.4.dist-info}/LICENSE +0 -0
- {a2p2-0.2.15.dist-info → a2p2-0.7.4.dist-info}/top_level.txt +0 -0
a2p2/jmmc/catalogs.py
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
__all__ = []
|
4
|
+
|
5
|
+
import logging
|
6
|
+
|
7
|
+
from ..client import A2P2ClientPreferences
|
8
|
+
|
9
|
+
from .utils import JmmcAPI
|
10
|
+
from . import PRODLABEL
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class Catalog():
|
16
|
+
""" Get remote access to read and update catalogs exposed through JMMC's API.
|
17
|
+
Credential can be explicitly given for method that require an authentication, else:
|
18
|
+
- a2p2 preferences login will be used if present (see a2p2 -c)
|
19
|
+
- or uses .netrc file
|
20
|
+
"""
|
21
|
+
|
22
|
+
def __init__(self, catalogName, username=None, password=None, prod=False, apiUrl=None):
|
23
|
+
self.catalogName = catalogName
|
24
|
+
self.prod = prod
|
25
|
+
|
26
|
+
# Manage prod & preprod or user provided access points
|
27
|
+
if apiUrl:
|
28
|
+
self.apiUrl = apiUrl # trust given url as catalogAPI if value is provided
|
29
|
+
elif self.prod:
|
30
|
+
self.apiUrl = "https://oidb.jmmc.fr/restxq/catalogs"
|
31
|
+
else:
|
32
|
+
self.apiUrl = "https://oidb-beta.jmmc.fr/restxq/catalogs"
|
33
|
+
|
34
|
+
self.api = JmmcAPI(self.apiUrl, username, password)
|
35
|
+
|
36
|
+
logger.info(f"Create catalog wrapper to access '{catalogName}' ({PRODLABEL[self.prod]} API at {self.api.rootURL})")
|
37
|
+
|
38
|
+
def list(self):
|
39
|
+
""" Get list of exposed catalogs on API associated to this catalog. """
|
40
|
+
return self.api._get("")
|
41
|
+
|
42
|
+
def metadata(self):
|
43
|
+
""" Get catalog metadata """
|
44
|
+
return self.api._get(f"/meta/{self.catalogName}")
|
45
|
+
|
46
|
+
def pis(self):
|
47
|
+
""" Get PIs from catalog and check for associated JMMC login in OiDB datapi table."""
|
48
|
+
return self.api._get(f"/accounts/{self.catalogName}")["pi"]
|
49
|
+
|
50
|
+
def piname(self, jmmcLogin=None):
|
51
|
+
""" Get the piname associated to the given jmmcLogin.
|
52
|
+
If jmmcLogin parameter is not provided, try to get jmmc.login preferences.
|
53
|
+
Only pi names returned by pis() can be retrieved.
|
54
|
+
|
55
|
+
usage: piname()
|
56
|
+
"""
|
57
|
+
if not jmmcLogin:
|
58
|
+
prefs = A2P2ClientPreferences()
|
59
|
+
jmmcLogin = prefs.getJmmcLogin()
|
60
|
+
if not jmmcLogin:
|
61
|
+
raise Exception(
|
62
|
+
"missing login parameter or jmmc.login preference.")
|
63
|
+
pis = self.pis()
|
64
|
+
for pi in pis:
|
65
|
+
if "login" in pi and pi["login"] == jmmcLogin:
|
66
|
+
return pi["name"]
|
67
|
+
|
68
|
+
def getRow(self, id):
|
69
|
+
""" Get a single catalog record for the given id.
|
70
|
+
|
71
|
+
usage: cat.getRow(42)
|
72
|
+
"""
|
73
|
+
return self.api._get(f"/{self.catalogName}/{id}")
|
74
|
+
|
75
|
+
def updateRow(self, id, values):
|
76
|
+
""" Update record identified by given id and associated values.
|
77
|
+
|
78
|
+
usage: cat.updateRows(42, {"col_a":"a", "col_b":"b" })
|
79
|
+
"""
|
80
|
+
return self.api._put(f"/{self.catalogName}/{id}", values)
|
81
|
+
|
82
|
+
def updateRows(self, values):
|
83
|
+
""" Update multiple rows.
|
84
|
+
Values must contain a list of dictionnary and each entry must contains id key among other columns.
|
85
|
+
|
86
|
+
usage: updateRows([ { "id":42, "col_a":"a" }, { "id":24, "col_b":"b" } ])
|
87
|
+
"""
|
88
|
+
|
89
|
+
# We may check befere sending payload that we always provide an id for every record
|
90
|
+
return self.api._put(f"/{self.catalogName}", values)
|
91
|
+
|
92
|
+
def addRows(self, values):
|
93
|
+
""" Add multiple rows.
|
94
|
+
Values is an array of row to add. id column values will be ignored.
|
95
|
+
|
96
|
+
usage: addCatalogRows([ { "id":42, "col_a":"a" }, { "id":24, "col_b":"b" } ])
|
97
|
+
"""
|
98
|
+
return self.api._post(f"/{self.catalogName}", json=values)
|
99
|
+
|
100
|
+
def getDelegations(self, pi=None):
|
101
|
+
""" Get -all- delegations.
|
102
|
+
A specific pi parameter may be given but requires admin priviledges. Else use current piname will be used by remote service.
|
103
|
+
|
104
|
+
usage: getDelegations()
|
105
|
+
"""
|
106
|
+
res = self.api._get(f"/delegations?pi={pi}")
|
107
|
+
if "delegations" in res:
|
108
|
+
return res["delegations"] # login and pi values not returned
|
109
|
+
|
110
|
+
return []
|
111
|
+
|
112
|
+
def updateDelegations(self, delegations):
|
113
|
+
""" Update delegations according given array of delegations.
|
114
|
+
A specific "pi" key may be given in delegations dict but requires admin privileges. Else current piname will be used by remote service.
|
115
|
+
"action" may be provided with "add" or "remove" value to set proper delegations (default add given delegation).
|
116
|
+
|
117
|
+
usage: addDelegations([{"pi":"mypiname", "catalogs":"mycats", "coi":["alice","bob"]}])
|
118
|
+
|
119
|
+
"""
|
120
|
+
return self.api._put("/delegations", json=delegations)
|
121
|
+
|
122
|
+
def removeDelegations(self, delegations):
|
123
|
+
""" Remove delegations according given array of delegations.
|
124
|
+
A specific pi key may be given but requires admin privileges. Else current piname will be used by remote service.
|
125
|
+
usage: removeDelegations([{}, {}])
|
126
|
+
"""
|
127
|
+
return "TODO"
|
128
|
+
|
129
|
+
|
@@ -0,0 +1,191 @@
|
|
1
|
+
from .models import _model
|
2
|
+
|
3
|
+
def punct( name, flux_weight=1.0, x=0.0, y=0.0, output_mode=None):
|
4
|
+
""" Returns the Fourier transform of a punctual object (Dirac function) at coordinates (X,Y)
|
5
|
+
(milliarcsecond).
|
6
|
+
|
7
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1. """
|
8
|
+
|
9
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "name":name, "type":"punct"}, output_mode)
|
10
|
+
|
11
|
+
def disk( name, flux_weight=1.0, x=0.0, y=0.0, diameter=0.0, output_mode=None):
|
12
|
+
""" Returns the Fourier transform of a normalized uniform disk of diameter DIAMETER
|
13
|
+
(milliarcsecond) and centered at coordinates (X,Y) (milliarcsecond).
|
14
|
+
|
15
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
|
16
|
+
|
17
|
+
The function returns an error if DIAMETER is negative. """
|
18
|
+
|
19
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "diameter" : diameter, "name":name, "type":"disk"}, output_mode)
|
20
|
+
|
21
|
+
def elong_disk( name, flux_weight=1.0, x=0.0, y=0.0, minor_axis_diameter=0.0, elong_ratio=1.0, major_axis_pos_angle=0.0, output_mode=None):
|
22
|
+
""" Returns the Fourier transform of a normalized ellipse centered at coordinates (X,Y)
|
23
|
+
(milliarcsecond) with a ratio ELONG_RATIO between the major diameter and the minor one
|
24
|
+
MINOR_AXIS_DIAMETER, turned from the positive vertical semi-axis (i.e. North direction)
|
25
|
+
with angle MAJOR_AXIS_POS_ANGLE, in degrees, towards to the positive horizontal semi-axis
|
26
|
+
(i.e. East direction). (the elongation is along the major_axis)
|
27
|
+
|
28
|
+
For avoiding degenerescence, the domain of variation of MAJOR_AXIS_POS_ANGLE is 180
|
29
|
+
degrees, for ex. from 0 to 180 degrees.
|
30
|
+
|
31
|
+
ELONG_RATIO = major_axis / minor_axis
|
32
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
|
33
|
+
|
34
|
+
The function returns an error if MINOR_AXIS_DIAMETER is negative or if ELONG_RATIO is
|
35
|
+
smaller than 1. """
|
36
|
+
|
37
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "minor_axis_diameter" : minor_axis_diameter, "elong_ratio" : elong_ratio, "major_axis_pos_angle" : major_axis_pos_angle, "name":name, "type":"elong_disk"}, output_mode)
|
38
|
+
|
39
|
+
def flatten_disk( name, flux_weight=1.0, x=0.0, y=0.0, major_axis_diameter=0.0, flatten_ratio=1.0, minor_axis_pos_angle=0.0, output_mode=None):
|
40
|
+
""" Returns the Fourier transform of a normalized ellipse centered at coordinates (X,Y)
|
41
|
+
(milliarcsecond) with a ratio FLATTEN_RATIO between the major diameter
|
42
|
+
MAJOR_AXIS_DIAMETER and the minor one, turned from the positive vertical semi-axis
|
43
|
+
(i.e. North direction) with angle MINOR_AXIS_POS_ANGLE, in degrees, towards to the
|
44
|
+
positive horizontal semi-axis (i.e. East direction). (the flattening is along the minor_axis)
|
45
|
+
|
46
|
+
For avoiding degenerescence, the domain of variation of MINOR_AXIS_POS_ANGLE is 180
|
47
|
+
degrees, for ex. from 0 to 180 degrees.
|
48
|
+
|
49
|
+
FLATTEN_RATIO = major_axis / minor_axis
|
50
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
|
51
|
+
|
52
|
+
The function returns an error if MAJOR_AXIS_DIAMETER is negative or if FLATTEN_RATIO
|
53
|
+
is smaller than 1. """
|
54
|
+
|
55
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "major_axis_diameter" : major_axis_diameter, "flatten_ratio" : flatten_ratio, "minor_axis_pos_angle" : minor_axis_pos_angle, "name":name, "type":"flatten_disk"}, output_mode)
|
56
|
+
|
57
|
+
def circle( name, flux_weight=1.0, x=0.0, y=0.0, diameter=0.0, output_mode=None):
|
58
|
+
""" Returns the Fourier transform of a normalized uniform circle of diameter DIAMETER
|
59
|
+
(milliarcsecond) and centered at coordinates (X,Y) (milliarcsecond).
|
60
|
+
|
61
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
|
62
|
+
|
63
|
+
The function returns an error if DIAMETER is negative. """
|
64
|
+
|
65
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "diameter" : diameter, "name":name, "type":"circle"}, output_mode)
|
66
|
+
|
67
|
+
def ring( name, flux_weight=1.0, x=0.0, y=0.0, diameter=0.0, width=0.0, output_mode=None):
|
68
|
+
""" Returns the Fourier transform of a normalized uniform ring with internal diameter
|
69
|
+
DIAMETER (milliarcsecond) and external diameter DIAMETER + WIDTH centered at coordinates
|
70
|
+
(X,Y) (milliarcsecond).
|
71
|
+
|
72
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
|
73
|
+
|
74
|
+
The function returns an error if DIAMETER or WIDTH are negative. """
|
75
|
+
|
76
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "diameter" : diameter, "width" : width, "name":name, "type":"ring"}, output_mode)
|
77
|
+
|
78
|
+
def elong_ring( name, flux_weight=1.0, x=0.0, y=0.0, minor_internal_diameter=0.0, elong_ratio=1.0, width=0.0, major_axis_pos_angle=0.0, output_mode=None):
|
79
|
+
""" Returns the Fourier transform of a normalized uniform elongated ring centered at
|
80
|
+
coordinates (X,Y) (milliarcsecond). The sizes of the function in two orthogonal directions
|
81
|
+
are given by the narrowest internal diameter (MINOR_INTERNAL_DIAMETER) and by the ratio
|
82
|
+
ELONG_RATIO between the widest internal diameter and MINOR_INTERNAL_DIAMETER,
|
83
|
+
in the same way as for an ellipse (the elongation is along the major_axis) :
|
84
|
+
|
85
|
+
ELONG_RATIO = MAJOR_INTERNAL_DIAMETER / MINOR_INTERNAL_DIAMETER.
|
86
|
+
In the direction of MINOR_INTERNAL_DIAMETER, the external diameter is
|
87
|
+
MINOR_INTERNAL_DIAMETER + WIDTH. In the direction of the widest internal diameter,
|
88
|
+
the width is magnified by the ratio ELONG_RATIO, so that the external diameter is
|
89
|
+
the elongated MAJOR_INTERNAL_DIAMETER + WIDTH * ELONG_RATIO.
|
90
|
+
MAJOR_AXIS_POS_ANGLE is measured in degrees, from the positive vertical semi-axis
|
91
|
+
(i.e. North direction) towards to the positive horizontal semi-axis (i.e. East direction).
|
92
|
+
For avoiding degenerescence, the domain of variation of MAJOR_AXIS_POS_ANGLE is 180
|
93
|
+
degrees, for ex. from 0 to 180 degrees.
|
94
|
+
|
95
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
|
96
|
+
|
97
|
+
The function returns an error if MINOR_INTERNAL_DIAMETER is negative or if ELONG_RATIO
|
98
|
+
is smaller than 1. """
|
99
|
+
|
100
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "minor_internal_diameter" : minor_internal_diameter, "elong_ratio" : elong_ratio, "width" : width, "major_axis_pos_angle" : major_axis_pos_angle, "name":name, "type":"elong_ring"}, output_mode)
|
101
|
+
|
102
|
+
def flatten_ring( name, flux_weight=1.0, x=0.0, y=0.0, major_internal_diameter=0.0, flatten_ratio=1.0, width=0.0, minor_axis_pos_angle=0.0, output_mode=None):
|
103
|
+
""" Returns the Fourier transform of a normalized uniform flattened ring centered at
|
104
|
+
coordinates (X,Y) (milliarcsecond). The sizes of the function in two orthogonal directions
|
105
|
+
are given by the widest internal diameter (MAJOR_INTERNAL_DIAMETER) and by the ratio
|
106
|
+
FLATTEN_RATIO between MAJOR_INTERNAL_DIAMETER and the narrowest internal diameter,
|
107
|
+
in the same way as for an ellipse (the flattening is along the minor axis) :
|
108
|
+
|
109
|
+
FLATTEN_RATIO = MAJOR_INTERNAL_DIAMETER / MINOR_INTERNAL_DIAMETER.
|
110
|
+
In the direction of MAJOR_INTERNAL_DIAMETER, the external diameter is
|
111
|
+
MAJOR_INTERNAL_DIAMETER + WIDTH. In the direction of the narrowest internal diameter,
|
112
|
+
the width is decreased by the ratio FLATTEN_RATIO, so that the external diameter is
|
113
|
+
the flattened MINOR_INTERNAL_DIAMETER + WIDTH / FLATTEN_RATIO.
|
114
|
+
MINOR_AXIS_POS_ANGLE is measured in degrees, from the positive vertical semi-axis
|
115
|
+
(i.e. North direction) towards to the positive horizontal semi-axis (i.e. East direction).
|
116
|
+
For avoiding degenerescence, the domain of variation of MINOR_AXIS_POS_ANGLE is 180
|
117
|
+
degrees, for ex. from 0 to 180 degrees.
|
118
|
+
|
119
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
|
120
|
+
|
121
|
+
The function returns an error if MAJOR_INTERNAL_DIAMETER is negative or if FLATTEN_RATIO
|
122
|
+
is smaller than 1. """
|
123
|
+
|
124
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "major_internal_diameter" : major_internal_diameter, "flatten_ratio" : flatten_ratio, "width" : width, "minor_axis_pos_angle" : minor_axis_pos_angle, "name":name, "type":"flatten_ring"}, output_mode)
|
125
|
+
|
126
|
+
def gaussian( name, flux_weight=1.0, x=0.0, y=0.0, fwhm=0.0, output_mode=None):
|
127
|
+
""" Returns the Fourier transform of a normalized gaussian with given FWHM (milliarcsecond)
|
128
|
+
centered at coordinates (X,Y) (milliarcsecond).
|
129
|
+
|
130
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
|
131
|
+
|
132
|
+
The function returns an error if FWHM is negative. """
|
133
|
+
|
134
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "fwhm" : fwhm, "name":name, "type":"gaussian"}, output_mode)
|
135
|
+
|
136
|
+
def elong_gaussian( name, flux_weight=1.0, x=0.0, y=0.0, minor_axis_fwhm=0.0, elong_ratio=1.0, major_axis_pos_angle=0.0, output_mode=None):
|
137
|
+
""" Returns the Fourier transform of a normalized elongated gaussian centered at coordinates
|
138
|
+
(X,Y) (milliarcsecond). The sizes of the function in two orthogonal directions are given by
|
139
|
+
the narrowest FWHM (MINOR_AXIS_FWHM) and by the ratio ELONG_RATIO between the largest
|
140
|
+
FWHM (MAJOR_AXIS_FWHM) and the MINOR_AXIS_FWHM, in the same way as for an ellipse
|
141
|
+
(the elongation is along the major_axis) :
|
142
|
+
|
143
|
+
ELONG_RATIO = MAJOR_AXIS_FWHM / MINOR_AXIS_FWHM.
|
144
|
+
MAJOR_AXIS_POS_ANGLE is measured in degrees, from the positive vertical semi-axis
|
145
|
+
(i.e. North direction) towards to the positive horizontal semi-axis (i.e. East direction).
|
146
|
+
For avoiding degenerescence, the domain of variation of MAJOR_AXIS_POS_ANGLE is 180
|
147
|
+
degrees, for ex. from 0 to 180 degrees.
|
148
|
+
|
149
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
|
150
|
+
|
151
|
+
The function returns an error if MINOR_AXIS_FWHM is negative or if ELONG_RATIO
|
152
|
+
is smaller than 1. """
|
153
|
+
|
154
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "minor_axis_fwhm" : minor_axis_fwhm, "elong_ratio" : elong_ratio, "major_axis_pos_angle" : major_axis_pos_angle, "name":name, "type":"elong_gaussian"}, output_mode)
|
155
|
+
|
156
|
+
def flatten_gaussian( name, flux_weight=1.0, x=0.0, y=0.0, major_axis_fwhm=0.0, flatten_ratio=1.0, minor_axis_pos_angle=0.0, output_mode=None):
|
157
|
+
""" Returns the Fourier transform of a normalized flattened gaussian centered at coordinates
|
158
|
+
(X,Y) (milliarcsecond). The sizes of the function in two orthogonal directions are given by
|
159
|
+
the largest FWHM (MAJOR_AXIS_FWHM) and by the ratio FLATTEN_RATIO between the largest
|
160
|
+
FWHM (MAJOR_AXIS_FWHM) and the MINOR_AXIS_FWHM, in the same way as for an ellipse
|
161
|
+
(the flattening is along the minor_axis) :
|
162
|
+
|
163
|
+
FLATTEN_RATIO = MAJOR_AXIS_FWHM / MINOR_AXIS_FWHM.
|
164
|
+
MINOR_AXIS_POS_ANGLE is measured in degrees, from the positive vertical semi-axis
|
165
|
+
(i.e. North direction) towards to the positive horizontal semi-axis (i.e. East direction).
|
166
|
+
For avoiding degenerescence, the domain of variation of MINOR_AXIS_POS_ANGLE is 180
|
167
|
+
degrees, for ex. from 0 to 180 degrees.
|
168
|
+
|
169
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
|
170
|
+
|
171
|
+
The function returns an error if MAJOR_AXIS_FWHM is negative or if FLATTEN_RATIO
|
172
|
+
is smaller than 1. """
|
173
|
+
|
174
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "major_axis_fwhm" : major_axis_fwhm, "flatten_ratio" : flatten_ratio, "minor_axis_pos_angle" : minor_axis_pos_angle, "name":name, "type":"flatten_gaussian"}, output_mode)
|
175
|
+
|
176
|
+
def limb_quadratic( name, flux_weight=1.0, x=0.0, y=0.0, diameter=0.0, a1_coeff=0.0, a2_coeff=0.0, output_mode=None):
|
177
|
+
""" Returns the Fourier transform of a center-to-limb darkened disk of diameter DIAMETER
|
178
|
+
(milliarcsecond) centered at coordinates (X,Y) (milliarcsecond).
|
179
|
+
|
180
|
+
The brightness distribution o, if expressed versus mu, the cosine of the azimuth of
|
181
|
+
a surface element of the star, follows a quadratic law of coefficients
|
182
|
+
A1_COEFF, A2_COEFF ([-1,1]), and is normalized for mu = 1 (center of the star).
|
183
|
+
o(mu) = 1 -A1_COEFF(1-mu) - A2_COEFF(1-mu)^2.
|
184
|
+
|
185
|
+
FLUX_WEIGHT is the intensity coefficient. FLUX_WEIGHT=1 means total energy is 1.
|
186
|
+
|
187
|
+
The function returns an error if DIAMETER is negative or if A1_COEFF or A2_coeff is
|
188
|
+
outside bounds [-1,1] """
|
189
|
+
|
190
|
+
return _model({"flux_weight" : flux_weight, "x" : x, "y" : y, "diameter" : diameter, "a1_coeff" : a1_coeff, "a2_coeff" : a2_coeff, "name":name, "type":"limb_quadratic"}, output_mode)
|
191
|
+
|
a2p2/jmmc/models.py
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
__all__ = []
|
4
|
+
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
import xml.etree.ElementTree as ET
|
8
|
+
import json
|
9
|
+
|
10
|
+
from .utils import JmmcAPI
|
11
|
+
from a2p2.ob import etree_to_dict
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
#
|
17
|
+
# first release done for SAMP interoperability with Aspro2
|
18
|
+
#
|
19
|
+
|
20
|
+
def modelsFromXml(xmlmodel):
|
21
|
+
""" Returns a python object representing JSON LIKE model(s) from given XML."""
|
22
|
+
# read xml models from Aspro2 and convert them to a JSON Model
|
23
|
+
e = ET.fromstring(xmlmodel)
|
24
|
+
_remove_namespace(e, "http://www.jmmc.fr/jmcs/models/0.1")
|
25
|
+
container = etree_to_dict(e)
|
26
|
+
modellist=container['model']['model']
|
27
|
+
if not isinstance(modellist, list):
|
28
|
+
modellist=[modellist]
|
29
|
+
|
30
|
+
models=[]
|
31
|
+
for m in modellist:
|
32
|
+
model={}
|
33
|
+
for p in m['parameter']:
|
34
|
+
model[p['type']]=p['value']
|
35
|
+
model['name']=m['name']
|
36
|
+
model['type']=m['type']
|
37
|
+
models.append(model)
|
38
|
+
|
39
|
+
return json.dumps(models)
|
40
|
+
|
41
|
+
|
42
|
+
def _remove_namespace(doc, namespace):
|
43
|
+
"""Remove namespace in the passed document in place.
|
44
|
+
from https://homework.nwsnet.de/releases/45be/"""
|
45
|
+
ns = u'{%s}' % namespace
|
46
|
+
nsl = len(ns)
|
47
|
+
for elem in doc.getiterator():
|
48
|
+
if elem.tag.startswith(ns):
|
49
|
+
elem.tag = elem.tag[nsl:]
|
50
|
+
|
51
|
+
|
52
|
+
def _model(models, output_mode=None):
|
53
|
+
""" Returns a serialisation of given model(s) to XML by default or another format given to output_mode value (only jsonlike at present time)."""
|
54
|
+
|
55
|
+
# deserialize if models is a string
|
56
|
+
if isinstance(models, str):
|
57
|
+
models=json.loads(models)
|
58
|
+
|
59
|
+
if output_mode:
|
60
|
+
return models
|
61
|
+
else :
|
62
|
+
return _xml_model(models)
|
63
|
+
|
64
|
+
def _xml_model(models, position=1):
|
65
|
+
""" Rough conversion of dict(s) to xml (Aspro2 namespaces)
|
66
|
+
if name key is not provided, position value will be used to compute suffix(es)"""
|
67
|
+
|
68
|
+
if isinstance(models, list):
|
69
|
+
modellist=[]
|
70
|
+
pos=1
|
71
|
+
for m in models:
|
72
|
+
modellist.append(_xml_model(m, pos))
|
73
|
+
pos+=1 # do not use models.index(m) since same str components would get the same index
|
74
|
+
return "".join(modellist)
|
75
|
+
|
76
|
+
model = models # models is now a single element
|
77
|
+
modeltype=model["type"]
|
78
|
+
if "name" in model.keys():
|
79
|
+
modelname=model["name"]
|
80
|
+
else:
|
81
|
+
modelname=modeltype+str(position)
|
82
|
+
|
83
|
+
|
84
|
+
params=""
|
85
|
+
paramNames=[ k for k in model.keys() if not k in ("type", "name")]
|
86
|
+
# at present time we must use Aspro2's approach with the same xml namespaces
|
87
|
+
# see
|
88
|
+
for p in paramNames:
|
89
|
+
params+=f""" <tm:parameter name="{modelname}_{p}" type="{p}"><value>{model[p]}</value></tm:parameter>\n"""
|
90
|
+
|
91
|
+
return f"""<tm:model name="{modelname}" type="{modeltype}">\n{params}</tm:model>\n"""
|
92
|
+
|
93
|
+
|
94
|
+
class Models():
|
95
|
+
""" Get analytical model's representations (in sync with Aspro2's ones).
|
96
|
+
"""
|
97
|
+
SAMP_UCD_MODEL="meta.code.class;meta.modelled" # use it for colums that would be filled by models below
|
98
|
+
|
99
|
+
# generated code below from an Asprox file that contains a single star with list of all supported models
|
100
|
+
# cd a2p2/jmmc
|
101
|
+
# xml sel -o "from .models import _model" -n -n -b -t -m "//tm:model" -o "def " -v "@type" -o "( name, " -m "tm:parameter" -v "@type" -o "=" -v "value" -o ", " -b -o " output_mode=None):" -n -o ' """ ' -v "desc" -o ' """ ' -n -n -o ' return _model({' -m "tm:parameter" -o '"' -v "@type" -o '" : ' -v "@type" -o ", " -b -o '"name":name, "type":"' -v "@type" -o '"}, output_mode)' -n -n models_template.asprox > generated_models.py
|
102
|
+
# xml sel -t -o " from .generated_models import " -m "//tm:model" -v "@type" -o ", " -b -n models_template.asprox
|
103
|
+
# cd -
|
104
|
+
from .generated_models import punct, disk, elong_disk, flatten_disk, circle, ring, elong_ring, flatten_ring, gaussian, elong_gaussian, flatten_gaussian, limb_quadratic
|
a2p2/jmmc/services.py
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
__all__ = []
|
4
|
+
|
5
|
+
import logging
|
6
|
+
|
7
|
+
from .catalogs import Catalog
|
8
|
+
from .webservices import CallIper
|
9
|
+
|
10
|
+
logger = logging.getLogger(__name__)
|
11
|
+
|
12
|
+
# TODO list and give access to whole services and their beta/alpha / pre-prod instances if existing
|
13
|
+
|
14
|
+
oidb_catalog = Catalog("oidb")
|
15
|
+
|
16
|
+
calliper = CallIper()
|
a2p2/jmmc/utils.py
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
from urllib3.util.retry import Retry
|
4
|
+
from requests.adapters import HTTPAdapter
|
5
|
+
from requests.auth import HTTPBasicAuth
|
6
|
+
import requests
|
7
|
+
|
8
|
+
from a2p2.client import A2P2ClientPreferences
|
9
|
+
__all__ = []
|
10
|
+
|
11
|
+
import logging
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
# Do not use next code directly - this is dev related code.
|
16
|
+
# Please have a look on the other jmmc modules for client code.
|
17
|
+
|
18
|
+
|
19
|
+
# Retry Handling for requests
|
20
|
+
# See https://www.peterbe.com/plog/best-practice-with-retries-with-requests
|
21
|
+
# Or https://findwork.dev/blog/advanced-usage-python-requests-timeouts-retries-hooks/
|
22
|
+
|
23
|
+
|
24
|
+
# TODO check that we return more infiormation for 500 error case because some server side code part do return 500
|
25
|
+
# and here we may
|
26
|
+
|
27
|
+
def requests_retry_session(
|
28
|
+
retries=3,
|
29
|
+
backoff_factor=1.0,
|
30
|
+
status_forcelist=(500, 502, 504)):
|
31
|
+
|
32
|
+
adapter = HTTPAdapter()
|
33
|
+
adapter.max_retries = Retry(
|
34
|
+
total=retries,
|
35
|
+
read=retries,
|
36
|
+
connect=retries,
|
37
|
+
backoff_factor=backoff_factor,
|
38
|
+
status_forcelist=status_forcelist,
|
39
|
+
respect_retry_after_header=False
|
40
|
+
)
|
41
|
+
session = requests.Session()
|
42
|
+
session.mount('http://', adapter)
|
43
|
+
session.mount('https://', adapter)
|
44
|
+
return session
|
45
|
+
|
46
|
+
# r = requests.request(method, url, headers=headers, data=json.dumps(data))
|
47
|
+
# -> r = requests_session.request(method, url, headers=headers, data=json.dumps(data))
|
48
|
+
|
49
|
+
|
50
|
+
class JmmcAPI():
|
51
|
+
def __init__(self, rootURL, username=None, password=None):
|
52
|
+
self.rootURL = rootURL
|
53
|
+
|
54
|
+
# credentials can be given :
|
55
|
+
# by parameters
|
56
|
+
# if username or password is None, try to get it from the a2p2 preferences ( run: a2p2 -c )
|
57
|
+
# if username or password still is None keep auth to None.
|
58
|
+
# a None auth will search for ~/.netrc files trying to fix 401 accesses
|
59
|
+
|
60
|
+
prefs = A2P2ClientPreferences()
|
61
|
+
if not username :
|
62
|
+
username = prefs.getJmmcLogin()
|
63
|
+
if not password :
|
64
|
+
password = prefs.getJmmcPassword()
|
65
|
+
|
66
|
+
# if we got complete credential
|
67
|
+
if username and password:
|
68
|
+
self.auth = HTTPBasicAuth(username, password)
|
69
|
+
else:
|
70
|
+
self.auth = None
|
71
|
+
|
72
|
+
self.requests_session = requests_retry_session()
|
73
|
+
|
74
|
+
logger.debug("Instanciating JmmcAPI on '%s'" % self.rootURL)
|
75
|
+
|
76
|
+
def _get(self, url):
|
77
|
+
return self._request('GET', url)
|
78
|
+
|
79
|
+
def _put(self, url, json):
|
80
|
+
return self._request('PUT', url, json)
|
81
|
+
|
82
|
+
def _post(self, url, json):
|
83
|
+
return self._request('POST', url, json)
|
84
|
+
|
85
|
+
def _post(self, url, **kwargs):
|
86
|
+
return self._request2('POST', url, **kwargs)
|
87
|
+
|
88
|
+
def _request2(self, method, url, **kwargs):
|
89
|
+
logger.info("performing %s request on %s" % (method, self.rootURL+url))
|
90
|
+
r = self.requests_session.request(
|
91
|
+
method, self.rootURL+url, **kwargs)
|
92
|
+
# handle response if any or throw an exception
|
93
|
+
if (r.status_code == 204): # No Content : everything is fine
|
94
|
+
return
|
95
|
+
elif 200 <= r.status_code < 300:
|
96
|
+
if 'Content-Type' in r.headers.keys() and 'application/json' in r.headers['Content-Type']:
|
97
|
+
return r.json()
|
98
|
+
else:
|
99
|
+
return r.content
|
100
|
+
# TODO enhance error handling ? Throw an exception ....
|
101
|
+
error = []
|
102
|
+
error.append("status_code is %s"%r.status_code)
|
103
|
+
if r.reason :
|
104
|
+
error.append(r.reason)
|
105
|
+
if "X-Http-Error-Description" in r.headers.keys():
|
106
|
+
error.append(r.headers["X-Http-Error-Description"])
|
107
|
+
|
108
|
+
raise Exception(error)
|
109
|
+
|
110
|
+
def _request(self, method, url, json=None, data=None, files=None):
|
111
|
+
logger.info("performing %s request on %s" % (method, self.rootURL+url))
|
112
|
+
r = self.requests_session.request(
|
113
|
+
method, self.rootURL+url, auth=self.auth, json=json, data=data, files=files)
|
114
|
+
# handle response if any or throw an exception
|
115
|
+
if (r.status_code == 204): # No Content : everything is fine
|
116
|
+
return
|
117
|
+
elif 200 <= r.status_code < 300:
|
118
|
+
if 'Content-Type' in r.headers.keys() and 'application/json' in r.headers['Content-Type']:
|
119
|
+
return r.json()
|
120
|
+
else:
|
121
|
+
return r.content
|
122
|
+
# TODO enhance error handling ? Throw an exception ....
|
123
|
+
error = []
|
124
|
+
error.append("status_code is %s"%r.status_code)
|
125
|
+
if r.reason :
|
126
|
+
error.append(r.reason)
|
127
|
+
if "X-Http-Error-Description" in r.headers.keys():
|
128
|
+
error.append(r.headers["X-Http-Error-Description"])
|
129
|
+
|
130
|
+
raise Exception(error)
|
a2p2/jmmc/webservices.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
__all__ = []
|
4
|
+
|
5
|
+
import logging
|
6
|
+
import json
|
7
|
+
|
8
|
+
from .utils import JmmcAPI
|
9
|
+
from . import PRODLABEL
|
10
|
+
|
11
|
+
logger = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
class CallIper():
|
14
|
+
""" Call IPER using its 'API' through simple methods.
|
15
|
+
|
16
|
+
from a2p2.jmmc import CallIper
|
17
|
+
calliper = CallIper()
|
18
|
+
diskresult = caliper.runDiskFit(pathname)
|
19
|
+
ud = diskresult["diameter"]
|
20
|
+
|
21
|
+
"""
|
22
|
+
def __init__(self, prod=False, apiUrl=None):
|
23
|
+
self.prod = prod
|
24
|
+
self.serviceName = "callIper"
|
25
|
+
|
26
|
+
# Manage prod & preprod or user provided access points
|
27
|
+
if apiUrl:
|
28
|
+
self.apiUrl = apiUrl # trust given url as callIperAPI if value is provided
|
29
|
+
elif self.prod:
|
30
|
+
self.apiUrl = "" # no api in production yet
|
31
|
+
raise Warning("sorry, no api for production yet")
|
32
|
+
else:
|
33
|
+
self.apiUrl = "http://apps.jmmc.fr/~mellag/LITproWebService/run.php"
|
34
|
+
|
35
|
+
self.api = JmmcAPI(self.apiUrl)
|
36
|
+
|
37
|
+
logger.info("Create calliper client to access '%s' (%s API at %s)" %
|
38
|
+
(self.serviceName, PRODLABEL[self.prod], self.api.rootURL))
|
39
|
+
|
40
|
+
# API will evolve in the future with parameter to select fitter, model and data to fit...
|
41
|
+
def runDiskFit(self, oifitsFilename, jsonOutput=True):
|
42
|
+
""" Ask CallIper to fit given data for a simple disk """
|
43
|
+
files = {'userfile': (oifitsFilename, open(oifitsFilename, 'rb'))}
|
44
|
+
data = {'method': 'runJsonFit'}
|
45
|
+
res=self.api._post("", data=data, files=files)
|
46
|
+
if jsonOutput:
|
47
|
+
return json.loads(res)
|
48
|
+
return res
|