Skip to content

Commit e51d50b

Browse files
authored
Merge pull request #62 from martinRenou/warp_input
Improve input property
2 parents 6af8e54 + 0e5af3e commit e51d50b

File tree

7 files changed

+285
-62
lines changed

7 files changed

+285
-62
lines changed

docs/source/api_reference/warp.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ If you only want to visualize the last component of your displacement data, simp
1919
2020
warped_mesh = Warp(mesh, input=(0, 0, ('displacement', 'z')))
2121
22-
If your mesh contains a 1-D ``Data``, you can also warp by selecting the component:
22+
If your mesh contains a 1-D ``Data``, you can also warp using this data by setting it to the wanted dimension:
2323

2424
.. code::
2525
26-
warped_mesh = Warp(mesh, input=(('height', 'value'), 0, 0))
26+
x_warped_mesh = Warp(mesh, input=('height', 0, 0)) # Warp by 'height' data on the x-axis
27+
z_warped_mesh = Warp(mesh, input=(0, 0, 'height')) # Warp by 'height' data on the z-axis
2728
2829
2930
Examples
@@ -110,8 +111,8 @@ Like other ipygany's effects, you can combine it with other effects. Here we app
110111
)
111112

112113
# Colorize by curvature
113-
colored_mesh = IsoColor(mesh, input=('height', 'value'), min=np.min(z), max=np.max(z))
114-
warped_mesh = Warp(colored_mesh, input=(0, 0, ('height', 'value')))
114+
colored_mesh = IsoColor(mesh, input='height', min=np.min(z), max=np.max(z))
115+
warped_mesh = Warp(colored_mesh, input=(0, 0, 'height'))
115116

116117
# Create a slider that will dynamically change the warp factor value
117118
warp_slider = FloatSlider(value=0, min=0, max=1)

ipygany/ipygany.py

Lines changed: 152 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import numpy as np
66

