Skip to content
Draft
Show file tree
Hide file tree
Changes from 105 commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
5a61b9f
drifting_generator.py - factor out get probe
JoeZiminski Jul 18, 2024
c5c1a0b
drifting_generator.py - factor out fixing of 'generate_templates_kwar…
JoeZiminski Jul 18, 2024
b010f8f
Factor out unit_factor calculation, fix wrong unit_locations dim in d…
JoeZiminski Jul 18, 2024
3a6300c
generate.py - factor out setup InjectTemplatesRecording setup.
JoeZiminski Jul 18, 2024
62d5755
generate.py - add extra outputs.
JoeZiminski Jul 23, 2024
89ebe2b
Add a debugging script.
JoeZiminski Sep 3, 2024
08c6ae3
Add session displacement generator.
JoeZiminski Sep 3, 2024
1f9ed1a
Add tests for session displacement generator.
JoeZiminski Sep 3, 2024
f5c7e49
Remove debugging script.
JoeZiminski Sep 3, 2024
7aeec39
Update docstring on 'calculate_displacement_unit_factor.
JoeZiminski Jan 8, 2025
8bceeaa
Remove print.
JoeZiminski Jan 8, 2025
2d0b93b
Add units to recording durations
JoeZiminski Mar 14, 2025
040d1ca
Remove uncessary changes to generate.py
JoeZiminski Mar 14, 2025
77f8c89
Merge branch 'add_session_displacement_generation' of github.com:JoeZ…
JoeZiminski Mar 14, 2025
27a67bb
Additional removal on generate.py
JoeZiminski Mar 14, 2025
6d36da1
typo fix (double ".")
JoeZiminski Mar 14, 2025
d66d636
Allow
JoeZiminski Mar 14, 2025
050e22a
Merge branch 'add_session_displacement_generation' of github.com:JoeZ…
JoeZiminski Mar 14, 2025
263f0d7
Make small adjustment to threshold for macOS.
JoeZiminski Mar 14, 2025
e35b2e3
Begin adding rough implementation.
JoeZiminski Jul 2, 2024
6590e0b
Doing some basic alignment.
JoeZiminski Jul 9, 2024
a5eec8f
Finish naive alignment to first session.
JoeZiminski Jul 9, 2024
d3356b3
Play around with slope drift for generate_drifting_recording.
JoeZiminski Jul 15, 2024
68525b2
Very minimal first working version.
JoeZiminski Jul 16, 2024
6ce724f
Start playing around with histogram estimation.
JoeZiminski Jul 30, 2024
d26f6fd
Continue playing with estimation.
JoeZiminski Jul 30, 2024
b95e932
Continue playing around with estimation.
JoeZiminski Jul 31, 2024
1a5402e
Add rough time estimatino.
JoeZiminski Aug 1, 2024
2355fd4
To some refactoring.
JoeZiminski Aug 12, 2024
bd49361
Add in alignment and interpolation functions.
JoeZiminski Aug 13, 2024
ed3071e
Fix scaling on histograms and bin_s estimation, add benchmarking init…
JoeZiminski Aug 14, 2024
bf49924
Adding some mp benchmarking.
JoeZiminski Aug 15, 2024
2777398
Small fixes for hpc.
JoeZiminski Aug 15, 2024
2b11de3
Add arg input for SLURM.
JoeZiminski Aug 15, 2024
66cc1be
sbatch file.
JoeZiminski Aug 15, 2024
4d46f29
small fixes to other methods.
JoeZiminski Aug 15, 2024
44921aa
Start adding widgets.
JoeZiminski Aug 21, 2024
55c26fd
Add first rough nonrigid.
JoeZiminski Aug 23, 2024
53ddf57
Try a recursive nonrigid alignment, doesn't really work.
JoeZiminski Aug 27, 2024
eb8a736
Revert "Try a recursive nonrigid alignment, doesn't really work."
JoeZiminski Aug 27, 2024
66e6466
Adding a few more options.
JoeZiminski Aug 27, 2024
ad5ed88
small changes prior to refactoring.
JoeZiminski Aug 28, 2024
d641bae
Small changes.
JoeZiminski Aug 28, 2024
3334e14
Remove old and benchmarking scripts.
JoeZiminski Aug 28, 2024
c7eb654
Begin tidying up.
JoeZiminski Aug 28, 2024
958e0b9
Major refactor, add smoothing, interpolation, better spatial binning.
JoeZiminski Aug 30, 2024
756c534
Update some notes.
JoeZiminski Aug 30, 2024
23ef544
Tidy up, expose aling to session X.
JoeZiminski Sep 2, 2024
cf4aca9
Thinking about trimmed versions and robust xcorr, leave until later.
JoeZiminski Sep 2, 2024
1558950
PLaying with large shifts.
JoeZiminski Sep 3, 2024
1bc19a2
Playing with alignment algorithm, completely fails when rigid shift l…
JoeZiminski Sep 3, 2024
d5d5ab1
some small tidying up
JoeZiminski Sep 4, 2024
1746df9
Lots of tidying up, need to look into nonrigid more something has reg…
JoeZiminski Sep 5, 2024
1bec728
Look further into rigid, it was working well, but the parameters chos…
JoeZiminski Sep 6, 2024
cde7faa
Fix corrected histogram to use same method as compute.
JoeZiminski Sep 6, 2024
be44468
Big refactor, getting close to first draft.
JoeZiminski Sep 6, 2024
aad90cf
Begin typing and documentation.
JoeZiminski Sep 12, 2024
0bce36a
Fix from motion correction function, adding typing and docstrings.
JoeZiminski Sep 13, 2024
d7fb138
Docstrings and typing for session_alignment.
JoeZiminski Sep 17, 2024
3e705b7
Continue adding docstring / types.
JoeZiminski Sep 27, 2024
a06c62c
Continue with docstrings.
JoeZiminski Sep 30, 2024
e651018
Small fixes
JoeZiminski Nov 21, 2024
975802a
Remove a TODO.
JoeZiminski Nov 21, 2024
8809f0f
Add recording shifts argument.
JoeZiminski Nov 21, 2024
cd77308
Update time estimate method.
JoeZiminski Nov 22, 2024
9aa1bfb
Fix time estimate, other tidy ups.
JoeZiminski Nov 27, 2024
3fd72fc
gaussian process first draft.
JoeZiminski Nov 29, 2024
ccf6d6d
remove some debug stuff from gp first draft.
JoeZiminski Nov 29, 2024
8c444f9
Very rough introduce 2D versions.
JoeZiminski Nov 29, 2024
06de591
Add scaling to GP, beter convergence.
JoeZiminski Dec 6, 2024
016e6b9
Tidy up.
JoeZiminski Dec 16, 2024
202ab6c
Tidy up and fixes for 'session_alignment.py'
JoeZiminski Dec 16, 2024
e49b6cb
Tidying up, begin fixing alignment alg.
JoeZiminski Dec 16, 2024
565ede3
Remove pickle files.
JoeZiminski Dec 17, 2024
a3c35b7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 17, 2024
955451e
Reformatting alignment methods and add 2D, need to tidy up.
JoeZiminski Dec 17, 2024
9eb80e6
delete big files.
JoeZiminski Dec 17, 2024
b11ce7f
Trying different alg
JoeZiminski Dec 19, 2024
e5dcdc6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 19, 2024
41f4b50
Playing with alg rigid shift but scales for better rigid shift, okay …
JoeZiminski Dec 19, 2024
27faccd
Testing on the session_alignment side.
JoeZiminski Dec 19, 2024
1c3c3d0
playing with nonrigid.
JoeZiminski Dec 20, 2024
549f6d0
Save.
JoeZiminski Dec 24, 2024
ca79a66
With additional windowing.
JoeZiminski Dec 24, 2024
150c2a3
Doing some tidying, use amplitudes much more TODO!
JoeZiminski Dec 25, 2024
f0ffa99
Remove additional improvements on alignment alg to work on later.
JoeZiminski Jan 13, 2025
62f9b2c
First draft add plotting 2D histograms.
JoeZiminski Jan 13, 2025
96a977c
Tidying up, checking alignment function.
JoeZiminski Jan 13, 2025
967e863
Add 2d histogram plot, move plots to SI widgets.
JoeZiminski Jan 13, 2025
1eec363
Update playing.py
JoeZiminski Jan 13, 2025
c6e9f06
Start adding tests.
JoeZiminski Jan 14, 2025
71ca388
Continue working on tests.
JoeZiminski Jan 15, 2025
d231e9b
Continue adding tests.
JoeZiminski Jan 16, 2025
b4b9e28
Continue! working on tests.
JoeZiminski Jan 17, 2025
42fb3de
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 17, 2025
2e77036
Tidy up tests and (slightly) improve num_iter handling.
JoeZiminski Jan 20, 2025
9bb06ec
Add how-to doc.
JoeZiminski Jan 21, 2025
3c9593b
Small changes to docs.
JoeZiminski Jan 21, 2025
7e35574
Tidying up tests.
JoeZiminski Jan 30, 2025
4bdc45c
fix rebase.
JoeZiminski Mar 14, 2025
7e9b039
Fixing after rebase.
JoeZiminski Mar 14, 2025
140a8c7
Remove files.
JoeZiminski Mar 14, 2025
74d4f9e
Fix tests.
JoeZiminski Mar 14, 2025
4542ec5
Fix bad import.
JoeZiminski Mar 14, 2025
03fdf1f
Fix tests, tidy up.
JoeZiminski Mar 14, 2025
47882a4
Some more test tidying and documentation.
JoeZiminski Mar 15, 2025
96ce603
Speed up tests.
JoeZiminski Mar 15, 2025
ad3b6ad
Continue fixing tests.
JoeZiminski Mar 16, 2025
0f8a3d4
Finalise tidying up tests.
JoeZiminski Mar 16, 2025
5a2bd1b
rework the docs to tutorial, add final TODOs.
JoeZiminski Mar 16, 2025
9d06801
Add a few more examples.
JoeZiminski Mar 19, 2025
caee4c8
Add documentation.
JoeZiminski Mar 20, 2025
20a5bfd
Fix import, remove debugging.
JoeZiminski Mar 20, 2025
cf2c6c8
Compute log transform after mean / median taken. Tidy up, fix tests.
JoeZiminski Mar 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions debugging/playing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from spikeinterface.generation.session_displacement_generator import generate_session_displacement_recordings
from spikeinterface.sortingcomponents.peak_detection import detect_peaks
from spikeinterface.sortingcomponents.peak_localization import localize_peaks

