diff --git a/magpylib/__init__.py b/magpylib/__init__.py index f67df2f4d..9eb5bd2a3 100644 --- a/magpylib/__init__.py +++ b/magpylib/__init__.py @@ -61,7 +61,7 @@ from magpylib import magnet from magpylib import misc from magpylib._src.defaults.defaults_classes import default_settings as defaults -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS +from magpylib._src.defaults.defaults_values import SUPPORTED_PLOTTING_BACKENDS from magpylib._src.display.display import show from magpylib._src.display.display import show_context from magpylib._src.fields import getB diff --git a/magpylib/_src/defaults/defaults_classes.py b/magpylib/_src/defaults/defaults_classes.py index 20ed85f8f..a19380e5c 100644 --- a/magpylib/_src/defaults/defaults_classes.py +++ b/magpylib/_src/defaults/defaults_classes.py @@ -1,272 +1,358 @@ -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS -from magpylib._src.defaults.defaults_utility import MagicProperties +import numpy as np +import param + +from magpylib._src.defaults.defaults_utility import MagicParameterized from magpylib._src.defaults.defaults_utility import color_validator from magpylib._src.defaults.defaults_utility import get_defaults_dict -from magpylib._src.defaults.defaults_utility import validate_property_class -from magpylib._src.style import DisplayStyle - - -class DefaultSettings(MagicProperties): - """Library default settings. - - Parameters - ---------- - display: dict or Display - `Display` class containing display settings. `('backend', 'animation', 'colorsequence' ...)` - """ - - def __init__( - self, - display=None, - **kwargs, - ): - super().__init__( - display=display, - **kwargs, - ) - self.reset() - - def reset(self): - """Resets all nested properties to their hard coded default values""" - self.update(get_defaults_dict(), _match_properties=False) - return self - - @property - def display(self): - """`Display` class containing display settings. - `('backend', 'animation', 'colorsequence')`""" - return self._display - - @display.setter - def display(self, val): - self._display = validate_property_class(val, "display", Display, self) - - -class Display(MagicProperties): - """ - Defines the properties for the plotting features. - - Properties - ---------- - backend: str, default='matplotlib' - Defines the plotting backend to be used by default, if not explicitly set in the `display` - function (e.g. 'matplotlib', 'plotly'). - Supported backends are defined in magpylib.SUPPORTED_PLOTTING_BACKENDS - - colorsequence: iterable, default= - ['#2E91E5', '#E15F99', '#1CA71C', '#FB0D0D', '#DA16FF', '#222A2A', - '#B68100', '#750D86', '#EB663B', '#511CFB', '#00A08B', '#FB00D1', - '#FC0080', '#B2828D', '#6C7C32', '#778AAE', '#862A16', '#A777F1', - '#620042', '#1616A7', '#DA60CA', '#6C4516', '#0D2A63', '#AF0038'] - An iterable of color values used to cycle trough for every object displayed. - A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color - - animation: dict or Animation - Defines the animation properties used by the `plotly` plotting backend when `animation=True` - in the `show` function. - - autosizefactor: int, default=10 - Defines at which scale objects like sensors and dipoles are displayed. - Specifically `object_size` = `canvas_size` / `AUTOSIZE_FACTOR`. - - styles: dict or DisplayStyle - Base class containing display styling properties for all object families. - """ - - @property - def backend(self): - """plotting backend to be used by default, if not explicitly set in the `display` - function (e.g. 'matplotlib', 'plotly'). - Supported backends are defined in magpylib.SUPPORTED_PLOTTING_BACKENDS""" - return self._backend - - @backend.setter - def backend(self, val): - backends = [*SUPPORTED_PLOTTING_BACKENDS, "auto"] - assert val is None or val in backends, ( - f"the `backend` property of {type(self).__name__} must be one of" - f"{backends}" - f" but received {repr(val)} instead" +from magpylib._src.defaults.defaults_utility import magic_to_dict +from magpylib._src.defaults.defaults_values import DEFAULTS +from magpylib._src.defaults.defaults_values import SUPPORTED_PLOTTING_BACKENDS + +# pylint: disable=missing-class-docstring + + +class Trace3d(MagicParameterized): + _allowed_backends = (*SUPPORTED_PLOTTING_BACKENDS, "generic") + + def __setattr__(self, name, value): + validation_func = getattr(self, f"_validate_{name}", None) + if validation_func is not None: + value = validation_func(value) + return super().__setattr__(name, value) + + backend = param.Selector( + default="generic", + objects=_allowed_backends, + doc=f""" + Plotting backend corresponding to the trace. Can be one of + {_allowed_backends}""", + ) + + constructor = param.String( + doc=""" + Model constructor function or method to be called to build a 3D-model object + (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend.""" + ) + + args = param.Parameter( + default=(), + doc=""" + Tuple or callable returning a tuple containing positional arguments for building a + 3D-model object.""", + ) + + kwargs = param.Parameter( + default={}, + doc=""" + Dictionary or callable returning a dictionary containing the keys/values pairs for + building a 3D-model object.""", + ) + + coordsargs = param.Dict( + default=None, + doc=""" + Tells Magpylib the name of the coordinate arrays to be moved or rotated. + by default: + - plotly backend: `{"x": "x", "y": "y", "z": "z"}` + - matplotlib backend: `{"x": "args[0]", "y": "args[1]", "z": "args[2]}"`""", + ) + + show = param.Boolean( + default=True, + doc="""Show/hide model3d object based on provided trace.""", + ) + + scale = param.Number( + default=1, + bounds=(0, None), + inclusive_bounds=(True, False), + softbounds=(0.1, 5), + doc=""" + Scaling factor by which the trace vertices coordinates should be multiplied by. + Be aware that if the object is not centered at the global CS origin, its position will also + be affected by scaling.""", + ) + + updatefunc = param.Callable( + doc=""" + Callable object with no arguments. Should return a dictionary with keys from the + trace parameters. If provided, the function is called at `show` time and updates the + trace parameters with the output dictionary. This allows to update a trace dynamically + depending on class attributes, and postpone the trace construction to when the object is + displayed.""" + ) + + def _validate_coordsargs(self, value): + assert isinstance(value, dict) and all(key in value for key in "xyz"), ( + f"the `coordsargs` property of {type(self).__name__} must be " + f"a dictionary with `'x', 'y', 'z'` keys" + f" but received {repr(value)} instead" ) - self._backend = val - - @property - def colorsequence(self): - """An iterable of color values used to cycle trough for every object displayed. - A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color""" - return self._colorsequence - - @colorsequence.setter - def colorsequence(self, val): - if val is not None: - name = type(self).__name__ - try: - val = tuple( - color_validator(c, allow_None=False, parent_name=f"{name}") - for c in val - ) - except TypeError as err: - raise ValueError( - f"The `colorsequence` property of {name} must be an " - f"iterable of colors but received {val!r} instead" - ) from err - - self._colorsequence = val - - @property - def animation(self): - """Animation properties used by the `plotly` plotting backend when `animation=True` - in the `show` function.""" - return self._animation - - @animation.setter - def animation(self, val): - self._animation = validate_property_class(val, "animation", Animation, self) - - @property - def autosizefactor(self): - """Defines at which scale objects like sensors and dipoles are displayed. - Specifically `object_size` = `canvas_size` / `AUTOSIZE_FACTOR`.""" - return self._autosizefactor - - @autosizefactor.setter - def autosizefactor(self, val): - assert val is None or isinstance(val, (int, float)) and val > 0, ( - f"the `autosizefactor` property of {type(self).__name__} must be a strictly positive" - f" number but received {repr(val)} instead" + return value + + def _validate_updatefunc(self, val): + """Validate updatefunc.""" + if val is None: + + def val(): + return {} + + msg = "" + valid_keys = self.param.values().keys() + if not callable(val): + msg = f"Instead received {type(val)}" + else: + test_val = val() + if not isinstance(test_val, dict): + msg = f"but callable returned type {type(test_val)}." + else: + bad_keys = [k for k in test_val.keys() if k not in valid_keys] + if bad_keys: + msg = f"but invalid output dictionary keys received: {bad_keys}." + + if msg: + raise ValueError( + f"The `updatefunc` property of {type(self).__name__} must be a callable returning " + f"a dictionary with a subset of following keys: {list(valid_keys)}.\n" + f"{msg}" + ) + return val + + +def validate_trace3d(trace, **kwargs): + """Validate and if necessary transform into Trace3d""" + updatefunc = None + trace_orig = trace + if trace is None: + trace = Trace3d() + if not isinstance(trace, Trace3d) and callable(trace): + updatefunc = trace + trace = Trace3d() + if isinstance(trace, dict): + trace = Trace3d(**trace) + if isinstance(trace, Trace3d): + trace.updatefunc = updatefunc + if kwargs: + trace.update(**kwargs) + trace.update(trace.updatefunc()) + else: + raise ValueError( + "A `Model3d` data element must be a `Trace3d` object or a dict with equivalent" + " parameters, or a callable returning a dict with equivalent parameters." + f" Instead received: {trace_orig!r}" ) - self._autosizefactor = val - - @property - def style(self): - """Base class containing display styling properties for all object families.""" - return self._style - - @style.setter - def style(self, val): - self._style = validate_property_class(val, "style", DisplayStyle, self) - - -class Animation(MagicProperties): - """ - Defines the animation properties used by the `plotly` plotting backend when `animation=True` - in the `display` function. - - Properties - ---------- - fps: str, default=30 - Target number of frames to be displayed per second. - - maxfps: str, default=50 - Maximum number of frames to be displayed per second before downsampling kicks in. + return trace + + +class Model3dLogic: + def __setattr__(self, name, value): + if name == "data": + if not isinstance(value, (tuple, list)): + value = [value] + value = [validate_trace3d(v) for v in value] + super().__setattr__(name, value) + + def add_trace(self, trace=None, **kwargs): + """Adds user-defined 3d model object which is positioned relatively to the main object to be + displayed and moved automatically with it. This feature also allows the user to replace the + original 3d representation of the object. + + trace: Trace3d instance, dict or callable + Trace object. Can be a `Trace3d` instance or an dictionary with equivalent key/values + pairs, or a callable returning the equivalent dictionary. + + backend: str + Plotting backend corresponding to the trace. Can be one of `['matplotlib', 'plotly']`. + + constructor: str + Model constructor function or method to be called to build a 3D-model object + (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend. + + args: tuple, default=None + Tuple or callable returning a tuple containing positional arguments for building a + 3D-model object. + + kwargs: dict or callable, default=None + Dictionary or callable returning a dictionary containing the keys/values pairs for + building a 3D-model object. + + coordsargs: dict, default=None + Tells magpylib the name of the coordinate arrays to be moved or rotated, + by default: `{"x": "x", "y": "y", "z": "z"}`, if False, object is not rotated. + + show: bool, default=None + Show/hide model3d object based on provided trace. + + scale: float, default=1 + Scaling factor by which the trace vertices coordinates are multiplied. + + updatefunc: callable, default=None + Callable object with no arguments. Should return a dictionary with keys from the + trace parameters. If provided, the function is called at `show` time and updates the + trace parameters with the output dictionary. This allows to update a trace dynamically + depending on class attributes, and postpone the trace construction to when the object is + displayed. + """ + self.data = [*self.data, validate_trace3d(trace, **kwargs)] + return self - maxframes: int, default=200 - Maximum total number of frames to be displayed before downsampling kicks in. - time: float, default=5 - Default animation time. +class ColorSequence(param.List): + def __set__(self, obj, val): + self._validate_value(val, self.allow_None) + val = [color_validator(v) for v in val] + super().__set__(obj, val) + + +class Color(param.Color): + def __set__(self, obj, val): + val = color_validator(val) + super().__set__(obj, val) + + +class PathLogic: + def __setattr__(self, name, value): + if name == "frames": + if isinstance(value, (tuple, list, np.ndarray)): + self.frames.indices = [int(v) for v in value] + elif ( + isinstance(value, (int, np.integer)) + and value is not False + and value is not True + ): + self.frames.step = value + else: + super().__setattr__(name, value) + return + super().__setattr__(name, value) + + +class TextLogic: + def __setattr__(self, name, value): + try: + super().__setattr__(name, value) + except ValueError: + # this allows to set a string value from parent class if there is a `text` child + # e.g. .description = "desc" -> .description.text = "desc" + if ( + isinstance(value, str) + and not name.startswith("_") + and isinstance(self.param[name], param.ClassSelector) + ): + child = getattr(self, name) + if "text" in child.param.values(): + child.text = value + return + raise + + +def get_frames_logic(): + @param.depends("indices", watch=True) + def _update_indices(self): + self.mode = "indices" + + @param.depends("step", watch=True) + def _update_step(self): + self.mode = "step" + + return locals() + + +def convert_to_param(dict_, parent=None): + """Convert nested defaults dict to nested MagicParameterized instances""" + parent = "" if not parent else parent[0].upper() + parent[1:] + params = {} + for key, val in dict_.items(): + if not isinstance(val, dict): + raise TypeError(f"{val} must be dict.") + if "$type" in val: + typ = None + typ_str = str(val["$type"]).capitalize() + args = {k: v for k, v in val.items() if k != "$type"} + if typ_str == "Color": + typ = Color + elif typ_str == "List": + typ = param.List + it_typ = str(args.get("item_type", None)).capitalize() + if it_typ == "Color": + args.pop("item_type", None) + typ = ColorSequence + elif it_typ == "Trace3d": + args.pop("item_type", Trace3d) + else: + typ = getattr(param, typ_str) + if typ is not None: + params[key] = typ(**args) + else: + name = parent + key[0].upper() + key[1:] + val = convert_to_param(val, parent=name) + params[key] = param.ClassSelector(class_=val, default=val()) + if parent.endswith("PathFrames"): + # param.depends does not work with adding a class to bases + params.update(get_frames_logic()) + class_ = type(parent, (MagicParameterized, TextLogic), params) + if parent.endswith("Path"): + class_ = type(parent, (class_, PathLogic), {}) + if parent.endswith("Model3d"): + class_ = type(parent, (class_, Model3dLogic), {}) + return class_ + + +Display = convert_to_param( + magic_to_dict(DEFAULTS, separator=".")["display"], parent="display" +) + + +class DefaultSettings(MagicParameterized): + """Library default settings. All default values get reset at class instantiation.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._declare_watchers() + with param.parameterized.batch_call_watchers(self): + self.reset() - slider: bool, default = True - If True, an interactive slider will be displayed and stay in sync with the animation, will - be hidden otherwise. + def reset(self): + """Resets all nested properties to their hard coded default values""" + self.update(get_defaults_dict(), match_properties=False) + return self - output: str, default = None - The path where to store the animation. Must end with `.mp4` or `.gif`. If only the suffix - is used, the file is only store in a temporary folder and deleted after the animation is - done. - """ - - @property - def maxfps(self): - """Maximum number of frames to be displayed per second before downsampling kicks in.""" - return self._maxfps - - @maxfps.setter - def maxfps(self, val): - assert val is None or isinstance(val, int) and val > 0, ( - f"The `maxfps` property of {type(self).__name__} must be a strictly positive" - f" integer but received {repr(val)} instead." - ) - self._maxfps = val - - @property - def fps(self): - """Target number of frames to be displayed per second.""" - return self._fps - - @fps.setter - def fps(self, val): - assert val is None or isinstance(val, int) and val > 0, ( - f"The `fps` property of {type(self).__name__} must be a strictly positive" - f" integer but received {repr(val)} instead." - ) - self._fps = val - - @property - def maxframes(self): - """Maximum total number of frames to be displayed before downsampling kicks in.""" - return self._maxframes - - @maxframes.setter - def maxframes(self, val): - assert val is None or isinstance(val, int) and val > 0, ( - f"The `maxframes` property of {type(self).__name__} must be a strictly positive" - f" integer but received {repr(val)} instead." - ) - self._maxframes = val - - @property - def time(self): - """Default animation time.""" - return self._time - - @time.setter - def time(self, val): - assert val is None or isinstance(val, int) and val > 0, ( - f"The `time` property of {type(self).__name__} must be a strictly positive" - f" integer but received {repr(val)} instead." - ) - self._time = val - - @property - def slider(self): - """show/hide slider""" - return self._slider - - @slider.setter - def slider(self, val): - assert val is None or isinstance(val, bool), ( - f"The `slider` property of {type(self).__name__} must be a either `True` or `False`" - f" but received {repr(val)} instead." - ) - self._slider = val - - @property - def output(self): - """Animation output type""" - return self._output - - @output.setter - def output(self, val): - if val is not None: - val = str(val) - valid = val.endswith("mp4") or val.endswith("gif") - assert val is None or valid, ( - f"The `output` property of {type(self).__name__} must be a either `mp4` or `gif` " - "or a valid path ending with `.mp4` or `.gif`" - f" but received {repr(val)} instead." - ) - self._output = val + def _declare_watchers(self): + props = get_defaults_dict(flatten=True, separator=".").keys() + for prop in props: + attrib_chain = prop.split(".") + child = attrib_chain[-1] + parent = self # start with self to go through dot chain + for attrib in attrib_chain[:-1]: + parent = getattr(parent, attrib) + parent.param.watch(self._set_to_defaults, parameter_names=[child]) + + @staticmethod + def _set_to_defaults(event): + """Sets class defaults whenever magpylib defaults parameters instance are modifed.""" + event.obj.__class__.param[event.name].default = event.new + + display = param.ClassSelector( + class_=Display, + default=Display(), + doc=""" + `Display` defaults-class containing display settings. + `(e.g. 'backend', 'animation', 'colorsequence', ...)`""", + ) default_settings = DefaultSettings() + +default_style_classes = { + k: v.__class__ + for k, v in default_settings.display.style.param.values().items() + if isinstance(v, param.Parameterized) +} +BaseStyle = default_style_classes.pop("base") +for fam, klass in default_style_classes.items(): + fam = fam.capitalize() + bases = (BaseStyle, klass) + if fam == "Triangularmesh": + bases += (default_style_classes["magnet"],) + klass_name = f"{fam}Style" + locals()[klass_name] = type(klass_name, bases, {}) diff --git a/magpylib/_src/defaults/defaults_utility.py b/magpylib/_src/defaults/defaults_utility.py index d44d6f3e2..b4dbeabf9 100644 --- a/magpylib/_src/defaults/defaults_utility.py +++ b/magpylib/_src/defaults/defaults_utility.py @@ -6,30 +6,12 @@ from copy import deepcopy from functools import lru_cache +import numpy as np +import param from matplotlib.colors import CSS4_COLORS as mcolors from magpylib._src.defaults.defaults_values import DEFAULTS -SUPPORTED_PLOTTING_BACKENDS = ("matplotlib", "plotly", "pyvista") - - -ALLOWED_SYMBOLS = (".", "+", "D", "d", "s", "x", "o") - -ALLOWED_LINESTYLES = ( - "solid", - "dashed", - "dotted", - "dashdot", - "loosely dotted", - "loosely dashdotted", - "-", - "--", - "-.", - ".", - ":", - (0, (1, 1)), -) - COLORS_SHORT_TO_LONG = { "r": "red", "g": "green", @@ -65,20 +47,29 @@ def __repr__(self): # pragma: no cover _DefaultValue = _DefaultType() -def get_defaults_dict(arg=None) -> dict: - """returns default dict or sub-dict based on `arg`. - (e.g. `get_defaults_dict('display.style')`) +def get_defaults_dict(arg=None, flatten=False, separator=".") -> dict: + """Return default dict or sub-dict based on `arg` (e.g. get_default_dict('display.style')). Returns ------- dict default sub dict + + flatten: bool + If `True`, the nested dictionary gets flatten out with provided separator for the + dictionary keys + + separator: str + the separator to be used when flattening the dictionary. Only applies if + `flatten=True` """ - dict_ = deepcopy(DEFAULTS) + dict_ = deepcopy(DEFAULTS_VALUES) if arg is not None: - for v in arg.split("."): + for v in arg.split(separator): dict_ = dict_[v] + if flatten: + dict_ = linearize_dict(dict_, separator=separator) return dict_ @@ -287,21 +278,6 @@ def color_validator(color_input, allow_None=True, parent_name=""): return color_new -def validate_property_class(val, name, class_, parent): - """validator for sub property""" - if isinstance(val, dict): - val = class_(**val) - elif val is None: - val = class_() - if not isinstance(val, class_): - raise ValueError( - f"the `{name}` property of `{type(parent).__name__}` must be an instance \n" - f"of `{class_}` or a dictionary with equivalent key/value pairs \n" - f"but received {repr(val)} instead" - ) - return val - - def validate_style_keys(style_kwargs): """validates style kwargs based on key up to first underscore. checks in the defaults structures the generally available style keys""" @@ -318,59 +294,189 @@ def validate_style_keys(style_kwargs): return style_kwargs -class MagicProperties: +def get_families(obj): + """get obj families""" + # pylint: disable=import-outside-toplevel + # pylint: disable=possibly-unused-variable + # pylint: disable=redefined-outer-name + from magpylib._src.display.traces_generic import MagpyMarkers as Markers + from magpylib._src.obj_classes.class_BaseExcitations import BaseCurrent as Current + from magpylib._src.obj_classes.class_BaseExcitations import BaseMagnet as Magnet + from magpylib._src.obj_classes.class_current_Circle import Circle + from magpylib._src.obj_classes.class_current_Polyline import Polyline + from magpylib._src.obj_classes.class_magnet_Cuboid import Cuboid + from magpylib._src.obj_classes.class_magnet_Cylinder import Cylinder + from magpylib._src.obj_classes.class_magnet_CylinderSegment import CylinderSegment + from magpylib._src.obj_classes.class_magnet_Sphere import Sphere + from magpylib._src.obj_classes.class_magnet_Tetrahedron import Tetrahedron + from magpylib._src.obj_classes.class_magnet_TriangularMesh import TriangularMesh + from magpylib._src.obj_classes.class_misc_CustomSource import CustomSource + from magpylib._src.obj_classes.class_misc_Dipole import Dipole + from magpylib._src.obj_classes.class_misc_Triangle import Triangle + from magpylib._src.obj_classes.class_Sensor import Sensor + + loc = locals() + obj_families = [] + for item, val in loc.items(): + if not item.startswith("_"): + try: + if isinstance(obj, val): + obj_families.append(item.lower()) + except TypeError: + pass + return obj_families + + +def get_style(obj, **kwargs): + """Returns default style based on increasing priority: + - style from defaults + - style from object + - style from kwargs arguments + """ + # parse kwargs into style an non-style arguments + style_kwargs = kwargs.get("style", {}) + style_kwargs.update( + {k[6:]: v for k, v in kwargs.items() if k.startswith("style") and k != "style"} + ) + style_kwargs = validate_style_keys(style_kwargs) + # create style class instance and update based on precedence + style = obj.style.copy() + keys = style.as_dict().keys() + style_kwargs = {k: v for k, v in style_kwargs.items() if k.split("_")[0] in keys} + style.update(**style_kwargs, match_properties=True) + + return style + + +def update_with_nested_dict(parameterized, nested_dict): + """updates parameterized object recursively via setters""" + # Using `batch_call_watchers` because it has the same underlying + # mechanism as with `param.update` + # See https://param.holoviz.org/user_guide/Dependencies_and_Watchers.html?highlight=batch_call + # #batch-call-watchers + with param.parameterized.batch_call_watchers(parameterized): + for pname, value in nested_dict.items(): + if isinstance(value, dict): + child = getattr(parameterized, pname) + if isinstance(child, param.Parameterized): + update_with_nested_dict(child, value) + continue + setattr(parameterized, pname, value) + + +def get_current_values_from_dict(obj, kwargs, match_properties=True): """ - Base Class to represent only the property attributes defined at initialization, after which the - class is frozen. This prevents user to create any attributes that are not defined as properties. + Returns the current nested dictionary of values from the given object based on the keys of the + the given kwargs. + Parameters + ---------- + obj: MagicParameterized: + MagicParameterized class instance - Raises - ------ - AttributeError - raises AttributeError if the object is not a property - """ """""" + kwargs, dict: + nested dictionary of values - __isfrozen = False + same_keys_only: + if True only keys in found in the `obj` class are allowed. - def __init__(self, **kwargs): - input_dict = {k: None for k in self._property_names_generator()} - if kwargs: - magic_kwargs = magic_to_dict(kwargs) - diff = set(magic_kwargs.keys()).difference(set(input_dict.keys())) - for attr in diff: - raise AttributeError( - f"{type(self).__name__} has no property '{attr}'" - f"\n Available properties are: {list(self._property_names_generator())}" + """ + new_dict = {} + for k, v in kwargs.items(): + try: + if isinstance(v, dict): + v = get_current_values_from_dict( + getattr(obj, k), v, match_properties=False ) - input_dict.update(magic_kwargs) - for k, v in input_dict.items(): - setattr(self, k, v) + else: + v = getattr(obj, k) + new_dict[k] = v + except AttributeError as e: + if match_properties: + raise AttributeError(e) from e + return new_dict + + +class MagicParameterized(param.Parameterized): + """Base Magic Parametrized class""" + + __isfrozen = False + + def __init__(self, arg=None, **kwargs): + super().__init__() self._freeze() + self.update(arg=arg, **kwargs) - def __setattr__(self, key, value): - if self.__isfrozen and not hasattr(self, key): + def __setattr__(self, name, value): + if self.__isfrozen and not hasattr(self, name) and not name.startswith("_"): raise AttributeError( - f"{type(self).__name__} has no property '{key}'" - f"\n Available properties are: {list(self._property_names_generator())}" + f"{type(self).__name__} has no parameter '{name}'" + f"\n Available parameters are: {list(self.as_dict().keys())}" ) - object.__setattr__(self, key, value) + try: + super().__setattr__(name, value) + except ValueError: + if name in self.param: + p = self.param[name] + # pylint: disable=unidiomatic-typecheck + if type(p) == param.ClassSelector: + value = {} if value is None else value + if isinstance(value, dict): + value = type(getattr(self, name))(**value) + return + if isinstance(value, (list, tuple, np.ndarray)): + if isinstance(p, param.List): + super().__setattr__(name, list(value)) + elif isinstance(p, param.Tuple): + super().__setattr__(name, tuple(value)) + return + raise def _freeze(self): self.__isfrozen = True - def _property_names_generator(self): - """returns a generator with class properties only""" - return ( - attr - for attr in dir(self) - if isinstance(getattr(type(self), attr, None), property) - ) + def update(self, arg=None, match_properties=True, **kwargs): + """ + Updates the class properties with provided arguments, supports magic underscore notation + + Parameters + ---------- + arg : dict + Dictionary of properties to be updated + match_properties: bool + If `True`, checks if provided properties over keyword arguments are matching the current + object properties. An error is raised if a non-matching property is found. + If `False`, the `update` method does not raise any error when an argument is not + matching a property. + kwargs : + Keyword/value pair of properties to be updated - def __repr__(self): - params = self._property_names_generator() - dict_str = ", ".join(f"{k}={repr(getattr(self,k))}" for k in params) - return f"{type(self).__name__}({dict_str})" + Returns + ------- + ParameterizedType + Updated parameterized object + """ + if arg is None: + arg = {} + elif isinstance(arg, MagicParameterized): + arg = arg.as_dict() + else: + arg = arg.copy() + arg.update(kwargs) + if arg: + arg = magic_to_dict(arg) + current_dict = get_current_values_from_dict( + self, arg, match_properties=match_properties + ) + new_dict = update_nested_dict( + current_dict, + arg, + same_keys_only=not match_properties, + replace_None_only=False, + ) + update_with_nested_dict(self, new_dict) + return self - def as_dict(self, flatten=False, separator="."): + def as_dict(self, flatten=False, separator="_"): """ returns recursively a nested dictionary with all properties objects of the class @@ -384,54 +490,23 @@ def as_dict(self, flatten=False, separator="."): the separator to be used when flattening the dictionary. Only applies if `flatten=True` """ - params = self._property_names_generator() dict_ = {} - for k in params: - val = getattr(self, k) + for key, val in self.param.values().items(): + if key == "name": + continue if hasattr(val, "as_dict"): - dict_[k] = val.as_dict() + dict_[key] = val.as_dict() else: - dict_[k] = val + dict_[key] = val if flatten: dict_ = linearize_dict(dict_, separator=separator) return dict_ - def update( - self, arg=None, _match_properties=True, _replace_None_only=False, **kwargs - ): - """ - Updates the class properties with provided arguments, supports magic underscore notation - - Parameters - ---------- - - _match_properties: bool - If `True`, checks if provided properties over keyword arguments are matching the current - object properties. An error is raised if a non-matching property is found. - If `False`, the `update` method does not raise any error when an argument is not - matching a property. - - _replace_None_only: - updates matching properties that are equal to `None` (not already been set) - - - Returns - ------- - self - """ - arg = {} if arg is None else arg.copy() - arg = magic_to_dict({**arg, **kwargs}) - current_dict = self.as_dict() - new_dict = update_nested_dict( - current_dict, - arg, - same_keys_only=not _match_properties, - replace_None_only=_replace_None_only, - ) - for k, v in new_dict.items(): - setattr(self, k, v) - return self - def copy(self): """returns a copy of the current class instance""" - return deepcopy(self) + return type(self)(**self.as_dict()) + + +DEFAULTS_VALUES = magic_to_dict( + {k: v["default"] for k, v in DEFAULTS.items()}, separator="." +) diff --git a/magpylib/_src/defaults/defaults_values.py b/magpylib/_src/defaults/defaults_values.py index 8b7ddb2fb..188fc9bdb 100644 --- a/magpylib/_src/defaults/defaults_values.py +++ b/magpylib/_src/defaults/defaults_values.py @@ -1,18 +1,90 @@ -"""Package level config defaults""" +SUPPORTED_PLOTTING_BACKENDS = ("matplotlib", "plotly", "pyvista") + +ALLOWED_PLOTTING_BACKENDS = ("auto", *SUPPORTED_PLOTTING_BACKENDS) +ALLOWED_SIZEMODES = ("scaled", "absolute") +ALLOWED_ORIENTATION_SYMBOLS = ("cone", "arrow3d") +ALLOWED_PIVOTS = ("tail", "middle", "tip") +ALLOWED_SYMBOLS = (".", "+", "D", "d", "s", "x", "o") +ALLOWED_LINESTYLES = ( + "solid", + "dashed", + "dotted", + "dashdot", + "loosely dotted", + "loosely dashdotted", + "-", + "--", + "-.", + ".", + ":", + (0, (1, 1)), +) DEFAULTS = { - "display": { - "autosizefactor": 10, - "animation": { - "fps": 20, - "maxfps": 30, - "maxframes": 200, - "time": 5, - "slider": True, - "output": None, - }, - "backend": "auto", - "colorsequence": ( + "display.autosizefactor": { + "$type": "Number", + "default": 10, + "bounds": (0, None), + "inclusive_bounds": (False, True), + "softbounds": (5, 15), + "doc": """ + Defines at which scale objects like sensors and dipoles are displayed. + -> object_size = canvas_size / autosizefactor""", + }, + "display.animation.fps": { + "$type": "Integer", + "default": 20, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": "Target number of frames to be displayed per second.", + }, + "display.animation.maxfps": { + "$type": "Integer", + "default": 30, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": "Maximum number of frames to be displayed per second before downsampling kicks in.", + }, + "display.animation.maxframes": { + "$type": "Integer", + "default": 200, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": "Maximum total number of frames to be displayed before downsampling kicks in.", + }, + "display.animation.time": { + "$type": "Number", + "default": 5, + "bounds": (0, None), + "inclusive_bounds": (False, None), + "doc": "Default animation time.", + }, + "display.animation.slider": { + "$type": "Boolean", + "default": True, + "doc": """If True, an interactive slider will be displayed and stay in sync with the + animation, will be hidden otherwise.""", + }, + "display.animation.output": { + "$type": "String", + "default": "", + "allow_None": True, + "doc": "Animation output type (either `mp4` or `gif` or ending with `.mp4` or `.gif`)", + "regex": r"^(mp4|gif|(.*\.(mp4|gif))?)$", + }, + "display.backend": { + "$type": "Selector", + "default": "auto", + "objects": ALLOWED_PLOTTING_BACKENDS, + "doc": """ + Plotting backend to be used by default, if not explicitly set in the `display` + function (e.g. 'matplotlib', 'plotly'). + Supported backends are defined in magpylib.SUPPORTED_PLOTTING_BACKENDS""", + }, + "display.colorsequence": { + "$type": "List", + "item_type": "Color", + "default": [ "#2E91E5", "#E15F99", "#1CA71C", @@ -37,141 +109,788 @@ "#0D2A63", "#AF0038", "#222A2A", - ), - "style": { - "base": { - "path": { - "line": {"width": 1, "style": "solid", "color": None}, - "marker": {"size": 3, "symbol": "o", "color": None}, - "show": True, - "frames": None, - "numbering": False, - }, - "description": {"show": True, "text": None}, - "legend": {"show": True, "text": None}, - "opacity": 1, - "model3d": {"showdefault": True, "data": []}, - "color": None, - }, - "magnet": { - "magnetization": { - "show": True, - "arrow": { - "show": True, - "size": 1, - "sizemode": "scaled", - "offset": 1, - "width": 2, - "style": "solid", - "color": None, - }, - "color": { - "north": "#E71111", - "middle": "#DDDDDD", - "south": "#00B050", - "transition": 0.2, - "mode": "tricolor", - }, - "mode": "auto", - } - }, - "current": { - "arrow": { - "show": True, - "size": 1, - "sizemode": "scaled", - "offset": 0.5, - "width": 1, - "style": "solid", - "color": None, - }, - "line": {"show": True, "width": 2, "style": "solid", "color": None}, - }, - "sensor": { - "size": 1, - "sizemode": "scaled", - "pixel": { - "size": 1, - "sizemode": "scaled", - "color": None, - "symbol": "o", - }, - "arrows": { - "x": {"color": "red"}, - "y": {"color": "green"}, - "z": {"color": "blue"}, - }, - }, - "dipole": {"size": 1, "sizemode": "scaled", "pivot": "middle"}, - "triangle": { - "magnetization": { - "show": True, - "arrow": { - "show": True, - "size": 1, - "sizemode": "scaled", - "offset": 1, - "width": 2, - "style": "solid", - "color": None, - }, - "color": { - "north": "#E71111", - "middle": "#DDDDDD", - "south": "#00B050", - "transition": 0.2, - "mode": "tricolor", - }, - "mode": "auto", - }, - "orientation": { - "show": True, - "size": 1, - "color": "grey", - "offset": 0.9, - "symbol": "arrow3d", - }, - }, - "triangularmesh": { - "orientation": { - "show": False, - "size": 1, - "color": "grey", - "offset": 0.9, - "symbol": "arrow3d", - }, - "mesh": { - "grid": { - "show": False, - "line": {"width": 2, "style": "solid", "color": "black"}, - "marker": {"size": 1, "symbol": "o", "color": "black"}, - }, - "open": { - "show": False, - "line": {"width": 2, "style": "solid", "color": "cyan"}, - "marker": {"size": 1, "symbol": "o", "color": "black"}, - }, - "disconnected": { - "show": False, - "line": {"width": 2, "style": "solid", "color": "black"}, - "marker": {"size": 5, "symbol": "o", "color": "black"}, - "colorsequence": ( - "red", - "blue", - "green", - "cyan", - "magenta", - "yellow", - ), - }, - "selfintersecting": { - "show": False, - "line": {"width": 2, "style": "solid", "color": "magenta"}, - "marker": {"size": 1, "symbol": "o", "color": "black"}, - }, - }, - }, - "markers": {"marker": {"size": 2, "color": "grey", "symbol": "x"}}, - }, + ], + "doc": """ + An iterable of color values used to cycle trough for every object displayed. + A color may be specified by + - a hex string (e.g. '#ff0000') + - an rgb/rgba string (e.g. 'rgb(255,0,0)') + - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') + - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') + - a named CSS color""", + }, + "display.style.base.label": { + "$type": "String", + "default": "", + "doc": "Object label.", + }, + "display.style.base.description.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide object description in legend (shown in parentheses).", + }, + "display.style.base.description.text": { + "$type": "String", + "default": "", + "doc": "Object description text.", + }, + "display.style.base.legend.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide legend.", + }, + "display.style.base.legend.text": { + "$type": "String", + "default": "", + "doc": "Custom legend text. Overrides complete legend.", + }, + "display.style.base.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Object explicit color", + }, + "display.style.base.opacity": { + "$type": "Number", + "default": 1, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """ + Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent.""", + }, + "display.style.base.path.line.width": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Path line width.", + }, + "display.style.base.path.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Path line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.base.path.line.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit Path line color. Takes object color by default.", + }, + "display.style.base.path.marker.size": { + "$type": "Number", + "default": 3, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Path marker size.", + }, + "display.style.base.path.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Path marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.base.path.marker.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Path marker color.", + }, + "display.style.base.path.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide path.", + }, + "display.style.base.path.frames.indices": { + "$type": "List", + "default": [-1], + "item_type": int, + "doc": "Array_like shape (n,) of integers: describes certain path indices.", + }, + "display.style.base.path.frames.step": { + "$type": "Integer", + "default": 1, + "bounds": (1, None), + "softbounds": (0, 10), + "doc": "Displays the object(s) at every i'th path position.", + }, + "display.style.base.path.frames.mode": { + "$type": "Selector", + "default": "indices", + "objects": ("indices", "step"), + "doc": """ + The object path frames mode. + - step: integer i: displays the object(s) at every i'th path position. + - indices: array_like shape (n,) of integers: describes certain path indices.""", + }, + "display.style.base.path.numbering": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide numbering on path positions. Only applies if show=True.", + }, + "display.style.base.model3d.showdefault": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide default 3D-model.", + }, + "display.style.base.model3d.data": { + "$type": "List", + "default": [], + "item_type": "Trace3d", + "doc": """ + A trace or list of traces where each is an instance of `Trace3d` or dictionary of + equivalent key/value pairs. Defines properties for an additional user-defined model3d + object which is positioned relatively to the main object to be displayed and moved + automatically with it. This feature also allows the user to replace the original 3d + representation of the object.""", + }, + "display.style.magnet.magnetization.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide magnetization indication (arrow and/or color).", + }, + "display.style.magnet.magnetization.arrow.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide magnetization arrow.", + }, + "display.style.magnet.magnetization.arrow.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Magnetization arrow size.", + }, + "display.style.magnet.magnetization.arrow.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.magnet.magnetization.arrow.offset": { + "$type": "Number", + "default": 1, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """ + Magnetization arrow offset. `offset=0` results in the arrow head to be + coincident with the start of the line, and `offset=1` with the end.""", + }, + "display.style.magnet.magnetization.arrow.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Magnetization arrow line width", + }, + "display.style.magnet.magnetization.arrow.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Magnetization arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.magnet.magnetization.arrow.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit magnetization arrow color. Takes the object color by default.", + }, + "display.style.magnet.magnetization.color.north": { + "$type": "Color", + "default": "#E71111", + "doc": "The color of the magnetic north pole.", + }, + "display.style.magnet.magnetization.color.middle": { + "$type": "Color", + "default": "#DDDDDD", + "doc": "The color between the magnetic poles.", + }, + "display.style.magnet.magnetization.color.south": { + "$type": "Color", + "default": "#00B050", + "doc": "The color of the magnetic south pole.", + }, + "display.style.magnet.magnetization.color.transition": { + "$type": "Number", + "default": 0.2, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """Sets the transition smoothness between poles colors. Must be between 0 and 1. + - `transition=0`: discrete transition + - `transition=1`: smoothest transition + """, + }, + "display.style.magnet.magnetization.color.mode": { + "$type": "Selector", + "default": "tricolor", + "objects": ("tricolor", "bicolor", "tricycle"), + "doc": """ + Sets the coloring mode for the magnetization. + - `'bicolor'`: only north and south poles are shown, middle color is hidden. + - `'tricolor'`: both pole colors and middle color are shown. + - `'tricycle'`: both pole colors are shown and middle color is replaced by a color cycling + through the color sequence.""", + }, + "display.style.magnet.magnetization.mode": { + "$type": "Selector", + "default": "auto", + "objects": ("auto", "arrow", "color", "arrow+color", "color+arrow"), + "doc": """ + One of {"auto", "arrow", "color", "arrow+color"}, default="auto" + Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means + that the chosen backend determines which mode is applied by its capability. If the backend + can display both and `auto` is chosen, the priority is given to `color`.""", + }, + "display.style.current.arrow.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide current arrow.", + }, + "display.style.current.arrow.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Current arrow size.", + }, + "display.style.current.arrow.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the current arrow size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.current.arrow.offset": { + "$type": "Number", + "default": 0.5, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """ + Current arrow offset. `offset=0` results in the arrow head to be coincident + with the start of the line, and `offset=1` with the end.""", + }, + "display.style.current.arrow.width": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Current arrow line width", + }, + "display.style.current.arrow.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Current arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.current.arrow.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit current arrow color. Takes the object color by default.", + }, + "display.style.current.line.show": { + "$type": "Boolean", + "default": True, + "doc": "", + }, + "display.style.current.line.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Current line width.", + }, + "display.style.current.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Current line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.current.line.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit current line color. Takes object color by default.", + }, + "display.style.sensor.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Sensor size.", + }, + "display.style.sensor.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the sensor size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.sensor.pixel.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Sensor pixel size.", + }, + "display.style.sensor.pixel.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the sensor pixel size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.sensor.pixel.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Sensor pixel color.", + }, + "display.style.sensor.pixel.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Pixel symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.sensor.arrows.x.color": { + "$type": "Color", + "default": "#ff0000", + "doc": "Sensor x-arrow color.", + }, + "display.style.sensor.arrows.x.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide sensor x-arrow.", + }, + "display.style.sensor.arrows.y.color": { + "$type": "Color", + "default": "#008000", + "doc": "Sensor y-arrow color.", + }, + "display.style.sensor.arrows.y.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide sensor y-arrow.", + }, + "display.style.sensor.arrows.z.color": { + "$type": "Color", + "default": "#0000FF", + "doc": "Sensor z-arrow color.", + }, + "display.style.sensor.arrows.z.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide sensor z-arrow.", + }, + "display.style.dipole.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Dipole size.", + }, + "display.style.dipole.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the dipole size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.dipole.pivot": { + "$type": "Selector", + "default": "middle", + "objects": ALLOWED_PIVOTS, + "doc": f""" + The part of the arrow that is anchored to the X, Y grid. The arrow rotates about + this point. Can be one of `{ALLOWED_PIVOTS}`""", + }, + "display.style.triangle.magnetization.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide magnetization indication (arrow and/or color).", + }, + "display.style.triangle.magnetization.arrow.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide magnetization arrow.", + }, + "display.style.triangle.magnetization.arrow.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Magnetization arrow size.", + }, + "display.style.triangle.magnetization.arrow.sizemode": { + "$type": "Selector", + "default": "scaled", + "objects": ALLOWED_SIZEMODES, + "doc": f"The way the object size gets defined. Can be one of `{ALLOWED_SIZEMODES}`", + }, + "display.style.triangle.magnetization.arrow.offset": { + "$type": "Number", + "default": 1, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """ + Magnetization arrow offset. `offset=0` results in the arrow head to be + coincident with the start of the line, and `offset=1` with the end.""", + }, + "display.style.triangle.magnetization.arrow.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Magnetization arrow line width", + }, + "display.style.triangle.magnetization.arrow.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Triangle magnetization arrow line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.triangle.magnetization.arrow.color": { + "$type": "Color", + "default": None, + "allow_None": True, + "doc": "Explicit triangle magnetization arrow color. Takes the object color by default.", + }, + "display.style.triangle.magnetization.color.north": { + "$type": "Color", + "default": "#E71111", + "doc": "The color of the magnetic north pole.", + }, + "display.style.triangle.magnetization.color.middle": { + "$type": "Color", + "default": "#DDDDDD", + "doc": "The color between the magnetic poles.", + }, + "display.style.triangle.magnetization.color.south": { + "$type": "Color", + "default": "#00B050", + "doc": "The color of the magnetic south pole.", + }, + "display.style.triangle.magnetization.color.transition": { + "$type": "Number", + "default": 0.2, + "bounds": (0, 1), + "inclusive_bounds": (True, True), + "softbounds": (0, 1), + "doc": """Sets the transition smoothness between poles colors. Must be between 0 and 1. + - `transition=0`: discrete transition + - `transition=1`: smoothest transition + """, + }, + "display.style.triangle.magnetization.color.mode": { + "$type": "Selector", + "default": "tricolor", + "objects": ("tricolor", "bicolor", "tricycle"), + "doc": """ + Sets the coloring mode for the magnetization. + - `'bicolor'`: only north and south poles are shown, middle color is hidden. + - `'tricolor'`: both pole colors and middle color are shown. + - `'tricycle'`: both pole colors are shown and middle color is replaced by a color cycling + through the color sequence.""", + }, + "display.style.triangle.magnetization.mode": { + "$type": "Selector", + "default": "auto", + "objects": ("auto", "arrow", "color", "arrow+color", "color+arrow"), + "doc": """ + One of {"auto", "arrow", "color", "arrow+color"}, default="auto" + Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means + that the chosen backend determines which mode is applied by its capability. If the backend + can display both and `auto` is chosen, the priority is given to `color`.""", + }, + "display.style.triangle.orientation.show": { + "$type": "Boolean", + "default": True, + "doc": "Show/hide orientation symbol.", + }, + "display.style.triangle.orientation.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Size of the orientation symbol", + }, + "display.style.triangle.orientation.color": { + "$type": "Color", + "default": "grey", + "doc": "Explicit orientation symbol color. Takes the objet color by default.", + }, + "display.style.triangle.orientation.offset": { + "$type": "Number", + "default": 0.9, + "bounds": (-2, 2), + "inclusive_bounds": (True, True), + "softbounds": (-0.9, 0.9), + "doc": """ + Orientation symbol offset, normal to the triangle surface. `offset=0` results + in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the + base""", + }, + "display.style.triangle.orientation.symbol": { + "$type": "Selector", + "default": "arrow3d", + "objects": ALLOWED_ORIENTATION_SYMBOLS, + "doc": f""" + Orientation symbol for the triangular faces. Can be one of: + {ALLOWED_ORIENTATION_SYMBOLS}""", + }, + "display.style.triangularmesh.orientation.show": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide orientation symbol.", + }, + "display.style.triangularmesh.orientation.size": { + "$type": "Number", + "default": 1, + "bounds": (0, None), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Size of the orientation symbol", + }, + "display.style.triangularmesh.orientation.color": { + "$type": "Color", + "default": "grey", + "doc": "Explicit orientation symbol color. Takes the objet color by default.", + }, + "display.style.triangularmesh.orientation.offset": { + "$type": "Number", + "default": 0.9, + "bounds": (-2, 2), + "inclusive_bounds": (True, True), + "softbounds": (-0.9, 0.9), + "doc": """ + Orientation symbol offset, normal to the triangle surface. `offset=0` results + in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the + base""", + }, + "display.style.triangularmesh.orientation.symbol": { + "$type": "Selector", + "default": "arrow3d", + "objects": ALLOWED_ORIENTATION_SYMBOLS, + "doc": f""" + Orientation symbol for the triangular faces. Can be one of: + {ALLOWED_ORIENTATION_SYMBOLS}""", + }, + "display.style.triangularmesh.mesh.grid.show": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide mesh grid", + }, + "display.style.triangularmesh.mesh.grid.line.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh grid line width.", + }, + "display.style.triangularmesh.mesh.grid.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh grid line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.triangularmesh.mesh.grid.line.color": { + "$type": "Color", + "default": "#000000", + "doc": "Explicit current line color. Takes object color by default.", + }, + "display.style.triangularmesh.mesh.grid.marker.size": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh grid marker size.", + }, + "display.style.triangularmesh.mesh.grid.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh grid marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.triangularmesh.mesh.grid.marker.color": { + "$type": "Color", + "default": "#000000", + "doc": "Mesh grid marker color.", + }, + "display.style.triangularmesh.mesh.open.show": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide mesh open", + }, + "display.style.triangularmesh.mesh.open.line.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh open line width.", + }, + "display.style.triangularmesh.mesh.open.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh open line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.triangularmesh.mesh.open.line.color": { + "$type": "Color", + "default": "#00FFFF", + "doc": "Explicit current line color. Takes object color by default.", + }, + "display.style.triangularmesh.mesh.open.marker.size": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh open marker size.", + }, + "display.style.triangularmesh.mesh.open.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh open marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.triangularmesh.mesh.open.marker.color": { + "$type": "Color", + "default": "#000000", + "doc": "Mesh open marker color.", + }, + "display.style.triangularmesh.mesh.disconnected.show": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide mesh disconnected", + }, + "display.style.triangularmesh.mesh.disconnected.line.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh disconnected line width.", + }, + "display.style.triangularmesh.mesh.disconnected.line.style": { + "$type": "Selector", + "default": "dashed", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh disconnected line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.triangularmesh.mesh.disconnected.line.color": { + "$type": "Color", + "default": "#FF00FF", + "doc": "Explicit current line color. Takes object color by default.", + }, + "display.style.triangularmesh.mesh.disconnected.marker.size": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh disconnected marker size.", + }, + "display.style.triangularmesh.mesh.disconnected.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh disconnected marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.triangularmesh.mesh.disconnected.marker.color": { + "$type": "Color", + "default": "#000000", + "doc": "Mesh disconnected marker color.", + }, + "display.style.triangularmesh.mesh.disconnected.colorsequence": { + "$type": "List", + "item_type": "Color", + "default": ["#FF0000", "#0000FF", "#008000", "#00FFFF", "#FF00FF", "#FFFF00"], + "doc": """ + An iterable of color values used to cycle trough for every disconnected part to be + displayed. A color may be specified by + - a hex string (e.g. '#ff0000') + - an rgb/rgba string (e.g. 'rgb(255,0,0)') + - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') + - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') + - a named CSS color""", + }, + "display.style.triangularmesh.mesh.selfintersecting.show": { + "$type": "Boolean", + "default": False, + "doc": "Show/hide mesh selfintersecting", + }, + "display.style.triangularmesh.mesh.selfintersecting.line.width": { + "$type": "Number", + "default": 2, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh selfintersecting line width.", + }, + "display.style.triangularmesh.mesh.selfintersecting.line.style": { + "$type": "Selector", + "default": "solid", + "objects": ALLOWED_LINESTYLES, + "doc": f"Mesh selfintersecting line style. Can be one of: {ALLOWED_LINESTYLES}.", + }, + "display.style.triangularmesh.mesh.selfintersecting.line.color": { + "$type": "Color", + "default": "#000000", + "doc": "Explicit current line color. Takes object color by default.", + }, + "display.style.triangularmesh.mesh.selfintersecting.marker.size": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Mesh selfintersecting marker size.", + }, + "display.style.triangularmesh.mesh.selfintersecting.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Mesh selfintersecting marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", + }, + "display.style.triangularmesh.mesh.selfintersecting.marker.color": { + "$type": "Color", + "default": "#000000", + "doc": "Mesh selfintersecting marker color.", + }, + "display.style.markers.marker.size": { + "$type": "Number", + "default": 1, + "bounds": (0, 20), + "inclusive_bounds": (True, True), + "softbounds": (1, 5), + "doc": "Markers marker size.", + }, + "display.style.markers.marker.color": { + "$type": "Color", + "default": "#808080", + "doc": "Markers marker color.", + }, + "display.style.markers.marker.symbol": { + "$type": "Selector", + "default": "o", + "objects": ALLOWED_SYMBOLS, + "doc": f"Markers marker symbol. Can be one of: {ALLOWED_SYMBOLS}.", }, } diff --git a/magpylib/_src/display/display.py b/magpylib/_src/display/display.py index 946144312..f02a97b5f 100644 --- a/magpylib/_src/display/display.py +++ b/magpylib/_src/display/display.py @@ -368,8 +368,8 @@ def show( ... polarization=(1,1,1), ... style_path_show=False ... ) - >>> magpy.defaults.display.style.magnet.magnetization.size = 2 - >>> src1.style.magnetization.size = 1 + >>> magpy.defaults.display.style.magnet.magnetization.arrow.size = 2 + >>> src1.style.magnetization.arrow.size = 1 >>> magpy.show(src1, src2, style_color='r') # doctest: +SKIP >>> # graphic output diff --git a/magpylib/_src/display/traces_generic.py b/magpylib/_src/display/traces_generic.py index 5f3cc21a8..8a41df821 100644 --- a/magpylib/_src/display/traces_generic.py +++ b/magpylib/_src/display/traces_generic.py @@ -15,10 +15,11 @@ import numpy as np import magpylib as magpy +from magpylib._src.defaults.defaults_classes import MarkersStyle from magpylib._src.defaults.defaults_classes import default_settings -from magpylib._src.defaults.defaults_utility import ALLOWED_LINESTYLES -from magpylib._src.defaults.defaults_utility import ALLOWED_SYMBOLS from magpylib._src.defaults.defaults_utility import linearize_dict +from magpylib._src.defaults.defaults_values import ALLOWED_LINESTYLES +from magpylib._src.defaults.defaults_values import ALLOWED_SYMBOLS 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 @@ -29,7 +30,6 @@ 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 slice_mesh_from_colorscale -from magpylib._src.style import DefaultMarkers from magpylib._src.utility import format_obj_input @@ -37,7 +37,7 @@ class MagpyMarkers: """A class that stores markers 3D-coordinates.""" def __init__(self, *markers): - self._style = DefaultMarkers() + self._style = MarkersStyle() self.markers = np.array(markers) @property @@ -510,7 +510,7 @@ def get_generic_traces( tr_generic["color"] = tr_generic.get("color", style.color) else: # pragma: no cover raise ValueError( - f"{ttype} is not supported, only 'scatter3d' and 'mesh3d' are" + f"{ttype!r} is not supported, only 'scatter3d' and 'mesh3d' are" ) tr_generic.update(linearize_dict(obj_extr_trace, separator="_")) traces_generic.append(tr_generic) @@ -640,7 +640,7 @@ def process_animation_kwargs(obj_list, animation=False, **kwargs): # pylint: disable=no-member anim_def = default_settings.display.animation.copy() - anim_def.update({k[10:]: v for k, v in kwargs.items()}, _match_properties=False) + anim_def.update({k[10:]: v for k, v in kwargs.items()}, match_properties=False) animation_kwargs = {f"animation_{k}": v for k, v in anim_def.as_dict().items()} kwargs = {k: v for k, v in kwargs.items() if not k.startswith("animation")} return kwargs, animation, animation_kwargs diff --git a/magpylib/_src/display/traces_utility.py b/magpylib/_src/display/traces_utility.py index 9ff442d24..2d2a0b2ef 100644 --- a/magpylib/_src/display/traces_utility.py +++ b/magpylib/_src/display/traces_utility.py @@ -9,9 +9,8 @@ import numpy as np from scipy.spatial.transform import Rotation as RotScipy -from magpylib._src.defaults.defaults_classes import default_settings +from magpylib._src.defaults.defaults_utility import get_style from magpylib._src.defaults.defaults_utility import linearize_dict -from magpylib._src.style import get_style from magpylib._src.utility import format_obj_input @@ -213,7 +212,7 @@ def draw_arrow_on_circle(sign, diameter, arrow_size, scaled=True, angle_pos_deg= return vertices -def get_rot_pos_from_path(obj, show_path=None): +def get_rot_pos_from_path(obj, frames): """ subsets orientations and positions depending on `show_path` value. examples: @@ -222,19 +221,13 @@ def get_rot_pos_from_path(obj, show_path=None): """ # pylint: disable=protected-access # pylint: disable=invalid-unary-operand-type - if show_path is None: - show_path = True pos = obj._position orient = obj._orientation path_len = pos.shape[0] - if show_path is True or show_path is False or show_path == 0: - inds = np.array([-1]) - elif isinstance(show_path, int): - inds = np.arange(path_len, dtype=int)[::-show_path] - elif hasattr(show_path, "__iter__") and not isinstance(show_path, str): - inds = np.array(show_path) - else: # pragma: no cover - raise ValueError(f"Invalid show_path value ({show_path})") + if frames.mode == "indices": + inds = np.array(frames.indices) + else: + inds = np.arange(path_len, dtype=int)[:: -frames.step] inds[inds >= path_len] = path_len - 1 inds = np.unique(inds) if inds.size == 0: @@ -279,7 +272,7 @@ def get_flatten_objects_properties_recursive( flat_objs = {} for subobj in obj_list_semi_flat: isCollection = getattr(subobj, "children", None) is not None - style = get_style(subobj, default_settings, **kwargs) + style = get_style(subobj, **kwargs) if style.label is None: style.label = str(type(subobj).__name__) legendgroup = f"{subobj}" if parent_legendgroup is None else parent_legendgroup diff --git a/magpylib/_src/fields/field_BH_cuboid.py b/magpylib/_src/fields/field_BH_cuboid.py index 19365d78a..6d942e65a 100644 --- a/magpylib/_src/fields/field_BH_cuboid.py +++ b/magpylib/_src/fields/field_BH_cuboid.py @@ -10,6 +10,8 @@ # pylint: disable=too-many-statements +# pylint: disable=too-many-statements + # CORE def magnet_cuboid_Bfield( @@ -62,6 +64,8 @@ def magnet_cuboid_Bfield( positions to their bottQ4 counterparts. see also Cichon: IEEE Sensors Journal, vol. 19, no. 7, April 1, 2019, p.2509 + + """ pol_x, pol_y, pol_z = polarizations.T a, b, c = dimensions.T / 2 diff --git a/magpylib/_src/fields/field_BH_cylinder.py b/magpylib/_src/fields/field_BH_cylinder.py index e71eea7d0..1489dd308 100644 --- a/magpylib/_src/fields/field_BH_cylinder.py +++ b/magpylib/_src/fields/field_BH_cylinder.py @@ -265,6 +265,7 @@ def BHJM_magnet_cylinder( observers: np.ndarray, dimension: np.ndarray, polarization: np.ndarray, + in_out="auto", ) -> np.ndarray: """ - Translate cylinder core fields to BHJM diff --git a/magpylib/_src/fields/field_BH_cylinder_segment.py b/magpylib/_src/fields/field_BH_cylinder_segment.py index 1a0f289e2..ea4a73983 100644 --- a/magpylib/_src/fields/field_BH_cylinder_segment.py +++ b/magpylib/_src/fields/field_BH_cylinder_segment.py @@ -2337,6 +2337,7 @@ def BHJM_cylinder_segment( observers: np.ndarray, dimension: np.ndarray, polarization: np.ndarray, + in_out="auto", ) -> np.ndarray: """ - translate cylinder segment field to BHJM @@ -2444,11 +2445,3 @@ def BHJM_cylinder_segment( raise ValueError( # pragma: no cover "`output_field_type` must be one of ('B', 'H', 'M', 'J'), " f"got {field!r}" ) - - # return convert_HBMJ( - # output_field_type=field, - # polarization=polarization, - # input_field_type="H", - # field_values=H_all, - # mask_inside=mask_inside & mask_not_on_surf, - # ) diff --git a/magpylib/_src/fields/field_BH_sphere.py b/magpylib/_src/fields/field_BH_sphere.py index 379d761cf..cd535dfe3 100644 --- a/magpylib/_src/fields/field_BH_sphere.py +++ b/magpylib/_src/fields/field_BH_sphere.py @@ -55,6 +55,7 @@ def BHJM_magnet_sphere( observers: np.ndarray, diameter: np.ndarray, polarization: np.ndarray, + in_out="auto", ) -> np.ndarray: """ - compute sphere field and translate to BHJM diff --git a/magpylib/_src/input_checks.py b/magpylib/_src/input_checks.py index b4b0a6d20..de3183a21 100644 --- a/magpylib/_src/input_checks.py +++ b/magpylib/_src/input_checks.py @@ -10,7 +10,7 @@ from magpylib import _src from magpylib._src.defaults.defaults_classes import default_settings -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS +from magpylib._src.defaults.defaults_values import SUPPORTED_PLOTTING_BACKENDS from magpylib._src.exceptions import MagpylibBadUserInput from magpylib._src.exceptions import MagpylibMissingInput from magpylib._src.utility import format_obj_input diff --git a/magpylib/_src/obj_classes/class_BaseExcitations.py b/magpylib/_src/obj_classes/class_BaseExcitations.py index e6d5e1733..deb78f7fa 100644 --- a/magpylib/_src/obj_classes/class_BaseExcitations.py +++ b/magpylib/_src/obj_classes/class_BaseExcitations.py @@ -5,6 +5,8 @@ import numpy as np +from magpylib._src.defaults.defaults_classes import CurrentStyle +from magpylib._src.defaults.defaults_classes import MagnetStyle from magpylib._src.exceptions import MagpylibDeprecationWarning from magpylib._src.fields.field_wrap_BH import getBH_level2 from magpylib._src.input_checks import check_format_input_scalar @@ -12,8 +14,6 @@ from magpylib._src.input_checks import validate_field_func from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr from magpylib._src.obj_classes.class_BaseGeo import BaseGeo -from magpylib._src.style import CurrentStyle -from magpylib._src.style import MagnetStyle from magpylib._src.utility import format_star_input diff --git a/magpylib/_src/obj_classes/class_BaseGeo.py b/magpylib/_src/obj_classes/class_BaseGeo.py index e01108f2e..c6b539e39 100644 --- a/magpylib/_src/obj_classes/class_BaseGeo.py +++ b/magpylib/_src/obj_classes/class_BaseGeo.py @@ -6,11 +6,11 @@ import numpy as np from scipy.spatial.transform import Rotation as R +from magpylib._src.defaults.defaults_classes import BaseStyle from magpylib._src.exceptions import MagpylibBadUserInput from magpylib._src.input_checks import check_format_input_orientation from magpylib._src.input_checks import check_format_input_vector from magpylib._src.obj_classes.class_BaseTransform import BaseTransform -from magpylib._src.style import BaseStyle from magpylib._src.utility import add_iteration_suffix @@ -358,7 +358,7 @@ def copy(self, **kwargs): ): # pylint: disable=no-member label = self.style.label - if label is None: + if not label: label = f"{type(self).__name__}_01" else: label = add_iteration_suffix(label) diff --git a/magpylib/_src/obj_classes/class_Collection.py b/magpylib/_src/obj_classes/class_Collection.py index 0e79fa38b..5c5fd6f74 100644 --- a/magpylib/_src/obj_classes/class_Collection.py +++ b/magpylib/_src/obj_classes/class_Collection.py @@ -465,7 +465,7 @@ def set_children_styles(self, arg=None, recursive=True, _validate=True, **kwargs >>> # We apply styles using underscore magic for magnetization vector size and a style >>> # dictionary for the color. >>> - >>> col.set_children_styles(magnetization_size=0.5) + >>> col.set_children_styles(magnetization_arrow_size=0.5) Collection(id=...) >>> col.set_children_styles({"color": "g"}) Collection(id=...) @@ -497,7 +497,7 @@ def set_children_styles(self, arg=None, recursive=True, _validate=True, **kwargs for k, v in style_kwargs.items() if k.split("_")[0] in child.style.as_dict() } - child.style.update(**style_kwargs_specific, _match_properties=True) + child.style.update(**style_kwargs_specific, match_properties=True) return self def _validate_getBH_inputs(self, *inputs): diff --git a/magpylib/_src/obj_classes/class_Sensor.py b/magpylib/_src/obj_classes/class_Sensor.py index dd6a60121..fe5b1d7de 100644 --- a/magpylib/_src/obj_classes/class_Sensor.py +++ b/magpylib/_src/obj_classes/class_Sensor.py @@ -2,13 +2,13 @@ import numpy as np +from magpylib._src.defaults.defaults_classes import SensorStyle from magpylib._src.display.traces_core import make_Sensor from magpylib._src.exceptions import MagpylibBadUserInput from magpylib._src.fields.field_wrap_BH import getBH_level2 from magpylib._src.input_checks import check_format_input_vector from magpylib._src.obj_classes.class_BaseDisplayRepr import BaseDisplayRepr from magpylib._src.obj_classes.class_BaseGeo import BaseGeo -from magpylib._src.style import SensorStyle from magpylib._src.utility import format_star_input diff --git a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py index 81fa877cd..9f2a4b627 100644 --- a/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py +++ b/magpylib/_src/obj_classes/class_magnet_TriangularMesh.py @@ -5,6 +5,7 @@ import numpy as np from scipy.spatial import ConvexHull # pylint: disable=no-name-in-module +from magpylib._src.defaults.defaults_classes import TriangularmeshStyle from magpylib._src.display.traces_core import make_TriangularMesh from magpylib._src.exceptions import MagpylibMissingInput from magpylib._src.fields.field_BH_triangularmesh import BHJM_magnet_trimesh @@ -18,7 +19,6 @@ from magpylib._src.obj_classes.class_BaseExcitations import BaseMagnet from magpylib._src.obj_classes.class_Collection import Collection from magpylib._src.obj_classes.class_misc_Triangle import Triangle -from magpylib._src.style import TriangularMeshStyle # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-public-methods @@ -116,7 +116,7 @@ class TriangularMesh(BaseMagnet): _field_func = staticmethod(BHJM_magnet_trimesh) _field_func_kwargs_ndim = {"polarization": 2, "mesh": 3} get_trace = make_TriangularMesh - _style_class = TriangularMeshStyle + _style_class = TriangularmeshStyle def __init__( self, @@ -521,7 +521,7 @@ def to_TriangleCollection(self): coll.position = self.position coll.orientation = self.orientation # pylint: disable=no-member - coll.style.update(self.style.as_dict(), _match_properties=False) + coll.style.update(self.style.as_dict(), match_properties=False) return coll @classmethod diff --git a/magpylib/_src/obj_classes/class_misc_Dipole.py b/magpylib/_src/obj_classes/class_misc_Dipole.py index 63872b45c..d7b9de194 100644 --- a/magpylib/_src/obj_classes/class_misc_Dipole.py +++ b/magpylib/_src/obj_classes/class_misc_Dipole.py @@ -2,11 +2,11 @@ import numpy as np +from magpylib._src.defaults.defaults_classes import DipoleStyle from magpylib._src.display.traces_core import make_Dipole from magpylib._src.fields.field_BH_dipole import BHJM_dipole from magpylib._src.input_checks import check_format_input_vector from magpylib._src.obj_classes.class_BaseExcitations import BaseSource -from magpylib._src.style import DipoleStyle from magpylib._src.utility import unit_prefix diff --git a/magpylib/_src/obj_classes/class_misc_Triangle.py b/magpylib/_src/obj_classes/class_misc_Triangle.py index fe4d1d850..6b49a590b 100644 --- a/magpylib/_src/obj_classes/class_misc_Triangle.py +++ b/magpylib/_src/obj_classes/class_misc_Triangle.py @@ -3,11 +3,11 @@ import numpy as np +from magpylib._src.defaults.defaults_classes import TriangleStyle from magpylib._src.display.traces_core import make_Triangle from magpylib._src.fields.field_BH_triangle import BHJM_triangle from magpylib._src.input_checks import check_format_input_vector from magpylib._src.obj_classes.class_BaseExcitations import BaseMagnet -from magpylib._src.style import TriangleStyle class Triangle(BaseMagnet): diff --git a/magpylib/_src/style.py b/magpylib/_src/style.py deleted file mode 100644 index 9c8c2127e..000000000 --- a/magpylib/_src/style.py +++ /dev/null @@ -1,2338 +0,0 @@ -"""Collection of classes for display styling.""" - -# pylint: disable=C0302 -# pylint: disable=too-many-instance-attributes -# pylint: disable=cyclic-import -import numpy as np - -from magpylib._src.defaults.defaults_utility import ALLOWED_LINESTYLES -from magpylib._src.defaults.defaults_utility import ALLOWED_SYMBOLS -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS -from magpylib._src.defaults.defaults_utility import MagicProperties -from magpylib._src.defaults.defaults_utility import color_validator -from magpylib._src.defaults.defaults_utility import get_defaults_dict -from magpylib._src.defaults.defaults_utility import validate_property_class -from magpylib._src.defaults.defaults_utility import validate_style_keys - -ALLOWED_SIZEMODES = ("scaled", "absolute") - - -def get_families(obj): - """get obj families""" - # pylint: disable=import-outside-toplevel - # pylint: disable=possibly-unused-variable - # pylint: disable=redefined-outer-name - from magpylib._src.display.traces_generic import MagpyMarkers as Markers - from magpylib._src.obj_classes.class_BaseExcitations import BaseCurrent as Current - from magpylib._src.obj_classes.class_BaseExcitations import BaseMagnet as Magnet - from magpylib._src.obj_classes.class_current_Circle import Circle - from magpylib._src.obj_classes.class_current_Polyline import Polyline - from magpylib._src.obj_classes.class_magnet_Cuboid import Cuboid - from magpylib._src.obj_classes.class_magnet_Cylinder import Cylinder - from magpylib._src.obj_classes.class_magnet_CylinderSegment import CylinderSegment - from magpylib._src.obj_classes.class_magnet_Sphere import Sphere - from magpylib._src.obj_classes.class_magnet_Tetrahedron import Tetrahedron - from magpylib._src.obj_classes.class_magnet_TriangularMesh import TriangularMesh - from magpylib._src.obj_classes.class_misc_CustomSource import CustomSource - from magpylib._src.obj_classes.class_misc_Dipole import Dipole - from magpylib._src.obj_classes.class_misc_Triangle import Triangle - from magpylib._src.obj_classes.class_Sensor import Sensor - - loc = locals() - obj_families = [] - for item, val in loc.items(): - if not item.startswith("_"): - try: - if isinstance(obj, val): - obj_families.append(item.lower()) - except TypeError: - pass - return obj_families - - -def get_style(obj, default_settings, **kwargs): - """Returns default style based on increasing priority: - - style from defaults - - style from object - - style from kwargs arguments - """ - obj_families = get_families(obj) - # parse kwargs into style an non-style arguments - style_kwargs = kwargs.get("style", {}) - style_kwargs.update( - {k[6:]: v for k, v in kwargs.items() if k.startswith("style") and k != "style"} - ) - - # retrieve default style dictionary, local import to avoid circular import - # pylint: disable=import-outside-toplevel - - default_style = default_settings.display.style - base_style_flat = default_style.base.as_dict(flatten=True, separator="_") - - # construct object specific dictionary base on style family and default style - for obj_family in obj_families: - family_style = getattr(default_style, obj_family, {}) - if family_style: - family_dict = family_style.as_dict(flatten=True, separator="_") - base_style_flat.update( - {k: v for k, v in family_dict.items() if v is not None} - ) - style_kwargs = validate_style_keys(style_kwargs) - - # create style class instance and update based on precedence - style = obj.style.copy() - style_kwargs_specific = { - k: v for k, v in style_kwargs.items() if k.split("_")[0] in style.as_dict() - } - style.update(**style_kwargs_specific, _match_properties=True) - style.update(**base_style_flat, _match_properties=False, _replace_None_only=True) - - return style - - -class Line(MagicProperties): - """Defines line styling properties. - - Parameters - ---------- - style: str, default=None - Can be one of: - `['solid', '-', 'dashed', '--', 'dashdot', '-.', 'dotted', '.', (0, (1, 1)), - 'loosely dotted', 'loosely dashdotted']` - - color: str, default=None - Line color. - - width: float, default=None - Positive number that defines the line width. - """ - - def __init__(self, style=None, color=None, width=None, **kwargs): - super().__init__(style=style, color=color, width=width, **kwargs) - - @property - def style(self): - """Line style.""" - return self._style - - @style.setter - def style(self, val): - assert val is None or val in ALLOWED_LINESTYLES, ( - f"The `style` property of {type(self).__name__} must be one of " - f"{ALLOWED_LINESTYLES},\n" - f"but received {repr(val)} instead." - ) - self._style = val - - @property - def color(self): - """Line color.""" - return self._color - - @color.setter - def color(self, val): - self._color = color_validator(val) - - @property - def width(self): - """Positive number that defines the line width.""" - return self._width - - @width.setter - def width(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `width` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._width = val - - -class BaseStyle(MagicProperties): - """Base class for display styling options of `BaseGeo` objects. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - legend: dict or `Legend` object, default=None - Object legend properties when displayed in a plot. Legend has the `{label} ({description})` - format. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - """ - - def __init__( - self, - label=None, - description=None, - legend=None, - color=None, - opacity=None, - path=None, - model3d=None, - **kwargs, - ): - super().__init__( - label=label, - description=description, - legend=legend, - color=color, - opacity=opacity, - path=path, - model3d=model3d, - **kwargs, - ) - - @property - def label(self): - """Label of the class instance, e.g. to be displayed in the legend.""" - return self._label - - @label.setter - def label(self, val): - self._label = val if val is None else str(val) - - @property - def description(self): - """Description with 'text' and 'show' properties.""" - return self._description - - @description.setter - def description(self, val): - if isinstance(val, str): - self._description = Description(text=val) - else: - self._description = validate_property_class( - val, "description", Description, self - ) - - @property - def legend(self): - """Legend with 'show' property.""" - return self._legend - - @legend.setter - def legend(self, val): - if isinstance(val, str): - self._legend = Legend(text=val) - else: - self._legend = validate_property_class(val, "legend", Legend, self) - - @property - def color(self): - """A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""" - return self._color - - @color.setter - def color(self, val): - self._color = color_validator(val, parent_name=f"{type(self).__name__}") - - @property - def opacity(self): - """Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent.""" - return self._opacity - - @opacity.setter - def opacity(self, val): - assert val is None or (isinstance(val, (float, int)) and 0 <= val <= 1), ( - "The `opacity` property must be a value betwen 0 and 1,\n" - f"but received {repr(val)} instead." - ) - self._opacity = val - - @property - def path(self): - """An instance of `Path` or dictionary of equivalent key/value pairs, defining the - object path marker and path line properties.""" - return self._path - - @path.setter - def path(self, val): - self._path = validate_property_class(val, "path", Path, self) - - @property - def model3d(self): - """3d object representation properties.""" - return self._model3d - - @model3d.setter - def model3d(self, val): - self._model3d = validate_property_class(val, "model3d", Model3d, self) - - -class Description(MagicProperties): - """Defines properties for a description object. - - Parameters - ---------- - text: str, default=None - Object description text. - - show: bool, default=None - If True, adds legend entry based on value. - """ - - def __init__(self, text=None, show=None, **kwargs): - super().__init__(text=text, show=show, **kwargs) - - @property - def text(self): - """Description text.""" - return self._text - - @text.setter - def text(self, val): - assert val is None or isinstance(val, str), ( - f"The `show` property of {type(self).__name__} must be a string,\n" - f"but received {repr(val)} instead." - ) - self._text = val - - @property - def show(self): - """If True, adds legend entry suffix based on value.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False,\n" - f"but received {repr(val)} instead." - ) - self._show = val - - -class Legend(MagicProperties): - """Defines properties for a legend object. - - Parameters - ---------- - text: str, default=None - Object description text. - - show: bool, default=None - If True, adds legend entry based on value. - """ - - def __init__(self, show=None, **kwargs): - super().__init__(show=show, **kwargs) - - @property - def text(self): - """Legend text.""" - return self._text - - @text.setter - def text(self, val): - assert val is None or isinstance(val, str), ( - f"The `show` property of {type(self).__name__} must be a string,\n" - f"but received {repr(val)} instead." - ) - self._text = val - - @property - def show(self): - """If True, adds legend entry based on value.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False,\n" - f"but received {repr(val)} instead." - ) - self._show = val - - -class Model3d(MagicProperties): - """Defines properties for the 3d model representation of magpylib objects. - - Parameters - ---------- - showdefault: bool, default=True - Shows/hides default 3d-model. - - data: dict or list of dicts, default=None - A trace or list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - """ - - def __init__(self, showdefault=True, data=None, **kwargs): - super().__init__(showdefault=showdefault, data=data, **kwargs) - - @property - def showdefault(self): - """If True, show default model3d object representation, else hide it.""" - return self._showdefault - - @showdefault.setter - def showdefault(self, val): - assert isinstance(val, bool), ( - f"The `showdefault` property of {type(self).__name__} must be " - f"one of `[True, False]`,\n" - f"but received {repr(val)} instead." - ) - self._showdefault = val - - @property - def data(self): - """Data of 3d object representation (trace or list of traces).""" - return self._data - - @data.setter - def data(self, val): - self._data = self._validate_data(val) - - def _validate_data(self, traces, **kwargs): - if traces is None: - traces = [] - elif not isinstance(traces, (list, tuple)): - traces = [traces] - new_traces = [] - for trace in traces: - updatefunc = None - if not isinstance(trace, Trace3d) and callable(trace): - updatefunc = trace - trace = Trace3d() - if not isinstance(trace, Trace3d): - trace = validate_property_class(trace, "data", Trace3d, self) - if updatefunc is not None: - trace.updatefunc = updatefunc - trace = trace.update(kwargs) - new_traces.append(trace) - return new_traces - - def add_trace(self, trace=None, **kwargs): - """Adds user-defined 3d model object which is positioned relatively to the main object to be - displayed and moved automatically with it. This feature also allows the user to replace the - original 3d representation of the object. - - trace: Trace3d instance, dict or callable - Trace object. Can be a `Trace3d` instance or an dictionary with equivalent key/values - pairs, or a callable returning the equivalent dictionary. - - backend: str - Plotting backend corresponding to the trace. Can be one of - `['generic', 'matplotlib', 'plotly']`. - - constructor: str - Model constructor function or method to be called to build a 3D-model object - (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend. - - args: tuple, default=None - Tuple or callable returning a tuple containing positional arguments for building a - 3D-model object. - - kwargs: dict or callable, default=None - Dictionary or callable returning a dictionary containing the keys/values pairs for - building a 3D-model object. - - coordsargs: dict, default=None - Tells magpylib the name of the coordinate arrays to be moved or rotated, - by default: `{"x": "x", "y": "y", "z": "z"}`, if False, object is not rotated. - - show: bool, default=None - Shows/hides model3d object based on provided trace. - - scale: float, default=1 - Scaling factor by which the trace vertices coordinates are multiplied. - - updatefunc: callable, default=None - Callable object with no arguments. Should return a dictionary with keys from the - trace parameters. If provided, the function is called at `show` time and updates the - trace parameters with the output dictionary. This allows to update a trace dynamically - depending on class attributes, and postpone the trace construction to when the object is - displayed. - """ - self._data += self._validate_data([trace], **kwargs) - return self - - -class Trace3d(MagicProperties): - """Defines properties for an additional user-defined 3d model object which is positioned - relatively to the main object to be displayed and moved automatically with it. This feature - also allows the user to replace the original 3d representation of the object. - - Parameters - ---------- - backend: str - Plotting backend corresponding to the trace. Can be one of - `['generic', 'matplotlib', 'plotly']`. - - constructor: str - Model constructor function or method to be called to build a 3D-model object - (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend. - - args: tuple, default=None - Tuple or callable returning a tuple containing positional arguments for building a - 3D-model object. - - kwargs: dict or callable, default=None - Dictionary or callable returning a dictionary containing the keys/values pairs for - building a 3D-model object. - - coordsargs: dict, default=None - Tells magpylib the name of the coordinate arrays to be moved or rotated, - by default: `{"x": "x", "y": "y", "z": "z"}`, if False, object is not rotated. - - show: bool, default=True - Shows/hides model3d object based on provided trace. - - scale: float, default=1 - Scaling factor by which the trace vertices coordinates are multiplied. - - updatefunc: callable, default=None - Callable object with no arguments. Should return a dictionary with keys from the - trace parameters. If provided, the function is called at `show` time and updates the - trace parameters with the output dictionary. This allows to update a trace dynamically - depending on class attributes, and postpone the trace construction to when the object is - displayed. - """ - - def __init__( - self, - backend="generic", - constructor=None, - args=None, - kwargs=None, - coordsargs=None, - show=True, - scale=1, - updatefunc=None, - **params, - ): - super().__init__( - backend=backend, - constructor=constructor, - args=args, - kwargs=kwargs, - coordsargs=coordsargs, - show=show, - scale=scale, - updatefunc=updatefunc, - **params, - ) - - @property - def args(self): - """Tuple or callable returning a tuple containing positional arguments for building a - 3D-model object.""" - return self._args - - @args.setter - def args(self, val): - if val is not None: - test_val = val - if callable(val): - test_val = val() - assert isinstance(test_val, tuple), ( - "The `trace` input must be a dictionary or a callable returning a dictionary,\n" - f"but received {type(val).__name__} instead." - ) - self._args = val - - @property - def kwargs(self): - """Dictionary or callable returning a dictionary containing the keys/values pairs for - building a 3D-model object.""" - return self._kwargs - - @kwargs.setter - def kwargs(self, val): - if val is not None: - test_val = val - if callable(val): - test_val = val() - assert isinstance(test_val, dict), ( - "The `kwargs` input must be a dictionary or a callable returning a dictionary,\n" - f"but received {type(val).__name__} instead." - ) - self._kwargs = val - - @property - def constructor(self): - """Model constructor function or method to be called to build a 3D-model object - (e.g. 'plot_trisurf', 'Mesh3d). Must be in accordance with the given plotting backend. - """ - return self._constructor - - @constructor.setter - def constructor(self, val): - assert val is None or isinstance(val, str), ( - f"The `constructor` property of {type(self).__name__} must be a string," - f"\nbut received {repr(val)} instead." - ) - self._constructor = val - - @property - def show(self): - """If True, show default model3d object representation, else hide it.""" - return self._show - - @show.setter - def show(self, val): - assert isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be " - f"one of `[True, False]`,\n" - f"but received {repr(val)} instead." - ) - self._show = val - - @property - def scale(self): - """Scaling factor by which the trace vertices coordinates are multiplied.""" - return self._scale - - @scale.setter - def scale(self, val): - assert isinstance(val, (int, float)) and val > 0, ( - f"The `scale` property of {type(self).__name__} must be a strictly positive number,\n" - f"but received {repr(val)} instead." - ) - self._scale = val - - @property - def coordsargs(self): - """Tells magpylib the name of the coordinate arrays to be moved or rotated, - by default: `{"x": "x", "y": "y", "z": "z"}`, if False, object is not rotated. - """ - return self._coordsargs - - @coordsargs.setter - def coordsargs(self, val): - assert val is None or ( - isinstance(val, dict) and all(key in val for key in "xyz") - ), ( - f"The `coordsargs` property of {type(self).__name__} must be " - f"a dictionary with `'x', 'y', 'z'` keys,\n" - f"but received {repr(val)} instead." - ) - self._coordsargs = val - - @property - def backend(self): - """Plotting backend corresponding to the trace. Can be one of - `['generic', 'matplotlib', 'plotly']`.""" - return self._backend - - @backend.setter - def backend(self, val): - backends = ["generic"] + list(SUPPORTED_PLOTTING_BACKENDS) - assert val is None or val in backends, ( - f"The `backend` property of {type(self).__name__} must be one of" - f"{backends},\n" - f"but received {repr(val)} instead." - ) - self._backend = val - - @property - def updatefunc(self): - """Callable object with no arguments. Should return a dictionary with keys from the - trace parameters. If provided, the function is called at `show` time and updates the - trace parameters with the output dictionary. This allows to update a trace dynamically - depending on class attributes, and postpone the trace construction to when the object is - displayed.""" - return self._updatefunc - - @updatefunc.setter - def updatefunc(self, val): - if val is None: - - def val(): - return {} - - msg = "" - valid_props = list(self._property_names_generator()) - if not callable(val): - msg = f"Instead received {type(val)}" - else: - test_val = val() - if not isinstance(test_val, dict): - msg = f"but callable returned type {type(test_val)}." - else: - bad_keys = [k for k in test_val.keys() if k not in valid_props] - if bad_keys: - msg = f"but invalid output dictionary keys received: {bad_keys}." - - assert msg == "", ( - f"The `updatefunc` property of {type(self).__name__} must be a callable returning a " - f"dictionary with a subset of following keys: {valid_props} keys.\n" - f"{msg}" - ) - self._updatefunc = val - - -class Magnetization(MagicProperties): - """Defines magnetization styling properties. - - Parameters - ---------- - show : bool, default=None - If True show magnetization direction. - - color: dict or MagnetizationColor object, default=None - Color properties showing the magnetization direction (for the plotly backend). - Only applies if `show=True`. - - arrow: dict or Arrow object, default=None, - Arrow properties. Only applies if mode='arrow'. - - mode: {"auto", "arrow", "color", "arrow+color"}, default="auto" - Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means - that the chosen backend determines which mode is applied by its capability. If the backend - can display both and `auto` is chosen, the priority is given to `color`. - """ - - def __init__(self, show=None, size=None, color=None, mode=None, **kwargs): - super().__init__(show=show, size=size, color=color, mode=mode, **kwargs) - - @property - def show(self): - """If True, show magnetization direction.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - "The `show` input must be either True or False,\n" - f"but received {repr(val)} instead." - ) - self._show = val - - @property - def size(self): - """Deprecated (please use arrow.size): Arrow size property.""" - return self.arrow.size - - @size.setter - def size(self, val): - if val is not None: - self.arrow.size = val - - @property - def color(self): - """Color properties showing the magnetization direction (for the plotly backend). - Applies only if `show=True`. - """ - return self._color - - @color.setter - def color(self, val): - self._color = validate_property_class(val, "color", MagnetizationColor, self) - - @property - def arrow(self): - """`Arrow` object or dict with `show, size, width, style, color` properties/keys.""" - return self._arrow - - @arrow.setter - def arrow(self, val): - self._arrow = validate_property_class(val, "magnetization", Arrow, self) - - @property - def mode(self): - """One of {"auto", "arrow", "color", "arrow+color"}, default="auto" - Magnetization can be displayed via arrows, color or both. By default `mode='auto'` means - that the chosen backend determines which mode is applied by its capability. If the backend - can display both and `auto` is chosen, the priority is given to `color`.""" - return self._mode - - @mode.setter - def mode(self, val): - allowed = ("auto", "arrow", "color", "arrow+color", "color+arrow") - assert val is None or val in allowed, ( - f"The `mode` input must None or be one of `{allowed}`,\n" - f"but received {repr(val)} instead." - ) - self._mode = val - - -class MagnetizationColor(MagicProperties): - """Defines the magnetization direction color styling properties. (Only relevant for - the plotly backend) - - Parameters - ---------- - north: str, default=None - Defines the color of the magnetic north pole. - - south: str, default=None - Defines the color of the magnetic south pole. - - middle: str, default=None - Defines the color between the magnetic poles. - - transition: float, default=None - Sets the transition smoothness between poles colors. Can be any value - in-between 0 (discrete) and 1(smooth). - - mode: str, default=None - Sets the coloring mode for the magnetization. - - `'bicolor'`: Only north and south pole colors are shown. - - `'tricolor'`: Both pole colors and middle color are shown. - - `'tricycle'`: Both pole colors are shown and middle color is replaced by a color cycling - through the default color sequence. - """ - - _allowed_modes = ("bicolor", "tricolor", "tricycle") - - def __init__( - self, north=None, south=None, middle=None, transition=None, mode=None, **kwargs - ): - super().__init__( - north=north, - middle=middle, - south=south, - transition=transition, - mode=mode, - **kwargs, - ) - - @property - def north(self): - """Color of the magnetic north pole.""" - return self._north - - @north.setter - def north(self, val): - self._north = color_validator(val) - - @property - def south(self): - """Color of the magnetic south pole.""" - return self._south - - @south.setter - def south(self, val): - self._south = color_validator(val) - - @property - def middle(self): - """Color between the magnetic poles.""" - return self._middle - - @middle.setter - def middle(self, val): - self._middle = color_validator(val) - - @property - def transition(self): - """Sets the transition smoothness between poles colors. Can be any value - in-between 0 (discrete) and 1(smooth). - """ - return self._transition - - @transition.setter - def transition(self, val): - assert ( - val is None or isinstance(val, (float, int)) and 0 <= val <= 1 - ), "color transition must be a value between 0 and 1" - self._transition = val - - @property - def mode(self): - """Sets the coloring mode for the magnetization. - - `'bicolor'`: Only north and south pole colors are shown. - - `'tricolor'`: Both pole colors and middle color are shown. - - `'tricycle'`: Both pole colors are shown and middle color is replaced by a color cycling - through the default color sequence. - """ - return self._mode - - @mode.setter - def mode(self, val): - assert val is None or val in self._allowed_modes, ( - f"The `mode` property of {type(self).__name__} must be one of" - f"{list(self._allowed_modes)},\n" - f"but received {repr(val)} instead." - ) - self._mode = val - - -class MagnetProperties: - """Defines styling properties of homogeneous magnet classes. - - Parameters - ---------- - magnetization: dict or `Magnetization` object, default=None - `Magnetization` instance with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - """ - - @property - def magnetization(self): - """`Magnetization` instance with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - """ - return self._magnetization - - @magnetization.setter - def magnetization(self, val): - self._magnetization = validate_property_class( - val, "magnetization", Magnetization, self - ) - - -class DefaultMagnet(MagicProperties, MagnetProperties): - """Defines styling properties of homogeneous magnet classes. - - Parameters - ---------- - magnetization: dict or Magnetization, default=None - """ - - def __init__(self, magnetization=None, **kwargs): - super().__init__(magnetization=magnetization, **kwargs) - - -class MagnetStyle(BaseStyle, MagnetProperties): - """Defines styling properties of homogeneous magnet classes. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - - magnetization: dict or Magnetization, default=None - Magnetization styling with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - -class MarkerLineProperties: - """Defines styling properties of Markers and Lines.""" - - @property - def show(self): - """Show/hide path. - - False: Shows object(s) at final path position and hides paths lines and markers. - - True: Shows object(s) shows object paths depending on `line`, `marker` and `frames` - parameters. - """ - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False,\n" - f"but received {repr(val)} instead." - ) - self._show = val - - @property - def marker(self): - """`Markers` object with 'color', 'symbol', 'size' properties.""" - return self._marker - - @marker.setter - def marker(self, val): - self._marker = validate_property_class(val, "marker", Marker, self) - - @property - def line(self): - """`Line` object with 'color', 'type', 'width' properties.""" - return self._line - - @line.setter - def line(self, val): - self._line = validate_property_class(val, "line", Line, self) - - -class GridMesh(MagicProperties, MarkerLineProperties): - """Defines styling properties of GridMesh objects - - Parameters - ---------- - show: bool, default=None - Show/hide Lines and Markers - - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - - line: dict or `Line` object, default=None - `Line` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - """ - - -class OpenMesh(MagicProperties, MarkerLineProperties): - """Defines styling properties of OpenMesh objects - - Parameters - ---------- - show: bool, default=None - Show/hide Lines and Markers - - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - - line: dict or `Line` object, default=None - `Line` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - """ - - -class DisconnectedMesh(MagicProperties, MarkerLineProperties): - """Defines styling properties of DisconnectedMesh objects - - Parameters - ---------- - show: bool, default=None - Show/hide Lines and Markers - - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - - line: dict or `Line` object, default=None - `Line` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - - colorsequence: iterable, default=["red", "blue", "green", "cyan", "magenta", "yellow"] - An iterable of color values used to cycle trough for every disconnected part of - disconnected triangular mesh object. - A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color - """ - - @property - def colorsequence(self): - """An iterable of color values used to cycle trough for every disconnected part of - disconnected triangular mesh object. - A color may be specified by - - a hex string (e.g. '#ff0000') - - an rgb/rgba string (e.g. 'rgb(255,0,0)') - - an hsl/hsla string (e.g. 'hsl(0,100%,50%)') - - an hsv/hsva string (e.g. 'hsv(0,100%,100%)') - - a named CSS color""" - return self._colorsequence - - @colorsequence.setter - def colorsequence(self, val): - if val is not None: - name = type(self).__name__ - try: - val = tuple( - color_validator(c, allow_None=False, parent_name=f"{name}") - for c in val - ) - except TypeError as err: - raise ValueError( - f"The `colorsequence` property of {name} must be an " - f"iterable of colors but received {val!r} instead" - ) from err - - self._colorsequence = val - - -class SelfIntersectingMesh(MagicProperties, MarkerLineProperties): - """Defines styling properties of SelfIntersectingMesh objects - - Parameters - ---------- - show: bool, default=None - Show/hide Lines and Markers - - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - - line: dict or `Line` object, default=None - `Line` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - """ - - -class TriMesh(MagicProperties): - """Defines TriMesh mesh properties. - - Parameters - ---------- - grid: dict or GridMesh, default=None - All mesh vertices and edges of a TriangularMesh object. - - open: dict or OpenMesh, default=None - Shows open mesh vertices and edges of a TriangularMesh object, if any. - - disconnected: dict or DisconnectedMesh, default=None - Shows disconnected bodies of a TriangularMesh object, if any. - - selfintersecting: dict or SelfIntersectingMesh, default=None - Shows self-intersecting triangles of a TriangularMesh object, if any. - """ - - @property - def grid(self): - """GridMesh` instance with `'show'` property - or a dictionary with equivalent key/value pairs. - """ - return self._grid - - @grid.setter - def grid(self, val): - self._grid = validate_property_class(val, "grid", GridMesh, self) - - @property - def open(self): - """OpenMesh` instance with `'show'` property - or a dictionary with equivalent key/value pairs. - """ - return self._open - - @open.setter - def open(self, val): - self._open = validate_property_class(val, "open", OpenMesh, self) - - @property - def disconnected(self): - """`DisconnectedMesh` instance with `'show'` property - or a dictionary with equivalent key/value pairs. - """ - return self._disconnected - - @disconnected.setter - def disconnected(self, val): - self._disconnected = validate_property_class( - val, "disconnected", DisconnectedMesh, self - ) - - @property - def selfintersecting(self): - """`SelfIntersectingMesh` instance with `'show'` property - or a dictionary with equivalent key/value pairs. - """ - return self._selfintersecting - - @selfintersecting.setter - def selfintersecting(self, val): - self._selfintersecting = validate_property_class( - val, "selfintersecting", SelfIntersectingMesh, self - ) - - -class Orientation(MagicProperties): - """Defines Triangle orientation properties. - - Parameters - ---------- - show: bool, default=True - Show/hide orientation symbol. - - size: float, default=1, - Size of the orientation symbol - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - offset: float, default=0.1 - Defines the orientation symbol offset, normal to the triangle surface. Must be a number - between [0,1], 0 resulting in the cone/arrow head to be coincident to the triangle surface - and 1 with the base. - - symbol: {"cone", "arrow3d"}: - Orientation symbol for the triangular faces. - """ - - _allowed_symbols = ("cone", "arrow3d") - - @property - def show(self): - """Show/hide arrow.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False,\n" - f"but received {repr(val)} instead." - ) - self._show = val - - @property - def size(self): - """Positive float for ratio of sensor to canvas size.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `size` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._size = val - - @property - def color(self): - """A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""" - return self._color - - @color.setter - def color(self, val): - self._color = color_validator(val, parent_name=f"{type(self).__name__}") - - @property - def offset(self): - """Defines the orientation symbol offset, normal to the triangle surface. `offset=0` results - in the cone/arrow head to be coincident to the triangle surface and `offset=1` with the - base. - """ - return self._offset - - @offset.setter - def offset(self, val): - assert val is None or (isinstance(val, (float, int))), ( - "The `offset` property must valid number\n" - f"but received {repr(val)} instead." - ) - self._offset = val - - @property - def symbol(self): - """Pixel symbol. Can be one of `("cone", "arrow3d")`.""" - return self._symbol - - @symbol.setter - def symbol(self, val): - assert val is None or val in self._allowed_symbols, ( - f"The `symbol` property of {type(self).__name__} must be one of" - f"{self._allowed_symbols},\n" - f"but received {repr(val)} instead." - ) - self._symbol = val - - -class TriangleProperties: - """Defines Triangle properties. - - Parameters - ---------- - orientation: dict or Orientation, default=None, - Orientation styling of triangles. - """ - - @property - def orientation(self): - """`Orientation` instance with `'show'` property - or a dictionary with equivalent key/value pairs. - """ - return self._orientation - - @orientation.setter - def orientation(self, val): - self._orientation = validate_property_class( - val, "orientation", Orientation, self - ) - - -class DefaultTriangle(MagicProperties, MagnetProperties, TriangleProperties): - """Defines styling properties of the Triangle class. - - Parameters - ---------- - magnetization: dict or Magnetization, default=None - Magnetization styling with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - - orientation: dict or Orientation, default=None, - Orientation of triangles styling with `'show'`, `'size'`, `'color', `'pivot'`, `'symbol'`` - properties or a dictionary with equivalent key/value pairs.. - """ - - def __init__(self, magnetization=None, orientation=None, **kwargs): - super().__init__(magnetization=magnetization, orientation=orientation, **kwargs) - - -class TriangleStyle(MagnetStyle, TriangleProperties): - """Defines styling properties of the Triangle class. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - - magnetization: dict or Magnetization, default=None - Magnetization styling with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - - orientation: dict or Orientation, default=None, - Orientation styling of triangles. - """ - - def __init__(self, orientation=None, **kwargs): - super().__init__(orientation=orientation, **kwargs) - - -class TriangularMeshProperties: - """Defines TriangularMesh properties.""" - - @property - def mesh(self): - """`TriMesh` instance with `'show', 'markers', 'line'` properties - or a dictionary with equivalent key/value pairs. - """ - return self._mesh - - @mesh.setter - def mesh(self, val): - self._mesh = validate_property_class(val, "mesh", TriMesh, self) - - -class DefaultTriangularMesh( - MagicProperties, MagnetProperties, TriangleProperties, TriangularMeshProperties -): - """Defines styling properties of homogeneous TriangularMesh magnet classes. - - Parameters - ---------- - magnetization: dict or Magnetization, default=None - Magnetization styling with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - - orientation: dict or Orientation, default=None - Orientation of triangles styling with `'show'`, `'size'`, `'color', `'pivot'`, `'symbol'`` - properties or a dictionary with equivalent key/value pairs. - - mesh: dict or TriMesh, default=None - TriMesh styling properties (e.g. `'grid', 'open', 'disconnected'`) - """ - - def __init__(self, magnetization=None, orientation=None, mesh=None, **kwargs): - super().__init__( - magnetization=magnetization, orientation=orientation, mesh=mesh, **kwargs - ) - - -class TriangularMeshStyle(MagnetStyle, TriangleProperties, TriangularMeshProperties): - """Defines styling properties of the TriangularMesh magnet class. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - - magnetization: dict or Magnetization, default=None - Magnetization styling with `'show'`, `'size'`, `'color'` properties - or a dictionary with equivalent key/value pairs. - - orientation: dict or Orientation, default=None, - Orientation styling of triangles. - - mesh: dict or TriMesh, default=None, - mesh styling of triangles. - """ - - def __init__(self, orientation=None, **kwargs): - super().__init__(orientation=orientation, **kwargs) - - -class ArrowCS(MagicProperties): - """Defines triple coordinate system arrow properties. - - Parameters - ---------- - x: dict or `ArrowSingle` object, default=None - x-direction `Arrowsingle` object or dict with equivalent key/value pairs - (e.g. `color`, `show`). - - y: dict or `ArrowSingle` object, default=None - y-direction `Arrowsingle` object or dict with equivalent key/value pairs - (e.g. `color`, `show`). - - z: dict or `ArrowSingle` object, default=None - z-direction `Arrowsingle` object or dict with equivalent key/value pairs - (e.g. `color`, `show`). - """ - - def __init__(self, x=None, y=None, z=None): - super().__init__(x=x, y=y, z=z) - - @property - def x(self): - """ - `ArrowSingle` object or dict with equivalent key/value pairs (e.g. `color`, `show`). - """ - return self._x - - @x.setter - def x(self, val): - self._x = validate_property_class(val, "x", ArrowSingle, self) - - @property - def y(self): - """ - `ArrowSingle` object or dict with equivalent key/value pairs (e.g. `color`, `show`). - """ - return self._y - - @y.setter - def y(self, val): - self._y = validate_property_class(val, "y", ArrowSingle, self) - - @property - def z(self): - """ - `ArrowSingle` object or dict with equivalent key/value pairs (e.g. `color`, `show`). - """ - return self._z - - @z.setter - def z(self, val): - self._z = validate_property_class(val, "z", ArrowSingle, self) - - -class ArrowSingle(MagicProperties): - """Single coordinate system arrow properties. - - Parameters - ---------- - show: bool, default=True - Show/hide arrow. - - color: color, default=None - Valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - """ - - def __init__(self, show=True, color=None): - super().__init__(show=show, color=color) - - @property - def show(self): - """Show/hide arrow.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False,\n" - f"but received {repr(val)} instead." - ) - self._show = val - - @property - def color(self): - """A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`.""" - return self._color - - @color.setter - def color(self, val): - self._color = color_validator(val, parent_name=f"{type(self).__name__}") - - -class SensorProperties: - """Defines the specific styling properties of the Sensor class. - - Parameters - ---------- - size: float, default=None - Positive float for ratio of sensor to canvas size. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the sensor size. If 'absolute', the `size` parameters - becomes the sensor size in meters. - - pixel: dict, Pixel, default=None - `Pixel` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - - arrows: dict, ArrowCS, default=None - `ArrowCS` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - """ - - @property - def size(self): - """Positive float for ratio of sensor to canvas size.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `size` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._size = val - - @property - def sizemode(self): - """Sizemode of the sensor.""" - return self._sizemode - - @sizemode.setter - def sizemode(self, val): - assert val is None or val in ALLOWED_SIZEMODES, ( - f"The `sizemode` property of {type(self).__name__} must be a one of " - f"{ALLOWED_SIZEMODES},\nbut received {repr(val)} instead." - ) - self._sizemode = val - - @property - def pixel(self): - """`Pixel` object or dict with equivalent key/value pairs (e.g. `color`, `size`).""" - return self._pixel - - @pixel.setter - def pixel(self, val): - self._pixel = validate_property_class(val, "pixel", Pixel, self) - - @property - def arrows(self): - """`ArrowCS` object or dict with equivalent key/value pairs (e.g. `color`, `size`).""" - return self._arrows - - @arrows.setter - def arrows(self, val): - self._arrows = validate_property_class(val, "arrows", ArrowCS, self) - - -class DefaultSensor(MagicProperties, SensorProperties): - """Defines styling properties of the Sensor class. - - Parameters - ---------- - size: float, default=None - Positive float for ratio of sensor to canvas size. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the sensor size. If 'absolute', the `size` parameters - becomes the sensor size in meters. - - pixel: dict, Pixel, default=None - `Pixel` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - - arrows: dict, ArrowCS, default=None - `ArrowCS` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - """ - - def __init__( - self, - size=None, - sizemode=None, - pixel=None, - arrows=None, - **kwargs, - ): - super().__init__( - size=size, - sizemode=sizemode, - pixel=pixel, - arrows=arrows, - **kwargs, - ) - - -class SensorStyle(BaseStyle, SensorProperties): - """Defines the styling properties of the Sensor class. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - - size: float, default=None - Positive float for ratio of sensor size to canvas size. - - pixel: dict, Pixel, default=None - `Pixel` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - - arrows: dict, ArrowCS, default=None - `ArrowCS` object or dict with equivalent key/value pairs (e.g. `color`, `size`). - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - -class Pixel(MagicProperties): - """Defines the styling properties of sensor pixels. - - Parameters - ---------- - size: float, default=1 - Positive float for relative pixel size. - - matplotlib backend: Pixel size is the marker size. - - plotly backend: Relative distance to nearest neighbor pixel. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the pixel size. If 'absolute', the `size` parameters - becomes the pixel size in meters. - - color: str, default=None - Defines the pixel color@property. - - symbol: str, default=None - Pixel symbol. Can be one of `['.', 'o', '+', 'D', 'd', 's', 'x']`. - Only applies for matplotlib plotting backend. - """ - - def __init__(self, size=1, sizemode=None, color=None, symbol=None, **kwargs): - super().__init__( - size=size, - sizemode=sizemode, - color=color, - symbol=symbol, - **kwargs, - ) - - @property - def size(self): - """Positive float for relative pixel size. - - matplotlib backend: Pixel size is the marker size. - - plotly backend: Relative distance to nearest neighbor pixel.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"the `size` property of {type(self).__name__} must be a positive number" - f"but received {repr(val)} instead." - ) - self._size = val - - @property - def sizemode(self): - """Sizemode of the pixel.""" - return self._sizemode - - @sizemode.setter - def sizemode(self, val): - assert val is None or val in ALLOWED_SIZEMODES, ( - f"The `sizemode` property of {type(self).__name__} must be a one of " - f"{ALLOWED_SIZEMODES},\nbut received {repr(val)} instead." - ) - self._sizemode = val - - @property - def color(self): - """Pixel color.""" - return self._color - - @color.setter - def color(self, val): - self._color = color_validator(val, parent_name=f"{type(self).__name__}") - - @property - def symbol(self): - """Pixel symbol. Can be one of `['.', 'o', '+', 'D', 'd', 's', 'x']`.""" - return self._symbol - - @symbol.setter - def symbol(self, val): - assert val is None or val in ALLOWED_SYMBOLS, ( - f"The `symbol` property of {type(self).__name__} must be one of" - f"{ALLOWED_SYMBOLS},\n" - f"but received {repr(val)} instead." - ) - self._symbol = val - - -class CurrentProperties: - """Defines styling properties of line current classes. - - Parameters - ---------- - arrow: dict or `Arrow` object, default=None - `Arrow` object or dict with `show, size, width, style, color` properties/keys. - - line: dict or `Line` object, default=None - `Line` object or dict with `show, width, style, color` properties/keys. - """ - - @property - def arrow(self): - """`Arrow` object or dict with `show, size, width, style, color` properties/keys.""" - return self._arrow - - @arrow.setter - def arrow(self, val): - self._arrow = validate_property_class(val, "current", Arrow, self) - - @property - def line(self): - """`Line` object or dict with `show, width, style, color` properties/keys.""" - return self._line - - @line.setter - def line(self, val): - self._line = validate_property_class(val, "line", CurrentLine, self) - - -class DefaultCurrent(MagicProperties, CurrentProperties): - """Defines the specific styling properties of line current classes. - - Parameters - ---------- - arrow: dict or `Arrow`object, default=None - `Arrow` object or dict with 'show', 'size' properties/keys. - """ - - def __init__(self, arrow=None, **kwargs): - super().__init__(arrow=arrow, **kwargs) - - -class CurrentStyle(BaseStyle, CurrentProperties): - """Defines styling properties of line current classes. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - - arrow: dict or `Arrow` object, default=None - `Arrow` object or dict with `'show'`, `'size'` properties/keys. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - -class Arrow(Line): - """Defines styling properties of current arrows. - - Parameters - ---------- - show: bool, default=None - Show/Hide arrow - - size: float, default=None - Positive number defining the size of the arrows. Effective value depends on the - `sizemode` parameter. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the arrow size. If 'absolute', the `size` parameters - becomes the arrow length in meters. - - offset: float, default=0.5 - Defines the arrow offset. `offset=0` results in the arrow head to be coincident to start - of the line, and `offset=1` with the end. - - style: str, default=None - Can be one of: - `['solid', '-', 'dashed', '--', 'dashdot', '-.', 'dotted', '.', (0, (1, 1)), - 'loosely dotted', 'loosely dashdotted']` - - color: str, default=None - Line color. - - width: float, default=None - Positive number that defines the line width. - """ - - def __init__(self, show=None, size=None, **kwargs): - super().__init__(show=show, size=size, **kwargs) - - @property - def show(self): - """Show/hide arrow showing current direction.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False," - f"but received {repr(val)} instead." - ) - self._show = val - - @property - def size(self): - """Positive number defining the size of the arrows.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `size` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._size = val - - @property - def sizemode(self): - """Sizemode of the arrows.""" - return self._sizemode - - @sizemode.setter - def sizemode(self, val): - assert val is None or val in ALLOWED_SIZEMODES, ( - f"The `sizemode` property of {type(self).__name__} must be a one of " - f"{ALLOWED_SIZEMODES},\nbut received {repr(val)} instead." - ) - self._sizemode = val - - @property - def offset(self): - """Defines the arrow offset. `offset=0` results in the arrow head to be coincident to start - of the line, and `offset=1` with the end. - """ - return self._offset - - @offset.setter - def offset(self, val): - assert val is None or (isinstance(val, (float, int))) and 0 <= val <= 1, ( - "The `offset` property must valid number between 0 and 1\n" - f"but received {repr(val)} instead." - ) - self._offset = val - - -class CurrentLine(Line): - """Defines styling properties of current lines. - - Parameters - ---------- - show: bool, default=None - Show/Hide arrow - - style: str, default=None - Can be one of: - `['solid', '-', 'dashed', '--', 'dashdot', '-.', 'dotted', '.', (0, (1, 1)), - 'loosely dotted', 'loosely dashdotted']` - - color: str, default=None - Line color. - - width: float, default=None - Positive number that defines the line width. - """ - - @property - def show(self): - """Show/hide current line.""" - return self._show - - @show.setter - def show(self, val): - assert val is None or isinstance(val, bool), ( - f"The `show` property of {type(self).__name__} must be either True or False," - f"but received {repr(val)} instead." - ) - self._show = val - - -class Marker(MagicProperties): - """Defines styling properties of plot markers. - - Parameters - ---------- - size: float, default=None - Marker size. - color: str, default=None - Marker color. - symbol: str, default=None - Marker symbol. Can be one of `['.', 'o', '+', 'D', 'd', 's', 'x']`. - """ - - def __init__(self, size=None, color=None, symbol=None, **kwargs): - super().__init__(size=size, color=color, symbol=symbol, **kwargs) - - @property - def size(self): - """Marker size.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `size` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._size = val - - @property - def color(self): - """Marker color.""" - return self._color - - @color.setter - def color(self, val): - self._color = color_validator(val) - - @property - def symbol(self): - """Marker symbol. Can be one of `['.', 'o', '+', 'D', 'd', 's', 'x']`.""" - return self._symbol - - @symbol.setter - def symbol(self, val): - assert val is None or val in ALLOWED_SYMBOLS, ( - f"The `symbol` property of {type(self).__name__} must be one of" - f"{ALLOWED_SYMBOLS},\n" - f"but received {repr(val)} instead." - ) - self._symbol = val - - -class DefaultMarkers(BaseStyle): - """Defines styling properties of the markers trace. - - Parameters - ---------- - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - """ - - def __init__(self, marker=None, **kwargs): - super().__init__(marker=marker, **kwargs) - - @property - def marker(self): - """`Markers` object with 'color', 'symbol', 'size' properties.""" - return self._marker - - @marker.setter - def marker(self, val): - self._marker = validate_property_class(val, "marker", Marker, self) - - -class DipoleProperties: - """Defines styling properties of dipoles. - - Parameters - ---------- - size: float - Positive value for ratio of dipole size to canvas size. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the dipole size. If 'absolute', the `size` parameters - becomes the dipole size in meters. - - pivot: str - The part of the arrow that is anchored to the X, Y grid. - The arrow rotates about this point. Can be one of `['tail', 'middle', 'tip']`. - """ - - _allowed_pivots = ("tail", "middle", "tip") - - @property - def size(self): - """Positive value for ratio of dipole size to canvas size.""" - return self._size - - @size.setter - def size(self, val): - assert val is None or isinstance(val, (int, float)) and val >= 0, ( - f"The `size` property of {type(self).__name__} must be a positive number,\n" - f"but received {repr(val)} instead." - ) - self._size = val - - @property - def sizemode(self): - """Sizemode of the dipole.""" - return self._sizemode - - @sizemode.setter - def sizemode(self, val): - assert val is None or val in ALLOWED_SIZEMODES, ( - f"The `sizemode` property of {type(self).__name__} must be a one of " - f"{ALLOWED_SIZEMODES},\nbut received {repr(val)} instead." - ) - self._sizemode = val - - @property - def pivot(self): - """The part of the arrow that is anchored to the X, Y grid. - The arrow rotates about this point. Can be one of `['tail', 'middle', 'tip']`. - """ - return self._pivot - - @pivot.setter - def pivot(self, val): - assert val is None or val in (self._allowed_pivots), ( - f"The `pivot` property of {type(self).__name__} must be one of " - f"{self._allowed_pivots},\n" - f"but received {repr(val)} instead." - ) - self._pivot = val - - -class DefaultDipole(MagicProperties, DipoleProperties): - """ - Defines styling properties of dipoles. - - Parameters - ---------- - size: float, default=None - Positive float for ratio of dipole size to canvas size. - - sizemode: {'scaled', 'absolute'}, default='scaled' - Defines the scale reference for the dipole size. If 'absolute', the `size` parameters - becomes the dipole size in meters. - - pivot: str, default=None - The part of the arrow that is anchored to the X, Y grid. - The arrow rotates about this point. Can be one of `['tail', 'middle', 'tip']`. - """ - - def __init__(self, size=None, sizemode=None, pivot=None, **kwargs): - super().__init__(size=size, sizemode=sizemode, pivot=pivot, **kwargs) - - -class DipoleStyle(BaseStyle, DipoleProperties): - """Defines the styling properties of dipole objects. - - Parameters - ---------- - label: str, default=None - Label of the class instance, e.g. to be displayed in the legend. - - description: dict or `Description` object, default=None - Object description properties. - - color: str, default=None - A valid css color. Can also be one of `['r', 'g', 'b', 'y', 'm', 'c', 'k', 'w']`. - - opacity: float, default=None - Object opacity between 0 and 1, where 1 is fully opaque and 0 is fully transparent. - - path: dict or `Path` object, default=None - An instance of `Path` or dictionary of equivalent key/value pairs, defining the object - path marker and path line properties. - - model3d: list of `Trace3d` objects, default=None - A list of traces where each is an instance of `Trace3d` or dictionary of equivalent - key/value pairs. Defines properties for an additional user-defined model3d object which is - positioned relatively to the main object to be displayed and moved automatically with it. - This feature also allows the user to replace the original 3d representation of the object. - - size: float, default=None - Positive float for ratio of dipole size to canvas size. - - pivot: str, default=None - The part of the arrow that is anchored to the X, Y grid. - The arrow rotates about this point. Can be one of `['tail', 'middle', 'tip']`. - """ - - def __init__(self, **kwargs): - super().__init__(**kwargs) - - -class Path(MagicProperties, MarkerLineProperties): - """Defines styling properties of an object's path. - - Parameters - ---------- - show: bool, default=None - Show/hide path. - - False: Shows object(s) at final path position and hides paths lines and markers. - - True: Shows object(s) shows object paths depending on `line`, `marker` and `frames` - parameters. - - marker: dict or `Markers` object, default=None - `Markers` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - - line: dict or `Line` object, default=None - `Line` object with 'color', 'symbol', 'size' properties, or dictionary with equivalent - key/value pairs. - - frames: int or array_like, shape (n,), default=None - Show copies of the 3D-model along the given path indices. - - integer i: Displays the object(s) at every i'th path position. - - array_like, shape (n,), dtype=int: Displays object(s) at given path indices. - - numbering: bool, default=False - Show/hide numbering on path positions. - """ - - def __init__( - self, show=None, marker=None, line=None, frames=None, numbering=None, **kwargs - ): - super().__init__( - show=show, - marker=marker, - line=line, - frames=frames, - numbering=numbering, - **kwargs, - ) - - @property - def frames(self): - """Show copies of the 3D-model along the given path indices. - - integer i: Displays the object(s) at every i'th path position. - - array_like shape (n,) of integers: Displays object(s) at given path indices. - """ - return self._frames - - @frames.setter - def frames(self, val): - is_valid_path = True - if hasattr(val, "__iter__") and not isinstance(val, str): - val = tuple(val) - if not all(np.issubdtype(type(v), int) for v in val): - is_valid_path = False - elif not (val is None or np.issubdtype(type(val), int)): - is_valid_path = False - assert is_valid_path, f"""The `frames` property of {type(self).__name__} must be either: -- integer i: Displays the object(s) at every i'th path position. -- array_like, shape (n,), dtype=int: Displays object(s) at given path indices. -but received {repr(val)} instead""" - self._frames = val - - @property - def numbering(self): - """Show/hide numbering on path positions. Only applies if show=True.""" - return self._numbering - - @numbering.setter - def numbering(self, val): - assert val is None or isinstance(val, bool), ( - f"The `numbering` property of {type(self).__name__} must be one of (True, False),\n" - f"but received {repr(val)} instead." - ) - self._numbering = val - - -class DisplayStyle(MagicProperties): - """Base class containing styling properties for all object families. The properties of the - sub-classes are set to hard coded defaults at class instantiation. - - Parameters - ---------- - base: dict or `Base` object, default=None - Base properties common to all families. - - magnet: dict or `Magnet` object, default=None - Magnet properties. - - current: dict or `Current` object, default=None - Current properties. - - dipole: dict or `Dipole` object, default=None - Dipole properties. - - triangle: dict or `Triangle` object, default=None - Triangle properties - - sensor: dict or `Sensor` object, default=None - Sensor properties. - - markers: dict or `Markers` object, default=None - Markers properties. - """ - - def __init__( - self, - base=None, - magnet=None, - current=None, - dipole=None, - triangle=None, - sensor=None, - markers=None, - **kwargs, - ): - super().__init__( - base=base, - magnet=magnet, - current=current, - dipole=dipole, - triangle=triangle, - sensor=sensor, - markers=markers, - **kwargs, - ) - # self.reset() - - def reset(self): - """Resets all nested properties to their hard coded default values.""" - self.update(get_defaults_dict("display.style"), _match_properties=False) - return self - - @property - def base(self): - """Base properties common to all families.""" - return self._base - - @base.setter - def base(self, val): - self._base = validate_property_class(val, "base", BaseStyle, self) - - @property - def magnet(self): - """Magnet default style class.""" - return self._magnet - - @magnet.setter - def magnet(self, val): - self._magnet = validate_property_class(val, "magnet", DefaultMagnet, self) - - @property - def triangularmesh(self): - """TriangularMesh default style class.""" - return self._triangularmesh - - @triangularmesh.setter - def triangularmesh(self, val): - self._triangularmesh = validate_property_class( - val, "triangularmesh", DefaultTriangularMesh, self - ) - - @property - def current(self): - """Current default style class.""" - return self._current - - @current.setter - def current(self, val): - self._current = validate_property_class(val, "current", DefaultCurrent, self) - - @property - def dipole(self): - """Dipole default style class.""" - return self._dipole - - @dipole.setter - def dipole(self, val): - self._dipole = validate_property_class(val, "dipole", DefaultDipole, self) - - @property - def triangle(self): - """Triangle default style class.""" - return self._triangle - - @triangle.setter - def triangle(self, val): - self._triangle = validate_property_class(val, "triangle", DefaultTriangle, self) - - @property - def sensor(self): - """Sensor default style class.""" - return self._sensor - - @sensor.setter - def sensor(self, val): - self._sensor = validate_property_class(val, "sensor", DefaultSensor, self) - - @property - def markers(self): - """Markers default style class.""" - return self._markers - - @markers.setter - def markers(self, val): - self._markers = validate_property_class(val, "markers", DefaultMarkers, self) diff --git a/magpylib/_src/utility.py b/magpylib/_src/utility.py index d62d377c1..1dc514d02 100644 --- a/magpylib/_src/utility.py +++ b/magpylib/_src/utility.py @@ -10,6 +10,7 @@ from typing import Sequence import numpy as np +from scipy.constants import mu_0 as MU0 from magpylib._src.exceptions import MagpylibBadUserInput @@ -282,7 +283,7 @@ def add_iteration_suffix(name): m = re.search(r"\d+$", name) n = "00" endstr = None - midchar = "_" if name[-1] != "_" else "" + midchar = "" if name.endswith("_") else "_" if m is not None: midchar = "" n = m.group() diff --git a/magpylib/graphics/__init__.py b/magpylib/graphics/__init__.py index 9963f8f39..7c1f10e5d 100644 --- a/magpylib/graphics/__init__.py +++ b/magpylib/graphics/__init__.py @@ -5,6 +5,6 @@ __all__ = ["model3d", "style", "Trace3d"] -from magpylib._src.style import Trace3d +from magpylib._src.defaults.defaults_classes import Trace3d from magpylib.graphics import model3d from magpylib.graphics import style diff --git a/magpylib/graphics/style/__init__.py b/magpylib/graphics/style/__init__.py index 5febd7c8d..d6bf6efc0 100644 --- a/magpylib/graphics/style/__init__.py +++ b/magpylib/graphics/style/__init__.py @@ -9,7 +9,7 @@ "SensorStyle", ] -from magpylib._src.style import CurrentStyle -from magpylib._src.style import DipoleStyle -from magpylib._src.style import MagnetStyle -from magpylib._src.style import SensorStyle +from magpylib._src.defaults.defaults_classes import CurrentStyle +from magpylib._src.defaults.defaults_classes import DipoleStyle +from magpylib._src.defaults.defaults_classes import MagnetStyle +from magpylib._src.defaults.defaults_classes import SensorStyle diff --git a/pyproject.toml b/pyproject.toml index 370f1e127..5877d2ef6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ dependencies = [ "scipy>=1.7", "matplotlib>=3.3", "plotly>=5.3", + "param>2.0", ] requires-python = ">=3.8" authors = [ diff --git a/tests/test_default_utils.py b/tests/test_default_utils.py index 84864ea7c..5624e4e71 100644 --- a/tests/test_default_utils.py +++ b/tests/test_default_utils.py @@ -1,9 +1,10 @@ from copy import deepcopy +import param import pytest from magpylib._src.defaults.defaults_utility import COLORS_SHORT_TO_LONG -from magpylib._src.defaults.defaults_utility import MagicProperties +from magpylib._src.defaults.defaults_utility import MagicParameterized from magpylib._src.defaults.defaults_utility import color_validator from magpylib._src.defaults.defaults_utility import get_defaults_dict from magpylib._src.defaults.defaults_utility import linearize_dict @@ -138,32 +139,18 @@ def test_bad_colors(color, allow_None, expected_exception): color_validator(color, allow_None=allow_None) -def test_MagicProperties(): - """test MagicProperties class""" +def test_MagicParameterized(): + """test MagicParameterized class""" - class BPsub1(MagicProperties): - "MagicProperties class" + class BPsub1(MagicParameterized): + "MagicParameterized class" - @property - def prop1(self): - """prop1""" - return self._prop1 + prop1 = param.Parameter() - @prop1.setter - def prop1(self, val): - self._prop1 = val + class BPsub2(MagicParameterized): + "MagicParameterized class" - class BPsub2(MagicProperties): - "MagicProperties class" - - @property - def prop2(self): - """prop2""" - return self._prop2 - - @prop2.setter - def prop2(self, val): - self._prop2 = val + prop2 = param.Parameter() bp1 = BPsub1(prop1=1) @@ -187,16 +174,11 @@ def prop2(self, val): with pytest.raises(AttributeError): bp1.update(prop1_prop2=10, prop3=4) - assert bp1.update(prop1_prop2=10, prop3=4, _match_properties=False).as_dict() == { + assert bp1.update(prop1_prop2=10, prop3=4, match_properties=False).as_dict() == { "prop1": {"prop2": 10} }, "magic property setting failed, should ignore `'prop3'`" - assert bp1.update(prop1_prop2=20, _replace_None_only=True).as_dict() == { - "prop1": {"prop2": 10} - }, "magic property setting failed, `prop2` should be remained unchanged `10`" - # check copy method - bp3 = bp2.copy() assert bp3 is not bp2, "failed copying, should return a different id" assert ( @@ -212,12 +194,35 @@ def prop2(self, val): with pytest.raises(AttributeError): BPsub1(a=0) # `a` is not a property in the class - # check repr - assert repr(MagicProperties()) == "MagicProperties()", "repr failed" - def test_get_defaults_dict(): """test get_defaults_dict""" s0 = get_defaults_dict("display.style") s1 = get_defaults_dict()["display"]["style"] assert s0 == s1, "dicts don't match" + + +def test_settings_precedence(): + magpy.defaults.reset() + mag_col_default = magpy.defaults.display.style.magnet.magnetization.color + c1 = magpy.magnet.Cuboid(polarization=(0, 0, 1), dimension=(1, 1, 1)) + + # assigning a dict + c1.style.magnetization = {"color_north": "#e71111", "color_south": "#00b050"} + assert c1.style.magnetization.color.north == "#e71111" + assert c1.style.magnetization.color.south == "#00b050" + + # assigning a dict, should fall back to defaults for unspecified values + c1.style.magnetization = {"color_south": "#00b050"} + assert c1.style.magnetization.color.north == mag_col_default.north + assert c1.style.magnetization.color.south == "#00b050" + + # assigning None, all should fall back to defaults + c1.style.magnetization = None + assert c1.style.magnetization.color.north == mag_col_default.north + assert c1.style.magnetization.color.south == mag_col_default.south + + # updating, updates specified only, other parameters remain + c1.style.magnetization.update(color_north="#e71111") + assert c1.style.magnetization.color.north == "#e71111" + assert c1.style.magnetization.color.south == mag_col_default.south diff --git a/tests/test_defaults.py b/tests/test_defaults.py index 6a2c99b57..0568c54fa 100644 --- a/tests/test_defaults.py +++ b/tests/test_defaults.py @@ -2,10 +2,9 @@ import magpylib as magpy from magpylib._src.defaults.defaults_classes import DefaultSettings -from magpylib._src.defaults.defaults_utility import ALLOWED_LINESTYLES -from magpylib._src.defaults.defaults_utility import ALLOWED_SYMBOLS -from magpylib._src.defaults.defaults_utility import SUPPORTED_PLOTTING_BACKENDS -from magpylib._src.style import DisplayStyle +from magpylib._src.defaults.defaults_values import ALLOWED_LINESTYLES +from magpylib._src.defaults.defaults_values import ALLOWED_SYMBOLS +from magpylib._src.defaults.defaults_values import SUPPORTED_PLOTTING_BACKENDS bad_inputs = { "display_autosizefactor": (0,), # float>0 @@ -13,10 +12,10 @@ "display_animation_fps": (0,), # int>0 "display_animation_time": (0,), # int>0 "display_animation_maxframes": (0,), # int>0 - "display_animation_slider": ("notbool"), # bool + "display_animation_slider": ("notbool",), # bool "display_animation_output": ("filename.badext", "badext"), # bool "display_backend": ("plotty",), # str typo - "display_colorsequence": (["#2E91E5", "wrongcolor"], 123), # iterable of colors + "display_colorsequence": (("#2E91E5", "wrongcolor"), 123), # iterable of colors "display_style_base_path_line_width": (-1,), # float>=0 "display_style_base_path_line_style": ("wrongstyle",), "display_style_base_path_line_color": ("wrongcolor",), # color @@ -24,7 +23,7 @@ "display_style_base_path_marker_symbol": ("wrongsymbol",), "display_style_base_path_marker_color": ("wrongcolor",), # color "display_style_base_path_show": ("notbool", 1), # bool - "display_style_base_path_frames": (True, False, ["1"], "1"), # int or iterable + "display_style_base_path_frames": (True, False, ["a"], "b"), # int or iterable "display_style_base_path_numbering": ("notbool",), # bool "display_style_base_description_show": ("notbool",), # bool "display_style_base_description_text": ( @@ -77,14 +76,7 @@ def get_bad_test_data(): bad_test_data = [] for k, tup in bad_inputs.items(): for v in tup: - if "description_text" not in k: - if "color" in k and "transition" not in k and "mode" not in k: - # color attributes use a the color validator, which raises a ValueError - errortype = ValueError - else: - # all other parameters raise AssertionError - errortype = AssertionError - bad_test_data.append((k, v, pytest.raises(errortype))) + bad_test_data.append((k, v, pytest.raises(ValueError))) return bad_test_data @@ -112,8 +104,8 @@ def test_defaults_bad_inputs(key, value, expected_errortype): "display_animation_output": ("filename.mp4", "gif"), # bool "display_backend": ["auto", *SUPPORTED_PLOTTING_BACKENDS], # str typo "display_colorsequence": ( - ("#2e91e5", "#0d2a63"), - ("blue", "red"), + ["#2e91e5", "#0d2a63"], + ["blue", "red"], ), # ]), # iterable of colors "display_style_base_path_line_width": (0, 1), # float>=0 "display_style_base_path_line_style": ALLOWED_LINESTYLES, @@ -122,7 +114,7 @@ def test_defaults_bad_inputs(key, value, expected_errortype): "display_style_base_path_marker_symbol": ALLOWED_SYMBOLS, "display_style_base_path_marker_color": ("blue", "#2E91E5"), # color "display_style_base_path_show": (True, False), # bool - "display_style_base_path_frames": (-1, (1, 3)), # int or iterable + # "display_style_base_path_frames": (1, (2, 3)), # int or iterable "display_style_base_path_numbering": (True, False), # bool "display_style_base_description_show": (True, False), # bool "display_style_base_description_text": ("a string",), # string @@ -130,7 +122,7 @@ def test_defaults_bad_inputs(key, value, expected_errortype): "display_style_base_model3d_showdefault": (True, False), "display_style_base_color": ("blue", "#2E91E5"), # color "display_style_magnet_magnetization_show": (True, False), - "display_style_magnet_magnetization_size": (0, 1), # float>=0 + "display_style_magnet_magnetization_arrow_size": (0, 1), # float>=0 "display_style_magnet_magnetization_color_north": ("blue", "#2E91E5"), "display_style_magnet_magnetization_color_middle": ("blue", "#2E91E5"), "display_style_magnet_magnetization_color_south": ("blue", "#2E91E5"), @@ -200,7 +192,7 @@ def test_defaults_good_inputs(key, value, expected): v0 = c for v in key.split("_"): v0 = getattr(v0, v) - assert v0 == expected, f"{key} should be {expected}, but received {v0} instead" + assert v0 == expected @pytest.mark.parametrize( @@ -225,7 +217,7 @@ def test_defaults_good_inputs(key, value, expected): ) def test_bad_style_classes(style_class): """testing properties which take classes as properties""" - c = DisplayStyle().reset() + c = DefaultSettings().display.style with pytest.raises(ValueError): c.update(**{style_class: "bad class"}) diff --git a/tests/test_display_matplotlib.py b/tests/test_display_matplotlib.py index 8a4fc464a..f86988bc6 100644 --- a/tests/test_display_matplotlib.py +++ b/tests/test_display_matplotlib.py @@ -404,20 +404,16 @@ def test_matplotlib_model3d_extra_updatefunc(): obj.show(canvas=ax, return_fig=True) with pytest.raises(ValueError): - updatefunc = "not callable" - obj.style.model3d.add_trace(updatefunc) + obj.style.model3d.add_trace("not callable") - with pytest.raises(AssertionError): - updatefunc = "not callable" - obj.style.model3d.add_trace(updatefunc=updatefunc) + with pytest.raises(ValueError): + obj.style.model3d.add_trace(updatefunc="not callable") - with pytest.raises(AssertionError): - updatefunc = lambda: "bad output type" - obj.style.model3d.add_trace(updatefunc=updatefunc) + with pytest.raises(ValueError): + obj.style.model3d.add_trace(updatefunc=lambda: "bad output type") - with pytest.raises(AssertionError): - updatefunc = lambda: {"bad_key": "some_value"} - obj.style.model3d.add_trace(updatefunc=updatefunc) + with pytest.raises(ValueError): + obj.style.model3d.add_trace(updatefunc=lambda: {"bad_key": "some_value"}) def test_empty_display(): diff --git a/tests/test_obj_BaseGeo.py b/tests/test_obj_BaseGeo.py index e6707cb49..d64251d06 100644 --- a/tests/test_obj_BaseGeo.py +++ b/tests/test_obj_BaseGeo.py @@ -421,7 +421,7 @@ def test_copy(): # check if label suffix iterated correctly assert bg1c.style.label == "label2" - assert bg2c.style.label is None + assert bg2c.style.label == "" assert bg3c.style.label == "BaseGeo_01" # check if style is passed correctly