Commit 8f7efae9ede7775861bce951e445364775d6de6a
1 parent
519258d7
Exists in
master
Big update for Components.
Showing
12 changed files
with
2437 additions
and
230 deletions
Show diff stats
docs/source/device_definition.rst
... | ... | @@ -16,9 +16,16 @@ In Guitastro we define the following **categories** of components: |
16 | 16 | |
17 | 17 | * **MountPointing**: It is a mechanical motorized mount able to point a direction in the sky using two axes. |
18 | 18 | * **DetectorFocuser**: It is a mechanical motorized motor placed close the detector to able changing the focus. |
19 | - | |
20 | -The communication chanel is the same for all the components. | |
21 | -The commands are contents to pilot the components. There are two types of commands: | |
19 | + | |
20 | +The complete list of supported categories can be listed: | |
21 | + | |
22 | +.. code-block:: python | |
23 | + | |
24 | + import guitastro | |
25 | + print(Component().categories) | |
26 | + | |
27 | +The communication chanel is the same for all the components included in the same device. | |
28 | +The commands are messages to pilot the components. There are two types of commands: | |
22 | 29 | |
23 | 30 | * **uniform**: From the side of a Guitastro user, the commands are the same for a given component category. For example to slew a mount the command is always "DO RADEC_GOTO". |
24 | 31 | * **native**: From the side of device controler, the commands are translated by guitastro into the device manufactured grammar. |
... | ... | @@ -67,12 +74,46 @@ A given category of component have a list of operations. For example for a Mount |
67 | 74 | 3. Integration of components into devices for Guitastro |
68 | 75 | ******************************************************* |
69 | 76 | |
70 | -The device codes a written outside the guitastro module. | |
71 | -For a given device the code is a module. For example, guitastro_device_meade_mounts. | |
77 | +The device codes are written outside the guitastro module. | |
78 | +For a given device, the code is a module. For example, guitastro_device_meade_mount. | |
72 | 79 | The device modules are downloaded in the site of Guitastro. |
73 | -To drive a device it is necessary to install guitastro the device module. | |
80 | +To drive a device it is necessary to install guitastro and the device module. | |
74 | 81 | |
75 | 82 | 4. Simulation and real devices |
76 | 83 | ****************************** |
77 | 84 | |
78 | -Blabla. | |
85 | +Take a example with the device module guitastro_device_meade_mount. | |
86 | +This module has a class Device_MeadeMount. This class is composed by two components: | |
87 | + | |
88 | + * The file component_mount_pointing_meade_mount provides the class ComponentMountPointingMeadeMount. This class inherits from ComponentMountPointing and adds the method _my_do that send native commands to the real device. | |
89 | + * The file component_detector_focuser_meade_mount provides the class ComponentDetectorFocuserMeadeMount. This class inherits from ComponentDetectorFocuser and adds the method _my_do that send native commands to the real device. | |
90 | + | |
91 | +Consider the component of category MountPointing. | |
92 | +The class ComponentMountPointing inherits from the class Component. | |
93 | +ComponentMountPointing concretes the method _do which provides a | |
94 | +simulator of a mount. kwargs of the class ComponentMountPointing | |
95 | +can be used to give optional parameters. | |
96 | + | |
97 | +To summarize: | |
98 | + | |
99 | + * The class **Component** provides basic methods: command, parameters, prop, init and attributes categories, channel, category, log, verbose, actions | |
100 | + * The class **ComponentMountPointing** overload mainly the method prop to define the list of actions/operations and _do for a simulator. | |
101 | + * The class **ComponentMountPointingMeadeMount** overload mainly the method _my_do for real driving of the mount. _my_do is called after _do. | |
102 | + * The class **Device_MeadeMount** provides communication methods: open, close, commandstring, command and provides information method components. | |
103 | + | |
104 | +The following example shows how to instanciate and send commands: | |
105 | + | |
106 | +.. code-block:: python | |
107 | + | |
108 | + import guitastro | |
109 | + dev = Device_MeadeMount() | |
110 | + print("Components are:") | |
111 | + for key, val in dev.components().items(): | |
112 | + print(f" * {key} of type {val[0]}") | |
113 | + dev.open(True) | |
114 | + dev.commandstring("mount SET target 'RADEC 12h56m -10d23m'") | |
115 | + dev.commandstring("mount DO RADEC_GOTO") | |
116 | + res = dev.commandstring("mount DO COORD") | |
117 | + print(f"Coordinates of the mount: {res}") | |
118 | + res = dev.commandstring("focuser DO COORD") | |
119 | + print(f"Position of the focuser: {res}") | ... | ... |
install/requirements.in
src/guitastro/__init__.py
src/guitastro/communications.py
... | ... | @@ -360,7 +360,7 @@ class Communication(): |
360 | 360 | err = self.ERR_CHAN_OPEN |
361 | 361 | except: |
362 | 362 | self._set_last_error_msg() |
363 | - msg = self._error_msg | |
363 | + msg = str(self._error_msg) | |
364 | 364 | raise CommunicationException(CommunicationException.ERR_CHAN_UNKNOWN, msg) |
365 | 365 | # --- some inits |
366 | 366 | if err == self.NO_ERROR: |
... | ... | @@ -447,6 +447,7 @@ class Communication(): |
447 | 447 | fid.reset_input_buffer() |
448 | 448 | # --- serial port is ready to put message |
449 | 449 | cmd = cmd + self._end_of_command_to_send |
450 | + #print(f"cmd={cmd}") | |
450 | 451 | fid.write(cmd.encode()) |
451 | 452 | fid.flush() |
452 | 453 | elif self._channel_type=="TCP": |
... | ... | @@ -494,7 +495,7 @@ class Communication(): |
494 | 495 | lignes = fid.recv(1024) |
495 | 496 | except: |
496 | 497 | err = self.ERR_CHAN_TCP_RECV_TIMEOUT |
497 | - #self.log.print("dt={} lignes={} type={}".format(dt,lignes,type(lignes))) | |
498 | + #print("dt={} lignes={} type={}".format(dt,lignes,type(lignes))) | |
498 | 499 | # --- format la sortie |
499 | 500 | if type(lignes)==bytes: |
500 | 501 | # --- TCP case (the try is useful for a pickle data) |
... | ... | @@ -506,7 +507,7 @@ class Communication(): |
506 | 507 | res = lignes.split(self._end_of_command_to_receive) |
507 | 508 | else: |
508 | 509 | res = lignes |
509 | - #self.log.print("READ {} = {}".format(self._port_chan,res)) | |
510 | + #print("READ {} = {}".format(self._port_chan,res)) | |
510 | 511 | return (err, res) |
511 | 512 | |
512 | 513 | def _set_delay_chan(self, delay_s:float): |
... | ... | @@ -610,7 +611,7 @@ class Communication(): |
610 | 611 | A string containing the response received. |
611 | 612 | """ |
612 | 613 | if self.verbose_chan==True: |
613 | - # self.log.self.log.print("Putread: "+cmd) | |
614 | + #print("Putread: "+cmd) | |
614 | 615 | pass |
615 | 616 | err = self.NO_ERROR |
616 | 617 | res = "" |
... | ... | @@ -623,32 +624,32 @@ class Communication(): |
623 | 624 | time.sleep(0.05) |
624 | 625 | dt = time.time() - t0 |
625 | 626 | if dt>2.0: |
626 | - #self.log.print("Serial port locked timeout for {}".format(cmd)) | |
627 | + print("Serial port locked timeout for {}".format(cmd)) | |
627 | 628 | break |
628 | 629 | # --- verify the lock state |
629 | 630 | if self._lock_putread == False: |
630 | 631 | # --- Case the lock state = False, the com is free |
631 | 632 | # --- lock during com operations |
632 | 633 | self._lock_putread = True |
633 | - #self.log.print("Serial port is locked 0 normaly by {}".format(cmd)) | |
634 | + #print("Serial port is locked 0 normaly by {}".format(cmd)) | |
634 | 635 | self.put_chan(cmd) |
635 | 636 | time.sleep(self._delay_put_read) |
636 | 637 | err, res = self.read_chan() |
637 | 638 | if err != self.NO_ERROR: |
638 | 639 | self._lock_putread = False |
639 | - #self.log.print("Serial port is unlocked 1 err={} res={} by {}".format(err,res,cmd)) | |
640 | + #print("Serial port is unlocked 1 err={} res={} by {}".format(err,res,cmd)) | |
640 | 641 | return (err, res) |
641 | 642 | if index>=0: |
642 | 643 | n = len(res) |
643 | 644 | if index>n-1: |
644 | 645 | index = n-1 |
645 | 646 | self._lock_putread = False |
646 | - #self.log.print("Serial port is unlocked 2 err={} res={} by {}".format(err,res,cmd)) | |
647 | + #print("Serial port is unlocked 2 err={} res={} by {}".format(err,res,cmd)) | |
647 | 648 | return (err, res[index]) |
648 | 649 | self._lock_putread = False |
649 | - #self.log.print("Serial port is unlocked 3 err={} res={} by {}".format(err,res,cmd)) | |
650 | + #print("Serial port is unlocked 3 err={} res={} by {}".format(err,res,cmd)) | |
650 | 651 | else: |
651 | - #self.log.print("Pb serial port is locked for {}".format(cmd)) | |
652 | + #print("Pb serial port is locked for {}".format(cmd)) | |
652 | 653 | err = self.ERR_CHAN_PUTREAD_LOCKED |
653 | 654 | return (err, res) |
654 | 655 | |
... | ... | @@ -676,6 +677,14 @@ class Communication(): |
676 | 677 | except: |
677 | 678 | pass |
678 | 679 | |
680 | + def __str__(self): | |
681 | + msg = "" | |
682 | + # ------------------------ | |
683 | + msg += f"=== Communication channel of type {self._channel_type} ===" | |
684 | + msg += "\n--- Channel parameters:" | |
685 | + for key, val in self._channel_params.items(): | |
686 | + msg += f"\n * {key} = {val}" | |
687 | + return msg | |
679 | 688 | |
680 | 689 | # ##################################################################### |
681 | 690 | # ##################################################################### | ... | ... |
src/guitastro/component.py
1 | 1 | import datetime |
2 | +from threading import Thread, Event | |
3 | +from queue import Queue | |
4 | +import traceback | |
5 | +import time | |
6 | +import sys | |
7 | +import ast | |
2 | 8 | |
3 | 9 | try: |
4 | - from .guitastrotools import GuitastroTools, GuitastroException | |
10 | + from .guitastrotools import GuitastroException | |
5 | 11 | except: |
6 | - from guitastrotools import GuitastroTools, GuitastroException | |
12 | + from guitastrotools import GuitastroException | |
7 | 13 | |
8 | 14 | # ##################################################################### |
9 | 15 | # ##################################################################### |
... | ... | @@ -13,6 +19,197 @@ except: |
13 | 19 | # ##################################################################### |
14 | 20 | # ##################################################################### |
15 | 21 | |
22 | +class ComponentDataBaseException(GuitastroException): | |
23 | + | |
24 | + ERR_ARG0_SELF_MANDATORY = 0 | |
25 | + ERR_ACTION_NOT_FOUND = 1 | |
26 | + ERR_OPERATION_NOT_FOUND = 2 | |
27 | + ERR_NOT_ENOUGH_PARAMETERS = 3 | |
28 | + | |
29 | + errors = [""]*4 | |
30 | + errors[ERR_ARG0_SELF_MANDATORY] = "Parameter *arg must be passed at less with arg[0]=self during instanciation" | |
31 | + | |
32 | +class ComponentDataBase(Thread, ComponentDataBaseException): | |
33 | + """Thread to manage a simple database between threads | |
34 | + | |
35 | + This simple database is useful to exchange data between thread started by a device manager. | |
36 | + Technically, the communication is done using a Queue. | |
37 | + | |
38 | + The database is instanciated from the main thread (self): | |
39 | + | |
40 | + :: | |
41 | + | |
42 | + from threading import Thread, Event | |
43 | + from queue import Queue | |
44 | + | |
45 | + self.queue = Queue() | |
46 | + db = ComponentDataBase(self) | |
47 | + db.start() | |
48 | + | |
49 | + The **self** passed to the database object allows to access to the Queue. | |
50 | + Do not forget to kill the thread when the use of the database is finished: | |
51 | + | |
52 | + :: | |
53 | + | |
54 | + db.stop() | |
55 | + | |
56 | + or | |
57 | + | |
58 | + :: | |
59 | + | |
60 | + del db | |
61 | + | |
62 | + The database is contituted by only one table. | |
63 | + The table is a dictionary. | |
64 | + Each entry of the table is an item of the dictionary. | |
65 | + | |
66 | + When the communication is done with another thread that using self, | |
67 | + use the put method of the Queue. | |
68 | + For example to create or update a new entry: | |
69 | + | |
70 | + :: | |
71 | + | |
72 | + # -- code of another thread which will be started by the main thread | |
73 | + class ComponentA(Thread): | |
74 | + | |
75 | + def __init__(self, upperself): | |
76 | + Thread.__init__(self) | |
77 | + self._queue = upperself._queue | |
78 | + self._stopevent = Event() | |
79 | + | |
80 | + def run(self): | |
81 | + try: | |
82 | + while True: | |
83 | + key = "my_param" | |
84 | + val = 10 | |
85 | + self._queue.put(f"{key} = {val}") | |
86 | + time.sleep(1) | |
87 | + except: | |
88 | + traceback.print_exc(file=sys.stdout) | |
89 | + | |
90 | + def stop(self): | |
91 | + self._stopevent.set() | |
92 | + | |
93 | + def __del__(self): | |
94 | + self.stop() | |
95 | + | |
96 | + # === Now consider the main thread === | |
97 | + | |
98 | + # -- code that starts the database in the main thread | |
99 | + self.queue = Queue() | |
100 | + db = ComponentDataBase(self) | |
101 | + db.start() | |
102 | + | |
103 | + # -- code that starts the CompnentA in the main thread | |
104 | + compa = ComponentA(self) | |
105 | + compa.start() | |
106 | + | |
107 | + # -- we kill all the threads after 10 seconds | |
108 | + time.sleep(10) | |
109 | + del compa | |
110 | + del db | |
111 | + | |
112 | + This code fill the database every second with the item | |
113 | + "my_param" = 10. | |
114 | + Queries can be only done from the thread to the database. | |
115 | + | |
116 | + When the communication is done with the parent thread | |
117 | + it is not need to use the Queue. In this condition, | |
118 | + use the query method. For example to create or update a new entry: | |
119 | + | |
120 | + :: | |
121 | + | |
122 | + # === We are in the main thread === | |
123 | + | |
124 | + # -- code that starts the database in the main thread | |
125 | + self.queue = Queue() | |
126 | + db = ComponentDataBase(self) | |
127 | + db.start() | |
128 | + | |
129 | + # -- code fill the database directly from the main thread | |
130 | + key = "my_param" | |
131 | + val = 10 | |
132 | + db.query(key, val) | |
133 | + | |
134 | + """ | |
135 | + | |
136 | + def __init__(self, *args, **kwargs): | |
137 | + Thread.__init__(self) | |
138 | + na = len(args) | |
139 | + if na == 0: | |
140 | + raise ComponentDataBaseException(ComponentDataBaseException.ERR_ARG0_SELF_MANDATORY) | |
141 | + self._upself = args[0] | |
142 | + self._queue = self._upself._queue | |
143 | + self._stopevent = Event() | |
144 | + self._param = {} | |
145 | + | |
146 | + def query(self, *args): | |
147 | + """Method to access i/o to the database directly | |
148 | + | |
149 | + :: | |
150 | + | |
151 | + db = ComponentDataBase() | |
152 | + | |
153 | + To fill the dabase table table with with | |
154 | + db.param("mykey", 10) | |
155 | + | |
156 | + | |
157 | + """ | |
158 | + #print(f"QUERY received args={args}") | |
159 | + na = len(args) | |
160 | + # --- case no parameter. We return the dictionary | |
161 | + if na == 0: | |
162 | + return self._param | |
163 | + args0 = args[0] | |
164 | + # --- | |
165 | + if isinstance(args0, dict): | |
166 | + # --- case args[0] is a dictionary. We update the dictionary | |
167 | + keyval = args0 | |
168 | + for key, val in keyval.items(): | |
169 | + self._param[key] = val | |
170 | + elif na==1 and isinstance(args0, str): | |
171 | + # --- case args[0] is a key. We return only the val of the key | |
172 | + key = args0 | |
173 | + elif na==2 and isinstance(args0, str): | |
174 | + # --- case args[0] is a key followed by a value. We update the val of the key | |
175 | + key = args0 | |
176 | + val = self._upself._literal_eval(args[1]) | |
177 | + self._param[key] = val | |
178 | + # --- We return only the val of the key | |
179 | + return self._param.get(key) # None if key is not in dict | |
180 | + | |
181 | + def run(self): | |
182 | + """Method to access i/o to the database with a queue | |
183 | + """ | |
184 | + while True: | |
185 | + try: | |
186 | + contents = self._queue.get() | |
187 | + #print(f"QUEUE received {contents}") | |
188 | + if isinstance(contents, dict) == True: | |
189 | + for key, val in contents.items(): | |
190 | + self._param[key] = val | |
191 | + else: | |
192 | + k = contents.find("=") | |
193 | + if k>0: | |
194 | + kvs = contents.split("=") | |
195 | + if len(kvs)>1: | |
196 | + key = kvs[0].strip() | |
197 | + val = kvs[1].strip() | |
198 | + self._param[key] = self._upself._literal_eval(val) | |
199 | + self._queue.task_done() | |
200 | + time.sleep(0.1) | |
201 | + except: | |
202 | + traceback.print_exc(file=sys.stdout) | |
203 | + # --- | |
204 | + # The thread termined properly | |
205 | + | |
206 | + def stop(self): | |
207 | + # --- stop any motion | |
208 | + self._stopevent.set() | |
209 | + | |
210 | + def __del__(self): | |
211 | + self.stop() | |
212 | + | |
16 | 213 | class ComponentException(GuitastroException): |
17 | 214 | |
18 | 215 | ERR_CHANNEL_NONE = 0 |
... | ... | @@ -116,11 +313,27 @@ class Component(ComponentException): |
116 | 313 | _result['errcode'] = -1 |
117 | 314 | _result['errmsg'] = "" |
118 | 315 | _result['log'] = "" |
316 | + _real = False | |
317 | + _verbose = 0 | |
318 | + | |
319 | + categories = ['MountPointing', 'DetectorFocuser'] | |
119 | 320 | |
120 | 321 | # ===================================================================== |
121 | 322 | # properties |
122 | 323 | # ===================================================================== |
123 | 324 | |
325 | + def _get_real(self)->bool: | |
326 | + """Get is the component is real or not | |
327 | + """ | |
328 | + return self._real | |
329 | + | |
330 | + def _set_real(self, truefalse: bool): | |
331 | + """Get is the component is real or not | |
332 | + """ | |
333 | + self._real = truefalse | |
334 | + | |
335 | + real = property(_get_real, _set_real) | |
336 | + | |
124 | 337 | def _get_channel(self): |
125 | 338 | """Get the channel of communication |
126 | 339 | """ |
... | ... | @@ -159,15 +372,11 @@ class Component(ComponentException): |
159 | 372 | def _get_verbose(self): |
160 | 373 | """Get the verbose level for the returns of the Component |
161 | 374 | """ |
162 | - if 'verbose' not in self._param.keys(): | |
163 | - self._set_verbose(0) | |
164 | - verb = self._param['verbose'] | |
165 | - msg = "" | |
166 | - if verb == 0: | |
375 | + if self._verbose == 0: | |
167 | 376 | msg = "value" |
168 | - if verb == 1: | |
377 | + if self._verbose == 1: | |
169 | 378 | msg = "value, log" |
170 | - return verb, msg | |
379 | + return self._verbose, msg | |
171 | 380 | |
172 | 381 | def _set_verbose(self, value): |
173 | 382 | """Set the verbose level for the returns of the Component |
... | ... | @@ -181,7 +390,7 @@ class Component(ComponentException): |
181 | 390 | except: |
182 | 391 | value = 0 |
183 | 392 | if 0 <= value <= 1: |
184 | - self._param['verbose'] = value | |
393 | + self._verbose = value | |
185 | 394 | |
186 | 395 | verbose = property(_get_verbose, _set_verbose) |
187 | 396 | |
... | ... | @@ -218,6 +427,7 @@ class Component(ComponentException): |
218 | 427 | res['category'] = 'Component' |
219 | 428 | res['actions'] = ["DO", "GET", "SET"] |
220 | 429 | res['DO'] = {} |
430 | + res['DO']['NATIVE'] = "The message is sent to the device with no transformation" | |
221 | 431 | # just a dummy example |
222 | 432 | res['DO']['NOTHING'] = "Nothing to do" |
223 | 433 | return res |
... | ... | @@ -278,8 +488,8 @@ class Component(ComponentException): |
278 | 488 | return result |
279 | 489 | |
280 | 490 | def init(self, *args, **kwargs): |
281 | - self._set("verbose", 0) | |
282 | - self._my_init(*args, **kwargs) | |
491 | + self._my_init1(*args, **kwargs) | |
492 | + self._my_init2(*args, **kwargs) | |
283 | 493 | |
284 | 494 | # ===================================================================== |
285 | 495 | # protected methods |
... | ... | @@ -299,6 +509,18 @@ class Component(ComponentException): |
299 | 509 | msg = f"At less {na} parameters must be given" |
300 | 510 | raise ComponentException(ComponentException.ERR_NOT_ENOUGH_PARAMETERS, msg) |
301 | 511 | |
512 | + def _literal_eval(self, node_or_string:str)->any: | |
513 | + """Transform a string into the best type as possible | |
514 | + """ | |
515 | + try: | |
516 | + val = ast.literal_eval(node_or_string) | |
517 | + except: | |
518 | + try: | |
519 | + val = self._upself._to_number(node_or_string) | |
520 | + except: | |
521 | + val = node_or_string | |
522 | + return val | |
523 | + | |
302 | 524 | def _fresult(self, value): |
303 | 525 | """Formated result for returns |
304 | 526 | """ |
... | ... | @@ -314,8 +536,14 @@ class Component(ComponentException): |
314 | 536 | def _set(self, *args, **kwargs): |
315 | 537 | self._verify_nargs(2, *args) |
316 | 538 | key = args[0] |
539 | + na = len(args) | |
317 | 540 | value = args[1] |
318 | - self._param[key] = value | |
541 | + if na>2: | |
542 | + value = str(args[1]) | |
543 | + for v in args[2:]: | |
544 | + value += " " + str(v) | |
545 | + print(f"value={value}") | |
546 | + self.database.query(key, value) | |
319 | 547 | self._my_set(*args, **kwargs) |
320 | 548 | |
321 | 549 | def _my_set(self, *args, **kwargs): |
... | ... | @@ -323,15 +551,13 @@ class Component(ComponentException): |
323 | 551 | |
324 | 552 | def _get(self, *args, **kwargs): |
325 | 553 | if len(args)==0: |
326 | - return self._param | |
554 | + return self.database.query() | |
327 | 555 | key = args[0] |
328 | - value = None | |
329 | - if key in self._param.keys(): | |
330 | - value = self._param[key] | |
331 | - v = self._my_get(*args, **kwargs) | |
332 | - if v != None: | |
333 | - value = v | |
334 | - return value | |
556 | + param = self.database.query(key) | |
557 | + my_param = self._my_get(*args, **kwargs) | |
558 | + if my_param != None: | |
559 | + param = my_param | |
560 | + return param | |
335 | 561 | |
336 | 562 | def _my_get(self, *args, **kwargs): |
337 | 563 | return None |
... | ... | @@ -345,6 +571,8 @@ class Component(ComponentException): |
345 | 571 | msg = f"{operation} not found amongst {operations}" |
346 | 572 | raise ComponentException(ComponentException.ERR_OPERATION_NOT_FOUND, msg) |
347 | 573 | # --- |
574 | + if operation == "NATIVE": | |
575 | + pass | |
348 | 576 | if operation == "NOTHING": |
349 | 577 | pass |
350 | 578 | result = self._my_do(*args, **kwargs) |
... | ... | @@ -353,7 +581,12 @@ class Component(ComponentException): |
353 | 581 | def _my_do(self, *args, **kwargs): |
354 | 582 | return None |
355 | 583 | |
356 | - def _my_init(self, *args, **kwargs): | |
584 | + def _my_init1(self, *args, **kwargs): | |
585 | + # before instanciation of the simulator | |
586 | + pass | |
587 | + | |
588 | + def _my_init2(self, *args, **kwargs): | |
589 | + # after instanciation of the simulator to configure it. | |
357 | 590 | pass |
358 | 591 | |
359 | 592 | # ===================================================================== |
... | ... | @@ -363,8 +596,17 @@ class Component(ComponentException): |
363 | 596 | # ===================================================================== |
364 | 597 | |
365 | 598 | def __init__(self, *args, **kwargs): |
599 | + # --- | |
600 | + self._queue = Queue() | |
601 | + self.database = ComponentDataBase(self) | |
602 | + self.database.start() | |
603 | + time.sleep(0.2) | |
604 | + # --- | |
366 | 605 | self.init(*args, **kwargs) |
367 | 606 | |
607 | + def __del__(self): | |
608 | + self.database.stop() | |
609 | + | |
368 | 610 | # ##################################################################### |
369 | 611 | # ##################################################################### |
370 | 612 | # ##################################################################### | ... | ... |
src/guitastro/component_detector_focuser.py
1 | + | |
2 | + | |
1 | 3 | try: |
2 | 4 | from .home import Home |
3 | 5 | except: |
... | ... | @@ -9,6 +11,11 @@ except: |
9 | 11 | from siteobs import Siteobs |
10 | 12 | |
11 | 13 | try: |
14 | + from .motoraxis import Motoraxis | |
15 | +except: | |
16 | + from motoraxis import Motoraxis | |
17 | + | |
18 | +try: | |
12 | 19 | from .component import Component, ComponentException |
13 | 20 | except: |
14 | 21 | from component import Component, ComponentException |
... | ... | @@ -35,6 +42,7 @@ class ComponentDetectorFocuserException(GuitastroException): |
35 | 42 | errors[ERR_FOCUSER_MUST_BE_STOPED] = "The focuser must be stoped before asking a motion" |
36 | 43 | errors[ERR_TARGET_OUTSIDE_LIMITS] = "The asked target is outside the limits" |
37 | 44 | |
45 | + | |
38 | 46 | class ComponentDetectorFocuser(ComponentDetectorFocuserException, Component, GuitastroTools): |
39 | 47 | """Component for Detector focuser |
40 | 48 | |
... | ... | @@ -43,6 +51,13 @@ class ComponentDetectorFocuser(ComponentDetectorFocuserException, Component, Gui |
43 | 51 | Usage : ComponentDetectorFocuser() |
44 | 52 | """ |
45 | 53 | |
54 | + # --- same as motoraxis.py | |
55 | + MOTION_STATE_UNKNOWN = -1 | |
56 | + MOTION_STATE_NOMOTION = 0 | |
57 | + MOTION_STATE_SLEWING = 1 | |
58 | + MOTION_STATE_DRIFTING = 2 | |
59 | + MOTION_STATE_MOVING = 3 | |
60 | + | |
46 | 61 | # ===================================================================== |
47 | 62 | # methods |
48 | 63 | # ===================================================================== |
... | ... | @@ -52,13 +67,66 @@ class ComponentDetectorFocuser(ComponentDetectorFocuserException, Component, Gui |
52 | 67 | """ |
53 | 68 | res = {} |
54 | 69 | res['category'] = 'DetectorFocuser' |
55 | - res['actions'] = {'GET', 'SET', 'DO'} | |
70 | + res['actions'] = ['GET', 'SET', 'DO'] | |
56 | 71 | res['DO'] = {} |
57 | 72 | res['DO']['GOTO'] = "Slew the focus at a position defined by the variable target" |
58 | 73 | res['DO']['COORD'] = "Get the current focus position" |
59 | 74 | res['DO']['STOP'] = "Stop the focus motion" |
60 | 75 | return res |
61 | 76 | |
77 | + def val2inc(self, val:float): | |
78 | + """Conversion coder from unit to increment | |
79 | + | |
80 | + Args: | |
81 | + | |
82 | + val: Value in unit defined by the variable 'unit'. | |
83 | + | |
84 | + Returns: | |
85 | + | |
86 | + Increments. | |
87 | + | |
88 | + """ | |
89 | + unit = self._get("unit") | |
90 | + inc = val | |
91 | + if unit=="phy": | |
92 | + phy = val | |
93 | + xnat2phy, xphy2nat = self._maxis.define_phy | |
94 | + rot, lin = xphy2nat(phy) | |
95 | + elif unit=="lin": | |
96 | + lin = val | |
97 | + rot = self._maxis.lin2rot(lin, self._maxis.SAVE_NONE) | |
98 | + elif unit=="rot": | |
99 | + rot = val | |
100 | + if unit=="rot" or unit=="lin" or unit=="phy": | |
101 | + inc = self._maxis.rot2inc(rot, self._maxis.SAVE_NONE) | |
102 | + return inc | |
103 | + | |
104 | + def inc2val(self, inc:float): | |
105 | + """Conversion coder from increment to unit | |
106 | + | |
107 | + Args: | |
108 | + | |
109 | + inc: Increment. | |
110 | + | |
111 | + Returns: | |
112 | + | |
113 | + Value in unit defined by the variable 'unit'. | |
114 | + | |
115 | + """ | |
116 | + unit = self._get("unit") | |
117 | + val = inc | |
118 | + if unit=="rot" or unit=="lin" or unit=="phy": | |
119 | + rot = self._maxis.inc2rot(inc, self._maxis.SAVE_AS_SIMU) | |
120 | + val = rot | |
121 | + if unit=="lin" or unit=="phy": | |
122 | + lin = self._maxis.rot2lin(rot, self._maxis.SAVE_AS_SIMU) | |
123 | + val = lin | |
124 | + if unit=="phy": | |
125 | + xnat2phy, xphy2nat = self._maxis.define_phy | |
126 | + self._maxis.physimu = xnat2phy(rot, lin) | |
127 | + val = self._maxis.physimu | |
128 | + return val | |
129 | + | |
62 | 130 | def _do(self, *args, **kwargs): |
63 | 131 | result = None |
64 | 132 | self._verify_nargs(1, *args) |
... | ... | @@ -68,43 +136,76 @@ class ComponentDetectorFocuser(ComponentDetectorFocuserException, Component, Gui |
68 | 136 | msg = f"{operation} not found amongst {operations}" |
69 | 137 | raise ComponentException(ComponentException.ERR_OPERATION_NOT_FOUND, msg) |
70 | 138 | # --- |
71 | - motion = self._get("motion") | |
139 | + #motion = self._get("motion") | |
140 | + #motion_simu = self.database.query("motion_simu") | |
72 | 141 | if operation == "GOTO": |
73 | - if motion == "stoped": | |
74 | - target = float(self._param["target"]) | |
75 | - lim_inf = self._mount_params["LIM_INF"] | |
76 | - lim_sup = self._mount_params["LIM_SUP"] | |
77 | - if target<lim_inf or target>lim_sup: | |
78 | - texte = f"Operation {operation} cannot be done because the target asked is {target} which is outside the limits {lim_inf} and {lim_sup}" | |
79 | - raise ComponentDetectorFocuserException(ComponentDetectorFocuserException.ERR_TARGET_OUTSIDE_LIMITS, texte) | |
80 | - # --- make the simulation | |
81 | - # TODO | |
82 | - self.log = f"start simulation of {operation}" | |
83 | - # --- call the real | |
84 | - self._my_do(*args, **kwargs) | |
85 | - # --- update the motion | |
86 | - self._set("motion", "slewing") | |
87 | - else: | |
88 | - # raise an error because the mount must be stoped before | |
89 | - msg = f"Operation {operation} cannot be done because the focuser is {motion}" | |
90 | - raise ComponentDetectorFocuserException(ComponentDetectorFocuserException.ERR_FOCUSER_MUST_BE_STOPED, msg) | |
142 | + target_raw = self.database.query("target") | |
143 | + target = float(target_raw) | |
144 | + inc = self.val2inc(target) | |
145 | + lim_inf = self._mount_params["LIM_INF"] | |
146 | + lim_sup = self._mount_params["LIM_SUP"] | |
147 | + if inc<lim_inf or inc>lim_sup: | |
148 | + texte = f"Operation {operation} cannot be done because the target asked is {inc} which is outside the limits {lim_inf} and {lim_sup}" | |
149 | + raise ComponentDetectorFocuserException(ComponentDetectorFocuserException.ERR_TARGET_OUTSIDE_LIMITS, texte) | |
150 | + # --- make the simulation | |
151 | + self.log = f"start simulation of {operation}" | |
152 | + velocity_raw = self.database.query("speed_slew") | |
153 | + velocity = float(velocity_raw) | |
154 | + self._maxis.simu_motion_start("SLEW", position=inc, velocity=velocity, frame='inc', drift=0) | |
155 | + self.database.query("motion_simu", self.MOTION_STATE_SLEWING) | |
156 | + # --- call the real | |
157 | + if self.real: | |
158 | + result = self._my_do(*args, **kwargs) | |
91 | 159 | elif operation == "STOP": |
92 | 160 | # --- make the simulation |
93 | - # TODO | |
94 | 161 | self.log = f"start simulation of {operation}" |
95 | - # --- update the motion | |
96 | - self._set("motion", "stoped") | |
162 | + self._maxis.simu_motion_stop() | |
163 | + self.database.query("motion_simu", self.MOTION_STATE_NOMOTION) | |
164 | + # --- call the real | |
165 | + if self.real: | |
166 | + result = self._my_do(*args, **kwargs) | |
97 | 167 | elif operation == "COORD": |
98 | - # --- make the simulation | |
99 | - # TODO | |
100 | 168 | self.log = f"start simulation of {operation}" |
101 | - result = 0 | |
169 | + # --- make the simulation | |
170 | + inc = self._maxis.simu_update_inc() | |
171 | + result = self.inc2val(inc) | |
172 | + # --- call the real | |
173 | + if self.real: | |
174 | + result = self._my_do(*args, **kwargs) | |
175 | + #self._maxis.synchro_simu2real() | |
176 | + else: | |
177 | + # --- only call the real method | |
178 | + self.log = f"start operation {operation}" | |
179 | + if self.real: | |
180 | + result = self._my_do(*args, **kwargs) | |
102 | 181 | return self._fresult(result) |
103 | 182 | |
104 | 183 | def _my_do(self, *args, **kwargs): |
105 | 184 | value = None |
106 | 185 | return value |
107 | 186 | |
187 | + def _my_get(self, *args, **kwargs): | |
188 | + key = args[0] | |
189 | + if key == "motion_simu": | |
190 | + self._maxis.simu_update_inc() | |
191 | + motion_simu = self._maxis.motion_state_simu | |
192 | + self._queue.put(f"motion_simu = {motion_simu}") | |
193 | + return motion_simu | |
194 | + return None | |
195 | + | |
196 | + def nat2phy(self, rot, lin, **kwargs): | |
197 | + # For Motoraxis | |
198 | + # Arguments are imposed | |
199 | + phy = lin | |
200 | + return phy | |
201 | + | |
202 | + def phy2nat(self, phy, **kwargs): | |
203 | + # For Motoraxis | |
204 | + # Arguments are imposed | |
205 | + lin = phy | |
206 | + rot = lin * 360 / self._mm_per_motor_rev | |
207 | + return rot, lin | |
208 | + | |
108 | 209 | def init(self, *args, **kwargs): |
109 | 210 | # --- Dico of optional parameters for all axis_types |
110 | 211 | param_optionals = {} |
... | ... | @@ -118,6 +219,8 @@ class ComponentDetectorFocuser(ComponentDetectorFocuserException, Component, Gui |
118 | 219 | param_optionals["LIM_SUP"] = (float,100000) |
119 | 220 | param_optionals["LOGO_FILE"] = (str,"") |
120 | 221 | param_optionals["CLIENT_DATA"] = (any,None) |
222 | + param_optionals["INC_PER_MOTOR_REV"] = (float, 1000.0) | |
223 | + param_optionals["MM_PER_MOTOR_REV"] = (float, 2.0) | |
121 | 224 | # --- Dico of axis_types and their parameters |
122 | 225 | mount_types = {} |
123 | 226 | mount_types["Z"]= {"MANDATORY" : {"NAME":[str,"Unknown"]}, "OPTIONAL" : {"LABEL":[str,"Detector focuser"]} } |
... | ... | @@ -135,11 +238,26 @@ class ComponentDetectorFocuser(ComponentDetectorFocuserException, Component, Gui |
135 | 238 | self.siteobs = self._mount_params["SITE"] |
136 | 239 | else: |
137 | 240 | self.siteobs = Home(self._mount_params["SITE"]) |
241 | + # --- Update the database using the queue | |
242 | + param = {} | |
243 | + param["motion_simu"] = self.MOTION_STATE_UNKNOWN | |
244 | + param["motion_real"] = self.MOTION_STATE_UNKNOWN | |
245 | + param["unit"] = "inc" # inc, rot, lin, phy | |
246 | + param["target"] = 0 # unit | |
247 | + param["speed_slew"] = 100 # unit/s | |
248 | + self._queue.put(param) | |
249 | + # --- | |
250 | + self._inc_per_motor_rev = self._mount_params["INC_PER_MOTOR_REV"] | |
251 | + self._mm_per_motor_rev = self._mount_params["MM_PER_MOTOR_REV"] | |
138 | 252 | # --- |
139 | - self._set("motion", "stoped") | |
140 | - self._set("target", 0) | |
253 | + self._my_init1(*args, **kwargs) | |
254 | + # --- Motor simulator | |
255 | + self._maxis = Motoraxis("LIN", name = "Focuser", inc_per_motor_rev=self._inc_per_motor_rev, inc0=0, senseinc=1, real=False, mm_per_motor_rev=self._mm_per_motor_rev) | |
256 | + self._maxis.define_phy = [self.nat2phy, self.phy2nat] | |
257 | + self._maxis.phy_unit = "inch" | |
141 | 258 | # --- |
142 | - self._my_init(*args, **kwargs) | |
259 | + self._my_init2(*args, **kwargs) | |
260 | + | |
143 | 261 | |
144 | 262 | # ##################################################################### |
145 | 263 | # ##################################################################### |
... | ... | @@ -163,7 +281,18 @@ if __name__ == "__main__": |
163 | 281 | """ |
164 | 282 | Basic example |
165 | 283 | """ |
284 | + import time | |
166 | 285 | comp = ComponentDetectorFocuser("Z", name="test") |
167 | - comp.init("Z", name="test") | |
286 | + inc_per_motor_rev = 1000.0 | |
287 | + mm_per_motor_rev = 2.0 | |
288 | + comp.init("Z", name="test", inc_per_motor_rev=inc_per_motor_rev, mm_per_motor_rev=mm_per_motor_rev) | |
168 | 289 | comp.verbose = 1 |
169 | - res = comp.command("DO", "COORD") | |
290 | + comp.command("SET", "unit", "inc") # inc | |
291 | + comp.command("SET", "target", 1500) # inc | |
292 | + comp.command("SET", "speed_slew", 700) # inc/s | |
293 | + comp.command("DO", "GOTO") | |
294 | + for k in range(4): | |
295 | + z, date = comp.command("DO", "COORD") | |
296 | + print(f"Focus={z:.2f}") | |
297 | + time.sleep(1) | |
298 | + # comp._maxis.disp() | ... | ... |
src/guitastro/component_mount_pointing.py
1 | +import numpy as np | |
2 | +import matplotlib.pyplot as plt | |
3 | +import math | |
4 | + | |
5 | +try: | |
6 | + from .dates import Date | |
7 | +except: | |
8 | + from dates import Date | |
9 | + | |
10 | +try: | |
11 | + from .filenames import FileNames | |
12 | +except: | |
13 | + from filenames import FileNames | |
14 | + | |
1 | 15 | try: |
2 | 16 | from .home import Home |
3 | 17 | except: |
... | ... | @@ -9,6 +23,16 @@ except: |
9 | 23 | from siteobs import Siteobs |
10 | 24 | |
11 | 25 | try: |
26 | + from .mountaxis import Mountaxis | |
27 | +except: | |
28 | + from mountaxis import Mountaxis | |
29 | + | |
30 | +try: | |
31 | + from .mountmodpoi import Mountmodpoi | |
32 | +except: | |
33 | + from mountmodpoi import Mountmodpoi | |
34 | + | |
35 | +try: | |
12 | 36 | from .ephemeris import Ephemeris |
13 | 37 | except: |
14 | 38 | from ephemeris import Ephemeris |
... | ... | @@ -19,9 +43,9 @@ except: |
19 | 43 | from component import Component, ComponentException |
20 | 44 | |
21 | 45 | try: |
22 | - from .guitastrotools import GuitastroTools, GuitastroException | |
46 | + from .guitastrotools import GuitastroException, GuitastroTools | |
23 | 47 | except: |
24 | - from guitastrotools import GuitastroTools, GuitastroException | |
48 | + from guitastrotools import GuitastroException, GuitastroTools | |
25 | 49 | |
26 | 50 | # ##################################################################### |
27 | 51 | # ##################################################################### |
... | ... | @@ -33,10 +57,23 @@ except: |
33 | 57 | |
34 | 58 | class ComponentMountPointingException(GuitastroException): |
35 | 59 | |
36 | - ERR_MOUNT_MUST_BE_STOPED = 0 | |
60 | + ERR_POINTING_MUST_BE_STOPED = 0 | |
61 | + ERR_TARGET_OUTSIDE_LIMITS = 1 | |
62 | + ERR_MOUNT_TYPE_NOT_SUPPORTED = 2 | |
63 | + ERR_PIERSIDE_MUST_BE_CHOSEN = 3 | |
64 | + ERR_SPEED_SLEW_LEN = 4 | |
65 | + ERR_AXIS_SYMBOL_NOT_FOUND = 5 | |
66 | + ERR_SIMU_KEY_NOT_FOUND = 6 | |
67 | + | |
68 | + errors = [""]*7 | |
69 | + errors[ERR_POINTING_MUST_BE_STOPED] = "The pointing must be stoped before asking a motion" | |
70 | + errors[ERR_TARGET_OUTSIDE_LIMITS] = "The asked target is outside the limits" | |
71 | + errors[ERR_MOUNT_TYPE_NOT_SUPPORTED] = "The asked mount type is not supported" | |
72 | + errors[ERR_PIERSIDE_MUST_BE_CHOSEN] = "Pier side must be chosen before calling the method (PIERSIDE_AUTO is not autorized)" | |
73 | + errors[ERR_SPEED_SLEW_LEN] = "speed_slew must have the same number of elements than the number of mount axes" | |
74 | + errors[ERR_AXIS_SYMBOL_NOT_FOUND] = "Axis symbol not found" | |
75 | + errors[ERR_SIMU_KEY_NOT_FOUND] = "Key 'simu' not found" | |
37 | 76 | |
38 | - errors = [""]*1 | |
39 | - errors[ERR_MOUNT_MUST_BE_STOPED] = "The mount must be stoped before asking a motion" | |
40 | 77 | |
41 | 78 | class ComponentMountPointing(ComponentMountPointingException, Component, GuitastroTools): |
42 | 79 | """Component for Mount pointing |
... | ... | @@ -46,70 +83,30 @@ class ComponentMountPointing(ComponentMountPointingException, Component, Guitast |
46 | 83 | Usage : ComponentMountPointing() |
47 | 84 | """ |
48 | 85 | |
49 | - # ===================================================================== | |
50 | - # methods | |
51 | - # ===================================================================== | |
52 | - | |
53 | - def prop(self): | |
54 | - """Component property concrete method | |
55 | - """ | |
56 | - res = {} | |
57 | - res['category'] = 'MountPointing' | |
58 | - res['actions'] = {'GET', 'SET', 'DO'} | |
59 | - res['DO'] = {} | |
60 | - res['DO']['RADEC_GOTO'] = "Slew the mount at a position defined by the variable target" | |
61 | - res['DO']['RADEC_COORD'] = "Get the current mount RA.Dec position" | |
62 | - res['DO']['STOP'] = "Stop the mount motions" | |
63 | - return res | |
86 | + # --- same as mountaxis.py | |
87 | + MOTION_STATE_UNKNOWN = -1 | |
88 | + MOTION_STATE_NOMOTION = 0 | |
89 | + MOTION_STATE_SLEWING = 1 | |
90 | + MOTION_STATE_DRIFTING = 2 | |
91 | + MOTION_STATE_MOVING = 3 | |
64 | 92 | |
65 | - def _do(self, *args, **kwargs): | |
66 | - result = None | |
67 | - self._verify_nargs(1, *args) | |
68 | - operation = args[0].upper() | |
69 | - operations = list(self.prop()['DO'].keys()) | |
70 | - if operation not in operations: | |
71 | - msg = f"{operation} not found amongst {operations}" | |
72 | - raise ComponentException(ComponentException.ERR_OPERATION_NOT_FOUND, msg) | |
73 | - # --- | |
74 | - motion = self._get("motion") | |
75 | - if operation == "RADEC_GOTO": | |
76 | - if motion == "stoped": | |
77 | - target = self._param["target"] | |
78 | - #print(f"Target={target}") | |
79 | - ra, dec, equinox, epoch, dra, ddec = self._eph.radec_speed(target, date="NOW", unit_ra="deg", unit_dec="deg") | |
80 | - # --- make the simulation | |
81 | - # TODO | |
82 | - self.log = f"start simulation of {operation}" | |
83 | - # --- call the real | |
84 | - kwargs = {} | |
85 | - kwargs['computed'] = (ra, dec, equinox, epoch, dra, ddec) | |
86 | - result = self._my_do(*args, **kwargs) | |
87 | - # --- update the motion | |
88 | - self._set("motion", "slewing") | |
89 | - else: | |
90 | - # raise an error because the mount must be stoped before | |
91 | - msg = f"Operation {operation} cannot be done because the focuser is {motion}" | |
92 | - raise ComponentMountPointingException(ComponentMountPointingException.ERR_MOUNT_MUST_BE_STOPED, msg) | |
93 | - elif operation == "STOP": | |
94 | - # --- make the simulation | |
95 | - self.log = f"start simulation of {operation}" | |
96 | - # TODO | |
97 | - # --- update the motion | |
98 | - self._set("motion", "stoped") | |
99 | - result = self._my_do(*args, **kwargs) | |
100 | - elif operation == "RADEC_COORD": | |
101 | - # --- make the simulation | |
102 | - # TODO | |
103 | - self.log = f"start simulation of {operation}" | |
104 | - result = self._my_do(*args, **kwargs) | |
105 | - result = "12h00m45.78s -06d55m23.2s" | |
106 | - return self._fresult(result) | |
93 | + # === constants for saving coords | |
94 | + SAVE_NONE = 0 | |
95 | + SAVE_AS_SIMU = 1 | |
96 | + SAVE_AS_REAL = 2 | |
97 | + SAVE_ALL = 3 | |
107 | 98 | |
108 | - def _my_do(self, *args, **kwargs): | |
109 | - value = None | |
110 | - return value | |
99 | + # ===================================================================== | |
100 | + # ===================================================================== | |
101 | + # General methods overriden | |
102 | + # ===================================================================== | |
103 | + # ===================================================================== | |
111 | 104 | |
112 | 105 | def init(self, *args, **kwargs): |
106 | + """ | |
107 | + Conversion from Uniform Python object into protocol language | |
108 | + Usage : ComponentMountPointing("HADEC", name="Test") | |
109 | + """ | |
113 | 110 | # --- Dico of optional parameters for all axis_types |
114 | 111 | param_optionals = {} |
115 | 112 | param_optionals["MODEL"] = (str, "") |
... | ... | @@ -126,6 +123,8 @@ class ComponentMountPointing(ComponentMountPointingException, Component, Guitast |
126 | 123 | param_optionals["LIMW_REVERSE"] = (float,-30) |
127 | 124 | param_optionals["LOGO_FILE"] = (str,"") |
128 | 125 | param_optionals["CLIENT_DATA"] = (any,None) |
126 | + param_optionals["PREFERENCE"] = (str,"BESTELEV") | |
127 | + param_optionals["DUSKELEV"] = (float,-7) | |
129 | 128 | # --- Dico of axis_types and their parameters |
130 | 129 | mount_types = {} |
131 | 130 | mount_types["HADEC"]= {"MANDATORY" : {"NAME":[str,"Unknown"]}, "OPTIONAL" : {"LABEL":[str,"Equatorial"]} } |
... | ... | @@ -141,25 +140,735 @@ class ComponentMountPointing(ComponentMountPointingException, Component, Guitast |
141 | 140 | self._model = self._mount_params["MODEL"] |
142 | 141 | self._manufacturer = self._mount_params["MANUFACTURER"] |
143 | 142 | self._serial_number= self._mount_params["SERIAL_NUMBER"] |
144 | - # === local | |
143 | + # === location and ephemeris | |
145 | 144 | if type(self._mount_params["SITE"])==Siteobs: |
146 | 145 | self.siteobs = self._mount_params["SITE"] |
147 | 146 | else: |
148 | 147 | self.siteobs = Home(self._mount_params["SITE"]) |
149 | - # --- init ephemeris | |
148 | + # === ephemeris | |
150 | 149 | self._eph = Ephemeris() |
150 | + self._eph.set_home(self.siteobs.home) | |
151 | + self._fn = FileNames() | |
152 | + self._fn.longitude(self.siteobs.longitude) | |
153 | + self._nightephem = None | |
154 | + # === Initial state real or simulation | |
155 | + real = self._mount_params["REAL"] | |
156 | + # --- | |
157 | + self._my_init1(*args, **kwargs) | |
158 | + # === Initialisation of axis list according the mount_type | |
159 | + self.axis = [] | |
160 | + if self._mount_type.find("HA")>=0: | |
161 | + current_axis = Mountaxis("HA", name = "Hour angle") | |
162 | + current_axis.update_inc0(0,0,current_axis.PIERSIDE_POS1) | |
163 | + self.axis.append(current_axis) | |
164 | + if self._mount_type.find("DEC")>=0: | |
165 | + current_axis = Mountaxis("DEC", name = "Declination") | |
166 | + current_axis.update_inc0(0,90,current_axis.PIERSIDE_POS1) | |
167 | + self.axis.append(current_axis) | |
168 | + if self._mount_type.find("AZ")>=0: | |
169 | + current_axis = Mountaxis("AZ", name = "Azimuth") | |
170 | + current_axis.update_inc0(0,0,current_axis.PIERSIDE_POS1) | |
171 | + self.axis.append(current_axis) | |
172 | + if self._mount_type.find("ELEV")>=0: | |
173 | + current_axis = Mountaxis("ELEV", name = "Elevation") | |
174 | + current_axis.update_inc0(0,90,current_axis.PIERSIDE_POS1) | |
175 | + self.axis.append(current_axis) | |
176 | + if self._mount_type.find("YAW")>=0: | |
177 | + current_axis = Mountaxis("YAW", name = "Yaw") # often fixed | |
178 | + current_axis.update_inc0(0,0,current_axis.PIERSIDE_POS1) | |
179 | + self.axis.append(current_axis) | |
180 | + if self._mount_type.find("ROLL")>=0: | |
181 | + current_axis = Mountaxis("ROLL", name = "Roll") | |
182 | + current_axis.update_inc0(0,0,current_axis.PIERSIDE_POS1) | |
183 | + self.axis.append(current_axis) | |
184 | + if self._mount_type.find("PITCH")>=0: | |
185 | + current_axis = Mountaxis("PITCH", name = "Pitch") | |
186 | + current_axis.update_inc0(0,90,current_axis.PIERSIDE_POS1) | |
187 | + self.axis.append(current_axis) | |
188 | + if self._mount_type.find("ROT")>=0: | |
189 | + current_axis = Mountaxis("ROT", name = "Rotator") | |
190 | + current_axis.update_inc0(0,0,current_axis.PIERSIDE_POS1) | |
191 | + self.axis.append(current_axis) | |
192 | + # === Default Setup | |
193 | + ratio_wheel_pulley = 1 ; # 5.25 | |
194 | + ratio_pulley_motor = 100.0 ; # harmonic reducer | |
195 | + inc_per_motor_rev = 1000.0 ; # IMC parameter. System Confg -> System Parameters - Distance/Revolution | |
196 | + for current_axis in self.axis: | |
197 | + current_axis.slewmax_deg_per_sec = 30 | |
198 | + current_axis.slew_deg_per_sec = 30 | |
199 | + current_axis.real = real | |
200 | + current_axis.latitude = self.siteobs.latitude | |
201 | + current_axis.ratio_wheel_pulley = ratio_wheel_pulley | |
202 | + current_axis.ratio_pulley_motor = ratio_pulley_motor | |
203 | + current_axis.inc_per_motor_rev = inc_per_motor_rev | |
204 | + # === | |
205 | + self.slewing_state = False | |
206 | + self.tracking_state = False | |
207 | + # === | |
208 | + if self._mount_type.find("AZ")>=0: | |
209 | + self.park_az = 0.0 | |
210 | + self.park_elev = 0.0 | |
211 | + else: | |
212 | + self.park_ha = 270.0 | |
213 | + self.park_dec = 90.0 | |
214 | + self.park_side = Mountaxis.PIERSIDE_POS1 | |
215 | + self.modpoi_regular = Mountmodpoi() | |
216 | + self.modpoi_regular.name = "For regular side" | |
217 | + self.modpoi_regular.latitude = self.siteobs.latitude | |
218 | + self.modpoi_regular.mount_type = self._mount_type | |
219 | + self.modpoi_flipped = Mountmodpoi() | |
220 | + self.modpoi_flipped.name = "For flipped side" | |
221 | + self.modpoi_flipped.latitude = self.siteobs.latitude | |
222 | + self.modpoi_flipped.mount_type = self._mount_type | |
223 | + self._apply_model = False | |
224 | + # === Client data (free for users) | |
225 | + self._client_data = self._mount_params["CLIENT_DATA"] | |
151 | 226 | # --- |
152 | - self._set("motion", "stoped") | |
153 | - self._set("target", "RADEC 12h 0d") | |
227 | + self._my_init2(*args, **kwargs) | |
228 | + # --- Default database | |
229 | + param = {} | |
230 | + param['speed_slew'] = [30]*len(self.axis) | |
231 | + self._queue.put(param) | |
232 | + | |
233 | + def prop(self): | |
234 | + """Component property concrete method | |
235 | + """ | |
236 | + res = {} | |
237 | + res['category'] = 'MountPointing' | |
238 | + res['actions'] = ['GET', 'SET', 'DO'] | |
239 | + res['DO'] = {} | |
240 | + res['DO']['RADEC_GOTO'] = "Slew the mount at a position defined by the variable target" | |
241 | + res['DO']['RADEC_COORD'] = "Get the current mount RA.Dec position" | |
242 | + res['DO']['STOP'] = "Stop the mount motions" | |
243 | + return res | |
244 | + | |
245 | + def _do_radec(self): | |
246 | + pass | |
247 | + | |
248 | + def _do(self, *args, **kwargs): | |
249 | + result = None | |
250 | + self._verify_nargs(1, *args) | |
251 | + operation = args[0].upper() | |
252 | + operations = list(self.prop()['DO'].keys()) | |
253 | + if operation not in operations: | |
254 | + msg = f"{operation} not found amongst {operations}" | |
255 | + raise ComponentException(ComponentException.ERR_OPERATION_NOT_FOUND, msg) | |
154 | 256 | # --- |
155 | - self._my_init(*args, **kwargs) | |
257 | + #motion = self._get("motion") | |
258 | + #motion_simu = self.database.query("motion_simu") | |
259 | + if operation == "RADEC_GOTO": | |
260 | + target = self.database.query("target") | |
261 | + cel = comp.target2cel(target) | |
262 | + pierside = Mountaxis.PIERSIDE_POS2 | |
263 | + rot, inc = comp.cel2inc(cel, pierside, comp.SAVE_AS_SIMU) | |
264 | + self.log = f"start simulation of {operation}" | |
265 | + speed_slew = self.database.query("speed_slew") | |
266 | + if len(speed_slew) < len(self.axis): | |
267 | + msg = f"speed_slew {speed_slew} must be a list of {len(self.axis)} elements" | |
268 | + raise ComponentException(ComponentException.ERR_SPEED_SLEW_LEN, msg) | |
269 | + k = 0 | |
270 | + for axis in self.axis: | |
271 | + velocity = speed_slew[0] # deg/s | |
272 | + velocity *= axis.inc_per_deg # inc/s | |
273 | + axis_symbol = axis.symbol | |
274 | + position = inc['simu'][axis_symbol] # inc | |
275 | + drift = inc['simu']['d'+axis_symbol] # inc/s | |
276 | + axis.simu_motion_start("SLEW", frame='inc', position=position, velocity=velocity, drift=drift) | |
277 | + self.database.query("motion_"+axis_symbol+"_simu", self.MOTION_STATE_SLEWING) | |
278 | + k += 1 | |
279 | + # --- call the real | |
280 | + if self.real: | |
281 | + result = self._my_do(*args, **kwargs) | |
282 | + elif operation == "STOP": | |
283 | + # --- make the simulation | |
284 | + self.log = f"start simulation of {operation}" | |
285 | + for axis in self.axis: | |
286 | + self.axis.simu_motion_stop() | |
287 | + self.database.query("motion_simu", self.MOTION_STATE_NOMOTION) | |
288 | + # --- call the real | |
289 | + if self.real: | |
290 | + result = self._my_do(*args, **kwargs) | |
291 | + elif operation == "RADEC_COORD": | |
292 | + self.log = f"start simulation of {operation}" | |
293 | + inc, rot, cel = comp.inc2cel(None, comp.SAVE_AS_SIMU) | |
294 | + ephem = comp.cel2astro(xcel) | |
295 | + result = ephem['ra_equinox'], ephem['dec_equinox'] | |
296 | + # --- call the real | |
297 | + if self.real: | |
298 | + result = self._my_do(*args, **kwargs) | |
299 | + #self._maxis.synchro_simu2real() | |
300 | + else: | |
301 | + # --- only call the real method | |
302 | + self.log = f"start operation {operation}" | |
303 | + if self.real: | |
304 | + result = self._my_do(*args, **kwargs) | |
305 | + return self._fresult(result) | |
306 | + | |
307 | + def _my_do(self, *args, **kwargs): | |
308 | + value = None | |
309 | + return value | |
310 | + | |
311 | + def _my_set(self, *args, **kwargs): | |
312 | + pass | |
313 | + | |
314 | + def _my_get(self, *args, **kwargs): | |
315 | + key = args[0] | |
316 | + if key == "motion_simu": | |
317 | + motion_simus = [] | |
318 | + for axis in self.axis: | |
319 | + self.axis.simu_update_inc() | |
320 | + motion_simu = self.axis.motion_state_simu | |
321 | + motion_simus.append(motion_simu) | |
322 | + self._queue.put(f"motion_simu = {motion_simus}") | |
323 | + return motion_simus | |
324 | + return None | |
325 | + | |
326 | + # ===================================================================== | |
327 | + # ===================================================================== | |
328 | + # Ephemeris methods | |
329 | + # ===================================================================== | |
330 | + # ===================================================================== | |
331 | + | |
332 | + def _night_ephem(self, target:str, date:Date="now")->dict: | |
333 | + # This method avoids to recompute many times the same ephemeris | |
334 | + # We add the refraction parameters | |
335 | + # nightephem = self._night_ephem("sun") | |
336 | + # dateephem = self._eph.date_ephem(nightephem) | |
337 | + night = self._fn.date2night(date) | |
338 | + rel_humidity = 0.6 | |
339 | + wavelength_nm = 600 | |
340 | + speed = True | |
341 | + return self._eph.night_ephem(target, night, None, None, siteobs=self.siteobs, preference=self._mount_params["PREFERENCE"], duskelev=self._mount_params["DUSKELEV"], rel_humidity=rel_humidity, wavelength_nm=wavelength_nm, speed=speed) | |
156 | 342 | |
157 | 343 | # ===================================================================== |
158 | 344 | # ===================================================================== |
159 | - # Special methods | |
345 | + # Coordinate system Target->Ephem->Astro->Cel->Rot->Inc methods | |
160 | 346 | # ===================================================================== |
161 | 347 | # ===================================================================== |
162 | 348 | |
349 | + def compute_pier_target(self, celb_target:float, pierside_start:int, imposed_side:int=Mountaxis.PIERSIDE_AUTO): | |
350 | + """ | |
351 | + Compute the predicted side of pier for the given ha,dec coordinates | |
352 | + | |
353 | + :param celb_target: Base angle target (unit is degrees). | |
354 | + :type celb_target: float | |
355 | + :param pierside_start: Current side | |
356 | + :type pierside_start: int | |
357 | + :param imposed_side: Side if it is imposed | |
358 | + :type imposed_side: int | |
359 | + | |
360 | + :returns: Pier side | |
361 | + :rtype: int | |
362 | + | |
363 | + * Pier side : Integer to indicate the back flip action: | |
364 | + | |
365 | + * PIERSIDE_POS1 (=1) pointing in normal position | |
366 | + * PIERSIDE_POS2 (=-1) pointing in back flip position | |
367 | + | |
368 | + """ | |
369 | + # --- compute the target pierside | |
370 | + if self.get_param("CAN_REVERSE")==True: | |
371 | + lim_side_east = self.get_param("LIME_REVERSE") ; # Tube west = PIERSIDE_POS1 = [-180 : lim_side_east] | |
372 | + lim_side_west = self.get_param("LIMW_REVERSE") ; # Tube east = PIERSIDE_POS2 = [lim_side_west : +180] | |
373 | + if imposed_side == Mountaxis.PIERSIDE_AUTO: | |
374 | + if celb_target>lim_side_west and celb_target<lim_side_east: | |
375 | + # --- the target position is in the both possibilitiy range | |
376 | + pierside_target = pierside_start | |
377 | + else: | |
378 | + if celb_target>lim_side_east: | |
379 | + # --- the target is after the limit of side=PIERSIDE_POS1 | |
380 | + pierside_target = Mountaxis.PIERSIDE_POS2 | |
381 | + else: | |
382 | + # --- the target is before the limit of side=PIERSIDE_POS2 | |
383 | + pierside_target = Mountaxis.PIERSIDE_POS1 | |
384 | + else: | |
385 | + pierside_target = imposed_side | |
386 | + else: | |
387 | + if pierside_start == Mountaxis.PIERSIDE_AUTO: | |
388 | + pierside_target = Mountaxis.PIERSIDE_POS1 | |
389 | + else: | |
390 | + pierside_target = pierside_start | |
391 | + return pierside_target | |
392 | + | |
393 | + def cel2astro(self, cel:dict)->dict: | |
394 | + jd = Date(cel['header']['date']).jd() | |
395 | + if self._mount_type == "HADEC" or self._mount_type == "HADECROT": | |
396 | + ha = cel['simu']['b'] | |
397 | + dec = cel['simu']['p'] | |
398 | + astro = self._eph.hadec2astro(ha, dec, jd) | |
399 | + elif self._mount_type == "AZELEV" or self._mount_type == "AZELEVROT": | |
400 | + az = cel['simu']['b'] | |
401 | + elev = cel['simu']['p'] | |
402 | + astro = self._eph.altaz2astro(az, elev, jd) | |
403 | + return astro | |
404 | + | |
405 | + | |
406 | + def target2cel(self, target:str, date:Date="now")->dict: | |
407 | + """Conversion from target to celestial mount coordinates (Cel). | |
408 | + | |
409 | + The complete conversion chain is: Target->Ephem->Astro->Cel->Rot->Inc | |
410 | + This method makes conversion Target->Ephem->Astro->Cel | |
411 | + | |
412 | + The Cel system is oriented as the mount (attribute _mount_type). | |
413 | + | |
414 | + Args: | |
415 | + | |
416 | + target: Target string to calculate ephemeris | |
417 | + date: Date of the pointing | |
418 | + | |
419 | + Returns: | |
420 | + | |
421 | + cel: Dictionary of celb, celp, celr, dcelb, dcelp, dcelr | |
422 | + | |
423 | + """ | |
424 | + # --- Get the ephemeris of the target | |
425 | + nightephem = self._night_ephem(target, date) | |
426 | + ephem = self._eph.date_ephem(nightephem, date) | |
427 | + # --- Init the output dict | |
428 | + cel = {} | |
429 | + cel['header'] = {} | |
430 | + for key, val in ephem.items(): | |
431 | + if isinstance(val, str): | |
432 | + cel['header'][key] = val | |
433 | + cel['header']['date'] = ephem['jd'] | |
434 | + cel['header']['mount_type'] = self._mount_type | |
435 | + # --- Extract the Celestial mount coordinates | |
436 | + if self._mount_type == "HADEC": | |
437 | + celb = ephem['ha']%360 | |
438 | + if celb>180: | |
439 | + celb -= 360 | |
440 | + cel['b'] = celb | |
441 | + cel['p'] = ephem['dec'] | |
442 | + cel['r'] = 0 | |
443 | + cel['db'] = ephem['dha'] | |
444 | + cel['dp'] = ephem['ddec'] | |
445 | + cel['dr'] = 0 | |
446 | + elif self._mount_type == "AZELEV": | |
447 | + celb = ephem['az'] | |
448 | + if celb>180: | |
449 | + celb -= 360 | |
450 | + cel['b'] = celb | |
451 | + cel['p'] = ephem['alz'] | |
452 | + cel['r'] = ephem['parallactic'] | |
453 | + cel['db'] = ephem['daz'] | |
454 | + cel['dp'] = ephem['dalt'] | |
455 | + cel['dr'] = ephem['parallactic'] | |
456 | + else: | |
457 | + msg = f"The method target2cel does not implement {self._mount_type} system" | |
458 | + raise ComponentMountPointingException(ComponentMountPointingException.ERR_MOUNT_TYPE_NOT_SUPPORTED, msg) | |
459 | + return cel | |
460 | + | |
461 | + def _select_axis(self, symbol:str)->Mountaxis: | |
462 | + """Return the Mountaxis object selected from its rotation symbol | |
463 | + | |
464 | + Args: | |
465 | + | |
466 | + symbol: Rotation symbol must amongst 'b', 'p', 'r' | |
467 | + | |
468 | + Returns: | |
469 | + | |
470 | + Mountaxis object corresponding to the input symbol | |
471 | + """ | |
472 | + for axis in self.axis: | |
473 | + if axis == None: | |
474 | + continue | |
475 | + if symbol == axis.symbol: | |
476 | + return axis | |
477 | + | |
478 | + def cel2inc(self, cel:dict, pierside:int, save:int)->tuple: | |
479 | + """Conversion from celestial mount coordinates (Cel) to rotation coordinates (Rot). | |
480 | + | |
481 | + The complete conversion chain is: Target->Ephem->Astro->Cel->Rot->Inc | |
482 | + This method makes conversion Cel->Rot | |
483 | + | |
484 | + The Rot system is cardinaly oriented to avoid singularities. | |
485 | + | |
486 | + Args: | |
487 | + | |
488 | + cel: Celestial mount coordinates. Dictionnary composed by at less: | |
489 | + | |
490 | + celb: Celestial mount coordinate of the basis axis (HA, AZ, etc.) | |
491 | + celp: Celestial mount coordinate of the polar axis (DEC, ELEV, etc.) | |
492 | + celr: Celestial mount coordinate of the rotator axis (PARALLACTIC, etc.) | |
493 | + dcelb: Velocity of celb (deg/s) | |
494 | + dcelp: Velocity of celp (deg/s) | |
495 | + dcelr: Velocity of celr (deg/s) | |
496 | + | |
497 | + pierside: Pier side can be Mountaxis().PIERSIDE_AUTO or Mountaxis().PIERSIDE_POS1 or Mountaxis().PIERSIDE_POS2. | |
498 | + save: SAVE_NONE or SAVE_AS_SIMU or SAVE_AS_REAL or SAVE_ALL. | |
499 | + | |
500 | + | |
501 | + Returns: | |
502 | + | |
503 | + rotb, rotp, rotr, drotb, drotp, drotr | |
504 | + | |
505 | + """ | |
506 | + if pierside == Mountaxis.PIERSIDE_AUTO: | |
507 | + raise ComponentMountPointingException(ComponentMountPointingException.ERR_PIERSIDE_MUST_BE_CHOSEN) | |
508 | + # --- rot dict | |
509 | + rot = {} | |
510 | + rot['header'] = cel['header'] | |
511 | + rot['simu'] = {} | |
512 | + rot['real'] = {} | |
513 | + # --- inc dict | |
514 | + inc = {} | |
515 | + inc['header'] = cel['header'] | |
516 | + inc['simu'] = {} | |
517 | + inc['real'] = {} | |
518 | + # --- save for simulations | |
519 | + if save==self.SAVE_ALL or save==self.SAVE_AS_SIMU: | |
520 | + savesimu = self.SAVE_AS_SIMU | |
521 | + else: | |
522 | + savesimu = self.SAVE_NONE | |
523 | + # --- save for real | |
524 | + if save==self.SAVE_ALL or save==self.SAVE_AS_REAL: | |
525 | + savereal = self.SAVE_AS_REAL | |
526 | + else: | |
527 | + savereal = self.SAVE_NONE | |
528 | + # --- loop over the mount axes | |
529 | + for axis in self.axis: | |
530 | + axis_symbol = axis.symbol | |
531 | + # --- update for simulations | |
532 | + rotsimu, drotsimu = axis.ang2rot(cel[axis_symbol], cel['d'+axis_symbol], pierside, savesimu) | |
533 | + incsimu, dincsimu = axis.rot2inc(rotsimu, drotsimu, savesimu) | |
534 | + rot['simu'][axis_symbol] = rotsimu | |
535 | + inc['simu'][axis_symbol] = incsimu | |
536 | + rot['simu']['d'+axis_symbol] = drotsimu | |
537 | + inc['simu']['d'+axis_symbol] = dincsimu | |
538 | + rot['simu']['pierside'] = pierside | |
539 | + # --- update for real | |
540 | + rotreal, drotreal = axis.ang2rot(cel[axis_symbol], cel['d'+axis_symbol], pierside, savereal) | |
541 | + increal, dincreal = axis.rot2inc(rotsimu, drotreal, savereal) | |
542 | + rot['real'][axis_symbol] = rotreal | |
543 | + inc['real'][axis_symbol] = increal | |
544 | + rot['real']['d'+axis_symbol] = drotreal | |
545 | + inc['real']['d'+axis_symbol] = dincreal | |
546 | + rot['real']['pierside'] = pierside | |
547 | + # --- | |
548 | + return rot, inc | |
549 | + | |
550 | + def _my_read_inc(self, inc:dict)->dict: | |
551 | + """Read increments of encoders | |
552 | + | |
553 | + This method must be overriden | |
554 | + """ | |
555 | + for axis in self.axis: | |
556 | + axis_symbol = axis.symbol | |
557 | + inc['real'][axis_symbol] = inc['simu'][axis_symbol] | |
558 | + return inc | |
559 | + | |
560 | + def read_inc(self, incsimu:dict=None)->dict: | |
561 | + """Read increments of encoders | |
562 | + """ | |
563 | + inc = {} | |
564 | + inc['header'] = {} | |
565 | + inc['header']['jd'] = Date("now").jd() | |
566 | + inc['simu'] = {} | |
567 | + inc['real'] = {} | |
568 | + if isinstance(incsimu, dict): | |
569 | + # --- Case of manual input | |
570 | + if 'simu' in incsimu.keys(): | |
571 | + incsimusimu = incsimu['simu'] | |
572 | + keys = incsimusimu.keys() | |
573 | + for axis in self.axis: | |
574 | + axis_symbol = axis.symbol | |
575 | + if axis_symbol in keys: | |
576 | + inc['simu'][axis_symbol] = incsimu['simu'][axis_symbol] | |
577 | + else: | |
578 | + msg = f"{axis_symbol} not found in the input dictionary incsimu['simu'] (keys are {keys})" | |
579 | + raise ComponentMountPointingException(ComponentMountPointingException.ERR_AXIS_SYMBOL_NOT_FOUND, msg) | |
580 | + else: | |
581 | + msg = f"The dict incsimu has not key 'simu' (keys are {incsimu.keys()})" | |
582 | + raise ComponentMountPointingException(ComponentMountPointingException.ERR_SIMU_KEY_NOT_FOUND, msg) | |
583 | + for axis in self.axis: | |
584 | + axis_symbol = axis.symbol | |
585 | + inc['real'][axis_symbol] = inc['simu'][axis_symbol] | |
586 | + return inc | |
587 | + # --- Case of simulation | |
588 | + for axis in self.axis: | |
589 | + axis_symbol = axis.symbol | |
590 | + inc['simu'][axis_symbol] = axis.simu_update_inc() | |
591 | + # --- Case of real | |
592 | + inc = self._my_read_inc(inc) | |
593 | + return inc | |
594 | + | |
595 | + def symbol2axis(self, symbol:str)->object: | |
596 | + """Return the axis object from the symbol | |
597 | + """ | |
598 | + for axis in self.axis: | |
599 | + if symbol == axis.symbol: | |
600 | + return axis | |
601 | + | |
602 | + def axes_first_p(self)->list: | |
603 | + """Return the axis objects list with symbol 'p' first | |
604 | + | |
605 | + This is useful to determine the pierside value | |
606 | + """ | |
607 | + axiss = [] | |
608 | + axiss.append(self.symbol2axis('p')) | |
609 | + for axis in self.axis: | |
610 | + if axis.symbol != 'p': | |
611 | + axiss.append(axis) | |
612 | + return axiss | |
613 | + | |
614 | + def inc2cel(self, incsimu:dict=None, save:int=Mountaxis.SAVE_NONE)->tuple: | |
615 | + """Conversion from reading increments to rotation and celestial coordinates (Rot, Cel). | |
616 | + | |
617 | + The complete conversion chain is: Inc->Rot->Cel-Astro | |
618 | + This method makes conversion Inc->Cel | |
619 | + | |
620 | + The Rot system is cardinaly oriented to avoid singularities. | |
621 | + | |
622 | + """ | |
623 | + inc = self.read_inc(incsimu) | |
624 | + # --- rot dict | |
625 | + rot = {} | |
626 | + rot['header'] = {} | |
627 | + rot['header']['jd'] = inc['header']['jd'] | |
628 | + rot['simu'] = {} | |
629 | + rot['real'] = {} | |
630 | + # --- cel dict | |
631 | + cel = {} | |
632 | + cel['header'] = {} | |
633 | + cel['header']['jd'] = inc['header']['jd'] | |
634 | + cel['simu'] = {} | |
635 | + cel['real'] = {} | |
636 | + # --- save for simulations | |
637 | + if save==self.SAVE_ALL or save==self.SAVE_AS_SIMU: | |
638 | + savesimu = self.SAVE_AS_SIMU | |
639 | + else: | |
640 | + savesimu = self.SAVE_NONE | |
641 | + # --- save for real | |
642 | + if save==self.SAVE_ALL or save==self.SAVE_AS_REAL: | |
643 | + savereal = self.SAVE_AS_REAL | |
644 | + else: | |
645 | + savereal = self.SAVE_NONE | |
646 | + # --- loop over the mount axes | |
647 | + # --- First axe must be 'p' to determine the pierside | |
648 | + for axis in self.axes_first_p(): | |
649 | + axis_symbol = axis.symbol | |
650 | + # --- update for simulations | |
651 | + rotsimu, pierside = axis.inc2rot(inc['simu'][axis_symbol], savesimu) | |
652 | + if axis_symbol == 'p': | |
653 | + piersidesimu = pierside | |
654 | + celsimu = axis.rot2cel(rotsimu, piersidesimu, savesimu) | |
655 | + rot['simu'][axis_symbol] = rotsimu | |
656 | + cel['simu'][axis_symbol] = celsimu | |
657 | + # --- update for real | |
658 | + rotreal, pierside = axis.inc2rot(inc['real'][axis_symbol], savereal) | |
659 | + if axis_symbol == 'p': | |
660 | + piersidereal = pierside | |
661 | + celreal = axis.rot2cel(rotreal, piersidereal, savereal) | |
662 | + rot['real'][axis_symbol] = rotreal | |
663 | + cel['real'][axis_symbol] = celreal | |
664 | + rot['simu']['pierside'] = piersidesimu | |
665 | + rot['real']['pierside'] = piersidereal | |
666 | + return inc, rot, cel | |
667 | + | |
668 | + # ===================================================================== | |
669 | + # ===================================================================== | |
670 | + # Method which plots the rot coordinate system | |
671 | + # ===================================================================== | |
672 | + # ===================================================================== | |
673 | + | |
674 | + def plot_rot(self, lati, azim, elev, rotb, rotp, outfile=""): | |
675 | + """ | |
676 | + Vizualize the rotation angles of the mount according the local coordinates | |
677 | + # --- Siteobs latitude | |
678 | + lati (deg) : Siteobs latitude | |
679 | + # --- Observer view | |
680 | + elev = 15 # turn around the X axis | |
681 | + azim = 140 # turn around the Z axis (azim=0 means W foreground, azim=90 means N foreground) | |
682 | + # --- rob, rotp | |
683 | + """ | |
684 | + if lati=="": | |
685 | + lati = self.siteobs.latitude | |
686 | + if lati>=0: | |
687 | + latitude = lati | |
688 | + cards="SNEW" | |
689 | + else: | |
690 | + latitude = -lati | |
691 | + cards="NSWE" | |
692 | + toplots = [] | |
693 | + # --- cartesian frame | |
694 | + options = {"linewisth":0.5} | |
695 | + toplots.append(["text",[0, 0, 0],"o",'k',{}]) | |
696 | + toplots.append(["line",[0, 0, 0],[1, 0, 0],'k',options]) | |
697 | + toplots.append(["line",[0, 0, 0],[-1, 0, 0],'k',options]) | |
698 | + toplots.append(["text",[1, 0, 0],cards[0],'k',{}]) | |
699 | + toplots.append(["text",[-1, 0, 0],cards[1],'k',{}]) | |
700 | + toplots.append(["text",[0, 1, 0],cards[2],'k',{}]) | |
701 | + toplots.append(["text",[0, -1, 0],cards[3],'k',{}]) | |
702 | + toplots.append(["line",[0, 0, 0],[0, 1, 0],'k',options]) | |
703 | + toplots.append(["line",[0, 0, 0],[0, -1, 0],'k',options]) | |
704 | + toplots.append(["line",[0, 0, 0],[0, 0, 1],'k',options]) | |
705 | + toplots.append(["line",[0, 0, 0],[0, 0, -1],'k',options]) | |
706 | + toplots.append(["text",[0, 0, 1.1],"zenith",'k',options]) | |
707 | + # --- great circle tangeant to the projection plane | |
708 | + toplots.append(["circle",1,[[90,0,0], [-elev,0,0], [0,0,-azim]],0,360,'k',options]) | |
709 | + #toplots.append(["circle",1,[[90,-elev,-azim]],0,360,'k',{"linewisth":1}]) | |
710 | + # --- local horizon | |
711 | + toplots.append(["circle",1,[[0,0,0]],0,360,'k',{"linewisth":0.5}]) | |
712 | + # --- local meridian | |
713 | + toplots.append(["circle",1,[[90,0,0]],0,360,'k',{"linewisth":0.5}]) | |
714 | + # --- local equator | |
715 | + rotxyzs = [[0, latitude, 0]] | |
716 | + options = {"linewisth":2} | |
717 | + color = 'r' | |
718 | + toplots.append(["circle",1,rotxyzs,0,360,color,options]) | |
719 | + toplots.append(["linefrom0",1,rotxyzs,color,options]) | |
720 | + toplots.append(["linefrom0",-1,rotxyzs,color,options]) | |
721 | + toplots.append(["textfrom0",1.05,rotxyzs,"x",color,options]) | |
722 | + rotxyzs = [[0, 0, 90]] | |
723 | + toplots.append(["linefrom0",1,rotxyzs,color,options]) | |
724 | + toplots.append(["linefrom0",-1,rotxyzs,color,options]) | |
725 | + toplots.append(["textfrom0",1.15,rotxyzs,"y",color,options]) | |
726 | + rotxyzs = [[0, latitude+90, 0]] | |
727 | + toplots.append(["linefrom0",1,rotxyzs,color,options]) | |
728 | + toplots.append(["linefrom0",-1,rotxyzs,color,options]) | |
729 | + toplots.append(["textfrom0",1.05,rotxyzs,"z = visible pole ({})".format(cards[1]),color,options]) | |
730 | + # --- pointing Rotp | |
731 | + rotxyzs = [[90,0,0], [0,0,rotb], [0, latitude, 0]] | |
732 | + options = {"linewisth":0.5} | |
733 | + color = 'r' | |
734 | + toplots.append(["circle",1,rotxyzs,0,360,color,options]) | |
735 | + options = {"linewisth":2} | |
736 | + color = 'g' | |
737 | + toplots.append(["circle",1,rotxyzs,90,90-rotp,color,options]) | |
738 | + rotxyzs = [[0,90-rotp,0], [0,0,rotb], [0, latitude, 0]] | |
739 | + options = {"linewisth":1} | |
740 | + toplots.append(["linefrom0",1,rotxyzs,color,options]) | |
741 | + toplots.append(["textfrom0",1.1,rotxyzs,"rotp",color,options]) | |
742 | + rotxyzs = [[0,90,0], [0,0,rotb], [0, latitude, 0]] | |
743 | + toplots.append(["linefrom0",1,rotxyzs,color,options]) | |
744 | + # --- pointing Rotb | |
745 | + rotxyzs = [[0,0,rotb], [0, latitude, 0]] | |
746 | + options = {"linewisth":2} | |
747 | + color = 'b' | |
748 | + toplots.append(["circle",1,rotxyzs,0,-rotb,color,options]) | |
749 | + options = {"linewisth":1} | |
750 | + toplots.append(["linefrom0",1,rotxyzs,color,options]) | |
751 | + toplots.append(["textfrom0",1.3,rotxyzs,"rotb",color,options]) | |
752 | + rotxyzs = [[0, latitude, 0]] | |
753 | + options = {"linewisth":1} | |
754 | + toplots.append(["linefrom0",1,rotxyzs,color,options]) | |
755 | + # === | |
756 | + fig = plt.figure() | |
757 | + #ax = fig.add_subplot(1,1,1,projection='3d') | |
758 | + #ax.view_init(elev=elev, azim=azim) | |
759 | + ax = fig.add_subplot(1,1,1) | |
760 | + for toplot in toplots: | |
761 | + ptype = toplot[0] | |
762 | + if ptype=="line": | |
763 | + dummy, xyz1, xyz2, color, options = toplot | |
764 | + xyz0s = [] | |
765 | + xyz0 = np.array(xyz1) | |
766 | + xyz0s.append(xyz0) | |
767 | + xyz0 = np.array(xyz2) | |
768 | + xyz0s.append(xyz0) | |
769 | + na = len(xyz0s) | |
770 | + elif ptype=="text": | |
771 | + dummy, xyz, text, color, options = toplot | |
772 | + xyz0s = [] | |
773 | + xyz0 = np.array(xyz) | |
774 | + xyz0s.append(xyz0) | |
775 | + na = len(xyz0s) | |
776 | + elif ptype=="linefrom0": | |
777 | + dummy, length, rotxyzs, color, options = toplot | |
778 | + xyz0s = [] | |
779 | + xyz0 = np.array([0, 0, 0]) | |
780 | + xyz0s.append(xyz0) | |
781 | + xyz0 = np.array([length, 0 ,0]) | |
782 | + xyz0s.append(xyz0) | |
783 | + na = len(xyz0s) | |
784 | + elif ptype=="textfrom0": | |
785 | + dummy, length, rotxyzs, text, color, options = toplot | |
786 | + xyz0s = [] | |
787 | + xyz0 = np.array([length, 0 ,0]) | |
788 | + xyz0s.append(xyz0) | |
789 | + na = len(xyz0s) | |
790 | + elif ptype=="circle": | |
791 | + dummy, radius, rotxyzs, ang1, ang2, color, options = toplot | |
792 | + na = 50 | |
793 | + alphas =np.linspace(ang1,ang2,na) | |
794 | + # --- cercle dans le plan (x,y) | |
795 | + xyz0s = [] | |
796 | + for alpha in alphas: | |
797 | + alpha = math.radians(alpha) | |
798 | + x = radius*math.cos(alpha) | |
799 | + y = radius*math.sin(alpha) | |
800 | + z = 0 | |
801 | + xyz0 = np.array([x, y, z]) | |
802 | + xyz0s.append(xyz0) | |
803 | + na = len(xyz0s) | |
804 | + else: | |
805 | + continue | |
806 | + # --- rotations | |
807 | + if ptype=="linefrom0" or ptype=="textfrom0" or ptype=="circle": | |
808 | + for rotxyz in rotxyzs: | |
809 | + rotx, roty, rotz = rotxyz | |
810 | + cosrx = math.cos(math.radians(rotx)) | |
811 | + sinrx = math.sin(math.radians(rotx)) | |
812 | + cosry = math.cos(math.radians(roty)) | |
813 | + sinry = math.sin(math.radians(roty)) | |
814 | + cosrz = math.cos(math.radians(rotz)) | |
815 | + sinrz = math.sin(math.radians(rotz)) | |
816 | + rotrx = np.array([ [1, 0, 0], [0, cosrx, -sinrx], [0, sinrx, cosrx] ]) | |
817 | + rotry = np.array([ [cosry, 0, -sinry], [0, 1, 0], [sinry, 0, cosry] ]) | |
818 | + rotrz = np.array([ [cosrz, -sinrz, 0], [sinrz, cosrz, 0] , [0, 0, 1] ]) | |
819 | + xyz1s = [] | |
820 | + for xyz in xyz0s: | |
821 | + xyz = np.dot(rotrx, xyz) | |
822 | + xyz = np.dot(rotry, xyz) | |
823 | + xyz = np.dot(rotrz, xyz) | |
824 | + xyz1s.append(xyz) | |
825 | + xyz0s = xyz1s # ready for a second rotation | |
826 | + else: | |
827 | + xyz1s = xyz0s | |
828 | + # --- projections | |
829 | + cosaz = math.cos(math.radians(azim)) | |
830 | + sinaz = math.sin(math.radians(azim)) | |
831 | + cosel = math.cos(math.radians(elev)) | |
832 | + sinel = math.sin(math.radians(elev)) | |
833 | + rotaz = np.array([ [cosaz, -sinaz, 0], [sinaz, cosaz, 0] , [0, 0, 1] ]) | |
834 | + #rotel = np.array([ [cosel, 0, -sinel], [0, 1, 0], [sinel, 0, cosel] ]) | |
835 | + rotel = np.array([ [1, 0, 0], [0, cosel, -sinel], [0, sinel, cosel] ]) | |
836 | + xyz2s = [] | |
837 | + for xyz in xyz1s: | |
838 | + xyz = np.dot(rotaz, xyz) | |
839 | + xyz = np.dot(rotel, xyz) | |
840 | + xyz2s.append(xyz) | |
841 | + # --- plot | |
842 | + if ptype=="textfrom0" or ptype=="text": | |
843 | + x,y,z = xyz2s[0] | |
844 | + h = ax.text(x,z,text) | |
845 | + h.set_color(color) | |
846 | + else: | |
847 | + for ka in range(0,na-1): | |
848 | + xyz1 = xyz2s[ka] | |
849 | + xyz2 = xyz2s[ka+1] | |
850 | + x = [xyz1[0], xyz2[0]] | |
851 | + y = [xyz1[1], xyz2[1]] | |
852 | + z = [xyz1[2], xyz2[2]] | |
853 | + if y[0]>1e-3 or y[1]>1e-3: | |
854 | + symbol = color+':' | |
855 | + else: | |
856 | + symbol = color+'-' | |
857 | + h = ax.plot(x,z,symbol,'linewidth',1.0) | |
858 | + for option in options.items(): | |
859 | + key = option[0] | |
860 | + val = option[1] | |
861 | + if key=="linewisth": | |
862 | + h[0].set_linewidth(val) | |
863 | + # --- | |
864 | + ax.axis('equal') | |
865 | + #fig.patch.set_visible(False) | |
866 | + ax.axis('off') | |
867 | + plt.title("Rotation angles for latitude {} deg".format(lati)) | |
868 | + if outfile!="": | |
869 | + plt.savefig(outfile, facecolor='w', edgecolor='w') | |
870 | + plt.show() | |
871 | + | |
163 | 872 | # ##################################################################### |
164 | 873 | # ##################################################################### |
165 | 874 | # ##################################################################### |
... | ... | @@ -169,8 +878,9 @@ class ComponentMountPointing(ComponentMountPointingException, Component, Guitast |
169 | 878 | # ##################################################################### |
170 | 879 | |
171 | 880 | if __name__ == "__main__": |
172 | - default = 0 | |
173 | - example = input(f"Select the example (0 to 0) ({default}) ") | |
881 | + | |
882 | + default = 2 | |
883 | + example = input(f"Select the example (0 to 2) ({default}) ") | |
174 | 884 | try: |
175 | 885 | example = int(example) |
176 | 886 | except: |
... | ... | @@ -182,7 +892,69 @@ if __name__ == "__main__": |
182 | 892 | """ |
183 | 893 | Basic example |
184 | 894 | """ |
185 | - comp = ComponentMountPointing("HADEC", name="test") | |
186 | - comp.init("HADEC", name="test") | |
895 | + # --- siteobs | |
896 | + siteobs = Siteobs("GPS 0 E 49 200") | |
897 | + # --- horizon | |
898 | + siteobs.horizon_altaz = [(0,40), (180,0), (360,40)] | |
899 | + # --- Component init | |
900 | + comp = ComponentMountPointing("HADEC", name="test", site=siteobs) | |
901 | + comp.init("HADEC", name="test", site=siteobs) | |
187 | 902 | comp.verbose = 1 |
188 | - res = comp.command("DO", "RADEC_COORD") | |
903 | + comp.command("SET", "target", "sun") | |
904 | + #comp.command("DO", "GOTO") | |
905 | + jd = Date("now").jd() | |
906 | + print(f"JD = {jd}") | |
907 | + nightephem = comp._night_ephem("sun") | |
908 | + eph, dpeh = comp._eph.date_ephem(nightephem) | |
909 | + | |
910 | + if example == 1: | |
911 | + # --- generate rotation system documentation images | |
912 | + import os | |
913 | + # --- rob, rotp | |
914 | + rotb = 60 | |
915 | + rotp = 30 # for pierside = -1 = Mountaxis.PIERSIDE_POS2 | |
916 | + home = Home("GPS 2.25 E 43.567 148") | |
917 | + siteobs = Siteobs(home) | |
918 | + mount = ComponentMountPointing("HADEC", name="Example Mount", siteobs=siteobs) | |
919 | + path = mount.conf_guitastro['path_docimages'] | |
920 | + if False: | |
921 | + # --- Siteobs latitude | |
922 | + latitude = 30 | |
923 | + # --- observer view | |
924 | + elev = 15 # tourne autour de l'axe x | |
925 | + azim = 140 # tourne autour de de l'axe z (azim=0 on regarde W devant, azim=90 N devant) | |
926 | + outfile = os.path.join(path, "rotbp_n.png") | |
927 | + else: | |
928 | + # --- Siteobs latitude | |
929 | + latitude = -30 | |
930 | + # --- observer view | |
931 | + elev = 15 # tourne autour de l'axe x | |
932 | + azim = 140 # tourne autour de de l'axe z (azim=0 on regarde W devant, azim=90 N devant) | |
933 | + outfile = os.path.join(path, "rotbp_s.png") | |
934 | + # --- call the method | |
935 | + mount.plot_rot(latitude, azim, elev, rotb, rotp, outfile) | |
936 | + | |
937 | + if example == 2: | |
938 | + """ | |
939 | + Basic example | |
940 | + """ | |
941 | + # --- siteobs | |
942 | + siteobs = Siteobs("GPS 0 E 49 200") | |
943 | + # --- horizon | |
944 | + siteobs.horizon_altaz = [(0,40), (180,0), (360,40)] | |
945 | + # --- Component init | |
946 | + comp = ComponentMountPointing("HADEC", name="test", site=siteobs) | |
947 | + comp.init("HADEC", name="test", site=siteobs) | |
948 | + target = "sun" | |
949 | + cel = comp.target2cel(target) | |
950 | + pierside = Mountaxis.PIERSIDE_POS2 | |
951 | + rot, inc = comp.cel2inc(cel, pierside, comp.SAVE_AS_SIMU) | |
952 | + #comp._queue.put("speed_slew = (5, 5)") | |
953 | + #comp.command("SET", "target", target) | |
954 | + #comp.command("DO", "RADEC_GOTO") | |
955 | + #param = comp.database.query() | |
956 | + xinc, xrot, xcel = comp.inc2cel(None, comp.SAVE_AS_SIMU) | |
957 | + astro = comp.cel2astro(xcel) | |
958 | + | |
959 | + | |
960 | + | ... | ... |
... | ... | @@ -0,0 +1,376 @@ |
1 | +# -*- coding: utf-8 -*- | |
2 | +import os | |
3 | +import sys | |
4 | +import shlex | |
5 | +#from atexist import register | |
6 | + | |
7 | +try: | |
8 | + # guitastro is installed with setup.py | |
9 | + from guitastro import Communication, GuitastroException, GuitastroTools, Ephemeris | |
10 | +except: | |
11 | + # guitastro is installed with only requirements.in | |
12 | + # guitastro_camera_* folders must be copied at the same root folder than guitastro | |
13 | + pwd = os.getcwd() | |
14 | + short_paths = ['../../../guitastro/src'] | |
15 | + for short_path in short_paths: | |
16 | + path = os.path.abspath(os.path.join(pwd, short_path)) | |
17 | + if path not in sys.path: | |
18 | + sys.path.insert(0, path) | |
19 | + from guitastro.communications import Communication | |
20 | + from guitastro.guitastrotools import GuitastroException, GuitastroTools | |
21 | + from guitastro.ephemeris import Ephemeris | |
22 | + | |
23 | +# ##################################################################### | |
24 | +# ##################################################################### | |
25 | +# ##################################################################### | |
26 | +# Class Device | |
27 | +# ##################################################################### | |
28 | +# ##################################################################### | |
29 | +# ##################################################################### | |
30 | + | |
31 | +class DeviceException(GuitastroException): | |
32 | + | |
33 | + ERR_FILE_NOT_EXISTS = 0 | |
34 | + ERR_COMMAND = 1 | |
35 | + ERR_COMPONENT_NOT_FOUND = 2 | |
36 | + | |
37 | + errors = [""]*3 | |
38 | + errors[ERR_FILE_NOT_EXISTS] = "The named file was not found" | |
39 | + errors[ERR_COMMAND] = "Command error" | |
40 | + errors[ERR_COMPONENT_NOT_FOUND] = "Component not found" | |
41 | + | |
42 | + | |
43 | +class Device(DeviceException, GuitastroTools): | |
44 | + """Abstract class for devices | |
45 | + | |
46 | + All commands are linked to the same communication channel | |
47 | + | |
48 | + This class is inherited by Device classes. | |
49 | + """ | |
50 | + | |
51 | + _real = False | |
52 | + _chan = None | |
53 | + | |
54 | +# ===================================================================== | |
55 | +# properties | |
56 | +# ===================================================================== | |
57 | + | |
58 | + def _get_real(self)->bool: | |
59 | + """Get is the device is real or not | |
60 | + """ | |
61 | + return self._real | |
62 | + | |
63 | + real = property(_get_real) | |
64 | + | |
65 | + def _get_param(self): | |
66 | + return self._unit_params | |
67 | + | |
68 | + param = property(_get_param) | |
69 | + | |
70 | + def _get_components(self): | |
71 | + """ Return a dictionary of components | |
72 | + | |
73 | + The key is the name of the component. | |
74 | + The value is a tupple: | |
75 | + | |
76 | + * category | |
77 | + * Python access to the object | |
78 | + | |
79 | + Returns: | |
80 | + | |
81 | + Dictionary of components if just_names=False (default). | |
82 | + | |
83 | + Example: | |
84 | + | |
85 | + :: | |
86 | + | |
87 | + dev = Device("NOTHING") | |
88 | + print("Components are:") | |
89 | + for key, val in dev.components.items(): | |
90 | + print(f" * {key} of type {val[0]}") | |
91 | + | |
92 | + """ | |
93 | + dico = {} | |
94 | + for name, comp in self._comp.items(): | |
95 | + category = comp.category | |
96 | + dico[name] = (category, comp) | |
97 | + return dico | |
98 | + | |
99 | + components = property(_get_components) | |
100 | + | |
101 | + def _get_component_names(self): | |
102 | + """ Return a list of component names | |
103 | + | |
104 | + Returns: | |
105 | + | |
106 | + List of component names. | |
107 | + | |
108 | + Example: | |
109 | + | |
110 | + :: | |
111 | + | |
112 | + dev = Device("NOTHING") | |
113 | + print("Components are:") | |
114 | + for name in dev.component_names: | |
115 | + print(f" * {name}") | |
116 | + | |
117 | + """ | |
118 | + return list(self._comp.keys()) | |
119 | + | |
120 | + component_names = property(_get_component_names) | |
121 | + | |
122 | +# ===================================================================== | |
123 | +# ===================================================================== | |
124 | +# Private methods | |
125 | +# ===================================================================== | |
126 | +# ===================================================================== | |
127 | + | |
128 | +# ===================================================================== | |
129 | +# ===================================================================== | |
130 | +# Methods for experimented users (debug, etc) | |
131 | +# ===================================================================== | |
132 | +# ===================================================================== | |
133 | + | |
134 | +# ===================================================================== | |
135 | +# ===================================================================== | |
136 | +# Methods for users | |
137 | +# ===================================================================== | |
138 | +# ===================================================================== | |
139 | + | |
140 | + def open(self, real:bool): | |
141 | + """ Open the communication channel | |
142 | + | |
143 | + Args: | |
144 | + | |
145 | + real: Open the communication in simulation mode is False. | |
146 | + | |
147 | + Example: | |
148 | + | |
149 | + Open the communication as a real channel. | |
150 | + | |
151 | + :: | |
152 | + | |
153 | + dev = Device("NOTHING") | |
154 | + dev.open(True) | |
155 | + | |
156 | + """ | |
157 | + self._real = real | |
158 | + if self._chan == None: | |
159 | + transport = self._unit_params["TRANSPORT"].upper() # something like "//./COM1" | |
160 | + port = self._unit_params["PORT"] # something like "//./COM1" | |
161 | + chan = Communication(transport, port = port, baud_rate=115200, DELAY_PUT_READ = 0.2, REAL = real) | |
162 | + chan.open_chan() | |
163 | + self._chan = chan | |
164 | + # - set the channel to all components | |
165 | + for component_name in self.component_names: | |
166 | + self._comp[component_name].channel = chan | |
167 | + self._comp[component_name].real = self._real | |
168 | + | |
169 | + def close(self): | |
170 | + """ Close the communication channel | |
171 | + | |
172 | + Example: | |
173 | + | |
174 | + Open the communication as a simulator channel and close it. | |
175 | + | |
176 | + :: | |
177 | + | |
178 | + dev = Device("NOTHING") | |
179 | + dev.open(False) | |
180 | + dev.close() | |
181 | + | |
182 | + """ | |
183 | + if self._real == True: | |
184 | + del(self._chan) | |
185 | + self._chan = None | |
186 | + # - reset the channel to all components | |
187 | + for component_name in self.component_names: | |
188 | + self._comp[component_name].channel = None | |
189 | + self._comp[component_name].real = False | |
190 | + | |
191 | + | |
192 | + def commandstring(self, cmd:str): | |
193 | + """Execute a command as a string entry | |
194 | + | |
195 | + Args: | |
196 | + | |
197 | + cmd: A string composed by "component_name action operation parameters" | |
198 | + | |
199 | + Returns: | |
200 | + | |
201 | + The result after the executoin of the command. | |
202 | + | |
203 | + Example: | |
204 | + | |
205 | + :: | |
206 | + | |
207 | + dev = Device("NOTHING") | |
208 | + dev.open(False) | |
209 | + dev.commandstring("mount SET target 50000") | |
210 | + | |
211 | + """ | |
212 | + cmds = shlex.split(cmd) | |
213 | + component_name = cmds[0] | |
214 | + action = cmds[1].upper() | |
215 | + args = cmds[2:] | |
216 | + return self.command(component_name, action, *args) | |
217 | + | |
218 | + def component(self, component_name:str): | |
219 | + if component_name in self.component_names: | |
220 | + return self._comp[component_name] | |
221 | + msg = f"Component {component_name} not found amongst {self.component_names}" | |
222 | + raise DeviceException(DeviceException.ERR_COMPONENT_NOT_FOUND, msg) | |
223 | + | |
224 | + | |
225 | + def command(self, component_name:str, action:str, *args, **kwargs): | |
226 | + """Execute a command as args and kwargs | |
227 | + | |
228 | + Args: | |
229 | + | |
230 | + component_name: The name of the component (see the method components to retreive the names). | |
231 | + action: The action to execute. | |
232 | + *args: args[0] is the operation. | |
233 | + **kwargs: Optional parameters. | |
234 | + | |
235 | + Returns: | |
236 | + | |
237 | + The result after the execution of the command. | |
238 | + | |
239 | + Example: | |
240 | + | |
241 | + :: | |
242 | + | |
243 | + dev = Device_Optec("TNCK") | |
244 | + dev.open(False) | |
245 | + dev.command("mount", "SET", "target", 5) | |
246 | + dev.command("mount", "DO", "GOTO") | |
247 | + | |
248 | + """ | |
249 | + if component_name not in self.component_names: | |
250 | + msg = f"Component {component_name} not found amongst {self.component_names}" | |
251 | + raise DeviceException(DeviceException.ERR_COMPONENT_NOT_FOUND, msg) | |
252 | + try: | |
253 | + result = self._comp[component_name].command(action, *args, **kwargs) | |
254 | + except: | |
255 | + msg = f"Problem with component {component_name} command {action} {args} {kwargs}" | |
256 | + raise DeviceException(DeviceException.ERR_COMMAND, msg) | |
257 | + return result | |
258 | + | |
259 | +# ===================================================================== | |
260 | +# ===================================================================== | |
261 | +# Special methods | |
262 | +# ===================================================================== | |
263 | +# ===================================================================== | |
264 | + | |
265 | + def __init__(self, *args, **kwargs): | |
266 | + """ | |
267 | + Conversion from Uniform Python object into protocol language | |
268 | + | |
269 | + Usage : | |
270 | + | |
271 | + DeviceOptec("Z", name="test") | |
272 | + """ | |
273 | + # === Decode params | |
274 | + # --- Dicos of optional and mandatory parameters | |
275 | + params_optional = {} | |
276 | + # --- | |
277 | + params_optional["NAME"] = (str, "Virtual device") | |
278 | + params_optional["MODEL"] = (str, "Abstract Device") | |
279 | + params_optional["MANUFACTURER"] = (str, "Virtual") | |
280 | + params_optional["SERIAL_NUMBER"] = (str, "") | |
281 | + params_optional["REAL"] = (bool, False) | |
282 | + params_optional["DESCRIPTION"] = (str, "Just an abstract class. No component composed.") | |
283 | + # --- Dico of unit_types and their parameters | |
284 | + unit_types = {} | |
285 | + # --- unit choice | |
286 | + unit_types["NOTHING"] = {"MANDATORY" : {"TRANSPORT":(str,"SERIAL")}, "OPTIONAL" : {"HOST":[str,"192.168.0.1"], "PORT":[int,1025]} } | |
287 | + # --- Decode args and kwargs parameters | |
288 | + self._unit_params = self.decode_args_kwargs(0, unit_types, params_optional, *args, **kwargs) | |
289 | + # === | |
290 | + self.unit_type = self._unit_params["SELECTED_ARG"] | |
291 | + # --- init ephemeris | |
292 | + eph = Ephemeris() | |
293 | + eph.set_home("148") | |
294 | + # === Instanciate components of the device | |
295 | + # This is a composition of Component classes | |
296 | + self._comp = {} | |
297 | + # in a concrete class you have to add components here | |
298 | + | |
299 | + #@register | |
300 | + def __del__(self): | |
301 | + try: | |
302 | + self.close() | |
303 | + except: | |
304 | + pass | |
305 | + | |
306 | + def __str__(self): | |
307 | + msg = "" | |
308 | + msg += f"=== Guitastro Device: {self._unit_params['NAME']} ===" | |
309 | + # ------------------------ | |
310 | + msg += "\n--- Device parameters:" | |
311 | + for key, val in self.param.items(): | |
312 | + msg += f"\n * {key} = {val}" | |
313 | + # ------------------------ | |
314 | + msg += "\n--- Components are:" | |
315 | + for key, val in self.components.items(): | |
316 | + msg += f"\n * {key} of type {val[0]}" | |
317 | + # ------------------------ | |
318 | + for component_name in self.component_names: | |
319 | + msg += f"\n--- Component {component_name}:" | |
320 | + comp = self.component(component_name) | |
321 | + dico = comp.prop() | |
322 | + for key, val in dico.items(): | |
323 | + if isinstance(val,dict) == True: | |
324 | + for keyy, vall in val.items(): | |
325 | + msg += f"\n * {key}.{keyy} of type {vall}" | |
326 | + else: | |
327 | + msg += f"\n * {key} = {val}" | |
328 | + param = comp.database.param() | |
329 | + for key, val in param.items(): | |
330 | + msg += f"\n * param {key} = {val}" | |
331 | + # ------------------------ | |
332 | + msg += "\n--- Communication status:" | |
333 | + msg += f"\n * Mode real = {self.real}" | |
334 | + if self.real: | |
335 | + if self._chan == None: | |
336 | + msg += "\nChannel is closed." | |
337 | + else: | |
338 | + msg += "\n" + str(self._chan) | |
339 | + return msg | |
340 | + | |
341 | +# ##################################################################### | |
342 | +# ##################################################################### | |
343 | +# ##################################################################### | |
344 | +# Main | |
345 | +# ##################################################################### | |
346 | +# ##################################################################### | |
347 | +# ##################################################################### | |
348 | + | |
349 | +if __name__ == "__main__": | |
350 | + default = 0 | |
351 | + example = input(f"Select the example (0 to 0) ({default}) ") | |
352 | + try: | |
353 | + example = int(example) | |
354 | + except: | |
355 | + example = default | |
356 | + | |
357 | + print("Example = {}".format(example)) | |
358 | + | |
359 | + if example == 0: | |
360 | + """ | |
361 | + Basic example | |
362 | + """ | |
363 | + dev = Device("NOTHING", transport="SERIAL") | |
364 | + # ------------------------ | |
365 | + print("*"*20,"\nDevice parameters:") | |
366 | + for key, val in dev.param.items(): | |
367 | + print(f" * {key} = {val}") | |
368 | + # ------------------------ | |
369 | + print("*"*20,"\nComponents are:") | |
370 | + for key, val in dev.components.items(): | |
371 | + print(f" * {key} of type {val[0]}") | |
372 | + # ------------------------ | |
373 | + dev.open(False) | |
374 | + dev.close() | |
375 | + print(dev) | |
376 | + | ... | ... |
src/guitastro/ephemeris.py
... | ... | @@ -65,9 +65,11 @@ class EphemerisException(GuitastroException): |
65 | 65 | """ |
66 | 66 | |
67 | 67 | TARGET_NOT_FOUND = 0 |
68 | + DATE_OUTSIDE_THE_NIGHT = 1 | |
68 | 69 | |
69 | - errors = [""]*1 | |
70 | + errors = [""]*2 | |
70 | 71 | errors[TARGET_NOT_FOUND] = "Target not found" |
72 | + errors[DATE_OUTSIDE_THE_NIGHT] = "The date is outside the night limits" | |
71 | 73 | |
72 | 74 | |
73 | 75 | |
... | ... | @@ -183,6 +185,7 @@ class Ephemeris(EphemerisException, GuitastroTools): |
183 | 185 | TARGET_TYPE_GCNC = 6 |
184 | 186 | TARGET_TYPE_MPC = 7 |
185 | 187 | TARGET_TYPE_RADECDRIFT = 8 |
188 | + TARGET_TYPE_HADEC = 9 | |
186 | 189 | |
187 | 190 | def __init__(self): |
188 | 191 | self._observatory = None |
... | ... | @@ -198,6 +201,8 @@ class Ephemeris(EphemerisException, GuitastroTools): |
198 | 201 | # --- |
199 | 202 | self._ts = skyfield.api.load.timescale() |
200 | 203 | self._earthsatellites = None |
204 | + # --- | |
205 | + self._computed_ephem = {} | |
201 | 206 | |
202 | 207 | def set_home(self, home): |
203 | 208 | """Set the home position (on Earth) |
... | ... | @@ -439,7 +444,6 @@ class Ephemeris(EphemerisException, GuitastroTools): |
439 | 444 | st0 = ligne.split()[1] |
440 | 445 | t0 = t0[0:10]+"T"+st0 |
441 | 446 | #print(f"ligne={ligne}") |
442 | - | |
443 | 447 | return sra, sdec, equinox, t0 |
444 | 448 | |
445 | 449 | def radec(self, target: str, **kwargs)-> tuple: |
... | ... | @@ -453,6 +457,7 @@ class Ephemeris(EphemerisException, GuitastroTools): |
453 | 457 | * 'RADECDRIFT': Followed by an equatorial Right Ascension, Declination position and the drift. |
454 | 458 | * 'CELESTRACK' or 'TLEFILES': Followed by a satellite name in the Celestrack TLE files. |
455 | 459 | * 'DATERADECS': Followed by a list of equatorial Right Ascension, Declination positions. |
460 | + * 'HADEC': Followed by a list of equatorial true Hour Angle, Declination positions. | |
456 | 461 | * 'TLE': Followed by a satellite defined by its TLE (Two Line Elements). |
457 | 462 | * 'GCNC': Followed by a number which is a GRB name (e.g. GCNC 990123) to search information in GCN circulars. |
458 | 463 | * 'MPC': Followed by a name which is a Solar System Body. |
... | ... | @@ -462,7 +467,8 @@ class Ephemeris(EphemerisException, GuitastroTools): |
462 | 467 | * 'date': To compute ephemeris for a given date ("now" for now). |
463 | 468 | * 'unit_ra': To indicate the output format (see Angle) |
464 | 469 | * 'unit_dec': To indicate the output format (see Angle) |
465 | - * 'target_type': To indicate the input format if the keyword is not indicated in the target string. | |
470 | + * 'target_type': To indicate the input format if the keyword is not indicated in the target string. | |
471 | + * 'target_type_only': To return the identified input format. | |
466 | 472 | |
467 | 473 | Returns: |
468 | 474 | |
... | ... | @@ -471,12 +477,17 @@ class Ephemeris(EphemerisException, GuitastroTools): |
471 | 477 | equinox: Equinox of the position |
472 | 478 | epoch: Date of the position when the object is moving |
473 | 479 | |
480 | + or | |
481 | + | |
482 | + target_type: The identified input format. | |
483 | + | |
474 | 484 | """ |
475 | 485 | equinox = "J2000" |
476 | 486 | date = "now" |
477 | 487 | unit_ra = "deg" # "H0.2" |
478 | 488 | unit_dec = "deg" # "d+090.1" |
479 | 489 | target_type = self.TARGET_TYPE_NAME |
490 | + target_type_only = False | |
480 | 491 | if len(kwargs) > 0: |
481 | 492 | keys = kwargs.keys() |
482 | 493 | if "date" in keys: |
... | ... | @@ -485,6 +496,8 @@ class Ephemeris(EphemerisException, GuitastroTools): |
485 | 496 | unit_ra = kwargs["unit_ra"] |
486 | 497 | if "unit_dec" in keys: |
487 | 498 | unit_dec = kwargs["unit_dec"] |
499 | + if "target_type_only" in keys: | |
500 | + target_type_only = kwargs["target_type_only"] | |
488 | 501 | if "target_type" in keys: |
489 | 502 | target_t = kwargs["target_type"].upper() |
490 | 503 | if target_t=="TLEFILES" or target_t=="CELESTRACK": |
... | ... | @@ -497,6 +510,8 @@ class Ephemeris(EphemerisException, GuitastroTools): |
497 | 510 | target_type = self.TARGET_TYPE_RADEC |
498 | 511 | elif target_t=="RADECDRIFT": |
499 | 512 | target_type = self.TARGET_TYPE_RADECDRIFT |
513 | + elif target_t=="HADEC": | |
514 | + target_type = self.TARGET_TYPE_HADEC | |
500 | 515 | elif target_t=="GCNC": |
501 | 516 | target_type = self.TARGET_TYPE_GCNC |
502 | 517 | elif target_t=="MPC": |
... | ... | @@ -521,6 +536,9 @@ class Ephemeris(EphemerisException, GuitastroTools): |
521 | 536 | elif target_t=="RADECDRIFT": |
522 | 537 | target_type = self.TARGET_TYPE_RADECDRIFT |
523 | 538 | target = target[len(res[0])+1:] |
539 | + elif target_t=="HADEC": | |
540 | + target_type = self.TARGET_TYPE_HADEC | |
541 | + target = target[len(res[0])+1:] | |
524 | 542 | elif target_t=="GCNC": |
525 | 543 | target_type = self.TARGET_TYPE_GCNC |
526 | 544 | target = target[len(res[0])+1:] |
... | ... | @@ -531,6 +549,11 @@ class Ephemeris(EphemerisException, GuitastroTools): |
531 | 549 | target_type = self.TARGET_TYPE_TLE |
532 | 550 | target = target[len(res[0])+1:] |
533 | 551 | # --- |
552 | + if target_type_only: | |
553 | + if target_type == self.TARGET_TYPE_NAME: | |
554 | + target_t = "NAME" | |
555 | + return target_t | |
556 | + # --- | |
534 | 557 | self._date2ts(date) |
535 | 558 | # --- |
536 | 559 | if target_type == self.TARGET_TYPE_MPC: |
... | ... | @@ -553,6 +576,12 @@ class Ephemeris(EphemerisException, GuitastroTools): |
553 | 576 | c = SkyCoord(target, unit=(u.hourangle, u.deg)) |
554 | 577 | ra, dec = c.to_string("hmsdms").split() |
555 | 578 | # --- |
579 | + if target_type == self.TARGET_TYPE_HADEC: | |
580 | + time = Time(epoch) | |
581 | + location = EarthLocation(lat=self.home.latitude*u.deg, lon=self.home.longitude*u.deg, height=self.home.altitude*u.m) | |
582 | + c = SkyCoord(target, unit=(u.hourangle, u.deg), frame="hadec", obstime = time, location=location) | |
583 | + ra, dec = c.icrs.to_string("hmsdms").split() | |
584 | + # --- | |
556 | 585 | if target_type == self.TARGET_TYPE_RADECDRIFT: |
557 | 586 | res = target.split() |
558 | 587 | # last two elements are dra, ddec. Not used here. |
... | ... | @@ -867,12 +896,80 @@ class Ephemeris(EphemerisException, GuitastroTools): |
867 | 896 | tk=273.15-97.7 |
868 | 897 | return p, tk |
869 | 898 | |
899 | + def date_ephem(self, ephem:dict, date:Date="now")->dict: | |
900 | + """Extract the ephemeris for a given date asked from a night ephemeris. | |
901 | + | |
902 | + Arg: | |
903 | + | |
904 | + ephem: A night ephemeris returned by the method night_ephem | |
905 | + date: The date of the calculation | |
906 | + | |
907 | + Returns: | |
908 | + | |
909 | + eph: The ephemeris at the date | |
910 | + deph: The differential ephemeris in units of inverse of seconds (deg/s for angles) | |
911 | + | |
912 | + """ | |
913 | + # jd = jd0 + k*djd | |
914 | + d = Date(date) | |
915 | + jd = d.jd() | |
916 | + jjds = ephem['jd'] | |
917 | + njd = len(jjds) | |
918 | + djd = jjds[1]-jjds[0] | |
919 | + n = round((jd-jjds[0])/djd) | |
920 | + if n<0 or n>njd: | |
921 | + msg = f"The date {d.iso(0)} is not inside the night {ephem['night']}" | |
922 | + raise EphemerisException(EphemerisException.DATE_OUTSIDE_THE_NIGHT, msg) | |
923 | + eph = {} | |
924 | + for key, val in ephem.items(): | |
925 | + if isinstance(val, np.ndarray): | |
926 | + eph[key] = val[n] # deg | |
927 | + else: | |
928 | + eph[key] = val # str | |
929 | + return eph | |
930 | + | |
931 | + def night_ephem(self, target, night:str, ephem_sun:dict=None, ephem_moon:dict=None, **kwargs)->dict: | |
932 | + """Same as target2night but avoids to recompute many times the same night ephemeris if target is the same | |
933 | + | |
934 | + All args and returns are the same than target2night. | |
935 | + """ | |
936 | + # | |
937 | + # _computed_ephem["night"] | |
938 | + # _computed_ephem["night"]["targets"] = List of targets (0..n-1) | |
939 | + # _computed_ephem["night"][0] = targets index 0 | |
940 | + # _computed_ephem["night"][...] | |
941 | + # _computed_ephem["night"][n-1] = targets index n-1 | |
942 | + if night in self._computed_ephem.keys(): | |
943 | + # --- this night is known | |
944 | + targets = self._computed_ephem[night]["targets"] | |
945 | + try: | |
946 | + # --- case target is ever computer. Return the history instead to recompute | |
947 | + indx = targets.index(target) | |
948 | + ephem = self._computed_ephem[night][indx] | |
949 | + return ephem | |
950 | + except: | |
951 | + # --- new target to be computed | |
952 | + indx = len(targets) | |
953 | + else: | |
954 | + # --- this night is not known, clear all the history | |
955 | + del self._computed_ephem | |
956 | + self._computed_ephem = {} | |
957 | + self._computed_ephem[night] = {} | |
958 | + indx = 0 | |
959 | + self._computed_ephem[night]["targets"] = [] | |
960 | + # --- Compute the night ephemeris | |
961 | + ephem = self.target2night(target, night, ephem_sun, ephem_moon, **kwargs) | |
962 | + # --- Add the ephem into the history | |
963 | + self._computed_ephem[night]["targets"].append(target) | |
964 | + self._computed_ephem[night][indx] = ephem | |
965 | + return ephem | |
966 | + | |
870 | 967 | def target2night(self, target, night:str, ephem_sun:dict=None, ephem_moon:dict=None, **kwargs)->dict: |
871 | 968 | """Compute the ephemeris at every second of local coodinates for a night |
872 | 969 | |
873 | 970 | Two computations are importants: |
874 | 971 | |
875 | - * 'observability': An integer defining why the target is visible or not. | |
972 | + * 'visibility': An integer defining why the target is visible or not. | |
876 | 973 | * 'observability': A float value from 0 (not observable) to 100 (best conditions to observe) |
877 | 974 | |
878 | 975 | Args: |
... | ... | @@ -886,6 +983,10 @@ class Ephemeris(EphemerisException, GuitastroTools): |
886 | 983 | * horizon: An object of the class Horizon of Guitastro that defines the horizon line. |
887 | 984 | * preference: A string "bestelev" or "immediate" to compute the observability |
888 | 985 | * duskelev: A float, the elevation of the Sun defining the start and end of the night. |
986 | + * wavelength_nm: A float, the observation wavelength (in nanometers) | |
987 | + * humidity: A float, the relative humidiy (between 0 and 1) | |
988 | + * speed: A boolean, True to compute the derivative of coordinates | |
989 | + * nsec: An integer, default is 86400 to compute for every seconds of the night. Else, compute only centered on the Date indicated in the night. Speed can be computed only for nsec >= 3. | |
889 | 990 | |
890 | 991 | Returns: |
891 | 992 | |
... | ... | @@ -899,6 +1000,7 @@ class Ephemeris(EphemerisException, GuitastroTools): |
899 | 1000 | * 'alt': Numpy array of the elevation (deg) |
900 | 1001 | * 'az': Numpy array of the azimut (deg) |
901 | 1002 | * 'ha': Numpy array of the apparent hour angle (deg) |
1003 | + * 'parallactic': Numpy array of the parallactic angle (deg) | |
902 | 1004 | * 'dec': Numpy array of the apparent declination (deg) |
903 | 1005 | * 'ra_equinox': Numpy array of the Right Ascension at the input equinox (deg) |
904 | 1006 | * 'dec_equinox': Numpy array of the Declination at the input equinox (deg) |
... | ... | @@ -916,7 +1018,6 @@ class Ephemeris(EphemerisException, GuitastroTools): |
916 | 1018 | if ephem_sun==None or ephem_moon==None, the distances between the target and the Sun and Moon will not be calculated. |
917 | 1019 | |
918 | 1020 | """ |
919 | - nsec = 86400 | |
920 | 1021 | if ephem_sun==None or ephem_moon==None: |
921 | 1022 | sunmoon = False |
922 | 1023 | else: |
... | ... | @@ -924,6 +1025,9 @@ class Ephemeris(EphemerisException, GuitastroTools): |
924 | 1025 | if 'horizon' in kwargs.keys(): |
925 | 1026 | hor = kwargs['horizon'] |
926 | 1027 | hor_az, hor_elev = hor.horizon_altaz |
1028 | + elif 'siteobs' in kwargs.keys(): | |
1029 | + siteobs = kwargs['siteobs'] | |
1030 | + hor_az, hor_elev = siteobs.horizon_altaz | |
927 | 1031 | else: |
928 | 1032 | hor_az = np.arange(0, 361) |
929 | 1033 | hor_elev = np.zeros(len(hor_az)) |
... | ... | @@ -935,32 +1039,74 @@ class Ephemeris(EphemerisException, GuitastroTools): |
935 | 1039 | duskelev = kwargs['duskelev'] |
936 | 1040 | else: |
937 | 1041 | duskelev = -7 |
1042 | + if 'humidity' in kwargs.keys(): | |
1043 | + rel_humidity = kwargs['humidity'] | |
1044 | + else: | |
1045 | + rel_humidity = 0.6 | |
1046 | + if 'wavelength_nm' in kwargs.keys(): | |
1047 | + wavelength_nm = kwargs['wavelength_nm'] | |
1048 | + else: | |
1049 | + wavelength_nm = 600 | |
1050 | + if 'speed' in kwargs.keys(): | |
1051 | + speed = kwargs['speed'] | |
1052 | + else: | |
1053 | + speed = False | |
1054 | + if 'nsec' in kwargs.keys(): | |
1055 | + nsec = int(kwargs['nsec']) | |
1056 | + else: | |
1057 | + nsec = 86400 | |
938 | 1058 | location = EarthLocation(lat=self.home.latitude*u.deg, lon=self.home.longitude*u.deg, height=self.home.altitude*u.m) |
1059 | + tan_lat = np.tan(np.radians(self.home.latitude)) | |
939 | 1060 | temp_k, pres_pa = self.altitude2tp(self.home.altitude) |
940 | 1061 | temperature = (temp_k + 273.15) * u.deg_C |
941 | 1062 | pressure = pres_pa * u.pascal |
1063 | + relative_humidity = rel_humidity | |
1064 | + obswl = wavelength_nm * u.nm | |
942 | 1065 | fn = FileNames() |
943 | 1066 | fn.longitude(self.home.longitude) |
944 | - jd1, jd2 = fn.night2date(night) | |
1067 | + # --- compute jd1, jd2 the limits of dates to compute the ephemeris | |
1068 | + if nsec==86400: | |
1069 | + # - we compute ephemeris for all the night | |
1070 | + jd1, jd2 = fn.night2date(night) | |
1071 | + else: | |
1072 | + # - we compute ephemeris only for a duration centered on the date indicated by night | |
1073 | + jd = Date(night).jd() | |
1074 | + night = fn.date2night(jd) | |
1075 | + #nsec = round(nsec) | |
1076 | + if nsec <= 1: | |
1077 | + nsec = 1 | |
1078 | + djd = (nsec-1)/86400. | |
1079 | + jd1, jd2 = jd-djd, jd+djd | |
1080 | + # --- identify the type of target | |
1081 | + target_type = self.radec(target, target_type_only=True) | |
945 | 1082 | # --- compute the drift |
946 | 1083 | date = Date((jd1+jd2)/2).iso() |
947 | - ra, dec, equinox, epoch, dra, ddec = self.radec_speed(target, date=date, unit_ra="deg", unit_dec="deg") | |
1084 | + ra_equinox, dec_equinox, equinox, epoch, dra, ddec = self.radec_speed(target, date=date, unit_ra="deg", unit_dec="deg") | |
948 | 1085 | ddrift = np.sqrt(dra*dra+ddec*ddec) # deg/s |
949 | 1086 | # --- adapt ndate according the drift |
950 | 1087 | ndate = 0 |
951 | 1088 | if dra==0 and ddec==0: |
952 | 1089 | drift = False |
953 | 1090 | epoch = Date((jd1+jd2)/2).iso() |
954 | - targ = SkyCoord(frame=ICRS, ra=ra*u.deg, dec=dec*u.deg, obstime=epoch) | |
1091 | + targ = SkyCoord(frame=ICRS, ra=ra_equinox*u.deg, dec=dec_equinox*u.deg, obstime=epoch) | |
955 | 1092 | else: |
956 | - ndate = int(np.floor(86400*abs(ddrift))) | |
1093 | + ndate = int(np.floor(86400*abs(ddrift)/10.0)) | |
957 | 1094 | drift = True |
958 | - print(f"ndate={ndate} drift={drift}") | |
959 | - if ndate < 50: | |
960 | - ndate = 50 | |
1095 | + #print(f"ndate={ndate} ddrift={ddrift}") | |
1096 | + # --- one computation every 30 min at minimum | |
1097 | + lim = 1440/30 | |
1098 | + if ndate < lim: | |
1099 | + ndate = round(lim) | |
1100 | + # --- one computation every 5 min at maximum | |
1101 | + lim = 1440/5 | |
1102 | + if ndate > lim: | |
1103 | + ndate = round(lim) | |
1104 | + # --- one computation every second at maximum | |
1105 | + if ndate > nsec: | |
1106 | + ndate = nsec | |
961 | 1107 | # --- prepare angles |
962 | 1108 | jds = np.linspace(jd1, jd2, ndate) |
963 | - nangle = 12 | |
1109 | + nangle = 13 | |
964 | 1110 | angles = np.zeros(nangle*ndate).reshape((nangle,ndate)) |
965 | 1111 | angle_offsets = np.zeros(nangle) |
966 | 1112 | angle_prevs = np.zeros(nangle) |
... | ... | @@ -969,16 +1115,33 @@ class Ephemeris(EphemerisException, GuitastroTools): |
969 | 1115 | for k in range(ndate): |
970 | 1116 | # --- compute celestial local angles |
971 | 1117 | jd = jds[k] |
1118 | + obstime = Time(jd, format="jd") | |
972 | 1119 | if drift == True: |
973 | 1120 | # --- recompute ra,dec for each date |
974 | 1121 | date = jd |
975 | - ra, dec, equinox, epoch, dra, ddec = self.radec_speed(target, date=date, unit_ra="deg", unit_dec="deg") | |
976 | - targ = SkyCoord(frame=ICRS, ra=ra*u.deg, dec=dec*u.deg, obstime=epoch) | |
977 | - time = Time(jd, format='jd') | |
978 | - hadec = targ.transform_to(HADec(obstime=time, location=location, pressure=pressure, temperature=temperature)) | |
979 | - altaz = targ.transform_to(AltAz(obstime=time, location=location, pressure=pressure, temperature=temperature)) | |
980 | - ra_rad = np.radians(ra) | |
1122 | + ra_equinox, dec_equinox, equinox, epoch, dra, ddec = self.radec_speed(target, date=date, unit_ra="deg", unit_dec="deg") | |
1123 | + targ = SkyCoord(frame=ICRS, ra=ra_equinox*u.deg, dec=dec_equinox*u.deg, obstime=epoch) | |
1124 | + if target_type == "HADEC": | |
1125 | + target = str(target) | |
1126 | + res = target.split() | |
1127 | + mtarget = target[len(res[0])+1:] | |
1128 | + targ = SkyCoord(mtarget, unit=(u.hourangle, u.deg), frame="hadec", obstime = obstime, location=location) | |
1129 | + hadec= targ | |
1130 | + else: | |
1131 | + hadec = targ.transform_to(HADec(obstime=obstime, location=location, pressure=pressure, temperature=temperature, relative_humidity=relative_humidity, obswl=obswl)) | |
1132 | + ha = hadec.ha.deg | |
1133 | + dec = hadec.dec.deg | |
1134 | + altaz = targ.transform_to(AltAz(obstime=obstime, location=location, pressure=pressure, temperature=temperature, relative_humidity=relative_humidity, obswl=obswl)) | |
1135 | + alt = altaz.alt.deg | |
1136 | + az = altaz.az.deg | |
1137 | + az -= 180 # astro azimut instead geo | |
1138 | + ha_rad = np.radians(ha) | |
981 | 1139 | dec_rad = np.radians(dec) |
1140 | + y = np.sin(ha_rad) | |
1141 | + x = tan_lat * np.cos(dec_rad) - np.sin(dec_rad) * np.cos(ha_rad) | |
1142 | + parallactic = np.degrees(np.arctan2(y,x)) | |
1143 | + ra_rad = np.radians(ra_equinox) | |
1144 | + dec_rad = np.radians(dec_equinox) | |
982 | 1145 | cos_phi = np.cos(ra_rad) |
983 | 1146 | sin_phi = np.sin(ra_rad) |
984 | 1147 | cos_theta = np.cos(dec_rad) |
... | ... | @@ -1009,12 +1172,13 @@ class Ephemeris(EphemerisException, GuitastroTools): |
1009 | 1172 | dist_sun = 0 |
1010 | 1173 | dist_moon = 0 |
1011 | 1174 | # --- ensure continue angles |
1012 | - angle_curs[0], angle_curs[1] = altaz.alt.deg, altaz.az.deg | |
1013 | - angle_curs[2], angle_curs[3] = hadec.ha.deg, hadec.dec.deg | |
1014 | - angle_curs[4], angle_curs[5] = ra, dec | |
1175 | + angle_curs[0], angle_curs[1] = alt, az | |
1176 | + angle_curs[2], angle_curs[3] = ha, dec | |
1177 | + angle_curs[4], angle_curs[5] = ra_equinox, dec_equinox | |
1015 | 1178 | angle_curs[6], angle_curs[7] = cos_phi, sin_phi |
1016 | 1179 | angle_curs[8], angle_curs[9] = cos_theta, sin_theta |
1017 | 1180 | angle_curs[10], angle_curs[11] = dist_sun, dist_moon |
1181 | + angle_curs[12] = parallactic | |
1018 | 1182 | # --- ensure continue angles |
1019 | 1183 | if k>0: |
1020 | 1184 | dif = angle_curs - angle_prevs |
... | ... | @@ -1029,8 +1193,12 @@ class Ephemeris(EphemerisException, GuitastroTools): |
1029 | 1193 | # --- interpolate angles for a full number of dates |
1030 | 1194 | jjds = np.linspace(jd1, jd2, nsec) |
1031 | 1195 | aangles = np.zeros(nangle*nsec).reshape((nangle,nsec)) |
1032 | - for kk in range(nangle): | |
1033 | - aangles[kk] = np.interp(jjds, jds, angles[kk]) | |
1196 | + if nsec == ndate: | |
1197 | + for kk in range(nangle): | |
1198 | + aangles[kk] = angles[kk] | |
1199 | + else: | |
1200 | + for kk in range(nangle): | |
1201 | + aangles[kk] = np.interp(jjds, jds, angles[kk]) | |
1034 | 1202 | # --- interpolated angles |
1035 | 1203 | alts = aangles[0] |
1036 | 1204 | azs = aangles[1] |
... | ... | @@ -1044,6 +1212,25 @@ class Ephemeris(EphemerisException, GuitastroTools): |
1044 | 1212 | sinthetas = aangles[9] |
1045 | 1213 | distsuns = aangles[10] |
1046 | 1214 | distmoons = aangles[11] |
1215 | + parallactics = aangles[12] | |
1216 | + # --- compute speed angles if needed | |
1217 | + if speed and len(jjds)>=3: | |
1218 | + djd = jjds[1]-jjds[0] | |
1219 | + dt = djd*86400 | |
1220 | + dt2 = dt*2 | |
1221 | + dangles = np.zeros(nangle*nsec).reshape((nangle,nsec)) | |
1222 | + for kk in range(nangle): | |
1223 | + y = aangles[kk] | |
1224 | + dangles[kk, 1:nsec-1] = (y[2:] - y[:-2])/dt2 | |
1225 | + dangles[kk, 0] = (y[1]-y[0])/dt | |
1226 | + dangles[kk, nsec-1] = (y[nsec-1]-y[nsec-2])/dt | |
1227 | + dalts = dangles[0] | |
1228 | + dazs = dangles[1] | |
1229 | + dhas = dangles[2] | |
1230 | + ddecs = dangles[3] | |
1231 | + draequinoxs = dangles[4] | |
1232 | + ddecequinoxs = dangles[5] | |
1233 | + dparallactics = dangles[12] | |
1047 | 1234 | # --- observability and visibility |
1048 | 1235 | visibilitys = np.zeros(nsec) |
1049 | 1236 | observabilitys = np.zeros(nsec) |
... | ... | @@ -1082,13 +1269,21 @@ class Ephemeris(EphemerisException, GuitastroTools): |
1082 | 1269 | observabilitys[kk] = alts[kk]/altmaxi*100 |
1083 | 1270 | # --- dictionary |
1084 | 1271 | eph = {} |
1085 | - eph['night'] = night | |
1086 | - eph['home'] = self.home.gps | |
1087 | - eph['target'] = target | |
1088 | - eph['ndate'] = ndate | |
1272 | + eph['header'] = {} | |
1273 | + eph['header']['night'] = night | |
1274 | + eph['header']['home'] = self.home.gps | |
1275 | + eph['header']['target'] = target | |
1276 | + eph['header']['ndate'] = ndate | |
1277 | + eph['header']['duskelev'] = duskelev | |
1278 | + eph['header']['preference'] = preference | |
1279 | + eph['header']['temperature_k'] = temp_k | |
1280 | + eph['header']['pressure_pa'] = pres_pa | |
1281 | + eph['header']['humidity_rel'] = rel_humidity | |
1282 | + eph['header']['wavelength_nm'] = wavelength_nm | |
1089 | 1283 | eph['jd'] = jjds |
1090 | 1284 | eph['alt'] = alts |
1091 | 1285 | eph['az'] = azs |
1286 | + eph['parallactic'] = parallactics | |
1092 | 1287 | eph['ha'] = has |
1093 | 1288 | eph['dec'] = decs |
1094 | 1289 | eph['ra_equinox'] = raequinoxs |
... | ... | @@ -1102,8 +1297,106 @@ class Ephemeris(EphemerisException, GuitastroTools): |
1102 | 1297 | eph['visibility'] = visibilitys |
1103 | 1298 | eph['observability'] = observabilitys |
1104 | 1299 | eph['horizon'] = horizons |
1300 | + if speed and len(jjds)>=3: | |
1301 | + eph['dalt'] = dalts | |
1302 | + eph['daz'] = dazs | |
1303 | + eph['dparallactic'] = dparallactics | |
1304 | + eph['dha'] = dhas | |
1305 | + eph['ddec'] = ddecs | |
1306 | + eph['dra_equinox'] = draequinoxs | |
1307 | + eph['ddec_equinox'] = ddecequinoxs | |
1105 | 1308 | return eph |
1106 | 1309 | |
1310 | + def hadec2ephem(self, ha, dec, date, **kwargs): | |
1311 | + if 'humidity' in kwargs.keys(): | |
1312 | + rel_humidity = kwargs['humidity'] | |
1313 | + else: | |
1314 | + rel_humidity = 0.6 | |
1315 | + if 'wavelength_nm' in kwargs.keys(): | |
1316 | + wavelength_nm = kwargs['wavelength_nm'] | |
1317 | + else: | |
1318 | + wavelength_nm = 600 | |
1319 | + # --- | |
1320 | + jd = Date(date).jd() | |
1321 | + obstime = Time(jd, format="jd") | |
1322 | + location = EarthLocation(lat=self.home.latitude*u.deg, lon=self.home.longitude*u.deg, height=self.home.altitude*u.m) | |
1323 | + temp_k, pres_pa = self.altitude2tp(self.home.altitude) | |
1324 | + temperature = (temp_k + 273.15) * u.deg_C | |
1325 | + pressure = pres_pa * u.pascal | |
1326 | + relative_humidity = rel_humidity | |
1327 | + obswl = wavelength_nm * u.nm | |
1328 | + # --- | |
1329 | + c = SkyCoord(frame="hadec", ha=ha*u.deg, dec=dec*u.deg, obstime=obstime, location=location) | |
1330 | + # --- | |
1331 | + altaz = c.transform_to(AltAz(obstime=obstime, location=location, pressure=pressure, temperature=temperature, relative_humidity=relative_humidity, obswl=obswl)) | |
1332 | + alt = altaz.alt.deg | |
1333 | + az = altaz.az.deg | |
1334 | + az -= 180 | |
1335 | + # --- | |
1336 | + radec = c.icrs | |
1337 | + ra_equinox, dec_equinox = radec.ra.deg, radec.dec.deg | |
1338 | + # --- | |
1339 | + ephem = {} | |
1340 | + ephem['header'] = {} | |
1341 | + ephem['header']['home'] = self.home.gps | |
1342 | + ephem['header']['temperature_k'] = temp_k | |
1343 | + ephem['header']['pressure_pa'] = pres_pa | |
1344 | + ephem['header']['humidity_rel'] = rel_humidity | |
1345 | + ephem['header']['wavelength_nm'] = wavelength_nm | |
1346 | + ephem['jd'] = jd | |
1347 | + ephem['ra_equinox'] = ra_equinox | |
1348 | + ephem['dec_equinox'] = dec_equinox | |
1349 | + ephem['ha'] = ha | |
1350 | + ephem['dec'] = dec | |
1351 | + ephem['az'] = az | |
1352 | + ephem['alt'] = alt | |
1353 | + return ephem | |
1354 | + | |
1355 | + def altaz2ephem(self, az, alt, date, **kwargs): | |
1356 | + if 'humidity' in kwargs.keys(): | |
1357 | + rel_humidity = kwargs['humidity'] | |
1358 | + else: | |
1359 | + rel_humidity = 0.6 | |
1360 | + if 'wavelength_nm' in kwargs.keys(): | |
1361 | + wavelength_nm = kwargs['wavelength_nm'] | |
1362 | + else: | |
1363 | + wavelength_nm = 600 | |
1364 | + # --- | |
1365 | + jd = Date(date).jd() | |
1366 | + obstime = Time(jd, format="jd") | |
1367 | + location = EarthLocation(lat=self.home.latitude*u.deg, lon=self.home.longitude*u.deg, height=self.home.altitude*u.m) | |
1368 | + temp_k, pres_pa = self.altitude2tp(self.home.altitude) | |
1369 | + temperature = (temp_k + 273.15) * u.deg_C | |
1370 | + pressure = pres_pa * u.pascal | |
1371 | + relative_humidity = rel_humidity | |
1372 | + obswl = wavelength_nm * u.nm | |
1373 | + # --- | |
1374 | + azg = az + 180 | |
1375 | + c = SkyCoord(frame="altaz", az=azg*u.deg, alt=alt*u.deg, obstime=obstime, location=location) | |
1376 | + # --- | |
1377 | + hadec = c.transform_to(HADec(obstime=obstime, location=location, pressure=pressure, temperature=temperature, relative_humidity=relative_humidity, obswl=obswl)) | |
1378 | + ha = hadec.ha.deg | |
1379 | + dec = hadec.dec.deg | |
1380 | + # --- | |
1381 | + radec = c.icrs | |
1382 | + ra_equinox, dec_equinox = radec.ra.deg, radec.dec.deg | |
1383 | + # --- | |
1384 | + ephem = {} | |
1385 | + ephem['header'] = {} | |
1386 | + ephem['header']['home'] = self.home.gps | |
1387 | + ephem['header']['temperature_k'] = temp_k | |
1388 | + ephem['header']['pressure_pa'] = pres_pa | |
1389 | + ephem['header']['humidity_rel'] = rel_humidity | |
1390 | + ephem['header']['wavelength_nm'] = wavelength_nm | |
1391 | + ephem['jd'] = jd | |
1392 | + ephem['ra_equinox'] = ra_equinox | |
1393 | + ephem['dec_equinox'] = dec_equinox | |
1394 | + ephem['ha'] = ha | |
1395 | + ephem['dec'] = dec | |
1396 | + ephem['az'] = az | |
1397 | + ephem['alt'] = alt | |
1398 | + return ephem | |
1399 | + | |
1107 | 1400 | # ##################################################################### |
1108 | 1401 | # ##################################################################### |
1109 | 1402 | # ##################################################################### |
... | ... | @@ -1114,8 +1407,8 @@ class Ephemeris(EphemerisException, GuitastroTools): |
1114 | 1407 | |
1115 | 1408 | if __name__ == "__main__": |
1116 | 1409 | |
1117 | - default = 11 | |
1118 | - example = input(f"Select the example (0 to 11) ({default}) ") | |
1410 | + default = 13 | |
1411 | + example = input(f"Select the example (0 to 13) ({default}) ") | |
1119 | 1412 | try: |
1120 | 1413 | example = int(example) |
1121 | 1414 | except: |
... | ... | @@ -1237,6 +1530,20 @@ if __name__ == "__main__": |
1237 | 1530 | |
1238 | 1531 | if example == 11: |
1239 | 1532 | """ |
1533 | + Simple ask to HADEC | |
1534 | + """ | |
1535 | + eph = Ephemeris() | |
1536 | + eph.set_home("guitalens") | |
1537 | + name = "My object" | |
1538 | + target = "HADEC 12 34 32.12 -01 45 46.2" | |
1539 | + #target_type = eph.radec(target, target_type_only=True) | |
1540 | + #ra, dec, equinox, epoch= eph.radec(target) | |
1541 | + #print(f"{name} ra={ra:.6f} dec={dec:.6f} equinox={equinox} epoch={epoch}") | |
1542 | + ra, dec, equinox, epoch, dra, ddec = eph.radec_speed(target) | |
1543 | + print(f"{name} ra={ra:.6f} dec={dec:.6f} dra={dra*3600:.5f} ddec={ddec*3600:.5f} equinox={equinox} epoch={epoch}") | |
1544 | + | |
1545 | + if example == 12: | |
1546 | + """ | |
1240 | 1547 | Compute the ephemeris of a target along all a night |
1241 | 1548 | """ |
1242 | 1549 | import matplotlib.pyplot as plt |
... | ... | @@ -1250,6 +1557,7 @@ if __name__ == "__main__": |
1250 | 1557 | # --- |
1251 | 1558 | eph = Ephemeris() |
1252 | 1559 | eph.set_home("guitalens") |
1560 | + nsec = 86400 | |
1253 | 1561 | night = "20230320" |
1254 | 1562 | preference = "bestelev" |
1255 | 1563 | duskelev = -7 |
... | ... | @@ -1259,22 +1567,28 @@ if __name__ == "__main__": |
1259 | 1567 | hor = Horizon(eph.home) |
1260 | 1568 | hor.horizon_altaz = [(0,40), (180,0), (360,40)] |
1261 | 1569 | hor_az, hor_elev = hor.horizon_altaz |
1570 | + # --- Test nsec | |
1571 | + if False: | |
1572 | + nsec = 3 | |
1573 | + night = "2023-03-20T12:00:00" | |
1262 | 1574 | # --- sun |
1263 | 1575 | target = "sun" |
1264 | 1576 | t0 = time.time() |
1265 | - ephem_sun = eph.target2night(target, night) | |
1577 | + ephem_sun = eph.target2night(target, night, None, None, nsec=nsec) | |
1266 | 1578 | dt = time.time()-t0 |
1267 | 1579 | print(f"SUN dt={dt}") |
1268 | 1580 | # --- moon |
1269 | 1581 | target = "moon" |
1270 | 1582 | t0 = time.time() |
1271 | - ephem_moon = eph.target2night(target, night) | |
1583 | + ephem_moon = eph.target2night(target, night, None, None, nsec=nsec) | |
1272 | 1584 | dt = time.time()-t0 |
1273 | 1585 | print(f"MOON dt={dt}") |
1274 | 1586 | # --- target |
1275 | 1587 | target = "RADEC 4h56m -12d23m" |
1588 | + target = "HADEC 2h +16d" | |
1276 | 1589 | t0 = time.time() |
1277 | - ephem = eph.target2night(target, night, ephem_sun, ephem_moon, horizon=hor, preference=preference, duskelev=duskelev) | |
1590 | + speed = True | |
1591 | + ephem = eph.target2night(target, night, ephem_sun, ephem_moon, horizon=hor, preference=preference, duskelev=duskelev, speed=speed, nsec=nsec) | |
1278 | 1592 | dt = time.time()-t0 |
1279 | 1593 | print(f"TARGET dt={dt}") |
1280 | 1594 | hours, date = compute_hours(ephem) |
... | ... | @@ -1286,3 +1600,11 @@ if __name__ == "__main__": |
1286 | 1600 | plt.grid() |
1287 | 1601 | plt.ylabel('Degrees') |
1288 | 1602 | plt.xlabel(f'Hours since {date}') |
1603 | + | |
1604 | + if example == 13: | |
1605 | + """ | |
1606 | + Transform HADEC or ALTAZ to ephem (a dict of all coordinates) | |
1607 | + """ | |
1608 | + eph = Ephemeris() | |
1609 | + eph.set_home("guitalens") | |
1610 | + ephem = eph.altaz2ephem(-90, 0, "now") | ... | ... |
src/guitastro/filenames.py
... | ... | @@ -1630,14 +1630,16 @@ class FileNames(FileNamesException, GuitastroTools): |
1630 | 1630 | jd_frac_noon = - self._longiau_deg / 360.0 |
1631 | 1631 | #print(f"jd_frac_noon={jd_frac_noon}") |
1632 | 1632 | # --- UTC JD |
1633 | - if date.lower()=="now": | |
1634 | - date_iso = datetime.datetime.utcnow().isoformat() | |
1635 | - #date_iso = '2021-08-16 10:57:00' | |
1636 | - #print(f"date_iso={date_iso}") | |
1637 | - t = astropy.time.Time(date_iso) | |
1638 | - jd = t.jd | |
1639 | - else: | |
1640 | - jd = Date(date).jd() | |
1633 | + jd = Date(date).jd() | |
1634 | + if False: | |
1635 | + if date.lower()=="now": | |
1636 | + date_iso = datetime.datetime.utcnow().isoformat() | |
1637 | + #date_iso = '2021-08-16 10:57:00' | |
1638 | + #print(f"date_iso={date_iso}") | |
1639 | + t = astropy.time.Time(date_iso) | |
1640 | + jd = t.jd | |
1641 | + else: | |
1642 | + jd = Date(date).jd() | |
1641 | 1643 | #print(f"jd={jd}") |
1642 | 1644 | # --- Noon JD |
1643 | 1645 | jd_noon = jd - jd_frac_noon | ... | ... |
src/guitastro/guitastrotools.py
... | ... | @@ -5,6 +5,7 @@ |
5 | 5 | |
6 | 6 | import doctest |
7 | 7 | import os |
8 | +#import benedict | |
8 | 9 | |
9 | 10 | # --- Configuration dictionary as a "global attribute" |
10 | 11 | conf_guitastro = {} |
... | ... | @@ -14,6 +15,8 @@ conf_guitastro['path'] = os.path.dirname(__file__) |
14 | 15 | conf_guitastro['path_data'] = os.path.abspath(os.path.join(conf_guitastro['path'],"..","..","tests","data")) |
15 | 16 | # - The root directory to record generated data (to executes tests and examples) |
16 | 17 | conf_guitastro['path_products'] = os.path.abspath(os.path.join(conf_guitastro['path'],"..","..","tests","products")) |
18 | +# - The root directory to record generated data (to add data in the documentation) | |
19 | +conf_guitastro['path_docimages'] = os.path.abspath(os.path.join(conf_guitastro['path'],"..","..","docs","source","doc_images")) | |
17 | 20 | if os.path.exists(conf_guitastro['path_products']) == False: |
18 | 21 | try: |
19 | 22 | # case using dev |
... | ... | @@ -25,6 +28,14 @@ if os.path.exists(conf_guitastro['path_products']) == False: |
25 | 28 | conf_guitastro['path_products'] = os.path.join(path_tmp,"guitastro","products") |
26 | 29 | os.makedirs(conf_guitastro['path_products'], exist_ok=True) |
27 | 30 | |
31 | +# ##################################################################### | |
32 | +# ##################################################################### | |
33 | +# ##################################################################### | |
34 | +# Class GuitastroException for all Guitastro exceptions | |
35 | +# ##################################################################### | |
36 | +# ##################################################################### | |
37 | +# ##################################################################### | |
38 | + | |
28 | 39 | class GuitastroException(Exception): |
29 | 40 | """Exception raised for errors in the classes of the package Guitastro. |
30 | 41 | """ |
... | ... | @@ -47,17 +58,28 @@ class GuitastroException(Exception): |
47 | 58 | message_total += message + ". " |
48 | 59 | super().__init__(message_total) |
49 | 60 | |
61 | +# ##################################################################### | |
62 | +# ##################################################################### | |
63 | +# ##################################################################### | |
64 | +# Class GuitastroTools for common tools of Guitastro classes | |
65 | +# ##################################################################### | |
66 | +# ##################################################################### | |
67 | +# ##################################################################### | |
50 | 68 | |
51 | 69 | class GuitastroTools(): |
52 | 70 | """General methods for inheritance of Guitastro classes. |
53 | 71 | """ |
54 | 72 | |
73 | + _gconfig = None | |
74 | + benedict = None | |
75 | + | |
55 | 76 | def copy_data2products(self): |
56 | 77 | """Copy test files from data to products folders. Return the product folder. |
57 | 78 | """ |
58 | 79 | import glob |
59 | 80 | import shutil |
60 | 81 | path_data = conf_guitastro['path_data'] |
82 | + #path_data = self.gconfig('guitastro.path_data') | |
61 | 83 | path_products = conf_guitastro['path_products'] |
62 | 84 | # --- count the number of files corresponding to the genename |
63 | 85 | wildcard = os.path.join(path_data ,"*") |
... | ... | @@ -198,10 +220,10 @@ class GuitastroTools(): |
198 | 220 | two dictionaries params_optional and params_mandatory. |
199 | 221 | """ |
200 | 222 | #print("arg_index={}".format(arg_index)) |
201 | - #print("params_args={}".format(params_args)) | |
202 | - #print("params_optional={}".format(params_optional)) | |
223 | + #print("params_args={}\n{}".format(params_args, "*"*10)) | |
224 | + #print("params_optional={}\n{}".format(params_optional, "*"*10)) | |
203 | 225 | #print("*args={}".format(args)) |
204 | - #print("**kwargs={}".format(kwargs)) | |
226 | + #print("**kwargs={}\n{}".format(kwargs, "*"*10)) | |
205 | 227 | # ========= valid *args |
206 | 228 | argc = len(args) |
207 | 229 | valid = 1 |
... | ... | @@ -218,6 +240,7 @@ class GuitastroTools(): |
218 | 240 | msg = "{} not found amongst args ({})".format(selected_arg,params_args.keys()) |
219 | 241 | if valid==0: |
220 | 242 | raise Exception(msg) |
243 | + #print("parameters={}\n{}".format(parameters, "*"*20)) | |
221 | 244 | # ========= valid **kwargs |
222 | 245 | valid = 0 |
223 | 246 | #if (kwargc==0): |
... | ... | @@ -256,20 +279,25 @@ class GuitastroTools(): |
256 | 279 | value = raw_value |
257 | 280 | dico_param[param_key] = value |
258 | 281 | else: |
259 | - print("- TOTO") | |
282 | + #print("- TOTO") | |
260 | 283 | msg = "{} not found amongst mandatory parameters ({})".format(param_key,params.keys()) |
261 | 284 | raise Exception(msg) |
262 | 285 | # --- input or default values of optional parameters |
263 | 286 | params = params_optional |
287 | + #print(">>> params={}".format(params)) | |
288 | + #print("*** selected_parameters={}".format(selected_parameters)) | |
264 | 289 | for parameter_key in parameters["OPTIONAL"]: |
265 | 290 | params[parameter_key] = parameters["OPTIONAL"][parameter_key] |
291 | + #print(">>> params={}".format(params)) | |
266 | 292 | for param_key in params: |
293 | + #print("** param_key={}".format(param_key)) | |
267 | 294 | if param_key in selected_parameters: |
268 | 295 | raw_value = selected_parameters[param_key] |
269 | 296 | type_conv = params[param_key][0] |
270 | 297 | else: |
271 | 298 | raw_value = params[param_key][1] |
272 | 299 | type_conv = params[param_key][0] |
300 | + #print("* type_conv={}".format(type_conv)) | |
273 | 301 | if type_conv==float and raw_value!=None: |
274 | 302 | value = float(raw_value) |
275 | 303 | elif type_conv==int and raw_value!=None: |
... | ... | @@ -279,9 +307,9 @@ class GuitastroTools(): |
279 | 307 | dico_param[param_key] = value |
280 | 308 | return dico_param |
281 | 309 | |
282 | -# ======================================================== | |
283 | -# === debug methods | |
284 | -# ======================================================== | |
310 | + # ======================================================== | |
311 | + # === debug methods | |
312 | + # ======================================================== | |
285 | 313 | |
286 | 314 | def infos(self, action:str="") -> None: |
287 | 315 | """Get informations about this class |
... | ... | @@ -355,6 +383,186 @@ class GuitastroTools(): |
355 | 383 | if (callable(getattr(self,varname))==True): |
356 | 384 | print(varname) |
357 | 385 | |
386 | + | |
387 | +# ##################################################################### | |
388 | +# ##################################################################### | |
389 | +# ##################################################################### | |
390 | +# Class GuitastroDev to generate automatic Guitastro Python code | |
391 | +# ##################################################################### | |
392 | +# ##################################################################### | |
393 | +# ##################################################################### | |
394 | + | |
395 | +class GuitastroDevException(GuitastroException): | |
396 | + | |
397 | + ERR_SRC_PATH_NOT_FOUND = 0 | |
398 | + ERR_COMP_NAME_TWO_WORDS = 1 | |
399 | + ERR_FILE_NOT_FOUND = 2 | |
400 | + | |
401 | + errors = [""]*3 | |
402 | + errors[ERR_SRC_PATH_NOT_FOUND] = "Source folder not found" | |
403 | + errors[ERR_COMP_NAME_TWO_WORDS] = "Component name must be styled as camel case of two words" | |
404 | + errors[ERR_FILE_NOT_FOUND] = "File not found" | |
405 | + | |
406 | +class GuitastroDev(GuitastroDevException): | |
407 | + """General methods to generate easily Guitastro code | |
408 | + """ | |
409 | + | |
410 | + def create_device_module(self, from_capname, to_capname): | |
411 | + """Create and fill a new source code folder for a device from another existing device folder | |
412 | + | |
413 | + Method for Astroguita developers only. | |
414 | + | |
415 | + ex. create_device_code("Optec", "Astromecca") | |
416 | + | |
417 | + """ | |
418 | + import shutil | |
419 | + # - Impose capitalize | |
420 | + from_capname = from_capname.replace(from_capname[0], from_capname[0].upper(), 1) | |
421 | + to_capname = to_capname.replace(to_capname[0], to_capname[0].upper(), 1) | |
422 | + # - Get lower | |
423 | + from_name = from_capname.lower() | |
424 | + to_name = to_capname.lower() | |
425 | + # - | |
426 | + path_base = os.path.abspath(os.path.join(conf_guitastro['path'], "..", "..", "..")) | |
427 | + prefix = "guitastro_device_" | |
428 | + # --- Set the input and output directories | |
429 | + path_from = os.path.join(path_base, prefix + from_name) | |
430 | + if os.path.exists(path_from) == False: | |
431 | + msg = f"The source folder {path_from} was not found " | |
432 | + raise GuitastroDevException(GuitastroDevException.ERR_SRC_PATH_NOT_FOUND, msg) | |
433 | + path_to = os.path.join(path_base, prefix + to_name) | |
434 | + # --- Copy all the interesting files into the new module | |
435 | + shutil.rmtree(path_to) | |
436 | + ignore = shutil.ignore_patterns('__pycache__', 'build', "de421.bsp") | |
437 | + shutil.copytree(path_from, path_to, ignore=ignore) | |
438 | + # --- List all the files in the module | |
439 | + root = path_to | |
440 | + inpfiles = [os.path.join(path, name) for path, subdirs, files in os.walk(root) for name in files] | |
441 | + # --- Change files with the new module name | |
442 | + inpds = [] | |
443 | + for inpfile in inpfiles: | |
444 | + inpf = os.path.basename(inpfile) | |
445 | + inpd = os.path.dirname(inpfile) | |
446 | + k = inpd.find(from_name) | |
447 | + if k>=0: | |
448 | + if inpd not in inpds: | |
449 | + inpds.append(inpd) | |
450 | + k = inpf.find(from_name) | |
451 | + if k>=0: | |
452 | + #print(inpf) | |
453 | + outf = inpf.replace(from_name, to_name) | |
454 | + outfile = os.path.join(inpd, outf) | |
455 | + os.rename(inpfile, outfile) | |
456 | + # --- Special files | |
457 | + wildcard = os.path.join(path_to, "docs", "source", "doc_images", "generated", "*_guitastro_device_*.png") | |
458 | + import glob | |
459 | + files = glob.glob(wildcard) | |
460 | + for file in files: | |
461 | + os.remove(file) | |
462 | + # --- Change directories with the new module name | |
463 | + for inpd in inpds: | |
464 | + outd = inpd.replace(from_name, to_name) | |
465 | + os.rename(inpd, outd) | |
466 | + # --- List all the files in the module | |
467 | + root = path_to | |
468 | + inpfiles = [os.path.join(path, name) for path, subdirs, files in os.walk(root) for name in files] | |
469 | + # --- Collect all the possibilitiesยฒ to find input name | |
470 | + inpnames = [] | |
471 | + inpnameu = from_name.upper() | |
472 | + inpnamel = len(from_name) | |
473 | + inpfilemods = [] | |
474 | + for inpfile in inpfiles: | |
475 | + try: | |
476 | + with open(inpfile, "r") as fid: | |
477 | + lines = fid.read() | |
478 | + except: | |
479 | + pass | |
480 | + linu = lines.upper() | |
481 | + try: | |
482 | + k0 = linu.index(inpnameu) | |
483 | + inpfilemods.append(inpfile) | |
484 | + #print(f"File {inpfile}:") | |
485 | + except: | |
486 | + k0 = -1 | |
487 | + while k0>0: | |
488 | + inpname = lines[k0:k0+inpnamel] | |
489 | + if inpname not in inpnames: | |
490 | + inpnames.append(inpname) | |
491 | + #print(f"k0={k0} : {inpname}") | |
492 | + try: | |
493 | + k0 = linu.index(inpnameu, k0+1) | |
494 | + except: | |
495 | + k0 = -1 | |
496 | + # --- Replace | |
497 | + for inpfilemod in inpfilemods: | |
498 | + with open(inpfilemod, "r") as fid: | |
499 | + lines = fid.read() | |
500 | + for inpname in inpnames: | |
501 | + if inpname == from_capname: | |
502 | + outname = to_capname | |
503 | + elif inpname.isupper(): | |
504 | + outname = to_name.upper() | |
505 | + else: | |
506 | + outname = to_name | |
507 | + lines = lines.replace(inpname, outname) | |
508 | + with open(inpfilemod, "w") as fid: | |
509 | + fid.write(lines) | |
510 | + return inpnames | |
511 | + | |
512 | + def _split_into_two_words(self, name:str): | |
513 | + """ Split a component name into two words according the camel case | |
514 | + """ | |
515 | + words = [] | |
516 | + k1 = 0 | |
517 | + for k in range(1,len(name)): | |
518 | + if name[k].isupper(): | |
519 | + words.append(name[k1:k]) | |
520 | + k1 = k | |
521 | + words.append(name[k1:]) | |
522 | + if len(words) != 2: | |
523 | + msg = f"The input component {name} has words {words}" | |
524 | + raise GuitastroDevException(GuitastroDevException.ERR_COMP_NAME_TWO_WORDS, msg) | |
525 | + return words | |
526 | + | |
527 | + def create_component(self, from_capname, to_capname): | |
528 | + """Create and fill a new source code file for a component from another existing component file | |
529 | + | |
530 | + Method for Astroguita developers only. | |
531 | + | |
532 | + :Usage: | |
533 | + | |
534 | + :: | |
535 | + | |
536 | + create_component("DetectorFocuser", "MountPointing") | |
537 | + | |
538 | + This example will create the file component_MountPointing.py | |
539 | + """ | |
540 | + #import shutil | |
541 | + # - Impose capitalize | |
542 | + from_capname = from_capname.replace(from_capname[0], from_capname[0].upper(), 1) | |
543 | + to_capname = to_capname.replace(to_capname[0], to_capname[0].upper(), 1) | |
544 | + # - Get two parts: DetectorFocuser -> Detector Focuser | |
545 | + from_words = self._split_into_two_words(from_capname) | |
546 | + to_words = self._split_into_two_words(to_capname) | |
547 | + # - file names | |
548 | + from_file = os.path.join(conf_guitastro['path'], "component_"+from_words[0].lower()+"_"+from_words[1].lower()+".py") | |
549 | + if os.path.exists(from_file) == False: | |
550 | + msg = f"The source file {from_file} was not found " | |
551 | + raise GuitastroDevException(GuitastroDevException.ERR_FILE_NOT_FOUND, msg) | |
552 | + to_file = os.path.join(conf_guitastro['path'], "component_"+to_words[0].lower()+"_"+to_words[1].lower()+".py") | |
553 | + #os.path.remove(to_file) | |
554 | + #shutil.copy(from_file, to_file) | |
555 | + # --- Replace | |
556 | + inpnames = [from_words[0].capitalize(), from_words[1].capitalize(), from_words[0].lower(), from_words[1].lower(), from_words[0].upper(), from_words[1].upper()] | |
557 | + outnames = [to_words[0].capitalize(), to_words[1].capitalize(), to_words[0].lower(), to_words[1].lower(), to_words[0].upper(), to_words[1].upper()] | |
558 | + with open(from_file, "r") as fid: | |
559 | + lines = fid.read() | |
560 | + for inpname, outname in zip(inpnames, outnames): | |
561 | + lines = lines.replace(inpname, outname) | |
562 | + with open(to_file, "w") as fid: | |
563 | + fid.write(lines) | |
564 | + return os.path.basename(to_file) | |
565 | + | |
358 | 566 | # ##################################################################### |
359 | 567 | # ##################################################################### |
360 | 568 | # ##################################################################### |
... | ... | @@ -365,13 +573,35 @@ class GuitastroTools(): |
365 | 573 | |
366 | 574 | if __name__ == "__main__": |
367 | 575 | |
368 | - example = 1 | |
576 | + default = 0 | |
577 | + example = input(f"Select the example (0 to 2) ({default}) ") | |
578 | + try: | |
579 | + example = int(example) | |
580 | + except: | |
581 | + example = default | |
582 | + | |
369 | 583 | print("Example = {}".format(example)) |
370 | 584 | |
371 | - if example == 1: | |
585 | + if example == 0: | |
372 | 586 | """ |
373 | 587 | Instrospection of method documentation. |
374 | 588 | """ |
375 | 589 | gta1 = GuitastroTools() |
376 | 590 | gta1.infos("doc_methods") |
377 | 591 | |
592 | + if example == 1: | |
593 | + """ | |
594 | + Create initial source code of a new device. | |
595 | + """ | |
596 | + gta1 = GuitastroDev() | |
597 | + res = gta1.create_device_module("Optec", "Astromecca") | |
598 | + print(f"res={res}") | |
599 | + | |
600 | + if example == 2: | |
601 | + """ | |
602 | + Create initial source code of a component file. | |
603 | + """ | |
604 | + gta1 = GuitastroDev() | |
605 | + res = gta1.create_component("DetectorFocuser", "MountyPointing") | |
606 | + print(f"res={res}") | |
607 | + | ... | ... |
src/guitastro/mountaxis.py
... | ... | @@ -16,7 +16,17 @@ except: |
16 | 16 | # ##################################################################### |
17 | 17 | # ##################################################################### |
18 | 18 | |
19 | -class Mountaxis(GuitastroTools): | |
19 | +class MountaxisException(GuitastroException): | |
20 | + """Exception raised for errors in the Mountaxis class. | |
21 | + """ | |
22 | + | |
23 | + MOUNTAXIS_TYPE_NOT_FOUND = 0 | |
24 | + | |
25 | + errors = [""]*1 | |
26 | + errors[MOUNTAXIS_TYPE_NOT_FOUND] = "Mount axis type not found" | |
27 | + | |
28 | + | |
29 | +class Mountaxis(MountaxisException, GuitastroTools): | |
20 | 30 | """ |
21 | 31 | Class to define an axis of a motor. |
22 | 32 | |
... | ... | @@ -103,12 +113,18 @@ class Mountaxis(GuitastroTools): |
103 | 113 | REAL = 0 |
104 | 114 | SIMU = 1 |
105 | 115 | |
106 | - # === Axis types enc | |
107 | - BASE = 0 | |
108 | - POLAR = 1 | |
109 | - ROT = 2 | |
110 | - YAW = 3 # equivalent to az = second BASE | |
111 | - AXIS_MAX = 4 | |
116 | + # === Axis types | |
117 | + axes = [] | |
118 | + axes.append(["HA", "b"]) | |
119 | + axes.append(["DEC", "p"]) | |
120 | + axes.append(["AZ", "b"]) | |
121 | + axes.append(["ELEV", "p"]) | |
122 | + axes.append(["YAW", "y"]) | |
123 | + axes.append(["ROLL", "b"]) | |
124 | + axes.append(["PITCH", "p"]) | |
125 | + axes.append(["ROT", "r"]) | |
126 | + AXIS_MAX = len(axes) | |
127 | + symbols = list({s for t, s in axes}) | |
112 | 128 | |
113 | 129 | # === Axis motion state |
114 | 130 | MOTION_STATE_UNKNOWN = -1 |
... | ... | @@ -502,36 +518,73 @@ class Mountaxis(GuitastroTools): |
502 | 518 | |
503 | 519 | return self._real |
504 | 520 | |
505 | - def _set_axis_type(self, axis_type:str) -> int: | |
521 | + def _set_axis_type(self, axis_type:str): | |
506 | 522 | """ |
507 | 523 | Set type and mechanical position of an axis on the mount. |
524 | + | |
508 | 525 | - BASE : Azimut or hour angle axis, |
509 | 526 | - POLAR : Elevation or declination axix, |
510 | 527 | - ROT : Derotator system for non equatorial mount (if equiped), |
511 | - - YAW : Equivalent to secondary azymtuh base (for Alt-Alt mount). | |
512 | - | |
513 | - :param axis_type : BASE = 0, POLAR = 1, ROT = 2, YAW = 3 | |
514 | - :returns: Error if value is not a real. | |
515 | - :rtype: int | |
516 | - """ | |
517 | - | |
528 | + - YAW : Equivalent to secondary azimut base (for Alt-Alt mount). | |
529 | + - ROLL : Equivalent to primary azimut base (for Alt-Alt mount). | |
530 | + | |
531 | + """ | |
532 | + axis_type = axis_type.upper() | |
533 | + indx = 0 | |
534 | + found = False | |
535 | + for axe in self.axes: | |
536 | + axe_type, axe_symbol = axe | |
537 | + if axis_type == axe_type: | |
538 | + self._axis_index = indx | |
539 | + self._symbol = axe_symbol | |
540 | + found = True | |
541 | + break | |
542 | + indx += 1 | |
543 | + if found == False: | |
544 | + axe_ts = [axe_t for axe_t, axe_symbol in self.axes] | |
545 | + msg = f"The mount axis type {axis_type} was not found amongst {axe_ts}" | |
546 | + raise MountaxisException(MountaxisException.MOUNTAXIS_TYPE_NOT_FOUND, msg) | |
518 | 547 | self._axis_type = axis_type |
519 | 548 | return self.NO_ERROR |
520 | 549 | |
521 | - def _get_axis_type(self) -> int: | |
522 | - """ | |
523 | - Get type and mechanical position of an axis on the mount. | |
550 | + def _get_axis_type(self) -> str: | |
551 | + """Get type and mechanical position of an axis on the mount. | |
552 | + | |
524 | 553 | - BASE : Azimut or hour angle axis, |
525 | 554 | - POLAR : Elevation or declination axix, |
526 | 555 | - ROT : Derotator system for non equatorial mount (if equiped), |
527 | - - YAW : Equivalent to secondary azymtuh base (for Alt-Alt mount). | |
556 | + - YAW : Equivalent to secondary azimut base (for Alt-Alt mount). | |
557 | + - ROLL : Equivalent to primary azimut base (for Alt-Alt mount). | |
528 | 558 | |
529 | - :returns: BASE = 0, POLAR = 1, ROT = 2, YAW = 3 | |
530 | - :rtype: int | |
531 | - """ | |
559 | + Returns: | |
560 | + | |
561 | + The axis type as a string. | |
532 | 562 | |
563 | + """ | |
533 | 564 | return self._axis_type |
534 | 565 | |
566 | + def _get_axis_index(self) -> int: | |
567 | + """ | |
568 | + Get the axis index of the mount. | |
569 | + | |
570 | + Returns: | |
571 | + | |
572 | + Index integer | |
573 | + | |
574 | + """ | |
575 | + return self._axis_index | |
576 | + | |
577 | + def _get_symbol(self) -> str: | |
578 | + """ | |
579 | + Get the axis symbol of the mount. | |
580 | + | |
581 | + Returns: | |
582 | + | |
583 | + Symbol amongst 'b', 'p', 'r' | |
584 | + | |
585 | + """ | |
586 | + return self._symbol | |
587 | + | |
535 | 588 | def _set_inc_per_sky_rev(self, inc_per_sky_rev:float): |
536 | 589 | """ |
537 | 590 | .. attention:: |
... | ... | @@ -800,6 +853,8 @@ class Mountaxis(GuitastroTools): |
800 | 853 | |
801 | 854 | name = property(_get_name , _set_name) |
802 | 855 | axis_type = property(_get_axis_type , _set_axis_type) |
856 | + axis_index = property(_get_axis_index ) | |
857 | + symbol = property(_get_symbol ) | |
803 | 858 | latitude = property(_get_latitude , _set_latitude) |
804 | 859 | language_protocol = property(_get_language_protocol , _set_language_protocol) |
805 | 860 | |
... | ... | @@ -832,6 +887,14 @@ class Mountaxis(GuitastroTools): |
832 | 887 | lim_cel_inf = property(_get_lim_cel_inf, _set_lim_cel_inf) |
833 | 888 | lim_cel_sup = property(_get_lim_cel_sup, _set_lim_cel_sup) |
834 | 889 | |
890 | + def symbol2type(self, symbol:str)->str: | |
891 | + """Returns the axis_type from a symbol 'b', 'p', 'r' | |
892 | + """ | |
893 | + for axe in self.axes: | |
894 | + axe_type, axe_symbol = axe | |
895 | + if symbol == axe_symbol and self._axis_type == axe_type: | |
896 | + return axe_type | |
897 | + | |
835 | 898 | def disp(self): |
836 | 899 | """ |
837 | 900 | Get information about an axis and print it on the console. Usefull for debug. |
... | ... | @@ -901,15 +964,15 @@ class Mountaxis(GuitastroTools): |
901 | 964 | msg_rot0 = "pole" |
902 | 965 | msg_pierside_inc2rot = "pierside = sign of rot" |
903 | 966 | if self._latitude<0: |
904 | - msg_rot2ang = "ang = -90 + abs(rot) (Southern hem.)" | |
967 | + msg_rot2cel = "ang = -90 + abs(rot) (Southern hem.)" | |
905 | 968 | msg_ang2rot = "rot = (90 + ang) * pierside (Southern hem.)" |
906 | 969 | else: |
907 | - msg_rot2ang = "ang = 90 - abs(rot) (Northern hem.)" | |
970 | + msg_rot2cel = "ang = 90 - abs(rot) (Northern hem.)" | |
908 | 971 | msg_ang2rot = "rot = (90 - ang) * pierside (Northern hem.)" |
909 | 972 | else: |
910 | 973 | msg_rot0 = "meridian" |
911 | 974 | msg_pierside_inc2rot = "pierside must be given by polar axis" |
912 | - msg_rot2ang = "ang = senseang * rot" | |
975 | + msg_rot2cel = "ang = senseang * rot" | |
913 | 976 | if self._pierside == self.PIERSIDE_POS1: |
914 | 977 | msg_ang2rot = "rot = -ang / senseang" |
915 | 978 | else: |
... | ... | @@ -950,7 +1013,7 @@ class Mountaxis(GuitastroTools): |
950 | 1013 | self.log.print("inc = {:12.1f} : inc is read from encoder ".format(inc)) |
951 | 1014 | self.log.print("rot = {:12.7f} : rot = (inc - inc0) * senseinc / inc_per_deg".format(rot)) |
952 | 1015 | self.log.print("pierside = {:d} : {}".format(pierside, msg_pierside_inc2rot)) |
953 | - self.log.print("ang = {:12.7f} : {} ".format(ang, msg_rot2ang)) | |
1016 | + self.log.print("ang = {:12.7f} : {} ".format(ang, msg_rot2cel)) | |
954 | 1017 | self.log.print("{} {} ANG = {} -> INC".format(20*"-",msg_simu,self.axis_type)) |
955 | 1018 | self.log.print("ang = {:12.7f} : Next target celestial angle {}".format(ang,self.axis_type)) |
956 | 1019 | self.log.print("pierside = {:d} : Next target pier side (+1 or -1)".format(pierside)) |
... | ... | @@ -989,7 +1052,7 @@ class Mountaxis(GuitastroTools): |
989 | 1052 | |
990 | 1053 | inc = self._inc |
991 | 1054 | rot, pierside = self.inc2rot(inc) |
992 | - ang = self.rot2ang(rot, pierside) | |
1055 | + ang = self.rot2cel(rot, pierside) | |
993 | 1056 | self._incsimu = inc |
994 | 1057 | self._rotsimu = rot |
995 | 1058 | self._angsimu = ang |
... | ... | @@ -1026,7 +1089,7 @@ class Mountaxis(GuitastroTools): |
1026 | 1089 | |
1027 | 1090 | inc = self._incsimu |
1028 | 1091 | rot, pierside = self.inc2rot(inc) |
1029 | - ang = self.rot2ang(rot, pierside) | |
1092 | + ang = self.rot2cel(rot, pierside) | |
1030 | 1093 | self._inc = inc |
1031 | 1094 | self._rot = rot |
1032 | 1095 | self._ang = ang |
... | ... | @@ -1143,7 +1206,7 @@ class Mountaxis(GuitastroTools): |
1143 | 1206 | self._pierside = pierside |
1144 | 1207 | return rot, pierside |
1145 | 1208 | |
1146 | - def rot2ang(self, rot:float, pierside:int, save:int=SAVE_NONE) -> float: | |
1209 | + def rot2cel(self, rot:float, pierside:int, save:int=SAVE_NONE) -> float: | |
1147 | 1210 | """ |
1148 | 1211 | Calculation of ang from rot and pierside. |
1149 | 1212 | |
... | ... | @@ -1182,7 +1245,7 @@ class Mountaxis(GuitastroTools): |
1182 | 1245 | |
1183 | 1246 | :: |
1184 | 1247 | |
1185 | - >>> axisb.rot2ang(10, axisb.PIERSIDE_POS1, axisb.SAVE_NONE) | |
1248 | + >>> axisb.rot2cel(10, axisb.PIERSIDE_POS1, axisb.SAVE_NONE) | |
1186 | 1249 | |
1187 | 1250 | """ |
1188 | 1251 | # compute apparent ang |
... | ... | @@ -1228,7 +1291,7 @@ class Mountaxis(GuitastroTools): |
1228 | 1291 | self._rot = rot |
1229 | 1292 | return ang |
1230 | 1293 | |
1231 | - def ang2rot(self, ang:float, pierside:int=PIERSIDE_POS1, save:int=SAVE_NONE) -> float: | |
1294 | + def ang2rot(self, ang:float, dang:float, pierside:int=PIERSIDE_POS1, save:int=SAVE_NONE) -> float: | |
1232 | 1295 | """ |
1233 | 1296 | Calculation rot from ang and pierside. |
1234 | 1297 | |
... | ... | @@ -1269,17 +1332,21 @@ class Mountaxis(GuitastroTools): |
1269 | 1332 | |
1270 | 1333 | >>> axisb.ang2rot(-10, axisb.PIERSIDE_POS1, axisb.SAVE_NONE) |
1271 | 1334 | |
1335 | + pierside must not be 0! | |
1272 | 1336 | """ |
1273 | 1337 | # compute apparent rot |
1274 | 1338 | rot = 0 |
1339 | + drot = dang | |
1275 | 1340 | if self._axis_type=="DEC" or self._axis_type=="ELEV": |
1276 | 1341 | if self._latitude<0: |
1277 | 1342 | # --- southern hemisphere |
1278 | 1343 | rot = (90 + ang) * pierside |
1344 | + drot *= -1 | |
1279 | 1345 | else: |
1280 | 1346 | # --- nothern hemisphere |
1281 | 1347 | rot = (90 - ang) * pierside |
1282 | 1348 | if self._axis_type=="HA" or self._axis_type=="AZ": |
1349 | + drot *= pierside | |
1283 | 1350 | if pierside==self.PIERSIDE_POS2: |
1284 | 1351 | ang -= 180 |
1285 | 1352 | if self._latitude<0: |
... | ... | @@ -1293,15 +1360,19 @@ class Mountaxis(GuitastroTools): |
1293 | 1360 | rot /= self._senseang |
1294 | 1361 | if save == self.SAVE_AS_SIMU: |
1295 | 1362 | self._angsimu = ang |
1363 | + self._dangsimu = dang | |
1296 | 1364 | self._piersidesimu = pierside |
1297 | 1365 | self._rotsimu = rot |
1366 | + self._drotsimu = drot | |
1298 | 1367 | elif save == self.SAVE_AS_REAL: |
1299 | 1368 | self._ang = ang |
1369 | + self._dang = dang | |
1300 | 1370 | self._pierside = pierside |
1301 | 1371 | self._rot = rot |
1302 | - return rot | |
1372 | + self._drot = drot | |
1373 | + return rot, drot | |
1303 | 1374 | |
1304 | - def rot2inc(self, rot:float, save:int=SAVE_NONE) -> float : | |
1375 | + def rot2inc(self, rot:float, drot:float, save:int=SAVE_NONE) -> float : | |
1305 | 1376 | """ |
1306 | 1377 | Calculation of inc from rot. |
1307 | 1378 | |
... | ... | @@ -1340,6 +1411,7 @@ class Mountaxis(GuitastroTools): |
1340 | 1411 | |
1341 | 1412 | """ |
1342 | 1413 | inc = self._inc0 + rot * self._inc_per_deg / self._senseinc |
1414 | + dinc = drot * self._inc_per_deg / self._senseinc | |
1343 | 1415 | # --- verify the limits |
1344 | 1416 | inc_per_sky_rev = self._inc_per_sky_rev |
1345 | 1417 | limn = -inc_per_sky_rev/2 |
... | ... | @@ -1351,11 +1423,15 @@ class Mountaxis(GuitastroTools): |
1351 | 1423 | # --- |
1352 | 1424 | if save == self.SAVE_AS_SIMU: |
1353 | 1425 | self._rotsimu = rot |
1426 | + self._drotsimu = drot | |
1354 | 1427 | self._incsimu = inc |
1428 | + self._dincsimu = dinc | |
1355 | 1429 | elif save == self.SAVE_AS_REAL: |
1356 | 1430 | self._rot = rot |
1431 | + self._drot = drot | |
1357 | 1432 | self._inc = inc |
1358 | - return inc | |
1433 | + self._dinc = dinc | |
1434 | + return inc, dinc | |
1359 | 1435 | |
1360 | 1436 | # ===================================================================== |
1361 | 1437 | # ===================================================================== |
... | ... | @@ -1396,6 +1472,10 @@ class Mountaxis(GuitastroTools): |
1396 | 1472 | |
1397 | 1473 | * FRAME (str). "inc" (by default) or "ang" |
1398 | 1474 | |
1475 | + * For all cases of motions: | |
1476 | + | |
1477 | + * NIGHTEPHEM (dict nightephem). {} (by default) else use the night ephemeris as a look up table | |
1478 | + | |
1399 | 1479 | Instanciation of the axis is mandatory. |
1400 | 1480 | |
1401 | 1481 | :Instanciation Usage: |
... | ... | @@ -1422,6 +1502,8 @@ class Mountaxis(GuitastroTools): |
1422 | 1502 | motion_types["CONTINUOUS"] = {"MANDATORY" : {"VELOCITY":[float,1.0]}, "OPTIONAL" : {"DRIFT":[float,0.0]} } |
1423 | 1503 | # --- Dico of optional parameters for all motion types |
1424 | 1504 | param_optionals = {"FRAME":(str,'inc')} ; # inc or ang |
1505 | + # --- Dico of optional parameters for all motion types | |
1506 | + param_optionals = {"NIGHTEPHEM":(dict,{})} ; # inc | |
1425 | 1507 | # ========= Decode params |
1426 | 1508 | self._simu_params = self.decode_args_kwargs(0,motion_types, param_optionals, *args, **kwargs) |
1427 | 1509 | # ========= Decode params | ... | ... |