from spikeinterface.preprocessing.inter_session_alignment import (
session_alignment,
)
from spikeinterface.widgets import plot_session_alignment, plot_activity_histogram_2d
import matplotlib.pyplot as plt

import spikeinterface.full as si
import numpy as np


si.set_global_job_kwargs(n_jobs=10)


if __name__ == '__main__':

# recordings_list, _ = generate_session_displacement_recordings(
# num_units=25,
# recording_durations=[800, 800, 800],
# recording_shifts=((0, 0), (0, -400), (0, 200)), # TODO: can see how well this is recaptured by comparing the displacements to the known displacement + gradient
# non_rigid_gradient=0.3,
# seed=57, # 52
# )

# --------------------------------------------------------------------------------------
# Load / generate some recordings
# --------------------------------------------------------------------------------------

# try num units 5 and 65
recordings_list, _ = generate_session_displacement_recordings(
num_units=5,
recording_durations=[25, 25, 25],
recording_shifts=((0, 0), (0, -200), (0, 150)),
non_rigid_gradient=None,
seed=55,
)

peaks_list, peak_locations_list = session_alignment.compute_peaks_locations_for_session_alignment(
recordings_list,
detect_kwargs={"method": "locally_exclusive"},
localize_peaks_kwargs={"method": "grid_convolution"},
)

