diff --git a/noctua/utils/check.py b/noctua/utils/check.py
index a697beae44ad6a3566dce0caeda432371793ab2b..9d12f7f2d89b72374eb614d8f585ee6fd688b77a 100644
--- a/noctua/utils/check.py
+++ b/noctua/utils/check.py
@@ -55,6 +55,7 @@ def content_errors(content):
return content_errors_inner
+
def request_errors(func):
'''
Decorator for handling exceptions on a class method.
@@ -77,12 +78,14 @@ def request_errors(func):
res = e.response
if res.status_code == 400:
- # API returns body like: '0x8000100a\r\nParameter(s) missing.\r\n'
+ # API returns body like: '0x8000100a\r\nParameter(s)
+ # missing.\r\n'
try:
# Attempt to parse the API-specific error
body_lines = res.text.strip().split('\r\n')
error_code = body_lines[0]
- error_message = body_lines[1] if len(body_lines) > 1 else "No error message provided."
+ error_message = body_lines[1] if len(
+ body_lines) > 1 else "No error message provided."
msg = f"{name}: Bad Request - API Error {error_code}: {error_message}"
log.error(msg)
this.error.append(msg)
diff --git a/public/.buildinfo b/public/.buildinfo
new file mode 100644
index 0000000000000000000000000000000000000000..f582aa4e89051e6ba9bec5a456d804136859f645
--- /dev/null
+++ b/public/.buildinfo
@@ -0,0 +1,4 @@
+# Sphinx build info version 1
+# This file records the configuration used when building these files. When it is not found, a full rebuild will be done.
+config: d212d744616d270066e51cd9c17679a8
+tags: 645f666f9bcd5a90fca523b33c5a78b7
diff --git a/public/.doctrees/environment.pickle b/public/.doctrees/environment.pickle
new file mode 100644
index 0000000000000000000000000000000000000000..bf9d847cc1bd7f1135433c4e5dacb0f0a54e0111
Binary files /dev/null and b/public/.doctrees/environment.pickle differ
diff --git a/public/.doctrees/index.doctree b/public/.doctrees/index.doctree
new file mode 100644
index 0000000000000000000000000000000000000000..4a971bd26909ba4ec69e7918579f4a2fc8fba096
Binary files /dev/null and b/public/.doctrees/index.doctree differ
diff --git a/public/.doctrees/modules.doctree b/public/.doctrees/modules.doctree
new file mode 100644
index 0000000000000000000000000000000000000000..4a9325d554913295f14afd1ced93dba079501878
Binary files /dev/null and b/public/.doctrees/modules.doctree differ
diff --git a/public/.doctrees/noctua.config.doctree b/public/.doctrees/noctua.config.doctree
new file mode 100644
index 0000000000000000000000000000000000000000..b9310fe3d69043ee7d7335bc755730030d1f3b8a
Binary files /dev/null and b/public/.doctrees/noctua.config.doctree differ
diff --git a/public/.doctrees/noctua.devices.doctree b/public/.doctrees/noctua.devices.doctree
new file mode 100644
index 0000000000000000000000000000000000000000..fdead3bb9a0c049bb34e805e2138a30ac2f2d60f
Binary files /dev/null and b/public/.doctrees/noctua.devices.doctree differ
diff --git a/public/.doctrees/noctua.doctree b/public/.doctrees/noctua.doctree
new file mode 100644
index 0000000000000000000000000000000000000000..9bfe6efb3b7e66f8f7dc34fa82e714613d9997ea
Binary files /dev/null and b/public/.doctrees/noctua.doctree differ
diff --git a/public/.doctrees/noctua.templates.doctree b/public/.doctrees/noctua.templates.doctree
new file mode 100644
index 0000000000000000000000000000000000000000..d50933013c71d2c4eaaca4d40ea734cf3c724ad2
Binary files /dev/null and b/public/.doctrees/noctua.templates.doctree differ
diff --git a/public/.doctrees/noctua.utils.doctree b/public/.doctrees/noctua.utils.doctree
new file mode 100644
index 0000000000000000000000000000000000000000..afd55bfdfa865fd973a747e1a59f5be41a367ea5
Binary files /dev/null and b/public/.doctrees/noctua.utils.doctree differ
diff --git a/public/_modules/astropy/modeling/parameters.html b/public/_modules/astropy/modeling/parameters.html
new file mode 100644
index 0000000000000000000000000000000000000000..e6364575246d711458c7e5e448692a6c9313b88e
--- /dev/null
+++ b/public/_modules/astropy/modeling/parameters.html
@@ -0,0 +1,891 @@
+
+
+
+
+
+# Licensed under a 3-clause BSD style license - see LICENSE.rst
+# pylint: disable=invalid-name
+
+"""
+This module defines classes that deal with parameters.
+
+It is unlikely users will need to work with these classes directly,
+unless they define their own models.
+"""
+
+importfunctools
+importnumbers
+importoperator
+
+importnumpyasnp
+
+fromastropy.unitsimportMagUnit,Quantity,dimensionless_unscaled
+fromastropy.utils.compatimportCOPY_IF_NEEDED
+
+from.utilsimportarray_repr_oneline,get_inputs_and_params
+
+__all__=["InputParameterError","Parameter","ParameterError"]
+
+
+classParameterError(Exception):
+"""Generic exception class for all exceptions pertaining to Parameters."""
+
+
+classInputParameterError(ValueError,ParameterError):
+"""Used for incorrect input parameter values and definitions."""
+
+
+classParameterDefinitionError(ParameterError):
+"""Exception in declaration of class-level Parameters."""
+
+
+def_tofloat(value):
+"""Convert a parameter to float or float array."""
+ ifnp.iterable(value):
+ try:
+ value=np.asanyarray(value,dtype=float)
+ except(TypeError,ValueError):
+ # catch arrays with strings or user errors like different
+ # types of parameters in a parameter set
+ raiseInputParameterError(
+ f"Parameter of {type(value)} could not be converted to float"
+ )
+ elifisinstance(value,Quantity):
+ # Quantities are fine as is
+ pass
+ elifisinstance(value,np.ndarray):
+ # A scalar/dimensionless array
+ value=float(value.item())
+ elifisinstance(value,(numbers.Number,np.number))andnotisinstance(value,bool):
+ value=float(value)
+ elifisinstance(value,bool):
+ raiseInputParameterError(
+ "Expected parameter to be of numerical type, not boolean"
+ )
+ else:
+ raiseInputParameterError(
+ f"Don't know how to convert parameter of {type(value)} to float"
+ )
+ returnvalue
+
+
+# Helpers for implementing operator overloading on Parameter
+
+
+def_binary_arithmetic_operation(op,reflected=False):
+ @functools.wraps(op)
+ defwrapper(self,val):
+ ifself.unitisnotNone:
+ self_value=Quantity(self.value,self.unit)
+ else:
+ self_value=self.value
+
+ ifreflected:
+ returnop(val,self_value)
+ else:
+ returnop(self_value,val)
+
+ returnwrapper
+
+
+def_binary_comparison_operation(op):
+ @functools.wraps(op)
+ defwrapper(self,val):
+ ifself.unitisnotNone:
+ self_value=Quantity(self.value,self.unit)
+ else:
+ self_value=self.value
+
+ returnop(self_value,val)
+
+ returnwrapper
+
+
+def_unary_arithmetic_operation(op):
+ @functools.wraps(op)
+ defwrapper(self):
+ ifself.unitisnotNone:
+ self_value=Quantity(self.value,self.unit)
+ else:
+ self_value=self.value
+
+ returnop(self_value)
+
+ returnwrapper
+
+
+classParameter:
+"""
+ Wraps individual parameters.
+
+ Since 4.0 Parameters are no longer descriptors and are based on a new
+ implementation of the Parameter class. Parameters now (as of 4.0) store
+ values locally (as instead previously in the associated model)
+
+ This class represents a model's parameter (in a somewhat broad sense). It
+ serves a number of purposes:
+
+ #. A type to be recognized by models and treated specially at class
+ initialization (i.e., if it is found that there is a class definition
+ of a Parameter, the model initializer makes a copy at the instance level).
+
+ #. Managing the handling of allowable parameter values and once defined,
+ ensuring updates are consistent with the Parameter definition. This
+ includes the optional use of units and quantities as well as transforming
+ values to an internally consistent representation (e.g., from degrees to
+ radians through the use of getters and setters).
+
+ #. Holding attributes of parameters relevant to fitting, such as whether
+ the parameter may be varied in fitting, or whether there are constraints
+ that must be satisfied.
+
+ See :ref:`astropy:modeling-parameters` for more details.
+
+ Parameters
+ ----------
+ name : str
+ parameter name
+
+ .. warning::
+
+ The fact that `Parameter` accepts ``name`` as an argument is an
+ implementation detail, and should not be used directly. When
+ defining a new `Model` class, parameter names are always
+ automatically defined by the class attribute they're assigned to.
+ description : str
+ parameter description
+ default : float or array
+ default value to use for this parameter
+ unit : `~astropy.units.Unit`
+ if specified, the parameter will be in these units, and when the
+ parameter is updated in future, it should be set to a
+ :class:`~astropy.units.Quantity` that has equivalent units.
+ getter : callable or `None`, optional
+ A function that wraps the raw (internal) value of the parameter
+ when returning the value through the parameter proxy (e.g., a
+ parameter may be stored internally as radians but returned to
+ the user as degrees). The internal value is what is used for
+ computations while the proxy value is what users will interact
+ with (passing and viewing). If ``getter`` is not `None`, then a
+ ``setter`` must also be input.
+ setter : callable or `None`, optional
+ A function that wraps any values assigned to this parameter; should
+ be the inverse of ``getter``. If ``setter`` is not `None`, then a
+ ``getter`` must also be input.
+ fixed : bool
+ if True the parameter is not varied during fitting
+ tied : callable or False
+ if callable is supplied it provides a way to link the value of this
+ parameter to another parameter (or some other arbitrary function)
+ min : float
+ the lower bound of a parameter
+ max : float
+ the upper bound of a parameter
+ bounds : tuple
+ specify min and max as a single tuple--bounds may not be specified
+ simultaneously with min or max
+ mag : bool
+ Specify if the unit of the parameter can be a Magnitude unit or not
+ """
+
+ constraints=("fixed","tied","bounds")
+"""
+ Types of constraints a parameter can have. Excludes 'min' and 'max'
+ which are just aliases for the first and second elements of the 'bounds'
+ constraint (which is represented as a 2-tuple). 'prior' and 'posterior'
+ are available for use by user fitters but are not used by any built-in
+ fitters as of this writing.
+ """
+
+ def__init__(
+ self,
+ name="",
+ description="",
+ default=None,
+ unit=None,
+ getter=None,
+ setter=None,
+ fixed=False,
+ tied=False,
+ min=None,
+ max=None,
+ bounds=None,
+ prior=None,
+ posterior=None,
+ mag=False,
+ ):
+ super().__init__()
+
+ self._model=None
+ self._model_required=False
+
+ if(setterisnotNoneandgetterisNone)or(
+ getterisnotNoneandsetterisNone
+ ):
+ raiseValueError("setter and getter must both be input")
+ self._setter=self._create_value_wrapper(setter,None)
+ self._getter=self._create_value_wrapper(getter,None)
+ self._name=name
+ self.__doc__=self._description=description.strip()
+
+ # We only need to perform this check on unbound parameters
+ ifisinstance(default,Quantity):
+ ifunitisnotNoneandnotunit.is_equivalent(default.unit):
+ raiseParameterDefinitionError(
+ f"parameter default {default} does not have units equivalent to "
+ f"the required unit {unit}"
+ )
+ unit=default.unit
+ default=default.value
+
+ self._default=default
+
+ self._mag=mag
+ self._set_unit(unit,force=True)
+ # Internal units correspond to raw_units held by the model in the
+ # previous implementation. The private _getter and _setter methods
+ # use this to convert to and from the public unit defined for the
+ # parameter.
+ self._internal_unit=None
+ ifnotself._model_required:
+ ifself._defaultisnotNone:
+ self.value=self._default
+ else:
+ self._value=None
+
+ # NOTE: These are *default* constraints--on model instances constraints
+ # are taken from the model if set, otherwise the defaults set here are
+ # used
+ ifboundsisnotNone:
+ ifminisnotNoneormaxisnotNone:
+ raiseValueError(
+ "bounds may not be specified simultaneously with min or "
+ f"max when instantiating Parameter {name}"
+ )
+ else:
+ bounds=(min,max)
+
+ self._fixed=fixed
+ self._tied=tied
+ self._bounds=bounds
+ self._order=None
+
+ self._validator=None
+ self._prior=prior
+ self._posterior=posterior
+
+ self._std=None
+
+ def__set_name__(self,owner,name):
+ self._name=name
+
+ def__len__(self):
+ val=self.value
+ ifval.shape==():
+ return1
+ else:
+ returnval.shape[0]
+
+ def__getitem__(self,key):
+ value=self.value
+ iflen(value.shape)==0:
+ # Wrap the value in a list so that getitem can work for sensible
+ # indices like [0] and [-1]
+ value=[value]
+ returnvalue[key]
+
+ def__setitem__(self,key,value):
+ # Get the existing value and check whether it even makes sense to
+ # apply this index
+ oldvalue=self.value
+ ifisinstance(key,slice):
+ iflen(oldvalue[key])==0:
+ raiseInputParameterError(
+ "Slice assignment outside the parameter dimensions for "
+ f"'{self.name}'"
+ )
+ foridx,valinzip(range(*key.indices(len(self))),value):
+ self.__setitem__(idx,val)
+ else:
+ try:
+ oldvalue[key]=value
+ exceptIndexError:
+ raiseInputParameterError(
+ f"Input dimension {key} invalid for {self.name!r} parameter with "
+ f"dimension {value.shape[0]}"
+ )# likely wrong
+
+ def__repr__(self):
+ args=f"'{self._name}'"
+ args+=f", value={self.value}"
+
+ ifself.unitisnotNoneandself.unit!=dimensionless_unscaled:
+ args+=f", unit={self.unit}"
+
+ forconsinself.constraints:
+ val=getattr(self,cons)
+ ifvalnotin(None,False,(None,None)):
+ # Maybe non-obvious, but False is the default for the fixed and
+ # tied constraints
+ args+=f", {cons}={val}"
+
+ returnf"{self.__class__.__name__}({args})"
+
+ @property
+ defname(self):
+"""Parameter name."""
+ returnself._name
+
+ @property
+ defdefault(self):
+"""Parameter default value."""
+ returnself._default
+
+ @property
+ defvalue(self):
+"""The unadorned value proxied by this parameter."""
+ ifself._getterisNoneandself._setterisNone:
+ value=self._value
+ else:
+ # This new implementation uses the names of internal_unit
+ # in place of raw_unit used previously. The contrast between
+ # internal values and units is that between the public
+ # units that the parameter advertises to what it actually
+ # uses internally.
+ ifself.internal_unit:
+ value=self._getter(
+ self._internal_value,self.internal_unit,self.unit
+ ).value
+ else:
+ value=self._getter(self._internal_value)
+
+ ifvalue.size==1:
+ # return scalar number as np.float64 object
+ returnnp.float64(value.item())
+
+ returnnp.float64(value)
+
+ @value.setter
+ defvalue(self,value):
+ ifisinstance(value,Quantity):
+ raiseTypeError(
+ "The .value property on parameters should be set"
+ " to unitless values, not Quantity objects. To set"
+ "a parameter to a quantity simply set the "
+ "parameter directly without using .value"
+ )
+ ifself._setterisNone:
+ self._value=np.array(value,dtype=np.float64)
+ else:
+ self._internal_value=np.array(self._setter(value),dtype=np.float64)
+
+ @property
+ defunit(self):
+"""
+ The unit attached to this parameter, if any.
+
+ On unbound parameters (i.e. parameters accessed through the
+ model class, rather than a model instance) this is the required/
+ default unit for the parameter.
+ """
+ returnself._unit
+
+ @unit.setter
+ defunit(self,unit):
+ ifself.unitisNone:
+ raiseValueError(
+ "Cannot attach units to parameters that were "
+ "not initially specified with units"
+ )
+ else:
+ raiseValueError(
+ "Cannot change the unit attribute directly, "
+ "instead change the parameter to a new quantity"
+ )
+
+ def_set_unit(self,unit,force=False):
+ ifforce:
+ ifisinstance(unit,MagUnit)andnotself._mag:
+ raiseValueError(
+ "This parameter does not support the magnitude units such as"
+ f" {unit}"
+ )
+ self._unit=unit
+ else:
+ self.unit=unit
+
+ @property
+ definternal_unit(self):
+"""
+ Return the internal unit the parameter uses for the internal value stored.
+ """
+ returnself._internal_unit
+
+ @internal_unit.setter
+ definternal_unit(self,internal_unit):
+"""
+ Set the unit the parameter will convert the supplied value to the
+ representation used internally.
+ """
+ self._internal_unit=internal_unit
+
+ @property
+ definput_unit(self):
+"""Unit for the input value."""
+ ifself.internal_unitisnotNone:
+ returnself.internal_unit
+ elifself.unitisnotNone:
+ returnself.unit
+ else:
+ returnNone
+
+ @property
+ defquantity(self):
+"""
+ This parameter, as a :class:`~astropy.units.Quantity` instance.
+ """
+ ifself.unitisNone:
+ returnNone
+ returnself.value*self.unit
+
+ @quantity.setter
+ defquantity(self,quantity):
+ ifnotisinstance(quantity,Quantity):
+ raiseTypeError(
+ "The .quantity attribute should be set to a Quantity object"
+ )
+ self.value=quantity.value
+ self._set_unit(quantity.unit,force=True)
+
+ @property
+ defshape(self):
+"""The shape of this parameter's value array."""
+ ifself._setterisNone:
+ returnself._value.shape
+ returnself._internal_value.shape
+
+ @shape.setter
+ defshape(self,value):
+ ifisinstance(self.value,np.generic):
+ ifvaluenotin((),(1,)):
+ raiseValueError("Cannot assign this shape to a scalar quantity")
+ else:
+ self.value.shape=value
+
+ @property
+ defsize(self):
+"""The size of this parameter's value array."""
+ returnnp.size(self.value)
+
+ @property
+ defstd(self):
+"""Standard deviation, if available from fit."""
+ returnself._std
+
+ @std.setter
+ defstd(self,value):
+ self._std=value
+
+ @property
+ defprior(self):
+ returnself._prior
+
+ @prior.setter
+ defprior(self,val):
+ self._prior=val
+
+ @property
+ defposterior(self):
+ returnself._posterior
+
+ @posterior.setter
+ defposterior(self,val):
+ self._posterior=val
+
+ @property
+ deffixed(self):
+"""
+ Boolean indicating if the parameter is kept fixed during fitting.
+ """
+ returnself._fixed
+
+ @fixed.setter
+ deffixed(self,value):
+"""Fix a parameter."""
+ ifnotisinstance(value,bool):
+ raiseValueError("Value must be boolean")
+ self._fixed=value
+
+ @property
+ deftied(self):
+"""
+ Indicates that this parameter is linked to another one.
+
+ A callable which provides the relationship of the two parameters.
+ """
+ returnself._tied
+
+ @tied.setter
+ deftied(self,value):
+"""Tie a parameter."""
+ ifnotcallable(value)andvaluenotin(False,None):
+ raiseTypeError("Tied must be a callable or set to False or None")
+ self._tied=value
+
+ @property
+ defbounds(self):
+"""The minimum and maximum values of a parameter as a tuple."""
+ returnself._bounds
+
+ @bounds.setter
+ defbounds(self,value):
+"""Set the minimum and maximum values of a parameter from a tuple."""
+ _min,_max=value
+ if_minisnotNone:
+ ifnotisinstance(_min,(numbers.Number,Quantity)):
+ raiseTypeError("Min value must be a number or a Quantity")
+ ifisinstance(_min,Quantity):
+ _min=float(_min.value)
+ else:
+ _min=float(_min)
+
+ if_maxisnotNone:
+ ifnotisinstance(_max,(numbers.Number,Quantity)):
+ raiseTypeError("Max value must be a number or a Quantity")
+ ifisinstance(_max,Quantity):
+ _max=float(_max.value)
+ else:
+ _max=float(_max)
+
+ self._bounds=(_min,_max)
+
+ @property
+ defmin(self):
+"""A value used as a lower bound when fitting a parameter."""
+ returnself.bounds[0]
+
+ @min.setter
+ defmin(self,value):
+"""Set a minimum value of a parameter."""
+ self.bounds=(value,self.max)
+
+ @property
+ defmax(self):
+"""A value used as an upper bound when fitting a parameter."""
+ returnself.bounds[1]
+
+ @max.setter
+ defmax(self,value):
+"""Set a maximum value of a parameter."""
+ self.bounds=(self.min,value)
+
+ @property
+ defvalidator(self):
+"""
+ Used as a decorator to set the validator method for a `Parameter`.
+ The validator method validates any value set for that parameter.
+ It takes two arguments--``self``, which refers to the `Model`
+ instance (remember, this is a method defined on a `Model`), and
+ the value being set for this parameter. The validator method's
+ return value is ignored, but it may raise an exception if the value
+ set on the parameter is invalid (typically an `InputParameterError`
+ should be raised, though this is not currently a requirement).
+
+ Note: Using this method as a decorator will cause problems with
+ pickling the model. An alternative is to assign the actual validator
+ function to ``Parameter._validator`` (see examples in modeling).
+
+ """
+
+ defvalidator(func,self=self):
+ ifcallable(func):
+ self._validator=func
+ returnself
+ else:
+ raiseValueError(
+ "This decorator method expects a callable.\n"
+ "The use of this method as a direct validator is\n"
+ "deprecated; use the new validate method instead\n"
+ )
+
+ returnvalidator
+
+ defvalidate(self,value):
+"""Run the validator on this parameter."""
+ ifself._validatorisnotNoneandself._modelisnotNone:
+ self._validator(self._model,value)
+
+ defcopy(
+ self,
+ name=None,
+ description=None,
+ default=None,
+ unit=None,
+ getter=None,
+ setter=None,
+ fixed=False,
+ tied=False,
+ min=None,
+ max=None,
+ bounds=None,
+ prior=None,
+ posterior=None,
+ ):
+"""
+ Make a copy of this `Parameter`, overriding any of its core attributes
+ in the process (or an exact copy).
+
+ The arguments to this method are the same as those for the `Parameter`
+ initializer. This simply returns a new `Parameter` instance with any
+ or all of the attributes overridden, and so returns the equivalent of:
+
+ .. code:: python
+
+ Parameter(self.name, self.description, ...)
+
+ """
+ kwargs=locals().copy()
+ delkwargs["self"]
+
+ forkey,valueinkwargs.items():
+ ifvalueisNone:
+ # Annoying special cases for min/max where are just aliases for
+ # the components of bounds
+ ifkeyin("min","max"):
+ continue
+ else:
+ ifhasattr(self,key):
+ value=getattr(self,key)
+ elifhasattr(self,"_"+key):
+ value=getattr(self,"_"+key)
+ kwargs[key]=value
+
+ returnself.__class__(**kwargs)
+
+ @property
+ defmodel(self):
+"""Return the model this parameter is associated with."""
+ returnself._model
+
+ @model.setter
+ defmodel(self,value):
+ self._model=value
+ self._setter=self._create_value_wrapper(self._setter,value)
+ self._getter=self._create_value_wrapper(self._getter,value)
+ ifself._model_required:
+ ifself._defaultisnotNone:
+ self.value=self._default
+ else:
+ self._value=None
+
+ @property
+ def_raw_value(self):
+"""
+ Currently for internal use only.
+
+ Like Parameter.value but does not pass the result through
+ Parameter.getter. By design this should only be used from bound
+ parameters.
+
+ This will probably be removed are retweaked at some point in the
+ process of rethinking how parameter values are stored/updated.
+ """
+ ifself._setter:
+ returnself._internal_value
+ returnself.value
+
+ def_create_value_wrapper(self,wrapper,model):
+"""Wraps a getter/setter function to support optionally passing in
+ a reference to the model object as the second argument.
+ If a model is tied to this parameter and its getter/setter supports
+ a second argument then this creates a partial function using the model
+ instance as the second argument.
+ """
+ ifisinstance(wrapper,np.ufunc):
+ ifwrapper.nin!=1:
+ raiseTypeError(
+ "A numpy.ufunc used for Parameter "
+ "getter/setter may only take one input "
+ "argument"
+ )
+
+ return_wrap_ufunc(wrapper)
+ elifwrapperisNone:
+ # Just allow non-wrappers to fall through silently, for convenience
+ returnNone
+ else:
+ inputs,_=get_inputs_and_params(wrapper)
+ nargs=len(inputs)
+
+ ifnargs==1:
+ pass
+ elifnargs==2:
+ self._model_required=True
+ ifmodelisnotNone:
+ # Don't make a partial function unless we're tied to a
+ # specific model instance
+ model_arg=inputs[1].name
+ wrapper=functools.partial(wrapper,**{model_arg:model})
+ else:
+ raiseTypeError(
+ "Parameter getter/setter must be a function "
+ "of either one or two arguments"
+ )
+
+ returnwrapper
+
+ def__array__(self,dtype=None,copy=COPY_IF_NEEDED):
+ # Make np.asarray(self) work a little more straightforwardly
+ arr=np.asarray(self.value,dtype=dtype)
+
+ ifself.unitisnotNone:
+ arr=Quantity(arr,self.unit,copy=copy,subok=True)
+
+ returnarr
+
+ def__bool__(self):
+ returnbool(np.all(self.value))
+
+ __add__=_binary_arithmetic_operation(operator.add)
+ __radd__=_binary_arithmetic_operation(operator.add,reflected=True)
+ __sub__=_binary_arithmetic_operation(operator.sub)
+ __rsub__=_binary_arithmetic_operation(operator.sub,reflected=True)
+ __mul__=_binary_arithmetic_operation(operator.mul)
+ __rmul__=_binary_arithmetic_operation(operator.mul,reflected=True)
+ __pow__=_binary_arithmetic_operation(operator.pow)
+ __rpow__=_binary_arithmetic_operation(operator.pow,reflected=True)
+ __truediv__=_binary_arithmetic_operation(operator.truediv)
+ __rtruediv__=_binary_arithmetic_operation(operator.truediv,reflected=True)
+ __eq__=_binary_comparison_operation(operator.eq)
+ __ne__=_binary_comparison_operation(operator.ne)
+ __lt__=_binary_comparison_operation(operator.lt)
+ __gt__=_binary_comparison_operation(operator.gt)
+ __le__=_binary_comparison_operation(operator.le)
+ __ge__=_binary_comparison_operation(operator.ge)
+ __neg__=_unary_arithmetic_operation(operator.neg)
+ __abs__=_unary_arithmetic_operation(operator.abs)
+
+
+defparam_repr_oneline(param):
+"""
+ Like array_repr_oneline but works on `Parameter` objects and supports
+ rendering parameters with units like quantities.
+ """
+ out=array_repr_oneline(param.value)
+ ifparam.unitisnotNoneandparam.unit!=dimensionless_unscaled:
+ out=f"{out}{param.unit!s}"
+ returnout
+
+
+def_wrap_ufunc(ufunc):
+ def_wrapper(value,raw_unit=None,orig_unit=None):
+"""
+ Wrap ufuncs to support passing in units
+ raw_unit is the unit of the value
+ orig_unit is the value after the ufunc has been applied
+ it is assumed ufunc(raw_unit) == orig_unit
+ """
+ iforig_unitisnotNone:
+ returnufunc(value)*orig_unit
+ elifraw_unitisnotNone:
+ returnufunc(value*raw_unit)
+
+ returnufunc(value)
+
+ return_wrapper
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/_modules/index.html b/public/_modules/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..48b7b930c7931c19db8c7e0d4cccd2615afb1543
--- /dev/null
+++ b/public/_modules/index.html
@@ -0,0 +1,110 @@
+
+
+
+
+
+
+
+ Overview: module code — Noctua 0.1 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""
+Interface with an Astelco OpenTSI-based device
+"""
+
+# System modules
+importtelnetlib
+
+# Other templates
+from..utilsimportcheck
+from..utils.loggerimportlog
+from.basedeviceimportBaseDevice
+
+
+
+[docs]
+classOpenTSI(BaseDevice):
+'''Base wrapper class for astelco OpenTSI stuff'''
+
+ def__init__(self,url):
+'''Constructor.'''
+
+ super().__init__(url)
+ self.url=url.split(":")[0]orurl
+ self.port=url.split(":")[1]or22
+ self.timeout=3
+ self.connection=True
+
+ @check.telnet_errors
+ def_connect(self):
+'''Setup a telnet connection to the cabinet.'''
+
+ tn=telnetlib.Telnet(self.url,self.port,timeout=3)
+ self.connection=True
+
+ returntn
+
+ @check.telnet_errors
+ def_send(self,get_or_set,message=None):
+'''
+ Connect to the cabinet and send messages to set or get properties
+ '''
+
+ # Login data for cabinet communication.
+ # It is a byte-string.
+ auth=b'auth plain "admin" "admin"\n'
+
+ # Send the message. it can be a string (even a multi-line
+ # string), or a list of strings
+
+ ifisinstance(message,str):
+ # Maybe is multi-line
+ msglist=message.split("\n")
+ # Removing empty elements
+ msglist=[xforxinmsglistifx]
+ elifisinstance(message,list):
+ msglist=message
+
+ # GET: Transforming every string in "1 get string\n2 get string\n"
+ # SET: Transforming every string in "1 set string=val\n2 set
+ # string=val"
+ msg=[f"{i+1}{get_or_set.upper()}{j}\n"fori,
+ jinenumerate(msglist)]
+ # From list to string, and byte-coded
+ msg=bytes("".join(msg),"utf+8")
+
+ # Connect to the cabinet.
+ tn=self._connect()
+ ifnottn:
+ self.connection=False
+ returnNone# {"Error": self.error}
+
+ tn.read_until(b"[ruder]\n")
+ tn.write(auth)
+ tn.read_until(b"AUTH OK 0 0\n")
+
+ # log.debug(msg)
+
+ # Send the message.
+ tn.write(msg)
+
+ # For each command, the *asyncrhronous* answer contains:
+ #
+ # COMMAND OK
+ # DATA INLINE ...
+ # COMMAND COMPLETE
+ #
+ # Append all the buffer line by line to a list.
+
+ tn_answer=[]
+ foriinrange(msg.count(b"\n")*3):
+ tn_answer.append(tn.read_until(b"\n"))
+
+ # Close the cabinet connection.
+ tn.close()
+
+ # log.debug(tn_answer)
+
+ # Keep only the data.
+ tn_values=[sforsintn_answerifb"DATA INLINE"ins]
+
+ # Create a python dictionary with name and value, as they are async.
+ telnet_dict=dict([m[14:-1].decode().split("=")formintn_values])
+
+ # # It can happen that values are missing, i.e. with cabinet off
+ # if get_or_set == "get":
+ # if len(telnet_dict) != len(msglist):
+ # log.warning(f"Astelco asked {len(msglist)} values, {len(telnet_dict)} returned.")
+ # log.debug(f"Asked: {msglist}")
+ # log.debug(f"Returned: {telnet_dict}")
+
+ # Try transforming values to int or float.
+ fork,vintelnet_dict.items():
+ ifget_or_set=="set":
+ print(k,v)
+
+ try:
+ telnet_dict[k]=int(v)
+ exceptBaseException:
+ try:
+ telnet_dict[k]=float(v)
+ exceptBaseException:
+ try:
+ telnet_dict[k]=str(v.replace('"',''))
+ exceptValueErrorase:
+ log.error("Astelco class error:")
+ log.error(e)
+
+ # Order by message, add None if something is missing.
+ res=[telnet_dict.get(m,None)forminmsglist]
+
+ iflen(res)>1:
+ returnres
+ else:
+ returnres[0]
+
+
+[docs]
+ defget(self,message):
+'''Send a telnet message to the cabinet to retrieve a value or a set
+ of values.'''
+
+ returnself._send("get",message)
+
+
+
+[docs]
+ defput(self,key,val):
+'''Send to telnet a key to set a a value.
+ keys and values can be both lists with the same element number.'''
+ ifisinstance(key,list):
+ message=[f"{k}={v}"fork,vinzip(key,val)]
+ else:
+ message=f"{key}={val}"
+
+ returnself._send("set",message)
+
+
+
+
+
+[docs]
+classTelescope(OpenTSI):
+'''Implementation of the Telescope commands mocking my Alpaca
+ Telescope wrapper.'''
+
+ @property
+ deftracking(self):
+'''Tracking status'''
+ message="POINTING.TRACK"
+ res=self.get(message)
+ try:
+ self._tracking=Trueifint(res)elseFalse
+ exceptTypeErrorase:
+ self._tracking=None
+ returnself._tracking
+
+ @tracking.setter
+ deftracking(self,b):
+'''(Re)Start/Stop tracking of telescope in function of currently
+ configured target'''
+''' 1 slew to currently configured target and start tracking'''
+''' 0 stop tracking'''
+ message="POINTING.TRACK"
+ track=1ifbelse0
+ self.put(message,track)
+ returnself.tracking
+
+
+[docs]
+ deftrack(self):
+'''More direct way to start tracking'''
+ self.tracking=True
+ returnself._tracking
+
+
+
+[docs]
+ defabort(self):
+'''Direct way to stop tracking'''
+'''Check if need to add other abort'''
+ self.tracking=False
+ returnself._tracking
+
+
+ @property
+ defis_moving(self):
+'''Check the motion state of the telescope'''
+'''Bit 0 One or more axes are moving
+ Bit 1 Trajectories are being executed
+ Bit 2 Movement is blocked (e.g. by limits)
+ Bit 3 Telescope is on target position (i.e. stopped or, if tracking, following the target)
+ Bit 4 Movement is restricted by given jerk/acceleration/velocity limits
+ Bit 5 Telescope is “unparked”, i.e. moved on startup position by READY=1 or PARK=0
+ Bit 6 Telescope is “parked”, i.e. moved on park position by READY=0 or PARK=1'''
+ message="TELESCOPE.MOTION_STATE"
+ res=self.get(message)
+ returnres
+
+ @property
+ defstatus(self):
+'''need to check exit and error from get function'''
+ message="TELESCOPE.STATUS.LIST"
+ res=self.get(message)
+ returnres
+
+ @property
+ defstate(self):
+
+ message="TELESCOPE.STATUS.GLOBAL"
+ res=self.get(message)
+ returnres
+
+
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+'''
+Sequencer module for Observation Blocks (OBs) in JSON format
+'''
+
+# System modules
+importargparse
+importimportlib
+importimportlib.resources
+importjson
+importos
+importsignal
+importsys
+fromdatetimeimportdatetime
+frompathlibimportPath# New import
+
+# Other templates
+from.utils.loggerimportlog# Corrected relative import
+
+
+
+[docs]
+classSequencer():
+'''
+ Manage a sequence of JSON Observation Blocks (OBs)
+ '''
+
+ def__init__(self,embedded=True):
+'''Constructor'''
+ self.ob_file=None
+ self.ob=[]# Content of the ob_file
+ self.error=[]
+ self.tpl=None# The running template
+ self.params={}# The parameters of the running template
+ self.quitting=False
+ self.embedded=embedded# To manage sys.exit if called alone
+ signal.signal(signal.SIGINT,self.interrupt)
+ self.original_sigint=signal.getsignal(signal.SIGINT)
+
+
+[docs]
+ defload_script(self,template_script_path,params={}):
+'''Load a python file where the template is implemented'''
+
+ # template_script_path could be a full path or a module path like "noctua.templates.lampsoff"
+ # For simplicity with argparse, we'll assume it's a file path for now,
+ # or a simple name if it's a bundled template.
+
+ script_path=Path(template_script_path)
+ ifnotscript_path.exists():
+ # Try to resolve it as a module within noctua.templates
+ try:
+ # Create noctua/templates/__init__.py if it doesn't exist
+ withimportlib.resources.path(f"noctua.templates",f"{template_script_path}.py")asp:
+ script_path=p
+ log.info(
+ f"Loading script '{template_script_path}' from package resources.")
+ exceptFileNotFoundError:
+ log.error(
+ f"Template script '{template_script_path}' not found as file or package resource.")
+ self.error.append(
+ f"Template script '{template_script_path}' not found.")
+ return
+
+ # The stem of the script file is used as the template name
+ # This matches how templates are named in JSON OBs
+ module_stem_name=script_path.stem
+
+ self.ob={"template":module_stem_name,"params":params}
+ self.ob_file=str(script_path)# Store the path for logging/reference
+ log.info(
+ f"SEQUENCER: Loaded script {
+self.ob_file} with params {params}")
+
+
+
+[docs]
+ defload_file(self,ob_file_path_str):
+'''Load a JSON file with an OB'''
+ ob_file_path=Path(ob_file_path_str)
+ self.ob_file=str(ob_file_path)
+
+ try:
+ withopen(ob_file_path,encoding="utf-8")asjsonfile:
+ log.info(f"SEQUENCER: Loading the ob from file {self.ob_file}")
+ self.error=[]
+ self.ob=json.load(jsonfile)
+ exceptjson.decoder.JSONDecodeErrorase:
+ msg=f"SEQUENCER: Probably malformed json in {self.ob_file}"
+ log.error(msg)
+ log.error(e)
+ self.error.append(msg)
+ self.error.append(str(e))
+ exceptUnicodeDecodeErrorase:
+ msg=f"SEQUENCER: Not a text file: {self.ob_file}"
+ log.error(msg)
+ log.error(e)
+ self.error.append(msg)
+ self.error.append(str(e))
+ exceptFileNotFoundError:
+ msg=f"SEQUENCER: OB file not found: {self.ob_file}"
+ log.error(msg)
+ self.error.append(msg)
+ exceptExceptionase:
+ msg=f"SEQUENCER: Not handled exception while loading {
+self.ob_file}"
+ log.error(msg)
+ log.error(e)
+ self.error.append(msg)
+ self.error.append(str(e))
+ ifnotself.embedded:
+ raiseSystemExit(e)frome
+
+
+
+[docs]
+ defexecute(self):
+'''Execute the template(s) contained in the loaded OB'''
+ ifnotself.ob:
+ log.error("SEQUENCER: No OB loaded to execute.")
+ self.error.append("No OB loaded.")
+ return
+
+ self.quitting=False
+ ob_list=self.obifisinstance(self.ob,list)else[self.ob]
+
+ log.info(
+ f"There are {
+len(ob_list)} templates in the OB: {
+self.ob_file}")
+ log.debug(ob_list)
+
+ now=datetime.utcnow()
+
+ fortemplate_iteminob_list:
+ ifself.quitting:
+ log.info("SEQUENCER: Execution quitting.")
+ break
+ log.info("--------------------------------------------")
+ try:
+ self.error=[]# Reset errors for this template execution
+
+ template_name=template_item.get("template")
+ ifnottemplate_name:
+ log.error(
+ "SEQUENCER: Template item missing 'template' key.")
+ self.error.append("Template item missing 'template' key.")
+ continue# Skip to next template if malformed
+
+ # Importing the module given its name in the json.
+ full_module_name=f"noctua.templates.{template_name}"
+ try:
+ tplmodule=importlib.import_module(full_module_name)
+ exceptModuleNotFoundErrorase:
+ log.error(
+ f"SEQUENCER: Template module '{full_module_name}' not found: {e}")
+ self.error.append(
+ f"Template module '{full_module_name}' not found.")
+ continue
+
+ # Every template module has a Template() class.
+ self.tpl=tplmodule.Template()
+ self.params=template_item.get("params",{})
+
+ # Run the template given its parameters in the json.
+ self.tpl.run(self.params)
+
+ exceptAttributeErrorase:
+ msg=f"SEQUENCER: Attribute Error in template '{template_name}'"
+ log.error(msg)
+ log.error(e,exc_info=True)# Log full traceback
+ self.error.append(msg)
+ self.error.append(str(e))
+
+ exceptKeyErrorase:
+ msg=f"SEQUENCER: Parameter '{e}' is not defined for template '{template_name}'"
+ log.error(msg)
+ self.error.append(msg)
+
+ exceptExceptionase:# Catch other unexpected errors during template execution
+ msg=f"SEQUENCER: Unexpected error during execution of template '{template_name}'"
+ log.error(msg)
+ log.error(e,exc_info=True)
+ self.error.append(msg)
+ self.error.append(str(e))
+
+ log.info("--------------------------------------------")
+ log.debug(
+ f"Total elapsed time for OB {
+self.ob_file}: {
+(
+datetime.utcnow()-
+now).total_seconds():.3f}s")
+
+
+
+[docs]
+ definterrupt(self,signum,frame):
+'''Intercept a CTRL+C instead of raising a KeyboardInterrupt exception
+ in order to be able to operate on the template class, for
+ example to modify a pause attribute or call a method.
+ '''
+
+ # signal.signal(signal.SIGINT, self.original_sigint)
+
+ sys.stderr.write("\n--- INTERRUPT ---\n")
+ sys.stderr.write("(P)ause, (R)esume, (N)ext template, (Q)uit:\n")
+ sys.stderr.flush()
+
+ raw_line=sys.stdin.readline()
+
+ try:
+ # answer = input(
+ # "(P)ause, (R)esume, (N)ext template, (Q)uit:\n")
+
+ ifnotraw_line:# THIS IS THE CHECK FOR EOF (Ctrl+D)
+ msg="SEQUENCER: CTRL-D (EOF) detected with readline!"
+ log.critical(msg)
+ ifnotself.embedded:
+ log.info("SEQUENCER: EOF in non-embedded mode. Forcing exit.")
+ ifhasattr(
+ self,
+ 'tpl')andself.tplandhasattr(
+ self.tpl,
+ 'abort'):
+ try:
+ self.tpl.abort()
+ exceptException:
+ pass# Best effort
+ os._exit(1)
+ else:
+ log.info("SEQUENCER: EOF in embedded mode. Signaling quit.")
+ self.quit()
+ return# Exit the interrupt handler
+
+ answer=raw_line.strip().lower()
+
+ ifanswer.startswith('p'):
+ self.pause()
+ elifanswer.startswith('r'):
+ self.resume()
+ elifanswer.startswith('n'):# Abort current template, move to next
+ self.abort_current_template()
+ elifanswer.startswith('q'):# Quit entire OB
+ self.quit()
+ else:
+ log.info("SEQUENCER: No valid action taken on interrupt.")
+
+ # except EOFError: # CTRL+D
+ # msg = "SEQUENCER: CTRL-D detected! Exiting for real!"
+ # log.critical(msg)
+ # if not self.embedded:
+ # os._exit(1) # Force exit if not embedded
+ # else:
+ # self.quit() # Attempt graceful quit if embedded
+
+ except(KeyboardInterrupt,RuntimeError):# If Ctrl+C is hit again during input
+ log.warning(
+ "SEQUENCER: Interrupted during interrupt handling. Quitting.")
+ self.quit()
+ exceptExceptionase:# Add a broad catch here for debugging
+ sys.stderr.write(
+ f"ERROR IN INTERRUPT HANDLER: {
+type(e).__name__}: {e}\n")
+ log.error("Error in interrupt handler",exc_info=True)
+ # Fallback to quitting if the handler itself fails
+ self.quit()
+ finally:
+ signal.signal(signal.SIGINT,self.interrupt)# Re-hook
+
+
+
+[docs]
+ defpause(self):
+'''
+ Put the pause attribute in the running template to True.
+ It will be checked in a template method.
+ '''
+ ifself.tplandhasattr(self.tpl,'paused'):
+ msg=f"SEQUENCER: pausing template {
+self.tpl.name} in ob {
+self.ob_file}"
+ log.warning(msg)
+ self.tpl.paused=True
+ else:
+ log.warning(
+ "SEQUENCER: No active template to pause or template doesn't support pausing.")
+
+
+
+[docs]
+ defresume(self):
+'''
+ Restore the pause attribute in the template class to its
+ original state. It will be checked in a template method.
+ '''
+ ifself.tplandhasattr(self.tpl,'paused'):
+ msg=f"SEQUENCER: resuming template {
+self.tpl.name} in ob {
+self.ob_file}"
+ log.warning(msg)
+ self.tpl.paused=False
+ else:
+ log.warning(
+ "SEQUENCER: No active template to resume or template doesn't support pausing.")
+
+
+
+[docs]
+ defabort_current_template(self):
+'''
+ Abort the currently running template. The sequencer will pass to the
+ following one, if any.
+ '''
+ ifself.tplandhasattr(
+ self.tpl,
+ 'aborted')andhasattr(
+ self.tpl,
+ 'abort'):
+ msg=f"SEQUENCER: Aborting current template {
+self.tpl.name} in OB {
+self.ob_file}."
+ log.error(msg)
+ self.error.append(msg)
+ self.tpl.aborted=True# Signal the template to stop its
+ # current paragraph self.tpl.abort() # Call template's
+ # specific abort logic (e.g., stop camera)
+ else:
+ log.warning(
+ "SEQUENCER: No active template to abort or template doesn't support aborting.")
+
+
+
+[docs]
+ defquit(self):
+'''Abort the execution of the whole OB'''
+
+ msg=f"SEQUENCER: Quit detected during OB {self.ob_file}."
+ log.error(msg)
+ self.error.append(msg)
+ self.quitting=True# Signal the main execution loop to stop
+
+ ifself.tplandhasattr(
+ self.tpl,
+ 'aborted')andhasattr(
+ self.tpl,
+ 'abort'):
+ log.warning(
+ "SEQUENCER: Aborting current template as part of quitting.")
+ self.abort_current_template()
+
+ ifnotself.embedded:
+ log.info("SEQUENCER: Exiting CLI.")
+ sys.exit(1)# Exit if running standalone
+
+
+
+
+
+[docs]
+defcli():
+'''
+ Command line interface for sequencer
+ '''
+
+ parser=argparse.ArgumentParser(
+ description="Noctua Observatory Sequencer: Loads and executes Observation Blocks (OBs) or individual template scripts.",
+ formatter_class=argparse.RawTextHelpFormatter)
+ parser.add_argument(
+ "target",
+ nargs='?',# Makes it optional
+ help=(
+ "Path to the JSON Observation Block (OB) file to execute.\n"
+ "If not provided and --script is not used, help is shown.\n"
+ "Example: defaults/bias.json\n"
+ "If the path is not absolute and the file is not in the current directory,\n"
+ "it will be searched for within the 'noctua.defaults' package resources."
+ )
+ )
+ parser.add_argument(
+ "-s",
+ "--script",
+ metavar="TEMPLATE_NAME_OR_PATH",
+ help=(
+ "Execute a single template script directly.\n"
+ "Provide the name of the template (e.g., 'lampsoff') or a path to a .py file.\n"
+ "If just a name, it's assumed to be in 'noctua.templates'.\n"
+ "Requires --params to be specified.\n"
+ "Example: -s lampsoff --params '{}'"))
+ parser.add_argument(
+ "-p","--params",
+ metavar="JSON_STRING",
+ help=(
+ "JSON string of parameters for the template when using --script.\n"
+ "Example: --params '{\"repeat\": 5, \"filter\": \"R\"}'"
+ ),
+ default="{}"# Default to empty JSON object
+ )
+
+ args=parser.parse_args()
+ seq=Sequencer(embedded=False)# For CLI, always run not embedded
+
+ # If no arguments are given, or only 'target' is missing without --script
+ ifnotargs.targetandnotargs.script:
+ parser.print_help()
+ sys.exit(0)
+
+ ifargs.script:
+ ifnotargs.params:
+ log.error("Error: --params must be provided when using --script.")
+ parser.print_help()
+ sys.exit(1)
+ try:
+ script_params=json.loads(args.params)
+ exceptjson.JSONDecodeError:
+ log.error(
+ f"Error: Invalid JSON string for --params: {args.params}")
+ parser.print_help()
+ sys.exit(1)
+
+ seq.load_script(args.script,script_params)
+ elifargs.target:
+ # Logic to load from CWD or package resources
+ ob_file_path=Path(args.target)
+ ifnotob_file_path.is_absolute()andnotob_file_path.exists():
+ try:
+ # Create noctua/defaults/__init__.py (can be empty) if it
+ # doesn't exist
+ withimportlib.resources.path("noctua.defaults",ob_file_path.name)asp:
+ log.info(
+ f"Loading OB '{
+ob_file_path.name}' from package resources.")
+ seq.load_file(str(p))
+ exceptFileNotFoundError:
+ log.error(
+ f"OB file '{
+args.target}' not found in CWD or package resources (noctua.defaults).")
+ sys.exit(1)
+ else:
+ seq.load_file(args.target)
+ else:
+ # Should not happen due to the check above, but as a safeguard
+ parser.print_help()
+ sys.exit(0)
+
+ ifnotseq.error:# Only execute if loading was successful
+ seq.execute()
+ else:
+ log.error(f"Failed to load OB/Script. Errors: {seq.error}")
+ sys.exit(1)# Exit with error if loading failed
+
+
+
+if__name__=="__main__":
+ # If called alone
+ cli()
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/_modules/noctua/utils/analysis.html b/public/_modules/noctua/utils/analysis.html
new file mode 100644
index 0000000000000000000000000000000000000000..bf66fe724f36a0278aa7566c10e1ce89338dfd2a
--- /dev/null
+++ b/public/_modules/noctua/utils/analysis.html
@@ -0,0 +1,367 @@
+
+
+
+
+
+
+
+ noctua.utils.analysis — Noctua 0.1 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+"""Collection of decorators to check several type of errors in
+specific types of functions.
+
+"""
+
+# System modules
+importsocket
+
+# Third-party modules
+importrequests
+frompyvantagepro.deviceimportNoDeviceException
+
+# Other templates
+from..utils.loggerimportlog
+
+# class ManualRetry(Exception):
+# '''
+# Manually raise an exception, so it is possible to handle a retry
+# '''
+
+
+
+[docs]
+defcontent_errors(content):
+'''
+ Decorator for handling exceptions.
+ Put it on top of the content() method
+ in the shinsapp class.
+ '''
+
+ defcontent_errors_inner(*args,**kwargs):
+'''
+ Modifies the content() function
+ '''
+
+ this=args[0]# It's the "self"
+
+ try:
+'''
+ This is the content method in basetemplate
+ which is implemented in each template.
+ If all goes well, it is executed smoothly.
+ '''
+ content(*args,**kwargs)
+
+ exceptExceptionase:
+'''
+ If an error occurs, the template is paused at the
+ beginning of the following paragraph.
+ '''
+
+ log.error(f"CONTENT: exception {e}")
+ this.paused=True
+
+ returncontent_errors_inner
+
+
+
+[docs]
+defrequest_errors(func):
+'''
+ Decorator for handling exceptions on a class method.
+ Put it on top of the class method.
+ '''
+ defrequest_errors_inner(*args,**kwargs):
+'''
+ Modifies the func() method.
+ '''
+
+ this=args[0]# it's the "self"
+ name=str(args[0].__class__)
+
+ try:
+ this.error=[]
+ res=func(*args,**kwargs)
+ returnres
+
+ exceptrequests.exceptions.HTTPErrorase:
+ res=e.response
+
+ ifres.status_code==400:
+ # API returns body like: '0x8000100a\r\nParameter(s) missing.\r\n'
+ try:
+ # Attempt to parse the API-specific error
+ body_lines=res.text.strip().split('\r\n')
+ error_code=body_lines[0]
+ error_message=body_lines[1]iflen(body_lines)>1else"No error message provided."
+ msg=f"{name}: Bad Request - API Error {error_code}: {error_message}"
+ log.error(msg)
+ this.error.append(msg)
+ except(IndexError,AttributeError):
+ # Fallback for unexpected 400 error format
+ msg=f"{name}: Bad Request with unparsable body."
+ log.error(msg)
+ log.error(f"Raw response: {res.text}")
+ this.error.append(msg)
+
+ elifres.status_code==500:
+ msg=f"{name}: Internal Server Error on device."
+ log.error(msg)
+ log.error(f"Reason: {res.reason}")
+ log.error(f"Response: {res.text}")
+ this.error.append(msg)
+ this.error.append(res.text)
+
+ else:# 404 Not Found, etc.
+ msg=f"{name}: HTTP Error - {e}"
+ log.error(msg)
+ this.error.append(str(e))
+
+ return
+
+ except(requests.exceptions.Timeout,requests.exceptions.ConnectTimeout)ase:
+ msg=f"{name}: Timeout or Connection timeout!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ return[]
+
+ exceptrequests.exceptions.TooManyRedirectsase:
+ msg=f"{name}: Bad request!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ return
+
+ exceptConnectionRefusedErrorase:
+ msg=f"{name}: Connection refused!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ return
+
+ exceptrequests.exceptions.ConnectionErrorase:
+ msg=f"{name}: Connection error!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ return[]
+
+ exceptAttributeErrorase:
+ msg=f"{name}: No data!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ return
+
+ exceptAssertionErrorase:
+ msg=f"{name}: Probably cabinet not ready!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ return
+
+ exceptTypeErrorase:
+ msg=f"{name}: Probably subsystem not responding!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ return
+
+ exceptrequests.exceptions.RequestExceptionase:
+ msg=f"{name}: Generic request exception!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ raiseSystemExit(e)frome
+
+ exceptExceptionase:
+ msg=f"{name}: Not handled error!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ raiseSystemExit(e)frome
+
+ returnrequest_errors_inner
+
+
+
+
+[docs]
+deftelnet_errors(func):
+'''
+ Decorator for handling exceptions on a class method.
+ Put it on top of the class method.
+ '''
+ deftelnet_errors_inner(*args,**kwargs):
+'''
+ Modifies the func() method.
+ '''
+
+ this=args[0]# it's the "self"
+ name=str(args[0].__class__)
+
+ try:
+ this.error=[]
+ returnfunc(*args,**kwargs)
+
+ exceptsocket.gaierrorase:
+ msg=f"{name}: Server not found!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ return
+
+ exceptsocket.timeoutase:
+ msg=f"{name}: Server timeout!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ return
+
+ exceptAttributeErrorase:
+ msg=f"No attribute with this name in {name}"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ return
+
+ exceptBrokenPipeErrorase:
+ msg=f"{name}: Probably sent request to telnet while waiting previous response"
+ log.debug(msg)
+ log.debug(e)
+ log.error(e)
+
+ exceptIOErrorase:
+ msg=f"{name}: Probably failed getting info from telnet"
+ log.debug(msg)
+ log.debug(e)
+ log.error(e)
+
+ exceptExceptionase:
+ msg=f"{name}: Not handled error"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ raiseSystemExit(e)
+
+ returntelnet_errors_inner
+
+
+
+
+[docs]
+defascom_errors(func):
+'''
+ Decorator for handling exceptions on a class method.
+ Put it on top of the class method.
+ '''
+ defascom_errors_inner(*args,**kwargs):
+'''
+ Modifies the func() method.
+ '''
+
+ this=args[0]# it's the "self"
+ name=str(args[0].__class__)
+
+ try:
+ this.error=[]
+ returnfunc(*args,**kwargs)
+
+ exceptExceptionase:
+ msg=f"{name}: Not handled error"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ raiseSystemExit(e)
+
+ returnascom_errors_inner
+
+
+
+
+[docs]
+defmeteo_errors(func):
+'''
+ Decorator for handling exceptions on a class method.
+ Put it on top of the class method.
+ '''
+ defmeteo_errors_inner(*args,**kwargs):
+'''
+ Modifies the func() method.
+ '''
+
+ this=args[0]# it's the "self"
+ name=str(args[0].__class__)
+
+ try:
+ this.error=[]
+ returnfunc(*args,**kwargs)
+
+ exceptNoDeviceExceptionase:
+ msg=f"{name}: Cannot connect to meteo station"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+
+ exceptBrokenPipeErrorase:
+ msg=f"{name}: Probably sent request to meteo station while waiting previous response"
+ log.debug(msg)
+ log.debug(e)
+
+ exceptIOErrorase:
+ msg=f"{name}: Probably failed getting info from Meteo station"
+ log.debug(msg)
+ log.debug(e)
+
+ exceptExceptionase:
+ msg=f"{name}: Not handled error"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ raiseSystemExit(e)
+
+ returnmeteo_errors_inner
+
+
+
+
+[docs]
+defsocket_errors(func):
+'''
+ Decorator for handling exceptions on a class method.
+ Put it on top of the class method.
+ '''
+ defsocket_errors_inner(*args,**kwargs):
+'''
+ Modifies the func() method.
+ '''
+
+ this=args[0]# it's the "self"
+ name=str(args[0].__class__)
+
+ try:
+ this.error=[]
+ returnfunc(*args,**kwargs)
+
+ exceptExceptionase:
+ msg=f"{name}: Not handled exception!"
+ log.error(msg)
+ log.error(e)
+ this.error.append(msg)
+ this.error.append(str(e))
+ return
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/_modules/noctua/utils/data_access_object.html b/public/_modules/noctua/utils/data_access_object.html
new file mode 100644
index 0000000000000000000000000000000000000000..3e7059b0ef1b71c06531bf71de5cfb6da05db552
--- /dev/null
+++ b/public/_modules/noctua/utils/data_access_object.html
@@ -0,0 +1,203 @@
+
+
+
+
+
+
+
+ noctua.utils.data_access_object — Noctua 0.1 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#!/usr/bin/env python
+
+"""Custom format log"""
+
+# System modules
+importdatetime
+importos# Will be used if log_path only returns a directory
+importsys
+
+# Third-party modules
+fromloguruimportlogger
+
+# Other templates
+from.structureimportlog_path
+
+
+
+
+
+
+# This ensures mylog() is called only once.
+log=mylog()
+
+
+
+[docs]
+defmain():
+"""Main function"""
+
+ # log is already initialized globally
+ log.debug("This is a debug message.")
+ log.info("This is an info message.")
+ log.warning("This is a warning message.")
+ log.error("This is an error message.")
+ log.critical("This is a critical message.")
+
+
+
+if__name__=="__main__":
+ main()
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/_modules/noctua/utils/structure.html b/public/_modules/noctua/utils/structure.html
new file mode 100644
index 0000000000000000000000000000000000000000..58844e82e8daef472ef371507bd6699dffa8d1da
--- /dev/null
+++ b/public/_modules/noctua/utils/structure.html
@@ -0,0 +1,246 @@
+
+
+
+
+
+
+
+ noctua.utils.structure — Noctua 0.1 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[docs]
+defdate_folder():
+"""Create a date folder string based on astronomical convention
+ (changes at midday UTC).
+ """
+ now=Time.now()
+ # If current UTC hour is before midday UTC, the "observing night"
+ # belongs to the previous calendar date.
+ ifnow.datetime.hour<12:
+ folder_date_obj=now.datetime.date()-timedelta(days=1)
+ else:
+ folder_date_obj=now.datetime.date()
+ returnfolder_date_obj.strftime("%Y-%m-%d")
+
+
+
+
+[docs]
+defframe_folder(header):
+"""
+ Create a folder depending on the image type in FITS header
+ """
+ frame=header[imagetyp]
+ ifisinstance(frame,int):
+ folder_name=dir_type[frame]
+ else:
+ frame_num_list=[vfork,vinframe_number.items()ifkinframe]
+ ifnotframe_num_list:
+ # Fallback if frame type string is not recognized
+ return"unknown_type"
+ folder_name=dir_type[frame_num_list[0]]
+ returnfolder_name
+
+
+
+
+[docs]
+deffits_path(header,dry=False):
+"""
+ Create a fits file path where the file will be stored
+ """
+
+ root=PROJECT_ROOT/DATA_FOLDER/FITS_FOLDER
+
+ date_str=date_folder()
+ date_path_part=Path(date_str)
+
+ frame_path_part=Path(frame_folder(header))
+ path=root/date_path_part/frame_path_part
+
+ ifnotdry:
+ path.mkdir(parents=True,exist_ok=True)
+ returnpath
+
+
+
+
+[docs]
+deflog_path(dry=False):
+"""
+ Returns the Path object for the log directory.
+ Creates it if it doesn't exist (unless dry=True).
+ """
+
+ path=PROJECT_ROOT/DATA_FOLDER/LOG_FOLDER
+
+ ifnotdry:
+ path.mkdir(parents=True,exist_ok=True)
+
+ # OARPAF.YYYY-MM-DD.foc
+ outfile=f"{FILE_PREFIX}.{LOG_EXT}"
+ outpath=path/outfile
+ returnoutpath
+
+ returnoutpath
+
+
+
+
+[docs]
+deffoc_path(timestamp,dry=False):
+"""
+ Create the focus output text file name and its path
+ """
+
+ path=PROJECT_ROOT/DATA_FOLDER/FOCUS_FOLDER
+
+ ifnotdry:
+ path.mkdir(parents=True,exist_ok=True)
+
+ # OARPAF.YYYY-MM-DD.foc
+ outfile=f"{FILE_PREFIX}.{timestamp}.{FOCUS_EXT}"
+ outpath=path/outfile
+ returnoutpath
+
+
+
+
+[docs]
+defsave_filename(infile_path_str):
+"""
+ Save a fits file in its path with an ESO-style filename.
+ """
+
+ inpath=Path(infile_path_str)
+ header=fits.getheader(inpath)
+
+ # '2021-12-28T20:09:56.163'
+ date_obs_str=header[dateobs]# DATE-OBS from FITS header
+ name_for_file=Time(date_obs_str).isot
+
+ outfile_name=f"{FILE_PREFIX}.{name_for_file}.{FITS_EXT}"
+ outfile=Path(outfile_name)
+
+ outdir=fits_path(header)# This already creates the directory
+ outpath=outdir/outfile
+
+ shutil.copy2(inpath,outpath)
+ returnstr(outpath)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/_modules/noctua/utils/url_stuff.html b/public/_modules/noctua/utils/url_stuff.html
new file mode 100644
index 0000000000000000000000000000000000000000..0afbe36ef963a36bf621c042ff5c35799ffa9158
--- /dev/null
+++ b/public/_modules/noctua/utils/url_stuff.html
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+ noctua.utils.url_stuff — Noctua 0.1 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Intercept a CTRL+C instead of raising a KeyboardInterrupt exception
+in order to be able to operate on the template class, for
+example to modify a pause attribute or call a method.
Names of the parameters that describe models of this type.
+
The parameters in this tuple are in the same order they should be passed in
+when initializing a model of a specific type. Some types of models, such
+as polynomial models, have a different number of parameters depending on
+some other property of the model, such as the degree.
+
When defining a custom model class the value of this attribute is
+automatically set by the ~astropy.modeling.Parameter attributes defined
+in the class body.
Names of the parameters that describe models of this type.
+
The parameters in this tuple are in the same order they should be passed in
+when initializing a model of a specific type. Some types of models, such
+as polynomial models, have a different number of parameters depending on
+some other property of the model, such as the degree.
+
When defining a custom model class the value of this attribute is
+automatically set by the ~astropy.modeling.Parameter attributes defined
+in the class body.