From a509c4ac5a7ad60cfec74b5f9e00819e0ab74847 Mon Sep 17 00:00:00 2001 From: Naohiro Nakata <212558462+Nakatanuki812@users.noreply.github.com> Date: Tue, 3 Feb 2026 07:15:45 +0000 Subject: [PATCH 1/2] # 249 Add qlook command for time offset --- decode/qlook.py | 447 +++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + uv.lock | 49 +++++- 3 files changed, 494 insertions(+), 3 deletions(-) diff --git a/decode/qlook.py b/decode/qlook.py index 4de2aa4..706ace9 100644 --- a/decode/qlook.py +++ b/decode/qlook.py @@ -17,7 +17,7 @@ from contextlib import contextmanager from logging import DEBUG, basicConfig, getLogger from pathlib import Path -from typing import Any, Literal, Optional, Sequence, Union +from typing import Any, Literal, Optional, Sequence, Union, Iterable, Dict from warnings import catch_warnings, simplefilter from datetime import datetime @@ -29,7 +29,8 @@ from astropy.units import Quantity from fire import Fire from matplotlib.figure import Figure -from scipy.optimize import curve_fit +from scipy.optimize import curve_fit, minimize +from iminuit import Minuit from . import assign, convert, load, make, plot, select, utils @@ -1451,6 +1452,448 @@ def make_pointing_toml_string(da, fit_res_params_dict, weight) -> str: return toml.dumps(result) +def shift_coords( + da: xr.DataArray, + time_coords: Iterable[str], + time_offset: np.timedelta64 = np.timedelta64(0, "ms"), +) -> xr.DataArray: + """Shifts the time coordinates of an Xarray object + by a specified offset and re-interpolates specified + coordinate variables onto the original time grid. + + Args: + da (xr.DataArray): The input Xarray DataArray to be shifted. + time_coords (Iterable[str]): A list of coordinate names to + be re-interpolated (e.g., ['lat', 'lon']). + time_offset (np.timedelta64): The time offset amount to shift. + Defaults to 0 ms. + + Returns: + xr.DataArray: A new DataArray with shifted + time and re-interpolated coordinates. + """ + shifted_time = da.time + time_offset + temp_obj = da.assign_coords(time=shifted_time) + interpolated_vars = {} + for var_name in time_coords: + var_to_interp = temp_obj.coords[var_name] + interpolated_var = var_to_interp.interp_like( + da, + method="linear", + kwargs={"fill_value": "extrapolate"}, + ) + interpolated_vars[var_name] = interpolated_var + result_obj = da.assign_coords(**interpolated_vars) + return result_obj + + +def gaussfit(cont: xr.DataArray) -> Dict[str, Any]: + """Performs a 2D Gaussian fitting on the input 2D map data. + + Args: + cont (xr.DataArray): The input 2D map data array + (e.g., intensity map). + + Returns: + Dict[str, Any]: A dictionary containing fitting parameters + (peak, x0, y0, etc.), + chi-squared values.Returns an empty dict if fitting fails. + """ + try: + mad = utils.mad(cont).item() + sigma = mad * SIGMA_OVER_MAD + + data = np.array(copy.deepcopy(cont).data) + data[np.isnan(data)] = 0.0 + + x, y = np.meshgrid(np.array(cont["lon"]), np.array(cont["lat"])) + + amp_guess = np.nanmax(data) + + max_idx = np.argmax(data) + x0_guess = x.ravel()[max_idx] + y0_guess = y.ravel()[max_idx] + + x_min, x_max = np.nanmin(x), np.nanmax(x) + y_min, y_max = np.nanmin(y), np.nanmax(y) + sigma_x_guess = (x_max - x_min) / 10.0 + sigma_y_guess = (y_max - y_min) / 10.0 + + if sigma_x_guess <= 0: + sigma_x_guess = 1e-4 + if sigma_y_guess <= 0: + sigma_y_guess = 1e-4 + + def fixed_offset_gaussian(xy, amp, x0, y0, sigma_x, sigma_y, theta): + return gaussian_2d(xy, amp, x0, y0, sigma_x, sigma_y, theta, 0) + + initial_guess = (amp_guess, x0_guess, y0_guess, sigma_x_guess, sigma_y_guess, 0) + bounds = ( + [0, -np.inf, -np.inf, 1e-10, 1e-10, -np.pi], + [np.inf, np.inf, np.inf, np.inf, np.inf, np.pi], + ) + + popt, pcov = curve_fit( + fixed_offset_gaussian, (x, y), data.ravel(), p0=initial_guess, bounds=bounds + ) + perr = np.sqrt(np.diag(pcov)) + + data_fitted = fixed_offset_gaussian((x, y), *popt).reshape(x.shape) + + chi2, reduced_chi2 = calc_chi2( + data, data_fitted, sigma, num_params=len(initial_guess) + ) + + popt_full = np.append(popt, 0) + perr_full = np.append(perr, 0) + + fit_res_params_dict = make_fit_res_params_dict( + popt_full, perr_full, chi2, reduced_chi2 + ) + + param_names = ["peak", "x0", "y0", "sigma_x", "sigma_y", "theta", "offset"] + for i, name in enumerate(param_names): + fit_res_params_dict[name] = popt_full[i] + fit_res_params_dict[f"{name}_err"] = perr_full[i] + + fit_res_params_dict["popt"] = popt_full # type: ignore + + return fit_res_params_dict + except Exception as error: + return {} + + +def get_result( + da: xr.DataArray, + time_offset: float, +) -> Dict[str, Any]: + """Calculates the Gaussian fitting result for a given time offset. + + Args: + da (xr.DataArray): The input DataArray. + time_offset (float): The time offset in milliseconds to be applied. + + Returns: + Dict[str, Any]: A dictionary containing the Gaussian fitting results + (parameters, chi2, etc.). + """ + + time_offset_ns = int(time_offset * 1000000) + time_offset_np = np.timedelta64(int(np.round(time_offset_ns)), "ns") + + da_shifted = shift_coords(da, ["lat", "lon"], time_offset_np) + da_scan = da_shifted[da_shifted.state == "SCAN"] + time_profile = da_scan.mean("chan", skipna=True) + peak_idx = time_profile.argmax(dim="time") + + peak_lon = da_scan.lon[peak_idx].item() + peak_lat = da_scan.lat[peak_idx].item() + + mask_spatial = (abs(da_shifted.lon - peak_lon) <= 60) & ( + abs(da_shifted.lat - peak_lat) <= 60 + ) + mask_scan = da_shifted.state == "SCAN" + mask_off = da_shifted.state != "SCAN" + da_cropped = da_shifted.where((mask_scan & mask_spatial) | mask_off, drop=True) + + map_data = mapping(da_cropped, da.frequency.min().item(), da.frequency.max().item()) + + result = gaussfit(map_data) + return result + + +def mapping(da: xr.DataArray, min_freq: float, max_freq: float) -> xr.DataArray: + """Generates a 2D map from the input DataArray by + performing sky subtraction and gridding. + + Args: + da (xr.DataArray): The input DataArray containing ON/OFF scan data. + min_freq (float): Minimum frequency for the map integration (GHz). + max_freq (float): Maximum frequency for the map integration (GHz). + + Returns: + xr.DataArray: The generated 2D map averaged over the frequency range. + Returns None if data is insufficient. + """ + + da_on = da[da.state == "SCAN"] + da_off = da[da.state != "SCAN"] + da_base = ( + da_off.groupby("scan") + .map(mean_in_time) + .interp_like( + da_on, + method="linear", + kwargs={"fill_value": "extrapolate"}, + ) + ) + with catch_warnings(): + simplefilter("ignore") + cube = make.cube(da_on - da_base.data, skycoord_grid="3 arcsec") + condition = (cube.frequency >= min_freq) & (cube.frequency <= max_freq) + map = cube.where(condition, drop=True).mean("chan") + return map + + +def visualize_correction( + da: xr.DataArray, + min_freq: float, + max_freq: float, + offset_val: float, + save_path: str, +) -> None: + """Visualizes the corrected map, best-fit model, + and residuals for a specific time offset. + + Args: + da (xr.DataArray): The input DataArray. + offset_val (float): The optimized time offset in milliseconds. + min_freq (float): Minimum frequency for visualization (GHz). + max_freq (float): Maximum frequency for visualization (GHz). + save_path (Optional[str]): File path to save the plot image. + If None, the plot is displayed but not saved. + + Returns: + None + """ + if np.isnan(offset_val): + return + + offset_ns = int(offset_val * 1_000_000) + offset_td = np.timedelta64(int(np.round(offset_ns)), "ns") + da_shifted = shift_coords(da, ["lat", "lon"], offset_td) + + data_map = mapping(da_shifted, min_freq, max_freq) + + fit_result = gaussfit(data_map) + + if "popt" not in fit_result: + return + + popt = fit_result["popt"] + + Z = np.array(data_map.data) + Z[np.isnan(Z)] = 0.0 + + X, Y = np.meshgrid(np.array(data_map["lon"]), np.array(data_map["lat"])) + + model_data = gaussian_2d((X, Y), *popt).reshape(X.shape) + residual_data = Z - model_data + + model_da = xr.DataArray( + model_data, coords=data_map.coords, dims=data_map.dims, name="Model" + ) + residual_da = xr.DataArray( + residual_data, coords=data_map.coords, dims=data_map.dims, name="Residual" + ) + + fig, axes = plt.subplots(1, 3, figsize=(18, 5), sharex=True, sharey=True) + + data_map.plot.pcolormesh( + ax=axes[0], cmap="coolwarm", cbar_kwargs={"label": "Intensity (K)"} + ) + axes[0].set_title(f"Data (Shifted: {offset_val:.2f} ms)") + axes[0].set_aspect("equal") + + model_da.plot.pcolormesh( + ax=axes[1], cmap="coolwarm", cbar_kwargs={"label": "Intensity (K)"} + ) + axes[1].set_title("Best-fit Model") + axes[1].set_aspect("equal") + axes[1].set_ylabel("") + + vmax_res = np.nanmax(np.abs(residual_data)) + residual_da.plot.pcolormesh( + ax=axes[2], + cmap="viridis", + vmin=-vmax_res, + vmax=vmax_res, + cbar_kwargs={"label": "Residual (K)"}, + ) + axes[2].set_title("Residual (Data - Model)") + axes[2].set_aspect("equal") + axes[2].set_ylabel("") + + plt.tight_layout() + + if save_path: + plt.savefig(save_path, dpi=150, bbox_inches="tight") + plt.close() + else: + plt.show() + + +def make_toml( + file_prefix: str, + min_freq: float, + max_freq: float, + opt_offset: float, + hesse_err: float, + init_val: float, + init_peak: float, + final_res: Dict[str, Any], +) -> str: + """Generates a TOML formatted string from the analysis results. + + Args: + file_prefix (str): Prefix used for file identification. + min_freq (float): Minimum frequency used. + max_freq (float): Maximum frequency used. + opt_offset (float): Optimized time offset. + hesse_err (float): Hessian error of the time offset. + init_val (float): Initial chi-squared value (at 0 offset). + opt_val (float): Optimized chi-squared value. + init_peak (float): Initial peak intensity. + opt_peak (float): Optimized peak intensity. + final_res (Dict[str, Any]): Dictionary containing final + fitting parameters. + + Returns: + str: A TOML formatted string. + """ + + def to_native(val): + if isinstance(val, (np.integer, int)): + return int(val) + elif isinstance(val, (np.floating, float)): + return float(val) + elif isinstance(val, np.ndarray): + return val.tolist() + return val + + data_dict = { + "file_id": file_prefix, + "min_freq": to_native(min_freq), + "max_freq": to_native(max_freq), + "optimal_offset": to_native(opt_offset), + "hesse_error": to_native(hesse_err), + "initial_chi2": to_native(init_val), + "initial_peak": to_native(init_peak), + } + + for k, v in final_res.items(): + if k != "popt": + data_dict[k] = to_native(v) + + return toml.dumps(data_dict) + + +def timeoffset_search( + dems: str, min_freq: float, max_freq: float, key: str = "chi2", save_dir="." +) -> None: + """Searches for the optimal time offset to minimize the specified + key (e.g., chi2) and saves the results. + + Args: + dems (str): Path to the DEMS file (Zarr/Zip). + min_freq (float): Minimum frequency for analysis (GHz). + max_freq (float): Maximum frequency for analysis (GHz). + key (str, optional): Key to minimize (default is 'chi2'). + save_dir (str, optional): Directory to save output files + (default is current directory). + + Returns: + Tuple[Optional[int], float, Dict[str, Any]]: A tuple containing + (Rounded optimal offset, Hesse error, Final result dictionary). + Returns (None, nan, nan, {}) if failed. + """ + + da = load.dems(dems, skycoord_frame="relative", data_scaling="brightness").compute() + da = da.where((da.frequency >= min_freq) & (da.frequency <= max_freq), drop=True) + + basename = dems + if basename.endswith(".zarr.zip"): + file_prefix = basename.replace(".zarr.zip", "") + elif basename.endswith(".zarr"): + file_prefix = basename.replace(".zarr", "") + else: + file_prefix = basename + + toml_path = f"{save_dir}/{file_prefix}_result.toml" + map_path = f"{save_dir}/{file_prefix}_maps.png" + init_res = get_result(da, 0.0) + + init_val = init_res.get(key, np.nan) + init_peak = init_res.get("peak", np.nan) + + def objective(offset: float) -> float: + if isinstance(offset, np.ndarray): + offset_val = offset[0] + else: + offset_val = offset + try: + offset_val = float(offset_val) + except: + return np.inf + if np.isnan(offset_val) or np.isinf(offset_val): + return np.inf + + try: + result_dict = get_result(da, offset_val) + current_val = result_dict.get(key, np.inf) + except Exception: + return np.inf + + if np.isnan(current_val): + return np.inf + return current_val + + initial_guess = [50.0] + bounds = [(-50, 100)] + + res_scipy = minimize( + lambda x: objective(x[0]), + initial_guess, + method="Powell", + bounds=bounds, + options={"ftol": 1e-3}, + ) + scipy_offset = res_scipy.x[0] if res_scipy.success else 50.0 + + m = Minuit(objective, offset=scipy_offset) # type: ignore + m.errordef = Minuit.LEAST_SQUARES + m.limits["offset"] = (0, 100) + m.errors["offset"] = 1.0 + m.tol = 1.0 + + m.simplex() + + try: + m.hesse() + except: + pass + + opt_offset = m.values["offset"] + hesse_err = m.errors["offset"] + + final_res = get_result(da, opt_offset) + + visualize_correction(da, min_freq, max_freq, opt_offset, save_path=map_path) + if np.isnan(opt_offset): + return None + + if toml_path: + try: + toml_str = make_toml( + file_prefix, + min_freq, + max_freq, + opt_offset, + hesse_err, + init_val, + init_peak, + final_res, + ) + + with open(toml_path, "w") as f: + f.write(toml_str) + print(f"Results saved to {toml_path}") + except Exception as e: + print(f"Failed to save TOML: {e}") + + return None + + def main() -> None: """Entry point of the decode-qlook command.""" diff --git a/pyproject.toml b/pyproject.toml index 2ce174b..147ad4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ dependencies = [ "dask>=2024,<2026", "dems>=2025.6,<2026.0", "fire>=0.5,<1.0", + "iminuit>=2,<3", "matplotlib>=3,<4", "ndtools>=1,<2", "numpy>=1.23,<3.0", diff --git a/uv.lock b/uv.lock index a35e5f9..abd1211 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.9, <3.13" resolution-markers = [ "python_full_version >= '3.12'", @@ -545,6 +545,7 @@ dependencies = [ { name = "dask", version = "2025.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "dems" }, { name = "fire" }, + { name = "iminuit" }, { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "matplotlib", version = "3.10.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, { name = "ndtools" }, @@ -588,6 +589,7 @@ requires-dist = [ { name = "dask", specifier = ">=2024,<2026" }, { name = "dems", specifier = ">=2025.6,<2026.0" }, { name = "fire", specifier = ">=0.5,<1.0" }, + { name = "iminuit", specifier = ">=2,<3" }, { name = "matplotlib", specifier = ">=3,<4" }, { name = "ndtools", specifier = ">=1,<2" }, { name = "numpy", specifier = ">=1.23,<3.0" }, @@ -760,6 +762,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, ] +[[package]] +name = "iminuit" +version = "2.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.10.*'" }, + { name = "numpy", version = "2.3.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/0d/48a09d00769634930b1bdbf1ea057a2747a81d07825cd6940784d8b6599f/iminuit-2.32.0.tar.gz", hash = "sha256:a32b34d18665959be75ad6bdb1dd80459bb94466c62b455631c00568accdf7d2", size = 1873843, upload-time = "2025-11-09T17:25:18.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/dc/5716712eb48e6415616554ad63d9353bdca99e992b02a01fbdd152689e3f/iminuit-2.32.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a0bebfc5aa4874cbd66ae3f18f70e1cce537f2b4be524665e061b39866460af", size = 435187, upload-time = "2025-11-09T17:23:12.121Z" }, + { url = "https://files.pythonhosted.org/packages/c3/12/b708845b547387e826aa528cc64635d2c528669108276fac1c5bb1eb254a/iminuit-2.32.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be39dbe99965d98a11373eb67d9bee8329ffad2e888f7031de1e7b0bfd84a04f", size = 384829, upload-time = "2025-11-09T17:23:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/41/8c/ba5cd544b2cc2da9e23b99250736a958a60c5751865f5bac5c2c41ae8471/iminuit-2.32.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6afb34720d53326d8f27ff107dddbd35b1ae7fc42bd0cb92ae45e950bc549d3a", size = 469352, upload-time = "2025-11-09T17:23:17.075Z" }, + { url = "https://files.pythonhosted.org/packages/25/44/5b23e15d5b31e40112acea297acfe3d6eb89a129f8d2002bd7136dfa120f/iminuit-2.32.0-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b3b8b2a3039f8e4b3f77764dc993e50640a643a3bbddfde9063257cea6a062c", size = 414064, upload-time = "2025-11-09T17:23:18.491Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9e/15ff22a30e086264b2b243d2cb786b6369b5cc52d802b7ebeb8999739b47/iminuit-2.32.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a5ecc1692f0a59ebf89b73bd250535cd54521023d2915136feff17c60d035d4c", size = 1417026, upload-time = "2025-11-09T17:23:20.212Z" }, + { url = "https://files.pythonhosted.org/packages/71/8b/662807e1ce2b878449d9c223c654e8a7fbd8f1803493a85a083a1d714654/iminuit-2.32.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:df62b0a82af95268c8fb34fd3911905c647c1f5ab23ae7213cebfaddef0e8867", size = 1489075, upload-time = "2025-11-09T17:23:22.406Z" }, + { url = "https://files.pythonhosted.org/packages/78/c6/0a37bc53d884f50e0f99d89ece6ad841301ae5f5592b5aee46c21612ce02/iminuit-2.32.0-cp310-cp310-win32.whl", hash = "sha256:920c76692d543340251879353a3dca122a6e50a9b768dd3f9838da213d0f87c9", size = 343461, upload-time = "2025-11-09T17:23:24.449Z" }, + { url = "https://files.pythonhosted.org/packages/cb/59/2741948ac7770bc42e4d2c4f0e50de227ecccffaa494b4b7287d96dd1a64/iminuit-2.32.0-cp310-cp310-win_amd64.whl", hash = "sha256:47c26a1e00883e29949b50b3601d731b7d3c1019aafe03f44913049e18a68a3c", size = 391894, upload-time = "2025-11-09T17:23:26.032Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b9/1c8b7bd111bfca6139352a10188ecd78f87f9dbd273295c81d3731bf2545/iminuit-2.32.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73ee5ead75394c3dc88087b3ab86c7d72e6bf993cd42d4656ac51a175e971635", size = 436283, upload-time = "2025-11-09T17:23:28.006Z" }, + { url = "https://files.pythonhosted.org/packages/f7/76/7e094e58ecf5072a3b1545dbdba2b7d790bb468ecbd6061ca574d76a14bf/iminuit-2.32.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dcf2a2a8d3f66b89450fc8494a6734868a6ffb8b72d311c15a4ba6e5b066322e", size = 385897, upload-time = "2025-11-09T17:23:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b9/25e03c5acdf5189ca5c00c51ed7379e28ef895f53f9d42cff6d3db9dce0f/iminuit-2.32.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02acccb353e120d22121d3a0bc6681ebc14f7557f676800aa51e4ef7014ebc08", size = 414995, upload-time = "2025-11-09T17:23:32.082Z" }, + { url = "https://files.pythonhosted.org/packages/d7/76/07057717d523d6e7ac3352240136b3a45424a30a71c699b8f2ed1017429a/iminuit-2.32.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a645476edda3efcfebad9fee353605abd122abf550f6ef28c56075c40ecdd4f", size = 448433, upload-time = "2025-11-09T17:23:34.11Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2d/7314dbe5a34c312d7e48802152a4cdf62191523ef9d278918b9d6ece2d4c/iminuit-2.32.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e74ce68294d1e0667e4f59d0c9fb088a55fd3a6520cd8dc0e7a2df3e202b16da", size = 1418158, upload-time = "2025-11-09T17:23:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/c8/58/47fe12a73e3e2712d1a4076904e845f7de8234aed0e3cec89dd423a81325/iminuit-2.32.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6eef1dd82f179122082f134eccf773fc9d5b481ff9baaca2d09840a3226590f2", size = 1489840, upload-time = "2025-11-09T17:23:37.708Z" }, + { url = "https://files.pythonhosted.org/packages/06/f8/6d0e1383349b3c98734b56162968c442a61f3cd8a45a1d6d7e4388a7e499/iminuit-2.32.0-cp311-cp311-win32.whl", hash = "sha256:561ecfae249636439c6217b82d31c5279f48ee4a38bb85d14ef23c639cd1e451", size = 344085, upload-time = "2025-11-09T17:23:39.821Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ae/0c81a0afbbf96699f3ca5c4ab31ec1047e018735ef651bd9691a212d6a36/iminuit-2.32.0-cp311-cp311-win_amd64.whl", hash = "sha256:dd93614b048023791221beb1b48bd00f87a44bcd92daff8667cff1f5a44ee013", size = 392694, upload-time = "2025-11-09T17:23:41.657Z" }, + { url = "https://files.pythonhosted.org/packages/2b/1f/0119927efef356b6ebc5e8c6861535f762bc701d0b808715332760768d3c/iminuit-2.32.0-cp311-cp311-win_arm64.whl", hash = "sha256:17cc66bf771eee6a18b273f2d967348424d3aa731fe4431cd589e4d77f0ae960", size = 385542, upload-time = "2025-11-09T17:23:43.504Z" }, + { url = "https://files.pythonhosted.org/packages/9d/c9/2d937a776e81930172694ccf9c1a7e0637e47384dcd3496940cae1e1a44b/iminuit-2.32.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:38b8baa9cc35d62aa2d7e3ec676423e4fd5120b0e178095a0f3d73d6d36e9e6b", size = 440476, upload-time = "2025-11-09T17:23:45.004Z" }, + { url = "https://files.pythonhosted.org/packages/a3/76/2b3281b4988e5d47534fc0bece70f3b9e31d8e943fe1b5f4a5831962c5cd/iminuit-2.32.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b4c818abeb0ad1677a336f8caaa5faf9545cfef14be4a9be7cd08c9036e2924b", size = 386749, upload-time = "2025-11-09T17:23:46.473Z" }, + { url = "https://files.pythonhosted.org/packages/4e/86/0aa16e7a5d8968e5d616fee577b7c188098d61925ccf899cc4423e91fd50/iminuit-2.32.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6bd0bd6f2c9e79675e84036e8305823e1bea8ad9f58cf8588f174be55eefa1b", size = 411064, upload-time = "2025-11-09T17:23:48.667Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/2bd147bb17df581b642d02a0360bf30c2263d60f8717dd81809726b746e9/iminuit-2.32.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:91e8a206d82bef31d2ed2b1420a85f8d604c8b03ad1edf2d47d87984bf3d7817", size = 448197, upload-time = "2025-11-09T17:23:50.224Z" }, + { url = "https://files.pythonhosted.org/packages/7c/cd/25449c3ae41e66aef33d8347075c5f12a30ce71254a86fd99a333be892ae/iminuit-2.32.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b9702617d9c6024ec0f3833ceeb031cddce5c7ec4d4982ad7e7d60c6954abab", size = 1413913, upload-time = "2025-11-09T17:23:51.965Z" }, + { url = "https://files.pythonhosted.org/packages/f0/fd/42b36fc73dbbad436c64791735c78e7502364ea2991ac46ead03c6e32ad7/iminuit-2.32.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:101f21e26d90b766f89f64a1497d70157bb5d5b969a1176fabec3ee8b38a4d56", size = 1489599, upload-time = "2025-11-09T17:23:54.092Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ce/96f473ae657ef9e5acc0770e8ab48ed4fcd51da10d15bba56d0150c4eb5c/iminuit-2.32.0-cp312-cp312-win32.whl", hash = "sha256:9110b2190557e7f4cbae0eaf79f7b6e69aed68a8c80d0132868a27f0f3846598", size = 343887, upload-time = "2025-11-09T17:23:55.918Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ab/bcf61e7466cb61310569c9ffe426bc9462fb7a5f9a04272f368389959260/iminuit-2.32.0-cp312-cp312-win_amd64.whl", hash = "sha256:b996f8f8ae186b761f16b3d163536728de92a451fce8ffb26acede7a23bd0af6", size = 393427, upload-time = "2025-11-09T17:23:57.773Z" }, + { url = "https://files.pythonhosted.org/packages/81/a6/7cafadc7037bf5efb0e80040652824002a562858e40b9e41e2cdc09318a1/iminuit-2.32.0-cp312-cp312-win_arm64.whl", hash = "sha256:3620b39572a3b66aeaf703729905d67b43adfb4051fb174331d090fa97914a6a", size = 386047, upload-time = "2025-11-09T17:23:59.321Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c4/2082347a9077497c4c52f626aadd2c9ba65d2bc509986fda20f1c28d3033/iminuit-2.32.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:00579545c0866ec123be8820aa506da4b5b64857203dc6195dd5917f3552f405", size = 435221, upload-time = "2025-11-09T17:25:07.381Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c4/9f0f586fb77d4653785f99df60f8ce62576940350debfdb40edfbf83ea31/iminuit-2.32.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:54956ade7ad19f056965fc875ddbf90ad12de86e72a912e861b8496fd20b9aec", size = 384888, upload-time = "2025-11-09T17:25:09.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/8c4ba7cc6be090f6a3f8a07d52b150feff7cc81d252405f670f31657e265/iminuit-2.32.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cd8736ff4dcf52217cf98d57b45246f943a922d84208545f0999d1054afb14b1", size = 469429, upload-time = "2025-11-09T17:25:10.792Z" }, + { url = "https://files.pythonhosted.org/packages/44/5a/08a2cf6927db1ee05c37b62eba8cd0df48a3e13a7f84cd13707d94390411/iminuit-2.32.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2e0580fd3694959eee3fae755f1f7712b0eeb9b15bf45d8ab44f14f77368eac9", size = 1489396, upload-time = "2025-11-09T17:25:12.932Z" }, + { url = "https://files.pythonhosted.org/packages/9e/86/0881060c764351058581b3247099703b73f7d96cac6761b26a42e6ee366d/iminuit-2.32.0-cp39-cp39-win32.whl", hash = "sha256:55f58b896f0b162a932bc2da9a5f4d852d310bd3622361803af2f1d26237f7f5", size = 343532, upload-time = "2025-11-09T17:25:15.123Z" }, + { url = "https://files.pythonhosted.org/packages/a1/56/1b6b7579a7eb76e2cb24db5e3d51ac0085a5d769cc46332205ff5e24cb3e/iminuit-2.32.0-cp39-cp39-win_amd64.whl", hash = "sha256:b43849cf0db32833cb364b2fec4921e991cdeb1164f16af626ebdd027c57e0e7", size = 406691, upload-time = "2025-11-09T17:25:16.773Z" }, +] + [[package]] name = "importlib-metadata" version = "8.7.0" From d9c9abe5ff17488b28851bf32b87a280b9025e9b Mon Sep 17 00:00:00 2001 From: Naohiro Nakata <212558462+Nakatanuki812@users.noreply.github.com> Date: Tue, 3 Feb 2026 07:36:44 +0000 Subject: [PATCH 2/2] #249 Add time_offset to qlook command line --- decode/qlook.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/decode/qlook.py b/decode/qlook.py index 706ace9..626ace5 100644 --- a/decode/qlook.py +++ b/decode/qlook.py @@ -5,6 +5,7 @@ "raster", "skydip", "still", + "timeoffset_search", "xscan", "yscan", "zscan", @@ -1910,6 +1911,7 @@ def main() -> None: "raster": raster, "skydip": skydip, "still": still, + "timeoffset_search": timeoffset_search, "xscan": xscan, "yscan": yscan, "zscan": zscan,