From ac107881991c6daec5f845387e5cb72079c26f63 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 9 Oct 2025 16:19:20 +0200 Subject: [PATCH 001/152] add sphere tab an split_pane --- app.py | 37 +++++++++++++++++++++----------- components/__init__.py | 0 components/split_pane.py | 46 ++++++++++++++++++++++++++++++++++++++++ pages/sphere.py | 20 +++++++++++++++++ 4 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 components/__init__.py create mode 100644 components/split_pane.py create mode 100644 pages/sphere.py diff --git a/app.py b/app.py index 8a62e17..232de1c 100644 --- a/app.py +++ b/app.py @@ -15,17 +15,30 @@ app = Dash(__name__, external_stylesheets=external_stylesheets, server=server, use_pages=True) # , suppress_callback_exceptions=True app.layout = dbc.Container([ - html.H1('ISAS Interactive'), - dbc.Nav([ - dbc.NavLink(html.Div(f"{page['name'].lower()}"), href=page["relative_path"], active="exact") - for page in dash.page_registry.values() - ], pills=True, className='bg-light'), - html.P(), - dash.page_container -], fluid=True) + html.H1('ISAS Interactive', + style={ + "caretColor": "transparent", + "userSelect": "none" + } + ), + dbc.Nav([ + dbc.NavLink( + html.Div( + f"{page['name'].lower()}"), + href=page["relative_path"], + active="exact", + className='rounded-3' + ) + for page in dash.page_registry.values() + ], pills=True, className='bg-light rounded-3'), + html.P(), + dash.page_container +], +fluid=True, +) if __name__ == '__main__': - # processes=6, threaded=False, - app.run(debug=True, threaded=True, host='0.0.0.0', port='8080') - # app.run(debug=True, processes=6, threaded=False, host='0.0.0.0', port='8080') - # app.run(debug=True) + # processes=6, threaded=False, + app.run(debug=True, threaded=True, host='0.0.0.0', port='8080') + # app.run(debug=True, processes=6, threaded=False, host='0.0.0.0', port='8080') + # app.run(debug=True) diff --git a/components/__init__.py b/components/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/components/split_pane.py b/components/split_pane.py new file mode 100644 index 0000000..1afa6b6 --- /dev/null +++ b/components/split_pane.py @@ -0,0 +1,46 @@ +from dash import html +from dash_resizable_panels import PanelGroup, Panel, PanelResizeHandle +import dash_bootstrap_components as dbc + + + +def SplitPane(children1, children2): + return html.Div([ + PanelGroup( + id='panel-group', + children=[ + Panel( + id='left-sidebar', + minSizePercentage=15, + children=[ + html.Div([ + dbc.Container(children1, fluid=True) + ], className="bg-light vh-100 w-100 rounded-3") + ], + ), + PanelResizeHandle( + id='resize-handle', + style={ + "flex": "0 0 20px", + "margin": "0 -10px", + "background": "transparent", + "cursor": "col-resize", + "userSelect": "none", + "position": "relative", + "zIndex": 2, + }, + ), + Panel( + id='plot-panel', + minSizePercentage=15, + children=[ + dbc.Container(children2, fluid=True) + ], + ) + ], + direction='horizontal', + className='vh-100 w-100 px-0', + ) + ], + className='vh-100 px-0', + ) \ No newline at end of file diff --git a/pages/sphere.py b/pages/sphere.py new file mode 100644 index 0000000..0da889b --- /dev/null +++ b/pages/sphere.py @@ -0,0 +1,20 @@ +import plotly +import dash +from dash import html +from dash_resizable_panels import PanelGroup, Panel, PanelResizeHandle +import dash_bootstrap_components as dbc + +from components.split_pane import SplitPane + +dash.register_page(__name__) + + +layout = SplitPane( + [ + html.P("this is where the controls will go") + ], + [ + html.P("this is where the plot will go") + ] + +) \ No newline at end of file From e30cca65eed4eeecf9c687e5a1ec88d3fa57f559 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 10 Oct 2025 11:45:24 +0200 Subject: [PATCH 002/152] basic vtk sphere test --- components/split_pane.py | 9 +++++---- pages/sphere.py | 23 +++++++++++++++++++++-- requirements.txt | 4 ++++ 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/components/split_pane.py b/components/split_pane.py index 1afa6b6..be340e7 100644 --- a/components/split_pane.py +++ b/components/split_pane.py @@ -4,7 +4,7 @@ -def SplitPane(children1, children2): +def SplitPane(children1, children2, default_size): return html.Div([ PanelGroup( id='panel-group', @@ -12,10 +12,11 @@ def SplitPane(children1, children2): Panel( id='left-sidebar', minSizePercentage=15, + defaultSizePercentage=default_size, children=[ html.Div([ dbc.Container(children1, fluid=True) - ], className="bg-light vh-100 w-100 rounded-3") + ], className="bg-light h-100 w-100 rounded-3") ], ), PanelResizeHandle( @@ -39,8 +40,8 @@ def SplitPane(children1, children2): ) ], direction='horizontal', - className='vh-100 w-100 px-0', + className='h-100 w-100 px-0', ) ], - className='vh-100 px-0', + className='h-100 px-0', ) \ No newline at end of file diff --git a/pages/sphere.py b/pages/sphere.py index 0da889b..c68d7e1 100644 --- a/pages/sphere.py +++ b/pages/sphere.py @@ -3,6 +3,7 @@ from dash import html from dash_resizable_panels import PanelGroup, Panel, PanelResizeHandle import dash_bootstrap_components as dbc +import dash_vtk from components.split_pane import SplitPane @@ -14,7 +15,25 @@ html.P("this is where the controls will go") ], [ - html.P("this is where the plot will go") - ] + dash_vtk.View( + children=[ + dash_vtk.GeometryRepresentation( + property={"color": [0.8, 0.8, 0.8]}, + children=[ + dash_vtk.Algorithm( + vtkClass="vtkSphereSource", + state={ + "phiResolution": 32, + "thetaResolution": 32, + }, + ) + ] + ) + ], + background=[1, 1, 1], + style={"height": "100vh", "width": "100%"}, + ) + ], + 30 ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cbc3e63..5635181 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ blinker==1.7.0 certifi==2024.2.2 charset-normalizer==3.3.2 click==8.1.7 +colorama==0.4.6 contourpy==1.2.1 cycler==0.12.1 dash==2.16.1 @@ -9,7 +10,9 @@ dash-bootstrap-components==1.6.0 dash-core-components==2.0.0 dash-design-kit==0.0.1 dash-html-components==2.0.0 +dash-resizable-panels==0.1.0 dash-table==5.0.0 +dash-vtk==0.0.9 dash_mantine_components==0.14.2 Flask==3.0.3 fonttools==4.51.0 @@ -38,5 +41,6 @@ tenacity==8.2.3 typing_extensions==4.11.0 tzdata==2024.1 urllib3==2.2.1 +vtk==9.5.2 Werkzeug==3.0.2 zipp==3.18.1 From 05eeeaf43000318fee7b13955dfe75ee6f470e4b Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 10 Oct 2025 20:51:02 +0200 Subject: [PATCH 003/152] added plotly sphere plot and rendering of samples (points) --- model/__init__.py | 0 model/sphere/__init__.py | 0 model/sphere/sphere.py | 18 +++++++++++ pages/sphere.py | 57 ++++++++++++++++++++++------------- pages/sphere_vtk.py | 64 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 model/__init__.py create mode 100644 model/sphere/__init__.py create mode 100644 model/sphere/sphere.py create mode 100644 pages/sphere_vtk.py diff --git a/model/__init__.py b/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/sphere/__init__.py b/model/sphere/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py new file mode 100644 index 0000000..92d6329 --- /dev/null +++ b/model/sphere/sphere.py @@ -0,0 +1,18 @@ +import numpy as np + +class Sphere: + def __init__(self, resolution=50, radius=1): + self.xyz = self.generate_xyz(resolution, radius) + self.mesh = np.array([]) + self.samples = np.array([]) + + def generate_xyz(self, resolution=50, radius=1): + phi = np.linspace(0, np.pi, resolution) + theta = np.linspace(0, 2 * np.pi, resolution) + phi, theta = np.meshgrid(phi, theta) + + x = radius * np.sin(phi) * np.cos(theta) + y = radius * np.sin(phi) * np.sin(theta) + z = radius * np.cos(phi) + + return x, y, z \ No newline at end of file diff --git a/pages/sphere.py b/pages/sphere.py index c68d7e1..310cef8 100644 --- a/pages/sphere.py +++ b/pages/sphere.py @@ -1,38 +1,55 @@ import plotly import dash -from dash import html +from dash import html, dcc from dash_resizable_panels import PanelGroup, Panel, PanelResizeHandle import dash_bootstrap_components as dbc -import dash_vtk +import plotly.graph_objects as go +import numpy as np from components.split_pane import SplitPane +from model.sphere.sphere import Sphere dash.register_page(__name__) +sphere = Sphere() +x, y, z = sphere.xyz +sphere.samples = np.array([ + [0,0,1], + [0,1,0], + [1,0,0] +]) + + +fig = go.Figure( + data=[ + go.Surface( + x=x, y=y, z=z, + showscale=False + ), + go.Scatter3d( + x=sphere.samples[:][0], y=sphere.samples[:][1], z=sphere.samples[:][2], + mode="markers", + marker=dict(size=4, color="red") + ) + ] +) + +fig.update_layout( + scene=dict( + aspectmode='data', + xaxis=dict(visible=False), + yaxis=dict(visible=False), + zaxis=dict(visible=False), + ), + margin=dict(l=0, r=0, t=0, b=0) +) layout = SplitPane( [ html.P("this is where the controls will go") ], [ - dash_vtk.View( - children=[ - dash_vtk.GeometryRepresentation( - property={"color": [0.8, 0.8, 0.8]}, - children=[ - dash_vtk.Algorithm( - vtkClass="vtkSphereSource", - state={ - "phiResolution": 32, - "thetaResolution": 32, - }, - ) - ] - ) - ], - background=[1, 1, 1], - style={"height": "100vh", "width": "100%"}, - ) + dcc.Graph(figure=fig) ], 30 diff --git a/pages/sphere_vtk.py b/pages/sphere_vtk.py new file mode 100644 index 0000000..70040a0 --- /dev/null +++ b/pages/sphere_vtk.py @@ -0,0 +1,64 @@ +import plotly +import dash +from dash import html +from dash_resizable_panels import PanelGroup, Panel, PanelResizeHandle +import dash_bootstrap_components as dbc +import dash_vtk +import numpy as np + +from components.split_pane import SplitPane +from model.sphere.sphere import Sphere + + +dash.register_page(__name__) + + +sphere = Sphere() +x, y, z = sphere.xyz +sphere.samples = np.array([ + [0,0,1], + [0,1,0], + [1,0,0] +]) + + + +layout = SplitPane( + [ + html.P("this is where the controls will go") + ], + [ + dash_vtk.View( + children=[ + dash_vtk.GeometryRepresentation( + property={"color": [0.8, 0.8, 0.8]}, + children=[ + dash_vtk.Algorithm( + vtkClass="vtkSphereSource", + state={ + "phiResolution": 32, + "thetaResolution": 32, + "radius": 1.0, + }, + ) + ] + ), + dash_vtk.GeometryRepresentation( + mapper={"scalarVisibility": False}, + property={"color": [1, 0, 0], "pointSize": 6}, + children=[ + dash_vtk.PolyData( + points=sphere.samples.ravel(), + # verts is an array like [1, 0, 1, 1, 1, 2, ...] + verts=np.c_[np.ones(sphere.samples.shape[0], dtype=np.int64), np.arange(sphere.samples.shape[0])].ravel().tolist(), + ) + ], + ), + ], + background=[1, 1, 1], + style={"height": "100vh", "width": "100%"}, + ) + ], + 30 + +) \ No newline at end of file From f7c0e152a96b7846483c9b16614efd81bcfa6777 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 13 Oct 2025 19:14:37 +0200 Subject: [PATCH 004/152] added random sampling for sphere and plugin based sampler loading --- model/distributions/__init__.py | 0 model/distributions/distribution_loader.py | 42 ++++++++++++ model/distributions/sphere/__init__.py | 0 model/distributions/sphere/random.py | 26 +++++++ model/distributions/sphere/random_test.py | 26 +++++++ .../sphere/sphere_distribution.py | 20 ++++++ model/sphere/sphere.py | 3 + pages/sphere.py | 67 ++++++++++++++++--- util/selectors/__init__.py | 0 util/selectors/selector.py | 6 ++ util/selectors/slider.py | 65 ++++++++++++++++++ 11 files changed, 246 insertions(+), 9 deletions(-) create mode 100644 model/distributions/__init__.py create mode 100644 model/distributions/distribution_loader.py create mode 100644 model/distributions/sphere/__init__.py create mode 100644 model/distributions/sphere/random.py create mode 100644 model/distributions/sphere/random_test.py create mode 100644 model/distributions/sphere/sphere_distribution.py create mode 100644 util/selectors/__init__.py create mode 100644 util/selectors/selector.py create mode 100644 util/selectors/slider.py diff --git a/model/distributions/__init__.py b/model/distributions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/distribution_loader.py b/model/distributions/distribution_loader.py new file mode 100644 index 0000000..6ae25c7 --- /dev/null +++ b/model/distributions/distribution_loader.py @@ -0,0 +1,42 @@ +import importlib +import inspect +import pkgutil + + +class DistributionLoader: + """ + Class for loading different probability distributions with a plugin pattern. + + modeled after: https://www.researchgate.net/figure/Class-diagram-for-the-Plugin-Pattern_fig6_221039844 + """ + def __init__(self, type, type_package): + self.distribution_type = type + self.distribution_package = type_package + self.distributions = {} + + self.load_distributions() + + + def load_distributions(self): + pkg = importlib.import_module(self.distribution_package) + if not hasattr(pkg, "__path__"): + raise ValueError(f"'{self.distribution_package}' is not a package (missing __path__).") + + for finder, name, ispkg in pkgutil.iter_modules(pkg.__path__): + if ispkg: + continue + module = importlib.import_module(f"{pkg.__name__}.{name}") + + for _, obj in inspect.getmembers(module, inspect.isclass): + # skip abstract, parametered intervace and non-subclasses + if obj is self.distribution_type: + continue + if not issubclass(obj, self.distribution_type): + continue + if inspect.isabstract(obj): + continue + + self.distributions[obj().get_name()] = obj() + + def get_distributions(self): + return self.distributions diff --git a/model/distributions/sphere/__init__.py b/model/distributions/sphere/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/sphere/random.py b/model/distributions/sphere/random.py new file mode 100644 index 0000000..80fb46b --- /dev/null +++ b/model/distributions/sphere/random.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod +from model.distributions.sphere.sphere_distribution import SphereDistribution +from util.selectors.slider import Slider +import numpy as np + +class SphereDistribution(SphereDistribution): + def __init__(self): + self.sample_options = [ + Slider("Number of Samples", 10, 100, 1000) + ] + + self.distribution_options = [] + + def get_name(self): + return "Random" + + def sample(self, sample_options): + sample_count = sample_options[0].state + + samples = np.random.normal(size=(sample_count, 3)) + samples /= np.linalg.norm(samples, axis=1)[:, np.newaxis] + + return samples + + def generate_mesh(self, distribution_options): + pass \ No newline at end of file diff --git a/model/distributions/sphere/random_test.py b/model/distributions/sphere/random_test.py new file mode 100644 index 0000000..861fd67 --- /dev/null +++ b/model/distributions/sphere/random_test.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod +from model.distributions.sphere.sphere_distribution import SphereDistribution +from util.selectors.slider import Slider +import numpy as np + +class SphereDistribution(SphereDistribution): + def __init__(self): + self.sample_options = [ + Slider("Number of Samples", 1, 5, 15) + ] + + self.distribution_options = [] + + def get_name(self): + return "Random2" + + def sample(self, sample_options): + sample_count = sample_options[0].state + + samples = np.random.normal(size=(sample_count, 3)) + samples /= np.linalg.norm(samples, axis=1)[:, np.newaxis] + + return samples + + def generate_mesh(self, distribution_options): + pass \ No newline at end of file diff --git a/model/distributions/sphere/sphere_distribution.py b/model/distributions/sphere/sphere_distribution.py new file mode 100644 index 0000000..88f00e1 --- /dev/null +++ b/model/distributions/sphere/sphere_distribution.py @@ -0,0 +1,20 @@ +from abc import ABC, abstractmethod + +class SphereDistribution(ABC): + def __init__(self): + self.sample_options = [] + self.distribution_options = [] + + # Returns the name of the distribution + @abstractmethod + def get_name(self): + pass + + # returns samples as a numpy array of shape (n, 3) + @abstractmethod + def sample(self, sample_options): + pass + + @abstractmethod + def generate_mesh(self, distribution_options): + pass \ No newline at end of file diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index 92d6329..958d8ba 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -1,10 +1,13 @@ import numpy as np +from model.distributions.distribution_loader import DistributionLoader +from model.distributions.sphere.sphere_distribution import SphereDistribution class Sphere: def __init__(self, resolution=50, radius=1): self.xyz = self.generate_xyz(resolution, radius) self.mesh = np.array([]) self.samples = np.array([]) + self.distributions = DistributionLoader(SphereDistribution, "model.distributions.sphere").get_distributions() def generate_xyz(self, resolution=50, radius=1): phi = np.linspace(0, np.pi, resolution) diff --git a/pages/sphere.py b/pages/sphere.py index 310cef8..313614d 100644 --- a/pages/sphere.py +++ b/pages/sphere.py @@ -1,6 +1,6 @@ import plotly import dash -from dash import html, dcc +from dash import html, dcc, callback, Input, Output, ALL from dash_resizable_panels import PanelGroup, Panel, PanelResizeHandle import dash_bootstrap_components as dbc import plotly.graph_objects as go @@ -13,21 +13,19 @@ sphere = Sphere() x, y, z = sphere.xyz -sphere.samples = np.array([ - [0,0,1], - [0,1,0], - [1,0,0] -]) fig = go.Figure( data=[ go.Surface( x=x, y=y, z=z, + colorscale="Viridis", showscale=False ), go.Scatter3d( - x=sphere.samples[:][0], y=sphere.samples[:][1], z=sphere.samples[:][2], + x=sphere.samples[:, 0] if sphere.samples.size else [], + y=sphere.samples[:, 1] if sphere.samples.size else [], + z=sphere.samples[:, 2] if sphere.samples.size else [], mode="markers", marker=dict(size=4, color="red") ) @@ -44,12 +42,63 @@ margin=dict(l=0, r=0, t=0, b=0) ) +@callback( + Output("distribution-options", "children"), + Input("distribution-selector", "value"), +) +def update_curr_distribution(selected_distribution): + options = sphere.distributions[selected_distribution].sample_options + + options_dcc = [opt.to_dash_component(f"id") for id, opt in enumerate(options)] + return options_dcc + +@callback( + Output("graph", "figure"), + Input({"type": "dynamic-option", "index": ALL}, "value"), + Input("distribution-selector", "value"), + Input("distribution-options", "children"), +) +def update_plot(options, selected_distribution, _): + dist_options = sphere.distributions[selected_distribution].sample_options + for opt, new_state in zip(dist_options, options): + opt.update_state(new_state) + + sphere.samples = sphere.distributions[selected_distribution].sample(dist_options) + + data = [ + go.Surface( + x=x, y=y, z=z, + showscale=False, + colorscale="Viridis", + ), + go.Scatter3d( + x=sphere.samples[:, 0], + y=sphere.samples[:, 1], + z=sphere.samples[:, 2], + mode="markers", + marker=dict(size=4, color="red") + ) + ] + return go.Figure(data=data, layout=fig.layout) + + layout = SplitPane( [ - html.P("this is where the controls will go") + html.Br(), + html.P("Select Distribution:"), + dcc.RadioItems( + id="distribution-selector", + options=(list(sphere.distributions.keys())), + value=sphere.distributions[list(sphere.distributions.keys())[0]].get_name(), + ), + html.Br(), + html.Hr(), + html.Br(), + + html.Div(id="distribution-options"), ], [ - dcc.Graph(figure=fig) + dcc.Graph(id="graph", figure=fig) ], 30 diff --git a/util/selectors/__init__.py b/util/selectors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/util/selectors/selector.py b/util/selectors/selector.py new file mode 100644 index 0000000..02a1e9d --- /dev/null +++ b/util/selectors/selector.py @@ -0,0 +1,6 @@ +from abc import ABC, abstractmethod + +class Selector(ABC): + @abstractmethod + def to_dash_component(self, id): + pass \ No newline at end of file diff --git a/util/selectors/slider.py b/util/selectors/slider.py new file mode 100644 index 0000000..25a89db --- /dev/null +++ b/util/selectors/slider.py @@ -0,0 +1,65 @@ +from dash import dcc, html +from util.selectors.selector import Selector + +SLIDER_OPT_AMOUNT = 100 +SLIDER_MARK_AMOUNT = 5 + +class Slider(Selector): + def __init__(self, name, min, state, max): + self.name = name + self.min = min + self.state = state + self.max = max + + + def calculate_step(self): + # rounds down to the neares human readable step + # eg. 1,5,25,50,100,250,500,1000, ... + + step_unrounded = (self.max - self.min) // SLIDER_OPT_AMOUNT + sequence = [1, 5, 25, 50] + + prev = 1 + + while True: + for s in sequence: + if step_unrounded < s: + return prev + prev = s + sequence = [s * 10 for s in sequence] + + def calculate_marks(self): + def round_nice_number(x): + if x >= 1000: + return round(x, -3) + elif x >= 100: + return round(x, -2) + else: + return x + + marks = {} + step = (self.max - self.min) / SLIDER_MARK_AMOUNT + for i in range(SLIDER_MARK_AMOUNT + 1): + value = int(self.min + i * step) + value = round_nice_number(value) + marks[value] = str(value) + return marks + + + def to_dash_component(self, id): + return html.Div([ + html.Label(self.name), + + dcc.Slider( + id={"type": "dynamic-option", "index": id}, + min=self.min, + max=self.max, + value=self.state, + tooltip={"placement": "bottom", "always_visible": True}, + step=self.calculate_step(), + marks=self.calculate_marks(), + ) + ]) + + def update_state(self, new_state): + self.state = int(new_state) \ No newline at end of file From cd65714aee011314aa26494ddf75a00c3a90fedf Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 14 Oct 2025 15:44:57 +0200 Subject: [PATCH 005/152] added basic torus rendering and dummy torus distribution --- model/distributions/torus/__init__.py | 0 model/distributions/torus/random.py | 20 ++++ .../distributions/torus/torus_distribution.py | 20 ++++ model/torus/__init__.py | 0 model/torus/torus.py | 27 +++++ pages/torus.py | 104 ++++++++++++++++++ 6 files changed, 171 insertions(+) create mode 100644 model/distributions/torus/__init__.py create mode 100644 model/distributions/torus/random.py create mode 100644 model/distributions/torus/torus_distribution.py create mode 100644 model/torus/__init__.py create mode 100644 model/torus/torus.py create mode 100644 pages/torus.py diff --git a/model/distributions/torus/__init__.py b/model/distributions/torus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/torus/random.py b/model/distributions/torus/random.py new file mode 100644 index 0000000..0afc579 --- /dev/null +++ b/model/distributions/torus/random.py @@ -0,0 +1,20 @@ +from abc import ABC, abstractmethod +from model.distributions.torus.torus_distribution import TorusDistribution +from util.selectors.slider import Slider +import numpy as np + +class SphereDistribution(TorusDistribution): + def __init__(self): + self.sample_options = [ + Slider("Number of Samples", 10, 100, 1000) + ] + self.distribution_options = [] + + def get_name(self): + return "Random" + + def sample(self, sample_options): + return np.array([]) + + def generate_mesh(self, distribution_options): + pass \ No newline at end of file diff --git a/model/distributions/torus/torus_distribution.py b/model/distributions/torus/torus_distribution.py new file mode 100644 index 0000000..1ff11af --- /dev/null +++ b/model/distributions/torus/torus_distribution.py @@ -0,0 +1,20 @@ +from abc import ABC, abstractmethod + +class TorusDistribution(ABC): + def __init__(self): + self.sample_options = [] + self.distribution_options = [] + + # Returns the name of the distribution + @abstractmethod + def get_name(self): + pass + + # returns samples as a TODO + @abstractmethod + def sample(self, sample_options): + pass + + @abstractmethod + def generate_mesh(self, distribution_options): + pass \ No newline at end of file diff --git a/model/torus/__init__.py b/model/torus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/torus/torus.py b/model/torus/torus.py new file mode 100644 index 0000000..0c76df1 --- /dev/null +++ b/model/torus/torus.py @@ -0,0 +1,27 @@ +import numpy as np +from model.distributions.distribution_loader import DistributionLoader +from model.distributions.torus.torus_distribution import TorusDistribution + +class Torus: + def __init__(self, resolution=50, r=1, R=3): + self.xyz = self.generate_xyz(resolution, r, R) + self.mesh = np.array([]) + self.samples = np.array([]) + self.distributions = DistributionLoader(TorusDistribution, "model.distributions.torus").get_distributions() + + def generate_xyz(self, resolution=50, r=1, R=3): + t = np.linspace(0, 2*np.pi, resolution) + p = np.linspace(0, 2*np.pi, resolution) + t,p = np.meshgrid(t,p) + + + return self.t_p_to_xyz(t, p, r, R) + + + @classmethod + def t_p_to_xyz(self, t, p, r=1, R=3): + x = (R + r * np.cos(p)) * np.cos(t) + y = (R + r * np.cos(p)) * np.sin(t) + z = r * np.sin(p) + + return x, y, z diff --git a/pages/torus.py b/pages/torus.py new file mode 100644 index 0000000..147addb --- /dev/null +++ b/pages/torus.py @@ -0,0 +1,104 @@ +import plotly +import dash +from dash import html, dcc, callback, Input, Output, ALL +from dash_resizable_panels import PanelGroup, Panel, PanelResizeHandle +import dash_bootstrap_components as dbc +import plotly.graph_objects as go +import numpy as np + +from components.split_pane import SplitPane +from model.torus.torus import Torus + +dash.register_page(__name__) + +torus = Torus() +x, y, z = torus.xyz + + +fig = go.Figure( + data=[ + go.Surface( + x=x, y=y, z=z, + colorscale="Viridis", + showscale=False + ), + go.Scatter3d( + x=torus.samples[:, 0] if torus.samples.size else [], + y=torus.samples[:, 1] if torus.samples.size else [], + z=torus.samples[:, 2] if torus.samples.size else [], + mode="markers", + marker=dict(size=4, color="red") + ) + ] +) + +fig.update_layout( + scene=dict( + aspectmode='data', + xaxis=dict(visible=False), + yaxis=dict(visible=False), + zaxis=dict(visible=False), + ), + margin=dict(l=0, r=0, t=0, b=0) +) + +@callback( + Output("distribution-options-torus", "children"), + Input("distribution-selector", "value"), +) +def update_curr_distribution(selected_distribution): + options = torus.distributions[selected_distribution].sample_options + + options_dcc = [opt.to_dash_component(f"id") for id, opt in enumerate(options)] + return options_dcc + +@callback( + Output("graph-torus", "figure"), + Input({"type": "dynamic-option", "index": ALL}, "value"), + Input("distribution-selector", "value"), + Input("distribution-options-torus", "children"), +) +def update_plot(options, selected_distribution, _): + dist_options = torus.distributions[selected_distribution].sample_options + for opt, new_state in zip(dist_options, options): + opt.update_state(new_state) + + torus.samples = torus.distributions[selected_distribution].sample(dist_options) + data = [ + go.Surface( + x=x, y=y, z=z, + showscale=False, + colorscale="Viridis", + ), + go.Scatter3d( + x=torus.samples[:, 0] if torus.samples.size else [], + y=torus.samples[:, 1] if torus.samples.size else [], + z=torus.samples[:, 2] if torus.samples.size else [], + mode="markers", + marker=dict(size=4, color="red") + ) + ] + return go.Figure(data=data, layout=fig.layout) + + +layout = SplitPane( + [ + html.Br(), + html.P("Select Distribution:"), + dcc.RadioItems( + id="distribution-selector", + options=(list(torus.distributions.keys())), + value=torus.distributions[list(torus.distributions.keys())[0]].get_name(), + ), + html.Br(), + html.Hr(), + html.Br(), + + html.Div(id="distribution-options-torus"), + ], + [ + dcc.Graph(id="graph-torus", figure=fig) + ], + 30 + +) \ No newline at end of file From d19837e950992479725353bcaf85eaaa69284a88 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 14 Oct 2025 16:47:42 +0200 Subject: [PATCH 006/152] added random sampling for torus --- model/distributions/torus/random.py | 9 ++++++++- model/distributions/torus/torus_distribution.py | 3 ++- model/torus/torus.py | 4 ++-- pages/torus.py | 12 ++++++------ 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/model/distributions/torus/random.py b/model/distributions/torus/random.py index 0afc579..a74abd5 100644 --- a/model/distributions/torus/random.py +++ b/model/distributions/torus/random.py @@ -14,7 +14,14 @@ def get_name(self): return "Random" def sample(self, sample_options): - return np.array([]) + sample_count = sample_options[0].state + + t = np.random.uniform(0, 2 * np.pi, sample_count) + p = np.random.uniform(0, 2 * np.pi, sample_count) + + samples = np.column_stack((t, p)) + + return samples def generate_mesh(self, distribution_options): pass \ No newline at end of file diff --git a/model/distributions/torus/torus_distribution.py b/model/distributions/torus/torus_distribution.py index 1ff11af..07b6d75 100644 --- a/model/distributions/torus/torus_distribution.py +++ b/model/distributions/torus/torus_distribution.py @@ -10,7 +10,8 @@ def __init__(self): def get_name(self): pass - # returns samples as a TODO + # returns samples as a numpy array of shape (n, 2) + # where (2) are the parameters (t, p) on the torus @abstractmethod def sample(self, sample_options): pass diff --git a/model/torus/torus.py b/model/torus/torus.py index 0c76df1..954e41d 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -18,8 +18,8 @@ def generate_xyz(self, resolution=50, r=1, R=3): return self.t_p_to_xyz(t, p, r, R) - @classmethod - def t_p_to_xyz(self, t, p, r=1, R=3): + @staticmethod + def t_p_to_xyz(t, p, r=1, R=3): x = (R + r * np.cos(p)) * np.cos(t) y = (R + r * np.cos(p)) * np.sin(t) z = r * np.sin(p) diff --git a/pages/torus.py b/pages/torus.py index 147addb..0addb05 100644 --- a/pages/torus.py +++ b/pages/torus.py @@ -23,9 +23,9 @@ showscale=False ), go.Scatter3d( - x=torus.samples[:, 0] if torus.samples.size else [], - y=torus.samples[:, 1] if torus.samples.size else [], - z=torus.samples[:, 2] if torus.samples.size else [], + x=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[0] if torus.samples.size else [], + y=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[1] if torus.samples.size else [], + z=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[2] if torus.samples.size else [], mode="markers", marker=dict(size=4, color="red") ) @@ -71,9 +71,9 @@ def update_plot(options, selected_distribution, _): colorscale="Viridis", ), go.Scatter3d( - x=torus.samples[:, 0] if torus.samples.size else [], - y=torus.samples[:, 1] if torus.samples.size else [], - z=torus.samples[:, 2] if torus.samples.size else [], + x=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[0] if torus.samples.size else [], + y=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[1] if torus.samples.size else [], + z=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[2] if torus.samples.size else [], mode="markers", marker=dict(size=4, color="red") ) From e4d126c8ad49a2f72d3afaa7fb65b524b6120a03 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 15 Oct 2025 13:52:30 +0200 Subject: [PATCH 007/152] refractor rendering of sphere and torus into common 3dRenderer --- app.py | 10 +++- model/manifold.py | 14 +++++ model/sphere/sphere.py | 9 ++- model/torus/torus.py | 17 +++++- pages/sphere.py | 85 ++-------------------------- pages/torus.py | 86 ++-------------------------- renderer/Object3DRenderer.py | 105 +++++++++++++++++++++++++++++++++++ renderer/__init__.py | 0 8 files changed, 160 insertions(+), 166 deletions(-) create mode 100644 model/manifold.py create mode 100644 renderer/Object3DRenderer.py create mode 100644 renderer/__init__.py diff --git a/app.py b/app.py index 232de1c..0b10a98 100644 --- a/app.py +++ b/app.py @@ -12,14 +12,20 @@ server = flask.Flask(__name__) -app = Dash(__name__, external_stylesheets=external_stylesheets, server=server, use_pages=True) # , suppress_callback_exceptions=True +app = Dash( + __name__, + external_stylesheets=external_stylesheets, + server=server, + use_pages=True, + suppress_callback_exceptions=True # this is needed because renderer generate callbacks dynamically with per instance uuids +) app.layout = dbc.Container([ html.H1('ISAS Interactive', style={ "caretColor": "transparent", "userSelect": "none" - } + } ), dbc.Nav([ dbc.NavLink( diff --git a/model/manifold.py b/model/manifold.py new file mode 100644 index 0000000..0135b90 --- /dev/null +++ b/model/manifold.py @@ -0,0 +1,14 @@ +from abc import ABC, abstractmethod + +class Manifold(ABC): + + # generates renderable xyz grid + @abstractmethod + def generate_xyz(self, *args, **kwargs): + pass + + # updates self.sample based on selected distribution and sample options + # also converts the sample output type to xyz coordinates + @abstractmethod + def update_sample(self, selected_distribution, sample_options): + pass \ No newline at end of file diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index 958d8ba..a83ce47 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -1,8 +1,8 @@ import numpy as np from model.distributions.distribution_loader import DistributionLoader from model.distributions.sphere.sphere_distribution import SphereDistribution - -class Sphere: +from model.manifold import Manifold +class Sphere(Manifold): def __init__(self, resolution=50, radius=1): self.xyz = self.generate_xyz(resolution, radius) self.mesh = np.array([]) @@ -18,4 +18,7 @@ def generate_xyz(self, resolution=50, radius=1): y = radius * np.sin(phi) * np.sin(theta) z = radius * np.cos(phi) - return x, y, z \ No newline at end of file + return x, y, z + + def update_sample(self, selected_distribution, sample_options): + self.samples = self.distributions[selected_distribution].sample(sample_options) diff --git a/model/torus/torus.py b/model/torus/torus.py index 954e41d..65b738a 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -1,14 +1,18 @@ import numpy as np from model.distributions.distribution_loader import DistributionLoader from model.distributions.torus.torus_distribution import TorusDistribution +from model.manifold import Manifold -class Torus: +class Torus(Manifold): def __init__(self, resolution=50, r=1, R=3): self.xyz = self.generate_xyz(resolution, r, R) self.mesh = np.array([]) self.samples = np.array([]) self.distributions = DistributionLoader(TorusDistribution, "model.distributions.torus").get_distributions() + self.r = r + self.R = R + def generate_xyz(self, resolution=50, r=1, R=3): t = np.linspace(0, 2*np.pi, resolution) p = np.linspace(0, 2*np.pi, resolution) @@ -16,6 +20,17 @@ def generate_xyz(self, resolution=50, r=1, R=3): return self.t_p_to_xyz(t, p, r, R) + + def update_sample(self, selected_distribution, sample_options): + new_sample = self.distributions[selected_distribution].sample(sample_options) + + if new_sample.size == 0: + self.samples = np.empty((0, 3), dtype=float) + return + + x, y, z = self.t_p_to_xyz(new_sample[:,0], new_sample[:,1], self.r, self.R) + + self.samples = np.column_stack((x, y, z)) @staticmethod diff --git a/pages/sphere.py b/pages/sphere.py index 313614d..2b44d2a 100644 --- a/pages/sphere.py +++ b/pages/sphere.py @@ -8,97 +8,22 @@ from components.split_pane import SplitPane from model.sphere.sphere import Sphere +from renderer.Object3DRenderer import Object3DRenderer dash.register_page(__name__) sphere = Sphere() -x, y, z = sphere.xyz - -fig = go.Figure( - data=[ - go.Surface( - x=x, y=y, z=z, - colorscale="Viridis", - showscale=False - ), - go.Scatter3d( - x=sphere.samples[:, 0] if sphere.samples.size else [], - y=sphere.samples[:, 1] if sphere.samples.size else [], - z=sphere.samples[:, 2] if sphere.samples.size else [], - mode="markers", - marker=dict(size=4, color="red") - ) - ] -) - -fig.update_layout( - scene=dict( - aspectmode='data', - xaxis=dict(visible=False), - yaxis=dict(visible=False), - zaxis=dict(visible=False), - ), - margin=dict(l=0, r=0, t=0, b=0) -) - -@callback( - Output("distribution-options", "children"), - Input("distribution-selector", "value"), -) -def update_curr_distribution(selected_distribution): - options = sphere.distributions[selected_distribution].sample_options - - options_dcc = [opt.to_dash_component(f"id") for id, opt in enumerate(options)] - return options_dcc - -@callback( - Output("graph", "figure"), - Input({"type": "dynamic-option", "index": ALL}, "value"), - Input("distribution-selector", "value"), - Input("distribution-options", "children"), -) -def update_plot(options, selected_distribution, _): - dist_options = sphere.distributions[selected_distribution].sample_options - for opt, new_state in zip(dist_options, options): - opt.update_state(new_state) - - sphere.samples = sphere.distributions[selected_distribution].sample(dist_options) - - data = [ - go.Surface( - x=x, y=y, z=z, - showscale=False, - colorscale="Viridis", - ), - go.Scatter3d( - x=sphere.samples[:, 0], - y=sphere.samples[:, 1], - z=sphere.samples[:, 2], - mode="markers", - marker=dict(size=4, color="red") - ) - ] - return go.Figure(data=data, layout=fig.layout) +renderer = Object3DRenderer(sphere) +options, graph = renderer.get_layout_components() layout = SplitPane( [ - html.Br(), - html.P("Select Distribution:"), - dcc.RadioItems( - id="distribution-selector", - options=(list(sphere.distributions.keys())), - value=sphere.distributions[list(sphere.distributions.keys())[0]].get_name(), - ), - html.Br(), - html.Hr(), - html.Br(), - - html.Div(id="distribution-options"), + *options ], [ - dcc.Graph(id="graph", figure=fig) + *graph ], 30 diff --git a/pages/torus.py b/pages/torus.py index 0addb05..7a007d9 100644 --- a/pages/torus.py +++ b/pages/torus.py @@ -8,96 +8,22 @@ from components.split_pane import SplitPane from model.torus.torus import Torus +from renderer.Object3DRenderer import Object3DRenderer + dash.register_page(__name__) torus = Torus() -x, y, z = torus.xyz - - -fig = go.Figure( - data=[ - go.Surface( - x=x, y=y, z=z, - colorscale="Viridis", - showscale=False - ), - go.Scatter3d( - x=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[0] if torus.samples.size else [], - y=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[1] if torus.samples.size else [], - z=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[2] if torus.samples.size else [], - mode="markers", - marker=dict(size=4, color="red") - ) - ] -) - -fig.update_layout( - scene=dict( - aspectmode='data', - xaxis=dict(visible=False), - yaxis=dict(visible=False), - zaxis=dict(visible=False), - ), - margin=dict(l=0, r=0, t=0, b=0) -) - -@callback( - Output("distribution-options-torus", "children"), - Input("distribution-selector", "value"), -) -def update_curr_distribution(selected_distribution): - options = torus.distributions[selected_distribution].sample_options - - options_dcc = [opt.to_dash_component(f"id") for id, opt in enumerate(options)] - return options_dcc - -@callback( - Output("graph-torus", "figure"), - Input({"type": "dynamic-option", "index": ALL}, "value"), - Input("distribution-selector", "value"), - Input("distribution-options-torus", "children"), -) -def update_plot(options, selected_distribution, _): - dist_options = torus.distributions[selected_distribution].sample_options - for opt, new_state in zip(dist_options, options): - opt.update_state(new_state) - - torus.samples = torus.distributions[selected_distribution].sample(dist_options) - data = [ - go.Surface( - x=x, y=y, z=z, - showscale=False, - colorscale="Viridis", - ), - go.Scatter3d( - x=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[0] if torus.samples.size else [], - y=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[1] if torus.samples.size else [], - z=(Torus.t_p_to_xyz(torus.samples[:, 0], torus.samples[:, 1]))[2] if torus.samples.size else [], - mode="markers", - marker=dict(size=4, color="red") - ) - ] - return go.Figure(data=data, layout=fig.layout) +renderer = Object3DRenderer(torus) +options, graph = renderer.get_layout_components() layout = SplitPane( [ - html.Br(), - html.P("Select Distribution:"), - dcc.RadioItems( - id="distribution-selector", - options=(list(torus.distributions.keys())), - value=torus.distributions[list(torus.distributions.keys())[0]].get_name(), - ), - html.Br(), - html.Hr(), - html.Br(), - - html.Div(id="distribution-options-torus"), + *options ], [ - dcc.Graph(id="graph-torus", figure=fig) + *graph ], 30 diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py new file mode 100644 index 0000000..403ee39 --- /dev/null +++ b/renderer/Object3DRenderer.py @@ -0,0 +1,105 @@ +from dash import html, dcc, callback, Input, Output, ALL +import plotly.graph_objects as go +import uuid + +class Object3DRenderer: + def __init__(self, object_3D): + # dash doesnt like duplicate calback functions + # so each renderer instance gets a uuid for suffixing + self.uuid = str(uuid.uuid4()) + + self.object = object_3D + + self.x, self.y, self.z = self.object.xyz + + + + self.fig = go.Figure( + data=[ + go.Surface( + x=self.x, y=self.y, z=self.z, + colorscale="Viridis", + showscale=False + ), + go.Scatter3d( + x=self.object.samples[:, 0] if self.object.samples.size else [], + y=self.object.samples[:, 1] if self.object.samples.size else [], + z=self.object.samples[:, 2] if self.object.samples.size else [], + mode="markers", + marker=dict(size=4, color="red") + ) + ] + ) + + self.fig.update_layout( + scene=dict( + aspectmode='data', + xaxis=dict(visible=False), + yaxis=dict(visible=False), + zaxis=dict(visible=False), + ), + margin=dict(l=0, r=0, t=0, b=0) + ) + + self._register_callbacks() + + def _register_callbacks(self): + + @callback( + Output(f"distribution-options-{self.uuid}", "children"), + Input("distribution-selector", "value"), + ) + def update_curr_distribution(selected_distribution): + options = self.object.distributions[selected_distribution].sample_options + + options_dcc = [opt.to_dash_component(f"id") for id, opt in enumerate(options)] + return options_dcc + + @callback( + Output(f"graph-{self.uuid}", "figure"), + Input({"type": "dynamic-option", "index": ALL}, "value"), + Input("distribution-selector", "value"), + Input(f"distribution-options-{self.uuid}", "children"), + ) + def update_plot(options, selected_distribution, _): + dist_options = self.object.distributions[selected_distribution].sample_options + for opt, new_state in zip(dist_options, options): + opt.update_state(new_state) + + self.object.update_sample(selected_distribution, dist_options) + + data = [ + go.Surface( + x=self.x, y=self.y, z=self.z, + showscale=False, + colorscale="Viridis", + ), + go.Scatter3d( + x=self.object.samples[:, 0], + y=self.object.samples[:, 1], + z=self.object.samples[:, 2], + mode="markers", + marker=dict(size=4, color="red") + ) + ] + return go.Figure(data=data, layout=self.fig.layout) + + def get_layout_components(self): + options = [ + html.Br(), + html.P("Select Distribution:"), + dcc.RadioItems( + id="distribution-selector", + options=(list(self.object.distributions.keys())), + value=self.object.distributions[list(self.object.distributions.keys())[0]].get_name(), + ), + html.Br(), + html.Hr(), + html.Br(), + + html.Div(id=f"distribution-options-{self.uuid}"), + ] + + graph = [dcc.Graph(id=f"graph-{self.uuid}", figure=self.fig)] + + return options, graph \ No newline at end of file diff --git a/renderer/__init__.py b/renderer/__init__.py new file mode 100644 index 0000000..e69de29 From 9c1db4b67c6eb4056a28a9f3a2de36a4539ee4ec Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 15 Oct 2025 14:05:33 +0200 Subject: [PATCH 008/152] added von mises fisher (via scypy) --- .../sphere/{random_test.py => vonmises_fisher.py} | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) rename model/distributions/sphere/{random_test.py => vonmises_fisher.py} (66%) diff --git a/model/distributions/sphere/random_test.py b/model/distributions/sphere/vonmises_fisher.py similarity index 66% rename from model/distributions/sphere/random_test.py rename to model/distributions/sphere/vonmises_fisher.py index 861fd67..f210f46 100644 --- a/model/distributions/sphere/random_test.py +++ b/model/distributions/sphere/vonmises_fisher.py @@ -2,23 +2,25 @@ from model.distributions.sphere.sphere_distribution import SphereDistribution from util.selectors.slider import Slider import numpy as np +import scipy class SphereDistribution(SphereDistribution): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 1, 5, 15) + Slider("Number of Samples", 1, 50, 1000), + Slider("Kappa (κ)", 1, 5, 15), ] self.distribution_options = [] def get_name(self): - return "Random2" + return "von Mises-Fisher" def sample(self, sample_options): sample_count = sample_options[0].state + kappa = sample_options[1].state - samples = np.random.normal(size=(sample_count, 3)) - samples /= np.linalg.norm(samples, axis=1)[:, np.newaxis] + samples = scipy.stats.vonmises_fisher.rvs(mu=[0,0,1], kappa=kappa, size=sample_count) return samples From 0bbd8de8a4021d31b96e2b2d28e7e974642ccce8 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 15 Oct 2025 16:23:50 +0200 Subject: [PATCH 009/152] fix docker --- dockerfile | 4 ++++ requirements.txt | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/dockerfile b/dockerfile index 7ffee44..8c81a57 100644 --- a/dockerfile +++ b/dockerfile @@ -10,5 +10,9 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./app.py /code/app.py COPY ./assets /code/assets COPY ./pages /code/pages +COPY ./util /code/util +COPY ./components /code/components +COPY ./renderer /code/renderer +COPY ./model /code/model CMD ["python","app.py"] diff --git a/requirements.txt b/requirements.txt index 5635181..c39e144 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,12 @@ blinker==1.7.0 +Brotli==1.1.0 certifi==2024.2.2 +cffi==2.0.0 charset-normalizer==3.3.2 click==8.1.7 colorama==0.4.6 contourpy==1.2.1 +curl_cffi==0.13.0 cycler==0.12.1 dash==2.16.1 dash-bootstrap-components==1.6.0 @@ -24,16 +27,19 @@ Jinja2==3.1.3 kiwisolver==1.4.5 MarkupSafe==2.1.5 matplotlib==3.8.4 +mutagen==1.47.0 nest-asyncio==1.6.0 numpy==1.26.4 packaging==24.0 pandas==2.2.2 pillow==10.3.0 plotly==5.21.0 +pycparser==2.23 +pycryptodomex==3.23.0 pyparsing==3.1.2 python-dateutil==2.9.0.post0 pytz==2024.1 -requests==2.31.0 +requests==2.32.5 retrying==1.3.4 scipy==1.13.0 six==1.16.0 @@ -42,5 +48,7 @@ typing_extensions==4.11.0 tzdata==2024.1 urllib3==2.2.1 vtk==9.5.2 +websockets==15.0.1 Werkzeug==3.0.2 +yt-dlp==2025.9.26 zipp==3.18.1 From 9204c416c1c7d83161f7c9f776685ba66c802f86 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 16 Oct 2025 16:02:53 +0200 Subject: [PATCH 010/152] refractored code so that distributions can have multiple sampling methods they can now be selected hiarchically --- model/distributions/distribution_loader.py | 6 +- .../sphere/sphere_distribution.py | 16 ++-- .../sphere/sphere_sampling_schema.py | 16 ++++ .../distributions/sphere/uniform/__init__.py | 0 .../sphere/{ => uniform}/random.py | 16 ++-- model/distributions/sphere/uniform/uniform.py | 21 ++++++ .../sphere/vonmises_fisher/__init__.py | 0 .../random.py} | 22 +++--- .../sphere/vonmises_fisher/vonmises_fisher.py | 24 ++++++ .../distributions/torus/torus_distribution.py | 18 +++-- .../torus/torus_sampling_schema.py | 16 ++++ model/distributions/torus/uniform/__init__.py | 0 .../torus/{ => uniform}/random.py | 12 +-- model/distributions/torus/uniform/uniform.py | 19 +++++ model/sphere/sphere.py | 7 +- model/torus/torus.py | 6 +- renderer/Object3DRenderer.py | 73 +++++++++++++++---- util/selectors/selector.py | 6 ++ util/selectors/slider.py | 3 + 19 files changed, 215 insertions(+), 66 deletions(-) create mode 100644 model/distributions/sphere/sphere_sampling_schema.py create mode 100644 model/distributions/sphere/uniform/__init__.py rename model/distributions/sphere/{ => uniform}/random.py (60%) create mode 100644 model/distributions/sphere/uniform/uniform.py create mode 100644 model/distributions/sphere/vonmises_fisher/__init__.py rename model/distributions/sphere/{vonmises_fisher.py => vonmises_fisher/random.py} (50%) create mode 100644 model/distributions/sphere/vonmises_fisher/vonmises_fisher.py create mode 100644 model/distributions/torus/torus_sampling_schema.py create mode 100644 model/distributions/torus/uniform/__init__.py rename model/distributions/torus/{ => uniform}/random.py (61%) create mode 100644 model/distributions/torus/uniform/uniform.py diff --git a/model/distributions/distribution_loader.py b/model/distributions/distribution_loader.py index 6ae25c7..8b3aef8 100644 --- a/model/distributions/distribution_loader.py +++ b/model/distributions/distribution_loader.py @@ -22,10 +22,8 @@ def load_distributions(self): if not hasattr(pkg, "__path__"): raise ValueError(f"'{self.distribution_package}' is not a package (missing __path__).") - for finder, name, ispkg in pkgutil.iter_modules(pkg.__path__): - if ispkg: - continue - module = importlib.import_module(f"{pkg.__name__}.{name}") + for finder, name, ispkg in pkgutil.walk_packages(pkg.__path__, prefix=pkg.__name__ + "."): + module = importlib.import_module(name) for _, obj in inspect.getmembers(module, inspect.isclass): # skip abstract, parametered intervace and non-subclasses diff --git a/model/distributions/sphere/sphere_distribution.py b/model/distributions/sphere/sphere_distribution.py index 88f00e1..24bb4e0 100644 --- a/model/distributions/sphere/sphere_distribution.py +++ b/model/distributions/sphere/sphere_distribution.py @@ -1,20 +1,24 @@ from abc import ABC, abstractmethod +from functools import cached_property + class SphereDistribution(ABC): def __init__(self): - self.sample_options = [] self.distribution_options = [] + # a list of objects that implement the torus_sampling_schema interface + # that can be used with this distribution + self.sampling_methods = [] + # Returns the name of the distribution @abstractmethod def get_name(self): pass - # returns samples as a numpy array of shape (n, 3) - @abstractmethod - def sample(self, sample_options): - pass + @cached_property + def sampling_method_dict(self): + return {m.get_name(): m for m in self.sampling_methods} @abstractmethod - def generate_mesh(self, distribution_options): + def get_pdf(self, distribution_options): pass \ No newline at end of file diff --git a/model/distributions/sphere/sphere_sampling_schema.py b/model/distributions/sphere/sphere_sampling_schema.py new file mode 100644 index 0000000..88ca033 --- /dev/null +++ b/model/distributions/sphere/sphere_sampling_schema.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod + +class SphereSamplingSchema(ABC): + def __init__(self): + self.sample_options = [] + + + # Returns the name of the sampling method + @abstractmethod + def get_name(self): + pass + + # returns samples as a numpy array of shape (n, 3) + @abstractmethod + def sample(self, sample_options, distribution_options): + pass \ No newline at end of file diff --git a/model/distributions/sphere/uniform/__init__.py b/model/distributions/sphere/uniform/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/sphere/random.py b/model/distributions/sphere/uniform/random.py similarity index 60% rename from model/distributions/sphere/random.py rename to model/distributions/sphere/uniform/random.py index 80fb46b..ad00f7b 100644 --- a/model/distributions/sphere/random.py +++ b/model/distributions/sphere/uniform/random.py @@ -1,26 +1,22 @@ from abc import ABC, abstractmethod -from model.distributions.sphere.sphere_distribution import SphereDistribution -from util.selectors.slider import Slider import numpy as np -class SphereDistribution(SphereDistribution): +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from util.selectors.slider import Slider + +class SphereUniformRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ Slider("Number of Samples", 10, 100, 1000) ] - self.distribution_options = [] - def get_name(self): return "Random" - def sample(self, sample_options): + def sample(self, sample_options, distribution_options): sample_count = sample_options[0].state samples = np.random.normal(size=(sample_count, 3)) samples /= np.linalg.norm(samples, axis=1)[:, np.newaxis] - return samples - - def generate_mesh(self, distribution_options): - pass \ No newline at end of file + return samples \ No newline at end of file diff --git a/model/distributions/sphere/uniform/uniform.py b/model/distributions/sphere/uniform/uniform.py new file mode 100644 index 0000000..fef5a4e --- /dev/null +++ b/model/distributions/sphere/uniform/uniform.py @@ -0,0 +1,21 @@ +from abc import ABC, abstractmethod +from util.selectors.slider import Slider +import numpy as np + +from model.distributions.sphere.sphere_distribution import SphereDistribution +from model.distributions.sphere.uniform.random import SphereUniformRandomSampling + + +class SphereUniformDistribution(SphereDistribution): + def __init__(self): + self.distribution_options = [] + self.sampling_methods = [ + SphereUniformRandomSampling(), + ] + + + def get_name(self): + return "Uniform" + + def get_pdf(self, distribution_options): + pass \ No newline at end of file diff --git a/model/distributions/sphere/vonmises_fisher/__init__.py b/model/distributions/sphere/vonmises_fisher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/sphere/vonmises_fisher.py b/model/distributions/sphere/vonmises_fisher/random.py similarity index 50% rename from model/distributions/sphere/vonmises_fisher.py rename to model/distributions/sphere/vonmises_fisher/random.py index f210f46..cdfb9f3 100644 --- a/model/distributions/sphere/vonmises_fisher.py +++ b/model/distributions/sphere/vonmises_fisher/random.py @@ -1,28 +1,24 @@ from abc import ABC, abstractmethod -from model.distributions.sphere.sphere_distribution import SphereDistribution -from util.selectors.slider import Slider import numpy as np import scipy -class SphereDistribution(SphereDistribution): +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from util.selectors.slider import Slider + + +class VonMisesRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ Slider("Number of Samples", 1, 50, 1000), - Slider("Kappa (κ)", 1, 5, 15), ] - self.distribution_options = [] - def get_name(self): - return "von Mises-Fisher" + return "Random" - def sample(self, sample_options): + def sample(self, sample_options, distribution_options): sample_count = sample_options[0].state - kappa = sample_options[1].state + kappa = distribution_options[0].state samples = scipy.stats.vonmises_fisher.rvs(mu=[0,0,1], kappa=kappa, size=sample_count) - return samples - - def generate_mesh(self, distribution_options): - pass \ No newline at end of file + return samples \ No newline at end of file diff --git a/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py new file mode 100644 index 0000000..04f11be --- /dev/null +++ b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from model.distributions.sphere.sphere_distribution import SphereDistribution +from util.selectors.slider import Slider +import numpy as np +import scipy + +from model.distributions.sphere.vonmises_fisher.random import VonMisesRandomSampling + + +class vonMisesFisherDistribution(SphereDistribution): + def __init__(self): + self.distribution_options = [ + Slider("Kappa (κ)", 1, 5, 15), + ] + + self.sampling_methods = [ + VonMisesRandomSampling(), + ] + + def get_name(self): + return "von Mises-Fisher" + + def get_pdf(self, distribution_options): + pass \ No newline at end of file diff --git a/model/distributions/torus/torus_distribution.py b/model/distributions/torus/torus_distribution.py index 07b6d75..f02b92b 100644 --- a/model/distributions/torus/torus_distribution.py +++ b/model/distributions/torus/torus_distribution.py @@ -1,21 +1,23 @@ from abc import ABC, abstractmethod +from functools import cached_property class TorusDistribution(ABC): def __init__(self): - self.sample_options = [] self.distribution_options = [] + # a list of objects that implement the torus_sampling_schema interface + # that can be used with this distribution + self.sampling_methods = [] + # Returns the name of the distribution @abstractmethod def get_name(self): pass - - # returns samples as a numpy array of shape (n, 2) - # where (2) are the parameters (t, p) on the torus - @abstractmethod - def sample(self, sample_options): - pass + @cached_property + def sampling_method_dict(self): + return {m.get_name(): m for m in self.sampling_methods} + @abstractmethod - def generate_mesh(self, distribution_options): + def get_pdf(self, distribution_options): pass \ No newline at end of file diff --git a/model/distributions/torus/torus_sampling_schema.py b/model/distributions/torus/torus_sampling_schema.py new file mode 100644 index 0000000..6b0d9c7 --- /dev/null +++ b/model/distributions/torus/torus_sampling_schema.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod + +class TorusSamplingSchema(ABC): + def __init__(self): + self.sample_options = [] + + # Returns the name of the sampling method + @abstractmethod + def get_name(self): + pass + + # returns samples as a numpy array of shape (n, 2) + # where (2) are the parameters (t, p) on the torus + @abstractmethod + def sample(self, sample_options, distribution_options): + pass \ No newline at end of file diff --git a/model/distributions/torus/uniform/__init__.py b/model/distributions/torus/uniform/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/torus/random.py b/model/distributions/torus/uniform/random.py similarity index 61% rename from model/distributions/torus/random.py rename to model/distributions/torus/uniform/random.py index a74abd5..886a415 100644 --- a/model/distributions/torus/random.py +++ b/model/distributions/torus/uniform/random.py @@ -1,19 +1,18 @@ from abc import ABC, abstractmethod -from model.distributions.torus.torus_distribution import TorusDistribution +from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema from util.selectors.slider import Slider import numpy as np -class SphereDistribution(TorusDistribution): +class TorusRandomUniformSampling(TorusSamplingSchema): def __init__(self): self.sample_options = [ Slider("Number of Samples", 10, 100, 1000) ] - self.distribution_options = [] def get_name(self): return "Random" - def sample(self, sample_options): + def sample(self, sample_options, distribution_options): sample_count = sample_options[0].state t = np.random.uniform(0, 2 * np.pi, sample_count) @@ -21,7 +20,4 @@ def sample(self, sample_options): samples = np.column_stack((t, p)) - return samples - - def generate_mesh(self, distribution_options): - pass \ No newline at end of file + return samples \ No newline at end of file diff --git a/model/distributions/torus/uniform/uniform.py b/model/distributions/torus/uniform/uniform.py new file mode 100644 index 0000000..ca2c0de --- /dev/null +++ b/model/distributions/torus/uniform/uniform.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from util.selectors.slider import Slider +import numpy as np + +from model.distributions.torus.torus_distribution import TorusDistribution +from model.distributions.torus.uniform.random import TorusRandomUniformSampling + +class UniformTorusDistribution(TorusDistribution): + def __init__(self): + self.distribution_options = [] + self.sampling_methods = [ + TorusRandomUniformSampling(), + ] + + def get_name(self): + return "Uniform" + + def get_pdf(self, distribution_options): + pass \ No newline at end of file diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index a83ce47..630b08e 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -20,5 +20,8 @@ def generate_xyz(self, resolution=50, radius=1): return x, y, z - def update_sample(self, selected_distribution, sample_options): - self.samples = self.distributions[selected_distribution].sample(sample_options) + def update_sample(self, selected_distribution, selected_sampling_method, sample_options, distribution_options): + dist = self.distributions[selected_distribution] + sampling_method = dist.sampling_method_dict[selected_sampling_method] + self.samples = sampling_method.sample(sample_options, distribution_options) + diff --git a/model/torus/torus.py b/model/torus/torus.py index 65b738a..880695c 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -21,8 +21,10 @@ def generate_xyz(self, resolution=50, r=1, R=3): return self.t_p_to_xyz(t, p, r, R) - def update_sample(self, selected_distribution, sample_options): - new_sample = self.distributions[selected_distribution].sample(sample_options) + def update_sample(self, selected_distribution, selected_sampling_method, sample_options, distribution_options): + dist = self.distributions[selected_distribution] + sampling_method = dist.sampling_method_dict[selected_sampling_method] + new_sample = sampling_method.sample(sample_options, distribution_options) if new_sample.size == 0: self.samples = np.empty((0, 3), dtype=float) diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 403ee39..0fbc3e6 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -1,4 +1,4 @@ -from dash import html, dcc, callback, Input, Output, ALL +from dash import html, dcc, callback, Input, Output, ALL, State import plotly.graph_objects as go import uuid @@ -8,6 +8,7 @@ def __init__(self, object_3D): # so each renderer instance gets a uuid for suffixing self.uuid = str(uuid.uuid4()) + # objects should have atleast one corresponding distribution self.object = object_3D self.x, self.y, self.z = self.object.xyz @@ -43,30 +44,65 @@ def __init__(self, object_3D): self._register_callbacks() + def _register_callbacks(self): + + # updates wich sampling methods are available once distribution is selected @callback( - Output(f"distribution-options-{self.uuid}", "children"), + Output(f"sampling-selector-{self.uuid}", "options"), Input("distribution-selector", "value"), ) - def update_curr_distribution(selected_distribution): - options = self.object.distributions[selected_distribution].sample_options - - options_dcc = [opt.to_dash_component(f"id") for id, opt in enumerate(options)] - return options_dcc + def update_sampling_methods(selected_distribution): + options = list(self.object.distributions[selected_distribution].sampling_method_dict.keys()) + return options + # updates the options (silders, etc) for the selected distribution and sampling method + @callback( + Output(f"distribution-options-{self.uuid}", "children"), + Output(f"sampling-options-{self.uuid}", "children"), + Input("distribution-selector", "value"), + Input(f"sampling-selector-{self.uuid}", "value") + ) + def update_curr_distribution(selected_distribution, selected_sampling): + # ids are given in the same order as options_dist and options_sampling + options_dist = self.object.distributions[selected_distribution].distribution_options + options_dist_dcc = [opt.to_dash_component(f"dist-{id}") for id, opt in enumerate(options_dist)] + + options_sampling = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling] + options_sampling_dcc = [opt.to_dash_component(f"sampling-{id}") for id, opt in enumerate(options_sampling.sample_options)] + return options_dist_dcc, options_sampling_dcc + + # updates the plot based on selected options @callback( Output(f"graph-{self.uuid}", "figure"), Input({"type": "dynamic-option", "index": ALL}, "value"), + State({"type": "dynamic-option", "index": ALL}, "id"), Input("distribution-selector", "value"), + Input(f"sampling-selector-{self.uuid}", "value"), Input(f"distribution-options-{self.uuid}", "children"), ) - def update_plot(options, selected_distribution, _): - dist_options = self.object.distributions[selected_distribution].sample_options - for opt, new_state in zip(dist_options, options): + def update_plot(values, ids, selected_distribution, selected_sampling, _): + dist_options = self.object.distributions[selected_distribution].distribution_options + sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options + + # the order of options might not be guaranteed, so we map them by their ids + id_value = {id_["index"]: v for id_, v in zip(ids, values)} + options_samp = [(k, v) for k, v in id_value.items() if str(k).startswith("sampling-")] + options_dist = [(k, v) for k, v in id_value.items() if str(k).startswith("dist-")] + + # and them sort them, so they are in the same order as sampling_options and dist_options + options_samp = sorted(options_samp, key=lambda x: int(x[0].split("-")[1])) + options_dist = sorted(options_dist, key=lambda x: int(x[0].split("-")[1])) + + + for opt, (id, new_state) in zip(sampling_options, options_samp): + opt.update_state(new_state) + + for opt, (id, new_state) in zip(dist_options, options_dist): opt.update_state(new_state) - self.object.update_sample(selected_distribution, dist_options) + self.object.update_sample(selected_distribution, selected_sampling, sampling_options, dist_options) data = [ go.Surface( @@ -85,19 +121,30 @@ def update_plot(options, selected_distribution, _): return go.Figure(data=data, layout=self.fig.layout) def get_layout_components(self): + initial_distribution = self.object.distributions[list(self.object.distributions.keys())[0]] + initial_sampling_method = initial_distribution.sampling_methods[0].get_name() if initial_distribution.sampling_methods else "no sampling methods found" + initial_sampling_options = [x.get_name() for x in initial_distribution.sampling_methods] + options = [ html.Br(), - html.P("Select Distribution:"), + html.P("Select Distribution and Sampling Method:"), dcc.RadioItems( id="distribution-selector", options=(list(self.object.distributions.keys())), - value=self.object.distributions[list(self.object.distributions.keys())[0]].get_name(), + value=initial_distribution.get_name() if self.object.distributions else "no distributions found", + ), + html.Br(), + dcc.RadioItems( + id=f"sampling-selector-{self.uuid}", + options=(list(initial_sampling_options)), + value=initial_sampling_method, ), html.Br(), html.Hr(), html.Br(), html.Div(id=f"distribution-options-{self.uuid}"), + html.Div(id=f"sampling-options-{self.uuid}"), ] graph = [dcc.Graph(id=f"graph-{self.uuid}", figure=self.fig)] diff --git a/util/selectors/selector.py b/util/selectors/selector.py index 02a1e9d..267489e 100644 --- a/util/selectors/selector.py +++ b/util/selectors/selector.py @@ -1,6 +1,12 @@ from abc import ABC, abstractmethod class Selector(ABC): + def __init__(self): + # this is the id of the last time the to_dash_component method was called + # to_dash_component must have been called at least once for this to be set + self.id = None + + # has the sideeffect of updating self.id @abstractmethod def to_dash_component(self, id): pass \ No newline at end of file diff --git a/util/selectors/slider.py b/util/selectors/slider.py index 25a89db..d88818d 100644 --- a/util/selectors/slider.py +++ b/util/selectors/slider.py @@ -10,6 +10,8 @@ def __init__(self, name, min, state, max): self.min = min self.state = state self.max = max + + self.id = None def calculate_step(self): @@ -47,6 +49,7 @@ def round_nice_number(x): def to_dash_component(self, id): + self.id = id return html.Div([ html.Label(self.name), From c05b6b61f1cd835fae9693a547a26dbc26e56222 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 16 Oct 2025 16:04:04 +0200 Subject: [PATCH 011/152] make slider allow drag updates --- util/selectors/slider.py | 1 + 1 file changed, 1 insertion(+) diff --git a/util/selectors/slider.py b/util/selectors/slider.py index d88818d..f850339 100644 --- a/util/selectors/slider.py +++ b/util/selectors/slider.py @@ -61,6 +61,7 @@ def to_dash_component(self, id): tooltip={"placement": "bottom", "always_visible": True}, step=self.calculate_step(), marks=self.calculate_marks(), + updatemode="drag", ) ]) From 21735ce5e6e03b01552b45b45a75837cc6148d30 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 16 Oct 2025 16:04:58 +0200 Subject: [PATCH 012/152] comment out sphere_vtk test for now --- pages/sphere_vtk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/sphere_vtk.py b/pages/sphere_vtk.py index 70040a0..5e6583d 100644 --- a/pages/sphere_vtk.py +++ b/pages/sphere_vtk.py @@ -10,7 +10,7 @@ from model.sphere.sphere import Sphere -dash.register_page(__name__) +#dash.register_page(__name__) sphere = Sphere() From 6843efd5c9643e435d9fc8823a67c231df8a9c5e Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 16 Oct 2025 19:47:20 +0200 Subject: [PATCH 013/152] added density function rendering for sphere --- .../sphere/sphere_distribution.py | 1 + model/distributions/sphere/uniform/uniform.py | 5 ++- .../sphere/vonmises_fisher/vonmises_fisher.py | 5 ++- .../distributions/torus/torus_distribution.py | 1 + model/manifold.py | 3 ++ model/sphere/sphere.py | 43 +++++++++++++++++++ renderer/Object3DRenderer.py | 13 +++++- 7 files changed, 67 insertions(+), 4 deletions(-) diff --git a/model/distributions/sphere/sphere_distribution.py b/model/distributions/sphere/sphere_distribution.py index 24bb4e0..e0ac3cc 100644 --- a/model/distributions/sphere/sphere_distribution.py +++ b/model/distributions/sphere/sphere_distribution.py @@ -19,6 +19,7 @@ def get_name(self): def sampling_method_dict(self): return {m.get_name(): m for m in self.sampling_methods} + # returns a functions that takes a point [x, y, z] and returns the pdf value at that point @abstractmethod def get_pdf(self, distribution_options): pass \ No newline at end of file diff --git a/model/distributions/sphere/uniform/uniform.py b/model/distributions/sphere/uniform/uniform.py index fef5a4e..79e0e32 100644 --- a/model/distributions/sphere/uniform/uniform.py +++ b/model/distributions/sphere/uniform/uniform.py @@ -18,4 +18,7 @@ def get_name(self): return "Uniform" def get_pdf(self, distribution_options): - pass \ No newline at end of file + # https://math.stackexchange.com/questions/2315341/how-to-write-a-proper-definition-of-the-uniform-distribution-on-unit-sphere + def pdf(x): + return 1/ (4*np.pi) + return pdf \ No newline at end of file diff --git a/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py index 04f11be..d690bc3 100644 --- a/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py +++ b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py @@ -21,4 +21,7 @@ def get_name(self): return "von Mises-Fisher" def get_pdf(self, distribution_options): - pass \ No newline at end of file + kappa = distribution_options[0].state + def pdf(x): + return scipy.stats.vonmises_fisher.pdf(x, mu=[0,0,1], kappa=kappa) + return pdf \ No newline at end of file diff --git a/model/distributions/torus/torus_distribution.py b/model/distributions/torus/torus_distribution.py index f02b92b..a67cf6f 100644 --- a/model/distributions/torus/torus_distribution.py +++ b/model/distributions/torus/torus_distribution.py @@ -18,6 +18,7 @@ def get_name(self): def sampling_method_dict(self): return {m.get_name(): m for m in self.sampling_methods} + # returns a functions that takes a point [x, y, z] and returns the pdf value at that point @abstractmethod def get_pdf(self, distribution_options): pass \ No newline at end of file diff --git a/model/manifold.py b/model/manifold.py index 0135b90..6139f92 100644 --- a/model/manifold.py +++ b/model/manifold.py @@ -11,4 +11,7 @@ def generate_xyz(self, *args, **kwargs): # also converts the sample output type to xyz coordinates @abstractmethod def update_sample(self, selected_distribution, sample_options): + pass + + def generate_trisurf(self, pdf, *args, **kwargs): pass \ No newline at end of file diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index 630b08e..2bc338a 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -1,7 +1,12 @@ import numpy as np +import numpy as np +from scipy.spatial import Delaunay +import plotly.figure_factory as ff + from model.distributions.distribution_loader import DistributionLoader from model.distributions.sphere.sphere_distribution import SphereDistribution from model.manifold import Manifold + class Sphere(Manifold): def __init__(self, resolution=50, radius=1): self.xyz = self.generate_xyz(resolution, radius) @@ -25,3 +30,41 @@ def update_sample(self, selected_distribution, selected_sampling_method, sample_ sampling_method = dist.sampling_method_dict[selected_sampling_method] self.samples = sampling_method.sample(sample_options, distribution_options) + + def generate_trisurf(self, pdf, resolution=50, radius=1, alpha=0.5): + phi = np.linspace(0, np.pi, resolution) + theta = np.linspace(0, 2 * np.pi, resolution) + phi, theta = np.meshgrid(phi, theta) + + phi = phi.flatten() + theta = theta.flatten() + + x = radius * np.sin(phi) * np.cos(theta) + y = radius * np.sin(phi) * np.sin(theta) + z = radius * np.cos(phi) + + points2D = np.vstack([phi,theta]).T + tri = Delaunay(points2D) + simplices = tri.simplices + + xzy = np.column_stack((x, y, z)) + dens = np.apply_along_axis(pdf, 1, xzy).astype(float) + + + # extrude by multiplying by 1 + dens * alpha + # not to scale, but the alpha makes the density function look more clearly + xzy_extruded = xzy * (1 + alpha * dens[:, np.newaxis]) + x, y, z = xzy_extruded[:,0], xzy_extruded[:,1], xzy_extruded[:,2] + + fig = ff.create_trisurf(x=x, y=y, z=z, + simplices=simplices, + show_colorbar=False, + ) + fig.data[0].update( + opacity=0, + color='black' + ) + + + return fig.data + diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 0fbc3e6..3b00a92 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -1,7 +1,8 @@ from dash import html, dcc, callback, Input, Output, ALL, State +import numpy as np import plotly.graph_objects as go import uuid - +import plotly.figure_factory as ff class Object3DRenderer: def __init__(self, object_3D): # dash doesnt like duplicate calback functions @@ -102,6 +103,7 @@ def update_plot(values, ids, selected_distribution, selected_sampling, _): for opt, (id, new_state) in zip(dist_options, options_dist): opt.update_state(new_state) + # samples self.object.update_sample(selected_distribution, selected_sampling, sampling_options, dist_options) data = [ @@ -116,8 +118,15 @@ def update_plot(values, ids, selected_distribution, selected_sampling, _): z=self.object.samples[:, 2], mode="markers", marker=dict(size=4, color="red") - ) + ), ] + + # meshed density function plot plot + pdf = self.object.distributions[selected_distribution].get_pdf(dist_options) + if pdf is not None: + mesh_data = self.object.generate_trisurf(pdf) + data.extend(mesh_data) + return go.Figure(data=data, layout=self.fig.layout) def get_layout_components(self): From 0913cf68b27e357760de96c63ba88275b8583f14 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 17 Oct 2025 11:47:59 +0200 Subject: [PATCH 014/152] fix trisurf crash --- model/sphere/sphere.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index 2bc338a..54c22c4 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -47,18 +47,26 @@ def generate_trisurf(self, pdf, resolution=50, radius=1, alpha=0.5): tri = Delaunay(points2D) simplices = tri.simplices - xzy = np.column_stack((x, y, z)) - dens = np.apply_along_axis(pdf, 1, xzy).astype(float) + xyz = np.column_stack((x, y, z)) + dens = np.apply_along_axis(pdf, 1, xyz).astype(float) + + # clamp color function just below max + # even though colorscale is not shown, this prevents ff.create_trisurf from crashing due to an overflow + def cf(xi, yi, zi, zmin=np.min(z), zmax=np.max(z)): + if zi > zmax: + zi = np.nextafter(zmax, zmin) + return zi # extrude by multiplying by 1 + dens * alpha # not to scale, but the alpha makes the density function look more clearly - xzy_extruded = xzy * (1 + alpha * dens[:, np.newaxis]) - x, y, z = xzy_extruded[:,0], xzy_extruded[:,1], xzy_extruded[:,2] + xyz_extruded = xyz * (1 + alpha * dens[:, np.newaxis]) + x, y, z = xyz_extruded[:,0], xyz_extruded[:,1], xyz_extruded[:,2] fig = ff.create_trisurf(x=x, y=y, z=z, simplices=simplices, show_colorbar=False, + color_func=cf ) fig.data[0].update( opacity=0, From ad6b1f99fa4737175a37e0b8eaf1c82673bc18d6 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 17 Oct 2025 11:58:07 +0200 Subject: [PATCH 015/152] bup Werkzeug version --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c39e144..18cf351 100644 --- a/requirements.txt +++ b/requirements.txt @@ -49,6 +49,6 @@ tzdata==2024.1 urllib3==2.2.1 vtk==9.5.2 websockets==15.0.1 -Werkzeug==3.0.2 +Werkzeug==3.0.3 yt-dlp==2025.9.26 zipp==3.18.1 From f4e5f04cae60692dc656ed0c6487e64ea19fb068 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 20 Oct 2025 13:53:12 +0200 Subject: [PATCH 016/152] performance improvements: introduce patched updates and vectorized calculation --- .../sphere/sphere_distribution.py | 2 +- model/distributions/sphere/uniform/random.py | 2 +- model/distributions/sphere/uniform/uniform.py | 3 +- .../sphere/vonmises_fisher/random.py | 2 +- .../distributions/torus/torus_distribution.py | 2 +- model/distributions/torus/uniform/random.py | 2 +- model/sphere/sphere.py | 6 +- renderer/Object3DRenderer.py | 135 +++++++++++++++--- util/selectors/slider.py | 4 +- 9 files changed, 132 insertions(+), 26 deletions(-) diff --git a/model/distributions/sphere/sphere_distribution.py b/model/distributions/sphere/sphere_distribution.py index e0ac3cc..556e17b 100644 --- a/model/distributions/sphere/sphere_distribution.py +++ b/model/distributions/sphere/sphere_distribution.py @@ -19,7 +19,7 @@ def get_name(self): def sampling_method_dict(self): return {m.get_name(): m for m in self.sampling_methods} - # returns a functions that takes a point [x, y, z] and returns the pdf value at that point + # returns a functions that takes a (N, 3) shape nparray and returns the pdf value at the points @abstractmethod def get_pdf(self, distribution_options): pass \ No newline at end of file diff --git a/model/distributions/sphere/uniform/random.py b/model/distributions/sphere/uniform/random.py index ad00f7b..705203f 100644 --- a/model/distributions/sphere/uniform/random.py +++ b/model/distributions/sphere/uniform/random.py @@ -7,7 +7,7 @@ class SphereUniformRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 10, 100, 1000) + Slider("Number of Samples", 10, 100, 500) ] def get_name(self): diff --git a/model/distributions/sphere/uniform/uniform.py b/model/distributions/sphere/uniform/uniform.py index 79e0e32..2572e0e 100644 --- a/model/distributions/sphere/uniform/uniform.py +++ b/model/distributions/sphere/uniform/uniform.py @@ -20,5 +20,6 @@ def get_name(self): def get_pdf(self, distribution_options): # https://math.stackexchange.com/questions/2315341/how-to-write-a-proper-definition-of-the-uniform-distribution-on-unit-sphere def pdf(x): - return 1/ (4*np.pi) + N = np.shape(x)[0] + return (1/ (4*np.pi)) * np.ones(N) return pdf \ No newline at end of file diff --git a/model/distributions/sphere/vonmises_fisher/random.py b/model/distributions/sphere/vonmises_fisher/random.py index cdfb9f3..618a35a 100644 --- a/model/distributions/sphere/vonmises_fisher/random.py +++ b/model/distributions/sphere/vonmises_fisher/random.py @@ -9,7 +9,7 @@ class VonMisesRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 1, 50, 1000), + Slider("Number of Samples", 1, 50, 500), ] def get_name(self): diff --git a/model/distributions/torus/torus_distribution.py b/model/distributions/torus/torus_distribution.py index a67cf6f..c03e93f 100644 --- a/model/distributions/torus/torus_distribution.py +++ b/model/distributions/torus/torus_distribution.py @@ -18,7 +18,7 @@ def get_name(self): def sampling_method_dict(self): return {m.get_name(): m for m in self.sampling_methods} - # returns a functions that takes a point [x, y, z] and returns the pdf value at that point + # returns a functions that takes a (N, 3) shape nparray and returns the pdf value at the points @abstractmethod def get_pdf(self, distribution_options): pass \ No newline at end of file diff --git a/model/distributions/torus/uniform/random.py b/model/distributions/torus/uniform/random.py index 886a415..3e4d0c9 100644 --- a/model/distributions/torus/uniform/random.py +++ b/model/distributions/torus/uniform/random.py @@ -6,7 +6,7 @@ class TorusRandomUniformSampling(TorusSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 10, 100, 1000) + Slider("Number of Samples", 10, 100, 500) ] def get_name(self): diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index 54c22c4..1dff546 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -14,6 +14,8 @@ def __init__(self, resolution=50, radius=1): self.samples = np.array([]) self.distributions = DistributionLoader(SphereDistribution, "model.distributions.sphere").get_distributions() + self.dist_cache = {} + def generate_xyz(self, resolution=50, radius=1): phi = np.linspace(0, np.pi, resolution) theta = np.linspace(0, 2 * np.pi, resolution) @@ -31,7 +33,7 @@ def update_sample(self, selected_distribution, selected_sampling_method, sample_ self.samples = sampling_method.sample(sample_options, distribution_options) - def generate_trisurf(self, pdf, resolution=50, radius=1, alpha=0.5): + def generate_trisurf(self, pdf, resolution=30, radius=1, alpha=0.5): phi = np.linspace(0, np.pi, resolution) theta = np.linspace(0, 2 * np.pi, resolution) phi, theta = np.meshgrid(phi, theta) @@ -48,7 +50,7 @@ def generate_trisurf(self, pdf, resolution=50, radius=1, alpha=0.5): simplices = tri.simplices xyz = np.column_stack((x, y, z)) - dens = np.apply_along_axis(pdf, 1, xyz).astype(float) + dens = pdf(xyz) # clamp color function just below max # even though colorscale is not shown, this prevents ff.create_trisurf from crashing due to an overflow diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 3b00a92..2909804 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -1,4 +1,5 @@ -from dash import html, dcc, callback, Input, Output, ALL, State +from functools import lru_cache +from dash import html, dcc, callback, Input, Output, ALL, State, Patch import numpy as np import plotly.graph_objects as go import uuid @@ -68,39 +69,41 @@ def update_sampling_methods(selected_distribution): def update_curr_distribution(selected_distribution, selected_sampling): # ids are given in the same order as options_dist and options_sampling options_dist = self.object.distributions[selected_distribution].distribution_options - options_dist_dcc = [opt.to_dash_component(f"dist-{id}") for id, opt in enumerate(options_dist)] + options_dist_dcc = [opt.to_dash_component("dist", id) for id, opt in enumerate(options_dist)] options_sampling = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling] - options_sampling_dcc = [opt.to_dash_component(f"sampling-{id}") for id, opt in enumerate(options_sampling.sample_options)] + options_sampling_dcc = [opt.to_dash_component("sampling", id) for id, opt in enumerate(options_sampling.sample_options)] return options_dist_dcc, options_sampling_dcc - # updates the plot based on selected options + # updates the plot based on selected sampling options @callback( Output(f"graph-{self.uuid}", "figure"), - Input({"type": "dynamic-option", "index": ALL}, "value"), - State({"type": "dynamic-option", "index": ALL}, "id"), + Input({"type": "dist", "index": ALL}, "value"), + State({"type": "dist", "index": ALL}, "id"), + Input({"type": "sampling", "index": ALL}, "value"), + State({"type": "sampling", "index": ALL}, "id"), Input("distribution-selector", "value"), Input(f"sampling-selector-{self.uuid}", "value"), Input(f"distribution-options-{self.uuid}", "children"), ) - def update_plot(values, ids, selected_distribution, selected_sampling, _): + def update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): dist_options = self.object.distributions[selected_distribution].distribution_options sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options # the order of options might not be guaranteed, so we map them by their ids - id_value = {id_["index"]: v for id_, v in zip(ids, values)} - options_samp = [(k, v) for k, v in id_value.items() if str(k).startswith("sampling-")] - options_dist = [(k, v) for k, v in id_value.items() if str(k).startswith("dist-")] + id_value_dist = [(id,v) for id, v in zip(ids_dist, values_dist)] + id_value_samp = [(id,v) for id, v in zip(ids_samp, values_samp)] + # and them sort them, so they are in the same order as sampling_options and dist_options - options_samp = sorted(options_samp, key=lambda x: int(x[0].split("-")[1])) - options_dist = sorted(options_dist, key=lambda x: int(x[0].split("-")[1])) + options_samp_new = sorted(id_value_samp, key=lambda x: int(x[0]["index"])) + options_dist_new = sorted(id_value_dist, key=lambda x: int(x[0]["index"])) - for opt, (id, new_state) in zip(sampling_options, options_samp): + for opt, (id, new_state) in zip(sampling_options, options_samp_new): opt.update_state(new_state) - for opt, (id, new_state) in zip(dist_options, options_dist): + for opt, (id, new_state) in zip(dist_options, options_dist_new): opt.update_state(new_state) # samples @@ -122,12 +125,112 @@ def update_plot(values, ids, selected_distribution, selected_sampling, _): ] # meshed density function plot plot - pdf = self.object.distributions[selected_distribution].get_pdf(dist_options) + + mesh_data = [] + pdf = self.object.distributions[selected_distribution].get_pdf(list(dist_options)) if pdf is not None: mesh_data = self.object.generate_trisurf(pdf) - data.extend(mesh_data) + data.extend(mesh_data) return go.Figure(data=data, layout=self.fig.layout) + + + + + # updates the plot based on selected sampling options + @callback( + Output(f"graph-{self.uuid}", "figure", allow_duplicate=True), + Input({"type": "dist", "index": ALL}, "value"), + State({"type": "dist", "index": ALL}, "id"), + Input({"type": "sampling", "index": ALL}, "value"), + State({"type": "sampling", "index": ALL}, "id"), + Input("distribution-selector", "value"), + Input(f"sampling-selector-{self.uuid}", "value"), + Input(f"distribution-options-{self.uuid}", "children"), + prevent_initial_call='initial_duplicate' + ) + def update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): + dist_options = self.object.distributions[selected_distribution].distribution_options + sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options + + # the order of options might not be guaranteed, so we map them by their ids + id_value_dist = [(id,v) for id, v in zip(ids_dist, values_dist)] + id_value_samp = [(id,v) for id, v in zip(ids_samp, values_samp)] + + + # and them sort them, so they are in the same order as sampling_options and dist_options + options_samp_new = sorted(id_value_samp, key=lambda x: int(x[0]["index"])) + options_dist_new = sorted(id_value_dist, key=lambda x: int(x[0]["index"])) + + + for opt, (id, new_state) in zip(sampling_options, options_samp_new): + opt.update_state(new_state) + + for opt, (id, new_state) in zip(dist_options, options_dist_new): + opt.update_state(new_state) + + # samples + self.object.update_sample(selected_distribution, selected_sampling, sampling_options, dist_options) + + + patched_figure = Patch() + + surface = go.Surface( + x=self.x, y=self.y, z=self.z, + showscale=False, + colorscale="Viridis", + ) + points = go.Scatter3d( + x=self.object.samples[:, 0], + y=self.object.samples[:, 1], + z=self.object.samples[:, 2], + mode="markers", + marker=dict(size=4, color="red") + ) + patched_figure["data"][0] = surface + patched_figure["data"][1] = points + + return patched_figure + + # updates the plot based on selected distribution options + @callback( + Output(f"graph-{self.uuid}", "figure", allow_duplicate=True), + Input({"type": "dist", "index": ALL}, "value"), + State({"type": "dist", "index": ALL}, "id"), + Input("distribution-selector", "value"), + Input(f"sampling-selector-{self.uuid}", "value"), + Input(f"distribution-options-{self.uuid}", "children"), + prevent_initial_call='initial_duplicate' + ) + def update_plot_dist(values_dist, ids_dist, selected_distribution, selected_sampling, _): + dist_options = self.object.distributions[selected_distribution].distribution_options + + # the order of options might not be guaranteed, so we map them by their ids + # and them sort them, so they are in the same order as sampling_options and dist_options + id_value_dist = [(id,v) for id, v in zip(ids_dist, values_dist)] + options_dist_new = sorted(id_value_dist, key=lambda x: int(x[0]["index"])) + + + for opt, (id, new_state) in zip(dist_options, options_dist_new): + opt.update_state(new_state) + + + # meshed density function plot plot + patched_figure = Patch() + + + pdf = self.object.distributions[selected_distribution].get_pdf(list(dist_options)) + if pdf is not None: + mesh_data = self.object.generate_trisurf(pdf)[0] + patched_figure["data"][2] = mesh_data + + else: + del patched_figure["data"][2] + + return patched_figure + + + def get_layout_components(self): initial_distribution = self.object.distributions[list(self.object.distributions.keys())[0]] diff --git a/util/selectors/slider.py b/util/selectors/slider.py index f850339..2364bc7 100644 --- a/util/selectors/slider.py +++ b/util/selectors/slider.py @@ -48,13 +48,13 @@ def round_nice_number(x): return marks - def to_dash_component(self, id): + def to_dash_component(self, _type, id): self.id = id return html.Div([ html.Label(self.name), dcc.Slider( - id={"type": "dynamic-option", "index": id}, + id={"type": _type, "index": id}, min=self.min, max=self.max, value=self.state, From 1b88e61286ebb889e3fbfeb29c7d756e46a9a874 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 20 Oct 2025 16:50:53 +0200 Subject: [PATCH 017/152] use poetry for dependecies --- dockerfile | 19 +- poetry.lock | 2322 ++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 38 + requirements.txt | 99 +- 4 files changed, 2418 insertions(+), 60 deletions(-) create mode 100644 poetry.lock create mode 100644 pyproject.toml diff --git a/dockerfile b/dockerfile index 8c81a57..4853026 100644 --- a/dockerfile +++ b/dockerfile @@ -1,11 +1,10 @@ -FROM python:3.10.12 +FROM python:3.10.12 AS base WORKDIR /code -COPY ./requirements.txt /code/requirements.txt - -RUN pip install --upgrade pip -RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt +RUN pip install --no-cache-dir poetry +COPY pyproject.toml poetry.lock /code/ +RUN poetry sync --no-root --without dev COPY ./app.py /code/app.py COPY ./assets /code/assets @@ -15,4 +14,12 @@ COPY ./components /code/components COPY ./renderer /code/renderer COPY ./model /code/model -CMD ["python","app.py"] + +FROM base AS tests +RUN apt-get update && apt-get install -y chromium chromium-driver +RUN poetry sync --no-root --with dev +COPY ./tests /code/tests + + +FROM base AS prod +CMD ["poetry", "run", "python","app.py"] diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..b62b469 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,2322 @@ +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. + +[[package]] +name = "attrs" +version = "25.4.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, +] + +[[package]] +name = "beautifulsoup4" +version = "4.14.2" +description = "Screen-scraping library" +optional = false +python-versions = ">=3.7.0" +groups = ["dev"] +files = [ + {file = "beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515"}, + {file = "beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e"}, +] + +[package.dependencies] +soupsieve = ">1.2" +typing-extensions = ">=4.0.0" + +[package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] +html5lib = ["html5lib"] +lxml = ["lxml"] + +[[package]] +name = "blinker" +version = "1.9.0" +description = "Fast, simple object-to-object and broadcast signaling" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, + {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, +] + +[[package]] +name = "certifi" +version = "2025.10.5" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, + {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, +] + +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +markers = "platform_python_implementation != \"PyPy\" or os_name == \"nt\" and implementation_name != \"pypy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + +[[package]] +name = "click" +version = "8.3.0" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main", "dev"] +files = [ + {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, + {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} + +[[package]] +name = "contourpy" +version = "1.3.2" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934"}, + {file = "contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512"}, + {file = "contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631"}, + {file = "contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f"}, + {file = "contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2"}, + {file = "contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0"}, + {file = "contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a"}, + {file = "contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445"}, + {file = "contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab"}, + {file = "contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7"}, + {file = "contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83"}, + {file = "contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd"}, + {file = "contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f"}, + {file = "contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878"}, + {file = "contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2"}, + {file = "contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415"}, + {file = "contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe"}, + {file = "contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441"}, + {file = "contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e"}, + {file = "contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912"}, + {file = "contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73"}, + {file = "contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb"}, + {file = "contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85"}, + {file = "contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841"}, + {file = "contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422"}, + {file = "contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef"}, + {file = "contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f"}, + {file = "contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9"}, + {file = "contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f"}, + {file = "contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532"}, + {file = "contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b"}, + {file = "contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52"}, + {file = "contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd"}, + {file = "contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1"}, + {file = "contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69"}, + {file = "contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c"}, + {file = "contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16"}, + {file = "contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad"}, + {file = "contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0"}, + {file = "contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5"}, + {file = "contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5"}, + {file = "contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54"}, +] + +[package.dependencies] +numpy = ">=1.23" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.15.0)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] + +[[package]] +name = "cryptography" +version = "46.0.3" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["dev"] +files = [ + {file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"}, + {file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"}, + {file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"}, + {file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"}, + {file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"}, + {file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"}, + {file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"}, + {file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"}, + {file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"}, + {file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"}, + {file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"}, + {file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"}, + {file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"}, + {file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"}, + {file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"}, + {file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"}, + {file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"}, + {file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"}, + {file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"}, + {file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"}, +] + +[package.dependencies] +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} +typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] + +[[package]] +name = "cycler" +version = "0.12.1" +description = "Composable style cycles" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, + {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, +] + +[package.extras] +docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] +tests = ["pytest", "pytest-cov", "pytest-xdist"] + +[[package]] +name = "dash" +version = "2.18.2" +description = "A Python framework for building reactive web-apps. Developed by Plotly." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "dash-2.18.2-py3-none-any.whl", hash = "sha256:0ce0479d1bc958e934630e2de7023b8a4558f23ce1f9f5a4b34b65eb3903a869"}, + {file = "dash-2.18.2.tar.gz", hash = "sha256:20e8404f73d0fe88ce2eae33c25bbc513cbe52f30d23a401fa5f24dbb44296c8"}, +] + +[package.dependencies] +beautifulsoup4 = {version = ">=4.8.2", optional = true, markers = "extra == \"testing\""} +cryptography = {version = "*", optional = true, markers = "extra == \"testing\""} +dash-core-components = "2.0.0" +dash-html-components = "2.0.0" +dash-table = "5.0.0" +dash-testing-stub = {version = ">=0.0.2", optional = true, markers = "extra == \"testing\""} +Flask = ">=1.0.4,<3.1" +importlib-metadata = "*" +lxml = {version = ">=4.6.2", optional = true, markers = "extra == \"testing\""} +multiprocess = {version = ">=0.70.12", optional = true, markers = "extra == \"testing\""} +nest-asyncio = "*" +percy = {version = ">=2.0.2", optional = true, markers = "extra == \"testing\""} +plotly = ">=5.0.0" +psutil = {version = ">=5.8.0", optional = true, markers = "extra == \"testing\""} +pytest = {version = ">=6.0.2", optional = true, markers = "extra == \"testing\""} +requests = [ + {version = "*"}, + {version = ">=2.21.0", extras = ["security"], optional = true, markers = "extra == \"testing\""}, +] +retrying = "*" +selenium = {version = ">=3.141.0,<=4.2.0", optional = true, markers = "extra == \"testing\""} +setuptools = "*" +typing-extensions = ">=4.1.1" +waitress = {version = ">=1.4.4", optional = true, markers = "extra == \"testing\""} +Werkzeug = "<3.1" + +[package.extras] +celery = ["celery[redis] (>=5.1.2)", "redis (>=3.5.3)"] +ci = ["black (==22.3.0)", "dash-dangerously-set-inner-html", "dash-flow-example (==0.0.5)", "flake8 (==7.0.0)", "flaky (==3.8.1)", "flask-talisman (==1.0.0)", "jupyterlab (<4.0.0)", "mimesis (<=11.1.0)", "mock (==4.0.3)", "numpy (<=1.26.3)", "openpyxl", "orjson (==3.10.3)", "pandas (>=1.4.0)", "pyarrow", "pylint (==3.0.3)", "pytest-mock", "pytest-rerunfailures", "pytest-sugar (==0.9.6)", "pyzmq (==25.1.2)", "xlrd (>=2.0.1)"] +compress = ["flask-compress"] +dev = ["PyYAML (>=5.4.1)", "coloredlogs (>=15.0.1)", "fire (>=0.4.0)"] +diskcache = ["diskcache (>=5.2.1)", "multiprocess (>=0.70.12)", "psutil (>=5.8.0)"] +testing = ["beautifulsoup4 (>=4.8.2)", "cryptography", "dash-testing-stub (>=0.0.2)", "lxml (>=4.6.2)", "multiprocess (>=0.70.12)", "percy (>=2.0.2)", "psutil (>=5.8.0)", "pytest (>=6.0.2)", "requests[security] (>=2.21.0)", "selenium (>=3.141.0,<=4.2.0)", "waitress (>=1.4.4)"] + +[[package]] +name = "dash-bootstrap-components" +version = "1.7.1" +description = "Bootstrap themed components for use in Plotly Dash" +optional = false +python-versions = "<4,>=3.9" +groups = ["main"] +files = [ + {file = "dash_bootstrap_components-1.7.1-py3-none-any.whl", hash = "sha256:5e8eae7ee1d013f69e272c68c1015b53ab71802460152088f33fffa90d245199"}, + {file = "dash_bootstrap_components-1.7.1.tar.gz", hash = "sha256:30d48340d6dc89831d6c06e400cd4236f0d5363562c05b2a922f21545695a082"}, +] + +[package.dependencies] +dash = ">=2.0.0" + +[package.extras] +pandas = ["numpy (>=2.0.2)", "pandas (>=2.2.3)"] + +[[package]] +name = "dash-core-components" +version = "2.0.0" +description = "Core component suite for Dash" +optional = false +python-versions = "*" +groups = ["main", "dev"] +files = [ + {file = "dash_core_components-2.0.0-py3-none-any.whl", hash = "sha256:52b8e8cce13b18d0802ee3acbc5e888cb1248a04968f962d63d070400af2e346"}, + {file = "dash_core_components-2.0.0.tar.gz", hash = "sha256:c6733874af975e552f95a1398a16c2ee7df14ce43fa60bb3718a3c6e0b63ffee"}, +] + +[[package]] +name = "dash-html-components" +version = "2.0.0" +description = "Vanilla HTML components for Dash" +optional = false +python-versions = "*" +groups = ["main", "dev"] +files = [ + {file = "dash_html_components-2.0.0-py3-none-any.whl", hash = "sha256:b42cc903713c9706af03b3f2548bda4be7307a7cf89b7d6eae3da872717d1b63"}, + {file = "dash_html_components-2.0.0.tar.gz", hash = "sha256:8703a601080f02619a6390998e0b3da4a5daabe97a1fd7a9cebc09d015f26e50"}, +] + +[[package]] +name = "dash-mantine-components" +version = "0.14.7" +description = "Plotly Dash Components based on Mantine" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "dash_mantine_components-0.14.7-py3-none-any.whl", hash = "sha256:be8e96abff5413852afbba497577892f931f93d686b375f35d180ffb4e1e1c4b"}, + {file = "dash_mantine_components-0.14.7.tar.gz", hash = "sha256:8157a98aa294406fa2ac884407b59a5a833092804e511a8fe782f3ee978c9b44"}, +] + +[package.dependencies] +dash = ">=2" + +[package.extras] +dev = ["black", "build", "dash[ci,dev,testing] (>=2.0)", "pytest (<8.1.0)", "pyyaml (>=5.0)", "selenium (<4.3.0)", "wheel"] + +[[package]] +name = "dash-resizable-panels" +version = "0.1.0" +description = "Dash component for resizable panel groups/layouts" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "dash_resizable_panels-0.1.0-py3-none-any.whl", hash = "sha256:d13a2ab809415c132266698bc4f0f22221f2202fa2c37bd1b6fce7158f334636"}, + {file = "dash_resizable_panels-0.1.0.tar.gz", hash = "sha256:e93243052168e8ba21fe09e5ffd7a0366de21aeab694122e2365ef156e2f8a57"}, +] + +[[package]] +name = "dash-table" +version = "5.0.0" +description = "Dash table" +optional = false +python-versions = "*" +groups = ["main", "dev"] +files = [ + {file = "dash_table-5.0.0-py3-none-any.whl", hash = "sha256:19036fa352bb1c11baf38068ec62d172f0515f73ca3276c79dee49b95ddc16c9"}, + {file = "dash_table-5.0.0.tar.gz", hash = "sha256:18624d693d4c8ef2ddec99a6f167593437a7ea0bf153aa20f318c170c5bc7308"}, +] + +[[package]] +name = "dash-testing-stub" +version = "0.0.2" +description = "Package installed with dash[testing] for optional loading of pytest dash plugin." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "dash-testing-stub-0.0.2.tar.gz", hash = "sha256:0a98f7da9fe41dd3a37d781bc1d5672319448fdf98e47fd867aff2123171a357"}, + {file = "dash_testing_stub-0.0.2-py3-none-any.whl", hash = "sha256:a44d530a77e1ede9c6528be4b5951f34c6109b419a09f2691422375ffa7d09de"}, +] + +[[package]] +name = "dash-vtk" +version = "0.0.9" +description = "React based declarative usage of vtk.js for Dash" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "dash_vtk-0.0.9-py3-none-any.whl", hash = "sha256:107a9df32c72acf681b265c454f315879eb6f1bac9da37d4792d3fe1ba21caf7"}, + {file = "dash_vtk-0.0.9.tar.gz", hash = "sha256:27e9b944963013aa5d52db38c8e72f5d315e59084902d35b2708080ce09ee578"}, +] + +[package.dependencies] +dash = "*" +vtk = "*" + +[[package]] +name = "dill" +version = "0.4.0" +description = "serialize all of Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, + {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "flask" +version = "3.0.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3"}, + {file = "flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842"}, +] + +[package.dependencies] +blinker = ">=1.6.2" +click = ">=8.1.3" +itsdangerous = ">=2.1.2" +Jinja2 = ">=3.1.2" +Werkzeug = ">=3.0.0" + +[package.extras] +async = ["asgiref (>=3.2)"] +dotenv = ["python-dotenv"] + +[[package]] +name = "fonttools" +version = "4.60.1" +description = "Tools to manipulate font files" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9a52f254ce051e196b8fe2af4634c2d2f02c981756c6464dc192f1b6050b4e28"}, + {file = "fonttools-4.60.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7420a2696a44650120cdd269a5d2e56a477e2bfa9d95e86229059beb1c19e15"}, + {file = "fonttools-4.60.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee0c0b3b35b34f782afc673d503167157094a16f442ace7c6c5e0ca80b08f50c"}, + {file = "fonttools-4.60.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:282dafa55f9659e8999110bd8ed422ebe1c8aecd0dc396550b038e6c9a08b8ea"}, + {file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4ba4bd646e86de16160f0fb72e31c3b9b7d0721c3e5b26b9fa2fc931dfdb2652"}, + {file = "fonttools-4.60.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:0b0835ed15dd5b40d726bb61c846a688f5b4ce2208ec68779bc81860adb5851a"}, + {file = "fonttools-4.60.1-cp310-cp310-win32.whl", hash = "sha256:1525796c3ffe27bb6268ed2a1bb0dcf214d561dfaf04728abf01489eb5339dce"}, + {file = "fonttools-4.60.1-cp310-cp310-win_amd64.whl", hash = "sha256:268ecda8ca6cb5c4f044b1fb9b3b376e8cd1b361cef275082429dc4174907038"}, + {file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7b4c32e232a71f63a5d00259ca3d88345ce2a43295bb049d21061f338124246f"}, + {file = "fonttools-4.60.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3630e86c484263eaac71d117085d509cbcf7b18f677906824e4bace598fb70d2"}, + {file = "fonttools-4.60.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5c1015318e4fec75dd4943ad5f6a206d9727adf97410d58b7e32ab644a807914"}, + {file = "fonttools-4.60.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e6c58beb17380f7c2ea181ea11e7db8c0ceb474c9dd45f48e71e2cb577d146a1"}, + {file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec3681a0cb34c255d76dd9d865a55f260164adb9fa02628415cdc2d43ee2c05d"}, + {file = "fonttools-4.60.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f4b5c37a5f40e4d733d3bbaaef082149bee5a5ea3156a785ff64d949bd1353fa"}, + {file = "fonttools-4.60.1-cp311-cp311-win32.whl", hash = "sha256:398447f3d8c0c786cbf1209711e79080a40761eb44b27cdafffb48f52bcec258"}, + {file = "fonttools-4.60.1-cp311-cp311-win_amd64.whl", hash = "sha256:d066ea419f719ed87bc2c99a4a4bfd77c2e5949cb724588b9dd58f3fd90b92bf"}, + {file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc"}, + {file = "fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877"}, + {file = "fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c"}, + {file = "fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401"}, + {file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903"}, + {file = "fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed"}, + {file = "fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6"}, + {file = "fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383"}, + {file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb"}, + {file = "fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4"}, + {file = "fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c"}, + {file = "fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77"}, + {file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199"}, + {file = "fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c"}, + {file = "fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272"}, + {file = "fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac"}, + {file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3"}, + {file = "fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85"}, + {file = "fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537"}, + {file = "fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003"}, + {file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08"}, + {file = "fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99"}, + {file = "fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6"}, + {file = "fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987"}, + {file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299"}, + {file = "fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01"}, + {file = "fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801"}, + {file = "fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc"}, + {file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc"}, + {file = "fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed"}, + {file = "fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259"}, + {file = "fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c"}, + {file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:122e1a8ada290423c493491d002f622b1992b1ab0b488c68e31c413390dc7eb2"}, + {file = "fonttools-4.60.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a140761c4ff63d0cb9256ac752f230460ee225ccef4ad8f68affc723c88e2036"}, + {file = "fonttools-4.60.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eae96373e4b7c9e45d099d7a523444e3554360927225c1cdae221a58a45b856"}, + {file = "fonttools-4.60.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:596ecaca36367027d525b3b426d8a8208169d09edcf8c7506aceb3a38bfb55c7"}, + {file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ee06fc57512144d8b0445194c2da9f190f61ad51e230f14836286470c99f854"}, + {file = "fonttools-4.60.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b42d86938e8dda1cd9a1a87a6d82f1818eaf933348429653559a458d027446da"}, + {file = "fonttools-4.60.1-cp39-cp39-win32.whl", hash = "sha256:8b4eb332f9501cb1cd3d4d099374a1e1306783ff95489a1026bde9eb02ccc34a"}, + {file = "fonttools-4.60.1-cp39-cp39-win_amd64.whl", hash = "sha256:7473a8ed9ed09aeaa191301244a5a9dbe46fe0bf54f9d6cd21d83044c3321217"}, + {file = "fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb"}, + {file = "fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9"}, +] + +[package.extras] +all = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\"", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0) ; python_version <= \"3.12\"", "xattr ; sys_platform == \"darwin\"", "zopfli (>=0.1.4)"] +graphite = ["lz4 (>=1.7.4.2)"] +interpolatable = ["munkres ; platform_python_implementation == \"PyPy\"", "pycairo", "scipy ; platform_python_implementation != \"PyPy\""] +lxml = ["lxml (>=4.0)"] +pathops = ["skia-pathops (>=0.5.0)"] +plot = ["matplotlib"] +repacker = ["uharfbuzz (>=0.23.0)"] +symfont = ["sympy"] +type1 = ["xattr ; sys_platform == \"darwin\""] +unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""] +woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, + {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, +] + +[package.dependencies] +zipp = ">=3.20" + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +perf = ["ipython"] +test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["pytest-mypy"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "kiwisolver" +version = "1.4.9" +description = "A fast implementation of the Cassowary constraint solver" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b"}, + {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f"}, + {file = "kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf"}, + {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9"}, + {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415"}, + {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b"}, + {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154"}, + {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48"}, + {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220"}, + {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586"}, + {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634"}, + {file = "kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611"}, + {file = "kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536"}, + {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16"}, + {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089"}, + {file = "kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543"}, + {file = "kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61"}, + {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1"}, + {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872"}, + {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26"}, + {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028"}, + {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771"}, + {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a"}, + {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464"}, + {file = "kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2"}, + {file = "kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7"}, + {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999"}, + {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2"}, + {file = "kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14"}, + {file = "kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04"}, + {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752"}, + {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77"}, + {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198"}, + {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d"}, + {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab"}, + {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2"}, + {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145"}, + {file = "kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54"}, + {file = "kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60"}, + {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8"}, + {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2"}, + {file = "kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f"}, + {file = "kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098"}, + {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed"}, + {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525"}, + {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78"}, + {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b"}, + {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799"}, + {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3"}, + {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c"}, + {file = "kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d"}, + {file = "kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07"}, + {file = "kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c"}, + {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386"}, + {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552"}, + {file = "kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3"}, + {file = "kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58"}, + {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4"}, + {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df"}, + {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6"}, + {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5"}, + {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf"}, + {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5"}, + {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce"}, + {file = "kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7"}, + {file = "kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891"}, + {file = "kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32"}, + {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527"}, + {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771"}, + {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e"}, + {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9"}, + {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb"}, + {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5"}, + {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa"}, + {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2"}, + {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f"}, + {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1"}, + {file = "kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d"}, +] + +[[package]] +name = "lxml" +version = "6.0.2" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e77dd455b9a16bbd2a5036a63ddbd479c19572af81b624e79ef422f929eef388"}, + {file = "lxml-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5d444858b9f07cefff6455b983aea9a67f7462ba1f6cbe4a21e8bf6791bf2153"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f952dacaa552f3bb8834908dddd500ba7d508e6ea6eb8c52eb2d28f48ca06a31"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:71695772df6acea9f3c0e59e44ba8ac50c4f125217e84aab21074a1a55e7e5c9"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f68764f35fd78d7c4cc4ef209a184c38b65440378013d24b8aecd327c3e0c8"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:058027e261afed589eddcfe530fcc6f3402d7fd7e89bfd0532df82ebc1563dba"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8ffaeec5dfea5881d4c9d8913a32d10cfe3923495386106e4a24d45300ef79c"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:f2e3b1a6bb38de0bc713edd4d612969dd250ca8b724be8d460001a387507021c"}, + {file = "lxml-6.0.2-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d6690ec5ec1cce0385cb20896b16be35247ac8c2046e493d03232f1c2414d321"}, + {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f2a50c3c1d11cad0ebebbac357a97b26aa79d2bcaf46f256551152aa85d3a4d1"}, + {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3efe1b21c7801ffa29a1112fab3b0f643628c30472d507f39544fd48e9549e34"}, + {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:59c45e125140b2c4b33920d21d83681940ca29f0b83f8629ea1a2196dc8cfe6a"}, + {file = "lxml-6.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:452b899faa64f1805943ec1c0c9ebeaece01a1af83e130b69cdefeda180bb42c"}, + {file = "lxml-6.0.2-cp310-cp310-win32.whl", hash = "sha256:1e786a464c191ca43b133906c6903a7e4d56bef376b75d97ccbb8ec5cf1f0a4b"}, + {file = "lxml-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:dacf3c64ef3f7440e3167aa4b49aa9e0fb99e0aa4f9ff03795640bf94531bcb0"}, + {file = "lxml-6.0.2-cp310-cp310-win_arm64.whl", hash = "sha256:45f93e6f75123f88d7f0cfd90f2d05f441b808562bf0bc01070a00f53f5028b5"}, + {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:13e35cbc684aadf05d8711a5d1b5857c92e5e580efa9a0d2be197199c8def607"}, + {file = "lxml-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b1675e096e17c6fe9c0e8c81434f5736c0739ff9ac6123c87c2d452f48fc938"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac6e5811ae2870953390452e3476694196f98d447573234592d30488147404d"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5aa0fc67ae19d7a64c3fe725dc9a1bb11f80e01f78289d05c6f62545affec438"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de496365750cc472b4e7902a485d3f152ecf57bd3ba03ddd5578ed8ceb4c5964"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:200069a593c5e40b8f6fc0d84d86d970ba43138c3e68619ffa234bc9bb806a4d"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d2de809c2ee3b888b59f995625385f74629707c9355e0ff856445cdcae682b7"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:b2c3da8d93cf5db60e8858c17684c47d01fee6405e554fb55018dd85fc23b178"}, + {file = "lxml-6.0.2-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:442de7530296ef5e188373a1ea5789a46ce90c4847e597856570439621d9c553"}, + {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2593c77efde7bfea7f6389f1ab249b15ed4aa5bc5cb5131faa3b843c429fbedb"}, + {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:3e3cb08855967a20f553ff32d147e14329b3ae70ced6edc2f282b94afbc74b2a"}, + {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2ed6c667fcbb8c19c6791bbf40b7268ef8ddf5a96940ba9404b9f9a304832f6c"}, + {file = "lxml-6.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b8f18914faec94132e5b91e69d76a5c1d7b0c73e2489ea8929c4aaa10b76bbf7"}, + {file = "lxml-6.0.2-cp311-cp311-win32.whl", hash = "sha256:6605c604e6daa9e0d7f0a2137bdc47a2e93b59c60a65466353e37f8272f47c46"}, + {file = "lxml-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e5867f2651016a3afd8dd2c8238baa66f1e2802f44bc17e236f547ace6647078"}, + {file = "lxml-6.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:4197fb2534ee05fd3e7afaab5d8bfd6c2e186f65ea7f9cd6a82809c887bd1285"}, + {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a59f5448ba2ceccd06995c95ea59a7674a10de0810f2ce90c9006f3cbc044456"}, + {file = "lxml-6.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8113639f3296706fbac34a30813929e29247718e88173ad849f57ca59754924"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a8bef9b9825fa8bc816a6e641bb67219489229ebc648be422af695f6e7a4fa7f"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:65ea18d710fd14e0186c2f973dc60bb52039a275f82d3c44a0e42b43440ea534"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c371aa98126a0d4c739ca93ceffa0fd7a5d732e3ac66a46e74339acd4d334564"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:700efd30c0fa1a3581d80a748157397559396090a51d306ea59a70020223d16f"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c33e66d44fe60e72397b487ee92e01da0d09ba2d66df8eae42d77b6d06e5eba0"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90a345bbeaf9d0587a3aaffb7006aa39ccb6ff0e96a57286c0cb2fd1520ea192"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:064fdadaf7a21af3ed1dcaa106b854077fbeada827c18f72aec9346847cd65d0"}, + {file = "lxml-6.0.2-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fbc74f42c3525ac4ffa4b89cbdd00057b6196bcefe8bce794abd42d33a018092"}, + {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ddff43f702905a4e32bc24f3f2e2edfe0f8fde3277d481bffb709a4cced7a1f"}, + {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6da5185951d72e6f5352166e3da7b0dc27aa70bd1090b0eb3f7f7212b53f1bb8"}, + {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:57a86e1ebb4020a38d295c04fc79603c7899e0df71588043eb218722dabc087f"}, + {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:2047d8234fe735ab77802ce5f2297e410ff40f5238aec569ad7c8e163d7b19a6"}, + {file = "lxml-6.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f91fd2b2ea15a6800c8e24418c0775a1694eefc011392da73bc6cef2623b322"}, + {file = "lxml-6.0.2-cp312-cp312-win32.whl", hash = "sha256:3ae2ce7d6fedfb3414a2b6c5e20b249c4c607f72cb8d2bb7cc9c6ec7c6f4e849"}, + {file = "lxml-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:72c87e5ee4e58a8354fb9c7c84cbf95a1c8236c127a5d1b7683f04bed8361e1f"}, + {file = "lxml-6.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:61cb10eeb95570153e0c0e554f58df92ecf5109f75eacad4a95baa709e26c3d6"}, + {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9b33d21594afab46f37ae58dfadd06636f154923c4e8a4d754b0127554eb2e77"}, + {file = "lxml-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c8963287d7a4c5c9a432ff487c52e9c5618667179c18a204bdedb27310f022f"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1941354d92699fb5ffe6ed7b32f9649e43c2feb4b97205f75866f7d21aa91452"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb2f6ca0ae2d983ded09357b84af659c954722bbf04dea98030064996d156048"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb2a12d704f180a902d7fa778c6d71f36ceb7b0d317f34cdc76a5d05aa1dd1df"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:6ec0e3f745021bfed19c456647f0298d60a24c9ff86d9d051f52b509663feeb1"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:846ae9a12d54e368933b9759052d6206a9e8b250291109c48e350c1f1f49d916"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef9266d2aa545d7374938fb5c484531ef5a2ec7f2d573e62f8ce722c735685fd"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:4077b7c79f31755df33b795dc12119cb557a0106bfdab0d2c2d97bd3cf3dffa6"}, + {file = "lxml-6.0.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a7c5d5e5f1081955358533be077166ee97ed2571d6a66bdba6ec2f609a715d1a"}, + {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:8f8d0cbd0674ee89863a523e6994ac25fd5be9c8486acfc3e5ccea679bad2679"}, + {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2cbcbf6d6e924c28f04a43f3b6f6e272312a090f269eff68a2982e13e5d57659"}, + {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dfb874cfa53340009af6bdd7e54ebc0d21012a60a4e65d927c2e477112e63484"}, + {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fb8dae0b6b8b7f9e96c26fdd8121522ce5de9bb5538010870bd538683d30e9a2"}, + {file = "lxml-6.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:358d9adae670b63e95bc59747c72f4dc97c9ec58881d4627fe0120da0f90d314"}, + {file = "lxml-6.0.2-cp313-cp313-win32.whl", hash = "sha256:e8cd2415f372e7e5a789d743d133ae474290a90b9023197fd78f32e2dc6873e2"}, + {file = "lxml-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:b30d46379644fbfc3ab81f8f82ae4de55179414651f110a1514f0b1f8f6cb2d7"}, + {file = "lxml-6.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:13dcecc9946dca97b11b7c40d29fba63b55ab4170d3c0cf8c0c164343b9bfdcf"}, + {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:b0c732aa23de8f8aec23f4b580d1e52905ef468afb4abeafd3fec77042abb6fe"}, + {file = "lxml-6.0.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4468e3b83e10e0317a89a33d28f7aeba1caa4d1a6fd457d115dd4ffe90c5931d"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:abd44571493973bad4598a3be7e1d807ed45aa2adaf7ab92ab7c62609569b17d"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:370cd78d5855cfbffd57c422851f7d3864e6ae72d0da615fca4dad8c45d375a5"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:901e3b4219fa04ef766885fb40fa516a71662a4c61b80c94d25336b4934b71c0"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:a4bf42d2e4cf52c28cc1812d62426b9503cdb0c87a6de81442626aa7d69707ba"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2c7fdaa4d7c3d886a42534adec7cfac73860b89b4e5298752f60aa5984641a0"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98a5e1660dc7de2200b00d53fa00bcd3c35a3608c305d45a7bbcaf29fa16e83d"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:dc051506c30b609238d79eda75ee9cab3e520570ec8219844a72a46020901e37"}, + {file = "lxml-6.0.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8799481bbdd212470d17513a54d568f44416db01250f49449647b5ab5b5dccb9"}, + {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9261bb77c2dab42f3ecd9103951aeca2c40277701eb7e912c545c1b16e0e4917"}, + {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:65ac4a01aba353cfa6d5725b95d7aed6356ddc0a3cd734de00124d285b04b64f"}, + {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:b22a07cbb82fea98f8a2fd814f3d1811ff9ed76d0fc6abc84eb21527596e7cc8"}, + {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d759cdd7f3e055d6bc8d9bec3ad905227b2e4c785dc16c372eb5b5e83123f48a"}, + {file = "lxml-6.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:945da35a48d193d27c188037a05fec5492937f66fb1958c24fc761fb9d40d43c"}, + {file = "lxml-6.0.2-cp314-cp314-win32.whl", hash = "sha256:be3aaa60da67e6153eb15715cc2e19091af5dc75faef8b8a585aea372507384b"}, + {file = "lxml-6.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa25afbadead523f7001caf0c2382afd272c315a033a7b06336da2637d92d6ed"}, + {file = "lxml-6.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:063eccf89df5b24e361b123e257e437f9e9878f425ee9aae3144c77faf6da6d8"}, + {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:6162a86d86893d63084faaf4ff937b3daea233e3682fb4474db07395794fa80d"}, + {file = "lxml-6.0.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:414aaa94e974e23a3e92e7ca5b97d10c0cf37b6481f50911032c69eeb3991bba"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48461bd21625458dd01e14e2c38dd0aea69addc3c4f960c30d9f59d7f93be601"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:25fcc59afc57d527cfc78a58f40ab4c9b8fd096a9a3f964d2781ffb6eb33f4ed"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5179c60288204e6ddde3f774a93350177e08876eaf3ab78aa3a3649d43eb7d37"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:967aab75434de148ec80597b75062d8123cadf2943fb4281f385141e18b21338"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d100fcc8930d697c6561156c6810ab4a508fb264c8b6779e6e61e2ed5e7558f9"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ca59e7e13e5981175b8b3e4ab84d7da57993eeff53c07764dcebda0d0e64ecd"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:957448ac63a42e2e49531b9d6c0fa449a1970dbc32467aaad46f11545be9af1d"}, + {file = "lxml-6.0.2-cp314-cp314t-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b7fc49c37f1786284b12af63152fe1d0990722497e2d5817acfe7a877522f9a9"}, + {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e19e0643cc936a22e837f79d01a550678da8377d7d801a14487c10c34ee49c7e"}, + {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:1db01e5cf14345628e0cbe71067204db658e2fb8e51e7f33631f5f4735fefd8d"}, + {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:875c6b5ab39ad5291588aed6925fac99d0097af0dd62f33c7b43736043d4a2ec"}, + {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cdcbed9ad19da81c480dfd6dd161886db6096083c9938ead313d94b30aadf272"}, + {file = "lxml-6.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:80dadc234ebc532e09be1975ff538d154a7fa61ea5031c03d25178855544728f"}, + {file = "lxml-6.0.2-cp314-cp314t-win32.whl", hash = "sha256:da08e7bb297b04e893d91087df19638dc7a6bb858a954b0cc2b9f5053c922312"}, + {file = "lxml-6.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:252a22982dca42f6155125ac76d3432e548a7625d56f5a273ee78a5057216eca"}, + {file = "lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c"}, + {file = "lxml-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a656ca105115f6b766bba324f23a67914d9c728dafec57638e2b92a9dcd76c62"}, + {file = "lxml-6.0.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c54d83a2188a10ebdba573f16bd97135d06c9ef60c3dc495315c7a28c80a263f"}, + {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:1ea99340b3c729beea786f78c38f60f4795622f36e305d9c9be402201efdc3b7"}, + {file = "lxml-6.0.2-cp38-cp38-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af85529ae8d2a453feee4c780d9406a5e3b17cee0dd75c18bd31adcd584debc3"}, + {file = "lxml-6.0.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fe659f6b5d10fb5a17f00a50eb903eb277a71ee35df4615db573c069bcf967ac"}, + {file = "lxml-6.0.2-cp38-cp38-win32.whl", hash = "sha256:5921d924aa5468c939d95c9814fa9f9b5935a6ff4e679e26aaf2951f74043512"}, + {file = "lxml-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:0aa7070978f893954008ab73bb9e3c24a7c56c054e00566a21b553dc18105fca"}, + {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2c8458c2cdd29589a8367c09c8f030f1d202be673f0ca224ec18590b3b9fb694"}, + {file = "lxml-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3fee0851639d06276e6b387f1c190eb9d7f06f7f53514e966b26bae46481ec90"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b2142a376b40b6736dfc214fd2902409e9e3857eff554fed2d3c60f097e62a62"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6b5b39cc7e2998f968f05309e666103b53e2edd01df8dc51b90d734c0825444"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4aec24d6b72ee457ec665344a29acb2d35937d5192faebe429ea02633151aad"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:b42f4d86b451c2f9d06ffb4f8bbc776e04df3ba070b9fe2657804b1b40277c48"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6cdaefac66e8b8f30e37a9b4768a391e1f8a16a7526d5bc77a7928408ef68e93"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:b738f7e648735714bbb82bdfd030203360cfeab7f6e8a34772b3c8c8b820568c"}, + {file = "lxml-6.0.2-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daf42de090d59db025af61ce6bdb2521f0f102ea0e6ea310f13c17610a97da4c"}, + {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:66328dabea70b5ba7e53d94aa774b733cf66686535f3bc9250a7aab53a91caaf"}, + {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:e237b807d68a61fc3b1e845407e27e5eb8ef69bc93fe8505337c1acb4ee300b6"}, + {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:ac02dc29fd397608f8eb15ac1610ae2f2f0154b03f631e6d724d9e2ad4ee2c84"}, + {file = "lxml-6.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:817ef43a0c0b4a77bd166dc9a09a555394105ff3374777ad41f453526e37f9cb"}, + {file = "lxml-6.0.2-cp39-cp39-win32.whl", hash = "sha256:bc532422ff26b304cfb62b328826bd995c96154ffd2bac4544f37dbb95ecaa8f"}, + {file = "lxml-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:995e783eb0374c120f528f807443ad5a83a656a8624c467ea73781fc5f8a8304"}, + {file = "lxml-6.0.2-cp39-cp39-win_arm64.whl", hash = "sha256:08b9d5e803c2e4725ae9e8559ee880e5328ed61aa0935244e0515d7d9dbec0aa"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e748d4cf8fef2526bb2a589a417eba0c8674e29ffcb570ce2ceca44f1e567bf6"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4ddb1049fa0579d0cbd00503ad8c58b9ab34d1254c77bc6a5576d96ec7853dba"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cb233f9c95f83707dae461b12b720c1af9c28c2d19208e1be03387222151daf5"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc456d04db0515ce3320d714a1eac7a97774ff0849e7718b492d957da4631dd4"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2613e67de13d619fd283d58bda40bff0ee07739f624ffee8b13b631abf33083d"}, + {file = "lxml-6.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:24a8e756c982c001ca8d59e87c80c4d9dcd4d9b44a4cbeb8d9be4482c514d41d"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1c06035eafa8404b5cf475bb37a9f6088b0aca288d4ccc9d69389750d5543700"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c7d13103045de1bdd6fe5d61802565f1a3537d70cd3abf596aa0af62761921ee"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a3c150a95fbe5ac91de323aa756219ef9cf7fde5a3f00e2281e30f33fa5fa4f"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60fa43be34f78bebb27812ed90f1925ec99560b0fa1decdb7d12b84d857d31e9"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21c73b476d3cfe836be731225ec3421fa2f048d84f6df6a8e70433dff1376d5a"}, + {file = "lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e"}, + {file = "lxml-6.0.2.tar.gz", hash = "sha256:cd79f3367bd74b317dda655dc8fcfa304d9eb6e4fb06b7168c5cf27f96e0cd62"}, +] + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html-clean = ["lxml_html_clean"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] + +[[package]] +name = "markupsafe" +version = "3.0.3" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, +] + +[[package]] +name = "matplotlib" +version = "3.10.7" +description = "Python plotting package" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "matplotlib-3.10.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:7ac81eee3b7c266dd92cee1cd658407b16c57eed08c7421fa354ed68234de380"}, + {file = "matplotlib-3.10.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:667ecd5d8d37813a845053d8f5bf110b534c3c9f30e69ebd25d4701385935a6d"}, + {file = "matplotlib-3.10.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1c51b846aca49a5a8b44fbba6a92d583a35c64590ad9e1e950dc88940a4297"}, + {file = "matplotlib-3.10.7-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a11c2e9e72e7de09b7b72e62f3df23317c888299c875e2b778abf1eda8c0a42"}, + {file = "matplotlib-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f19410b486fdd139885ace124e57f938c1e6a3210ea13dd29cab58f5d4bc12c7"}, + {file = "matplotlib-3.10.7-cp310-cp310-win_amd64.whl", hash = "sha256:b498e9e4022f93de2d5a37615200ca01297ceebbb56fe4c833f46862a490f9e3"}, + {file = "matplotlib-3.10.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:53b492410a6cd66c7a471de6c924f6ede976e963c0f3097a3b7abfadddc67d0a"}, + {file = "matplotlib-3.10.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9749313deb729f08207718d29c86246beb2ea3fdba753595b55901dee5d2fd6"}, + {file = "matplotlib-3.10.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2222c7ba2cbde7fe63032769f6eb7e83ab3227f47d997a8453377709b7fe3a5a"}, + {file = "matplotlib-3.10.7-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e91f61a064c92c307c5a9dc8c05dc9f8a68f0a3be199d9a002a0622e13f874a1"}, + {file = "matplotlib-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6f1851eab59ca082c95df5a500106bad73672645625e04538b3ad0f69471ffcc"}, + {file = "matplotlib-3.10.7-cp311-cp311-win_amd64.whl", hash = "sha256:6516ce375109c60ceec579e699524e9d504cd7578506f01150f7a6bc174a775e"}, + {file = "matplotlib-3.10.7-cp311-cp311-win_arm64.whl", hash = "sha256:b172db79759f5f9bc13ef1c3ef8b9ee7b37b0247f987fbbbdaa15e4f87fd46a9"}, + {file = "matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748"}, + {file = "matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f"}, + {file = "matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0"}, + {file = "matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695"}, + {file = "matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65"}, + {file = "matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee"}, + {file = "matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8"}, + {file = "matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f"}, + {file = "matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c"}, + {file = "matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1"}, + {file = "matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632"}, + {file = "matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84"}, + {file = "matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815"}, + {file = "matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7"}, + {file = "matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355"}, + {file = "matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b"}, + {file = "matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67"}, + {file = "matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67"}, + {file = "matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84"}, + {file = "matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2"}, + {file = "matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf"}, + {file = "matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100"}, + {file = "matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f"}, + {file = "matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715"}, + {file = "matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1"}, + {file = "matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722"}, + {file = "matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866"}, + {file = "matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb"}, + {file = "matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1"}, + {file = "matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4"}, + {file = "matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318"}, + {file = "matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca"}, + {file = "matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc"}, + {file = "matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8"}, + {file = "matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c"}, + {file = "matplotlib-3.10.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5c09cf8f2793f81368f49f118b6f9f937456362bee282eac575cca7f84cda537"}, + {file = "matplotlib-3.10.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:de66744b2bb88d5cd27e80dfc2ec9f0517d0a46d204ff98fe9e5f2864eb67657"}, + {file = "matplotlib-3.10.7-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:53cc80662dd197ece414dd5b66e07370201515a3eaf52e7c518c68c16814773b"}, + {file = "matplotlib-3.10.7-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:15112bcbaef211bd663fa935ec33313b948e214454d949b723998a43357b17b0"}, + {file = "matplotlib-3.10.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d2a959c640cdeecdd2ec3136e8ea0441da59bcaf58d67e9c590740addba2cb68"}, + {file = "matplotlib-3.10.7-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3886e47f64611046bc1db523a09dd0a0a6bed6081e6f90e13806dd1d1d1b5e91"}, + {file = "matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7"}, +] + +[package.dependencies] +contourpy = ">=1.0.1" +cycler = ">=0.10" +fonttools = ">=4.22.0" +kiwisolver = ">=1.3.1" +numpy = ">=1.23" +packaging = ">=20.0" +pillow = ">=8" +pyparsing = ">=3" +python-dateutil = ">=2.7" + +[package.extras] +dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] + +[[package]] +name = "multiprocess" +version = "0.70.18" +description = "better multiprocessing and multithreading in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "multiprocess-0.70.18-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25d4012dcaaf66b9e8e955f58482b42910c2ee526d532844d8bcf661bbc604df"}, + {file = "multiprocess-0.70.18-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:06b19433de0d02afe5869aec8931dd5c01d99074664f806c73896b0d9e527213"}, + {file = "multiprocess-0.70.18-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6fa1366f994373aaf2d4738b0f56e707caeaa05486e97a7f71ee0853823180c2"}, + {file = "multiprocess-0.70.18-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b8940ae30139e04b076da6c5b83e9398585ebdf0f2ad3250673fef5b2ff06d6"}, + {file = "multiprocess-0.70.18-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0929ba95831adb938edbd5fb801ac45e705ecad9d100b3e653946b7716cb6bd3"}, + {file = "multiprocess-0.70.18-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4d77f8e4bfe6c6e2e661925bbf9aed4d5ade9a1c6502d5dfc10129b9d1141797"}, + {file = "multiprocess-0.70.18-pp38-pypy38_pp73-macosx_10_9_arm64.whl", hash = "sha256:2dbaae9bffa1fb2d58077c0044ffe87a8c8974e90fcf778cdf90e139c970d42a"}, + {file = "multiprocess-0.70.18-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bcac5a4e81f1554d98d1bba963eeb1bd24966432f04fcbd29b6e1a16251ad712"}, + {file = "multiprocess-0.70.18-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c0c7cd75d0987ab6166d64e654787c781dbacbcbcaaede4c1ffe664720b3e14b"}, + {file = "multiprocess-0.70.18-pp39-pypy39_pp73-macosx_10_13_arm64.whl", hash = "sha256:9fd8d662f7524a95a1be7cbea271f0b33089fe792baabec17d93103d368907da"}, + {file = "multiprocess-0.70.18-pp39-pypy39_pp73-macosx_10_13_x86_64.whl", hash = "sha256:3fbba48bfcd932747c33f0b152b26207c4e0840c35cab359afaff7a8672b1031"}, + {file = "multiprocess-0.70.18-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:5f9be0342e597dde86152c10442c5fb6c07994b1c29de441b7a3a08b0e6be2a0"}, + {file = "multiprocess-0.70.18-py310-none-any.whl", hash = "sha256:60c194974c31784019c1f459d984e8f33ee48f10fcf42c309ba97b30d9bd53ea"}, + {file = "multiprocess-0.70.18-py311-none-any.whl", hash = "sha256:5aa6eef98e691281b3ad923be2832bf1c55dd2c859acd73e5ec53a66aae06a1d"}, + {file = "multiprocess-0.70.18-py312-none-any.whl", hash = "sha256:9b78f8e5024b573730bfb654783a13800c2c0f2dfc0c25e70b40d184d64adaa2"}, + {file = "multiprocess-0.70.18-py313-none-any.whl", hash = "sha256:871743755f43ef57d7910a38433cfe41319e72be1bbd90b79c7a5ac523eb9334"}, + {file = "multiprocess-0.70.18-py38-none-any.whl", hash = "sha256:dbf705e52a154fe5e90fb17b38f02556169557c2dd8bb084f2e06c2784d8279b"}, + {file = "multiprocess-0.70.18-py39-none-any.whl", hash = "sha256:e78ca805a72b1b810c690b6b4cc32579eba34f403094bbbae962b7b5bf9dfcb8"}, + {file = "multiprocess-0.70.18.tar.gz", hash = "sha256:f9597128e6b3e67b23956da07cf3d2e5cba79e2f4e0fba8d7903636663ec6d0d"}, +] + +[package.dependencies] +dill = ">=0.4.0" + +[[package]] +name = "narwhals" +version = "2.9.0" +description = "Extremely lightweight compatibility layer between dataframe libraries" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "narwhals-2.9.0-py3-none-any.whl", hash = "sha256:c59f7de4763004ae81691ce16df71b4e55aead0ead7ccde8c8f2ef8c9559c765"}, + {file = "narwhals-2.9.0.tar.gz", hash = "sha256:d8cde40a6a8a7049d8e66608b7115ab19464acc6f305d136a8dc8ba396c4acfe"}, +] + +[package.extras] +cudf = ["cudf (>=24.10.0)"] +dask = ["dask[dataframe] (>=2024.8)"] +duckdb = ["duckdb (>=1.1)"] +ibis = ["ibis-framework (>=6.0.0)", "packaging", "pyarrow-hotfix", "rich"] +modin = ["modin"] +pandas = ["pandas (>=1.1.3)"] +polars = ["polars (>=0.20.4)"] +pyarrow = ["pyarrow (>=13.0.0)"] +pyspark = ["pyspark (>=3.5.0)"] +pyspark-connect = ["pyspark[connect] (>=3.5.0)"] +sqlframe = ["sqlframe (>=3.22.0,!=3.39.3)"] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +groups = ["main", "dev"] +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] + +[[package]] +name = "outcome" +version = "1.3.0.post0" +description = "Capture the outcome of Python function calls." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, + {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, +] + +[package.dependencies] +attrs = ">=19.2.0" + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pandas" +version = "2.3.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, + {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, + {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, + {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, + {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, + {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, + {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, + {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, +] + +[package.dependencies] +numpy = {version = ">=1.22.4", markers = "python_version < \"3.11\""} +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "percy" +version = "2.0.2" +description = "Python client library for visual regression testing with Percy (https://percy.io)." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "percy-2.0.2-py2.py3-none-any.whl", hash = "sha256:c1647b768810e9453220a7721a5d52cec560dee913d13c1e29b713703f4f223e"}, + {file = "percy-2.0.2.tar.gz", hash = "sha256:6238612dc401fa5c221c0ad7738f7ea43e48fe2695f6423e785ee2bc940f021d"}, +] + +[package.dependencies] +requests = ">=2.14.0" + +[[package]] +name = "pillow" +version = "12.0.0" +description = "Python Imaging Library (fork)" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "pillow-12.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:3adfb466bbc544b926d50fe8f4a4e6abd8c6bffd28a26177594e6e9b2b76572b"}, + {file = "pillow-12.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ac11e8ea4f611c3c0147424eae514028b5e9077dd99ab91e1bd7bc33ff145e1"}, + {file = "pillow-12.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d49e2314c373f4c2b39446fb1a45ed333c850e09d0c59ac79b72eb3b95397363"}, + {file = "pillow-12.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7b2a63fd6d5246349f3d3f37b14430d73ee7e8173154461785e43036ffa96ca"}, + {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d64317d2587c70324b79861babb9c09f71fbb780bad212018874b2c013d8600e"}, + {file = "pillow-12.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d77153e14b709fd8b8af6f66a3afbb9ed6e9fc5ccf0b6b7e1ced7b036a228782"}, + {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32ed80ea8a90ee3e6fa08c21e2e091bba6eda8eccc83dbc34c95169507a91f10"}, + {file = "pillow-12.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c828a1ae702fc712978bda0320ba1b9893d99be0badf2647f693cc01cf0f04fa"}, + {file = "pillow-12.0.0-cp310-cp310-win32.whl", hash = "sha256:bd87e140e45399c818fac4247880b9ce719e4783d767e030a883a970be632275"}, + {file = "pillow-12.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:455247ac8a4cfb7b9bc45b7e432d10421aea9fc2e74d285ba4072688a74c2e9d"}, + {file = "pillow-12.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:6ace95230bfb7cd79ef66caa064bbe2f2a1e63d93471c3a2e1f1348d9f22d6b7"}, + {file = "pillow-12.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0fd00cac9c03256c8b2ff58f162ebcd2587ad3e1f2e397eab718c47e24d231cc"}, + {file = "pillow-12.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3475b96f5908b3b16c47533daaa87380c491357d197564e0ba34ae75c0f3257"}, + {file = "pillow-12.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:110486b79f2d112cf6add83b28b627e369219388f64ef2f960fef9ebaf54c642"}, + {file = "pillow-12.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5269cc1caeedb67e6f7269a42014f381f45e2e7cd42d834ede3c703a1d915fe3"}, + {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa5129de4e174daccbc59d0a3b6d20eaf24417d59851c07ebb37aeb02947987c"}, + {file = "pillow-12.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bee2a6db3a7242ea309aa7ee8e2780726fed67ff4e5b40169f2c940e7eb09227"}, + {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:90387104ee8400a7b4598253b4c406f8958f59fcf983a6cea2b50d59f7d63d0b"}, + {file = "pillow-12.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bc91a56697869546d1b8f0a3ff35224557ae7f881050e99f615e0119bf934b4e"}, + {file = "pillow-12.0.0-cp311-cp311-win32.whl", hash = "sha256:27f95b12453d165099c84f8a8bfdfd46b9e4bda9e0e4b65f0635430027f55739"}, + {file = "pillow-12.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b583dc9070312190192631373c6c8ed277254aa6e6084b74bdd0a6d3b221608e"}, + {file = "pillow-12.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:759de84a33be3b178a64c8ba28ad5c135900359e85fb662bc6e403ad4407791d"}, + {file = "pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371"}, + {file = "pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082"}, + {file = "pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f"}, + {file = "pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d"}, + {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953"}, + {file = "pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8"}, + {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79"}, + {file = "pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba"}, + {file = "pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0"}, + {file = "pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a"}, + {file = "pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad"}, + {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643"}, + {file = "pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4"}, + {file = "pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399"}, + {file = "pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5"}, + {file = "pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b"}, + {file = "pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3"}, + {file = "pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07"}, + {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e"}, + {file = "pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344"}, + {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27"}, + {file = "pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79"}, + {file = "pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098"}, + {file = "pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905"}, + {file = "pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a"}, + {file = "pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3"}, + {file = "pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced"}, + {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b"}, + {file = "pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d"}, + {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a"}, + {file = "pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe"}, + {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee"}, + {file = "pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef"}, + {file = "pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9"}, + {file = "pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b"}, + {file = "pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47"}, + {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9"}, + {file = "pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2"}, + {file = "pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a"}, + {file = "pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b"}, + {file = "pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad"}, + {file = "pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01"}, + {file = "pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c"}, + {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e"}, + {file = "pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e"}, + {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9"}, + {file = "pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab"}, + {file = "pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b"}, + {file = "pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b"}, + {file = "pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0"}, + {file = "pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6"}, + {file = "pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6"}, + {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1"}, + {file = "pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e"}, + {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca"}, + {file = "pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925"}, + {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8"}, + {file = "pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4"}, + {file = "pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52"}, + {file = "pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a"}, + {file = "pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b22bd8c974942477156be55a768f7aa37c46904c175be4e158b6a86e3a6b7ca8"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:805ebf596939e48dbb2e4922a1d3852cfc25c38160751ce02da93058b48d252a"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cae81479f77420d217def5f54b5b9d279804d17e982e0f2fa19b1d1e14ab5197"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeaefa96c768fc66818730b952a862235d68825c178f1b3ffd4efd7ad2edcb7c"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f2d0abef9e4e2f349305a4f8cc784a8a6c2f58a8c4892eea13b10a943bd26e"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdee52571a343d721fb2eb3b090a82d959ff37fc631e3f70422e0c2e029f3e76"}, + {file = "pillow-12.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:b290fd8aa38422444d4b50d579de197557f182ef1068b75f5aa8558638b8d0a5"}, + {file = "pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] +tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] +xmp = ["defusedxml"] + +[[package]] +name = "plotly" +version = "6.3.1" +description = "An open-source interactive data visualization library for Python" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "plotly-6.3.1-py3-none-any.whl", hash = "sha256:8b4420d1dcf2b040f5983eed433f95732ed24930e496d36eb70d211923532e64"}, + {file = "plotly-6.3.1.tar.gz", hash = "sha256:dd896e3d940e653a7ce0470087e82c2bd903969a55e30d1b01bb389319461bb0"}, +] + +[package.dependencies] +narwhals = ">=1.15.1" +packaging = "*" + +[package.extras] +dev = ["plotly[dev-optional]"] +dev-build = ["build", "jupyter", "plotly[dev-core]"] +dev-core = ["pytest", "requests", "ruff (==0.11.12)"] +dev-optional = ["anywidget", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "numpy", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "plotly[dev-build]", "plotly[kaleido]", "polars[timezone]", "pyarrow", "pyshp", "pytz", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] +express = ["numpy"] +kaleido = ["kaleido (>=1.0.0)"] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "psutil" +version = "7.1.1" +description = "Cross-platform lib for process and system monitoring." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "psutil-7.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:8fa59d7b1f01f0337f12cd10dbd76e4312a4d3c730a4fedcbdd4e5447a8b8460"}, + {file = "psutil-7.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:2a95104eae85d088891716db676f780c1404fc15d47fde48a46a5d61e8f5ad2c"}, + {file = "psutil-7.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98629cd8567acefcc45afe2f4ba1e9290f579eacf490a917967decce4b74ee9b"}, + {file = "psutil-7.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92ebc58030fb054fa0f26c3206ef01c31c29d67aee1367e3483c16665c25c8d2"}, + {file = "psutil-7.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:146a704f224fb2ded2be3da5ac67fc32b9ea90c45b51676f9114a6ac45616967"}, + {file = "psutil-7.1.1-cp37-abi3-win32.whl", hash = "sha256:295c4025b5cd880f7445e4379e6826f7307e3d488947bf9834e865e7847dc5f7"}, + {file = "psutil-7.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:9b4f17c5f65e44f69bd3a3406071a47b79df45cf2236d1f717970afcb526bcd3"}, + {file = "psutil-7.1.1-cp37-abi3-win_arm64.whl", hash = "sha256:5457cf741ca13da54624126cd5d333871b454ab133999a9a103fb097a7d7d21a"}, + {file = "psutil-7.1.1.tar.gz", hash = "sha256:092b6350145007389c1cfe5716050f02030a05219d90057ea867d18fe8d372fc"}, +] + +[package.extras] +dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] +test = ["pytest", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "setuptools", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] + +[[package]] +name = "pycparser" +version = "2.23" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "(platform_python_implementation != \"PyPy\" or os_name == \"nt\" and implementation_name != \"pypy\") and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"}, + {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pyopenssl" +version = "25.3.0" +description = "Python wrapper module around the OpenSSL library" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pyopenssl-25.3.0-py3-none-any.whl", hash = "sha256:1fda6fc034d5e3d179d39e59c1895c9faeaf40a79de5fc4cbbfbe0d36f4a77b6"}, + {file = "pyopenssl-25.3.0.tar.gz", hash = "sha256:c981cb0a3fd84e8602d7afc209522773b94c1c2446a3c710a75b06fe1beae329"}, +] + +[package.dependencies] +cryptography = ">=45.0.7,<47" +typing-extensions = {version = ">=4.9", markers = "python_version < \"3.13\" and python_version >= \"3.8\""} + +[package.extras] +docs = ["sphinx (!=5.2.0,!=5.2.0.post0,!=7.2.5)", "sphinx_rtd_theme"] +test = ["pretend", "pytest (>=3.0.1)", "pytest-rerunfailures"] + +[[package]] +name = "pyparsing" +version = "3.2.5" +description = "pyparsing - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e"}, + {file = "pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pysocks" +version = "1.7.1" +description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, + {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, + {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, +] + +[[package]] +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2025.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, +] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "retrying" +version = "1.4.2" +description = "Retrying" +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +files = [ + {file = "retrying-1.4.2-py3-none-any.whl", hash = "sha256:bbc004aeb542a74f3569aeddf42a2516efefcdaff90df0eb38fbfbf19f179f59"}, + {file = "retrying-1.4.2.tar.gz", hash = "sha256:d102e75d53d8d30b88562d45361d6c6c934da06fab31bd81c0420acb97a8ba39"}, +] + +[[package]] +name = "scipy" +version = "1.15.3" +description = "Fundamental algorithms for scientific computing in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f"}, + {file = "scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82"}, + {file = "scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e"}, + {file = "scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c"}, + {file = "scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65"}, + {file = "scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889"}, + {file = "scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9"}, + {file = "scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594"}, + {file = "scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477"}, + {file = "scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45"}, + {file = "scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e"}, + {file = "scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539"}, + {file = "scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb"}, + {file = "scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825"}, + {file = "scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11"}, + {file = "scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126"}, + {file = "scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e"}, + {file = "scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723"}, + {file = "scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4"}, + {file = "scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5"}, + {file = "scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca"}, + {file = "scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf"}, +] + +[package.dependencies] +numpy = ">=1.23.5,<2.5" + +[package.extras] +dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.19.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja ; sys_platform != \"emscripten\"", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] + +[[package]] +name = "selenium" +version = "4.2.0" +description = "" +optional = false +python-versions = "~=3.7" +groups = ["dev"] +files = [ + {file = "selenium-4.2.0-py3-none-any.whl", hash = "sha256:ba5b2633f43cf6fe9d308fa4a6996e00a101ab9cb1aad6fd91ae1f3dbe57f56f"}, +] + +[package.dependencies] +trio = ">=0.17,<1.0" +trio-websocket = ">=0.9,<1.0" +urllib3 = {version = ">=1.26,<2.0", extras = ["secure", "socks"]} + +[[package]] +name = "setuptools" +version = "80.9.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, + {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, +] + +[[package]] +name = "soupsieve" +version = "2.8" +description = "A modern CSS selector implementation for Beautiful Soup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "soupsieve-2.8-py3-none-any.whl", hash = "sha256:0cc76456a30e20f5d7f2e14a98a4ae2ee4e5abdc7c5ea0aafe795f344bc7984c"}, + {file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"}, +] + +[[package]] +name = "tomli" +version = "2.3.0" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, + {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf"}, + {file = "tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845"}, + {file = "tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c"}, + {file = "tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456"}, + {file = "tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac"}, + {file = "tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f"}, + {file = "tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8"}, + {file = "tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6"}, + {file = "tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876"}, + {file = "tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b"}, + {file = "tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b"}, + {file = "tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f"}, + {file = "tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05"}, + {file = "tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606"}, + {file = "tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e"}, + {file = "tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc"}, + {file = "tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879"}, + {file = "tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005"}, + {file = "tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463"}, + {file = "tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77"}, + {file = "tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530"}, + {file = "tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67"}, + {file = "tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f"}, + {file = "tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0"}, + {file = "tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba"}, + {file = "tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b"}, + {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, +] + +[[package]] +name = "trio" +version = "0.31.0" +description = "A friendly Python library for async concurrency and I/O" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "trio-0.31.0-py3-none-any.whl", hash = "sha256:b5d14cd6293d79298b49c3485ffd9c07e3ce03a6da8c7dfbe0cb3dd7dc9a4774"}, + {file = "trio-0.31.0.tar.gz", hash = "sha256:f71d551ccaa79d0cb73017a33ef3264fde8335728eb4c6391451fe5d253a9d5b"}, +] + +[package.dependencies] +attrs = ">=23.2.0" +cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +idna = "*" +outcome = "*" +sniffio = ">=1.3.0" +sortedcontainers = "*" + +[[package]] +name = "trio-websocket" +version = "0.12.2" +description = "WebSocket library for Trio" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"}, + {file = "trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae"}, +] + +[package.dependencies] +exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} +outcome = ">=1.2.0" +trio = ">=0.11" +wsproto = ">=0.14" + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + +[[package]] +name = "urllib3" +version = "1.26.20" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e"}, + {file = "urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32"}, +] + +[package.dependencies] +certifi = {version = "*", optional = true, markers = "extra == \"secure\""} +cryptography = {version = ">=1.3.4", optional = true, markers = "extra == \"secure\""} +idna = {version = ">=2.0.0", optional = true, markers = "extra == \"secure\""} +pyOpenSSL = {version = ">=0.14", optional = true, markers = "extra == \"secure\""} +PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} +urllib3-secure-extra = {version = "*", optional = true, markers = "extra == \"secure\""} + +[package.extras] +brotli = ["brotli (==1.0.9) ; os_name != \"nt\" and python_version < \"3\" and platform_python_implementation == \"CPython\"", "brotli (>=1.0.9) ; python_version >= \"3\" and platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; (os_name != \"nt\" or python_version >= \"3\") and platform_python_implementation != \"CPython\"", "brotlipy (>=0.6.0) ; os_name == \"nt\" and python_version < \"3\""] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress ; python_version == \"2.7\"", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "urllib3-secure-extra" +version = "0.1.0" +description = "Marker library to detect whether urllib3 was installed with the deprecated [secure] extra" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "urllib3-secure-extra-0.1.0.tar.gz", hash = "sha256:ee9409cbfeb4b8609047be4c32fb4317870c602767e53fd8a41005ebe6a41dff"}, + {file = "urllib3_secure_extra-0.1.0-py2.py3-none-any.whl", hash = "sha256:f7adcb108b4d12a4b26b99eb60e265d087f435052a76aefa396b6ee85e9a6ef9"}, +] + +[[package]] +name = "vtk" +version = "9.5.2" +description = "VTK is an open-source toolkit for 3D computer graphics, image processing, and visualization" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "vtk-9.5.2-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:9ca87122352cf3c8748fee73c48930efa46fe1a868149a1f760bc17e8fae27ba"}, + {file = "vtk-9.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6da02d69dcf2d42472ec8c227e6a8406cedea53d3928af97f8d4e776ff89c95f"}, + {file = "vtk-9.5.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c0ba9cc4b5cd463a1984dfac6d0a9eeef888b273208739f8ebc46d392ddabb93"}, + {file = "vtk-9.5.2-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c9254f864ebef3d69666a1feedf09cad129e4c91f85ca804c38cf8addedb2748"}, + {file = "vtk-9.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:7c56dbd02e5b4ec0422886bf9e26059ad2d4622857dbfb90d9ed254104fd9d6c"}, + {file = "vtk-9.5.2-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:afcbc6dc122ebba877793940fda8fd2cbe14e1dae590e6872ea74894abdab9be"}, + {file = "vtk-9.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:005877a568b96cf00ceb5bec268cf102db756bed509cb240fa40ada414a24bf0"}, + {file = "vtk-9.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2e2fe2535483adb1ba8cc83a0dc296faaffa2505808a3b04f697084f656e5f84"}, + {file = "vtk-9.5.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0248aab2ee51a69fadcdcf74697a045e2d525009a35296100eed2211f0cca2bb"}, + {file = "vtk-9.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:f78674fd265022499ea6b7f03d7f11a861e89e1df043592a82e4f5235c537ef5"}, + {file = "vtk-9.5.2-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:cf5dbc93b6806b08799204430a4fc4bea74290c1c101fa64f1a4703144087fa3"}, + {file = "vtk-9.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cce212b911d13fb0ca36d339f658c9db1ff27a5a730cdddd5d0c6b2ec24c15b1"}, + {file = "vtk-9.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:454711c51038824ddc75f955e1064c4e214b452c2e67083f01a8b43fc0ed62cb"}, + {file = "vtk-9.5.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:9fce9688f0dede00dc6f3b046037c5fa8378479fa8303a353fd69afae4078d9a"}, + {file = "vtk-9.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:5357bccdf8629373195cab871e45c50383d052d316192aa48f45bd9f87bafccb"}, + {file = "vtk-9.5.2-cp313-cp313-macosx_10_10_x86_64.whl", hash = "sha256:1eae5016620a5fd78f4918256ea65dbe100a7c3ce68f763b64523f06aaaeafbc"}, + {file = "vtk-9.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:29ad766e308dcaa23b36261180cd9960215f48815b31c7ac2aa52edc88e21ef7"}, + {file = "vtk-9.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:11cf870c05539e9f82f4a5adf450384e0be4ee6cc80274f9502715a4139e2777"}, + {file = "vtk-9.5.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:3c4b658d61815cb87177f4e94281396c9be5a28798464a2c6fa0897b1bba282f"}, + {file = "vtk-9.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:974783b8865e2ddc2818d3090705b6bc6bf8ae40346d67f9a43485fabcfb3a99"}, + {file = "vtk-9.5.2-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:227c5e6e9195aa9d92a64d6d07d09f000576b5df231522b5c156a3c4c4190d69"}, + {file = "vtk-9.5.2-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b5d0a89e893d9279ba9742d0bbd47d7dfac96fccd8fb9d024bb8aa098fde5637"}, + {file = "vtk-9.5.2-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:371d9068f5cb25861aa51c1d1792fffce5a44032dbece55412562429c5f257cc"}, + {file = "vtk-9.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:7cf2e2e12184c018388f06fbffcb93ea9e478ca4bf636c3f66bd7503e2230298"}, + {file = "vtk-9.5.2-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:9b148e57837d1fd2a8a72f171a0fb40872837dea191f673f2b7ec397935c754e"}, + {file = "vtk-9.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6a3b27f22d7e15f6a2d60510e70d75dac4ed2a53600e31275b67fedc45afbcc0"}, + {file = "vtk-9.5.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b6b91968581132b0d96142a08d50028efa5aa7a876d4aff6de1664e99e006c89"}, + {file = "vtk-9.5.2-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:2b37670d56de32935eeadee58e1a9a0b5d3847294ca24ea9329101089be5de83"}, + {file = "vtk-9.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:1ac9ff528892e585f8f3286b26a90250bd6ea9107c38e6e194939f6f28269ad6"}, +] + +[package.dependencies] +matplotlib = ">=2.0.0" + +[package.extras] +numpy = ["numpy (>=1.9)"] +web = ["wslink (>=1.0.4)"] + +[[package]] +name = "waitress" +version = "3.0.2" +description = "Waitress WSGI server" +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "waitress-3.0.2-py3-none-any.whl", hash = "sha256:c56d67fd6e87c2ee598b76abdd4e96cfad1f24cacdea5078d382b1f9d7b5ed2e"}, + {file = "waitress-3.0.2.tar.gz", hash = "sha256:682aaaf2af0c44ada4abfb70ded36393f0e307f4ab9456a215ce0020baefc31f"}, +] + +[package.extras] +docs = ["Sphinx (>=1.8.1)", "docutils", "pylons-sphinx-themes (>=1.0.9)"] +testing = ["coverage (>=7.6.0)", "pytest", "pytest-cov"] + +[[package]] +name = "werkzeug" +version = "3.0.6" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17"}, + {file = "werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[[package]] +name = "wsproto" +version = "1.2.0" +description = "WebSockets state-machine based protocol implementation" +optional = false +python-versions = ">=3.7.0" +groups = ["dev"] +files = [ + {file = "wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736"}, + {file = "wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065"}, +] + +[package.dependencies] +h11 = ">=0.9.0,<1" + +[[package]] +name = "zipp" +version = "3.23.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, +] + +[package.extras] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] +cover = ["pytest-cov"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +enabler = ["pytest-enabler (>=2.2)"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +type = ["pytest-mypy"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10,<3.11" +content-hash = "4666886735150cd159fd58a73e8538e66e9328f0fb90bbf7539c2555fa206f3d" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..10b0db7 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,38 @@ +[project] +name = "web-app" +version = "0.1.0" +description = "Interactive Visualization Webapp with Plotly/Dash" +authors = [ + {name = "Vlad Korsakov",email = "ulqba@student.kit.edu"}, + {name = "Daniel Frisch",email = "daniel.frisch@kit.edu"} +] +readme = "README.md" +requires-python = ">=3.10,<3.11" +dependencies = [ + "dash (>=2.16,<3.0)", + "dash-bootstrap-components (>=1.6,<2.0)", + "numpy (>=1.26,<2.0)", + "dash-mantine-components (>=0.14,<0.15)", + "pandas (>=2.2,<3.0)", + "dash-vtk (>=0.0.9,<0.0.10)", + "dash-resizable-panels (>=0.1.0,<0.2.0)", + "scipy (>=1.13,<2.0)" +] + +[dependency-groups] +dev = [ + "pytest (>=8.4.2,<9.0.0)", + "dash[testing] (>=2.16,<3.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry] +package-mode = false + +[tool.poetry.requires-plugins] +poetry-plugin-export = ">=1.8" + diff --git a/requirements.txt b/requirements.txt index 18cf351..680f831 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,54 +1,45 @@ -blinker==1.7.0 -Brotli==1.1.0 -certifi==2024.2.2 -cffi==2.0.0 -charset-normalizer==3.3.2 -click==8.1.7 -colorama==0.4.6 -contourpy==1.2.1 -curl_cffi==0.13.0 -cycler==0.12.1 -dash==2.16.1 -dash-bootstrap-components==1.6.0 -dash-core-components==2.0.0 -dash-design-kit==0.0.1 -dash-html-components==2.0.0 -dash-resizable-panels==0.1.0 -dash-table==5.0.0 -dash-vtk==0.0.9 -dash_mantine_components==0.14.2 -Flask==3.0.3 -fonttools==4.51.0 -gunicorn==22.0.0 -idna==3.7 -importlib_metadata==7.1.0 -itsdangerous==2.2.0 -Jinja2==3.1.3 -kiwisolver==1.4.5 -MarkupSafe==2.1.5 -matplotlib==3.8.4 -mutagen==1.47.0 -nest-asyncio==1.6.0 -numpy==1.26.4 -packaging==24.0 -pandas==2.2.2 -pillow==10.3.0 -plotly==5.21.0 -pycparser==2.23 -pycryptodomex==3.23.0 -pyparsing==3.1.2 -python-dateutil==2.9.0.post0 -pytz==2024.1 -requests==2.32.5 -retrying==1.3.4 -scipy==1.13.0 -six==1.16.0 -tenacity==8.2.3 -typing_extensions==4.11.0 -tzdata==2024.1 -urllib3==2.2.1 -vtk==9.5.2 -websockets==15.0.1 -Werkzeug==3.0.3 -yt-dlp==2025.9.26 -zipp==3.18.1 +blinker==1.9.0 ; python_version == "3.10" +certifi==2025.10.5 ; python_version == "3.10" +charset-normalizer==3.4.4 ; python_version == "3.10" +click==8.3.0 ; python_version == "3.10" +colorama==0.4.6 ; python_version == "3.10" and platform_system == "Windows" +contourpy==1.3.2 ; python_version == "3.10" +cycler==0.12.1 ; python_version == "3.10" +dash-bootstrap-components==1.7.1 ; python_version == "3.10" +dash-core-components==2.0.0 ; python_version == "3.10" +dash-html-components==2.0.0 ; python_version == "3.10" +dash-mantine-components==0.14.7 ; python_version == "3.10" +dash-resizable-panels==0.1.0 ; python_version == "3.10" +dash-table==5.0.0 ; python_version == "3.10" +dash-vtk==0.0.9 ; python_version == "3.10" +dash==2.18.2 ; python_version == "3.10" +flask==3.0.3 ; python_version == "3.10" +fonttools==4.60.1 ; python_version == "3.10" +idna==3.11 ; python_version == "3.10" +importlib-metadata==8.7.0 ; python_version == "3.10" +itsdangerous==2.2.0 ; python_version == "3.10" +jinja2==3.1.6 ; python_version == "3.10" +kiwisolver==1.4.9 ; python_version == "3.10" +markupsafe==3.0.3 ; python_version == "3.10" +matplotlib==3.10.7 ; python_version == "3.10" +narwhals==2.9.0 ; python_version == "3.10" +nest-asyncio==1.6.0 ; python_version == "3.10" +numpy==1.26.4 ; python_version == "3.10" +packaging==25.0 ; python_version == "3.10" +pandas==2.3.3 ; python_version == "3.10" +pillow==12.0.0 ; python_version == "3.10" +plotly==6.3.1 ; python_version == "3.10" +pyparsing==3.2.5 ; python_version == "3.10" +python-dateutil==2.9.0.post0 ; python_version == "3.10" +pytz==2025.2 ; python_version == "3.10" +requests==2.32.5 ; python_version == "3.10" +retrying==1.4.2 ; python_version == "3.10" +scipy==1.15.3 ; python_version == "3.10" +setuptools==80.9.0 ; python_version == "3.10" +six==1.17.0 ; python_version == "3.10" +typing-extensions==4.15.0 ; python_version == "3.10" +tzdata==2025.2 ; python_version == "3.10" +urllib3==1.26.20 ; python_version == "3.10" +vtk==9.5.2 ; python_version == "3.10" +werkzeug==3.0.6 ; python_version == "3.10" +zipp==3.23.0 ; python_version == "3.10" From ff57bcd1d035f982d3ddac6e2774f7b211aeccfa Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 20 Oct 2025 20:09:51 +0200 Subject: [PATCH 018/152] added docker compose and end-to-end testcases --- app.py | 1 + docker-compose.yml | 16 ++++++++++++++ dockerfile | 4 ++-- pages/sphere.py | 2 +- pages/torus.py | 2 +- poetry.lock | 24 ++++++++++++++++++++- pyproject.toml | 3 ++- renderer/Object3DRenderer.py | 39 +++++++++++++++++----------------- server_commands.txt | 12 ++++++++++- tests/__init__.py | 0 tests/end_to_end_test.py | 41 ++++++++++++++++++++++++++++++++++++ 11 files changed, 117 insertions(+), 27 deletions(-) create mode 100644 docker-compose.yml create mode 100644 tests/__init__.py create mode 100644 tests/end_to_end_test.py diff --git a/app.py b/app.py index 0b10a98..e7c50ad 100644 --- a/app.py +++ b/app.py @@ -17,6 +17,7 @@ external_stylesheets=external_stylesheets, server=server, use_pages=True, + serve_locally=True, suppress_callback_exceptions=True # this is needed because renderer generate callbacks dynamically with per instance uuids ) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..545e414 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +services: + web: + build: + context: . + target: prod + image: web_app:latest + ports: + - "8080:8080" + restart: unless-stopped + + tests: + build: + context: . + target: tests + image: web_app:tests + profiles: ["test"] diff --git a/dockerfile b/dockerfile index 4853026..07bcab0 100644 --- a/dockerfile +++ b/dockerfile @@ -19,7 +19,7 @@ FROM base AS tests RUN apt-get update && apt-get install -y chromium chromium-driver RUN poetry sync --no-root --with dev COPY ./tests /code/tests - +CMD ["poetry", "run", "pytest", "--headless"] FROM base AS prod -CMD ["poetry", "run", "python","app.py"] +CMD ["poetry", "run", "gunicorn", "--workers", "32", "--bind", "0.0.0.0:8080", "app:server"] diff --git a/pages/sphere.py b/pages/sphere.py index 2b44d2a..c948bc4 100644 --- a/pages/sphere.py +++ b/pages/sphere.py @@ -14,7 +14,7 @@ sphere = Sphere() -renderer = Object3DRenderer(sphere) +renderer = Object3DRenderer(sphere, "sphere") options, graph = renderer.get_layout_components() diff --git a/pages/torus.py b/pages/torus.py index 7a007d9..00782fe 100644 --- a/pages/torus.py +++ b/pages/torus.py @@ -15,7 +15,7 @@ torus = Torus() -renderer = Object3DRenderer(torus) +renderer = Object3DRenderer(torus, "torus") options, graph = renderer.get_layout_components() layout = SplitPane( diff --git a/poetry.lock b/poetry.lock index b62b469..03e7590 100644 --- a/poetry.lock +++ b/poetry.lock @@ -776,6 +776,28 @@ type1 = ["xattr ; sys_platform == \"darwin\""] unicode = ["unicodedata2 (>=15.1.0) ; python_version <= \"3.12\""] woff = ["brotli (>=1.0.1) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\"", "zopfli (>=0.1.4)"] +[[package]] +name = "gunicorn" +version = "23.0.0" +description = "WSGI HTTP Server for UNIX" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, + {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] +gevent = ["gevent (>=1.4.0)"] +setproctitle = ["setproctitle"] +testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] +tornado = ["tornado (>=0.2)"] + [[package]] name = "h11" version = "0.16.0" @@ -2319,4 +2341,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.11" -content-hash = "4666886735150cd159fd58a73e8538e66e9328f0fb90bbf7539c2555fa206f3d" +content-hash = "dbbea2c61cb0bdcfd4f5f175080b423942c9727de11c4c93dc011ec6f1533c00" diff --git a/pyproject.toml b/pyproject.toml index 10b0db7..21d060c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,8 @@ dependencies = [ "pandas (>=2.2,<3.0)", "dash-vtk (>=0.0.9,<0.0.10)", "dash-resizable-panels (>=0.1.0,<0.2.0)", - "scipy (>=1.13,<2.0)" + "scipy (>=1.13,<2.0)", + "gunicorn (>=23.0.0,<24.0.0)" ] [dependency-groups] diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 2909804..ca9e849 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -2,13 +2,12 @@ from dash import html, dcc, callback, Input, Output, ALL, State, Patch import numpy as np import plotly.graph_objects as go -import uuid import plotly.figure_factory as ff class Object3DRenderer: - def __init__(self, object_3D): + def __init__(self, object_3D, id): # dash doesnt like duplicate calback functions # so each renderer instance gets a uuid for suffixing - self.uuid = str(uuid.uuid4()) + self.id = id # objects should have atleast one corresponding distribution self.object = object_3D @@ -52,7 +51,7 @@ def _register_callbacks(self): # updates wich sampling methods are available once distribution is selected @callback( - Output(f"sampling-selector-{self.uuid}", "options"), + Output(f"sampling-selector-{self.id}", "options"), Input("distribution-selector", "value"), ) def update_sampling_methods(selected_distribution): @@ -61,10 +60,10 @@ def update_sampling_methods(selected_distribution): # updates the options (silders, etc) for the selected distribution and sampling method @callback( - Output(f"distribution-options-{self.uuid}", "children"), - Output(f"sampling-options-{self.uuid}", "children"), + Output(f"distribution-options-{self.id}", "children"), + Output(f"sampling-options-{self.id}", "children"), Input("distribution-selector", "value"), - Input(f"sampling-selector-{self.uuid}", "value") + Input(f"sampling-selector-{self.id}", "value") ) def update_curr_distribution(selected_distribution, selected_sampling): # ids are given in the same order as options_dist and options_sampling @@ -77,14 +76,14 @@ def update_curr_distribution(selected_distribution, selected_sampling): # updates the plot based on selected sampling options @callback( - Output(f"graph-{self.uuid}", "figure"), + Output(f"graph-{self.id}", "figure"), Input({"type": "dist", "index": ALL}, "value"), State({"type": "dist", "index": ALL}, "id"), Input({"type": "sampling", "index": ALL}, "value"), State({"type": "sampling", "index": ALL}, "id"), Input("distribution-selector", "value"), - Input(f"sampling-selector-{self.uuid}", "value"), - Input(f"distribution-options-{self.uuid}", "children"), + Input(f"sampling-selector-{self.id}", "value"), + Input(f"distribution-options-{self.id}", "children"), ) def update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): dist_options = self.object.distributions[selected_distribution].distribution_options @@ -139,14 +138,14 @@ def update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_di # updates the plot based on selected sampling options @callback( - Output(f"graph-{self.uuid}", "figure", allow_duplicate=True), + Output(f"graph-{self.id}", "figure", allow_duplicate=True), Input({"type": "dist", "index": ALL}, "value"), State({"type": "dist", "index": ALL}, "id"), Input({"type": "sampling", "index": ALL}, "value"), State({"type": "sampling", "index": ALL}, "id"), Input("distribution-selector", "value"), - Input(f"sampling-selector-{self.uuid}", "value"), - Input(f"distribution-options-{self.uuid}", "children"), + Input(f"sampling-selector-{self.id}", "value"), + Input(f"distribution-options-{self.id}", "children"), prevent_initial_call='initial_duplicate' ) def update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): @@ -194,12 +193,12 @@ def update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_di # updates the plot based on selected distribution options @callback( - Output(f"graph-{self.uuid}", "figure", allow_duplicate=True), + Output(f"graph-{self.id}", "figure", allow_duplicate=True), Input({"type": "dist", "index": ALL}, "value"), State({"type": "dist", "index": ALL}, "id"), Input("distribution-selector", "value"), - Input(f"sampling-selector-{self.uuid}", "value"), - Input(f"distribution-options-{self.uuid}", "children"), + Input(f"sampling-selector-{self.id}", "value"), + Input(f"distribution-options-{self.id}", "children"), prevent_initial_call='initial_duplicate' ) def update_plot_dist(values_dist, ids_dist, selected_distribution, selected_sampling, _): @@ -247,7 +246,7 @@ def get_layout_components(self): ), html.Br(), dcc.RadioItems( - id=f"sampling-selector-{self.uuid}", + id=f"sampling-selector-{self.id}", options=(list(initial_sampling_options)), value=initial_sampling_method, ), @@ -255,10 +254,10 @@ def get_layout_components(self): html.Hr(), html.Br(), - html.Div(id=f"distribution-options-{self.uuid}"), - html.Div(id=f"sampling-options-{self.uuid}"), + html.Div(id=f"distribution-options-{self.id}"), + html.Div(id=f"sampling-options-{self.id}"), ] - graph = [dcc.Graph(id=f"graph-{self.uuid}", figure=self.fig)] + graph = [dcc.Graph(id=f"graph-{self.id}", figure=self.fig)] return options, graph \ No newline at end of file diff --git a/server_commands.txt b/server_commands.txt index 533cba7..f3d2cdb 100644 --- a/server_commands.txt +++ b/server_commands.txt @@ -1,4 +1,3 @@ - # Create server https://portal.bw-cloud.org/project/instances/ https://www.bw-cloud.org/de/bwcloud_scope/nutzen @@ -30,6 +29,17 @@ nohup python app.py & nohup gunicorn --bind 0.0.0.0:8080 --workers 32 app:server & TODO use daemon +# run with docker +(install docker (compose before)) +docker compose up -d --build + +run tests: +docker compose --profile test build tests +docker compose --profile test run --rm tests + +turn server off (not needed updating image): +docker compose down + # Test in Browser http://193.196.39.120:8080 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/end_to_end_test.py b/tests/end_to_end_test.py new file mode 100644 index 0000000..f035245 --- /dev/null +++ b/tests/end_to_end_test.py @@ -0,0 +1,41 @@ +import dash +from app import app +import pytest + +PAGES_WITH_PLOTLY_PLOTS = ['/conditional', '/gauss1d', '/gauss2d', '/sphere', '/torus'] +PAGES_WITH_AT_LEAST_ONE_SLIDER = ['/conditional', '/gauss1d', '/gauss2d', '/sphere', '/torus'] + +# basic launch web-app test +def test_001_home_end_to_end(dash_duo): + dash_duo.start_server(app) + + assert dash_duo.get_logs() == [], "browser console should contain no error" + + dash_duo.wait_for_text_to_equal("h1", "ISAS Interactive", timeout=10) + +@pytest.mark.parametrize("path", PAGES_WITH_PLOTLY_PLOTS) +def test_002_page_loads_plots(dash_duo, path): + dash_duo.start_server(app) + + # navbutton should appear + link = dash_duo.wait_for_element(f'a.nav-link[href="{path}"]', timeout=10) + link.click() + + # a plot should appear + dash_duo.wait_for_element("div.js-plotly-plot", timeout=10) + + assert dash_duo.get_logs() == [], "browser console should contain no error" + + +@pytest.mark.parametrize("path", PAGES_WITH_PLOTLY_PLOTS) +def test_003_has_one_slider(dash_duo, path): + dash_duo.start_server(app) + + # navbutton should appear + link = dash_duo.wait_for_element(f'a.nav-link[href="{path}"]', timeout=10) + link.click() + + # a plot should appear + dash_duo.wait_for_element("div.rc-slider", timeout=10) + + assert dash_duo.get_logs() == [], "browser console should contain no error" \ No newline at end of file From 094ef6625154fa84ece48c8105db41a5b9112806 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 20 Oct 2025 20:11:32 +0200 Subject: [PATCH 019/152] fix outdated comment --- tests/end_to_end_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/end_to_end_test.py b/tests/end_to_end_test.py index f035245..6f4b9d3 100644 --- a/tests/end_to_end_test.py +++ b/tests/end_to_end_test.py @@ -35,7 +35,7 @@ def test_003_has_one_slider(dash_duo, path): link = dash_duo.wait_for_element(f'a.nav-link[href="{path}"]', timeout=10) link.click() - # a plot should appear + # a slider should appear dash_duo.wait_for_element("div.rc-slider", timeout=10) assert dash_duo.get_logs() == [], "browser console should contain no error" \ No newline at end of file From 6b19253662a8c81ae1dd4e1d631d67a5bebce2e6 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 21 Oct 2025 14:10:02 +0200 Subject: [PATCH 020/152] add github action test workflow --- .github/workflows/test_only.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/test_only.yml diff --git a/.github/workflows/test_only.yml b/.github/workflows/test_only.yml new file mode 100644 index 0000000..b1d32b5 --- /dev/null +++ b/.github/workflows/test_only.yml @@ -0,0 +1,14 @@ +name : Tests +on: + push: + branches: + - vlad/devel +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Build Docker Image + run: docker compose --profile test build tests + - name: Run Tests + run: docker compose --profile test run --rm tests From 7d3eadfdf9b3dbd3b348b7ff7788b985b08b0eaf Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 21 Oct 2025 15:32:30 +0200 Subject: [PATCH 021/152] update doc and lower thread count --- dockerfile | 2 +- server_commands.txt | 13 ++++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/dockerfile b/dockerfile index 07bcab0..18dda7b 100644 --- a/dockerfile +++ b/dockerfile @@ -22,4 +22,4 @@ COPY ./tests /code/tests CMD ["poetry", "run", "pytest", "--headless"] FROM base AS prod -CMD ["poetry", "run", "gunicorn", "--workers", "32", "--bind", "0.0.0.0:8080", "app:server"] +CMD ["poetry", "run", "gunicorn", "--workers", "1", "--bind", "0.0.0.0:8080", "app:server"] diff --git a/server_commands.txt b/server_commands.txt index f3d2cdb..5d46673 100644 --- a/server_commands.txt +++ b/server_commands.txt @@ -29,15 +29,22 @@ nohup python app.py & nohup gunicorn --bind 0.0.0.0:8080 --workers 32 app:server & TODO use daemon +# install docker +sudo apt update +sudo apt install -y docker.io docker-compose +sudo systemctl enable --now docker +sudo usermod -aG docker $USER +(then exit ssh and log back in) + # run with docker -(install docker (compose before)) -docker compose up -d --build +('docker-compose' for old docker version from debian repos, 'docker compose' for newer versions like ubuntu-latest github runner) +docker-compose up -d --build run tests: docker compose --profile test build tests docker compose --profile test run --rm tests -turn server off (not needed updating image): +turn server off (not needed for updating image): docker compose down # Test in Browser From ecc2eaf106156a0a677e667e718a90f065c67097 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 21 Oct 2025 17:10:42 +0200 Subject: [PATCH 022/152] new server ips --- README.md | 2 +- server_commands.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5a09e58..c9f993c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # web-app - Currently served at: http://193.196.39.120:8080 -- Test server: http://193.196.39.193:8080 +- Test server: http://193.196.39.84:8080 diff --git a/server_commands.txt b/server_commands.txt index 5d46673..2ca4ba9 100644 --- a/server_commands.txt +++ b/server_commands.txt @@ -38,7 +38,7 @@ sudo usermod -aG docker $USER # run with docker ('docker-compose' for old docker version from debian repos, 'docker compose' for newer versions like ubuntu-latest github runner) -docker-compose up -d --build +docker-compose --profile default up -d --build run tests: docker compose --profile test build tests From d2d2b1c1f6259b0ee3ac0221af5d13fee0a87aad Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 22 Oct 2025 15:26:58 +0200 Subject: [PATCH 023/152] added a bunch of distributions from sphstat --- .../distributions/sphere/bingham/__init__.py | 0 model/distributions/sphere/bingham/bingham.py | 24 ++++++ model/distributions/sphere/bingham/random.py | 29 +++++++ model/distributions/sphere/kent/__init__.py | 0 model/distributions/sphere/kent/kent.py | 29 +++++++ model/distributions/sphere/kent/random.py | 40 +++++++++ model/distributions/sphere/watson/__init__.py | 0 model/distributions/sphere/watson/random.py | 33 +++++++ model/distributions/sphere/watson/watson.py | 26 ++++++ model/sphere/sphere.py | 8 ++ poetry.lock | 85 ++++++++++++++++++- pyproject.toml | 3 +- requirements.txt | 6 ++ util/selectors/slider.py | 2 +- util/selectors/slider_float.py | 39 +++++++++ 15 files changed, 321 insertions(+), 3 deletions(-) create mode 100644 model/distributions/sphere/bingham/__init__.py create mode 100644 model/distributions/sphere/bingham/bingham.py create mode 100644 model/distributions/sphere/bingham/random.py create mode 100644 model/distributions/sphere/kent/__init__.py create mode 100644 model/distributions/sphere/kent/kent.py create mode 100644 model/distributions/sphere/kent/random.py create mode 100644 model/distributions/sphere/watson/__init__.py create mode 100644 model/distributions/sphere/watson/random.py create mode 100644 model/distributions/sphere/watson/watson.py create mode 100644 util/selectors/slider_float.py diff --git a/model/distributions/sphere/bingham/__init__.py b/model/distributions/sphere/bingham/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/sphere/bingham/bingham.py b/model/distributions/sphere/bingham/bingham.py new file mode 100644 index 0000000..6dae81b --- /dev/null +++ b/model/distributions/sphere/bingham/bingham.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from model.distributions.sphere.sphere_distribution import SphereDistribution +from util.selectors.slider_float import FloatSlider +import numpy as np + +from model.distributions.sphere.bingham.random import BinghamRandomSampling + + +class BinghampDistribution(SphereDistribution): + def __init__(self): + self.distribution_options = [ + FloatSlider("Lambda 1 (λ₁)", -0.49, 0, 0), + FloatSlider("Lambda 2 (λ₂)", -0.49, 0, 0), + ] + + self.sampling_methods = [ + BinghamRandomSampling() + ] + + def get_name(self): + return "Bingham" + + def get_pdf(self, distribution_options): + return None \ No newline at end of file diff --git a/model/distributions/sphere/bingham/random.py b/model/distributions/sphere/bingham/random.py new file mode 100644 index 0000000..104fe09 --- /dev/null +++ b/model/distributions/sphere/bingham/random.py @@ -0,0 +1,29 @@ +from abc import ABC, abstractmethod +import numpy as np +import scipy +import sphstat + +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from util.selectors.slider import Slider + + +class BinghamRandomSampling(SphereSamplingSchema): + def __init__(self): + self.sample_options = [ + Slider("Number of Samples", 1, 50, 100), + ] + + def get_name(self): + return "Random" + + def sample(self, sample_options, distribution_options): + l1 = distribution_options[0].state + l2 = distribution_options[1].state + + lambdas = np.sort(np.array([l1, l2]))[::-1] + + numsamp = sample_options[0].state + samples = sphstat.distributions.bingham(numsamp, lambdas)["points"] + samples_array = np.vstack(samples) + + return samples_array \ No newline at end of file diff --git a/model/distributions/sphere/kent/__init__.py b/model/distributions/sphere/kent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/sphere/kent/kent.py b/model/distributions/sphere/kent/kent.py new file mode 100644 index 0000000..4221254 --- /dev/null +++ b/model/distributions/sphere/kent/kent.py @@ -0,0 +1,29 @@ +from abc import ABC, abstractmethod +from model.distributions.sphere.sphere_distribution import SphereDistribution +from util.selectors.slider import Slider +from util.selectors.slider_float import FloatSlider +import numpy as np + +from model.distributions.sphere.kent.random import KentRandomSampling + + +class KentDistribution(SphereDistribution): + def __init__(self): + self.distribution_options = [ + Slider("κ (kappa)", 0.0, 10.0, 50.0), + Slider("β (beta)", 0.0, 2.0, 25.0), + FloatSlider("mu: Mean vector of Kent distribution: (θ)", 0, 0, np.pi), + FloatSlider("mu: Mean vector of Kent distribution: (φ)", 0, 0, 2 * np.pi), + FloatSlider("mu0: Mean vector of the Fisher part: (θ)", 0, np.pi, np.pi), # default values so that distibution is initially visible + FloatSlider("mu0: Mean vector of the Fisher part: (φ)", 0, 0, 2 * np.pi), + ] + + self.sampling_methods = [ + KentRandomSampling() + ] + + def get_name(self): + return "Kent (5-parameter Fisher-Bingham - FB5)" + + def get_pdf(self, distribution_options): + return None \ No newline at end of file diff --git a/model/distributions/sphere/kent/random.py b/model/distributions/sphere/kent/random.py new file mode 100644 index 0000000..c360491 --- /dev/null +++ b/model/distributions/sphere/kent/random.py @@ -0,0 +1,40 @@ +from abc import ABC, abstractmethod +import numpy as np +import scipy +import sphstat + +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from util.selectors.slider import Slider +from model.sphere.sphere import Sphere + + +class KentRandomSampling(SphereSamplingSchema): + def __init__(self): + self.sample_options = [ + Slider("Number of Samples", 1, 50, 100), + ] + + def get_name(self): + return "Random" + + def sample(self, sample_options, distribution_options): + kappa = distribution_options[0].state + beta = distribution_options[1].state + beta = min(beta, kappa / 2) # TODO make this dynamic + + mu_theta = distribution_options[2].state + mu_phi = distribution_options[3].state + mu0_theta = distribution_options[4].state + mu0_phi = distribution_options[5].state + + mu1, mu2, mu3 = Sphere.spherical_to_cartesian(mu_theta, mu_phi) + mu0_1, mu0_2, mu0_3 = Sphere.spherical_to_cartesian(mu0_theta, mu0_phi) + + mu = [mu1, mu2, mu3] + mu0 = [mu0_1, mu0_2, mu0_3] + + numsamp = sample_options[0].state + samples = sphstat.distributions.kent(numsamp, kappa, beta, mu, mu0)["points"] + samples_array = np.vstack(samples) + + return samples_array \ No newline at end of file diff --git a/model/distributions/sphere/watson/__init__.py b/model/distributions/sphere/watson/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/sphere/watson/random.py b/model/distributions/sphere/watson/random.py new file mode 100644 index 0000000..3356336 --- /dev/null +++ b/model/distributions/sphere/watson/random.py @@ -0,0 +1,33 @@ +from abc import ABC, abstractmethod +import numpy as np +import scipy +import sphstat + +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from util.selectors.slider import Slider +from model.sphere.sphere import Sphere + + +class WatsonRandomSampling(SphereSamplingSchema): + def __init__(self): + self.sample_options = [ + Slider("Number of Samples", 1, 50, 100), + ] + + def get_name(self): + return "Random" + + def sample(self, sample_options, distribution_options): + kappa = distribution_options[0].state + + theta = distribution_options[1].state + phi = distribution_options[2].state + + numsamp = sample_options[0].state + + lamb, mu, nu = Sphere.spherical_to_cartesian(theta, phi) + + samples = sphstat.distributions.watson(numsamp, lamb, mu, nu, kappa)["points"] + samples_array = np.vstack(samples) + + return samples_array \ No newline at end of file diff --git a/model/distributions/sphere/watson/watson.py b/model/distributions/sphere/watson/watson.py new file mode 100644 index 0000000..a817fd4 --- /dev/null +++ b/model/distributions/sphere/watson/watson.py @@ -0,0 +1,26 @@ +from abc import ABC, abstractmethod +from model.distributions.sphere.sphere_distribution import SphereDistribution +from util.selectors.slider_float import FloatSlider +from util.selectors.slider import Slider +import numpy as np + +from model.distributions.sphere.watson.random import WatsonRandomSampling + + +class WatsonDistribution(SphereDistribution): + def __init__(self): + self.distribution_options = [ + Slider("κ (kappa)", 0.0, 10.0, 50.0), + FloatSlider("direction: polar angle (θ)", 0, 0, np.pi), + FloatSlider("direction: azimuthal angle (φ)", 0, 0, 2 * np.pi), + ] + + self.sampling_methods = [ + WatsonRandomSampling() + ] + + def get_name(self): + return "Watson" + + def get_pdf(self, distribution_options): + return None \ No newline at end of file diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index 1dff546..61b03ff 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -77,4 +77,12 @@ def cf(xi, yi, zi, zmin=np.min(z), zmax=np.max(z)): return fig.data + + @staticmethod + def spherical_to_cartesian(theta, phi, r=1): + x = r * np.sin(theta) * np.cos(phi) + y = r * np.sin(theta) * np.sin(phi) + z = r * np.cos(theta) + + return x, y, z diff --git a/poetry.lock b/poetry.lock index 03e7590..671e6c8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -654,6 +654,18 @@ files = [ graph = ["objgraph (>=1.7.2)"] profile = ["gprof2dot (>=2022.7.29)"] +[[package]] +name = "et-xmlfile" +version = "2.0.0" +description = "An implementation of lxml.xmlfile for the standard library" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"}, + {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, +] + [[package]] name = "exceptiongroup" version = "1.3.0" @@ -1336,6 +1348,24 @@ python-dateutil = ">=2.7" [package.extras] dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] +[[package]] +name = "mpmath" +version = "1.3.0" +description = "Python library for arbitrary-precision floating-point arithmetic" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c"}, + {file = "mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f"}, +] + +[package.extras] +develop = ["codecov", "pycodestyle", "pytest (>=4.6)", "pytest-cov", "wheel"] +docs = ["sphinx"] +gmpy = ["gmpy2 (>=2.1.0a4) ; platform_python_implementation != \"PyPy\""] +tests = ["pytest (>=4.6)"] + [[package]] name = "multiprocess" version = "0.70.18" @@ -1451,6 +1481,21 @@ files = [ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] +[[package]] +name = "openpyxl" +version = "3.1.5" +description = "A Python library to read/write Excel 2010 xlsx/xlsm files" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, + {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, +] + +[package.dependencies] +et-xmlfile = "*" + [[package]] name = "outcome" version = "1.3.0.post0" @@ -2071,6 +2116,44 @@ files = [ {file = "soupsieve-2.8.tar.gz", hash = "sha256:e2dd4a40a628cb5f28f6d4b0db8800b8f581b65bb380b97de22ba5ca8d72572f"}, ] +[[package]] +name = "sphstat" +version = "1.0.6" +description = "A Python 3 package for inferential statistics on vectorial data on the unit sphere" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "sphstat-1.0.6.tar.gz", hash = "sha256:e085b097bc8f49a9287c62672dc26906b21b70febf62bc822d455969e5b1241c"}, +] + +[package.dependencies] +matplotlib = "*" +numpy = "*" +openpyxl = "*" +pandas = "*" +scipy = "*" +setuptools = ">=65.5.1" +sympy = "*" + +[[package]] +name = "sympy" +version = "1.14.0" +description = "Computer algebra system (CAS) in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5"}, + {file = "sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517"}, +] + +[package.dependencies] +mpmath = ">=1.1.0,<1.4" + +[package.extras] +dev = ["hypothesis (>=6.70.0)", "pytest (>=7.1.0)"] + [[package]] name = "tomli" version = "2.3.0" @@ -2341,4 +2424,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.11" -content-hash = "dbbea2c61cb0bdcfd4f5f175080b423942c9727de11c4c93dc011ec6f1533c00" +content-hash = "9aad083a086d08b6e7b649d20be271c9f6efed88d63cccb617023153d88a61cf" diff --git a/pyproject.toml b/pyproject.toml index 21d060c..43f0f56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,8 @@ dependencies = [ "dash-vtk (>=0.0.9,<0.0.10)", "dash-resizable-panels (>=0.1.0,<0.2.0)", "scipy (>=1.13,<2.0)", - "gunicorn (>=23.0.0,<24.0.0)" + "gunicorn (>=23.0.0,<24.0.0)", + "sphstat (>=1.0.6,<2.0.0)" ] [dependency-groups] diff --git a/requirements.txt b/requirements.txt index 680f831..94f3626 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,8 +13,10 @@ dash-resizable-panels==0.1.0 ; python_version == "3.10" dash-table==5.0.0 ; python_version == "3.10" dash-vtk==0.0.9 ; python_version == "3.10" dash==2.18.2 ; python_version == "3.10" +et-xmlfile==2.0.0 ; python_version == "3.10" flask==3.0.3 ; python_version == "3.10" fonttools==4.60.1 ; python_version == "3.10" +gunicorn==23.0.0 ; python_version == "3.10" idna==3.11 ; python_version == "3.10" importlib-metadata==8.7.0 ; python_version == "3.10" itsdangerous==2.2.0 ; python_version == "3.10" @@ -22,9 +24,11 @@ jinja2==3.1.6 ; python_version == "3.10" kiwisolver==1.4.9 ; python_version == "3.10" markupsafe==3.0.3 ; python_version == "3.10" matplotlib==3.10.7 ; python_version == "3.10" +mpmath==1.3.0 ; python_version == "3.10" narwhals==2.9.0 ; python_version == "3.10" nest-asyncio==1.6.0 ; python_version == "3.10" numpy==1.26.4 ; python_version == "3.10" +openpyxl==3.1.5 ; python_version == "3.10" packaging==25.0 ; python_version == "3.10" pandas==2.3.3 ; python_version == "3.10" pillow==12.0.0 ; python_version == "3.10" @@ -37,6 +41,8 @@ retrying==1.4.2 ; python_version == "3.10" scipy==1.15.3 ; python_version == "3.10" setuptools==80.9.0 ; python_version == "3.10" six==1.17.0 ; python_version == "3.10" +sphstat==1.0.6 ; python_version == "3.10" +sympy==1.14.0 ; python_version == "3.10" typing-extensions==4.15.0 ; python_version == "3.10" tzdata==2025.2 ; python_version == "3.10" urllib3==1.26.20 ; python_version == "3.10" diff --git a/util/selectors/slider.py b/util/selectors/slider.py index 2364bc7..174969c 100644 --- a/util/selectors/slider.py +++ b/util/selectors/slider.py @@ -5,7 +5,7 @@ SLIDER_MARK_AMOUNT = 5 class Slider(Selector): - def __init__(self, name, min, state, max): + def __init__(self, name, min, state, max, custom_constraints=id): self.name = name self.min = min self.state = state diff --git a/util/selectors/slider_float.py b/util/selectors/slider_float.py new file mode 100644 index 0000000..6954828 --- /dev/null +++ b/util/selectors/slider_float.py @@ -0,0 +1,39 @@ +from dash import dcc, html +from util.selectors.selector import Selector + +SLIDER_OPT_AMOUNT = 100 +SLIDER_MARK_AMOUNT = 5 + +class FloatSlider(Selector): + def __init__(self, name, min, state, max, transform_tooltip=None): + self.name = name + self.min = min + self.state = state + self.max = max + + self.id = None + self.transform_tooltip = transform_tooltip + + + def to_dash_component(self, _type, id): + if self.transform_tooltip is None: + tooltip = {"placement": "bottom", "always_visible": True} + else: + tooltip = {"placement": "bottom", "always_visible": True, "transform": self.transform_tooltip} + + self.id = id + return html.Div([ + html.Label(self.name), + + dcc.Slider( + id={"type": _type, "index": id}, + min=self.min, + max=self.max, + value=self.state, + tooltip=tooltip, + updatemode="drag", + ) + ]) + + def update_state(self, new_state): + self.state = new_state \ No newline at end of file From 293b10287155293f75c9a6b46d98eb81fe3587f5 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 23 Oct 2025 15:56:43 +0200 Subject: [PATCH 024/152] fix patch --- model/sphere/sphere.py | 4 +- renderer/Object3DRenderer.py | 110 +++++++++-------------------------- 2 files changed, 28 insertions(+), 86 deletions(-) diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index 61b03ff..c29174f 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -33,7 +33,7 @@ def update_sample(self, selected_distribution, selected_sampling_method, sample_ self.samples = sampling_method.sample(sample_options, distribution_options) - def generate_trisurf(self, pdf, resolution=30, radius=1, alpha=0.5): + def generate_mesh(self, pdf, resolution=30, radius=1, alpha=0.5): phi = np.linspace(0, np.pi, resolution) theta = np.linspace(0, 2 * np.pi, resolution) phi, theta = np.meshgrid(phi, theta) @@ -76,7 +76,7 @@ def cf(xi, yi, zi, zmin=np.min(z), zmax=np.max(z)): ) - return fig.data + return fig.data[1].x, fig.data[1].y, fig.data[1].z @staticmethod def spherical_to_cartesian(theta, phi, r=1): diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index ca9e849..382789a 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -15,21 +15,32 @@ def __init__(self, object_3D, id): self.x, self.y, self.z = self.object.xyz - + # inital figure self.fig = go.Figure( data=[ go.Surface( + name="Surface", x=self.x, y=self.y, z=self.z, colorscale="Viridis", - showscale=False + showscale=False, + showlegend=True, ), go.Scatter3d( + name="Samples", x=self.object.samples[:, 0] if self.object.samples.size else [], y=self.object.samples[:, 1] if self.object.samples.size else [], z=self.object.samples[:, 2] if self.object.samples.size else [], mode="markers", - marker=dict(size=4, color="red") - ) + marker=dict(size=4, color="red"), + ), + go.Scatter3d( + name="Density", + x=[], y=[], z=[], + mode="lines", + line=dict(color="black", width=1), + showlegend=True, + + ), ] ) @@ -73,67 +84,6 @@ def update_curr_distribution(selected_distribution, selected_sampling): options_sampling = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling] options_sampling_dcc = [opt.to_dash_component("sampling", id) for id, opt in enumerate(options_sampling.sample_options)] return options_dist_dcc, options_sampling_dcc - - # updates the plot based on selected sampling options - @callback( - Output(f"graph-{self.id}", "figure"), - Input({"type": "dist", "index": ALL}, "value"), - State({"type": "dist", "index": ALL}, "id"), - Input({"type": "sampling", "index": ALL}, "value"), - State({"type": "sampling", "index": ALL}, "id"), - Input("distribution-selector", "value"), - Input(f"sampling-selector-{self.id}", "value"), - Input(f"distribution-options-{self.id}", "children"), - ) - def update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): - dist_options = self.object.distributions[selected_distribution].distribution_options - sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options - - # the order of options might not be guaranteed, so we map them by their ids - id_value_dist = [(id,v) for id, v in zip(ids_dist, values_dist)] - id_value_samp = [(id,v) for id, v in zip(ids_samp, values_samp)] - - - # and them sort them, so they are in the same order as sampling_options and dist_options - options_samp_new = sorted(id_value_samp, key=lambda x: int(x[0]["index"])) - options_dist_new = sorted(id_value_dist, key=lambda x: int(x[0]["index"])) - - - for opt, (id, new_state) in zip(sampling_options, options_samp_new): - opt.update_state(new_state) - - for opt, (id, new_state) in zip(dist_options, options_dist_new): - opt.update_state(new_state) - - # samples - self.object.update_sample(selected_distribution, selected_sampling, sampling_options, dist_options) - - data = [ - go.Surface( - x=self.x, y=self.y, z=self.z, - showscale=False, - colorscale="Viridis", - ), - go.Scatter3d( - x=self.object.samples[:, 0], - y=self.object.samples[:, 1], - z=self.object.samples[:, 2], - mode="markers", - marker=dict(size=4, color="red") - ), - ] - - # meshed density function plot plot - - mesh_data = [] - pdf = self.object.distributions[selected_distribution].get_pdf(list(dist_options)) - if pdf is not None: - mesh_data = self.object.generate_trisurf(pdf) - data.extend(mesh_data) - - return go.Figure(data=data, layout=self.fig.layout) - - # updates the plot based on selected sampling options @@ -173,21 +123,11 @@ def update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_di patched_figure = Patch() - - surface = go.Surface( - x=self.x, y=self.y, z=self.z, - showscale=False, - colorscale="Viridis", - ) - points = go.Scatter3d( - x=self.object.samples[:, 0], - y=self.object.samples[:, 1], - z=self.object.samples[:, 2], - mode="markers", - marker=dict(size=4, color="red") - ) - patched_figure["data"][0] = surface - patched_figure["data"][1] = points + + + patched_figure["data"][1].x = self.object.samples[:, 0] + patched_figure["data"][1].y = self.object.samples[:, 1] + patched_figure["data"][1].z = self.object.samples[:, 2] return patched_figure @@ -220,11 +160,13 @@ def update_plot_dist(values_dist, ids_dist, selected_distribution, selected_samp pdf = self.object.distributions[selected_distribution].get_pdf(list(dist_options)) if pdf is not None: - mesh_data = self.object.generate_trisurf(pdf)[0] - patched_figure["data"][2] = mesh_data - + x, y, z = self.object.generate_mesh(pdf) else: - del patched_figure["data"][2] + x, y, z = [], [], [] + + patched_figure["data"][2].x = x + patched_figure["data"][2].y = y + patched_figure["data"][2].z = z return patched_figure From be275ba3d38e7e5945dc544e5706ff7fd4e0df96 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 23 Oct 2025 17:11:20 +0200 Subject: [PATCH 025/152] add pyRecEst as dependency currently something about the package metadata on the main branch seems broken, so use latest tagged release from 2 years ago --- poetry.lock | 59 +++++++++++++++++++++++++++++++++++++++++++------- pyproject.toml | 3 ++- 2 files changed, 53 insertions(+), 9 deletions(-) diff --git a/poetry.lock b/poetry.lock index 671e6c8..5808b3b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -302,11 +302,11 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main", "dev"] +markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "contourpy" @@ -672,7 +672,7 @@ version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -684,6 +684,22 @@ typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "filterpy" +version = "1.4.5" +description = "Kalman filtering and optimal estimation library" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "filterpy-1.4.5.zip", hash = "sha256:4f2a4d39e4ea601b9ab42b2db08b5918a9538c168cff1c6895ae26646f3d73b1"}, +] + +[package.dependencies] +matplotlib = "*" +numpy = "*" +scipy = "*" + [[package]] name = "flask" version = "3.0.3" @@ -867,7 +883,7 @@ version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, @@ -1773,7 +1789,7 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -1825,7 +1841,7 @@ version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -1869,6 +1885,33 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyrecest" +version = "0.1.0" +description = "Framework for recursive Bayesian estimation in Python." +optional = false +python-versions = "^3.10" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +filterpy = "*" +matplotlib = "*" +mpmath = "*" +numpy = "*" +pytest = "*" +scipy = "*" + +[package.extras] +healpy-support = [] + +[package.source] +type = "git" +url = "https://github.com/KIT-ISAS/pyRecEst.git" +reference = "7ee536d" +resolved_reference = "7ee536d3da48f05855d2d4d2eec6321e065e9a85" + [[package]] name = "pysocks" version = "1.7.1" @@ -1888,7 +1931,7 @@ version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, @@ -2160,7 +2203,7 @@ version = "2.3.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, @@ -2424,4 +2467,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.11" -content-hash = "9aad083a086d08b6e7b649d20be271c9f6efed88d63cccb617023153d88a61cf" +content-hash = "b9626147de58dbe29de73850ba9e3811d0de9a157dbb761d2eec2d6053d0ad91" diff --git a/pyproject.toml b/pyproject.toml index 43f0f56..54e0d42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,8 @@ dependencies = [ "dash-resizable-panels (>=0.1.0,<0.2.0)", "scipy (>=1.13,<2.0)", "gunicorn (>=23.0.0,<24.0.0)", - "sphstat (>=1.0.6,<2.0.0)" + "sphstat (>=1.0.6,<2.0.0)", + "pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@7ee536d", ] [dependency-groups] From 4bb962844b7423789dfc942a2391bc88fdbf5f35 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 23 Oct 2025 17:17:36 +0200 Subject: [PATCH 026/152] pyRecEst: use tag instead of commit hash --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 54e0d42..a1d9cb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "scipy (>=1.13,<2.0)", "gunicorn (>=23.0.0,<24.0.0)", "sphstat (>=1.0.6,<2.0.0)", - "pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@7ee536d", + "pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@0.1.1", ] [dependency-groups] From da5f4b95d2e87f1e807ddd592bd16fccb69d94f9 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 24 Oct 2025 10:00:07 +0200 Subject: [PATCH 027/152] update poetry.lock --- poetry.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 5808b3b..cbf49a3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2467,4 +2467,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.11" -content-hash = "b9626147de58dbe29de73850ba9e3811d0de9a157dbb761d2eec2d6053d0ad91" +content-hash = "b375ecd041a9df25d8d3701a89510f53f03c012fc540f78f7751e19e9f29e28e" From 0bf21054addf640354f43b7ae9748c62051185e9 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 24 Oct 2025 10:09:44 +0200 Subject: [PATCH 028/152] fix legend --- renderer/Object3DRenderer.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 382789a..908565a 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -54,6 +54,13 @@ def __init__(self, object_3D, id): margin=dict(l=0, r=0, t=0, b=0) ) + self.fig.update_layout(legend=dict( + yanchor="top", + y=0.99, + xanchor="right", + x=0.0 + )) + self._register_callbacks() From 6f3b4e9267e9397850901d2ec82553d192e71d26 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 24 Oct 2025 10:59:13 +0200 Subject: [PATCH 029/152] update requirements.txt and remove readme.txt because it is outdated --- readme.txt | 8 -------- requirements.txt | 10 +++++++++- 2 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 readme.txt diff --git a/readme.txt b/readme.txt deleted file mode 100644 index 06fdc52..0000000 --- a/readme.txt +++ /dev/null @@ -1,8 +0,0 @@ - -source .venv/bin/activate -pip install dash -pip install pandas -pip install dash-bootstrap-components -pip install dash-mantine-components -pip install numpy - diff --git a/requirements.txt b/requirements.txt index 94f3626..be2c677 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ blinker==1.9.0 ; python_version == "3.10" certifi==2025.10.5 ; python_version == "3.10" charset-normalizer==3.4.4 ; python_version == "3.10" click==8.3.0 ; python_version == "3.10" -colorama==0.4.6 ; python_version == "3.10" and platform_system == "Windows" +colorama==0.4.6 ; (platform_system == "Windows" or sys_platform == "win32") and python_version == "3.10" contourpy==1.3.2 ; python_version == "3.10" cycler==0.12.1 ; python_version == "3.10" dash-bootstrap-components==1.7.1 ; python_version == "3.10" @@ -14,11 +14,14 @@ dash-table==5.0.0 ; python_version == "3.10" dash-vtk==0.0.9 ; python_version == "3.10" dash==2.18.2 ; python_version == "3.10" et-xmlfile==2.0.0 ; python_version == "3.10" +exceptiongroup==1.3.0 ; python_version == "3.10" +filterpy==1.4.5 ; python_version == "3.10" flask==3.0.3 ; python_version == "3.10" fonttools==4.60.1 ; python_version == "3.10" gunicorn==23.0.0 ; python_version == "3.10" idna==3.11 ; python_version == "3.10" importlib-metadata==8.7.0 ; python_version == "3.10" +iniconfig==2.3.0 ; python_version == "3.10" itsdangerous==2.2.0 ; python_version == "3.10" jinja2==3.1.6 ; python_version == "3.10" kiwisolver==1.4.9 ; python_version == "3.10" @@ -33,7 +36,11 @@ packaging==25.0 ; python_version == "3.10" pandas==2.3.3 ; python_version == "3.10" pillow==12.0.0 ; python_version == "3.10" plotly==6.3.1 ; python_version == "3.10" +pluggy==1.6.0 ; python_version == "3.10" +pygments==2.19.2 ; python_version == "3.10" pyparsing==3.2.5 ; python_version == "3.10" +pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@7ee536d3da48f05855d2d4d2eec6321e065e9a85 ; python_version == "3.10" +pytest==8.4.2 ; python_version == "3.10" python-dateutil==2.9.0.post0 ; python_version == "3.10" pytz==2025.2 ; python_version == "3.10" requests==2.32.5 ; python_version == "3.10" @@ -43,6 +50,7 @@ setuptools==80.9.0 ; python_version == "3.10" six==1.17.0 ; python_version == "3.10" sphstat==1.0.6 ; python_version == "3.10" sympy==1.14.0 ; python_version == "3.10" +tomli==2.3.0 ; python_version == "3.10" typing-extensions==4.15.0 ; python_version == "3.10" tzdata==2025.2 ; python_version == "3.10" urllib3==1.26.20 ; python_version == "3.10" From d53cee8551974bd509692af0a896a96a4f895034 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 24 Oct 2025 16:41:19 +0200 Subject: [PATCH 030/152] add fibonachi latice to uniform and mises fischer also fix minor bug in sampling strategy selection --- .../sphere/uniform/fibonachi_lattice.py | 40 +++++++++++++++++++ model/distributions/sphere/uniform/uniform.py | 2 + .../sphere/vonmises_fisher/fibonachi.py | 35 ++++++++++++++++ .../sphere/vonmises_fisher/vonmises_fisher.py | 3 +- poetry.lock | 2 +- renderer/Object3DRenderer.py | 17 ++++++-- 6 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 model/distributions/sphere/uniform/fibonachi_lattice.py create mode 100644 model/distributions/sphere/vonmises_fisher/fibonachi.py diff --git a/model/distributions/sphere/uniform/fibonachi_lattice.py b/model/distributions/sphere/uniform/fibonachi_lattice.py new file mode 100644 index 0000000..69f29bb --- /dev/null +++ b/model/distributions/sphere/uniform/fibonachi_lattice.py @@ -0,0 +1,40 @@ +from abc import ABC, abstractmethod +import numpy as np + + +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from util.selectors.slider import Slider + +class SphereUniformFibSampling(SphereSamplingSchema): + def __init__(self): + self.sample_options = [ + Slider("Number of Samples", 10, 100, 500) + ] + + def get_name(self): + return "Fibonacci Lattice" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + gold_seq = (1+5**0.5)/2 # golden ratio + + indices = np.arange(0, sample_count) + + # centered rank-1 lattice generator + # see https://isas.iar.kit.edu/pdf/SDFMFI23_Frisch.pdf Forumla 2 + equidistant_generator = (2 * indices + 1) / (2 * sample_count) + w = equidistant_generator + + # w is gererated in [0, 1], we need it in [-1, 1] + w = 1 - 2 * w + + # map to sphere + # based on https://isas.iar.kit.edu/pdf/SDFMFI23_Frisch.pdf, Forumla 39 and 40 + x_i_f_0 = w + x_i_f_1 = np.sqrt(1-w**2) * np.cos( (2 * np.pi * indices) / gold_seq) + x_i_f_2 = np.sqrt(1-w**2) * np.sin( (2 * np.pi * indices) / gold_seq) + x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) + + + return x_i_f \ No newline at end of file diff --git a/model/distributions/sphere/uniform/uniform.py b/model/distributions/sphere/uniform/uniform.py index 2572e0e..38a61b1 100644 --- a/model/distributions/sphere/uniform/uniform.py +++ b/model/distributions/sphere/uniform/uniform.py @@ -4,6 +4,7 @@ from model.distributions.sphere.sphere_distribution import SphereDistribution from model.distributions.sphere.uniform.random import SphereUniformRandomSampling +from model.distributions.sphere.uniform.fibonachi_lattice import SphereUniformFibSampling class SphereUniformDistribution(SphereDistribution): @@ -11,6 +12,7 @@ def __init__(self): self.distribution_options = [] self.sampling_methods = [ SphereUniformRandomSampling(), + SphereUniformFibSampling() ] diff --git a/model/distributions/sphere/vonmises_fisher/fibonachi.py b/model/distributions/sphere/vonmises_fisher/fibonachi.py new file mode 100644 index 0000000..bf8c034 --- /dev/null +++ b/model/distributions/sphere/vonmises_fisher/fibonachi.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod +import numpy as np +import scipy + +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from util.selectors.slider import Slider + + +class VonMisesFibSampling(SphereSamplingSchema): + def __init__(self): + self.sample_options = [ + Slider("Number of Samples", 1, 50, 500), + ] + + def get_name(self): + return "Fibonacci Lattice" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + k = distribution_options[0].state # kappa + + gold_seq = (1+5**0.5)/2 # golden ratio + + indices = np.arange(0, sample_count) + + w = 1 + (1/k) * np.log1p((2*indices -1)/ (2*sample_count ) * np.expm1(-2 * k)) + w = np.clip(w, -1.0, 1.0) # clamp to avoid sqrt warnings due to numerical issues + + # based on https://isas.iar.kit.edu/pdf/SDFMFI23_Frisch.pdf, Forumla 39 and 40 + x_i_f_0 = w + x_i_f_1 = np.sqrt(1-w**2) * np.cos( (2 * np.pi * indices) / gold_seq) + x_i_f_2 = np.sqrt(1-w**2) * np.sin( (2 * np.pi * indices) / gold_seq) + x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) # order so that mu=[0, 0, 1] + + return x_i_f \ No newline at end of file diff --git a/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py index d690bc3..39ff95b 100644 --- a/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py +++ b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py @@ -5,7 +5,7 @@ import scipy from model.distributions.sphere.vonmises_fisher.random import VonMisesRandomSampling - +from model.distributions.sphere.vonmises_fisher.fibonachi import VonMisesFibSampling class vonMisesFisherDistribution(SphereDistribution): def __init__(self): @@ -15,6 +15,7 @@ def __init__(self): self.sampling_methods = [ VonMisesRandomSampling(), + VonMisesFibSampling() ] def get_name(self): diff --git a/poetry.lock b/poetry.lock index cbf49a3..f86271d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1909,7 +1909,7 @@ healpy-support = [] [package.source] type = "git" url = "https://github.com/KIT-ISAS/pyRecEst.git" -reference = "7ee536d" +reference = "0.1.1" resolved_reference = "7ee536d3da48f05855d2d4d2eec6321e065e9a85" [[package]] diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 908565a..b8030d4 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -3,6 +3,7 @@ import numpy as np import plotly.graph_objects as go import plotly.figure_factory as ff +import dash class Object3DRenderer: def __init__(self, object_3D, id): # dash doesnt like duplicate calback functions @@ -70,11 +71,15 @@ def _register_callbacks(self): # updates wich sampling methods are available once distribution is selected @callback( Output(f"sampling-selector-{self.id}", "options"), + Output(f"sampling-selector-{self.id}", "value"), Input("distribution-selector", "value"), ) def update_sampling_methods(selected_distribution): options = list(self.object.distributions[selected_distribution].sampling_method_dict.keys()) - return options + + # set safe initial value + initial_value = options[0] + return options, initial_value # updates the options (silders, etc) for the selected distribution and sampling method @callback( @@ -106,9 +111,13 @@ def update_curr_distribution(selected_distribution, selected_sampling): prevent_initial_call='initial_duplicate' ) def update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): - dist_options = self.object.distributions[selected_distribution].distribution_options - sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options - + try: + dist_options = self.object.distributions[selected_distribution].distribution_options + sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options + except KeyError: + # got stale values, ignore + return dash.no_update + # the order of options might not be guaranteed, so we map them by their ids id_value_dist = [(id,v) for id, v in zip(ids_dist, values_dist)] id_value_samp = [(id,v) for id, v in zip(ids_samp, values_samp)] From 793388abe965848b1696a411b5f529b63599d493 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 24 Oct 2025 18:39:57 +0200 Subject: [PATCH 031/152] use new fibonacci latices for mesh generation --- model/sphere/sphere.py | 50 +++++++++++++++--------------------- renderer/Object3DRenderer.py | 2 +- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index c29174f..eb729d9 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -1,11 +1,14 @@ import numpy as np import numpy as np from scipy.spatial import Delaunay +from scipy.spatial import ConvexHull import plotly.figure_factory as ff from model.distributions.distribution_loader import DistributionLoader from model.distributions.sphere.sphere_distribution import SphereDistribution from model.manifold import Manifold +from model.distributions.sphere.uniform.fibonachi_lattice import SphereUniformFibSampling +from util.selectors.slider import Slider class Sphere(Manifold): def __init__(self, resolution=50, radius=1): @@ -14,7 +17,10 @@ def __init__(self, resolution=50, radius=1): self.samples = np.array([]) self.distributions = DistributionLoader(SphereDistribution, "model.distributions.sphere").get_distributions() - self.dist_cache = {} + + self.mesh_xyz = self._init_mesh() + + def generate_xyz(self, resolution=50, radius=1): phi = np.linspace(0, np.pi, resolution) @@ -33,50 +39,36 @@ def update_sample(self, selected_distribution, selected_sampling_method, sample_ self.samples = sampling_method.sample(sample_options, distribution_options) - def generate_mesh(self, pdf, resolution=30, radius=1, alpha=0.5): - phi = np.linspace(0, np.pi, resolution) - theta = np.linspace(0, 2 * np.pi, resolution) - phi, theta = np.meshgrid(phi, theta) + def generate_mesh(self, pdf, alpha=1): + # mesh_xyz has nans for line segments, mask before passing to pdf + mask = np.all(np.isfinite(self.mesh_xyz), axis=-1) + dens = pdf(self.mesh_xyz[mask]) - phi = phi.flatten() - theta = theta.flatten() + xyz_extruded = np.full_like(self.mesh_xyz, np.nan, dtype=float) # full of nans - x = radius * np.sin(phi) * np.cos(theta) - y = radius * np.sin(phi) * np.sin(theta) - z = radius * np.cos(phi) + xyz_extruded[mask] = self.mesh_xyz[mask] * (1 + alpha * dens[:, np.newaxis]) + return xyz_extruded[:,0], xyz_extruded[:,1], xyz_extruded[:,2] - points2D = np.vstack([phi,theta]).T - tri = Delaunay(points2D) - simplices = tri.simplices + def _init_mesh(self, resolution=1000): + xyz = SphereUniformFibSampling.sample(None, [Slider("Number of Samples", 10, resolution, resolution)] , []) + x, y, z = xyz[:,0], xyz[:,1], xyz[:,2] - xyz = np.column_stack((x, y, z)) - dens = pdf(xyz) + hull = ConvexHull(xyz) + simplices = hull.simplices - # clamp color function just below max - # even though colorscale is not shown, this prevents ff.create_trisurf from crashing due to an overflow def cf(xi, yi, zi, zmin=np.min(z), zmax=np.max(z)): if zi > zmax: zi = np.nextafter(zmax, zmin) return zi - - # extrude by multiplying by 1 + dens * alpha - # not to scale, but the alpha makes the density function look more clearly - xyz_extruded = xyz * (1 + alpha * dens[:, np.newaxis]) - x, y, z = xyz_extruded[:,0], xyz_extruded[:,1], xyz_extruded[:,2] - fig = ff.create_trisurf(x=x, y=y, z=z, simplices=simplices, show_colorbar=False, color_func=cf ) - fig.data[0].update( - opacity=0, - color='black' - ) - - return fig.data[1].x, fig.data[1].y, fig.data[1].z + arr = np.column_stack((fig.data[1].x, fig.data[1].y, fig.data[1].z)) + return np.where(arr == None, np.nan, arr).astype(float) # line segments have None in them, put them to nan so we can do mult later @staticmethod def spherical_to_cartesian(theta, phi, r=1): diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index b8030d4..93bfc22 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -38,7 +38,7 @@ def __init__(self, object_3D, id): name="Density", x=[], y=[], z=[], mode="lines", - line=dict(color="black", width=1), + line=dict(color="#212121", width=0.5), showlegend=True, ), From c1d592723da0205da814fe9ce8eb9981edb73af9 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 24 Oct 2025 20:40:51 +0200 Subject: [PATCH 032/152] changed slider opt amount --- util/selectors/slider.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/selectors/slider.py b/util/selectors/slider.py index 174969c..54581f4 100644 --- a/util/selectors/slider.py +++ b/util/selectors/slider.py @@ -1,7 +1,7 @@ from dash import dcc, html from util.selectors.selector import Selector -SLIDER_OPT_AMOUNT = 100 +SLIDER_OPT_AMOUNT = 30 SLIDER_MARK_AMOUNT = 5 class Slider(Selector): From 2b3c977c563a886ca716ecf64913ca1f0a1e546f Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 27 Oct 2025 16:25:36 +0100 Subject: [PATCH 033/152] add norming for von miser --- .../sphere/vonmises_fisher/vonmises_fisher.py | 11 ++++++++++- model/sphere/sphere.py | 2 +- renderer/Object3DRenderer.py | 5 +++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py index 39ff95b..7e52ebe 100644 --- a/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py +++ b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py @@ -22,7 +22,16 @@ def get_name(self): return "von Mises-Fisher" def get_pdf(self, distribution_options): + alpha = 0.7 # scale + kappa = distribution_options[0].state def pdf(x): - return scipy.stats.vonmises_fisher.pdf(x, mu=[0,0,1], kappa=kappa) + + misf = scipy.stats.vonmises_fisher.pdf(x, mu=[0,0,1], kappa=kappa) + max = np.max(misf) + + norm = misf / max + norm = norm * alpha + return norm + return pdf \ No newline at end of file diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index eb729d9..11c6b32 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -49,7 +49,7 @@ def generate_mesh(self, pdf, alpha=1): xyz_extruded[mask] = self.mesh_xyz[mask] * (1 + alpha * dens[:, np.newaxis]) return xyz_extruded[:,0], xyz_extruded[:,1], xyz_extruded[:,2] - def _init_mesh(self, resolution=1000): + def _init_mesh(self, resolution=3000): xyz = SphereUniformFibSampling.sample(None, [Slider("Number of Samples", 10, resolution, resolution)] , []) x, y, z = xyz[:,0], xyz[:,1], xyz[:,2] diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 93bfc22..8a86411 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -61,6 +61,11 @@ def __init__(self, object_3D, id): xanchor="right", x=0.0 )) + config = { + 'responsive': True, + 'scrollZoom': True, + } + self.fig.show(config=config) self._register_callbacks() From a6f0e06b222464d981d79ae125f5d8024df80b41 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 27 Oct 2025 16:39:08 +0100 Subject: [PATCH 034/152] addaptive sample marker size --- renderer/Object3DRenderer.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 8a86411..690c6c3 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -150,6 +150,13 @@ def update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_di patched_figure["data"][1].y = self.object.samples[:, 1] patched_figure["data"][1].z = self.object.samples[:, 2] + # set size based on number of samples + sample_count = self.object.samples.shape[0] + marker_size = (1/ np.sqrt(sample_count) ) * 30 # about 3 for sample size 100; scaled by sqrt + marker_size = np.minimum(4.7,marker_size) + + + patched_figure["data"][1].marker.size = marker_size return patched_figure # updates the plot based on selected distribution options From ebbfed61d213181bb5af1ff0a79f079a64ee192a Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 27 Oct 2025 16:46:20 +0100 Subject: [PATCH 035/152] black border around samples --- renderer/Object3DRenderer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 690c6c3..d591c36 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -32,7 +32,11 @@ def __init__(self, object_3D, id): y=self.object.samples[:, 1] if self.object.samples.size else [], z=self.object.samples[:, 2] if self.object.samples.size else [], mode="markers", - marker=dict(size=4, color="red"), + marker=dict( + size=4, + color="red", + line=dict(width=1, color="black") + ), ), go.Scatter3d( name="Density", From 423108e679ed5f2129273eb9abdf17671a6d74db Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 27 Oct 2025 17:15:39 +0100 Subject: [PATCH 036/152] fix passing config the wrong way --- renderer/Object3DRenderer.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index d591c36..2e7ecf3 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -17,6 +17,11 @@ def __init__(self, object_3D, id): # inital figure + self.config = { + 'responsive': True, + 'scrollZoom': True, + } + self.fig = go.Figure( data=[ go.Surface( @@ -65,12 +70,6 @@ def __init__(self, object_3D, id): xanchor="right", x=0.0 )) - config = { - 'responsive': True, - 'scrollZoom': True, - } - self.fig.show(config=config) - self._register_callbacks() @@ -232,6 +231,6 @@ def get_layout_components(self): html.Div(id=f"sampling-options-{self.id}"), ] - graph = [dcc.Graph(id=f"graph-{self.id}", figure=self.fig)] + graph = [dcc.Graph(id=f"graph-{self.id}", figure=self.fig, config=self.config)] return options, graph \ No newline at end of file From 7f4f5b63fdfa709db96c70fb445eafc5b996e5bd Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 28 Oct 2025 16:44:32 +0100 Subject: [PATCH 037/152] fixed distributions and other minor changes --- app.py | 3 +- components/split_pane.py | 3 +- model/distributions/sphere/bingham/bingham.py | 4 +- model/distributions/sphere/watson/random.py | 4 +- model/distributions/sphere/watson/watson.py | 2 - poetry.lock | 484 +++++++++++++++++- pyproject.toml | 2 +- renderer/Object3DRenderer.py | 2 +- 8 files changed, 480 insertions(+), 24 deletions(-) diff --git a/app.py b/app.py index e7c50ad..a7a7e09 100644 --- a/app.py +++ b/app.py @@ -39,9 +39,10 @@ for page in dash.page_registry.values() ], pills=True, className='bg-light rounded-3'), html.P(), - dash.page_container + html.Div(dash.page_container, className="flex-grow-1") ], fluid=True, +className="vh-100 d-flex flex-column" ) if __name__ == '__main__': diff --git a/components/split_pane.py b/components/split_pane.py index be340e7..8c51fe4 100644 --- a/components/split_pane.py +++ b/components/split_pane.py @@ -16,7 +16,8 @@ def SplitPane(children1, children2, default_size): children=[ html.Div([ dbc.Container(children1, fluid=True) - ], className="bg-light h-100 w-100 rounded-3") + ], className="bg-light h-100 w-100 rounded-3", style={'overflowY': 'auto'}) + ], ), PanelResizeHandle( diff --git a/model/distributions/sphere/bingham/bingham.py b/model/distributions/sphere/bingham/bingham.py index 6dae81b..300e952 100644 --- a/model/distributions/sphere/bingham/bingham.py +++ b/model/distributions/sphere/bingham/bingham.py @@ -9,8 +9,8 @@ class BinghampDistribution(SphereDistribution): def __init__(self): self.distribution_options = [ - FloatSlider("Lambda 1 (λ₁)", -0.49, 0, 0), - FloatSlider("Lambda 2 (λ₂)", -0.49, 0, 0), + FloatSlider("Lambda 1 (λ₁)", 0, 0, 10), + FloatSlider("Lambda 2 (λ₂)", 0, 0, 10), ] self.sampling_methods = [ diff --git a/model/distributions/sphere/watson/random.py b/model/distributions/sphere/watson/random.py index 3356336..146a65d 100644 --- a/model/distributions/sphere/watson/random.py +++ b/model/distributions/sphere/watson/random.py @@ -20,8 +20,8 @@ def get_name(self): def sample(self, sample_options, distribution_options): kappa = distribution_options[0].state - theta = distribution_options[1].state - phi = distribution_options[2].state + theta = 0 # can be hardcoded because the user can just turn the sphere + phi = 0 numsamp = sample_options[0].state diff --git a/model/distributions/sphere/watson/watson.py b/model/distributions/sphere/watson/watson.py index a817fd4..914805e 100644 --- a/model/distributions/sphere/watson/watson.py +++ b/model/distributions/sphere/watson/watson.py @@ -11,8 +11,6 @@ class WatsonDistribution(SphereDistribution): def __init__(self): self.distribution_options = [ Slider("κ (kappa)", 0.0, 10.0, 50.0), - FloatSlider("direction: polar angle (θ)", 0, 0, np.pi), - FloatSlider("direction: azimuthal angle (φ)", 0, 0, 2 * np.pi), ] self.sampling_methods = [ diff --git a/poetry.lock b/poetry.lock index f86271d..0e35a7f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,75 @@ # This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +[[package]] +name = "astropy" +version = "6.1.7" +description = "Astronomy and astrophysics core library" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "astropy-6.1.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:be954c5f7707a089609053665aeb76493b79e5c4753c39486761bc6d137bf040"}, + {file = "astropy-6.1.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b5e48df5ab2e3e521e82a7233a4b1159d071e64e6cbb76c45415dc68d3b97af1"}, + {file = "astropy-6.1.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55c78252633c644361e2f7092d71f80ef9c2e6649f08d97711d9f19af514aedc"}, + {file = "astropy-6.1.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985e5e74489d23f1a11953b6b283fccde3f46cb6c68fee4f7228e5f6d8350ba9"}, + {file = "astropy-6.1.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc2ea28ed41a3d92c39b1481d9c5be016ae58d68f144f3fd8cecffe503525bab"}, + {file = "astropy-6.1.7-cp310-cp310-win32.whl", hash = "sha256:4e4badadd8dfa5dca08fd86e9a50a3a91af321975859f5941579e6b7ce9ba199"}, + {file = "astropy-6.1.7-cp310-cp310-win_amd64.whl", hash = "sha256:8d7f6727689288ee08fc0a4a297fc7e8089d01718321646bd00fea0906ad63dc"}, + {file = "astropy-6.1.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09edca01276ee63f7b2ff511da9bfb432068ba3242e27ef27d76e5a171087b7e"}, + {file = "astropy-6.1.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:072f62a67992393beb016dc80bee8fb994fda9aa69e945f536ed8ac0e51291e6"}, + {file = "astropy-6.1.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2706156d3646f9c9a7fc810475d8ab0df4c717beefa8326552576a0f8ddca20"}, + {file = "astropy-6.1.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcd99e627692f8e58bb3097d330bfbd109a22e00dab162a67f203b0a0601ad2c"}, + {file = "astropy-6.1.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b0ebbcb637b2e9bcb73011f2b7890d7a3f5a41b66ccaad7c28f065e81e28f0b2"}, + {file = "astropy-6.1.7-cp311-cp311-win32.whl", hash = "sha256:192b12ede49cd828362ab1a6ede2367fe203f4d851804ec22fa92e009a524281"}, + {file = "astropy-6.1.7-cp311-cp311-win_amd64.whl", hash = "sha256:3cac64bcdf570c947019bd2bc96711eeb2c7763afe192f18c9551e52a6c296b2"}, + {file = "astropy-6.1.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f2a8bcbb1306052cc38c9eed2c9331bfafe2582b499a7321946abf74b26eb256"}, + {file = "astropy-6.1.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:eaf88878684f9d31aff36475c90d101f4cff22fdd4fd50098d9950fd56994df7"}, + {file = "astropy-6.1.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cb8cd231e53556e4eebe0393ea95a8cea6b2ff4187c95ac4ff8b17e7a8da823"}, + {file = "astropy-6.1.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ad36334d138a4f71d6fdcf225a98ad1dad6c343da4362d5a47a71f5c9da3ca9"}, + {file = "astropy-6.1.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dd731c526869d0c68507be7b31dd10871b7c44d310bb5495476505560c83cd33"}, + {file = "astropy-6.1.7-cp312-cp312-win32.whl", hash = "sha256:662bacd7ae42561e038cbd85eea3b749308cf3575611a745b60f034d3350c97a"}, + {file = "astropy-6.1.7-cp312-cp312-win_amd64.whl", hash = "sha256:5b4d02a98a0bf91ff7fd4ef0bd0ecca83c9497338cb88b61ec9f971350688222"}, + {file = "astropy-6.1.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fbeaf04427987c0c6fa2e579eb40011802b06fba6b3a7870e082d5c693564e1b"}, + {file = "astropy-6.1.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab6e88241a14185b9404b02246329185b70292984aa0616b20a0628dfe4f4ebb"}, + {file = "astropy-6.1.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0529c75565feaabb629946806b4763ae7b02069aeff4c3b56a69e8a9e638500"}, + {file = "astropy-6.1.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c5ec347631da77573fc729ba04e5d89a3bc94500bf6037152a2d0f9965ae1ce"}, + {file = "astropy-6.1.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc496f87aaccaa5c6624acc985b8770f039c5bbe74b120c8ed7bad3698e24e1b"}, + {file = "astropy-6.1.7-cp313-cp313-win32.whl", hash = "sha256:b1e01d534383c038dbf8664b964fa4ea818c7419318830d3c732c750c64115c6"}, + {file = "astropy-6.1.7-cp313-cp313-win_amd64.whl", hash = "sha256:af08cf2b0368f1ea585eb26a55d99a2de9e9b0bd30aba84b5329059c3ec33590"}, + {file = "astropy-6.1.7.tar.gz", hash = "sha256:a405ac186306b6cb152e6df2f7444ab8bd764e4127d7519da1b3ae4dd65357ef"}, +] + +[package.dependencies] +astropy-iers-data = ">=0.2024.10.28.0.34.7" +numpy = ">=1.23" +packaging = ">=19.0" +pyerfa = ">=2.0.1.1" +PyYAML = ">=3.13" + +[package.extras] +all = ["asdf-astropy (>=0.3)", "astropy[recommended]", "astropy[typing]", "beautifulsoup4", "bleach", "bottleneck", "certifi", "dask[array]", "fsspec[http] (>=2023.4.0)", "h5py", "html5lib", "ipython (>=4.2)", "jplephem", "mpmath", "pandas", "pre-commit", "pyarrow (>=7.0.0)", "pytest (>=7.0)", "pytz", "s3fs (>=2023.4.0)", "sortedcontainers"] +docs = ["Jinja2 (>=3.1.3)", "astropy[recommended]", "matplotlib (>=3.9.1)", "numpy (<2.0)", "pytest (>=7.0)", "sphinx", "sphinx-astropy[confv2] (>=1.9.1)", "sphinx-changelog (>=1.2.0)", "sphinx_design", "sphinxcontrib-globalsubs (>=0.1.1)", "tomli ; python_version < \"3.11\""] +recommended = ["matplotlib (>=3.5.0,!=3.5.2)", "scipy (>=1.8)"] +test = ["pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist", "threadpoolctl"] +test-all = ["array-api-strict", "astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "sgp4 (>=2.3)", "skyfield (>=1.20)"] +typing = ["typing_extensions (>=4.0.0)"] + +[[package]] +name = "astropy-iers-data" +version = "0.2025.10.27.0.39.10" +description = "IERS Earth Rotation and Leap Second tables for the astropy core package" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "astropy_iers_data-0.2025.10.27.0.39.10-py3-none-any.whl", hash = "sha256:aa91d24155eec2e7dafffd9ce0f046cd1fd6bcd995a76904aba3ef3f6836c4dc"}, + {file = "astropy_iers_data-0.2025.10.27.0.39.10.tar.gz", hash = "sha256:2a0630f810bcba7978cc5f3f92a45910b5ea95d885302b1879b0132e920302ed"}, +] + +[package.extras] +docs = ["pytest"] +test = ["hypothesis", "pytest", "pytest-remotedata"] + [[package]] name = "attrs" version = "25.4.0" @@ -12,6 +82,26 @@ files = [ {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, ] +[[package]] +name = "beartype" +version = "0.22.4" +description = "Unbearably fast near-real-time pure-Python runtime-static type-checker." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "beartype-0.22.4-py3-none-any.whl", hash = "sha256:7967a1cee01fee42e47da69c58c92da10ba5bcfb8072686e48487be5201e3d10"}, + {file = "beartype-0.22.4.tar.gz", hash = "sha256:68284c7803efd190b1b4639a0ab1a17677af9571b8a2ef5a169d10cb8955b01f"}, +] + +[package.extras] +dev = ["autoapi (>=0.9.0)", "celery", "click", "coverage (>=5.5)", "equinox ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "fastmcp ; python_version > \"3.9.0\" and python_version < \"3.14.0\"", "jax[cpu] ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "jaxtyping ; sys_platform == \"linux\"", "langchain ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "numba ; python_version < \"3.14.0\"", "numpy ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera (>=0.26.0) ; python_version < \"3.14.0\"", "poetry", "polars ; python_version < \"3.14.0\"", "pydata-sphinx-theme (<=0.7.2)", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "redis", "rich-click", "setuptools", "sphinx", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)", "sqlalchemy", "torch ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "tox (>=3.20.1)", "typer", "typing-extensions (>=3.10.0.0)", "xarray ; python_version < \"3.14.0\""] +doc-ghp = ["mkdocs-material[imaging] (>=9.6.0)", "mkdocstrings-python (>=1.16.0)", "mkdocstrings-python-xref (>=1.16.0)"] +doc-rtd = ["autoapi (>=0.9.0)", "pydata-sphinx-theme (<=0.7.2)", "setuptools", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)"] +test = ["celery", "click", "coverage (>=5.5)", "equinox ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "fastmcp ; python_version > \"3.9.0\" and python_version < \"3.14.0\"", "jax[cpu] ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "jaxtyping ; sys_platform == \"linux\"", "langchain ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "numba ; python_version < \"3.14.0\"", "numpy ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera (>=0.26.0) ; python_version < \"3.14.0\"", "poetry", "polars ; python_version < \"3.14.0\"", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "redis", "rich-click", "sphinx", "sqlalchemy", "torch ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "tox (>=3.20.1)", "typer", "typing-extensions (>=3.10.0.0)", "xarray ; python_version < \"3.14.0\""] +test-tox = ["celery", "click", "equinox ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "fastmcp ; python_version > \"3.9.0\" and python_version < \"3.14.0\"", "jax[cpu] ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "jaxtyping ; sys_platform == \"linux\"", "langchain ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "numba ; python_version < \"3.14.0\"", "numpy ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera (>=0.26.0) ; python_version < \"3.14.0\"", "poetry", "polars ; python_version < \"3.14.0\"", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "redis", "rich-click", "sphinx", "sqlalchemy", "torch ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "typer", "typing-extensions (>=3.10.0.0)", "xarray ; python_version < \"3.14.0\""] +test-tox-coverage = ["coverage (>=5.5)"] + [[package]] name = "beautifulsoup4" version = "4.14.2" @@ -302,11 +392,11 @@ description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" groups = ["main", "dev"] -markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +markers = {main = "platform_system == \"Windows\"", dev = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "contourpy" @@ -672,7 +762,7 @@ version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, @@ -883,7 +973,7 @@ version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.10" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, @@ -1497,6 +1587,48 @@ files = [ {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] +[[package]] +name = "numpy-quaternion" +version = "2024.0.12" +description = "Add a quaternion dtype to NumPy" +optional = false +python-versions = "<3.14,>=3.10" +groups = ["main"] +files = [ + {file = "numpy_quaternion-2024.0.12-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:606b0fb2d58a0b15419432a3714020b8c60fc6a829d9d1e962ae21c769e95dad"}, + {file = "numpy_quaternion-2024.0.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:27d8ffaa871ba67ca599008692926bb45b964f1eedd83021699a137a0d510a7f"}, + {file = "numpy_quaternion-2024.0.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c48c61c0feb952347486816a0fe5d2712a1f57a427ed28cf845c570135d592c3"}, + {file = "numpy_quaternion-2024.0.12-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:536f8448c26d9af2747fc10f7ed8f9b308c840afbae14c617a1af9e02e5eb3ed"}, + {file = "numpy_quaternion-2024.0.12-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76de6b14aaf5a2578bc03d7d2118235b8c668cc3aeab4fdc48fda1f189f31f1c"}, + {file = "numpy_quaternion-2024.0.12-cp310-cp310-win_amd64.whl", hash = "sha256:f862f0a1a9ad60966fdc37ee61c233da105e6776da70da81c010adc082743798"}, + {file = "numpy_quaternion-2024.0.12-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:19e6ba157bc15d48f8d6f8be25f200d3a0858ae655e54a30bd423ca0d3aa5659"}, + {file = "numpy_quaternion-2024.0.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb8087c737422ff893163782dd99188e159aac5dcddec83538ad53a172755d3d"}, + {file = "numpy_quaternion-2024.0.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa37c4102f47dc5e675dd6714f390edcc6a9c3151a427d34bd5778e71f26d489"}, + {file = "numpy_quaternion-2024.0.12-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16f797e1733d04b48f3ad86b4e38398d49415f28a9d7d55b5e541533c3badddf"}, + {file = "numpy_quaternion-2024.0.12-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989e6edf66b8b083354b24e7303d405ed9cd76d29b37852474794c8b2cab5b38"}, + {file = "numpy_quaternion-2024.0.12-cp311-cp311-win_amd64.whl", hash = "sha256:63b959ed309dbd0e8da2e41f2574b4b8f3d8f972bf3c49fdc54de56cdf28687f"}, + {file = "numpy_quaternion-2024.0.12-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f7c96c1faac4d6c6ab2505beb26a82f2e8b3d3470d72c92af1837bc8f883378e"}, + {file = "numpy_quaternion-2024.0.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:947a5afbf1f42f6a86c5ea288165631bde3eb243fcb50e50af611149863bb541"}, + {file = "numpy_quaternion-2024.0.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:28cbece557ab04b393b1864f8790d1b4482e49cbc98566549ec3490e5af4d8db"}, + {file = "numpy_quaternion-2024.0.12-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8257cad3b9586d04e4c8f9a2d182b72a35e068d0e787dd5560838275be87537"}, + {file = "numpy_quaternion-2024.0.12-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33a993241d7f885654c5496db2f581ff86f4cb6a298671419956223cab3535c2"}, + {file = "numpy_quaternion-2024.0.12-cp312-cp312-win_amd64.whl", hash = "sha256:aea04cc3d01676fca03ca23b7c723a191d31af3cff853ddc47a2422647a03939"}, + {file = "numpy_quaternion-2024.0.12-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:66d3072b9f01b853b32ea99670bb739631aef36ac2317fd845e90b2c887ebd6c"}, + {file = "numpy_quaternion-2024.0.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c7c302a828b80d2553be50bbde978ed0b6c1dfc7be310afaa083acc75a2e6b3"}, + {file = "numpy_quaternion-2024.0.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e54e41ca9991832aa0303c8956151f1933fc15b5645ef5b8fb6b93356ee30b8b"}, + {file = "numpy_quaternion-2024.0.12-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f947b8fdccb1cfe6d1157578477741e0c51ba1bb4d43358c5fbb7b26183942fc"}, + {file = "numpy_quaternion-2024.0.12-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02503230d3e5678e955ad1a64cd1738ff2739b9a1cd3fe58d5bc98fd491908f"}, + {file = "numpy_quaternion-2024.0.12-cp313-cp313-win_amd64.whl", hash = "sha256:9ce0fa7af5075b0b0c39bb1ac80fb3fb8b9c5ae3c2140672933391b295f84210"}, + {file = "numpy_quaternion-2024.0.12.tar.gz", hash = "sha256:5ecb4e310e732bc21687474c1bc6cd6187d5c22da78d342379fe81f12f46037f"}, +] + +[package.dependencies] +numpy = ">=1.25,<3" +scipy = ">=1.5,<2" + +[package.extras] +docs = ["mkdocs-material", "mkdocstrings-python", "pymdown-extensions"] + [[package]] name = "openpyxl" version = "3.1.5" @@ -1759,6 +1891,23 @@ test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] xmp = ["defusedxml"] +[[package]] +name = "platformdirs" +version = "4.5.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3"}, + {file = "platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312"}, +] + +[package.extras] +docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] +type = ["mypy (>=1.18.2)"] + [[package]] name = "plotly" version = "6.3.1" @@ -1789,7 +1938,7 @@ version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, @@ -1799,6 +1948,28 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "pooch" +version = "1.8.2" +description = "A friend to fetch your data files" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "pooch-1.8.2-py3-none-any.whl", hash = "sha256:3529a57096f7198778a5ceefd5ac3ef0e4d06a6ddaf9fc2d609b806f25302c47"}, + {file = "pooch-1.8.2.tar.gz", hash = "sha256:76561f0de68a01da4df6af38e9955c4c9d1a5c90da73f7e40276a5728ec83d10"}, +] + +[package.dependencies] +packaging = ">=20.0" +platformdirs = ">=2.5.0" +requests = ">=2.19.0" + +[package.extras] +progress = ["tqdm (>=4.41.0,<5.0.0)"] +sftp = ["paramiko (>=2.7.0)"] +xxhash = ["xxhash (>=1.4.3)"] + [[package]] name = "psutil" version = "7.1.1" @@ -1835,13 +2006,41 @@ files = [ {file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"}, ] +[[package]] +name = "pyerfa" +version = "2.0.1.5" +description = "Python bindings for ERFA" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyerfa-2.0.1.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b282d7c60c4c47cf629c484c17ac504fcb04abd7b3f4dfcf53ee042afc3a5944"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:be1aeb70390dd03a34faf96749d5cabc58437410b4aab7213c512323932427df"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0603e8e1b839327d586c8a627cdc634b795e18b007d84f0cda5500a0908254e"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e43c7194e3242083f2350b46c09fd4bf8ba1bcc0ebd1460b98fc47fe2389906"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:07b80cd70701f5d066b1ac8cce406682cfcd667a1186ec7d7ade597239a6021d"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-win32.whl", hash = "sha256:d30b9b0df588ed5467e529d851ea324a67239096dd44703125072fd11b351ea2"}, + {file = "pyerfa-2.0.1.5-cp39-abi3-win_amd64.whl", hash = "sha256:66292d437dcf75925b694977aa06eb697126e7b86553e620371ed3e48b5e0ad0"}, + {file = "pyerfa-2.0.1.5-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4991dee680ff36c87911d8faa4c7d1aa6278ad9b5e0d16158cf22fa7d74ba25c"}, + {file = "pyerfa-2.0.1.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690e258294202c86f479e78e80fd235cd27bd717f7f60062fccc3dbd6ef0b1a9"}, + {file = "pyerfa-2.0.1.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:171ce9676a448a7eb555f03aa19ad5c749dbced1ce4f9923e4d93443c4a9c612"}, + {file = "pyerfa-2.0.1.5.tar.gz", hash = "sha256:17d6b24fe4846c65d5e7d8c362dcb08199dc63b30a236aedd73875cc83e1f6c0"}, +] + +[package.dependencies] +numpy = ">=1.19.3" + +[package.extras] +docs = ["sphinx-astropy (>=1.3)"] +test = ["pytest", "pytest-doctestplus (>=0.7)"] + [[package]] name = "pygments" version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, @@ -1887,30 +2086,82 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyrecest" -version = "0.1.0" +version = "0.8.0" description = "Framework for recursive Bayesian estimation in Python." optional = false -python-versions = "^3.10" +python-versions = ">=3.10,<3.13" groups = ["main"] files = [] develop = false [package.dependencies] +beartype = "*" filterpy = "*" matplotlib = "*" mpmath = "*" numpy = "*" -pytest = "*" -scipy = "*" +numpy-quaternion = "*" +pyshtools = "*" +scipy = "^1.14.1" +shapely = "*" [package.extras] healpy-support = [] +jax-support = [] +pytorch-support = [] [package.source] type = "git" url = "https://github.com/KIT-ISAS/pyRecEst.git" -reference = "0.1.1" -resolved_reference = "7ee536d3da48f05855d2d4d2eec6321e065e9a85" +reference = "002c7757e2a5a81e4e57da5c1b657c5f9bd5cf6e" +resolved_reference = "002c7757e2a5a81e4e57da5c1b657c5f9bd5cf6e" + +[[package]] +name = "pyshtools" +version = "4.13.1" +description = "SHTOOLS - Spherical Harmonic Tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyshtools-4.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f6235c5616c17a5b214af2575571dee7eb15f58ec5b382081ace47960a10e14"}, + {file = "pyshtools-4.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4059d67721c7ba033ae591cad8f32fa26149892e833e292d512824bbedce445"}, + {file = "pyshtools-4.13.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:2926cbef344092ced413575001f012eeb001ef88d5c7e0fd5d45e21449471416"}, + {file = "pyshtools-4.13.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:d7b566cfe7499f19e4d7db2eb336e1ad9a20faf609788e7b696bb3ad03312aa5"}, + {file = "pyshtools-4.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:31a3549c6d0b8299b5f7d7369c46d541c02e045f2db65b7b01949e0fdd0de60d"}, + {file = "pyshtools-4.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ea6b21d7d1e22361c7902b500a3a5da5f7ca24d8c866b3aa319a83f8ab09a96"}, + {file = "pyshtools-4.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4a1c1917fc7e968f27e0551d784f64eab7e344549b0e9bc77700b3b5c8f617a9"}, + {file = "pyshtools-4.13.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:c73d6cdeee11fc058ba840ab12037a3390fa6ae18bca507f9ade559489aa0c09"}, + {file = "pyshtools-4.13.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:0d8dfec4aa43486b0ea7f23654bf63f26309c4448dc8a06df43cd9affe18db11"}, + {file = "pyshtools-4.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:afc93c79de48b9e8ce7eb71e518356c1c11221a0791b37c827ef5fbd48d93e55"}, + {file = "pyshtools-4.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:8e3131d2fd148b1f1089ae8e044c8b28ef988033fc1315ff600b65964fa179e0"}, + {file = "pyshtools-4.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bd367d5a389a2e7d6985ad4d093c486c13c47b1d632dd28ff273a0f73490ab"}, + {file = "pyshtools-4.13.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:53bbeb0a01fe8b6b7e662b05b3d0f67685d61c2c8f36cad363455923aeaa5353"}, + {file = "pyshtools-4.13.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f9a4d22a3305540788928421fb5673c102f14fd78904012e53a44bf438c2484e"}, + {file = "pyshtools-4.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:71f5ea1cddcb5b210a12488dc4899e50219e6fb8727d03c3584d433ac62e9248"}, + {file = "pyshtools-4.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f010fc0d610063ab16d7c6048fac6b611dc5d3398c53b515ef6789ee27c9b5c5"}, + {file = "pyshtools-4.13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:487f8368308ebac20b1eb2c6bd03a927f27121a02bdeeafe371b998776f34f71"}, + {file = "pyshtools-4.13.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:0ad505acc3e6e1a9839938e959d3c188fadcf915aad9c7680d9211246a55e32b"}, + {file = "pyshtools-4.13.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:dc798e61614a7411a90bc63d1a44faf55cfc90c842284cd27167759ea72a4e31"}, + {file = "pyshtools-4.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:8994277075608fb8eeaa5d1e43b66dfc50c2f5b276b3325197cd7d2107fcf875"}, + {file = "pyshtools-4.13.1.tar.gz", hash = "sha256:cc4a323e9cbc905c04ae9e2e9fedeea6d76f3315a6863ede353a4dec87b8c018"}, +] + +[package.dependencies] +astropy = ">=4.0" +matplotlib = ">=3.3" +numpy = ">=1.23.5" +pooch = ">=1.1" +requests = "*" +scipy = ">=0.14.0" +tqdm = "*" +xarray = "*" + +[package.extras] +cartopy = ["cartopy (>=0.18.0)"] +ducc = ["ducc0 (>=0.15)"] +palettable = ["palettable (>=3.3)"] +pygmt = ["pygmt (>=0.7)"] [[package]] name = "pysocks" @@ -1931,7 +2182,7 @@ version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, @@ -1976,6 +2227,89 @@ files = [ {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + [[package]] name = "requests" version = "2.32.5" @@ -2111,6 +2445,80 @@ enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +[[package]] +name = "shapely" +version = "2.1.2" +description = "Manipulation and analysis of geometric objects" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f"}, + {file = "shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea"}, + {file = "shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f"}, + {file = "shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142"}, + {file = "shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4"}, + {file = "shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0"}, + {file = "shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e"}, + {file = "shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f"}, + {file = "shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618"}, + {file = "shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d"}, + {file = "shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09"}, + {file = "shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26"}, + {file = "shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7"}, + {file = "shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2"}, + {file = "shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6"}, + {file = "shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc"}, + {file = "shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94"}, + {file = "shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359"}, + {file = "shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3"}, + {file = "shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b"}, + {file = "shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc"}, + {file = "shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d"}, + {file = "shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454"}, + {file = "shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179"}, + {file = "shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8"}, + {file = "shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a"}, + {file = "shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e"}, + {file = "shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6"}, + {file = "shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af"}, + {file = "shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd"}, + {file = "shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350"}, + {file = "shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715"}, + {file = "shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40"}, + {file = "shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b"}, + {file = "shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801"}, + {file = "shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0"}, + {file = "shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c"}, + {file = "shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99"}, + {file = "shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf"}, + {file = "shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c"}, + {file = "shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223"}, + {file = "shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c"}, + {file = "shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df"}, + {file = "shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf"}, + {file = "shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4"}, + {file = "shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc"}, + {file = "shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566"}, + {file = "shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c"}, + {file = "shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a"}, + {file = "shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076"}, + {file = "shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1"}, + {file = "shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0"}, + {file = "shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26"}, + {file = "shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0"}, + {file = "shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735"}, + {file = "shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9"}, + {file = "shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9"}, +] + +[package.dependencies] +numpy = ">=1.21" + +[package.extras] +docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] +test = ["pytest", "pytest-cov", "scipy-doctest"] + [[package]] name = "six" version = "1.17.0" @@ -2203,7 +2611,7 @@ version = "2.3.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] +groups = ["dev"] files = [ {file = "tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45"}, {file = "tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba"}, @@ -2249,6 +2657,28 @@ files = [ {file = "tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549"}, ] +[[package]] +name = "tqdm" +version = "4.67.1" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[package.extras] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] +notebook = ["ipywidgets (>=6)"] +slack = ["slack-sdk"] +telegram = ["requests"] + [[package]] name = "trio" version = "0.31.0" @@ -2444,6 +2874,32 @@ files = [ [package.dependencies] h11 = ">=0.9.0,<1" +[[package]] +name = "xarray" +version = "2025.6.1" +description = "N-D labeled arrays and datasets in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "xarray-2025.6.1-py3-none-any.whl", hash = "sha256:8b988b47f67a383bdc3b04c5db475cd165e580134c1f1943d52aee4a9c97651b"}, + {file = "xarray-2025.6.1.tar.gz", hash = "sha256:a84f3f07544634a130d7dc615ae44175419f4c77957a7255161ed99c69c7c8b0"}, +] + +[package.dependencies] +numpy = ">=1.24" +packaging = ">=23.2" +pandas = ">=2.1" + +[package.extras] +accel = ["bottleneck", "flox", "numba (>=0.54)", "numbagg", "opt_einsum", "scipy"] +complete = ["xarray[accel,etc,io,parallel,viz]"] +etc = ["sparse"] +io = ["cftime", "fsspec", "h5netcdf", "netCDF4", "pooch", "pydap ; python_version < \"3.10\"", "scipy", "zarr"] +parallel = ["dask[complete]"] +types = ["pandas-stubs", "scipy-stubs", "types-PyYAML", "types-Pygments", "types-colorama", "types-decorator", "types-defusedxml", "types-docutils", "types-networkx", "types-openpyxl", "types-pexpect", "types-psutil", "types-pycurl", "types-python-dateutil", "types-pytz", "types-setuptools"] +viz = ["cartopy", "matplotlib", "nc-time-axis", "seaborn"] + [[package]] name = "zipp" version = "3.23.0" @@ -2467,4 +2923,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.11" -content-hash = "b375ecd041a9df25d8d3701a89510f53f03c012fc540f78f7751e19e9f29e28e" +content-hash = "8b6c1e5300ee6ef84a7b8830e0dd0d135db6eb4c913b60b5ebd176f0d37b30b7" diff --git a/pyproject.toml b/pyproject.toml index a1d9cb5..67f16bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "scipy (>=1.13,<2.0)", "gunicorn (>=23.0.0,<24.0.0)", "sphstat (>=1.0.6,<2.0.0)", - "pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@0.1.1", + "pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@002c7757e2a5a81e4e57da5c1b657c5f9bd5cf6e", ] [dependency-groups] diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 2e7ecf3..14c6b6e 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -231,6 +231,6 @@ def get_layout_components(self): html.Div(id=f"sampling-options-{self.id}"), ] - graph = [dcc.Graph(id=f"graph-{self.id}", figure=self.fig, config=self.config)] + graph = [dcc.Graph(id=f"graph-{self.id}", figure=self.fig, config=self.config, style={'height': '100%'})] return options, graph \ No newline at end of file From a42ffff2b924c806599c0f8cd5d67e5b4accbed8 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 29 Oct 2025 11:48:35 +0100 Subject: [PATCH 038/152] finally fix css --- app.py | 2 +- assets/full_page.css | 13 +++++++++++++ components/split_pane.py | 19 ++++++++++--------- 3 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 assets/full_page.css diff --git a/app.py b/app.py index a7a7e09..4dbdde1 100644 --- a/app.py +++ b/app.py @@ -39,7 +39,7 @@ for page in dash.page_registry.values() ], pills=True, className='bg-light rounded-3'), html.P(), - html.Div(dash.page_container, className="flex-grow-1") + html.Div(dash.page_container, className="flex-grow-1 d-flex flex-column", id="outer-page-container", style={'minHeight': '0'}) ], fluid=True, className="vh-100 d-flex flex-column" diff --git a/assets/full_page.css b/assets/full_page.css new file mode 100644 index 0000000..cb06408 --- /dev/null +++ b/assets/full_page.css @@ -0,0 +1,13 @@ +/* Dash puts pages inside 2 divs that are not styleable otherwise */ +#outer-page-container > * { /* this is the empty div dash puts pages inside */ + flex-grow: 1; + display: flex; + flex-direction: column; + min-height: 0; +} +#_pages_content { + flex-grow: 1; + display: flex; + flex-direction: column; + min-height: 0; +} \ No newline at end of file diff --git a/components/split_pane.py b/components/split_pane.py index 8c51fe4..175b7ec 100644 --- a/components/split_pane.py +++ b/components/split_pane.py @@ -14,10 +14,10 @@ def SplitPane(children1, children2, default_size): minSizePercentage=15, defaultSizePercentage=default_size, children=[ - html.Div([ - dbc.Container(children1, fluid=True) - ], className="bg-light h-100 w-100 rounded-3", style={'overflowY': 'auto'}) - + html.Div( + children1, + className="bg-light h-100 w-100 rounded-3 p-3 pb-5", style={'overflowY': 'scroll', 'overflowX': 'hidden', 'minHeight': '0'}, + ) ], ), PanelResizeHandle( @@ -35,14 +35,15 @@ def SplitPane(children1, children2, default_size): Panel( id='plot-panel', minSizePercentage=15, - children=[ - dbc.Container(children2, fluid=True) - ], + children=children2, ) ], direction='horizontal', - className='h-100 w-100 px-0', + className='w-100 px-0 pb-2', + style={'minHeight': '0'} ) ], - className='h-100 px-0', + className='px-0 flex-grow-1 d-flex flex-column', + id='pangelgroup-parent-container', + style={'minHeight': '0'} ) \ No newline at end of file From 0b7dedca15f7e153d4f2b95d6a29e2b082484400 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 29 Oct 2025 11:57:24 +0100 Subject: [PATCH 039/152] put legend inside plot area --- renderer/Object3DRenderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 14c6b6e..f7ec43d 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -68,7 +68,7 @@ def __init__(self, object_3D, id): yanchor="top", y=0.99, xanchor="right", - x=0.0 + x=0.1, )) self._register_callbacks() From 871a6d53d9226a0cf50377b2b88fd2704259283a Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 29 Oct 2025 12:09:44 +0100 Subject: [PATCH 040/152] simplify kent parameters --- model/distributions/sphere/kent/kent.py | 2 -- model/distributions/sphere/kent/random.py | 11 +++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/model/distributions/sphere/kent/kent.py b/model/distributions/sphere/kent/kent.py index 4221254..c69a2a5 100644 --- a/model/distributions/sphere/kent/kent.py +++ b/model/distributions/sphere/kent/kent.py @@ -12,8 +12,6 @@ def __init__(self): self.distribution_options = [ Slider("κ (kappa)", 0.0, 10.0, 50.0), Slider("β (beta)", 0.0, 2.0, 25.0), - FloatSlider("mu: Mean vector of Kent distribution: (θ)", 0, 0, np.pi), - FloatSlider("mu: Mean vector of Kent distribution: (φ)", 0, 0, 2 * np.pi), FloatSlider("mu0: Mean vector of the Fisher part: (θ)", 0, np.pi, np.pi), # default values so that distibution is initially visible FloatSlider("mu0: Mean vector of the Fisher part: (φ)", 0, 0, 2 * np.pi), ] diff --git a/model/distributions/sphere/kent/random.py b/model/distributions/sphere/kent/random.py index c360491..ceb07ca 100644 --- a/model/distributions/sphere/kent/random.py +++ b/model/distributions/sphere/kent/random.py @@ -22,10 +22,13 @@ def sample(self, sample_options, distribution_options): beta = distribution_options[1].state beta = min(beta, kappa / 2) # TODO make this dynamic - mu_theta = distribution_options[2].state - mu_phi = distribution_options[3].state - mu0_theta = distribution_options[4].state - mu0_phi = distribution_options[5].state + + # can be hardcoded because the user can just turn the sphere + # (only affects where the distribution is centered) + mu_theta = 0 + mu_phi = 0 + mu0_theta = distribution_options[2].state + mu0_phi = distribution_options[3].state mu1, mu2, mu3 = Sphere.spherical_to_cartesian(mu_theta, mu_phi) mu0_1, mu0_2, mu0_3 = Sphere.spherical_to_cartesian(mu0_theta, mu0_phi) From 301f005aa7550196ee474d55e38d1d6ff4a40d16 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 29 Oct 2025 14:04:54 +0100 Subject: [PATCH 041/152] added bingham pdf --- model/distributions/sphere/bingham/bingham.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/model/distributions/sphere/bingham/bingham.py b/model/distributions/sphere/bingham/bingham.py index 300e952..b5d4f16 100644 --- a/model/distributions/sphere/bingham/bingham.py +++ b/model/distributions/sphere/bingham/bingham.py @@ -3,6 +3,10 @@ from util.selectors.slider_float import FloatSlider import numpy as np +import pyrecest._backend +from pyrecest.backend import array +from pyrecest.distributions import BinghamDistribution + from model.distributions.sphere.bingham.random import BinghamRandomSampling @@ -21,4 +25,16 @@ def get_name(self): return "Bingham" def get_pdf(self, distribution_options): - return None \ No newline at end of file + l1 = distribution_options[0].state + l2 = distribution_options[1].state + + lambdas = np.sort(np.array([l1, l2, 0]))[::-1] + lambdas = -lambdas + print(lambdas) + M = np.eye(3) + bingham_dist = BinghamDistribution(M=array(M), Z=array(lambdas)) + + + def pdf(x): + return bingham_dist.pdf(array(x)) + return pdf \ No newline at end of file From 8227e7b75cec79eab7da156e23d49f9e0d20beac Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 29 Oct 2025 14:24:40 +0100 Subject: [PATCH 042/152] added watson and scaling --- model/distributions/sphere/bingham/bingham.py | 16 +++++++++----- model/distributions/sphere/watson/watson.py | 21 ++++++++++++++++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/model/distributions/sphere/bingham/bingham.py b/model/distributions/sphere/bingham/bingham.py index b5d4f16..030fd64 100644 --- a/model/distributions/sphere/bingham/bingham.py +++ b/model/distributions/sphere/bingham/bingham.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from model.distributions.sphere.sphere_distribution import SphereDistribution -from util.selectors.slider_float import FloatSlider +from util.selectors.slider import Slider import numpy as np import pyrecest._backend @@ -13,8 +13,8 @@ class BinghampDistribution(SphereDistribution): def __init__(self): self.distribution_options = [ - FloatSlider("Lambda 1 (λ₁)", 0, 0, 10), - FloatSlider("Lambda 2 (λ₂)", 0, 0, 10), + Slider("Lambda 1 (λ₁)", 0, 0, 10), + Slider("Lambda 2 (λ₂)", 0, 0, 10), ] self.sampling_methods = [ @@ -24,7 +24,9 @@ def __init__(self): def get_name(self): return "Bingham" - def get_pdf(self, distribution_options): + def get_pdf(self, distribution_options): + alpha = 0.7 # scale + l1 = distribution_options[0].state l2 = distribution_options[1].state @@ -36,5 +38,9 @@ def get_pdf(self, distribution_options): def pdf(x): - return bingham_dist.pdf(array(x)) + bing = bingham_dist.pdf(array(x)) + max = np.max(bing) + norm = bing / max + norm = norm * alpha + return norm return pdf \ No newline at end of file diff --git a/model/distributions/sphere/watson/watson.py b/model/distributions/sphere/watson/watson.py index 914805e..fded3be 100644 --- a/model/distributions/sphere/watson/watson.py +++ b/model/distributions/sphere/watson/watson.py @@ -3,6 +3,9 @@ from util.selectors.slider_float import FloatSlider from util.selectors.slider import Slider import numpy as np +import pyrecest._backend +from pyrecest.backend import array +from pyrecest.distributions import WatsonDistribution as WatsonDistributionPyrecest from model.distributions.sphere.watson.random import WatsonRandomSampling @@ -10,7 +13,7 @@ class WatsonDistribution(SphereDistribution): def __init__(self): self.distribution_options = [ - Slider("κ (kappa)", 0.0, 10.0, 50.0), + Slider("κ (kappa)", 1.0, 10.0, 50.0), ] self.sampling_methods = [ @@ -20,5 +23,17 @@ def __init__(self): def get_name(self): return "Watson" - def get_pdf(self, distribution_options): - return None \ No newline at end of file + def get_pdf(self, distribution_options): + alpha = 0.5 # scale + kappa = distribution_options[0].state + mu = array([0.0, 0.0, 1.0]) + + watson_dist = WatsonDistributionPyrecest(mu=mu, kappa=kappa) + + def pdf(x): + wts = watson_dist.pdf(array(x)) + max = np.max(wts) + norm = wts / max + norm = norm * alpha + return norm + return pdf \ No newline at end of file From 79881b35f79e5a4f7eea26780f3ed0299ca0ee18 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 29 Oct 2025 15:30:12 +0100 Subject: [PATCH 043/152] begin implementation of kent pdf --- model/distributions/sphere/bingham/bingham.py | 1 - model/distributions/sphere/kent/kent.py | 53 +++++++++++++++++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/model/distributions/sphere/bingham/bingham.py b/model/distributions/sphere/bingham/bingham.py index 030fd64..c5892a6 100644 --- a/model/distributions/sphere/bingham/bingham.py +++ b/model/distributions/sphere/bingham/bingham.py @@ -32,7 +32,6 @@ def get_pdf(self, distribution_options): lambdas = np.sort(np.array([l1, l2, 0]))[::-1] lambdas = -lambdas - print(lambdas) M = np.eye(3) bingham_dist = BinghamDistribution(M=array(M), Z=array(lambdas)) diff --git a/model/distributions/sphere/kent/kent.py b/model/distributions/sphere/kent/kent.py index c69a2a5..c80fe53 100644 --- a/model/distributions/sphere/kent/kent.py +++ b/model/distributions/sphere/kent/kent.py @@ -1,11 +1,12 @@ from abc import ABC, abstractmethod from model.distributions.sphere.sphere_distribution import SphereDistribution +from model.sphere.sphere import Sphere from util.selectors.slider import Slider from util.selectors.slider_float import FloatSlider import numpy as np from model.distributions.sphere.kent.random import KentRandomSampling - +from sphstat.descriptives import rotationmatrix_withaxis class KentDistribution(SphereDistribution): def __init__(self): @@ -23,5 +24,51 @@ def __init__(self): def get_name(self): return "Kent (5-parameter Fisher-Bingham - FB5)" - def get_pdf(self, distribution_options): - return None \ No newline at end of file + def get_pdf(self, distribution_options): + # we don't compute the normalization constant $c(\kappa, \beta)$ here + # because the density fucntions are plotted not to scale anyway + c = 1.0 + + kappa = distribution_options[0].state + beta = distribution_options[1].state + beta = min(beta, kappa / 2) + + # hardcoded, same as in kent random sampling + mu_theta = 0 + mu_phi = 0 + mu0_theta = distribution_options[2].state + mu0_phi = distribution_options[3].state + + mu1, mu2, mu3 = Sphere.spherical_to_cartesian(mu_theta, mu_phi) + mu0_1, mu0_2, mu0_3 = Sphere.spherical_to_cartesian(mu0_theta, mu0_phi) + + y1 = mu = [mu1, mu2, mu3] + y2 = mu0 = [mu0_1, mu0_2, mu0_3] + + y1 = np.array(y1).reshape(3, 1) + y2 = np.array(y2).reshape(3, 1) + y1 = y1 / np.linalg.norm(y1) + y2 = y2 / np.linalg.norm(y2) + + y3 = np.cross(mu, mu0) + y3 = y3 / np.linalg.norm(y3) + + + y3 = np.array([1, 0, 0]).reshape(3, 1) + y2 = np.array([0, 1, 0]).reshape(3, 1) + y1 = np.array([0, 0, 1]).reshape(3, 1) + + + + def pdf(x): + #print(f"{y1.T.shape} * {x.T.shape}") + kent = 1/c * np.exp( + kappa * (y1.T @ x.T) + + beta * ( (y2.T @ x.T)**2 - (y3.T @ x.T)**2 ) + ) + kent = kent.flatten() + max = np.max(kent) + norm = kent / max + return norm + return pdf + \ No newline at end of file From 4a9edb2a4b132f1340cab506634b372d7e3bb4ee Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 30 Oct 2025 17:06:40 +0100 Subject: [PATCH 044/152] add fibonacci to watson --- .../distributions/sphere/watson/fibonachi.py | 93 +++++++++++++++++++ model/distributions/sphere/watson/watson.py | 4 +- server_commands.txt | 3 + 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 model/distributions/sphere/watson/fibonachi.py diff --git a/model/distributions/sphere/watson/fibonachi.py b/model/distributions/sphere/watson/fibonachi.py new file mode 100644 index 0000000..98f77f0 --- /dev/null +++ b/model/distributions/sphere/watson/fibonachi.py @@ -0,0 +1,93 @@ +from abc import ABC, abstractmethod +import numpy as np +import scipy +import scipy.integrate +import scipy.interpolate +import sphstat +from pyrecest.backend import array +from pyrecest.distributions import WatsonDistribution as WatsonDistributionPyrecest + +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from util.selectors.slider import Slider +from model.sphere.sphere import Sphere + + +class WatsonFibonachiSampling(SphereSamplingSchema): + def __init__(self): + self.sample_options = [ + Slider("Number of Samples", 1, 50, 100), + ] + + def get_name(self): + return "Fibonacci Lattice" + + def sample(self, sample_options, distribution_options): + kappa = distribution_options[0].state + sample_count = sample_options[0].state + + mu = array([0.0, 0.0, 1.0]) + watson_dist = WatsonDistributionPyrecest(mu=mu, kappa=kappa) + + + ''' + Note: the watson pdf is symetrical + This means that it will always produce the same value for different + azimuth angles (phi) at a given polar angle (theta). + ''' + def pdf(phi, theta): + # polar angle: 0 ≤ θ ≤ π (theta) + # azimuth: 0 ≤ φ < 2π (phi) + x, y, z = Sphere.spherical_to_cartesian(theta=theta, phi=phi) + x = np.column_stack((x, y, z)) + wts = watson_dist.pdf(array(x)) + return wts + + def f(t,y): + # ring at lattitude theta has radius 2pi * sin(theta) + return (2*np.pi) * pdf(0, t) * np.sin(t) # choose phi = 0 because of symmetry + + t_span = (0, np.pi) # theta from 0 to pi + y0 = 0 # the value of the integrated pdf at 0 is 0 + + # now compute the ode + sol = scipy.integrate.solve_ivp(f, t_span, [y0]) + + x = sol.t + y = sol.y[0] + + + # due to numerical issues, for large kappa and samplecount, y can be slightly non monotonic + # monotonicity is needed for interpolation, so maximum.accumulate then bump by eps + + y = np.maximum.accumulate(y) + diffs = np.diff(y) + mask = diffs <= 0 + if np.any(mask): + eps = 1e-10 + y = y + eps * np.arange(len(x)) + y = np.maximum.accumulate(y) + + # now interpolate, but we swamp x and y so whe get the inverse function + # this works because the function is monotonic + # use PCHIP interpolation + try: + q = scipy.interpolate.PchipInterpolator(x=y, y=x) + except Exception as e: + print(y) + print("----") + print(x) + raise e + + + i = np.linspace(0, 1, sample_count) + theta_i = q(i) + w = np.cos(theta_i) + + indices = np.arange(0, sample_count) + gold_seq = (1+5**0.5)/2 # golden ratio + + x_i_f_0 = w + x_i_f_1 = np.sqrt(1-w**2) * np.cos( (2 * np.pi * indices) / gold_seq) + x_i_f_2 = np.sqrt(1-w**2) * np.sin( (2 * np.pi * indices) / gold_seq) + x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) # order so that mu=[0, 0, 1] + return x_i_f \ No newline at end of file diff --git a/model/distributions/sphere/watson/watson.py b/model/distributions/sphere/watson/watson.py index fded3be..40dfd6e 100644 --- a/model/distributions/sphere/watson/watson.py +++ b/model/distributions/sphere/watson/watson.py @@ -8,6 +8,7 @@ from pyrecest.distributions import WatsonDistribution as WatsonDistributionPyrecest from model.distributions.sphere.watson.random import WatsonRandomSampling +from model.distributions.sphere.watson.fibonachi import WatsonFibonachiSampling class WatsonDistribution(SphereDistribution): @@ -17,7 +18,8 @@ def __init__(self): ] self.sampling_methods = [ - WatsonRandomSampling() + WatsonRandomSampling(), + WatsonFibonachiSampling(), ] def get_name(self): diff --git a/server_commands.txt b/server_commands.txt index 2ca4ba9..a95c290 100644 --- a/server_commands.txt +++ b/server_commands.txt @@ -47,6 +47,9 @@ docker compose --profile test run --rm tests turn server off (not needed for updating image): docker compose down +clean up onld images: +docker image prune -a -f + # Test in Browser http://193.196.39.120:8080 From 30f11f1744ad692c81c3412d2619333f4237f1db Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 31 Oct 2025 14:24:36 +0100 Subject: [PATCH 045/152] added mode switching to torus --- pages/torus.py | 4 +- renderer/Object3DAnd2DRenderer.py | 120 ++++++++++++++++++++++++++++++ renderer/Object3DRenderer.py | 99 +++++++++++++----------- 3 files changed, 177 insertions(+), 46 deletions(-) create mode 100644 renderer/Object3DAnd2DRenderer.py diff --git a/pages/torus.py b/pages/torus.py index 00782fe..b6515ba 100644 --- a/pages/torus.py +++ b/pages/torus.py @@ -8,14 +8,14 @@ from components.split_pane import SplitPane from model.torus.torus import Torus -from renderer.Object3DRenderer import Object3DRenderer +from renderer.Object3DAnd2DRenderer import Object3DAnd2DRenderer dash.register_page(__name__) torus = Torus() -renderer = Object3DRenderer(torus, "torus") +renderer = Object3DAnd2DRenderer(torus, "torus") options, graph = renderer.get_layout_components() layout = SplitPane( diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py new file mode 100644 index 0000000..0676259 --- /dev/null +++ b/renderer/Object3DAnd2DRenderer.py @@ -0,0 +1,120 @@ +from dash import html, dcc, callback, Input, Output, ALL, State, Patch, no_update +import plotly.graph_objects as go + +from renderer.Object3DRenderer import Object3DRenderer + +class Object3DAnd2DRenderer(Object3DRenderer): + def __init__(self, object, id): + # the renderer starts out in 3d mode + super().__init__(object, id, register_3d_callbacks=False) + self.register_plot_callbacks() + self.register_mode_callbacks() + + self.fig_2d = go.Figure() + + def register_plot_callbacks(self): + # updates the plot based on selected sampling options + @callback( + Output(f"graph-{self.id}", "figure", allow_duplicate=True), + State(f"mode-selector-{self.id}", "value"), + Input(f"mode-done-{self.id}", "data"), + Input({"type": "dist", "index": ALL}, "value"), + State({"type": "dist", "index": ALL}, "id"), + Input({"type": "sampling", "index": ALL}, "value"), + State({"type": "sampling", "index": ALL}, "id"), + Input("distribution-selector", "value"), + Input(f"sampling-selector-{self.id}", "value"), + Input(f"distribution-options-{self.id}", "children"), + prevent_initial_call='initial_duplicate' + ) + def update_plot_sample_callback(mode, _mode_counter, values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): + if mode == "3D View": + return self.update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _) + else: + return self.update_plot_sample_2d(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _) + + # updates the plot based on selected distribution options + @callback( + Output(f"graph-{self.id}", "figure", allow_duplicate=True), + State(f"mode-selector-{self.id}", "value"), + Input(f"mode-done-{self.id}", "data"), + Input({"type": "dist", "index": ALL}, "value"), + State({"type": "dist", "index": ALL}, "id"), + Input("distribution-selector", "value"), + Input(f"sampling-selector-{self.id}", "value"), + Input(f"distribution-options-{self.id}", "children"), + prevent_initial_call='initial_duplicate' + ) + def update_plot_dist_callback(mode, _mode_counter, values_dist, ids_dist, selected_distribution, selected_sampling, _): + if mode == "3D View": + return self.update_plot_dist(values_dist, ids_dist, selected_distribution, selected_sampling, _) + else: + return self.update_plot_dist_2d(values_dist, ids_dist, selected_distribution, selected_sampling, _) + + + def register_mode_callbacks(self): + @callback( + Output(f"graph-{self.id}", "figure"), + Output(f"mode-done-{self.id}", "data"), + Input(f"mode-selector-{self.id}", "value"), + State(f"mode-done-{self.id}", "data"), + ) + def switch_mode(mode, data): + new_data = (data + 1 if data is not None else 1) + if mode == "3D View": + return self.fig, new_data + else: + return self.fig_2d, new_data + + def update_plot_sample_2d(self, values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): + return no_update + + def update_plot_dist_2d(self, values_dist, ids_dist, selected_distribution, selected_sampling, _): + return no_update + + def get_layout_components(self): + initial_distribution = self.object.distributions[list(self.object.distributions.keys())[0]] + initial_sampling_method = initial_distribution.sampling_methods[0].get_name() if initial_distribution.sampling_methods else "no sampling methods found" + initial_sampling_options = [x.get_name() for x in initial_distribution.sampling_methods] + + options = [ + html.P("Select Visualization Mode:"), + + # needed to create dependecy between mode selector and plot update callbacks + # to insure they are fired after the plot mode finished changing + # data is just a counter that is incremented each time the mode is changed, + # so dash detects a change and fires callbacks depending on it (update samp and dist) + dcc.Store(id=f"mode-done-{self.id}", data=0), + + dcc.RadioItems( + id=f"mode-selector-{self.id}", + options=["3D View", "2D View"], + value="3D View", + inline=True, + labelStyle={"marginRight": "15px"} + ), + html.Hr(), + html.Br(), + html.P("Select Distribution and Sampling Method:"), + dcc.RadioItems( + id="distribution-selector", + options=(list(self.object.distributions.keys())), + value=initial_distribution.get_name() if self.object.distributions else "no distributions found", + ), + html.Br(), + dcc.RadioItems( + id=f"sampling-selector-{self.id}", + options=(list(initial_sampling_options)), + value=initial_sampling_method, + ), + html.Br(), + html.Hr(), + html.Br(), + + html.Div(id=f"distribution-options-{self.id}"), + html.Div(id=f"sampling-options-{self.id}"), + ] + + graph = [dcc.Graph(id=f"graph-{self.id}", figure=self.fig, config=self.config, style={'height': '100%'})] + + return options, graph \ No newline at end of file diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index f7ec43d..2d645c5 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -5,7 +5,7 @@ import plotly.figure_factory as ff import dash class Object3DRenderer: - def __init__(self, object_3D, id): + def __init__(self, object_3D, id, register_3d_callbacks=True): # dash doesnt like duplicate calback functions # so each renderer instance gets a uuid for suffixing self.id = id @@ -71,6 +71,8 @@ def __init__(self, object_3D, id): x=0.1, )) self._register_callbacks() + if register_3d_callbacks: + self._register_3d_plot_callbacks() def _register_callbacks(self): @@ -105,6 +107,7 @@ def update_curr_distribution(selected_distribution, selected_sampling): options_sampling_dcc = [opt.to_dash_component("sampling", id) for id, opt in enumerate(options_sampling.sample_options)] return options_dist_dcc, options_sampling_dcc + def _register_3d_plot_callbacks(self): # updates the plot based on selected sampling options @callback( @@ -118,61 +121,69 @@ def update_curr_distribution(selected_distribution, selected_sampling): Input(f"distribution-options-{self.id}", "children"), prevent_initial_call='initial_duplicate' ) - def update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): - try: - dist_options = self.object.distributions[selected_distribution].distribution_options - sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options - except KeyError: - # got stale values, ignore - return dash.no_update - - # the order of options might not be guaranteed, so we map them by their ids - id_value_dist = [(id,v) for id, v in zip(ids_dist, values_dist)] - id_value_samp = [(id,v) for id, v in zip(ids_samp, values_samp)] - + def update_plot_sample_callback(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): + return self.update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _) + + # updates the plot based on selected distribution options + @callback( + Output(f"graph-{self.id}", "figure", allow_duplicate=True), + Input({"type": "dist", "index": ALL}, "value"), + State({"type": "dist", "index": ALL}, "id"), + Input("distribution-selector", "value"), + Input(f"sampling-selector-{self.id}", "value"), + Input(f"distribution-options-{self.id}", "children"), + prevent_initial_call='initial_duplicate' + ) + def update_plot_dist_callback(values_dist, ids_dist, selected_distribution, selected_sampling, _): + return self.update_plot_dist(values_dist, ids_dist, selected_distribution, selected_sampling, _) - # and them sort them, so they are in the same order as sampling_options and dist_options - options_samp_new = sorted(id_value_samp, key=lambda x: int(x[0]["index"])) - options_dist_new = sorted(id_value_dist, key=lambda x: int(x[0]["index"])) + + + def update_plot_sample(self, values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): + try: + dist_options = self.object.distributions[selected_distribution].distribution_options + sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options + except KeyError: + # got stale values, ignore + return dash.no_update + + # the order of options might not be guaranteed, so we map them by their ids + id_value_dist = [(id,v) for id, v in zip(ids_dist, values_dist)] + id_value_samp = [(id,v) for id, v in zip(ids_samp, values_samp)] + + # and them sort them, so they are in the same order as sampling_options and dist_options + options_samp_new = sorted(id_value_samp, key=lambda x: int(x[0]["index"])) + options_dist_new = sorted(id_value_dist, key=lambda x: int(x[0]["index"])) - for opt, (id, new_state) in zip(sampling_options, options_samp_new): - opt.update_state(new_state) - for opt, (id, new_state) in zip(dist_options, options_dist_new): - opt.update_state(new_state) + for opt, (id, new_state) in zip(sampling_options, options_samp_new): + opt.update_state(new_state) - # samples - self.object.update_sample(selected_distribution, selected_sampling, sampling_options, dist_options) + for opt, (id, new_state) in zip(dist_options, options_dist_new): + opt.update_state(new_state) + # samples + self.object.update_sample(selected_distribution, selected_sampling, sampling_options, dist_options) - patched_figure = Patch() + patched_figure = Patch() - patched_figure["data"][1].x = self.object.samples[:, 0] - patched_figure["data"][1].y = self.object.samples[:, 1] - patched_figure["data"][1].z = self.object.samples[:, 2] - # set size based on number of samples - sample_count = self.object.samples.shape[0] - marker_size = (1/ np.sqrt(sample_count) ) * 30 # about 3 for sample size 100; scaled by sqrt - marker_size = np.minimum(4.7,marker_size) - + patched_figure["data"][1].x = self.object.samples[:, 0] + patched_figure["data"][1].y = self.object.samples[:, 1] + patched_figure["data"][1].z = self.object.samples[:, 2] - patched_figure["data"][1].marker.size = marker_size - return patched_figure + # set size based on number of samples + sample_count = self.object.samples.shape[0] + marker_size = (1/ np.sqrt(sample_count) ) * 30 # about 3 for sample size 100; scaled by sqrt + marker_size = np.minimum(4.7,marker_size) - # updates the plot based on selected distribution options - @callback( - Output(f"graph-{self.id}", "figure", allow_duplicate=True), - Input({"type": "dist", "index": ALL}, "value"), - State({"type": "dist", "index": ALL}, "id"), - Input("distribution-selector", "value"), - Input(f"sampling-selector-{self.id}", "value"), - Input(f"distribution-options-{self.id}", "children"), - prevent_initial_call='initial_duplicate' - ) - def update_plot_dist(values_dist, ids_dist, selected_distribution, selected_sampling, _): + + patched_figure["data"][1].marker.size = marker_size + return patched_figure + + def update_plot_dist(self, values_dist, ids_dist, selected_distribution, selected_sampling, _): dist_options = self.object.distributions[selected_distribution].distribution_options # the order of options might not be guaranteed, so we map them by their ids From 3f545fe34ec3d915320b840d20a14249fff8146a Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 31 Oct 2025 15:31:03 +0100 Subject: [PATCH 046/152] add basic samples plotting in torus 2d --- model/manifold.py | 3 ++ model/torus/torus.py | 7 +++ renderer/Object3DAnd2DRenderer.py | 79 ++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/model/manifold.py b/model/manifold.py index 6139f92..6f8e771 100644 --- a/model/manifold.py +++ b/model/manifold.py @@ -1,6 +1,9 @@ from abc import ABC, abstractmethod class Manifold(ABC): + def __init__(self): + # optional axis labels if the manifold supports 2D rendering + self.axes_2d = None # generates renderable xyz grid @abstractmethod diff --git a/model/torus/torus.py b/model/torus/torus.py index 880695c..443c885 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -8,11 +8,17 @@ def __init__(self, resolution=50, r=1, R=3): self.xyz = self.generate_xyz(resolution, r, R) self.mesh = np.array([]) self.samples = np.array([]) + self.samples_2d = np.array([]) self.distributions = DistributionLoader(TorusDistribution, "model.distributions.torus").get_distributions() self.r = r self.R = R + self.axes_2d = ( + np.arange(0, 2.5 * np.pi, np.pi / 2), # 0, π/2, π, 3π/2, 2π + ["0", "π/2", "π", "3π/2", "2π"] + ) + def generate_xyz(self, resolution=50, r=1, R=3): t = np.linspace(0, 2*np.pi, resolution) p = np.linspace(0, 2*np.pi, resolution) @@ -33,6 +39,7 @@ def update_sample(self, selected_distribution, selected_sampling_method, sample_ x, y, z = self.t_p_to_xyz(new_sample[:,0], new_sample[:,1], self.r, self.R) self.samples = np.column_stack((x, y, z)) + self.samples_2d = new_sample @staticmethod diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index 0676259..5eacf3b 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -10,7 +10,48 @@ def __init__(self, object, id): self.register_plot_callbacks() self.register_mode_callbacks() - self.fig_2d = go.Figure() + self.fig_2d = go.Figure( + data=[ + go.Scatter( + name="Samples", + x=[], + y=[], + mode="markers", + marker=dict( + size=6, + color="red", + line=dict(width=1, color="black") + ), + marker_color="red", + marker_size=6, + marker_line_color="black", + showlegend=True, + ), + ] + ) + self.fig_2d.update_layout(legend=dict( + yanchor="top", + y=0.99, + xanchor="left", + x=0.01, + )) + self.fig_2d.update_layout(dragmode="pan") + + if object.axes_2d is not None: + self.fig_2d.update_xaxes( + title_text="t", + tickmode="array", + tickvals=object.axes_2d[0], + ticktext=object.axes_2d[1], + zeroline=False, + ) + self.fig_2d.update_yaxes( + title_text="p", + tickmode="array", + tickvals=object.axes_2d[0], + ticktext=object.axes_2d[1], + zeroline=False, + ) def register_plot_callbacks(self): # updates the plot based on selected sampling options @@ -67,7 +108,41 @@ def switch_mode(mode, data): return self.fig_2d, new_data def update_plot_sample_2d(self, values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): - return no_update + try: + dist_options = self.object.distributions[selected_distribution].distribution_options + sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options + except KeyError: + # got stale values, ignore + return no_update + + # the order of options might not be guaranteed, so we map them by their ids + id_value_dist = [(id,v) for id, v in zip(ids_dist, values_dist)] + id_value_samp = [(id,v) for id, v in zip(ids_samp, values_samp)] + + + # and them sort them, so they are in the same order as sampling_options and dist_options + options_samp_new = sorted(id_value_samp, key=lambda x: int(x[0]["index"])) + options_dist_new = sorted(id_value_dist, key=lambda x: int(x[0]["index"])) + + + for opt, (id, new_state) in zip(sampling_options, options_samp_new): + opt.update_state(new_state) + + for opt, (id, new_state) in zip(dist_options, options_dist_new): + opt.update_state(new_state) + + # samples + self.object.update_sample(selected_distribution, selected_sampling, sampling_options, dist_options) + + patched_figure = Patch() + tp = self.object.samples_2d + + # x is p, y is t + patched_figure["data"][0].x = tp[:, 1] + patched_figure["data"][0].y = tp[:, 0] + + return patched_figure + def update_plot_dist_2d(self, values_dist, ids_dist, selected_distribution, selected_sampling, _): return no_update From 57b4f81bcbb2b98d6c04d1a16eef4ba8789482eb Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 31 Oct 2025 15:38:29 +0100 Subject: [PATCH 047/152] remove weird buttons on 2d plot --- renderer/Object3DRenderer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 2d645c5..4a3dda8 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -20,6 +20,7 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): self.config = { 'responsive': True, 'scrollZoom': True, + "modeBarButtonsToRemove": ["select2d", "lasso2d"], } self.fig = go.Figure( From bceb41d2e051a0a1fcf8253a7af128c76f10604f Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 3 Nov 2025 17:44:56 +0100 Subject: [PATCH 048/152] added logslider for sample and mises kappa also made the 2d-plot use gl --- assets/tooltip.js | 14 ++++ model/distributions/sphere/bingham/random.py | 6 +- model/distributions/sphere/kent/random.py | 4 +- .../sphere/uniform/fibonachi_lattice.py | 4 +- model/distributions/sphere/uniform/random.py | 4 +- model/distributions/sphere/uniform/uniform.py | 1 - .../sphere/vonmises_fisher/fibonachi.py | 5 +- .../sphere/vonmises_fisher/random.py | 5 +- .../sphere/vonmises_fisher/vonmises_fisher.py | 6 +- .../distributions/sphere/watson/fibonachi.py | 4 +- model/distributions/sphere/watson/random.py | 4 +- model/distributions/torus/uniform/random.py | 4 +- renderer/Object3DAnd2DRenderer.py | 2 +- util/selectors/silder_log.py | 81 +++++++++++++++++++ 14 files changed, 117 insertions(+), 27 deletions(-) create mode 100644 util/selectors/silder_log.py diff --git a/assets/tooltip.js b/assets/tooltip.js index 8472412..0c1dded 100644 --- a/assets/tooltip.js +++ b/assets/tooltip.js @@ -5,4 +5,18 @@ window.dccFunctions.trafo_L = function(value) { } else { return Math.round(Math.pow(10, value)); } +} +window.dccFunctions.transform_log_nice = function(value) { + let x = Math.pow(10, value); + // same as transform_up in log_slider.py + if (x == 0){ + return 0; + } + const sign = Math.sign(x); + x = Math.abs(x); + + let step = Math.pow(10, Math.floor(Math.log10(x))) / 10; + let nice_value = sign * Math.round(x / step) * step; + nice_value = Number(nice_value.toFixed(4)); + return nice_value; } \ No newline at end of file diff --git a/model/distributions/sphere/bingham/random.py b/model/distributions/sphere/bingham/random.py index 104fe09..66730cd 100644 --- a/model/distributions/sphere/bingham/random.py +++ b/model/distributions/sphere/bingham/random.py @@ -4,14 +4,12 @@ import sphstat from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema -from util.selectors.slider import Slider - +from util.selectors.silder_log import LogSlider class BinghamRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 1, 50, 100), - ] + LogSlider("Number of Samples", 10, 100, 10000) ] def get_name(self): return "Random" diff --git a/model/distributions/sphere/kent/random.py b/model/distributions/sphere/kent/random.py index ceb07ca..7fcd30f 100644 --- a/model/distributions/sphere/kent/random.py +++ b/model/distributions/sphere/kent/random.py @@ -4,14 +4,14 @@ import sphstat from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema -from util.selectors.slider import Slider +from util.selectors.silder_log import LogSlider from model.sphere.sphere import Sphere class KentRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 1, 50, 100), + LogSlider("Number of Samples", 10, 100, 10000), ] def get_name(self): diff --git a/model/distributions/sphere/uniform/fibonachi_lattice.py b/model/distributions/sphere/uniform/fibonachi_lattice.py index 69f29bb..b0264c4 100644 --- a/model/distributions/sphere/uniform/fibonachi_lattice.py +++ b/model/distributions/sphere/uniform/fibonachi_lattice.py @@ -3,12 +3,12 @@ from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema -from util.selectors.slider import Slider +from util.selectors.silder_log import LogSlider class SphereUniformFibSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 10, 100, 500) + LogSlider("Number of Samples", 10, 100, 10000) ] def get_name(self): diff --git a/model/distributions/sphere/uniform/random.py b/model/distributions/sphere/uniform/random.py index 705203f..cea582b 100644 --- a/model/distributions/sphere/uniform/random.py +++ b/model/distributions/sphere/uniform/random.py @@ -2,12 +2,12 @@ import numpy as np from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema -from util.selectors.slider import Slider +from util.selectors.silder_log import LogSlider class SphereUniformRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 10, 100, 500) + LogSlider("Number of Samples", 10, 100, 10000) ] def get_name(self): diff --git a/model/distributions/sphere/uniform/uniform.py b/model/distributions/sphere/uniform/uniform.py index 38a61b1..d0cafdc 100644 --- a/model/distributions/sphere/uniform/uniform.py +++ b/model/distributions/sphere/uniform/uniform.py @@ -1,5 +1,4 @@ from abc import ABC, abstractmethod -from util.selectors.slider import Slider import numpy as np from model.distributions.sphere.sphere_distribution import SphereDistribution diff --git a/model/distributions/sphere/vonmises_fisher/fibonachi.py b/model/distributions/sphere/vonmises_fisher/fibonachi.py index bf8c034..a89d6d9 100644 --- a/model/distributions/sphere/vonmises_fisher/fibonachi.py +++ b/model/distributions/sphere/vonmises_fisher/fibonachi.py @@ -3,13 +3,12 @@ import scipy from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema -from util.selectors.slider import Slider - +from util.selectors.silder_log import LogSlider class VonMisesFibSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 1, 50, 500), + LogSlider("Number of Samples", 10, 100, 10000), ] def get_name(self): diff --git a/model/distributions/sphere/vonmises_fisher/random.py b/model/distributions/sphere/vonmises_fisher/random.py index 618a35a..ab01883 100644 --- a/model/distributions/sphere/vonmises_fisher/random.py +++ b/model/distributions/sphere/vonmises_fisher/random.py @@ -3,13 +3,12 @@ import scipy from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema -from util.selectors.slider import Slider - +from util.selectors.silder_log import LogSlider class VonMisesRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 1, 50, 500), + LogSlider("Number of Samples", 10, 100, 10000), ] def get_name(self): diff --git a/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py index 7e52ebe..6edae92 100644 --- a/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py +++ b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod from model.distributions.sphere.sphere_distribution import SphereDistribution -from util.selectors.slider import Slider +from util.selectors.silder_log import LogSlider import numpy as np import scipy @@ -10,7 +10,7 @@ class vonMisesFisherDistribution(SphereDistribution): def __init__(self): self.distribution_options = [ - Slider("Kappa (κ)", 1, 5, 15), + LogSlider("Kappa (κ)", 1, 5, 100), ] self.sampling_methods = [ @@ -23,8 +23,8 @@ def get_name(self): def get_pdf(self, distribution_options): alpha = 0.7 # scale - kappa = distribution_options[0].state + def pdf(x): misf = scipy.stats.vonmises_fisher.pdf(x, mu=[0,0,1], kappa=kappa) diff --git a/model/distributions/sphere/watson/fibonachi.py b/model/distributions/sphere/watson/fibonachi.py index 98f77f0..653fd15 100644 --- a/model/distributions/sphere/watson/fibonachi.py +++ b/model/distributions/sphere/watson/fibonachi.py @@ -8,14 +8,14 @@ from pyrecest.distributions import WatsonDistribution as WatsonDistributionPyrecest from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema -from util.selectors.slider import Slider +from util.selectors.silder_log import LogSlider from model.sphere.sphere import Sphere class WatsonFibonachiSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 1, 50, 100), + LogSlider("Number of Samples", 10, 100, 10000), ] def get_name(self): diff --git a/model/distributions/sphere/watson/random.py b/model/distributions/sphere/watson/random.py index 146a65d..6f54c0d 100644 --- a/model/distributions/sphere/watson/random.py +++ b/model/distributions/sphere/watson/random.py @@ -4,14 +4,14 @@ import sphstat from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema -from util.selectors.slider import Slider +from util.selectors.silder_log import LogSlider from model.sphere.sphere import Sphere class WatsonRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 1, 50, 100), + LogSlider("Number of Samples", 10, 100, 10000), ] def get_name(self): diff --git a/model/distributions/torus/uniform/random.py b/model/distributions/torus/uniform/random.py index 3e4d0c9..8c49fe3 100644 --- a/model/distributions/torus/uniform/random.py +++ b/model/distributions/torus/uniform/random.py @@ -1,12 +1,12 @@ from abc import ABC, abstractmethod from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema -from util.selectors.slider import Slider +from util.selectors.silder_log import LogSlider import numpy as np class TorusRandomUniformSampling(TorusSamplingSchema): def __init__(self): self.sample_options = [ - Slider("Number of Samples", 10, 100, 500) + LogSlider("Number of Samples", 10, 100, 10000) ] def get_name(self): diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index 5eacf3b..b641207 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -12,7 +12,7 @@ def __init__(self, object, id): self.fig_2d = go.Figure( data=[ - go.Scatter( + go.Scattergl( name="Samples", x=[], y=[], diff --git a/util/selectors/silder_log.py b/util/selectors/silder_log.py new file mode 100644 index 0000000..3f5dcc5 --- /dev/null +++ b/util/selectors/silder_log.py @@ -0,0 +1,81 @@ +from dash import dcc, html +import numpy as np +from util.selectors.selector import Selector + +SLIDER_OPT_AMOUNT = 30 +SLIDER_MARK_AMOUNT = 5 + +class LogSlider(Selector): + def __init__(self, name, min, state, max, custom_constraints=id): + self.name = name + self.min = min + self.state = state + self.max = max + + self.id = None + + self.log_max = np.log10(self.max) + self.log_min = np.log10(self.min) + + + def calculate_step(self): + step = (self.log_max - self.log_min) / SLIDER_OPT_AMOUNT + return step + + def calculate_marks(self): + marks = {} + step = (self.log_max - self.log_min) / SLIDER_MARK_AMOUNT + + for i in range(SLIDER_MARK_AMOUNT + 1): + position = self.log_min + i * step + + if position % 1 == 0: # it doesnt like float keys for "integer" values + position = int(position) + + value = self.transfrom_up(position) + marks[position] = f"{value:.6g}" # 6 significant digits + return marks + + @staticmethod + def round_nice_number(x): + # rounds to "nice" number with less than ~5% error + if x == 0: + return 0 + sign = np.sign(x) + x = abs(x) + + # step ~10% of x => ~5% rounding error + step = (10 ** np.floor(np.log10(x))) / 10 + nice = sign * round(x / step) * step + + return int(nice) if float(nice).is_integer() else nice + + # same as transform_pow_10 in tooltip.js + def transfrom_up(self, value): + return LogSlider.round_nice_number(10 ** value) + + # not quite the inverse of transfrom_up due to rounding + def transfrom_down(self, value): + if value <= 0: + return 0 + return np.log10(value) + + def to_dash_component(self, _type, id): + self.id = id + return html.Div([ + html.Label(self.name), + + dcc.Slider( + id={"type": _type, "index": id}, + min=self.log_min, + max=self.log_max, + value=self.transfrom_down(self.state), + tooltip={"placement": "bottom", "always_visible": True, "transform": "transform_log_nice"}, + step=self.calculate_step(), + marks=self.calculate_marks(), + updatemode="drag", + ) + ]) + + def update_state(self, new_state): + self.state = self.transfrom_up(new_state) \ No newline at end of file From 1bf1c3eccb65b6d3b58c4d483eabf3ba81e2f4e5 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 3 Nov 2025 20:15:11 +0100 Subject: [PATCH 049/152] make sample size be affected by device pixel ratio --- assets/dpr.js | 7 +++++++ renderer/Object3DAnd2DRenderer.py | 19 +++++++++++++++---- renderer/Object3DRenderer.py | 21 +++++++++++++++------ 3 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 assets/dpr.js diff --git a/assets/dpr.js b/assets/dpr.js new file mode 100644 index 0000000..e8376bb --- /dev/null +++ b/assets/dpr.js @@ -0,0 +1,7 @@ +window.dash_clientside = Object.assign({}, window.dash_clientside, { + utils: { + getDevicePixelRatio: function(_) { + return window.devicePixelRatio || 1; + } + } +}); \ No newline at end of file diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index b641207..fef3a20 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -1,4 +1,5 @@ from dash import html, dcc, callback, Input, Output, ALL, State, Patch, no_update +import numpy as np import plotly.graph_objects as go from renderer.Object3DRenderer import Object3DRenderer @@ -66,13 +67,14 @@ def register_plot_callbacks(self): Input("distribution-selector", "value"), Input(f"sampling-selector-{self.id}", "value"), Input(f"distribution-options-{self.id}", "children"), + State(f"device-pixel-ratio-{self.id}", "data"), prevent_initial_call='initial_duplicate' ) - def update_plot_sample_callback(mode, _mode_counter, values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): + def update_plot_sample_callback(mode, _mode_counter, values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _, dpr): if mode == "3D View": - return self.update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _) + return self.update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _, dpr) else: - return self.update_plot_sample_2d(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _) + return self.update_plot_sample_2d(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _, dpr) # updates the plot based on selected distribution options @callback( @@ -107,7 +109,7 @@ def switch_mode(mode, data): else: return self.fig_2d, new_data - def update_plot_sample_2d(self, values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): + def update_plot_sample_2d(self, values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _, dpr): try: dist_options = self.object.distributions[selected_distribution].distribution_options sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options @@ -137,6 +139,13 @@ def update_plot_sample_2d(self, values_dist, ids_dist, values_samp, ids_samp, se patched_figure = Patch() tp = self.object.samples_2d + # marker size scaling + sample_count = self.object.samples.shape[0] + marker_size = (10 * (sample_count / 100) ** (-0.35)) / dpr + marker_size = np.minimum(10,marker_size) + + patched_figure["data"][0].marker.size = marker_size + # x is p, y is t patched_figure["data"][0].x = tp[:, 1] patched_figure["data"][0].y = tp[:, 0] @@ -153,6 +162,8 @@ def get_layout_components(self): initial_sampling_options = [x.get_name() for x in initial_distribution.sampling_methods] options = [ + dcc.Store(id=f"device-pixel-ratio-{self.id}", data=1), + html.P("Select Visualization Mode:"), # needed to create dependecy between mode selector and plot update callbacks diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 4a3dda8..596b27b 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -1,5 +1,5 @@ from functools import lru_cache -from dash import html, dcc, callback, Input, Output, ALL, State, Patch +from dash import html, dcc, callback, Input, Output, ALL, State, Patch, clientside_callback, ClientsideFunction import numpy as np import plotly.graph_objects as go import plotly.figure_factory as ff @@ -9,6 +9,7 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): # dash doesnt like duplicate calback functions # so each renderer instance gets a uuid for suffixing self.id = id + self.device_pixel_ratio = 1.0 # objects should have atleast one corresponding distribution self.object = object_3D @@ -78,6 +79,12 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): def _register_callbacks(self): + # gets device pixel ratio from scaling of samples + clientside_callback( + ClientsideFunction(namespace="utils", function_name="getDevicePixelRatio"), + Output(f"device-pixel-ratio-{self.id}", "data"), + Input(f"graph-{self.id}", "figure"), + ) # updates wich sampling methods are available once distribution is selected @callback( @@ -120,10 +127,11 @@ def _register_3d_plot_callbacks(self): Input("distribution-selector", "value"), Input(f"sampling-selector-{self.id}", "value"), Input(f"distribution-options-{self.id}", "children"), + State(f"device-pixel-ratio-{self.id}", "data"), prevent_initial_call='initial_duplicate' ) - def update_plot_sample_callback(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): - return self.update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _) + def update_plot_sample_callback(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _, dpr): + return self.update_plot_sample(values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _, dpr) # updates the plot based on selected distribution options @callback( @@ -140,7 +148,7 @@ def update_plot_dist_callback(values_dist, ids_dist, selected_distribution, sele - def update_plot_sample(self, values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _): + def update_plot_sample(self, values_dist, ids_dist, values_samp, ids_samp, selected_distribution, selected_sampling, _, dpr): try: dist_options = self.object.distributions[selected_distribution].distribution_options sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options @@ -177,8 +185,8 @@ def update_plot_sample(self, values_dist, ids_dist, values_samp, ids_samp, selec # set size based on number of samples sample_count = self.object.samples.shape[0] - marker_size = (1/ np.sqrt(sample_count) ) * 30 # about 3 for sample size 100; scaled by sqrt - marker_size = np.minimum(4.7,marker_size) + marker_size = (10 * (sample_count / 100) ** (-0.35)) / dpr + marker_size = np.minimum(10,marker_size) patched_figure["data"][1].marker.size = marker_size @@ -222,6 +230,7 @@ def get_layout_components(self): initial_sampling_options = [x.get_name() for x in initial_distribution.sampling_methods] options = [ + dcc.Store(id=f"device-pixel-ratio-{self.id}", data=1), html.Br(), html.P("Select Distribution and Sampling Method:"), dcc.RadioItems( From 5eb8c7ba11818905c93b0a7de231722c3999ea64 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 5 Nov 2025 09:29:57 +0100 Subject: [PATCH 050/152] fix kent --- model/distributions/sphere/kent/kent.py | 22 ------ model/distributions/sphere/kent/random.py | 23 ++---- poetry.lock | 95 +++++++++++++++-------- pyproject.toml | 1 + 4 files changed, 69 insertions(+), 72 deletions(-) diff --git a/model/distributions/sphere/kent/kent.py b/model/distributions/sphere/kent/kent.py index c80fe53..4a0fe26 100644 --- a/model/distributions/sphere/kent/kent.py +++ b/model/distributions/sphere/kent/kent.py @@ -13,8 +13,6 @@ def __init__(self): self.distribution_options = [ Slider("κ (kappa)", 0.0, 10.0, 50.0), Slider("β (beta)", 0.0, 2.0, 25.0), - FloatSlider("mu0: Mean vector of the Fisher part: (θ)", 0, np.pi, np.pi), # default values so that distibution is initially visible - FloatSlider("mu0: Mean vector of the Fisher part: (φ)", 0, 0, 2 * np.pi), ] self.sampling_methods = [ @@ -34,26 +32,6 @@ def get_pdf(self, distribution_options): beta = min(beta, kappa / 2) # hardcoded, same as in kent random sampling - mu_theta = 0 - mu_phi = 0 - mu0_theta = distribution_options[2].state - mu0_phi = distribution_options[3].state - - mu1, mu2, mu3 = Sphere.spherical_to_cartesian(mu_theta, mu_phi) - mu0_1, mu0_2, mu0_3 = Sphere.spherical_to_cartesian(mu0_theta, mu0_phi) - - y1 = mu = [mu1, mu2, mu3] - y2 = mu0 = [mu0_1, mu0_2, mu0_3] - - y1 = np.array(y1).reshape(3, 1) - y2 = np.array(y2).reshape(3, 1) - y1 = y1 / np.linalg.norm(y1) - y2 = y2 / np.linalg.norm(y2) - - y3 = np.cross(mu, mu0) - y3 = y3 / np.linalg.norm(y3) - - y3 = np.array([1, 0, 0]).reshape(3, 1) y2 = np.array([0, 1, 0]).reshape(3, 1) y1 = np.array([0, 0, 1]).reshape(3, 1) diff --git a/model/distributions/sphere/kent/random.py b/model/distributions/sphere/kent/random.py index 7fcd30f..b0bcd42 100644 --- a/model/distributions/sphere/kent/random.py +++ b/model/distributions/sphere/kent/random.py @@ -2,6 +2,7 @@ import numpy as np import scipy import sphstat +from kent_distribution import kent2 from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema from util.selectors.silder_log import LogSlider @@ -22,22 +23,8 @@ def sample(self, sample_options, distribution_options): beta = distribution_options[1].state beta = min(beta, kappa / 2) # TODO make this dynamic - - # can be hardcoded because the user can just turn the sphere - # (only affects where the distribution is centered) - mu_theta = 0 - mu_phi = 0 - mu0_theta = distribution_options[2].state - mu0_phi = distribution_options[3].state - - mu1, mu2, mu3 = Sphere.spherical_to_cartesian(mu_theta, mu_phi) - mu0_1, mu0_2, mu0_3 = Sphere.spherical_to_cartesian(mu0_theta, mu0_phi) - - mu = [mu1, mu2, mu3] - mu0 = [mu0_1, mu0_2, mu0_3] - + kent = kent2([1, 0, 0], [0, 1, 0], [0, 0, 1], kappa, beta) numsamp = sample_options[0].state - samples = sphstat.distributions.kent(numsamp, kappa, beta, mu, mu0)["points"] - samples_array = np.vstack(samples) - - return samples_array \ No newline at end of file + samp = kent.rvs(n_samples=numsamp) + xyz = samp[:, [2, 1, 0]] # samples get returned in z,y,x order + return xyz \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 0e35a7f..a6a67c3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -56,14 +56,14 @@ typing = ["typing_extensions (>=4.0.0)"] [[package]] name = "astropy-iers-data" -version = "0.2025.10.27.0.39.10" +version = "0.2025.11.3.0.38.37" description = "IERS Earth Rotation and Leap Second tables for the astropy core package" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "astropy_iers_data-0.2025.10.27.0.39.10-py3-none-any.whl", hash = "sha256:aa91d24155eec2e7dafffd9ce0f046cd1fd6bcd995a76904aba3ef3f6836c4dc"}, - {file = "astropy_iers_data-0.2025.10.27.0.39.10.tar.gz", hash = "sha256:2a0630f810bcba7978cc5f3f92a45910b5ea95d885302b1879b0132e920302ed"}, + {file = "astropy_iers_data-0.2025.11.3.0.38.37-py3-none-any.whl", hash = "sha256:f7e6bf830d1c6d022abaf39739a9dc7e42ea912675113da921967fe9a87d1de4"}, + {file = "astropy_iers_data-0.2025.11.3.0.38.37.tar.gz", hash = "sha256:e018a3efbb6a4e4ea5a5a2a73bb3575b0123f3643b415a7327ec1dbd169787dd"}, ] [package.extras] @@ -84,22 +84,22 @@ files = [ [[package]] name = "beartype" -version = "0.22.4" +version = "0.22.5" description = "Unbearably fast near-real-time pure-Python runtime-static type-checker." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "beartype-0.22.4-py3-none-any.whl", hash = "sha256:7967a1cee01fee42e47da69c58c92da10ba5bcfb8072686e48487be5201e3d10"}, - {file = "beartype-0.22.4.tar.gz", hash = "sha256:68284c7803efd190b1b4639a0ab1a17677af9571b8a2ef5a169d10cb8955b01f"}, + {file = "beartype-0.22.5-py3-none-any.whl", hash = "sha256:d9743dd7cd6d193696eaa1e025f8a70fb09761c154675679ff236e61952dfba0"}, + {file = "beartype-0.22.5.tar.gz", hash = "sha256:516a9096cc77103c96153474fa35c3ebcd9d36bd2ec8d0e3a43307ced0fa6341"}, ] [package.extras] -dev = ["autoapi (>=0.9.0)", "celery", "click", "coverage (>=5.5)", "equinox ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "fastmcp ; python_version > \"3.9.0\" and python_version < \"3.14.0\"", "jax[cpu] ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "jaxtyping ; sys_platform == \"linux\"", "langchain ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "numba ; python_version < \"3.14.0\"", "numpy ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera (>=0.26.0) ; python_version < \"3.14.0\"", "poetry", "polars ; python_version < \"3.14.0\"", "pydata-sphinx-theme (<=0.7.2)", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "redis", "rich-click", "setuptools", "sphinx", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)", "sqlalchemy", "torch ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "tox (>=3.20.1)", "typer", "typing-extensions (>=3.10.0.0)", "xarray ; python_version < \"3.14.0\""] +dev = ["autoapi (>=0.9.0)", "celery", "click", "coverage (>=5.5)", "equinox ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "fastmcp ; python_version > \"3.9.0\" and python_version < \"3.14.0\"", "jax[cpu] ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "jaxtyping ; sys_platform == \"linux\"", "langchain ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "numba ; python_version < \"3.14.0\"", "numpy ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera (>=0.26.0) ; python_version < \"3.14.0\"", "poetry", "polars ; python_version < \"3.14.0\"", "pydata-sphinx-theme (<=0.7.2)", "pygments", "pyright (>=1.1.370)", "pytest (>=6.2.0)", "redis", "rich-click", "setuptools", "sphinx", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)", "sqlalchemy", "torch ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "tox (>=3.20.1)", "typer", "typing-extensions (>=3.10.0.0)", "xarray ; python_version < \"3.14.0\""] doc-ghp = ["mkdocs-material[imaging] (>=9.6.0)", "mkdocstrings-python (>=1.16.0)", "mkdocstrings-python-xref (>=1.16.0)"] doc-rtd = ["autoapi (>=0.9.0)", "pydata-sphinx-theme (<=0.7.2)", "setuptools", "sphinx (>=4.2.0,<6.0.0)", "sphinxext-opengraph (>=0.7.5)"] -test = ["celery", "click", "coverage (>=5.5)", "equinox ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "fastmcp ; python_version > \"3.9.0\" and python_version < \"3.14.0\"", "jax[cpu] ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "jaxtyping ; sys_platform == \"linux\"", "langchain ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "numba ; python_version < \"3.14.0\"", "numpy ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera (>=0.26.0) ; python_version < \"3.14.0\"", "poetry", "polars ; python_version < \"3.14.0\"", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "redis", "rich-click", "sphinx", "sqlalchemy", "torch ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "tox (>=3.20.1)", "typer", "typing-extensions (>=3.10.0.0)", "xarray ; python_version < \"3.14.0\""] -test-tox = ["celery", "click", "equinox ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "fastmcp ; python_version > \"3.9.0\" and python_version < \"3.14.0\"", "jax[cpu] ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "jaxtyping ; sys_platform == \"linux\"", "langchain ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "numba ; python_version < \"3.14.0\"", "numpy ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera (>=0.26.0) ; python_version < \"3.14.0\"", "poetry", "polars ; python_version < \"3.14.0\"", "pygments", "pyright (>=1.1.370)", "pytest (>=4.0.0)", "redis", "rich-click", "sphinx", "sqlalchemy", "torch ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "typer", "typing-extensions (>=3.10.0.0)", "xarray ; python_version < \"3.14.0\""] +test = ["celery", "click", "coverage (>=5.5)", "equinox ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "fastmcp ; python_version > \"3.9.0\" and python_version < \"3.14.0\"", "jax[cpu] ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "jaxtyping ; sys_platform == \"linux\"", "langchain ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "numba ; python_version < \"3.14.0\"", "numpy ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera (>=0.26.0) ; python_version < \"3.14.0\"", "poetry", "polars ; python_version < \"3.14.0\"", "pygments", "pyright (>=1.1.370)", "pytest (>=6.2.0)", "redis", "rich-click", "sphinx", "sqlalchemy", "torch ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "tox (>=3.20.1)", "typer", "typing-extensions (>=3.10.0.0)", "xarray ; python_version < \"3.14.0\""] +test-tox = ["celery", "click", "equinox ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "fastmcp ; python_version > \"3.9.0\" and python_version < \"3.14.0\"", "jax[cpu] ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "jaxtyping ; sys_platform == \"linux\"", "langchain ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "mypy (>=0.800) ; platform_python_implementation != \"PyPy\"", "nuitka (>=1.2.6) ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "numba ; python_version < \"3.14.0\"", "numpy ; python_version < \"3.14.0\" and sys_platform != \"darwin\" and platform_python_implementation != \"PyPy\"", "pandera (>=0.26.0) ; python_version < \"3.14.0\"", "poetry", "polars ; python_version < \"3.14.0\"", "pygments", "pyright (>=1.1.370)", "pytest (>=6.2.0)", "redis", "rich-click", "sphinx", "sqlalchemy", "torch ; sys_platform == \"linux\" and python_version < \"3.14.0\"", "typer", "typing-extensions (>=3.10.0.0)", "xarray ; python_version < \"3.14.0\""] test-tox-coverage = ["coverage (>=5.5)"] [[package]] @@ -1009,6 +1009,27 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "kent-distribution" +version = "0.1.0" +description = "Implements calculation of the density and fitting (using maximum likelihood estimate) of the Kent distribution. A unittest is performed if kent_distribution.py is called from the command line." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +matplotlib = "*" +numpy = "*" +scipy = "*" + +[package.source] +type = "git" +url = "https://github.com/Vlad-Kor/kent_distribution" +reference = "HEAD" +resolved_reference = "1637440397d97c51ad688fdcc8e2c96c6db48d77" + [[package]] name = "kiwisolver" version = "1.4.9" @@ -1506,14 +1527,14 @@ dill = ">=0.4.0" [[package]] name = "narwhals" -version = "2.9.0" +version = "2.10.2" description = "Extremely lightweight compatibility layer between dataframe libraries" optional = false python-versions = ">=3.9" groups = ["main", "dev"] files = [ - {file = "narwhals-2.9.0-py3-none-any.whl", hash = "sha256:c59f7de4763004ae81691ce16df71b4e55aead0ead7ccde8c8f2ef8c9559c765"}, - {file = "narwhals-2.9.0.tar.gz", hash = "sha256:d8cde40a6a8a7049d8e66608b7115ab19464acc6f305d136a8dc8ba396c4acfe"}, + {file = "narwhals-2.10.2-py3-none-any.whl", hash = "sha256:059cd5c6751161b97baedcaf17a514c972af6a70f36a89af17de1a0caf519c43"}, + {file = "narwhals-2.10.2.tar.gz", hash = "sha256:ff738a08bc993cbb792266bec15346c1d85cc68fdfe82a23283c3713f78bd354"}, ] [package.extras] @@ -1910,14 +1931,14 @@ type = ["mypy (>=1.18.2)"] [[package]] name = "plotly" -version = "6.3.1" +version = "6.4.0" description = "An open-source interactive data visualization library for Python" optional = false python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "plotly-6.3.1-py3-none-any.whl", hash = "sha256:8b4420d1dcf2b040f5983eed433f95732ed24930e496d36eb70d211923532e64"}, - {file = "plotly-6.3.1.tar.gz", hash = "sha256:dd896e3d940e653a7ce0470087e82c2bd903969a55e30d1b01bb389319461bb0"}, + {file = "plotly-6.4.0-py3-none-any.whl", hash = "sha256:a1062eafbdc657976c2eedd276c90e184ccd6c21282a5e9ee8f20efca9c9a4c5"}, + {file = "plotly-6.4.0.tar.gz", hash = "sha256:68c6db2ed2180289ef978f087841148b7efda687552276da15a6e9b92107052a"}, ] [package.dependencies] @@ -1930,7 +1951,7 @@ dev-build = ["build", "jupyter", "plotly[dev-core]"] dev-core = ["pytest", "requests", "ruff (==0.11.12)"] dev-optional = ["anywidget", "colorcet", "fiona (<=1.9.6) ; python_version <= \"3.8\"", "geopandas", "inflect", "numpy", "orjson", "pandas", "pdfrw", "pillow", "plotly-geo", "plotly[dev-build]", "plotly[kaleido]", "polars[timezone]", "pyarrow", "pyshp", "pytz", "scikit-image", "scipy", "shapely", "statsmodels", "vaex ; python_version <= \"3.9\"", "xarray"] express = ["numpy"] -kaleido = ["kaleido (>=1.0.0)"] +kaleido = ["kaleido (>=1.1.0)"] [[package]] name = "pluggy" @@ -1972,25 +1993,35 @@ xxhash = ["xxhash (>=1.4.3)"] [[package]] name = "psutil" -version = "7.1.1" +version = "7.1.3" description = "Cross-platform lib for process and system monitoring." optional = false python-versions = ">=3.6" groups = ["dev"] files = [ - {file = "psutil-7.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:8fa59d7b1f01f0337f12cd10dbd76e4312a4d3c730a4fedcbdd4e5447a8b8460"}, - {file = "psutil-7.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:2a95104eae85d088891716db676f780c1404fc15d47fde48a46a5d61e8f5ad2c"}, - {file = "psutil-7.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:98629cd8567acefcc45afe2f4ba1e9290f579eacf490a917967decce4b74ee9b"}, - {file = "psutil-7.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92ebc58030fb054fa0f26c3206ef01c31c29d67aee1367e3483c16665c25c8d2"}, - {file = "psutil-7.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:146a704f224fb2ded2be3da5ac67fc32b9ea90c45b51676f9114a6ac45616967"}, - {file = "psutil-7.1.1-cp37-abi3-win32.whl", hash = "sha256:295c4025b5cd880f7445e4379e6826f7307e3d488947bf9834e865e7847dc5f7"}, - {file = "psutil-7.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:9b4f17c5f65e44f69bd3a3406071a47b79df45cf2236d1f717970afcb526bcd3"}, - {file = "psutil-7.1.1-cp37-abi3-win_arm64.whl", hash = "sha256:5457cf741ca13da54624126cd5d333871b454ab133999a9a103fb097a7d7d21a"}, - {file = "psutil-7.1.1.tar.gz", hash = "sha256:092b6350145007389c1cfe5716050f02030a05219d90057ea867d18fe8d372fc"}, + {file = "psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc"}, + {file = "psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0"}, + {file = "psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7"}, + {file = "psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251"}, + {file = "psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa"}, + {file = "psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee"}, + {file = "psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353"}, + {file = "psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b"}, + {file = "psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9"}, + {file = "psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f"}, + {file = "psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7"}, + {file = "psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264"}, + {file = "psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab"}, + {file = "psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880"}, + {file = "psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3"}, + {file = "psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b"}, + {file = "psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd"}, + {file = "psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1"}, + {file = "psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74"}, ] [package.extras] -dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] +dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] test = ["pytest", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "setuptools", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] [[package]] @@ -2681,14 +2712,14 @@ telegram = ["requests"] [[package]] name = "trio" -version = "0.31.0" +version = "0.32.0" description = "A friendly Python library for async concurrency and I/O" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "trio-0.31.0-py3-none-any.whl", hash = "sha256:b5d14cd6293d79298b49c3485ffd9c07e3ce03a6da8c7dfbe0cb3dd7dc9a4774"}, - {file = "trio-0.31.0.tar.gz", hash = "sha256:f71d551ccaa79d0cb73017a33ef3264fde8335728eb4c6391451fe5d253a9d5b"}, + {file = "trio-0.32.0-py3-none-any.whl", hash = "sha256:4ab65984ef8370b79a76659ec87aa3a30c5c7c83ff250b4de88c29a8ab6123c5"}, + {file = "trio-0.32.0.tar.gz", hash = "sha256:150f29ec923bcd51231e1d4c71c7006e65247d68759dd1c19af4ea815a25806b"}, ] [package.dependencies] @@ -2923,4 +2954,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.11" -content-hash = "8b6c1e5300ee6ef84a7b8830e0dd0d135db6eb4c913b60b5ebd176f0d37b30b7" +content-hash = "969bc9727e129e907615d65c1b0a700e699ea9e1d965715241cd1d57e12e8b12" diff --git a/pyproject.toml b/pyproject.toml index 67f16bf..e3af8be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,6 +20,7 @@ dependencies = [ "gunicorn (>=23.0.0,<24.0.0)", "sphstat (>=1.0.6,<2.0.0)", "pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@002c7757e2a5a81e4e57da5c1b657c5f9bd5cf6e", + "kent-distribution @ git+https://github.com/Vlad-Kor/kent_distribution", ] [dependency-groups] From bdbd8d4a7a8a2689a1d8cda2bc5142e0018f910f Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 5 Nov 2025 12:30:50 +0100 Subject: [PATCH 051/152] change resolution ob 3d objects --- model/sphere/sphere.py | 2 +- model/torus/torus.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index 11c6b32..fccff6c 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -11,7 +11,7 @@ from util.selectors.slider import Slider class Sphere(Manifold): - def __init__(self, resolution=50, radius=1): + def __init__(self, resolution=200, radius=1): self.xyz = self.generate_xyz(resolution, radius) self.mesh = np.array([]) self.samples = np.array([]) diff --git a/model/torus/torus.py b/model/torus/torus.py index 443c885..6b7c046 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -4,7 +4,7 @@ from model.manifold import Manifold class Torus(Manifold): - def __init__(self, resolution=50, r=1, R=3): + def __init__(self, resolution=100, r=1, R=3): self.xyz = self.generate_xyz(resolution, r, R) self.mesh = np.array([]) self.samples = np.array([]) From 07a5f02c63b58e6b33e8940c0df2cf383bbe8e67 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 7 Nov 2025 17:47:35 +0100 Subject: [PATCH 052/152] add aditional method for watson fibonachi computation via inverse ode --- .../distributions/sphere/watson/fibonachi.py | 194 ++++++++++++++++-- .../watson/{random.py => random_sampling.py} | 4 + model/distributions/sphere/watson/watson.py | 4 +- model/sphere/sphere.py | 2 +- poetry.lock | 18 ++ pyproject.toml | 3 +- 6 files changed, 208 insertions(+), 17 deletions(-) rename model/distributions/sphere/watson/{random.py => random_sampling.py} (83%) diff --git a/model/distributions/sphere/watson/fibonachi.py b/model/distributions/sphere/watson/fibonachi.py index 653fd15..ce0939c 100644 --- a/model/distributions/sphere/watson/fibonachi.py +++ b/model/distributions/sphere/watson/fibonachi.py @@ -6,6 +6,7 @@ import sphstat from pyrecest.backend import array from pyrecest.distributions import WatsonDistribution as WatsonDistributionPyrecest +from scipy.special import erf, erfi, erfinv from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema from util.selectors.silder_log import LogSlider @@ -20,8 +21,12 @@ def __init__(self): def get_name(self): return "Fibonacci Lattice" - + + def sample(self, sample_options, distribution_options): + return self.sample_events(sample_options, distribution_options) + + def sample_inverse_interpolation(self, sample_options, distribution_options): kappa = distribution_options[0].state sample_count = sample_options[0].state @@ -44,12 +49,13 @@ def pdf(phi, theta): def f(t,y): # ring at lattitude theta has radius 2pi * sin(theta) - return (2*np.pi) * pdf(0, t) * np.sin(t) # choose phi = 0 because of symmetry + # 2pi comes from integrating over phi from 0 to 2pi + # sin(theta): d S^2 = sin(theta) d theta d phi + return 2* np.pi * pdf(0, t) * np.sin(t) # choose phi = 0 because of symmetry t_span = (0, np.pi) # theta from 0 to pi y0 = 0 # the value of the integrated pdf at 0 is 0 - # now compute the ode sol = scipy.integrate.solve_ivp(f, t_span, [y0]) x = sol.t @@ -58,7 +64,6 @@ def f(t,y): # due to numerical issues, for large kappa and samplecount, y can be slightly non monotonic # monotonicity is needed for interpolation, so maximum.accumulate then bump by eps - y = np.maximum.accumulate(y) diffs = np.diff(y) mask = diffs <= 0 @@ -70,16 +75,10 @@ def f(t,y): # now interpolate, but we swamp x and y so whe get the inverse function # this works because the function is monotonic # use PCHIP interpolation - try: - q = scipy.interpolate.PchipInterpolator(x=y, y=x) - except Exception as e: - print(y) - print("----") - print(x) - raise e + q = scipy.interpolate.PchipInterpolator(x=y, y=x) - i = np.linspace(0, 1, sample_count) + i = np.linspace(0, 1, sample_count, endpoint=False) + 0.5/sample_count # avoid poles by using centered kronecker lattice variant theta_i = q(i) w = np.cos(theta_i) @@ -90,4 +89,173 @@ def f(t,y): x_i_f_1 = np.sqrt(1-w**2) * np.cos( (2 * np.pi * indices) / gold_seq) x_i_f_2 = np.sqrt(1-w**2) * np.sin( (2 * np.pi * indices) / gold_seq) x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) # order so that mu=[0, 0, 1] - return x_i_f \ No newline at end of file + return x_i_f + + # same as sample, but using inverse ODE to solve, simmilar to https://isas.iar.kit.edu/pdf/FUSION25_Frisch.pdf Sec. V.D + # doesn't work for really big kappa due to numerical issues + def sample_inverse_ode(self, sample_options, distribution_options): + kappa = distribution_options[0].state + sample_count = sample_options[0].state + + mu = array([0.0, 0.0, 1.0]) + watson_dist = WatsonDistributionPyrecest(mu=mu, kappa=kappa) + + + ''' + Note: the watson pdf is symetrical + This means that it will always produce the same value for different + azimuth angles (phi) at a given polar angle (theta). + ''' + def pdf(phi, theta): + # polar angle: 0 ≤ θ ≤ π (theta) + # azimuth: 0 ≤ φ < 2π (phi) + x, y, z = Sphere.spherical_to_cartesian(theta=theta, phi=phi) + x = np.column_stack((x, y, z)) + wts = watson_dist.pdf(array(x)) + return wts + + # compute this is the inverse of the f from the above sample() method + def f(p,w): + # w = cos(theta), because sin(theta) is 0 for 0 and pi => divide by 0 + w = np.clip(w, -1, 1) + theta = np.arccos(w) + return 1/ ( (2*np.pi) * pdf(0, theta)) # choose phi = 0 because of symmetry + + + i = np.linspace(0, 1, sample_count) + + p_span = (0,1) + y0 = -1 + + + # now compute the ode + sol = scipy.integrate.solve_ivp(f, p_span, [y0], t_eval=i, dense_output=True, method="Radau") + + i = np.linspace(0, 1, sample_count, endpoint=False) + 0.5/sample_count + w = sol.sol(i) # shape (1,n) + w = np.squeeze(w) + + indices = np.arange(0, sample_count) + gold_seq = (1+5**0.5)/2 # golden ratio + + w = np.clip(w, -1, 1) # needed due to floating point impresision + + x_i_f_0 = w + x_i_f_1 = np.sqrt(1-w**2) * np.cos( (2 * np.pi * indices) / gold_seq) + x_i_f_2 = np.sqrt(1-w**2) * np.sin( (2 * np.pi * indices) / gold_seq) + x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) # order so that mu=[0, 0, 1] + return x_i_f + + + def sample_closed(self, sample_options, distribution_options): + def erfi_inv(x): + x = np.asarray(x, dtype=float) + + def _scalar_inv(y): + if y == 0.0: + return 0.0 + sgn = 1.0 if y > 0 else -1.0 + y = abs(y) + + # Find an upper bracket hi with erfi(hi) >= y (erfi is monotone) + hi = max(1.0, 0.5 * np.sqrt(np.pi) * y) # decent first guess + while erfi(hi) < y: + hi *= 2.0 + if hi > 30: # erfi(30) is huge + break + + # Solve g(t) = erfi(t) - y = 0 on [0, hi] + g = lambda t: erfi(t) - y + t = scipy.optimize.brentq(g, 0.0, hi, xtol=1e-12, rtol=1e-12, maxiter=200) + return sgn * t + + return np.vectorize(_scalar_inv, otypes=[float])(x) + + + sample_count = sample_options[0].state + k = distribution_options[0].state # kappa + + gold_seq = (1+5**0.5)/2 # golden ratio + + indices = np.arange(0, sample_count) + + + if k > 0: + w = 1 / (np.sqrt(k)) * erfi_inv( ((1-2*indices + sample_count)/ sample_count) * erfi(np.sqrt(k)) ) + elif k < 0: + la = -k + w = 1 / (np.sqrt(la)) * erfinv( ((2*indices +1 - sample_count)/ sample_count) * erf(np.sqrt(la)) ) + elif k == 0: + w = ((2*indices +1 - sample_count)/ sample_count) + + + w = np.clip(w, -1.0, 1.0) # clamp to avoid sqrt warnings due to numerical issues + + x_i_f_0 = w + x_i_f_1 = np.sqrt(1-w**2) * np.cos( (2 * np.pi * indices) / gold_seq) + x_i_f_2 = np.sqrt(1-w**2) * np.sin( (2 * np.pi * indices) / gold_seq) + x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) + + return x_i_f + + + def sample_events(self, sample_options, distribution_options): + kappa = distribution_options[0].state + sample_count = sample_options[0].state + + mu = array([0.0, 0.0, 1.0]) + watson_dist = WatsonDistributionPyrecest(mu=mu, kappa=kappa) + + + ''' + Note: the watson pdf is symetrical + This means that it will always produce the same value for different + azimuth angles (phi) at a given polar angle (theta). + ''' + def pdf(phi, theta): + # polar angle: 0 ≤ θ ≤ π (theta) + # azimuth: 0 ≤ φ < 2π (phi) + x, y, z = Sphere.spherical_to_cartesian(theta=theta, phi=phi) + x = np.column_stack((x, y, z)) + wts = watson_dist.pdf(array(x)) + return wts + + def f(t,y): + # ring at lattitude theta has radius 2pi * sin(theta) + # 2pi comes from integrating over phi from 0 to 2pi + # sin(theta): d S^2 = sin(theta) d theta d phi + return 2* np.pi * pdf(0, t) * np.sin(t) # choose phi = 0 because of symmetry + + t_span = (0, np.pi) # theta from 0 to pi + y0 = 0 # the value of the integrated pdf at 0 is 0 + + + # targets for events (centered variant, see above coment) + i = np.linspace(0, 1, sample_count, endpoint=False) + 0.5/sample_count + events = [] + for target in i: + def make_event(target): + def event(t, y): + return y[0] - target + return event + event = make_event(target) + event.terminal = False + event.direction = 1 # increasing + events.append(event) + + + sol = scipy.integrate.solve_ivp(f, t_span, [y0], events=events) + + event_thetas_i = np.array(sol.t_events).squeeze() + + w = np.cos(event_thetas_i) + + indices = np.arange(0, sample_count) + gold_seq = (1+5**0.5)/2 # golden ratio + + x_i_f_0 = w + x_i_f_1 = np.sqrt(1-w**2) * np.cos( (2 * np.pi * indices) / gold_seq) + x_i_f_2 = np.sqrt(1-w**2) * np.sin( (2 * np.pi * indices) / gold_seq) + x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) # order so that mu=[0, 0, 1] + return x_i_f + diff --git a/model/distributions/sphere/watson/random.py b/model/distributions/sphere/watson/random_sampling.py similarity index 83% rename from model/distributions/sphere/watson/random.py rename to model/distributions/sphere/watson/random_sampling.py index 6f54c0d..1e8338a 100644 --- a/model/distributions/sphere/watson/random.py +++ b/model/distributions/sphere/watson/random_sampling.py @@ -24,6 +24,10 @@ def sample(self, sample_options, distribution_options): phi = 0 numsamp = sample_options[0].state + if kappa == 0: # become uniform distribution + samples = np.random.normal(size=(numsamp, 3)) + samples /= np.linalg.norm(samples, axis=1)[:, np.newaxis] + return samples lamb, mu, nu = Sphere.spherical_to_cartesian(theta, phi) diff --git a/model/distributions/sphere/watson/watson.py b/model/distributions/sphere/watson/watson.py index 40dfd6e..fd8bfa3 100644 --- a/model/distributions/sphere/watson/watson.py +++ b/model/distributions/sphere/watson/watson.py @@ -7,14 +7,14 @@ from pyrecest.backend import array from pyrecest.distributions import WatsonDistribution as WatsonDistributionPyrecest -from model.distributions.sphere.watson.random import WatsonRandomSampling +from model.distributions.sphere.watson.random_sampling import WatsonRandomSampling from model.distributions.sphere.watson.fibonachi import WatsonFibonachiSampling class WatsonDistribution(SphereDistribution): def __init__(self): self.distribution_options = [ - Slider("κ (kappa)", 1.0, 10.0, 50.0), + Slider("κ (kappa)", -50, 10.0, 50.0), ] self.sampling_methods = [ diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index fccff6c..3513dea 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -11,7 +11,7 @@ from util.selectors.slider import Slider class Sphere(Manifold): - def __init__(self, resolution=200, radius=1): + def __init__(self, resolution=200, radius=0.999): self.xyz = self.generate_xyz(resolution, radius) self.mesh = np.array([]) self.samples = np.array([]) diff --git a/poetry.lock b/poetry.lock index a6a67c3..ff5f22b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2115,6 +2115,24 @@ files = [ [package.extras] diagrams = ["jinja2", "railroad-diagrams"] +[[package]] +name = "pyperf" +version = "2.9.0" +description = "Python module to run and analyze benchmarks" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyperf-2.9.0-py3-none-any.whl", hash = "sha256:215673fb60f3fbbc6c7a90b609b0806d8466fded8dd502bf5731e2b92506076e"}, + {file = "pyperf-2.9.0.tar.gz", hash = "sha256:dbe0feef8ec1a465df191bba2576149762d15a8c9985c9fea93ab625d875c362"}, +] + +[package.dependencies] +psutil = ">=5.9.0" + +[package.extras] +dev = ["tox"] + [[package]] name = "pyrecest" version = "0.8.0" diff --git a/pyproject.toml b/pyproject.toml index e3af8be..350bc58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,8 @@ dependencies = [ [dependency-groups] dev = [ "pytest (>=8.4.2,<9.0.0)", - "dash[testing] (>=2.16,<3.0)" + "dash[testing] (>=2.16,<3.0)", + "pyperf (>=2.9.0,<3.0.0)" ] From a9508fc4d7a2f4f8225e15581e27f05c1902e3df Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 10 Nov 2025 17:39:01 +0100 Subject: [PATCH 053/152] add benchmark of different methods --- .../sphere/watson/benchmark_fib_starts.py | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 model/distributions/sphere/watson/benchmark_fib_starts.py diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py new file mode 100644 index 0000000..9730823 --- /dev/null +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -0,0 +1,70 @@ +from model.distributions.sphere.watson.fibonachi import WatsonFibonachiSampling +from util.selectors.slider_float import FloatSlider +import pyperf +import statistics + +sampler = WatsonFibonachiSampling() +methods = { + "Closed-Form" : sampler.sample_closed, + "Inverse Interpolation" : sampler.sample_inverse_interpolation, + "Inverse ODE" : sampler.sample_inverse_ode, + "ODE Event Locations" : sampler.sample_events, +} + + +def benchmark_kappa(method, kappa, sample_count, times=50): + distribution_options = [FloatSlider("", 0, kappa, 100)] + sampling_options = [FloatSlider("", 0, sample_count, sample_count)] + for time in range(times): + method(sampling_options, distribution_options) + +def bench_single_kappa(kappa, sample_count): + results = {} + for method_name, method in methods.items(): + bench_name = f"Watson Fibonacci Sampling: {method_name} (kappa={kappa})" + benchmark = runner.bench_func(bench_name, benchmark_kappa, method, kappa, sample_count, 1) # TODO: make this higher + results[method_name] = benchmark + + return results + +def bench_multiple_kappa(): + sample_count = 1000 + all_results = {} + for kappa in range(-30, 31, 10): # TODO: set step lower + res = bench_single_kappa(kappa, sample_count) + for name, bench in res.items(): + if name not in all_results: + all_results[name] = [] + all_results[name].append((kappa, bench)) + return all_results + +def bench_multiple_sample_counts(kappa): + all_results = {} + for sample_count in range(100, 1001, 100): # TODO: set step lower + res = bench_single_kappa(kappa, sample_count) + for name, bench in res.items(): + if name not in all_results: + all_results[name] = [] + all_results[name].append((sample_count, bench)) + return all_results + + +def plot_benches(results): + import plotly.express as px + for bench in results: + print("plotting") + print("bench:", bench.mean()) + rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] + px.line(rows, x="kappa", y="time", color="name", markers=True).show() + + + + + +if __name__ == "__main__": + runner = pyperf.Runner() + mult_kappa = bench_multiple_kappa() + if not runner.args.worker: + plot_benches(mult_kappa) + + \ No newline at end of file From 2ffe8d6ef2b3aa6216d06ae5243945171c4572e5 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 10 Nov 2025 17:48:51 +0100 Subject: [PATCH 054/152] added doc how to run --- model/distributions/sphere/watson/benchmark_fib_starts.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index 9730823..2d53c63 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -1,8 +1,14 @@ +''' +Run this file directly from project root with: +PYTHONPATH=$PWD poetry run python model/distributions/sphere/watson/benchmark_fib_starts.py +''' + from model.distributions.sphere.watson.fibonachi import WatsonFibonachiSampling from util.selectors.slider_float import FloatSlider import pyperf import statistics + sampler = WatsonFibonachiSampling() methods = { "Closed-Form" : sampler.sample_closed, From 80436c4383b93fe8203368541bd92101ab3d38b0 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 10 Nov 2025 19:27:24 +0100 Subject: [PATCH 055/152] fix bug --- model/distributions/sphere/watson/benchmark_fib_starts.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index 2d53c63..9eefd05 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -57,11 +57,9 @@ def bench_multiple_sample_counts(kappa): def plot_benches(results): import plotly.express as px - for bench in results: - print("plotting") - print("bench:", bench.mean()) - rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] - px.line(rows, x="kappa", y="time", color="name", markers=True).show() + + rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] + px.line(rows, x="kappa", y="time", color="name", markers=True).show() From 1eced99b859e61bf7cc6adc0a849f645f8eb097a Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 10 Nov 2025 20:00:03 +0100 Subject: [PATCH 056/152] adjust values --- .../sphere/watson/benchmark_fib_starts.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index 9eefd05..16fd6de 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -28,15 +28,15 @@ def bench_single_kappa(kappa, sample_count): results = {} for method_name, method in methods.items(): bench_name = f"Watson Fibonacci Sampling: {method_name} (kappa={kappa})" - benchmark = runner.bench_func(bench_name, benchmark_kappa, method, kappa, sample_count, 1) # TODO: make this higher + benchmark = runner.bench_func(bench_name, benchmark_kappa, method, kappa, sample_count, 5) results[method_name] = benchmark return results def bench_multiple_kappa(): - sample_count = 1000 + sample_count = 10000 all_results = {} - for kappa in range(-30, 31, 10): # TODO: set step lower + for kappa in range(-30, 31, 2): res = bench_single_kappa(kappa, sample_count) for name, bench in res.items(): if name not in all_results: @@ -46,7 +46,7 @@ def bench_multiple_kappa(): def bench_multiple_sample_counts(kappa): all_results = {} - for sample_count in range(100, 1001, 100): # TODO: set step lower + for sample_count in range(100, 1001, 10): res = bench_single_kappa(kappa, sample_count) for name, bench in res.items(): if name not in all_results: @@ -55,11 +55,11 @@ def bench_multiple_sample_counts(kappa): return all_results -def plot_benches(results): +def plot_benches(results, title): import plotly.express as px rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] - px.line(rows, x="kappa", y="time", color="name", markers=True).show() + px.line(rows, x="kappa", y="time", color="name", markers=True, title=title).show() @@ -68,7 +68,12 @@ def plot_benches(results): if __name__ == "__main__": runner = pyperf.Runner() mult_kappa = bench_multiple_kappa() + mult_samples = bench_multiple_sample_counts(-10) + mult_samples = bench_multiple_sample_counts(10) + if not runner.args.worker: - plot_benches(mult_kappa) + plot_benches(mult_kappa, "Watson Fibonacci Sampling Benchmark: time taken for various kappa values (10000 samples)") + plot_benches(mult_samples, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=-10)") + plot_benches(mult_samples, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=10)") \ No newline at end of file From ff4243f1013d46e46124ed1a024b942945468c82 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 10 Nov 2025 20:02:17 +0100 Subject: [PATCH 057/152] add plot title --- .../sphere/watson/benchmark_fib_starts.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index 16fd6de..6f09269 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -55,7 +55,7 @@ def bench_multiple_sample_counts(kappa): return all_results -def plot_benches(results, title): +def plot_benches(results, title, x_label): import plotly.express as px rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] @@ -68,12 +68,12 @@ def plot_benches(results, title): if __name__ == "__main__": runner = pyperf.Runner() mult_kappa = bench_multiple_kappa() - mult_samples = bench_multiple_sample_counts(-10) - mult_samples = bench_multiple_sample_counts(10) + mult_samples_neg_10 = bench_multiple_sample_counts(-10) + mult_samples_10 = bench_multiple_sample_counts(10) if not runner.args.worker: - plot_benches(mult_kappa, "Watson Fibonacci Sampling Benchmark: time taken for various kappa values (10000 samples)") - plot_benches(mult_samples, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=-10)") - plot_benches(mult_samples, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=10)") + plot_benches(mult_kappa, "Watson Fibonacci Sampling Benchmark: time taken for various kappa values (10000 samples)", "kappa") + plot_benches(mult_samples_neg_10, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=-10)", "sample count") + plot_benches(mult_samples_10, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=10)", "sample count") \ No newline at end of file From 981786f09232344db2bc44d7d8678f31c8a84fe4 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 10 Nov 2025 20:25:03 +0100 Subject: [PATCH 058/152] fix bug in fibonachi event thingy --- model/distributions/sphere/watson/fibonachi.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/model/distributions/sphere/watson/fibonachi.py b/model/distributions/sphere/watson/fibonachi.py index ce0939c..325cf23 100644 --- a/model/distributions/sphere/watson/fibonachi.py +++ b/model/distributions/sphere/watson/fibonachi.py @@ -245,8 +245,16 @@ def event(t, y): sol = scipy.integrate.solve_ivp(f, t_span, [y0], events=events) - - event_thetas_i = np.array(sol.t_events).squeeze() + try: + event_thetas_i = np.array(sol.t_events).squeeze() + except ValueError: + # this sometimes happens for large samples counts, like kappa=-30 with 10k samples misses 2points + # its probably fine to continue + print("Warning: some points might have been missed") + event_thetas_i = np.array([te[0] if te.size else np.nan for te in sol.t_events], float) + event_thetas_i = event_thetas_i[~np.isnan(event_thetas_i)] + + sample_count = event_thetas_i.shape[0] w = np.cos(event_thetas_i) From df26c584ef2064606b55a7c3419065afec53ce0e Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 11 Nov 2025 06:52:02 +0100 Subject: [PATCH 059/152] fix duplicate benchmark names --- .../sphere/watson/benchmark_fib_starts.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index 6f09269..a1d10e0 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -24,10 +24,10 @@ def benchmark_kappa(method, kappa, sample_count, times=50): for time in range(times): method(sampling_options, distribution_options) -def bench_single_kappa(kappa, sample_count): +def bench_single_kappa(kappa, sample_count, id): results = {} for method_name, method in methods.items(): - bench_name = f"Watson Fibonacci Sampling: {method_name} (kappa={kappa})" + bench_name = f"Watson Fibonacci Sampling: {method_name} (kappa={kappa}) [{id}]" benchmark = runner.bench_func(bench_name, benchmark_kappa, method, kappa, sample_count, 5) results[method_name] = benchmark @@ -37,7 +37,7 @@ def bench_multiple_kappa(): sample_count = 10000 all_results = {} for kappa in range(-30, 31, 2): - res = bench_single_kappa(kappa, sample_count) + res = bench_single_kappa(kappa, sample_count, "Multiple Kappa") for name, bench in res.items(): if name not in all_results: all_results[name] = [] @@ -47,7 +47,7 @@ def bench_multiple_kappa(): def bench_multiple_sample_counts(kappa): all_results = {} for sample_count in range(100, 1001, 10): - res = bench_single_kappa(kappa, sample_count) + res = bench_single_kappa(kappa, sample_count, f"Multiple Sample Counts (kappa={kappa})") for name, bench in res.items(): if name not in all_results: all_results[name] = [] @@ -59,7 +59,7 @@ def plot_benches(results, title, x_label): import plotly.express as px rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] - px.line(rows, x="kappa", y="time", color="name", markers=True, title=title).show() + px.line(rows, x=x_label, y="time", color="name", markers=True, title=title).show() From 2bdd4ff8d69c71f65fe303a9ef26d5383943de61 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 11 Nov 2025 07:02:29 +0100 Subject: [PATCH 060/152] fix more duplicate titles --- model/distributions/sphere/watson/benchmark_fib_starts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index a1d10e0..5cc2902 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -47,7 +47,7 @@ def bench_multiple_kappa(): def bench_multiple_sample_counts(kappa): all_results = {} for sample_count in range(100, 1001, 10): - res = bench_single_kappa(kappa, sample_count, f"Multiple Sample Counts (kappa={kappa})") + res = bench_single_kappa(kappa, sample_count, f"Multiple Sample Counts (kappa={kappa}, sample_count={sample_count})") for name, bench in res.items(): if name not in all_results: all_results[name] = [] From 0334155100283d51fb21de532acd1e9b7951844e Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 11 Nov 2025 15:50:55 +0100 Subject: [PATCH 061/152] abstrct 2dplotsettings --- model/manifold.py | 4 ++-- model/torus/torus.py | 8 +++++++- renderer/Object3DAnd2DRenderer.py | 10 +++++----- renderer/PlotSettings2d.py | 7 +++++++ 4 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 renderer/PlotSettings2d.py diff --git a/model/manifold.py b/model/manifold.py index 6f8e771..434139f 100644 --- a/model/manifold.py +++ b/model/manifold.py @@ -2,8 +2,8 @@ class Manifold(ABC): def __init__(self): - # optional axis labels if the manifold supports 2D rendering - self.axes_2d = None + # optional, setting for if the manifold supports 2d plotting + self.plot_settings_2d = None # generates renderable xyz grid @abstractmethod diff --git a/model/torus/torus.py b/model/torus/torus.py index 6b7c046..ce54033 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -2,6 +2,7 @@ from model.distributions.distribution_loader import DistributionLoader from model.distributions.torus.torus_distribution import TorusDistribution from model.manifold import Manifold +from renderer.PlotSettings2d import PlotSettings2D class Torus(Manifold): def __init__(self, resolution=100, r=1, R=3): @@ -14,10 +15,15 @@ def __init__(self, resolution=100, r=1, R=3): self.r = r self.R = R - self.axes_2d = ( + axes_2d = ( np.arange(0, 2.5 * np.pi, np.pi / 2), # 0, π/2, π, 3π/2, 2π ["0", "π/2", "π", "3π/2", "2π"] ) + self.plot_settings_2d = PlotSettings2D( + axes_2d_x=axes_2d, + axes_2d_y=axes_2d, + lock_aspect_ratio=True + ) def generate_xyz(self, resolution=50, r=1, R=3): t = np.linspace(0, 2*np.pi, resolution) diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index fef3a20..3794e39 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -38,19 +38,19 @@ def __init__(self, object, id): )) self.fig_2d.update_layout(dragmode="pan") - if object.axes_2d is not None: + if object.plot_settings_2d is not None: self.fig_2d.update_xaxes( title_text="t", tickmode="array", - tickvals=object.axes_2d[0], - ticktext=object.axes_2d[1], + tickvals=object.plot_settings_2d.axes_2d_x[0], + ticktext=object.plot_settings_2d.axes_2d_x[1], zeroline=False, ) self.fig_2d.update_yaxes( title_text="p", tickmode="array", - tickvals=object.axes_2d[0], - ticktext=object.axes_2d[1], + tickvals=object.plot_settings_2d.axes_2d_y[0], + ticktext=object.plot_settings_2d.axes_2d_y[1], zeroline=False, ) diff --git a/renderer/PlotSettings2d.py b/renderer/PlotSettings2d.py new file mode 100644 index 0000000..dcf7c4c --- /dev/null +++ b/renderer/PlotSettings2d.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + +@dataclass +class PlotSettings2D: + axes_2d_x: tuple + axes_2d_y: tuple + lock_aspect_ratio: bool From 44040aa8817c89433b6920c2d9cf88867026ec43 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 11 Nov 2025 15:53:58 +0100 Subject: [PATCH 062/152] added locking aspect ratio --- renderer/Object3DAnd2DRenderer.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index 3794e39..e1db1b9 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -54,6 +54,12 @@ def __init__(self, object, id): zeroline=False, ) + if object.plot_settings_2d.lock_aspect_ratio: + self.fig_2d.update_yaxes( + scaleanchor = "x", + scaleratio = 1, + ) + def register_plot_callbacks(self): # updates the plot based on selected sampling options @callback( From 9e7c81737500757809aa960e1ff9a23104b28dff Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 11 Nov 2025 17:00:31 +0100 Subject: [PATCH 063/152] implement periodicity --- model/torus/torus.py | 6 +++- renderer/Object3DAnd2DRenderer.py | 53 ++++++++++++++++++++++++++++--- renderer/PlotSettings2d.py | 5 +++ 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/model/torus/torus.py b/model/torus/torus.py index ce54033..615dd93 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -22,7 +22,11 @@ def __init__(self, resolution=100, r=1, R=3): self.plot_settings_2d = PlotSettings2D( axes_2d_x=axes_2d, axes_2d_y=axes_2d, - lock_aspect_ratio=True + lock_aspect_ratio=True, + periodic_x=True, + periodic_y=True, + periodic_x_amount=2 * np.pi, + periodic_y_amount=2 * np.pi, ) def generate_xyz(self, resolution=50, r=1, R=3): diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index e1db1b9..270f947 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -11,6 +11,14 @@ def __init__(self, object, id): self.register_plot_callbacks() self.register_mode_callbacks() + self.per_x = object.plot_settings_2d.periodic_x + self.per_y = object.plot_settings_2d.periodic_y + + self.perx_x_amount = object.plot_settings_2d.periodic_x_amount + self.pery_y_amount = object.plot_settings_2d.periodic_y_amount + + padd = 0.5 + self.fig_2d = go.Figure( data=[ go.Scattergl( @@ -23,11 +31,21 @@ def __init__(self, object, id): color="red", line=dict(width=1, color="black") ), - marker_color="red", - marker_size=6, - marker_line_color="black", showlegend=True, ), + + go.Scattergl( + name="Samples (periodic extension)", + x=[], + y=[], + mode="markers", + marker=dict( + size=6, + color="#a2acbd", + line=dict(width=1, color="#a2acbd") + ), + showlegend=(object.plot_settings_2d.periodic_x or object.plot_settings_2d.periodic_y) + ) ] ) self.fig_2d.update_layout(legend=dict( @@ -60,6 +78,19 @@ def __init__(self, object, id): scaleratio = 1, ) + # so that it doent autoscale for periodicity points + padd = 0.5 + if self.per_x: + self.fig_2d.update_xaxes( + range=[0 - padd, self.perx_x_amount + padd], + ) + if self.per_y: + self.fig_2d.update_yaxes( + range=[0 - padd, self.pery_y_amount + padd], + ) + + + def register_plot_callbacks(self): # updates the plot based on selected sampling options @callback( @@ -150,12 +181,26 @@ def update_plot_sample_2d(self, values_dist, ids_dist, values_samp, ids_samp, se marker_size = (10 * (sample_count / 100) ** (-0.35)) / dpr marker_size = np.minimum(10,marker_size) - patched_figure["data"][0].marker.size = marker_size + patched_figure["data"][0].marker.size = marker_size * 1.5 + patched_figure["data"][1].marker.size = marker_size # x is p, y is t patched_figure["data"][0].x = tp[:, 1] patched_figure["data"][0].y = tp[:, 0] + ext_x = np.array([]) + ext_y = np.array([]) + if self.per_x: + ext_x = np.concatenate([tp[:,1] - self.perx_x_amount, tp[:,1] + self.perx_x_amount]) + ext_y = np.concatenate([tp[:,0], tp[:,0]]) + + if self.per_y: + ext_x = np.concatenate([ext_x, tp[:,1], tp[:,1]]) + ext_y = np.concatenate([ext_y, tp[:,0] - self.pery_y_amount, tp[:,0] + self.pery_y_amount]) + + if self.per_x or self.per_y: + patched_figure["data"][1].x = ext_x + patched_figure["data"][1].y = ext_y return patched_figure diff --git a/renderer/PlotSettings2d.py b/renderer/PlotSettings2d.py index dcf7c4c..3eb4c37 100644 --- a/renderer/PlotSettings2d.py +++ b/renderer/PlotSettings2d.py @@ -5,3 +5,8 @@ class PlotSettings2D: axes_2d_x: tuple axes_2d_y: tuple lock_aspect_ratio: bool + + periodic_x: bool = False + periodic_y: bool = False + periodic_x_amount: float = 0.0 # amount to shift for periodicity + periodic_y_amount: float = 0.0 From 676f166baa1fed1c4be4d2d71124bf95f975642c Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 11 Nov 2025 22:45:26 +0100 Subject: [PATCH 064/152] fix benchmark label bug --- .../sphere/watson/benchmark_fib_starts.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index 5cc2902..9dbcfb9 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -57,8 +57,10 @@ def bench_multiple_sample_counts(kappa): def plot_benches(results, title, x_label): import plotly.express as px - - rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] + if x_label == "sample_count": + rows = [dict(name=n, sample_count=k, time=t.mean()) for n, pts in results.items() for k, t in pts] + else: + rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] px.line(rows, x=x_label, y="time", color="name", markers=True, title=title).show() @@ -73,7 +75,7 @@ def plot_benches(results, title, x_label): if not runner.args.worker: plot_benches(mult_kappa, "Watson Fibonacci Sampling Benchmark: time taken for various kappa values (10000 samples)", "kappa") - plot_benches(mult_samples_neg_10, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=-10)", "sample count") - plot_benches(mult_samples_10, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=10)", "sample count") + plot_benches(mult_samples_neg_10, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=-10)", "sample_count") + plot_benches(mult_samples_10, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=10)", "sample_count") \ No newline at end of file From 63ec3c0c82da6bf1931faedf7717051a89a5044b Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 11 Nov 2025 23:01:58 +0100 Subject: [PATCH 065/152] output image instead of opening browser --- .../sphere/watson/benchmark_fib_starts.py | 17 +- poetry.lock | 279 ++++++++++++++++++ pyproject.toml | 3 +- 3 files changed, 291 insertions(+), 8 deletions(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index 9dbcfb9..b1c4aa3 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -57,14 +57,17 @@ def bench_multiple_sample_counts(kappa): def plot_benches(results, title, x_label): import plotly.express as px - if x_label == "sample_count": - rows = [dict(name=n, sample_count=k, time=t.mean()) for n, pts in results.items() for k, t in pts] - else: - rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] - px.line(rows, x=x_label, y="time", color="name", markers=True, title=title).show() - + try: + if x_label == "sample_count": + rows = [dict(name=n, sample_count=k, time=t.mean()) for n, pts in results.items() for k, t in pts] + else: + rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] + fig = px.line(rows, x=x_label, y="time", color="name", markers=True, title=title) + fig.write_image(f"{title.replace(' ', '_').replace(':', '')}.svg") + except Exception as e: + print("Generating plot failed, dumping data:", e) + print(results.items()) - if __name__ == "__main__": diff --git a/poetry.lock b/poetry.lock index ff5f22b..103af62 100644 --- a/poetry.lock +++ b/poetry.lock @@ -370,6 +370,22 @@ files = [ {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, ] +[[package]] +name = "choreographer" +version = "1.2.1" +description = "Devtools Protocol implementation for chrome." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "choreographer-1.2.1-py3-none-any.whl", hash = "sha256:9af5385effa3c204dbc337abf7ac74fd8908ced326a15645dc31dde75718c77e"}, + {file = "choreographer-1.2.1.tar.gz", hash = "sha256:022afd72b1e9b0bcb950420b134e70055a294c791b6f36cfb47d89745b701b5f"}, +] + +[package.dependencies] +logistro = ">=2.0.1" +simplejson = ">=3.19.3" + [[package]] name = "click" version = "8.3.0" @@ -1009,6 +1025,25 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "kaleido" +version = "1.2.0" +description = "Plotly graph export library" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "kaleido-1.2.0-py3-none-any.whl", hash = "sha256:c27ed82b51df6b923d0e656feac221343a0dbcd2fb9bc7e6b1db97f61e9a1513"}, + {file = "kaleido-1.2.0.tar.gz", hash = "sha256:fa621a14423e8effa2895a2526be00af0cf21655be1b74b7e382c171d12e71ef"}, +] + +[package.dependencies] +choreographer = ">=1.1.1" +logistro = ">=1.0.8" +orjson = ">=3.10.15" +packaging = "*" +pytest-timeout = ">=2.4.0" + [[package]] name = "kent-distribution" version = "0.1.0" @@ -1141,6 +1176,18 @@ files = [ {file = "kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d"}, ] +[[package]] +name = "logistro" +version = "2.0.1" +description = "Simple wrapper over logging for a couple basic features" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "logistro-2.0.1-py3-none-any.whl", hash = "sha256:06ffa127b9fb4ac8b1972ae6b2a9d7fde57598bf5939cd708f43ec5bba2d31eb"}, + {file = "logistro-2.0.1.tar.gz", hash = "sha256:8446affc82bab2577eb02bfcbcae196ae03129287557287b6a070f70c1985047"}, +] + [[package]] name = "lxml" version = "6.0.2" @@ -1665,6 +1712,103 @@ files = [ [package.dependencies] et-xmlfile = "*" +[[package]] +name = "orjson" +version = "3.11.4" +description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "orjson-3.11.4-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e3aa2118a3ece0d25489cbe48498de8a5d580e42e8d9979f65bf47900a15aba1"}, + {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a69ab657a4e6733133a3dca82768f2f8b884043714e8d2b9ba9f52b6efef5c44"}, + {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3740bffd9816fc0326ddc406098a3a8f387e42223f5f455f2a02a9f834ead80c"}, + {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65fd2f5730b1bf7f350c6dc896173d3460d235c4be007af73986d7cd9a2acd23"}, + {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fdc3ae730541086158d549c97852e2eea6820665d4faf0f41bf99df41bc11ea"}, + {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e10b4d65901da88845516ce9f7f9736f9638d19a1d483b3883dc0182e6e5edba"}, + {file = "orjson-3.11.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb6a03a678085f64b97f9d4a9ae69376ce91a3a9e9b56a82b1580d8e1d501aff"}, + {file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c82e4f0b1c712477317434761fbc28b044c838b6b1240d895607441412371ac"}, + {file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:d58c166a18f44cc9e2bad03a327dc2d1a3d2e85b847133cfbafd6bfc6719bd79"}, + {file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:94f206766bf1ea30e1382e4890f763bd1eefddc580e08fec1ccdc20ddd95c827"}, + {file = "orjson-3.11.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:41bf25fb39a34cf8edb4398818523277ee7096689db352036a9e8437f2f3ee6b"}, + {file = "orjson-3.11.4-cp310-cp310-win32.whl", hash = "sha256:fa9627eba4e82f99ca6d29bc967f09aba446ee2b5a1ea728949ede73d313f5d3"}, + {file = "orjson-3.11.4-cp310-cp310-win_amd64.whl", hash = "sha256:23ef7abc7fca96632d8174ac115e668c1e931b8fe4dde586e92a500bf1914dcc"}, + {file = "orjson-3.11.4-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5e59d23cd93ada23ec59a96f215139753fbfe3a4d989549bcb390f8c00370b39"}, + {file = "orjson-3.11.4-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:5c3aedecfc1beb988c27c79d52ebefab93b6c3921dbec361167e6559aba2d36d"}, + {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da9e5301f1c2caa2a9a4a303480d79c9ad73560b2e7761de742ab39fe59d9175"}, + {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8873812c164a90a79f65368f8f96817e59e35d0cc02786a5356f0e2abed78040"}, + {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d7feb0741ebb15204e748f26c9638e6665a5fa93c37a2c73d64f1669b0ddc63"}, + {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01ee5487fefee21e6910da4c2ee9eef005bee568a0879834df86f888d2ffbdd9"}, + {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d40d46f348c0321df01507f92b95a377240c4ec31985225a6668f10e2676f9a"}, + {file = "orjson-3.11.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95713e5fc8af84d8edc75b785d2386f653b63d62b16d681687746734b4dfc0be"}, + {file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ad73ede24f9083614d6c4ca9a85fe70e33be7bf047ec586ee2363bc7418fe4d7"}, + {file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:842289889de515421f3f224ef9c1f1efb199a32d76d8d2ca2706fa8afe749549"}, + {file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3b2427ed5791619851c52a1261b45c233930977e7de8cf36de05636c708fa905"}, + {file = "orjson-3.11.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c36e524af1d29982e9b190573677ea02781456b2e537d5840e4538a5ec41907"}, + {file = "orjson-3.11.4-cp311-cp311-win32.whl", hash = "sha256:87255b88756eab4a68ec61837ca754e5d10fa8bc47dc57f75cedfeaec358d54c"}, + {file = "orjson-3.11.4-cp311-cp311-win_amd64.whl", hash = "sha256:e2d5d5d798aba9a0e1fede8d853fa899ce2cb930ec0857365f700dffc2c7af6a"}, + {file = "orjson-3.11.4-cp311-cp311-win_arm64.whl", hash = "sha256:6bb6bb41b14c95d4f2702bce9975fda4516f1db48e500102fc4d8119032ff045"}, + {file = "orjson-3.11.4-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:d4371de39319d05d3f482f372720b841c841b52f5385bd99c61ed69d55d9ab50"}, + {file = "orjson-3.11.4-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:e41fd3b3cac850eaae78232f37325ed7d7436e11c471246b87b2cd294ec94853"}, + {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600e0e9ca042878c7fdf189cf1b028fe2c1418cc9195f6cb9824eb6ed99cb938"}, + {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7bbf9b333f1568ef5da42bc96e18bf30fd7f8d54e9ae066d711056add508e415"}, + {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806363144bb6e7297b8e95870e78d30a649fdc4e23fc84daa80c8ebd366ce44"}, + {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad355e8308493f527d41154e9053b86a5be892b3b359a5c6d5d95cda23601cb2"}, + {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8a7517482667fb9f0ff1b2f16fe5829296ed7a655d04d68cd9711a4d8a4e708"}, + {file = "orjson-3.11.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97eb5942c7395a171cbfecc4ef6701fc3c403e762194683772df4c54cfbb2210"}, + {file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:149d95d5e018bdd822e3f38c103b1a7c91f88d38a88aada5c4e9b3a73a244241"}, + {file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:624f3951181eb46fc47dea3d221554e98784c823e7069edb5dbd0dc826ac909b"}, + {file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:03bfa548cf35e3f8b3a96c4e8e41f753c686ff3d8e182ce275b1751deddab58c"}, + {file = "orjson-3.11.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:525021896afef44a68148f6ed8a8bf8375553d6066c7f48537657f64823565b9"}, + {file = "orjson-3.11.4-cp312-cp312-win32.whl", hash = "sha256:b58430396687ce0f7d9eeb3dd47761ca7d8fda8e9eb92b3077a7a353a75efefa"}, + {file = "orjson-3.11.4-cp312-cp312-win_amd64.whl", hash = "sha256:c6dbf422894e1e3c80a177133c0dda260f81428f9de16d61041949f6a2e5c140"}, + {file = "orjson-3.11.4-cp312-cp312-win_arm64.whl", hash = "sha256:d38d2bc06d6415852224fcc9c0bfa834c25431e466dc319f0edd56cca81aa96e"}, + {file = "orjson-3.11.4-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2d6737d0e616a6e053c8b4acc9eccea6b6cce078533666f32d140e4f85002534"}, + {file = "orjson-3.11.4-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:afb14052690aa328cc118a8e09f07c651d301a72e44920b887c519b313d892ff"}, + {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38aa9e65c591febb1b0aed8da4d469eba239d434c218562df179885c94e1a3ad"}, + {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f2cf4dfaf9163b0728d061bebc1e08631875c51cd30bf47cb9e3293bfbd7dcd5"}, + {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89216ff3dfdde0e4070932e126320a1752c9d9a758d6a32ec54b3b9334991a6a"}, + {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9daa26ca8e97fae0ce8aa5d80606ef8f7914e9b129b6b5df9104266f764ce436"}, + {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c8b2769dc31883c44a9cd126560327767f848eb95f99c36c9932f51090bfce9"}, + {file = "orjson-3.11.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1469d254b9884f984026bd9b0fa5bbab477a4bfe558bba6848086f6d43eb5e73"}, + {file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:68e44722541983614e37117209a194e8c3ad07838ccb3127d96863c95ec7f1e0"}, + {file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e7805fda9672c12be2f22ae124dcd7b03928d6c197544fe12174b86553f3196"}, + {file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:04b69c14615fb4434ab867bf6f38b2d649f6f300af30a6705397e895f7aec67a"}, + {file = "orjson-3.11.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:639c3735b8ae7f970066930e58cf0ed39a852d417c24acd4a25fc0b3da3c39a6"}, + {file = "orjson-3.11.4-cp313-cp313-win32.whl", hash = "sha256:6c13879c0d2964335491463302a6ca5ad98105fc5db3565499dcb80b1b4bd839"}, + {file = "orjson-3.11.4-cp313-cp313-win_amd64.whl", hash = "sha256:09bf242a4af98732db9f9a1ec57ca2604848e16f132e3f72edfd3c5c96de009a"}, + {file = "orjson-3.11.4-cp313-cp313-win_arm64.whl", hash = "sha256:a85f0adf63319d6c1ba06fb0dbf997fced64a01179cf17939a6caca662bf92de"}, + {file = "orjson-3.11.4-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:42d43a1f552be1a112af0b21c10a5f553983c2a0938d2bbb8ecd8bc9fb572803"}, + {file = "orjson-3.11.4-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:26a20f3fbc6c7ff2cb8e89c4c5897762c9d88cf37330c6a117312365d6781d54"}, + {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e3f20be9048941c7ffa8fc523ccbd17f82e24df1549d1d1fe9317712d19938e"}, + {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aac364c758dc87a52e68e349924d7e4ded348dedff553889e4d9f22f74785316"}, + {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5c54a6d76e3d741dcc3f2707f8eeb9ba2a791d3adbf18f900219b62942803b1"}, + {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f28485bdca8617b79d44627f5fb04336897041dfd9fa66d383a49d09d86798bc"}, + {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bfc2a484cad3585e4ba61985a6062a4c2ed5c7925db6d39f1fa267c9d166487f"}, + {file = "orjson-3.11.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e34dbd508cb91c54f9c9788923daca129fe5b55c5b4eebe713bf5ed3791280cf"}, + {file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b13c478fa413d4b4ee606ec8e11c3b2e52683a640b006bb586b3041c2ca5f606"}, + {file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:724ca721ecc8a831b319dcd72cfa370cc380db0bf94537f08f7edd0a7d4e1780"}, + {file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:977c393f2e44845ce1b540e19a786e9643221b3323dae190668a98672d43fb23"}, + {file = "orjson-3.11.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e539e382cf46edec157ad66b0b0872a90d829a6b71f17cb633d6c160a223155"}, + {file = "orjson-3.11.4-cp314-cp314-win32.whl", hash = "sha256:d63076d625babab9db5e7836118bdfa086e60f37d8a174194ae720161eb12394"}, + {file = "orjson-3.11.4-cp314-cp314-win_amd64.whl", hash = "sha256:0a54d6635fa3aaa438ae32e8570b9f0de36f3f6562c308d2a2a452e8b0592db1"}, + {file = "orjson-3.11.4-cp314-cp314-win_arm64.whl", hash = "sha256:78b999999039db3cf58f6d230f524f04f75f129ba3d1ca2ed121f8657e575d3d"}, + {file = "orjson-3.11.4-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:405261b0a8c62bcbd8e2931c26fdc08714faf7025f45531541e2b29e544b545b"}, + {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af02ff34059ee9199a3546f123a6ab4c86caf1708c79042caf0820dc290a6d4f"}, + {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b2eba969ea4203c177c7b38b36c69519e6067ee68c34dc37081fac74c796e10"}, + {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0baa0ea43cfa5b008a28d3c07705cf3ada40e5d347f0f44994a64b1b7b4b5350"}, + {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80fd082f5dcc0e94657c144f1b2a3a6479c44ad50be216cf0c244e567f5eae19"}, + {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e3704d35e47d5bee811fb1cbd8599f0b4009b14d451c4c57be5a7e25eb89a13"}, + {file = "orjson-3.11.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:caa447f2b5356779d914658519c874cf3b7629e99e63391ed519c28c8aea4919"}, + {file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bba5118143373a86f91dadb8df41d9457498226698ebdf8e11cbb54d5b0e802d"}, + {file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:622463ab81d19ef3e06868b576551587de8e4d518892d1afab71e0fbc1f9cffc"}, + {file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3e0a700c4b82144b72946b6629968df9762552ee1344bfdb767fecdd634fbd5a"}, + {file = "orjson-3.11.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6e18a5c15e764e5f3fc569b47872450b4bcea24f2a6354c0a0e95ad21045d5a9"}, + {file = "orjson-3.11.4-cp39-cp39-win32.whl", hash = "sha256:fb1c37c71cad991ef4d89c7a634b5ffb4447dbd7ae3ae13e8f5ee7f1775e7ab1"}, + {file = "orjson-3.11.4-cp39-cp39-win_amd64.whl", hash = "sha256:e2985ce8b8c42d00492d0ed79f2bd2b6460d00f2fa671dfde4bf2e02f49bf5c6"}, + {file = "orjson-3.11.4.tar.gz", hash = "sha256:39485f4ab4c9b30a3943cfe99e1a213c4776fb69e8abd68f66b83d5a0b0fdc6d"}, +] + [[package]] name = "outcome" version = "1.3.0.post0" @@ -2249,6 +2393,21 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-timeout" +version = "2.4.0" +description = "pytest plugin to abort hanging tests" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2"}, + {file = "pytest_timeout-2.4.0.tar.gz", hash = "sha256:7e68e90b01f9eff71332b25001f85c75495fc4e3a836701876183c4bcfd0540a"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2568,6 +2727,126 @@ numpy = ">=1.21" docs = ["matplotlib", "numpydoc (==1.1.*)", "sphinx", "sphinx-book-theme", "sphinx-remove-toctrees"] test = ["pytest", "pytest-cov", "scipy-doctest"] +[[package]] +name = "simplejson" +version = "3.20.2" +description = "Simple, fast, extensible JSON encoder/decoder for Python" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.5" +groups = ["dev"] +files = [ + {file = "simplejson-3.20.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:11847093fd36e3f5a4f595ff0506286c54885f8ad2d921dfb64a85bce67f72c4"}, + {file = "simplejson-3.20.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:4d291911d23b1ab8eb3241204dd54e3ec60ddcd74dfcb576939d3df327205865"}, + {file = "simplejson-3.20.2-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:da6d16d7108d366bbbf1c1f3274662294859c03266e80dd899fc432598115ea4"}, + {file = "simplejson-3.20.2-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:9ddf9a07694c5bbb4856271cbc4247cc6cf48f224a7d128a280482a2f78bae3d"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:3a0d2337e490e6ab42d65a082e69473717f5cc75c3c3fb530504d3681c4cb40c"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8ba88696351ed26a8648f8378a1431223f02438f8036f006d23b4f5b572778fa"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:00bcd408a4430af99d1f8b2b103bb2f5133bb688596a511fcfa7db865fbb845e"}, + {file = "simplejson-3.20.2-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4fc62feb76f590ccaff6f903f52a01c58ba6423171aa117b96508afda9c210f0"}, + {file = "simplejson-3.20.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6d7286dc11af60a2f76eafb0c2acde2d997e87890e37e24590bb513bec9f1bc5"}, + {file = "simplejson-3.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c01379b4861c3b0aa40cba8d44f2b448f5743999aa68aaa5d3ef7049d4a28a2d"}, + {file = "simplejson-3.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a16b029ca25645b3bc44e84a4f941efa51bf93c180b31bd704ce6349d1fc77c1"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e22a5fb7b1437ffb057e02e1936a3bfb19084ae9d221ec5e9f4cf85f69946b6"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8b6ff02fc7b8555c906c24735908854819b0d0dc85883d453e23ca4c0445d01"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bfc1c396ad972ba4431130b42307b2321dba14d988580c1ac421ec6a6b7cee3"}, + {file = "simplejson-3.20.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a97249ee1aee005d891b5a211faf58092a309f3d9d440bc269043b08f662eda"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f1036be00b5edaddbddbb89c0f80ed229714a941cfd21e51386dc69c237201c2"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d6f5bacb8cdee64946b45f2680afa3f54cd38e62471ceda89f777693aeca4e4"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8db6841fb796ec5af632f677abf21c6425a1ebea0d9ac3ef1a340b8dc69f52b8"}, + {file = "simplejson-3.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c0a341f7cc2aae82ee2b31f8a827fd2e51d09626f8b3accc441a6907c88aedb7"}, + {file = "simplejson-3.20.2-cp310-cp310-win32.whl", hash = "sha256:27f9c01a6bc581d32ab026f515226864576da05ef322d7fc141cd8a15a95ce53"}, + {file = "simplejson-3.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:c0a63ec98a4547ff366871bf832a7367ee43d047bcec0b07b66c794e2137b476"}, + {file = "simplejson-3.20.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:06190b33cd7849efc413a5738d3da00b90e4a5382fd3d584c841ac20fb828c6f"}, + {file = "simplejson-3.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4ad4eac7d858947a30d2c404e61f16b84d16be79eb6fb316341885bdde864fa8"}, + {file = "simplejson-3.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b392e11c6165d4a0fde41754a0e13e1d88a5ad782b245a973dd4b2bdb4e5076a"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51eccc4e353eed3c50e0ea2326173acdc05e58f0c110405920b989d481287e51"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:306e83d7c331ad833d2d43c76a67f476c4b80c4a13334f6e34bb110e6105b3bd"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f820a6ac2ef0bc338ae4963f4f82ccebdb0824fe9caf6d660670c578abe01013"}, + {file = "simplejson-3.20.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e7a066528a5451433eb3418184f05682ea0493d14e9aae690499b7e1eb6b81"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:438680ddde57ea87161a4824e8de04387b328ad51cfdf1eaf723623a3014b7aa"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cac78470ae68b8d8c41b6fca97f5bf8e024ca80d5878c7724e024540f5cdaadb"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7524e19c2da5ef281860a3d74668050c6986be15c9dd99966034ba47c68828c2"}, + {file = "simplejson-3.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e9b6d845a603b2eef3394eb5e21edb8626cd9ae9a8361d14e267eb969dbe413"}, + {file = "simplejson-3.20.2-cp311-cp311-win32.whl", hash = "sha256:47d8927e5ac927fdd34c99cc617938abb3624b06ff86e8e219740a86507eb961"}, + {file = "simplejson-3.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:ba4edf3be8e97e4713d06c3d302cba1ff5c49d16e9d24c209884ac1b8455520c"}, + {file = "simplejson-3.20.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4376d5acae0d1e91e78baeba4ee3cf22fbf6509d81539d01b94e0951d28ec2b6"}, + {file = "simplejson-3.20.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f8fe6de652fcddae6dec8f281cc1e77e4e8f3575249e1800090aab48f73b4259"}, + {file = "simplejson-3.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25ca2663d99328d51e5a138f22018e54c9162438d831e26cfc3458688616eca8"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12a6b2816b6cab6c3fd273d43b1948bc9acf708272074c8858f579c394f4cbc9"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac20dc3fcdfc7b8415bfc3d7d51beccd8695c3f4acb7f74e3a3b538e76672868"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0804d04564e70862ef807f3e1ace2cc212ef0e22deb1b3d6f80c45e5882c6b"}, + {file = "simplejson-3.20.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:979ce23ea663895ae39106946ef3d78527822d918a136dbc77b9e2b7f006237e"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a2ba921b047bb029805726800819675249ef25d2f65fd0edb90639c5b1c3033c"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:12d3d4dc33770069b780cc8f5abef909fe4a3f071f18f55f6d896a370fd0f970"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aff032a59a201b3683a34be1169e71ddda683d9c3b43b261599c12055349251e"}, + {file = "simplejson-3.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:30e590e133b06773f0dc9c3f82e567463df40598b660b5adf53eb1c488202544"}, + {file = "simplejson-3.20.2-cp312-cp312-win32.whl", hash = "sha256:8d7be7c99939cc58e7c5bcf6bb52a842a58e6c65e1e9cdd2a94b697b24cddb54"}, + {file = "simplejson-3.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:2c0b4a67e75b945489052af6590e7dca0ed473ead5d0f3aad61fa584afe814ab"}, + {file = "simplejson-3.20.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90d311ba8fcd733a3677e0be21804827226a57144130ba01c3c6a325e887dd86"}, + {file = "simplejson-3.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:feed6806f614bdf7f5cb6d0123cb0c1c5f40407ef103aa935cffaa694e2e0c74"}, + {file = "simplejson-3.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6b1d8d7c3e1a205c49e1aee6ba907dcb8ccea83651e6c3e2cb2062f1e52b0726"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552f55745044a24c3cb7ec67e54234be56d5d6d0e054f2e4cf4fb3e297429be5"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2da97ac65165d66b0570c9e545786f0ac7b5de5854d3711a16cacbcaa8c472d"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f59a12966daa356bf68927fca5a67bebac0033cd18b96de9c2d426cd11756cd0"}, + {file = "simplejson-3.20.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133ae2098a8e162c71da97cdab1f383afdd91373b7ff5fe65169b04167da976b"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7977640af7b7d5e6a852d26622057d428706a550f7f5083e7c4dd010a84d941f"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b530ad6d55e71fa9e93e1109cf8182f427a6355848a4ffa09f69cc44e1512522"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bd96a7d981bf64f0e42345584768da4435c05b24fd3c364663f5fbc8fabf82e3"}, + {file = "simplejson-3.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f28ee755fadb426ba2e464d6fcf25d3f152a05eb6b38e0b4f790352f5540c769"}, + {file = "simplejson-3.20.2-cp313-cp313-win32.whl", hash = "sha256:472785b52e48e3eed9b78b95e26a256f59bb1ee38339be3075dad799e2e1e661"}, + {file = "simplejson-3.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:a1a85013eb33e4820286139540accbe2c98d2da894b2dcefd280209db508e608"}, + {file = "simplejson-3.20.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a135941a50795c934bdc9acc74e172b126e3694fe26de3c0c1bc0b33ea17e6ce"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ba488decb18738f5d6bd082018409689ed8e74bc6c4d33a0b81af6edf1c9f4"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d81f8e982923d5e9841622ff6568be89756428f98a82c16e4158ac32b92a3787"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cdad497ccb1edc5020bef209e9c3e062a923e8e6fca5b8a39f0fb34380c8a66c"}, + {file = "simplejson-3.20.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a3f1db97bcd9fb592928159af7a405b18df7e847cbcc5682a209c5b2ad5d6b1"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:215b65b0dc2c432ab79c430aa4f1e595f37b07a83c1e4c4928d7e22e6b49a748"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:ece4863171ba53f086a3bfd87f02ec3d6abc586f413babfc6cf4de4d84894620"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_ppc64le.whl", hash = "sha256:4a76d7c47d959afe6c41c88005f3041f583a4b9a1783cf341887a3628a77baa0"}, + {file = "simplejson-3.20.2-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:e9b0523582a57d9ea74f83ecefdffe18b2b0a907df1a9cef06955883341930d8"}, + {file = "simplejson-3.20.2-cp36-cp36m-win32.whl", hash = "sha256:16366591c8e08a4ac76b81d76a3fc97bf2bcc234c9c097b48d32ea6bfe2be2fe"}, + {file = "simplejson-3.20.2-cp36-cp36m-win_amd64.whl", hash = "sha256:732cf4c4ac1a258b4e9334e1e40a38303689f432497d3caeb491428b7547e782"}, + {file = "simplejson-3.20.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6c3a98e21e5f098e4f982ef302ebb1e681ff16a5d530cfce36296bea58fe2396"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10cf9ca1363dc3711c72f4ec7c1caed2bbd9aaa29a8d9122e31106022dc175c6"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:106762f8aedf3fc3364649bfe8dc9a40bf5104f872a4d2d86bae001b1af30d30"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b21659898b7496322e99674739193f81052e588afa8b31b6a1c7733d8829b925"}, + {file = "simplejson-3.20.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78fa1db6a02bca88829f2b2057c76a1d2dc2fccb8c5ff1199e352f213e9ec719"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:156139d94b660448ec8a4ea89f77ec476597f752c2ff66432d3656704c66b40e"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:b2620ac40be04dff08854baf6f4df10272f67079f61ed1b6274c0e840f2e2ae1"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:9ccef5b5d3e3ac5d9da0a0ca1d2de8cf2b0fb56b06aa0ab79325fa4bcc5a1d60"}, + {file = "simplejson-3.20.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:f526304c2cc9fd8b8d18afacb75bc171650f83a7097b2c92ad6a431b5d7c1b72"}, + {file = "simplejson-3.20.2-cp37-cp37m-win32.whl", hash = "sha256:e0f661105398121dd48d9987a2a8f7825b8297b3b2a7fe5b0d247370396119d5"}, + {file = "simplejson-3.20.2-cp37-cp37m-win_amd64.whl", hash = "sha256:dab98625b3d6821e77ea59c4d0e71059f8063825a0885b50ed410e5c8bd5cb66"}, + {file = "simplejson-3.20.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b8205f113082e7d8f667d6cd37d019a7ee5ef30b48463f9de48e1853726c6127"}, + {file = "simplejson-3.20.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fc8da64929ef0ff16448b602394a76fd9968a39afff0692e5ab53669df1f047f"}, + {file = "simplejson-3.20.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfe704864b5fead4f21c8d448a89ee101c9b0fc92a5f40b674111da9272b3a90"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40ca7cbe7d2f423b97ed4e70989ef357f027a7e487606628c11b79667639dc84"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cec1868b237fe9fb2d466d6ce0c7b772e005aadeeda582d867f6f1ec9710cad"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:792debfba68d8dd61085ffb332d72b9f5b38269cda0c99f92c7a054382f55246"}, + {file = "simplejson-3.20.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e022b2c4c54cb4855e555f64aa3377e3e5ca912c372fa9e3edcc90ebbad93dce"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:5de26f11d5aca575d3825dddc65f69fdcba18f6ca2b4db5cef16f41f969cef15"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:e2162b2a43614727ec3df75baeda8881ab129824aa1b49410d4b6c64f55a45b4"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e11a1d6b2f7e72ca546bdb4e6374b237ebae9220e764051b867111df83acbd13"}, + {file = "simplejson-3.20.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:daf7cd18fe99eb427fa6ddb6b437cfde65125a96dc27b93a8969b6fe90a1dbea"}, + {file = "simplejson-3.20.2-cp38-cp38-win32.whl", hash = "sha256:da795ea5f440052f4f497b496010e2c4e05940d449ea7b5c417794ec1be55d01"}, + {file = "simplejson-3.20.2-cp38-cp38-win_amd64.whl", hash = "sha256:6a4b5e7864f952fcce4244a70166797d7b8fd6069b4286d3e8403c14b88656b6"}, + {file = "simplejson-3.20.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b3bf76512ccb07d47944ebdca44c65b781612d38b9098566b4bb40f713fc4047"}, + {file = "simplejson-3.20.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:214e26acf2dfb9ff3314e65c4e168a6b125bced0e2d99a65ea7b0f169db1e562"}, + {file = "simplejson-3.20.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2fb1259ca9c385b0395bad59cdbf79535a5a84fb1988f339a49bfbc57455a35a"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34e028a2ba8553a208ded1da5fa8501833875078c4c00a50dffc33622057881"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b538f9d9e503b0dd43af60496780cb50755e4d8e5b34e5647b887675c1ae9fee"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab998e416ded6c58f549a22b6a8847e75a9e1ef98eb9fbb2863e1f9e61a4105b"}, + {file = "simplejson-3.20.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a8f1c307edf5fbf0c6db3396c5d3471409c4a40c7a2a466fbc762f20d46601a"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5a7bbac80bdb82a44303f5630baee140aee208e5a4618e8b9fde3fc400a42671"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:5ef70ec8fe1569872e5a3e4720c1e1dcb823879a3c78bc02589eb88fab920b1f"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:cb11c09c99253a74c36925d461c86ea25f0140f3b98ff678322734ddc0f038d7"}, + {file = "simplejson-3.20.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:66f7c78c6ef776f8bd9afaad455e88b8197a51e95617bcc44b50dd974a7825ba"}, + {file = "simplejson-3.20.2-cp39-cp39-win32.whl", hash = "sha256:619ada86bfe3a5aa02b8222ca6bfc5aa3e1075c1fb5b3263d24ba579382df472"}, + {file = "simplejson-3.20.2-cp39-cp39-win_amd64.whl", hash = "sha256:44a6235e09ca5cc41aa5870a952489c06aa4aee3361ae46daa947d8398e57502"}, + {file = "simplejson-3.20.2-py3-none-any.whl", hash = "sha256:3b6bb7fb96efd673eac2e4235200bfffdc2353ad12c54117e1e4e2fc485ac017"}, + {file = "simplejson-3.20.2.tar.gz", hash = "sha256:5fe7a6ce14d1c300d80d08695b7f7e633de6cd72c80644021874d985b3393649"}, +] + [[package]] name = "six" version = "1.17.0" diff --git a/pyproject.toml b/pyproject.toml index 350bc58..f3f4145 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,8 @@ dependencies = [ dev = [ "pytest (>=8.4.2,<9.0.0)", "dash[testing] (>=2.16,<3.0)", - "pyperf (>=2.9.0,<3.0.0)" + "pyperf (>=2.9.0,<3.0.0)", + "kaleido (>=1.2.0,<2.0.0)" ] From 43f4d8a7ac374ba7bda6c1f7338f7d4f1fdb82eb Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 11 Nov 2025 23:07:04 +0100 Subject: [PATCH 066/152] write html as fallback --- model/distributions/sphere/watson/benchmark_fib_starts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index b1c4aa3..6b84be0 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -67,6 +67,8 @@ def plot_benches(results, title, x_label): except Exception as e: print("Generating plot failed, dumping data:", e) print(results.items()) + print("Trying to save html as fallback") + fig.write_html(f"{title.replace(' ', '_').replace(':', '')}.html", include_plotlyjs="cdn", full_html=True) From 853cdb9a1756a1c8fdf464fe2fdad1b52db072ec Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 13 Nov 2025 09:35:21 +0100 Subject: [PATCH 067/152] choose better strat based on benchmarks --- model/distributions/sphere/watson/fibonachi.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/model/distributions/sphere/watson/fibonachi.py b/model/distributions/sphere/watson/fibonachi.py index 325cf23..664f510 100644 --- a/model/distributions/sphere/watson/fibonachi.py +++ b/model/distributions/sphere/watson/fibonachi.py @@ -24,8 +24,13 @@ def get_name(self): def sample(self, sample_options, distribution_options): - return self.sample_events(sample_options, distribution_options) - + kappa = distribution_options[0].state + if kappa < 0: + return self.sample_closed(sample_options, distribution_options) + elif kappa < 30: + return self.sample_inverse_ode(sample_options, distribution_options) + else: + return self.sample_closed(sample_options, distribution_options) def sample_inverse_interpolation(self, sample_options, distribution_options): kappa = distribution_options[0].state sample_count = sample_options[0].state From 1ddcc9e40015668202c3b6c93de428649fdbf9b1 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 13 Nov 2025 09:52:02 +0100 Subject: [PATCH 068/152] fix error due to orjson being pulled in --- util/selectors/silder_log.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/util/selectors/silder_log.py b/util/selectors/silder_log.py index 3f5dcc5..f58bcbf 100644 --- a/util/selectors/silder_log.py +++ b/util/selectors/silder_log.py @@ -31,8 +31,10 @@ def calculate_marks(self): if position % 1 == 0: # it doesnt like float keys for "integer" values position = int(position) + else: + position = float(position) - value = self.transfrom_up(position) + value = int(self.transfrom_up(position)) if float(self.transfrom_up(position)).is_integer() else float(self.transfrom_up(position)) marks[position] = f"{value:.6g}" # 6 significant digits return marks From 710b1c83cd1adb89271c06176a2e4657dd127a04 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 13 Nov 2025 10:37:47 +0100 Subject: [PATCH 069/152] increasy accuracy of the inverse interpolation method --- model/distributions/sphere/watson/fibonachi.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/model/distributions/sphere/watson/fibonachi.py b/model/distributions/sphere/watson/fibonachi.py index 664f510..170b24c 100644 --- a/model/distributions/sphere/watson/fibonachi.py +++ b/model/distributions/sphere/watson/fibonachi.py @@ -30,7 +30,7 @@ def sample(self, sample_options, distribution_options): elif kappa < 30: return self.sample_inverse_ode(sample_options, distribution_options) else: - return self.sample_closed(sample_options, distribution_options) + return self.sample_inverse_interpolation(sample_options, distribution_options) def sample_inverse_interpolation(self, sample_options, distribution_options): kappa = distribution_options[0].state sample_count = sample_options[0].state @@ -61,7 +61,7 @@ def f(t,y): t_span = (0, np.pi) # theta from 0 to pi y0 = 0 # the value of the integrated pdf at 0 is 0 - sol = scipy.integrate.solve_ivp(f, t_span, [y0]) + sol = scipy.integrate.solve_ivp(f, t_span, [y0], rtol=1e-9, atol=1e-12) x = sol.t y = sol.y[0] @@ -69,13 +69,10 @@ def f(t,y): # due to numerical issues, for large kappa and samplecount, y can be slightly non monotonic # monotonicity is needed for interpolation, so maximum.accumulate then bump by eps - y = np.maximum.accumulate(y) - diffs = np.diff(y) - mask = diffs <= 0 - if np.any(mask): - eps = 1e-10 - y = y + eps * np.arange(len(x)) - y = np.maximum.accumulate(y) + y = np.maximum.accumulate(y) + y /= y[-1] # normalize to [0,1] + eps = 1e-14 + y += eps * np.arange(len(y)) # now interpolate, but we swamp x and y so whe get the inverse function # this works because the function is monotonic From 3711bc4fd94895626a1419ba0a6012721982f70f Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 13 Nov 2025 10:48:49 +0100 Subject: [PATCH 070/152] add how to tune before benchmark --- model/distributions/sphere/watson/benchmark_fib_starts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index 6b84be0..429d278 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -1,5 +1,7 @@ ''' Run this file directly from project root with: + +sudo "$(poetry run which python)" -m pyperf system tune PYTHONPATH=$PWD poetry run python model/distributions/sphere/watson/benchmark_fib_starts.py ''' From 13cd7c0878639e2101067b1b475c4b4470b3412f Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 13 Nov 2025 10:55:10 +0100 Subject: [PATCH 071/152] adjust titles --- model/distributions/sphere/watson/benchmark_fib_starts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index 429d278..29abbd2 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -81,8 +81,8 @@ def plot_benches(results, title, x_label): mult_samples_10 = bench_multiple_sample_counts(10) if not runner.args.worker: - plot_benches(mult_kappa, "Watson Fibonacci Sampling Benchmark: time taken for various kappa values (10000 samples)", "kappa") - plot_benches(mult_samples_neg_10, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=-10)", "sample_count") - plot_benches(mult_samples_10, "Watson Fibonacci Sampling Benchmark: time taken for various sample counts (kappa=10)", "sample_count") + plot_benches(mult_kappa, "time taken for various kappa values (10000 samples)", "kappa") + plot_benches(mult_samples_neg_10, "time taken for various sample counts (kappa=-10)", "sample_count") + plot_benches(mult_samples_10, "time taken for various sample counts (kappa=10)", "sample_count") \ No newline at end of file From 5e2f41f69e698b82d71273f890595107bbfcb8c5 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 13 Nov 2025 11:13:05 +0100 Subject: [PATCH 072/152] also add in corners for 2d periodicity --- renderer/Object3DAnd2DRenderer.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index 270f947..875d0f5 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -197,7 +197,25 @@ def update_plot_sample_2d(self, values_dist, ids_dist, values_samp, ids_samp, se if self.per_y: ext_x = np.concatenate([ext_x, tp[:,1], tp[:,1]]) ext_y = np.concatenate([ext_y, tp[:,0] - self.pery_y_amount, tp[:,0] + self.pery_y_amount]) - + + if self.per_x and self.per_y: # all four corners + ext_x = np.concatenate([ + ext_x, + tp[:,1] + self.perx_x_amount, + tp[:,1] + self.perx_x_amount, + tp[:,1] - self.perx_x_amount, + tp[:,1] - self.perx_x_amount + ]) + + ext_y = np.concatenate([ + ext_y, + tp[:,0] + self.pery_y_amount, + tp[:,0] - self.pery_y_amount, + tp[:,0] + self.pery_y_amount, + tp[:,0] - self.pery_y_amount + ]) + + if self.per_x or self.per_y: patched_figure["data"][1].x = ext_x patched_figure["data"][1].y = ext_y From 40e3dec33bf5866b5a407ccb97e1e8617bf949c8 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 13 Nov 2025 12:04:08 +0100 Subject: [PATCH 073/152] fix avoid importing the benchmarks automaticaly --- model/distributions/distribution_loader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/model/distributions/distribution_loader.py b/model/distributions/distribution_loader.py index 8b3aef8..355eb05 100644 --- a/model/distributions/distribution_loader.py +++ b/model/distributions/distribution_loader.py @@ -23,6 +23,8 @@ def load_distributions(self): raise ValueError(f"'{self.distribution_package}' is not a package (missing __path__).") for finder, name, ispkg in pkgutil.walk_packages(pkg.__path__, prefix=pkg.__name__ + "."): + if "benchmark" in name: + continue module = importlib.import_module(name) for _, obj in inspect.getmembers(module, inspect.isclass): From 97415675214f4f86201ebb226223f11f659816b9 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 17 Nov 2025 12:41:10 +0100 Subject: [PATCH 074/152] introduce cylinder manifold --- model/cylinder/__init__.py | 0 model/cylinder/cylinder.py | 66 +++++++++++++++++++ .../cylinder/cylinder_distribution.py | 24 +++++++ .../cylinder/cylinder_sampling_schema.py | 16 +++++ .../cylinder/uniform/__init__.py | 0 .../distributions/cylinder/uniform/random.py | 23 +++++++ .../distributions/cylinder/uniform/uniform.py | 19 ++++++ model/manifold.py | 20 +++++- model/torus/torus.py | 2 + pages/cylinder.py | 30 +++++++++ renderer/Object3DAnd2DRenderer.py | 4 +- renderer/Object3DRenderer.py | 4 ++ renderer/PlotSettings2d.py | 3 + 13 files changed, 206 insertions(+), 5 deletions(-) create mode 100644 model/cylinder/__init__.py create mode 100644 model/cylinder/cylinder.py create mode 100644 model/distributions/cylinder/cylinder_distribution.py create mode 100644 model/distributions/cylinder/cylinder_sampling_schema.py create mode 100644 model/distributions/cylinder/uniform/__init__.py create mode 100644 model/distributions/cylinder/uniform/random.py create mode 100644 model/distributions/cylinder/uniform/uniform.py create mode 100644 pages/cylinder.py diff --git a/model/cylinder/__init__.py b/model/cylinder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/cylinder/cylinder.py b/model/cylinder/cylinder.py new file mode 100644 index 0000000..2d322f7 --- /dev/null +++ b/model/cylinder/cylinder.py @@ -0,0 +1,66 @@ +import numpy as np +from model.distributions.distribution_loader import DistributionLoader +from model.distributions.torus.torus_distribution import TorusDistribution +from model.manifold import Manifold +from renderer.PlotSettings2d import PlotSettings2D + +class Cylinder(Manifold): + def __init__(self, resolution=100, r=1): + self.xyz = self.generate_xyz(resolution, r) + self.mesh = np.array([]) + self.samples = np.array([]) + self.samples_2d = np.array([]) + self.distributions = DistributionLoader(TorusDistribution, "model.distributions.torus").get_distributions() + + self.r = r + + axes_2d = ( + np.arange(0, 2.5 * np.pi, np.pi / 2), # 0, π/2, π, 3π/2, 2π + ["0", "π/2", "π", "3π/2", "2π"] + ) + self.plot_settings_2d = PlotSettings2D( + axes_2d_x=axes_2d, + axes_2d_y=axes_2d, + lock_aspect_ratio=False, + periodic_x=True, + periodic_y=False, + periodic_x_amount=2 * np.pi, + x_title="p", + y_title="z", + ) + + self.camera_settings_3d = dict( + eye=dict(x=2, y=2, z=2), + center=dict(x=0, y=0, z=0), + ) + + def generate_xyz(self, resolution=50, r=1): + p = np.linspace(0, 2*np.pi, resolution) + z = np.linspace(0, 2*np.pi, resolution) + t,p = np.meshgrid(p,z) + + + return self.t_p_to_xyz(t, p, r) + + def update_sample(self, selected_distribution, selected_sampling_method, sample_options, distribution_options): + dist = self.distributions[selected_distribution] + sampling_method = dist.sampling_method_dict[selected_sampling_method] + new_sample = sampling_method.sample(sample_options, distribution_options) + + if new_sample.size == 0: + self.samples = np.empty((0, 3), dtype=float) + return + + x, y, z = self.t_p_to_xyz(new_sample[:,0], new_sample[:,1], self.r) + + self.samples = np.column_stack((x, y, z)) + self.samples_2d = new_sample + + + @staticmethod + def t_p_to_xyz(p, z, r=1): + x = r * np.cos(p) + y = r * np.sin(p) + z = r * z + + return x, y, z diff --git a/model/distributions/cylinder/cylinder_distribution.py b/model/distributions/cylinder/cylinder_distribution.py new file mode 100644 index 0000000..c99b5cf --- /dev/null +++ b/model/distributions/cylinder/cylinder_distribution.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from functools import cached_property + +class CylinderDistribution(ABC): + def __init__(self): + self.distribution_options = [] + # a list of objects that implement the cylinder_sampling_schema interface + # that can be used with this distribution + self.sampling_methods = [] + + + # Returns the name of the distribution + @abstractmethod + def get_name(self): + pass + + @cached_property + def sampling_method_dict(self): + return {m.get_name(): m for m in self.sampling_methods} + + # returns a functions that takes a (N, 3) shape nparray and returns the pdf value at the points + @abstractmethod + def get_pdf(self, distribution_options): + pass \ No newline at end of file diff --git a/model/distributions/cylinder/cylinder_sampling_schema.py b/model/distributions/cylinder/cylinder_sampling_schema.py new file mode 100644 index 0000000..8dea18e --- /dev/null +++ b/model/distributions/cylinder/cylinder_sampling_schema.py @@ -0,0 +1,16 @@ +from abc import ABC, abstractmethod + +class CylinderSamplingSchema(ABC): + def __init__(self): + self.sample_options = [] + + # Returns the name of the sampling method + @abstractmethod + def get_name(self): + pass + + # returns samples as a numpy array of shape (n, 2) + # where (2) are the parameters (p, z) (angle, height) on the torus + @abstractmethod + def sample(self, sample_options, distribution_options): + pass \ No newline at end of file diff --git a/model/distributions/cylinder/uniform/__init__.py b/model/distributions/cylinder/uniform/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/cylinder/uniform/random.py b/model/distributions/cylinder/uniform/random.py new file mode 100644 index 0000000..643f578 --- /dev/null +++ b/model/distributions/cylinder/uniform/random.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractmethod +from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema +from util.selectors.silder_log import LogSlider +import numpy as np + +class CylinderRandomUniformSampling(CylinderSamplingSchema): + def __init__(self): + self.sample_options = [ + LogSlider("Number of Samples", 10, 100, 10000) + ] + + def get_name(self): + return "Random" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + t = np.random.uniform(0, 2 * np.pi, sample_count) + p = np.random.uniform(0, 2 * np.pi, sample_count) + + samples = np.column_stack((t, p)) + + return samples \ No newline at end of file diff --git a/model/distributions/cylinder/uniform/uniform.py b/model/distributions/cylinder/uniform/uniform.py new file mode 100644 index 0000000..ddd15f4 --- /dev/null +++ b/model/distributions/cylinder/uniform/uniform.py @@ -0,0 +1,19 @@ +from abc import ABC, abstractmethod +from util.selectors.slider import Slider +import numpy as np + +from model.distributions.cylinder.cylinder_distribution import CylinderDistribution +from model.distributions.cylinder.uniform.random import CylinderRandomUniformSampling + +class UniformCylinderDistribution(CylinderDistribution): + def __init__(self): + self.distribution_options = [] + self.sampling_methods = [ + CylinderRandomUniformSampling(), + ] + + def get_name(self): + return "Uniform" + + def get_pdf(self, distribution_options): + pass \ No newline at end of file diff --git a/model/manifold.py b/model/manifold.py index 434139f..57ff177 100644 --- a/model/manifold.py +++ b/model/manifold.py @@ -1,9 +1,23 @@ from abc import ABC, abstractmethod class Manifold(ABC): - def __init__(self): - # optional, setting for if the manifold supports 2d plotting - self.plot_settings_2d = None + # optional initial settings for 3d camera + @property + def camera_settings_3d(self): + return getattr(self, "_camera_settings_3d", None) + + @camera_settings_3d.setter + def camera_settings_3d(self, value): + self._camera_settings_3d = value + + # optional, setting for if the manifold supports 2d plotting + @property + def plot_settings_2d(self): + return getattr(self, "_plot_settings_2d", None) + + @plot_settings_2d.setter + def plot_settings_2d(self, value): + self._plot_settings_2d = value # generates renderable xyz grid @abstractmethod diff --git a/model/torus/torus.py b/model/torus/torus.py index 615dd93..af58792 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -27,6 +27,8 @@ def __init__(self, resolution=100, r=1, R=3): periodic_y=True, periodic_x_amount=2 * np.pi, periodic_y_amount=2 * np.pi, + x_title="t", + y_title="p", ) def generate_xyz(self, resolution=50, r=1, R=3): diff --git a/pages/cylinder.py b/pages/cylinder.py new file mode 100644 index 0000000..dde3d1d --- /dev/null +++ b/pages/cylinder.py @@ -0,0 +1,30 @@ +import plotly +import dash +from dash import html, dcc, callback, Input, Output, ALL +from dash_resizable_panels import PanelGroup, Panel, PanelResizeHandle +import dash_bootstrap_components as dbc +import plotly.graph_objects as go +import numpy as np + +from components.split_pane import SplitPane +from model.cylinder.cylinder import Cylinder +from renderer.Object3DAnd2DRenderer import Object3DAnd2DRenderer + + +dash.register_page(__name__) + +cylinder = Cylinder() + +renderer = Object3DAnd2DRenderer(cylinder, "cylinder") +options, graph = renderer.get_layout_components() + +layout = SplitPane( + [ + *options + ], + [ + *graph + ], + 30 + +) \ No newline at end of file diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index 875d0f5..f8e0f76 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -58,14 +58,14 @@ def __init__(self, object, id): if object.plot_settings_2d is not None: self.fig_2d.update_xaxes( - title_text="t", + title_text=object.plot_settings_2d.x_title, tickmode="array", tickvals=object.plot_settings_2d.axes_2d_x[0], ticktext=object.plot_settings_2d.axes_2d_x[1], zeroline=False, ) self.fig_2d.update_yaxes( - title_text="p", + title_text=object.plot_settings_2d.y_title, tickmode="array", tickvals=object.plot_settings_2d.axes_2d_y[0], ticktext=object.plot_settings_2d.axes_2d_y[1], diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 596b27b..27a4632 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -72,6 +72,10 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): xanchor="right", x=0.1, )) + + if (self.object.camera_settings_3d is not None): + self.fig.update_layout(scene=dict(camera=self.object.camera_settings_3d)) + self._register_callbacks() if register_3d_callbacks: self._register_3d_plot_callbacks() diff --git a/renderer/PlotSettings2d.py b/renderer/PlotSettings2d.py index 3eb4c37..428bfba 100644 --- a/renderer/PlotSettings2d.py +++ b/renderer/PlotSettings2d.py @@ -10,3 +10,6 @@ class PlotSettings2D: periodic_y: bool = False periodic_x_amount: float = 0.0 # amount to shift for periodicity periodic_y_amount: float = 0.0 + + x_title: str = "" # title for axis + y_title: str = "" From f76d95de0ed78f51fbcf0eb9a54586ee1d63d583 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 17 Nov 2025 14:49:36 +0100 Subject: [PATCH 075/152] added fibonacci kronecker to cylinder --- model/cylinder/cylinder.py | 4 +-- .../cylinder/uniform/fibonacci_kronecker.py | 27 +++++++++++++++++++ .../distributions/cylinder/uniform/uniform.py | 2 ++ .../sphere/uniform/fibonachi_lattice.py | 2 +- 4 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 model/distributions/cylinder/uniform/fibonacci_kronecker.py diff --git a/model/cylinder/cylinder.py b/model/cylinder/cylinder.py index 2d322f7..b73edbc 100644 --- a/model/cylinder/cylinder.py +++ b/model/cylinder/cylinder.py @@ -1,6 +1,6 @@ import numpy as np from model.distributions.distribution_loader import DistributionLoader -from model.distributions.torus.torus_distribution import TorusDistribution +from model.distributions.cylinder.cylinder_distribution import CylinderDistribution from model.manifold import Manifold from renderer.PlotSettings2d import PlotSettings2D @@ -10,7 +10,7 @@ def __init__(self, resolution=100, r=1): self.mesh = np.array([]) self.samples = np.array([]) self.samples_2d = np.array([]) - self.distributions = DistributionLoader(TorusDistribution, "model.distributions.torus").get_distributions() + self.distributions = DistributionLoader(CylinderDistribution, "model.distributions.cylinder").get_distributions() self.r = r diff --git a/model/distributions/cylinder/uniform/fibonacci_kronecker.py b/model/distributions/cylinder/uniform/fibonacci_kronecker.py new file mode 100644 index 0000000..990e391 --- /dev/null +++ b/model/distributions/cylinder/uniform/fibonacci_kronecker.py @@ -0,0 +1,27 @@ +from abc import ABC, abstractmethod +from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema +from util.selectors.silder_log import LogSlider +import numpy as np + +class CylinderFibUniformSampling(CylinderSamplingSchema): + def __init__(self): + self.sample_options = [ + LogSlider("Number of Samples", 10, 100, 10000) + ] + + def get_name(self): + return "Fibonacci-Kronecker Lattice" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + indices = np.arange(0, sample_count) + gol = (1+5**0.5)/2 + + # centered rank-1 lattice generator + equidistant_generator = (2 * indices + 1) / (2 * sample_count) + + z = equidistant_generator + p = (indices / gol) % 1 + + return np.column_stack((p * 2 * np.pi, z * 2 * np.pi)) \ No newline at end of file diff --git a/model/distributions/cylinder/uniform/uniform.py b/model/distributions/cylinder/uniform/uniform.py index ddd15f4..3f6eab7 100644 --- a/model/distributions/cylinder/uniform/uniform.py +++ b/model/distributions/cylinder/uniform/uniform.py @@ -4,12 +4,14 @@ from model.distributions.cylinder.cylinder_distribution import CylinderDistribution from model.distributions.cylinder.uniform.random import CylinderRandomUniformSampling +from model.distributions.cylinder.uniform.fibonacci_kronecker import CylinderFibUniformSampling class UniformCylinderDistribution(CylinderDistribution): def __init__(self): self.distribution_options = [] self.sampling_methods = [ CylinderRandomUniformSampling(), + CylinderFibUniformSampling(), ] def get_name(self): diff --git a/model/distributions/sphere/uniform/fibonachi_lattice.py b/model/distributions/sphere/uniform/fibonachi_lattice.py index b0264c4..0f53060 100644 --- a/model/distributions/sphere/uniform/fibonachi_lattice.py +++ b/model/distributions/sphere/uniform/fibonachi_lattice.py @@ -12,7 +12,7 @@ def __init__(self): ] def get_name(self): - return "Fibonacci Lattice" + return "Fibonacci-Kronecker Lattice" def sample(self, sample_options, distribution_options): sample_count = sample_options[0].state From 922181860179e0d82344ca7b9773dba303bd3c87 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 17 Nov 2025 16:18:18 +0100 Subject: [PATCH 076/152] added fibonacci rank 1 lattice --- assets/tooltip.js | 9 ++++ .../cylinder/uniform/fibonacci_rank_1.py | 35 ++++++++++++ .../distributions/cylinder/uniform/uniform.py | 3 +- .../sphere/uniform/fibonacci_rank_1.py | 30 +++++++++++ model/distributions/sphere/uniform/uniform.py | 5 +- .../torus/uniform/fibonacci_rank_1.py | 25 +++++++++ model/distributions/torus/uniform/uniform.py | 2 + poetry.lock | 2 +- pyproject.toml | 1 + util/selectors/slider_fib.py | 54 +++++++++++++++++++ 10 files changed, 162 insertions(+), 4 deletions(-) create mode 100644 model/distributions/cylinder/uniform/fibonacci_rank_1.py create mode 100644 model/distributions/sphere/uniform/fibonacci_rank_1.py create mode 100644 model/distributions/torus/uniform/fibonacci_rank_1.py create mode 100644 util/selectors/slider_fib.py diff --git a/assets/tooltip.js b/assets/tooltip.js index 0c1dded..4bfe989 100644 --- a/assets/tooltip.js +++ b/assets/tooltip.js @@ -19,4 +19,13 @@ window.dccFunctions.transform_log_nice = function(value) { let nice_value = sign * Math.round(x / step) * step; nice_value = Number(nice_value.toFixed(4)); return nice_value; +} + +window.dccFunctions.transform_fib = function(value) { + function fibonacci(n) { + return n < 1 ? 0 + : n <= 2 ? 1 + : fibonacci(n - 1) + fibonacci(n - 2) + } + return fibonacci(value); } \ No newline at end of file diff --git a/model/distributions/cylinder/uniform/fibonacci_rank_1.py b/model/distributions/cylinder/uniform/fibonacci_rank_1.py new file mode 100644 index 0000000..7466571 --- /dev/null +++ b/model/distributions/cylinder/uniform/fibonacci_rank_1.py @@ -0,0 +1,35 @@ +from abc import ABC, abstractmethod +from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema +from util.selectors.slider_fib import SliderFib +import numpy as np +import sympy as sp + +class CylinderFibRank1UniformSampling(CylinderSamplingSchema): + def __init__(self): + self.sample_options = [ + SliderFib("Number of Samples", 2, 34, 21, 9) + ] + + def get_name(self): + return "Fibonacci-Rank-1 Lattice" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + k = sample_options[0].idx + + z, p = self.get_rank_1(sample_count, k) + + return np.column_stack((p * 2 * np.pi, z * 2 * np.pi)) + + @staticmethod + def get_rank_1(sample_count, k): + indices = np.arange(0, sample_count) + + # centered rank-1 lattice + F_k = int(sp.fibonacci(k - 1)) + F_k_p_1 = sample_count # int(sp.fibonacci(k)) + + z = (indices * (1/F_k_p_1) + (1/(2*F_k_p_1)) ) % 1 + p = (indices * (F_k/F_k_p_1) + (1/(2*F_k_p_1)) ) % 1 + + return z, p \ No newline at end of file diff --git a/model/distributions/cylinder/uniform/uniform.py b/model/distributions/cylinder/uniform/uniform.py index 3f6eab7..03fda98 100644 --- a/model/distributions/cylinder/uniform/uniform.py +++ b/model/distributions/cylinder/uniform/uniform.py @@ -5,13 +5,14 @@ from model.distributions.cylinder.cylinder_distribution import CylinderDistribution from model.distributions.cylinder.uniform.random import CylinderRandomUniformSampling from model.distributions.cylinder.uniform.fibonacci_kronecker import CylinderFibUniformSampling - +from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling class UniformCylinderDistribution(CylinderDistribution): def __init__(self): self.distribution_options = [] self.sampling_methods = [ CylinderRandomUniformSampling(), CylinderFibUniformSampling(), + CylinderFibRank1UniformSampling(), ] def get_name(self): diff --git a/model/distributions/sphere/uniform/fibonacci_rank_1.py b/model/distributions/sphere/uniform/fibonacci_rank_1.py new file mode 100644 index 0000000..c17314f --- /dev/null +++ b/model/distributions/sphere/uniform/fibonacci_rank_1.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod +import numpy as np + +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling +from util.selectors.slider_fib import SliderFib +from model.sphere.sphere import Sphere + +class SphereFibRank1UniformSampling(SphereSamplingSchema): + def __init__(self): + self.sample_options = [ + SliderFib("Number of Samples", 2, 34, 21, 9) + ] + self.sampler = CylinderFibRank1UniformSampling() + + def get_name(self): + return "Fibonacci-Rank-1 Lattice" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + t, p = self.sampler.get_rank_1(sample_count, sample_options[0].idx) + + z = 1 - 2 * t # uniform in [-1, 1] + phi = p * (2 * np.pi) + x = np.sqrt(1 - z**2) * np.cos(phi) + y = np.sqrt(1 - z**2) * np.sin(phi) + + + return np.column_stack((x, y, z)) \ No newline at end of file diff --git a/model/distributions/sphere/uniform/uniform.py b/model/distributions/sphere/uniform/uniform.py index d0cafdc..11f39cd 100644 --- a/model/distributions/sphere/uniform/uniform.py +++ b/model/distributions/sphere/uniform/uniform.py @@ -4,14 +4,15 @@ from model.distributions.sphere.sphere_distribution import SphereDistribution from model.distributions.sphere.uniform.random import SphereUniformRandomSampling from model.distributions.sphere.uniform.fibonachi_lattice import SphereUniformFibSampling - +from model.distributions.sphere.uniform.fibonacci_rank_1 import SphereFibRank1UniformSampling class SphereUniformDistribution(SphereDistribution): def __init__(self): self.distribution_options = [] self.sampling_methods = [ SphereUniformRandomSampling(), - SphereUniformFibSampling() + SphereUniformFibSampling(), + SphereFibRank1UniformSampling(), ] diff --git a/model/distributions/torus/uniform/fibonacci_rank_1.py b/model/distributions/torus/uniform/fibonacci_rank_1.py new file mode 100644 index 0000000..be36619 --- /dev/null +++ b/model/distributions/torus/uniform/fibonacci_rank_1.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod +import numpy as np + +from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling +from util.selectors.slider_fib import SliderFib +from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema + +class TorusFibRank1UniformSampling(TorusSamplingSchema): + def __init__(self): + self.sample_options = [ + SliderFib("Number of Samples", 2, 34, 21, 9) + ] + self.sampler = CylinderFibRank1UniformSampling() + + def get_name(self): + return "Fibonacci-Rank-1 Lattice" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + t, p = self.sampler.get_rank_1(sample_count, sample_options[0].idx) + + samples = np.column_stack((t * (2* np.pi), p * (2* np.pi))) + + return samples \ No newline at end of file diff --git a/model/distributions/torus/uniform/uniform.py b/model/distributions/torus/uniform/uniform.py index ca2c0de..70babb8 100644 --- a/model/distributions/torus/uniform/uniform.py +++ b/model/distributions/torus/uniform/uniform.py @@ -4,12 +4,14 @@ from model.distributions.torus.torus_distribution import TorusDistribution from model.distributions.torus.uniform.random import TorusRandomUniformSampling +from model.distributions.torus.uniform.fibonacci_rank_1 import TorusFibRank1UniformSampling class UniformTorusDistribution(TorusDistribution): def __init__(self): self.distribution_options = [] self.sampling_methods = [ TorusRandomUniformSampling(), + TorusFibRank1UniformSampling(), ] def get_name(self): diff --git a/poetry.lock b/poetry.lock index 103af62..2779dbd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3251,4 +3251,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.11" -content-hash = "969bc9727e129e907615d65c1b0a700e699ea9e1d965715241cd1d57e12e8b12" +content-hash = "b4e51cf951334efcb04908fd8b8e3e4137a37f7bc95f5c816ccbe5fe4dca95f0" diff --git a/pyproject.toml b/pyproject.toml index f3f4145..2b1d8df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "sphstat (>=1.0.6,<2.0.0)", "pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@002c7757e2a5a81e4e57da5c1b657c5f9bd5cf6e", "kent-distribution @ git+https://github.com/Vlad-Kor/kent_distribution", + "sympy (>=1.14.0,<2.0.0)", ] [dependency-groups] diff --git a/util/selectors/slider_fib.py b/util/selectors/slider_fib.py new file mode 100644 index 0000000..262f653 --- /dev/null +++ b/util/selectors/slider_fib.py @@ -0,0 +1,54 @@ +from dash import dcc, html +import numpy as np +from util.selectors.selector import Selector +import sympy as sp + + +SLIDER_MARK_AMOUNT = 5 +""" +Silder that only selects Fibonacci numbers within a given range. +min and max are the indices in the Fibonacci sequence. +state is not an index, but the actual Fibonacci number at a valid index. +""" +class SliderFib(Selector): + def __init__(self, name, min, state, max, idx): + self.name = name + self.min = min + self.state = state + self.idx = idx + self.max = max + + self.id = None + + def to_dash_component(self, _type, id): + self.id = id + return html.Div([ + html.Label(self.name), + + dcc.Slider( + id={"type": _type, "index": id}, + min=self.min, + max=self.max, + value=self.idx, + tooltip={"placement": "bottom", "always_visible": True, "transform": "transform_fib"}, + step=1, + marks=self.calculate_marks(), + updatemode="drag", + ) + ]) + + def calculate_marks(self): + + marks = {} + step = (self.max - self.min) / SLIDER_MARK_AMOUNT + for i in range(SLIDER_MARK_AMOUNT + 1): + value = int(self.min + i * step) + marks[value] = str(sp.fibonacci(value)) + return marks + + + def update_state(self, new_state): + self.state = int(sp.fibonacci(new_state)) + self.idx = int(new_state) + + \ No newline at end of file From 7ae856005dc6449a2359d7da5b57670ecea98336 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 17 Nov 2025 21:06:25 +0100 Subject: [PATCH 077/152] add mesh to cylinder --- model/cylinder/cylinder.py | 80 ++++++++++++++++++- .../distributions/cylinder/uniform/uniform.py | 6 +- model/manifold.py | 2 +- model/sphere/sphere.py | 1 - 4 files changed, 83 insertions(+), 6 deletions(-) diff --git a/model/cylinder/cylinder.py b/model/cylinder/cylinder.py index b73edbc..318e0af 100644 --- a/model/cylinder/cylinder.py +++ b/model/cylinder/cylinder.py @@ -1,8 +1,15 @@ import numpy as np +from scipy.spatial import ConvexHull +import plotly.figure_factory as ff +from scipy.spatial import Delaunay + + from model.distributions.distribution_loader import DistributionLoader from model.distributions.cylinder.cylinder_distribution import CylinderDistribution from model.manifold import Manifold from renderer.PlotSettings2d import PlotSettings2D +from model.distributions.cylinder.uniform.fibonacci_kronecker import CylinderFibUniformSampling +from util.selectors.slider import Slider class Cylinder(Manifold): def __init__(self, resolution=100, r=1): @@ -13,6 +20,7 @@ def __init__(self, resolution=100, r=1): self.distributions = DistributionLoader(CylinderDistribution, "model.distributions.cylinder").get_distributions() self.r = r + self.mesh_xyz = self._init_mesh() axes_2d = ( np.arange(0, 2.5 * np.pi, np.pi / 2), # 0, π/2, π, 3π/2, 2π @@ -40,7 +48,7 @@ def generate_xyz(self, resolution=50, r=1): t,p = np.meshgrid(p,z) - return self.t_p_to_xyz(t, p, r) + return self.p_z_to_xyz(t, p, r) def update_sample(self, selected_distribution, selected_sampling_method, sample_options, distribution_options): dist = self.distributions[selected_distribution] @@ -51,16 +59,82 @@ def update_sample(self, selected_distribution, selected_sampling_method, sample_ self.samples = np.empty((0, 3), dtype=float) return - x, y, z = self.t_p_to_xyz(new_sample[:,0], new_sample[:,1], self.r) + x, y, z = self.p_z_to_xyz(new_sample[:,0], new_sample[:,1], self.r) self.samples = np.column_stack((x, y, z)) self.samples_2d = new_sample @staticmethod - def t_p_to_xyz(p, z, r=1): + def p_z_to_xyz(p, z, r=1): x = r * np.cos(p) y = r * np.sin(p) z = r * z return x, y, z + + def generate_mesh(self, pdf, alpha=1): + # mesh_xyz has nans for line segments, mask before passing to pdf + mask = np.all(np.isfinite(self.mesh_xyz), axis=-1) + dens = pdf(self.mesh_xyz[mask]) # only extrude on p, dont change z + + xyz_extruded = np.full_like(self.mesh_xyz, np.nan, dtype=float) # full of nans + + xyz_extruded[..., :2][mask] = self.mesh_xyz[..., :2][mask] * (1 + alpha * dens[:, np.newaxis]) + xyz_extruded[..., 2][mask] = self.mesh_xyz[..., 2][mask] # z unchanged + return xyz_extruded[:,0], xyz_extruded[:,1], xyz_extruded[:,2] + + def _init_mesh(self, resolution=3000): + pz = CylinderFibUniformSampling.sample(None, [Slider("Number of Samples", 10, resolution, resolution)] , []) + pz[:, 1] = -0.1 + (pz[:, 1]) * 1.1 # extend slightly beyond [0, 2pi] so the top and bottom looks better + x, y, z = self.p_z_to_xyz(pz[:,0], pz[:,1], self.r) + + simplices = Delaunay(pz).simplices + ok_mask = (pz[:, 0] > np.pi / 4) & (pz[:, 0] < 7 * np.pi / 4) # ugly seam + ok_idx = np.where(ok_mask)[0] + + # throw away all triangles around the seam + good_vertices = np.isin(simplices, ok_idx) + good_triangles = good_vertices.any(axis=1) + simplices = simplices[good_triangles] + + + # shift p by pi and throw away triangles at other seam, that is at the opposite side + pz2 = np.copy(pz) + pz2[:,0] = (pz2[:,0]+np.pi) % (2 * np.pi) + + simplices_2 = Delaunay(pz2).simplices + ok_mask_2 = (pz2[:, 0] > np.pi / 4) & (pz2[:, 0] < 7 * np.pi / 4) # ugly seam + ok_idx_2 = np.where(ok_mask_2)[0] + + good_vertices_2 = np.isin(simplices_2, ok_idx_2) + good_triangles_2 = good_vertices_2.any(axis=1) + simplices_2 = simplices_2[good_triangles_2] + + # merge both halves + simplices_merged = np.vstack((simplices, simplices_2)) + simplices_merged = np.unique(simplices_merged, axis=0) + + # throw away points at bottom so they dont stretch across + eps = 1e-2 + ok_mask_no_across = (pz[:, 1] > 0 + eps) & (pz[:, 1] < (2 * np.pi - eps)) + ok_idx_no_across = np.where(ok_mask_no_across)[0] + good_vertices_no_across = np.isin(simplices_merged, ok_idx_no_across) + good_triangles_no_across = good_vertices_no_across.all(axis=1) + simplices_merged = simplices_merged[good_triangles_no_across] + + + + def cf(xi, yi, zi, zmin=np.min(z), zmax=np.max(z)): + if zi > zmax: + zi = np.nextafter(zmax, zmin) + return zi + + fig = ff.create_trisurf(x=x, y=y, z=z, + simplices=simplices_merged, + show_colorbar=False, + color_func=cf + ) + + arr = np.column_stack((fig.data[1].x, fig.data[1].y, fig.data[1].z)) + return np.where(arr == None, np.nan, arr).astype(float) # line segments have None in them, put them to nan so we can do mult later diff --git a/model/distributions/cylinder/uniform/uniform.py b/model/distributions/cylinder/uniform/uniform.py index 03fda98..c4c3887 100644 --- a/model/distributions/cylinder/uniform/uniform.py +++ b/model/distributions/cylinder/uniform/uniform.py @@ -19,4 +19,8 @@ def get_name(self): return "Uniform" def get_pdf(self, distribution_options): - pass \ No newline at end of file + scaling_factor = 5 # pdf plotted not to scale + def pdf(x): + N = np.shape(x)[0] + return (1 / (4 * np.pi * np.pi)) * np.ones(N) * scaling_factor + return pdf \ No newline at end of file diff --git a/model/manifold.py b/model/manifold.py index 57ff177..e7dbccf 100644 --- a/model/manifold.py +++ b/model/manifold.py @@ -30,5 +30,5 @@ def generate_xyz(self, *args, **kwargs): def update_sample(self, selected_distribution, sample_options): pass - def generate_trisurf(self, pdf, *args, **kwargs): + def generate_mesh(self, pdf, *args, **kwargs): pass \ No newline at end of file diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index 3513dea..a0c646a 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -1,6 +1,5 @@ import numpy as np import numpy as np -from scipy.spatial import Delaunay from scipy.spatial import ConvexHull import plotly.figure_factory as ff From 95bec1960cd50d1716298c4d94021816cf823301 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 18 Nov 2025 15:11:11 +0100 Subject: [PATCH 078/152] added mesh to torus --- model/distributions/torus/uniform/uniform.py | 6 +- model/torus/torus.py | 89 ++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) diff --git a/model/distributions/torus/uniform/uniform.py b/model/distributions/torus/uniform/uniform.py index 70babb8..a8a38c1 100644 --- a/model/distributions/torus/uniform/uniform.py +++ b/model/distributions/torus/uniform/uniform.py @@ -18,4 +18,8 @@ def get_name(self): return "Uniform" def get_pdf(self, distribution_options): - pass \ No newline at end of file + scaling_factor = 5 # pdf plotted not to scale + def pdf(x): + N = np.shape(x)[0] + return (1 / (4 * np.pi * np.pi)) * np.ones(N) * scaling_factor + return pdf \ No newline at end of file diff --git a/model/torus/torus.py b/model/torus/torus.py index af58792..2216e43 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -1,8 +1,13 @@ import numpy as np +import plotly.figure_factory as ff +from scipy.spatial import Delaunay + from model.distributions.distribution_loader import DistributionLoader from model.distributions.torus.torus_distribution import TorusDistribution from model.manifold import Manifold from renderer.PlotSettings2d import PlotSettings2D +from model.distributions.cylinder.uniform.fibonacci_kronecker import CylinderFibUniformSampling +from util.selectors.slider import Slider class Torus(Manifold): def __init__(self, resolution=100, r=1, R=3): @@ -15,6 +20,8 @@ def __init__(self, resolution=100, r=1, R=3): self.r = r self.R = R + self.mesh_xyz = self._init_mesh() + axes_2d = ( np.arange(0, 2.5 * np.pi, np.pi / 2), # 0, π/2, π, 3π/2, 2π ["0", "π/2", "π", "3π/2", "2π"] @@ -61,3 +68,85 @@ def t_p_to_xyz(t, p, r=1, R=3): z = r * np.sin(p) return x, y, z + + @staticmethod + def xyz_to_t_p(x, y, z, r=1, R=3): + t = np.arctan2(y, x) + p = np.arctan2(z, np.sqrt(x**2 + y**2) - R) + return t, p + + + + def generate_mesh(self, pdf, alpha=1): + #return self.mesh_xyz[:,0], self.mesh_xyz[:,1], self.mesh_xyz[:,2] + + # mesh_xyz has nans for line segments, mask before passing to pdf + mask = np.all(np.isfinite(self.mesh_xyz), axis=-1) + dens = pdf(self.mesh_xyz[mask]) + + xyz_extruded = np.full_like(self.mesh_xyz, np.nan, dtype=float) # full of nans + tp = self.xyz_to_t_p(self.mesh_xyz[mask][:,0], self.mesh_xyz[mask][:,1], self.mesh_xyz[mask][:,2], self.r, self.R) + xyz_new = self.t_p_to_xyz(tp[0], tp[1], self.r * (1 + alpha * dens), self.R) + xyz_extruded[mask] = np.stack(xyz_new, axis=1) + return xyz_extruded[:,0], xyz_extruded[:,1], xyz_extruded[:,2] + + + def _init_mesh(self, resolution=4000): + tp = CylinderFibUniformSampling.sample(None, [Slider("Number of Samples", 10, resolution, resolution)] , []) + x, y, z = self.t_p_to_xyz(tp[:,0], tp[:,1], self.r) + + + + def simplices_with_mask(_tp): + _simplices = Delaunay(_tp).simplices + + # only inner, avoid pi/4 border + ok_mask = (_tp[:, 0] > np.pi / 4) & (_tp[:, 0] < 7 * np.pi / 4) & (_tp[:, 1] > np.pi / 4) & (_tp[:, 1] < 7 * np.pi / 4) + ok_idx = np.where(ok_mask)[0] + + # throw away all triangles around the border + good_vertices = np.isin(_simplices, ok_idx) + good_triangles = good_vertices.any(axis=1) + _simplices = _simplices[good_triangles] + return _simplices + + + # shift p by pi/2 and cluster 4 quadrants with overlap + tp1 = np.copy(tp) + tp1[:,0] = (tp1[:,0]+(np.pi/2)) % (2 * np.pi) + tp1[:,1] = (tp1[:,1]+(np.pi/2)) % (2 * np.pi) + simplices_1 = simplices_with_mask(tp1) + + tp2 = np.copy(tp) + tp2[:,0] = (tp2[:,0]+(np.pi/2)) % (2 * np.pi) + tp2[:,1] = (tp2[:,1]-(np.pi/2)) % (2 * np.pi) + simplices_2 = simplices_with_mask(tp2) + + tp3 = np.copy(tp) + tp3[:,0] = (tp3[:,0]-(np.pi/2)) % (2 * np.pi) + tp3[:,1] = (tp3[:,1]-(np.pi/2)) % (2 * np.pi) + simplices_3 = simplices_with_mask(tp3) + + tp4 = np.copy(tp) + tp4[:,0] = (tp4[:,0]-(np.pi/2)) % (2 * np.pi) + tp4[:,1] = (tp4[:,1]+(np.pi/2)) % (2 * np.pi) + simplices_4 = simplices_with_mask(tp4) + + # merge all four quadrants + simplices_merged = np.vstack((simplices_1, simplices_2, simplices_3, simplices_4)) + simplices_merged = np.unique(simplices_merged, axis=0) + + def cf(xi, yi, zi, zmin=np.min(z), zmax=np.max(z)): + if zi > zmax: + zi = np.nextafter(zmax, zmin) + return zi + + fig = ff.create_trisurf(x=x, y=y, z=z, + simplices=simplices_merged, + show_colorbar=False, + color_func=cf + ) + + arr = np.column_stack((fig.data[1].x, fig.data[1].y, fig.data[1].z)) + return np.where(arr == None, np.nan, arr).astype(float) # line segments have None in them, put them to nan so we can do mult later + From 9a18162f2b0165763e38c934f024a06ffdd000d6 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 19 Nov 2025 15:36:51 +0100 Subject: [PATCH 079/152] fix initial marker size --- renderer/Object3DRenderer.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 27a4632..04a19bc 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -23,6 +23,12 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): 'scrollZoom': True, "modeBarButtonsToRemove": ["select2d", "lasso2d"], } + if self.object.samples.size: + sample_count = self.object.samples.shape[0] + marker_size = (10 * (sample_count / 100) ** (-0.35)) / self.device_pixel_ratio + marker_size = np.minimum(10,marker_size) + else: + marker_size = 4 self.fig = go.Figure( data=[ @@ -40,7 +46,7 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): z=self.object.samples[:, 2] if self.object.samples.size else [], mode="markers", marker=dict( - size=4, + size=marker_size, color="red", line=dict(width=1, color="black") ), From 5383d335ff70f730e9a88df912444f3e7f58f08b Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 20 Nov 2025 17:10:28 +0100 Subject: [PATCH 080/152] added the ability to dynamically render markdown --- model/distribution.py | 29 +++++++++++++++++++ .../cylinder/cylinder_distribution.py | 4 ++- .../cylinder/cylinder_sampling_schema.py | 4 +-- .../sphere/sphere_distribution.py | 3 +- .../sphere/sphere_sampling_schema.py | 3 +- .../distributions/torus/torus_distribution.py | 3 +- .../torus/torus_sampling_schema.py | 3 +- model/sampling_schema.py | 22 ++++++++++++++ renderer/Object3DAnd2DRenderer.py | 10 +++++++ renderer/Object3DRenderer.py | 25 +++++++++++++++- 10 files changed, 98 insertions(+), 8 deletions(-) create mode 100644 model/distribution.py create mode 100644 model/sampling_schema.py diff --git a/model/distribution.py b/model/distribution.py new file mode 100644 index 0000000..a02a2a8 --- /dev/null +++ b/model/distribution.py @@ -0,0 +1,29 @@ +from abc import ABC, abstractmethod +from functools import cached_property + +class Distribution(ABC): + def __init__(self): + self.distribution_options = [] + self.sampling_methods = [] + + + # Returns the name of the distribution + @abstractmethod + def get_name(self): + pass + + @cached_property + def sampling_method_dict(self): + return {m.get_name(): m for m in self.sampling_methods} + + @abstractmethod + def get_pdf(self, distribution_options): + pass + + @property + def info_md(self): + return getattr(self, "_info_md", "") + + @info_md.setter + def info_md(self, value): + self._info_md = value \ No newline at end of file diff --git a/model/distributions/cylinder/cylinder_distribution.py b/model/distributions/cylinder/cylinder_distribution.py index c99b5cf..318850e 100644 --- a/model/distributions/cylinder/cylinder_distribution.py +++ b/model/distributions/cylinder/cylinder_distribution.py @@ -1,7 +1,9 @@ from abc import ABC, abstractmethod from functools import cached_property +from model.distribution import Distribution -class CylinderDistribution(ABC): + +class CylinderDistribution(Distribution): def __init__(self): self.distribution_options = [] # a list of objects that implement the cylinder_sampling_schema interface diff --git a/model/distributions/cylinder/cylinder_sampling_schema.py b/model/distributions/cylinder/cylinder_sampling_schema.py index 8dea18e..6f69037 100644 --- a/model/distributions/cylinder/cylinder_sampling_schema.py +++ b/model/distributions/cylinder/cylinder_sampling_schema.py @@ -1,6 +1,6 @@ from abc import ABC, abstractmethod - -class CylinderSamplingSchema(ABC): +from model.sampling_schema import SamplingSchema +class CylinderSamplingSchema(SamplingSchema): def __init__(self): self.sample_options = [] diff --git a/model/distributions/sphere/sphere_distribution.py b/model/distributions/sphere/sphere_distribution.py index 556e17b..d782611 100644 --- a/model/distributions/sphere/sphere_distribution.py +++ b/model/distributions/sphere/sphere_distribution.py @@ -1,8 +1,9 @@ from abc import ABC, abstractmethod from functools import cached_property +from model.distribution import Distribution -class SphereDistribution(ABC): +class SphereDistribution(Distribution): def __init__(self): self.distribution_options = [] # a list of objects that implement the torus_sampling_schema interface diff --git a/model/distributions/sphere/sphere_sampling_schema.py b/model/distributions/sphere/sphere_sampling_schema.py index 88ca033..504f896 100644 --- a/model/distributions/sphere/sphere_sampling_schema.py +++ b/model/distributions/sphere/sphere_sampling_schema.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod +from model.sampling_schema import SamplingSchema -class SphereSamplingSchema(ABC): +class SphereSamplingSchema(SamplingSchema): def __init__(self): self.sample_options = [] diff --git a/model/distributions/torus/torus_distribution.py b/model/distributions/torus/torus_distribution.py index c03e93f..9acba3e 100644 --- a/model/distributions/torus/torus_distribution.py +++ b/model/distributions/torus/torus_distribution.py @@ -1,7 +1,8 @@ from abc import ABC, abstractmethod from functools import cached_property +from model.distribution import Distribution -class TorusDistribution(ABC): +class TorusDistribution(Distribution): def __init__(self): self.distribution_options = [] # a list of objects that implement the torus_sampling_schema interface diff --git a/model/distributions/torus/torus_sampling_schema.py b/model/distributions/torus/torus_sampling_schema.py index 6b0d9c7..ccebfef 100644 --- a/model/distributions/torus/torus_sampling_schema.py +++ b/model/distributions/torus/torus_sampling_schema.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod +from model.sampling_schema import SamplingSchema -class TorusSamplingSchema(ABC): +class TorusSamplingSchema(SamplingSchema): def __init__(self): self.sample_options = [] diff --git a/model/sampling_schema.py b/model/sampling_schema.py new file mode 100644 index 0000000..7f42ccd --- /dev/null +++ b/model/sampling_schema.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod + +class SamplingSchema(ABC): + def __init__(self): + self.sample_options = [] + + # Returns the name of the sampling method + @abstractmethod + def get_name(self): + pass + + @abstractmethod + def sample(self, sample_options, distribution_options): + pass + + @property + def info_md(self): + return getattr(self, "_info_md", "") + + @info_md.setter + def info_md(self, value): + self._info_md = value \ No newline at end of file diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index f8e0f76..adc25dc 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -268,6 +268,16 @@ def get_layout_components(self): html.Div(id=f"distribution-options-{self.id}"), html.Div(id=f"sampling-options-{self.id}"), + + html.Hr(id=f"distribution-info-divider-{self.id}", hidden=True), + + dcc.Markdown(id=f"distribution-info-markdown-{self.id}", mathjax=True), + + html.Hr(id=f"sampling-method-info-divider-{self.id}", hidden=True), + + dcc.Markdown(id=f"sampling-method-info-markdown-{self.id}", mathjax=True), + + dcc.Markdown(id=f"sampling-method-info-markdown-{self.id}", mathjax=True), ] graph = [dcc.Graph(id=f"graph-{self.id}", figure=self.fig, config=self.config, style={'height': '100%'})] diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 04a19bc..699a033 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -110,9 +110,14 @@ def update_sampling_methods(selected_distribution): return options, initial_value # updates the options (silders, etc) for the selected distribution and sampling method + # also updates the info markdowns and their hr lines @callback( Output(f"distribution-options-{self.id}", "children"), Output(f"sampling-options-{self.id}", "children"), + Output(f"distribution-info-markdown-{self.id}", "children"), + Output(f"sampling-method-info-markdown-{self.id}", "children"), + Output(f"distribution-info-divider-{self.id}", "hidden"), + Output(f"sampling-method-info-divider-{self.id}", "hidden"), Input("distribution-selector", "value"), Input(f"sampling-selector-{self.id}", "value") ) @@ -123,7 +128,15 @@ def update_curr_distribution(selected_distribution, selected_sampling): options_sampling = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling] options_sampling_dcc = [opt.to_dash_component("sampling", id) for id, opt in enumerate(options_sampling.sample_options)] - return options_dist_dcc, options_sampling_dcc + + dist_info_md = self.object.distributions[selected_distribution].info_md + sampling_info_md = options_sampling.info_md + + dist_hidden = dist_info_md is None or dist_info_md.strip() == "" + sampling_hidden = sampling_info_md is None or sampling_info_md.strip() == "" + + + return options_dist_dcc, options_sampling_dcc, dist_info_md, sampling_info_md, dist_hidden, sampling_hidden def _register_3d_plot_callbacks(self): @@ -260,6 +273,16 @@ def get_layout_components(self): html.Div(id=f"distribution-options-{self.id}"), html.Div(id=f"sampling-options-{self.id}"), + + html.Hr(id=f"distribution-info-divider-{self.id}", hidden=True), + + dcc.Markdown(id=f"distribution-info-markdown-{self.id}", mathjax=True), + + html.Hr(id=f"sampling-method-info-divider-{self.id}", hidden=True), + + dcc.Markdown(id=f"sampling-method-info-markdown-{self.id}", mathjax=True), + + ] graph = [dcc.Graph(id=f"graph-{self.id}", figure=self.fig, config=self.config, style={'height': '100%'})] From 7a0814d4ee500be988d4288f471b7aafa0eb96e1 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 20 Nov 2025 17:29:36 +0100 Subject: [PATCH 081/152] add "warning" css for blockquote --- assets/blockquote.css | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 assets/blockquote.css diff --git a/assets/blockquote.css b/assets/blockquote.css new file mode 100644 index 0000000..76e7b54 --- /dev/null +++ b/assets/blockquote.css @@ -0,0 +1,23 @@ +/* Danger / error-style blockquote inside dcc.Markdown */ +blockquote { + border-left: 4px solid #e53935; + background-color: #ffebee; + padding: 1rem 1.25rem 1rem 2.7rem; + margin: 1rem 0; + color: #b71c1c; + border-radius: 6px; + position: relative; +} + +/* Warning icon */ +blockquote::before { + content: "⚠"; + position: absolute; + left: 0.9rem; + top: 0.9rem; + font-size: 1.1rem; +} + +blockquote p { + margin: 0; +} From c97b3cbceefe6a0647761e46d885f521719f1259 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 20 Nov 2025 17:40:45 +0100 Subject: [PATCH 082/152] add kronecker to torus with warning --- .../torus/uniform/fibonacci_kronecker.py | 31 +++++++++++++++++++ model/distributions/torus/uniform/uniform.py | 4 ++- 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 model/distributions/torus/uniform/fibonacci_kronecker.py diff --git a/model/distributions/torus/uniform/fibonacci_kronecker.py b/model/distributions/torus/uniform/fibonacci_kronecker.py new file mode 100644 index 0000000..4f4b427 --- /dev/null +++ b/model/distributions/torus/uniform/fibonacci_kronecker.py @@ -0,0 +1,31 @@ +from abc import ABC, abstractmethod +import numpy as np + +from util.selectors.silder_log import LogSlider +from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema + +class TorusKroneckerUniformSampling(TorusSamplingSchema): + def __init__(self): + self.sample_options = [ + LogSlider("Number of Samples", 10, 100, 10000) + ] + self.info_md = """ + > Warning: Mapping the Kronecker lattice to the torus is not recommended in practice, as it is only periodic on one axis. + It is included for educational purposes only.""" + + def get_name(self): + return "Fibonacci-Kronecker Lattice" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + indices = np.arange(0, sample_count) + gol = (1+5**0.5)/2 + + # centered rank-1 lattice generator + equidistant_generator = (2 * indices + 1) / (2 * sample_count) + + z = equidistant_generator + p = (indices / gol) % 1 + + return np.column_stack((p * 2 * np.pi, z * 2 * np.pi)) \ No newline at end of file diff --git a/model/distributions/torus/uniform/uniform.py b/model/distributions/torus/uniform/uniform.py index a8a38c1..23bf6a2 100644 --- a/model/distributions/torus/uniform/uniform.py +++ b/model/distributions/torus/uniform/uniform.py @@ -5,14 +5,16 @@ from model.distributions.torus.torus_distribution import TorusDistribution from model.distributions.torus.uniform.random import TorusRandomUniformSampling from model.distributions.torus.uniform.fibonacci_rank_1 import TorusFibRank1UniformSampling - +from model.distributions.torus.uniform.fibonacci_kronecker import TorusKroneckerUniformSampling class UniformTorusDistribution(TorusDistribution): def __init__(self): self.distribution_options = [] self.sampling_methods = [ TorusRandomUniformSampling(), TorusFibRank1UniformSampling(), + TorusKroneckerUniformSampling(), ] + def get_name(self): return "Uniform" From 831a754faee8e8de72ab4f3c571a9f0b7e888f3c Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 24 Nov 2025 14:15:31 +0100 Subject: [PATCH 083/152] update gitigonre --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index c58c411..cb60acb 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,5 @@ cython_debug/ #.idea/ #.kate-swp + +.vscode/ From bb8674434f628e9899b2cc154f8a81254528efeb Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 24 Nov 2025 15:57:40 +0100 Subject: [PATCH 084/152] implement partially wraped random sampling --- model/cylinder/cylinder.py | 3 +- .../partially_wraped_normal/__init__.py | 0 .../partially_warpped_normal.py | 24 ++++++++++++++ .../partially_wraped_normal/random.py | 33 +++++++++++++++++++ model/torus/torus.py | 3 +- renderer/Object3DAnd2DRenderer.py | 18 ++++++++-- renderer/Object3DRenderer.py | 11 ++++--- renderer/PlotSettings2d.py | 2 ++ 8 files changed, 85 insertions(+), 9 deletions(-) create mode 100644 model/distributions/cylinder/partially_wraped_normal/__init__.py create mode 100644 model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py create mode 100644 model/distributions/cylinder/partially_wraped_normal/random.py diff --git a/model/cylinder/cylinder.py b/model/cylinder/cylinder.py index 318e0af..3eef64e 100644 --- a/model/cylinder/cylinder.py +++ b/model/cylinder/cylinder.py @@ -35,6 +35,7 @@ def __init__(self, resolution=100, r=1): periodic_x_amount=2 * np.pi, x_title="p", y_title="z", + reverse_x_y_axis=False, ) self.camera_settings_3d = dict( @@ -55,7 +56,7 @@ def update_sample(self, selected_distribution, selected_sampling_method, sample_ sampling_method = dist.sampling_method_dict[selected_sampling_method] new_sample = sampling_method.sample(sample_options, distribution_options) - if new_sample.size == 0: + if (new_sample is None) or new_sample.size == 0: self.samples = np.empty((0, 3), dtype=float) return diff --git a/model/distributions/cylinder/partially_wraped_normal/__init__.py b/model/distributions/cylinder/partially_wraped_normal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py new file mode 100644 index 0000000..501263d --- /dev/null +++ b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py @@ -0,0 +1,24 @@ +from abc import ABC, abstractmethod +from util.selectors.slider_float import FloatSlider +import numpy as np + +from model.distributions.cylinder.cylinder_distribution import CylinderDistribution +from model.distributions.cylinder.partially_wraped_normal.random import CylinderRandomPWNSampling + + +class PartiallyWrappedNormalDistribution(CylinderDistribution): + def __init__(self): + self.distribution_options = [ + FloatSlider("Sigma x (σₓ)", 0, 0.5, 5.0), + FloatSlider("Sigma y (σᵧ)", 0, 0.5, 1.0), + FloatSlider("Correlation (ρ)", -1, 0.1, 1), + ] + self.sampling_methods = [ + CylinderRandomPWNSampling(), + ] + + def get_name(self): + return "Partially Wrapped Normal" + + def get_pdf(self, distribution_options): + pass \ No newline at end of file diff --git a/model/distributions/cylinder/partially_wraped_normal/random.py b/model/distributions/cylinder/partially_wraped_normal/random.py new file mode 100644 index 0000000..cedd9ea --- /dev/null +++ b/model/distributions/cylinder/partially_wraped_normal/random.py @@ -0,0 +1,33 @@ +from abc import ABC, abstractmethod +from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema +from util.selectors.silder_log import LogSlider +import numpy as np + +class CylinderRandomPWNSampling(CylinderSamplingSchema): + def __init__(self): + self.sample_options = [ + LogSlider("Number of Samples", 10, 100, 10000) + ] + + def get_name(self): + return "Random" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + sigma_x = distribution_options[0].state + sigma_y = distribution_options[1].state + correlation = distribution_options[2].state + + Cov = np.array([ + [sigma_x**2, correlation * sigma_x * sigma_y], + [correlation * sigma_x * sigma_y, sigma_y**2] + ]) + + mean = np.array([np.pi, np.pi]) + + samples = np.random.multivariate_normal(mean, Cov, sample_count) + samples[:,0] = samples[:,0] % (2 * np.pi) + + + return samples \ No newline at end of file diff --git a/model/torus/torus.py b/model/torus/torus.py index 2216e43..f6056e3 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -36,6 +36,7 @@ def __init__(self, resolution=100, r=1, R=3): periodic_y_amount=2 * np.pi, x_title="t", y_title="p", + reverse_x_y_axis=True, # x is p, y is t ) def generate_xyz(self, resolution=50, r=1, R=3): @@ -51,7 +52,7 @@ def update_sample(self, selected_distribution, selected_sampling_method, sample_ sampling_method = dist.sampling_method_dict[selected_sampling_method] new_sample = sampling_method.sample(sample_options, distribution_options) - if new_sample.size == 0: + if (new_sample is None) or new_sample.size == 0: self.samples = np.empty((0, 3), dtype=float) return diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index adc25dc..38c8ee9 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -17,6 +17,8 @@ def __init__(self, object, id): self.perx_x_amount = object.plot_settings_2d.periodic_x_amount self.pery_y_amount = object.plot_settings_2d.periodic_y_amount + self.reverse_x_y_axis = object.plot_settings_2d.reverse_x_y_axis + padd = 0.5 self.fig_2d = go.Figure( @@ -178,15 +180,25 @@ def update_plot_sample_2d(self, values_dist, ids_dist, values_samp, ids_samp, se # marker size scaling sample_count = self.object.samples.shape[0] - marker_size = (10 * (sample_count / 100) ** (-0.35)) / dpr - marker_size = np.minimum(10,marker_size) + if sample_count == 0: + marker_size = 0 # no samples, no size + else: + marker_size = (10 * (sample_count / 100) ** (-0.35)) / dpr + marker_size = np.minimum(10,marker_size) patched_figure["data"][0].marker.size = marker_size * 1.5 patched_figure["data"][1].marker.size = marker_size - # x is p, y is t + if not self.reverse_x_y_axis: + # True: (eg torus: x is p, y is t) + # False: (eg cylinder: x is p, y is z) + tp[:, [0, 1]] = tp[:, [1, 0]] # swap order, below code assumes self.reverse_x_y_axis is True + patched_figure["data"][0].x = tp[:, 1] patched_figure["data"][0].y = tp[:, 0] + + + ext_x = np.array([]) ext_y = np.array([]) diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 699a033..09811af 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -23,7 +23,7 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): 'scrollZoom': True, "modeBarButtonsToRemove": ["select2d", "lasso2d"], } - if self.object.samples.size: + if self.object.samples.size and self.object.samples.shape[0] != 0: sample_count = self.object.samples.shape[0] marker_size = (10 * (sample_count / 100) ** (-0.35)) / self.device_pixel_ratio marker_size = np.minimum(10,marker_size) @@ -207,9 +207,12 @@ def update_plot_sample(self, values_dist, ids_dist, values_samp, ids_samp, selec patched_figure["data"][1].z = self.object.samples[:, 2] # set size based on number of samples - sample_count = self.object.samples.shape[0] - marker_size = (10 * (sample_count / 100) ** (-0.35)) / dpr - marker_size = np.minimum(10,marker_size) + if self.object.samples.size == 0: + marker_size = 0 + else: + sample_count = self.object.samples.shape[0] + marker_size = (10 * (sample_count / 100) ** (-0.35)) / dpr + marker_size = np.minimum(10,marker_size) patched_figure["data"][1].marker.size = marker_size diff --git a/renderer/PlotSettings2d.py b/renderer/PlotSettings2d.py index 428bfba..ef05c20 100644 --- a/renderer/PlotSettings2d.py +++ b/renderer/PlotSettings2d.py @@ -13,3 +13,5 @@ class PlotSettings2D: x_title: str = "" # title for axis y_title: str = "" + + reverse_x_y_axis: bool = False # if set to True in the (n,2) shaped data, first column is y and second is x From 9e987d59a9fde2d92ecfbbf455a6f8c4ef3e9f28 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 24 Nov 2025 16:25:21 +0100 Subject: [PATCH 085/152] added pdf to partially wraped --- model/cylinder/cylinder.py | 8 +++- .../partially_warpped_normal.py | 40 +++++++++++++++++-- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/model/cylinder/cylinder.py b/model/cylinder/cylinder.py index 3eef64e..b131abe 100644 --- a/model/cylinder/cylinder.py +++ b/model/cylinder/cylinder.py @@ -13,7 +13,7 @@ class Cylinder(Manifold): def __init__(self, resolution=100, r=1): - self.xyz = self.generate_xyz(resolution, r) + self.xyz = self.generate_xyz(resolution, 0.999) self.mesh = np.array([]) self.samples = np.array([]) self.samples_2d = np.array([]) @@ -74,6 +74,12 @@ def p_z_to_xyz(p, z, r=1): return x, y, z + @staticmethod + def xyz_to_p_z(x, y, z): + p = np.arctan2(y, x) % (2 * np.pi) + + return p, z + def generate_mesh(self, pdf, alpha=1): # mesh_xyz has nans for line segments, mask before passing to pdf mask = np.all(np.isfinite(self.mesh_xyz), axis=-1) diff --git a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py index 501263d..e7a4805 100644 --- a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py +++ b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py @@ -1,10 +1,11 @@ -from abc import ABC, abstractmethod from util.selectors.slider_float import FloatSlider import numpy as np +from scipy.stats import multivariate_normal + from model.distributions.cylinder.cylinder_distribution import CylinderDistribution from model.distributions.cylinder.partially_wraped_normal.random import CylinderRandomPWNSampling - +from model.cylinder.cylinder import Cylinder class PartiallyWrappedNormalDistribution(CylinderDistribution): def __init__(self): @@ -21,4 +22,37 @@ def get_name(self): return "Partially Wrapped Normal" def get_pdf(self, distribution_options): - pass \ No newline at end of file + sigma_x = distribution_options[0].state + sigma_y = distribution_options[1].state + correlation = distribution_options[2].state + + Cov = np.array([ + [sigma_x**2, correlation * sigma_x * sigma_y], + [correlation * sigma_x * sigma_y, sigma_y**2] + ]) + + mean = np.array([np.pi, np.pi]) + + dist = multivariate_normal(mean=mean, cov=Cov) + + def pdf(x): + alpha = 0.7 # scale + + p, z = Cylinder.xyz_to_p_z(x[:,0], x[:,1], x[:,2]) + p_z = np.column_stack((p, z)) + + # wrap until 3 * sigma_x + k = int(np.ceil(3 * sigma_x / (2 * np.pi))) + + total_pdf = np.zeros(p_z.shape[0]) + for i in range(-k, k+1): + shifted_samples = np.column_stack((p_z[:,0] + i * 2 * np.pi, p_z[:,1])) + total_pdf += dist.pdf(shifted_samples) + + max = np.max(total_pdf) + + norm = total_pdf / max + norm = norm * alpha + return norm + + return pdf From cce154f31ec764a10e4b4e3f797a238d3e16d947 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 24 Nov 2025 16:35:16 +0100 Subject: [PATCH 086/152] implement wrapped gauss on torus --- .../torus/wrapped_normal/__init__.py | 0 .../torus/wrapped_normal/random.py | 34 +++++++++++++++++++ .../torus/wrapped_normal/wrapped_normal.py | 23 +++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 model/distributions/torus/wrapped_normal/__init__.py create mode 100644 model/distributions/torus/wrapped_normal/random.py create mode 100644 model/distributions/torus/wrapped_normal/wrapped_normal.py diff --git a/model/distributions/torus/wrapped_normal/__init__.py b/model/distributions/torus/wrapped_normal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/torus/wrapped_normal/random.py b/model/distributions/torus/wrapped_normal/random.py new file mode 100644 index 0000000..bff11e6 --- /dev/null +++ b/model/distributions/torus/wrapped_normal/random.py @@ -0,0 +1,34 @@ +from abc import ABC, abstractmethod +from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema +from util.selectors.silder_log import LogSlider +import numpy as np + +class TorusRandomWrappedSampling(TorusSamplingSchema): + def __init__(self): + self.sample_options = [ + LogSlider("Number of Samples", 10, 100, 10000) + ] + + def get_name(self): + return "Random" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + sigma_x = distribution_options[0].state + sigma_y = distribution_options[1].state + correlation = distribution_options[2].state + + Cov = np.array([ + [sigma_x**2, correlation * sigma_x * sigma_y], + [correlation * sigma_x * sigma_y, sigma_y**2] + ]) + + mean = np.array([np.pi, np.pi]) + + samples = np.random.multivariate_normal(mean, Cov, sample_count) + samples[:,0] = samples[:,0] % (2 * np.pi) + samples[:,1] = samples[:,1] % (2 * np.pi) + + + return samples \ No newline at end of file diff --git a/model/distributions/torus/wrapped_normal/wrapped_normal.py b/model/distributions/torus/wrapped_normal/wrapped_normal.py new file mode 100644 index 0000000..118e992 --- /dev/null +++ b/model/distributions/torus/wrapped_normal/wrapped_normal.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractmethod +from util.selectors.slider_float import FloatSlider +import numpy as np + +from model.distributions.torus.torus_distribution import TorusDistribution +from model.distributions.torus.wrapped_normal.random import TorusRandomWrappedSampling +class UniformTorusDistribution(TorusDistribution): + def __init__(self): + self.distribution_options = [ + FloatSlider("Sigma x (σₓ)", 0, 0.5, 5.0), + FloatSlider("Sigma y (σᵧ)", 0, 0.5, 5.0), + FloatSlider("Correlation (ρ)", -1, 0.1, 1), + ] + self.sampling_methods = [ + TorusRandomWrappedSampling(), + ] + + + def get_name(self): + return "Wrapped Normal" + + def get_pdf(self, distribution_options): + pass \ No newline at end of file From aeaf2d5e206abc1388cefe929881720a7e9c3ea8 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 24 Nov 2025 16:48:40 +0100 Subject: [PATCH 087/152] implement pdf for wrapped gaussian --- .../torus/wrapped_normal/random.py | 8 ++-- .../torus/wrapped_normal/wrapped_normal.py | 45 +++++++++++++++++-- model/torus/torus.py | 2 +- 3 files changed, 46 insertions(+), 9 deletions(-) diff --git a/model/distributions/torus/wrapped_normal/random.py b/model/distributions/torus/wrapped_normal/random.py index bff11e6..8a00d8d 100644 --- a/model/distributions/torus/wrapped_normal/random.py +++ b/model/distributions/torus/wrapped_normal/random.py @@ -15,13 +15,13 @@ def get_name(self): def sample(self, sample_options, distribution_options): sample_count = sample_options[0].state - sigma_x = distribution_options[0].state - sigma_y = distribution_options[1].state + sigma_t = distribution_options[0].state + sigma_p = distribution_options[1].state correlation = distribution_options[2].state Cov = np.array([ - [sigma_x**2, correlation * sigma_x * sigma_y], - [correlation * sigma_x * sigma_y, sigma_y**2] + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] ]) mean = np.array([np.pi, np.pi]) diff --git a/model/distributions/torus/wrapped_normal/wrapped_normal.py b/model/distributions/torus/wrapped_normal/wrapped_normal.py index 118e992..f9266e5 100644 --- a/model/distributions/torus/wrapped_normal/wrapped_normal.py +++ b/model/distributions/torus/wrapped_normal/wrapped_normal.py @@ -1,14 +1,16 @@ -from abc import ABC, abstractmethod from util.selectors.slider_float import FloatSlider import numpy as np +from scipy.stats import multivariate_normal + from model.distributions.torus.torus_distribution import TorusDistribution from model.distributions.torus.wrapped_normal.random import TorusRandomWrappedSampling +from model.torus.torus import Torus class UniformTorusDistribution(TorusDistribution): def __init__(self): self.distribution_options = [ - FloatSlider("Sigma x (σₓ)", 0, 0.5, 5.0), - FloatSlider("Sigma y (σᵧ)", 0, 0.5, 5.0), + FloatSlider("Sigma t (σₜ)", 0, 0.5, 5.0), + FloatSlider("Sigma p (σₚ)", 0, 0.5, 5.0), FloatSlider("Correlation (ρ)", -1, 0.1, 1), ] self.sampling_methods = [ @@ -20,4 +22,39 @@ def get_name(self): return "Wrapped Normal" def get_pdf(self, distribution_options): - pass \ No newline at end of file + sigma_t = distribution_options[0].state + sigma_p = distribution_options[1].state + correlation = distribution_options[2].state + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + mean = np.array([np.pi, np.pi]) + + dist = multivariate_normal(mean=mean, cov=Cov) + + def pdf(x): + alpha = 0.7 # scale + + t, p = Torus.xyz_to_t_p(x[:,0], x[:,1], x[:,2]) + t_p = np.column_stack((t,p)) + + # wrap until 3 * sigma_x + k_t = int(np.ceil(3 * sigma_t / (2 * np.pi))) + k_p = int(np.ceil(3 * sigma_p / (2 * np.pi))) + + total_pdf = np.zeros(t_p.shape[0]) + for i in range(-k_t, k_t+1): + for j in range(-k_p, k_p+1): + shifted_samples = np.column_stack((t_p[:,0] + i * 2 * np.pi, t_p[:,1] + j * 2 * np.pi)) + total_pdf += dist.pdf(shifted_samples) + + max = np.max(total_pdf) + + norm = total_pdf / max + norm = norm * alpha + return norm + + return pdf \ No newline at end of file diff --git a/model/torus/torus.py b/model/torus/torus.py index f6056e3..38f86cd 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -11,7 +11,7 @@ class Torus(Manifold): def __init__(self, resolution=100, r=1, R=3): - self.xyz = self.generate_xyz(resolution, r, R) + self.xyz = self.generate_xyz(resolution, r - 0.01, R - 0.01) # slightly smaller to avoid artifacts from mesh self.mesh = np.array([]) self.samples = np.array([]) self.samples_2d = np.array([]) From 71138ffc188e1620d3d05501c27de23289903c9e Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 24 Nov 2025 19:06:53 +0100 Subject: [PATCH 088/152] added heatmap to 2d plot --- model/cylinder/cylinder.py | 5 +++ model/manifold.py | 5 +++ model/torus/torus.py | 6 ++++ renderer/Object3DAnd2DRenderer.py | 55 ++++++++++++++++++++++++++++++- renderer/PlotSettings2d.py | 3 ++ 5 files changed, 73 insertions(+), 1 deletion(-) diff --git a/model/cylinder/cylinder.py b/model/cylinder/cylinder.py index b131abe..7daaa52 100644 --- a/model/cylinder/cylinder.py +++ b/model/cylinder/cylinder.py @@ -36,6 +36,7 @@ def __init__(self, resolution=100, r=1): x_title="p", y_title="z", reverse_x_y_axis=False, + color_location=(0,0,2*np.pi, 2*np.pi), ) self.camera_settings_3d = dict( @@ -80,6 +81,10 @@ def xyz_to_p_z(x, y, z): return p, z + def pdf_2d(self, xy, pdf): + x, y, z = self.p_z_to_xyz(xy[:,0], xy[:,1], self.r) + return pdf(np.column_stack((x, y, z))) + def generate_mesh(self, pdf, alpha=1): # mesh_xyz has nans for line segments, mask before passing to pdf mask = np.all(np.isfinite(self.mesh_xyz), axis=-1) diff --git a/model/manifold.py b/model/manifold.py index e7dbccf..294defa 100644 --- a/model/manifold.py +++ b/model/manifold.py @@ -31,4 +31,9 @@ def update_sample(self, selected_distribution, sample_options): pass def generate_mesh(self, pdf, *args, **kwargs): + pass + + # uses the objects pararametrization to convert 2d coords to 3d coords and apply pdf + # optional, only if the manifold supports 2d plotting + def pdf_2d(self, xy, pdf): pass \ No newline at end of file diff --git a/model/torus/torus.py b/model/torus/torus.py index 38f86cd..ce4b941 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -37,6 +37,7 @@ def __init__(self, resolution=100, r=1, R=3): x_title="t", y_title="p", reverse_x_y_axis=True, # x is p, y is t + color_location=(0,0,2*np.pi, 2*np.pi), ) def generate_xyz(self, resolution=50, r=1, R=3): @@ -47,6 +48,11 @@ def generate_xyz(self, resolution=50, r=1, R=3): return self.t_p_to_xyz(t, p, r, R) + def pdf_2d(self, xy, pdf): + x, y, z = self.t_p_to_xyz(xy[:,0], xy[:,1], self.r, self.R) + return pdf(np.column_stack((x, y, z))) + + def update_sample(self, selected_distribution, selected_sampling_method, sample_options, distribution_options): dist = self.distributions[selected_distribution] sampling_method = dist.sampling_method_dict[selected_sampling_method] diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index 38c8ee9..56d8979 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -19,6 +19,11 @@ def __init__(self, object, id): self.reverse_x_y_axis = object.plot_settings_2d.reverse_x_y_axis + x_min, y_min, x_max, y_max = object.plot_settings_2d.color_location + self.color_heatmap_x = np.linspace(x_min, x_max, 100) + self.color_heatmap_y = np.linspace(y_min, y_max, 100) + self.color_meshgrid = np.meshgrid(self.color_heatmap_x, self.color_heatmap_y, indexing="xy") + padd = 0.5 self.fig_2d = go.Figure( @@ -47,6 +52,20 @@ def __init__(self, object, id): line=dict(width=1, color="#a2acbd") ), showlegend=(object.plot_settings_2d.periodic_x or object.plot_settings_2d.periodic_y) + ), + + go.Heatmap( + name="PDF", + x=self.color_heatmap_x, + y=self.color_heatmap_y, + z=np.zeros((100,100)), + colorscale="Plasma", + zmin=0.0, + zmax=1.0, + zsmooth="best", + colorbar=dict(title="pdf"), + showscale=True, + showlegend=True, ) ] ) @@ -231,11 +250,45 @@ def update_plot_sample_2d(self, values_dist, ids_dist, values_samp, ids_samp, se if self.per_x or self.per_y: patched_figure["data"][1].x = ext_x patched_figure["data"][1].y = ext_y + return patched_figure def update_plot_dist_2d(self, values_dist, ids_dist, selected_distribution, selected_sampling, _): - return no_update + patched_figure = Patch() + try: + dist_options = self.object.distributions[selected_distribution].distribution_options + except KeyError: + # got stale values, ignore + return no_update + + # the order of options might not be guaranteed, so we map them by their ids + id_value_dist = [(id,v) for id, v in zip(ids_dist, values_dist)] + + + # and them sort them, so they are in the same order as sampling_options and dist_options + options_dist_new = sorted(id_value_dist, key=lambda x: int(x[0]["index"])) + + + for opt, (id, new_state) in zip(dist_options, options_dist_new): + opt.update_state(new_state) + + pdf = self.object.distributions[selected_distribution].get_pdf(list(dist_options)) + + # pdf heatmap + X, Y = self.color_meshgrid + if self.reverse_x_y_axis: + xy = np.column_stack((Y.ravel(), X.ravel())) + else: + xy = np.column_stack((X.ravel(), Y.ravel())) + z_flat = self.object.pdf_2d(xy, pdf).reshape(X.shape) + + z = z_flat.reshape(X.shape) + print("2D PDF min/max:", np.min(z), np.max(z)) + + patched_figure["data"][2].z = z + + return patched_figure def get_layout_components(self): initial_distribution = self.object.distributions[list(self.object.distributions.keys())[0]] diff --git a/renderer/PlotSettings2d.py b/renderer/PlotSettings2d.py index ef05c20..c3e00d7 100644 --- a/renderer/PlotSettings2d.py +++ b/renderer/PlotSettings2d.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +import numpy as np @dataclass class PlotSettings2D: @@ -15,3 +16,5 @@ class PlotSettings2D: y_title: str = "" reverse_x_y_axis: bool = False # if set to True in the (n,2) shaped data, first column is y and second is x + + color_location: tuple = (0,0,2*np.pi, 2*np.pi) # location of where pdf is mapped: (min_x, min_y, max_x, max_y) From b74661e870bf77dc6582642b665bb74c942e9d87 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 25 Nov 2025 16:25:07 +0100 Subject: [PATCH 089/152] add cartesian grid for uniform --- assets/tooltip.js | 4 ++ .../cylinder/uniform/cartesian.py | 25 +++++++++ .../distributions/cylinder/uniform/uniform.py | 2 + .../distributions/sphere/uniform/cartesian.py | 26 +++++++++ model/distributions/sphere/uniform/uniform.py | 2 + .../distributions/torus/uniform/cartesian.py | 24 +++++++++ .../torus/uniform/fibonacci_kronecker.py | 2 +- model/distributions/torus/uniform/uniform.py | 2 + util/cartesian_util.py | 20 +++++++ util/selectors/slider_square.py | 54 +++++++++++++++++++ 10 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 model/distributions/cylinder/uniform/cartesian.py create mode 100644 model/distributions/sphere/uniform/cartesian.py create mode 100644 model/distributions/torus/uniform/cartesian.py create mode 100644 util/cartesian_util.py create mode 100644 util/selectors/slider_square.py diff --git a/assets/tooltip.js b/assets/tooltip.js index 4bfe989..29a8863 100644 --- a/assets/tooltip.js +++ b/assets/tooltip.js @@ -28,4 +28,8 @@ window.dccFunctions.transform_fib = function(value) { : fibonacci(n - 1) + fibonacci(n - 2) } return fibonacci(value); +} + +window.dccFunctions.transform_square = function(value) { + return value * value; } \ No newline at end of file diff --git a/model/distributions/cylinder/uniform/cartesian.py b/model/distributions/cylinder/uniform/cartesian.py new file mode 100644 index 0000000..bc59a64 --- /dev/null +++ b/model/distributions/cylinder/uniform/cartesian.py @@ -0,0 +1,25 @@ +from abc import ABC, abstractmethod +import numpy as np + +from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema +from util.selectors.slider_square import SliderSquare +from util.cartesian_util import CartesianUtil as cu + + +class CylinderCartesianUniformSampling(CylinderSamplingSchema): + def __init__(self): + self.sample_options = [ + SliderSquare("Number of Samples", 4, 64, 100, 4) + ] + self.info_md = """ + > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. + It is included for demonstration purposes only.""" + + def get_name(self): + return "Cartesian Grid" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + n = int(np.sqrt(sample_count)) + return cu.generate_cartesian_grid(n, (2 * np.pi, 2 * np.pi)) \ No newline at end of file diff --git a/model/distributions/cylinder/uniform/uniform.py b/model/distributions/cylinder/uniform/uniform.py index c4c3887..a1c0eab 100644 --- a/model/distributions/cylinder/uniform/uniform.py +++ b/model/distributions/cylinder/uniform/uniform.py @@ -6,6 +6,7 @@ from model.distributions.cylinder.uniform.random import CylinderRandomUniformSampling from model.distributions.cylinder.uniform.fibonacci_kronecker import CylinderFibUniformSampling from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling +from model.distributions.cylinder.uniform.cartesian import CylinderCartesianUniformSampling class UniformCylinderDistribution(CylinderDistribution): def __init__(self): self.distribution_options = [] @@ -13,6 +14,7 @@ def __init__(self): CylinderRandomUniformSampling(), CylinderFibUniformSampling(), CylinderFibRank1UniformSampling(), + CylinderCartesianUniformSampling(), ] def get_name(self): diff --git a/model/distributions/sphere/uniform/cartesian.py b/model/distributions/sphere/uniform/cartesian.py new file mode 100644 index 0000000..874d569 --- /dev/null +++ b/model/distributions/sphere/uniform/cartesian.py @@ -0,0 +1,26 @@ +import numpy as np + +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from util.selectors.slider_square import SliderSquare +from util.cartesian_util import CartesianUtil as cu +from model.sphere.sphere import Sphere + +class SphereCartesianUniformSampling(SphereSamplingSchema): + def __init__(self): + self.sample_options = [ + SliderSquare("Number of Samples", 4, 64, 100, 4) + ] + self.info_md = """ + > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. + It is included for demonstration purposes only.""" + + def get_name(self): + return "Cartesian Grid" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + n = int(np.sqrt(sample_count)) + grid_spherical = cu.generate_cartesian_grid(n, (np.pi, 2 * np.pi)) + x, y, z = Sphere.spherical_to_cartesian(grid_spherical[:,0], grid_spherical[:,1]) + return np.column_stack((x, y, z)) \ No newline at end of file diff --git a/model/distributions/sphere/uniform/uniform.py b/model/distributions/sphere/uniform/uniform.py index 11f39cd..f44930c 100644 --- a/model/distributions/sphere/uniform/uniform.py +++ b/model/distributions/sphere/uniform/uniform.py @@ -5,6 +5,7 @@ from model.distributions.sphere.uniform.random import SphereUniformRandomSampling from model.distributions.sphere.uniform.fibonachi_lattice import SphereUniformFibSampling from model.distributions.sphere.uniform.fibonacci_rank_1 import SphereFibRank1UniformSampling +from model.distributions.sphere.uniform.cartesian import SphereCartesianUniformSampling class SphereUniformDistribution(SphereDistribution): def __init__(self): @@ -13,6 +14,7 @@ def __init__(self): SphereUniformRandomSampling(), SphereUniformFibSampling(), SphereFibRank1UniformSampling(), + SphereCartesianUniformSampling(), ] diff --git a/model/distributions/torus/uniform/cartesian.py b/model/distributions/torus/uniform/cartesian.py new file mode 100644 index 0000000..2a8feb6 --- /dev/null +++ b/model/distributions/torus/uniform/cartesian.py @@ -0,0 +1,24 @@ +import numpy as np + +from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema +from util.selectors.slider_square import SliderSquare +from util.cartesian_util import CartesianUtil as cu + + +class TorusCartesianUniformSampling(TorusSamplingSchema): + def __init__(self): + self.sample_options = [ + SliderSquare("Number of Samples", 4, 64, 100, 4) + ] + self.info_md = """ + > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. + It is included for demonstration purposes only.""" + + def get_name(self): + return "Cartesian Grid" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + n = int(np.sqrt(sample_count)) + return cu.generate_cartesian_grid(n, (2 * np.pi, 2 * np.pi)) \ No newline at end of file diff --git a/model/distributions/torus/uniform/fibonacci_kronecker.py b/model/distributions/torus/uniform/fibonacci_kronecker.py index 4f4b427..606e52c 100644 --- a/model/distributions/torus/uniform/fibonacci_kronecker.py +++ b/model/distributions/torus/uniform/fibonacci_kronecker.py @@ -11,7 +11,7 @@ def __init__(self): ] self.info_md = """ > Warning: Mapping the Kronecker lattice to the torus is not recommended in practice, as it is only periodic on one axis. - It is included for educational purposes only.""" + It is included for demonstration purposes only.""" def get_name(self): return "Fibonacci-Kronecker Lattice" diff --git a/model/distributions/torus/uniform/uniform.py b/model/distributions/torus/uniform/uniform.py index 23bf6a2..3db3829 100644 --- a/model/distributions/torus/uniform/uniform.py +++ b/model/distributions/torus/uniform/uniform.py @@ -6,6 +6,7 @@ from model.distributions.torus.uniform.random import TorusRandomUniformSampling from model.distributions.torus.uniform.fibonacci_rank_1 import TorusFibRank1UniformSampling from model.distributions.torus.uniform.fibonacci_kronecker import TorusKroneckerUniformSampling +from model.distributions.torus.uniform.cartesian import TorusCartesianUniformSampling class UniformTorusDistribution(TorusDistribution): def __init__(self): self.distribution_options = [] @@ -13,6 +14,7 @@ def __init__(self): TorusRandomUniformSampling(), TorusFibRank1UniformSampling(), TorusKroneckerUniformSampling(), + TorusCartesianUniformSampling(), ] diff --git a/util/cartesian_util.py b/util/cartesian_util.py new file mode 100644 index 0000000..bbd21e6 --- /dev/null +++ b/util/cartesian_util.py @@ -0,0 +1,20 @@ +import numpy as np + +class CartesianUtil: + + """ Generate a Cartesian grid of samples. + n: number of samples along one dimension (total samples = n*n) + widths: tuple of widths along each dimension + Returns: array of shape (n*n, 2) with samples + """ + @staticmethod + def generate_cartesian_grid(n, widths): + indices = np.arange(n) + + # use cell centers of an n x n grid on [0, 1) x [0, 1), then scale to widths + t = (indices + 0.5) / n * widths[0] + p = (indices + 0.5) / n * widths[1] + + t_grid, p_grid = np.meshgrid(t, p, indexing="ij") + + return np.column_stack((t_grid.ravel(), p_grid.ravel())) \ No newline at end of file diff --git a/util/selectors/slider_square.py b/util/selectors/slider_square.py new file mode 100644 index 0000000..8f7ce18 --- /dev/null +++ b/util/selectors/slider_square.py @@ -0,0 +1,54 @@ +from dash import dcc, html +import numpy as np +from util.selectors.selector import Selector +import sympy as sp + + +SLIDER_MARK_AMOUNT = 5 +""" +Silder that only selects perfect squares within a given range. +min and max are the indices of the square (eg. i^2). +state is not an index, but the actual Square number at a valid index. +""" +class SliderSquare(Selector): + def __init__(self, name, min, state, max, idx): + self.name = name + self.min = min + self.state = state + self.idx = idx + self.max = max + + self.id = None + + def to_dash_component(self, _type, id): + self.id = id + return html.Div([ + html.Label(self.name), + + dcc.Slider( + id={"type": _type, "index": id}, + min=self.min, + max=self.max, + value=self.idx, + tooltip={"placement": "bottom", "always_visible": True, "transform": "transform_square"}, + step=1, + marks=self.calculate_marks(), + updatemode="drag", + ) + ]) + + def calculate_marks(self): + + marks = {} + step = (self.max - self.min) / SLIDER_MARK_AMOUNT + for i in range(SLIDER_MARK_AMOUNT + 1): + value = int(self.min + i * step) + marks[value] = str((value**2)) + return marks + + + def update_state(self, new_state): + self.state = int((new_state**2)) + self.idx = int(new_state) + + \ No newline at end of file From ddf5258a9f8bd8c59388964fe2c5aa3770b53047 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 25 Nov 2025 16:38:45 +0100 Subject: [PATCH 090/152] allow singular in wrapped and partially wrapped --- .../partially_wraped_normal/partially_warpped_normal.py | 2 +- model/distributions/torus/wrapped_normal/wrapped_normal.py | 2 +- renderer/Object3DAnd2DRenderer.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py index e7a4805..8d3318c 100644 --- a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py +++ b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py @@ -33,7 +33,7 @@ def get_pdf(self, distribution_options): mean = np.array([np.pi, np.pi]) - dist = multivariate_normal(mean=mean, cov=Cov) + dist = multivariate_normal(mean=mean, cov=Cov, allow_singular=True) def pdf(x): alpha = 0.7 # scale diff --git a/model/distributions/torus/wrapped_normal/wrapped_normal.py b/model/distributions/torus/wrapped_normal/wrapped_normal.py index f9266e5..9269e63 100644 --- a/model/distributions/torus/wrapped_normal/wrapped_normal.py +++ b/model/distributions/torus/wrapped_normal/wrapped_normal.py @@ -33,7 +33,7 @@ def get_pdf(self, distribution_options): mean = np.array([np.pi, np.pi]) - dist = multivariate_normal(mean=mean, cov=Cov) + dist = multivariate_normal(mean=mean, cov=Cov, allow_singular=True) def pdf(x): alpha = 0.7 # scale diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index 56d8979..b4d8554 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -284,7 +284,6 @@ def update_plot_dist_2d(self, values_dist, ids_dist, selected_distribution, sele z_flat = self.object.pdf_2d(xy, pdf).reshape(X.shape) z = z_flat.reshape(X.shape) - print("2D PDF min/max:", np.min(z), np.max(z)) patched_figure["data"][2].z = z From f18b8449de53050593feacc8ba0df7c6b7435e6b Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 25 Nov 2025 17:12:57 +0100 Subject: [PATCH 091/152] added von mises cartesian --- .../sphere/vonmises_fisher/cartesian.py | 39 +++++++++++++++++++ .../sphere/vonmises_fisher/vonmises_fisher.py | 5 ++- 2 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 model/distributions/sphere/vonmises_fisher/cartesian.py diff --git a/model/distributions/sphere/vonmises_fisher/cartesian.py b/model/distributions/sphere/vonmises_fisher/cartesian.py new file mode 100644 index 0000000..3bc50dd --- /dev/null +++ b/model/distributions/sphere/vonmises_fisher/cartesian.py @@ -0,0 +1,39 @@ +from abc import ABC, abstractmethod +import numpy as np +import scipy + +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from util.selectors.slider_square import SliderSquare +from util.cartesian_util import CartesianUtil as cu + +class VonMisesCartesianSampling(SphereSamplingSchema): + def __init__(self): + self.sample_options = [ + SliderSquare("Number of Samples", 4, 64, 100, 4) + ] + self.info_md = """ + > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. + It is included for demonstration purposes only.""" + + def get_name(self): + return "Cartesian Grid" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + n = int(np.sqrt(sample_count)) + k = distribution_options[0].state # kappa + + grid = cu.generate_cartesian_grid(n, (1, 1)) + x, y = grid[:,0], grid[:,1] + phi = 2 * np.pi * y # azimuthal angle, [0, 2pi] uniform + + w = 1 + (1/k) * np.log1p(x * np.expm1(-2 * k)) + w = np.clip(w, -1.0, 1.0) # clamp to avoid sqrt warnings due to numerical issues + + x_i_f_0 = w + x_i_f_1 = np.sqrt(1-w**2) * np.cos( phi) + x_i_f_2 = np.sqrt(1-w**2) * np.sin( phi) + x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) # order so that mu=[0, 0, 1] + return x_i_f + + diff --git a/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py index 6edae92..6af0b7e 100644 --- a/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py +++ b/model/distributions/sphere/vonmises_fisher/vonmises_fisher.py @@ -6,7 +6,7 @@ from model.distributions.sphere.vonmises_fisher.random import VonMisesRandomSampling from model.distributions.sphere.vonmises_fisher.fibonachi import VonMisesFibSampling - +from model.distributions.sphere.vonmises_fisher.cartesian import VonMisesCartesianSampling class vonMisesFisherDistribution(SphereDistribution): def __init__(self): self.distribution_options = [ @@ -15,7 +15,8 @@ def __init__(self): self.sampling_methods = [ VonMisesRandomSampling(), - VonMisesFibSampling() + VonMisesFibSampling(), + VonMisesCartesianSampling(), ] def get_name(self): From 96afec4da4217b9d1af5fb25de0847e38e8991f5 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 25 Nov 2025 17:44:01 +0100 Subject: [PATCH 092/152] added cartesian watson --- .../distributions/sphere/watson/cartesian.py | 57 +++++++++++++++++++ .../distributions/sphere/watson/fibonachi.py | 9 +-- model/distributions/sphere/watson/watson.py | 2 + 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 model/distributions/sphere/watson/cartesian.py diff --git a/model/distributions/sphere/watson/cartesian.py b/model/distributions/sphere/watson/cartesian.py new file mode 100644 index 0000000..a42352a --- /dev/null +++ b/model/distributions/sphere/watson/cartesian.py @@ -0,0 +1,57 @@ +from abc import ABC, abstractmethod +import numpy as np +import scipy +from scipy.special import erf, erfi, erfinv + + +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from model.distributions.sphere.watson.fibonachi import WatsonFibonachiSampling as wf +from util.selectors.slider_square import SliderSquare +from util.cartesian_util import CartesianUtil as cu + +class WatsonCartesianSampling(SphereSamplingSchema): + def __init__(self): + self.sample_options = [ + SliderSquare("Number of Samples", 4, 64, 100, 4) + ] + self.info_md = """ + > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. + It is included for demonstration purposes only.""" + + def get_name(self): + return "Cartesian Grid" + + def sample(self, sample_options, distribution_options): + # map cartesian grid to watson, using inverse cdf closed form, see fibonachi.py for reference + + # for kappa = 0, w=cos(theta) is uniform in [−1,1] and phi is uniform in [0,2pi] + # this is contrary to uniform_samping where theta is uniform in [0, pi] and phi in [0, 2pi] + + + sample_count = sample_options[0].state + n = int(np.sqrt(sample_count)) + k = distribution_options[0].state # kappa + + grid = cu.generate_cartesian_grid(n, (1, 1)) + x, y = grid[:,0], grid[:,1] + x = 2*x -1 # map x from [0,1] to [-1, 1] + phi = 2 * np.pi * y # azimuthal angle, [0, 2pi] uniform + + if k > 0: + w = 1 / (np.sqrt(k)) * wf.erfi_inv( x * erfi(np.sqrt(k)) ) + elif k < 0: + la = -k + w = 1 / (np.sqrt(la)) * erfinv( x * erf(np.sqrt(la)) ) + elif k == 0: + w = x + + + w = np.clip(w, -1.0, 1.0) # clamp to avoid sqrt warnings due to numerical issues + + x_i_f_0 = w + x_i_f_1 = np.sqrt(1-w**2) * np.cos( phi) + x_i_f_2 = np.sqrt(1-w**2) * np.sin( phi) + x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) # order so that mu=[0, 0, 1] + return x_i_f + + diff --git a/model/distributions/sphere/watson/fibonachi.py b/model/distributions/sphere/watson/fibonachi.py index 170b24c..198f5a3 100644 --- a/model/distributions/sphere/watson/fibonachi.py +++ b/model/distributions/sphere/watson/fibonachi.py @@ -148,9 +148,8 @@ def f(p,w): x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) # order so that mu=[0, 0, 1] return x_i_f - - def sample_closed(self, sample_options, distribution_options): - def erfi_inv(x): + @staticmethod + def erfi_inv(x): x = np.asarray(x, dtype=float) def _scalar_inv(y): @@ -174,6 +173,8 @@ def _scalar_inv(y): return np.vectorize(_scalar_inv, otypes=[float])(x) + def sample_closed(self, sample_options, distribution_options): + sample_count = sample_options[0].state k = distribution_options[0].state # kappa @@ -183,7 +184,7 @@ def _scalar_inv(y): if k > 0: - w = 1 / (np.sqrt(k)) * erfi_inv( ((1-2*indices + sample_count)/ sample_count) * erfi(np.sqrt(k)) ) + w = 1 / (np.sqrt(k)) * self.erfi_inv( ((1-2*indices + sample_count)/ sample_count) * erfi(np.sqrt(k)) ) elif k < 0: la = -k w = 1 / (np.sqrt(la)) * erfinv( ((2*indices +1 - sample_count)/ sample_count) * erf(np.sqrt(la)) ) diff --git a/model/distributions/sphere/watson/watson.py b/model/distributions/sphere/watson/watson.py index fd8bfa3..45e5314 100644 --- a/model/distributions/sphere/watson/watson.py +++ b/model/distributions/sphere/watson/watson.py @@ -9,6 +9,7 @@ from model.distributions.sphere.watson.random_sampling import WatsonRandomSampling from model.distributions.sphere.watson.fibonachi import WatsonFibonachiSampling +from model.distributions.sphere.watson.cartesian import WatsonCartesianSampling class WatsonDistribution(SphereDistribution): @@ -20,6 +21,7 @@ def __init__(self): self.sampling_methods = [ WatsonRandomSampling(), WatsonFibonachiSampling(), + WatsonCartesianSampling(), ] def get_name(self): From e6e9c94ff9d174da71965ac56dd8f5f3947d843a Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 26 Nov 2025 11:19:47 +0100 Subject: [PATCH 093/152] use rank-1 fib for torus mesh --- model/torus/torus.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/model/torus/torus.py b/model/torus/torus.py index ce4b941..5c72469 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -6,8 +6,8 @@ from model.distributions.torus.torus_distribution import TorusDistribution from model.manifold import Manifold from renderer.PlotSettings2d import PlotSettings2D -from model.distributions.cylinder.uniform.fibonacci_kronecker import CylinderFibUniformSampling -from util.selectors.slider import Slider +from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling +from util.selectors.slider_fib import SliderFib class Torus(Manifold): def __init__(self, resolution=100, r=1, R=3): @@ -98,8 +98,14 @@ def generate_mesh(self, pdf, alpha=1): return xyz_extruded[:,0], xyz_extruded[:,1], xyz_extruded[:,2] - def _init_mesh(self, resolution=4000): - tp = CylinderFibUniformSampling.sample(None, [Slider("Number of Samples", 10, resolution, resolution)] , []) + def _init_mesh(self, resolution=(4181, 19)): + + #tp = CylinderFibRank1UniformSampling.sample(None, [SliderFib("Number of Samples", 10, resolution[0], resolution[0], resolution[1])] , []) + k = resolution[1] + samp_count = resolution[0] + t, p = CylinderFibRank1UniformSampling.get_rank_1(samp_count, k) + tp = np.column_stack((t * 2 * np.pi, p * 2 * np.pi)) + x, y, z = self.t_p_to_xyz(tp[:,0], tp[:,1], self.r) From 82c5c9c74032212f708de04290765b7058ca717f Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 26 Nov 2025 15:49:03 +0100 Subject: [PATCH 094/152] added fibonachi for wrapped gaussian looks kind of wrong in some angles, idk if thats intended --- .../torus/wrapped_normal/fibonacci.py | 68 +++++++++++++++++++ .../torus/wrapped_normal/wrapped_normal.py | 2 + 2 files changed, 70 insertions(+) create mode 100644 model/distributions/torus/wrapped_normal/fibonacci.py diff --git a/model/distributions/torus/wrapped_normal/fibonacci.py b/model/distributions/torus/wrapped_normal/fibonacci.py new file mode 100644 index 0000000..2cd1c32 --- /dev/null +++ b/model/distributions/torus/wrapped_normal/fibonacci.py @@ -0,0 +1,68 @@ +from abc import ABC, abstractmethod +import numpy as np +from scipy.stats import norm + +from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling +from util.selectors.slider_fib import SliderFib +from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema + +class TorusFibRank1WNSampling(TorusSamplingSchema): + def __init__(self): + self.sample_options = [ + SliderFib("Number of Samples", 2, 34, 21, 9) + ] + self.sampler = CylinderFibRank1UniformSampling() + + def get_name(self): + return "Fibonacci-Rank-1 Lattice" + + def sample(self, sample_options, distribution_options): + # see https://isas.iar.kit.edu/pdf/Fusion21_Frisch.pdf + sample_count = sample_options[0].state + + t, p = self.sampler.get_rank_1(sample_count, sample_options[0].idx) + + fib_grid = np.column_stack((t , p)) + + sigma_t = distribution_options[0].state + sigma_p = distribution_options[1].state + correlation = distribution_options[2].state + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = self.transform_grid_gaussian(fib_grid, np.pi, Cov) + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + gaus_grid[:,1] = gaus_grid[:,1] % (2 * np.pi) + return gaus_grid + + + @staticmethod + def transform_grid_gaussian(grid, mu, cov): + eps = 1e-9 + grid = np.clip(grid, eps, 1 - eps) # avoid inf in ppf + + gaus = norm.ppf(grid) + + var = np.mean(gaus**2, axis=0) + + gaus = gaus / np.sqrt(var) + + # scale with eigen decomposition + ew, V = np.linalg.eig(cov) + + D = np.diag(np.sqrt(ew)) + + gaus = gaus.T # (2,L) + + gaus = V @ D @ gaus # (2,2) @ (2,2) @ (2,L) -> (2,L) + + gaus = gaus.T # (L,2) + + gaus += mu # mu = [pi, pi] + + return gaus \ No newline at end of file diff --git a/model/distributions/torus/wrapped_normal/wrapped_normal.py b/model/distributions/torus/wrapped_normal/wrapped_normal.py index 9269e63..017c73a 100644 --- a/model/distributions/torus/wrapped_normal/wrapped_normal.py +++ b/model/distributions/torus/wrapped_normal/wrapped_normal.py @@ -5,6 +5,7 @@ from model.distributions.torus.torus_distribution import TorusDistribution from model.distributions.torus.wrapped_normal.random import TorusRandomWrappedSampling +from model.distributions.torus.wrapped_normal.fibonacci import TorusFibRank1WNSampling from model.torus.torus import Torus class UniformTorusDistribution(TorusDistribution): def __init__(self): @@ -15,6 +16,7 @@ def __init__(self): ] self.sampling_methods = [ TorusRandomWrappedSampling(), + TorusFibRank1WNSampling(), ] From 2af57b413ec38beaca3fc0e6c2e2eb209bf4cebf Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 26 Nov 2025 17:23:56 +0100 Subject: [PATCH 095/152] added slider manual input --- model/distributions/sphere/uniform/random.py | 5 +- renderer/Object3DAnd2DRenderer.py | 14 +-- renderer/Object3DRenderer.py | 99 +++++++++++++++++-- util/selectors/selector.py | 19 +++- util/selectors/silder_log.py | 6 +- util/selectors/silder_manual_input_wrapper.py | 51 ++++++++++ util/selectors/slider.py | 6 +- util/selectors/slider_fib.py | 30 +++++- util/selectors/slider_float.py | 6 +- util/selectors/slider_square.py | 18 +++- 10 files changed, 218 insertions(+), 36 deletions(-) create mode 100644 util/selectors/silder_manual_input_wrapper.py diff --git a/model/distributions/sphere/uniform/random.py b/model/distributions/sphere/uniform/random.py index cea582b..d43c17f 100644 --- a/model/distributions/sphere/uniform/random.py +++ b/model/distributions/sphere/uniform/random.py @@ -2,12 +2,13 @@ import numpy as np from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema -from util.selectors.silder_log import LogSlider +from util.selectors.silder_log import LogSlider +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper class SphereUniformRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000) + SliderManualInputWrapper(LogSlider("Number of Samples", 10, 100, 10000), None) ] def get_name(self): diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index b4d8554..76b0551 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -118,10 +118,10 @@ def register_plot_callbacks(self): Output(f"graph-{self.id}", "figure", allow_duplicate=True), State(f"mode-selector-{self.id}", "value"), Input(f"mode-done-{self.id}", "data"), - Input({"type": "dist", "index": ALL}, "value"), - State({"type": "dist", "index": ALL}, "id"), - Input({"type": "sampling", "index": ALL}, "value"), - State({"type": "sampling", "index": ALL}, "id"), + Input({"type": "dist", "renderer": self.id, "index": ALL}, "value"), + State({"type": "dist", "renderer": self.id, "index": ALL}, "id"), + Input({"type": "sampling", "renderer": self.id, "index": ALL}, "value"), + State({"type": "sampling", "renderer": self.id, "index": ALL}, "id"), Input("distribution-selector", "value"), Input(f"sampling-selector-{self.id}", "value"), Input(f"distribution-options-{self.id}", "children"), @@ -139,8 +139,8 @@ def update_plot_sample_callback(mode, _mode_counter, values_dist, ids_dist, valu Output(f"graph-{self.id}", "figure", allow_duplicate=True), State(f"mode-selector-{self.id}", "value"), Input(f"mode-done-{self.id}", "data"), - Input({"type": "dist", "index": ALL}, "value"), - State({"type": "dist", "index": ALL}, "id"), + Input({"type": "dist", "renderer": self.id, "index": ALL}, "value"), + State({"type": "dist", "renderer": self.id, "index": ALL}, "id"), Input("distribution-selector", "value"), Input(f"sampling-selector-{self.id}", "value"), Input(f"distribution-options-{self.id}", "children"), @@ -346,4 +346,4 @@ def get_layout_components(self): graph = [dcc.Graph(id=f"graph-{self.id}", figure=self.fig, config=self.config, style={'height': '100%'})] - return options, graph \ No newline at end of file + return options, graph diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 09811af..609efbe 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -1,5 +1,5 @@ from functools import lru_cache -from dash import html, dcc, callback, Input, Output, ALL, State, Patch, clientside_callback, ClientsideFunction +from dash import html, dcc, callback, Input, Output, ALL, State, Patch, clientside_callback, ClientsideFunction, MATCH, no_update import numpy as np import plotly.graph_objects as go import plotly.figure_factory as ff @@ -124,10 +124,10 @@ def update_sampling_methods(selected_distribution): def update_curr_distribution(selected_distribution, selected_sampling): # ids are given in the same order as options_dist and options_sampling options_dist = self.object.distributions[selected_distribution].distribution_options - options_dist_dcc = [opt.to_dash_component("dist", id) for id, opt in enumerate(options_dist)] + options_dist_dcc = [opt.to_dash_component("dist", id, self.id) for id, opt in enumerate(options_dist)] options_sampling = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling] - options_sampling_dcc = [opt.to_dash_component("sampling", id) for id, opt in enumerate(options_sampling.sample_options)] + options_sampling_dcc = [opt.to_dash_component("sampling", id, self.id) for id, opt in enumerate(options_sampling.sample_options)] dist_info_md = self.object.distributions[selected_distribution].info_md sampling_info_md = options_sampling.info_md @@ -138,15 +138,94 @@ def update_curr_distribution(selected_distribution, selected_sampling): return options_dist_dcc, options_sampling_dcc, dist_info_md, sampling_info_md, dist_hidden, sampling_hidden + + # optional manual input + @callback( + Output({"type": "sampling", "renderer": self.id, "index": MATCH}, "value"), + Output({"type": "manual_input-sampling", "renderer": self.id, "index": MATCH}, "value"), + Input({"type": "manual_input-sampling", "renderer": self.id, "index": MATCH}, "value"), + Input({"type": "sampling", "renderer": self.id, "index": MATCH}, "value"), + State("distribution-selector", "value"), + State(f"sampling-selector-{self.id}", "value"), + prevent_initial_call=True, + ) + def manual_input_changed(val, val_silder, selected_distribution, selected_sampling): + source = dash.ctx.triggered_id["type"] + + if val is None and source == "manual_input-sampling": + return no_update, no_update + + sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options + + # find the wrapper that called this + wrapper = None + for opt in sampling_options: + id = getattr(opt, "id", None) + if id is not None and id == dash.ctx.triggered_id["index"]: + wrapper = opt + break + + if wrapper is None: + return no_update, no_update + + if source == "manual_input-sampling": # manual input changed, update slider + + if wrapper.check_input is None or wrapper.check_input(val): + slider_value = wrapper.update_state_manual(val) + return slider_value, no_update + else: + return no_update, no_update + + else: # slider changed, update manual input + return no_update, wrapper.slider.transfrom_up(val_silder) + + @callback( + Output({"type": "dist", "renderer": self.id, "index": MATCH}, "value"), + Output({"type": "manual_input-dist", "renderer": self.id, "index": MATCH}, "value"), + Input({"type": "manual_input-dist", "renderer": self.id, "index": MATCH}, "value"), + Input({"type": "dist", "renderer": self.id, "index": MATCH}, "value"), + State("distribution-selector", "value"), + State(f"sampling-selector-{self.id}", "value"), + prevent_initial_call=True, + ) + def manual_input_dist_changed(val_manual, val_slider, selected_distribution, selected_sampling): + source = dash.ctx.triggered_id["type"] + + if val_manual is None and source == "manual_input-dist": + return no_update, no_update + + dist_options = self.object.distributions[selected_distribution].distribution_options + + wrapper = None + for opt in dist_options: + opt_id = getattr(opt, "id", None) + if opt_id is not None and opt_id == dash.ctx.triggered_id["index"]: + wrapper = opt + break + + if wrapper is None: + return no_update, no_update + + if source == "manual_input-dist": + # check_input is given by distribution/ sampling method, if None, no special constraints are given + # slider.is_valid is given by the slider itself, can be less strict + if (wrapper.check_input is None or wrapper.check_input(val_manual)) and wrapper.slider.is_valid(val_manual): + slider_value = wrapper.update_state_manual(val_manual) + return slider_value, no_update + return no_update, no_update + + # slider changed, sync manual input display + return no_update, wrapper.slider.transfrom_up(val_slider) + def _register_3d_plot_callbacks(self): # updates the plot based on selected sampling options @callback( Output(f"graph-{self.id}", "figure", allow_duplicate=True), - Input({"type": "dist", "index": ALL}, "value"), - State({"type": "dist", "index": ALL}, "id"), - Input({"type": "sampling", "index": ALL}, "value"), - State({"type": "sampling", "index": ALL}, "id"), + Input({"type": "dist", "renderer": self.id, "index": ALL}, "value"), + State({"type": "dist", "renderer": self.id, "index": ALL}, "id"), + Input({"type": "sampling", "renderer": self.id, "index": ALL}, "value"), + State({"type": "sampling", "renderer": self.id, "index": ALL}, "id"), Input("distribution-selector", "value"), Input(f"sampling-selector-{self.id}", "value"), Input(f"distribution-options-{self.id}", "children"), @@ -159,8 +238,8 @@ def update_plot_sample_callback(values_dist, ids_dist, values_samp, ids_samp, se # updates the plot based on selected distribution options @callback( Output(f"graph-{self.id}", "figure", allow_duplicate=True), - Input({"type": "dist", "index": ALL}, "value"), - State({"type": "dist", "index": ALL}, "id"), + Input({"type": "dist", "renderer": self.id, "index": ALL}, "value"), + State({"type": "dist", "renderer": self.id, "index": ALL}, "id"), Input("distribution-selector", "value"), Input(f"sampling-selector-{self.id}", "value"), Input(f"distribution-options-{self.id}", "children"), @@ -290,4 +369,4 @@ def get_layout_components(self): graph = [dcc.Graph(id=f"graph-{self.id}", figure=self.fig, config=self.config, style={'height': '100%'})] - return options, graph \ No newline at end of file + return options, graph diff --git a/util/selectors/selector.py b/util/selectors/selector.py index 267489e..5708c51 100644 --- a/util/selectors/selector.py +++ b/util/selectors/selector.py @@ -8,5 +8,20 @@ def __init__(self): # has the sideeffect of updating self.id @abstractmethod - def to_dash_component(self, id): - pass \ No newline at end of file + def to_dash_component(self, _type, id, renderer_id): + pass + + def transfrom_down(x): + # identity by default, override in subclasses if needed + # does need to be defined if dash slider_values do not correspond to self.state + return x + + def transfrom_up(x): + # identity by default, override in subclasses if needed + # does need to be defined if dash slider_values do not correspond to self.state + return x + + def is_valid(x): + # by default all values are valid + # override in subclasses if needed + return True diff --git a/util/selectors/silder_log.py b/util/selectors/silder_log.py index f58bcbf..d442d49 100644 --- a/util/selectors/silder_log.py +++ b/util/selectors/silder_log.py @@ -62,13 +62,13 @@ def transfrom_down(self, value): return 0 return np.log10(value) - def to_dash_component(self, _type, id): + def to_dash_component(self, _type, id, renderer_id): self.id = id return html.Div([ html.Label(self.name), dcc.Slider( - id={"type": _type, "index": id}, + id={"type": _type, "index": id, "renderer": renderer_id}, min=self.log_min, max=self.log_max, value=self.transfrom_down(self.state), @@ -80,4 +80,4 @@ def to_dash_component(self, _type, id): ]) def update_state(self, new_state): - self.state = self.transfrom_up(new_state) \ No newline at end of file + self.state = self.transfrom_up(new_state) diff --git a/util/selectors/silder_manual_input_wrapper.py b/util/selectors/silder_manual_input_wrapper.py new file mode 100644 index 0000000..12f37f4 --- /dev/null +++ b/util/selectors/silder_manual_input_wrapper.py @@ -0,0 +1,51 @@ +from util.selectors.selector import Selector +from dash import dcc, html, callback, Input, Output, ALL + + +class SliderManualInputWrapper(): + def __init__(self, slider, check_input): + self.id = None # unique id for dash callback + self.slider = slider + self.check_input = check_input + + def to_dash_component(self, _type, id, renderer_id): + slider_component = self.slider.to_dash_component(_type, id, renderer_id) + self.id = id + self.renderer_id = renderer_id + + component = html.Div([ + html.Div( + children=[slider_component], + #style={"flex": "1"} + style={"display": "inline-block", "width": r"calc(100% - 5rem)", "verticalAlign": "bottom"} + ), + html.Div( + children=[dcc.Input( + id={"type": f"manual_input-{_type}", "index": id, "renderer": renderer_id}, + type="number", + value=self.slider.state, + #style={"width": "5rem"}, + style={"width": "100%"}, + )], + style={"display": "inline-block", "width": "5rem", "verticalAlign": "bottom", "margin-bottom": "1rem"} + ), + + ]) + return component + + def update_state(self, new_state): # only called by slider callback, gauranteed valid + self.slider.update_state(new_state) + + def update_state_manual(self, manual_value): + #Update from a manual text/number input; converts to slider domain if needed.""" + if hasattr(self.slider, "transfrom_down"): + slider_value = self.slider.transfrom_down(manual_value) + else: + slider_value = manual_value + + self.slider.update_state(slider_value) + return slider_value + + @property + def state(self): + return self.slider.state diff --git a/util/selectors/slider.py b/util/selectors/slider.py index 54581f4..61ee9b3 100644 --- a/util/selectors/slider.py +++ b/util/selectors/slider.py @@ -48,13 +48,13 @@ def round_nice_number(x): return marks - def to_dash_component(self, _type, id): + def to_dash_component(self, _type, id, renderer_id): self.id = id return html.Div([ html.Label(self.name), dcc.Slider( - id={"type": _type, "index": id}, + id={"type": _type, "index": id, "renderer": renderer_id}, min=self.min, max=self.max, value=self.state, @@ -66,4 +66,4 @@ def to_dash_component(self, _type, id): ]) def update_state(self, new_state): - self.state = int(new_state) \ No newline at end of file + self.state = int(new_state) diff --git a/util/selectors/slider_fib.py b/util/selectors/slider_fib.py index 262f653..8c9f5f1 100644 --- a/util/selectors/slider_fib.py +++ b/util/selectors/slider_fib.py @@ -20,13 +20,13 @@ def __init__(self, name, min, state, max, idx): self.id = None - def to_dash_component(self, _type, id): + def to_dash_component(self, _type, id, renderer_id): self.id = id return html.Div([ html.Label(self.name), dcc.Slider( - id={"type": _type, "index": id}, + id={"type": _type, "index": id, "renderer": renderer_id}, min=self.min, max=self.max, value=self.idx, @@ -51,4 +51,28 @@ def update_state(self, new_state): self.state = int(sp.fibonacci(new_state)) self.idx = int(new_state) - \ No newline at end of file + def transfrom_up(x): + return int(sp.fibonacci(x)) + + def transfrom_down(x): + for i in range(0, x + 1): + if sp.fibonacci(i) == x: + return i + raise ValueError(f"{x} is not a Fibonacci number") + + @staticmethod + def is_valid(n): + if n < 0: + return False + # A number is a Fibonacci number if and only if one or both of (5*n^2 + 4) or (5*n^2 - 4) is a perfect square + # https://en.wikipedia.org/wiki/Fibonacci_sequence + test1 = 5 * n * n + 4 + test2 = 5 * n * n - 4 + + def is_perfect_square(x): + s = int(np.sqrt(x)) + return s * s == x + + return is_perfect_square(test1) or is_perfect_square(test2) + + diff --git a/util/selectors/slider_float.py b/util/selectors/slider_float.py index 6954828..fcbd1f2 100644 --- a/util/selectors/slider_float.py +++ b/util/selectors/slider_float.py @@ -15,7 +15,7 @@ def __init__(self, name, min, state, max, transform_tooltip=None): self.transform_tooltip = transform_tooltip - def to_dash_component(self, _type, id): + def to_dash_component(self, _type, id, renderer_id): if self.transform_tooltip is None: tooltip = {"placement": "bottom", "always_visible": True} else: @@ -26,7 +26,7 @@ def to_dash_component(self, _type, id): html.Label(self.name), dcc.Slider( - id={"type": _type, "index": id}, + id={"type": _type, "index": id, "renderer": renderer_id}, min=self.min, max=self.max, value=self.state, @@ -36,4 +36,4 @@ def to_dash_component(self, _type, id): ]) def update_state(self, new_state): - self.state = new_state \ No newline at end of file + self.state = new_state diff --git a/util/selectors/slider_square.py b/util/selectors/slider_square.py index 8f7ce18..3620472 100644 --- a/util/selectors/slider_square.py +++ b/util/selectors/slider_square.py @@ -20,13 +20,13 @@ def __init__(self, name, min, state, max, idx): self.id = None - def to_dash_component(self, _type, id): + def to_dash_component(self, _type, id, renderer_id): self.id = id return html.Div([ html.Label(self.name), dcc.Slider( - id={"type": _type, "index": id}, + id={"type": _type, "index": id, "renderer": renderer_id}, min=self.min, max=self.max, value=self.idx, @@ -51,4 +51,16 @@ def update_state(self, new_state): self.state = int((new_state**2)) self.idx = int(new_state) - \ No newline at end of file + def transfrom_up(x): + return int((x**2)) + + def transfrom_down(x): + for i in range(0, int(np.sqrt(x)) + 1): + if (i**2) == x: + return i + raise ValueError(f"{x} is not a perfect square number") + + @staticmethod + def is_valid(x): + s = int(np.sqrt(x)) + return s * s == x From e6b71f2be00b2a9e8423d385df2358ba0e6bdb62 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 26 Nov 2025 20:11:34 +0100 Subject: [PATCH 096/152] added manual input to every samplecount slider --- .../cylinder/partially_wraped_normal/random.py | 4 +++- model/distributions/cylinder/uniform/cartesian.py | 3 ++- model/distributions/cylinder/uniform/fibonacci_kronecker.py | 3 ++- model/distributions/cylinder/uniform/fibonacci_rank_1.py | 3 ++- model/distributions/cylinder/uniform/random.py | 4 +++- model/distributions/sphere/bingham/random.py | 5 ++++- model/distributions/sphere/kent/random.py | 5 +++-- model/distributions/sphere/uniform/cartesian.py | 4 +++- model/distributions/sphere/uniform/fibonacci_rank_1.py | 4 +++- model/distributions/sphere/uniform/fibonachi_lattice.py | 6 ++++-- model/distributions/sphere/uniform/random.py | 4 ++-- model/distributions/sphere/vonmises_fisher/cartesian.py | 4 +++- model/distributions/sphere/vonmises_fisher/fibonachi.py | 3 ++- model/distributions/sphere/vonmises_fisher/random.py | 4 +++- model/distributions/sphere/watson/cartesian.py | 4 +++- model/distributions/sphere/watson/fibonachi.py | 4 +++- model/distributions/sphere/watson/random_sampling.py | 4 +++- model/distributions/torus/uniform/cartesian.py | 3 ++- model/distributions/torus/uniform/fibonacci_kronecker.py | 3 ++- model/distributions/torus/uniform/fibonacci_rank_1.py | 3 ++- model/distributions/torus/uniform/random.py | 4 +++- model/distributions/torus/wrapped_normal/fibonacci.py | 4 +++- model/distributions/torus/wrapped_normal/random.py | 3 ++- util/selectors/silder_manual_input_wrapper.py | 6 +++++- util/selectors/slider_fib.py | 4 ++-- util/selectors/slider_square.py | 4 ++-- 26 files changed, 71 insertions(+), 31 deletions(-) diff --git a/model/distributions/cylinder/partially_wraped_normal/random.py b/model/distributions/cylinder/partially_wraped_normal/random.py index cedd9ea..7d20984 100644 --- a/model/distributions/cylinder/partially_wraped_normal/random.py +++ b/model/distributions/cylinder/partially_wraped_normal/random.py @@ -2,11 +2,13 @@ from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema from util.selectors.silder_log import LogSlider import numpy as np +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class CylinderRandomPWNSampling(CylinderSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000) + MI(LogSlider("Number of Samples", 10, 100, 10000)) ] def get_name(self): diff --git a/model/distributions/cylinder/uniform/cartesian.py b/model/distributions/cylinder/uniform/cartesian.py index bc59a64..7735965 100644 --- a/model/distributions/cylinder/uniform/cartesian.py +++ b/model/distributions/cylinder/uniform/cartesian.py @@ -4,12 +4,13 @@ from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema from util.selectors.slider_square import SliderSquare from util.cartesian_util import CartesianUtil as cu +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI class CylinderCartesianUniformSampling(CylinderSamplingSchema): def __init__(self): self.sample_options = [ - SliderSquare("Number of Samples", 4, 64, 100, 4) + MI(SliderSquare("Number of Samples", 4, 64, 100, 4)) ] self.info_md = """ > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. diff --git a/model/distributions/cylinder/uniform/fibonacci_kronecker.py b/model/distributions/cylinder/uniform/fibonacci_kronecker.py index 990e391..5839222 100644 --- a/model/distributions/cylinder/uniform/fibonacci_kronecker.py +++ b/model/distributions/cylinder/uniform/fibonacci_kronecker.py @@ -2,11 +2,12 @@ from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema from util.selectors.silder_log import LogSlider import numpy as np +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI class CylinderFibUniformSampling(CylinderSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000) + MI(LogSlider("Number of Samples", 10, 100, 10000)) ] def get_name(self): diff --git a/model/distributions/cylinder/uniform/fibonacci_rank_1.py b/model/distributions/cylinder/uniform/fibonacci_rank_1.py index 7466571..007c0c2 100644 --- a/model/distributions/cylinder/uniform/fibonacci_rank_1.py +++ b/model/distributions/cylinder/uniform/fibonacci_rank_1.py @@ -3,11 +3,12 @@ from util.selectors.slider_fib import SliderFib import numpy as np import sympy as sp +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI class CylinderFibRank1UniformSampling(CylinderSamplingSchema): def __init__(self): self.sample_options = [ - SliderFib("Number of Samples", 2, 34, 21, 9) + MI(SliderFib("Number of Samples", 2, 34, 21, 9)) ] def get_name(self): diff --git a/model/distributions/cylinder/uniform/random.py b/model/distributions/cylinder/uniform/random.py index 643f578..8d1746d 100644 --- a/model/distributions/cylinder/uniform/random.py +++ b/model/distributions/cylinder/uniform/random.py @@ -2,11 +2,13 @@ from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema from util.selectors.silder_log import LogSlider import numpy as np +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class CylinderRandomUniformSampling(CylinderSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000) + MI(LogSlider("Number of Samples", 10, 100, 10000)) ] def get_name(self): diff --git a/model/distributions/sphere/bingham/random.py b/model/distributions/sphere/bingham/random.py index 66730cd..6251dfb 100644 --- a/model/distributions/sphere/bingham/random.py +++ b/model/distributions/sphere/bingham/random.py @@ -5,11 +5,14 @@ from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema from util.selectors.silder_log import LogSlider +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class BinghamRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000) ] + MI(LogSlider("Number of Samples", 10, 100, 10000)) + ] def get_name(self): return "Random" diff --git a/model/distributions/sphere/kent/random.py b/model/distributions/sphere/kent/random.py index b0bcd42..0f8ce76 100644 --- a/model/distributions/sphere/kent/random.py +++ b/model/distributions/sphere/kent/random.py @@ -6,13 +6,14 @@ from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema from util.selectors.silder_log import LogSlider -from model.sphere.sphere import Sphere +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class KentRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000), + MI(LogSlider("Number of Samples", 10, 100, 10000)), ] def get_name(self): diff --git a/model/distributions/sphere/uniform/cartesian.py b/model/distributions/sphere/uniform/cartesian.py index 874d569..bd71453 100644 --- a/model/distributions/sphere/uniform/cartesian.py +++ b/model/distributions/sphere/uniform/cartesian.py @@ -4,11 +4,13 @@ from util.selectors.slider_square import SliderSquare from util.cartesian_util import CartesianUtil as cu from model.sphere.sphere import Sphere +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class SphereCartesianUniformSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - SliderSquare("Number of Samples", 4, 64, 100, 4) + MI(SliderSquare("Number of Samples", 4, 64, 100, 4)) ] self.info_md = """ > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. diff --git a/model/distributions/sphere/uniform/fibonacci_rank_1.py b/model/distributions/sphere/uniform/fibonacci_rank_1.py index c17314f..0515491 100644 --- a/model/distributions/sphere/uniform/fibonacci_rank_1.py +++ b/model/distributions/sphere/uniform/fibonacci_rank_1.py @@ -5,11 +5,13 @@ from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling from util.selectors.slider_fib import SliderFib from model.sphere.sphere import Sphere +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class SphereFibRank1UniformSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - SliderFib("Number of Samples", 2, 34, 21, 9) + MI(SliderFib("Number of Samples", 2, 34, 21, 9)) ] self.sampler = CylinderFibRank1UniformSampling() diff --git a/model/distributions/sphere/uniform/fibonachi_lattice.py b/model/distributions/sphere/uniform/fibonachi_lattice.py index 0f53060..558526d 100644 --- a/model/distributions/sphere/uniform/fibonachi_lattice.py +++ b/model/distributions/sphere/uniform/fibonachi_lattice.py @@ -3,12 +3,14 @@ from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema -from util.selectors.silder_log import LogSlider +from util.selectors.silder_log import LogSlider +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class SphereUniformFibSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000) + MI(LogSlider("Number of Samples", 10, 100, 10000)) ] def get_name(self): diff --git a/model/distributions/sphere/uniform/random.py b/model/distributions/sphere/uniform/random.py index d43c17f..4735ac9 100644 --- a/model/distributions/sphere/uniform/random.py +++ b/model/distributions/sphere/uniform/random.py @@ -3,12 +3,12 @@ from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema from util.selectors.silder_log import LogSlider -from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI class SphereUniformRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - SliderManualInputWrapper(LogSlider("Number of Samples", 10, 100, 10000), None) + MI(LogSlider("Number of Samples", 10, 100, 10000)) ] def get_name(self): diff --git a/model/distributions/sphere/vonmises_fisher/cartesian.py b/model/distributions/sphere/vonmises_fisher/cartesian.py index 3bc50dd..3dfd91c 100644 --- a/model/distributions/sphere/vonmises_fisher/cartesian.py +++ b/model/distributions/sphere/vonmises_fisher/cartesian.py @@ -5,11 +5,13 @@ from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema from util.selectors.slider_square import SliderSquare from util.cartesian_util import CartesianUtil as cu +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class VonMisesCartesianSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - SliderSquare("Number of Samples", 4, 64, 100, 4) + MI(SliderSquare("Number of Samples", 4, 64, 100, 4)) ] self.info_md = """ > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. diff --git a/model/distributions/sphere/vonmises_fisher/fibonachi.py b/model/distributions/sphere/vonmises_fisher/fibonachi.py index a89d6d9..6c63da0 100644 --- a/model/distributions/sphere/vonmises_fisher/fibonachi.py +++ b/model/distributions/sphere/vonmises_fisher/fibonachi.py @@ -4,11 +4,12 @@ from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema from util.selectors.silder_log import LogSlider +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI class VonMisesFibSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000), + MI(LogSlider("Number of Samples", 10, 100, 10000)), ] def get_name(self): diff --git a/model/distributions/sphere/vonmises_fisher/random.py b/model/distributions/sphere/vonmises_fisher/random.py index ab01883..3910dd7 100644 --- a/model/distributions/sphere/vonmises_fisher/random.py +++ b/model/distributions/sphere/vonmises_fisher/random.py @@ -4,11 +4,13 @@ from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema from util.selectors.silder_log import LogSlider +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class VonMisesRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000), + MI(LogSlider("Number of Samples", 10, 100, 10000)), ] def get_name(self): diff --git a/model/distributions/sphere/watson/cartesian.py b/model/distributions/sphere/watson/cartesian.py index a42352a..49f7e21 100644 --- a/model/distributions/sphere/watson/cartesian.py +++ b/model/distributions/sphere/watson/cartesian.py @@ -8,11 +8,13 @@ from model.distributions.sphere.watson.fibonachi import WatsonFibonachiSampling as wf from util.selectors.slider_square import SliderSquare from util.cartesian_util import CartesianUtil as cu +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class WatsonCartesianSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - SliderSquare("Number of Samples", 4, 64, 100, 4) + MI(SliderSquare("Number of Samples", 4, 64, 100, 4)) ] self.info_md = """ > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. diff --git a/model/distributions/sphere/watson/fibonachi.py b/model/distributions/sphere/watson/fibonachi.py index 198f5a3..a4417bf 100644 --- a/model/distributions/sphere/watson/fibonachi.py +++ b/model/distributions/sphere/watson/fibonachi.py @@ -11,12 +11,14 @@ from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema from util.selectors.silder_log import LogSlider from model.sphere.sphere import Sphere +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class WatsonFibonachiSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000), + MI(LogSlider("Number of Samples", 10, 100, 10000)), ] def get_name(self): diff --git a/model/distributions/sphere/watson/random_sampling.py b/model/distributions/sphere/watson/random_sampling.py index 1e8338a..8028f4f 100644 --- a/model/distributions/sphere/watson/random_sampling.py +++ b/model/distributions/sphere/watson/random_sampling.py @@ -6,12 +6,14 @@ from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema from util.selectors.silder_log import LogSlider from model.sphere.sphere import Sphere +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class WatsonRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000), + MI(LogSlider("Number of Samples", 10, 100, 10000)), ] def get_name(self): diff --git a/model/distributions/torus/uniform/cartesian.py b/model/distributions/torus/uniform/cartesian.py index 2a8feb6..16f6e4f 100644 --- a/model/distributions/torus/uniform/cartesian.py +++ b/model/distributions/torus/uniform/cartesian.py @@ -3,12 +3,13 @@ from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema from util.selectors.slider_square import SliderSquare from util.cartesian_util import CartesianUtil as cu +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI class TorusCartesianUniformSampling(TorusSamplingSchema): def __init__(self): self.sample_options = [ - SliderSquare("Number of Samples", 4, 64, 100, 4) + MI(SliderSquare("Number of Samples", 4, 64, 100, 4)) ] self.info_md = """ > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. diff --git a/model/distributions/torus/uniform/fibonacci_kronecker.py b/model/distributions/torus/uniform/fibonacci_kronecker.py index 606e52c..e2edd17 100644 --- a/model/distributions/torus/uniform/fibonacci_kronecker.py +++ b/model/distributions/torus/uniform/fibonacci_kronecker.py @@ -3,11 +3,12 @@ from util.selectors.silder_log import LogSlider from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI class TorusKroneckerUniformSampling(TorusSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000) + MI(LogSlider("Number of Samples", 10, 100, 10000)) ] self.info_md = """ > Warning: Mapping the Kronecker lattice to the torus is not recommended in practice, as it is only periodic on one axis. diff --git a/model/distributions/torus/uniform/fibonacci_rank_1.py b/model/distributions/torus/uniform/fibonacci_rank_1.py index be36619..e555d3d 100644 --- a/model/distributions/torus/uniform/fibonacci_rank_1.py +++ b/model/distributions/torus/uniform/fibonacci_rank_1.py @@ -4,11 +4,12 @@ from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling from util.selectors.slider_fib import SliderFib from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI class TorusFibRank1UniformSampling(TorusSamplingSchema): def __init__(self): self.sample_options = [ - SliderFib("Number of Samples", 2, 34, 21, 9) + MI(SliderFib("Number of Samples", 2, 34, 21, 9)) ] self.sampler = CylinderFibRank1UniformSampling() diff --git a/model/distributions/torus/uniform/random.py b/model/distributions/torus/uniform/random.py index 8c49fe3..015eddc 100644 --- a/model/distributions/torus/uniform/random.py +++ b/model/distributions/torus/uniform/random.py @@ -2,11 +2,13 @@ from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema from util.selectors.silder_log import LogSlider import numpy as np +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class TorusRandomUniformSampling(TorusSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000) + MI(LogSlider("Number of Samples", 10, 100, 10000)) ] def get_name(self): diff --git a/model/distributions/torus/wrapped_normal/fibonacci.py b/model/distributions/torus/wrapped_normal/fibonacci.py index 2cd1c32..9d19d52 100644 --- a/model/distributions/torus/wrapped_normal/fibonacci.py +++ b/model/distributions/torus/wrapped_normal/fibonacci.py @@ -5,11 +5,13 @@ from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling from util.selectors.slider_fib import SliderFib from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI + class TorusFibRank1WNSampling(TorusSamplingSchema): def __init__(self): self.sample_options = [ - SliderFib("Number of Samples", 2, 34, 21, 9) + MI(SliderFib("Number of Samples", 2, 34, 21, 9)) ] self.sampler = CylinderFibRank1UniformSampling() diff --git a/model/distributions/torus/wrapped_normal/random.py b/model/distributions/torus/wrapped_normal/random.py index 8a00d8d..b1fd1ef 100644 --- a/model/distributions/torus/wrapped_normal/random.py +++ b/model/distributions/torus/wrapped_normal/random.py @@ -2,11 +2,12 @@ from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema from util.selectors.silder_log import LogSlider import numpy as np +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI class TorusRandomWrappedSampling(TorusSamplingSchema): def __init__(self): self.sample_options = [ - LogSlider("Number of Samples", 10, 100, 10000) + MI(LogSlider("Number of Samples", 10, 100, 10000)) ] def get_name(self): diff --git a/util/selectors/silder_manual_input_wrapper.py b/util/selectors/silder_manual_input_wrapper.py index 12f37f4..8eec34d 100644 --- a/util/selectors/silder_manual_input_wrapper.py +++ b/util/selectors/silder_manual_input_wrapper.py @@ -3,7 +3,7 @@ class SliderManualInputWrapper(): - def __init__(self, slider, check_input): + def __init__(self, slider, check_input=None): self.id = None # unique id for dash callback self.slider = slider self.check_input = check_input @@ -49,3 +49,7 @@ def update_state_manual(self, manual_value): @property def state(self): return self.slider.state + + @property + def idx(self): + return self.slider.idx diff --git a/util/selectors/slider_fib.py b/util/selectors/slider_fib.py index 8c9f5f1..9bb2523 100644 --- a/util/selectors/slider_fib.py +++ b/util/selectors/slider_fib.py @@ -51,10 +51,10 @@ def update_state(self, new_state): self.state = int(sp.fibonacci(new_state)) self.idx = int(new_state) - def transfrom_up(x): + def transfrom_up(self, x): return int(sp.fibonacci(x)) - def transfrom_down(x): + def transfrom_down(self, x): for i in range(0, x + 1): if sp.fibonacci(i) == x: return i diff --git a/util/selectors/slider_square.py b/util/selectors/slider_square.py index 3620472..5ebd749 100644 --- a/util/selectors/slider_square.py +++ b/util/selectors/slider_square.py @@ -51,10 +51,10 @@ def update_state(self, new_state): self.state = int((new_state**2)) self.idx = int(new_state) - def transfrom_up(x): + def transfrom_up(self, x): return int((x**2)) - def transfrom_down(x): + def transfrom_down(self, x): for i in range(0, int(np.sqrt(x)) + 1): if (i**2) == x: return i From aaa5b1d9007b0d0e945c70db087f670c55cd86a9 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 26 Nov 2025 20:21:59 +0100 Subject: [PATCH 097/152] fix bugs with manual input --- model/distributions/sphere/bingham/random.py | 2 +- renderer/Object3DAnd2DRenderer.py | 12 ++-- renderer/Object3DRenderer.py | 60 +++++++++---------- util/selectors/selector.py | 6 +- util/selectors/silder_log.py | 6 +- util/selectors/silder_manual_input_wrapper.py | 4 +- util/selectors/slider.py | 6 +- util/selectors/slider_fib.py | 14 ++--- util/selectors/slider_float.py | 6 +- util/selectors/slider_square.py | 12 ++-- 10 files changed, 68 insertions(+), 60 deletions(-) diff --git a/model/distributions/sphere/bingham/random.py b/model/distributions/sphere/bingham/random.py index 6251dfb..86c65b1 100644 --- a/model/distributions/sphere/bingham/random.py +++ b/model/distributions/sphere/bingham/random.py @@ -12,7 +12,7 @@ class BinghamRandomSampling(SphereSamplingSchema): def __init__(self): self.sample_options = [ MI(LogSlider("Number of Samples", 10, 100, 10000)) - ] + ] def get_name(self): return "Random" diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index 76b0551..b0be4c4 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -118,10 +118,10 @@ def register_plot_callbacks(self): Output(f"graph-{self.id}", "figure", allow_duplicate=True), State(f"mode-selector-{self.id}", "value"), Input(f"mode-done-{self.id}", "data"), - Input({"type": "dist", "renderer": self.id, "index": ALL}, "value"), - State({"type": "dist", "renderer": self.id, "index": ALL}, "id"), - Input({"type": "sampling", "renderer": self.id, "index": ALL}, "value"), - State({"type": "sampling", "renderer": self.id, "index": ALL}, "id"), + Input({"type": "dist", "renderer": self.id, "index": ALL, "manual": ALL}, "value"), + State({"type": "dist", "renderer": self.id, "index": ALL, "manual": ALL}, "id"), + Input({"type": "sampling", "renderer": self.id, "index": ALL, "manual": ALL}, "value"), + State({"type": "sampling", "renderer": self.id, "index": ALL, "manual": ALL}, "id"), Input("distribution-selector", "value"), Input(f"sampling-selector-{self.id}", "value"), Input(f"distribution-options-{self.id}", "children"), @@ -139,8 +139,8 @@ def update_plot_sample_callback(mode, _mode_counter, values_dist, ids_dist, valu Output(f"graph-{self.id}", "figure", allow_duplicate=True), State(f"mode-selector-{self.id}", "value"), Input(f"mode-done-{self.id}", "data"), - Input({"type": "dist", "renderer": self.id, "index": ALL}, "value"), - State({"type": "dist", "renderer": self.id, "index": ALL}, "id"), + Input({"type": "dist", "renderer": self.id, "index": ALL, "manual": ALL}, "value"), + State({"type": "dist", "renderer": self.id, "index": ALL, "manual": ALL}, "id"), Input("distribution-selector", "value"), Input(f"sampling-selector-{self.id}", "value"), Input(f"distribution-options-{self.id}", "children"), diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 609efbe..01639e5 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -141,10 +141,10 @@ def update_curr_distribution(selected_distribution, selected_sampling): # optional manual input @callback( - Output({"type": "sampling", "renderer": self.id, "index": MATCH}, "value"), + Output({"type": "sampling", "renderer": self.id, "index": MATCH, "manual": True}, "value"), Output({"type": "manual_input-sampling", "renderer": self.id, "index": MATCH}, "value"), Input({"type": "manual_input-sampling", "renderer": self.id, "index": MATCH}, "value"), - Input({"type": "sampling", "renderer": self.id, "index": MATCH}, "value"), + Input({"type": "sampling", "renderer": self.id, "index": MATCH, "manual": True}, "value"), State("distribution-selector", "value"), State(f"sampling-selector-{self.id}", "value"), prevent_initial_call=True, @@ -170,7 +170,7 @@ def manual_input_changed(val, val_silder, selected_distribution, selected_sampli if source == "manual_input-sampling": # manual input changed, update slider - if wrapper.check_input is None or wrapper.check_input(val): + if (wrapper.check_input is None or wrapper.check_input(val)) and wrapper.slider.is_valid(val): slider_value = wrapper.update_state_manual(val) return slider_value, no_update else: @@ -180,10 +180,10 @@ def manual_input_changed(val, val_silder, selected_distribution, selected_sampli return no_update, wrapper.slider.transfrom_up(val_silder) @callback( - Output({"type": "dist", "renderer": self.id, "index": MATCH}, "value"), + Output({"type": "dist", "renderer": self.id, "index": MATCH, "manual": True}, "value"), Output({"type": "manual_input-dist", "renderer": self.id, "index": MATCH}, "value"), Input({"type": "manual_input-dist", "renderer": self.id, "index": MATCH}, "value"), - Input({"type": "dist", "renderer": self.id, "index": MATCH}, "value"), + Input({"type": "dist", "renderer": self.id, "index": MATCH, "manual": True}, "value"), State("distribution-selector", "value"), State(f"sampling-selector-{self.id}", "value"), prevent_initial_call=True, @@ -222,10 +222,10 @@ def _register_3d_plot_callbacks(self): # updates the plot based on selected sampling options @callback( Output(f"graph-{self.id}", "figure", allow_duplicate=True), - Input({"type": "dist", "renderer": self.id, "index": ALL}, "value"), - State({"type": "dist", "renderer": self.id, "index": ALL}, "id"), - Input({"type": "sampling", "renderer": self.id, "index": ALL}, "value"), - State({"type": "sampling", "renderer": self.id, "index": ALL}, "id"), + Input({"type": "dist", "renderer": self.id, "index": ALL, "manual": ALL}, "value"), + State({"type": "dist", "renderer": self.id, "index": ALL, "manual": ALL}, "id"), + Input({"type": "sampling", "renderer": self.id, "index": ALL, "manual": ALL}, "value"), + State({"type": "sampling", "renderer": self.id, "index": ALL, "manual": ALL}, "id"), Input("distribution-selector", "value"), Input(f"sampling-selector-{self.id}", "value"), Input(f"distribution-options-{self.id}", "children"), @@ -238,8 +238,8 @@ def update_plot_sample_callback(values_dist, ids_dist, values_samp, ids_samp, se # updates the plot based on selected distribution options @callback( Output(f"graph-{self.id}", "figure", allow_duplicate=True), - Input({"type": "dist", "renderer": self.id, "index": ALL}, "value"), - State({"type": "dist", "renderer": self.id, "index": ALL}, "id"), + Input({"type": "dist", "renderer": self.id, "index": ALL, "manual": ALL}, "value"), + State({"type": "dist", "renderer": self.id, "index": ALL, "manual": ALL}, "id"), Input("distribution-selector", "value"), Input(f"sampling-selector-{self.id}", "value"), Input(f"distribution-options-{self.id}", "children"), @@ -298,33 +298,33 @@ def update_plot_sample(self, values_dist, ids_dist, values_samp, ids_samp, selec return patched_figure def update_plot_dist(self, values_dist, ids_dist, selected_distribution, selected_sampling, _): - dist_options = self.object.distributions[selected_distribution].distribution_options + dist_options = self.object.distributions[selected_distribution].distribution_options + + # the order of options might not be guaranteed, so we map them by their ids + id_value_dist = [(id,v) for id, v in zip(ids_dist, values_dist)] - # the order of options might not be guaranteed, so we map them by their ids - # and them sort them, so they are in the same order as sampling_options and dist_options - id_value_dist = [(id,v) for id, v in zip(ids_dist, values_dist)] - options_dist_new = sorted(id_value_dist, key=lambda x: int(x[0]["index"])) + options_dist_new = sorted(id_value_dist, key=lambda x: int(x[0]["index"])) - for opt, (id, new_state) in zip(dist_options, options_dist_new): - opt.update_state(new_state) + for opt, (id, new_state) in zip(dist_options, options_dist_new): + opt.update_state(new_state) - # meshed density function plot plot - patched_figure = Patch() + # meshed density function plot plot + patched_figure = Patch() - - pdf = self.object.distributions[selected_distribution].get_pdf(list(dist_options)) - if pdf is not None: - x, y, z = self.object.generate_mesh(pdf) - else: - x, y, z = [], [], [] + + pdf = self.object.distributions[selected_distribution].get_pdf(list(dist_options)) + if pdf is not None: + x, y, z = self.object.generate_mesh(pdf) + else: + x, y, z = [], [], [] - patched_figure["data"][2].x = x - patched_figure["data"][2].y = y - patched_figure["data"][2].z = z + patched_figure["data"][2].x = x + patched_figure["data"][2].y = y + patched_figure["data"][2].z = z - return patched_figure + return patched_figure diff --git a/util/selectors/selector.py b/util/selectors/selector.py index 5708c51..4259ba8 100644 --- a/util/selectors/selector.py +++ b/util/selectors/selector.py @@ -11,17 +11,17 @@ def __init__(self): def to_dash_component(self, _type, id, renderer_id): pass - def transfrom_down(x): + def transfrom_down(self, x): # identity by default, override in subclasses if needed # does need to be defined if dash slider_values do not correspond to self.state return x - def transfrom_up(x): + def transfrom_up(self, x): # identity by default, override in subclasses if needed # does need to be defined if dash slider_values do not correspond to self.state return x - def is_valid(x): + def is_valid(self, x): # by default all values are valid # override in subclasses if needed return True diff --git a/util/selectors/silder_log.py b/util/selectors/silder_log.py index d442d49..a16d83f 100644 --- a/util/selectors/silder_log.py +++ b/util/selectors/silder_log.py @@ -62,13 +62,15 @@ def transfrom_down(self, value): return 0 return np.log10(value) - def to_dash_component(self, _type, id, renderer_id): + def to_dash_component(self, _type, id, renderer_id, manual=False): self.id = id + slider_id = {"type": _type, "index": id, "renderer": renderer_id, "manual": manual} + return html.Div([ html.Label(self.name), dcc.Slider( - id={"type": _type, "index": id, "renderer": renderer_id}, + id=slider_id, min=self.log_min, max=self.log_max, value=self.transfrom_down(self.state), diff --git a/util/selectors/silder_manual_input_wrapper.py b/util/selectors/silder_manual_input_wrapper.py index 8eec34d..9d2f5dd 100644 --- a/util/selectors/silder_manual_input_wrapper.py +++ b/util/selectors/silder_manual_input_wrapper.py @@ -9,14 +9,13 @@ def __init__(self, slider, check_input=None): self.check_input = check_input def to_dash_component(self, _type, id, renderer_id): - slider_component = self.slider.to_dash_component(_type, id, renderer_id) + slider_component = self.slider.to_dash_component(_type, id, renderer_id, manual=True) self.id = id self.renderer_id = renderer_id component = html.Div([ html.Div( children=[slider_component], - #style={"flex": "1"} style={"display": "inline-block", "width": r"calc(100% - 5rem)", "verticalAlign": "bottom"} ), html.Div( @@ -24,7 +23,6 @@ def to_dash_component(self, _type, id, renderer_id): id={"type": f"manual_input-{_type}", "index": id, "renderer": renderer_id}, type="number", value=self.slider.state, - #style={"width": "5rem"}, style={"width": "100%"}, )], style={"display": "inline-block", "width": "5rem", "verticalAlign": "bottom", "margin-bottom": "1rem"} diff --git a/util/selectors/slider.py b/util/selectors/slider.py index 61ee9b3..da35230 100644 --- a/util/selectors/slider.py +++ b/util/selectors/slider.py @@ -48,13 +48,15 @@ def round_nice_number(x): return marks - def to_dash_component(self, _type, id, renderer_id): + def to_dash_component(self, _type, id, renderer_id, manual=False): self.id = id + slider_id = {"type": _type, "index": id, "renderer": renderer_id, "manual": manual} + return html.Div([ html.Label(self.name), dcc.Slider( - id={"type": _type, "index": id, "renderer": renderer_id}, + id=slider_id, min=self.min, max=self.max, value=self.state, diff --git a/util/selectors/slider_fib.py b/util/selectors/slider_fib.py index 9bb2523..4815667 100644 --- a/util/selectors/slider_fib.py +++ b/util/selectors/slider_fib.py @@ -20,13 +20,15 @@ def __init__(self, name, min, state, max, idx): self.id = None - def to_dash_component(self, _type, id, renderer_id): + def to_dash_component(self, _type, id, renderer_id, manual=False): self.id = id + slider_id = {"type": _type, "index": id, "renderer": renderer_id, "manual": manual} + return html.Div([ html.Label(self.name), dcc.Slider( - id={"type": _type, "index": id, "renderer": renderer_id}, + id=slider_id, min=self.min, max=self.max, value=self.idx, @@ -55,13 +57,12 @@ def transfrom_up(self, x): return int(sp.fibonacci(x)) def transfrom_down(self, x): - for i in range(0, x + 1): - if sp.fibonacci(i) == x: + for i in range(0, x + 3): + if int(sp.fibonacci(i)) == x: return i raise ValueError(f"{x} is not a Fibonacci number") - @staticmethod - def is_valid(n): + def is_valid(self, n): if n < 0: return False # A number is a Fibonacci number if and only if one or both of (5*n^2 + 4) or (5*n^2 - 4) is a perfect square @@ -72,7 +73,6 @@ def is_valid(n): def is_perfect_square(x): s = int(np.sqrt(x)) return s * s == x - return is_perfect_square(test1) or is_perfect_square(test2) diff --git a/util/selectors/slider_float.py b/util/selectors/slider_float.py index fcbd1f2..c5fef61 100644 --- a/util/selectors/slider_float.py +++ b/util/selectors/slider_float.py @@ -15,18 +15,20 @@ def __init__(self, name, min, state, max, transform_tooltip=None): self.transform_tooltip = transform_tooltip - def to_dash_component(self, _type, id, renderer_id): + def to_dash_component(self, _type, id, renderer_id, manual=False): if self.transform_tooltip is None: tooltip = {"placement": "bottom", "always_visible": True} else: tooltip = {"placement": "bottom", "always_visible": True, "transform": self.transform_tooltip} self.id = id + slider_id = {"type": _type, "index": id, "renderer": renderer_id, "manual": manual} + return html.Div([ html.Label(self.name), dcc.Slider( - id={"type": _type, "index": id, "renderer": renderer_id}, + id=slider_id, min=self.min, max=self.max, value=self.state, diff --git a/util/selectors/slider_square.py b/util/selectors/slider_square.py index 5ebd749..0597425 100644 --- a/util/selectors/slider_square.py +++ b/util/selectors/slider_square.py @@ -20,13 +20,15 @@ def __init__(self, name, min, state, max, idx): self.id = None - def to_dash_component(self, _type, id, renderer_id): + def to_dash_component(self, _type, id, renderer_id, manual=False): self.id = id + slider_id = {"type": _type, "index": id, "renderer": renderer_id, "manual": manual} + return html.Div([ html.Label(self.name), dcc.Slider( - id={"type": _type, "index": id, "renderer": renderer_id}, + id=slider_id, min=self.min, max=self.max, value=self.idx, @@ -60,7 +62,9 @@ def transfrom_down(self, x): return i raise ValueError(f"{x} is not a perfect square number") - @staticmethod - def is_valid(x): + def is_valid(self, x): + if x < 0: + return False + s = int(np.sqrt(x)) return s * s == x From f47125456ca654fb5bf8bc74b40e140c1f0bad0f Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 27 Nov 2025 09:30:19 +0100 Subject: [PATCH 098/152] make cylinder flat at the top --- model/cylinder/cylinder.py | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/model/cylinder/cylinder.py b/model/cylinder/cylinder.py index 7daaa52..4235731 100644 --- a/model/cylinder/cylinder.py +++ b/model/cylinder/cylinder.py @@ -13,7 +13,7 @@ class Cylinder(Manifold): def __init__(self, resolution=100, r=1): - self.xyz = self.generate_xyz(resolution, 0.999) + self.xyz = self.generate_xyz(resolution, 0.998, r_height=1) self.mesh = np.array([]) self.samples = np.array([]) self.samples_2d = np.array([]) @@ -44,13 +44,21 @@ def __init__(self, resolution=100, r=1): center=dict(x=0, y=0, z=0), ) - def generate_xyz(self, resolution=50, r=1): + def generate_xyz(self, resolution=50, r=1, r_height=None): + if r_height is None: # allow pointionally setting height different from radius + r_height = r # usefull for making cylinders slightly thinner so mesh doesnt interfere + + p = np.linspace(0, 2*np.pi, resolution) z = np.linspace(0, 2*np.pi, resolution) t,p = np.meshgrid(p,z) + x = r * np.cos(t) + y = r * np.sin(t) + z = r_height * p + - return self.p_z_to_xyz(t, p, r) + return x, y, z def update_sample(self, selected_distribution, selected_sampling_method, sample_options, distribution_options): dist = self.distributions[selected_distribution] @@ -98,7 +106,17 @@ def generate_mesh(self, pdf, alpha=1): def _init_mesh(self, resolution=3000): pz = CylinderFibUniformSampling.sample(None, [Slider("Number of Samples", 10, resolution, resolution)] , []) - pz[:, 1] = -0.1 + (pz[:, 1]) * 1.1 # extend slightly beyond [0, 2pi] so the top and bottom looks better + #pz[:, 1] = -0.1 + (pz[:, 1]) * 1.1 # extend slightly beyond [0, 2pi] so the top and bottom looks better + + # add ring at top and bottom + z_bottom = 0 + z_top = 2 * np.pi + num_points_ring = int(np.sqrt(resolution)) + for j in range(num_points_ring): + p = (j / num_points_ring) * 2 * np.pi + pz = np.vstack((pz, np.array([p, z_bottom]))) + pz = np.vstack((pz, np.array([p, z_top]))) + x, y, z = self.p_z_to_xyz(pz[:,0], pz[:,1], self.r) simplices = Delaunay(pz).simplices @@ -128,12 +146,12 @@ def _init_mesh(self, resolution=3000): simplices_merged = np.unique(simplices_merged, axis=0) # throw away points at bottom so they dont stretch across - eps = 1e-2 - ok_mask_no_across = (pz[:, 1] > 0 + eps) & (pz[:, 1] < (2 * np.pi - eps)) - ok_idx_no_across = np.where(ok_mask_no_across)[0] - good_vertices_no_across = np.isin(simplices_merged, ok_idx_no_across) - good_triangles_no_across = good_vertices_no_across.all(axis=1) - simplices_merged = simplices_merged[good_triangles_no_across] + # eps = 1e-2 + # ok_mask_no_across = (pz[:, 1] > 0 + eps) & (pz[:, 1] < (2 * np.pi - eps)) + # ok_idx_no_across = np.where(ok_mask_no_across)[0] + # good_vertices_no_across = np.isin(simplices_merged, ok_idx_no_across) + # good_triangles_no_across = good_vertices_no_across.all(axis=1) + # simplices_merged = simplices_merged[good_triangles_no_across] From 029ccb9e10f35c4fb2a85c34e8588603109e4298 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 27 Nov 2025 09:39:17 +0100 Subject: [PATCH 099/152] bump export image res --- renderer/Object3DRenderer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 01639e5..ce5877c 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -22,6 +22,10 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): 'responsive': True, 'scrollZoom': True, "modeBarButtonsToRemove": ["select2d", "lasso2d"], + "toImageButtonOptions": { + "format": "png", + "scale": 4, + } } if self.object.samples.size and self.object.samples.shape[0] != 0: sample_count = self.object.samples.shape[0] From 80c833b881c5e51a183364a5b5238071b01315c1 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 27 Nov 2025 15:10:47 +0100 Subject: [PATCH 100/152] fixed view moving on update --- renderer/Object3DAnd2DRenderer.py | 2 ++ renderer/Object3DRenderer.py | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index b0be4c4..d7ca56b 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -76,6 +76,8 @@ def __init__(self, object, id): x=0.01, )) self.fig_2d.update_layout(dragmode="pan") + # keep user zoom/pan while data is being patched + self.fig_2d.update_layout(uirevision=f"object-2d-{self.id}") if object.plot_settings_2d is not None: self.fig_2d.update_xaxes( diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index ce5877c..bf4e6e8 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -73,7 +73,9 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): yaxis=dict(visible=False), zaxis=dict(visible=False), ), - margin=dict(l=0, r=0, t=0, b=0) + margin=dict(l=0, r=0, t=0, b=0), + # keep camera/zoom/pan while data is patched + uirevision=f"object-3d-{self.id}", ) self.fig.update_layout(legend=dict( From 068a867372adc3f90fcd744c87fd6dd5a541f857 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 27 Nov 2025 15:55:25 +0100 Subject: [PATCH 101/152] added pi slider --- util/selectors/slider_pi.py | 101 ++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 util/selectors/slider_pi.py diff --git a/util/selectors/slider_pi.py b/util/selectors/slider_pi.py new file mode 100644 index 0000000..5dbcfa1 --- /dev/null +++ b/util/selectors/slider_pi.py @@ -0,0 +1,101 @@ +from dash import dcc, html +from util.selectors.selector import Selector +import numpy as np + +SLIDER_OPT_AMOUNT = 100 +SLIDER_MARK_AMOUNT = 5 + +class PiSlider(Selector): + """ + Slider for values between 0 and 2pi or pi., like FloatSlider but with pi notation on marks. + min, state, max shall be given relative to pi, eg. min=0, max=2 for 0 to 2pi + """ + + def __init__(self, name, min, state, max, transform_tooltip=None): + self.name = name + self.min = min + self.state = state * np.pi + self.max = max + + self.id = None + self.transform_tooltip = transform_tooltip + + def calculate_marks(self): + # for small marks + if self.max == 2 and self.min == 0: + return { + 0: "0", + 0.5: "½π", + 1: "π", + 1.5: "3⁄2π", + 2: "2π" + } + if self.max == 1 and self.min == 0: + return { + 0: "0", + 0.25: "¼π", + 0.5: "½π", + 0.75: "¾π", + 1: "π" + } + + + # for bigger values of pi use clean integer values + def round_nice_number(x): + if x >= 1000: + return round(x, -3) + elif x >= 100: + return round(x, -2) + else: + return x + + marks = {} + step = (self.max - self.min) / SLIDER_MARK_AMOUNT + for i in range(SLIDER_MARK_AMOUNT + 1): + value = int(self.min + i * step) + value = round_nice_number(value) + marks[value] = str(value) + "π" + return marks + + + def to_dash_component(self, _type, id, renderer_id, manual=False): + if self.transform_tooltip is None: + tooltip = { + "placement": "bottom", + "always_visible": True, + "template": "{value}π" + } + else: + tooltip = { + "placement": "bottom", + "always_visible": True, + "transform": self.transform_tooltip, + "template": "{value}π" + } + + self.id = id + slider_id = {"type": _type, "index": id, "renderer": renderer_id, "manual": manual} + + return html.Div([ + html.Label(self.name), + + dcc.Slider( + id=slider_id, + min=self.min, + max=self.max, + value=self.transfrom_down(self.state), + tooltip=tooltip, + updatemode="drag", + marks=self.calculate_marks(), + ) + ]) + + def update_state(self, new_state): + self.state = self.transfrom_up(new_state) + + + def transfrom_up(self, x): + return x * np.pi + + def transfrom_down(self, x): + return x / np.pi From 8dc7e5b164ea93fccb2763509705c4dc530c5ca8 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 27 Nov 2025 15:55:40 +0100 Subject: [PATCH 102/152] added mean for partialy warpped and wrapped --- .../partially_warpped_normal.py | 16 ++++++++++------ .../cylinder/partially_wraped_normal/random.py | 10 ++++++---- .../torus/wrapped_normal/fibonacci.py | 13 ++++++++----- .../torus/wrapped_normal/random.py | 10 ++++++---- .../torus/wrapped_normal/wrapped_normal.py | 18 +++++++++++------- 5 files changed, 41 insertions(+), 26 deletions(-) diff --git a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py index 8d3318c..1d5c9fb 100644 --- a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py +++ b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py @@ -1,8 +1,8 @@ -from util.selectors.slider_float import FloatSlider import numpy as np from scipy.stats import multivariate_normal - +from util.selectors.slider_float import FloatSlider +from util.selectors.slider_pi import PiSlider from model.distributions.cylinder.cylinder_distribution import CylinderDistribution from model.distributions.cylinder.partially_wraped_normal.random import CylinderRandomPWNSampling from model.cylinder.cylinder import Cylinder @@ -10,6 +10,8 @@ class PartiallyWrappedNormalDistribution(CylinderDistribution): def __init__(self): self.distribution_options = [ + PiSlider("Mean x (μₓ)", 0, 1, 2), + PiSlider("Mean y (μᵧ)", 0, 1, 2), FloatSlider("Sigma x (σₓ)", 0, 0.5, 5.0), FloatSlider("Sigma y (σᵧ)", 0, 0.5, 1.0), FloatSlider("Correlation (ρ)", -1, 0.1, 1), @@ -22,16 +24,18 @@ def get_name(self): return "Partially Wrapped Normal" def get_pdf(self, distribution_options): - sigma_x = distribution_options[0].state - sigma_y = distribution_options[1].state - correlation = distribution_options[2].state + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_x = distribution_options[2].state + sigma_y = distribution_options[3].state + correlation = distribution_options[4].state Cov = np.array([ [sigma_x**2, correlation * sigma_x * sigma_y], [correlation * sigma_x * sigma_y, sigma_y**2] ]) - mean = np.array([np.pi, np.pi]) + mean = np.array([mean_x, mean_y]) dist = multivariate_normal(mean=mean, cov=Cov, allow_singular=True) diff --git a/model/distributions/cylinder/partially_wraped_normal/random.py b/model/distributions/cylinder/partially_wraped_normal/random.py index 7d20984..b30c520 100644 --- a/model/distributions/cylinder/partially_wraped_normal/random.py +++ b/model/distributions/cylinder/partially_wraped_normal/random.py @@ -17,16 +17,18 @@ def get_name(self): def sample(self, sample_options, distribution_options): sample_count = sample_options[0].state - sigma_x = distribution_options[0].state - sigma_y = distribution_options[1].state - correlation = distribution_options[2].state + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_x = distribution_options[2].state + sigma_y = distribution_options[3].state + correlation = distribution_options[4].state Cov = np.array([ [sigma_x**2, correlation * sigma_x * sigma_y], [correlation * sigma_x * sigma_y, sigma_y**2] ]) - mean = np.array([np.pi, np.pi]) + mean = np.array([mean_x, mean_y]) samples = np.random.multivariate_normal(mean, Cov, sample_count) samples[:,0] = samples[:,0] % (2 * np.pi) diff --git a/model/distributions/torus/wrapped_normal/fibonacci.py b/model/distributions/torus/wrapped_normal/fibonacci.py index 9d19d52..9295a30 100644 --- a/model/distributions/torus/wrapped_normal/fibonacci.py +++ b/model/distributions/torus/wrapped_normal/fibonacci.py @@ -26,16 +26,18 @@ def sample(self, sample_options, distribution_options): fib_grid = np.column_stack((t , p)) - sigma_t = distribution_options[0].state - sigma_p = distribution_options[1].state - correlation = distribution_options[2].state + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state Cov = np.array([ [sigma_t**2, correlation * sigma_t * sigma_p], [correlation * sigma_t * sigma_p, sigma_p**2] ]) - gaus_grid = self.transform_grid_gaussian(fib_grid, np.pi, Cov) + gaus_grid = self.transform_grid_gaussian(fib_grid, (mean_x, mean_y), Cov) # wrapp gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) @@ -65,6 +67,7 @@ def transform_grid_gaussian(grid, mu, cov): gaus = gaus.T # (L,2) - gaus += mu # mu = [pi, pi] + gaus[:,0] += mu[0] + gaus[:,1] += mu[1] return gaus \ No newline at end of file diff --git a/model/distributions/torus/wrapped_normal/random.py b/model/distributions/torus/wrapped_normal/random.py index b1fd1ef..117ab5f 100644 --- a/model/distributions/torus/wrapped_normal/random.py +++ b/model/distributions/torus/wrapped_normal/random.py @@ -16,16 +16,18 @@ def get_name(self): def sample(self, sample_options, distribution_options): sample_count = sample_options[0].state - sigma_t = distribution_options[0].state - sigma_p = distribution_options[1].state - correlation = distribution_options[2].state + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state Cov = np.array([ [sigma_t**2, correlation * sigma_t * sigma_p], [correlation * sigma_t * sigma_p, sigma_p**2] ]) - mean = np.array([np.pi, np.pi]) + mean = np.array([mean_x, mean_y]) samples = np.random.multivariate_normal(mean, Cov, sample_count) samples[:,0] = samples[:,0] % (2 * np.pi) diff --git a/model/distributions/torus/wrapped_normal/wrapped_normal.py b/model/distributions/torus/wrapped_normal/wrapped_normal.py index 017c73a..6cbc7bc 100644 --- a/model/distributions/torus/wrapped_normal/wrapped_normal.py +++ b/model/distributions/torus/wrapped_normal/wrapped_normal.py @@ -1,8 +1,8 @@ -from util.selectors.slider_float import FloatSlider import numpy as np from scipy.stats import multivariate_normal - +from util.selectors.slider_float import FloatSlider +from util.selectors.slider_pi import PiSlider from model.distributions.torus.torus_distribution import TorusDistribution from model.distributions.torus.wrapped_normal.random import TorusRandomWrappedSampling from model.distributions.torus.wrapped_normal.fibonacci import TorusFibRank1WNSampling @@ -10,8 +10,10 @@ class UniformTorusDistribution(TorusDistribution): def __init__(self): self.distribution_options = [ - FloatSlider("Sigma t (σₜ)", 0, 0.5, 5.0), + PiSlider("Mean p (μₚ)", 0, 1, 2), + PiSlider("Mean t (μₜ)", 0, 1, 2), FloatSlider("Sigma p (σₚ)", 0, 0.5, 5.0), + FloatSlider("Sigma t (σₜ)", 0, 0.5, 5.0), FloatSlider("Correlation (ρ)", -1, 0.1, 1), ] self.sampling_methods = [ @@ -24,16 +26,18 @@ def get_name(self): return "Wrapped Normal" def get_pdf(self, distribution_options): - sigma_t = distribution_options[0].state - sigma_p = distribution_options[1].state - correlation = distribution_options[2].state + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state Cov = np.array([ [sigma_t**2, correlation * sigma_t * sigma_p], [correlation * sigma_t * sigma_p, sigma_p**2] ]) - mean = np.array([np.pi, np.pi]) + mean = np.array([mean_x, mean_y]) dist = multivariate_normal(mean=mean, cov=Cov, allow_singular=True) From 76e9856b6fe52ed5f5d053bf60c96ef58cae936e Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 28 Nov 2025 14:02:11 +0100 Subject: [PATCH 103/152] added more fib and cartesian to pwn and wn --- .../partially_wraped_normal/cartesian.py | 52 ++++++++++++++++++ .../fibonacci_kronecker.py | 49 +++++++++++++++++ .../fibonacci_rank_1.py | 48 +++++++++++++++++ .../partially_warpped_normal.py | 6 +++ .../torus/wrapped_normal/cartesian.py | 53 +++++++++++++++++++ .../torus/wrapped_normal/fibonacci.py | 31 ++--------- .../wrapped_normal/fibonacci_kronecker.py | 50 +++++++++++++++++ .../torus/wrapped_normal/wrapped_normal.py | 4 ++ renderer/Object3DRenderer.py | 7 ++- util/gaus_util.py | 30 +++++++++++ 10 files changed, 300 insertions(+), 30 deletions(-) create mode 100644 model/distributions/cylinder/partially_wraped_normal/cartesian.py create mode 100644 model/distributions/cylinder/partially_wraped_normal/fibonacci_kronecker.py create mode 100644 model/distributions/cylinder/partially_wraped_normal/fibonacci_rank_1.py create mode 100644 model/distributions/torus/wrapped_normal/cartesian.py create mode 100644 model/distributions/torus/wrapped_normal/fibonacci_kronecker.py create mode 100644 util/gaus_util.py diff --git a/model/distributions/cylinder/partially_wraped_normal/cartesian.py b/model/distributions/cylinder/partially_wraped_normal/cartesian.py new file mode 100644 index 0000000..5c71a87 --- /dev/null +++ b/model/distributions/cylinder/partially_wraped_normal/cartesian.py @@ -0,0 +1,52 @@ +from abc import ABC, abstractmethod +import numpy as np +from scipy.stats import norm + +from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling +from util.selectors.slider_square import SliderSquare +from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from util.gaus_util import GausUtil as gu +from util.cartesian_util import CartesianUtil as cu + +class CylinderFibCartPWNSampling(CylinderSamplingSchema): + def __init__(self): + self.sample_options = [ + MI(SliderSquare("Number of Samples", 4, 16, 100, 4)) + ] + self.sampler = CylinderFibRank1UniformSampling() + self.info_md = """ + > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. + > Sample count may appear off due to samples overlapping. + > It is included for demonstration purposes only.""" + + def get_name(self): + return "Cartesian Grid" + + def sample(self, sample_options, distribution_options): + # see https://isas.iar.kit.edu/pdf/Fusion21_Frisch.pdf + sample_count = sample_options[0].state + + n = int(np.sqrt(sample_count)) + + grid = cu.generate_cartesian_grid(n, (2 * np.pi, 2 * np.pi)) + + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = gu.transform_grid_gaussian(grid, (mean_x, mean_y), Cov) + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + return gaus_grid + + + \ No newline at end of file diff --git a/model/distributions/cylinder/partially_wraped_normal/fibonacci_kronecker.py b/model/distributions/cylinder/partially_wraped_normal/fibonacci_kronecker.py new file mode 100644 index 0000000..a56281f --- /dev/null +++ b/model/distributions/cylinder/partially_wraped_normal/fibonacci_kronecker.py @@ -0,0 +1,49 @@ +import numpy as np + +from util.selectors.silder_log import LogSlider +from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from util.gaus_util import GausUtil as gu + + +class CylinderFibKroneckerPWNSampling(CylinderSamplingSchema): + def __init__(self): + self.sample_options = [ + MI(LogSlider("Number of Samples", 10, 100, 10000)) + ] + + def get_name(self): + return "Fibonacci-Kronecker Lattice" + + def sample(self, sample_options, distribution_options): + # see https://isas.iar.kit.edu/pdf/Fusion21_Frisch.pdf + sample_count = sample_options[0].state + + indices = np.arange(0, sample_count) + gol = (1+5**0.5)/2 + + # centered rank-1 lattice generator + equidistant_generator = (2 * indices + 1) / (2 * sample_count) + + t = equidistant_generator + p = (indices / gol) % 1 + + + fib_grid = np.column_stack((t , p)) + + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = gu.transform_grid_gaussian(fib_grid, (mean_x, mean_y), Cov) + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + return gaus_grid \ No newline at end of file diff --git a/model/distributions/cylinder/partially_wraped_normal/fibonacci_rank_1.py b/model/distributions/cylinder/partially_wraped_normal/fibonacci_rank_1.py new file mode 100644 index 0000000..fed9a05 --- /dev/null +++ b/model/distributions/cylinder/partially_wraped_normal/fibonacci_rank_1.py @@ -0,0 +1,48 @@ +from abc import ABC, abstractmethod +import numpy as np +from scipy.stats import norm + +from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling +from util.selectors.slider_fib import SliderFib +from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from util.gaus_util import GausUtil as gu + +class CylinderFibRank1PWNSampling(CylinderSamplingSchema): + def __init__(self): + self.sample_options = [ + MI(SliderFib("Number of Samples", 2, 34, 21, 9)) + ] + self.sampler = CylinderFibRank1UniformSampling() + + def get_name(self): + return "Fibonacci-Rank-1 Lattice" + + def sample(self, sample_options, distribution_options): + # see https://isas.iar.kit.edu/pdf/Fusion21_Frisch.pdf + sample_count = sample_options[0].state + + t, p = self.sampler.get_rank_1(sample_count, sample_options[0].idx) + + fib_grid = np.column_stack((t , p)) + + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = gu.transform_grid_gaussian(fib_grid, (mean_x, mean_y), Cov) + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + + return gaus_grid + + + \ No newline at end of file diff --git a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py index 1d5c9fb..5bb0ad1 100644 --- a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py +++ b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py @@ -5,6 +5,9 @@ from util.selectors.slider_pi import PiSlider from model.distributions.cylinder.cylinder_distribution import CylinderDistribution from model.distributions.cylinder.partially_wraped_normal.random import CylinderRandomPWNSampling +from model.distributions.cylinder.partially_wraped_normal.fibonacci_rank_1 import CylinderFibRank1PWNSampling +from model.distributions.cylinder.partially_wraped_normal.fibonacci_kronecker import CylinderFibKroneckerPWNSampling +from model.distributions.cylinder.partially_wraped_normal.cartesian import CylinderFibCartPWNSampling from model.cylinder.cylinder import Cylinder class PartiallyWrappedNormalDistribution(CylinderDistribution): @@ -18,6 +21,9 @@ def __init__(self): ] self.sampling_methods = [ CylinderRandomPWNSampling(), + CylinderFibRank1PWNSampling(), + CylinderFibKroneckerPWNSampling(), + CylinderFibCartPWNSampling(), ] def get_name(self): diff --git a/model/distributions/torus/wrapped_normal/cartesian.py b/model/distributions/torus/wrapped_normal/cartesian.py new file mode 100644 index 0000000..46d5e00 --- /dev/null +++ b/model/distributions/torus/wrapped_normal/cartesian.py @@ -0,0 +1,53 @@ +from abc import ABC, abstractmethod +import numpy as np +from scipy.stats import norm + +from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling +from util.selectors.slider_square import SliderSquare +from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from util.gaus_util import GausUtil as gu +from util.cartesian_util import CartesianUtil as cu + +class TorusFibCartWNSampling(TorusSamplingSchema): + def __init__(self): + self.sample_options = [ + MI(SliderSquare("Number of Samples", 4, 16, 100, 4)) + ] + self.sampler = CylinderFibRank1UniformSampling() + self.info_md = """ + > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. + > Sample count may appear off due to samples overlapping. + > It is included for demonstration purposes only.""" + + def get_name(self): + return "Cartesian Grid" + + def sample(self, sample_options, distribution_options): + # see https://isas.iar.kit.edu/pdf/Fusion21_Frisch.pdf + sample_count = sample_options[0].state + + n = int(np.sqrt(sample_count)) + + grid = cu.generate_cartesian_grid(n, (2 * np.pi, 2 * np.pi)) + + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = gu.transform_grid_gaussian(grid, (mean_x, mean_y), Cov) + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + gaus_grid[:,1] = gaus_grid[:,1] % (2 * np.pi) + return gaus_grid + + + \ No newline at end of file diff --git a/model/distributions/torus/wrapped_normal/fibonacci.py b/model/distributions/torus/wrapped_normal/fibonacci.py index 9295a30..78b1f0c 100644 --- a/model/distributions/torus/wrapped_normal/fibonacci.py +++ b/model/distributions/torus/wrapped_normal/fibonacci.py @@ -6,7 +6,7 @@ from util.selectors.slider_fib import SliderFib from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI - +from util.gaus_util import GausUtil as gu class TorusFibRank1WNSampling(TorusSamplingSchema): def __init__(self): @@ -37,7 +37,7 @@ def sample(self, sample_options, distribution_options): [correlation * sigma_t * sigma_p, sigma_p**2] ]) - gaus_grid = self.transform_grid_gaussian(fib_grid, (mean_x, mean_y), Cov) + gaus_grid = gu.transform_grid_gaussian(fib_grid, (mean_x, mean_y), Cov) # wrapp gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) @@ -45,29 +45,4 @@ def sample(self, sample_options, distribution_options): return gaus_grid - @staticmethod - def transform_grid_gaussian(grid, mu, cov): - eps = 1e-9 - grid = np.clip(grid, eps, 1 - eps) # avoid inf in ppf - - gaus = norm.ppf(grid) - - var = np.mean(gaus**2, axis=0) - - gaus = gaus / np.sqrt(var) - - # scale with eigen decomposition - ew, V = np.linalg.eig(cov) - - D = np.diag(np.sqrt(ew)) - - gaus = gaus.T # (2,L) - - gaus = V @ D @ gaus # (2,2) @ (2,2) @ (2,L) -> (2,L) - - gaus = gaus.T # (L,2) - - gaus[:,0] += mu[0] - gaus[:,1] += mu[1] - - return gaus \ No newline at end of file + \ No newline at end of file diff --git a/model/distributions/torus/wrapped_normal/fibonacci_kronecker.py b/model/distributions/torus/wrapped_normal/fibonacci_kronecker.py new file mode 100644 index 0000000..47dab0d --- /dev/null +++ b/model/distributions/torus/wrapped_normal/fibonacci_kronecker.py @@ -0,0 +1,50 @@ +import numpy as np + +from util.selectors.silder_log import LogSlider +from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from util.gaus_util import GausUtil as gu + + +class TorusFibKroneckerWNSampling(TorusSamplingSchema): + def __init__(self): + self.sample_options = [ + MI(LogSlider("Number of Samples", 10, 100, 10000)) + ] + + def get_name(self): + return "Fibonacci-Kronecker Lattice" + + def sample(self, sample_options, distribution_options): + # see https://isas.iar.kit.edu/pdf/Fusion21_Frisch.pdf + sample_count = sample_options[0].state + + indices = np.arange(0, sample_count) + gol = (1+5**0.5)/2 + + # centered rank-1 lattice generator + equidistant_generator = (2 * indices + 1) / (2 * sample_count) + + t = equidistant_generator + p = (indices / gol) % 1 + + + fib_grid = np.column_stack((t , p)) + + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = gu.transform_grid_gaussian(fib_grid, (mean_x, mean_y), Cov) + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + gaus_grid[:,1] = gaus_grid[:,1] % (2 * np.pi) + return gaus_grid \ No newline at end of file diff --git a/model/distributions/torus/wrapped_normal/wrapped_normal.py b/model/distributions/torus/wrapped_normal/wrapped_normal.py index 6cbc7bc..adeb132 100644 --- a/model/distributions/torus/wrapped_normal/wrapped_normal.py +++ b/model/distributions/torus/wrapped_normal/wrapped_normal.py @@ -6,6 +6,8 @@ from model.distributions.torus.torus_distribution import TorusDistribution from model.distributions.torus.wrapped_normal.random import TorusRandomWrappedSampling from model.distributions.torus.wrapped_normal.fibonacci import TorusFibRank1WNSampling +from model.distributions.torus.wrapped_normal.fibonacci_kronecker import TorusFibKroneckerWNSampling +from model.distributions.torus.wrapped_normal.cartesian import TorusFibCartWNSampling from model.torus.torus import Torus class UniformTorusDistribution(TorusDistribution): def __init__(self): @@ -19,6 +21,8 @@ def __init__(self): self.sampling_methods = [ TorusRandomWrappedSampling(), TorusFibRank1WNSampling(), + TorusFibKroneckerWNSampling(), + TorusFibCartWNSampling(), ] diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index bf4e6e8..cf0449a 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -29,8 +29,11 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): } if self.object.samples.size and self.object.samples.shape[0] != 0: sample_count = self.object.samples.shape[0] - marker_size = (10 * (sample_count / 100) ** (-0.35)) / self.device_pixel_ratio - marker_size = np.minimum(10,marker_size) + if sample_count == 0: + marker_size = 4 # default + else: + marker_size = (10 * (sample_count / 100) ** (-0.35)) / self.device_pixel_ratio + marker_size = np.minimum(10,marker_size) else: marker_size = 4 diff --git a/util/gaus_util.py b/util/gaus_util.py new file mode 100644 index 0000000..91464c9 --- /dev/null +++ b/util/gaus_util.py @@ -0,0 +1,30 @@ +import numpy as np +from scipy.stats import norm + +class GausUtil: + @staticmethod + def transform_grid_gaussian(grid, mu, cov): + eps = 1e-9 + grid = np.clip(grid, eps, 1 - eps) # avoid inf in ppf + + gaus = norm.ppf(grid) + + var = np.mean(gaus**2, axis=0) + + gaus = gaus / np.sqrt(var) + + # scale with eigen decomposition + ew, V = np.linalg.eig(cov) + + D = np.diag(np.sqrt(ew)) + + gaus = gaus.T # (2,L) + + gaus = V @ D @ gaus # (2,2) @ (2,2) @ (2,L) -> (2,L) + + gaus = gaus.T # (L,2) + + gaus[:,0] += mu[0] + gaus[:,1] += mu[1] + + return gaus \ No newline at end of file From d32c33b982195dc08f55358266a9511211e478ec Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 1 Dec 2025 15:05:35 +0100 Subject: [PATCH 104/152] fix objects not being completely stateless --- model/cylinder/cylinder.py | 25 ++++++++----------------- model/manifold.py | 3 ++- model/sphere/sphere.py | 5 ++--- model/torus/torus.py | 14 +++++++------- renderer/Object3DAnd2DRenderer.py | 6 +++--- renderer/Object3DRenderer.py | 29 +++++++++++------------------ 6 files changed, 33 insertions(+), 49 deletions(-) diff --git a/model/cylinder/cylinder.py b/model/cylinder/cylinder.py index 4235731..ce4d6d0 100644 --- a/model/cylinder/cylinder.py +++ b/model/cylinder/cylinder.py @@ -14,9 +14,7 @@ class Cylinder(Manifold): def __init__(self, resolution=100, r=1): self.xyz = self.generate_xyz(resolution, 0.998, r_height=1) - self.mesh = np.array([]) - self.samples = np.array([]) - self.samples_2d = np.array([]) + self.distributions = DistributionLoader(CylinderDistribution, "model.distributions.cylinder").get_distributions() self.r = r @@ -66,13 +64,16 @@ def update_sample(self, selected_distribution, selected_sampling_method, sample_ new_sample = sampling_method.sample(sample_options, distribution_options) if (new_sample is None) or new_sample.size == 0: - self.samples = np.empty((0, 3), dtype=float) - return + samples = np.empty((0, 3), dtype=float) + samples_2d = np.empty((0, 2), dtype=float) + return (samples, samples_2d) x, y, z = self.p_z_to_xyz(new_sample[:,0], new_sample[:,1], self.r) - self.samples = np.column_stack((x, y, z)) - self.samples_2d = new_sample + samples = np.column_stack((x, y, z)) + samples_2d = new_sample + + return (samples, samples_2d) @staticmethod @@ -145,16 +146,6 @@ def _init_mesh(self, resolution=3000): simplices_merged = np.vstack((simplices, simplices_2)) simplices_merged = np.unique(simplices_merged, axis=0) - # throw away points at bottom so they dont stretch across - # eps = 1e-2 - # ok_mask_no_across = (pz[:, 1] > 0 + eps) & (pz[:, 1] < (2 * np.pi - eps)) - # ok_idx_no_across = np.where(ok_mask_no_across)[0] - # good_vertices_no_across = np.isin(simplices_merged, ok_idx_no_across) - # good_triangles_no_across = good_vertices_no_across.all(axis=1) - # simplices_merged = simplices_merged[good_triangles_no_across] - - - def cf(xi, yi, zi, zmin=np.min(z), zmax=np.max(z)): if zi > zmax: zi = np.nextafter(zmax, zmin) diff --git a/model/manifold.py b/model/manifold.py index 294defa..01849bf 100644 --- a/model/manifold.py +++ b/model/manifold.py @@ -24,7 +24,8 @@ def plot_settings_2d(self, value): def generate_xyz(self, *args, **kwargs): pass - # updates self.sample based on selected distribution and sample options + # returns samples based on selected distribution and sample options + # samples are returned as a tuple (saples_xyz, samples_2d | None) # also converts the sample output type to xyz coordinates @abstractmethod def update_sample(self, selected_distribution, sample_options): diff --git a/model/sphere/sphere.py b/model/sphere/sphere.py index a0c646a..fdf1140 100644 --- a/model/sphere/sphere.py +++ b/model/sphere/sphere.py @@ -12,8 +12,7 @@ class Sphere(Manifold): def __init__(self, resolution=200, radius=0.999): self.xyz = self.generate_xyz(resolution, radius) - self.mesh = np.array([]) - self.samples = np.array([]) + self.distributions = DistributionLoader(SphereDistribution, "model.distributions.sphere").get_distributions() @@ -35,7 +34,7 @@ def generate_xyz(self, resolution=50, radius=1): def update_sample(self, selected_distribution, selected_sampling_method, sample_options, distribution_options): dist = self.distributions[selected_distribution] sampling_method = dist.sampling_method_dict[selected_sampling_method] - self.samples = sampling_method.sample(sample_options, distribution_options) + return (sampling_method.sample(sample_options, distribution_options), None) def generate_mesh(self, pdf, alpha=1): diff --git a/model/torus/torus.py b/model/torus/torus.py index 5c72469..0f47058 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -12,9 +12,7 @@ class Torus(Manifold): def __init__(self, resolution=100, r=1, R=3): self.xyz = self.generate_xyz(resolution, r - 0.01, R - 0.01) # slightly smaller to avoid artifacts from mesh - self.mesh = np.array([]) - self.samples = np.array([]) - self.samples_2d = np.array([]) + self.distributions = DistributionLoader(TorusDistribution, "model.distributions.torus").get_distributions() self.r = r @@ -59,13 +57,15 @@ def update_sample(self, selected_distribution, selected_sampling_method, sample_ new_sample = sampling_method.sample(sample_options, distribution_options) if (new_sample is None) or new_sample.size == 0: - self.samples = np.empty((0, 3), dtype=float) - return + samples = np.empty((0, 3), dtype=float) + samples_2d = np.empty((0, 2), dtype=float) + return (samples, samples_2d) x, y, z = self.t_p_to_xyz(new_sample[:,0], new_sample[:,1], self.r, self.R) - self.samples = np.column_stack((x, y, z)) - self.samples_2d = new_sample + samples = np.column_stack((x, y, z)) + samples_2d = new_sample + return (samples, samples_2d) @staticmethod diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/Object3DAnd2DRenderer.py index d7ca56b..73efe59 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/Object3DAnd2DRenderer.py @@ -194,13 +194,13 @@ def update_plot_sample_2d(self, values_dist, ids_dist, values_samp, ids_samp, se opt.update_state(new_state) # samples - self.object.update_sample(selected_distribution, selected_sampling, sampling_options, dist_options) + samples, samples_2d = self.object.update_sample(selected_distribution, selected_sampling, sampling_options, dist_options) patched_figure = Patch() - tp = self.object.samples_2d + tp = samples_2d # marker size scaling - sample_count = self.object.samples.shape[0] + sample_count = samples.shape[0] if sample_count == 0: marker_size = 0 # no samples, no size else: diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index cf0449a..bb31c1d 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -27,15 +27,8 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): "scale": 4, } } - if self.object.samples.size and self.object.samples.shape[0] != 0: - sample_count = self.object.samples.shape[0] - if sample_count == 0: - marker_size = 4 # default - else: - marker_size = (10 * (sample_count / 100) ** (-0.35)) / self.device_pixel_ratio - marker_size = np.minimum(10,marker_size) - else: - marker_size = 4 + + marker_size = 4 # initial size, will be updated based on samples self.fig = go.Figure( data=[ @@ -48,9 +41,9 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): ), go.Scatter3d( name="Samples", - x=self.object.samples[:, 0] if self.object.samples.size else [], - y=self.object.samples[:, 1] if self.object.samples.size else [], - z=self.object.samples[:, 2] if self.object.samples.size else [], + x=[], + y=[], + z=[], mode="markers", marker=dict( size=marker_size, @@ -284,21 +277,21 @@ def update_plot_sample(self, values_dist, ids_dist, values_samp, ids_samp, selec opt.update_state(new_state) # samples - self.object.update_sample(selected_distribution, selected_sampling, sampling_options, dist_options) + samples, _ = self.object.update_sample(selected_distribution, selected_sampling, sampling_options, dist_options) patched_figure = Patch() - patched_figure["data"][1].x = self.object.samples[:, 0] - patched_figure["data"][1].y = self.object.samples[:, 1] - patched_figure["data"][1].z = self.object.samples[:, 2] + patched_figure["data"][1].x = samples[:, 0] + patched_figure["data"][1].y = samples[:, 1] + patched_figure["data"][1].z = samples[:, 2] # set size based on number of samples - if self.object.samples.size == 0: + if samples.size == 0: marker_size = 0 else: - sample_count = self.object.samples.shape[0] + sample_count = samples.shape[0] marker_size = (10 * (sample_count / 100) ** (-0.35)) / dpr marker_size = np.minimum(10,marker_size) From 88044413a45c452024dd5ffc98a36090f5e8d8e0 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 1 Dec 2025 15:29:02 +0100 Subject: [PATCH 105/152] removed warning from external library by switching own fork --- poetry.lock | 13 +++++++++---- pyproject.toml | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/poetry.lock b/poetry.lock index 2779dbd..65ee2ff 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2902,9 +2902,8 @@ description = "A Python 3 package for inferential statistics on vectorial data o optional = false python-versions = ">=3.8" groups = ["main"] -files = [ - {file = "sphstat-1.0.6.tar.gz", hash = "sha256:e085b097bc8f49a9287c62672dc26906b21b70febf62bc822d455969e5b1241c"}, -] +files = [] +develop = false [package.dependencies] matplotlib = "*" @@ -2915,6 +2914,12 @@ scipy = "*" setuptools = ">=65.5.1" sympy = "*" +[package.source] +type = "git" +url = "https://github.com/Vlad-Kor/sphstat" +reference = "HEAD" +resolved_reference = "fad7f8779d4b4f8eeccae0f5e1e2ed6dd7280a22" + [[package]] name = "sympy" version = "1.14.0" @@ -3251,4 +3256,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.11" -content-hash = "b4e51cf951334efcb04908fd8b8e3e4137a37f7bc95f5c816ccbe5fe4dca95f0" +content-hash = "c82a202be54f6a4615afad2b04cd40b613894e842671ea77f3c43239bade1539" diff --git a/pyproject.toml b/pyproject.toml index 2b1d8df..0784956 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dependencies = [ "dash-resizable-panels (>=0.1.0,<0.2.0)", "scipy (>=1.13,<2.0)", "gunicorn (>=23.0.0,<24.0.0)", - "sphstat (>=1.0.6,<2.0.0)", + "sphstat @ git+https://github.com/Vlad-Kor/sphstat", "pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@002c7757e2a5a81e4e57da5c1b657c5f9bd5cf6e", "kent-distribution @ git+https://github.com/Vlad-Kor/kent_distribution", "sympy (>=1.14.0,<2.0.0)", From 3801824a3ed287d4e2dc77ba65903f78ecf06840 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 2 Dec 2025 15:51:48 +0100 Subject: [PATCH 106/152] changed wrong name --- model/distributions/torus/wrapped_normal/wrapped_normal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/distributions/torus/wrapped_normal/wrapped_normal.py b/model/distributions/torus/wrapped_normal/wrapped_normal.py index adeb132..8f55e76 100644 --- a/model/distributions/torus/wrapped_normal/wrapped_normal.py +++ b/model/distributions/torus/wrapped_normal/wrapped_normal.py @@ -9,7 +9,7 @@ from model.distributions.torus.wrapped_normal.fibonacci_kronecker import TorusFibKroneckerWNSampling from model.distributions.torus.wrapped_normal.cartesian import TorusFibCartWNSampling from model.torus.torus import Torus -class UniformTorusDistribution(TorusDistribution): +class WrappedNormalTorusDistribution(TorusDistribution): def __init__(self): self.distribution_options = [ PiSlider("Mean p (μₚ)", 0, 1, 2), From af4102ea432c30a038ea70b8794d608b333064fa Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 2 Dec 2025 17:02:40 +0100 Subject: [PATCH 107/152] make images even larger --- renderer/Object3DRenderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index bb31c1d..672bc38 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -24,7 +24,7 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): "modeBarButtonsToRemove": ["select2d", "lasso2d"], "toImageButtonOptions": { "format": "png", - "scale": 4, + "scale": 8, } } From d284cba2502c8e3c22d8dae06615a5ba8357ec8b Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 3 Dec 2025 16:44:44 +0100 Subject: [PATCH 108/152] adjust grid to 0-1 instead of 0-2pi --- .../cylinder/partially_wraped_normal/cartesian.py | 6 +++--- model/distributions/torus/wrapped_normal/cartesian.py | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/model/distributions/cylinder/partially_wraped_normal/cartesian.py b/model/distributions/cylinder/partially_wraped_normal/cartesian.py index 5c71a87..5826fd6 100644 --- a/model/distributions/cylinder/partially_wraped_normal/cartesian.py +++ b/model/distributions/cylinder/partially_wraped_normal/cartesian.py @@ -29,7 +29,7 @@ def sample(self, sample_options, distribution_options): n = int(np.sqrt(sample_count)) - grid = cu.generate_cartesian_grid(n, (2 * np.pi, 2 * np.pi)) + grid = cu.generate_cartesian_grid(n, (1.0, 1.0)) mean_x = distribution_options[0].state mean_y = distribution_options[1].state @@ -44,9 +44,9 @@ def sample(self, sample_options, distribution_options): gaus_grid = gu.transform_grid_gaussian(grid, (mean_x, mean_y), Cov) - # wrapp + # wrap gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) return gaus_grid - \ No newline at end of file + diff --git a/model/distributions/torus/wrapped_normal/cartesian.py b/model/distributions/torus/wrapped_normal/cartesian.py index 46d5e00..6a9bbe6 100644 --- a/model/distributions/torus/wrapped_normal/cartesian.py +++ b/model/distributions/torus/wrapped_normal/cartesian.py @@ -29,7 +29,7 @@ def sample(self, sample_options, distribution_options): n = int(np.sqrt(sample_count)) - grid = cu.generate_cartesian_grid(n, (2 * np.pi, 2 * np.pi)) + grid = cu.generate_cartesian_grid(n, (1.0, 1.0)) mean_x = distribution_options[0].state mean_y = distribution_options[1].state @@ -47,7 +47,4 @@ def sample(self, sample_options, distribution_options): # wrapp gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) gaus_grid[:,1] = gaus_grid[:,1] % (2 * np.pi) - return gaus_grid - - - \ No newline at end of file + return gaus_grid \ No newline at end of file From 4a9ac738c9274ada46e77aff8462322de3f983e1 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 3 Dec 2025 17:10:49 +0100 Subject: [PATCH 109/152] changed comment about samples overlapping --- .../distributions/cylinder/partially_wraped_normal/cartesian.py | 1 - model/distributions/torus/wrapped_normal/cartesian.py | 1 - 2 files changed, 2 deletions(-) diff --git a/model/distributions/cylinder/partially_wraped_normal/cartesian.py b/model/distributions/cylinder/partially_wraped_normal/cartesian.py index 5826fd6..cac93f2 100644 --- a/model/distributions/cylinder/partially_wraped_normal/cartesian.py +++ b/model/distributions/cylinder/partially_wraped_normal/cartesian.py @@ -17,7 +17,6 @@ def __init__(self): self.sampler = CylinderFibRank1UniformSampling() self.info_md = """ > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. - > Sample count may appear off due to samples overlapping. > It is included for demonstration purposes only.""" def get_name(self): diff --git a/model/distributions/torus/wrapped_normal/cartesian.py b/model/distributions/torus/wrapped_normal/cartesian.py index 6a9bbe6..d91de08 100644 --- a/model/distributions/torus/wrapped_normal/cartesian.py +++ b/model/distributions/torus/wrapped_normal/cartesian.py @@ -17,7 +17,6 @@ def __init__(self): self.sampler = CylinderFibRank1UniformSampling() self.info_md = """ > Warning: Using the Cartesian Grid is not recomended in practise, as it yields bad results. - > Sample count may appear off due to samples overlapping. > It is included for demonstration purposes only.""" def get_name(self): From ba2535baaa65e4729cbf1deda2f1e0ea6809f857 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 4 Dec 2025 15:40:37 +0100 Subject: [PATCH 110/152] added new layout to gaus1d gaus2d and conditional --- components/label.py | 8 + components/popup_box.py | 48 ++++ pages/conditional.py | 269 +++++++++++--------- pages/gauss1D.py | 359 ++++++++++++++------------ pages/gauss2D.py | 544 +++++++++++++++++++++------------------- 5 files changed, 686 insertions(+), 542 deletions(-) create mode 100644 components/label.py create mode 100644 components/popup_box.py diff --git a/components/label.py b/components/label.py new file mode 100644 index 0000000..6078548 --- /dev/null +++ b/components/label.py @@ -0,0 +1,8 @@ +from dash import html + +# wrapps component in a label +def Label(label, component): + return html.Div([ + html.Label(label), + component, + ]) \ No newline at end of file diff --git a/components/popup_box.py b/components/popup_box.py new file mode 100644 index 0000000..bdf8d67 --- /dev/null +++ b/components/popup_box.py @@ -0,0 +1,48 @@ +import dash_bootstrap_components as dbc +from dash import Dash, html, dcc, Input, Output, callback, State + +def PopupBox(id, label, title, children): + @callback( + Output(f"modal-{label}-{id}", "is_open"), + Input(f"popup-box-{label}-{id}", "n_clicks"), + Input(f"close-{label}-{id}", "n_clicks"), + State(f"modal-{label}-{id}", "is_open"), + ) + def toggle_modal(n1, n2, is_open): + if n1 or n2: + return not is_open + return is_open + + + button = html.Div( + children=[ + dbc.Button(label, id=f"popup-box-{label}-{id}", n_clicks=0) + ], + style={ + "caretColor": "transparent", + "userSelect": "none" + } + ) + + modal = dbc.Modal( + id=f"modal-{label}-{id}", + is_open=False, + children=[ + html.Div( + children=[dbc.ModalHeader(dbc.ModalTitle(title), close_button=True)], + style={ + "caretColor": "transparent", + "userSelect": "none" + } + ), + dbc.ModalBody(children), + dbc.ModalFooter( + dbc.Button("Close", id=f"close-{label}-{id}", n_clicks=0) + ), + ], + size="xl", + centered=True, + scrollable=True, + ) + return button, modal + diff --git a/pages/conditional.py b/pages/conditional.py index 2477c1b..a96435b 100644 --- a/pages/conditional.py +++ b/pages/conditional.py @@ -7,6 +7,10 @@ from numpy.random import randint from numpy.linalg import det, solve +from components.popup_box import PopupBox +from components.split_pane import SplitPane +from components.label import Label + dash.register_page(__name__) # Colors @@ -30,14 +34,14 @@ def gauss1(x, μ, C): - return 1/sqrt(2*pi*C) * exp(-1/2 * square((x-μ))/C) + return 1/sqrt(2*pi*C) * exp(-1/2 * square((x-μ))/C) def gauss2(x, y, μ, C): - d = array([x-μ[0], y-μ[1]]) - d = d.reshape(-1, 1) # to column vector - f = 1/sqrt(det(2*pi*C)) * exp(-1/2 * matmul(d.T, solve(C, d))) - return f[0][0] + d = array([x-μ[0], y-μ[1]]) + d = d.reshape(-1, 1) # to column vector + f = 1/sqrt(det(2*pi*C)) * exp(-1/2 * matmul(d.T, solve(C, d))) + return f[0][0] # Initialize Plot @@ -50,130 +54,163 @@ def gauss2(x, y, μ, C): fig.add_trace(go.Scatter3d(name='Slice f(x,ŷ)', x=xv, y=yv*0+yv[0], mode='lines', z=xv*0, marker_color=col_slice, showlegend=True, hoverinfo='skip', line={'width': 8})) fig.update_xaxes(range=rangx, tickmode='array', tickvals=list(range(rangx[0], rangx[1]+1))) fig.update_yaxes(range=rangy, tickmode='array', tickvals=list(range(rangy[0], rangy[1]+1)), scaleanchor="x", scaleratio=1) -# fig.update_layout(legend=dict(orientation='v', yanchor='top', xanchor='right')) -fig.update_layout(legend=dict(orientation='h', yanchor='bottom', xanchor='right', y=1.02, x=1)) fig.update_layout(transition_duration=100, transition_easing='linear') fig.update_scenes(camera_projection_type="orthographic") fig.update_scenes(aspectmode="cube") # fig.update_scenes(xaxis_nticks=1) # fig.update_scenes(yaxis_nticks=1) fig.update_scenes(zaxis_nticks=1) +fig.update_layout(margin=dict(l=0, r=0, t=0, b=0, pad=0)) + +fig.update_layout( + legend=dict( + orientation="v", + yanchor="top", + y=0.99, + xanchor="right", + x=0.1, + ) +) config = { - 'toImageButtonOptions': { - 'format': 'png', # png, svg, pdf, jpeg, webp - 'width': None, # None: use currently-rendered size - 'height': None, - 'filename': 'conditional', - } -} - -style = { - 'resize': 'both', - 'overflow': 'auto', - 'width': f'{relwidth}vw', - 'height': f'{relheight}vw' + 'toImageButtonOptions': { + 'format': 'png', # png, svg, pdf, jpeg, webp + 'width': None, # None: use currently-rendered size + 'height': None, + 'filename': 'conditional', + }, + 'responsive': True, + 'scrollZoom': True, } -layout = dbc.Container( - dbc.Col([ - # Plot - dcc.Graph(id="joint-graph", figure=fig, config=config, style=style), - - html.P(), # style={"margin-bottom": "3cm"} - - # y Slider - dcc.Slider(id="joint-y", min=smin, max=smax, value=randint(smin*10, smax*10)/10, updatemode='drag', marks=None, - tooltip={"template": "ŷ={value}", "placement": "bottom", "always_visible": True}), - - # ρ Slider - dcc.Slider(id="joint-ρ", min=-1, max=1, value=randint(-9, 9)/10, updatemode='mouseup', marks=None, - tooltip={"template": "ρ={value}", "placement": "bottom", "always_visible": True}), - - # Description - dcc.Markdown( - r''' - ## 2D Gaussian - Interactive visualizaton of the 2D Gaussian and its marginal and conditional density. - - $$ - f(\underline x) = \mathcal{N}(\underline x; \underline \mu, \textbf{C}) = - \frac{1}{2\pi \sqrt{\det(\textbf{C})}} - \cdot \exp\!\left\{ -\frac{1}{2} - \cdot (\underline x - \underline \mu)^\top \textbf{C}^{-1} (\underline x - \underline \mu) \right\} \enspace, - \quad \underline{x}\in \mathbb{R}^2 \enspace, \quad \textbf{C} \enspace \text{positive semidefinite} \enspace. - $$ - - ### Formulas and Literature - The Gaussian parameters are restricted to - $$ - \underline \mu = \begin{bmatrix}0 \\ 0\end{bmatrix}\,, \quad - \textbf{C} = \begin{bmatrix}1 & \rho \\ \rho & 1\end{bmatrix} \enspace. - $$ - - Formulas for marginalization and conditioning of are given in the - [[MatrixCookbook](https://www.math.uwaterloo.ca/~hwolkowi/matrixcookbook.pdf)]. - - Note that the 1D and 2D densities are scaled with respect to each other such that 2D joint and 1D marginal have - the same height and therefore the same shape when looking on the x-z plane. - - - ### Interactivity - - GUI - - plot size: initial size from window width; then drag bottom-right corner of graph - - rotate: left mouse click - - pan: right mouse click - - zoom: mouse wheel - - add/remove lines: click in legend - - value in state space (slider) - - value to condition on $\hat{y}$ - - density parameter (slider) - - correlation coefficient $\rho$ - ''', - mathjax=True), - ]), fluid=True, className="g-0") +# Description +info_text = dcc.Markdown( + r''' + ## 2D Gaussian + Interactive visualizaton of the 2D Gaussian and its marginal and conditional density. + + $$ + f(\underline x) = \mathcal{N}(\underline x; \underline \mu, \textbf{C}) = + \frac{1}{2\pi \sqrt{\det(\textbf{C})}} + \cdot \exp\!\left\{ -\frac{1}{2} + \cdot (\underline x - \underline \mu)^\top \textbf{C}^{-1} (\underline x - \underline \mu) \right\} \enspace, + \quad \underline{x}\in \mathbb{R}^2 \enspace, \quad \textbf{C} \enspace \text{positive semidefinite} \enspace. + $$ + + ### Formulas and Literature + The Gaussian parameters are restricted to + $$ + \underline \mu = \begin{bmatrix}0 \\ 0\end{bmatrix}\,, \quad + \textbf{C} = \begin{bmatrix}1 & \rho \\ \rho & 1\end{bmatrix} \enspace. + $$ + + Formulas for marginalization and conditioning of are given in the + [[MatrixCookbook](https://www.math.uwaterloo.ca/~hwolkowi/matrixcookbook.pdf)]. + + Note that the 1D and 2D densities are scaled with respect to each other such that 2D joint and 1D marginal have + the same height and therefore the same shape when looking on the x-z plane. + + + ### Interactivity + - GUI + - rotate: left mouse click + - pan: right mouse click + - zoom: mouse wheel + - add/remove lines: click in legend + - value in state space (slider) + - value to condition on $\hat{y}$ + - density parameter (slider) + - correlation coefficient $\rho$ + ''', + mathjax=True +) +layout = SplitPane([ + dbc.Container( + dbc.Col([ + html.Br(), + + # y Slider + Label("Condition on ŷ", + dcc.Slider( + id="joint-y", + min=smin, + max=smax, + value=randint(smin*10, smax*10)/10, + updatemode='drag', marks=None, + tooltip={"template": "ŷ={value}", "placement": "bottom", "always_visible": True} + ), + ), + + # ρ Slider + Label("Correlation ρ", + dcc.Slider( + id="joint-ρ", + min=-1, max=1, + value=randint(-9, 9)/10, + updatemode='mouseup', + marks=None, + tooltip={"template": "ρ={value}", "placement": "bottom", "always_visible": True} + ) + ), + + html.Hr(), + html.Br(), + + # Info Popup + *PopupBox("joint-info", "Learn More", "Additional Information", info_text), + + ]), + fluid=True, + className="g-0"), + ], + [ + # Plot + dcc.Graph(id="joint-graph", figure=fig, config=config, style={'height': '100%'}), + ], + 30 +) @callback( - Output("joint-graph", "figure"), - Input("joint-y", "value"), - Input("joint-ρ", "value"), + Output("joint-graph", "figure"), + Input("joint-y", "value"), + Input("joint-ρ", "value"), ) def update(ys, ρ): - patched_fig = Patch() - # Joint Parameters - μ = zeros([2, 1]) - sx = 1 - sy = 1 - # TODO special treatment for singular density - ρ = sign(ρ) * min(abs(ρ), .9999) - C = array([[sx**2, sx*sy*ρ], [sx*sy*ρ, sy**2]]) - # Marginal Parameters - µMarginal = µ[0] - CMarginal = C[0, 0] - marginal_fac = 1 / gauss1(0, 0, CMarginal) - # Density has been modified? - if (callback_context.triggered_id == "joint-ρ") | (callback_context.triggered_id is None): - zMarginal = gauss1(xv, µMarginal, CMarginal) - patched_fig['data'][1]['z'] = zMarginal * marginal_fac - # Compute new joint density values - # TODO should be more elegant than 2 for loops - zJoint = xm*0 - for i in range(xm.shape[0]): - for j in range(xm.shape[1]): - zJoint[i, j] = gauss2(xm[i, j], ym[i, j], μ, C) - zJoint = zJoint / gauss2(0, 0, zeros([2, 1]), C) # rescale to height 1 - patched_fig['data'][0]['z'] = zJoint - # Compute Conditional - # https://www.math.uwaterloo.ca/~hwolkowi/matrixcookbook.pdf - µCond = µ[0] + C[0, 1] / C[1, 1] * (ys-µ[1]) - CCond = C[0, 0] - C[0, 1] / C[1, 1] * C[0, 1] - zCond = gauss1(xv, µCond, CCond) - zSlice = zCond / gauss1(0, 0, CCond) * gauss1(ys, µ[1], C[1, 1]) / gauss1(0, 0, C[1, 1]) - # Plot Conditional - patched_fig['data'][2]['z'] = zCond * marginal_fac - # Plot Joint Slice - patched_fig['data'][3]['y'] = xv*0+ys - patched_fig['data'][3]['z'] = zSlice + 1e-3 - return patched_fig + patched_fig = Patch() + # Joint Parameters + μ = zeros([2, 1]) + sx = 1 + sy = 1 + # TODO special treatment for singular density + ρ = sign(ρ) * min(abs(ρ), .9999) + C = array([[sx**2, sx*sy*ρ], [sx*sy*ρ, sy**2]]) + # Marginal Parameters + µMarginal = µ[0] + CMarginal = C[0, 0] + marginal_fac = 1 / gauss1(0, 0, CMarginal) + # Density has been modified? + if (callback_context.triggered_id == "joint-ρ") | (callback_context.triggered_id is None): + zMarginal = gauss1(xv, µMarginal, CMarginal) + patched_fig['data'][1]['z'] = zMarginal * marginal_fac + # Compute new joint density values + # TODO should be more elegant than 2 for loops + zJoint = xm*0 + for i in range(xm.shape[0]): + for j in range(xm.shape[1]): + zJoint[i, j] = gauss2(xm[i, j], ym[i, j], μ, C) + zJoint = zJoint / gauss2(0, 0, zeros([2, 1]), C) # rescale to height 1 + patched_fig['data'][0]['z'] = zJoint + # Compute Conditional + # https://www.math.uwaterloo.ca/~hwolkowi/matrixcookbook.pdf + µCond = µ[0] + C[0, 1] / C[1, 1] * (ys-µ[1]) + CCond = C[0, 0] - C[0, 1] / C[1, 1] * C[0, 1] + zCond = gauss1(xv, µCond, CCond) + zSlice = zCond / gauss1(0, 0, CCond) * gauss1(ys, µ[1], C[1, 1]) / gauss1(0, 0, C[1, 1]) + # Plot Conditional + patched_fig['data'][2]['z'] = zCond * marginal_fac + # Plot Joint Slice + patched_fig['data'][3]['y'] = xv*0+ys + patched_fig['data'][3]['z'] = zSlice + 1e-3 + return patched_fig diff --git a/pages/gauss1D.py b/pages/gauss1D.py index 8d43629..bb3f333 100644 --- a/pages/gauss1D.py +++ b/pages/gauss1D.py @@ -7,107 +7,126 @@ from numpy.random import randn, randint from scipy.special import erfinv +from components.popup_box import PopupBox +from components.split_pane import SplitPane + dash.register_page(__name__) methods = ['iid', 'Golden-Sequence', 'Equidistant', 'Unscented'] config = { - 'toImageButtonOptions': { - 'format': 'svg', # png, svg, pdf, jpeg, webp - 'height': None, # None: use currently-rendered size - 'width': None, - 'filename': 'gauss2d', - }, - 'modeBarButtonsToRemove': ['zoom'], - 'scrollZoom': True, + 'toImageButtonOptions': { + 'format': 'svg', # png, svg, pdf, jpeg, webp + 'height': None, # None: use currently-rendered size + 'width': None, + 'filename': 'gauss2d', + }, + 'modeBarButtonsToRemove': ['zoom'], + 'scrollZoom': True, } -style = { - 'resize': 'both', - 'overflow': 'auto', -} +# Description +info_text = dcc.Markdown( + r''' + ## 1D Gaussian + Interactive visualization of the univariate Gaussian density + + $$ + f(x) = \frac{1}{\sqrt{2\pi}\sigma} + \cdot \exp\!\left\{ -\frac{1}{2} \cdot \left(\frac{x-\mu}{\sigma}\right)^2 \right\}\enspace, \quad x\in \mathbb{R} \enspace, + $$ + + with mean $\mu \in \mathbb{R}$ and standard deviation $\sigma \in \mathbb{R}_+$ . + + It was discovered by Karl Friedrich Gauß (1777-1855) in Göttingen, Germany. + + ### Formulas + - quantile function + $Q(p) = \mu + \sigma\, \sqrt{2}\, \mathrm{erf}^{-1}(2p-1)$ + - uniform to Gaussian + $x_i^{\text{Gauss}} = Q(x_i^{\text{uni}})$ + - golden Kronecker sequence + $x_i^{\text{uni}}=\mod( \Phi \cdot (i+z), 1) \enspace, \quad i \in \{1,2,\ldots,L\}\enspace, \quad z \in \mathbb{Z}$ + - equidistant samples + $x_i^{\text{uni}} = \frac{2 i - 1 + \gamma}{2 L} \enspace, \quad i \in \{1,2,\ldots,L\}\enspace,\quad \gamma\in[-1,1]$ + - unscented (𝐿=2) + $x_1=\mu-\sigma\enspace, \quad x_2=\mu+\sigma$ + - unscented (𝐿=3) + TODO + + ### Interactivity + - GUI + - add/remove lines: click in legend + - sampling methods (radiobutton) + - independent identically distributed (iid), the usual random samples + - golden sequence, a low-discrepancy Kronecker sequence based on the golden ratio + - equidistant, with identical amount of probability mass for all samples + - unscented transform sampling (𝐿=2) + - sampling parameter (slider) + - iid: dice again + - golden: integer offset 𝑧 + - equidistant: offset 𝛾 + - unscented: TODO + - number of Samples 𝐿 (slider) + - density parameters (slider) + - mean 𝜇 + - standard deviation 𝜎 + ''', + mathjax=True +) -layout = dbc.Container( - dbc.Col([ - # Plot - dcc.Graph(id="gauss1D-graph", config=config, style=style), - - # Sampling Strategy RadioItems - dbc.RadioItems(id='gauss1D-smethod', - options=[{"label": x, "value": x} for x in methods], - value=methods[randint(len(methods))], - inline=True), - - html.P(), # style={"margin-bottom": "3cm"} - - # param Slider - dcc.Slider(id="gauss1D-p", min=0, max=1, value=randint(3, 7)/10, - tooltip={"template": "p={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), - - # L Slider - dcc.Slider(id="gauss1D-L", min=0, max=100, step=1, value=randint(5, 25), - tooltip={"template": "L={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), - - # μ Slider - dcc.Slider(id="gauss1D-μ", min=-5, max=5, step=0.01, value=randint(-20, 20)/10, - tooltip={"template": "µ={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), - - # σ Slider - dcc.Slider(id="gauss1D-σ", min=0, max=5, step=0.01, value=randint(5, 20)/10, - tooltip={"template": 'σ={value}', "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), - - # Description - dcc.Markdown( - r''' - ## 1D Gaussian - Interactive visualizaton of the univariate Gaussian density - - $$ - f(x) = \frac{1}{\sqrt{2\pi}\sigma} - \cdot \exp\!\left\{ -\frac{1}{2} \cdot \left(\frac{x-\mu}{\sigma}\right)^2 \right\}\enspace, \quad x\in \mathbb{R} \enspace, - $$ - - with mean $\mu \in \mathbb{R}$ and standard deviation $\sigma \in \mathbb{R}_+$ . - - It was discovered by Karl Friedrich Gauß (1777-1855) in Göttingen, Germany. - - ### Formulas - - quantile function - $Q(p) = \mu + \sigma\, \sqrt{2}\, \mathrm{erf}^{-1}(2p-1)$ - - uniform to Gaussian - $x_i^{\text{Gauss}} = Q(x_i^{\text{uni}})$ - - golden Kronecker sequence - $x_i^{\text{uni}}=\mod( \Phi \cdot (i+z), 1) \enspace, \quad i \in \{1,2,\ldots,L\}\enspace, \quad z \in \mathbb{Z}$ - - equidistant samples - $x_i^{\text{uni}} = \frac{2 i - 1 + \gamma}{2 L} \enspace, \quad i \in \{1,2,\ldots,L\}\enspace,\quad \gamma\in[-1,1]$ - - unscented (𝐿=2) - $x_1=\mu-\sigma\enspace, \quad x_2=\mu+\sigma$ - - unscented (𝐿=3) - TODO - - ### Interactivity - - GUI - - plot size: initial size from window width; then drag bottom-right corner of graph - - add/remove lines: click in legend - - sampling methods (radiobutton) - - independent identically distributed (iid), the usual random samples - - golden sequence, a low-discrepancy Kronecker sequence based on the golden ratio - - equidistant, with identical amount of probability mass for all samples - - unscented transform sampling (𝐿=2) - - sampling parameter (slider) - - iid: dice again - - golden: integer offset 𝑧 - - equidistant: offset 𝛾 - - unscented: TODO - - number of Samples 𝐿 (slider) - - density parameters (slider) - - mean 𝜇 - - standard deviation 𝜎 - ''', - mathjax=True), - - ]), fluid=True, className="g-0") +layout = SplitPane([ + dbc.Container( + dbc.Col([ + html.P("Select Sampling Method:"), + html.Br(), + + # Sampling Strategy RadioItems + dbc.RadioItems(id='gauss1D-smethod', + options=[{"label": x, "value": x} for x in methods], + value=methods[randint(len(methods))], + inline=True), + + html.Br(), + html.Hr(), + html.Br(), + + # param Slider + dcc.Slider(id="gauss1D-p", min=0, max=1, value=randint(3, 7)/10, + tooltip={"template": "p={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), + + # L Slider + dcc.Slider(id="gauss1D-L", min=0, max=100, step=1, value=randint(5, 25), + tooltip={"template": "L={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), + + # μ Slider + dcc.Slider(id="gauss1D-μ", min=-5, max=5, step=0.01, value=randint(-20, 20)/10, + tooltip={"template": "µ={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), + + # σ Slider + dcc.Slider(id="gauss1D-σ", min=0, max=5, step=0.01, value=randint(5, 20)/10, + tooltip={"template": 'σ={value}', "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), + + html.Hr(), + html.Br(), + + # Info Popup + *PopupBox("gauss1D-info", "Learn More", "Additional Information", info_text), + + + ]) + , + fluid=True, + className="g-0" + ) + ], + [ + # Plot + dcc.Graph(id="gauss1D-graph", config=config, style={'height': '100%'}), + ], + 30 +) col_density = plotly.colors.qualitative.Plotly[1] @@ -116,87 +135,95 @@ @callback( - Output('gauss1D-p', 'min'), - Output('gauss1D-p', 'max'), - Output('gauss1D-p', 'value'), - Output('gauss1D-p', 'step'), - Output('gauss1D-p', 'tooltip'), - Output('gauss1D-L', 'disabled'), - Input("gauss1D-smethod", "value"), + Output('gauss1D-p', 'min'), + Output('gauss1D-p', 'max'), + Output('gauss1D-p', 'value'), + Output('gauss1D-p', 'step'), + Output('gauss1D-p', 'tooltip'), + Output('gauss1D-L', 'disabled'), + Input("gauss1D-smethod", "value"), ) def update_smethod(smethod): - patched_tooltip = Patch() - match smethod: - case 'iid': - patched_tooltip.template = "dice" - return 0, 1, .5, 0.001, patched_tooltip, False - case 'Golden-Sequence': - patched_tooltip.template = "z={value}" - return -50, 50, 0, 1, patched_tooltip, False - case 'Equidistant': - patched_tooltip.template = "γ={value}" - return -1, 1, 0, 0.001, patched_tooltip, False - case 'Unscented': - patched_tooltip.template = "{value}" - return 0, 2, 1, 0.001, patched_tooltip, True - case _: - raise Exception("Wrong smethod") + patched_tooltip = Patch() + match smethod: + case 'iid': + patched_tooltip.template = "dice" + return 0, 1, .5, 0.001, patched_tooltip, False + case 'Golden-Sequence': + patched_tooltip.template = "z={value}" + return -50, 50, 0, 1, patched_tooltip, False + case 'Equidistant': + patched_tooltip.template = "γ={value}" + return -1, 1, 0, 0.001, patched_tooltip, False + case 'Unscented': + patched_tooltip.template = "{value}" + return 0, 2, 1, 0.001, patched_tooltip, True + case _: + raise Exception("Wrong smethod") @callback( - Output("gauss1D-graph", "figure"), - Input("gauss1D-smethod", "value"), - Input("gauss1D-p", "value"), - Input("gauss1D-L", "value"), - Input("gauss1D-μ", "value"), - Input("gauss1D-σ", "value"), + Output("gauss1D-graph", "figure"), + Input("gauss1D-smethod", "value"), + Input("gauss1D-p", "value"), + Input("gauss1D-L", "value"), + Input("gauss1D-μ", "value"), + Input("gauss1D-σ", "value"), ) def update(smethod, p, L, μ, σ): - fig = go.Figure() - if σ == 0: - # Dirac Delta - fig.add_trace(go.Scatter(x=[rang[0], μ, μ, μ, rang[1]], y=[0, 0, 1, 0, 0], hoverinfo='skip', line={'width': 5}, name='Dirac Delta', marker_color=col_density, showlegend=True)) - if L > 0: - fig.add_trace(go.Scatter(x=[μ, μ], y=[0, 1], name='Samples', mode='lines', marker_color=col_samples, showlegend=True)) - else: - # Draw Samples - xGauss = None - match smethod: - case 'iid': - # xUni = sort(rand(L)) - xGauss = sort(randn(L)*σ + μ) - case 'Golden-Sequence': - xUni = (sqrt(5)-1)/2 * (arange(L)+1+round(p)) % 1 - case 'Equidistant': - xUni = (2*arange(L)+1+p)/(2*L) - case 'Unscented': - # TODO scaled unscented etc - xGauss = array([μ-σ, μ+σ]) # TODO parameter - case _: - raise Exception("Wrong smethod") - # Transform Samples - if xGauss is None: - xGauss = σ*sqrt(2)*erfinv(2*xUni-1) + μ - L2 = len(xGauss) - sample_height = full([1, L2], gauss1(0, 0, σ)) - # sample_height = full([1, L], 1/L) - # Plot Density - s = linspace(rang[0], rang[1], 500) - # https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html - # TODO lighter fillcolor: fillcolor=matplotlib.colors.to_rgba('#aabbcc80') - fig.add_trace(go.Scatter(x=s, y=gauss1(s, μ, σ), hoverinfo='skip', line={'width': 5}, line_shape='spline', name='Density', fill='tozeroy', marker_color=col_density, showlegend=True)) - # Plot Samples - xp = vstack((xGauss, xGauss, full([1, L2], nan))).T.flatten() - yp = vstack((full([1, L2], 0), sample_height, full([1, L2], nan))).T.flatten() - fig.add_trace(go.Scatter(x=xp, y=yp, name='Samples', mode='lines', marker_color=col_samples, showlegend=True)) - # Style - fig.update_xaxes(range=rang, tickmode='array', tickvals=list(range(-5, 6))) - fig.update_yaxes(range=[0, None], fixedrange=True) - fig.update_layout(legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)) - fig.update_layout(dragmode="pan") - # fig.update_layout(transition_duration=100, transition_easing='linear') - return fig + fig = go.Figure() # TODO use Patch here too + + if σ == 0: + # Dirac Delta + fig.add_trace(go.Scatter(x=[rang[0], μ, μ, μ, rang[1]], y=[0, 0, 1, 0, 0], hoverinfo='skip', line={'width': 5}, name='Dirac Delta', marker_color=col_density, showlegend=True)) + if L > 0: + fig.add_trace(go.Scatter(x=[μ, μ], y=[0, 1], name='Samples', mode='lines', marker_color=col_samples, showlegend=True)) + else: + # Draw Samples + xGauss = None + match smethod: + case 'iid': + # xUni = sort(rand(L)) + xGauss = sort(randn(L)*σ + μ) + case 'Golden-Sequence': + xUni = (sqrt(5)-1)/2 * (arange(L)+1+round(p)) % 1 + case 'Equidistant': + xUni = (2*arange(L)+1+p)/(2*L) + case 'Unscented': + # TODO scaled unscented etc + xGauss = array([μ-σ, μ+σ]) # TODO parameter + case _: + raise Exception("Wrong smethod") + # Transform Samples + if xGauss is None: + xGauss = σ*sqrt(2)*erfinv(2*xUni-1) + μ + L2 = len(xGauss) + sample_height = full([1, L2], gauss1(0, 0, σ)) + # sample_height = full([1, L], 1/L) + # Plot Density + s = linspace(rang[0], rang[1], 500) + # https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html + # TODO lighter fillcolor: fillcolor=matplotlib.colors.to_rgba('#aabbcc80') + fig.add_trace(go.Scatter(x=s, y=gauss1(s, μ, σ), hoverinfo='skip', line={'width': 5}, line_shape='spline', name='Density', fill='tozeroy', marker_color=col_density, showlegend=True)) + # Plot Samples + xp = vstack((xGauss, xGauss, full([1, L2], nan))).T.flatten() + yp = vstack((full([1, L2], 0), sample_height, full([1, L2], nan))).T.flatten() + fig.add_trace(go.Scatter(x=xp, y=yp, name='Samples', mode='lines', marker_color=col_samples, showlegend=True)) + # Style + fig.update_xaxes(range=rang, tickmode='array', tickvals=list(range(-5, 6))) + fig.update_yaxes(range=[0, None], fixedrange=True) + fig.update_layout(legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)) + fig.update_layout(dragmode="pan") + + fig.update_layout( + legend=dict( + orientation="v", + xanchor="right", + x=0.1, + ) + ) + return fig def gauss1(x, μ, σ): - return 1/sqrt(2*pi*σ) * exp(-1/2 * square((x-μ)/σ)) + return 1/sqrt(2*pi*σ) * exp(-1/2 * square((x-μ)/σ)) diff --git a/pages/gauss2D.py b/pages/gauss2D.py index d914f4e..4bdf6fe 100644 --- a/pages/gauss2D.py +++ b/pages/gauss2D.py @@ -10,6 +10,9 @@ from numpy.linalg import cholesky, eig, det, inv from scipy.special import erfinv +from components.popup_box import PopupBox +from components.split_pane import SplitPane + dash.register_page(__name__) @@ -19,18 +22,18 @@ # Get Samples from Library (and load if not available) def get_data(url): - if not (url in data_dict): - try: - data = pandas.read_csv(url, header=None).to_numpy() - except HTTPError: - # URL doesn't exist - data = full([2, 0], nan) - data_dict[url] = data - return data_dict[url] + if not (url in data_dict): + try: + data = pandas.read_csv(url, header=None).to_numpy() + except HTTPError: + # URL doesn't exist + data = full([2, 0], nan) + data_dict[url] = data + return data_dict[url] def url_SND_LCD(D, L): - return f'https://raw.githubusercontent.com/KIT-ISAS/deterministic-samples-csv/main/standard-normal/glcd/D{D}-N{L}.csv' + return f'https://raw.githubusercontent.com/KIT-ISAS/deterministic-samples-csv/main/standard-normal/glcd/D{D}-N{L}.csv' # Define Parameters @@ -54,284 +57,305 @@ def url_SND_LCD(D, L): # Initialize Plot # https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html fig = go.Figure() -fig.add_trace(go.Scatter(name='Density', x=[0], y=[0], mode='lines', marker_color=col_density, showlegend=True, hoverinfo='skip', line={'width': 3}, line_shape='spline', fill='tozerox')) -fig.add_trace(go.Scatter(name='Samples', x=[0], y=[0], mode='markers', marker_color=col_samples, marker_line_color='black', marker_opacity=1, showlegend=True)) +fig.add_trace(go.Scattergl(name='Density', x=[0], y=[0], mode='lines', marker_color=col_density, showlegend=True, hoverinfo='skip', line={'width': 3}, line_shape='linear', fill='tozerox')) +fig.add_trace(go.Scattergl(name='Samples', x=[0], y=[0], mode='markers', marker_color=col_samples, marker_line_color='black', marker_opacity=1, showlegend=True)) fig.update_xaxes(range=rangx, tickmode='array', tickvals=list(range(rangx[0], rangx[1]+1))) fig.update_yaxes(range=rangy, tickmode='array', tickvals=list(range(rangy[0], rangy[1]+1)), scaleanchor="x", scaleratio=1) fig.update_layout(legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)) # fig.update_layout(transition_duration=100, transition_easing='linear') fig.update_layout(modebar_add=['drawopenpath', 'eraseshape'], newshape_line_color='cyan', dragmode='pan') +fig.update_layout( + legend=dict( + orientation="v", + xanchor="right", + x=0.1, + ) +) + config = { - 'toImageButtonOptions': { - 'format': 'svg', # png, svg, pdf, jpeg, webp - 'height': None, # None: use currently-rendered size - 'width': None, - 'filename': 'gauss2d', - }, - 'scrollZoom': True, + 'toImageButtonOptions': { + 'format': 'jpeg', # png, svg, pdf, jpeg, webp + 'height': None, # None: use currently-rendered size + 'width': None, + 'filename': 'gauss2d', + }, + 'scrollZoom': True, } -style = { - 'resize': 'both', - 'overflow': 'auto', - 'width': f'{relwidth}vw', - 'height': f'{relheight}vw', -} -layout = dbc.Container( - dbc.Col([ - # Plot - dcc.Graph(id="gauss2D-graph", figure=fig, config=config, style=style), - - # Sampling Strategy RadioItems - dbc.RadioItems(id='gauss2D-smethod', - options=[{"label": x, "value": x} for x in smethods], - value=smethods[randint(len(smethods))], - inline=True), - - # Transformation Method RadioItems - dbc.RadioItems(id='gauss2D-tmethod', - options=[{"label": x, "value": x} for x in tmethods], - value=tmethods[randint(len(tmethods))], - inline=True), - - html.P(), # style={"margin-bottom": "3cm"} - - # param Slider - dcc.Slider(id="gauss2D-p", min=0, max=1, value=randint(3, 7)/10, updatemode='drag', marks=None, - tooltip={"template": "p={value}", "placement": "bottom", "always_visible": True}), - - # L Slider - dcc.Slider(id="gauss2D-L", min=log10(1.2), max=4.001, step=0.001, value=2, updatemode='drag', marks=None, # persistence=True, - tooltip={"template": "L={value}", "placement": "bottom", "always_visible": True, "transform": "trafo_L"}), - - # σ Slider - dcc.Slider(id="gauss2D-σx", min=0, max=5, step=0.01, value=1, updatemode='drag', marks=None, - tooltip={"template": 'σx={value}', "placement": "bottom", "always_visible": True}), - dcc.Slider(id="gauss2D-σy", min=0, max=5, step=0.01, value=1, updatemode='drag', marks=None, - tooltip={"template": 'σy={value}', "placement": "bottom", "always_visible": True}), - - # ρ Slider - dcc.Slider(id="gauss2D-ρ", min=-1, max=1, step=0.001, value=0, updatemode='drag', marks=None, - tooltip={"template": 'ρ={value}', "placement": "bottom", "always_visible": True}), - - # Description - dcc.Markdown( - r''' - ## 2D Gaussian - Interactive visualizaton of the bivariate Gaussian density - - $$ - f(\underline x) = \mathcal{N}(\underline x; \underline \mu, \textbf{C}) = - \frac{1}{2\pi \sqrt{\det(\textbf{C})}} - \cdot \exp\!\left\{ -\frac{1}{2} - \cdot (\underline x - \underline \mu)^\top \textbf{C}^{-1} (\underline x - \underline \mu) \right\} \enspace, - \quad \underline{x}\in \mathbb{R}^2 \enspace, \quad \textbf{C} \enspace \text{positive semidefinite} \enspace. - $$ - - ### Formulas and Literature - - quantile function - $Q(p) = \sqrt{2}\, \mathrm{erf}^{-1}(2p-1)$ - - uniform to SND - $\underline x_i^{\text{SND}} = Q(\underline x_i^{\text{uni}})$ - - SND to Gauss: Cholesky - $\underline x_i^{\text{Gauss}} = \mathrm{chol}(\textbf{C}) \cdot \underline x_i^{\text{SND}}$ - - SND to Gauss: Eigendecomposition - [[Frisch23](https://isif.org/media/generalized-fibonacci-grid-low-discrepancy-point-set-optimal-deterministic-gaussian), eq. 18], - [[Frisch21](https://ieeexplore.ieee.org/document/9626975), eq. 4] - $\underline x_i^{\text{Gauss}} = \mathbf{V} \cdot \sqrt{\mathbf{D}} \cdot \underline x_i^{\text{SND}}$ - - Fibonacci-Kronecker Lattice - $\underline x_i^{\text{uni}} = \begin{bmatrix}\mod( \Phi \cdot (i+z), 1) \\ \frac{2 i - 1 + \gamma}{2 L} \end{bmatrix} \enspace, - \quad i \in \{1,2,\ldots,L\}\enspace, \quad z \in \mathbb{Z} \enspace, \quad \gamma\in[-1,1]$ - - LCD: Localized Cumulative Distribution - [[Hanebeck08](https://ieeexplore.ieee.org/document/4648104)], - [[Hanebeck09](https://ieeexplore.ieee.org/document/5400649)], - loaded from [library](https://github.com/KIT-ISAS/deterministic-samples-csv) - $K(\underline x - \underline m, b) = \exp\!\left\{ -\frac{1}{2} \cdot \left\Vert \frac{\underline x - \underline m}{b} \right\Vert_2^2 \right\} \enspace, \quad - F(\underline m, b) = \int_{\mathbb{R}^2} f(\underline x) \, K(\underline x - \underline m, b) \, \mathrm{d} \underline x \enspace,$ - $\widetilde f(x) = \mathcal{N}(\underline x; \underline 0, \textbf{I}) \enspace, \quad - f(\underline x) = \sum_{i=1}^L \delta(\underline x - \underline x_i) \enspace,$ - $D = \int_{\mathbb{R}_+} w(b) \int_{\mathbb{R}^2} \left( \widetilde F(\underline m, b) - F(\underline m, b) \right)^2 \mathrm{d} \underline m \, \mathrm{d} b \enspace,$ - $\left\{\underline x_i^{\text{SND}}\right\}_{i=0}^L = \arg \min_{\underline x_i} \{D\}$ - - SP-Julier14: Sigma Points - [[Julier04](https://ieeexplore.ieee.org/document/1271397), eq. 12] - $L=2\cdot d + 1 \enspace, \quad i\in\{1,2,\dots,d\} \enspace,$ - $\underline x_0=\underline 0 \enspace, \quad W_0 < 1 \enspace,$ - $\underline x_i = \sqrt{\frac{L}{1-W_0}} \cdot \underline e_i \enspace, \quad W_i = \frac{1-W_0}{2 L} \enspace,$ - $\underline x_{i+L} = -x_i \enspace, \quad W_{i+L} = W_i$ - - SP-Menegaz11: Minimum Sigma Set - [[Menegaz11](https://ieeexplore.ieee.org/abstract/document/6161480), eq. 2-8] - $L=d+1 \enspace, \quad i \in \{1,2, \dots d\} \enspace,$ - $0 < w_0 < 1 \enspace, \quad - \alpha=\sqrt{\frac{1-w_0}{d}} \enspace, \quad - C = \sqrt{\mathbf{I} - \alpha^2 \cdot \mathbf 1} \enspace, \quad - \underline x_0 = - \frac{\alpha}{\sqrt{w_0}} \cdot \underline 1$ - $w_{1\colon n} = \mathrm{diag}(w_0 \cdot \alpha^2 \cdot C^{-1} \cdot \mathbf{1} \cdot (C^\top)^{-1}) \enspace,$ - $\underline x_{1\colon n} = C \cdot (\sqrt{\mathbf{I} \cdot w_{1\colon n}})^{-1}$ - ### Interactivity - - GUI - - plot size: initial size from window width; then drag bottom-right corner of graph - - add/remove lines: click in legend - - sampling methods (radiobutton) - - Independent identically distributed (iid), the usual random samples. - - Fibonacci-Kronecker lattice, combination of 1D golden sequence and equidistant. Use with eigendecomposition for best homogeneity. - - LCD SND samples. - - Sigma Points - Julier04. - - transformation methods (radiobutton) - - Cholesky decomposition. - - Eigenvalue-Eigenvector decomposition. - - sampling parameter (slider) - - iid: dice again - - Fibonacci: integer offset 𝑧, offset 𝛾 - - LCD: SND rotation 𝛼, a proxy for dependency on initial guess during optimization - - sigma points: scaling parameter - - number of Samples 𝐿 (slider) - - density parameters (slider) - - standard deviation $\sigma_x$ - - standard deviation $\sigma_y$ - - correlation coefficient $\rho$ - ''', - mathjax=True), - ]), fluid=True, className="g-0") +# Description +info_text = dcc.Markdown( + r''' + ## 2D Gaussian + Interactive visualizaton of the bivariate Gaussian density + + $$ + f(\underline x) = \mathcal{N}(\underline x; \underline \mu, \textbf{C}) = + \frac{1}{2\pi \sqrt{\det(\textbf{C})}} + \cdot \exp\!\left\{ -\frac{1}{2} + \cdot (\underline x - \underline \mu)^\top \textbf{C}^{-1} (\underline x - \underline \mu) \right\} \enspace, + \quad \underline{x}\in \mathbb{R}^2 \enspace, \quad \textbf{C} \enspace \text{positive semidefinite} \enspace. + $$ + + ### Formulas and Literature + - quantile function + $Q(p) = \sqrt{2}\, \mathrm{erf}^{-1}(2p-1)$ + - uniform to SND + $\underline x_i^{\text{SND}} = Q(\underline x_i^{\text{uni}})$ + - SND to Gauss: Cholesky + $\underline x_i^{\text{Gauss}} = \mathrm{chol}(\textbf{C}) \cdot \underline x_i^{\text{SND}}$ + - SND to Gauss: Eigendecomposition + [[Frisch23](https://isif.org/media/generalized-fibonacci-grid-low-discrepancy-point-set-optimal-deterministic-gaussian), eq. 18], + [[Frisch21](https://ieeexplore.ieee.org/document/9626975), eq. 4] + $\underline x_i^{\text{Gauss}} = \mathbf{V} \cdot \sqrt{\mathbf{D}} \cdot \underline x_i^{\text{SND}}$ + - Fibonacci-Kronecker Lattice + $\underline x_i^{\text{uni}} = \begin{bmatrix}\mod( \Phi \cdot (i+z), 1) \\ \frac{2 i - 1 + \gamma}{2 L} \end{bmatrix} \enspace, + \quad i \in \{1,2,\ldots,L\}\enspace, \quad z \in \mathbb{Z} \enspace, \quad \gamma\in[-1,1]$ + - LCD: Localized Cumulative Distribution + [[Hanebeck08](https://ieeexplore.ieee.org/document/4648104)], + [[Hanebeck09](https://ieeexplore.ieee.org/document/5400649)], + loaded from [library](https://github.com/KIT-ISAS/deterministic-samples-csv) + $K(\underline x - \underline m, b) = \exp\!\left\{ -\frac{1}{2} \cdot \left\Vert \frac{\underline x - \underline m}{b} \right\Vert_2^2 \right\} \enspace, \quad + F(\underline m, b) = \int_{\mathbb{R}^2} f(\underline x) \, K(\underline x - \underline m, b) \, \mathrm{d} \underline x \enspace,$ + $\widetilde f(x) = \mathcal{N}(\underline x; \underline 0, \textbf{I}) \enspace, \quad + f(\underline x) = \sum_{i=1}^L \delta(\underline x - \underline x_i) \enspace,$ + $D = \int_{\mathbb{R}_+} w(b) \int_{\mathbb{R}^2} \left( \widetilde F(\underline m, b) - F(\underline m, b) \right)^2 \mathrm{d} \underline m \, \mathrm{d} b \enspace,$ + $\left\{\underline x_i^{\text{SND}}\right\}_{i=0}^L = \arg \min_{\underline x_i} \{D\}$ + - SP-Julier14: Sigma Points + [[Julier04](https://ieeexplore.ieee.org/document/1271397), eq. 12] + $L=2\cdot d + 1 \enspace, \quad i\in\{1,2,\dots,d\} \enspace,$ + $\underline x_0=\underline 0 \enspace, \quad W_0 < 1 \enspace,$ + $\underline x_i = \sqrt{\frac{L}{1-W_0}} \cdot \underline e_i \enspace, \quad W_i = \frac{1-W_0}{2 L} \enspace,$ + $\underline x_{i+L} = -x_i \enspace, \quad W_{i+L} = W_i$ + - SP-Menegaz11: Minimum Sigma Set + [[Menegaz11](https://ieeexplore.ieee.org/abstract/document/6161480), eq. 2-8] + $L=d+1 \enspace, \quad i \in \{1,2, \dots d\} \enspace,$ + $0 < w_0 < 1 \enspace, \quad + \alpha=\sqrt{\frac{1-w_0}{d}} \enspace, \quad + C = \sqrt{\mathbf{I} - \alpha^2 \cdot \mathbf 1} \enspace, \quad + \underline x_0 = - \frac{\alpha}{\sqrt{w_0}} \cdot \underline 1$ + $w_{1\colon n} = \mathrm{diag}(w_0 \cdot \alpha^2 \cdot C^{-1} \cdot \mathbf{1} \cdot (C^\top)^{-1}) \enspace,$ + $\underline x_{1\colon n} = C \cdot (\sqrt{\mathbf{I} \cdot w_{1\colon n}})^{-1}$ + ### Interactivity + - GUI + - add/remove lines: click in legend + - sampling methods (radiobutton) + - Independent identically distributed (iid), the usual random samples. + - Fibonacci-Kronecker lattice, combination of 1D golden sequence and equidistant. Use with eigendecomposition for best homogeneity. + - LCD SND samples. + - Sigma Points - Julier04. + - transformation methods (radiobutton) + - Cholesky decomposition. + - Eigenvalue-Eigenvector decomposition. + - sampling parameter (slider) + - iid: dice again + - Fibonacci: integer offset 𝑧, offset 𝛾 + - LCD: SND rotation 𝛼, a proxy for dependency on initial guess during optimization + - sigma points: scaling parameter + - number of Samples 𝐿 (slider) + - density parameters (slider) + - standard deviation $\sigma_x$ + - standard deviation $\sigma_y$ + - correlation coefficient $\rho$ + ''', + mathjax=True +) + +layout =SplitPane([ + dbc.Container( + dbc.Col([ + html.P("Select Sampling Method:"), + html.Br(), + + # Sampling Strategy RadioItems + dbc.RadioItems(id='gauss2D-smethod', + options=[{"label": x, "value": x} for x in smethods], + value=smethods[randint(len(smethods))], + inline=True), + + # Transformation Method RadioItems + dbc.RadioItems(id='gauss2D-tmethod', + options=[{"label": x, "value": x} for x in tmethods], + value=tmethods[randint(len(tmethods))], + inline=True), + + html.Br(), + html.Hr(), + html.Br(), + + # param Slider + dcc.Slider(id="gauss2D-p", min=0, max=1, value=randint(3, 7)/10, updatemode='drag', marks=None, + tooltip={"template": "p={value}", "placement": "bottom", "always_visible": True}), + + # L Slider + dcc.Slider(id="gauss2D-L", min=log10(1.2), max=4.001, step=0.001, value=2, updatemode='drag', marks=None, # persistence=True, + tooltip={"template": "L={value}", "placement": "bottom", "always_visible": True, "transform": "trafo_L"}), + + # σ Slider + dcc.Slider(id="gauss2D-σx", min=0, max=5, step=0.01, value=1, updatemode='drag', marks=None, + tooltip={"template": 'σx={value}', "placement": "bottom", "always_visible": True}), + dcc.Slider(id="gauss2D-σy", min=0, max=5, step=0.01, value=1, updatemode='drag', marks=None, + tooltip={"template": 'σy={value}', "placement": "bottom", "always_visible": True}), + + # ρ Slider + dcc.Slider(id="gauss2D-ρ", min=-1, max=1, step=0.001, value=0, updatemode='drag', marks=None, + tooltip={"template": 'ρ={value}', "placement": "bottom", "always_visible": True}), + + html.Hr(), + html.Br(), + + + # Info Popup + *PopupBox("gauss2D-info", "Learn More", "Additional Information", info_text), + ]), + fluid=True, + className="g-0") + ], + [ + # Plot + dcc.Graph(id="gauss2D-graph", figure=fig, config=config, style={'height': '100%'}), + ], + 30 +) @callback( - Output('gauss2D-p', 'min'), - Output('gauss2D-p', 'max'), - Output('gauss2D-p', 'value'), - Output('gauss2D-p', 'step'), - Output('gauss2D-p', 'tooltip'), - Output('gauss2D-L', 'disabled'), - Input("gauss2D-smethod", "value"), + Output('gauss2D-p', 'min'), + Output('gauss2D-p', 'max'), + Output('gauss2D-p', 'value'), + Output('gauss2D-p', 'step'), + Output('gauss2D-p', 'tooltip'), + Output('gauss2D-L', 'disabled'), + Input("gauss2D-smethod", "value"), ) def update_smethod(smethod): - patched_tooltip = Patch() - match smethod: - case 'iid': - patched_tooltip.template = "dice" - # min, max, value, step, tooltip - return 0, 1, .5, 0.001, patched_tooltip, False - case 'Fibonacci': - patched_tooltip.template = "z={value}" - return -50, 50, 0, 1, patched_tooltip, False - case 'LCD': - patched_tooltip.template = "α={value}°" - return -360, 360, 0, 0.1, patched_tooltip, False - case 'SP-Julier04': - patched_tooltip.template = "W₀={value}" - return -2, 1, .1, 0.001, patched_tooltip, True - case 'SP-Menegaz11': - patched_tooltip.template = "Wₙ₊₁={value}" - return 0, 1, 1/3, 0.001, patched_tooltip, True - case _: - raise Exception("Wrong smethod") + patched_tooltip = Patch() + match smethod: + case 'iid': + patched_tooltip.template = "dice" + # min, max, value, step, tooltip + return 0, 1, .5, 0.001, patched_tooltip, False + case 'Fibonacci': + patched_tooltip.template = "z={value}" + return -50, 50, 0, 1, patched_tooltip, False + case 'LCD': + patched_tooltip.template = "α={value}°" + return -360, 360, 0, 0.1, patched_tooltip, False + case 'SP-Julier04': + patched_tooltip.template = "W₀={value}" + return -2, 1, .1, 0.001, patched_tooltip, True + case 'SP-Menegaz11': + patched_tooltip.template = "Wₙ₊₁={value}" + return 0, 1, 1/3, 0.001, patched_tooltip, True + case _: + raise Exception("Wrong smethod") @callback( - Output("gauss2D-graph", "figure"), - Input("gauss2D-smethod", "value"), - Input("gauss2D-tmethod", "value"), - Input("gauss2D-p", "value"), - Input("gauss2D-L", "value"), - Input("gauss2D-σx", "value"), - Input("gauss2D-σy", "value"), - Input("gauss2D-ρ", "value"), + Output("gauss2D-graph", "figure"), + Input("gauss2D-smethod", "value"), + Input("gauss2D-tmethod", "value"), + Input("gauss2D-p", "value"), + Input("gauss2D-L", "value"), + Input("gauss2D-σx", "value"), + Input("gauss2D-σy", "value"), + Input("gauss2D-ρ", "value"), ) def update(smethod, tmethod, p, L0, σx, σy, ρ): - # Slider Transform, - L = trafo_L(L0) - # Mean - # μ = array([[μx], [μy]]) - μ = array([[0], [0]]) - # Covariance - C = array([[square(σx), σx*σy*ρ], [σx*σy*ρ, square(σy)]]) - C_D, C_R = eig(C) - C_D = C_D[..., None] # to column vector - - patched_fig = Patch() - # Draw SND - weights = None - match smethod: - case 'iid': - xySND = randn(2, L) - case 'Fibonacci': - # TODO 2nd parameter - xUni = (sqrt(5)-1)/2 * (arange(L)+1+round(p)) % 1 - yUni = (2*arange(L)+1)/(2*L) # +p - xyUni = vstack((xUni, yUni)) - xySND = sqrt(2)*erfinv(2*xyUni-1) - case 'LCD': - xySND = get_data(url_SND_LCD(2, L)) - xySND = matmul(rot(p), xySND) - case 'SP-Julier04': - # https://ieeexplore.ieee.org/abstract/document/1271397 - Nx = 2 # dimension - x0 = zeros([Nx, 1]) - W0 = full([1, 1], p) # parameter, W0<1 - x1 = sqrt(Nx/(1-W0) * identity(Nx)) - W1 = full([1, Nx], (1-W0)/(2*Nx)) - x2 = -x1 - W2 = W1 - xySND = hstack((x0, x1, x2)) - weights = hstack((W0, W1, W2)) - case 'SP-Menegaz11': - # https://ieeexplore.ieee.org/abstract/document/6161480 - n = 2 # dimension - w0 = p # parameter, 0 Date: Fri, 5 Dec 2025 15:58:09 +0100 Subject: [PATCH 111/152] use newton for erfi_inf --- .../distributions/sphere/watson/fibonachi.py | 67 ++++++++++--------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/model/distributions/sphere/watson/fibonachi.py b/model/distributions/sphere/watson/fibonachi.py index a4417bf..22a9a8f 100644 --- a/model/distributions/sphere/watson/fibonachi.py +++ b/model/distributions/sphere/watson/fibonachi.py @@ -26,13 +26,8 @@ def get_name(self): def sample(self, sample_options, distribution_options): - kappa = distribution_options[0].state - if kappa < 0: - return self.sample_closed(sample_options, distribution_options) - elif kappa < 30: - return self.sample_inverse_ode(sample_options, distribution_options) - else: - return self.sample_inverse_interpolation(sample_options, distribution_options) + return self.sample_closed(sample_options, distribution_options) + def sample_inverse_interpolation(self, sample_options, distribution_options): kappa = distribution_options[0].state sample_count = sample_options[0].state @@ -149,30 +144,42 @@ def f(p,w): x_i_f_2 = np.sqrt(1-w**2) * np.sin( (2 * np.pi * indices) / gold_seq) x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) # order so that mu=[0, 0, 1] return x_i_f - + @staticmethod - def erfi_inv(x): - x = np.asarray(x, dtype=float) - - def _scalar_inv(y): - if y == 0.0: - return 0.0 - sgn = 1.0 if y > 0 else -1.0 - y = abs(y) - - # Find an upper bracket hi with erfi(hi) >= y (erfi is monotone) - hi = max(1.0, 0.5 * np.sqrt(np.pi) * y) # decent first guess - while erfi(hi) < y: - hi *= 2.0 - if hi > 30: # erfi(30) is huge - break - - # Solve g(t) = erfi(t) - y = 0 on [0, hi] - g = lambda t: erfi(t) - y - t = scipy.optimize.brentq(g, 0.0, hi, xtol=1e-12, rtol=1e-12, maxiter=200) - return sgn * t - - return np.vectorize(_scalar_inv, otypes=[float])(x) + def erfi_inv(y, iters=8, thresh=1.5): + y = np.asarray(y, dtype=float) + sgn = np.sign(y) + z = np.abs(y) + + x = np.zeros_like(z) + mask = z > 0 + if not np.any(mask): + return x # all zeros + + z_nz = z[mask] + x0 = np.empty_like(z_nz) + + # Small region: Taylor + small = z_nz <= thresh + large = ~small + + x0[small] = z_nz[small] * np.sqrt(np.pi) / 2.0 + + # Large region: leading asymptotic, ignoring 1/x + if np.any(large): + t = np.log(z_nz[large] * np.sqrt(np.pi)) + t = np.maximum(t, 0.0) + x0[large] = np.sqrt(t) + + # Newton + x_new = x0 + for _ in range(iters): + fx = erfi(x_new) - z_nz + dfx = 2.0 / np.sqrt(np.pi) * np.exp(x_new**2) + x_new = x_new - fx / dfx + + x[mask] = x_new + return sgn * x def sample_closed(self, sample_options, distribution_options): From 34f6544262fafede19cdc5d65517a34e370c414a Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Fri, 5 Dec 2025 16:42:18 +0100 Subject: [PATCH 112/152] improve mobile support zomming is behaving weird, might be related to https://github.com/plotly/plotly.js/issues/5560 --- assets/full_page.css | 34 +++++++++++++++++++++++++++++++++- components/split_pane.py | 5 ++++- pages/conditional.py | 14 +++++++------- renderer/Object3DRenderer.py | 8 +++++--- 4 files changed, 49 insertions(+), 12 deletions(-) diff --git a/assets/full_page.css b/assets/full_page.css index cb06408..349cc9e 100644 --- a/assets/full_page.css +++ b/assets/full_page.css @@ -10,4 +10,36 @@ display: flex; flex-direction: column; min-height: 0; -} \ No newline at end of file +} + + +#panelgroup-parent-container { + min-height: 0; +} + +.split-pane-group { + height: 100%; +} + +@media (min-width: 768px) { + .split-pane-handle { + cursor: col-resize; + } +} + +/* < md: "mobile" layout */ +@media (max-width: 767.98px) { + .split-pane-group { + flex-direction: column !important; + } + + + .split-pane-left .bg-light { + max-height: 40vh; + overflow-y: auto; + } + + .split-pane-handle { + display: none !important; /* no resize on mobile */ + } +} diff --git a/components/split_pane.py b/components/split_pane.py index 175b7ec..2121dcc 100644 --- a/components/split_pane.py +++ b/components/split_pane.py @@ -11,6 +11,7 @@ def SplitPane(children1, children2, default_size): children=[ Panel( id='left-sidebar', + className="split-pane-left", minSizePercentage=15, defaultSizePercentage=default_size, children=[ @@ -22,6 +23,7 @@ def SplitPane(children1, children2, default_size): ), PanelResizeHandle( id='resize-handle', + className="split-pane-handle", style={ "flex": "0 0 20px", "margin": "0 -10px", @@ -34,12 +36,13 @@ def SplitPane(children1, children2, default_size): ), Panel( id='plot-panel', + className="split-pane-right", minSizePercentage=15, children=children2, ) ], direction='horizontal', - className='w-100 px-0 pb-2', + className='split-pane-group w-100 px-0 pb-2', style={'minHeight': '0'} ) ], diff --git a/pages/conditional.py b/pages/conditional.py index a96435b..5808744 100644 --- a/pages/conditional.py +++ b/pages/conditional.py @@ -56,7 +56,7 @@ def gauss2(x, y, μ, C): fig.update_yaxes(range=rangy, tickmode='array', tickvals=list(range(rangy[0], rangy[1]+1)), scaleanchor="x", scaleratio=1) fig.update_layout(transition_duration=100, transition_easing='linear') fig.update_scenes(camera_projection_type="orthographic") -fig.update_scenes(aspectmode="cube") +fig.update_scenes(aspectmode="auto") # fig.update_scenes(xaxis_nticks=1) # fig.update_scenes(yaxis_nticks=1) fig.update_scenes(zaxis_nticks=1) @@ -64,12 +64,12 @@ def gauss2(x, y, μ, C): fig.update_layout( legend=dict( - orientation="v", - yanchor="top", - y=0.99, - xanchor="right", - x=0.1, - ) + yanchor="top", + y=0.98, + xanchor="left", + x=0.02, + bgcolor="rgba(255,255,255,0.7)" + ) ) config = { diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 672bc38..936a723 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -74,11 +74,13 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): uirevision=f"object-3d-{self.id}", ) + # Keep legend overlayed in the corner so it does not steal width from narrow layouts self.fig.update_layout(legend=dict( yanchor="top", - y=0.99, - xanchor="right", - x=0.1, + y=0.98, + xanchor="left", + x=0.02, + bgcolor="rgba(255,255,255,0.7)" )) if (self.object.camera_settings_3d is not None): From 1ea16a64b3749c2a37cf5b8723a9d1c82e156f58 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 12:58:32 +0100 Subject: [PATCH 113/152] Fib-Kronecker: start counting at 1 on golden ratio axis --- .../cylinder/partially_wraped_normal/fibonacci_kronecker.py | 4 +++- .../distributions/torus/wrapped_normal/fibonacci_kronecker.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/model/distributions/cylinder/partially_wraped_normal/fibonacci_kronecker.py b/model/distributions/cylinder/partially_wraped_normal/fibonacci_kronecker.py index a56281f..268c5be 100644 --- a/model/distributions/cylinder/partially_wraped_normal/fibonacci_kronecker.py +++ b/model/distributions/cylinder/partially_wraped_normal/fibonacci_kronecker.py @@ -20,13 +20,15 @@ def sample(self, sample_options, distribution_options): sample_count = sample_options[0].state indices = np.arange(0, sample_count) + indices_p1 = np.arange(0, sample_count + 1) gol = (1+5**0.5)/2 # centered rank-1 lattice generator equidistant_generator = (2 * indices + 1) / (2 * sample_count) t = equidistant_generator - p = (indices / gol) % 1 + p = (indices_p1 / gol) % 1 + p = p[1:] fib_grid = np.column_stack((t , p)) diff --git a/model/distributions/torus/wrapped_normal/fibonacci_kronecker.py b/model/distributions/torus/wrapped_normal/fibonacci_kronecker.py index 47dab0d..28bdc16 100644 --- a/model/distributions/torus/wrapped_normal/fibonacci_kronecker.py +++ b/model/distributions/torus/wrapped_normal/fibonacci_kronecker.py @@ -20,13 +20,15 @@ def sample(self, sample_options, distribution_options): sample_count = sample_options[0].state indices = np.arange(0, sample_count) + indices_p1 = np.arange(0, sample_count + 1) gol = (1+5**0.5)/2 # centered rank-1 lattice generator equidistant_generator = (2 * indices + 1) / (2 * sample_count) t = equidistant_generator - p = (indices / gol) % 1 + p = (indices_p1 / gol) % 1 + p = p[1:] fib_grid = np.column_stack((t , p)) From aa143cdaa57937c5debbaf2829861c9a3d8ac550 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 13:28:27 +0100 Subject: [PATCH 114/152] Fibonacci-Rank-1: remove first index, sample at 0 --- assets/tooltip.js | 55 +++++++++++-------- .../fibonacci_rank_1.py | 6 +- .../cylinder/uniform/fibonacci_rank_1.py | 6 +- .../torus/wrapped_normal/fibonacci.py | 6 +- util/selectors/slider_fib.py | 21 +++++-- 5 files changed, 59 insertions(+), 35 deletions(-) diff --git a/assets/tooltip.js b/assets/tooltip.js index 29a8863..241acaa 100644 --- a/assets/tooltip.js +++ b/assets/tooltip.js @@ -1,35 +1,44 @@ window.dccFunctions = window.dccFunctions || {}; window.dccFunctions.trafo_L = function(value) { - if (value < Math.log10(1.25)) { - return 0; - } else { - return Math.round(Math.pow(10, value)); - } + if (value < Math.log10(1.25)) { + return 0; + } else { + return Math.round(Math.pow(10, value)); + } } window.dccFunctions.transform_log_nice = function(value) { - let x = Math.pow(10, value); - // same as transform_up in log_slider.py - if (x == 0){ - return 0; - } - const sign = Math.sign(x); - x = Math.abs(x); + let x = Math.pow(10, value); + // same as transform_up in log_slider.py + if (x == 0){ + return 0; + } + const sign = Math.sign(x); + x = Math.abs(x); - let step = Math.pow(10, Math.floor(Math.log10(x))) / 10; - let nice_value = sign * Math.round(x / step) * step; - nice_value = Number(nice_value.toFixed(4)); - return nice_value; + let step = Math.pow(10, Math.floor(Math.log10(x))) / 10; + let nice_value = sign * Math.round(x / step) * step; + nice_value = Number(nice_value.toFixed(4)); + return nice_value; } window.dccFunctions.transform_fib = function(value) { - function fibonacci(n) { - return n < 1 ? 0 - : n <= 2 ? 1 - : fibonacci(n - 1) + fibonacci(n - 2) - } - return fibonacci(value); + function fibonacci(n) { + return n < 1 ? 0 + : n <= 2 ? 1 + : fibonacci(n - 1) + fibonacci(n - 2) + } + return fibonacci(value); +} + +window.dccFunctions.transform_fib_m1 = function(value) { + function fibonacci(n) { + return n < 1 ? 0 + : n <= 2 ? 1 + : fibonacci(n - 1) + fibonacci(n - 2) + } + return fibonacci(value) - 1; } window.dccFunctions.transform_square = function(value) { - return value * value; + return value * value; } \ No newline at end of file diff --git a/model/distributions/cylinder/partially_wraped_normal/fibonacci_rank_1.py b/model/distributions/cylinder/partially_wraped_normal/fibonacci_rank_1.py index fed9a05..e69a026 100644 --- a/model/distributions/cylinder/partially_wraped_normal/fibonacci_rank_1.py +++ b/model/distributions/cylinder/partially_wraped_normal/fibonacci_rank_1.py @@ -11,7 +11,7 @@ class CylinderFibRank1PWNSampling(CylinderSamplingSchema): def __init__(self): self.sample_options = [ - MI(SliderFib("Number of Samples", 2, 34, 21, 9)) + MI(SliderFib("Number of Samples", 3, 33, 21, 9, minus_1=True)) ] self.sampler = CylinderFibRank1UniformSampling() @@ -20,9 +20,9 @@ def get_name(self): def sample(self, sample_options, distribution_options): # see https://isas.iar.kit.edu/pdf/Fusion21_Frisch.pdf - sample_count = sample_options[0].state + sample_count = sample_options[0].state + 1 # because minus_1 is true slider displays fib(n)-1 - t, p = self.sampler.get_rank_1(sample_count, sample_options[0].idx) + t, p = self.sampler.get_rank_1(sample_count, sample_options[0].idx, without_first_point=True) fib_grid = np.column_stack((t , p)) diff --git a/model/distributions/cylinder/uniform/fibonacci_rank_1.py b/model/distributions/cylinder/uniform/fibonacci_rank_1.py index 007c0c2..2669e67 100644 --- a/model/distributions/cylinder/uniform/fibonacci_rank_1.py +++ b/model/distributions/cylinder/uniform/fibonacci_rank_1.py @@ -23,7 +23,7 @@ def sample(self, sample_options, distribution_options): return np.column_stack((p * 2 * np.pi, z * 2 * np.pi)) @staticmethod - def get_rank_1(sample_count, k): + def get_rank_1(sample_count, k, without_first_point=False): indices = np.arange(0, sample_count) # centered rank-1 lattice @@ -33,4 +33,8 @@ def get_rank_1(sample_count, k): z = (indices * (1/F_k_p_1) + (1/(2*F_k_p_1)) ) % 1 p = (indices * (F_k/F_k_p_1) + (1/(2*F_k_p_1)) ) % 1 + if without_first_point: + z = z[1:] + p = p[1:] + return z, p \ No newline at end of file diff --git a/model/distributions/torus/wrapped_normal/fibonacci.py b/model/distributions/torus/wrapped_normal/fibonacci.py index 78b1f0c..368a126 100644 --- a/model/distributions/torus/wrapped_normal/fibonacci.py +++ b/model/distributions/torus/wrapped_normal/fibonacci.py @@ -11,7 +11,7 @@ class TorusFibRank1WNSampling(TorusSamplingSchema): def __init__(self): self.sample_options = [ - MI(SliderFib("Number of Samples", 2, 34, 21, 9)) + MI(SliderFib("Number of Samples", 3, 33, 21, 9, minus_1=True)) ] self.sampler = CylinderFibRank1UniformSampling() @@ -20,9 +20,9 @@ def get_name(self): def sample(self, sample_options, distribution_options): # see https://isas.iar.kit.edu/pdf/Fusion21_Frisch.pdf - sample_count = sample_options[0].state + sample_count = sample_options[0].state + 1 # because minus_1 is true slider displays fib(n)-1 - t, p = self.sampler.get_rank_1(sample_count, sample_options[0].idx) + t, p = self.sampler.get_rank_1(sample_count, sample_options[0].idx, without_first_point=True) fib_grid = np.column_stack((t , p)) diff --git a/util/selectors/slider_fib.py b/util/selectors/slider_fib.py index 4815667..2e7e326 100644 --- a/util/selectors/slider_fib.py +++ b/util/selectors/slider_fib.py @@ -9,9 +9,10 @@ Silder that only selects Fibonacci numbers within a given range. min and max are the indices in the Fibonacci sequence. state is not an index, but the actual Fibonacci number at a valid index. +if minus_1 is set, Fibonacci numbers -1 are used instead. (initial) state is should then also be Fibonacci number -1. """ class SliderFib(Selector): - def __init__(self, name, min, state, max, idx): + def __init__(self, name, min, state, max, idx, minus_1=False): self.name = name self.min = min self.state = state @@ -19,6 +20,7 @@ def __init__(self, name, min, state, max, idx): self.max = max self.id = None + self.minus_1 = minus_1 def to_dash_component(self, _type, id, renderer_id, manual=False): self.id = id @@ -32,7 +34,7 @@ def to_dash_component(self, _type, id, renderer_id, manual=False): min=self.min, max=self.max, value=self.idx, - tooltip={"placement": "bottom", "always_visible": True, "transform": "transform_fib"}, + tooltip={"placement": "bottom", "always_visible": True, "transform": "transform_fib" if not self.minus_1 else "transform_fib_m1"}, step=1, marks=self.calculate_marks(), updatemode="drag", @@ -45,24 +47,33 @@ def calculate_marks(self): step = (self.max - self.min) / SLIDER_MARK_AMOUNT for i in range(SLIDER_MARK_AMOUNT + 1): value = int(self.min + i * step) - marks[value] = str(sp.fibonacci(value)) + if not self.minus_1: + marks[value] = str(sp.fibonacci(value)) + else: + marks[value] = str(sp.fibonacci(value) - 1) return marks def update_state(self, new_state): - self.state = int(sp.fibonacci(new_state)) + self.state = int(sp.fibonacci(new_state) - (1 if self.minus_1 else 0)) self.idx = int(new_state) def transfrom_up(self, x): - return int(sp.fibonacci(x)) + return int(sp.fibonacci(x) - (1 if self.minus_1 else 0)) def transfrom_down(self, x): + if self.minus_1: + x = x + 1 + for i in range(0, x + 3): if int(sp.fibonacci(i)) == x: return i raise ValueError(f"{x} is not a Fibonacci number") def is_valid(self, n): + if self.minus_1: + n = n + 1 + if n < 0: return False # A number is a Fibonacci number if and only if one or both of (5*n^2 + 4) or (5*n^2 - 4) is a perfect square From 3e8fa4555ca3a87a5d003d8524ebbabf22978a01 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 13:37:52 +0100 Subject: [PATCH 115/152] export as jpeg --- pages/conditional.py | 2 +- renderer/Object3DRenderer.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/conditional.py b/pages/conditional.py index 5808744..c6f2eb9 100644 --- a/pages/conditional.py +++ b/pages/conditional.py @@ -74,7 +74,7 @@ def gauss2(x, y, μ, C): config = { 'toImageButtonOptions': { - 'format': 'png', # png, svg, pdf, jpeg, webp + 'format': 'jpeg', # png, svg, pdf, jpeg, webp 'width': None, # None: use currently-rendered size 'height': None, 'filename': 'conditional', diff --git a/renderer/Object3DRenderer.py b/renderer/Object3DRenderer.py index 936a723..29125a7 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/Object3DRenderer.py @@ -23,7 +23,7 @@ def __init__(self, object_3D, id, register_3d_callbacks=True): 'scrollZoom': True, "modeBarButtonsToRemove": ["select2d", "lasso2d"], "toImageButtonOptions": { - "format": "png", + "format": "jpeg", "scale": 8, } } From f4ba904598f2c1127658764a0e1efeb0a2548a1c Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 13:41:14 +0100 Subject: [PATCH 116/152] added crash resistance for dynamic module loading --- model/distributions/distribution_loader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/model/distributions/distribution_loader.py b/model/distributions/distribution_loader.py index 355eb05..2e86883 100644 --- a/model/distributions/distribution_loader.py +++ b/model/distributions/distribution_loader.py @@ -25,7 +25,11 @@ def load_distributions(self): for finder, name, ispkg in pkgutil.walk_packages(pkg.__path__, prefix=pkg.__name__ + "."): if "benchmark" in name: continue - module = importlib.import_module(name) + try: + module = importlib.import_module(name) + except Exception as e: + print(f"Could not import distribution module '{name}': {e}") + continue for _, obj in inspect.getmembers(module, inspect.isclass): # skip abstract, parametered intervace and non-subclasses From 841f852e88a71f6c7a9045aab6f3d446f9a905c9 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 14:01:53 +0100 Subject: [PATCH 117/152] added logarithmix plots in benchmark results --- .../sphere/watson/benchmark_fib_starts.py | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index 29abbd2..cdc8f9a 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -9,7 +9,7 @@ from util.selectors.slider_float import FloatSlider import pyperf import statistics - +import numpy as np sampler = WatsonFibonachiSampling() methods = { @@ -56,16 +56,36 @@ def bench_multiple_sample_counts(kappa): all_results[name].append((sample_count, bench)) return all_results +def bench_multiple_sample_counts_log(kappa): + all_results = {} + sample_counts = np.unique(np.logspace(2, 5, num=90, dtype=int)) + for sample_count in sample_counts: + res = bench_single_kappa(kappa, sample_count, f"Multiple Sample Counts Log (kappa={kappa}, sample_count={sample_count})") + for name, bench in res.items(): + if name not in all_results: + all_results[name] = [] + all_results[name].append((sample_count, bench)) + return all_results + -def plot_benches(results, title, x_label): +def plot_benches(results, title, filename, x_label, log_x=False, log_y=False): import plotly.express as px try: if x_label == "sample_count": rows = [dict(name=n, sample_count=k, time=t.mean()) for n, pts in results.items() for k, t in pts] else: rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] - fig = px.line(rows, x=x_label, y="time", color="name", markers=True, title=title) - fig.write_image(f"{title.replace(' ', '_').replace(':', '')}.svg") + fig = px.line( + rows, + x=x_label, + y="time", + color="name", + markers=True, + title=title, + log_x=log_x, + log_y=log_y, + ) + fig.write_image(f"{filename.replace(' ', '_').replace(':', '')}.svg") except Exception as e: print("Generating plot failed, dumping data:", e) print(results.items()) @@ -76,13 +96,22 @@ def plot_benches(results, title, x_label): if __name__ == "__main__": runner = pyperf.Runner() + + mult_kappa = bench_multiple_kappa() - mult_samples_neg_10 = bench_multiple_sample_counts(-10) - mult_samples_10 = bench_multiple_sample_counts(10) + #mult_samples_neg_10 = bench_multiple_sample_counts(-10) + #mult_samples_10 = bench_multiple_sample_counts(10) + log_mult_samples_10 = bench_multiple_sample_counts_log(10) + log_mult_samples_neg_10 = bench_multiple_sample_counts_log(-10) + if not runner.args.worker: - plot_benches(mult_kappa, "time taken for various kappa values (10000 samples)", "kappa") - plot_benches(mult_samples_neg_10, "time taken for various sample counts (kappa=-10)", "sample_count") - plot_benches(mult_samples_10, "time taken for various sample counts (kappa=10)", "sample_count") + plot_benches(mult_kappa, "time taken for various kappa values (10000 samples)", "time taken for various kappa values (10000 samples)", "kappa") + plot_benches(mult_kappa, "time taken for various kappa values log scale (10000 samples)", "time taken for various kappa values log scale (10000 samples)", "kappa", log_y=True) - \ No newline at end of file + #plot_benches(mult_samples_10, "time taken for various sample counts (kappa=10)", "time taken for various sample counts (kappa=10)", "sample_count") + plot_benches(log_mult_samples_10, "time taken for various sample counts (kappa=10)", "time taken for various sample counts log scale (kappa=10)", "sample_count", log_x=True, log_y=True) + + #plot_benches(mult_samples_neg_10, "time taken for various sample counts (kappa=-10)", "time taken for various sample counts (kappa=-10)", "sample_count") + plot_benches(log_mult_samples_neg_10, "time taken for various sample counts (kappa=-10)", "time taken for various sample counts log scale (kappa=-10)", "sample_count", log_x=True, log_y=True) + From 4665e76d84f4273a18c2fb48dce91c4f9c0617e8 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 14:39:18 +0100 Subject: [PATCH 118/152] rename renderer and add abstract renderer Class --- model/cylinder/cylinder.py | 2 +- model/torus/torus.py | 2 +- pages/cylinder.py | 2 +- pages/sphere.py | 2 +- pages/torus.py | 2 +- ...3DAnd2DRenderer.py => object_3D_and_2D_renderer.py} | 2 +- .../{Object3DRenderer.py => object_3D_renderer.py} | 4 +++- renderer/{PlotSettings2d.py => plot_settings_2d.py} | 0 renderer/renderer.py | 10 ++++++++++ 9 files changed, 19 insertions(+), 7 deletions(-) rename renderer/{Object3DAnd2DRenderer.py => object_3D_and_2D_renderer.py} (99%) rename renderer/{Object3DRenderer.py => object_3D_renderer.py} (99%) rename renderer/{PlotSettings2d.py => plot_settings_2d.py} (100%) create mode 100644 renderer/renderer.py diff --git a/model/cylinder/cylinder.py b/model/cylinder/cylinder.py index ce4d6d0..237f593 100644 --- a/model/cylinder/cylinder.py +++ b/model/cylinder/cylinder.py @@ -7,7 +7,7 @@ from model.distributions.distribution_loader import DistributionLoader from model.distributions.cylinder.cylinder_distribution import CylinderDistribution from model.manifold import Manifold -from renderer.PlotSettings2d import PlotSettings2D +from renderer.plot_settings_2d import PlotSettings2D from model.distributions.cylinder.uniform.fibonacci_kronecker import CylinderFibUniformSampling from util.selectors.slider import Slider diff --git a/model/torus/torus.py b/model/torus/torus.py index 0f47058..c4376b0 100644 --- a/model/torus/torus.py +++ b/model/torus/torus.py @@ -5,7 +5,7 @@ from model.distributions.distribution_loader import DistributionLoader from model.distributions.torus.torus_distribution import TorusDistribution from model.manifold import Manifold -from renderer.PlotSettings2d import PlotSettings2D +from renderer.plot_settings_2d import PlotSettings2D from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling from util.selectors.slider_fib import SliderFib diff --git a/pages/cylinder.py b/pages/cylinder.py index dde3d1d..a3365a5 100644 --- a/pages/cylinder.py +++ b/pages/cylinder.py @@ -8,7 +8,7 @@ from components.split_pane import SplitPane from model.cylinder.cylinder import Cylinder -from renderer.Object3DAnd2DRenderer import Object3DAnd2DRenderer +from renderer.object_3D_and_2D_renderer import Object3DAnd2DRenderer dash.register_page(__name__) diff --git a/pages/sphere.py b/pages/sphere.py index c948bc4..ee02bf7 100644 --- a/pages/sphere.py +++ b/pages/sphere.py @@ -8,7 +8,7 @@ from components.split_pane import SplitPane from model.sphere.sphere import Sphere -from renderer.Object3DRenderer import Object3DRenderer +from renderer.object_3D_renderer import Object3DRenderer dash.register_page(__name__) diff --git a/pages/torus.py b/pages/torus.py index b6515ba..2d91257 100644 --- a/pages/torus.py +++ b/pages/torus.py @@ -8,7 +8,7 @@ from components.split_pane import SplitPane from model.torus.torus import Torus -from renderer.Object3DAnd2DRenderer import Object3DAnd2DRenderer +from renderer.object_3D_and_2D_renderer import Object3DAnd2DRenderer dash.register_page(__name__) diff --git a/renderer/Object3DAnd2DRenderer.py b/renderer/object_3D_and_2D_renderer.py similarity index 99% rename from renderer/Object3DAnd2DRenderer.py rename to renderer/object_3D_and_2D_renderer.py index 73efe59..1a32210 100644 --- a/renderer/Object3DAnd2DRenderer.py +++ b/renderer/object_3D_and_2D_renderer.py @@ -2,7 +2,7 @@ import numpy as np import plotly.graph_objects as go -from renderer.Object3DRenderer import Object3DRenderer +from renderer.object_3D_renderer import Object3DRenderer class Object3DAnd2DRenderer(Object3DRenderer): def __init__(self, object, id): diff --git a/renderer/Object3DRenderer.py b/renderer/object_3D_renderer.py similarity index 99% rename from renderer/Object3DRenderer.py rename to renderer/object_3D_renderer.py index 29125a7..a3b11aa 100644 --- a/renderer/Object3DRenderer.py +++ b/renderer/object_3D_renderer.py @@ -4,7 +4,9 @@ import plotly.graph_objects as go import plotly.figure_factory as ff import dash -class Object3DRenderer: + +from renderer.renderer import Renderer +class Object3DRenderer(Renderer): def __init__(self, object_3D, id, register_3d_callbacks=True): # dash doesnt like duplicate calback functions # so each renderer instance gets a uuid for suffixing diff --git a/renderer/PlotSettings2d.py b/renderer/plot_settings_2d.py similarity index 100% rename from renderer/PlotSettings2d.py rename to renderer/plot_settings_2d.py diff --git a/renderer/renderer.py b/renderer/renderer.py new file mode 100644 index 0000000..c17af90 --- /dev/null +++ b/renderer/renderer.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod + +class Renderer(ABC): + + """ + returns a tuple of the a list of settings components (eg. for the sidebar) and a list of plot components + """ + @abstractmethod + def get_layout_components(self): + pass \ No newline at end of file From b8486b6ee044f6882ddd930904cc9fa3c1c0b202 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 15:13:56 +0100 Subject: [PATCH 119/152] refractor gaus1d --- model/distributions/gaus1d/__init__.py | 0 model/distributions/gaus1d/gaus1d.py | 180 ++++++++++++++ model/distributions/gaus1d/info_text.py | 51 ++++ model/selfcontained_distribution.py | 12 + pages/gauss1D.py | 227 +----------------- .../selfcontained_distribution_renderer.py | 11 + 6 files changed, 263 insertions(+), 218 deletions(-) create mode 100644 model/distributions/gaus1d/__init__.py create mode 100644 model/distributions/gaus1d/gaus1d.py create mode 100644 model/distributions/gaus1d/info_text.py create mode 100644 model/selfcontained_distribution.py create mode 100644 renderer/selfcontained_distribution_renderer.py diff --git a/model/distributions/gaus1d/__init__.py b/model/distributions/gaus1d/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/gaus1d/gaus1d.py b/model/distributions/gaus1d/gaus1d.py new file mode 100644 index 0000000..9ea2931 --- /dev/null +++ b/model/distributions/gaus1d/gaus1d.py @@ -0,0 +1,180 @@ +import plotly +import dash +from dash import dcc, html, Input, Output, callback, Patch +import plotly.graph_objects as go +import dash_bootstrap_components as dbc +from numpy import sqrt, linspace, vstack, pi, nan, full, exp, square, sort, arange, array +from numpy.random import randn, randint +from scipy.special import erfinv + +from components.popup_box import PopupBox + +from model.distributions.gaus1d.info_text import info_text +from model.selfcontained_distribution import SelfContainedDistribution + +class Gaus1D(SelfContainedDistribution): + def __init__(self): + self.methods = ['iid', 'Golden-Sequence', 'Equidistant', 'Unscented'] + + self.config = { + 'toImageButtonOptions': { + 'format': 'svg', # png, svg, pdf, jpeg, webp + 'height': None, # None: use currently-rendered size + 'width': None, + 'filename': 'gauss2d', + }, + 'modeBarButtonsToRemove': ['zoom'], + 'scrollZoom': True, + } + + self.rang = [-5, 5] + self.col_density = plotly.colors.qualitative.Plotly[1] + self.col_samples = plotly.colors.qualitative.Plotly[0] + + + self.settings_layout =[ + dbc.Container( + dbc.Col([ + html.P("Select Sampling Method:"), + html.Br(), + + # Sampling Strategy RadioItems + dbc.RadioItems(id='gauss1D-smethod', + options=[{"label": x, "value": x} for x in self.methods], + value=self.methods[randint(len(self.methods))], + inline=True), + + html.Br(), + html.Hr(), + html.Br(), + + # param Slider + dcc.Slider(id="gauss1D-p", min=0, max=1, value=randint(3, 7)/10, + tooltip={"template": "p={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), + + # L Slider + dcc.Slider(id="gauss1D-L", min=0, max=100, step=1, value=randint(5, 25), + tooltip={"template": "L={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), + + # μ Slider + dcc.Slider(id="gauss1D-μ", min=-5, max=5, step=0.01, value=randint(-20, 20)/10, + tooltip={"template": "µ={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), + + # σ Slider + dcc.Slider(id="gauss1D-σ", min=0, max=5, step=0.01, value=randint(5, 20)/10, + tooltip={"template": 'σ={value}', "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), + + html.Hr(), + html.Br(), + + # Info Popup + *PopupBox("gauss1D-info", "Learn More", "Additional Information", info_text), + + + ]) + , + fluid=True, + className="g-0" + ) + ] + self.plot_layout = [ + dcc.Graph(id="gauss1D-graph", config=self.config, style={'height': '100%'}), + ] + + self._register_callbacks() + + def _register_callbacks(self): + @callback( + Output('gauss1D-p', 'min'), + Output('gauss1D-p', 'max'), + Output('gauss1D-p', 'value'), + Output('gauss1D-p', 'step'), + Output('gauss1D-p', 'tooltip'), + Output('gauss1D-L', 'disabled'), + Input("gauss1D-smethod", "value"), + ) + def update_smethod(smethod): + patched_tooltip = Patch() + match smethod: + case 'iid': + patched_tooltip.template = "dice" + return 0, 1, .5, 0.001, patched_tooltip, False + case 'Golden-Sequence': + patched_tooltip.template = "z={value}" + return -50, 50, 0, 1, patched_tooltip, False + case 'Equidistant': + patched_tooltip.template = "γ={value}" + return -1, 1, 0, 0.001, patched_tooltip, False + case 'Unscented': + patched_tooltip.template = "{value}" + return 0, 2, 1, 0.001, patched_tooltip, True + case _: + raise Exception("Wrong smethod") + + + @callback( + Output("gauss1D-graph", "figure"), + Input("gauss1D-smethod", "value"), + Input("gauss1D-p", "value"), + Input("gauss1D-L", "value"), + Input("gauss1D-μ", "value"), + Input("gauss1D-σ", "value"), + ) + def update(smethod, p, L, μ, σ): + fig = go.Figure() # TODO use Patch here too + + if σ == 0: + # Dirac Delta + fig.add_trace(go.Scatter(x=[self.rang[0], μ, μ, μ, self.rang[1]], y=[0, 0, 1, 0, 0], hoverinfo='skip', line={'width': 5}, name='Dirac Delta', marker_color=self.col_density, showlegend=True)) + if L > 0: + fig.add_trace(go.Scatter(x=[μ, μ], y=[0, 1], name='Samples', mode='lines', marker_color=self.col_samples, showlegend=True)) + else: + # Draw Samples + xGauss = None + match smethod: + case 'iid': + # xUni = sort(rand(L)) + xGauss = sort(randn(L)*σ + μ) + case 'Golden-Sequence': + xUni = (sqrt(5)-1)/2 * (arange(L)+1+round(p)) % 1 + case 'Equidistant': + xUni = (2*arange(L)+1+p)/(2*L) + case 'Unscented': + # TODO scaled unscented etc + xGauss = array([μ-σ, μ+σ]) # TODO parameter + case _: + raise Exception("Wrong smethod") + # Transform Samples + if xGauss is None: + xGauss = σ*sqrt(2)*erfinv(2*xUni-1) + μ + L2 = len(xGauss) + sample_height = full([1, L2], self.gauss1(0, 0, σ)) + # sample_height = full([1, L], 1/L) + # Plot Density + s = linspace(self.rang[0], self.rang[1], 500) + # https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html + # TODO lighter fillcolor: fillcolor=matplotlib.colors.to_rgba('#aabbcc80') + fig.add_trace(go.Scatter(x=s, y=self.gauss1(s, μ, σ), hoverinfo='skip', line={'width': 5}, line_shape='spline', name='Density', fill='tozeroy', marker_color=self.col_density, showlegend=True)) + # Plot Samples + xp = vstack((xGauss, xGauss, full([1, L2], nan))).T.flatten() + yp = vstack((full([1, L2], 0), sample_height, full([1, L2], nan))).T.flatten() + fig.add_trace(go.Scatter(x=xp, y=yp, name='Samples', mode='lines', marker_color=self.col_samples, showlegend=True)) + # Style + fig.update_xaxes(range=self.rang, tickmode='array', tickvals=list(range(-5, 6))) + fig.update_yaxes(range=[0, None], fixedrange=True) + fig.update_layout(legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)) + fig.update_layout(dragmode="pan") + + fig.update_layout( + legend=dict( + orientation="v", + xanchor="right", + x=0.1, + ) + ) + return fig + + @staticmethod + def gauss1(x, μ, σ): + return 1/sqrt(2*pi*σ) * exp(-1/2 * square((x-μ)/σ)) + diff --git a/model/distributions/gaus1d/info_text.py b/model/distributions/gaus1d/info_text.py new file mode 100644 index 0000000..b0ebab9 --- /dev/null +++ b/model/distributions/gaus1d/info_text.py @@ -0,0 +1,51 @@ +from dash import dcc + +# Description +info_text = dcc.Markdown( + r''' + ## 1D Gaussian + Interactive visualization of the univariate Gaussian density + + $$ + f(x) = \frac{1}{\sqrt{2\pi}\sigma} + \cdot \exp\!\left\{ -\frac{1}{2} \cdot \left(\frac{x-\mu}{\sigma}\right)^2 \right\}\enspace, \quad x\in \mathbb{R} \enspace, + $$ + + with mean $\mu \in \mathbb{R}$ and standard deviation $\sigma \in \mathbb{R}_+$ . + + It was discovered by Karl Friedrich Gauß (1777-1855) in Göttingen, Germany. + + ### Formulas + - quantile function + $Q(p) = \mu + \sigma\, \sqrt{2}\, \mathrm{erf}^{-1}(2p-1)$ + - uniform to Gaussian + $x_i^{\text{Gauss}} = Q(x_i^{\text{uni}})$ + - golden Kronecker sequence + $x_i^{\text{uni}}=\mod( \Phi \cdot (i+z), 1) \enspace, \quad i \in \{1,2,\ldots,L\}\enspace, \quad z \in \mathbb{Z}$ + - equidistant samples + $x_i^{\text{uni}} = \frac{2 i - 1 + \gamma}{2 L} \enspace, \quad i \in \{1,2,\ldots,L\}\enspace,\quad \gamma\in[-1,1]$ + - unscented (𝐿=2) + $x_1=\mu-\sigma\enspace, \quad x_2=\mu+\sigma$ + - unscented (𝐿=3) + TODO + + ### Interactivity + - GUI + - add/remove lines: click in legend + - sampling methods (radiobutton) + - independent identically distributed (iid), the usual random samples + - golden sequence, a low-discrepancy Kronecker sequence based on the golden ratio + - equidistant, with identical amount of probability mass for all samples + - unscented transform sampling (𝐿=2) + - sampling parameter (slider) + - iid: dice again + - golden: integer offset 𝑧 + - equidistant: offset 𝛾 + - unscented: TODO + - number of Samples 𝐿 (slider) + - density parameters (slider) + - mean 𝜇 + - standard deviation 𝜎 + ''', + mathjax=True +) \ No newline at end of file diff --git a/model/selfcontained_distribution.py b/model/selfcontained_distribution.py new file mode 100644 index 0000000..60a5dd2 --- /dev/null +++ b/model/selfcontained_distribution.py @@ -0,0 +1,12 @@ + +""" +This is a class of distributions that do not utelize the dynamic distribution loading system, +and provide all their callbacks and data internally. + +Meant to be used for simple distributions, where using the distribution loading system would be overkill. +To be used with the selfcontained_distribution_renderer. +""" +class SelfContainedDistribution: + def __init__(self): + self.settings_layout = [] # to be set by subclass + self.plot_layout = [] # to be set by subclass \ No newline at end of file diff --git a/pages/gauss1D.py b/pages/gauss1D.py index bb3f333..dcdc742 100644 --- a/pages/gauss1D.py +++ b/pages/gauss1D.py @@ -1,229 +1,20 @@ -import plotly import dash -from dash import dcc, html, Input, Output, callback, Patch -import plotly.graph_objects as go -import dash_bootstrap_components as dbc -from numpy import sqrt, linspace, vstack, pi, nan, full, exp, square, sort, arange, array -from numpy.random import randn, randint -from scipy.special import erfinv -from components.popup_box import PopupBox from components.split_pane import SplitPane +from model.distributions.gaus1d.gaus1d import Gaus1D +from renderer.selfcontained_distribution_renderer import SelfContainedDistributionRenderer as Renderer dash.register_page(__name__) -methods = ['iid', 'Golden-Sequence', 'Equidistant', 'Unscented'] +renderer = Renderer(Gaus1D()) +options, graph = renderer.get_layout_components() -config = { - 'toImageButtonOptions': { - 'format': 'svg', # png, svg, pdf, jpeg, webp - 'height': None, # None: use currently-rendered size - 'width': None, - 'filename': 'gauss2d', - }, - 'modeBarButtonsToRemove': ['zoom'], - 'scrollZoom': True, -} - -# Description -info_text = dcc.Markdown( - r''' - ## 1D Gaussian - Interactive visualization of the univariate Gaussian density - - $$ - f(x) = \frac{1}{\sqrt{2\pi}\sigma} - \cdot \exp\!\left\{ -\frac{1}{2} \cdot \left(\frac{x-\mu}{\sigma}\right)^2 \right\}\enspace, \quad x\in \mathbb{R} \enspace, - $$ - - with mean $\mu \in \mathbb{R}$ and standard deviation $\sigma \in \mathbb{R}_+$ . - - It was discovered by Karl Friedrich Gauß (1777-1855) in Göttingen, Germany. - - ### Formulas - - quantile function - $Q(p) = \mu + \sigma\, \sqrt{2}\, \mathrm{erf}^{-1}(2p-1)$ - - uniform to Gaussian - $x_i^{\text{Gauss}} = Q(x_i^{\text{uni}})$ - - golden Kronecker sequence - $x_i^{\text{uni}}=\mod( \Phi \cdot (i+z), 1) \enspace, \quad i \in \{1,2,\ldots,L\}\enspace, \quad z \in \mathbb{Z}$ - - equidistant samples - $x_i^{\text{uni}} = \frac{2 i - 1 + \gamma}{2 L} \enspace, \quad i \in \{1,2,\ldots,L\}\enspace,\quad \gamma\in[-1,1]$ - - unscented (𝐿=2) - $x_1=\mu-\sigma\enspace, \quad x_2=\mu+\sigma$ - - unscented (𝐿=3) - TODO - - ### Interactivity - - GUI - - add/remove lines: click in legend - - sampling methods (radiobutton) - - independent identically distributed (iid), the usual random samples - - golden sequence, a low-discrepancy Kronecker sequence based on the golden ratio - - equidistant, with identical amount of probability mass for all samples - - unscented transform sampling (𝐿=2) - - sampling parameter (slider) - - iid: dice again - - golden: integer offset 𝑧 - - equidistant: offset 𝛾 - - unscented: TODO - - number of Samples 𝐿 (slider) - - density parameters (slider) - - mean 𝜇 - - standard deviation 𝜎 - ''', - mathjax=True -) - - -layout = SplitPane([ - dbc.Container( - dbc.Col([ - html.P("Select Sampling Method:"), - html.Br(), - - # Sampling Strategy RadioItems - dbc.RadioItems(id='gauss1D-smethod', - options=[{"label": x, "value": x} for x in methods], - value=methods[randint(len(methods))], - inline=True), - - html.Br(), - html.Hr(), - html.Br(), - - # param Slider - dcc.Slider(id="gauss1D-p", min=0, max=1, value=randint(3, 7)/10, - tooltip={"template": "p={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), - - # L Slider - dcc.Slider(id="gauss1D-L", min=0, max=100, step=1, value=randint(5, 25), - tooltip={"template": "L={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), - - # μ Slider - dcc.Slider(id="gauss1D-μ", min=-5, max=5, step=0.01, value=randint(-20, 20)/10, - tooltip={"template": "µ={value}", "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), - - # σ Slider - dcc.Slider(id="gauss1D-σ", min=0, max=5, step=0.01, value=randint(5, 20)/10, - tooltip={"template": 'σ={value}', "placement": "bottom", "always_visible": True}, updatemode='drag', marks=None), - - html.Hr(), - html.Br(), - - # Info Popup - *PopupBox("gauss1D-info", "Learn More", "Additional Information", info_text), - - - ]) - , - fluid=True, - className="g-0" - ) +layout = SplitPane( + [ + *options ], [ - # Plot - dcc.Graph(id="gauss1D-graph", config=config, style={'height': '100%'}), + *graph ], 30 -) - - -col_density = plotly.colors.qualitative.Plotly[1] -col_samples = plotly.colors.qualitative.Plotly[0] -rang = [-5, 5] - - -@callback( - Output('gauss1D-p', 'min'), - Output('gauss1D-p', 'max'), - Output('gauss1D-p', 'value'), - Output('gauss1D-p', 'step'), - Output('gauss1D-p', 'tooltip'), - Output('gauss1D-L', 'disabled'), - Input("gauss1D-smethod", "value"), -) -def update_smethod(smethod): - patched_tooltip = Patch() - match smethod: - case 'iid': - patched_tooltip.template = "dice" - return 0, 1, .5, 0.001, patched_tooltip, False - case 'Golden-Sequence': - patched_tooltip.template = "z={value}" - return -50, 50, 0, 1, patched_tooltip, False - case 'Equidistant': - patched_tooltip.template = "γ={value}" - return -1, 1, 0, 0.001, patched_tooltip, False - case 'Unscented': - patched_tooltip.template = "{value}" - return 0, 2, 1, 0.001, patched_tooltip, True - case _: - raise Exception("Wrong smethod") - - -@callback( - Output("gauss1D-graph", "figure"), - Input("gauss1D-smethod", "value"), - Input("gauss1D-p", "value"), - Input("gauss1D-L", "value"), - Input("gauss1D-μ", "value"), - Input("gauss1D-σ", "value"), -) -def update(smethod, p, L, μ, σ): - fig = go.Figure() # TODO use Patch here too - - if σ == 0: - # Dirac Delta - fig.add_trace(go.Scatter(x=[rang[0], μ, μ, μ, rang[1]], y=[0, 0, 1, 0, 0], hoverinfo='skip', line={'width': 5}, name='Dirac Delta', marker_color=col_density, showlegend=True)) - if L > 0: - fig.add_trace(go.Scatter(x=[μ, μ], y=[0, 1], name='Samples', mode='lines', marker_color=col_samples, showlegend=True)) - else: - # Draw Samples - xGauss = None - match smethod: - case 'iid': - # xUni = sort(rand(L)) - xGauss = sort(randn(L)*σ + μ) - case 'Golden-Sequence': - xUni = (sqrt(5)-1)/2 * (arange(L)+1+round(p)) % 1 - case 'Equidistant': - xUni = (2*arange(L)+1+p)/(2*L) - case 'Unscented': - # TODO scaled unscented etc - xGauss = array([μ-σ, μ+σ]) # TODO parameter - case _: - raise Exception("Wrong smethod") - # Transform Samples - if xGauss is None: - xGauss = σ*sqrt(2)*erfinv(2*xUni-1) + μ - L2 = len(xGauss) - sample_height = full([1, L2], gauss1(0, 0, σ)) - # sample_height = full([1, L], 1/L) - # Plot Density - s = linspace(rang[0], rang[1], 500) - # https://plotly.com/python-api-reference/generated/plotly.graph_objects.Scatter.html - # TODO lighter fillcolor: fillcolor=matplotlib.colors.to_rgba('#aabbcc80') - fig.add_trace(go.Scatter(x=s, y=gauss1(s, μ, σ), hoverinfo='skip', line={'width': 5}, line_shape='spline', name='Density', fill='tozeroy', marker_color=col_density, showlegend=True)) - # Plot Samples - xp = vstack((xGauss, xGauss, full([1, L2], nan))).T.flatten() - yp = vstack((full([1, L2], 0), sample_height, full([1, L2], nan))).T.flatten() - fig.add_trace(go.Scatter(x=xp, y=yp, name='Samples', mode='lines', marker_color=col_samples, showlegend=True)) - # Style - fig.update_xaxes(range=rang, tickmode='array', tickvals=list(range(-5, 6))) - fig.update_yaxes(range=[0, None], fixedrange=True) - fig.update_layout(legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)) - fig.update_layout(dragmode="pan") - - fig.update_layout( - legend=dict( - orientation="v", - xanchor="right", - x=0.1, - ) - ) - return fig - - -def gauss1(x, μ, σ): - return 1/sqrt(2*pi*σ) * exp(-1/2 * square((x-μ)/σ)) +) \ No newline at end of file diff --git a/renderer/selfcontained_distribution_renderer.py b/renderer/selfcontained_distribution_renderer.py new file mode 100644 index 0000000..7b7530d --- /dev/null +++ b/renderer/selfcontained_distribution_renderer.py @@ -0,0 +1,11 @@ +from model.selfcontained_distribution import SelfContainedDistribution + +class SelfContainedDistributionRenderer: + def __init__(self, distribution: SelfContainedDistribution): + self.distribution = distribution + + self.plot_layout = distribution.plot_layout + self.settings_layout = distribution.settings_layout + + def get_layout_components(self): + return (self.settings_layout, self.plot_layout) \ No newline at end of file From 33922472aa452591b8bc0c1d0afeaa9453b4d20c Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 15:29:17 +0100 Subject: [PATCH 120/152] move gaus1d info into md file --- model/distributions/gaus1d/gaus1d.py | 8 +++- model/distributions/gaus1d/info_text.md | 43 +++++++++++++++++++++ model/distributions/gaus1d/info_text.py | 51 ------------------------- pages/gauss2D.py | 2 +- 4 files changed, 50 insertions(+), 54 deletions(-) create mode 100644 model/distributions/gaus1d/info_text.md delete mode 100644 model/distributions/gaus1d/info_text.py diff --git a/model/distributions/gaus1d/gaus1d.py b/model/distributions/gaus1d/gaus1d.py index 9ea2931..55f5c8b 100644 --- a/model/distributions/gaus1d/gaus1d.py +++ b/model/distributions/gaus1d/gaus1d.py @@ -6,10 +6,10 @@ from numpy import sqrt, linspace, vstack, pi, nan, full, exp, square, sort, arange, array from numpy.random import randn, randint from scipy.special import erfinv +from pathlib import Path from components.popup_box import PopupBox -from model.distributions.gaus1d.info_text import info_text from model.selfcontained_distribution import SelfContainedDistribution class Gaus1D(SelfContainedDistribution): @@ -32,6 +32,10 @@ def __init__(self): self.col_samples = plotly.colors.qualitative.Plotly[0] + path = Path(__file__).parent / "info_text.md" + with open(path, 'r') as f: + self.info_text = dcc.Markdown(f.read(), mathjax=True) + self.settings_layout =[ dbc.Container( dbc.Col([ @@ -68,7 +72,7 @@ def __init__(self): html.Br(), # Info Popup - *PopupBox("gauss1D-info", "Learn More", "Additional Information", info_text), + *PopupBox("gauss1D-info", "Learn More", "Additional Information", self.info_text), ]) diff --git a/model/distributions/gaus1d/info_text.md b/model/distributions/gaus1d/info_text.md new file mode 100644 index 0000000..8db73ca --- /dev/null +++ b/model/distributions/gaus1d/info_text.md @@ -0,0 +1,43 @@ +## 1D Gaussian +Interactive visualization of the univariate Gaussian density + +$$ +f(x) = \frac{1}{\sqrt{2\pi}\sigma} +\cdot \exp\!\left\{ -\frac{1}{2} \cdot \left(\frac{x-\mu}{\sigma}\right)^2 \right\}\enspace, \quad x\in \mathbb{R} \enspace, +$$ + +with mean $\mu \in \mathbb{R}$ and standard deviation $\sigma \in \mathbb{R}_+$ . + +It was discovered by Karl Friedrich Gauß (1777-1855) in Göttingen, Germany. + +### Formulas +- quantile function +$Q(p) = \mu + \sigma\, \sqrt{2}\, \mathrm{erf}^{-1}(2p-1)$ +- uniform to Gaussian +$x_i^{\text{Gauss}} = Q(x_i^{\text{uni}})$ +- golden Kronecker sequence +$x_i^{\text{uni}}=\mod( \Phi \cdot (i+z), 1) \enspace, \quad i \in \{1,2,\ldots,L\}\enspace, \quad z \in \mathbb{Z}$ +- equidistant samples +$x_i^{\text{uni}} = \frac{2 i - 1 + \gamma}{2 L} \enspace, \quad i \in \{1,2,\ldots,L\}\enspace,\quad \gamma\in[-1,1]$ +- unscented (𝐿=2) +$x_1=\mu-\sigma\enspace, \quad x_2=\mu+\sigma$ +- unscented (𝐿=3) +TODO + +### Interactivity +- GUI +- add/remove lines: click in legend +- sampling methods (radiobutton) +- independent identically distributed (iid), the usual random samples +- golden sequence, a low-discrepancy Kronecker sequence based on the golden ratio +- equidistant, with identical amount of probability mass for all samples +- unscented transform sampling (𝐿=2) +- sampling parameter (slider) +- iid: dice again +- golden: integer offset 𝑧 +- equidistant: offset 𝛾 +- unscented: TODO +- number of Samples 𝐿 (slider) +- density parameters (slider) +- mean 𝜇 +- standard deviation 𝜎 diff --git a/model/distributions/gaus1d/info_text.py b/model/distributions/gaus1d/info_text.py deleted file mode 100644 index b0ebab9..0000000 --- a/model/distributions/gaus1d/info_text.py +++ /dev/null @@ -1,51 +0,0 @@ -from dash import dcc - -# Description -info_text = dcc.Markdown( - r''' - ## 1D Gaussian - Interactive visualization of the univariate Gaussian density - - $$ - f(x) = \frac{1}{\sqrt{2\pi}\sigma} - \cdot \exp\!\left\{ -\frac{1}{2} \cdot \left(\frac{x-\mu}{\sigma}\right)^2 \right\}\enspace, \quad x\in \mathbb{R} \enspace, - $$ - - with mean $\mu \in \mathbb{R}$ and standard deviation $\sigma \in \mathbb{R}_+$ . - - It was discovered by Karl Friedrich Gauß (1777-1855) in Göttingen, Germany. - - ### Formulas - - quantile function - $Q(p) = \mu + \sigma\, \sqrt{2}\, \mathrm{erf}^{-1}(2p-1)$ - - uniform to Gaussian - $x_i^{\text{Gauss}} = Q(x_i^{\text{uni}})$ - - golden Kronecker sequence - $x_i^{\text{uni}}=\mod( \Phi \cdot (i+z), 1) \enspace, \quad i \in \{1,2,\ldots,L\}\enspace, \quad z \in \mathbb{Z}$ - - equidistant samples - $x_i^{\text{uni}} = \frac{2 i - 1 + \gamma}{2 L} \enspace, \quad i \in \{1,2,\ldots,L\}\enspace,\quad \gamma\in[-1,1]$ - - unscented (𝐿=2) - $x_1=\mu-\sigma\enspace, \quad x_2=\mu+\sigma$ - - unscented (𝐿=3) - TODO - - ### Interactivity - - GUI - - add/remove lines: click in legend - - sampling methods (radiobutton) - - independent identically distributed (iid), the usual random samples - - golden sequence, a low-discrepancy Kronecker sequence based on the golden ratio - - equidistant, with identical amount of probability mass for all samples - - unscented transform sampling (𝐿=2) - - sampling parameter (slider) - - iid: dice again - - golden: integer offset 𝑧 - - equidistant: offset 𝛾 - - unscented: TODO - - number of Samples 𝐿 (slider) - - density parameters (slider) - - mean 𝜇 - - standard deviation 𝜎 - ''', - mathjax=True -) \ No newline at end of file diff --git a/pages/gauss2D.py b/pages/gauss2D.py index 4bdf6fe..0b386fb 100644 --- a/pages/gauss2D.py +++ b/pages/gauss2D.py @@ -358,4 +358,4 @@ def trafo_L(L0): def rot(a): ar = deg2rad(a) - return array([[cos(ar), -sin(ar)], [sin(ar), cos(ar)]]) + return array([[cos(ar), -sin(ar)], [sin(ar), cos(ar)]]) \ No newline at end of file From ef602fa0a8effcd9f4145ba887feb28a221a8597 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 16:09:20 +0100 Subject: [PATCH 121/152] refractor gauss2d --- model/distributions/gaus1d/gaus1d.py | 1 - model/distributions/gaus2d/__init__.py | 0 model/distributions/gaus2d/gaus2d.py | 311 ++++++++++++++++++++ model/distributions/gaus2d/info_text.md | 71 +++++ pages/gauss2D.py | 359 +----------------------- 5 files changed, 391 insertions(+), 351 deletions(-) create mode 100644 model/distributions/gaus2d/__init__.py create mode 100644 model/distributions/gaus2d/gaus2d.py create mode 100644 model/distributions/gaus2d/info_text.md diff --git a/model/distributions/gaus1d/gaus1d.py b/model/distributions/gaus1d/gaus1d.py index 55f5c8b..3f4af3e 100644 --- a/model/distributions/gaus1d/gaus1d.py +++ b/model/distributions/gaus1d/gaus1d.py @@ -9,7 +9,6 @@ from pathlib import Path from components.popup_box import PopupBox - from model.selfcontained_distribution import SelfContainedDistribution class Gaus1D(SelfContainedDistribution): diff --git a/model/distributions/gaus2d/__init__.py b/model/distributions/gaus2d/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/gaus2d/gaus2d.py b/model/distributions/gaus2d/gaus2d.py new file mode 100644 index 0000000..f849d12 --- /dev/null +++ b/model/distributions/gaus2d/gaus2d.py @@ -0,0 +1,311 @@ +from pathlib import Path +import plotly +import dash +import pandas +from urllib.error import HTTPError +from dash import dcc, html, Input, Output, callback, Patch +import plotly.graph_objects as go +import dash_bootstrap_components as dbc +from numpy import sqrt, linspace, vstack, hstack, pi, nan, full, exp, square, arange, array, sin, cos, diff, matmul, log10, deg2rad, identity, ones, zeros, diag, cov, mean +from numpy.random import randn, randint +from numpy.linalg import cholesky, eig, det, inv +from scipy.special import erfinv + +from components.popup_box import PopupBox +from model.selfcontained_distribution import SelfContainedDistribution + +# Samples Library +# technically, this has state, but its fine because its just a cache +data_dict = {} + +# Get Samples from Library (and load if not available) +def get_data(url): + if not (url in data_dict): + try: + data = pandas.read_csv(url, header=None).to_numpy() + except HTTPError: + # URL doesn't exist + data = full([2, 0], nan) + data_dict[url] = data + return data_dict[url] + +class Gaus2D(SelfContainedDistribution): + def __init__(self): + self.smethods = ['iid', 'Fibonacci', 'LCD', 'SP-Julier04', 'SP-Menegaz11'] # Sampling methods + self.tmethods = ['Cholesky', 'Eigendecomposition'] # Transformation methods + + # Colors + self.col_density = plotly.colors.qualitative.Plotly[1] + self.col_samples = plotly.colors.qualitative.Plotly[0] + + self.config = { + 'toImageButtonOptions': { + 'format': 'jpeg', # png, svg, pdf, jpeg, webp + 'height': None, # None: use currently-rendered size + 'width': None, + 'filename': 'gauss2d', + }, + 'scrollZoom': True, + } + + # axis limits + self.rangx = [-5, 5] + self.rangy = [-4, 4] + + # plot size relative to window size + relwidth = 95 + self.relheight = round((relwidth/diff(self.rangx)*diff(self.rangy))[0]) + + # Gauss ellipse + s = linspace(0, 2*pi, 500) + self.circ = vstack((cos(s), sin(s))) * 2 + + self.fig = go.Figure( + data=[ + go.Scattergl(name='Density', + x=[0], + y=[0], + mode='lines', + marker_color=self.col_density, + showlegend=True, + hoverinfo='skip', + line={'width': 3}, + line_shape='linear', + fill='tozerox' + ), + go.Scattergl( + name='Samples', + x=[0], + y=[0], + mode='markers', + marker_color=self.col_samples, + marker_line_color='black', + marker_opacity=1, + showlegend=True + ) + ], + ) + self.fig.update_xaxes(range=self.rangx, tickmode='array', tickvals=list(range(self.rangx[0], self.rangx[1]+1))) + self.fig.update_yaxes(range=self.rangy, tickmode='array', tickvals=list(range(self.rangy[0], self.rangy[1]+1)), scaleanchor="x", scaleratio=1) + self.fig.update_layout(legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1)) + self.fig.update_layout(modebar_add=['drawopenpath', 'eraseshape'], newshape_line_color='cyan', dragmode='pan') + + self.fig.update_layout( + legend=dict( + orientation="v", + xanchor="right", + x=0.1, + ) + ) + + path = Path(__file__).parent / "info_text.md" + with open(path, 'r') as f: + self.info_text = dcc.Markdown(f.read(), mathjax=True) + + self.settings_layout = [ + dbc.Container( + dbc.Col([ + html.P("Select Sampling Method:"), + html.Br(), + + # Sampling Strategy RadioItems + dbc.RadioItems(id='gauss2D-smethod', + options=[{"label": x, "value": x} for x in self.smethods], + value=self.smethods[randint(len(self.smethods))], + inline=True), + + # Transformation Method RadioItems + dbc.RadioItems(id='gauss2D-tmethod', + options=[{"label": x, "value": x} for x in self.tmethods], + value=self.tmethods[randint(len(self.tmethods))], + inline=True), + + html.Br(), + html.Hr(), + html.Br(), + + # param Slider + dcc.Slider(id="gauss2D-p", min=0, max=1, value=randint(3, 7)/10, updatemode='drag', marks=None, + tooltip={"template": "p={value}", "placement": "bottom", "always_visible": True}), + + # L Slider + dcc.Slider(id="gauss2D-L", min=log10(1.2), max=4.001, step=0.001, value=2, updatemode='drag', marks=None, # persistence=True, + tooltip={"template": "L={value}", "placement": "bottom", "always_visible": True, "transform": "trafo_L"}), + + # σ Slider + dcc.Slider(id="gauss2D-σx", min=0, max=5, step=0.01, value=1, updatemode='drag', marks=None, + tooltip={"template": 'σx={value}', "placement": "bottom", "always_visible": True}), + dcc.Slider(id="gauss2D-σy", min=0, max=5, step=0.01, value=1, updatemode='drag', marks=None, + tooltip={"template": 'σy={value}', "placement": "bottom", "always_visible": True}), + + # ρ Slider + dcc.Slider(id="gauss2D-ρ", min=-1, max=1, step=0.001, value=0, updatemode='drag', marks=None, + tooltip={"template": 'ρ={value}', "placement": "bottom", "always_visible": True}), + + html.Hr(), + html.Br(), + + + # Info Popup + *PopupBox("gauss2D-info", "Learn More", "Additional Information", self.info_text), + ]), + fluid=True, + className="g-0") + ] + + self.plot_layout = [ + dcc.Graph(id="gauss2D-graph", figure=self.fig, config=self.config, style={'height': '100%'}), + ] + + self._register_callbacks() + + def _register_callbacks(self): + @callback( + Output('gauss2D-p', 'min'), + Output('gauss2D-p', 'max'), + Output('gauss2D-p', 'value'), + Output('gauss2D-p', 'step'), + Output('gauss2D-p', 'tooltip'), + Output('gauss2D-L', 'disabled'), + Input("gauss2D-smethod", "value"), + ) + def update_smethod(smethod): + patched_tooltip = Patch() + match smethod: + case 'iid': + patched_tooltip.template = "dice" + # min, max, value, step, tooltip + return 0, 1, .5, 0.001, patched_tooltip, False + case 'Fibonacci': + patched_tooltip.template = "z={value}" + return -50, 50, 0, 1, patched_tooltip, False + case 'LCD': + patched_tooltip.template = "α={value}°" + return -360, 360, 0, 0.1, patched_tooltip, False + case 'SP-Julier04': + patched_tooltip.template = "W₀={value}" + return -2, 1, .1, 0.001, patched_tooltip, True + case 'SP-Menegaz11': + patched_tooltip.template = "Wₙ₊₁={value}" + return 0, 1, 1/3, 0.001, patched_tooltip, True + case _: + raise Exception("Wrong smethod") + + + @callback( + Output("gauss2D-graph", "figure"), + Input("gauss2D-smethod", "value"), + Input("gauss2D-tmethod", "value"), + Input("gauss2D-p", "value"), + Input("gauss2D-L", "value"), + Input("gauss2D-σx", "value"), + Input("gauss2D-σy", "value"), + Input("gauss2D-ρ", "value"), + ) + def update(smethod, tmethod, p, L0, σx, σy, ρ): + # Slider Transform, + L = self.trafo_L(L0) + # Mean + # μ = array([[μx], [μy]]) + μ = array([[0], [0]]) + # Covariance + C = array([[square(σx), σx*σy*ρ], [σx*σy*ρ, square(σy)]]) + C_D, C_R = eig(C) + C_D = C_D[..., None] # to column vector + + patched_fig = Patch() + # Draw SND + weights = None + match smethod: + case 'iid': + xySND = randn(2, L) + case 'Fibonacci': + # TODO 2nd parameter + xUni = (sqrt(5)-1)/2 * (arange(L)+1+round(p)) % 1 + yUni = (2*arange(L)+1)/(2*L) # +p + xyUni = vstack((xUni, yUni)) + xySND = sqrt(2)*erfinv(2*xyUni-1) + case 'LCD': + xySND = get_data(self.url_SND_LCD(2, L)) + xySND = matmul(self.rot(p), xySND) + case 'SP-Julier04': + # https://ieeexplore.ieee.org/abstract/document/1271397 + Nx = 2 # dimension + x0 = zeros([Nx, 1]) + W0 = full([1, 1], p) # parameter, W0<1 + x1 = sqrt(Nx/(1-W0) * identity(Nx)) + W1 = full([1, Nx], (1-W0)/(2*Nx)) + x2 = -x1 + W2 = W1 + xySND = hstack((x0, x1, x2)) + weights = hstack((W0, W1, W2)) + case 'SP-Menegaz11': + # https://ieeexplore.ieee.org/abstract/document/6161480 + n = 2 # dimension + w0 = p # parameter, 0 Date: Mon, 8 Dec 2025 16:42:26 +0100 Subject: [PATCH 122/152] refractored conditional --- model/distributions/conditional/__init__.py | 0 .../distributions/conditional/conditional.py | 235 ++++++++++++++++++ model/distributions/conditional/info_text.md | 35 +++ pages/conditional.py | 214 +--------------- 4 files changed, 280 insertions(+), 204 deletions(-) create mode 100644 model/distributions/conditional/__init__.py create mode 100644 model/distributions/conditional/conditional.py create mode 100644 model/distributions/conditional/info_text.md diff --git a/model/distributions/conditional/__init__.py b/model/distributions/conditional/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/distributions/conditional/conditional.py b/model/distributions/conditional/conditional.py new file mode 100644 index 0000000..c2d094b --- /dev/null +++ b/model/distributions/conditional/conditional.py @@ -0,0 +1,235 @@ +from pathlib import Path +import plotly +import dash +from dash import dcc, html, Input, Output, callback, Patch, callback_context +import plotly.graph_objects as go +import dash_bootstrap_components as dbc +from numpy import sqrt, linspace, pi, sign, exp, square, array, diff, matmul, zeros, meshgrid +from numpy.random import randint +from numpy.linalg import det, solve + +from components.popup_box import PopupBox +from components.label import Label + + +from model.selfcontained_distribution import SelfContainedDistribution + +class Conditional(SelfContainedDistribution): + def __init__(self): + # Colors + col_marginal = plotly.colors.qualitative.Plotly[0] + col_conditional = plotly.colors.qualitative.Plotly[1] + col_slice = plotly.colors.qualitative.Plotly[4] + # axis limits + rangx = [-4, 4] + rangy = [-4, 4] + # slider range + smin = rangy[0] + smax = rangy[1] + # plot size relative to window size + relwidth = 95 + relheight = round((relwidth/diff(rangx)*diff(rangy))[0]) + + # Grid + self.xv = linspace(rangx[0], rangx[1], 100) + self.yv = linspace(rangy[0], rangy[1], 100) + self.xm, self.ym = meshgrid(self.xv, self.yv) + + self.config = { + 'toImageButtonOptions': { + 'format': 'jpeg', # png, svg, pdf, jpeg, webp + 'width': None, # None: use currently-rendered size + 'height': None, + 'filename': 'conditional', + }, + 'responsive': True, + 'scrollZoom': True, + } + + self.fig = go.Figure( + data=[ + go.Surface( + name='Joint f(x,y)', + x=self.xm, + y=self.ym, + z=self.xm*0, + hoverinfo='skip', + colorscale='Cividis', + showlegend=True, + showscale=False, + reversescale=False, + contours={ + "z": { + "show": True, + "start": 0.1, + "end": 1, + "size": 0.1, + "width": 1, + "color": "white" + } + } + ), + go.Scatter3d( + name='Marginal f(x)', + x=self.xv, + y=self.yv*0+self.yv[0], + mode='lines', + z=self.xv*0, + marker_color=col_marginal, + showlegend=True, + hoverinfo='skip', + line={'width': 10}, + # surfaceaxis=2 # wait for: https://github.com/plotly/plotly.js/issues/2352 + ), + go.Scatter3d( + name='Conditional f(x|ŷ)', + x=self.xv, y=self.yv*0+self.yv[0], + mode='lines', + z=self.xv*0, + marker_color=col_conditional, + showlegend=True, + hoverinfo='skip', + line={'width': 4}, + # surfaceaxis=2 # wait for: https://github.com/plotly/plotly.js/issues/2352 + ), + go.Scatter3d( + name='Slice f(x,ŷ)', + x=self.xv, + y=self.yv*0+self.yv[0], + mode='lines', + z=self.xv*0, + marker_color=col_slice, + showlegend=True, + hoverinfo='skip', + line={'width': 8} + ), + ] + ) + self.fig.update_xaxes(range=rangx, tickmode='array', tickvals=list(range(rangx[0], rangx[1]+1))) + self.fig.update_yaxes(range=rangy, tickmode='array', tickvals=list(range(rangy[0], rangy[1]+1)), scaleanchor="x", scaleratio=1) + self.fig.update_layout(transition_duration=100, transition_easing='linear') + self.fig.update_scenes(camera_projection_type="orthographic") + self.fig.update_scenes(aspectmode="auto") + # fig.update_scenes(xaxis_nticks=1) + # fig.update_scenes(yaxis_nticks=1) + self.fig.update_scenes(zaxis_nticks=1) + self.fig.update_layout(margin=dict(l=0, r=0, t=0, b=0, pad=0)) + + self.fig.update_layout( + legend=dict( + yanchor="top", + y=0.98, + xanchor="left", + x=0.02, + bgcolor="rgba(255,255,255,0.7)" + ) + ) + + path = Path(__file__).parent / "info_text.md" + with open(path, 'r') as f: + self.info_text = dcc.Markdown(f.read(), mathjax=True) + + self.settings_layout = [ + dbc.Container( + dbc.Col([ + html.Br(), + + # y Slider + Label("Condition on ŷ", + dcc.Slider( + id="joint-y", + min=smin, + max=smax, + value=randint(smin*10, smax*10)/10, + updatemode='drag', marks=None, + tooltip={"template": "ŷ={value}", "placement": "bottom", "always_visible": True} + ), + ), + + # ρ Slider + Label("Correlation ρ", + dcc.Slider( + id="joint-ρ", + min=-1, max=1, + value=randint(-9, 9)/10, + updatemode='mouseup', + marks=None, + tooltip={"template": "ρ={value}", "placement": "bottom", "always_visible": True} + ) + ), + + html.Hr(), + html.Br(), + + # Info Popup + *PopupBox("joint-info", "Learn More", "Additional Information", self.info_text), + + ]), + fluid=True, + className="g-0" + ), + ] + self.plot_layout = [ + dcc.Graph(id="joint-graph", figure=self.fig, config=self.config, style={'height': '100%'}), + ] + + self._register_callbacks() + + def _register_callbacks(self): + @callback( + Output("joint-graph", "figure"), + Input("joint-y", "value"), + Input("joint-ρ", "value"), + ) + def update(ys, ρ): + patched_fig = Patch() + # Joint Parameters + μ = zeros([2, 1]) + sx = 1 + sy = 1 + # TODO special treatment for singular density + ρ = sign(ρ) * min(abs(ρ), .9999) + C = array([[sx**2, sx*sy*ρ], [sx*sy*ρ, sy**2]]) + # Marginal Parameters + µMarginal = µ[0] + CMarginal = C[0, 0] + marginal_fac = 1 / self.gauss1(0, 0, CMarginal) + # Density has been modified? + if (callback_context.triggered_id == "joint-ρ") | (callback_context.triggered_id is None): + zMarginal = self.gauss1(self.xv, µMarginal, CMarginal) + patched_fig['data'][1]['z'] = zMarginal * marginal_fac + # Compute new joint density values + # TODO should be more elegant than 2 for loops + zJoint = self.xm*0 + for i in range(self.xm.shape[0]): + for j in range(self.xm.shape[1]): + zJoint[i, j] = self.gauss2(self.xm[i, j], self.ym[i, j], μ, C) + zJoint = zJoint / self.gauss2(0, 0, zeros([2, 1]), C) # rescale to height 1 + patched_fig['data'][0]['z'] = zJoint + # Compute Conditional + # https://www.math.uwaterloo.ca/~hwolkowi/matrixcookbook.pdf + µCond = µ[0] + C[0, 1] / C[1, 1] * (ys-µ[1]) + CCond = C[0, 0] - C[0, 1] / C[1, 1] * C[0, 1] + zCond = self.gauss1(self.xv, µCond, CCond) + zSlice = zCond / self.gauss1(0, 0, CCond) * self.gauss1(ys, µ[1], C[1, 1]) / self.gauss1(0, 0, C[1, 1]) + # Plot Conditional + patched_fig['data'][2]['z'] = zCond * marginal_fac + # Plot Joint Slice + patched_fig['data'][3]['y'] = self.xv*0+ys + patched_fig['data'][3]['z'] = zSlice + 1e-3 + return patched_fig + + @staticmethod + def gauss1(x, μ, C): + return 1/sqrt(2*pi*C) * exp(-1/2 * square((x-μ))/C) + + + @staticmethod + def gauss2(x, y, μ, C): + d = array([x-μ[0], y-μ[1]]) + d = d.reshape(-1, 1) # to column vector + f = 1/sqrt(det(2*pi*C)) * exp(-1/2 * matmul(d.T, solve(C, d))) + return f[0][0] + + + diff --git a/model/distributions/conditional/info_text.md b/model/distributions/conditional/info_text.md new file mode 100644 index 0000000..8863da1 --- /dev/null +++ b/model/distributions/conditional/info_text.md @@ -0,0 +1,35 @@ +## 2D Gaussian +Interactive visualizaton of the 2D Gaussian and its marginal and conditional density. + +$$ +f(\underline x) = \mathcal{N}(\underline x; \underline \mu, \textbf{C}) = +\frac{1}{2\pi \sqrt{\det(\textbf{C})}} +\cdot \exp\!\left\{ -\frac{1}{2} +\cdot (\underline x - \underline \mu)^\top \textbf{C}^{-1} (\underline x - \underline \mu) \right\} \enspace, +\quad \underline{x}\in \mathbb{R}^2 \enspace, \quad \textbf{C} \enspace \text{positive semidefinite} \enspace. +$$ + +### Formulas and Literature +The Gaussian parameters are restricted to +$$ +\underline \mu = \begin{bmatrix}0 \\ 0\end{bmatrix}\,, \quad +\textbf{C} = \begin{bmatrix}1 & \rho \\ \rho & 1\end{bmatrix} \enspace. +$$ + +Formulas for marginalization and conditioning of are given in the +[[MatrixCookbook](https://www.math.uwaterloo.ca/~hwolkowi/matrixcookbook.pdf)]. + +Note that the 1D and 2D densities are scaled with respect to each other such that 2D joint and 1D marginal have +the same height and therefore the same shape when looking on the x-z plane. + + +### Interactivity +- GUI + - rotate: left mouse click + - pan: right mouse click + - zoom: mouse wheel + - add/remove lines: click in legend +- value in state space (slider) + - value to condition on $\hat{y}$ +- density parameter (slider) + - correlation coefficient $\rho$ \ No newline at end of file diff --git a/pages/conditional.py b/pages/conditional.py index c6f2eb9..11d5a6a 100644 --- a/pages/conditional.py +++ b/pages/conditional.py @@ -1,216 +1,22 @@ -import plotly import dash -from dash import dcc, html, Input, Output, callback, Patch, callback_context -import plotly.graph_objects as go -import dash_bootstrap_components as dbc -from numpy import sqrt, linspace, pi, sign, exp, square, array, diff, matmul, zeros, meshgrid -from numpy.random import randint -from numpy.linalg import det, solve -from components.popup_box import PopupBox + from components.split_pane import SplitPane -from components.label import Label +from model.distributions.conditional.conditional import Conditional +from renderer.selfcontained_distribution_renderer import SelfContainedDistributionRenderer as Renderer dash.register_page(__name__) -# Colors -col_marginal = plotly.colors.qualitative.Plotly[0] -col_conditional = plotly.colors.qualitative.Plotly[1] -col_slice = plotly.colors.qualitative.Plotly[4] -# axis limits -rangx = [-4, 4] -rangy = [-4, 4] -# slider range -smin = rangy[0] -smax = rangy[1] -# plot size relative to window size -relwidth = 95 -relheight = round((relwidth/diff(rangx)*diff(rangy))[0]) - -# Grid -xv = linspace(rangx[0], rangx[1], 100) -yv = linspace(rangy[0], rangy[1], 100) -xm, ym = meshgrid(xv, yv) - - -def gauss1(x, μ, C): - return 1/sqrt(2*pi*C) * exp(-1/2 * square((x-μ))/C) - - -def gauss2(x, y, μ, C): - d = array([x-μ[0], y-μ[1]]) - d = d.reshape(-1, 1) # to column vector - f = 1/sqrt(det(2*pi*C)) * exp(-1/2 * matmul(d.T, solve(C, d))) - return f[0][0] - - -# Initialize Plot -# https://plotly.com/python/3d-surface-plots/ -fig = go.Figure() -fig.add_trace(go.Surface(name='Joint f(x,y)', x=xm, y=ym, z=xm*0, hoverinfo='skip', colorscale='Cividis', showlegend=True, showscale=False, reversescale=False, contours={"z": {"show": True, "start": 0.1, "end": 1, "size": 0.1, "width": 1, "color": "white"}})) -# fig.add_trace(go.Mesh3d(name='Joint', x=x, y=y, z=z, intensity=z, colorscale='Viridis')) # can deal with vector xyz, but hover coords not in foreground -fig.add_trace(go.Scatter3d(name='Marginal f(x)', x=xv, y=yv*0+yv[0], mode='lines', z=xv*0, marker_color=col_marginal, showlegend=True, hoverinfo='skip', line={'width': 10})) # , surfaceaxis=2 wait for: https://github.com/plotly/plotly.js/issues/2352 -fig.add_trace(go.Scatter3d(name='Conditional f(x|ŷ)', x=xv, y=yv*0+yv[0], mode='lines', z=xv*0, marker_color=col_conditional, showlegend=True, hoverinfo='skip', line={'width': 4})) # , surfaceaxis=2 wait for: https://github.com/plotly/plotly.js/issues/2352 -fig.add_trace(go.Scatter3d(name='Slice f(x,ŷ)', x=xv, y=yv*0+yv[0], mode='lines', z=xv*0, marker_color=col_slice, showlegend=True, hoverinfo='skip', line={'width': 8})) -fig.update_xaxes(range=rangx, tickmode='array', tickvals=list(range(rangx[0], rangx[1]+1))) -fig.update_yaxes(range=rangy, tickmode='array', tickvals=list(range(rangy[0], rangy[1]+1)), scaleanchor="x", scaleratio=1) -fig.update_layout(transition_duration=100, transition_easing='linear') -fig.update_scenes(camera_projection_type="orthographic") -fig.update_scenes(aspectmode="auto") -# fig.update_scenes(xaxis_nticks=1) -# fig.update_scenes(yaxis_nticks=1) -fig.update_scenes(zaxis_nticks=1) -fig.update_layout(margin=dict(l=0, r=0, t=0, b=0, pad=0)) - -fig.update_layout( - legend=dict( - yanchor="top", - y=0.98, - xanchor="left", - x=0.02, - bgcolor="rgba(255,255,255,0.7)" - ) -) - -config = { - 'toImageButtonOptions': { - 'format': 'jpeg', # png, svg, pdf, jpeg, webp - 'width': None, # None: use currently-rendered size - 'height': None, - 'filename': 'conditional', - }, - 'responsive': True, - 'scrollZoom': True, -} - -# Description -info_text = dcc.Markdown( - r''' - ## 2D Gaussian - Interactive visualizaton of the 2D Gaussian and its marginal and conditional density. +renderer = Renderer(Conditional()) +options, graph = renderer.get_layout_components() - $$ - f(\underline x) = \mathcal{N}(\underline x; \underline \mu, \textbf{C}) = - \frac{1}{2\pi \sqrt{\det(\textbf{C})}} - \cdot \exp\!\left\{ -\frac{1}{2} - \cdot (\underline x - \underline \mu)^\top \textbf{C}^{-1} (\underline x - \underline \mu) \right\} \enspace, - \quad \underline{x}\in \mathbb{R}^2 \enspace, \quad \textbf{C} \enspace \text{positive semidefinite} \enspace. - $$ - - ### Formulas and Literature - The Gaussian parameters are restricted to - $$ - \underline \mu = \begin{bmatrix}0 \\ 0\end{bmatrix}\,, \quad - \textbf{C} = \begin{bmatrix}1 & \rho \\ \rho & 1\end{bmatrix} \enspace. - $$ - - Formulas for marginalization and conditioning of are given in the - [[MatrixCookbook](https://www.math.uwaterloo.ca/~hwolkowi/matrixcookbook.pdf)]. - - Note that the 1D and 2D densities are scaled with respect to each other such that 2D joint and 1D marginal have - the same height and therefore the same shape when looking on the x-z plane. - - - ### Interactivity - - GUI - - rotate: left mouse click - - pan: right mouse click - - zoom: mouse wheel - - add/remove lines: click in legend - - value in state space (slider) - - value to condition on $\hat{y}$ - - density parameter (slider) - - correlation coefficient $\rho$ - ''', - mathjax=True -) - -layout = SplitPane([ - dbc.Container( - dbc.Col([ - html.Br(), - - # y Slider - Label("Condition on ŷ", - dcc.Slider( - id="joint-y", - min=smin, - max=smax, - value=randint(smin*10, smax*10)/10, - updatemode='drag', marks=None, - tooltip={"template": "ŷ={value}", "placement": "bottom", "always_visible": True} - ), - ), - - # ρ Slider - Label("Correlation ρ", - dcc.Slider( - id="joint-ρ", - min=-1, max=1, - value=randint(-9, 9)/10, - updatemode='mouseup', - marks=None, - tooltip={"template": "ρ={value}", "placement": "bottom", "always_visible": True} - ) - ), - - html.Hr(), - html.Br(), - - # Info Popup - *PopupBox("joint-info", "Learn More", "Additional Information", info_text), - - ]), - fluid=True, - className="g-0"), +layout = SplitPane( + [ + *options ], [ - # Plot - dcc.Graph(id="joint-graph", figure=fig, config=config, style={'height': '100%'}), + *graph ], 30 -) - -@callback( - Output("joint-graph", "figure"), - Input("joint-y", "value"), - Input("joint-ρ", "value"), -) -def update(ys, ρ): - patched_fig = Patch() - # Joint Parameters - μ = zeros([2, 1]) - sx = 1 - sy = 1 - # TODO special treatment for singular density - ρ = sign(ρ) * min(abs(ρ), .9999) - C = array([[sx**2, sx*sy*ρ], [sx*sy*ρ, sy**2]]) - # Marginal Parameters - µMarginal = µ[0] - CMarginal = C[0, 0] - marginal_fac = 1 / gauss1(0, 0, CMarginal) - # Density has been modified? - if (callback_context.triggered_id == "joint-ρ") | (callback_context.triggered_id is None): - zMarginal = gauss1(xv, µMarginal, CMarginal) - patched_fig['data'][1]['z'] = zMarginal * marginal_fac - # Compute new joint density values - # TODO should be more elegant than 2 for loops - zJoint = xm*0 - for i in range(xm.shape[0]): - for j in range(xm.shape[1]): - zJoint[i, j] = gauss2(xm[i, j], ym[i, j], μ, C) - zJoint = zJoint / gauss2(0, 0, zeros([2, 1]), C) # rescale to height 1 - patched_fig['data'][0]['z'] = zJoint - # Compute Conditional - # https://www.math.uwaterloo.ca/~hwolkowi/matrixcookbook.pdf - µCond = µ[0] + C[0, 1] / C[1, 1] * (ys-µ[1]) - CCond = C[0, 0] - C[0, 1] / C[1, 1] * C[0, 1] - zCond = gauss1(xv, µCond, CCond) - zSlice = zCond / gauss1(0, 0, CCond) * gauss1(ys, µ[1], C[1, 1]) / gauss1(0, 0, C[1, 1]) - # Plot Conditional - patched_fig['data'][2]['z'] = zCond * marginal_fac - # Plot Joint Slice - patched_fig['data'][3]['y'] = xv*0+ys - patched_fig['data'][3]['z'] = zSlice + 1e-3 - return patched_fig +) \ No newline at end of file From ef889fcd720a537f658f012db441676c7c91bfaf Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 16:59:19 +0100 Subject: [PATCH 123/152] update requirements.txt TODO maybe remove it? we are using poetry anyways --- requirements.txt | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index be2c677..712837b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,11 @@ +astropy-iers-data==0.2025.11.3.0.38.37 ; python_version == "3.10" +astropy==6.1.7 ; python_version == "3.10" +beartype==0.22.5 ; python_version == "3.10" blinker==1.9.0 ; python_version == "3.10" certifi==2025.10.5 ; python_version == "3.10" charset-normalizer==3.4.4 ; python_version == "3.10" click==8.3.0 ; python_version == "3.10" -colorama==0.4.6 ; (platform_system == "Windows" or sys_platform == "win32") and python_version == "3.10" +colorama==0.4.6 ; python_version == "3.10" and platform_system == "Windows" contourpy==1.3.2 ; python_version == "3.10" cycler==0.12.1 ; python_version == "3.10" dash-bootstrap-components==1.7.1 ; python_version == "3.10" @@ -14,46 +17,50 @@ dash-table==5.0.0 ; python_version == "3.10" dash-vtk==0.0.9 ; python_version == "3.10" dash==2.18.2 ; python_version == "3.10" et-xmlfile==2.0.0 ; python_version == "3.10" -exceptiongroup==1.3.0 ; python_version == "3.10" filterpy==1.4.5 ; python_version == "3.10" flask==3.0.3 ; python_version == "3.10" fonttools==4.60.1 ; python_version == "3.10" gunicorn==23.0.0 ; python_version == "3.10" idna==3.11 ; python_version == "3.10" importlib-metadata==8.7.0 ; python_version == "3.10" -iniconfig==2.3.0 ; python_version == "3.10" itsdangerous==2.2.0 ; python_version == "3.10" jinja2==3.1.6 ; python_version == "3.10" +kent-distribution @ git+https://github.com/Vlad-Kor/kent_distribution@1637440397d97c51ad688fdcc8e2c96c6db48d77 ; python_version == "3.10" kiwisolver==1.4.9 ; python_version == "3.10" markupsafe==3.0.3 ; python_version == "3.10" matplotlib==3.10.7 ; python_version == "3.10" mpmath==1.3.0 ; python_version == "3.10" -narwhals==2.9.0 ; python_version == "3.10" +narwhals==2.10.2 ; python_version == "3.10" nest-asyncio==1.6.0 ; python_version == "3.10" +numpy-quaternion==2024.0.12 ; python_version == "3.10" numpy==1.26.4 ; python_version == "3.10" openpyxl==3.1.5 ; python_version == "3.10" packaging==25.0 ; python_version == "3.10" pandas==2.3.3 ; python_version == "3.10" pillow==12.0.0 ; python_version == "3.10" -plotly==6.3.1 ; python_version == "3.10" -pluggy==1.6.0 ; python_version == "3.10" -pygments==2.19.2 ; python_version == "3.10" +platformdirs==4.5.0 ; python_version == "3.10" +plotly==6.4.0 ; python_version == "3.10" +pooch==1.8.2 ; python_version == "3.10" +pyerfa==2.0.1.5 ; python_version == "3.10" pyparsing==3.2.5 ; python_version == "3.10" -pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@7ee536d3da48f05855d2d4d2eec6321e065e9a85 ; python_version == "3.10" -pytest==8.4.2 ; python_version == "3.10" +pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@002c7757e2a5a81e4e57da5c1b657c5f9bd5cf6e ; python_version == "3.10" +pyshtools==4.13.1 ; python_version == "3.10" python-dateutil==2.9.0.post0 ; python_version == "3.10" pytz==2025.2 ; python_version == "3.10" +pyyaml==6.0.3 ; python_version == "3.10" requests==2.32.5 ; python_version == "3.10" retrying==1.4.2 ; python_version == "3.10" scipy==1.15.3 ; python_version == "3.10" setuptools==80.9.0 ; python_version == "3.10" +shapely==2.1.2 ; python_version == "3.10" six==1.17.0 ; python_version == "3.10" -sphstat==1.0.6 ; python_version == "3.10" +sphstat @ git+https://github.com/Vlad-Kor/sphstat@fad7f8779d4b4f8eeccae0f5e1e2ed6dd7280a22 ; python_version == "3.10" sympy==1.14.0 ; python_version == "3.10" -tomli==2.3.0 ; python_version == "3.10" +tqdm==4.67.1 ; python_version == "3.10" typing-extensions==4.15.0 ; python_version == "3.10" tzdata==2025.2 ; python_version == "3.10" urllib3==1.26.20 ; python_version == "3.10" vtk==9.5.2 ; python_version == "3.10" werkzeug==3.0.6 ; python_version == "3.10" +xarray==2025.6.1 ; python_version == "3.10" zipp==3.23.0 ; python_version == "3.10" From a24984a8ea046413db96012c0b206c3a7ea21674 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 17:05:57 +0100 Subject: [PATCH 124/152] added favicon.ico --- assets/favicon.ico | Bin 0 -> 5430 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 assets/favicon.ico diff --git a/assets/favicon.ico b/assets/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..824a92413e38a1ab805001c3ee210bdb6ed6e9d6 GIT binary patch literal 5430 zcmc&&eP~oy7Jsgrj&U;c-ix?FvXV?@CSOZeB*Y=c%)E~*B4r6hid&Xainv`_WRX%N zl#&HmiiB2HO_RxcZ&Eh|TcPw1DI)$M(nd3Ni-?sV5<(G?MWjfO5Ja-S-<>(*%Q$X@ z{bL7i&b{aJ-gD16_q=G@I_(#lVQBcZYJdKvrnPg{T3UY&{3pP5Vo4UF*QvjnW&vOQ zTU}jU=h<=lL`JJq^K5jh?-;|Lt?wW2tB(*T<3vhZ?_054;21|i&j~(o&3Vs>Z}SJD zG0Yb*o?twMo-@Kj7&!Vn#AB?G$G7$05npf{7d$uK>)Xb`z%lRomhn2~hrqM!WgF7u zIkh8JhxI++rvls9jXivc^~JC}WIx6jCXNF<0eFsZ?%75k=C?lGrK@uWuAabpX^p&V z0M(`>8JsHe&j0|DgZFcgzonUs|iK5B*9peM+>7JL1w+J8Bk2nYX0{+W!uI|A$z5wnz*A@Te;!VIU`OpE|J!QF- zGQ5s^Zt*f&1b)9z;5|6z{;)jfz8&8~n;45;CK`bcu1{uT3GhD3TUs5?cM>$*tv*iv zYI&}4XBqH3_8fg5{5J->Z(;r8Shu#Gc--ONMV1h_1;kHp7zeu^@JH2fjE@1oDOh?J zpgK>O7dB?{*^OJmINn29egyD5#u@1B1^joIuQ@dr$#j1{lt|4sh4Hw{bKo6TG2ogH zupS-nkD7to_~bL&*etxQskuzLvpAn@zxw{C*!KyTcudn)SO1s6I@5|_VMv|% zo2pGfgnqIY!jW!mOy567ZIKg*Js&_!Jb7N$eB|4)e&HiTt++@&)Z0oe z@~wtfA^((k>cK$%8!Ms@`e(vA!gJ($7tXvSyqr^B)|lTI{jC-inVR-(J6V!77x1o`f)`kDjkim5V8u=U}vjLeZ|$+GTc!Bw9>Kd`Y!@i=Btc&Z`p6!p%0n&0v) zj>rR_T%zoR9o<$f!?n5}?hixg)%FX>(Vw%AT5yg$gyu&oV4_V6-UU=!AKvW?fgS5mdvc9Sf~$t8b?=mPn?DJy+CJ+rc^KpH^OW#~A)b--B_89B z;F0&cm2Ss4DY!7?onXIvMz0{@jslLgWX5NWDLPbZ75M060M@#nOdjv-axfYyzzLc}9 z=cp?K4VBti%RXe-*WIk_0l9|#pAWy&(6h`O6kG6_%83cvsN^1c%O>RN67N!U74WSf zGK9746VG>+vCOwqWYLR`h%EZ|%fwG)<7wispWC#9o>ZNU_lP>HYBl*#8~FWkQRD@_ z$Uf=$oslGDZa}v3mZAr4BF(enBh>3#@i&z%ycw|fSoC_9en9q3Jo?#6%@-cR|7M?b z+{I@4p1!8ONhhXqKFCH@y!)>H(WxEUrt%qYiEiw9v4m?}4*N~$0)Ngv`KTZIR38L* zUvSiqckUc}C}pc$575`g#Akd5#a1o&GWIElTK6t(aImjKZ>zqyVCQ@GG>Yb!yn&F^`ht#lnuA>4q|bLbJQU9?oFZp;u^8@ zFs!529JL@I??f(E=}Btw5r_C*A}{L+dqmFRzx&9&zjBTkdy9OshVQ?BkuJge*@hf^ zaIr8@yPpz1*4$Fg!~Zxt@$?69k0C!s`}8~Uu&Zhz_VL0uO!RhZXCsF)e?VJ2vZgN< zy1+>*x?OUBXIFNR->SD}ZwLDm*@&tI$jv^rAJiz3uLbd~w-w)M`%;~AA0^x8-qf_S zHTOV!sE5ILN_ebai;;G$gYnP$*tm*s!y(}h!(X>wX-=eyZ=^b} z&ZN7(yN~a^Blz}HF*v@np(z;D_i~_)>_Z$Khp&|lob9maVP8xeY7dAF!4sn9+%Ds9 zAubM6*VLe~*Bdm3Sl{uD=(e_A>B6__jrQ*ER#TnD%WZA5y_)s|zVXdz`1Ot&{Qr$p ziaYFKWvo}*B6je7voBQS|*V}j#UKfAk?00ED-CcZ4|F?Di zi{Bn9&PBJtz%nXlr;RZyk`@|lTnO7Av$U9p{$J=}h4QFD4R~ zGn#g(9(jCbEx-8?HTX#QyM?yYyRLdsRR{Nn?Q`x~^;==<%djrSFz)TOL}IR~t^M1> zZSB`a;Dgncwr^icv|l+yIn*{2JwUIFF)O--wyO1W?PoV`XWe-bJwPn_&W0Azg?h7% t`Ac7?I<5|31j)|g4B#JutJvV1CZm>J!BLmg1HsjT)w)9e3i!WF{{uF#nrHw3 literal 0 HcmV?d00001 From 5d179a1d31096d761210608ace7f3641a71235a0 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Mon, 8 Dec 2025 17:07:11 +0100 Subject: [PATCH 125/152] update comment --- app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.py b/app.py index 4dbdde1..aa673bc 100644 --- a/app.py +++ b/app.py @@ -4,7 +4,7 @@ import dash_bootstrap_components as dbc # Usage Locally: -# $ python app.py +# $ poetry run python app.py # select theme # https://dash-bootstrap-components.opensource.faculty.ai/docs/themes/ From 90b66c5cb6420bb0244ec477b5d98e20e2afd126 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 11 Dec 2025 13:29:31 +0100 Subject: [PATCH 126/152] fix artefact --- model/distributions/gaus2d/gaus2d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/distributions/gaus2d/gaus2d.py b/model/distributions/gaus2d/gaus2d.py index f849d12..7f1de69 100644 --- a/model/distributions/gaus2d/gaus2d.py +++ b/model/distributions/gaus2d/gaus2d.py @@ -71,7 +71,7 @@ def __init__(self): hoverinfo='skip', line={'width': 3}, line_shape='linear', - fill='tozerox' + fill='toself' ), go.Scattergl( name='Samples', From b8fd9d5e906a92d34dd2c9601b926ad81a943904 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 17 Dec 2025 10:07:34 +0100 Subject: [PATCH 127/152] added angle to gaus2d inspired by work from @kevkev2ro but reimplemented from scratch --- model/distributions/gaus2d/gaus2d.py | 55 +++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/model/distributions/gaus2d/gaus2d.py b/model/distributions/gaus2d/gaus2d.py index 7f1de69..808320c 100644 --- a/model/distributions/gaus2d/gaus2d.py +++ b/model/distributions/gaus2d/gaus2d.py @@ -3,14 +3,16 @@ import dash import pandas from urllib.error import HTTPError -from dash import dcc, html, Input, Output, callback, Patch +from dash import dcc, html, Input, Output, callback, Patch, ctx, no_update import plotly.graph_objects as go import dash_bootstrap_components as dbc +import numpy as np from numpy import sqrt, linspace, vstack, hstack, pi, nan, full, exp, square, arange, array, sin, cos, diff, matmul, log10, deg2rad, identity, ones, zeros, diag, cov, mean from numpy.random import randn, randint from numpy.linalg import cholesky, eig, det, inv from scipy.special import erfinv + from components.popup_box import PopupBox from model.selfcontained_distribution import SelfContainedDistribution @@ -141,6 +143,10 @@ def __init__(self): # ρ Slider dcc.Slider(id="gauss2D-ρ", min=-1, max=1, step=0.001, value=0, updatemode='drag', marks=None, tooltip={"template": 'ρ={value}', "placement": "bottom", "always_visible": True}), + + # ρ Slider + dcc.Slider(id="angle", min=0, max=180, step=2, value=0, updatemode='drag', marks=None, + tooltip={"template": 'angle={value}°', "placement": "bottom", "always_visible": True}), html.Hr(), html.Br(), @@ -194,6 +200,10 @@ def update_smethod(smethod): @callback( Output("gauss2D-graph", "figure"), + Output("gauss2D-σx", "value"), + Output("gauss2D-σy", "value"), + Output("gauss2D-ρ", "value"), + Output("angle", "value"), Input("gauss2D-smethod", "value"), Input("gauss2D-tmethod", "value"), Input("gauss2D-p", "value"), @@ -201,15 +211,51 @@ def update_smethod(smethod): Input("gauss2D-σx", "value"), Input("gauss2D-σy", "value"), Input("gauss2D-ρ", "value"), + Input("angle", "value"), ) - def update(smethod, tmethod, p, L0, σx, σy, ρ): + def update(smethod, tmethod, p, L0, sigma_x, sigma_y, rho, angle): + trig = ctx.triggered_id + + def _rot2d(angle_deg): + a = np.deg2rad(angle_deg) + c, s = np.cos(a), np.sin(a) + return np.array([[c, -s], + [s, c]], dtype=float) + + + + + if trig == "angle": + R = _rot2d(angle) + C = np.array([[sigma_x**2, sigma_x*sigma_y*rho], + [sigma_x*sigma_y*rho, sigma_y**2]]) + w, _ = np.linalg.eigh(C) + w = np.sort(w)[::-1] + C_rot = R @ np.diag(w) @ R.T + sigma_x = np.sqrt(C_rot[0, 0]) + sigma_y = np.sqrt(C_rot[1, 1]) + if sigma_x > 0: + rho = C_rot[0, 1] / (sigma_x * sigma_y) + else: + rho = 0.0 + silder_changes = (sigma_x, sigma_y, rho, no_update) + else: + if np.isclose(rho, 0) and np.isclose(sigma_x, sigma_y): + angle = 0.0 + else: + angle = 0.5 * np.arctan2(2 * rho * sigma_x * sigma_y, sigma_x**2 - sigma_y**2) + angle = np.rad2deg(angle) + angle = angle % 180 + silder_changes = (no_update, no_update, no_update, angle) + + # Slider Transform, L = self.trafo_L(L0) # Mean # μ = array([[μx], [μy]]) μ = array([[0], [0]]) # Covariance - C = array([[square(σx), σx*σy*ρ], [σx*σy*ρ, square(σy)]]) + C = array([[square(sigma_x), sigma_x*sigma_y*rho], [sigma_x*sigma_y*rho, square(sigma_y)]]) C_D, C_R = eig(C) C_D = C_D[..., None] # to column vector @@ -271,7 +317,6 @@ def update(smethod, tmethod, p, L0, σx, σy, ρ): else: weights = weights.flatten() sizes = sqrt(abs(weights) * L2) * det(2*pi*C)**(1/4) / sqrt(L2) * 70 - # print(hstack((cov(xyG, bias=True, aweights=weights), C))) # Plot Ellipse elp = matmul(C_R, sqrt(C_D) * self.circ) + μ patched_fig['data'][0]['x'] = elp[0, :] @@ -281,7 +326,7 @@ def update(smethod, tmethod, p, L0, σx, σy, ρ): patched_fig['data'][1]['y'] = xyG[1, :] patched_fig['data'][1]['marker']['size'] = sizes patched_fig['data'][1]['marker']['line']['width'] = sizes/20 - return patched_fig + return patched_fig, *silder_changes @staticmethod From e84e5ce6e30da5a5e1bd9f63cb2b068aa5613114 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 18 Dec 2025 16:36:19 +0100 Subject: [PATCH 128/152] added deterministic gauss library --- poetry.lock | 171 ++++++++++++++++++++++++++++++++++++++----------- pyproject.toml | 3 +- 2 files changed, 134 insertions(+), 40 deletions(-) diff --git a/poetry.lock b/poetry.lock index 65ee2ff..c6aaec9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -744,6 +744,27 @@ files = [ dash = "*" vtk = "*" +[[package]] +name = "deterministic-gaussian-sampling-fibonacci" +version = "0.2.0" +description = "Deterministic sampling via orthogonal inverse transform of low-discrepancy Fibonacci grids." +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [] +develop = false + +[package.dependencies] +h5py = ">=3.15.1,<4.0.0" +numpy = ">=2.0.0,<3.0.0" +scipy = {version = ">=1.15,<1.16", markers = "python_version < \"3.11\""} + +[package.source] +type = "git" +url = "https://github.com/KIT-ISAS/deterministic_gaussian_sampling_fibonacci" +reference = "HEAD" +resolved_reference = "93034064d8f9d016a02e2fa8ea955f7a2a2b6777" + [[package]] name = "dill" version = "0.4.0" @@ -944,6 +965,59 @@ files = [ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] +[[package]] +name = "h5py" +version = "3.15.1" +description = "Read and write HDF5 files from Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "h5py-3.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67e59f6c2f19a32973a40f43d9a088ae324fe228c8366e25ebc57ceebf093a6b"}, + {file = "h5py-3.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e2f471688402c3404fa4e13466e373e622fd4b74b47b56cfdff7cc688209422"}, + {file = "h5py-3.15.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c45802bcb711e128a6839cb6c01e9ac648dc55df045c9542a675c771f15c8d5"}, + {file = "h5py-3.15.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64ce3f6470adb87c06e3a8dd1b90e973699f1759ad79bfa70c230939bff356c9"}, + {file = "h5py-3.15.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4411c1867b9899a25e983fff56d820a66f52ac326bbe10c7cdf7d832c9dcd883"}, + {file = "h5py-3.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2cbc4104d3d4aca9d6db8c0c694555e255805bfeacf9eb1349bda871e26cacbe"}, + {file = "h5py-3.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:01f55111ca516f5568ae7a7fc8247dfce607de331b4467ee8a9a6ed14e5422c7"}, + {file = "h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243"}, + {file = "h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509"}, + {file = "h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b"}, + {file = "h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf"}, + {file = "h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c"}, + {file = "h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831"}, + {file = "h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878"}, + {file = "h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427"}, + {file = "h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c"}, + {file = "h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76"}, + {file = "h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e"}, + {file = "h5py-3.15.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25c8843fec43b2cc368aa15afa1cdf83fc5e17b1c4e10cd3771ef6c39b72e5ce"}, + {file = "h5py-3.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a308fd8681a864c04423c0324527237a0484e2611e3441f8089fd00ed56a8171"}, + {file = "h5py-3.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4a016df3f4a8a14d573b496e4d1964deb380e26031fc85fb40e417e9131888a"}, + {file = "h5py-3.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:59b25cf02411bf12e14f803fef0b80886444c7fe21a5ad17c6a28d3f08098a1e"}, + {file = "h5py-3.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:61d5a58a9851e01ee61c932bbbb1c98fe20aba0a5674776600fb9a361c0aa652"}, + {file = "h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8440fd8bee9500c235ecb7aa1917a0389a2adb80c209fa1cc485bd70e0d94a5"}, + {file = "h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab2219dbc6fcdb6932f76b548e2b16f34a1f52b7666e998157a4dfc02e2c4123"}, + {file = "h5py-3.15.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8cb02c3a96255149ed3ac811eeea25b655d959c6dd5ce702c9a95ff11859eb5"}, + {file = "h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8"}, + {file = "h5py-3.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59b0d63b318bf3cc06687def2b45afd75926bbc006f7b8cd2b1a231299fc8599"}, + {file = "h5py-3.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e02fe77a03f652500d8bff288cbf3675f742fc0411f5a628fa37116507dc7cc0"}, + {file = "h5py-3.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:dea78b092fd80a083563ed79a3171258d4a4d307492e7cf8b2313d464c82ba52"}, + {file = "h5py-3.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:c256254a8a81e2bddc0d376e23e2a6d2dc8a1e8a2261835ed8c1281a0744cd97"}, + {file = "h5py-3.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f4fb0567eb8517c3ecd6b3c02c4f4e9da220c8932604960fd04e24ee1254763"}, + {file = "h5py-3.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:954e480433e82d3872503104f9b285d369048c3a788b2b1a00e53d1c47c98dd2"}, + {file = "h5py-3.15.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd125c131889ebbef0849f4a0e29cf363b48aba42f228d08b4079913b576bb3a"}, + {file = "h5py-3.15.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28a20e1a4082a479b3d7db2169f3a5034af010b90842e75ebbf2e9e49eb4183e"}, + {file = "h5py-3.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8df5267f545b4946df8ca0d93d23382191018e4cda2deda4c2cedf9a010e13"}, + {file = "h5py-3.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99d374a21f7321a4c6ab327c4ab23bd925ad69821aeb53a1e75dd809d19f67fa"}, + {file = "h5py-3.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:9c73d1d7cdb97d5b17ae385153472ce118bed607e43be11e9a9deefaa54e0734"}, + {file = "h5py-3.15.1-cp314-cp314-win_arm64.whl", hash = "sha256:a6d8c5a05a76aca9a494b4c53ce8a9c29023b7f64f625c6ce1841e92a362ccdf"}, + {file = "h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69"}, +] + +[package.dependencies] +numpy = ">=1.21.2" + [[package]] name = "idna" version = "3.11" @@ -1611,48 +1685,67 @@ files = [ [[package]] name = "numpy" -version = "1.26.4" +version = "2.2.6" description = "Fundamental package for array computing in Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, - {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, - {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, - {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, - {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, - {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, - {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, - {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, - {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, - {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, - {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, - {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, - {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, - {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, - {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, - {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, - {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, - {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, - {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, - {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, - {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, - {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, - {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163"}, + {file = "numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf"}, + {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83"}, + {file = "numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915"}, + {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680"}, + {file = "numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289"}, + {file = "numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d"}, + {file = "numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42"}, + {file = "numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491"}, + {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a"}, + {file = "numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf"}, + {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1"}, + {file = "numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab"}, + {file = "numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47"}, + {file = "numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3"}, + {file = "numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282"}, + {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87"}, + {file = "numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249"}, + {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49"}, + {file = "numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de"}, + {file = "numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4"}, + {file = "numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d"}, + {file = "numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566"}, + {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f"}, + {file = "numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f"}, + {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868"}, + {file = "numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d"}, + {file = "numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd"}, + {file = "numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40"}, + {file = "numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8"}, + {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f"}, + {file = "numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa"}, + {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571"}, + {file = "numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1"}, + {file = "numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff"}, + {file = "numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543"}, + {file = "numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00"}, + {file = "numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd"}, ] [[package]] @@ -3256,4 +3349,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.11" -content-hash = "c82a202be54f6a4615afad2b04cd40b613894e842671ea77f3c43239bade1539" +content-hash = "6e066e4b072266b8d1efca875200d7c4e1425e2ce8974aed8f835ba2f6b54c16" diff --git a/pyproject.toml b/pyproject.toml index 0784956..5366006 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ requires-python = ">=3.10,<3.11" dependencies = [ "dash (>=2.16,<3.0)", "dash-bootstrap-components (>=1.6,<2.0)", - "numpy (>=1.26,<2.0)", + "numpy (>=2.0,<3.0)", "dash-mantine-components (>=0.14,<0.15)", "pandas (>=2.2,<3.0)", "dash-vtk (>=0.0.9,<0.0.10)", @@ -22,6 +22,7 @@ dependencies = [ "pyrecest @ git+https://github.com/KIT-ISAS/pyRecEst.git@002c7757e2a5a81e4e57da5c1b657c5f9bd5cf6e", "kent-distribution @ git+https://github.com/Vlad-Kor/kent_distribution", "sympy (>=1.14.0,<2.0.0)", + "deterministic-gaussian-sampling-fibonacci @ git+https://github.com/KIT-ISAS/deterministic_gaussian_sampling_fibonacci", ] [dependency-groups] From d2aa33bf2f73a6b0107a6c771de14f6a3427a6e2 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 18 Dec 2025 16:50:47 +0100 Subject: [PATCH 129/152] added frolov to pwn --- .../partially_wraped_normal/frolov.py | 42 +++++++++++++++++++ .../improved_frolov.py | 42 +++++++++++++++++++ .../partially_warpped_normal.py | 5 +++ 3 files changed, 89 insertions(+) create mode 100644 model/distributions/cylinder/partially_wraped_normal/frolov.py create mode 100644 model/distributions/cylinder/partially_wraped_normal/improved_frolov.py diff --git a/model/distributions/cylinder/partially_wraped_normal/frolov.py b/model/distributions/cylinder/partially_wraped_normal/frolov.py new file mode 100644 index 0000000..507c449 --- /dev/null +++ b/model/distributions/cylinder/partially_wraped_normal/frolov.py @@ -0,0 +1,42 @@ +import numpy as np +from deterministic_gaussian_sampling_fibonacci import sample_gaussian_fibonacci + +from util.selectors.silder_log import LogSlider +from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from util.gaus_util import GausUtil as gu + + +class CFrolovPWNSampling(CylinderSamplingSchema): + def __init__(self): + def _check_input(val): + return val >= 1 and val <= 100003 and isinstance(val, int) + + self.sample_options = [ + MI(LogSlider("Number of Samples", 10, 100, 10000)) + ] + + def get_name(self): + return "Classical Frolov" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state + + mu = np.array([mean_x, mean_y]) + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "ClassicalFrolov") + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + return gaus_grid \ No newline at end of file diff --git a/model/distributions/cylinder/partially_wraped_normal/improved_frolov.py b/model/distributions/cylinder/partially_wraped_normal/improved_frolov.py new file mode 100644 index 0000000..8dae74e --- /dev/null +++ b/model/distributions/cylinder/partially_wraped_normal/improved_frolov.py @@ -0,0 +1,42 @@ +import numpy as np +from deterministic_gaussian_sampling_fibonacci import sample_gaussian_fibonacci + +from util.selectors.silder_log import LogSlider +from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from util.gaus_util import GausUtil as gu + + +class IFrolovPWNSampling(CylinderSamplingSchema): + def __init__(self): + def _check_input(val): + return val >= 1 and val <= 100003 and isinstance(val, int) + + self.sample_options = [ + MI(LogSlider("Number of Samples", 10, 100, 10000)) + ] + + def get_name(self): + return "Improved Frolov" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state + + mu = np.array([mean_x, mean_y]) + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "ImprovedFrolov") + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + return gaus_grid \ No newline at end of file diff --git a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py index 5bb0ad1..ee4e603 100644 --- a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py +++ b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py @@ -8,6 +8,9 @@ from model.distributions.cylinder.partially_wraped_normal.fibonacci_rank_1 import CylinderFibRank1PWNSampling from model.distributions.cylinder.partially_wraped_normal.fibonacci_kronecker import CylinderFibKroneckerPWNSampling from model.distributions.cylinder.partially_wraped_normal.cartesian import CylinderFibCartPWNSampling +from model.distributions.cylinder.partially_wraped_normal.frolov import CFrolovPWNSampling +from model.distributions.cylinder.partially_wraped_normal.improved_frolov import IFrolovPWNSampling + from model.cylinder.cylinder import Cylinder class PartiallyWrappedNormalDistribution(CylinderDistribution): @@ -24,6 +27,8 @@ def __init__(self): CylinderFibRank1PWNSampling(), CylinderFibKroneckerPWNSampling(), CylinderFibCartPWNSampling(), + CFrolovPWNSampling(), + IFrolovPWNSampling(), ] def get_name(self): From ffbaff618f13314b83e95906b7c676801b374c4e Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 18 Dec 2025 16:55:55 +0100 Subject: [PATCH 130/152] added frolov to wn --- .../torus/wrapped_normal/frolov.py | 42 +++++++++++++++++++ .../torus/wrapped_normal/improved_frolov.py | 42 +++++++++++++++++++ .../torus/wrapped_normal/wrapped_normal.py | 4 ++ 3 files changed, 88 insertions(+) create mode 100644 model/distributions/torus/wrapped_normal/frolov.py create mode 100644 model/distributions/torus/wrapped_normal/improved_frolov.py diff --git a/model/distributions/torus/wrapped_normal/frolov.py b/model/distributions/torus/wrapped_normal/frolov.py new file mode 100644 index 0000000..72a472e --- /dev/null +++ b/model/distributions/torus/wrapped_normal/frolov.py @@ -0,0 +1,42 @@ +import numpy as np +from deterministic_gaussian_sampling_fibonacci import sample_gaussian_fibonacci + +from util.selectors.silder_log import LogSlider +from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from util.gaus_util import GausUtil as gu + + +class CFrolovWNSampling(TorusSamplingSchema): + def __init__(self): + def _check_input(val): + return val >= 1 and val <= 100003 and isinstance(val, int) + + self.sample_options = [ + MI(LogSlider("Number of Samples", 10, 100, 10000)) + ] + + def get_name(self): + return "Classical Frolov" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state + + mu = np.array([mean_x, mean_y]) + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "ClassicalFrolov") + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + return gaus_grid \ No newline at end of file diff --git a/model/distributions/torus/wrapped_normal/improved_frolov.py b/model/distributions/torus/wrapped_normal/improved_frolov.py new file mode 100644 index 0000000..2dbb118 --- /dev/null +++ b/model/distributions/torus/wrapped_normal/improved_frolov.py @@ -0,0 +1,42 @@ +import numpy as np +from deterministic_gaussian_sampling_fibonacci import sample_gaussian_fibonacci + +from util.selectors.silder_log import LogSlider +from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from util.gaus_util import GausUtil as gu + + +class IFrolovWNSampling(TorusSamplingSchema): + def __init__(self): + def _check_input(val): + return val >= 1 and val <= 100003 and isinstance(val, int) + + self.sample_options = [ + MI(LogSlider("Number of Samples", 10, 100, 10000)) + ] + + def get_name(self): + return "Improved Frolov" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state + + mu = np.array([mean_x, mean_y]) + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "ImprovedFrolov") + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + return gaus_grid \ No newline at end of file diff --git a/model/distributions/torus/wrapped_normal/wrapped_normal.py b/model/distributions/torus/wrapped_normal/wrapped_normal.py index 8f55e76..4abd73b 100644 --- a/model/distributions/torus/wrapped_normal/wrapped_normal.py +++ b/model/distributions/torus/wrapped_normal/wrapped_normal.py @@ -8,6 +8,8 @@ from model.distributions.torus.wrapped_normal.fibonacci import TorusFibRank1WNSampling from model.distributions.torus.wrapped_normal.fibonacci_kronecker import TorusFibKroneckerWNSampling from model.distributions.torus.wrapped_normal.cartesian import TorusFibCartWNSampling +from model.distributions.torus.wrapped_normal.frolov import CFrolovWNSampling +from model.distributions.torus.wrapped_normal.improved_frolov import IFrolovWNSampling from model.torus.torus import Torus class WrappedNormalTorusDistribution(TorusDistribution): def __init__(self): @@ -23,6 +25,8 @@ def __init__(self): TorusFibRank1WNSampling(), TorusFibKroneckerWNSampling(), TorusFibCartWNSampling(), + CFrolovWNSampling(), + IFrolovWNSampling(), ] From 3c8a992e6847d4bd440321547df894db9984d6f2 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 18 Dec 2025 17:06:52 +0100 Subject: [PATCH 131/152] fix wrap on wm --- model/distributions/torus/wrapped_normal/frolov.py | 1 + model/distributions/torus/wrapped_normal/improved_frolov.py | 1 + 2 files changed, 2 insertions(+) diff --git a/model/distributions/torus/wrapped_normal/frolov.py b/model/distributions/torus/wrapped_normal/frolov.py index 72a472e..f17a271 100644 --- a/model/distributions/torus/wrapped_normal/frolov.py +++ b/model/distributions/torus/wrapped_normal/frolov.py @@ -39,4 +39,5 @@ def sample(self, sample_options, distribution_options): # wrapp gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + gaus_grid[:,1] = gaus_grid[:,1] % (2 * np.pi) return gaus_grid \ No newline at end of file diff --git a/model/distributions/torus/wrapped_normal/improved_frolov.py b/model/distributions/torus/wrapped_normal/improved_frolov.py index 2dbb118..a07118d 100644 --- a/model/distributions/torus/wrapped_normal/improved_frolov.py +++ b/model/distributions/torus/wrapped_normal/improved_frolov.py @@ -39,4 +39,5 @@ def sample(self, sample_options, distribution_options): # wrapp gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + gaus_grid[:,1] = gaus_grid[:,1] % (2 * np.pi) return gaus_grid \ No newline at end of file From 34ab8e97ab51ad36e338d854aa11bd728c21384d Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Sun, 11 Jan 2026 20:58:41 +0100 Subject: [PATCH 132/152] changed the changelog --- pages/home.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pages/home.py b/pages/home.py index fd138da..cb6e3c6 100644 --- a/pages/home.py +++ b/pages/home.py @@ -15,6 +15,7 @@ Version History: - 2024-04 | Daniel Frisch | gauss1d, gauss2d + - 2026-01 | Vlad Korsakov | sphere, torus, cylinder and improved UI TODO: - Sample Reduction From 387df9295b2596048bc9a627f1890c76953aa857 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Sun, 11 Jan 2026 21:00:50 +0100 Subject: [PATCH 133/152] changed color scheme --- renderer/object_3D_and_2D_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/object_3D_and_2D_renderer.py b/renderer/object_3D_and_2D_renderer.py index 1a32210..c98ce07 100644 --- a/renderer/object_3D_and_2D_renderer.py +++ b/renderer/object_3D_and_2D_renderer.py @@ -59,7 +59,7 @@ def __init__(self, object, id): x=self.color_heatmap_x, y=self.color_heatmap_y, z=np.zeros((100,100)), - colorscale="Plasma", + colorscale="Viridis", zmin=0.0, zmax=1.0, zsmooth="best", From 60dcd09db5cc386bc431d64d09f2d74e510146ff Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 13 Jan 2026 16:07:09 +0100 Subject: [PATCH 134/152] change colorscale again for more contrast --- renderer/object_3D_and_2D_renderer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/renderer/object_3D_and_2D_renderer.py b/renderer/object_3D_and_2D_renderer.py index c98ce07..c9f917b 100644 --- a/renderer/object_3D_and_2D_renderer.py +++ b/renderer/object_3D_and_2D_renderer.py @@ -59,7 +59,7 @@ def __init__(self, object, id): x=self.color_heatmap_x, y=self.color_heatmap_y, z=np.zeros((100,100)), - colorscale="Viridis", + colorscale="cividis", zmin=0.0, zmax=1.0, zsmooth="best", From 3e944e6332b4257d6e4333a3360b83be3fe50dfa Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 13 Jan 2026 16:07:29 +0100 Subject: [PATCH 135/152] added fibonacci frolov --- .../fibonacci_frolov.py | 42 ++++++++++++++++++ .../partially_warpped_normal.py | 2 + .../torus/wrapped_normal/fibonacci_frolov.py | 43 +++++++++++++++++++ .../torus/wrapped_normal/wrapped_normal.py | 2 + 4 files changed, 89 insertions(+) create mode 100644 model/distributions/cylinder/partially_wraped_normal/fibonacci_frolov.py create mode 100644 model/distributions/torus/wrapped_normal/fibonacci_frolov.py diff --git a/model/distributions/cylinder/partially_wraped_normal/fibonacci_frolov.py b/model/distributions/cylinder/partially_wraped_normal/fibonacci_frolov.py new file mode 100644 index 0000000..6335fd4 --- /dev/null +++ b/model/distributions/cylinder/partially_wraped_normal/fibonacci_frolov.py @@ -0,0 +1,42 @@ +import numpy as np +from deterministic_gaussian_sampling_fibonacci import sample_gaussian_fibonacci + +from util.selectors.silder_log import LogSlider +from model.distributions.cylinder.cylinder_sampling_schema import CylinderSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from util.gaus_util import GausUtil as gu + + +class FFrolovPWNSampling(CylinderSamplingSchema): + def __init__(self): + def _check_input(val): + return val >= 1 and val <= 100003 and isinstance(val, int) + + self.sample_options = [ + MI(LogSlider("Number of Samples", 10, 100, 10000)) + ] + + def get_name(self): + return "Fibonacci Frolov" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state + + mu = np.array([mean_x, mean_y]) + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "Fibonacci") + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + return gaus_grid \ No newline at end of file diff --git a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py index ee4e603..a4a0d21 100644 --- a/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py +++ b/model/distributions/cylinder/partially_wraped_normal/partially_warpped_normal.py @@ -10,6 +10,7 @@ from model.distributions.cylinder.partially_wraped_normal.cartesian import CylinderFibCartPWNSampling from model.distributions.cylinder.partially_wraped_normal.frolov import CFrolovPWNSampling from model.distributions.cylinder.partially_wraped_normal.improved_frolov import IFrolovPWNSampling +from model.distributions.cylinder.partially_wraped_normal.fibonacci_frolov import FFrolovPWNSampling from model.cylinder.cylinder import Cylinder @@ -29,6 +30,7 @@ def __init__(self): CylinderFibCartPWNSampling(), CFrolovPWNSampling(), IFrolovPWNSampling(), + FFrolovPWNSampling(), ] def get_name(self): diff --git a/model/distributions/torus/wrapped_normal/fibonacci_frolov.py b/model/distributions/torus/wrapped_normal/fibonacci_frolov.py new file mode 100644 index 0000000..c1b8b2e --- /dev/null +++ b/model/distributions/torus/wrapped_normal/fibonacci_frolov.py @@ -0,0 +1,43 @@ +import numpy as np +from deterministic_gaussian_sampling_fibonacci import sample_gaussian_fibonacci + +from util.selectors.silder_log import LogSlider +from model.distributions.torus.torus_sampling_schema import TorusSamplingSchema +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from util.gaus_util import GausUtil as gu + + +class FFrolovWNSampling(TorusSamplingSchema): + def __init__(self): + def _check_input(val): + return val >= 1 and val <= 100003 and isinstance(val, int) + + self.sample_options = [ + MI(LogSlider("Number of Samples", 10, 100, 10000)) + ] + + def get_name(self): + return "Fibonacci Frolov" + + def sample(self, sample_options, distribution_options): + sample_count = sample_options[0].state + + mean_x = distribution_options[0].state + mean_y = distribution_options[1].state + sigma_t = distribution_options[2].state + sigma_p = distribution_options[3].state + correlation = distribution_options[4].state + + mu = np.array([mean_x, mean_y]) + + Cov = np.array([ + [sigma_t**2, correlation * sigma_t * sigma_p], + [correlation * sigma_t * sigma_p, sigma_p**2] + ]) + + gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "Fibonacci") + + # wrapp + gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) + gaus_grid[:,1] = gaus_grid[:,1] % (2 * np.pi) + return gaus_grid \ No newline at end of file diff --git a/model/distributions/torus/wrapped_normal/wrapped_normal.py b/model/distributions/torus/wrapped_normal/wrapped_normal.py index 4abd73b..2b03bfe 100644 --- a/model/distributions/torus/wrapped_normal/wrapped_normal.py +++ b/model/distributions/torus/wrapped_normal/wrapped_normal.py @@ -10,6 +10,7 @@ from model.distributions.torus.wrapped_normal.cartesian import TorusFibCartWNSampling from model.distributions.torus.wrapped_normal.frolov import CFrolovWNSampling from model.distributions.torus.wrapped_normal.improved_frolov import IFrolovWNSampling +from model.distributions.torus.wrapped_normal.fibonacci_frolov import FFrolovWNSampling from model.torus.torus import Torus class WrappedNormalTorusDistribution(TorusDistribution): def __init__(self): @@ -27,6 +28,7 @@ def __init__(self): TorusFibCartWNSampling(), CFrolovWNSampling(), IFrolovWNSampling(), + FFrolovWNSampling(), ] From fdcde6ed67cd6beb0218c51f2c53260e8090334e Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 13 Jan 2026 16:50:43 +0100 Subject: [PATCH 136/152] added frolov to gaus2d --- model/distributions/gaus2d/gaus2d.py | 45 ++++++++++++++++++++++------ poetry.lock | 4 +-- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/model/distributions/gaus2d/gaus2d.py b/model/distributions/gaus2d/gaus2d.py index 808320c..7899600 100644 --- a/model/distributions/gaus2d/gaus2d.py +++ b/model/distributions/gaus2d/gaus2d.py @@ -3,7 +3,7 @@ import dash import pandas from urllib.error import HTTPError -from dash import dcc, html, Input, Output, callback, Patch, ctx, no_update +from dash import dcc, html, Input, Output, callback, Patch, ctx, no_update, State import plotly.graph_objects as go import dash_bootstrap_components as dbc import numpy as np @@ -11,7 +11,7 @@ from numpy.random import randn, randint from numpy.linalg import cholesky, eig, det, inv from scipy.special import erfinv - +import deterministic_gaussian_sampling_fibonacci as dgsf from components.popup_box import PopupBox from model.selfcontained_distribution import SelfContainedDistribution @@ -33,7 +33,7 @@ def get_data(url): class Gaus2D(SelfContainedDistribution): def __init__(self): - self.smethods = ['iid', 'Fibonacci', 'LCD', 'SP-Julier04', 'SP-Menegaz11'] # Sampling methods + self.smethods = ['iid', 'Fibonacci', 'LCD', 'SP-Julier04', 'SP-Menegaz11', 'Classical Frolov', 'Improved Frolov', 'Fibonacci Frolov'] # Sampling methods self.tmethods = ['Cholesky', 'Eigendecomposition'] # Transformation methods # Colors @@ -116,6 +116,8 @@ def __init__(self): value=self.smethods[randint(len(self.smethods))], inline=True), + html.Br(), + # Transformation Method RadioItems dbc.RadioItems(id='gauss2D-tmethod', options=[{"label": x, "value": x} for x in self.tmethods], @@ -173,27 +175,39 @@ def _register_callbacks(self): Output('gauss2D-p', 'step'), Output('gauss2D-p', 'tooltip'), Output('gauss2D-L', 'disabled'), + Output('gauss2D-tmethod', 'className'), + Output('gauss2D-p', 'className'), # to hide when not needed Input("gauss2D-smethod", "value"), ) def update_smethod(smethod): patched_tooltip = Patch() + patched_tooltip.always_visible = True match smethod: case 'iid': patched_tooltip.template = "dice" # min, max, value, step, tooltip - return 0, 1, .5, 0.001, patched_tooltip, False + return 0, 1, .5, 0.001, patched_tooltip, False, 'visible', 'visible' case 'Fibonacci': patched_tooltip.template = "z={value}" - return -50, 50, 0, 1, patched_tooltip, False + return -50, 50, 0, 1, patched_tooltip, False, 'visible', 'visible' case 'LCD': patched_tooltip.template = "α={value}°" - return -360, 360, 0, 0.1, patched_tooltip, False + return -360, 360, 0, 0.1, patched_tooltip, False, 'visible', 'visible' case 'SP-Julier04': patched_tooltip.template = "W₀={value}" - return -2, 1, .1, 0.001, patched_tooltip, True + return -2, 1, .1, 0.001, patched_tooltip, True, 'visible', 'visible' case 'SP-Menegaz11': patched_tooltip.template = "Wₙ₊₁={value}" - return 0, 1, 1/3, 0.001, patched_tooltip, True + return 0, 1, 1/3, 0.001, patched_tooltip, True, 'visible', 'visible' + case 'Classical Frolov': + patched_tooltip.always_visible = False + return no_update, no_update, no_update, no_update, patched_tooltip, False, 'invisible', 'invisible' + case 'Improved Frolov': + patched_tooltip.always_visible = False + return no_update, no_update, no_update, no_update, patched_tooltip, False, 'invisible', 'invisible' + case 'Fibonacci Frolov': + patched_tooltip.always_visible = False + return no_update, no_update, no_update, no_update, patched_tooltip, False, 'invisible', 'invisible' case _: raise Exception("Wrong smethod") @@ -214,6 +228,8 @@ def update_smethod(smethod): Input("angle", "value"), ) def update(smethod, tmethod, p, L0, sigma_x, sigma_y, rho, angle): + if smethod in ['Classical Frolov', 'Improved Frolov', 'Fibonacci Frolov']: + tmethod = None trig = ctx.triggered_id def _rot2d(angle_deg): @@ -298,6 +314,15 @@ def _rot2d(angle_deg): # x1 = CC / sqrt(W1) xySND = hstack((x0, x1)) weights = hstack((p, w1)) + case 'Classical Frolov' | 'Improved Frolov' | 'Fibonacci Frolov': + method = smethod.replace(' ', '') + method = 'Fibonacci' if method == 'FibonacciFrolov' else method + if L > 0: + samples = dgsf.sample_gaussian_fibonacci(μ.flatten(), C, L, method) + else: + samples = np.empty((L, 2)) + xyG = samples.T + case _: raise Exception("Wrong smethod") match tmethod: @@ -305,10 +330,12 @@ def _rot2d(angle_deg): xyG = matmul(cholesky(C), xySND) + μ case 'Eigendecomposition': xyG = matmul(C_R, sqrt(C_D) * xySND) + μ + case None: + pass case _: raise Exception("Wrong smethod") # Sample weights to scatter sizes - L2 = xySND.shape[1] # actual number of saamples + L2 = xySND.shape[1] if tmethod is not None else xyG.shape[1] # actual number of saamples if L2 == 0: sizes = 10 else: diff --git a/poetry.lock b/poetry.lock index c6aaec9..c866be1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -746,7 +746,7 @@ vtk = "*" [[package]] name = "deterministic-gaussian-sampling-fibonacci" -version = "0.2.0" +version = "0.2.1" description = "Deterministic sampling via orthogonal inverse transform of low-discrepancy Fibonacci grids." optional = false python-versions = ">=3.10" @@ -763,7 +763,7 @@ scipy = {version = ">=1.15,<1.16", markers = "python_version < \"3.11\""} type = "git" url = "https://github.com/KIT-ISAS/deterministic_gaussian_sampling_fibonacci" reference = "HEAD" -resolved_reference = "93034064d8f9d016a02e2fa8ea955f7a2a2b6777" +resolved_reference = "6d6e940a5eb4753835a43ff338bac5a8e9be8792" [[package]] name = "dill" From 5491ec61733e28e9403e3ffd38b3ce908870622b Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 13 Jan 2026 17:37:28 +0100 Subject: [PATCH 137/152] bump library version --- poetry.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index c866be1..ed8979e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -746,7 +746,7 @@ vtk = "*" [[package]] name = "deterministic-gaussian-sampling-fibonacci" -version = "0.2.1" +version = "0.2.2" description = "Deterministic sampling via orthogonal inverse transform of low-discrepancy Fibonacci grids." optional = false python-versions = ">=3.10" @@ -763,7 +763,7 @@ scipy = {version = ">=1.15,<1.16", markers = "python_version < \"3.11\""} type = "git" url = "https://github.com/KIT-ISAS/deterministic_gaussian_sampling_fibonacci" reference = "HEAD" -resolved_reference = "6d6e940a5eb4753835a43ff338bac5a8e9be8792" +resolved_reference = "4d06e05460e108f19dd2f3eea37f61001cfdb8b3" [[package]] name = "dill" From 628976fed0e390567ff987d7b8bbec003bfa341b Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 14 Jan 2026 16:51:09 +0100 Subject: [PATCH 138/152] update lib --- poetry.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index ed8979e..a3442ed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -746,7 +746,7 @@ vtk = "*" [[package]] name = "deterministic-gaussian-sampling-fibonacci" -version = "0.2.2" +version = "0.2.3" description = "Deterministic sampling via orthogonal inverse transform of low-discrepancy Fibonacci grids." optional = false python-versions = ">=3.10" @@ -763,7 +763,7 @@ scipy = {version = ">=1.15,<1.16", markers = "python_version < \"3.11\""} type = "git" url = "https://github.com/KIT-ISAS/deterministic_gaussian_sampling_fibonacci" reference = "HEAD" -resolved_reference = "4d06e05460e108f19dd2f3eea37f61001cfdb8b3" +resolved_reference = "3b107de4fb3a7a6fe34a023899c10d7be4077c87" [[package]] name = "dill" From 0dedf27fb1d57c29343f1e7b3825554d5a53171f Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 14 Jan 2026 16:51:23 +0100 Subject: [PATCH 139/152] add cholesky/eigen switch back to frolov samples --- model/distributions/gaus2d/gaus2d.py | 31 ++++++++++++---------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/model/distributions/gaus2d/gaus2d.py b/model/distributions/gaus2d/gaus2d.py index 7899600..517e6cd 100644 --- a/model/distributions/gaus2d/gaus2d.py +++ b/model/distributions/gaus2d/gaus2d.py @@ -175,7 +175,6 @@ def _register_callbacks(self): Output('gauss2D-p', 'step'), Output('gauss2D-p', 'tooltip'), Output('gauss2D-L', 'disabled'), - Output('gauss2D-tmethod', 'className'), Output('gauss2D-p', 'className'), # to hide when not needed Input("gauss2D-smethod", "value"), ) @@ -186,28 +185,28 @@ def update_smethod(smethod): case 'iid': patched_tooltip.template = "dice" # min, max, value, step, tooltip - return 0, 1, .5, 0.001, patched_tooltip, False, 'visible', 'visible' + return 0, 1, .5, 0.001, patched_tooltip, False, 'visible' case 'Fibonacci': patched_tooltip.template = "z={value}" - return -50, 50, 0, 1, patched_tooltip, False, 'visible', 'visible' + return -50, 50, 0, 1, patched_tooltip, False, 'visible' case 'LCD': patched_tooltip.template = "α={value}°" - return -360, 360, 0, 0.1, patched_tooltip, False, 'visible', 'visible' + return -360, 360, 0, 0.1, patched_tooltip, False, 'visible' case 'SP-Julier04': patched_tooltip.template = "W₀={value}" - return -2, 1, .1, 0.001, patched_tooltip, True, 'visible', 'visible' + return -2, 1, .1, 0.001, patched_tooltip, True, 'visible' case 'SP-Menegaz11': patched_tooltip.template = "Wₙ₊₁={value}" - return 0, 1, 1/3, 0.001, patched_tooltip, True, 'visible', 'visible' + return 0, 1, 1/3, 0.001, patched_tooltip, True, 'visible' case 'Classical Frolov': patched_tooltip.always_visible = False - return no_update, no_update, no_update, no_update, patched_tooltip, False, 'invisible', 'invisible' + return no_update, no_update, no_update, no_update, patched_tooltip, False, 'invisible' case 'Improved Frolov': patched_tooltip.always_visible = False - return no_update, no_update, no_update, no_update, patched_tooltip, False, 'invisible', 'invisible' + return no_update, no_update, no_update, no_update, patched_tooltip, False, 'invisible' case 'Fibonacci Frolov': patched_tooltip.always_visible = False - return no_update, no_update, no_update, no_update, patched_tooltip, False, 'invisible', 'invisible' + return no_update, no_update, no_update, no_update, patched_tooltip, False, 'invisible' case _: raise Exception("Wrong smethod") @@ -228,8 +227,6 @@ def update_smethod(smethod): Input("angle", "value"), ) def update(smethod, tmethod, p, L0, sigma_x, sigma_y, rho, angle): - if smethod in ['Classical Frolov', 'Improved Frolov', 'Fibonacci Frolov']: - tmethod = None trig = ctx.triggered_id def _rot2d(angle_deg): @@ -318,11 +315,11 @@ def _rot2d(angle_deg): method = smethod.replace(' ', '') method = 'Fibonacci' if method == 'FibonacciFrolov' else method if L > 0: - samples = dgsf.sample_gaussian_fibonacci(μ.flatten(), C, L, method) + grid = dgsf.get_uniform_grid(2, L, method) else: - samples = np.empty((L, 2)) - xyG = samples.T - + grid = np.empty((L, 2)) + xyUni = grid.T + xySND = sqrt(2)*erfinv(2*xyUni-1) case _: raise Exception("Wrong smethod") match tmethod: @@ -330,12 +327,10 @@ def _rot2d(angle_deg): xyG = matmul(cholesky(C), xySND) + μ case 'Eigendecomposition': xyG = matmul(C_R, sqrt(C_D) * xySND) + μ - case None: - pass case _: raise Exception("Wrong smethod") # Sample weights to scatter sizes - L2 = xySND.shape[1] if tmethod is not None else xyG.shape[1] # actual number of saamples + L2 = xySND.shape[1] # actual number of saamples if L2 == 0: sizes = 10 else: From 8c37a080018eb75d583384b970e1a9013501a85f Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 14 Jan 2026 17:02:41 +0100 Subject: [PATCH 140/152] dont do covariance correction on frolov --- .../partially_wraped_normal/fibonacci_frolov.py | 2 +- .../cylinder/partially_wraped_normal/frolov.py | 2 +- .../partially_wraped_normal/improved_frolov.py | 2 +- .../torus/wrapped_normal/fibonacci_frolov.py | 2 +- model/distributions/torus/wrapped_normal/frolov.py | 2 +- .../torus/wrapped_normal/improved_frolov.py | 2 +- util/gaus_util.py | 10 +++++++++- 7 files changed, 15 insertions(+), 7 deletions(-) diff --git a/model/distributions/cylinder/partially_wraped_normal/fibonacci_frolov.py b/model/distributions/cylinder/partially_wraped_normal/fibonacci_frolov.py index 6335fd4..42e6550 100644 --- a/model/distributions/cylinder/partially_wraped_normal/fibonacci_frolov.py +++ b/model/distributions/cylinder/partially_wraped_normal/fibonacci_frolov.py @@ -35,7 +35,7 @@ def sample(self, sample_options, distribution_options): [correlation * sigma_t * sigma_p, sigma_p**2] ]) - gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "Fibonacci") + gaus_grid = gu.sample_frolov_gaussian(mu, Cov, sample_count, "Fibonacci") # wrapp gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) diff --git a/model/distributions/cylinder/partially_wraped_normal/frolov.py b/model/distributions/cylinder/partially_wraped_normal/frolov.py index 507c449..3b6addd 100644 --- a/model/distributions/cylinder/partially_wraped_normal/frolov.py +++ b/model/distributions/cylinder/partially_wraped_normal/frolov.py @@ -35,7 +35,7 @@ def sample(self, sample_options, distribution_options): [correlation * sigma_t * sigma_p, sigma_p**2] ]) - gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "ClassicalFrolov") + gaus_grid = gu.sample_frolov_gaussian(mu, Cov, sample_count, "ClassicalFrolov") # wrapp gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) diff --git a/model/distributions/cylinder/partially_wraped_normal/improved_frolov.py b/model/distributions/cylinder/partially_wraped_normal/improved_frolov.py index 8dae74e..e2e15e6 100644 --- a/model/distributions/cylinder/partially_wraped_normal/improved_frolov.py +++ b/model/distributions/cylinder/partially_wraped_normal/improved_frolov.py @@ -35,7 +35,7 @@ def sample(self, sample_options, distribution_options): [correlation * sigma_t * sigma_p, sigma_p**2] ]) - gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "ImprovedFrolov") + gaus_grid = gu.sample_frolov_gaussian(mu, Cov, sample_count, "ImprovedFrolov") # wrapp gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) diff --git a/model/distributions/torus/wrapped_normal/fibonacci_frolov.py b/model/distributions/torus/wrapped_normal/fibonacci_frolov.py index c1b8b2e..78112d6 100644 --- a/model/distributions/torus/wrapped_normal/fibonacci_frolov.py +++ b/model/distributions/torus/wrapped_normal/fibonacci_frolov.py @@ -35,7 +35,7 @@ def sample(self, sample_options, distribution_options): [correlation * sigma_t * sigma_p, sigma_p**2] ]) - gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "Fibonacci") + gaus_grid = gu.sample_frolov_gaussian(mu, Cov, sample_count, "Fibonacci") # wrapp gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) diff --git a/model/distributions/torus/wrapped_normal/frolov.py b/model/distributions/torus/wrapped_normal/frolov.py index f17a271..524309f 100644 --- a/model/distributions/torus/wrapped_normal/frolov.py +++ b/model/distributions/torus/wrapped_normal/frolov.py @@ -35,7 +35,7 @@ def sample(self, sample_options, distribution_options): [correlation * sigma_t * sigma_p, sigma_p**2] ]) - gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "ClassicalFrolov") + gaus_grid = gu.sample_frolov_gaussian(mu, Cov, sample_count, "ClassicalFrolov") # wrapp gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) diff --git a/model/distributions/torus/wrapped_normal/improved_frolov.py b/model/distributions/torus/wrapped_normal/improved_frolov.py index a07118d..a9cf852 100644 --- a/model/distributions/torus/wrapped_normal/improved_frolov.py +++ b/model/distributions/torus/wrapped_normal/improved_frolov.py @@ -35,7 +35,7 @@ def sample(self, sample_options, distribution_options): [correlation * sigma_t * sigma_p, sigma_p**2] ]) - gaus_grid = sample_gaussian_fibonacci(mu, Cov, sample_count, "ImprovedFrolov") + gaus_grid = gu.sample_frolov_gaussian(mu, Cov, sample_count, "ImprovedFrolov") # wrapp gaus_grid[:,0] = gaus_grid[:,0] % (2 * np.pi) diff --git a/util/gaus_util.py b/util/gaus_util.py index 91464c9..62c54aa 100644 --- a/util/gaus_util.py +++ b/util/gaus_util.py @@ -1,5 +1,7 @@ import numpy as np from scipy.stats import norm +from deterministic_gaussian_sampling_fibonacci import get_uniform_grid + class GausUtil: @staticmethod @@ -27,4 +29,10 @@ def transform_grid_gaussian(grid, mu, cov): gaus[:,0] += mu[0] gaus[:,1] += mu[1] - return gaus \ No newline at end of file + return gaus + + @staticmethod + def sample_frolov_gaussian(mu, cov, sample_count, variant="ClassicalFrolov"): + grid = get_uniform_grid(2, sample_count, variant) + gaus_grid = GausUtil.transform_grid_gaussian(grid, mu, cov) + return gaus_grid \ No newline at end of file From 0a97ab8fc694ab81ccb5e278a2c5e219d0967a0b Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 15 Jan 2026 13:10:20 +0100 Subject: [PATCH 141/152] fixed manual input on multi-worker systems --- renderer/object_3D_renderer.py | 37 +++++++++++++++++----------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/renderer/object_3D_renderer.py b/renderer/object_3D_renderer.py index a3b11aa..c5b7bb2 100644 --- a/renderer/object_3D_renderer.py +++ b/renderer/object_3D_renderer.py @@ -156,23 +156,22 @@ def update_curr_distribution(selected_distribution, selected_sampling): prevent_initial_call=True, ) def manual_input_changed(val, val_silder, selected_distribution, selected_sampling): - source = dash.ctx.triggered_id["type"] + triggered_id = dash.ctx.triggered_id + source = triggered_id["type"] if val is None and source == "manual_input-sampling": return no_update, no_update sampling_options = self.object.distributions[selected_distribution].sampling_method_dict[selected_sampling].sample_options - # find the wrapper that called this - wrapper = None - for opt in sampling_options: - id = getattr(opt, "id", None) - if id is not None and id == dash.ctx.triggered_id["index"]: - wrapper = opt - break - - if wrapper is None: + try: + index = int(triggered_id["index"]) + except (TypeError, ValueError): + return no_update, no_update + if index < 0 or index >= len(sampling_options): return no_update, no_update + + wrapper = sampling_options[index] if source == "manual_input-sampling": # manual input changed, update slider @@ -195,23 +194,23 @@ def manual_input_changed(val, val_silder, selected_distribution, selected_sampli prevent_initial_call=True, ) def manual_input_dist_changed(val_manual, val_slider, selected_distribution, selected_sampling): - source = dash.ctx.triggered_id["type"] + triggered_id = dash.ctx.triggered_id + source = triggered_id["type"] if val_manual is None and source == "manual_input-dist": return no_update, no_update dist_options = self.object.distributions[selected_distribution].distribution_options - wrapper = None - for opt in dist_options: - opt_id = getattr(opt, "id", None) - if opt_id is not None and opt_id == dash.ctx.triggered_id["index"]: - wrapper = opt - break - - if wrapper is None: + try: + index = int(triggered_id["index"]) + except (TypeError, ValueError): + return no_update, no_update + if index < 0 or index >= len(dist_options): return no_update, no_update + wrapper = dist_options[index] + if source == "manual_input-dist": # check_input is given by distribution/ sampling method, if None, no special constraints are given # slider.is_valid is given by the slider itself, can be less strict From 1182d1275648f501d8be67c1b78eeeda43b43f4e Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 21 Jan 2026 16:59:04 +0100 Subject: [PATCH 142/152] added fibonacci-rank-1 to watson --- .../distributions/sphere/watson/fibonachi.py | 2 +- .../sphere/watson/fibonachi_rank1.py | 58 +++++++++++++++++++ model/distributions/sphere/watson/watson.py | 2 + 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 model/distributions/sphere/watson/fibonachi_rank1.py diff --git a/model/distributions/sphere/watson/fibonachi.py b/model/distributions/sphere/watson/fibonachi.py index 22a9a8f..30895bd 100644 --- a/model/distributions/sphere/watson/fibonachi.py +++ b/model/distributions/sphere/watson/fibonachi.py @@ -22,7 +22,7 @@ def __init__(self): ] def get_name(self): - return "Fibonacci Lattice" + return "Fibonacci-Kronecker Lattice" def sample(self, sample_options, distribution_options): diff --git a/model/distributions/sphere/watson/fibonachi_rank1.py b/model/distributions/sphere/watson/fibonachi_rank1.py new file mode 100644 index 0000000..0018bf7 --- /dev/null +++ b/model/distributions/sphere/watson/fibonachi_rank1.py @@ -0,0 +1,58 @@ +from abc import ABC, abstractmethod +from matplotlib.pyplot import grid +import numpy as np +import scipy +import scipy.integrate +import scipy.interpolate +import sphstat +from pyrecest.backend import array +from pyrecest.distributions import WatsonDistribution as WatsonDistributionPyrecest +from scipy.special import erf, erfi, erfinv + +from model.distributions.cylinder.uniform.fibonacci_rank_1 import CylinderFibRank1UniformSampling +from model.distributions.sphere.sphere_sampling_schema import SphereSamplingSchema +from util.selectors.slider_fib import SliderFib +from model.sphere.sphere import Sphere +from util.selectors.silder_manual_input_wrapper import SliderManualInputWrapper as MI +from model.distributions.sphere.watson.fibonachi import WatsonFibonachiSampling as wf + + + +class WatsonFibonachiRank1Sampling(SphereSamplingSchema): + def __init__(self): + self.sample_options = [ + MI(SliderFib("Number of Samples", 3, 33, 21, 9, minus_1=True)), + ] + self.sampler = CylinderFibRank1UniformSampling() + + + def get_name(self): + return "Fibonacci-Rank-1 Lattice" + + + def sample(self, sample_options, distribution_options): + + sample_count = sample_options[0].state + 1 # because minus_1 is true slider displays fib(n)-1 + k = distribution_options[0].state # kappa + + x, y = self.sampler.get_rank_1(sample_count, sample_options[0].idx, without_first_point=True) + + x = 2*x -1 # map x from [0,1] to [-1, 1] + phi = 2 * np.pi * y # azimuthal angle, [0, 2pi] uniform + + if k > 0: + w = 1 / (np.sqrt(k)) * wf.erfi_inv( x * erfi(np.sqrt(k)) ) + elif k < 0: + la = -k + w = 1 / (np.sqrt(la)) * erfinv( x * erf(np.sqrt(la)) ) + elif k == 0: + w = x + + + w = np.clip(w, -1.0, 1.0) # clamp to avoid sqrt warnings due to numerical issues + + x_i_f_0 = w + x_i_f_1 = np.sqrt(1-w**2) * np.cos( phi) + x_i_f_2 = np.sqrt(1-w**2) * np.sin( phi) + x_i_f = np.column_stack((x_i_f_1, x_i_f_2, x_i_f_0)) # order so that mu=[0, 0, 1] + return x_i_f \ No newline at end of file diff --git a/model/distributions/sphere/watson/watson.py b/model/distributions/sphere/watson/watson.py index 45e5314..a525c71 100644 --- a/model/distributions/sphere/watson/watson.py +++ b/model/distributions/sphere/watson/watson.py @@ -10,6 +10,7 @@ from model.distributions.sphere.watson.random_sampling import WatsonRandomSampling from model.distributions.sphere.watson.fibonachi import WatsonFibonachiSampling from model.distributions.sphere.watson.cartesian import WatsonCartesianSampling +from model.distributions.sphere.watson.fibonachi_rank1 import WatsonFibonachiRank1Sampling class WatsonDistribution(SphereDistribution): @@ -22,6 +23,7 @@ def __init__(self): WatsonRandomSampling(), WatsonFibonachiSampling(), WatsonCartesianSampling(), + WatsonFibonachiRank1Sampling(), ] def get_name(self): From d331d7e86d284d03485fe856716fa8db9234d9a0 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 22 Jan 2026 09:36:14 +0100 Subject: [PATCH 143/152] benchmark: changed how plotting works --- .../sphere/watson/benchmark_fib_starts.py | 101 ++++++++++++++---- 1 file changed, 80 insertions(+), 21 deletions(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index cdc8f9a..392f5ab 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -5,6 +5,9 @@ PYTHONPATH=$PWD poetry run python model/distributions/sphere/watson/benchmark_fib_starts.py ''' +import json +from pathlib import Path +import plotly.express as px from model.distributions.sphere.watson.fibonachi import WatsonFibonachiSampling from util.selectors.slider_float import FloatSlider import pyperf @@ -30,7 +33,7 @@ def bench_single_kappa(kappa, sample_count, id): results = {} for method_name, method in methods.items(): bench_name = f"Watson Fibonacci Sampling: {method_name} (kappa={kappa}) [{id}]" - benchmark = runner.bench_func(bench_name, benchmark_kappa, method, kappa, sample_count, 5) + benchmark = runner.bench_func(bench_name, benchmark_kappa, method, kappa, sample_count, 1) results[method_name] = benchmark return results @@ -68,29 +71,86 @@ def bench_multiple_sample_counts_log(kappa): return all_results -def plot_benches(results, title, filename, x_label, log_x=False, log_y=False): - import plotly.express as px - try: - if x_label == "sample_count": - rows = [dict(name=n, sample_count=k, time=t.mean()) for n, pts in results.items() for k, t in pts] - else: - rows = [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] - fig = px.line( - rows, - x=x_label, - y="time", - color="name", - markers=True, - title=title, - log_x=log_x, - log_y=log_y, +def _sanitize_filename(name): + return name.replace(" ", "_").replace(":", "") + +def _rows_from_results(results, x_label): + if x_label == "sample_count": + return [dict(name=n, sample_count=k, time=t.mean()) for n, pts in results.items() for k, t in pts] + if x_label == "kappa": + return [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] + raise ValueError(f"Unsupported x_label: {x_label}") + +def _plot_rows(rows, title, filename, x_label, log_x=False, log_y=False): + fig = px.line( + rows, + x=x_label, + y="time", + color="name", + markers=True, + log_x=log_x, + log_y=log_y, + ) + fig.update_layout( + legend=dict( + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="left", + x=0, ) - fig.write_image(f"{filename.replace(' ', '_').replace(':', '')}.svg") + ) + try: + fig.write_image(f"{_sanitize_filename(filename)}.svg") except Exception as e: print("Generating plot failed, dumping data:", e) - print(results.items()) + print(rows) print("Trying to save html as fallback") - fig.write_html(f"{title.replace(' ', '_').replace(':', '')}.html", include_plotlyjs="cdn", full_html=True) + fig.write_html(f"{_sanitize_filename(title)}.html", include_plotlyjs="cdn", full_html=True) + +def plot_benches(results, title=None, filename=None, x_label=None, log_x=None, log_y=None, json_filename=None): + + if isinstance(results, (str, Path)): + json_path = Path(results) + with json_path.open("r", encoding="utf-8") as handle: + payload = json.load(handle) + rows = payload["rows"] + if title is None: + title = payload.get("title", json_path.stem) + if filename is None: + filename = payload.get("filename", json_path.stem) + if x_label is None: + x_label = payload.get("x_label") + if log_x is None: + log_x = payload.get("log_x", False) + if log_y is None: + log_y = payload.get("log_y", False) + if x_label is None: + raise ValueError("x_label is required when replotting from JSON") + _plot_rows(rows, title, filename, x_label, log_x=log_x, log_y=log_y) + return + + if title is None or filename is None or x_label is None: + raise ValueError("title, filename, and x_label are required for raw benchmark data") + if log_x is None: + log_x = False + if log_y is None: + log_y = False + + rows = _rows_from_results(results, x_label) + payload = { + "title": title, + "filename": filename, + "x_label": x_label, + "log_x": log_x, + "log_y": log_y, + "rows": rows, + } + if json_filename is None: + json_filename = f"{_sanitize_filename(filename)}.json" + with open(json_filename, "w", encoding="utf-8") as handle: + json.dump(payload, handle, indent=2, sort_keys=True) + _plot_rows(rows, title, filename, x_label, log_x=log_x, log_y=log_y) @@ -114,4 +174,3 @@ def plot_benches(results, title, filename, x_label, log_x=False, log_y=False): #plot_benches(mult_samples_neg_10, "time taken for various sample counts (kappa=-10)", "time taken for various sample counts (kappa=-10)", "sample_count") plot_benches(log_mult_samples_neg_10, "time taken for various sample counts (kappa=-10)", "time taken for various sample counts log scale (kappa=-10)", "sample_count", log_x=True, log_y=True) - From 53dde86548939200f18951975de99233ff377734 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 22 Jan 2026 14:22:21 +0100 Subject: [PATCH 144/152] fix bug where watson_random returns 2L samples instead of L --- .../sphere/watson/random_sampling.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/model/distributions/sphere/watson/random_sampling.py b/model/distributions/sphere/watson/random_sampling.py index 8028f4f..3155e86 100644 --- a/model/distributions/sphere/watson/random_sampling.py +++ b/model/distributions/sphere/watson/random_sampling.py @@ -34,6 +34,17 @@ def sample(self, sample_options, distribution_options): lamb, mu, nu = Sphere.spherical_to_cartesian(theta, phi) samples = sphstat.distributions.watson(numsamp, lamb, mu, nu, kappa)["points"] - samples_array = np.vstack(samples) - - return samples_array \ No newline at end of file + if numsamp > 1: + m = len(samples) + n = m // 2 + assert numsamp == n, "Unexpected number of samples generated" + rng = np.random.default_rng(None) + pts = np.vstack(samples) # shape (2numsamp,3) + A = pts[:n] + B = pts[n:] + choose_B = rng.random(n) < 0.5 + out = np.where(choose_B[:, None], B, A) + return out + else: + samples_array = np.vstack(samples) + return samples_array \ No newline at end of file From becafb57d10c98545ab569f434dfbd6da581d609 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Sun, 25 Jan 2026 14:24:52 +0100 Subject: [PATCH 145/152] fixed json serialization --- .../sphere/watson/benchmark_fib_starts.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index 392f5ab..bdc278e 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -75,10 +75,25 @@ def _sanitize_filename(name): return name.replace(" ", "_").replace(":", "") def _rows_from_results(results, x_label): + def _to_builtin(value): + if isinstance(value, np.integer): + return int(value) + if isinstance(value, np.floating): + return float(value) + return value + if x_label == "sample_count": - return [dict(name=n, sample_count=k, time=t.mean()) for n, pts in results.items() for k, t in pts] + return [ + dict(name=n, sample_count=_to_builtin(k), time=_to_builtin(t.mean())) + for n, pts in results.items() + for k, t in pts + ] if x_label == "kappa": - return [dict(name=n, kappa=k, time=t.mean()) for n, pts in results.items() for k, t in pts] + return [ + dict(name=n, kappa=_to_builtin(k), time=_to_builtin(t.mean())) + for n, pts in results.items() + for k, t in pts + ] raise ValueError(f"Unsupported x_label: {x_label}") def _plot_rows(rows, title, filename, x_label, log_x=False, log_y=False): From 4d6d70490570acc755b7fd63bac2b23d12939342 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 27 Jan 2026 10:44:28 +0100 Subject: [PATCH 146/152] benchmark: added way to plot from json directly --- .gitignore | 2 ++ .../sphere/watson/benchmark_fib_starts.py | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/.gitignore b/.gitignore index cb60acb..454bfcd 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,5 @@ cython_debug/ #.kate-swp .vscode/ +time_taken_for_various* + diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index bdc278e..ee6e17b 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -6,6 +6,7 @@ ''' import json +import os from pathlib import Path import plotly.express as px from model.distributions.sphere.watson.fibonachi import WatsonFibonachiSampling @@ -115,6 +116,11 @@ def _plot_rows(rows, title, filename, x_label, log_x=False, log_y=False): x=0, ) ) + if log_x: + fig.update_xaxes(dtick=1) + if log_y: + fig.update_yaxes(dtick=1) + try: fig.write_image(f"{_sanitize_filename(filename)}.svg") except Exception as e: @@ -170,6 +176,21 @@ def plot_benches(results, title=None, filename=None, x_label=None, log_x=None, l if __name__ == "__main__": + plot_from_json = os.getenv("PLOT_FROM_JSON") + if plot_from_json: + if plot_from_json != "1": + json_paths = [p.strip() for p in plot_from_json.split(",") if p.strip()] + else: + json_paths = [ + f"{_sanitize_filename('time taken for various kappa values (10000 samples)')}.json", + f"{_sanitize_filename('time taken for various kappa values log scale (10000 samples)')}.json", + f"{_sanitize_filename('time taken for various sample counts log scale (kappa=10)')}.json", + f"{_sanitize_filename('time taken for various sample counts log scale (kappa=-10)')}.json", + ] + for json_path in json_paths: + plot_benches(json_path) + raise SystemExit(0) + runner = pyperf.Runner() From 26bfbebd10a0cc7315bb7f55cf1aae033b06f30a Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 27 Jan 2026 10:55:29 +0100 Subject: [PATCH 147/152] changed how plots look --- .../distributions/sphere/watson/benchmark_fib_starts.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index ee6e17b..f1d3731 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -3,6 +3,10 @@ sudo "$(poetry run which python)" -m pyperf system tune PYTHONPATH=$PWD poetry run python model/distributions/sphere/watson/benchmark_fib_starts.py + + +you can also plot from existing JSON files by setting the environment variable: +PLOT_FROM_JSON=1 PYTHONPATH=$PWD poetry run python model/distributions/sphere/watson/benchmark_fib_starts.py ''' import json @@ -106,6 +110,7 @@ def _plot_rows(rows, title, filename, x_label, log_x=False, log_y=False): markers=True, log_x=log_x, log_y=log_y, + labels={"time": "time in s"}, ) fig.update_layout( legend=dict( @@ -114,12 +119,14 @@ def _plot_rows(rows, title, filename, x_label, log_x=False, log_y=False): y=1.02, xanchor="left", x=0, - ) + ), + font=dict(size=26), ) if log_x: fig.update_xaxes(dtick=1) if log_y: fig.update_yaxes(dtick=1) + fig.update_layout(legend_title_text="") try: fig.write_image(f"{_sanitize_filename(filename)}.svg") From a185083e8336864bf753d8371db381845243178c Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Tue, 27 Jan 2026 11:26:10 +0100 Subject: [PATCH 148/152] minor change to benchmark plots --- model/distributions/sphere/watson/benchmark_fib_starts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/model/distributions/sphere/watson/benchmark_fib_starts.py b/model/distributions/sphere/watson/benchmark_fib_starts.py index f1d3731..c815664 100644 --- a/model/distributions/sphere/watson/benchmark_fib_starts.py +++ b/model/distributions/sphere/watson/benchmark_fib_starts.py @@ -110,7 +110,7 @@ def _plot_rows(rows, title, filename, x_label, log_x=False, log_y=False): markers=True, log_x=log_x, log_y=log_y, - labels={"time": "time in s"}, + labels={"time": "time in s", "sample_count": "sample count"}, ) fig.update_layout( legend=dict( From 62f0ef737c59caff76a124595d4e88f7691d80f3 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 5 Feb 2026 10:58:39 +0100 Subject: [PATCH 149/152] update libs --- poetry.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/poetry.lock b/poetry.lock index a3442ed..e8e56dd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. [[package]] name = "astropy" @@ -746,7 +746,7 @@ vtk = "*" [[package]] name = "deterministic-gaussian-sampling-fibonacci" -version = "0.2.3" +version = "0.2.4" description = "Deterministic sampling via orthogonal inverse transform of low-discrepancy Fibonacci grids." optional = false python-versions = ">=3.10" @@ -763,7 +763,7 @@ scipy = {version = ">=1.15,<1.16", markers = "python_version < \"3.11\""} type = "git" url = "https://github.com/KIT-ISAS/deterministic_gaussian_sampling_fibonacci" reference = "HEAD" -resolved_reference = "3b107de4fb3a7a6fe34a023899c10d7be4077c87" +resolved_reference = "fa97bc2793e87424fe56e208a5932a6cd6ec8ea2" [[package]] name = "dill" @@ -3349,4 +3349,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<3.11" -content-hash = "6e066e4b072266b8d1efca875200d7c4e1425e2ce8974aed8f835ba2f6b54c16" +content-hash = "527df857cfe567834036aece62eb46ae0d47b15233b2ff08faf1ee45523ab7d8" From 0d9aa95f9a52423f0b0f1d08e464f9a70d26d64c Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Thu, 5 Feb 2026 11:22:15 +0100 Subject: [PATCH 150/152] pin poetry version in docker --- dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dockerfile b/dockerfile index 18dda7b..add5fb8 100644 --- a/dockerfile +++ b/dockerfile @@ -2,7 +2,8 @@ FROM python:3.10.12 AS base WORKDIR /code -RUN pip install --no-cache-dir poetry +ARG POETRY_VERSION=2.3.1 +RUN pip install --no-cache-dir "poetry==${POETRY_VERSION}" COPY pyproject.toml poetry.lock /code/ RUN poetry sync --no-root --without dev From abcf0f5f9c53dffddf5037cfacbdbbb8e91e0c30 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 25 Feb 2026 16:49:10 +0100 Subject: [PATCH 151/152] update requirements.txt (the use of poetry is recommended, requirements.txt only as alternative) --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 712837b..d9eda04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,11 +16,13 @@ dash-resizable-panels==0.1.0 ; python_version == "3.10" dash-table==5.0.0 ; python_version == "3.10" dash-vtk==0.0.9 ; python_version == "3.10" dash==2.18.2 ; python_version == "3.10" +deterministic-gaussian-sampling-fibonacci @ git+https://github.com/KIT-ISAS/deterministic_gaussian_sampling_fibonacci@fa97bc2793e87424fe56e208a5932a6cd6ec8ea2 ; python_version == "3.10" et-xmlfile==2.0.0 ; python_version == "3.10" filterpy==1.4.5 ; python_version == "3.10" flask==3.0.3 ; python_version == "3.10" fonttools==4.60.1 ; python_version == "3.10" gunicorn==23.0.0 ; python_version == "3.10" +h5py==3.15.1 ; python_version == "3.10" idna==3.11 ; python_version == "3.10" importlib-metadata==8.7.0 ; python_version == "3.10" itsdangerous==2.2.0 ; python_version == "3.10" @@ -33,7 +35,7 @@ mpmath==1.3.0 ; python_version == "3.10" narwhals==2.10.2 ; python_version == "3.10" nest-asyncio==1.6.0 ; python_version == "3.10" numpy-quaternion==2024.0.12 ; python_version == "3.10" -numpy==1.26.4 ; python_version == "3.10" +numpy==2.2.6 ; python_version == "3.10" openpyxl==3.1.5 ; python_version == "3.10" packaging==25.0 ; python_version == "3.10" pandas==2.3.3 ; python_version == "3.10" From 0f994b2c73767882473958662d7172aed36d4400 Mon Sep 17 00:00:00 2001 From: Vlad Korsakov Date: Wed, 25 Feb 2026 16:54:30 +0100 Subject: [PATCH 152/152] also test main and staging --- .github/workflows/test_only.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_only.yml b/.github/workflows/test_only.yml index b1d32b5..d1f9c57 100644 --- a/.github/workflows/test_only.yml +++ b/.github/workflows/test_only.yml @@ -3,6 +3,8 @@ on: push: branches: - vlad/devel + - main + - staging jobs: test: runs-on: ubuntu-latest