@@ -30,3 +30,134 @@ def residual_map(self) -> aa.ArrayIrregular:
3030 residual_map .append (np .sqrt (min (distances )))
3131
3232 return aa .ArrayIrregular (values = residual_map )
33+
34+
35+ class Fit :
36+ def __init__ (
37+ self ,
38+ data : aa .Grid2DIrregular ,
39+ noise_map : aa .ArrayIrregular ,
40+ model_positions : np .ndarray ,
41+ ):
42+ """
43+ Compare the multiple image points observed to those produced by a model.
44+
45+ Parameters
46+ ----------
47+ data
48+ Observed multiple image coordinates
49+ noise_map
50+ The noise associated with each observed image coordinate
51+ model_positions
52+ The multiple image coordinates produced by the model
53+ """
54+ self .data = data
55+ self .noise_map = noise_map
56+ self .model_positions = model_positions
57+
58+ @staticmethod
59+ def square_distance (
60+ coord1 : np .array ,
61+ coord2 : np .array ,
62+ ) -> float :
63+ """
64+ Calculate the square distance between two points.
65+
66+ Parameters
67+ ----------
68+ coord1
69+ coord2
70+ The two points to calculate the distance between
71+
72+ Returns
73+ -------
74+ The square distance between the two points
75+ """
76+ return (coord1 [0 ] - coord2 [0 ]) ** 2 + (coord1 [1 ] - coord2 [1 ]) ** 2
77+
78+ def log_p (
79+ self ,
80+ data_position : np .array ,
81+ model_position : np .array ,
82+ sigma : float ,
83+ ) -> float :
84+ """
85+ Compute the log probability of a given model coordinate explaining
86+ a given observed coordinate. Accounts for noise, with noiser image
87+ coordinates having a comparatively lower log probability.
88+
89+ Parameters
90+ ----------
91+ data_position
92+ The observed coordinate
93+ model_position
94+ The model coordinate
95+ sigma
96+ The noise associated with the observed coordinate
97+
98+ Returns
99+ -------
100+ The log probability of the model coordinate explaining the observed coordinate
101+ """
102+ chi2 = self .square_distance (data_position , model_position ) / sigma ** 2
103+ return - np .log (np .sqrt (2 * np .pi * sigma ** 2 )) - 0.5 * chi2
104+
105+ def log_likelihood (self ) -> float :
106+ """
107+ Compute the log likelihood of the model image coordinates explaining the observed image coordinates.
108+
109+ This is the sum across all permutations of the observed image coordinates of the log probability of each
110+ model image coordinate explaining the observed image coordinate.
111+
112+ For example, if there are two observed image coordinates and two model image coordinates, the log likelihood
113+ is the sum of the log probabilities:
114+
115+ P(data_0 | model_0) * P(data_1 | model_1)
116+ + P(data_0 | model_1) * P(data_1 | model_0)
117+ + P(data_0 | model_0) * P(data_1 | model_0)
118+ + P(data_0 | model_1) * P(data_1 | model_1)
119+
120+ This is every way in which the coordinates generated by the model can explain the observed coordinates.
121+ """
122+ n_non_nan_model_positions = np .count_nonzero (
123+ ~ np .isnan (
124+ self .model_positions ,
125+ ).any (axis = 1 )
126+ )
127+ n_permutations = n_non_nan_model_positions ** len (self .data )
128+ return - np .log (n_permutations ) + np .sum (self .all_permutations_log_likelihoods ())
129+
130+ def all_permutations_log_likelihoods (self ) -> np .array :
131+ """
132+ Compute the log likelihood for each permutation whereby the model could explain the observed image coordinates.
133+
134+ For example, if there are two observed image coordinates and two model image coordinates, the log likelihood
135+ for each permutation is:
136+
137+ P(data_0 | model_0) * P(data_1 | model_1)
138+ P(data_0 | model_1) * P(data_1 | model_0)
139+ P(data_0 | model_0) * P(data_1 | model_0)
140+ P(data_0 | model_1) * P(data_1 | model_1)
141+
142+ This is every way in which the coordinates generated by the model can explain the observed coordinates.
143+ """
144+ return np .array (
145+ [
146+ np .log (
147+ np .sum (
148+ [
149+ np .exp (
150+ self .log_p (
151+ data_position ,
152+ model_position ,
153+ sigma ,
154+ )
155+ )
156+ for model_position in self .model_positions
157+ if not np .isnan (model_position ).any ()
158+ ]
159+ )
160+ )
161+ for data_position , sigma in zip (self .data , self .noise_map )
162+ ]
163+ )
0 commit comments