Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Antialiased lines for categorical aggregates #1081

Merged
merged 3 commits into from
May 4, 2022
Merged

Antialiased lines for categorical aggregates #1081

merged 3 commits into from
May 4, 2022

Conversation

ianthomas23
Copy link
Member

This is a WIP to fix issue #1079. It works with numba disabled but not with numba enabled so needs further work.

Test code:

import datashader as ds, numpy as np, holoviews as hv
from holoviews.operation.datashader import datashade
hv.extension('bokeh')

def time_series(T=1, N=100, mu=0.1, sigma=0.1, S0=20):
    dt = float(T)/N
    t = np.linspace(0, T, N)
    W = np.random.standard_normal(size = N) 
    W = np.cumsum(W)*np.sqrt(dt)
    X = (mu-0.5*sigma**2)*t + sigma*W 
    S = S0*np.exp(X)
    return S

np.random.seed(12)
nlines = 6
npts = 100
lines = {i: hv.Curve(time_series(N=npts, S0=200+np.random.rand())) for i in range(nlines)}

hv.Layout([datashade(hv.NdOverlay(lines, kdims='k'), line_width=lw, aggregator=ds.by('k', ds.count()))
           for lw in [0, 1, 2.5]])

When jupyter lab is run using NUMBA_DISABLE_JIT=1 jupyter lab it gives the desired output (left image uses non-antialiased code, the other 2 images use the new antialiased code):
aa_cat

If run without disabling numba the initial failure is (full traceback is at the bottom of this post):

Cannot unify array(float32, 3d, C) and array(float32, 2d, A) for 'agg.2', defined at /home/iant/github/datashader/datashader/glyphs/line.py (758)