# --------------------------------------------------------------------------------------
# Do the estimation
# --------------------------------------------------------------------------------------
# For each session, an 'activity histogram' is generated. This can be `entire_session`
# or the session can be chunked into segments and some summary generated taken over then.
# This might be useful if periods of the recording have weird kinetics or noise.
# See `session_alignment.py` for docs on these settings.

non_rigid_window_kwargs = session_alignment.get_non_rigid_window_kwargs()
non_rigid_window_kwargs["rigid"] = False
# non_rigid_window_kwargs["num_shifts_global"] = 500
# non_rigid_window_kwargs["num_shifts_block"] = 24 # TODO: it makes no sense for this to be larger than the windo
non_rigid_window_kwargs["win_step_um"] = 125
non_rigid_window_kwargs["win_scale_um"] = 60

estimate_histogram_kwargs = session_alignment.get_estimate_histogram_kwargs()
estimate_histogram_kwargs["method"] = "chunked_median"
estimate_histogram_kwargs["histogram_type"] = "activity_2d"
estimate_histogram_kwargs["bin_um"] = 2
estimate_histogram_kwargs["log_scale"] = True
estimate_histogram_kwargs["weight_with_amplitude"] = False

compute_alignment_kwargs = session_alignment.get_compute_alignment_kwargs()
compute_alignment_kwargs["interpolate"] = False

