Changelog#
2.2.1 (2026-11-10)#
Events.merge_table()andEpochs.merge_table(): add the columns of a behaviour logfigshare support
Example dataset
nback_001+ case studyAll plotting functions follow the same conventions
Breaking changes#
pupil_plot()isplot_pupil(),pupil_plot_segments()isplot_pupil_segments()Plotting homogenized
2.2.0 (2026-11-10)#
This version is a major refactor of the package without changing functionality (many bugs fixed, though). Scripts will need update to the new syntax.
Breaking changes#
One data class,
EyeDatainstead of per modality, pupil methods are ind.pupil, gaze methods ind.gazesimilarly, plotter classes (
EyePlotter,PupilPlotter,GazePlotter) were removedMask layers: multiple layers for reasons data may be masked
Plain numbers are ms everywhere
get_intervals()andget_epochs(event, window)GenericEyeDataremovedSegmentedEyeData.intervalsisSegmentedEyeData.epochsSegmentedEyeDataholds several channelsGroupLevelSegmentedDatawas removedscale()/unscale():center=/scale=The functions for numpy arrays are only in
pp.signalmerge_intervals()isconcat_intervals()Intervals.plot_highlight()was removed; the plots shade intervals withhighlight=downsample()stores the actual sampling rate (fs/factor)Gaze plots (heatmap, scanpath) draw y downward (screen pixels)
The time axis of all plots is in seconds by default
nblinks()was removed (useblink_stats()orlayer_stats())
Added#
scale(method=...)for common transformationsseg.pupil.baseline_correct(method=...)faster
import pypillometryNamespaces
d.pupil,d.gazeandd.plotSegments of several channels share their epochs
seg.gazefor regions of interest:roi_hits()User guide page “Regions of interest”
seg.average(by=...)averages all trials or groups of them -concat_segments()Pipelines with pipe operator:
d | pp.pupil.blinks_detect()Mask layers per channel
pp.MaskPolicyto decide which samples to maskpp.Epochs(event-locked windows)Warning for events on another clock
Events.onset(label)find onset of a special eventhighlight=andhighlight_color=inpupil_plotpypil-upgrade: converts saved files to the current formatd.plot.plot_masked(policy=...)to show the interpolated stretches"<start>"and"<end>"for interval selectorsd.layer_stats(): stats for each mask layer per channelpp.Tableandpp.RecordDictseg.plot_carpet(): all segments of a channel as an image
Fixed#
from_eyelink()with non-English day and month namesDownload progress bars hidden in
pp.nologging()fsafterdownsampledid not match the time vectorreset_timedid not shift blinks stored in time unitsFakeEyeData.sim_datawas not cut or downsampledMasks only grew: re-running a detector added to the old mask
Experimental setup and calibration were sometimes lost
plot_heatmap(show_masked=True)with NaN samplesA
plot_rangewith start after end raises an errorThe sample at the end of
plot_rangewas not plottedscale()twice thenunscale()left the data scaledpypilview --eyes right FILEtook the file for an eyeCircularROI.from_degrees/RectangularROI.from_degreespypillometry.viewer.tweak(the function) was replaced by its submodulepupil.estimate_response(eyes=...)removed parameterspupil.estimate_response()on data without events raises aValueErrorpp.nologging()could leave logging disabledsummary(),Intervals.stats()return Python numberspupil.blinks_detect()with an unknown strategycrop()with start after end or outside the dataRe-running
pupil.blinks_detect()afterpupil.blinks_interpolate()found no blinks (it read the interpolated values) and cleared the blink layer
2.1.0 (2026-11-02)#
Breaking changes#
EyeData.get_pupildata()was removed, useto_pupildata()Eventsnow need time units, sample indices areunits="samples"Converting
Intervalsbetween sample indices and time now requires the datasub_slice()was renamed tocrop();sub_slice()is a deprecated aliasget_intervals(),stat_per_event():intervalwas renamed towindow
Added#
EyeData.to_pupildata()andto_gazedata()quality_report(): data-quality measuresDefault quality thresholds based on reference data from six datasets
pypilinfo --quality/--html/--descriptions: quality reportsHDF5 files:
write_file()/write_pickle()write HDF5 if filename ends.h5Excluded periods (e.g., breaks, calibration):
exclude_intervals()remove_intervals(): remove periods (e.g., breaks)get_intervals():"<start>"and"<end>"Events from behaviour logs:
Events.from_table()sync_clocks()(fits offset and drift between clocks)Event metadata (e.g., trial, condition, reaction time):
get_intervals()passes the metadata
Changed#
pupil_estimate_response()does not use excluded periods and NaN samples for the fit; events with less thanmin_valid(default 0.5) of their response outside them are left out (coefficient NaN)get_intervals()with two events pairs each first event with the next second event (e.g.,("End Block", "Start Block")gives the breaks)Saved files (pickle and HDF5) contain a versioned state of the object. Files from earlier versions are upgraded when they are read.
Fixed#
pupil_estimate_response()uses more efficient solverdownsample()respects masked samplesset_events()stored onsets ofEventswithunits=Noneas msSegments of index intervals that are not event-locked wrong time
plot_intervals()with index intervals mixed sample indices and ms
2.0.0 (2026-10-28)#
Breaking changes#
Python 3.11 or newer is required.
inplace=Nonenow follows the object setting.fill_time_discontinuities()fills gaps with NaN (was 0), so filled-in samples are masked for all variables, not only pupil.apply_history()was removedExperimentalSetup.degrees_to_mm()take plain numbers as degreesPositive y is up in all mm coordinates
new module
pp.studyhosting all study-related io functionsInternal helpers no longer exported at the top level
blinks_merge_close()defaults toapply_mask=False(returns the merged blinks), likepupil_blinks_detect()nprange,p_asym_laplac,p_asym_laplac_kappaandtrans_logistic_vecare no longer exported at the top level (still inpp.convenience)stat_event_intervalis no longer exported at the top level (usestat_per_event())pintis no longer used, replaced byunits.py
Added#
files are now saved with compression by default
pypilinfocommand prints a summary of EDF or pickle filesTimes with units (
units=can be omitted)pp.parse_time()converts times with unitspp.set_default_inplace()andpp.get_default_inplace(): package-level default forinplace(initiallyTrue).detect_blinks_zero()acceptsblink_val=np.nanto detect runs of NaN.Reading from SSH URLs (
ssh://[user@]host[:port]/path)pypilview: interactive viewer EDF file stored pypil object
Changed#
read_pickle()saves HTTP(S) downloads to a file (temporary by default)Packaging moved from
setup.py/requirements.txttopyproject.toml.Top-level imports are static, so IDEs can complete and resolve
pp.*.detect_blinks_velocity(): velocity runs with exactlymin_onset_len/min_offset_lensamples are accepted (was: must be longer)interpolate_intervals(margin=...)uses the sign ofmargin(relative to start/end of the interval);margin[0] > 0ormargin[1] < 0raise an errorget_example_data()caches downloaded datasets in~/.cache/pypillometry(orcache_dir) instead of downloading them on every call
Fixed#
pp.signalandpp.loggingwerescipy.signaland the standard-librarylogginginstead of the package’s own submodules.fill_time_discontinuities(): samples between two gaps were dropped, so time and data had different lengthspupil_blinks_detect(): the “zero” strategy never detected blinksstat_per_event()gave wrong results for intervals not in msgroup_segments()/GroupLevelSegmentedData.from_segments()dropped masksstat_per_event()ignored masksSegmentedEyeData.plot()/GroupLevelSegmentedData.plot()warn ifmeanfctorvarfctignore masks (e.g.,np.median)meanfct/varfctwithout anaxisargument raise an errorscale()computed mean and SD including masked samplesfit_foreshortening()used masked samples for fittingGazeData.summary()was missing theeyesentryExperimentalSetup: parse string (e.g."30 deg")test_selector()raised aNameErrorfixedsub_slice()dropped all masks and did not shift stored blinksdownsample()dropped all masks and did not rescale stored blinksfill_time_discontinuities()called after creation dropped all masksHistory: internal calls no longer recorded, setters are now recorded
get_intervals()between two events fixedset_experimental_setup()lost the eye-to-screen distance when updating other parametersplot_heatmap()/plot_scanpath()with a single eye as stringget_example_data("rlmw_010_edf")failed (loader was missing)detect_blinks_zero()missed runs at the start/end of the signalpupil_estimate_baseline()raises a clear error for data with NaNEyeDataDict.copy()kept only masks for NaN valuesfit_foreshortening(intervals=...)treated all intervals as msview()of raw arrays withouttimebaseline_pupil_model()used a 4 ms instead of a 4 s response kernelpupil_blinks_interpolate()left blinks NaN when the anchor points fell into a neighbouring blink; such blinks are now mergedmerge_eyes()/pupil_merge_eyes(): samples masked in all eyes lost their values (0 or NaN, e.g., interpolated blinks)blink_onsets_mahot()(used bypupil_blinks_interpolate()) searched the offset too late for blinks near the start of the dataPupilData.from_eyelink()/GazeData.from_eyelink()raised aTypeErrorIntervals.plot_highlight(zorder=...)raised aTypeErrorIntervals.stats()raised aValueErrorfor empty intervalsIntervalsrepr showed “None” as unit for index intervalsIntervals.from_mask(units=...)labelled sample indices with time units without converting themview(highlight=...): index intervals failed for raw arrays and were converted although the x-axis is in samples; the passed Intervals were modifiedplot_intervals(show_mask=True)showed only the mask of the last eye/variableplot_masked(units=None, show_events=True)drew events in ms on an index axisplot_timeseries_segments(ylim=...)/pupil_plot_segments(ylim=...)set the limits only for the last subplotscale()rejected integers andunscale()rejected numbers formean/sdExperimentalSetup.mm_to_pixels()without screen size raised aTypeErrorinstead of a clearValueErrorExperimentalSetup.from_dict()warned about plain numbers for stored valuesdetect_blinks_velocity()raised aValueErrorif no sample was validdetect_blinks_velocity(): blinks without offset ended one past the last sample (other offsets are inclusive)fake_gaze_fixations()left trailing samples at (0, 0) when the fixations were shorter thandurationpupil_kernel()was sampled slightly slower thanfs(spacingduration/(n-1))interpolate_intervals()used masked samples (e.g., other blinks) as anchor pointsget_available_eyes()/get_available_variables()returned eyes/variables in random order; now in the order they were addedread_pickle()/read_eyelink()of URLs withoutcache_file/cache_dirleft the temporary download directory behindload_study_osf()printed to stdout instead of logging and showed the download size only withforce_download=TrueExample datasets: missing spaces in descriptions; “rlmw_002” was named “test short”
pupil_response()with freenparandtmaxpassed the unsupporteddispoption to scipy (OptimizeWarning); progress is now loggedStan baseline: the model was set up on every call and could not be compiled in a read-only install; results were not reproducible (new
seedargument)tweak(): “?” did not open the helpnblinks()was annotated as returningint(returns a dict)get_segments(): the number of samples per segment depended on the timing of the first event, sogroup_segments()could failExperimentalSetup.screen_point_to_eye_frame(): tilting the screen moved its center towards the eye (z wasd*cos(pitch)*cos(yaw)instead ofd)pupil_blinks_interpolate()skipped blinks without a velocity drop before them, leaving NaNs in the dataSegmentedEyeData.baseline_correct(),Events.filter()andplot_timeseries_segments(interv=...)failed with unitsplot_experimental_setup(calibration=...),set_experimental_setup()(updating an existing setup) andfit_foreshortening()gave spurious warningstweak()showed a wrong time axis until the view was zoomedrepr()ofFakeEyeDatawas garbled; summaries andpypilinfoprint rounded numbersAPI signatures showed
units=<object object>
Documentation#
Updated website to a more modern feel.
Updated docstrings across the package (eyedata classes, signal, plotting, intervals, io, viewer, experimental setup).
Fixed broken links, references and markup in the docs and notebooks.
Docs workflow pinned to Python 3.13.