File "../../github/datashader/datashader/glyphs/line.py", line 758:
def _full_antialias(line_width, antialias_combination, i, x0, x1, y0, y1,
    <source elided>

    xmax = agg.shape[1]-1
    ^

The agg argument to this function is a 2D numpy array for non-categorical aggregations, which works fine in the master branch, and a 3D numpy for categorical aggregations (the length of the third axis is the number of categories).

I believe that the prior code deals with this by passing down an appropriate numba-compiled append function created here:

def make_append(bases, cols, calls, glyph, categorical):

which access the agg and up until now this has not been necessary for the new antialiasing code.

It would be great if someone has some insight into how we can get this working in the short term, even if the solution is inelegant. In the medium term I have volunteered to become proficient with numba so that I can refactor datashader's numba code in the belief that it can be simplified as numba is now much cleverer than when this was written n years ago.

Full traceback:

WARNING:param.dynamic_operation: Callable raised "TypingError('Failed in nopython mode pipeline (step: nopython frontend)\n\x1b[1m\x1b[1m\x1b[1m\x1b[1mFailed in nopython mode pipeline (step: nopython frontend)\n\x1b[1m\x1b[1m\x1b[1m\x1b[1mFailed in nopython mode pipeline (step: nopython frontend)\n\x1b[1m\x1b[1m\x1b[1m\x1b[1mFailed in nopython mode pipeline (step: nopython frontend)\n\x1b[1m\x1b[1mCannot unify array(float32, 3d, C) and array(float32, 2d, A) for \'agg.2\', defined at /home/iant/github/datashader/datashader/glyphs/line.py (758)\n\x1b[1m\nFile "../../github/datashader/datashader/glyphs/line.py", line 758:\x1b[0m\n\x1b[1mdef _full_antialias(line_width, antialias_combination, i, x0, x1, y0, y1,\n    <source elided>\n\n\x1b[1m    xmax = agg.shape[1]-1\n\x1b[0m    \x1b[1m^\x1b[0m\x1b[0m\n\x1b[0m\n\x1b[0m\x1b[1mDuring: typing of assignment at /home/iant/github/datashader/datashader/glyphs/line.py (758)\x1b[0m\n\x1b[1m\nFile "../../github/datashader/datashader/glyphs/line.py", line 758:\x1b[0m\n\x1b[1mdef _full_antialias(line_width, antialias_combination, i, x0, x1, y0, y1,\n    <source elided>\n\n\x1b[1m    xmax = agg.shape[1]-1\n\x1b[0m    \x1b[1m^\x1b[0m\x1b[0m\n\n\x1b[0m\x1b[1mDuring: resolving callee type: type(CPUDispatcher(<function _full_antialias at 0x7f5739c67160>))\x1b[0m\n\x1b[0m\x1b[1mDuring: typing of call at <draw_segment> (40)\n\x1b[0m\n\x1b[0m\x1b[1mDuring: resolving callee type: type(CPUDispatcher(<function _full_antialias at 0x7f5739c67160>))\x1b[0m\n\x1b[0m\x1b[1mDuring: typing of call at <draw_segment> (40)\n\x1b[0m\n\x1b[1m\nFile "<draw_segment>", line 40:\x1b[0m\n\x1b[1m<source missing, REPL/exec in use?>\x1b[0m\n\n\x1b[0m\x1b[1mDuring: resolving callee type: type(CPUDispatcher(<function draw_segment at 0x7f5718b544c0>))\x1b[0m\n\x1b[0m\x1b[1mDuring: typing of call at <perform_extend_line> (21)\n\x1b[0m\n\x1b[0m\x1b[1mDuring: resolving callee type: type(CPUDispatcher(<function draw_segment at 0x7f5718b544c0>))\x1b[0m\n\x1b[0m\x1b[1mDuring: typing of call at <perform_extend_line> (21)\n\x1b[0m\n\x1b[1m\nFile "<perform_extend_line>", line 21:\x1b[0m\n\x1b[1m<source missing, REPL/exec in use?>\x1b[0m\n\n\x1b[0m\x1b[1mDuring: resolving callee type: type(CPUDispatcher(<function perform_extend_line at 0x7f5718b54310>))\x1b[0m\n\x1b[0m\x1b[1mDuring: typing of call at <extend_cpu> (8)\n\x1b[0m\n\x1b[0m\x1b[1mDuring: resolving callee type: type(CPUDispatcher(<function perform_extend_line at 0x7f5718b54310>))\x1b[0m\n\x1b[0m\x1b[1mDuring: typing of call at <extend_cpu> (8)\n\x1b[0m\n\x1b[1m\nFile "<extend_cpu>", line 8:\x1b[0m\n\x1b[1m<source missing, REPL/exec in use?>\x1b[0m\n')".
Invoked as dynamic_operation(height=400, scale=1.0, width=400, x_range=None, y_range=None)

---------------------------------------------------------------------------
TypingError                               Traceback (most recent call last)
File ~/.miniconda/envs/datashader_latest/lib/python3.8/site-packages/IPython/core/formatters.py:973, in MimeBundleFormatter.__call__(self, obj, include, exclude)
    970     method = get_real_method(obj, self.print_method)
    972     if method is not None:
--> 973         return method(include=include, exclude=exclude)
    974     return None
    975 else:

File ~/github/holoviews/holoviews/core/dimension.py:1316, in Dimensioned._repr_mimebundle_(self, include, exclude)
   1309 def _repr_mimebundle_(self, include=None, exclude=None):
   1310     """
   1311     Resolves the class hierarchy for the class rendering the
   1312     object using any display hooks registered on Store.display
   1313     hooks.  The output of all registered display_hooks is then
   1314     combined and returned.
   1315     """
-> 1316     return Store.render(self)

File ~/github/holoviews/holoviews/core/options.py:1405, in Store.render(cls, obj)
   1403 data, metadata = {}, {}
   1404 for hook in hooks:
-> 1405     ret = hook(obj)
   1406     if ret is None:
   1407         continue

File ~/github/holoviews/holoviews/ipython/display_hooks.py:282, in pprint_display(obj)
    280 if not ip.display_formatter.formatters['text/plain'].pprint:
    281     return None
--> 282 return display(obj, raw_output=True)

File ~/github/holoviews/holoviews/ipython/display_hooks.py:255, in display(obj, raw_output, **kwargs)
    253 elif isinstance(obj, (Layout, NdLayout, AdjointLayout)):
    254     with option_state(obj):
--> 255         output = layout_display(obj)
    256 elif isinstance(obj, (HoloMap, DynamicMap)):
    257     with option_state(obj):

File ~/github/holoviews/holoviews/ipython/display_hooks.py:146, in display_hook.<locals>.wrapped(element)
    144 try:
    145     max_frames = OutputSettings.options['max_frames']
--> 146     mimebundle = fn(element, max_frames=max_frames)
    147     if mimebundle is None:
    148         return {}, {}

File ~/github/holoviews/holoviews/ipython/display_hooks.py:220, in layout_display(layout, max_frames)
    217     max_frame_warning(max_frames)
    218     return None
--> 220 return render(layout)

File ~/github/holoviews/holoviews/ipython/display_hooks.py:68, in render(obj, **kwargs)
     65 if renderer.fig == 'pdf':
     66     renderer = renderer.instance(fig='png')
---> 68 return renderer.components(obj, **kwargs)

File ~/github/holoviews/holoviews/plotting/renderer.py:410, in Renderer.components(self, obj, fmt, comm, **kwargs)
    408 doc = Document()
    409 with config.set(embed=embed):
--> 410     model = plot.layout._render_model(doc, comm)
    411 if embed:
    412     return render_model(model, comm)

File ~/.miniconda/envs/datashader_latest/lib/python3.8/site-packages/panel/viewable.py:460, in Renderable._render_model(self, doc, comm)
    458 if comm is None:
    459     comm = state._comm_manager.get_server_comm()
--> 460 model = self.get_root(doc, comm)
    462 if config.embed:
    463     embed_state(self, model, doc,
    464                 json=config.embed_json,
    465                 json_prefix=config.embed_json_prefix,
    466                 save_path=config.embed_save_path,
    467                 load_path=config.embed_load_path,
    468                 progress=False)

File ~/.miniconda/envs/datashader_latest/lib/python3.8/site-packages/panel/viewable.py:518, in Renderable.get_root(self, doc, comm, preprocess)
    501 """
    502 Returns the root model and applies pre-processing hooks
    503 
   (...)
    515 Returns the bokeh model corresponding to this panel object
    516 """
    517 doc = init_doc(doc)
--> 518 root = self._get_model(doc, comm=comm)
    519 if preprocess:
    520     self._preprocess(root)

File ~/.miniconda/envs/datashader_latest/lib/python3.8/site-packages/panel/layout/base.py:122, in Panel._get_model(self, doc, root, parent, comm)
    120 if root is None:
    121     root = model
--> 122 objects = self._get_objects(model, [], doc, root, comm)
    123 props = dict(self._init_params(), objects=objects)
    124 model.update(**self._process_param_change(props))

File ~/.miniconda/envs/datashader_latest/lib/python3.8/site-packages/panel/layout/base.py:112, in Panel._get_objects(self, model, old_objects, doc, root, comm)
    110 else:
    111     try:
--> 112         child = pane._get_model(doc, root, model, comm)
    113     except RerenderError:
    114         return self._get_objects(model, current_objects[:i], doc, root, comm)

File ~/.miniconda/envs/datashader_latest/lib/python3.8/site-packages/panel/pane/holoviews.py:248, in HoloViews._get_model(self, doc, root, parent, comm)
    246     plot = self.object
    247 else:
--> 248     plot = self._render(doc, comm, root)
    250 plot.pane = self
    251 backend = plot.renderer.backend

File ~/.miniconda/envs/datashader_latest/lib/python3.8/site-packages/panel/pane/holoviews.py:321, in HoloViews._render(self, doc, comm, root)
    318     if comm:
    319         kwargs['comm'] = comm
--> 321 return renderer.get_plot(self.object, **kwargs)

File ~/github/holoviews/holoviews/plotting/bokeh/renderer.py:73, in BokehRenderer.get_plot(self_or_cls, obj, doc, renderer, **kwargs)
     66 @bothmethod
     67 def get_plot(self_or_cls, obj, doc=None, renderer=None, **kwargs):
     68     """
     69     Given a HoloViews Viewable return a corresponding plot instance.
     70     Allows supplying a document attach the plot to, useful when
     71     combining the bokeh model with another plot.
     72     """
---> 73     plot = super(BokehRenderer, self_or_cls).get_plot(obj, doc, renderer, **kwargs)
     74     if plot.document is None:
     75         plot.document = Document() if self_or_cls.notebook_context else curdoc()

File ~/github/holoviews/holoviews/plotting/renderer.py:220, in Renderer.get_plot(self_or_cls, obj, doc, renderer, comm, **kwargs)
    217     raise SkipRendering(msg.format(dims=dims))
    219 # Initialize DynamicMaps with first data item
--> 220 initialize_dynamic(obj)
    222 if not renderer:
    223     renderer = self_or_cls

File ~/github/holoviews/holoviews/plotting/util.py:254, in initialize_dynamic(obj)
    252     continue
    253 if not len(dmap):
--> 254     dmap[dmap._initial_key()]

File ~/github/holoviews/holoviews/core/spaces.py:1344, in DynamicMap.__getitem__(self, key)
   1342 # Not a cross product and nothing cached so compute element.
   1343 if cache is not None: return cache
-> 1344 val = self._execute_callback(*tuple_key)
   1345 if data_slice:
   1346     val = self._dataslice(val, data_slice)

File ~/github/holoviews/holoviews/core/spaces.py:1111, in DynamicMap._execute_callback(self, *args)
   1108     kwargs['_memoization_hash_'] = hash_items
   1110 with dynamicmap_memoization(self.callback, self.streams):
-> 1111     retval = self.callback(*args, **kwargs)
   1112 return self._style(retval)

File ~/github/holoviews/holoviews/core/spaces.py:708, in Callable.__call__(self, *args, **kwargs)
    705     args, kwargs = (), dict(pos_kwargs, **kwargs)
    707 try:
--> 708     ret = self.callable(*args, **kwargs)
    709 except KeyError:
    710     # KeyError is caught separately because it is used to signal
    711     # invalid keys on DynamicMap and should not warn
    712     raise

File ~/github/holoviews/holoviews/util/__init__.py:1044, in Dynamic._dynamic_operation.<locals>.dynamic_operation(*key, **kwargs)
   1042 def dynamic_operation(*key, **kwargs):
   1043     key, obj = resolve(key, kwargs)
-> 1044     return apply(obj, *key, **kwargs)

File ~/github/holoviews/holoviews/util/__init__.py:1036, in Dynamic._dynamic_operation.<locals>.apply(element, *key, **kwargs)
   1034 def apply(element, *key, **kwargs):
   1035     kwargs = dict(util.resolve_dependent_kwargs(self.p.kwargs), **kwargs)
-> 1036     processed = self._process(element, key, kwargs)
   1037     if (self.p.link_dataset and isinstance(element, Dataset) and
   1038         isinstance(processed, Dataset) and processed._dataset is None):
   1039         processed._dataset = element.dataset

File ~/github/holoviews/holoviews/util/__init__.py:1018, in Dynamic._process(self, element, key, kwargs)
   1016 elif isinstance(self.p.operation, Operation):
   1017     kwargs = {k: v for k, v in kwargs.items() if k in self.p.operation.param}
-> 1018     return self.p.operation.process_element(element, key, **kwargs)
   1019 else:
   1020     return self.p.operation(element, **kwargs)

File ~/github/holoviews/holoviews/core/operation.py:194, in Operation.process_element(self, element, key, **params)
    191 else:
    192     self.p = param.ParamOverrides(self, params,
    193                                   allow_extra_keywords=self._allow_extra_keywords)
--> 194 return self._apply(element, key)

File ~/github/holoviews/holoviews/core/operation.py:141, in Operation._apply(self, element, key)
    139     if not in_method:
    140         element._in_method = True
--> 141 ret = self._process(element, key)
    142 if hasattr(element, '_in_method') and not in_method:
    143     element._in_method = in_method

File ~/github/holoviews/holoviews/operation/datashader.py:1579, in datashade._process(self, element, key)
   1578 def _process(self, element, key=None):
-> 1579     agg = rasterize._process(self, element, key)
   1580     shaded = shade._process(self, agg, key)
   1581     return shaded

File ~/github/holoviews/holoviews/operation/datashader.py:1558, in rasterize._process(self, element, key)
   1555     op = transform.instance(**{k:v for k,v in extended_kws.items()
   1556                                if k in transform.param})
   1557     op._precomputed = self._precomputed
-> 1558     element = element.map(op, predicate)
   1559     self._precomputed = op._precomputed
   1561 unused_params = list(all_supplied_kws - all_allowed_kws)

File ~/github/holoviews/holoviews/core/dimension.py:707, in LabelledData.map(self, map_fn, specs, clone)
    705         if new_val is not None:
    706             deep_mapped[k] = new_val
--> 707     if applies: deep_mapped = map_fn(deep_mapped)
    708     return deep_mapped
    709 else:

File ~/github/holoviews/holoviews/core/operation.py:214, in Operation.__call__(self, element, **kwargs)
    210         return element.clone([(k, self._apply(el, key=k))
    211                               for k, el in element.items()])
    212     elif ((self._per_element and isinstance(element, Element)) or
    213           (not self._per_element and isinstance(element, ViewableElement))):
--> 214         return self._apply(element)
    215 elif 'streams' not in kwargs:
    216     kwargs['streams'] = self.p.streams

File ~/github/holoviews/holoviews/core/operation.py:141, in Operation._apply(self, element, key)
    139     if not in_method:
    140         element._in_method = True
--> 141 ret = self._process(element, key)
    142 if hasattr(element, '_in_method') and not in_method:
    143     element._in_method = in_method

File ~/github/holoviews/holoviews/operation/datashader.py:503, in aggregate._process(self, element, key)
    498 with warnings.catch_warnings():
    499     warnings.filterwarnings(
    500         action='ignore', message='casting datetime64',
    501         category=FutureWarning
    502     )
--> 503     agg = getattr(cvs, glyph)(dfdata, x.name, y.name, agg_fn, **agg_kwargs)
    504 if 'x_axis' in agg.coords and 'y_axis' in agg.coords:
    505     agg = agg.rename({'x_axis': x, 'y_axis': y})

File ~/github/datashader/datashader/core.py:448, in Canvas.line(self, source, x, y, agg, axis, geometry, line_width, antialias)
    445     # Switch agg to floating point.
    446     agg = rd._reduction_to_floating_point(agg)
--> 448 return bypixel(source, self, glyph, agg)

File ~/github/datashader/datashader/core.py:1280, in bypixel(source, canvas, glyph, agg)
   1278 with np.warnings.catch_warnings():
   1279     np.warnings.filterwarnings('ignore', r'All-NaN (slice|axis) encountered')
-> 1280     return bypixel.pipeline(source, schema, canvas, glyph, agg)

File ~/github/datashader/datashader/utils.py:109, in Dispatcher.__call__(self, head, *rest, **kwargs)
    107 typ = type(head)
    108 if typ in lk:
--> 109     return lk[typ](head, *rest, **kwargs)
    110 for cls in getmro(typ)[1:]:
    111     if cls in lk:

File ~/github/datashader/datashader/data_libraries/pandas.py:17, in pandas_pipeline(df, schema, canvas, glyph, summary)
     15 @bypixel.pipeline.register(pd.DataFrame)
     16 def pandas_pipeline(df, schema, canvas, glyph, summary):
---> 17     return glyph_dispatch(glyph, df, schema, canvas, summary)

File ~/github/datashader/datashader/utils.py:112, in Dispatcher.__call__(self, head, *rest, **kwargs)
    110 for cls in getmro(typ)[1:]:
    111     if cls in lk:
--> 112         return lk[cls](head, *rest, **kwargs)
    113 raise TypeError("No dispatch for {0} type".format(typ))

File ~/github/datashader/datashader/data_libraries/pandas.py:46, in default(glyph, source, schema, canvas, summary, cuda)
     42 y_axis = canvas.y_axis.compute_index(y_st, height)
     44 bases = create((height, width))
---> 46 extend(bases, source, x_st + y_st, x_range + y_range)
     48 return finalize(bases,
     49                 cuda=cuda,
     50                 coords=OrderedDict([(glyph.x_label, x_axis),
     51                                     (glyph.y_label, y_axis)]),
     52                 dims=[glyph.y_label, glyph.x_label])

File ~/github/datashader/datashader/glyphs/line.py:97, in LineAxis0._internal_build_extend.<locals>.extend(aggs, df, vt, bounds, plot_start)
     94     do_extend = extend_cpu
     96 # line may be clipped, then mapped to pixels
---> 97 do_extend(
     98     sx, tx, sy, ty, xmin, xmax, ymin, ymax,
     99     xs, ys, plot_start, *aggs_and_cols
    100 )

File ~/.miniconda/envs/datashader_latest/lib/python3.8/site-packages/numba/core/dispatcher.py:468, in _DispatcherBase._compile_for_args(self, *args, **kws)
    464         msg = (f"{str(e).rstrip()} \n\nThis error may have been caused "
    465                f"by the following argument(s):\n{args_str}\n")
    466         e.patch_message(msg)
--> 468     error_rewrite(e, 'typing')
    469 except errors.UnsupportedError as e:
    470     # Something unsupported is present in the user code, add help info
    471     error_rewrite(e, 'unsupported_error')

File ~/.miniconda/envs/datashader_latest/lib/python3.8/site-packages/numba/core/dispatcher.py:409, in _DispatcherBase._compile_for_args.<locals>.error_rewrite(e, issue_type)
    407     raise e
    408 else:
--> 409     raise e.with_traceback(None)

TypingError: Failed in nopython mode pipeline (step: nopython frontend)
Failed in nopython mode pipeline (step: nopython frontend)
Failed in nopython mode pipeline (step: nopython frontend)
Failed in nopython mode pipeline (step: nopython frontend)
Cannot unify array(float32, 3d, C) and array(float32, 2d, A) for 'agg.2', defined at /home/iant/github/datashader/datashader/glyphs/line.py (758)

File "../../github/datashader/datashader/glyphs/line.py", line 758:
def _full_antialias(line_width, antialias_combination, i, x0, x1, y0, y1,
    <source elided>

    xmax = agg.shape[1]-1
    ^

During: typing of assignment at /home/iant/github/datashader/datashader/glyphs/line.py (758)

File "../../github/datashader/datashader/glyphs/line.py", line 758:
def _full_antialias(line_width, antialias_combination, i, x0, x1, y0, y1,
    <source elided>

    xmax = agg.shape[1]-1
    ^

During: resolving callee type: type(CPUDispatcher(<function _full_antialias at 0x7f5739c67160>))
During: typing of call at <draw_segment> (40)

During: resolving callee type: type(CPUDispatcher(<function _full_antialias at 0x7f5739c67160>))
During: typing of call at <draw_segment> (40)


File "<draw_segment>", line 40:
<source missing, REPL/exec in use?>

During: resolving callee type: type(CPUDispatcher(<function draw_segment at 0x7f5718b544c0>))
During: typing of call at <perform_extend_line> (21)

During: resolving callee type: type(CPUDispatcher(<function draw_segment at 0x7f5718b544c0>))
During: typing of call at <perform_extend_line> (21)


File "<perform_extend_line>", line 21:
<source missing, REPL/exec in use?>

During: resolving callee type: type(CPUDispatcher(<function perform_extend_line at 0x7f5718b54310>))
During: typing of call at <extend_cpu> (8)

During: resolving callee type: type(CPUDispatcher(<function perform_extend_line at 0x7f5718b54310>))
During: typing of call at <extend_cpu> (8)


File "<extend_cpu>", line 8:
<source missing, REPL/exec in use?>

@ianthomas23 ianthomas23 changed the title Antialiased lines for categorical aggregates [WIP] Antialiased lines for categorical aggregates May 3, 2022
@stuartarchibald
Copy link
Contributor

@ianthomas23 Does this summarise the issue you are seeing?

from numba import njit
import numpy as np

@njit
def foo(*aggs_and_cols):
    i = 0
    agg = aggs_and_cols[0] # This agg is 3D
    if aggs_and_cols[0].ndim == 3: # This could be any predicate
        cat_index = aggs_and_cols[1][i]
        agg = agg[:, :, cat_index] # This agg is 2D

    xmax = agg.shape[1] - 1 # What's this agg? 2D or 3D?

args = (np.zeros((3, 4, 5)), (0, 0))

foo(*args)

The problem is that Numba cannot work out the type of agg as it changes depending on whether a branch is taken.

@jbednar
Copy link
Member

jbednar commented May 3, 2022

The problem is that Numba cannot work out the type of agg as it changes depending on whether a branch is taken.

Thanks! If so, the usual way we've dealt with that in Datashader is to make sure the branch is taken before reaching Numba, so that Numba always sees the same type.

@ianthomas23
Copy link
Member Author

@ianthomas23 Does this summarise the issue you are seeing?

Yes it does, thank you.

Elsewhere in the code we have multiple Numba-compiled functions, each of which accepts only 2D or 3D arrays, and we pass the appropriate one of these to low-level functions like this one as an argument. The choice of which function is determined a long way up the call stack, presumably before any Numba code. So that makes sense now.

@stuartarchibald
Copy link
Contributor

Numba could in theory deal with this use case, it's just not wired up to do it yet. I'll open an issue about it because it's something that's feasible. To get this working now, two options, they are a bit contrived but should let compilation succeed.

  1. Force the type information about the array that is used in the branch predicate into an argument to a function, Numba has special code to deal with predicates based on this and can eliminate dead branches. Example:
from numba import njit
import numpy as np

@njit
def bar(aggs_and_cols0, aggs_and_cols, i):
    # Numba knows how to make this into a constant and then "prune" one of the
    # branches.
    if aggs_and_cols0.ndim == 3:
        print("3D")
        tmp = aggs_and_cols0
        cat_index = aggs_and_cols[1][i]
        agg = tmp[:, :, cat_index]
        return agg
    else:
        print("2D")
        return aggs_and_cols0

@njit
def foo(*aggs_and_cols):
    i = 0
    agg = bar(aggs_and_cols[0], aggs_and_cols, i)
    xmax = agg.shape[1] - 1

# 3D agg
args = (np.zeros((3, 4, 5)), (0, 0))

foo(*args)

# 2D agg
args = (np.zeros((10, 20)), (0, 0))

foo(*args)
  1. Use the more involved but more powerful numba.extending.overload API to do the dispatch (docs https://numba.readthedocs.io/en/stable/extending/overloading-guide.html):
from numba import njit
from numba.extending import overload
import numpy as np

def agg_dispatch(agg):
    # write a python impl here if you want it to work with NUMBA_DISABLE_JIT=1
    pass

@overload(agg_dispatch)
def ol_agg_dispatch(aggs_and_cols, i):
    # In this part, you have access to the type information about aggs_and_cols
    # Typically, Numba implementations would check the types to make sure they
    # are appropriate for the implementations they are going to dispatch to.
    agg = aggs_and_cols[0]
    if agg.ndim == 2:
        def impl(aggs_and_cols, i): # i is unused, just ignore it
            print("2d agg")
            agg = aggs_and_cols[0]
            return agg
        return impl
    elif agg.ndim == 3:
        def impl(aggs_and_cols, i):
            print("3d agg")
            tmp = aggs_and_cols[0]
            cat_index = aggs_and_cols[1][i]
            agg = tmp[:, :, cat_index]
            return agg
        return impl
    else:
        raise TypeError("Not supported")


@njit
def foo(*aggs_and_cols):
    i = 0
    agg = agg_dispatch(aggs_and_cols, i)
    xmax = agg.shape[1] - 1

# 3D agg
args = (np.zeros((3, 4, 5)), (0, 0))

foo(*args)

# 2D agg
args = (np.zeros((10, 20)), (0, 0))

foo(*args)

@ianthomas23
Copy link
Member Author

@stuartarchibald Thankyou, that is really useful.

@stuartarchibald
Copy link
Contributor

@stuartarchibald Thankyou, that is really useful.

No problem. I've opened numba/numba#8021 to track, in theory I think this can be handled.

@ianthomas23
Copy link
Member Author

I have implemented @stuartarchibald's second fix. It works really well, and the changes are localised so they cannot affect anything outside of the antialiased line code.

Test added.

I'm removing the WIP label as this is ready to go if the CI passes.

@ianthomas23 ianthomas23 changed the title [WIP] Antialiased lines for categorical aggregates Antialiased lines for categorical aggregates May 4, 2022
@codecov
Copy link

codecov bot commented May 4, 2022

Codecov Report

Merging #1081 (7f402ec) into master (442f76c) will increase coverage by 0.03%.
The diff coverage is 91.66%.

@@            Coverage Diff             @@
##           master    #1081      +/-   ##
==========================================
+ Coverage   83.31%   83.34%   +0.03%     
==========================================
  Files          34       34              
  Lines        7473     7488      +15     
==========================================
+ Hits         6226     6241      +15     
  Misses       1247     1247              
Impacted Files Coverage Δ
datashader/glyphs/line.py 81.17% <86.66%> (+0.09%) ⬆️
datashader/core.py 88.08% <100.00%> (+0.13%) ⬆️
datashader/reductions.py 83.48% <100.00%> (+0.24%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 442f76c...7f402ec. Read the comment docs.

@ianthomas23
Copy link
Member Author

I have excluded the new @overloaded function from codecov as codecov never uses this numba-jitted function, it uses the non-numba-jitted function above it. Without this exclusion the addition of the function causes a decrease in codecov and hence CI failure.

Copy link
Member

@jbednar jbednar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks!

@stuartarchibald
Copy link
Contributor

I have implemented @stuartarchibald's second fix. It works really well, and the changes are localised so they cannot affect anything outside of the antialiased line code.

Great, glad the fix worked!

@philippjfr philippjfr added this to the v0.14.1 milestone Jun 10, 2022
@ianthomas23 ianthomas23 modified the milestones: v0.14.2, v0.14.1 Jul 18, 2022
@ianthomas23 ianthomas23 deleted the aa_cat_agg branch July 19, 2023 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants