Skip to content

Commit 941d405

Browse files
authored
Merge pull request #1159 from rhayes777/feature/quick_update
Feature/quick update
2 parents 89d6774 + fb76039 commit 941d405

41 files changed

Lines changed: 309 additions & 214 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

autofit/config/general.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
jax:
22
use_jax: false # If True, PyAutoFit uses JAX internally, whereas False uses normal Numpy.
3-
analysis:
4-
n_cores: 1 # The number of cores a parallelized sum of Analysis classes uses by default.
3+
updates:
4+
iterations_per_quick_update: 1e99 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit.
5+
iterations_per_full_update: 1e99 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow.
56
hpc:
67
hpc_mode: false # If True, use HPC mode, which disables GUI visualization, logging to screen and other settings which are not suited to running on a super computer.
7-
iterations_per_update: 5000 # The number of iterations between every update (visualization, results output, etc) in HPC mode.
8+
iterations_per_quick_update: 1e99 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit.
9+
iterations_per_full_update: 1e99 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow.
810
inversion:
911
check_reconstruction: true # If True, the inversion's reconstruction is checked to ensure the solution of a meshs's mapper is not an invalid solution where the values are all the same.
1012
reconstruction_vmax_factor: 0.5 # Plots of an Inversion's reconstruction use the reconstructed data's bright value multiplied by this factor.

autofit/config/non_linear/mcmc.yaml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,6 @@ Emcee:
2626
number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing.
2727
printing:
2828
silence: false # If True, the default print output of the non-linear search is silcened and not printed by the Python interpreter.
29-
updates:
30-
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
31-
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).
3229
Zeus:
3330
run:
3431
check_walkers: true
@@ -58,6 +55,6 @@ Zeus:
5855
printing:
5956
silence: false # If True, the default print output of the non-linear search is silenced and not printed by the Python interpreter.
6057

61-
updates:
62-
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
58+
iterations_per_full_update: 500 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow.
59+
iterations_per_quick_update: 500 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit.
6360
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).

autofit/config/non_linear/mle.yaml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ PySwarmsGlobal:
2424
number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing.
2525
printing:
2626
silence: false # If True, the default print output of the non-linear search is silcened and not printed by the Python interpreter.
27-
updates:
28-
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
27+
iterations_per_full_update: 500 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow.
28+
iterations_per_quick_update: 500 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit.
2929
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).
3030
PySwarmsLocal:
3131
run:
@@ -46,8 +46,8 @@ PySwarmsLocal:
4646
number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing.
4747
printing:
4848
silence: false # If True, the default print output of the non-linear search is silcened and not printed by the Python interpreter.
49-
updates:
50-
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
49+
iterations_per_full_update: 500 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow.
50+
iterations_per_quick_update: 500 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit.
5151
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).
5252
BFGS:
5353
search:
@@ -70,8 +70,8 @@ BFGS:
7070
number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing.
7171
printing:
7272
silence: false # If True, the default print output of the non-linear search is silcened and not printed by the Python interpreter.
73-
updates:
74-
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
73+
iterations_per_full_update: 500 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow.
74+
iterations_per_quick_update: 500 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit.
7575
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).
7676
LBFGS:
7777
search:
@@ -94,8 +94,8 @@ LBFGS:
9494
number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing.
9595
printing:
9696
silence: false # If True, the default print output of the non-linear search is silcened and not printed by the Python interpreter.
97-
updates:
98-
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
97+
iterations_per_full_update: 500 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow.
98+
iterations_per_quick_update: 500 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit.
9999
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).
100100
Drawer:
101101
search:
@@ -108,6 +108,6 @@ Drawer:
108108
number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing.
109109
printing:
110110
silence: false # If True, the default print output of the non-linear search is silcened and not printed by the Python interpreter.
111-
updates:
112-
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
111+
iterations_per_full_update: 500 # Non-linear search iterations between every full update, which outputs all visuals and result fits (e.g. model.result, search.summary), this exits the search and can be slow.
112+
iterations_per_quick_update: 500 # Non-linear search iterations between every quick update, which just displays the maximum likelihood model fit.
113113
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).

autofit/config/non_linear/nest.yaml

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ DynestyStatic:
3333
force_x1_cpu: false # Force Dynesty to not use Python multiprocessing Pool, which can fix issues on certain operating systems.
3434
printing:
3535
silence: false # If True, the default print output of the non-linear search is silenced and not printed by the Python interpreter.
36-
updates:
37-
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
38-
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).
3936
DynestyDynamic:
4037
search:
4138
bootstrap: null
@@ -64,9 +61,6 @@ DynestyDynamic:
6461
force_x1_cpu: false # Force Dynesty to not use Python multiprocessing Pool, which can fix issues on certain operating systems.
6562
printing:
6663
silence: false # If True, the default print output of the non-linear search is silenced and not printed by the Python interpreter.
67-
updates:
68-
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
69-
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).
7064
Nautilus:
7165
search:
7266
n_live: 3000 # Number of so-called live points. New bounds are constructed so that they encompass the live points.
@@ -93,9 +87,6 @@ Nautilus:
9387
force_x1_cpu: false # Force Dynesty to not use Python multiprocessing Pool, which can fix issues on certain operating systems.
9488
printing:
9589
silence: false # If True, the default print output of the non-linear search is silenced and not printed by the Python interpreter.
96-
updates:
97-
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
98-
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).
9990
UltraNest:
10091
search:
10192
draw_multiple: true
@@ -140,6 +131,3 @@ UltraNest:
140131
number_of_cores: 1 # The number of cores the search is parallelized over by default, using Python multiprocessing.
141132
printing:
142133
silence: false # If True, the default print output of the non-linear search is silenced and not printed by the Python interpreter.
143-
updates:
144-
iterations_per_update: 500 # The number of iterations of the non-linear search performed between every 'update', where an update performs tasks like outputting model.results.
145-
remove_state_files_at_end: true # Whether to remove the savestate of the seach (e.g. the Emcee hdf5 file) at the end to save hard-disk space (results are still stored as PyAutoFit pickles and loadable).

