Source code for smlmlp.modules.Config_LP.Config

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Date          : 2026-02-25
# Author        : Lancelot PINCET
# GitHub        : https://github.com/LancelotPincet
# Library       : smlmLP
# Module        : Config

"""
This class stores configuration values in an object that can be saved and loaded.
"""

import json
from contextlib import ExitStack
from pathlib import Path

import numpy as np
import tifffile as tiff

from arrlp import coordinates
from corelp import folder, prop, selfkwargs
from smlmlp import Camera, Channel, metadatum
from stacklp import shapetif



[docs] class Config() : """ Store configuration values in an object that can be saved and loaded. Parameters ---------- *tif_paths TIFF paths used to initialize frame and camera dimensions. config : Config, dict, path-like, or None, default=None Existing configuration source. locs : Locs or None, default=None Optional localization container used for data-dependent defaults. **kwargs Configuration metadata values to assign. Attributes ---------- cameras : list[Camera] Camera metadata containers. locs : Locs or None Optional localization container. Examples -------- >>> from smlmlp import Config >>> instance = Config() """ # Init def __init__(self, *tif_paths, config=None, locs=None, **kwargs) : """Initialize configuration from paths, metadata, and keyword values.""" self.locs = locs self.ncameras = max(len(tif_paths), 1) if config is not None : if isinstance(config, Config) : config = config.metadata if isinstance(config, str) or isinstance(config,Path('').__class__): config_file = Path(config).with_suffix('.json') if config_file.exists() : with open(config_file, "r") as file: config = json.load(file) config_folder = config_file.parent / "_config_data" if config_folder.exists() : data = {} for file in config_folder.glob('*.npy') : key = file.stem data[key] = array_convert(np.load(file, allow_pickle=True)) config["Data"] = data else : raise SyntaxError(f'config path was not recognized: {config}') for group_dict in config.values() : selfkwargs(self, group_dict) selfkwargs(self, kwargs) # Opening files if len(tif_paths) : with ExitStack() as stack : tifs = [stack.enter_context(tiff.TiffFile(file)) for file in tif_paths] shapes = [shapetif(tif) for tif in tifs] for shape in shapes : if shape[0] != shapes[0][0] : raise ValueError('All tiff files do not have the same number of frames which is not possible for a single SMLM acquisition') self.nframes = shapes[0][0] self.cameras_npixels = [shape[1:3] for shape in shapes] @property def metadata(self) : """Return explicitly stored metadata grouped by metadata section.""" data = {} for group_name, group_list in metadatum.groups.items() : data_dict = {} data[group_name] = data_dict for datum in group_list : value = getattr(self, f'_{datum}', None) if value is not None : data_dict[datum] = json_convert(value) return data
[docs] def save(self, path, file=None) : """Save metadata JSON and array data to disk.""" path = Path(path) if file is not None : path = path / file path = path.with_suffix('.json') metadata = self.metadata data = metadata.pop('Data') with open(path, 'w') as json_file: json.dump(metadata, json_file, indent=4) config_folder = folder(path.parent / "_config_data", warning=False) for key, value in data.items() : np.save(config_folder / f"{key}.npy", array_convert(value))
# Cameras @metadatum('Cameras') def ncameras(self) : """Return ncameras.""" raise SyntaxError('Should not be called') @ncameras.setter def ncameras(self, value) : """Set ncameras.""" self.cameras = [Camera(self) for _ in range(int(value))] @property def _ncameras(self) : """Return ncameras.""" return len(self.cameras) @property def FOV_max_um(self) : """Return FOV max um.""" ymin = min([camera.FOV_max_um[0] for camera in self.cameras]) xmin = min([camera.FOV_max_um[1] for camera in self.cameras]) return ymin, xmin @property def frame_bytes(self) : # gigabytes/frame """Return frame bytes.""" return sum([camera.frame_bytes for camera in self.cameras]) # Channels @property def nchannels(self) : """Return nchannels.""" return sum(self.cameras_nchannels) @property def channels(self) : """Return channels.""" return [channel for camera in self.cameras for channel in camera.channels] @property def channels_shapes(self) : """Return channels shapes.""" return [(bb[3]-bb[1], bb[2]-bb[0]) for bboxes in self.cameras_bbox for bb in bboxes] @property def glob_pixel_nm(self) : """Return glob pixel nm.""" return ( min([pix[0] for pix in self.channels_pixels_nm]), min([pix[1] for pix in self.channels_pixels_nm]), ) # Loads @metadatum('Loads', dtype=int) def cuda(self) : """Return cuda.""" return 0 @metadatum('Loads', dtype=int) def parallel(self) : """Return parallel.""" return 0 @metadatum('Loads', dtype=int) def nframes(self) : """Return nframes.""" return 60000 @metadatum('Loads', dtype=int) def loaded(self) : """Return loaded.""" return 256 @property def pad(self) : """Return pad.""" median_window_fr = int(self.median_window_ms / self.exposure_ms) temporal_kernel_length = int(self.temporal_kernel_shape[0]) pad_max = int((self.loaded - 2) // 3) return min(max(median_window_fr, temporal_kernel_length), pad_max) @property def chunk(self) : """Return chunk.""" return self.loaded - self.pad * 2 # Time @metadatum('Cameras', dtype=float) def exposure_ms(self) : # [ms] """Return exposure ms.""" return 50. @metadatum('Blinks') def on_time_ms(self) : # ms """Return on time ms.""" return 50. # Background configurations @metadatum('Backgrounds', dtype=float) def median_window_ms(self) : # For temporal median [ms] """Return median window ms.""" return self.on_time_ms * 10 @property def median_window_fr(self) : """Return median window fr.""" return int(round(self.median_window_ms / self.exposure_ms)) @median_window_fr.setter def median_window_fr(self, value) : """Set median window fr.""" self.median_window_ms = value * self.exposure_ms @metadatum('Backgrounds', dtype=bool) def do_temporal_median(self) : # For temporal median """Return do temporal median.""" return True @metadatum('Backgrounds', dtype=float) def mean_radius_nm(self) : # For spatial mean [nm] """Return mean radius nm.""" return max(self.channels_psf_sigmas_nm) * 8 @metadatum('Backgrounds', dtype=bool) def do_spatial_mean(self) : # For spatial mean """Return do spatial mean.""" return True @metadatum('Backgrounds', dtype=float) def opening_radius_nm(self) : # For spatial opening [nm] """Return opening radius nm.""" return max(self.channels_psf_sigmas_nm) * 2 @metadatum('Backgrounds', dtype=bool) def do_spatial_opening(self) : # For spatial opening """Return do spatial opening.""" return False # Signal configurations @metadatum('Signals', dtype=bool) def do_spatial_filter(self) : # For spatial filter """Return do spatial filter.""" return True @metadatum('Signals', dtype=bool) def do_temporal_filter(self) : # For temporal filter """Return do temporal filter.""" return False @metadatum('Signals') def spatial_subtract_factor(self) : """Return spatial subtract factor.""" return 2.5 @spatial_subtract_factor.setter def spatial_subtract_factor(self, value) : """Set spatial subtract factor.""" self._spatial_subtract_factor = value for channel in self.channels: channel._spatial_kernel = None @metadatum('Signals') def temporal_subtract_factor(self) : """Return temporal subtract factor.""" return 0. @temporal_subtract_factor.setter def temporal_subtract_factor(self, value) : """Set temporal subtract factor.""" self._temporal_subtract_factor = value self._temporal_kernel = None @property def temporal_kernel_shape(self) : """Return temporal kernel shape.""" on_frames = self.on_time_ms / self.exposure_ms if self.temporal_subtract_factor > 1 : on_frames *= self.temporal_subtract_factor on_frames = int(np.ceil(on_frames)) return (on_frames * 10 + 1), @property def on_time_kernel(self) : """Return on time kernel.""" T, = coordinates(shape=self.temporal_kernel_shape, pixel=self.exposure_ms) from funclp import Exponential1 exponential = Exponential1(tau=self.on_time_ms) k = exponential(np.abs(T)) k /= k.sum() return k.astype(np.float32) @property def temporal_subtract_kernel(self) : """Return temporal subtract kernel.""" if self.temporal_subtract_factor <= 1 : return None exposure = self.exposure_ms / self.temporal_subtract_factor T, = coordinates(shape=self.temporal_kernel_shape, pixel=exposure) from funclp import Exponential1 exponential = Exponential1(tau=self.on_time_ms) k = exponential(np.abs(T)) k /= k.sum() return k.astype(np.float32) @prop(cache=True) def temporal_kernel(self) : """Return temporal kernel.""" if self.temporal_subtract_factor <= 1 : return self.on_time_kernel k = self.on_time_kernel - self.temporal_subtract_kernel return k - k.mean() # Detection @metadatum('Detections') def snr_thresh(self) : """Return snr thresh.""" return 4. # Crops @metadatum('Crops') def crop_nm(self) : # nm """Return crop nm.""" h = max([cz[0] for cz in self.channels_default_crops_nm]) w = max([cz[1] for cz in self.channels_default_crops_nm]) return max(h, w) # Localization @metadatum('Localizations') def optimizer(self) : # string """Return optimizer.""" return "lm" @optimizer.setter def optimizer(self, value) : """Set optimizer.""" assert type(value) is str assert value.lower() in ['lm'] self._optimizer = value.lower() @metadatum('Localizations') def estimator(self) : # string """Return estimator.""" return "mle" @estimator.setter def estimator(self, value) : """Set estimator.""" assert type(value) is str assert value.lower() in ["mle", "lse"] self._estimator = value.lower() @metadatum('Localizations') def distribution(self) : # string """Return distribution.""" return "poisson" @distribution.setter def distribution(self, value) : """Set distribution.""" assert type(value) is str assert value.lower() in ["poisson", "normal"] self._distribution = value.lower() # Globdet @metadatum('Globdetections') def globdet_use(self) : # list """Defines if uses globdet.""" return False @metadatum('Globdetections') def globdet_mode(self) : # string """Return globdet mode.""" return "mean" @globdet_mode.setter def globdet_mode(self, value) : """Set globdet mode.""" assert type(value) is str assert value.lower() in ["mean", "std"] self._globdet_mode = value.lower() @metadatum('Globdetections') def globdet_groups(self) : # list """Return globdet groups.""" return [np.asarray([i for i in range(self.nchannels)])] # Globloc @metadatum('Globlocalizations') def globloc_use(self) : # list """Defines if uses globloc.""" return False # Effective values @metadatum('Effective') def intensity_channels(self) : """Return intensity channels.""" return [idx for idx in range(1, self.nchannels + 1)] @intensity_channels.setter def intensity_channels(self, value) : """Set intensity channels.""" if np.ndim(value) == 0 : self._intensity_channels = [value] else : self._intensity_channels = value @metadatum('Effective') def x_channels(self) : """Return x channels.""" return [idx for idx in range(1, self.nchannels + 1)] @x_channels.setter def x_channels(self, value) : """Set x channels.""" if np.ndim(value) == 0 : self._x_channels = [value] else : self._x_channels = value @metadatum('Effective') def y_channels(self) : """Return y channels.""" return [idx for idx in range(1, self.nchannels + 1)] @y_channels.setter def y_channels(self, value) : """Set y channels.""" if np.ndim(value) == 0 : self._y_channels = [value] else : self._y_channels = value @metadatum('Effective') def z_channels(self) : """Return z channels.""" return [idx for idx in range(1, self.nchannels + 1)] @z_channels.setter def z_channels(self, value) : """Set z channels.""" if np.ndim(value) == 0 : self._z_channels = [value] else : self._z_channels = value # Methods @metadatum('Methods') def x_method(self) : # string """Return x method.""" return "fit" @x_method.setter def x_method(self, value) : """Set x method.""" assert type(value) is str assert value.lower() in ["det", "fit", "modloc", "timeloc"] self._x_method = value.lower() @metadatum('Methods') def y_method(self) : # string """Return y method.""" return "fit" @y_method.setter def y_method(self, value) : """Set y method.""" assert type(value) is str assert value.lower() in ["det", "fit", "modloc", "timeloc"] self._y_method = value.lower() @metadatum('Methods') def z_method(self) : # string """Return z method.""" return "fit" @z_method.setter def z_method(self, value) : """Set z method.""" assert type(value) is str assert value.lower() in ["fit", "astig", "biplane", "donald", "modloc", "timeloc", "miet", "qtirf"] self._z_method = value.lower() @metadatum('Methods') def azimuth_method(self) : # string """Return azimuth method.""" return "polar3d" @azimuth_method.setter def azimuth_method(self, value) : """Set azimuth method.""" assert type(value) is str assert value.lower() in ["polar2d", "polar3d"] self._azimuth_method = value.lower() @metadatum('Methods') def tilt_method(self) : # string """Return tilt method.""" return "polar3d" @tilt_method.setter def tilt_method(self, value) : """Set tilt method.""" assert type(value) is str assert value.lower() in ["polar3d"] self._tilt_method = value.lower() @metadatum('Methods') def phase_method(self) : # string """Return phase method.""" return "demodulated" @phase_method.setter def phase_method(self, value) : """Set phase method.""" assert type(value) is str assert value.lower() in ["demodulated", "sequential"] self._phase_method = value.lower() @metadatum('Methods') def lifetime_method(self) : # string """Return lifetime method.""" return "iflim" @lifetime_method.setter def lifetime_method(self, value) : """Set lifetime method.""" assert type(value) is str assert value.lower() in ["tcspc", "iflim", "dpflim"] self._lifetime_method = value.lower() @metadatum('Methods') def drift_method(self) : # string """Return drift method.""" return "none" @drift_method.setter def drift_method(self, value) : """Set drift method.""" assert type(value) is str assert value.lower() in ["none", "crosscorr", "comet", "aim", "meanshift"] self._drift_method = value.lower() @metadatum('Methods') def demix_method(self) : # string """Return demix method.""" return "flux" @demix_method.setter def demix_method(self, value) : """Set demix method.""" assert type(value) is str assert value.lower() in ["flux", "spectral", "lifetime"] self._demix_method = value.lower() @metadatum('Methods') def demix2d_method(self) : # string """Return demix2d method.""" return "spectral" @demix2d_method.setter def demix2d_method(self, value) : """Set demix2d method.""" assert type(value) is str assert value.lower() in ["spectral"] self._demix2d_method = value.lower() # Targets @metadatum('Targets') def dyes(self) : # string list """Return dyes.""" return ['unknown'] @dyes.setter def dyes(self, value) : """Set dyes.""" for v in value : assert type(v) is str value = [v.lower() for v in value] self._dyes = value @property def ndyes(self) : """Return ndyes.""" return len(self.dyes) # Image @metadatum('Data') def irradiance_image(self) : # 2D image """Return irradiance image.""" if self.locs is not None : from smlmlp import image_pixel return image_pixel(self.os, None, locs=self.locs)[0] return None # Association @metadatum('Localizations') def channel_association_radius_nm(self) : # nm """Return channel association radius nm.""" return 30. @metadatum('Localizations') def blink_association_radius_nm(self) : # nm """Return blink association radius nm.""" if self.locs is not None : from smlmlp import associate_consecutive_frames_radius return associate_consecutive_frames_radius(locs=self.locs)[0] return 30. @metadatum('Localizations') def blink_z_association_radius_nm(self) : # nm """Return blink z association radius nm.""" return 100. @metadatum('Localizations') def track_association_radius_nm(self) : # nm """Return track association radius nm.""" return 500. # Ratio @metadatum('Ratio') def spectral_x_channels(self) : # nm """Return spectral x channels.""" return [idx for idx in range(1, self.nchannels)] if self.nchannels > 1 else [self.nchannels] @metadatum('Ratio') def spectral_y_channels(self) : # nm """Return spectral y channels.""" return [self.nchannels] @metadatum('Ratio') def biplane_x_channels(self) : # nm """Return biplane x channels.""" return [idx for idx in range(1, self.nchannels)] if self.nchannels > 1 else [self.nchannels] @metadatum('Ratio') def biplane_y_channels(self) : # nm """Return biplane y channels.""" return [self.nchannels] @metadatum('Ratio') def donald_x_channels(self) : # nm """Return donald x channels.""" return [idx for idx in range(1, self.nchannels)] if self.nchannels > 1 else [self.nchannels] @metadatum('Ratio') def donald_y_channels(self) : # nm """Return donald y channels.""" return [self.nchannels] @metadatum('Ratio') def iflim_x_channels(self) : # nm """Return iflim x channels.""" return [idx for idx in range(1, self.nchannels)] if self.nchannels > 1 else [self.nchannels] @metadatum('Ratio') def iflim_y_channels(self) : # nm """Return iflim y channels.""" return [self.nchannels] @metadatum('Ratio') def dpflim_x_channels(self) : # nm """Return dpflim x channels.""" return [idx for idx in range(1, self.nchannels)] if self.nchannels > 1 else [self.nchannels] @metadatum('Ratio') def dpflim_y_channels(self) : # nm """Return dpflim y channels.""" return [self.nchannels] # Modloc @metadatum('Modloc') def modloc_transverse_angle_deg(self) : # deg """Return modloc transverse angle deg.""" return 0. @metadatum('Modloc') def modloc_axial_angle_deg(self) : # nm """Return modloc axial angle deg.""" return 0. @metadatum('Modloc') def modloc_channels_indices(self) : # nm """Return modloc channels indices.""" return [1, 2, 3, 4] @metadatum('Modloc') def modloc_sequential_frames(self) : # nm """Return modloc sequential frames.""" return 1 @metadatum('Modloc') def modloc_dephases_rad(self) : # nm """Return modloc dephases rad.""" return [0, np.pi/2, np.pi, 3*np.pi/2] # Calibrations @metadatum('Data') def x_timeloc_calibration(self) : # nm / Hz """Return x timeloc calibration.""" return None @metadatum('Data') def y_timeloc_calibration(self) : # nm / Hz """Return y timeloc calibration.""" return None @metadatum('Data') def z_timeloc_calibration(self) : # nm / Hz """Return z timeloc calibration.""" return None @metadatum('Data') def z_astig_calibration(self) : # nm / ratio """Return z astig calibration.""" return None @metadatum('Data') def z_biplane_calibration(self) : # nm / ratio """Return z biplane calibration.""" return None @metadatum('Data') def z_donald_calibration(self) : # nm / ratio """Return z donald calibration.""" return None @metadatum('Data') def z_miet_calibration(self) : # nm / ns """Return z miet calibration.""" return None @metadatum('Data') def z_qtirf_calibration(self) : # nm / ns """Return z qtirf calibration.""" return None @metadatum('Data') def lifetime_iflim_calibration(self) : # ns / ratio """Return lifetime iflim calibration.""" return None # Rendering @metadatum('Rendering') def pixel_sr_nm(self) : """Return pixel sr nm.""" return 15. # Time @metadatum('Time') def frames_per_sequence(self) : """Return frames per sequence.""" return 100 @metadatum('Time') def zstack_speed(self) : # nm / frame """Return zstack speed.""" return 10. # Drifts @metadatum('Drift') def crosscorr_frames_per_segment(self) : """Return crosscorr frames per segment.""" return 1000. @metadatum('Drift') def crosscorr_outlier_fraction(self) : """Return crosscorr outlier fraction.""" return 0.1 @metadatum('Drift') def crosscorr_recompute(self) : """Return crosscorr recompute.""" return True @metadatum('Drift') def meanshift_frames_per_segment(self) : """Return meanshift frames per segment.""" return 10. @metadatum('Drift') def meanshift_outlier_fraction(self) : """Return meanshift outlier fraction.""" return 0.1 @metadatum('Drift') def meanshift_recompute(self) : """Return meanshift recompute.""" return True @metadatum('Drift') def meanshift_max_iter(self) : """Return meanshift max iter.""" return 100 @metadatum('Drift') def meanshift_tol_nm(self) : """Return meanshift tol nm.""" return 1. @metadatum('Drift') def meanshift_max_drift_nm(self) : """Return meanshift max drift nm.""" return 300. @metadatum('Drift') def aim_frames_per_segment(self) : """Return aim frames per segment.""" return 10. @metadatum('Drift') def aim_outlier_fraction(self) : """Return aim outlier fraction.""" return 0.1 @metadatum('Drift') def aim_recompute(self) : """Return aim recompute.""" return True @metadatum('Drift') def aim_kde_bandwidth_nm(self) : """Return aim kde bandwidth nm.""" return 40. @metadatum('Drift') def aim_learning_rate(self) : """Return aim learning rate.""" return 0.5 @metadatum('Drift') def aim_max_iter(self) : """Return aim max iter.""" return 200 @metadatum('Drift') def aim_tol(self) : """Return aim tol.""" return 1e-3 @metadatum('Drift') def aim_lambda_smooth(self) : """Return aim lambda smooth.""" return 0.5 @metadatum('Drift') def comet_frames_per_segment(self) : """Return comet frames per segment.""" return 10. @metadatum('Drift') def comet_recompute(self) : """Return comet recompute.""" return True @metadatum('Drift') def comet_max_drift_nm(self) : """Return comet max drift nm.""" return 300. @metadatum('Drift') def comet_tol(self) : """Return comet tol.""" return 1e-4 # Clusters @metadatum('Clusters') def dbscan_eps(self) : # nm """Return dbscan eps.""" return 50. @metadatum('Clusters') def dbscan_min_points(self) : """Return dbscan min points.""" return 10.
def get_datas(data) : """Return the plural metadata name while preserving unit suffixes.""" suffix = '' for unit in ['_nm', '_um', '_mm', '_deg', '_rad', '_us', '_ms', '_s', '_pix', '_fr'] : if data.endswith(unit) : data, suffix = data[:-len(unit)], data[-len(unit):] break if data.endswith('us') : data = f'{data[:-2]}i' elif data.endswith('ex') : data = f'{data[:-2]}ices' elif data.endswith('ix') : data = f'{data[:-2]}ices' elif data.endswith('ox') : data = f'{data[:-2]}oxes' elif not data.endswith('s') and not data.endswith('x') and data[-1].upper() != data[-1] : data = f'{data}s' return data + suffix for data, group in Camera.metadata : datas = get_datas(data) @metadatum(group, name=f'cameras_{datas}') def camera_property(self, data=data) : """Return camera property.""" return [getattr(camera, data) for camera in self.cameras] @camera_property.setter def camera_property(self, value, data=data) : """Set camera property.""" try : if len(value) != self.ncameras : raise ValueError('Value set does not have same number of elements as cameras') except TypeError : value = [value for _ in range(self.ncameras)] for camera, v in zip(self.cameras, value) : setattr(camera, data, v) setattr(Config, f'cameras_{datas}', camera_property) @property def _camera_property(self, data=data) : """Return camera property.""" value = [getattr(camera, f'_{data}', None) for camera in self.cameras] bool_ = [v is None for v in value] return None if any(bool_) else value setattr(Config, f'_cameras_{datas}', _camera_property) @property def channel_property(self, data=data) : """Return channel property.""" return [getattr(channel, data) for channel in self.channels] setattr(Config, f'channels_{datas}', channel_property) for data in Camera.properties : datas = get_datas(data) @property def camera_property(self, data=data) : """Return camera property.""" return [getattr(camera, data) for camera in self.cameras] @camera_property.setter def camera_property(self, value, data=data) : """Set camera property.""" try : if len(value) != self.ncameras : raise ValueError('Value set does not have same number of elements as cameras') except TypeError : value = [value for _ in range(self.ncameras)] for camera, v in zip(self.cameras, value) : setattr(camera, data, v) setattr(Config, f'cameras_{datas}', camera_property) @property def channel_property(self, data=data) : """Return channel property.""" return [getattr(channel, data) for channel in self.channels] setattr(Config, f'channels_{datas}', channel_property) for data, group in Channel.metadata : datas = get_datas(data) @metadatum(group, name=f'channels_{datas}') def channel_property(self, data=data) : """Return channel property.""" return [getattr(channel, data) for channel in self.channels] @channel_property.setter def channel_property(self, value, data=data) : """Set channel property.""" try : if len(value) != self.nchannels : raise ValueError('Value set does not have same number of elements as channels') except TypeError : value = [value for _ in range(self.nchannels)] for channel, v in zip(self.channels, value) : setattr(channel, data, v) setattr(Config, f'channels_{datas}', channel_property) @property def _channel_property(self, data=data) : """Return channel property.""" value = [getattr(channel, f'_{data}', None) for channel in self.channels] bool_ = [v is None for v in value] return None if any(bool_) else value setattr(Config, f'_channels_{datas}', _channel_property) for data in Channel.properties : datas = get_datas(data) @property def channel_property(self, data=data) : """Return channel property.""" return [getattr(channel, data) for channel in self.channels] @channel_property.setter def channel_property(self, value, data=data) : """Set channel property.""" try : if len(value) != self.nchannels : raise ValueError('Value set does not have same number of elements as channels') except TypeError : value = [value for _ in range(self.nchannels)] for channel, v in zip(self.channels, value) : setattr(channel, data, v) setattr(Config, f'channels_{datas}', channel_property) def array_convert(value) : """Convert array-like data while preserving numeric dtypes when possible.""" try : array = np.asarray(value) except ValueError : return np.array(value, dtype=object) if array.dtype != object : return array try : typed_array = np.asarray(array.tolist()) except ValueError : return array if np.issubdtype(typed_array.dtype, np.number) or np.issubdtype( typed_array.dtype, np.bool_ ) : return typed_array return array def json_convert(value) : """Convert numpy and container values to JSON-serializable objects.""" if isinstance(value, bool) or isinstance(value, np.bool_) : value = bool(value) if isinstance(value, int) or isinstance(value, np.integer) : value = int(value) if isinstance(value, float) or isinstance(value, np.floating) : value = float(value) if isinstance(value, tuple) or isinstance(value, list) : value = [json_convert(v) for v in value] if isinstance(value, np.ndarray) : value = value.ravel() if np.issubdtype(value.dtype, np.bool_) : value = value.astype(bool) elif np.issubdtype(value.dtype, np.integer) : value = value.astype(int) elif np.issubdtype(value.dtype, np.floating) : value = value.astype(float) value = list(value) return value if __name__ == "__main__": from corelp import test test(__file__)