Commit 8f7efae9ede7775861bce951e445364775d6de6a

Authored by Alain Klotz
1 parent 519258d7
Exists in master

Big update for Components.

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
... ... @@ -14,6 +14,7 @@
14 14 matplotlib==3.5.1
15 15 psutil==5.9.0
16 16 lxml
  17 +python-benedict
17 18  
18 19 # installed from matplotlib, ... :
19 20 matplotlib-inline ; sys_platform == "linux"
... ...
src/guitastro/__init__.py
... ... @@ -144,6 +144,7 @@ guitastro.component_mount_pointing
144 144 from __future__ import (absolute_import, division, print_function,
145 145 unicode_literals)
146 146  
  147 +# print(f'Invoking __init__.py for {__name__}')
147 148 import glob
148 149 import os
149 150  
... ...
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__ == &quot;__main__&quot;:
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__ == &quot;__main__&quot;:
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 +
... ...
src/guitastro/device.py 0 โ†’ 100644
... ... @@ -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__ == &quot;__main__&quot;:
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__ == &quot;__main__&quot;:
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__ == &quot;__main__&quot;:
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__ == &quot;__main__&quot;:
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[&#39;path&#39;] = 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[&#39;path_products&#39;]) == 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
... ...