From c06d83300417c198a2175b0aeddbf5d8918286aa Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Fri, 31 May 2024 21:54:21 +0200 Subject: [PATCH 01/55] draft --- magpylib/_src/display/display.py | 17 +++++++------ magpylib/_src/display/traces_generic.py | 8 +++++-- magpylib/_src/display/traces_utility.py | 32 ++++++++++++++++--------- magpylib/_src/utility.py | 19 +++++++++++++++ 4 files changed, 54 insertions(+), 22 deletions(-) diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 946144312..2cbaadf2e 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -11,6 +11,7 @@ from magpylib._src.defaults.defaults_utility import get_defaults_dict from magpylib._src.display.traces_generic import MagpyMarkers from magpylib._src.display.traces_generic import get_frames +from magpylib._src.display.traces_utility import DEFAULT_ROW_COL_PARAMS from magpylib._src.display.traces_utility import process_show_input_objs from magpylib._src.input_checks import check_format_input_backend from magpylib._src.input_checks import check_format_input_vector @@ -141,9 +142,6 @@ def get_show_func(backend): ) -ROW_COL_SPECIFIC_NAMES = ("row", "col", "output", "sumup", "pixel_agg", "in_out") - - def infer_backend(canvas): """Infers the plotting backend from canvas and environment""" # pylint: disable=import-outside-toplevel @@ -193,9 +191,10 @@ def _show( # process input objs objects, obj_list_flat, max_rows, max_cols, subplot_specs = process_show_input_objs( - objects, **{k: v for k, v in kwargs.items() if k in ROW_COL_SPECIFIC_NAMES} + objects, + **{k: v for k, v in kwargs.items() if k in DEFAULT_ROW_COL_PARAMS}, ) - kwargs = {k: v for k, v in kwargs.items() if k not in ROW_COL_SPECIFIC_NAMES} + kwargs = {k: v for k, v in kwargs.items() if k not in DEFAULT_ROW_COL_PARAMS} kwargs["max_rows"], kwargs["max_cols"] = max_rows, max_cols kwargs["subplot_specs"] = subplot_specs @@ -407,9 +406,9 @@ def show( } ) if ctx.isrunning: - rco = {k: v for k, v in kwargs.items() if k in ROW_COL_SPECIFIC_NAMES} + rco = {k: v for k, v in kwargs.items() if k in DEFAULT_ROW_COL_PARAMS} ctx.kwargs.update( - {k: v for k, v in kwargs.items() if k not in ROW_COL_SPECIFIC_NAMES} + {k: v for k, v in kwargs.items() if k not in DEFAULT_ROW_COL_PARAMS} ) ctx_objects = tuple({**o, **rco} for o in ctx.objects_from_ctx) objects, *_ = process_show_input_objs(ctx_objects + objects, **rco) @@ -452,11 +451,11 @@ def show_context( ) try: ctx.isrunning = True - rco = {k: v for k, v in kwargs.items() if k in ROW_COL_SPECIFIC_NAMES} + rco = {k: v for k, v in kwargs.items() if k in DEFAULT_ROW_COL_PARAMS} objects, *_ = process_show_input_objs(objects, **rco) ctx.objects_from_ctx += tuple(objects) ctx.kwargs.update( - {k: v for k, v in kwargs.items() if k not in ROW_COL_SPECIFIC_NAMES} + {k: v for k, v in kwargs.items() if k not in DEFAULT_ROW_COL_PARAMS} ) yield ctx ctx.show_return_value = _show(*ctx.objects, **ctx.kwargs) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 9d82a48cb..e6542bd20 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -419,6 +419,7 @@ def get_generic_traces( extra_backend=False, row=1, col=1, + units_length="mm", **kwargs, ) -> list: """ @@ -530,7 +531,9 @@ def get_generic_traces( name_suff = tr.pop("name_suffix", None) name = tr.get("name", "") if legendtext is None else legendtext for orient, pos in zip(orientations, positions): - tr1 = place_and_orient_model3d(tr, orientation=orient, position=pos) + tr1 = place_and_orient_model3d( + tr, orientation=orient, position=pos, units_length=units_length + ) if name_suff is not None: tr1["name"] = f"{name}{name_suff}" temp_rot_traces.append(tr1) @@ -788,12 +791,13 @@ def get_row_col_traces(flat_objs_props, extra_backend=False, autosize=None, **kw if len(rco_obj) >= 2 and style_temp: # deepcopy style only if obj is in multiple subplots. obj._style = style_temp.copy() - params["row"], params["col"], output_typ = rco + params["row"], params["col"], output_typ, units_length = rco if output_typ == "model3d": out_traces = get_generic_traces( obj, extra_backend=extra_backend, autosize=autosize, + units_length=units_length, **params, ) if extra_backend: diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index d0851986e..d82ca65a9 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -13,6 +13,17 @@ from magpylib._src.defaults.defaults_utility import linearize_dict from magpylib._src.style import get_style from magpylib._src.utility import format_obj_input +from magpylib._src.utility import get_unit_factor + +DEFAULT_ROW_COL_PARAMS = { + "row": 1, + "col": 1, + "output": "model3d", + "sumup": True, + "pixel_agg": "mean", + "in_out": "auto", + "units_length": "m", +} def get_legend_label(obj, style=None, suffix=True): @@ -34,17 +45,21 @@ def get_legend_label(obj, style=None, suffix=True): def place_and_orient_model3d( model_kwargs, + *, model_args=None, orientation=None, position=None, coordsargs=None, scale=1, return_model_args=False, + units_length="m", **kwargs, ): """places and orients mesh3d dict""" - if orientation is None and position is None: + if orientation is None and position is None and units_length == "m" and scale == 1: return {**model_kwargs, **kwargs} + unit_factor = get_unit_factor(units_length, target_unit="m") + scale_factor = scale / unit_factor position = (0.0, 0.0, 0.0) if position is None else position position = np.array(position, dtype=float) new_model_dict = {} @@ -63,7 +78,7 @@ def place_and_orient_model3d( if orientation is not None: vertices = orientation.apply(vertices) - new_vertices = (vertices * scale + position).T + new_vertices = (vertices * scale_factor + position).T new_vertices = np.reshape(new_vertices, vert_shape) for i, k in enumerate("xyz"): key = coordsargs[k] @@ -257,7 +272,9 @@ def get_flatten_objects_properties(*objs, colorsequence, **kwargs): props["row_cols"] = flat_objs[subobj]["row_cols"] elif "row_cols" not in props: props["row_cols"] = [] - props["row_cols"].extend([(obj["row"], obj["col"], obj["output"])]) + props["row_cols"].extend( + [(obj["row"], obj["col"], obj["output"], obj["units_length"])] + ) flat_objs.update(flat_sub_objs) kwargs = {k: v for k, v in kwargs.items() if not k.startswith("style")} return flat_objs, kwargs @@ -578,14 +595,7 @@ def subdivide_mesh_by_facecolor(trace): def process_show_input_objs(objs, **kwargs): """Extract max_rows and max_cols from obj list of dicts""" - defaults = { - "row": 1, - "col": 1, - "output": "model3d", - "sumup": True, - "pixel_agg": "mean", - "in_out": "auto", - } + defaults = DEFAULT_ROW_COL_PARAMS.copy() max_rows = max_cols = 1 flat_objs = [] new_objs = {} diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index d62d377c1..75d3d1e30 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -237,6 +237,25 @@ def filter_objects(obj_list, allow="sources+sensors", warn=True): 24: "Y", # yotta } +_UNIT_PREFIX_REVERSED = {v: k for k, v in _UNIT_PREFIX.items()} + + +@lru_cache(maxsize=None) +def get_unit_factor(unit_input, *, target_unit): + """return unit factor based on input and target unit""" + pref, factor_power = "", None + if unit_input: + if len(unit_input) == 2: + pref, *_ = unit_input + factor_power = _UNIT_PREFIX_REVERSED.get(pref, None) + if factor_power is None or len(unit_input) > 2: + valid_inputs = [f"{k}{target_unit}" for k in _UNIT_PREFIX_REVERSED] + raise ValueError( + f"Invalid unit input, must be one of {valid_inputs} got {unit_input!r}" + ) + factor = 10**factor_power + return factor + def unit_prefix(number, unit="", precision=3, char_between="") -> str: """ From b110200924bbffe62543e69c4b75ea093d284c38 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Sun, 2 Jun 2024 01:04:25 +0200 Subject: [PATCH 02/55] add zoom by subplots, fix axes labeling --- magpylib/_src/display/backend_matplotlib.py | 5 +- magpylib/_src/display/backend_plotly.py | 36 ++++-- magpylib/_src/display/display.py | 6 - magpylib/_src/display/traces_generic.py | 133 +++++++++++--------- magpylib/_src/display/traces_utility.py | 97 +++++++------- 5 files changed, 156 insertions(+), 121 deletions(-) diff --git a/magpylib/_src/display/backend_matplotlib.py b/magpylib/_src/display/backend_matplotlib.py index 3aecfbdcb..1ad47e747 100644 --- a/magpylib/_src/display/backend_matplotlib.py +++ b/magpylib/_src/display/backend_matplotlib.py @@ -249,6 +249,7 @@ def display_matplotlib( """Display objects and paths graphically using the matplotlib library.""" frames = data["frames"] ranges = data["ranges"] + labels = data["labels"] fig_kwargs = {} if not fig_kwargs else fig_kwargs fig_kwargs = {"dpi": 80, **fig_kwargs} @@ -353,8 +354,8 @@ def draw_frame(frame_ind): count = count_with_labels.get(row_col_num, 0) if ax.name == "3d": ax.set( - **{f"{k}label": f"{k} (m)" for k in "xyz"}, - **{f"{k}lim": r for k, r in zip("xyz", ranges)}, + **{f"{k}label": labels[row_col_num][k] for k in "xyz"}, + **{f"{k}lim": r for k, r in zip("xyz", ranges[row_col_num])}, ) ax.set_box_aspect(aspect=(1, 1, 1)) if 0 < count <= legend_maxitems: diff --git a/magpylib/_src/display/backend_plotly.py b/magpylib/_src/display/backend_plotly.py index 56efb9a97..eca3fae77 100644 --- a/magpylib/_src/display/backend_plotly.py +++ b/magpylib/_src/display/backend_plotly.py @@ -75,7 +75,7 @@ def match_args(ttype: str): return set(named_args) -def apply_fig_ranges(fig, ranges, apply2d=True): +def apply_fig_ranges(fig, ranges, labels, apply2d=True): """This is a helper function which applies the ranges properties of the provided `fig` object according to a provided ranges. All three space direction will be equal and match the maximum of the ranges needed to display all objects, including their paths. @@ -92,15 +92,26 @@ def apply_fig_ranges(fig, ranges, apply2d=True): ------- None: NoneType """ - fig.update_scenes( - **{ - f"{k}axis": {"range": ranges[i], "autorange": False, "title": f"{k} (m)"} - for i, k in enumerate("xyz") - }, - aspectratio={k: 1 for k in "xyz"}, - aspectmode="manual", - camera_eye={"x": 1, "y": -1.5, "z": 1.4}, - ) + for rc, ranges in ranges.items(): + row, col = rc + kwargs = { + **{ + f"{k}axis": { + "range": ranges[i], + "autorange": False, + "title": labels[rc][k], + } + for i, k in enumerate("xyz") + }, + "aspectratio": {k: 1 for k in "xyz"}, + "aspectmode": "manual", + "camera_eye": {"x": 1, "y": -1.5, "z": 1.4}, + } + + # pylint: disable=protected-access + if fig._grid_ref is not None: + kwargs.update({"row": row, "col": col}) + fig.update_scenes(**kwargs) if apply2d: apply_2d_ranges(fig) @@ -274,7 +285,6 @@ def process_extra_trace(model): def display_plotly( data, - zoom=1, canvas=None, renderer=None, return_fig=False, @@ -347,9 +357,9 @@ def display_plotly( ) ranges = data["ranges"] if extra_data: - ranges = get_scene_ranges(*frames[0]["data"], zoom=zoom) + ranges = get_scene_ranges(*frames[0]["data"]) if update_layout: - apply_fig_ranges(fig, ranges, apply2d=isanimation) + apply_fig_ranges(fig, ranges, labels=data["labels"], apply2d=isanimation) fig.update_layout( legend_itemsizing="constant", # legend_groupclick="toggleitem", diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 2cbaadf2e..6086313b2 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -55,7 +55,6 @@ def show( cls, *objs, backend, - zoom=0, title=None, max_rows=None, max_cols=None, @@ -118,13 +117,11 @@ def show( objs, supports_colorgradient=self.supports["colorgradient"], backend=backend, - zoom=zoom, title=title, **frame_kwargs, ) return self.show_func_getter()( data, - zoom=zoom, max_rows=max_rows, max_cols=max_cols, subplot_specs=subplot_specs, @@ -180,7 +177,6 @@ def _show( *objects, backend=None, animation=False, - zoom=0, markers=None, **kwargs, ): @@ -203,7 +199,6 @@ def _show( # input checks backend = check_format_input_backend(backend) - check_input_zoom(zoom) check_input_animation(animation) check_format_input_vector( markers, @@ -231,7 +226,6 @@ def _show( return RegisteredBackend.show( backend=backend, *objects, - zoom=zoom, animation=animation, **kwargs, ) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index e6542bd20..2b28f6702 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -20,10 +20,11 @@ from magpylib._src.defaults.defaults_utility import ALLOWED_SYMBOLS from magpylib._src.defaults.defaults_utility import linearize_dict from magpylib._src.display.traces_utility import draw_arrowed_line -from magpylib._src.display.traces_utility import get_flatten_objects_properties from magpylib._src.display.traces_utility import get_legend_label +from magpylib._src.display.traces_utility import get_objects_props_by_row_col from magpylib._src.display.traces_utility import get_rot_pos_from_path from magpylib._src.display.traces_utility import get_scene_ranges +from magpylib._src.display.traces_utility import get_unit_factor from magpylib._src.display.traces_utility import getColorscale from magpylib._src.display.traces_utility import getIntensity from magpylib._src.display.traces_utility import group_traces @@ -246,7 +247,7 @@ def get_trace2D_dict( return trace -def get_generic_traces_2D( +def get_traces_2D( *, objects, output=("Bx", "By", "Bz"), @@ -409,7 +410,7 @@ def process_extra_trace(model): return trace3d -def get_generic_traces( +def get_generic_traces3D( input_obj, autosize=None, legendgroup=None, @@ -719,7 +720,7 @@ def extract_animation_properties( return path_indices, exp, frame_duration -def draw_frame(objs, colorsequence=None, zoom=0.0, autosize=None, **kwargs) -> Tuple: +def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: """ Creates traces from input `objs` and provided parameters, updates the size of objects like Sensors and Dipoles in `kwargs` depending on the canvas size. @@ -738,82 +739,101 @@ def draw_frame(objs, colorsequence=None, zoom=0.0, autosize=None, **kwargs) -> T style_path_frames = kwargs.get( "style_path_frames", [-1] ) # get before next func strips style - flat_objs_props, kwargs = get_flatten_objects_properties( + objs_props_by_row_col, kwargs = get_objects_props_by_row_col( *objs, colorsequence=colorsequence, **kwargs ) - traces_dict, traces_to_resize_dict, extra_backend_traces = get_row_col_traces( - flat_objs_props, **kwargs - ) - traces = [t for tr in traces_dict.values() for t in tr] - ranges = get_scene_ranges(*traces, *extra_backend_traces, zoom=zoom) - if autosize is None or autosize == "return": - # pylint: disable=no-member - autosize = np.mean(np.diff(ranges)) / default_settings.display.autosizefactor - - traces_dict_2, _, extra_backend_traces2 = get_row_col_traces( - traces_to_resize_dict, autosize=autosize, **kwargs - ) - traces_dict.update(traces_dict_2) - extra_backend_traces.extend(extra_backend_traces2) + traces_dict = {} + extra_backend_traces = [] + autosize_out = {} + labels = {} + zoom = {} + for rc, objs_props in objs_props_by_row_col.items(): + if objs_props["rc_params"]["output"] != "model3d": + continue + rc_keys = ("row", "col", "units_length") + rc_params = {k: v for k, v in objs_props["rc_params"].items() if k in rc_keys} + traces_dict_1, extra_backend_traces_1 = get_traces_3D( + objs_props["objects"], **rc_params, **kwargs + ) + if autosize is None or autosize == "return": + labels[rc] = {k: f"{k} ({rc_params['units_length']})" for k in "xyz"} + zoom[rc] = objs_props["rc_params"]["zoom"] + traces = [t for tr in traces_dict_1.values() for t in tr] + ranges = get_scene_ranges( + *traces, *extra_backend_traces_1, zoom=objs_props["rc_params"]["zoom"] + ) + # pylint: disable=no-member + unit_factor = get_unit_factor(rc_params["units_length"], target_unit="m") + autosize_out[rc] = ( + np.mean(np.diff(ranges[rc])) / default_settings.display.autosizefactor + ) * unit_factor + to_resize_keys = { + k for k, v in traces_dict_1.items() if v and "_autosize" in v[0] + } + flat_objs_props = { + k: v for k, v in objs_props["objects"].items() if k in to_resize_keys + } + traces_dict_2, extra_backend_traces_2 = get_traces_3D( + flat_objs_props, autosize=autosize_out.get(rc, None), **rc_params, **kwargs + ) + traces_dict.update( + {(k, *rc): v for k, v in {**traces_dict_1, **traces_dict_2}.items()} + ) + extra_backend_traces.extend([*extra_backend_traces_1, *extra_backend_traces_2]) traces = group_traces(*[t for tr in traces_dict.values() for t in tr]) + obj_list_2d = [o for o in objs if o["output"] != "model3d"] for objs_2d in obj_list_2d: - traces2d = get_generic_traces_2D( + traces2d = get_traces_2D( **objs_2d, style_path_frames=style_path_frames, ) traces.extend(traces2d) - return traces, autosize, ranges, extra_backend_traces + return ( + traces, + extra_backend_traces, + {"autosize": autosize_out, "labels": labels, "zoom": zoom}, + ) -def get_row_col_traces(flat_objs_props, extra_backend=False, autosize=None, **kwargs): +def get_traces_3D(flat_objs_props, extra_backend=False, autosize=None, **kwargs): """Return traces, traces to resize and extra_backend_traces""" # pylint: disable=protected-access extra_backend_traces = [] traces_dict = {} - traces_to_resize_dict = {} for obj, params in flat_objs_props.items(): - params.update(kwargs) if autosize is None and getattr(obj, "_autosize", False): - traces_to_resize_dict[obj] = {**params} # temporary coordinates to be able to calculate ranges x, y, z = obj._position.T - traces_dict[obj] = [{"x": x, "y": y, "z": z}] + traces_dict[obj] = [{"x": x, "y": y, "z": z, "_autosize": True}] else: + params.update(kwargs) traces_dict[obj] = [] - rco_obj = params.pop("row_cols") orig_style = getattr(obj, "_style", None) try: style_temp = params.pop("style", None) - for rco in rco_obj: - # temporary replace style attribute - obj._style = style_temp - if len(rco_obj) >= 2 and style_temp: - # deepcopy style only if obj is in multiple subplots. - obj._style = style_temp.copy() - params["row"], params["col"], output_typ, units_length = rco - if output_typ == "model3d": - out_traces = get_generic_traces( - obj, - extra_backend=extra_backend, - autosize=autosize, - units_length=units_length, - **params, - ) - if extra_backend: - extra_backend_traces.extend( - out_traces.get(extra_backend, []) - ) - traces_dict[obj].extend(out_traces["generic"]) + # temporary replace style attribute + obj._style = style_temp + if style_temp: + # deepcopy style only if obj is in multiple subplots. + obj._style = style_temp.copy() + out_traces = get_generic_traces3D( + obj, + extra_backend=extra_backend, + autosize=autosize, + **params, + ) + if extra_backend: + extra_backend_traces.extend(out_traces.get(extra_backend, [])) + traces_dict[obj].extend(out_traces["generic"]) finally: obj._style = orig_style - return traces_dict, traces_to_resize_dict, extra_backend_traces + return traces_dict, extra_backend_traces def get_frames( objs, colorsequence=None, - zoom=1, title=None, animation=False, supports_colorgradient=True, @@ -847,25 +867,25 @@ def get_frames( ) # create frame for each path index or downsampled path index frames = [] - autosize = "return" + title_str = title + rc_params = {"autosize": "return"} for i, ind in enumerate(path_indices): extra_backend_traces = [] if animation: kwargs["style_path_frames"] = [ind] title = "Animation 3D - " if title is None else title title_str = f"""{title}path index: {ind+1:0{exp}d}""" - traces, autosize_init, ranges, extra_backend_traces = draw_frame( + traces, extra_backend_traces, rc_params_temp = draw_frame( objs, - colorsequence, - zoom, - autosize=autosize, + colorsequence=colorsequence, + autosize=rc_params["autosize"], supports_colorgradient=supports_colorgradient, extra_backend=backend, **kwargs, ) if i == 0: # get the dipoles and sensors autosize from first frame - autosize = autosize_init + rc_params = rc_params_temp frames.append( { "data": traces, @@ -877,10 +897,11 @@ def get_frames( clean_legendgroups(frames) traces = [t for frame in frames for t in frame["data"]] - ranges = get_scene_ranges(*traces, *extra_backend_traces, zoom=zoom) + ranges = get_scene_ranges(*traces, *extra_backend_traces, zoom=rc_params["zoom"]) out = { "frames": frames, "ranges": ranges, + "labels": rc_params["labels"], "input_kwargs": {**kwargs, **animation_kwargs}, } if animation: diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index d82ca65a9..ca627da07 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -22,6 +22,7 @@ "sumup": True, "pixel_agg": "mean", "in_out": "auto", + "zoom": 0, "units_length": "m", } @@ -58,8 +59,7 @@ def place_and_orient_model3d( """places and orients mesh3d dict""" if orientation is None and position is None and units_length == "m" and scale == 1: return {**model_kwargs, **kwargs} - unit_factor = get_unit_factor(units_length, target_unit="m") - scale_factor = scale / unit_factor + length_factor = get_unit_factor(units_length, target_unit="m") position = (0.0, 0.0, 0.0) if position is None else position position = np.array(position, dtype=float) new_model_dict = {} @@ -78,7 +78,7 @@ def place_and_orient_model3d( if orientation is not None: vertices = orientation.apply(vertices) - new_vertices = (vertices * scale_factor + position).T + new_vertices = (vertices * scale + position).T / length_factor new_vertices = np.reshape(new_vertices, vert_shape) for i, k in enumerate("xyz"): key = coordsargs[k] @@ -259,7 +259,7 @@ def get_rot_pos_from_path(obj, show_path=None): return rots, poss, inds -def get_flatten_objects_properties(*objs, colorsequence, **kwargs): +def get_objects_props_by_row_col(*objs, colorsequence, **kwargs): """Return flat dict with objs as keys object properties as values. Properties include: row_cols, style, legendgroup, legendtext""" flat_objs = {} @@ -268,14 +268,14 @@ def get_flatten_objects_properties(*objs, colorsequence, **kwargs): *obj["objects"], colorsequence=colorsequence, **kwargs ) for subobj, props in flat_sub_objs.items(): - if subobj in flat_objs: - props["row_cols"] = flat_objs[subobj]["row_cols"] - elif "row_cols" not in props: - props["row_cols"] = [] - props["row_cols"].extend( - [(obj["row"], obj["col"], obj["output"], obj["units_length"])] - ) - flat_objs.update(flat_sub_objs) + rc = obj["row"], obj["col"] + if rc not in flat_objs: + flat_objs[rc] = { + "objects": {}, + "rc_params": {k: v for k, v in obj.items() if k != "objects"}, + } + flat_objs[rc]["objects"][subobj] = props + kwargs = {k: v for k, v in kwargs.items() if not k.startswith("style")} return flat_objs, kwargs @@ -484,38 +484,46 @@ def getColorscale( return colorscale -def get_scene_ranges(*traces, zoom=1) -> np.ndarray: +def get_scene_ranges(*traces, zoom=0) -> np.ndarray: """ Returns 3x2 array of the min and max ranges in x,y,z directions of input traces. Traces can be any plotly trace object or a dict, with x,y,z numbered parameters. """ - trace3d_found = False - if traces: - ranges = {k: [] for k in "xyz"} - for tr in traces: - coords = "xyz" - if "constructor" in tr: - verts, *_ = get_vertices_from_model( - model_args=tr.get("args", None), - model_kwargs=tr.get("kwargs", None), - coordsargs=tr.get("coordsargs", None), - ) - tr = dict(zip("xyz", verts)) - if "z" in tr: # only extend range for 3d traces - trace3d_found = True - pts = np.array([tr[k] for k in coords], dtype="float64").T - try: # for mesh3d, use only vertices part of faces for range calculation - inds = np.array([tr[k] for k in "ijk"], dtype="int64").T - pts = pts[inds] - except KeyError: - # for 2d meshes, nothing special needed - pass - pts = pts.reshape(-1, 3) - if pts.size != 0: - min_max = np.nanmin(pts, axis=0), np.nanmax(pts, axis=0) - for v, min_, max_ in zip(ranges.values(), *min_max): - v.extend([min_, max_]) - if trace3d_found: + ranges_rc = {} + tr_dim_count = {} + for tr in traces: + rc = tr.get("row", 1), tr.get("col", 1) + if rc not in ranges_rc: + ranges_rc[rc] = {k: [] for k in "xyz"} + tr_dim_count[rc] = {"2D": 0, "3D": 0} + coords = "xyz" + if "constructor" in tr: + verts, *_ = get_vertices_from_model( + model_args=tr.get("args", None), + model_kwargs=tr.get("kwargs", None), + coordsargs=tr.get("coordsargs", None), + ) + tr = dict(zip("xyz", verts)) + if "z" not in tr: # only extend range for 3d traces + tr_dim_count[rc]["2D"] += 1 + else: + tr_dim_count[rc]["3D"] += 1 + ranges_rc[rc]["trace3d_found"] = True + pts = np.array([tr[k] for k in coords], dtype="float64").T + try: # for mesh3d, use only vertices part of faces for range calculation + inds = np.array([tr[k] for k in "ijk"], dtype="int64").T + pts = pts[inds] + except KeyError: + # for 2d meshes, nothing special needed + pass + pts = pts.reshape(-1, 3) + if pts.size != 0: + min_max = np.nanmin(pts, axis=0), np.nanmax(pts, axis=0) + for v, min_, max_ in zip(ranges_rc[rc].values(), *min_max): + v.extend([min_, max_]) + for rc, ranges in ranges_rc.items(): + if tr_dim_count[rc]["3D"]: + zo = zoom[rc] if isinstance(zoom, dict) else zoom # SET 3D PLOT BOUNDARIES # collect min/max from all elements r = np.array([[np.nanmin(v), np.nanmax(v)] for v in ranges.values()]) @@ -523,10 +531,11 @@ def get_scene_ranges(*traces, zoom=1) -> np.ndarray: m = size.max() / 2 m = 1 if m == 0 else m center = r.mean(axis=1) - ranges = np.array([center - m * (1 + zoom), center + m * (1 + zoom)]).T - if not traces or not trace3d_found: - ranges = np.array([[-1.0, 1.0]] * 3) - return ranges + ranges = np.array([center - m * (1 + zo), center + m * (1 + zo)]).T + else: + ranges = np.array([[-1.0, 1.0]] * 3) + ranges_rc[rc] = ranges + return ranges_rc def group_traces(*traces): From 998b7c74e5079f35a735d2e52953cc6df3159d8b Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 3 Jun 2024 11:59:24 +0200 Subject: [PATCH 03/55] fix empty subplots --- magpylib/_src/display/traces_generic.py | 2 +- magpylib/_src/display/traces_utility.py | 2 ++ tests/test_display_matplotlib.py | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 2b28f6702..f29a36dd5 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -745,7 +745,7 @@ def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: traces_dict = {} extra_backend_traces = [] autosize_out = {} - labels = {} + labels = {(1, 1): {k: "" for k in "xyz"}} zoom = {} for rc, objs_props in objs_props_by_row_col.items(): if objs_props["rc_params"]["output"] != "model3d": diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index ca627da07..96eebb111 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -535,6 +535,8 @@ def get_scene_ranges(*traces, zoom=0) -> np.ndarray: else: ranges = np.array([[-1.0, 1.0]] * 3) ranges_rc[rc] = ranges + if not ranges_rc: + ranges_rc[(1, 1)] = np.array([[-1.0, 1.0]] * 3) return ranges_rc diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index 8a4fc464a..db79434d8 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -422,8 +422,7 @@ def test_matplotlib_model3d_extra_updatefunc(): def test_empty_display(): """should not fail if nothing to display""" - ax = plt.subplot(projection="3d") - magpy.show(canvas=ax, backend="matplotlib", return_fig=True) + magpy.show(backend="matplotlib", return_fig=True) def test_graphics_model_mpl(): From ceb90718cdaada1a2d59fd9f3c942e7f7a731942 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 3 Jun 2024 13:07:37 +0200 Subject: [PATCH 04/55] fix subplot ranges and animation --- magpylib/_src/display/backend_matplotlib.py | 9 +++++---- magpylib/_src/display/traces_generic.py | 4 ++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/display/backend_matplotlib.py b/magpylib/_src/display/backend_matplotlib.py index 1ad47e747..206de393a 100644 --- a/magpylib/_src/display/backend_matplotlib.py +++ b/magpylib/_src/display/backend_matplotlib.py @@ -353,10 +353,11 @@ def draw_frame(frame_ind): for row_col_num, ax in axes.items(): count = count_with_labels.get(row_col_num, 0) if ax.name == "3d": - ax.set( - **{f"{k}label": labels[row_col_num][k] for k in "xyz"}, - **{f"{k}lim": r for k, r in zip("xyz", ranges[row_col_num])}, - ) + if row_col_num in ranges: + ax.set( + **{f"{k}label": labels[row_col_num][k] for k in "xyz"}, + **{f"{k}lim": r for k, r in zip("xyz", ranges[row_col_num])}, + ) ax.set_box_aspect(aspect=(1, 1, 1)) if 0 < count <= legend_maxitems: lg_kw = {"bbox_to_anchor": (1.04, 1), "loc": "upper left"} diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index f29a36dd5..cd6f0a954 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -256,6 +256,8 @@ def get_traces_2D( sumup=True, pixel_agg=None, in_out="auto", + units_length="m", + zoom=0, style_path_frames=None, ): """draws and animates sensor values over a path in a subplot""" @@ -767,6 +769,8 @@ def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: autosize_out[rc] = ( np.mean(np.diff(ranges[rc])) / default_settings.display.autosizefactor ) * unit_factor + else: + autosize_out = autosize to_resize_keys = { k for k, v in traces_dict_1.items() if v and "_autosize" in v[0] } From 31a88f941bcfe0860cc8a268fc5f5630efe6d9b3 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 3 Jun 2024 13:40:55 +0200 Subject: [PATCH 05/55] fix zoom check --- magpylib/_src/display/display.py | 1 - magpylib/_src/display/traces_utility.py | 3 ++- magpylib/_src/input_checks.py | 9 ++------- tests/test_input_checks.py | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 6086313b2..ec3513eb2 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -16,7 +16,6 @@ from magpylib._src.input_checks import check_format_input_backend from magpylib._src.input_checks import check_format_input_vector from magpylib._src.input_checks import check_input_animation -from magpylib._src.input_checks import check_input_zoom from magpylib._src.utility import check_path_format disp_args = get_defaults_dict("display").keys() diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 96eebb111..1ad30abf7 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -11,6 +11,7 @@ from magpylib._src.defaults.defaults_classes import default_settings from magpylib._src.defaults.defaults_utility import linearize_dict +from magpylib._src.input_checks import check_input_zoom from magpylib._src.style import get_style from magpylib._src.utility import format_obj_input from magpylib._src.utility import get_unit_factor @@ -616,7 +617,7 @@ def process_show_input_objs(objs, **kwargs): obj = {**defaults, **obj, **kwargs} else: obj = {**defaults, "objects": obj, **kwargs} - + check_input_zoom(obj.get("zoom", None)) obj["objects"] = format_obj_input( obj["objects"], allow="sources+sensors+collections" ) diff --git a/magpylib/_src/input_checks.py b/magpylib/_src/input_checks.py index 434c6a8b4..54b2a029d 100644 --- a/magpylib/_src/input_checks.py +++ b/magpylib/_src/input_checks.py @@ -69,14 +69,9 @@ def check_array_shape(inp: np.ndarray, dims: tuple, shape_m1: int, length=None, def check_input_zoom(inp): """check show zoom input""" - if not isinstance(inp, numbers.Number): - raise MagpylibBadUserInput( - "Input parameter `zoom` must be a number `zoom>=0`.\n" - f"Instead received {inp}." - ) - if inp < 0: + if not (isinstance(inp, numbers.Number) and inp >= 0): raise MagpylibBadUserInput( - "Input parameter `zoom` must be a number `zoom>=0`.\n" + "Input parameter `zoom` must be a positive number or zero.\n" f"Instead received {inp}." ) diff --git a/tests/test_input_checks.py b/tests/test_input_checks.py index 68024e801..b92c048b9 100644 --- a/tests/test_input_checks.py +++ b/tests/test_input_checks.py @@ -553,7 +553,7 @@ def test_input_show_zoom_bad(zoom): """bad show zoom inputs""" x = magpy.Sensor() with pytest.raises(MagpylibBadUserInput): - magpy.show(x, zoom=zoom) + magpy.show(x, zoom=zoom, return_fig=True, backend="plotly") @pytest.mark.parametrize( From 98786fa0288cc22e144616ff8e5e763c0fe404af Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 3 Jun 2024 14:09:56 +0200 Subject: [PATCH 06/55] fix labeling --- magpylib/_src/display/backend_plotly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/display/backend_plotly.py b/magpylib/_src/display/backend_plotly.py index eca3fae77..69737ca35 100644 --- a/magpylib/_src/display/backend_plotly.py +++ b/magpylib/_src/display/backend_plotly.py @@ -99,7 +99,7 @@ def apply_fig_ranges(fig, ranges, labels, apply2d=True): f"{k}axis": { "range": ranges[i], "autorange": False, - "title": labels[rc][k], + "title": labels.get(rc, {k: "" for k in "xyz"})[k], } for i, k in enumerate("xyz") }, From d5004d505c4b361d78c019d61106b171b91169dc Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 3 Jun 2024 17:38:46 +0200 Subject: [PATCH 07/55] fix generic extra trace scaling --- magpylib/_src/display/traces_generic.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index cd6f0a954..cd58163f2 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -534,9 +534,7 @@ def get_generic_traces3D( name_suff = tr.pop("name_suffix", None) name = tr.get("name", "") if legendtext is None else legendtext for orient, pos in zip(orientations, positions): - tr1 = place_and_orient_model3d( - tr, orientation=orient, position=pos, units_length=units_length - ) + tr1 = place_and_orient_model3d(tr, orientation=orient, position=pos) if name_suff is not None: tr1["name"] = f"{name}{name_suff}" temp_rot_traces.append(tr1) @@ -549,6 +547,7 @@ def get_generic_traces3D( path_traces_generic = group_traces(*path_traces_generic) for tr in path_traces_generic: + tr.update(place_and_orient_model3d(tr, units_length=units_length)) tr.update(row=row, col=col) if tr.get("opacity", None) is None: tr["opacity"] = style.opacity From b208c365fbd7a79cab24af70f422c13bf83c04a4 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 3 Jun 2024 18:18:05 +0200 Subject: [PATCH 08/55] fix extra non generic scaling --- magpylib/_src/display/traces_generic.py | 25 +++++++++++++------------ magpylib/_src/display/traces_utility.py | 2 +- magpylib/_src/utility.py | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index cd58163f2..411d2a751 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -387,7 +387,7 @@ def get_label_and_color(obj): return traces -def process_extra_trace(model): +def process_extra_trace(model, units_length): "process extra trace attached to some magpylib object" extr = model["model3d"] model_kwargs = {**(extr.kwargs() if callable(extr.kwargs) else extr.kwargs)} @@ -405,6 +405,7 @@ def process_extra_trace(model): position=model["position"], coordsargs=extr.coordsargs, scale=extr.scale, + units_length=units_length, return_model_args=True, ) trace3d["kwargs"].update(kwargs) @@ -500,24 +501,24 @@ def get_generic_traces3D( extr.update(extr.updatefunc()) # update before checking backend if extr.backend == "generic": extr.update(extr.updatefunc()) - tr_generic = {"opacity": style.opacity} + tr_non_generic = {"opacity": style.opacity} ttype = extr.constructor.lower() obj_extr_trace = extr.kwargs() if callable(extr.kwargs) else extr.kwargs obj_extr_trace = {"type": ttype, **obj_extr_trace} if ttype == "scatter3d": for k in ("marker", "line"): - tr_generic[f"{k}_color"] = tr_generic.get( + tr_non_generic[f"{k}_color"] = tr_non_generic.get( f"{k}_color", style.color ) elif ttype == "mesh3d": - tr_generic["showscale"] = tr_generic.get("showscale", False) - tr_generic["color"] = tr_generic.get("color", style.color) + tr_non_generic["showscale"] = tr_non_generic.get("showscale", False) + tr_non_generic["color"] = tr_non_generic.get("color", style.color) else: # pragma: no cover raise ValueError( f"{ttype} is not supported, only 'scatter3d' and 'mesh3d' are" ) - tr_generic.update(linearize_dict(obj_extr_trace, separator="_")) - traces_generic.append(tr_generic) + tr_non_generic.update(linearize_dict(obj_extr_trace, separator="_")) + traces_generic.append(tr_non_generic) if is_mag_arrows: mag = input_obj.magnetization @@ -577,7 +578,7 @@ def get_generic_traces3D( extr.update(extr.updatefunc()) # update before checking backend if extr.backend == extra_backend: for orient, pos in zip(orientations, positions): - tr_generic = { + tr_non_generic = { "model3d": extr, "position": pos, "orientation": orient, @@ -595,8 +596,8 @@ def get_generic_traces3D( "col": col, }, } - tr_generic = process_extra_trace(tr_generic) - path_traces_extra_non_generic_backend.append(tr_generic) + tr_non_generic = process_extra_trace(tr_non_generic, units_length) + path_traces_extra_non_generic_backend.append(tr_non_generic) out.update({extra_backend: path_traces_extra_non_generic_backend}) return out @@ -764,10 +765,10 @@ def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: *traces, *extra_backend_traces_1, zoom=objs_props["rc_params"]["zoom"] ) # pylint: disable=no-member - unit_factor = get_unit_factor(rc_params["units_length"], target_unit="m") + length_factor = get_unit_factor(rc_params["units_length"], target_unit="m") autosize_out[rc] = ( np.mean(np.diff(ranges[rc])) / default_settings.display.autosizefactor - ) * unit_factor + ) / length_factor else: autosize_out = autosize to_resize_keys = { diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 1ad30abf7..771c53089 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -79,7 +79,7 @@ def place_and_orient_model3d( if orientation is not None: vertices = orientation.apply(vertices) - new_vertices = (vertices * scale + position).T / length_factor + new_vertices = (vertices * scale + position).T * length_factor new_vertices = np.reshape(new_vertices, vert_shape) for i, k in enumerate("xyz"): key = coordsargs[k] diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 75d3d1e30..6a431ab4d 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -253,7 +253,7 @@ def get_unit_factor(unit_input, *, target_unit): raise ValueError( f"Invalid unit input, must be one of {valid_inputs} got {unit_input!r}" ) - factor = 10**factor_power + factor = 1 / (10**factor_power) return factor From 5b2e6d39612590c7924c461d48ab252809c131ee Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 3 Jun 2024 18:27:10 +0200 Subject: [PATCH 09/55] pylint --- magpylib/_src/display/backend_plotly.py | 10 +++++----- magpylib/_src/display/traces_generic.py | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/magpylib/_src/display/backend_plotly.py b/magpylib/_src/display/backend_plotly.py index 69737ca35..25758c738 100644 --- a/magpylib/_src/display/backend_plotly.py +++ b/magpylib/_src/display/backend_plotly.py @@ -75,7 +75,7 @@ def match_args(ttype: str): return set(named_args) -def apply_fig_ranges(fig, ranges, labels, apply2d=True): +def apply_fig_ranges(fig, ranges_rc, labels, apply2d=True): """This is a helper function which applies the ranges properties of the provided `fig` object according to a provided ranges. All three space direction will be equal and match the maximum of the ranges needed to display all objects, including their paths. @@ -92,7 +92,7 @@ def apply_fig_ranges(fig, ranges, labels, apply2d=True): ------- None: NoneType """ - for rc, ranges in ranges.items(): + for rc, ranges in ranges_rc.items(): row, col = rc kwargs = { **{ @@ -355,11 +355,11 @@ def display_plotly( rows=rows_list, cols=cols_list, ) - ranges = data["ranges"] + ranges_rc = data["ranges"] if extra_data: - ranges = get_scene_ranges(*frames[0]["data"]) + ranges_rc = get_scene_ranges(*frames[0]["data"]) if update_layout: - apply_fig_ranges(fig, ranges, labels=data["labels"], apply2d=isanimation) + apply_fig_ranges(fig, ranges_rc, labels=data["labels"], apply2d=isanimation) fig.update_layout( legend_itemsizing="constant", # legend_groupclick="toggleitem", diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 411d2a751..39e7bcd99 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -256,9 +256,10 @@ def get_traces_2D( sumup=True, pixel_agg=None, in_out="auto", + style_path_frames=None, + # pylint: disable=unused-argument units_length="m", zoom=0, - style_path_frames=None, ): """draws and animates sensor values over a path in a subplot""" # pylint: disable=import-outside-toplevel From 0bd2eb462819523548cc29b6b6a3255d2e3c6e37 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 3 Jun 2024 19:11:44 +0200 Subject: [PATCH 10/55] add cm and dm as units_length inputs --- magpylib/_src/utility.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 6a431ab4d..516c2ced6 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -241,13 +241,17 @@ def filter_objects(obj_list, allow="sources+sensors", warn=True): @lru_cache(maxsize=None) -def get_unit_factor(unit_input, *, target_unit): +def get_unit_factor(unit_input, *, target_unit, deci_centi=True): """return unit factor based on input and target unit""" pref, factor_power = "", None if unit_input: if len(unit_input) == 2: pref, *_ = unit_input - factor_power = _UNIT_PREFIX_REVERSED.get(pref, None) + prefs = _UNIT_PREFIX_REVERSED + if deci_centi: + prefs = {**_UNIT_PREFIX_REVERSED, "d": -1, "c": -2} + factor_power = prefs.get(pref, None) + if factor_power is None or len(unit_input) > 2: valid_inputs = [f"{k}{target_unit}" for k in _UNIT_PREFIX_REVERSED] raise ValueError( From 0223123ace434e5ee1f1ff0112f1799418d335a1 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 3 Jun 2024 19:14:34 +0200 Subject: [PATCH 11/55] fix ranges on non generic extra traces --- magpylib/_src/display/traces_utility.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 771c53089..c339de0dc 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -493,18 +493,20 @@ def get_scene_ranges(*traces, zoom=0) -> np.ndarray: ranges_rc = {} tr_dim_count = {} for tr in traces: - rc = tr.get("row", 1), tr.get("col", 1) - if rc not in ranges_rc: - ranges_rc[rc] = {k: [] for k in "xyz"} - tr_dim_count[rc] = {"2D": 0, "3D": 0} coords = "xyz" + rc = tr.get("row", 1), tr.get("col", 1) if "constructor" in tr: verts, *_ = get_vertices_from_model( model_args=tr.get("args", None), model_kwargs=tr.get("kwargs", None), coordsargs=tr.get("coordsargs", None), ) + kwex = tr["kwargs_extra"] tr = dict(zip("xyz", verts)) + rc = kwex["row"], kwex["col"] + if rc not in ranges_rc: + ranges_rc[rc] = {k: [] for k in "xyz"} + tr_dim_count[rc] = {"2D": 0, "3D": 0} if "z" not in tr: # only extend range for 3d traces tr_dim_count[rc]["2D"] += 1 else: From a66dc84576c401135329dda79cbae57194d5e201 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 3 Jun 2024 19:24:09 +0200 Subject: [PATCH 12/55] fix edge case range where no trace in subplot --- magpylib/_src/display/traces_generic.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 39e7bcd99..8adf7dfd5 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -762,14 +762,14 @@ def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: labels[rc] = {k: f"{k} ({rc_params['units_length']})" for k in "xyz"} zoom[rc] = objs_props["rc_params"]["zoom"] traces = [t for tr in traces_dict_1.values() for t in tr] - ranges = get_scene_ranges( + ranges_rc = get_scene_ranges( *traces, *extra_backend_traces_1, zoom=objs_props["rc_params"]["zoom"] ) - # pylint: disable=no-member + ranges = ranges_rc.get(rc, ranges_rc[(1, 1)]) length_factor = get_unit_factor(rc_params["units_length"], target_unit="m") - autosize_out[rc] = ( - np.mean(np.diff(ranges[rc])) / default_settings.display.autosizefactor - ) / length_factor + # pylint: disable=no-member + factor = default_settings.display.autosizefactor * length_factor + autosize_out[rc] = np.mean(np.diff(ranges)) / factor else: autosize_out = autosize to_resize_keys = { From 6054403731903b0a94d31c827f3bad35ae58c462 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 4 Jun 2024 00:30:16 +0200 Subject: [PATCH 13/55] rework row cols conflicting inputs checks --- magpylib/_src/display/traces_generic.py | 3 +- magpylib/_src/display/traces_utility.py | 63 +++++++++++++------------ magpylib/_src/utility.py | 61 ++++++++++++++++++++++++ tests/test_display_matplotlib.py | 5 +- 4 files changed, 99 insertions(+), 33 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 8adf7dfd5..cce3b8fa0 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -765,11 +765,10 @@ def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: ranges_rc = get_scene_ranges( *traces, *extra_backend_traces_1, zoom=objs_props["rc_params"]["zoom"] ) - ranges = ranges_rc.get(rc, ranges_rc[(1, 1)]) length_factor = get_unit_factor(rc_params["units_length"], target_unit="m") # pylint: disable=no-member factor = default_settings.display.autosizefactor * length_factor - autosize_out[rc] = np.mean(np.diff(ranges)) / factor + autosize_out[rc] = np.mean(np.diff(ranges_rc[rc])) / factor else: autosize_out = autosize to_resize_keys = { diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index c339de0dc..95b153c59 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -15,6 +15,7 @@ from magpylib._src.style import get_style from magpylib._src.utility import format_obj_input from magpylib._src.utility import get_unit_factor +from magpylib._src.utility import merge_dicts_with_conflict_check DEFAULT_ROW_COL_PARAMS = { "row": 1, @@ -610,49 +611,51 @@ def subdivide_mesh_by_facecolor(trace): def process_show_input_objs(objs, **kwargs): """Extract max_rows and max_cols from obj list of dicts""" defaults = DEFAULT_ROW_COL_PARAMS.copy() - max_rows = max_cols = 1 - flat_objs = [] - new_objs = {} - subplot_specs = {} + identifiers = ("row", "col") + unique_fields = tuple(k for k in defaults if k not in identifiers) + sources_and_sensors_only = [] + new_objs = [] for obj in objs: + # add missing kwargs if isinstance(obj, dict): obj = {**defaults, **obj, **kwargs} else: obj = {**defaults, "objects": obj, **kwargs} - check_input_zoom(obj.get("zoom", None)) + + # extend objects list obj["objects"] = format_obj_input( obj["objects"], allow="sources+sensors+collections" ) - flat_objs.extend(format_obj_input(obj["objects"], allow="sources+sensors")) - if obj["row"] is not None: - max_rows = max(max_rows, obj["row"]) - if obj["col"] is not None: - max_cols = max(max_cols, obj["col"]) - out = obj["output"] - key = (obj["row"], obj["col"], out if isinstance(out, str) else tuple(out)) - if key not in new_objs: - new_objs[key] = obj - else: - new_objs[key]["objects"] = list( - dict.fromkeys(new_objs[key]["objects"] + obj["objects"]) - ) - current_subplot_specs = subplot_specs.get(key[:2], obj["output"]) - if current_subplot_specs != obj["output"]: - raise ValueError( - f"Row/Col {key[:2]}, received conflicting output types " - f"{current_subplot_specs!r} vs {obj['output']!r}" - ) - subplot_specs[key[:2]] = obj["output"] + sources_and_sensors_only.extend( + format_obj_input(obj["objects"], allow="sources+sensors") + ) + new_objs.append(obj) + row_col_dict = merge_dicts_with_conflict_check( + new_objs, + target="objects", + identifiers=identifiers, + unique_fields=unique_fields, + ) + + # create subplot specs grid + row_cols = [*row_col_dict] + max_rows, max_cols = np.max(row_cols, axis=0).astype(int) if row_cols else (1, 1) + # convert to int to avoid np.int32 type conflicting with plolty subplot specs + max_rows, max_cols = int(max_rows), int(max_cols) specs = np.array([[{"type": "scene"}] * max_cols] * max_rows) - for inds, out in subplot_specs.items(): - if out != "model3d": - specs[inds[0] - 1, inds[1] - 1] = {"type": "xy"} + for rc, obj in row_col_dict.items(): + if obj["output"] != "model3d": + specs[rc[0] - 1, rc[1] - 1] = {"type": "xy"} if max_rows == 1 and max_cols == 1: max_rows = max_cols = None + + for obj in row_col_dict.values(): + check_input_zoom(obj.get("zoom", None)) + return ( - list(new_objs.values()), - list(dict.fromkeys(flat_objs)), + list(row_col_dict.values()), + list(dict.fromkeys(sources_and_sensors_only)), max_rows, max_cols, specs, diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 516c2ced6..e27062afc 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -421,3 +421,64 @@ def has_parameter(func: Callable, param_name: str) -> bool: """Check if input function has a specific parameter""" sig = signature(func) return param_name in sig.parameters + + +def merge_dicts_with_conflict_check(objs, *, target, identifiers, unique_fields): + """ + Merge dictionaries ensuring unique identifier fields don't lead to conflict. + + Parameters + ---------- + objs : list of dicts + List of dictionaries to be merged based on identifier fields. + target : str + The key in the dictionaries whose values are lists to be merged. + identifiers : list of str + Keys used to identify a unique dictionary. + unique_fields : list of str + Additional keys that must not conflict across merged dictionaries. + + Returns + ------- + dict of dicts + Merged dictionaries with combined `target` lists, ensuring no conflicts + in `unique_fields`. + + Raises + ------ + ValueError + If a conflict is detected in `unique_fields` for any `identifiers`. + + Notes + ----- + `objs` should be a list of dictionaries. Identifiers determine uniqueness, + and merging is done by extending the lists in the `target` key. If any of + the `unique_fields` conflict with previously tracked identifiers, a + `ValueError` is raised detailing the conflict. + + """ + merged_dict = {} + tracker = {} + for obj in objs: + key_dict = {k: obj[k] for k in identifiers} + key = tuple(key_dict.values()) + tracker_previous = tracker.get(key, None) + tracker_actual = tuple(obj[field] for field in unique_fields) + if key in tracker and tracker_previous != tracker_actual: + diff = [ + f"{f!r} first got {a!r} then {t!r}" + for f, a, t in zip(unique_fields, tracker_actual, tracker_previous) + if a != t + ] + raise ValueError( + f"Conflicting parameters detected for {key_dict}: {', '.join(diff)}." + ) + tracker[key] = tracker_actual + + if key not in merged_dict: + merged_dict[key] = obj + else: + merged_dict[key][target] = list( + dict.fromkeys([*merged_dict[key][target], *obj[target]]) + ) + return merged_dict diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index db79434d8..8575731a4 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -522,7 +522,10 @@ def test_bad_show_inputs(): ) with pytest.raises( ValueError, - match=r"Row/Col .* received conflicting output types.*", + match=( + r"Conflicting parameters detected for {'row': 1, 'col': 1}:" + r" 'output' first got 'model3d' then 'Bx'." + ), ): with magpy.show_context(animation=False, sumup=True, pixel_agg="mean") as s: s.show(cyl1, sensor, col=1, output="Bx") From fc0e790c74b135a4795eba8ba2f7e37ae2d39303 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 4 Jun 2024 10:42:01 +0200 Subject: [PATCH 14/55] update github actions --- .github/workflows/codeql.yml | 2 +- .github/workflows/python-app.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d7a6835b5..fdb64f535 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/setup-python@v5 - name: Initialize CodeQL uses: github/codeql-action/init@v2 diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index c06d4d8ce..fd77edb18 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -16,7 +16,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 - name: Setup Python uses: actions/setup-python@v4 with: @@ -45,7 +45,7 @@ jobs: steps: - name: Setup headless display uses: pyvista/setup-headless-display-action@v2 - - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: fetch-depth: 0 - name: Setup python for test ${{ matrix.py }} @@ -69,7 +69,7 @@ jobs: id-token: write steps: - name: Checkout source - uses: actions/checkout@v4 + uses: actions/setup-python@v5 - name: Set up Python 3.10 uses: actions/setup-python@v4 with: From 8a58de63d2e36442c57d2a2c39c78ae90336838d Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 4 Jun 2024 10:51:03 +0200 Subject: [PATCH 15/55] bump codeql actions versions --- .github/workflows/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fdb64f535..a23916378 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -27,17 +27,17 @@ jobs: uses: actions/setup-python@v5 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: config-file: ./.github/codeql/codeql-config.yml languages: ${{ matrix.language }} queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v2 + uses: github/codeql-action/autobuild@v3 if: ${{ matrix.language == 'javascript' || matrix.language == 'python' }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 with: category: "/language:${{ matrix.language }}" From 5166298b8ef2a2cef7032cc8e6a1f29ffde76229 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 4 Jun 2024 12:04:13 +0200 Subject: [PATCH 16/55] fix checkout --- .github/workflows/python-app.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index fd77edb18..c06d4d8ce 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -16,7 +16,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/setup-python@v5 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: @@ -45,7 +45,7 @@ jobs: steps: - name: Setup headless display uses: pyvista/setup-headless-display-action@v2 - - uses: actions/setup-python@v5 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup python for test ${{ matrix.py }} @@ -69,7 +69,7 @@ jobs: id-token: write steps: - name: Checkout source - uses: actions/setup-python@v5 + uses: actions/checkout@v4 - name: Set up Python 3.10 uses: actions/setup-python@v4 with: From e5a6e5cdea8116eed3c7eb4ad77db3f30a884680 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 4 Jun 2024 12:05:54 +0200 Subject: [PATCH 17/55] fix version --- .github/workflows/codeql.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a23916378..f9046a523 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: steps: - name: Checkout - uses: actions/setup-python@v5 + uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 From 11b368e419cb724516028888ae4e26c04c224b6f Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 4 Jun 2024 12:09:31 +0200 Subject: [PATCH 18/55] update --- .github/workflows/python-app.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index c06d4d8ce..1784a0458 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.11" - name: Set up testing tools and environment for pylint @@ -49,7 +49,7 @@ jobs: with: fetch-depth: 0 - name: Setup python for test ${{ matrix.py }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.py }} - name: Install tox @@ -71,7 +71,7 @@ jobs: - name: Checkout source uses: actions/checkout@v4 - name: Set up Python 3.10 - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.10" - name: Install flit From 24d90eeead101c64fbf5be26c11b1d9715072d61 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 4 Jun 2024 14:22:04 +0200 Subject: [PATCH 19/55] move to dev version --- CHANGELOG.md | 3 +++ README.md | 4 ++-- magpylib/__init__.py | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bc837e3c..2e42473b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## [Unreleased] - YYYY-MM-DD + + ## [5.0.3] - 2024-06-03 - Fix subplot object properties propagation ([#780](https://github.com/magpylib/magpylib/pull/780)) diff --git a/README.md b/README.md index d4b5998ec..e53873454 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Conda Cloud - MyBinder link + MyBinder link black @@ -136,7 +136,7 @@ A valid software citation could be author = {{Michael-Ortner et al.}}, title = {magpylib}, url = {https://magpylib.readthedocs.io/en/latest/}, - version = {5.0.3}, + version = {5.1.0dev}, date = {2023-06-25}, } ``` diff --git a/magpylib/__init__.py b/magpylib/__init__.py index 16290f12b..e04e76c49 100644 --- a/magpylib/__init__.py +++ b/magpylib/__init__.py @@ -28,7 +28,7 @@ """ # module level dunders -__version__ = "5.0.3" +__version__ = "5.1.0dev" __author__ = "Michael Ortner & Alexandre Boisselet" __credits__ = "The Magpylib community" __all__ = [ From dc63fbcbc8b429f390b3a350ec0ceb75cf6dc2e3 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 4 Jun 2024 17:53:40 +0200 Subject: [PATCH 20/55] add tests --- magpylib/_src/display/traces_generic.py | 3 +-- magpylib/_src/utility.py | 29 +++++++++++++++---------- tests/test_display_matplotlib.py | 10 +++++++++ tests/test_display_plotly.py | 27 +++++++++++++++++++++++ 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index cce3b8fa0..f60ef5077 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -446,7 +446,6 @@ def get_generic_traces3D( # pylint: disable=too-many-nested-blocks # pylint: disable=protected-access # pylint: disable=import-outside-toplevel - style = input_obj.style is_mag_arrows = False is_mag = hasattr(input_obj, "magnetization") and hasattr(style, "magnetization") @@ -811,7 +810,7 @@ def get_traces_3D(flat_objs_props, extra_backend=False, autosize=None, **kwargs) x, y, z = obj._position.T traces_dict[obj] = [{"x": x, "y": y, "z": z, "_autosize": True}] else: - params.update(kwargs) + params = {**params, **kwargs} traces_dict[obj] = [] orig_style = getattr(obj, "_style", None) try: diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index e27062afc..f5e36cc5b 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -243,19 +243,24 @@ def filter_objects(obj_list, allow="sources+sensors", warn=True): @lru_cache(maxsize=None) def get_unit_factor(unit_input, *, target_unit, deci_centi=True): """return unit factor based on input and target unit""" - pref, factor_power = "", None - if unit_input: - if len(unit_input) == 2: - pref, *_ = unit_input - prefs = _UNIT_PREFIX_REVERSED - if deci_centi: - prefs = {**_UNIT_PREFIX_REVERSED, "d": -1, "c": -2} - factor_power = prefs.get(pref, None) - - if factor_power is None or len(unit_input) > 2: - valid_inputs = [f"{k}{target_unit}" for k in _UNIT_PREFIX_REVERSED] + if unit_input == target_unit: + return 1 + pref, suff, factor_power = "", "", None + prefs = _UNIT_PREFIX_REVERSED + if deci_centi: + prefs = {**_UNIT_PREFIX_REVERSED, "d": -1, "c": -2} + unit_input_str = str(unit_input) + if unit_input_str: + if len(unit_input_str) >= 2: + pref, *suff = unit_input_str + suff = "".join(suff) + if suff == target_unit: + factor_power = prefs.get(pref, None) + + if factor_power is None or len(unit_input_str) > 2: + valid_inputs = [f"{k}{target_unit}" for k in prefs] raise ValueError( - f"Invalid unit input, must be one of {valid_inputs} got {unit_input!r}" + f"Invalid unit input ({unit_input!r}), must be one of {valid_inputs}" ) factor = 1 / (10**factor_power) return factor diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index 8575731a4..f2e8f3b07 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -617,3 +617,13 @@ def test_show_legend(): s2.style.legend = "full legend replace" s3.style.description = "description replace only" magpy.show(s1, s2, s3, return_fig=True) + + +@pytest.mark.parametrize("units_length", ["mT", "inch", "dam", "e"]) +def test_bad_units_length(units_length): + """test units lenghts""" + + c = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) + + with pytest.raises(ValueError, match=r"Invalid unit input.*"): + c.show(units_length=units_length, return_fig=True, backend="matpotlib") diff --git a/tests/test_display_plotly.py b/tests/test_display_plotly.py index b9f51abfd..3e0458008 100644 --- a/tests/test_display_plotly.py +++ b/tests/test_display_plotly.py @@ -3,6 +3,7 @@ import pytest import magpylib as magpy +from magpylib._src.display.traces_utility import get_unit_factor from magpylib._src.exceptions import MagpylibBadUserInput # pylint: disable=assignment-from-no-return @@ -383,3 +384,29 @@ def test_legends(): ) assert [t.name for t in fig.data] == ["Plotly extra trace (1m|1m|1m)"] * 4 assert [t.showlegend for t in fig.data] == [True, False, False, False] + + +def test_units_length(): + """test units lenghts""" + + dims = (1, 2, 3) + c1 = magpy.magnet.Cuboid(dimension=dims, polarization=(1, 2, 3)) + inputs = [ + {"objects": c1, "row": 1, "col": 1, "units_length": "m", "zoom": 3}, + {"objects": c1, "row": 1, "col": 2, "units_length": "dm", "zoom": 2}, + {"objects": c1, "row": 2, "col": 1, "units_length": "cm", "zoom": 1}, + {"objects": c1, "row": 2, "col": 2, "units_length": "mm", "zoom": 0}, + ] + fig = magpy.show( + *inputs, + backend="plotly", + return_fig=True, + ) + for ind, inp in enumerate(inputs): + scene = getattr(fig.layout, f"scene{'' if ind==0 else ind+1}") + for j, k in enumerate("xyz"): + ax = getattr(scene, f"{k}axis") + assert ax.title.text == f"{k} ({inp['units_length']})" + factor = get_unit_factor(inp["units_length"], target_unit="m") + r = (inp["zoom"] + 1) / 2 * factor * max(dims) + assert ax.range == (-r, r) From b65caeeca39254dbbba7e5a8cdd2eb825918b3b5 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 4 Jun 2024 18:08:35 +0200 Subject: [PATCH 21/55] add docs entry --- docs/_pages/user_guide/docs/docs_graphics.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/_pages/user_guide/docs/docs_graphics.md b/docs/_pages/user_guide/docs/docs_graphics.md index 4ab213db8..5fc44c814 100644 --- a/docs/_pages/user_guide/docs/docs_graphics.md +++ b/docs/_pages/user_guide/docs/docs_graphics.md @@ -422,3 +422,23 @@ with magpy.show_context(loop, sens, animation=True) as sc: sc.show(output=["Hx", "Hy", "Hz"], row=2) sc.show(output="Hxyz", col=2, row=2) ``` + + +### Canvas length units + +When displaying very small Magpylib objects, the axes scaling in meters might be inadequate and you may want to use other units that fit the system dimensions more nicely. The example below shows how to display an object (in this case the same) with different length units and zoom levels. + +```{code-cell} ipython3 +import magpylib as magpy + +c1 = magpy.magnet.Cuboid( + dimension=(1, 1, 1), + polarization=(1, 2, 3), +) + +with magpy.show_context(c1, backend="plotly", plotly_fig_layout_height=800) as s: + s.show(row=1, col=1, units_length="m", zoom=3) + s.show(row=1, col=2, units_length="dm", zoom=2) + s.show(row=2, col=1, units_length="cm", zoom=1) + s.show(row=2, col=2, units_length="mm", zoom=0) +``` From 3f41c478672c339223bc0b685ff5716f42113d65 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 4 Jun 2024 18:14:04 +0200 Subject: [PATCH 22/55] fix typo in test --- tests/test_display_matplotlib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index f2e8f3b07..890aabfbd 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -626,4 +626,4 @@ def test_bad_units_length(units_length): c = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) with pytest.raises(ValueError, match=r"Invalid unit input.*"): - c.show(units_length=units_length, return_fig=True, backend="matpotlib") + c.show(units_length=units_length, return_fig=True, backend="matplotlib") From 14c4670391773c385780349a7d57a6fc3ddcd8dd Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Tue, 4 Jun 2024 22:08:20 +0200 Subject: [PATCH 23/55] pylint --- tests/test_display_plotly.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_display_plotly.py b/tests/test_display_plotly.py index 3e0458008..ce7fe7501 100644 --- a/tests/test_display_plotly.py +++ b/tests/test_display_plotly.py @@ -404,7 +404,7 @@ def test_units_length(): ) for ind, inp in enumerate(inputs): scene = getattr(fig.layout, f"scene{'' if ind==0 else ind+1}") - for j, k in enumerate("xyz"): + for k in "xyz": ax = getattr(scene, f"{k}axis") assert ax.title.text == f"{k} ({inp['units_length']})" factor = get_unit_factor(inp["units_length"], target_unit="m") From 2507c324349ac75ed4d8c1a4ae05699c7d08c9f1 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 5 Jun 2024 10:01:13 +0200 Subject: [PATCH 24/55] refactor --- magpylib/_src/display/backend_plotly.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/magpylib/_src/display/backend_plotly.py b/magpylib/_src/display/backend_plotly.py index 25758c738..263c0ce82 100644 --- a/magpylib/_src/display/backend_plotly.py +++ b/magpylib/_src/display/backend_plotly.py @@ -75,15 +75,17 @@ def match_args(ttype: str): return set(named_args) -def apply_fig_ranges(fig, ranges_rc, labels, apply2d=True): +def apply_fig_ranges(fig, ranges_rc, labels_rc, apply2d=True): """This is a helper function which applies the ranges properties of the provided `fig` object - according to a provided ranges. All three space direction will be equal and match the - maximum of the ranges needed to display all objects, including their paths. + according to a provided ranges for each subplot. All three space direction will be equal and + match the maximum of the ranges needed to display all objects, including their paths. Parameters ---------- - ranges: array of dimension=(3,2) + ranges_rc: dict of arrays of dimension=(3,2) min and max graph range + labels_rc: dict of dicts + contains a dict with 'x', 'y', 'z' keys and respective labels as strings for each subplot apply2d: bool, default = True applies fixed range also on 2d traces @@ -94,12 +96,13 @@ def apply_fig_ranges(fig, ranges_rc, labels, apply2d=True): """ for rc, ranges in ranges_rc.items(): row, col = rc + labels = labels_rc.get(rc, {k: "" for k in "xyz"}) kwargs = { **{ f"{k}axis": { "range": ranges[i], "autorange": False, - "title": labels.get(rc, {k: "" for k in "xyz"})[k], + "title": labels[k], } for i, k in enumerate("xyz") }, @@ -359,7 +362,9 @@ def display_plotly( if extra_data: ranges_rc = get_scene_ranges(*frames[0]["data"]) if update_layout: - apply_fig_ranges(fig, ranges_rc, labels=data["labels"], apply2d=isanimation) + apply_fig_ranges( + fig, ranges_rc, labels_rc=data["labels"], apply2d=isanimation + ) fig.update_layout( legend_itemsizing="constant", # legend_groupclick="toggleitem", From 1e86e43035b12fcd188acdd190c9d3242552af34 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 5 Jun 2024 10:54:36 +0200 Subject: [PATCH 25/55] fix autosize scaling with units --- magpylib/_src/display/traces_generic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index f60ef5077..6591a54c5 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -807,7 +807,8 @@ def get_traces_3D(flat_objs_props, extra_backend=False, autosize=None, **kwargs) for obj, params in flat_objs_props.items(): if autosize is None and getattr(obj, "_autosize", False): # temporary coordinates to be able to calculate ranges - x, y, z = obj._position.T + factor = get_unit_factor(kwargs.get("units_length", "m"), target_unit="m") + x, y, z = obj._position.T * factor traces_dict[obj] = [{"x": x, "y": y, "z": z, "_autosize": True}] else: params = {**params, **kwargs} From 36e648c789466a9ba9217b0aba6fd9df22dffd2e Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 5 Jun 2024 15:38:44 +0200 Subject: [PATCH 26/55] ad units_length=None als option to remove axes labels --- magpylib/_src/display/traces_generic.py | 7 ++++--- magpylib/_src/utility.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 7a7284328..ebf4471c0 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -748,7 +748,7 @@ def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: traces_dict = {} extra_backend_traces = [] autosize_out = {} - labels = {(1, 1): {k: "" for k in "xyz"}} + labels = {(1, 1): {k: k for k in "xyz"}} zoom = {} for rc, objs_props in objs_props_by_row_col.items(): if objs_props["rc_params"]["output"] != "model3d": @@ -759,11 +759,12 @@ def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: objs_props["objects"], **rc_params, **kwargs ) if autosize is None or autosize == "return": - labels[rc] = {k: f"{k} ({rc_params['units_length']})" for k in "xyz"} + unit_str = "" if not (ul := rc_params["units_length"]) else f" {ul}" + labels[rc] = {k: f"{k}{unit_str}" for k in "xyz"} zoom[rc] = objs_props["rc_params"]["zoom"] traces = [t for tr in traces_dict_1.values() for t in tr] ranges_rc = get_scene_ranges( - *traces, *extra_backend_traces_1, zoom=objs_props["rc_params"]["zoom"] + *traces, *extra_backend_traces_1, zoom=zoom[rc] ) length_factor = get_unit_factor(rc_params["units_length"], target_unit="m") # pylint: disable=no-member diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index f5e36cc5b..6fcbad687 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -243,7 +243,7 @@ def filter_objects(obj_list, allow="sources+sensors", warn=True): @lru_cache(maxsize=None) def get_unit_factor(unit_input, *, target_unit, deci_centi=True): """return unit factor based on input and target unit""" - if unit_input == target_unit: + if unit_input is None or unit_input == target_unit: return 1 pref, suff, factor_power = "", "", None prefs = _UNIT_PREFIX_REVERSED From da50da578f3f8ef7eed34f6b50c424e9f5ea492c Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 5 Jun 2024 15:43:44 +0200 Subject: [PATCH 27/55] fix axes label in parentheses --- magpylib/_src/display/traces_generic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index ebf4471c0..b9773f915 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -759,7 +759,7 @@ def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: objs_props["objects"], **rc_params, **kwargs ) if autosize is None or autosize == "return": - unit_str = "" if not (ul := rc_params["units_length"]) else f" {ul}" + unit_str = "" if not (ul := rc_params["units_length"]) else f" ({ul})" labels[rc] = {k: f"{k}{unit_str}" for k in "xyz"} zoom[rc] = objs_props["rc_params"]["zoom"] traces = [t for tr in traces_dict_1.values() for t in tr] From 11980b65c246da9d4dcb9ac959b073b19a3e8312 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 5 Jun 2024 18:36:27 +0200 Subject: [PATCH 28/55] remove old code --- magpylib/_src/display/traces_utility.py | 1 - 1 file changed, 1 deletion(-) diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 95b153c59..0922da84f 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -512,7 +512,6 @@ def get_scene_ranges(*traces, zoom=0) -> np.ndarray: tr_dim_count[rc]["2D"] += 1 else: tr_dim_count[rc]["3D"] += 1 - ranges_rc[rc]["trace3d_found"] = True pts = np.array([tr[k] for k in coords], dtype="float64").T try: # for mesh3d, use only vertices part of faces for range calculation inds = np.array([tr[k] for k in "ijk"], dtype="int64").T From 355cb8ebb7cc3e7503c4d7010aa19e9d8ecc0a06 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 5 Jun 2024 22:29:17 +0200 Subject: [PATCH 29/55] refactor to allow units_length="auto" --- magpylib/_src/display/traces_generic.py | 78 ++++++++++++++----------- magpylib/_src/display/traces_utility.py | 23 ++++++-- magpylib/_src/utility.py | 14 +++-- tests/test_display_plotly.py | 2 +- 4 files changed, 75 insertions(+), 42 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index b9773f915..7d0b4fe85 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -24,14 +24,16 @@ from magpylib._src.display.traces_utility import get_objects_props_by_row_col from magpylib._src.display.traces_utility import get_rot_pos_from_path from magpylib._src.display.traces_utility import get_scene_ranges -from magpylib._src.display.traces_utility import get_unit_factor from magpylib._src.display.traces_utility import getColorscale from magpylib._src.display.traces_utility import getIntensity from magpylib._src.display.traces_utility import group_traces from magpylib._src.display.traces_utility import place_and_orient_model3d +from magpylib._src.display.traces_utility import rescale_traces from magpylib._src.display.traces_utility import slice_mesh_from_colorscale from magpylib._src.style import DefaultMarkers from magpylib._src.utility import format_obj_input +from magpylib._src.utility import get_unit_factor +from magpylib._src.utility import unit_prefix class MagpyMarkers: @@ -389,7 +391,7 @@ def get_label_and_color(obj): return traces -def process_extra_trace(model, units_length): +def process_extra_trace(model): "process extra trace attached to some magpylib object" extr = model["model3d"] model_kwargs = {**(extr.kwargs() if callable(extr.kwargs) else extr.kwargs)} @@ -398,6 +400,7 @@ def process_extra_trace(model, units_length): "constructor": extr.constructor, "kwargs": model_kwargs, "args": model_args, + "coordsargs": extr.coordsargs, "kwargs_extra": model["kwargs_extra"], } kwargs, args = place_and_orient_model3d( @@ -407,7 +410,6 @@ def process_extra_trace(model, units_length): position=model["position"], coordsargs=extr.coordsargs, scale=extr.scale, - units_length=units_length, return_model_args=True, ) trace3d["kwargs"].update(kwargs) @@ -425,7 +427,6 @@ def get_generic_traces3D( extra_backend=False, row=1, col=1, - units_length="mm", **kwargs, ) -> list: """ @@ -549,7 +550,7 @@ def get_generic_traces3D( path_traces_generic = group_traces(*path_traces_generic) for tr in path_traces_generic: - tr.update(place_and_orient_model3d(tr, units_length=units_length)) + tr.update(place_and_orient_model3d(tr)) tr.update(row=row, col=col) if tr.get("opacity", None) is None: tr["opacity"] = style.opacity @@ -597,7 +598,7 @@ def get_generic_traces3D( "col": col, }, } - tr_non_generic = process_extra_trace(tr_non_generic, units_length) + tr_non_generic = process_extra_trace(tr_non_generic) path_traces_extra_non_generic_backend.append(tr_non_generic) out.update({extra_backend: path_traces_extra_non_generic_backend}) return out @@ -723,7 +724,7 @@ def extract_animation_properties( return path_indices, exp, frame_duration -def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: +def draw_frame(objs, *, colorsequence, rc_params, **kwargs) -> Tuple: """ Creates traces from input `objs` and provided parameters, updates the size of objects like Sensors and Dipoles in `kwargs` depending on the canvas size. @@ -747,31 +748,27 @@ def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: ) traces_dict = {} extra_backend_traces = [] - autosize_out = {} - labels = {(1, 1): {k: k for k in "xyz"}} - zoom = {} + rc_params = {} if rc_params is None else rc_params for rc, objs_props in objs_props_by_row_col.items(): if objs_props["rc_params"]["output"] != "model3d": continue - rc_keys = ("row", "col", "units_length") - rc_params = {k: v for k, v in objs_props["rc_params"].items() if k in rc_keys} + rc_params[rc] = rc_params.get(rc, {}) + rc_params[rc]["units_length"] = objs_props["rc_params"]["units_length"] + rc_keys = ("row", "col") + rc_kwargs = {k: v for k, v in objs_props["rc_params"].items() if k in rc_keys} traces_dict_1, extra_backend_traces_1 = get_traces_3D( - objs_props["objects"], **rc_params, **kwargs + objs_props["objects"], **rc_kwargs, **kwargs ) - if autosize is None or autosize == "return": - unit_str = "" if not (ul := rc_params["units_length"]) else f" ({ul})" - labels[rc] = {k: f"{k}{unit_str}" for k in "xyz"} - zoom[rc] = objs_props["rc_params"]["zoom"] + rc_params[rc]["autosize"] = rc_params.get(rc, {}).get("autosize", None) + if rc_params[rc]["autosize"] is None: + rc_params[rc]["zoom"] = objs_props["rc_params"]["zoom"] traces = [t for tr in traces_dict_1.values() for t in tr] ranges_rc = get_scene_ranges( - *traces, *extra_backend_traces_1, zoom=zoom[rc] + *traces, *extra_backend_traces_1, zoom=rc_params[rc]["zoom"] ) - length_factor = get_unit_factor(rc_params["units_length"], target_unit="m") # pylint: disable=no-member - factor = default_settings.display.autosizefactor * length_factor - autosize_out[rc] = np.mean(np.diff(ranges_rc[rc])) / factor - else: - autosize_out = autosize + autosizefactor = default_settings.display.autosizefactor + rc_params[rc]["autosize"] = np.mean(np.diff(ranges_rc[rc])) / autosizefactor to_resize_keys = { k for k, v in traces_dict_1.items() if v and "_autosize" in v[0] } @@ -779,7 +776,7 @@ def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: k: v for k, v in objs_props["objects"].items() if k in to_resize_keys } traces_dict_2, extra_backend_traces_2 = get_traces_3D( - flat_objs_props, autosize=autosize_out.get(rc, None), **rc_params, **kwargs + flat_objs_props, autosize=rc_params[rc]["autosize"], **rc_kwargs, **kwargs ) traces_dict.update( {(k, *rc): v for k, v in {**traces_dict_1, **traces_dict_2}.items()} @@ -797,7 +794,7 @@ def draw_frame(objs, colorsequence=None, autosize=None, **kwargs) -> Tuple: return ( traces, extra_backend_traces, - {"autosize": autosize_out, "labels": labels, "zoom": zoom}, + rc_params, ) @@ -809,8 +806,7 @@ def get_traces_3D(flat_objs_props, extra_backend=False, autosize=None, **kwargs) for obj, params in flat_objs_props.items(): if autosize is None and getattr(obj, "_autosize", False): # temporary coordinates to be able to calculate ranges - factor = get_unit_factor(kwargs.get("units_length", "m"), target_unit="m") - x, y, z = obj._position.T * factor + x, y, z = obj._position.T traces_dict[obj] = [{"x": x, "y": y, "z": z, "_autosize": True}] else: params = {**params, **kwargs} @@ -875,7 +871,7 @@ def get_frames( frames = [] title_str = title - rc_params = {"autosize": "return"} + rc_params = {} for i, ind in enumerate(path_indices): extra_backend_traces = [] if animation: @@ -885,7 +881,7 @@ def get_frames( traces, extra_backend_traces, rc_params_temp = draw_frame( objs, colorsequence=colorsequence, - autosize=rc_params["autosize"], + rc_params=rc_params, supports_colorgradient=supports_colorgradient, extra_backend=backend, **kwargs, @@ -900,14 +896,30 @@ def get_frames( "extra_backend_traces": extra_backend_traces, } ) - clean_legendgroups(frames) traces = [t for frame in frames for t in frame["data"]] - ranges = get_scene_ranges(*traces, *extra_backend_traces, zoom=rc_params["zoom"]) + zoom = {rc: v["zoom"] for rc, v in rc_params.items()} + ranges_rc = get_scene_ranges(*traces, *extra_backend_traces, zoom=zoom) + labels_rc = {(1, 1): {k: "" for k in "xyz"}} + scale_factors_rc = {} + for rc, params in rc_params.items(): + units_length = params["units_length"] + if units_length == "auto": + rmax = np.amax(np.abs(ranges_rc[rc])) + units_length = f"{unit_prefix(rmax, as_tuple=True)[2]}m" + unit_str = "" if not (units_length) else f" ({units_length})" + labels_rc[rc] = {k: f"{k}{unit_str}" for k in "xyz"} + scale_factors_rc[rc] = get_unit_factor(units_length, target_unit="m") + ranges_rc[rc] *= scale_factors_rc[rc] + + for frame in frames: + for key in ("data", "extra_backend_traces"): + frame[key] = rescale_traces(frame[key], factors=scale_factors_rc) + out = { "frames": frames, - "ranges": ranges, - "labels": rc_params["labels"], + "ranges": ranges_rc, + "labels": labels_rc, "input_kwargs": {**kwargs, **animation_kwargs}, } if animation: diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 0922da84f..97ef10d68 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -14,7 +14,6 @@ from magpylib._src.input_checks import check_input_zoom from magpylib._src.style import get_style from magpylib._src.utility import format_obj_input -from magpylib._src.utility import get_unit_factor from magpylib._src.utility import merge_dicts_with_conflict_check DEFAULT_ROW_COL_PARAMS = { @@ -55,13 +54,12 @@ def place_and_orient_model3d( coordsargs=None, scale=1, return_model_args=False, - units_length="m", + length_factor=1, **kwargs, ): """places and orients mesh3d dict""" - if orientation is None and position is None and units_length == "m" and scale == 1: + if orientation is None and position is None and length_factor == 1: return {**model_kwargs, **kwargs} - length_factor = get_unit_factor(units_length, target_unit="m") position = (0.0, 0.0, 0.0) if position is None else position position = np.array(position, dtype=float) new_model_dict = {} @@ -543,6 +541,23 @@ def get_scene_ranges(*traces, zoom=0) -> np.ndarray: return ranges_rc +def rescale_traces(traces, factors): + """Rescale traces based on scale factors by (row,col) index""" + for ind, tr in enumerate(traces): + rc = tr.get("row", 1), tr.get("col", 1) + kw = {} + if "constructor" in tr: + kwex = tr["kwargs_extra"] + rc = kwex["row"], kwex["col"] + kw.update( + model_args=tr.get("args", None), + coordsargs=tr.get("coordsargs", None), + ) + if "z" in tr: # rescale only 3d traces + traces[ind] = place_and_orient_model3d(tr, length_factor=factors[rc], **kw) + return traces + + def group_traces(*traces): """Group and merge mesh traces with similar properties. This drastically improves browser rendering performance when displaying a lot of mesh3d objects.""" diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 6fcbad687..0fda4f2ee 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -266,7 +266,7 @@ def get_unit_factor(unit_input, *, target_unit, deci_centi=True): return factor -def unit_prefix(number, unit="", precision=3, char_between="") -> str: +def unit_prefix(number, unit="", precision=3, char_between="", as_tuple=False) -> str: """ displays a number with given unit and precision and uses unit prefixes for the exponents from yotta (y) to Yocto (Y). If the exponent is smaller or bigger, falls back to scientific notation. @@ -281,10 +281,13 @@ def unit_prefix(number, unit="", precision=3, char_between="") -> str: char_between : str, optional character to insert between number of prefix. Can be " " or any string, if a space is wanted before the unit symbol , by default "" + as_tuple: bool, optional + if True returns (new_number_str, char_between, prefix, unit) tuple + else returns the joined string Returns ------- - str - returns formatted number as string + str or tuple + returns formatted number as string or tuple """ digits = int(log10(abs(number))) // 3 * 3 if number != 0 else 0 prefix = _UNIT_PREFIX.get(digits, "") @@ -292,7 +295,10 @@ def unit_prefix(number, unit="", precision=3, char_between="") -> str: if prefix == "": digits = 0 new_number_str = f"{number / 10 ** digits:.{precision}g}" - return f"{new_number_str}{char_between}{prefix}{unit}" + res = (new_number_str, char_between, prefix, unit) + if as_tuple: + return res + return "".join(f"{v}" for v in res) def add_iteration_suffix(name): diff --git a/tests/test_display_plotly.py b/tests/test_display_plotly.py index 2435e8c58..a684e8b35 100644 --- a/tests/test_display_plotly.py +++ b/tests/test_display_plotly.py @@ -3,8 +3,8 @@ import pytest import magpylib as magpy -from magpylib._src.display.traces_utility import get_unit_factor from magpylib._src.exceptions import MagpylibBadUserInput +from magpylib._src.utility import get_unit_factor # pylint: disable=assignment-from-no-return # pylint: disable=no-member From 09d25f777361a958539080d4859379a7cad6dadc Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Thu, 6 Jun 2024 00:35:29 +0200 Subject: [PATCH 30/55] fix same color for different suplots --- magpylib/_src/display/traces_utility.py | 32 ++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 97ef10d68..1f94c6a3c 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -262,22 +262,26 @@ def get_rot_pos_from_path(obj, show_path=None): def get_objects_props_by_row_col(*objs, colorsequence, **kwargs): """Return flat dict with objs as keys object properties as values. Properties include: row_cols, style, legendgroup, legendtext""" - flat_objs = {} + flat_objs_rc = {} + rc_params_by_obj = {} for obj in objs: - flat_sub_objs = get_flatten_objects_properties_recursive( - *obj["objects"], colorsequence=colorsequence, **kwargs - ) - for subobj, props in flat_sub_objs.items(): - rc = obj["row"], obj["col"] - if rc not in flat_objs: - flat_objs[rc] = { - "objects": {}, - "rc_params": {k: v for k, v in obj.items() if k != "objects"}, - } - flat_objs[rc]["objects"][subobj] = props - + rc = obj["row"], obj["col"] + rc_params = {k: v for k, v in obj.items() if k != "objects"} + for subobj in obj["objects"]: + if subobj not in rc_params_by_obj: + rc_params_by_obj[subobj] = [] + rc_params_by_obj[subobj].append(rc_params) + flat_sub_objs = get_flatten_objects_properties_recursive( + *rc_params_by_obj, colorsequence=colorsequence, **kwargs + ) + for obj, rc_params_list in rc_params_by_obj.items(): + for rc_params in rc_params_list: + rc = rc_params["row"], rc_params["col"] + if rc not in flat_objs_rc: + flat_objs_rc[rc] = {"objects": {}, "rc_params": rc_params} + flat_objs_rc[rc]["objects"][obj] = flat_sub_objs[obj] kwargs = {k: v for k, v in kwargs.items() if not k.startswith("style")} - return flat_objs, kwargs + return flat_objs_rc, kwargs def get_flatten_objects_properties_recursive( From d3dbfda92898b562b88fa95311f07ee13194fa0c Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Thu, 6 Jun 2024 01:05:11 +0200 Subject: [PATCH 31/55] remove unused variable (code ql) --- magpylib/_src/display/traces_utility.py | 1 - 1 file changed, 1 deletion(-) diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 1f94c6a3c..41dd00e32 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -265,7 +265,6 @@ def get_objects_props_by_row_col(*objs, colorsequence, **kwargs): flat_objs_rc = {} rc_params_by_obj = {} for obj in objs: - rc = obj["row"], obj["col"] rc_params = {k: v for k, v in obj.items() if k != "objects"} for subobj in obj["objects"]: if subobj not in rc_params_by_obj: From 9b9ffed0f228de5adc962c42c4e219cf5f4f9039 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Thu, 6 Jun 2024 20:50:42 +0200 Subject: [PATCH 32/55] update changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e42473b2..85cfbb930 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog ## [Unreleased] - YYYY-MM-DD - +- Added `units_length` input to the `show` function to allow displaying axes with different length units. This parameter can be set individually for each subplot. ([#786](https://github.com/magpylib/magpylib/pull/786)) +- Fixed markers legend not being suppressible ([#789](https://github.com/magpylib/magpylib/pull/789)) ## [5.0.3] - 2024-06-03 From 1ab59824cdcdbffa0cd389740c2a46fa13c4ab0c Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Fri, 7 Jun 2024 17:41:56 +0200 Subject: [PATCH 33/55] update docs --- docs/_pages/user_guide/docs/docs_graphics.md | 24 ++++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/_pages/user_guide/docs/docs_graphics.md b/docs/_pages/user_guide/docs/docs_graphics.md index 5fc44c814..6cce8f201 100644 --- a/docs/_pages/user_guide/docs/docs_graphics.md +++ b/docs/_pages/user_guide/docs/docs_graphics.md @@ -4,9 +4,9 @@ jupytext: extension: .md format_name: myst format_version: 0.13 - jupytext_version: 1.16.0 + jupytext_version: 1.16.1 kernelspec: - display_name: Python 3 + display_name: Python 3 (ipykernel) language: python name: python3 orphan: true @@ -423,22 +423,22 @@ with magpy.show_context(loop, sens, animation=True) as sc: sc.show(output="Hxyz", col=2, row=2) ``` - ### Canvas length units When displaying very small Magpylib objects, the axes scaling in meters might be inadequate and you may want to use other units that fit the system dimensions more nicely. The example below shows how to display an object (in this case the same) with different length units and zoom levels. +```{tip} +Setting `units_length="auto"` will infer the most suitable units based on the maximum range of the system. +``` + ```{code-cell} ipython3 import magpylib as magpy -c1 = magpy.magnet.Cuboid( - dimension=(1, 1, 1), - polarization=(1, 2, 3), -) +c1 = magpy.magnet.Cuboid(dimension=(0.001, 0.001, 0.001), polarization=(1, 2, 3)) -with magpy.show_context(c1, backend="plotly", plotly_fig_layout_height=800) as s: - s.show(row=1, col=1, units_length="m", zoom=3) - s.show(row=1, col=2, units_length="dm", zoom=2) - s.show(row=2, col=1, units_length="cm", zoom=1) - s.show(row=2, col=2, units_length="mm", zoom=0) +with magpy.show_context(c1, backend="matplotlib") as s: + s.show(row=1, col=1, units_length="auto", zoom=0) + s.show(row=1, col=2, units_length="mm", zoom=1) + s.show(row=2, col=1, units_length="µm", zoom=2) + s.show(row=2, col=2, units_length="m", zoom=3) ``` From 1ba998249307bc6b8b29608fdaabf012eecdcd96 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sat, 8 Jun 2024 10:52:38 +0200 Subject: [PATCH 34/55] fix collection subobj not being displayed --- magpylib/_src/display/traces_utility.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 41dd00e32..4c71d6405 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -267,9 +267,11 @@ def get_objects_props_by_row_col(*objs, colorsequence, **kwargs): for obj in objs: rc_params = {k: v for k, v in obj.items() if k != "objects"} for subobj in obj["objects"]: - if subobj not in rc_params_by_obj: - rc_params_by_obj[subobj] = [] - rc_params_by_obj[subobj].append(rc_params) + children = getattr(subobj, "children_all", [subobj]) + for child in children: + if child not in rc_params_by_obj: + rc_params_by_obj[child] = [] + rc_params_by_obj[child].append(rc_params) flat_sub_objs = get_flatten_objects_properties_recursive( *rc_params_by_obj, colorsequence=colorsequence, **kwargs ) From d61a7333a6ac60a061c17b430025843bbcb6e8ec Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Sat, 8 Jun 2024 11:08:55 +0200 Subject: [PATCH 35/55] fix collection precedence in show --- magpylib/_src/display/traces_utility.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 4c71d6405..f5b426d6c 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -264,6 +264,7 @@ def get_objects_props_by_row_col(*objs, colorsequence, **kwargs): Properties include: row_cols, style, legendgroup, legendtext""" flat_objs_rc = {} rc_params_by_obj = {} + obj_list_semi_flat = [o for obj in objs for o in obj["objects"]] for obj in objs: rc_params = {k: v for k, v in obj.items() if k != "objects"} for subobj in obj["objects"]: @@ -273,7 +274,7 @@ def get_objects_props_by_row_col(*objs, colorsequence, **kwargs): rc_params_by_obj[child] = [] rc_params_by_obj[child].append(rc_params) flat_sub_objs = get_flatten_objects_properties_recursive( - *rc_params_by_obj, colorsequence=colorsequence, **kwargs + *obj_list_semi_flat, colorsequence=colorsequence, **kwargs ) for obj, rc_params_list in rc_params_by_obj.items(): for rc_params in rc_params_list: From 298eb66c8e186c838892b052a6d67396935278b1 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 10 Jun 2024 12:03:14 +0200 Subject: [PATCH 36/55] fix colorsquence on same obj in different subplot --- magpylib/_src/display/traces_utility.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index f5b426d6c..a54229bc3 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -300,7 +300,7 @@ def get_flatten_objects_properties_recursive( if color_cycle is None: color_cycle = cycle(colorsequence) flat_objs = {} - for subobj in obj_list_semi_flat: + for subobj in dict.fromkeys(obj_list_semi_flat): isCollection = getattr(subobj, "children", None) is not None style = get_style(subobj, default_settings, **kwargs) if style.label is None: @@ -322,18 +322,17 @@ def get_flatten_objects_properties_recursive( "showlegend": parent_showlegend, } if isCollection: - flat_objs.update( - get_flatten_objects_properties_recursive( - *subobj.children, - colorsequence=colorsequence, - color_cycle=color_cycle, - parent_legendgroup=legendgroup, - parent_color=style.color, - parent_label=label, - parent_showlegend=style.legend.show, - **kwargs, - ) + new_ojbs = get_flatten_objects_properties_recursive( + *subobj.children, + colorsequence=colorsequence, + color_cycle=color_cycle, + parent_legendgroup=legendgroup, + parent_color=style.color, + parent_label=label, + parent_showlegend=style.legend.show, + **kwargs, ) + flat_objs = {**new_ojbs, **flat_objs} return flat_objs From 350ccdacce9e31e3383e3c2e88b9cf1cb6544a22 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 10 Jun 2024 12:03:28 +0200 Subject: [PATCH 37/55] add color precedence tests --- tests/test_display_plotly.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_display_plotly.py b/tests/test_display_plotly.py index a684e8b35..a120e40b6 100644 --- a/tests/test_display_plotly.py +++ b/tests/test_display_plotly.py @@ -396,6 +396,32 @@ def test_legends(): assert [t.showlegend for t in fig.data] == [False] +def test_color_precedence(): + """Test if color precedence is respected when calling in nested collections""" + c1 = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) + c2 = c1.copy(position=(1, 0, 0)) + c3 = c1.copy(position=(2, 0, 0)) + coll = magpy.Collection(c1, magpy.Collection(c2, c3)) + kw = dict( + backend="plotly", + style_magnetization_show=False, + colorsequence=["red", "blue", "green"], + return_fig=True, + ) + fig = magpy.show(coll, **kw) + assert [tr["color"] for tr in fig.data] == ["red"] + + fig = magpy.show(*coll, **kw) + assert [tr["color"] for tr in fig.data] == ["red", "blue"] + + fig = magpy.show(*coll.sources_all, **kw) + assert [tr["color"] for tr in fig.data] == ["red", "blue", "green"] + + fig = magpy.show({"objects": c1, "col": 1}, {"objects": c1, "col": 2}, **kw) + # sane obj in different subplot should have same color + assert [tr["color"] for tr in fig.data] == ["red", "red"] + + def test_units_length(): """test units lenghts""" From 6bc14bc77613d0589b0644114232e231ce216c36 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 10 Jun 2024 12:05:41 +0200 Subject: [PATCH 38/55] set units_length default to "auto" --- magpylib/_src/display/traces_utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index a54229bc3..a59322b25 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -24,7 +24,7 @@ "pixel_agg": "mean", "in_out": "auto", "zoom": 0, - "units_length": "m", + "units_length": "auto", } From d2ab5b8310933d0fca8e2c741bc99c0f7f99fea7 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 10 Jun 2024 13:26:51 +0200 Subject: [PATCH 39/55] fix line color matching 2D vs 3D --- magpylib/_src/display/traces_generic.py | 24 ++++++------ magpylib/_src/utility.py | 17 +++++++++ tests/test_display_plotly.py | 51 ++++++++++++++++++++++--- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 7d0b4fe85..cd861f33e 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -33,6 +33,7 @@ from magpylib._src.style import DefaultMarkers from magpylib._src.utility import format_obj_input from magpylib._src.utility import get_unit_factor +from magpylib._src.utility import style_temp_edit from magpylib._src.utility import unit_prefix @@ -260,6 +261,7 @@ def get_traces_2D( pixel_agg=None, in_out="auto", style_path_frames=None, + styles=None, # pylint: disable=unused-argument units_length="m", zoom=0, @@ -326,7 +328,8 @@ def get_obj_list_str(objs): return obj_lst_str def get_label_and_color(obj): - style = obj.style + style = styles.get(obj, None) + style = obj.style if style is None else style label = get_legend_label(obj, style=style) color = getattr(style, "color", None) return label, color @@ -785,9 +788,15 @@ def draw_frame(objs, *, colorsequence, rc_params, **kwargs) -> Tuple: traces = group_traces(*[t for tr in traces_dict.values() for t in tr]) obj_list_2d = [o for o in objs if o["output"] != "model3d"] + styles = { + obj: params.get("style", None) + for o_rc in objs_props_by_row_col.values() + for obj, params in o_rc["objects"].items() + } for objs_2d in obj_list_2d: traces2d = get_traces_2D( **objs_2d, + styles=styles, style_path_frames=style_path_frames, ) traces.extend(traces2d) @@ -800,25 +809,18 @@ def draw_frame(objs, *, colorsequence, rc_params, **kwargs) -> Tuple: def get_traces_3D(flat_objs_props, extra_backend=False, autosize=None, **kwargs): """Return traces, traces to resize and extra_backend_traces""" - # pylint: disable=protected-access extra_backend_traces = [] traces_dict = {} for obj, params in flat_objs_props.items(): if autosize is None and getattr(obj, "_autosize", False): # temporary coordinates to be able to calculate ranges + # pylint: disable=protected-access x, y, z = obj._position.T traces_dict[obj] = [{"x": x, "y": y, "z": z, "_autosize": True}] else: params = {**params, **kwargs} traces_dict[obj] = [] - orig_style = getattr(obj, "_style", None) - try: - style_temp = params.pop("style", None) - # temporary replace style attribute - obj._style = style_temp - if style_temp: - # deepcopy style only if obj is in multiple subplots. - obj._style = style_temp.copy() + with style_temp_edit(obj, style_temp=params.pop("style", None), copy=True): out_traces = get_generic_traces3D( obj, extra_backend=extra_backend, @@ -828,8 +830,6 @@ def get_traces_3D(flat_objs_props, extra_backend=False, autosize=None, **kwargs) if extra_backend: extra_backend_traces.extend(out_traces.get(extra_backend, [])) traces_dict[obj].extend(out_traces["generic"]) - finally: - obj._style = orig_style return traces_dict, extra_backend_traces diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index 0fda4f2ee..89a24fcff 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -3,6 +3,7 @@ # pylint: disable=import-outside-toplevel # pylint: disable=cyclic-import # import numbers +from contextlib import contextmanager from functools import lru_cache from inspect import signature from math import log10 @@ -493,3 +494,19 @@ def merge_dicts_with_conflict_check(objs, *, target, identifiers, unique_fields) dict.fromkeys([*merged_dict[key][target], *obj[target]]) ) return merged_dict + + +@contextmanager +def style_temp_edit(obj, style_temp, copy=True): + """Temporary replace style to allow edits before returning to original state""" + # pylint: disable=protected-access + orig_style = getattr(obj, "_style", None) + try: + # temporary replace style attribute + obj._style = style_temp + if style_temp and copy: + # deepcopy style only if obj is in multiple subplots. + obj._style = style_temp.copy() + yield + finally: + obj._style = orig_style diff --git a/tests/test_display_plotly.py b/tests/test_display_plotly.py index a120e40b6..60a4d682a 100644 --- a/tests/test_display_plotly.py +++ b/tests/test_display_plotly.py @@ -402,12 +402,12 @@ def test_color_precedence(): c2 = c1.copy(position=(1, 0, 0)) c3 = c1.copy(position=(2, 0, 0)) coll = magpy.Collection(c1, magpy.Collection(c2, c3)) - kw = dict( - backend="plotly", - style_magnetization_show=False, - colorsequence=["red", "blue", "green"], - return_fig=True, - ) + kw = { + "backend": "plotly", + "style_magnetization_show": False, + "colorsequence": ["red", "blue", "green"], + "return_fig": True, + } fig = magpy.show(coll, **kw) assert [tr["color"] for tr in fig.data] == ["red"] @@ -422,6 +422,45 @@ def test_color_precedence(): assert [tr["color"] for tr in fig.data] == ["red", "red"] +def test_colors_output2d(): + """Tests if lines have objects corresponding colors in ouptut=Bx, By...""" + l1 = magpy.current.Circle( + current=1, + diameter=1, + style_label="L1", + style_arrow_show=False, + ) + l2 = l1.copy(diameter=2) + s1 = magpy.Sensor( + pixel=[[0, 0, 0], [0, 1, 0]], + position=np.linspace((-1, 0, 1), (1, 0, 1), 10), + style_label="S", + style_model3d_showdefault=False, + ) + s2 = s1.copy().move((0, 0, 1)) + objs = {"objects": [l1, l2, s1, s2]} + kw = { + "return_fig": True, + "colorsequence": ["red", "blue", "green", "cyan"], + } + kw2d = {"output": "Bx", "col": 2} + + def get_scatters2d(fig): + return [t.line.color for t in fig.data if t.type == "scatter"] + + fig = magpy.show(objs, {**objs, **kw2d, "sumup": True}, **kw) + assert get_scatters2d(fig) == ["green", "cyan"] + + fig = magpy.show(objs, {**objs, **kw2d, "sumup": True, "pixel_agg": None}, **kw) + assert get_scatters2d(fig) == [*["green"] * 2, *["cyan"] * 2] + + fig = magpy.show(objs, {**objs, **kw2d, "sumup": False}, **kw) + assert get_scatters2d(fig) == [*["red"] * 2, *["blue"] * 2] + + fig = magpy.show(objs, {**objs, **kw2d, "sumup": False, "pixel_agg": None}, **kw) + assert get_scatters2d(fig) == [*["red"] * 4, *["blue"] * 4] + + def test_units_length(): """test units lenghts""" From 4bbb11e742233f6567d23fab59c0360ebdc56189 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 10 Jun 2024 17:43:50 +0200 Subject: [PATCH 40/55] refactor --- magpylib/_src/display/traces_generic.py | 145 ++++++++++++------------ 1 file changed, 71 insertions(+), 74 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index cd861f33e..23bf911cf 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -252,8 +252,7 @@ def get_trace2D_dict( def get_traces_2D( - *, - objects, + *objects, output=("Bx", "By", "Bz"), row=None, col=None, @@ -727,6 +726,32 @@ def extract_animation_properties( return path_indices, exp, frame_duration +def get_traces_3D(flat_objs_props, extra_backend=False, autosize=None, **kwargs): + """Return traces, traces to resize and extra_backend_traces""" + extra_backend_traces = [] + traces_dict = {} + for obj, params in flat_objs_props.items(): + if autosize is None and getattr(obj, "_autosize", False): + # temporary coordinates to be able to calculate ranges + # pylint: disable=protected-access + x, y, z = obj._position.T + traces_dict[obj] = [{"x": x, "y": y, "z": z, "_autosize": True}] + else: + params = {**params, **kwargs} + traces_dict[obj] = [] + with style_temp_edit(obj, style_temp=params.pop("style", None), copy=True): + out_traces = get_generic_traces3D( + obj, + extra_backend=extra_backend, + autosize=autosize, + **params, + ) + if extra_backend: + extra_backend_traces.extend(out_traces.get(extra_backend, [])) + traces_dict[obj].extend(out_traces["generic"]) + return traces_dict, extra_backend_traces + + def draw_frame(objs, *, colorsequence, rc_params, **kwargs) -> Tuple: """ Creates traces from input `objs` and provided parameters, updates the size of objects like @@ -746,91 +771,63 @@ def draw_frame(objs, *, colorsequence, rc_params, **kwargs) -> Tuple: style_path_frames = kwargs.get( "style_path_frames", [-1] ) # get before next func strips style - objs_props_by_row_col, kwargs = get_objects_props_by_row_col( + objs_rc, kwargs = get_objects_props_by_row_col( *objs, colorsequence=colorsequence, **kwargs ) traces_dict = {} extra_backend_traces = [] rc_params = {} if rc_params is None else rc_params - for rc, objs_props in objs_props_by_row_col.items(): - if objs_props["rc_params"]["output"] != "model3d": - continue - rc_params[rc] = rc_params.get(rc, {}) - rc_params[rc]["units_length"] = objs_props["rc_params"]["units_length"] - rc_keys = ("row", "col") - rc_kwargs = {k: v for k, v in objs_props["rc_params"].items() if k in rc_keys} - traces_dict_1, extra_backend_traces_1 = get_traces_3D( - objs_props["objects"], **rc_kwargs, **kwargs - ) - rc_params[rc]["autosize"] = rc_params.get(rc, {}).get("autosize", None) - if rc_params[rc]["autosize"] is None: - rc_params[rc]["zoom"] = objs_props["rc_params"]["zoom"] - traces = [t for tr in traces_dict_1.values() for t in tr] - ranges_rc = get_scene_ranges( - *traces, *extra_backend_traces_1, zoom=rc_params[rc]["zoom"] + for rc, props in objs_rc.items(): + if props["rc_params"]["output"] == "model3d": + rc_params[rc] = rc_params.get(rc, {}) + rc_params[rc]["units_length"] = props["rc_params"]["units_length"] + rc_keys = ("row", "col") + rc_kwargs = {k: v for k, v in props["rc_params"].items() if k in rc_keys} + traces_d1, traces_ex1 = get_traces_3D( + props["objects"], **rc_kwargs, **kwargs ) - # pylint: disable=no-member - autosizefactor = default_settings.display.autosizefactor - rc_params[rc]["autosize"] = np.mean(np.diff(ranges_rc[rc])) / autosizefactor - to_resize_keys = { - k for k, v in traces_dict_1.items() if v and "_autosize" in v[0] - } - flat_objs_props = { - k: v for k, v in objs_props["objects"].items() if k in to_resize_keys - } - traces_dict_2, extra_backend_traces_2 = get_traces_3D( - flat_objs_props, autosize=rc_params[rc]["autosize"], **rc_kwargs, **kwargs - ) - traces_dict.update( - {(k, *rc): v for k, v in {**traces_dict_1, **traces_dict_2}.items()} - ) - extra_backend_traces.extend([*extra_backend_traces_1, *extra_backend_traces_2]) + rc_params[rc]["autosize"] = rc_params.get(rc, {}).get("autosize", None) + if rc_params[rc]["autosize"] is None: + zoom = rc_params[rc]["zoom"] = props["rc_params"]["zoom"] + traces = [t for tr in traces_d1.values() for t in tr] + ranges_rc = get_scene_ranges(*traces, *traces_ex1, zoom=zoom) + # pylint: disable=no-member + factor = default_settings.display.autosizefactor + rc_params[rc]["autosize"] = np.mean(np.diff(ranges_rc[rc])) / factor + to_resize_keys = { + k for k, v in traces_d1.items() if v and "_autosize" in v[0] + } + flat_objs_props = { + k: v for k, v in props["objects"].items() if k in to_resize_keys + } + traces_d2, traces_ex2 = get_traces_3D( + flat_objs_props, + autosize=rc_params[rc]["autosize"], + **rc_kwargs, + **kwargs, + ) + traces_dict.update( + {(k, *rc): v for k, v in {**traces_d1, **traces_d2}.items()} + ) + extra_backend_traces.extend([*traces_ex1, *traces_ex2]) traces = group_traces(*[t for tr in traces_dict.values() for t in tr]) - obj_list_2d = [o for o in objs if o["output"] != "model3d"] styles = { obj: params.get("style", None) - for o_rc in objs_props_by_row_col.values() + for o_rc in objs_rc.values() for obj, params in o_rc["objects"].items() } - for objs_2d in obj_list_2d: - traces2d = get_traces_2D( - **objs_2d, - styles=styles, - style_path_frames=style_path_frames, - ) - traces.extend(traces2d) - return ( - traces, - extra_backend_traces, - rc_params, - ) - -def get_traces_3D(flat_objs_props, extra_backend=False, autosize=None, **kwargs): - """Return traces, traces to resize and extra_backend_traces""" - extra_backend_traces = [] - traces_dict = {} - for obj, params in flat_objs_props.items(): - if autosize is None and getattr(obj, "_autosize", False): - # temporary coordinates to be able to calculate ranges - # pylint: disable=protected-access - x, y, z = obj._position.T - traces_dict[obj] = [{"x": x, "y": y, "z": z, "_autosize": True}] - else: - params = {**params, **kwargs} - traces_dict[obj] = [] - with style_temp_edit(obj, style_temp=params.pop("style", None), copy=True): - out_traces = get_generic_traces3D( - obj, - extra_backend=extra_backend, - autosize=autosize, - **params, - ) - if extra_backend: - extra_backend_traces.extend(out_traces.get(extra_backend, [])) - traces_dict[obj].extend(out_traces["generic"]) - return traces_dict, extra_backend_traces + for props in objs_rc.values(): + if props["rc_params"]["output"] != "model3d": + traces2d = get_traces_2D( + *props["objects"], + **props["rc_params"], + styles=styles, + style_path_frames=style_path_frames, + ) + traces.extend(traces2d) + return traces, extra_backend_traces, rc_params def get_frames( From db3810c6dcb601444755114637446ba2d662f30e Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Mon, 10 Jun 2024 23:18:54 +0200 Subject: [PATCH 41/55] refactor --- magpylib/_src/display/display.py | 18 +++++++--- magpylib/_src/display/traces_generic.py | 46 ++++++++++++++----------- magpylib/_src/display/traces_utility.py | 16 +++++---- 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 019ae0222..09133a152 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -12,13 +12,14 @@ from magpylib._src.display.traces_generic import MagpyMarkers from magpylib._src.display.traces_generic import get_frames from magpylib._src.display.traces_utility import DEFAULT_ROW_COL_PARAMS +from magpylib._src.display.traces_utility import linearize_dict from magpylib._src.display.traces_utility import process_show_input_objs from magpylib._src.input_checks import check_format_input_backend from magpylib._src.input_checks import check_format_input_vector from magpylib._src.input_checks import check_input_animation from magpylib._src.utility import check_path_format -disp_args = get_defaults_dict("display").keys() +disp_args = set(get_defaults_dict("display")) class RegisteredBackend: @@ -82,12 +83,18 @@ def show( f"\nFalling back to: {params}" ) kwargs.update(params) - frame_kwargs = { + display_kwargs = { k: v for k, v in kwargs.items() - if any(k.startswith(arg) for arg in disp_args) + if any(k.startswith(arg) for arg in disp_args - {"style"}) + } + style_kwargs = {k: v for k, v in kwargs.items() if k.startswith("style")} + style_kwargs = linearize_dict(style_kwargs, separator="_") + kwargs = { + k: v + for k, v in kwargs.items() + if (k not in display_kwargs and k not in style_kwargs) } - kwargs = {k: v for k, v in kwargs.items() if k not in frame_kwargs} backend_kwargs = { k[len(backend) + 1 :]: v for k, v in kwargs.items() @@ -117,7 +124,8 @@ def show( supports_colorgradient=self.supports["colorgradient"], backend=backend, title=title, - **frame_kwargs, + style_kwargs=style_kwargs, + **display_kwargs, ) return self.show_func_getter()( data, diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 23bf911cf..0a515435e 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -206,7 +206,7 @@ def get_trace2D_dict( field_str, coords_str, obj_lst_str, - frame_focus_inds, + focus_inds, frames_indices, mode, label_suff, @@ -223,7 +223,7 @@ def get_trace2D_dict( else: y = np.linalg.norm(y, axis=0) marker_size = np.array([2] * len(frames_indices)) - marker_size[frame_focus_inds] = 15 + marker_size[focus_inds] = 15 title = f"{field_str}{''.join(coords_str)}" trace = { "mode": "lines+markers", @@ -259,7 +259,6 @@ def get_traces_2D( sumup=True, pixel_agg=None, in_out="auto", - style_path_frames=None, styles=None, # pylint: disable=unused-argument units_length="m", @@ -311,12 +310,20 @@ def get_traces_2D( in_out=in_out, ) BH_array = BH_array.swapaxes(1, 2) # swap axes to have sensors first, path second - frames_indices = np.arange(0, BH_array.shape[2]) - frame_focus_inds = [-1] if style_path_frames is None else style_path_frames - if isinstance(frame_focus_inds, numbers.Number): - # pylint: disable=invalid-unary-operand-type - frame_focus_inds = frames_indices[::-style_path_frames] + + def get_focus_inds(*objs): + focus_inds = [] + for obj in objs: + style = styles.get(obj, obj.style) + frames = style.path.frames + inds = [] if frames is None else frames + if isinstance(inds, numbers.Number): + # pylint: disable=invalid-unary-operand-type + inds = frames_indices[::-frames] + focus_inds.extend(inds) + focus_inds = list(dict.fromkeys(focus_inds)) + return focus_inds if focus_inds else [-1] def get_obj_list_str(objs): if len(objs) < 8: @@ -346,9 +353,10 @@ def get_label_and_color(obj): label_src, color_src = get_label_and_color(src) symbols = cycle(ALLOWED_SYMBOLS[:6]) for sens_ind, sens in enumerate(sensors): + focus_inds = get_focus_inds(src, sens) label_sens, color_sens = get_label_and_color(sens) label_suff = label_sens - if mode == "sensors": + if not sumup: label, color = label_src, color_src else: label_suff = ( @@ -378,7 +386,7 @@ def get_label_and_color(obj): BH, **param, obj_lst_str=obj_lst_str, - frame_focus_inds=frame_focus_inds, + focus_inds=focus_inds, frames_indices=frames_indices, mode=mode, label_suff=label_suff, @@ -752,7 +760,7 @@ def get_traces_3D(flat_objs_props, extra_backend=False, autosize=None, **kwargs) return traces_dict, extra_backend_traces -def draw_frame(objs, *, colorsequence, rc_params, **kwargs) -> Tuple: +def draw_frame(objs, *, colorsequence, rc_params, style_kwargs, **kwargs) -> Tuple: """ Creates traces from input `objs` and provided parameters, updates the size of objects like Sensors and Dipoles in `kwargs` depending on the canvas size. @@ -767,12 +775,10 @@ def draw_frame(objs, *, colorsequence, rc_params, **kwargs) -> Tuple: colorsequence = default_settings.display.colorsequence # dipoles and sensors use autosize, the trace building has to be put at the back of the queue. # autosize is calculated from the other traces overall scene range - - style_path_frames = kwargs.get( - "style_path_frames", [-1] - ) # get before next func strips style - objs_rc, kwargs = get_objects_props_by_row_col( - *objs, colorsequence=colorsequence, **kwargs + objs_rc = get_objects_props_by_row_col( + *objs, + colorsequence=colorsequence, + style_kwargs=style_kwargs, ) traces_dict = {} extra_backend_traces = [] @@ -817,14 +823,12 @@ def draw_frame(objs, *, colorsequence, rc_params, **kwargs) -> Tuple: for o_rc in objs_rc.values() for obj, params in o_rc["objects"].items() } - for props in objs_rc.values(): if props["rc_params"]["output"] != "model3d": traces2d = get_traces_2D( *props["objects"], **props["rc_params"], styles=styles, - style_path_frames=style_path_frames, ) traces.extend(traces2d) return traces, extra_backend_traces, rc_params @@ -837,6 +841,7 @@ def get_frames( animation=False, supports_colorgradient=True, backend="generic", + style_kwargs=None, **kwargs, ): """This is a helper function which generates frames with generic traces to be provided to @@ -872,7 +877,7 @@ def get_frames( for i, ind in enumerate(path_indices): extra_backend_traces = [] if animation: - kwargs["style_path_frames"] = [ind] + style_kwargs["style_path_frames"] = [ind] title = "Animation 3D - " if title is None else title title_str = f"""{title}path index: {ind+1:0{exp}d}""" traces, extra_backend_traces, rc_params_temp = draw_frame( @@ -881,6 +886,7 @@ def get_frames( rc_params=rc_params, supports_colorgradient=supports_colorgradient, extra_backend=backend, + style_kwargs=style_kwargs, **kwargs, ) if i == 0: # get the dipoles and sensors autosize from first frame diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index a59322b25..429f6bb06 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -259,7 +259,7 @@ def get_rot_pos_from_path(obj, show_path=None): return rots, poss, inds -def get_objects_props_by_row_col(*objs, colorsequence, **kwargs): +def get_objects_props_by_row_col(*objs, colorsequence, style_kwargs): """Return flat dict with objs as keys object properties as values. Properties include: row_cols, style, legendgroup, legendtext""" flat_objs_rc = {} @@ -274,7 +274,9 @@ def get_objects_props_by_row_col(*objs, colorsequence, **kwargs): rc_params_by_obj[child] = [] rc_params_by_obj[child].append(rc_params) flat_sub_objs = get_flatten_objects_properties_recursive( - *obj_list_semi_flat, colorsequence=colorsequence, **kwargs + *obj_list_semi_flat, + style_kwargs=style_kwargs, + colorsequence=colorsequence, ) for obj, rc_params_list in rc_params_by_obj.items(): for rc_params in rc_params_list: @@ -282,19 +284,18 @@ def get_objects_props_by_row_col(*objs, colorsequence, **kwargs): if rc not in flat_objs_rc: flat_objs_rc[rc] = {"objects": {}, "rc_params": rc_params} flat_objs_rc[rc]["objects"][obj] = flat_sub_objs[obj] - kwargs = {k: v for k, v in kwargs.items() if not k.startswith("style")} - return flat_objs_rc, kwargs + return flat_objs_rc def get_flatten_objects_properties_recursive( *obj_list_semi_flat, + style_kwargs=None, colorsequence=None, color_cycle=None, parent_legendgroup=None, parent_color=None, parent_label=None, parent_showlegend=None, - **kwargs, ): """returns a flat dict -> (obj: display_props, ...) from nested collections""" if color_cycle is None: @@ -302,7 +303,8 @@ def get_flatten_objects_properties_recursive( flat_objs = {} for subobj in dict.fromkeys(obj_list_semi_flat): isCollection = getattr(subobj, "children", None) is not None - style = get_style(subobj, default_settings, **kwargs) + style_kwargs = {} if style_kwargs is None else style_kwargs + style = get_style(subobj, default_settings, **style_kwargs) if style.label is None: style.label = str(type(subobj).__name__) legendgroup = f"{subobj}" if parent_legendgroup is None else parent_legendgroup @@ -330,7 +332,7 @@ def get_flatten_objects_properties_recursive( parent_color=style.color, parent_label=label, parent_showlegend=style.legend.show, - **kwargs, + style_kwargs=style_kwargs, ) flat_objs = {**new_ojbs, **flat_objs} return flat_objs From 22b40ce822ddbda7d395c8f59386feaa8dcaa93a Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 11 Jun 2024 10:08:32 +0200 Subject: [PATCH 42/55] add special case marker color to 2d traces --- magpylib/_src/display/traces_generic.py | 37 ++++++++++++------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 0a515435e..5c50558bb 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -210,9 +210,6 @@ def get_trace2D_dict( frames_indices, mode, label_suff, - color, - symbol, - linestyle, **kwargs, ): """return a 2d trace based on field and parameters""" @@ -222,7 +219,7 @@ def get_trace2D_dict( y = y[0] else: y = np.linalg.norm(y, axis=0) - marker_size = np.array([2] * len(frames_indices)) + marker_size = np.array([3] * len(frames_indices)) marker_size[focus_inds] = 15 title = f"{field_str}{''.join(coords_str)}" trace = { @@ -239,15 +236,11 @@ def get_trace2D_dict( ), "x": frames_indices, "y": y[frames_indices], - "line_dash": linestyle, - "line_color": color, "marker_size": marker_size, - "marker_color": color, - "marker_symbol": symbol, "showlegend": True, "legendgroup": f"{title}{label_suff}", - **kwargs, } + trace.update(kwargs) return trace @@ -291,13 +284,13 @@ def get_traces_2D( if field_str not in "BHMJ" and set(coords_str).difference(set("xyz")): raise ValueError( "The `output` parameter must start with 'B', 'H', 'M', 'J' " - "and be followed by a combination of 'x', 'y', 'z' (e.g. 'Bxy' or ('Bxy', 'Hz') )" + "and be followed by a combination of 'x', 'y', 'z' (e.g. 'Bxy' or ('Bxy', 'Bz') )" f"\nreceived {out!r} instead" ) output_params[out] = { "field_str": field_str, "coords_str": coords_str, - "linestyle": linestyle, + "line_dash": linestyle, } BH_array = getBH_level2( sources, @@ -356,13 +349,15 @@ def get_label_and_color(obj): focus_inds = get_focus_inds(src, sens) label_sens, color_sens = get_label_and_color(sens) label_suff = label_sens - if not sumup: - label, color = label_src, color_src - else: + label = label_src + line_color = color_src + marker_color = color_sens if len(sensors) > 1 else None + if sumup: + line_color = color_sens + label = label_sens label_suff = ( f"{label_src}" if len(sources) == 1 else f"{len(sources)} sources" ) - label, color = label_sens, color_sens num_of_pix = ( len(sens.pixel.reshape(-1, 3)) if (not isinstance(sens, magpy.Collection)) @@ -373,13 +368,13 @@ def get_label_and_color(obj): pix_suff = "" num_of_pix_to_show = 1 if pixel_agg else num_of_pix for pix_ind in range(num_of_pix_to_show): - symbol = next(symbols) + marker_symbol = next(symbols) BH = BH_array[src_ind, sens_ind, :, pix_ind] if num_of_pix > 1: if pixel_agg: - pix_suff = f" ({num_of_pix} pixels {pixel_agg})" + pix_suff = f" - {num_of_pix} pixels {pixel_agg}" else: - pix_suff = f" (pixel {pix_ind})" + pix_suff = f" - pixel {pix_ind}" for param in output_params.values(): traces.append( get_trace2D_dict( @@ -391,8 +386,10 @@ def get_label_and_color(obj): mode=mode, label_suff=label_suff, name=f"{label}{pix_suff}", - color=color, - symbol=symbol, + line_color=line_color, + marker_color=marker_color, + marker_line_color=marker_color, + marker_symbol=marker_symbol, type="scatter", row=row, col=col, From 957ba913a2ac2125fcac20ffe92ea043a558f525 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 11 Jun 2024 10:28:25 +0200 Subject: [PATCH 43/55] refactor show_func --- magpylib/_src/display/display.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 09133a152..5a5468f2d 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -31,14 +31,14 @@ def __init__( self, *, name, - show_func_getter, + show_func, supports_animation, supports_subplots, supports_colorgradient, supports_animation_output, ): self.name = name - self.show_func_getter = show_func_getter + self.show_func = show_func self.supports = { "animation": supports_animation, "subplots": supports_subplots, @@ -127,7 +127,7 @@ def show( style_kwargs=style_kwargs, **display_kwargs, ) - return self.show_func_getter()( + return self.show_func( data, max_rows=max_rows, max_cols=max_cols, @@ -138,12 +138,12 @@ def show( ) -def get_show_func(backend): +def show_func(backend): """Return the backend show function""" # defer import to show call. Importerror should only fail if unavalaible backend is called - return lambda: getattr( + return lambda *args, backend=backend, **kwargs: getattr( import_module(f"magpylib._src.display.backend_{backend}"), f"display_{backend}" - ) + )(*args, **kwargs) def infer_backend(canvas): @@ -483,7 +483,7 @@ def reset(self, reset_show_return_value=True): RegisteredBackend( name="matplotlib", - show_func_getter=get_show_func("matplotlib"), + show_func=show_func("matplotlib"), supports_animation=True, supports_subplots=True, supports_colorgradient=False, @@ -493,7 +493,7 @@ def reset(self, reset_show_return_value=True): RegisteredBackend( name="plotly", - show_func_getter=get_show_func("plotly"), + show_func=show_func("plotly"), supports_animation=True, supports_subplots=True, supports_colorgradient=True, @@ -502,7 +502,7 @@ def reset(self, reset_show_return_value=True): RegisteredBackend( name="pyvista", - show_func_getter=get_show_func("pyvista"), + show_func=show_func("pyvista"), supports_animation=True, supports_subplots=True, supports_colorgradient=True, From afe7688d7d58671e1297f6c8f279fd6eff4e266c Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 11 Jun 2024 10:56:01 +0200 Subject: [PATCH 44/55] fix different output units in 2d subplots --- magpylib/_src/display/traces_generic.py | 45 +++++++++++++++++-------- 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 5c50558bb..fa810f3a8 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -210,6 +210,8 @@ def get_trace2D_dict( frames_indices, mode, label_suff, + units_polarization, + units_magnetization, **kwargs, ): """return a 2d trace based on field and parameters""" @@ -222,6 +224,11 @@ def get_trace2D_dict( marker_size = np.array([3] * len(frames_indices)) marker_size[focus_inds] = 15 title = f"{field_str}{''.join(coords_str)}" + unit = ( + units_polarization + if field_str in "BJ" + else units_magnetization if field_str in "HM" else "" + ) trace = { "mode": "lines+markers", "legendgrouptitle_text": f"{title}" @@ -229,7 +236,7 @@ def get_trace2D_dict( "text": mode, "hovertemplate": ( "Path index: %{x} " - f"{title}: " + "%{y:.3s}T
" + f"{title}: %{{y:.3s}}{unit}
" f"{'sources'}:
{obj_lst_str['sources']}
" f"{'sensors'}:
{obj_lst_str['sensors']}" # "", @@ -253,6 +260,8 @@ def get_traces_2D( pixel_agg=None, in_out="auto", styles=None, + units_polarization="T", + units_magnetization="A/m", # pylint: disable=unused-argument units_length="m", zoom=0, @@ -277,6 +286,7 @@ def get_traces_2D( if not isinstance(output, (list, tuple)): output = [output] output_params = {} + field_str_list = [] for out, linestyle in zip(output, cycle(ALLOWED_LINESTYLES[:6])): field_str, *coords_str = out if not coords_str: @@ -287,23 +297,28 @@ def get_traces_2D( "and be followed by a combination of 'x', 'y', 'z' (e.g. 'Bxy' or ('Bxy', 'Bz') )" f"\nreceived {out!r} instead" ) + field_str_list.append(field_str) output_params[out] = { "field_str": field_str, "coords_str": coords_str, "line_dash": linestyle, } - BH_array = getBH_level2( - sources, - sensors, - sumup=sumup, - squeeze=False, - field=field_str, - pixel_agg=pixel_agg, - output="ndarray", - in_out=in_out, - ) - BH_array = BH_array.swapaxes(1, 2) # swap axes to have sensors first, path second - frames_indices = np.arange(0, BH_array.shape[2]) + field_str_list = list(dict.fromkeys(field_str_list)) + BH_array = {} + for field_str in field_str_list: + BH_array[field_str] = getBH_level2( + sources, + sensors, + sumup=sumup, + squeeze=False, + field=field_str, + pixel_agg=pixel_agg, + output="ndarray", + in_out=in_out, + ) + # swap axes to have sensors first, path second + BH_array[field_str] = BH_array[field_str].swapaxes(1, 2) + frames_indices = np.arange(0, BH_array[field_str_list[0]].shape[2]) def get_focus_inds(*objs): focus_inds = [] @@ -369,13 +384,13 @@ def get_label_and_color(obj): num_of_pix_to_show = 1 if pixel_agg else num_of_pix for pix_ind in range(num_of_pix_to_show): marker_symbol = next(symbols) - BH = BH_array[src_ind, sens_ind, :, pix_ind] if num_of_pix > 1: if pixel_agg: pix_suff = f" - {num_of_pix} pixels {pixel_agg}" else: pix_suff = f" - pixel {pix_ind}" for param in output_params.values(): + BH = BH_array[param["field_str"]][src_ind, sens_ind, :, pix_ind] traces.append( get_trace2D_dict( BH, @@ -393,6 +408,8 @@ def get_label_and_color(obj): type="scatter", row=row, col=col, + units_polarization=units_polarization, + units_magnetization=units_magnetization, ) ) return traces From 584e5605b269dcf894984fdcf52c1ab758e865d6 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 11 Jun 2024 11:00:36 +0200 Subject: [PATCH 45/55] pylint --- magpylib/_src/display/display.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 5a5468f2d..e3f7445d3 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -138,7 +138,7 @@ def show( ) -def show_func(backend): +def get_show_func(backend): """Return the backend show function""" # defer import to show call. Importerror should only fail if unavalaible backend is called return lambda *args, backend=backend, **kwargs: getattr( @@ -483,7 +483,7 @@ def reset(self, reset_show_return_value=True): RegisteredBackend( name="matplotlib", - show_func=show_func("matplotlib"), + show_func=get_show_func("matplotlib"), supports_animation=True, supports_subplots=True, supports_colorgradient=False, @@ -493,7 +493,7 @@ def reset(self, reset_show_return_value=True): RegisteredBackend( name="plotly", - show_func=show_func("plotly"), + show_func=get_show_func("plotly"), supports_animation=True, supports_subplots=True, supports_colorgradient=True, @@ -502,7 +502,7 @@ def reset(self, reset_show_return_value=True): RegisteredBackend( name="pyvista", - show_func=show_func("pyvista"), + show_func=get_show_func("pyvista"), supports_animation=True, supports_subplots=True, supports_colorgradient=True, From 9040c2acb0287f5949688f2e9c2ad87a352cce25 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 11 Jun 2024 15:12:15 +0200 Subject: [PATCH 46/55] fix extra trace rescaling --- magpylib/_src/display/traces_generic.py | 4 +- magpylib/_src/display/traces_utility.py | 74 ++++++++++++++----------- 2 files changed, 44 insertions(+), 34 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index fa810f3a8..848442970 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -427,7 +427,7 @@ def process_extra_trace(model): "coordsargs": extr.coordsargs, "kwargs_extra": model["kwargs_extra"], } - kwargs, args = place_and_orient_model3d( + kwargs, args, coordsargs = place_and_orient_model3d( model_kwargs=model_kwargs, model_args=model_args, orientation=model["orientation"], @@ -435,7 +435,9 @@ def process_extra_trace(model): coordsargs=extr.coordsargs, scale=extr.scale, return_model_args=True, + return_coordsargs=True, ) + trace3d["coordsargs"] = coordsargs trace3d["kwargs"].update(kwargs) trace3d["args"] = args return trace3d diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 429f6bb06..85d87bcc9 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -54,44 +54,48 @@ def place_and_orient_model3d( coordsargs=None, scale=1, return_model_args=False, + return_coordsargs=False, length_factor=1, **kwargs, ): """places and orients mesh3d dict""" if orientation is None and position is None and length_factor == 1: - return {**model_kwargs, **kwargs} - position = (0.0, 0.0, 0.0) if position is None else position - position = np.array(position, dtype=float) - new_model_dict = {} - if model_args is None: - model_args = () - new_model_args = list(model_args) - vertices, coordsargs, useargs = get_vertices_from_model( - model_kwargs, model_args, coordsargs - ) - - # sometimes traces come as (n,m,3) shape - vert_shape = vertices.shape - vertices = np.reshape(vertices, (3, -1)) - - vertices = vertices.T - - if orientation is not None: - vertices = orientation.apply(vertices) - new_vertices = (vertices * scale + position).T * length_factor - new_vertices = np.reshape(new_vertices, vert_shape) - for i, k in enumerate("xyz"): - key = coordsargs[k] - if useargs: - ind = int(key[5]) - new_model_args[ind] = new_vertices[i] - else: - new_model_dict[key] = new_vertices[i] - new_model_kwargs = {**model_kwargs, **new_model_dict, **kwargs} + new_model_kwargs = {**model_kwargs, **kwargs} + new_model_args = model_args + else: + position = (0.0, 0.0, 0.0) if position is None else position + position = np.array(position, dtype=float) + new_model_dict = {} + if model_args is None: + model_args = () + new_model_args = list(model_args) + vertices, coordsargs, useargs = get_vertices_from_model( + model_kwargs, model_args, coordsargs + ) + # sometimes traces come as (n,m,3) shape + vert_shape = vertices.shape + vertices = np.reshape(vertices, (3, -1)) + + vertices = vertices.T + + if orientation is not None: + vertices = orientation.apply(vertices) + new_vertices = (vertices * scale + position).T * length_factor + new_vertices = np.reshape(new_vertices, vert_shape) + for i, k in enumerate("xyz"): + key = coordsargs[k] + if useargs: + ind = int(key[5]) + new_model_args[ind] = new_vertices[i] + else: + new_model_dict[key] = new_vertices[i] + new_model_kwargs = {**model_kwargs, **new_model_dict, **kwargs} out = (new_model_kwargs,) if return_model_args: out += (new_model_args,) + if return_coordsargs: + out += (coordsargs,) return out[0] if len(out) == 1 else out @@ -551,17 +555,21 @@ def get_scene_ranges(*traces, zoom=0) -> np.ndarray: def rescale_traces(traces, factors): """Rescale traces based on scale factors by (row,col) index""" for ind, tr in enumerate(traces): - rc = tr.get("row", 1), tr.get("col", 1) - kw = {} if "constructor" in tr: kwex = tr["kwargs_extra"] rc = kwex["row"], kwex["col"] - kw.update( + kwargs, args = place_and_orient_model3d( + model_kwargs=tr.get("kwargs", None), model_args=tr.get("args", None), coordsargs=tr.get("coordsargs", None), + length_factor=factors[rc], + return_model_args=True, ) + tr["kwargs"].update(kwargs) + tr["args"] = args if "z" in tr: # rescale only 3d traces - traces[ind] = place_and_orient_model3d(tr, length_factor=factors[rc], **kw) + rc = tr.get("row", 1), tr.get("col", 1) + traces[ind] = place_and_orient_model3d(tr, length_factor=factors[rc]) return traces From 8593313c3ab7eecab20437e9ef61323afb013a38 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 11 Jun 2024 16:07:42 +0200 Subject: [PATCH 47/55] fix single to resize trace on different subplots --- magpylib/_src/display/traces_generic.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 848442970..37b1f1b36 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -755,13 +755,14 @@ def get_traces_3D(flat_objs_props, extra_backend=False, autosize=None, **kwargs) extra_backend_traces = [] traces_dict = {} for obj, params in flat_objs_props.items(): + params = {**params, **kwargs} if autosize is None and getattr(obj, "_autosize", False): # temporary coordinates to be able to calculate ranges # pylint: disable=protected-access x, y, z = obj._position.T - traces_dict[obj] = [{"x": x, "y": y, "z": z, "_autosize": True}] + rc_dict = {k: v for k, v in params.items() if k in ("row", "col")} + traces_dict[obj] = [{"x": x, "y": y, "z": z, "_autosize": True, **rc_dict}] else: - params = {**params, **kwargs} traces_dict[obj] = [] with style_temp_edit(obj, style_temp=params.pop("style", None), copy=True): out_traces = get_generic_traces3D( From a9968eed92737eb37fb123ca3bb27024bc24c16b Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 12 Jun 2024 14:15:38 +0200 Subject: [PATCH 48/55] fix rescaling when vertices are None instead fo np.nan --- magpylib/_src/display/traces_utility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 85d87bcc9..40fb36de6 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -74,7 +74,7 @@ def place_and_orient_model3d( ) # sometimes traces come as (n,m,3) shape vert_shape = vertices.shape - vertices = np.reshape(vertices, (3, -1)) + vertices = np.reshape(vertices.astype(float), (3, -1)) vertices = vertices.T From 9d054fc175db428586588baa448310b16a97fc53 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 12 Jun 2024 14:43:59 +0200 Subject: [PATCH 49/55] fix pyvista streamlines example --- .../_pages/user_guide/examples/examples_vis_pv_streamlines.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_pages/user_guide/examples/examples_vis_pv_streamlines.md b/docs/_pages/user_guide/examples/examples_vis_pv_streamlines.md index ac1e53b3d..8fc2fd30a 100644 --- a/docs/_pages/user_guide/examples/examples_vis_pv_streamlines.md +++ b/docs/_pages/user_guide/examples/examples_vis_pv_streamlines.md @@ -48,8 +48,8 @@ strl = grid.streamlines_from_source( # Create a Pyvista plotting scene pl = pv.Plotter() -# Add magnet to scene -magpy.show(magnet, canvas=pl, backend="pyvista") +# Add magnet to scene - streamlines units are assumed to be meters +magpy.show(magnet, canvas=pl, units_length="m", backend="pyvista") # Prepare legend parameters legend_args = { From 6e4f38f948b8df0ff4a263502d0a11342fe03ef4 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Wed, 12 Jun 2024 14:51:35 +0200 Subject: [PATCH 50/55] fix collection model3d not being displayed --- magpylib/_src/display/traces_utility.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 40fb36de6..63013fc5e 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -3,6 +3,7 @@ # pylint: disable=too-many-branches from collections import defaultdict from functools import lru_cache +from itertools import chain from itertools import cycle from typing import Tuple @@ -272,8 +273,8 @@ def get_objects_props_by_row_col(*objs, colorsequence, style_kwargs): for obj in objs: rc_params = {k: v for k, v in obj.items() if k != "objects"} for subobj in obj["objects"]: - children = getattr(subobj, "children_all", [subobj]) - for child in children: + children = getattr(subobj, "children_all", []) + for child in chain([subobj], children): if child not in rc_params_by_obj: rc_params_by_obj[child] = [] rc_params_by_obj[child].append(rc_params) From d9af5160166c63bf70143ab7e76a4b66605ef626 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Tue, 18 Jun 2024 01:28:11 +0200 Subject: [PATCH 51/55] replace `np.NINF` with `-np.inf` --- magpylib/_src/fields/field_BH_dipole.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/magpylib/_src/fields/field_BH_dipole.py b/magpylib/_src/fields/field_BH_dipole.py index ac7455d6a..3a20651d2 100644 --- a/magpylib/_src/fields/field_BH_dipole.py +++ b/magpylib/_src/fields/field_BH_dipole.py @@ -58,7 +58,7 @@ def dipole_Hfield( if np.any(mask1): with np.errstate(divide="ignore", invalid="ignore"): H[mask1] = moments[mask1] / 0.0 - np.nan_to_num(H, copy=False, posinf=np.inf, neginf=np.NINF) + np.nan_to_num(H, copy=False, posinf=np.inf, neginf=-np.inf) return H From f159ca5f43cedda9fd482f314188d7a41300d133 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Tue, 18 Jun 2024 01:39:52 +0200 Subject: [PATCH 52/55] pylint --- tests/test_field_cylinder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_field_cylinder.py b/tests/test_field_cylinder.py index c0fe6bc50..171ab0084 100644 --- a/tests/test_field_cylinder.py +++ b/tests/test_field_cylinder.py @@ -219,7 +219,7 @@ def test_cylinder_field1(): nulll = np.zeros(N) eins = np.ones(N) - d, h, _ = dim.T + d, h, _ = dim.T # pylint: disable=no-member dim5 = np.array([nulll, d / 2, h, nulll, eins * 360]).T B1 = BHJM_cylinder_segment( field="B", observers=poso, polarization=magg, dimension=dim5 @@ -400,6 +400,7 @@ def test_cylinder_tile_vs_fem(): amp3 = np.linalg.norm(B3, axis=1) amp4 = np.linalg.norm(B4, axis=1) + # pylint: disable=unsubscriptable-object assert np.amax((fd1[:, 1:] * 1000 - B1).T / amp1) < 0.05 assert np.amax((fd2[5:-5, 1:] * 1000 - B2[5:-5]).T / amp2[5:-5]) < 0.05 assert np.amax((fd3[:, 1:] * 1000 - B3).T / amp3) < 0.05 From bc35bd1d289ccd2aa991900cf3cf63755d8eda53 Mon Sep 17 00:00:00 2001 From: Alexandre Boisselet Date: Tue, 18 Jun 2024 01:44:35 +0200 Subject: [PATCH 53/55] =?UTF-8?q?replace=20=C2=B4np.row=5Fstack=C2=B4=20wi?= =?UTF-8?q?th=20`np.vstack`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- magpylib/_src/fields/field_BH_circle.py | 2 +- magpylib/_src/fields/field_BH_cylinder.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/magpylib/_src/fields/field_BH_circle.py b/magpylib/_src/fields/field_BH_circle.py index da82a8a99..bf430bebe 100644 --- a/magpylib/_src/fields/field_BH_circle.py +++ b/magpylib/_src/fields/field_BH_circle.py @@ -78,7 +78,7 @@ def current_circle_Hfield( Hz = -pf * cel_iter(q, p, np.ones(n5), cc, ss, p, q) # input is I -> output must be H-field - return np.row_stack((Hr, np.zeros(n5), Hz)) * 795774.7154594767 # *1e7/4/np.pi + return np.vstack((Hr, np.zeros(n5), Hz)) * 795774.7154594767 # *1e7/4/np.pi def BHJM_circle( diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index e71eea7d0..8f5cf4e65 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -71,7 +71,7 @@ def magnet_cylinder_axial_Bfield(z0: np.ndarray, r: np.ndarray, z: np.ndarray) - / np.pi ) - return np.row_stack((Br, np.zeros(n), Bz)) + return np.vstack((Br, np.zeros(n), Bz)) # CORE @@ -257,7 +257,7 @@ def magnet_cylinder_diametral_Hfield( ) ) - return np.row_stack((Hr, Hphi, Hz)) + return np.vstack((Hr, Hphi, Hz)) def BHJM_magnet_cylinder( From 035a1da6db7f07feecb3cc65ebde8f14359fbec7 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 18 Jun 2024 10:12:08 +0200 Subject: [PATCH 54/55] update changelog --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bc837e3c..f56d38441 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,10 @@ # Changelog -## [5.0.3] - 2024-06-03 +## Unreleased +- Add support for Numpy 2.0 ([#795](https://github.com/magpylib/magpylib/pull/789)) +- Fix markers legend not being suppressible ([#795](https://github.com/magpylib/magpylib/pull/789)) +## [5.0.3] - 2024-06-03 - Fix subplot object properties propagation ([#780](https://github.com/magpylib/magpylib/pull/780)) - Migrate to pydata-sphinx-theme and fix docs search function ([#762](https://github.com/magpylib/magpylib/pull/762)) - Fix docs version-switcher ([#782](https://github.com/magpylib/magpylib/pull/782)) From 35c323f19225634bb0e7e21e50a2b8d3c92dfa44 Mon Sep 17 00:00:00 2001 From: "Boisselet Alexandre (IFAT DC ATV SC D TE2)" Date: Tue, 18 Jun 2024 10:17:54 +0200 Subject: [PATCH 55/55] bump version --- CHANGELOG.md | 5 +++-- README.md | 4 ++-- docs/_static/switcher.json | 4 ++-- magpylib/__init__.py | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f56d38441..6cdfd25e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## Unreleased +## [5.0.4] - 2024-06-18 - Add support for Numpy 2.0 ([#795](https://github.com/magpylib/magpylib/pull/789)) - Fix markers legend not being suppressible ([#795](https://github.com/magpylib/magpylib/pull/789)) @@ -469,7 +469,8 @@ The first official release of the Magpylib library. --- -[Unreleased]:https://github.com/magpylib/magpylib/compare/5.0.3...HEAD +[Unreleased]:https://github.com/magpylib/magpylib/compare/5.0.4...HEAD +[5.0.4]:https://github.com/magpylib/magpylib/compare/5.0.3...5.0.4 [5.0.3]:https://github.com/magpylib/magpylib/compare/5.0.2...5.0.3 [5.0.2]:https://github.com/magpylib/magpylib/compare/5.0.1...5.0.2 [5.0.1]:https://github.com/magpylib/magpylib/compare/5.0.0...5.0.1 diff --git a/README.md b/README.md index d4b5998ec..485c0f9fb 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Conda Cloud - MyBinder link + MyBinder link black @@ -136,7 +136,7 @@ A valid software citation could be author = {{Michael-Ortner et al.}}, title = {magpylib}, url = {https://magpylib.readthedocs.io/en/latest/}, - version = {5.0.3}, + version = {5.0.4}, date = {2023-06-25}, } ``` diff --git a/docs/_static/switcher.json b/docs/_static/switcher.json index 931420ee3..de64f2ff2 100644 --- a/docs/_static/switcher.json +++ b/docs/_static/switcher.json @@ -4,8 +4,8 @@ "url": "https://magpylib.readthedocs.io/en/latest/" }, { - "name": "5.0.3 (stable)", - "version": "5.0.3", + "name": "5.0.4 (stable)", + "version": "5.0.4", "url": "https://magpylib.readthedocs.io/en/stable", "preferred": true }, diff --git a/magpylib/__init__.py b/magpylib/__init__.py index 16290f12b..f4d092c57 100644 --- a/magpylib/__init__.py +++ b/magpylib/__init__.py @@ -28,7 +28,7 @@ """ # module level dunders -__version__ = "5.0.3" +__version__ = "5.0.4" __author__ = "Michael Ortner & Alexandre Boisselet" __credits__ = "The Magpylib community" __all__ = [