fluxplot 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. fluxplot/__init__.py +115 -0
  2. fluxplot/_fieldmap.py +97 -0
  3. fluxplot/_mesh_reduce.py +54 -0
  4. fluxplot/_scene3d_size.py +95 -0
  5. fluxplot/_viewer/THIRD-PARTY.txt +23 -0
  6. fluxplot/_viewer/flux-model3d-viewer.min.js +4221 -0
  7. fluxplot/_viewer/stamp.json +4 -0
  8. fluxplot/api.py +1196 -0
  9. fluxplot/autotag.py +164 -0
  10. fluxplot/base.mplstyle +0 -0
  11. fluxplot/brackets.py +242 -0
  12. fluxplot/canonical_json.py +23 -0
  13. fluxplot/capture.py +150 -0
  14. fluxplot/colorcheck.py +285 -0
  15. fluxplot/colors.py +727 -0
  16. fluxplot/colorscale.py +477 -0
  17. fluxplot/data.py +178 -0
  18. fluxplot/definitions/colormaps.json +1639 -0
  19. fluxplot/definitions/flexoki.tokens.json +2571 -0
  20. fluxplot/definitions/palettes.json +2547 -0
  21. fluxplot/descriptors.py +87 -0
  22. fluxplot/fields.py +611 -0
  23. fluxplot/fits.py +240 -0
  24. fluxplot/glb.py +84 -0
  25. fluxplot/ids.py +173 -0
  26. fluxplot/images.py +362 -0
  27. fluxplot/integrity.py +27 -0
  28. fluxplot/manifest.py +788 -0
  29. fluxplot/mesh3d.py +376 -0
  30. fluxplot/panels.py +284 -0
  31. fluxplot/postprocess.py +638 -0
  32. fluxplot/presets.py +66 -0
  33. fluxplot/provenance.py +177 -0
  34. fluxplot/raster.py +295 -0
  35. fluxplot/recipe.py +178 -0
  36. fluxplot/render.py +66 -0
  37. fluxplot/roles.py +147 -0
  38. fluxplot/scene3d.py +386 -0
  39. fluxplot/scene3d_manifest.py +112 -0
  40. fluxplot/scene3d_viewer.py +633 -0
  41. fluxplot/schemas/.gitkeep +0 -0
  42. fluxplot/schemas/manifest.schema.json +2479 -0
  43. fluxplot/schemas/recipe.schema.json +179 -0
  44. fluxplot/schemas/scene3d.schema.json +461 -0
  45. fluxplot/seaborn_adapters.py +323 -0
  46. fluxplot/signature_fluxplots/__init__.py +18 -0
  47. fluxplot/signature_fluxplots/_colour.py +412 -0
  48. fluxplot/signature_fluxplots/fluxbox.py +433 -0
  49. fluxplot/signature_fluxplots/glowbar.py +769 -0
  50. fluxplot/signature_fluxplots/hexmatrix.py +927 -0
  51. fluxplot/stats/__init__.py +63 -0
  52. fluxplot/stats/_common.py +196 -0
  53. fluxplot/stats/multi_group.py +443 -0
  54. fluxplot/stats/paired.py +209 -0
  55. fluxplot/stats/two_group.py +149 -0
  56. fluxplot/style.py +469 -0
  57. fluxplot/surface.py +487 -0
  58. fluxplot/surface3d.py +197 -0
  59. fluxplot/tagger.py +561 -0
  60. fluxplot/version.py +19 -0
  61. fluxplot-0.1.0.dist-info/METADATA +1199 -0
  62. fluxplot-0.1.0.dist-info/RECORD +65 -0
  63. fluxplot-0.1.0.dist-info/WHEEL +4 -0
  64. fluxplot-0.1.0.dist-info/licenses/LICENSE +21 -0
  65. fluxplot-0.1.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +472 -0