autofit/non_linear/analysis/analysis.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,5 +300,5 @@ def profile_log_likelihood_function(self, paths: AbstractPaths, instance):
300300
"""
301301
pass
302302

303-
def latent_lh_dict_from(self, **kwargs):
304-
return None
303+
def perform_quick_update(self, paths, instance):
304+
raise NotImplementedError

autofit/non_linear/fitness.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
from autofit.jax_wrapper import numpy as xp
1515
from autofit import exc
1616

17+
from autofit.text import text_util
18+
1719

1820
from autofit.mapper.prior_model.abstract import AbstractPriorModel
1921
from autofit.non_linear.paths.abstract import AbstractPaths
@@ -40,7 +42,8 @@ def __init__(
4042
resample_figure_of_merit: float = -xp.inf,
4143
convert_to_chi_squared: bool = False,
4244
store_history: bool = False,
43-
use_jax_vmap : bool = False
45+
use_jax_vmap : bool = False,
46+
iterations_per_quick_update: Optional[int] = None,
4447
):
4548
"""
4649
Interfaces with any non-linear search to fit the model to the data and return a log likelihood via
@@ -120,6 +123,11 @@ def __init__(
120123
if self.use_jax_vmap:
121124
self._call = self._vmap
122125

126+
self.iterations_per_quick_update = iterations_per_quick_update
127+
self.quick_update_max_lh_parameters = None
128+
self.quick_update_max_lh = -xp.inf
129+
self.quick_update_count = 0
130+
123131
if self.paths is not None:
124132
self.check_log_likelihood(fitness=self)
125133

@@ -189,25 +197,128 @@ def call_wrap(self, parameters):
189197
the log-likelihood itself or another objective function value,
190198
depending on configuration.
191199
"""
200+
192201
if self.use_jax_vmap:
193202
if len(np.array(parameters).shape) == 1:
194203
parameters = np.array(parameters)[None, :]
195204

196205
figure_of_merit = self._call(parameters)
197206

207+
if self.convert_to_chi_squared:
208+
figure_of_merit *= -0.5
209+
198210
if self.fom_is_log_likelihood:
199211
log_likelihood = figure_of_merit
200212
else:
201213
log_prior_list = xp.array(self.model.log_prior_list_from_vector(vector=parameters))
202214
log_likelihood = figure_of_merit - xp.sum(log_prior_list)
203215

216+
self.manage_quick_update(parameters=parameters, log_likelihood=log_likelihood)
217+
218+
if self.convert_to_chi_squared:
219+
log_likelihood *= -2.0
220+
204221
if self.store_history:
205222

206223
self.parameters_history_list.append(parameters)
207224
self.log_likelihood_history_list.append(log_likelihood)
208225

209226
return figure_of_merit
210227

228+
def manage_quick_update(self, parameters, log_likelihood):
229+
"""
230+
Manage quick updates during the non-linear search.
231+
232+
A "quick update" is a lightweight visualization of the current best-fit
233+
(maximum likelihood) model parameters. This provides fast feedback on the
234+
progress of the fit without waiting for the full analysis to complete.
235+
236+
It does not require leaving the active non-linear search, and is
237+
therefore faster than the full analysis visualization.
238+
239+
Workflow:
240+
----------
241+
1. Track the number of likelihood evaluations since the last quick update.
242+
2. Identify the maximum log-likelihood from the current batch of evaluations.
243+
- If `log_likelihood` is an array (batched evaluations), find the best
244+
index with `argmax`.
245+
- If it’s just a scalar (single evaluation), treat it as one update.
246+
3. If a new maximum likelihood is found, update:
247+
- `self.quick_update_max_lh` (best log-likelihood value so far).
248+
- `self.quick_update_max_lh_parameters` (corresponding parameter vector).
249+
4. Once the number of evaluations exceeds
250+
`self.iterations_per_quick_update`, generate a quick visualization of
251+
the current max-likelihood model via
252+
`self.analysis.perform_quick_update()`.
253+
254+
Parameters
255+
----------
256+
parameters : array-like
257+
The parameter vectors evaluated in this batch. Shape is typically
258+
(n_batch, n_param).
259+
log_likelihood : float or array-like
260+
The corresponding log-likelihood(s). If batched, must have shape
261+
(n_batch,).
262+
263+
Notes
264+
-----
265+
- Quick updates are optional and controlled by
266+
`self.iterations_per_quick_update`.
267+
- If the `analysis` class does not implement
268+
`perform_quick_update`, the update is silently skipped.
269+
- This mechanism is intended for fast, coarse visualization only,
270+
not detailed science-quality outputs.
271+
"""
272+
273+
if self.iterations_per_quick_update is None:
274+
return
275+
276+
try:
277+
278+
best_idx = xp.argmax(log_likelihood)
279+
best_log_likelihood = log_likelihood[best_idx]
280+
best_parameters = parameters[best_idx]
281+
total_updates = log_likelihood.shape[0]
282+
283+
except AttributeError:
284+
285+
best_log_likelihood = log_likelihood
286+
best_parameters = parameters
287+
total_updates = 1
288+
289+
if best_log_likelihood > self.quick_update_max_lh:
290+
self.quick_update_max_lh = best_log_likelihood
291+
self.quick_update_max_lh_parameters = best_parameters
292+
293+
self.quick_update_count += total_updates
294+
295+
if self.quick_update_count >= self.iterations_per_quick_update:
296+
297+
start_time = time.time()
298+
299+
logger.info("Performing quick update of maximum log likelihood fit image and model.results")
300+
301+
instance = self.model.instance_from_vector(vector=self.quick_update_max_lh_parameters)
302+
303+
try:
304+
self.analysis.perform_quick_update(self.paths, instance)
305+
except NotImplementedError:
306+
pass
307+
308+
result_info = text_util.result_max_lh_info_from(
309+
max_log_likelihood_sample=self.quick_update_max_lh_parameters.tolist(),
310+
max_log_likelihood=self.quick_update_max_lh,
311+
model=self.model,
312+
)
313+
result_info = "\n".join(result_info)
314+
315+
logger.info(result_info)
316+
self.paths.output_model_results(result_info=result_info)
317+
318+
self.quick_update_count = 0
319+
320+
logger.info(f"Quick update complete in {time.time() - start_time} seconds.")
321+
211322
@timeout(timeout_seconds)
212323
def __call__(self, parameters, *kwargs):
213324
"""

autofit/non_linear/paths/abstract.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -438,9 +438,7 @@ def save_summary(
438438
result_info = text_util.result_info_from(
439439
samples=samples,
440440
)
441-
filename = self.output_path / "model.results"
442-
with open_(filename, "w") as f:
443-
f.write(result_info)
441+
self.output_model_results(result_info=result_info)
444442

445443
if latent_samples:
446444
result_info = text_util.result_info_from(
@@ -473,3 +471,10 @@ def _covariance_file(self) -> Path:
473471
@property
474472
def _info_file(self) -> Path:
475473
return self._files_path / "samples_info.json"
474+
475+
def output_model_results(self, result_info):
476+
477+
filename = self.output_path / "model.results"
478+
479+
with open_(filename, "w") as f:
480+
f.write(result_info)

0 commit comments

Comments
 (0)