77
from traitlets import (
8-
Bool, Dict, Unicode, List, Instance, CFloat, Tuple, Union, default, validate
8+
Bool, Dict, Unicode, List, Instance, CFloat, Tuple, TraitError, Union, default, validate
99
)
1010
from traittypes import Array
1111
from ipywidgets import (
@@ -70,6 +70,15 @@ def __init__(self, name, components, **kwargs):
7070
"""Create a new Data instance given its name and components."""
7171
super(Data, self).__init__(name=name, components=components, **kwargs)
7272

73+
@property
74+
def dim(self):
75+
"""Get the data dimension."""
76+
return len(self.components)
77+
78+
def as_input(self):
79+
"""Internal method of ipygany. Do not use this."""
80+
return [(self.name, comp.name) for comp in self.components]
81+
7382
def __getitem__(self, key):
7483
"""Get a component by name or index."""
7584
if isinstance(key, str):
@@ -97,13 +106,10 @@ def _grid_data_to_data_widget(grid_data):
97106
"""Turn a vtk grid into Data widgets."""
98107
data = []
99108
for key, value in grid_data.items():
100-
d = Data(
101-
name=key,
102-
components=[
103-
Component(name=comp_name, array=comp['array'])
104-
for comp_name, comp in value.items()
105-
]
106-
)
109+
d = Data(key, [
110+
Component(comp_name, comp['array'])
111+
for comp_name, comp in value.items()
112+
])
107113
data.append(d)
108114

109115
return data
@@ -135,20 +141,32 @@ class Block(_GanyWidgetBase):
135141

136142
def __getitem__(self, key):
137143
"""Get a component by name or index."""
138-
if not isinstance(key, tuple) or len(key) != 2:
139-
raise KeyError('You can only access data by (data_name, component_name) tuple.')
144+
if not (isinstance(key, str) or (isinstance(key, tuple) and len(key) == 2)):
145+
raise KeyError('You can only access data by (data_name, component_name) tuple or data_name string.')
146+
147+
# This prevents failures when this method is called in the constructor
148+
# self.data is not yet initialized
149+
actual_data = self.data if len(self.data) else (self.parent.data if self.parent else [])
140150

151+
# If the key is a string, we assume it's the data name
152+
if isinstance(key, str):
153+
for data in actual_data:
154+
if data.name == key:
155+
return data
156+
raise KeyError('Data {} not found.'.format(key))
157+
158+
# Otherwise it's a (data name, component name) tuple
141159
data_name = key[0]
142160
component_name = key[1]
143161

144162
if isinstance(data_name, str):
145-
for data in self.data:
163+
for data in actual_data:
146164
if data.name == data_name:
147165
return data[component_name]
148166
raise KeyError('Data {} not found.'.format(data_name))
149167

150168
if isinstance(data_name, int):
151-
return self.data[data_name][component_name]
169+
return actual_data[data_name][component_name]
152170

153171
raise KeyError('Invalid key {}.'.format(key))
154172

@@ -397,110 +415,204 @@ class Effect(Block):
397415

398416
_model_name = Unicode('EffectModel').tag(sync=True)
399417

418+
input = Union((Tuple(), Unicode(), CFloat())).tag(sync=True)
419+
400420
parent = Instance(Block).tag(sync=True, **widget_serialization)
401421

402422
def __init__(self, parent, **kwargs):
403423
"""Create an Effect on the given Mesh or Effect output."""
404424
super(Effect, self).__init__(parent=parent, **kwargs)
405425

426+
@property
427+
def data(self):
428+
"""Get data."""
429+
return self.parent.data
430+
431+
@property
432+
def input_dim(self):
433+
"""Input dimension."""
434+
return 0
435+
436+
@default('input')
437+
def _default_input(self):
438+
if not len(self.data):
439+
if self.input_dim == 0 or self.input_dim == 1:
440+
return 0
441+
return tuple(0 for _ in range(self.input_dim))
442+
443+
return self._validate_input_impl(self.data[0].name)
444+
445+
@validate('input')
446+
def _validate_input(self, proposal):
447+
return self._validate_input_impl(proposal['value'])
448+
449+
def _validate_input_impl(self, value):
450+
# Input is a data name
451+
if isinstance(value, str):
452+
input_data = self[value]
453+
454+
# Simply use this data
455+
if input_data.dim == self.input_dim:
456+
return input_data.name
457+
458+
# Take all the components and fill in with zeros
459+
if input_data.dim < self.input_dim:
460+
chosen_input = input_data.as_input()
461+
462+
while len(chosen_input) != self.input_dim:
463+
chosen_input.append(0.)
464+
465+
return chosen_input
466+
467+
# input_data.dim > self.input_dim, take only the first self.input_dim components
468+
return input_data.as_input()[:self.input_dim]
469+
470+
# Input as a tuple
471+
if isinstance(value, (tuple, list)):
472+
if self.input_dim == 1 and len(value) == 2:
473+
return self._validate_input_component(value)
474+
475+
if len(value) != self.input_dim:
476+
raise TraitError('input is of dimension {} but expected input dimension is {}'.format(len(value), self.input_dim))
477+
478+
# Check all elements in the tuple
479+
return tuple(self._validate_input_component(el) for el in value)
480+
481+
# Input is a number
482+
if isinstance(value, (float, int)) and self.input_dim == 1:
483+
return value
484+
485+
raise TraitError('{} is not a valid input'.format(value))
486+
487+
def _validate_input_component(self, value):
488+
# Component selection by name
489+
if isinstance(value, (tuple, list)):
490+
if len(value) != 2:
491+
raise TraitError('{} is not a valid component'.format(value))
492+
493+
try:
494+
self[value[0], value[1]]
495+
except KeyError:
496+
raise TraitError('{} is not a valid component'.format(value))
497+
498+
return value
499+
500+
# Data selection by name
501+
if isinstance(value, str):
502+
try:
503+
data = self[value]
504+
except KeyError:
505+
raise TraitError('{} is not a valid data'.format(value))
506+
507+
if data.dim != 1:
508+
raise TraitError('{} is ambiguous, please select a component'.format(value))
509+
510+
return (data.name, data.components[0].name)
511+
512+
if isinstance(value, (float, int)):
513+
return value
514+
515+
raise TraitError('{} is not a valid input'.format(value))
516+
406517

407518
class Warp(Effect):
408519
"""A warp effect to another block."""
409520

410521
_model_name = Unicode('WarpModel').tag(sync=True)
411522

412-
input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True)
413-
414523
offset = Union((Tuple(trait=Unicode, minlen=3, maxlen=3), CFloat(0.)), default_value=0.).tag(sync=True)
415524
factor = Union((Tuple(trait=Unicode, minlen=3, maxlen=3), CFloat(0.)), default_value=1.).tag(sync=True)
416525

417-
@default('input')
418-
def _default_input(self):
419-
return self.parent.data[0].name
526+
@property
527+
def input_dim(self):
528+
"""Input dimension."""
529+
return 3
420530

421531

422532
class Alpha(Effect):
423533
"""An transparency effect to another block."""
424534

425535
_model_name = Unicode('AlphaModel').tag(sync=True)
426536

427-
input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True)
428-
429537
@default('input')
430538
def _default_input(self):
431539
return 0.7
432540

541+
@property
542+
def input_dim(self):
543+
"""Input dimension."""
544+
return 1
545+
433546

434547
class RGB(Effect):
435548
"""A color effect to another block."""
436549

