GIRAFFE Pipeline Reference Manual

gimodel.c
1/*
2 * This file is part of the GIRAFFE Pipeline
3 * Copyright (C) 2002-2019 European Southern Observatory
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 */
19
20#ifdef HAVE_CONFIG_H
21# include <config.h>
22#endif
23
24#include <math.h>
25#include <string.h>
26
27#include <cxtypes.h>
28#include <cxmemory.h>
29#include <cxmessages.h>
30#include <cxstrutils.h>
31
32#include <cpl_error.h>
33
34#include "gierror.h"
35#include "gimodels.h"
36
37
46inline static void
47_giraffe_model_set_flag(GiModel *self, cxint idx, cxbool value)
48{
49
50 cx_assert(self != NULL);
51
52 if (self->parameters.flags == NULL) {
53 self->parameters.flags = cx_calloc(self->parameters.count,
54 sizeof(cxint));
55 }
56
57 if (value == TRUE) {
58 if (self->parameters.flags[idx] == 0) {
59 self->parameters.flags[idx] = 1;
60 self->fit.nfree += 1;
61 }
62 }
63 else {
64 if (self->parameters.flags[idx] == 1) {
65 self->parameters.flags[idx] = 0;
66 self->fit.nfree -= 1;
67 }
68 }
69
70 return;
71
72}
73
74
75inline static cxbool
76_giraffe_model_get_flag(const GiModel *self, cxint idx)
77{
78
79 cx_assert(self != NULL);
80
81 if (self->parameters.flags == NULL) {
82 return FALSE;
83 }
84
85 return self->parameters.flags[idx] == 0 ? FALSE : TRUE;
86
87}
88
89
90inline static cxdouble
91_giraffe_compute_rsquare(cxdouble rss, cpl_matrix *y, cxint n)
92{
93
94 register cxint i;
95
96 register cxdouble my = 0.;
97 register cxdouble sy = 0.;
98 register cxdouble ss = 0.;
99
100 cxdouble r = 0.;
101 cxdouble *_y = cpl_matrix_get_data(y);
102
103
104 if (n < 1) {
105 return 0.;
106 }
107
108 for (i = 0; i < n; i++) {
109 my += _y[i];
110 }
111 my /= n;
112
113
114 for (i = 0; i < n; i++) {
115 sy = _y[i] - my;
116 ss += sy * sy;
117 }
118
119 r = rss / ss;
120
121 if (isnan(r)) {
122 return 0.;
123 }
124
125 return 1. - r;
126
127}
128
129
130inline static cxint
131_giraffe_model_fit(GiModel *self, cpl_matrix *x, cpl_matrix *y,
132 cpl_matrix *sigma, cxint ndata, cxint start,
133 cxint stride)
134{
135
136 cxint status = 0;
137
138 cxdouble _chisq = 0.;
139
140 GiFitParams setup;
141
142
143 if ((cpl_matrix_get_nrow(x) != cpl_matrix_get_nrow(y)) ||
144 (cpl_matrix_get_nrow(x) != cpl_matrix_get_nrow(sigma))) {
145 return -128;
146 }
147
148 if (cpl_matrix_get_ncol(x) != self->arguments.count) {
149 return -128;
150 }
151
152
153 /*
154 * Check data selection range
155 */
156
157 if (cpl_matrix_get_nrow(y) <= start + stride * (ndata - 1)) {
158 return -255;
159 }
160
161
162 /*
163 * Setup the fit
164 */
165
166 setup.iterations = self->fit.setup.iterations;
167 setup.tests = self->fit.setup.tests;
168 setup.dchisq = self->fit.setup.delta;
169
170 if (self->fit.covariance != NULL) {
171 cpl_matrix_set_size(self->fit.covariance, self->parameters.count,
172 self->parameters.count);
173 cpl_matrix_fill(self->fit.covariance, 0.);
174 }
175 else {
176 self->fit.covariance = cpl_matrix_new(self->parameters.count,
177 self->parameters.count);
178 }
179
180
181 /*
182 * Fit the data
183 */
184
185 giraffe_error_push();
186
187 status = giraffe_nlfit(x, y, sigma, ndata, self->parameters.values,
188 self->parameters.limits, self->parameters.flags,
189 self->parameters.count, self->fit.covariance,
190 &_chisq, self->model, &setup);
191
192 if (status < 0) {
193
194 if (cpl_error_get_code() == CPL_ERROR_NONE) {
195 giraffe_error_pop();
196 }
197
198 return status;
199
200 }
201
202 if (cpl_error_get_code() != CPL_ERROR_NONE) {
203 return -255;
204 }
205
206 giraffe_error_pop();
207
208 self->fit.df = ndata - self->fit.nfree;
209 self->fit.iterations = status;
210 self->fit.chisq = _chisq;
211 self->fit.rsquare = _giraffe_compute_rsquare(self->fit.chisq, y, ndata);
212
213 return 0;
214
215}
216
217
218GiModel *
219giraffe_model_new(const cxchar *name)
220{
221
222 register cxint i = 0;
223
224 GiModel *self = NULL;
225
226
227 if (name == NULL) {
228 return NULL;
229 }
230
231 while (giraffe_models[i].name != NULL) {
232
233 if (strcmp(name, giraffe_models[i].name) == 0) {
234
235 self = cx_calloc(1, sizeof(GiModel));
236
237 giraffe_error_push();
238
239 giraffe_models[i].ctor(self, &giraffe_models[i]);
240
241 if (cpl_error_get_code() != CPL_ERROR_NONE) {
242
243 giraffe_model_delete(self);
244 self = NULL;
245
246 break;
247
248 }
249
250 break;
251
252 }
253
254 ++i;
255
256 }
257
258 self->fit.setup.iterations = 0;
259 self->fit.setup.tests = 0;
260 self->fit.setup.delta = 0.;
261
262 self->fit.iterations = 0;
263 self->fit.nfree = 0;
264 self->fit.df = 0;
265 self->fit.covariance = NULL;
266
267 return self;
268
269}
270
271
272GiModel *
273giraffe_model_clone(const GiModel *other)
274{
275
276 GiModel *self = NULL;
277
278
279 if (other != NULL) {
280
281 self = giraffe_model_new(other->name);
282
283 /*
284 * The model constructor creates already an a property list and
285 * a matrix as container for the argument and parameter names and
286 * values. We get rid of them here before we create the copies.
287 * This way is simpler than copying each argument and parameter
288 * individually.
289 */
290
291 cpl_propertylist_delete(self->arguments.names);
292 self->arguments.names =
293 cpl_propertylist_duplicate(other->arguments.names);
294
295 cpl_matrix_delete(self->arguments.values);
296 self->arguments.values =
297 cpl_matrix_duplicate(other->arguments.values);
298
299 self->arguments.count = other->arguments.count;
300
301 cx_assert(cpl_propertylist_get_size(self->arguments.names) ==
302 self->arguments.count);
303 cx_assert(cpl_matrix_get_nrow(self->arguments.values) *
304 cpl_matrix_get_ncol(self->arguments.values) ==
305 self->arguments.count);
306
307
308 cpl_propertylist_delete(self->parameters.names);
309 self->parameters.names =
310 cpl_propertylist_duplicate(other->parameters.names);
311
312 cpl_matrix_delete(self->parameters.values);
313 self->parameters.values =
314 cpl_matrix_duplicate(other->parameters.values);
315
316 self->parameters.count = other->parameters.count;
317
318 cx_assert(cpl_propertylist_get_size(self->parameters.names) ==
319 self->parameters.count);
320 cx_assert(cpl_matrix_get_nrow(self->parameters.values) *
321 cpl_matrix_get_ncol(self->parameters.values) ==
322 self->parameters.count);
323
324 self->fit.setup = other->fit.setup;
325 self->fit.iterations = other->fit.iterations;
326 self->fit.nfree = other->fit.nfree;
327 self->fit.df = other->fit.df;
328
329 if (other->fit.covariance == NULL) {
330 self->fit.covariance = NULL;
331 }
332 else {
333 self->fit.covariance =
334 cpl_matrix_duplicate(other->fit.covariance);
335 }
336
337 }
338
339 return self;
340
341}
342
343void
344giraffe_model_delete(GiModel *self)
345{
346
347 if (self) {
348
349 register cxint i = 0;
350
351 while (giraffe_models[i].name != NULL) {
352
353 if (strcmp(self->name, giraffe_models[i].name) == 0) {
354 giraffe_models[i].dtor(self);
355 cx_free(self);
356
357 break;
358 }
359
360 ++i;
361
362 }
363
364 }
365
366 return;
367
368}
369
370
371const cxchar *
372giraffe_model_get_name(const GiModel *self)
373{
374
375 cx_assert(self != NULL);
376 return self->name;
377
378}
379
380
381GiModelType
382giraffe_model_get_type(const GiModel *self)
383{
384
385 cx_assert(self != NULL);
386 return self->type;
387
388}
389
390
391cxsize
392giraffe_model_count_arguments(const GiModel *self)
393{
394
395 cx_assert(self != NULL);
396 return self->arguments.count;
397
398}
399
400
401cxsize
402giraffe_model_count_parameters(const GiModel *self)
403{
404
405 cx_assert(self != NULL);
406 return self->parameters.count;
407
408}
409
410
411const cxchar*
412giraffe_model_argument_name(const GiModel* self, cxsize position)
413{
414
415 const cpl_property* p = NULL;
416
417
418 cx_assert(self != NULL);
419
420 p = cpl_propertylist_get(self->arguments.names, position);
421 if (p == NULL) {
422 return NULL;
423 }
424
425 return cpl_property_get_name(p);
426
427}
428
429
430const cxchar*
431giraffe_model_parameter_name(const GiModel* self, cxsize position)
432{
433
434 const cpl_property* p = NULL;
435
436
437 cx_assert(self != NULL);
438
439 p = cpl_propertylist_get(self->parameters.names, position);
440 if (p == NULL) {
441 return NULL;
442 }
443
444 return cpl_property_get_name(p);
445
446}
447
448
449cxint
450giraffe_model_set_argument(GiModel *self, const cxchar *name, cxdouble value)
451{
452
453 const cxchar *const fctid = "giraffe_model_set_argument";
454
455
456 cx_assert(self != NULL);
457
458 if (name == NULL) {
459 cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
460 return 1;
461 }
462
463 if (!cpl_propertylist_has(self->arguments.names, name)) {
464 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
465 return 1;
466 }
467 else {
468
469 register cxint idx = cpl_propertylist_get_int(self->arguments.names,
470 name);
471
472 cpl_matrix_set(self->arguments.values, idx, 0, value);
473
474 }
475
476 return 0;
477
478}
479
480
481cxdouble
482giraffe_model_get_argument(const GiModel *self, const cxchar *name)
483{
484
485 const cxchar *const fctid = "giraffe_model_get_argument";
486
487 register cxint idx;
488
489
490 cx_assert(self != NULL);
491
492 if (name == NULL) {
493 cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
494 return 0.;
495 }
496
497 if (!cpl_propertylist_has(self->arguments.names, name)) {
498 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
499 return 0.;
500 }
501
502
503 idx = cpl_propertylist_get_int(self->arguments.names, name);
504
505 return cpl_matrix_get(self->arguments.values, idx, 0);
506
507}
508
509
510cxint
511giraffe_model_set_parameter(GiModel *self, const cxchar *name,
512 cxdouble value)
513{
514
515 const cxchar *const fctid = "giraffe_model_set_parameter";
516
517
518 cx_assert(self != NULL);
519
520 if (name == NULL) {
521 cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
522 return 1;
523 }
524
525 if (!cpl_propertylist_has(self->parameters.names, name)) {
526 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
527 return 1;
528 }
529 else {
530
531 register cxint idx = cpl_propertylist_get_int(self->parameters.names,
532 name);
533
534 cpl_matrix_set(self->parameters.values, idx, 0, value);
535
536 }
537
538 return 0;
539
540}
541
542
543cxdouble
544giraffe_model_get_parameter(const GiModel *self, const cxchar *name)
545{
546
547 const cxchar *const fctid = "giraffe_model_get_parameter";
548
549 register cxint idx;
550
551
552 cx_assert(self != NULL);
553
554 if (name == NULL) {
555 cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
556 return 0.;
557 }
558
559 if (!cpl_propertylist_has(self->parameters.names, name)) {
560 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
561 return 0.;
562 }
563
564
565 idx = cpl_propertylist_get_int(self->parameters.names, name);
566
567 return cpl_matrix_get(self->parameters.values, idx, 0);
568
569}
570
571
572cxint
573giraffe_model_freeze_parameter(GiModel *self, const cxchar *name)
574{
575
576 const cxchar *const fctid = "giraffe_model_freeze_parameter";
577
578
579 if (self == NULL) {
580 cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
581 return 1;
582 }
583
584 if (name == NULL) {
585 cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
586 return 1;
587 }
588
589 if (!cpl_propertylist_has(self->parameters.names, name)) {
590 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
591 return 1;
592 }
593 else {
594
595 register cxint idx = cpl_propertylist_get_int(self->parameters.names,
596 name);
597
598 _giraffe_model_set_flag(self, idx, FALSE);
599
600 }
601
602 return 0;
603
604}
605
606
607cxint
608giraffe_model_thaw_parameter(GiModel *self, const cxchar *name)
609{
610
611 const cxchar *const fctid = "giraffe_model_thaw_parameter";
612
613
614 cx_assert(self != NULL);
615
616 if (name == NULL) {
617 cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
618 return 1;
619 }
620
621 if (!cpl_propertylist_has(self->parameters.names, name)) {
622 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
623 return 1;
624 }
625 else {
626
627 register cxint idx = cpl_propertylist_get_int(self->parameters.names,
628 name);
629
630 _giraffe_model_set_flag(self, idx, TRUE);
631
632 }
633
634 return 0;
635
636}
637
638
639cxbool
640giraffe_model_frozen_parameter(const GiModel *self, const cxchar *name)
641{
642
643 const cxchar *const fctid = "giraffe_model_frozen_parameter";
644
645 register cxint idx;
646
647
648 cx_assert(self != NULL);
649
650 if (name == NULL) {
651 cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
652 return FALSE;
653 }
654
655 if (!cpl_propertylist_has(self->parameters.names, name)) {
656 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
657 return FALSE;
658 }
659
660
661 idx = cpl_propertylist_get_int(self->parameters.names, name);
662
663 return _giraffe_model_get_flag(self, idx) == FALSE;
664
665}
666
667
668cxint
669giraffe_model_freeze(GiModel *self)
670{
671
672 register cxint i;
673
674
675 cx_assert(self != NULL);
676
677 for (i = 0; i < cpl_propertylist_get_size(self->parameters.names); i++) {
678
679 const cpl_property *p = cpl_propertylist_get(self->parameters.names,
680 i);
681
682 if (p == NULL) {
683 return 1;
684 }
685
686 _giraffe_model_set_flag(self, cpl_property_get_int(p), FALSE);
687
688 }
689
690 return 0;
691
692}
693
694
695cxint
696giraffe_model_thaw(GiModel *self)
697{
698
699 register cxint i;
700
701
702 cx_assert(self != NULL);
703
704 for (i = 0; i < cpl_propertylist_get_size(self->parameters.names); i++) {
705
706 const cpl_property *p = cpl_propertylist_get(self->parameters.names,
707 i);
708
709 if (p == NULL) {
710 return 1;
711 }
712
713 _giraffe_model_set_flag(self, cpl_property_get_int(p), TRUE);
714
715 }
716
717 return 0;
718
719}
720
721
722cxint
723giraffe_model_evaluate(const GiModel *self, cxdouble *result, cxint *status)
724{
725
726 const cxchar *const fctid = "giraffe_model_evaluate";
727
728 cxdouble _result = 0.;
729 cxdouble *arg = NULL;
730 cxdouble *par = NULL;
731
732
733 cx_assert(self != NULL);
734
735 arg = cpl_matrix_get_data(self->arguments.values);
736
737 if (arg == NULL) {
738 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
739 return 2;
740 }
741
742 par = cpl_matrix_get_data(self->parameters.values);
743
744 if (par == NULL) {
745 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
746 return 3;
747 }
748
749 giraffe_error_push();
750
751 self->model(&_result, arg, par, self->parameters.count, NULL, NULL);
752
753 if (cpl_error_get_code() != CPL_ERROR_NONE) {
754
755 if (status) {
756 *status = 1;
757 }
758
759 return 4;
760
761 }
762
763 giraffe_error_pop();
764
765 *result = _result;
766 *status = 0;
767
768 return 0;
769
770}
771
772
773cxint
774giraffe_model_set_iterations(GiModel *self, cxint iterations)
775{
776
777 cx_assert(self != NULL);
778
779 if (iterations < 1) {
780 return 1;
781 }
782
783 self->fit.setup.iterations = iterations;
784
785 return 0;
786
787}
788
789
790cxint
791giraffe_model_get_iterations(const GiModel *self)
792{
793
794 cx_assert(self != NULL);
795
796 return self->fit.setup.iterations;
797
798}
799
800
801cxint
802giraffe_model_set_tests(GiModel *self, cxint tests)
803{
804
805 cx_assert(self != NULL);
806
807 if (tests < 1) {
808 return 1;
809 }
810
811 self->fit.setup.tests = tests;
812
813 return 0;
814
815}
816
817
818cxint
819giraffe_model_get_tests(const GiModel *self)
820{
821
822 cx_assert(self != NULL);
823
824 return self->fit.setup.tests;
825
826}
827
828
829cxint
830giraffe_model_set_delta(GiModel *self, cxdouble delta)
831{
832
833 cx_assert(self != NULL);
834
835 if (delta < 0.) {
836 return 1;
837 }
838
839 self->fit.setup.delta = delta;
840
841 return 0;
842
843}
844
845
846cxdouble
847giraffe_model_get_delta(const GiModel *self)
848{
849
850 cx_assert(self != NULL);
851
852 return self->fit.setup.delta;
853
854}
855
856
857cxint
858giraffe_model_get_position(const GiModel *self)
859{
860
861 cx_assert(self != NULL);
862
863 if (self->fit.iterations <= 0) {
864 return -1;
865 }
866
867 return self->fit.iterations;
868
869}
870
871
872cxdouble
873giraffe_model_get_variance(const GiModel *self, const cxchar *name)
874{
875
876 const cxchar *const fctid = "giraffe_model_get_variance";
877
878
879 cxdouble variance = 0.;
880
881
882 cx_assert(self != NULL);
883
884 if (name == NULL) {
885 cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
886 return variance;
887 }
888
889 if (!cpl_propertylist_has(self->parameters.names, name)) {
890 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
891 return variance;
892 }
893 else {
894
895 if (self->fit.covariance == NULL) {
896 cpl_error_set(fctid, CPL_ERROR_DATA_NOT_FOUND);
897 return variance;
898 }
899 else {
900
901 register cxint idx =
902 cpl_propertylist_get_int(self->parameters.names, name);
903
904 variance = cpl_matrix_get(self->fit.covariance, idx, idx);
905
906 }
907
908 }
909
910 return variance;
911
912}
913
914
915cxdouble
916giraffe_model_get_sigma(const GiModel *self, const cxchar *name)
917{
918
919 const cxchar *const fctid = "giraffe_model_get_sigma";
920
921
922 cxdouble sigma = 0.;
923
924
925 cx_assert(self != NULL);
926
927 if (name == NULL) {
928 cpl_error_set(fctid, CPL_ERROR_NULL_INPUT);
929 return sigma;
930 }
931
932 if (!cpl_propertylist_has(self->parameters.names, name)) {
933 cpl_error_set(fctid, CPL_ERROR_ILLEGAL_INPUT);
934 return sigma;
935 }
936 else {
937
938 if (self->fit.covariance == NULL) {
939 cpl_error_set(fctid, CPL_ERROR_DATA_NOT_FOUND);
940 return sigma;
941 }
942 else {
943
944 register cxint idx =
945 cpl_propertylist_get_int(self->parameters.names, name);
946
947 sigma = cpl_matrix_get(self->fit.covariance, idx, idx);
948
949 if (isnan(sigma) || sigma < 0.) {
950 sigma = 0.;
951 }
952 else {
953 sigma = sqrt(sigma);
954 }
955
956 }
957
958 }
959
960 return sigma;
961
962}
963
964
965cxint
966giraffe_model_get_df(const GiModel *self)
967{
968
969 cx_assert(self != NULL);
970
971 return self->fit.df;
972
973}
974
975
976cxdouble
977giraffe_model_get_chisq(const GiModel *self)
978{
979
980 cx_assert(self != NULL);
981
982 return self->fit.chisq;
983
984}
985
986
987cxdouble
988giraffe_model_get_rsquare(const GiModel *self)
989{
990
991 cx_assert(self != NULL);
992
993 return self->fit.rsquare;
994
995}
996
997
998cxint
999giraffe_model_fit(GiModel *self, cpl_matrix *x, cpl_matrix *y,
1000 cpl_matrix *sigma)
1001{
1002
1003 cxint ndata = 0;
1004
1005
1006 cx_assert(self != NULL);
1007
1008 if (x == NULL || y == NULL) {
1009 return -128;
1010 }
1011
1012 if (sigma == NULL) {
1013 return -128;
1014 }
1015
1016 ndata = cpl_matrix_get_nrow(y);
1017
1018 return _giraffe_model_fit(self, x, y, sigma, ndata, 0, 1);
1019
1020}
1021
1022
1023cxint
1024giraffe_model_fit_sequence(GiModel *self, cpl_matrix *x, cpl_matrix *y,
1025 cpl_matrix *sigma, cxint ndata, cxint start,
1026 cxint stride)
1027{
1028
1029 cx_assert(self != NULL);
1030
1031 /* FIXME: Currently only (start == 0 && stride == 1) is supported! */
1032 cx_assert((start == 0) || (stride == 1));
1033
1034
1035 if (x == NULL || y == NULL) {
1036 return -128;
1037 }
1038
1039 if (sigma == NULL) {
1040 return -128;
1041 }
1042
1043 if ((start < 0) || (stride < 0)) {
1044 return -128;
1045 }
1046
1047 return _giraffe_model_fit(self, x, y, sigma, ndata, 0, 1);
1048
1049}
cxint giraffe_nlfit(cpl_matrix *x, cpl_matrix *y, cpl_matrix *sigma, cxint ndata, cpl_matrix *a, cpl_matrix *delta, cxint *ia, cxint ma, cpl_matrix *alpha, cxdouble *chisq, GiFitFunc funcs, const GiFitParams *setup)
Levenberg-Marquardt non-linear fit driver.
Definition: gilevenberg.c:488
Non-linear fit control parameters.
Definition: gilevenberg.h:39
cxint iterations
Definition: gilevenberg.h:45
cxdouble dchisq
Definition: gilevenberg.h:56
cxint tests
Definition: gilevenberg.h:51

This file is part of the GIRAFFE Pipeline Reference Manual 2.16.14.
Documentation copyright © 2002-2006 European Southern Observatory.
Generated on Wed Nov 20 2024 21:40:14 by doxygen 1.9.6 written by Dimitri van Heesch, © 1997-2004