corrected_recordings_list, extra_info = session_alignment.align_sessions(
recordings_list,
peaks_list,
peak_locations_list,
alignment_order="to_session_1", # "to_session_X" or "to_middle"
non_rigid_window_kwargs=non_rigid_window_kwargs,
estimate_histogram_kwargs=estimate_histogram_kwargs,
compute_alignment_kwargs=compute_alignment_kwargs,
)

plot_session_alignment(
recordings_list,
peaks_list,
peak_locations_list,
extra_info["session_histogram_list"],
**extra_info["corrected"],
spatial_bin_centers=extra_info["spatial_bin_centers"],
drift_raster_map_kwargs={"clim":(-250, 0), "scatter_decimate": 10}
)
plt.show()

if estimate_histogram_kwargs["histogram_type"] == "activity_2d":
plot_activity_histogram_2d(
extra_info["session_histogram_list"],
extra_info["spatial_bin_centers"],
extra_info["corrected"]["corrected_session_histogram_list"]
)
plt.show()
194 changes: 194 additions & 0 deletions doc/how_to/inter_session_alignment.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
How to perform inter-session alignment
======================================

In this how-to, we will assess and correct changes in probe position across multiple experimental sessions
using `inter-session alignment`.

This is often valuable for chronic-recording experiments, where the goal is to track units across sessions

A full tutorial, including details on the many settings for this procedure, can be found `here <TODO: ADD LINK (INTERNAL)>`_.

Running inter-session alignment
-------------------------------

In SpikeInterface, it is recommended to perform inter-session alignment
following within-session motion correction (if used) and before whitening.

Preprocessed recordings should first be stored in a list:

.. code:: python

recordings_list = [prepro_session_1, prepro_session_2, ...]

Here, we will simulate such an experiment by generating a pair of sessions in
which the probe is displaced 200 micrometers (μm) along its y-axis (depth).

First, we will import all required packages and functions:

.. code:: python

# TODO: should add all of the below to spikeinterface.full? (CHECK)
import spikeinterface.full as si
from spikeinterface.generation.session_displacement_generator import generate_session_displacement_recordings
from spikeinterface.preprocessing.inter_session_alignment import session_alignment
from spikeinterface.widgets import plot_session_alignment, plot_activity_histogram_2d
import matplotlib.pyplot as plt


and then generating the test recordings:

.. code:: python

recordings_list, _ = generate_session_displacement_recordings( # TODO: add to spikeinterface.full ?
num_units=5,
recording_durations=[10, 10],
recording_shifts=((0, 0), (0, 200)), # (x offset, y offset) pairs
)

We won't explicitly preprocess these recordings in this how-to, but you can imagine
preprocessing steps have already been run (e.g. filtering, common reference etc.).

To run inter-session alignment, peaks must be detected and localised
as the locations of firing neurons are used to anchor the sessions alignment.

If you are **running inter-session alignment following motion correction**, the peaks will
already be detected and localised. In this case, please jump to
:ref:`inter-session alignment after motion correction <with_motion_correction>`.

In this section we will assume motion correction was not run, so we need to compute the peaks:

.. code:: python