fluxplot/surface3d.py ADDED
@@ -0,0 +1,197 @@
1
+ """Named categorical and continuous 3D surface fields with the 2D surface color law."""
2
+ from __future__ import annotations
3
+ from collections.abc import Mapping
4
+ from copy import deepcopy
5
+ import warnings
6
+ import numpy as np
7
+ from matplotlib.colors import to_hex, to_rgba
8
+ from matplotlib.ticker import MaxNLocator
9
+ from ._fieldmap import _normalise_missing, infer_kind, categorical_colors, category_name, continuous_mapping
10
+ from .surface import _face_labels, _face_values
11
+ from .mesh3d import (load_mesh, _states, _prepare_part, _reference, _publish, _named_meshes,
12
+ _state_for_part, _state_vertices, _check_max_faces, _check_alpha, _with_alpha,
13
+ _warn_unknown_palette)
14
+ from .scene3d import Scene3D, MeshPart, _clean_float
15
+ from .ids import series_id, slugify
16
+ from .descriptors import Mark
17
+
18
+
19
+ def _face_rows_subset(subset, faces):
20
+ """True when every triangle of ``subset`` is also a triangle of ``faces``."""
21
+ def rows(a):
22
+ a = np.ascontiguousarray(a, dtype=np.int64)
23
+ return a.view(np.dtype((np.void, a.dtype.itemsize * a.shape[1]))).ravel()
24
+ return bool(np.isin(rows(subset), rows(faces)).all())
25
+
26
+
27
+ def surface3d(scene,values,*,series,surfaces,kind='auto',categories=None,palette=None,cmap=None,
28
+ color_range=None,percentile=None,missing_below=None,missing_values=(),missing_color='#D8D8D8',
29
+ colorbar=False,cbar_label=None,cbar_ticks=None,legend=None,legend_missing=False,label=None,
30
+ alpha=None,states=None,sequence=False,share_topology_with=None,morph_group=None,max_faces=None):
31
+ """Add a per-vertex value map on a mesh: a categorical atlas or a continuous field.
32
+
33
+ The value-to-colour law is the one :func:`fp.surface <fluxplot.surface>` uses (same
34
+ arguments and missing-data rules). The raw values are saved too, so Flux can change
35
+ the colormap and limits without rerunning Python.
36
+
37
+ Parameters
38
+ ----------
39
+ scene
40
+ The :func:`fp.scene3d <fluxplot.scene3d>` to add to.
41
+ values
42
+ One value per vertex: an array in mesh order (concatenated when ``surfaces`` is a
43
+ mapping) or ``{name: array}`` matching ``surfaces``.
44
+ series
45
+ Part-id prefix: label maps give ``<series>.<category>``, continuous maps
46
+ ``<series>.field``; faces touching missing data form ``<series>.missing``.
47
+ surfaces
48
+ One mesh (any :func:`mesh3d` input) or ``{name: mesh}``, e.g. both hemispheres.
49
+ A mapping is drawn as one combined mesh; for separately hideable hemispheres,
50
+ call ``surface3d`` once per hemisphere with its own series and a shared
51
+ ``color_range``.
52
+ kind
53
+ ``"label"`` (``"categorical"``), ``"continuous"``, or ``"auto"`` (label when the
54
+ data are integral and have at most 32 distinct values).
55
+ categories
56
+ ``{code: name}`` for label maps. Unnamed codes become ``category-<code>``
57
+ (negative codes ``category-m<abs>``).
58
+ palette
59
+ ``{name_or_code: color}`` for label maps; fixed, never remapped. Codes need
60
+ ``categories``.
61
+ cmap, color_range, percentile
62
+ Continuous styling. ``cmap`` is a Colormap or a name: matplotlib's, or fluxplot's
63
+ collections (``"emerald"``, ``"crameri.batlow"``). ``percentile=(2, 98)`` clips
64
+ to those percentiles; an explicit ``color_range=(lo, hi)`` wins. Pass the same
65
+ ``color_range`` to several calls (e.g. two hemispheres) for identical colours.
66
+ missing_below, missing_values, missing_color
67
+ Values below ``missing_below`` or in ``missing_values`` are missing, like NaN
68
+ and infinities; zero stays a real value. Missing faces use ``missing_color``.
69
+ colorbar, cbar_label, cbar_ticks
70
+ Add a colorbar for a continuous map, with an optional title and fixed ticks.
71
+ legend, legend_missing
72
+ Add the category parts to the scene legend (default: on for label maps). A
73
+ continuous field has no legend swatch; its key is the colorbar.
74
+ ``legend_missing=True`` adds the missing-data part.
75
+ label
76
+ For a continuous map, the field's display label. For a label map it names the
77
+ whole series (as in ``fp.surface``); categories keep their names.
78
+ alpha
79
+ Opacity in [0, 1] for every part of this call.
80
+ states, sequence, share_topology_with, morph_group, max_faces
81
+ As in :func:`mesh3d`. States are other shapes of the same mesh (bare N×3
82
+ vertex arrays are fine), e.g. ``states={"inflated": inflated}``.
83
+
84
+ Returns
85
+ -------
86
+ list of MeshPart
87
+ The new parts, in order.
88
+
89
+ Examples
90
+ --------
91
+ >>> cx = fp.scene3d(figsize=(3, 3), units="mm")
92
+ >>> fp.surface3d(cx, thickness, series="thickness", surfaces=pial, kind="continuous",
93
+ ... cmap="emerald", percentile=(2, 98), colorbar=True,
94
+ ... cbar_label="Thickness (mm)", states={"inflated": inflated})
95
+ """
96
+ if not isinstance(scene,Scene3D): raise TypeError('first argument must be fp.scene3d()')
97
+ max_faces=_check_max_faces(max_faces)
98
+ alpha=_check_alpha(alpha)
99
+ meshes,named=_named_meshes(surfaces)
100
+ if not meshes: raise ValueError('surfaces mapping is empty')
101
+ shapes=_states(states,sequence)
102
+ verts=[]; faces=[]; vals=[]; state_parts={k:[] for k in shapes}; offset=0
103
+ flat=None if isinstance(values,Mapping) else np.asarray(values,dtype=float)
104
+ for name,spec in meshes.items():
105
+ v,f=load_mesh(spec); n=len(v)
106
+ if isinstance(values,Mapping):
107
+ if name not in values: raise ValueError(f'values has no array for surface {name!r}')
108
+ val=np.asarray(values[name],dtype=float)
109
+ else:
110
+ val=flat[offset:offset+n]
111
+ if val.shape!=(n,): raise ValueError(f'{name}: exactly one value per vertex is required ({len(val)} values for {n} vertices)')
112
+ vals.append(_normalise_missing(val,missing_below,missing_values))
113
+ verts.append(v); faces.append(f+offset)
114
+ for state,target in shapes.items():
115
+ shape=_state_for_part(target,state,name,named)
116
+ state_parts[state].append(_state_vertices(shape,v,f,state,f'{series}.{name}' if named else str(series)))
117
+ offset+=n
118
+ if flat is not None and flat.shape!=(offset,): raise ValueError(f'values length must equal the total vertex count ({flat.size} values for {offset} vertices)')
119
+ v=np.concatenate(verts); f=np.concatenate(faces); data=np.concatenate(vals)
120
+ finite=data[np.isfinite(data)]; kind=infer_kind(finite,kind)
121
+ if not finite.size: warnings.warn('surface3d(): every vertex is missing; drawing no-data',stacklevel=2)
122
+ # Make all scientific non-finites missing before face assignment and serialization.
123
+ data[~np.isfinite(data)]=np.nan
124
+ fv=_face_labels(data,f) if kind=='label' else _face_values(data,f)
125
+ missing=~np.isfinite(fv)
126
+ groups={}
127
+ field_spec=None; rgba=None
128
+ if kind=='label':
129
+ for code in np.unique(fv[~missing]):
130
+ name=str(category_name(code,categories))
131
+ if name=='missing': raise ValueError("category name 'missing' is reserved for no-data")
132
+ groups.setdefault(name,np.zeros(len(f),dtype=bool))
133
+ groups[name]|=fv==code
134
+ known=[*groups,*(categories or {}),*(categories or {}).values()]
135
+ _warn_unknown_palette(palette,list(dict.fromkeys(known)),
136
+ hint='; palette codes need categories={code: name}, or key it by part name')
137
+ resolved=categorical_colors(groups,palette,categories)
138
+ else:
139
+ if palette:
140
+ warnings.warn('palette= applies to label maps and is ignored for a continuous field; use cmap=',stacklevel=2)
141
+ cmap_obj,norm=continuous_mapping(finite,cmap,color_range,percentile)
142
+ rgba=np.asarray(cmap_obj(norm(data)))
143
+ rgba[~np.isfinite(data)]=to_rgba(missing_color)
144
+ # Dense stops preserve the used mapping as portable data; no cmap package in Flux.
145
+ points=np.linspace(0,1,256)
146
+ field_spec={'cmap':{'name':cmap_obj.name,'stops':[[float(x),to_hex(cmap_obj(x),keep_alpha=False)] for x in points]},
147
+ 'range':[float(norm.vmin),float(norm.vmax)],'missingColor':to_hex(missing_color)}
148
+ if percentile is not None: field_spec['rule']={'percentile':list(percentile)}
149
+ if cbar_label is not None: field_spec['label']=str(cbar_label)
150
+ if cbar_ticks is not None:
151
+ ticks=[float(x) for x in cbar_ticks]
152
+ else:
153
+ ticks=[_clean_float(x) for x in MaxNLocator(nbins=5).tick_values(norm.vmin,norm.vmax)]
154
+ field_spec['ticks']=[x for x in ticks if norm.vmin<=x<=norm.vmax]
155
+ groups['field']=~missing; resolved={'field':'#FFFFFF'}
156
+ if missing.any(): groups['missing']=missing; resolved['missing']=to_hex(missing_color)
157
+ if max_faces is not None and max_faces<sum(bool(mask.any()) for mask in groups.values()):
158
+ raise ValueError('max_faces must allow at least one triangle per semantic part')
159
+ allstates={k:np.concatenate(a) for k,a in state_parts.items()}
160
+ alloc=deepcopy(scene._alloc); parts=[]; marks=[]; legend_ids=[]
161
+ from ._mesh_reduce import face_budgets
162
+ nonempty=[(name,mask) for name,mask in sorted(groups.items()) if mask.any()]
163
+ budgets=face_budgets([mask.sum() for name,mask in nonempty],max_faces)
164
+ for (name,mask),budget in zip(nonempty,budgets):
165
+ pid=alloc.take(series_id(series,slugify(name)))
166
+ ref=_reference(share_topology_with,pid,single=len(groups)==1)
167
+ if (isinstance(ref,MeshPart) and ref.source_count==len(v) and not np.array_equal(ref.source_faces,f[mask])
168
+ and _face_rows_subset(ref.source_faces,f)):
169
+ what='category labels' if kind=='label' and name!='missing' else 'missing-data vertices'
170
+ raise ValueError(f'{pid}: same mesh as its reference {ref.id}, but the {what} differ, so this part '
171
+ 'covers different faces; morph partners need identical labels and missing data')
172
+ # States and reference must use this exact face subset so category boundaries persist.
173
+ part=_prepare_part(pid,(v,f[mask]),_with_alpha(resolved[name],alpha),allstates,reference=ref,max_faces=budget,
174
+ values=data if kind=='continuous' or name=='missing' else None,
175
+ colors=rgba if kind=='continuous' and name=='field' else None)
176
+ if kind=='continuous' and name=='field':
177
+ part.colors=np.asarray(cmap_obj(norm(part.values)))
178
+ part.colors[~np.isfinite(part.values)]=to_rgba(missing_color)
179
+ partkind='missing' if name=='missing' else ('field' if kind=='continuous' else 'mesh')
180
+ # A continuous field is the call's one data part, so label= names it; category
181
+ # parts keep their names and label= names the series (as in 2D surface).
182
+ part_label=str(label) if partkind=='field' and label is not None else str(name)
183
+ spec={'id':pid,'role':'surface-field' if partkind=='field' else 'surface-region','kind':partkind,
184
+ 'node':pid,'series':str(series),'label':part_label,'color':part.color}
185
+ if partkind=='field': spec['field']=field_spec
186
+ parts.append(part); marks.append(Mark(role=spec['role'],series=str(series),name=name,kind='surface',label=label,gid=pid,data={'scene3d':spec}))
187
+ if partkind=='mesh' or (partkind=='missing' and legend_missing): legend_ids.append(pid)
188
+ field_ids=[m.gid for m in marks if m.data['scene3d']['kind']=='field']
189
+ if colorbar and field_ids:
190
+ pid=alloc.take(series_id(series,'colorbar')); field_id=field_ids[0]
191
+ marks.append(Mark(role='colorbar',series=str(series),name='colorbar',gid=pid,data={'scene3d':{'id':pid,'role':'colorbar','kind':'furniture','field':field_id}}))
192
+ if legend and kind=='continuous' and not legend_missing:
193
+ warnings.warn('a continuous field has no legend swatch; use colorbar=True for its key '
194
+ '(legend_missing=True still lists the missing-data part)',stacklevel=2)
195
+ result=_publish(scene,parts,marks,alloc,sequence=sequence,morph_group=morph_group)
196
+ if legend if legend is not None else kind=='label': scene._legend_entries.extend(legend_ids)
197
+ return result