437550
_model_name = Unicode('RGBModel').tag(sync=True)
438551

439-
input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True)
552+
@property
553+
def input_dim(self):
554+
"""Input dimension."""
555+
return 3
440556

441557

442558
class IsoColor(Effect):
443559
"""An IsoColor effect to another block."""
444560

445561
_model_name = Unicode('IsoColorModel').tag(sync=True)
446562

447-
input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True)
448-
449563
min = CFloat(0.).tag(sync=True)
450564
max = CFloat(0.).tag(sync=True)
451565

452-
@default('input')
453-
def _default_input(self):
454-
return self.parent.data[0].name
566+
@property
567+
def input_dim(self):
568+
"""Input dimension."""
569+
return 1
455570

456571

457572
class IsoSurface(Effect):
458573
"""An IsoSurface effect to another block."""
459574

460575
_model_name = Unicode('IsoSurfaceModel').tag(sync=True)
461576

462-
input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True)
463-
464577
value = CFloat(0.).tag(sync=True)
465578
dynamic = Bool(False).tag(sync=True)
466579

467-
@default('input')
468-
def _default_input(self):
469-
return self.parent.data[0].name
580+
@property
581+
def input_dim(self):
582+
"""Input dimension."""
583+
return 1
470584

471585

472586
class Threshold(Effect):
473587
"""An Threshold effect to another block."""
474588

475589
_model_name = Unicode('ThresholdModel').tag(sync=True)
476590

477-
input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True)
478-
479591
min = CFloat(0.).tag(sync=True)
480592
max = CFloat(0.).tag(sync=True)
481593
dynamic = Bool(False).tag(sync=True)
482594
inclusive = Bool(True).tag(sync=True)
483595

484-
@default('input')
485-
def _default_input(self):
486-
return self.parent.data[0].name
596+
@property
597+
def input_dim(self):
598+
"""Input dimension."""
599+
return 1
487600

488601

489602
class UnderWater(Effect):
490603
"""An nice UnderWater effect to another block."""
491604

492605
_model_name = Unicode('UnderWaterModel').tag(sync=True)
493606

494-
input = Union((Tuple(trait=Unicode, minlen=2, maxlen=2), Unicode(), CFloat(0.))).tag(sync=True)
495-
496607
default_color = Color('#F2FFD2').tag(sync=True)
497608
texture = Instance(Image, allow_none=True, default_value=None).tag(sync=True, **widget_serialization)
498609
texture_scale = CFloat(2.).tag(sync=True)
499610
texture_position = Tuple(minlen=2, maxlen=2, default_value=(1., 1., 0.)).tag(sync=True)
500611

501-
@default('input')
502-
def _default_input(self):
503-
return self.parent.data[0].name
612+
@property
613+
def input_dim(self):
614+
"""Input dimension."""
615+
return 1
504616

505617

506618
class Water(Effect):

tests/__init__.py

Whitespace-only changes.

tests/test_ipygany.py

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,23 @@
44

55
from ipydatawidgets import NDArrayWidget
66

7-
from ipygany import PolyMesh, Data, Component
7+
from ipygany import PolyMesh, Component
88

9-
10-
vertices = np.array([
11-
[0., 0., 0.],
12-
[0., 0., 0.],
13-
[0., 0., 0.],
14-
])
15-
16-
triangles = np.array([
17-
[0, 1, 2],
18-
])
19-
20-
data_1d = Data(name='1d', components=[Component('x', np.array([0., 0., 0.]))])
21-
data_3d = Data('3d', [
22-
Component(name='x', array=np.array([1., 1., 1.])),
23-
Component('y', np.array([2., 2., 2.])),
24-
Component('z', np.array([3., 3., 3.])),
25-
])
9+
from .utils import get_test_assets
2610

2711

2812
def test_data_access():
13+
vertices, triangles, data_1d, data_3d = get_test_assets()
14+
2915
poly = PolyMesh(vertices=vertices, triangle_indices=triangles, data=[data_1d, data_3d])
3016

3117
assert np.all(np.equal(poly['1d', 'x'].array, np.array([0., 0., 0.])))
3218
assert np.all(np.equal(poly['3d', 'y'].array, np.array([2., 2., 2.])))
3319

3420

3521
def test_mesh_data_creation():
22+
vertices, triangles, data_1d, data_3d = get_test_assets()
23+
3624
poly = PolyMesh(vertices=vertices, triangle_indices=triangles, data={
3725
'1d': [Component('x', np.array([0., 0., 0.]))],
3826
'2d': [Component('x', np.array([1., 1., 1.])), Component('y', np.array([2., 2., 2.]))]

0 commit comments

Comments
 (0)