peaks_list, peak_locations_list = session_alignment.compute_peaks_locations_for_session_alignment(
recordings_list,
detect_kwargs={"method": "locally_exclusive"},
localize_peaks_kwargs={"method": "grid_convolution"},
)

The peak locations (before correction) can be visualised with the plotting function:

.. code:: python

plot_session_alignment(
recordings_list,
peaks_list,
peak_locations_list,
drift_raster_map_kwargs={"clim":(-250, 0)} # fix the color limit across plots for easy comparison
)
plt.show()

Now, we are ready to perform inter-session alignment. There are many options associated
with this method—the simplest way to edit these is to fetch the default options
with the getter function and make select changes as required:

.. code:: python

estimate_histogram_kwargs = session_alignment.get_estimate_histogram_kwargs()
estimate_histogram_kwargs["histogram_type"] = "activity_2d" # TODO: RENAME

corrected_recordings_list, extra_info = session_alignment.align_sessions(
recordings_list,
peaks_list,
peak_locations_list,
estimate_histogram_kwargs=estimate_histogram_kwargs
)

To assess the performance of inter-session alignment, ``plot_session_alignment()``
will plot both the original and corrected recordings:

.. code:: python

plot_session_alignment( # TODO: is this signature confusing?
recordings_list,
peaks_list,
peak_locations_list,
extra_info["session_histogram_list"],
**extra_info["corrected"],
spatial_bin_centers=extra_info["spatial_bin_centers"],
drift_raster_map_kwargs={"clim":(-250, 0)}
)
plt.show()

As we have used 2d histograms for alignment, we can also plot these with ``plot_activity_histogram_2d()``.

.. _with_motion_correction:

Inter-session alignment after motion correction
-----------------------------------------------

If motion correction has already been performed, it is possible to reuse the
previously computed peaks and peak locations, avoiding the need for re-computation.
We will use the special function` `align_sessions_after_motion_correction()`` for this case.

Critically, the last preprocessing step prior to inter-session alignment should be motion correction.
This ensures the correction for inter-session alignment will be **added directly to the motion correction**.
This is beneficial as it avoids interpolating the data (i.e. shifting the traces) more than once.

.. admonition:: Warning
:class: warning

To ensure that inter-session alignment adds the displacement directly to the motion-corrected recording
to avoid repeated interpolation, motion correction must be the final operation applied to the recording
prior to inter-session alignment.

You can verify this by confirming the recording is an ``InterpolateMotionRecording`` with:

.. code:: python

type(recording) # quick check, should print `InterpolateMotionRecording`

from spikeinterace.sortingcomponents.motion.motion_utils import InterpolateMotionRecording

assert isinstance(recording, InterpolateMotionRecording) # error if not true

``align_sessions_after_motion_correction()`` will raise an error if the passed recordings
are not all ``InterpolateMotionRecordings``.

Let's first create some test data. We can create a recording with motion errors,
then split it in two to simulate two separate sessions:

.. code:: python

# Generate the recording with motion artefact
motion_recording, _ = si.generate_drifting_recording(duration=100)
total_duration = motion_recording.get_duration()
split_time = total_duration / 2

# Split in two to simulate two sessions
recording_part1 = motion_recording.time_slice(start_time=0, end_time=split_time)
recording_part2 = motion_recording.time_slice(start_time=split_time, end_time=total_duration)


Next, motion correction is performed, storing the results in a list:

.. code:: python

# perform motion correction on each session, storing the outputs in lists
recordings_list = []
motion_info_list = []
for recording in [recording_part1, recording_part2]:

rec, motion_info = si.correct_motion(recording, output_motion_info=True, preset="rigid_fast")

recordings_list.append(rec)
motion_info_list.append(motion_info)

Now, we are ready to use ``align_sessions_after_motion_correction()``
to align the motion-corrected sessions. This function should always be used
for aligning motion-corrected sessions, as it ensures the alignment
parameters are properly matched.

We can pass any arguments directly to ``align_sessions`` using the ``align_sessions_kwargs`` argument:

.. code:: python

estimate_histogram_kwargs = session_alignment.get_estimate_histogram_kwargs()
estimate_histogram_kwargs["histogram_type"] = "activity_2d" # TODO: RENAME

align_sessions_kwargs = {"estimate_histogram_kwargs": estimate_histogram_kwargs}

corrected_recordings_list, extra_info = session_alignment.align_sessions_after_motion_correction(
recordings_list, motion_info_list, align_sessions_kwargs
)

As above, the inter-session alignment can be assessed using ``plot_session_alignment()``.
Binary file not shown.
73 changes: 73 additions & 0 deletions playing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from spikeinterface.generation import generate_drifting_recording
from spikeinterface.preprocessing.motion import correct_motion
from spikeinterface.sortingcomponents.motion.motion_interpolation import InterpolateMotionRecording
from spikeinterface.sortingcomponents.peak_detection import detect_peaks
from spikeinterface.generation.session_displacement_generator import generate_session_displacement_recordings
from spikeinterface.generation import generate_ground_truth_recording
from spikeinterface.core import get_noise_levels
from spikeinterface.sortingcomponents.peak_localization import localize_peaks


recordings_list, _ = generate_session_displacement_recordings(
num_units=5,
recording_durations=[1, 1],
recording_shifts=((0, 0), (0, 250)),
# TODO: can see how well this is recaptured by comparing the displacements to the known displacement + gradient
non_rigid_gradient=None, # 0.1, # 0.1,
seed=55, # 52
generate_sorting_kwargs=dict(firing_rates=(100, 250), refractory_period_ms=4.0),
generate_unit_locations_kwargs=dict(
margin_um=0.0,
# if this is say 20, then units go off the edge of the probe and are such low amplitude they are not picked up.
minimum_z=0.0,
maximum_z=2.0,
minimum_distance=18.0,
max_iteration=100,
distance_strict=False,
),
generate_noise_kwargs=dict(noise_levels=(0.0, 1.0), spatial_decay=1.0),
)
rec = recordings_list[1]

detect_kwargs = {
"method": "locally_exclusive",
"peak_sign": "neg",
"detect_threshold": 25,
"exclude_sweep_ms": 0.1,
"radius_um": 75,
"noise_levels": None,
"random_chunk_kwargs": {},
}
localize_peaks_kwargs = {"method": "grid_convolution"}

# noise_levels = get_noise_levels(rec, return_scaled=False)
rec_0 = recordings_list[0]
rec_1 = recordings_list[1]

peaks_before_0 = detect_peaks(rec_0, **detect_kwargs) # noise_levels=noise_levels,
peaks_before_1 = detect_peaks(rec_1, **detect_kwargs)

proc_rec_0, motion_info_0 = correct_motion(rec_0, preset="rigid_fast", detect_kwargs=detect_kwargs, localize_peaks_kwargs=localize_peaks_kwargs, output_motion_info=True)
proc_rec_1, motion_info_1 = correct_motion(rec_1, preset="rigid_fast", detect_kwargs=detect_kwargs, localize_peaks_kwargs=localize_peaks_kwargs, output_motion_info=True)

peaks_after_0 = detect_peaks(proc_rec_0, **detect_kwargs) # noise_levels=noise_levels
peaks_after_1 = detect_peaks(proc_rec_1, **detect_kwargs)


import spikeinterface.full as si
import matplotlib.pyplot as plt

# TODO: need to test multi-shank
plot = si.plot_traces(rec_1, order_channel_by_depth=True) # , time_range=(0, 0.1))
x = peaks_before_1["sample_index"] * (1/ rec_1.get_sampling_frequency())
y = rec_1.get_channel_locations()[peaks_before_1["channel_index"], 1]
plot.ax.scatter(x, y, color="r", s=2)
plt.show()

plot = si.plot_traces(proc_rec_1, order_channel_by_depth=True)
x = peaks_after_1["sample_index"] * (1/ proc_rec_1.get_sampling_frequency())
y = rec_1.get_channel_locations()[peaks_after_1["channel_index"], 1]
plot.ax.scatter(x, y, color="r", s=2)
plt.show()

breakpoint()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ test = [
"pytest-dependency",
"pytest-cov",
"psutil",

"pytest-mock",
"huggingface_hub",

# preprocessing
Expand Down
Loading
Loading