This repository was archived by the owner on Feb 26, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 10
Expand file tree
/
Copy pathcell_densities.py
More file actions
1164 lines (1005 loc) · 42.1 KB
/
Copy pathcell_densities.py
File metadata and controls
1164 lines (1005 loc) · 42.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""Generate and save cell densities
A density value is a non-negative float number corresponding to the number of cells in mm^3.
A density field is a 3D volumetric array assigning to each voxel a density value, that is
the mean cell density within this voxel.
In BBP terminology, a density field is often referred to as a volumetric density array.
This script computes and saves the following cell densities under the form of density fields.
* overall cell density
* overall glia cell density and overall neuron density
* among glial cells:
- astrocyte density
- oligodendrocyte density
- microglia density
* among neuron cells:
- inhibitory neuron (a.k.a GAD67+) density:
- PV+ density
- SST+ density
- VIP+ density
- excitatory neuron density
Note: if M is a gene marker (e.g., GAD67), then M+ denotes the cells reacting to M (e.g., GAD67+).
Density estimates are based on datasets produced by in-situ hybridization experiments of the
Allen Institute for Brain Science (AIBS). We used in particular AIBS genetic marker datasets and
the Nissl volume of the Allen Mouse Brain Annotation Atlas.
Genetic marker stained intensity and Nissl stained intensity are assumed to be a good indicator
of the soma density in a population of interest.
It is assumed throughout that such intensities depend "almost" linearly on the cell density when
restricted to a brain region, but we shall not give a precise meaning to the word "almost".
The implementation of this module is based on the methods of
- 'A Cell Atlas for the Mouse Brain' by C. Eroe et al., 2018,
- 'Atlas of inhibitory neurons in the mouse brain' by D. Rodarie et al., 2021,
and the code provided by the authors.
"""
# pylint: disable=too-many-lines
import json
import logging
import os
from pathlib import Path
from typing import Callable, Dict
import click
import numpy as np
import pandas as pd
from atlas_commons.app_utils import (
EXISTING_FILE_PATH,
assert_properties,
common_atlas_options,
log_args,
set_verbose,
verbose_option,
)
from atlas_commons.typing import FloatArray
from voxcell import RegionMap, VoxelData # type: ignore
from atlas_densities.app.utils import AD_PATH, DATA_PATH
from atlas_densities.densities import (
excitatory_inhibitory_splitting,
inhibitory_neuron_densities_optimization,
refined_inhibitory_neuron_densities,
utils,
)
from atlas_densities.densities.cell_counts import (
extract_inhibitory_neurons_dataframe,
glia_cell_counts,
inhibitory_data,
)
from atlas_densities.densities.cell_density import compute_cell_density
from atlas_densities.densities.excel_reader import (
read_homogenous_neuron_type_regions,
read_measurements,
)
from atlas_densities.densities.fitting import linear_fitting
from atlas_densities.densities.glia_densities import compute_glia_densities
from atlas_densities.densities.inhibitory_neuron_density import compute_inhibitory_neuron_density
from atlas_densities.densities.measurement_to_density import (
measurement_to_average_density,
remove_non_density_measurements,
)
from atlas_densities.exceptions import AtlasDensitiesError
EXCITATORY_SPLIT_CORTEX_ALL_TO_EXC_MTYPES = (
DATA_PATH / "mtypes" / "mapping_cortex_all_to_exc_mtypes.csv"
)
EXCITATORY_SPLIT_METADATA = DATA_PATH / "metadata" / "excitatory-inhibitory-splitting.json"
HOMOGENOUS_REGIONS_PATH = DATA_PATH / "measurements" / "homogenous_regions.csv"
HOMOGENOUS_REGIONS_REL_PATH = HOMOGENOUS_REGIONS_PATH.relative_to(AD_PATH)
LINPROG_PATH = "doc/source/bbpp82_628_linprog.pdf"
ALGORITHMS: Dict[str, Callable] = {
"keep-proportions": refined_inhibitory_neuron_densities.create_inhibitory_neuron_densities,
"linprog": inhibitory_neuron_densities_optimization.create_inhibitory_neuron_densities,
}
L = logging.getLogger(__name__)
def _zero_negative_values(array: FloatArray) -> None:
"""
Zero negative values resulting from round-off errors.
Modifies `array` in place.
Args:
array: float numpy array. We expect most of the `array` values to be non-negative.
Negative values should be negligible when compared to positive values.
Raises:
AtlasDensitiesError if the absolute value of the sum of all negative values exceeds
1 percent of the sum of all positive values, or if the smallest negative value is
not negligible wrt to the mean of the non-negative values.
"""
negative_mask = array < 0.0
if np.count_nonzero(negative_mask) == 0:
return
non_negative_mask = np.invert(negative_mask)
if np.abs(np.sum(array[negative_mask])) / np.sum(array[non_negative_mask]) > 0.01:
raise AtlasDensitiesError(
"The absolute value of the sum of all negative values exceeds"
" 1 percent of the sum of all positive values"
)
ratio = np.abs(np.min(array[negative_mask])) / np.mean(array[non_negative_mask])
if not np.isclose(ratio, 0.0, atol=1e-08):
raise AtlasDensitiesError(
"The smallest negative value is not negligible wrt to "
"the mean of all non-negative values."
)
array[negative_mask] = 0.0
def _get_voxel_volume_in_mm3(voxel_data: "VoxelData") -> float:
"""
Returns the voxel volume of `voxel_data` in mm^3.
Note: the voxel_dimensions of `voxel_data` are assumed to be
expressed in um (micron = 1e-6 m).
Args:
voxel_data: VoxelData object whose voxel volume will be computed.
Returns:
The volume in mm^3 of a `voxel_data` voxel.
"""
return voxel_data.voxel_volume / 1e9
@click.group()
@verbose_option
def app(verbose):
"""Run the cell densities CLI"""
set_verbose(L, verbose)
@app.command()
@common_atlas_options
@click.option(
"--nissl-path",
type=EXISTING_FILE_PATH,
required=True,
help=("The path to the AIBS Nissl stains nrrd file."),
)
@click.option(
"--output-path",
type=str,
required=True,
help="Path where to write the output cell density nrrd file."
"A voxel value is a number of cells per mm^3",
)
@click.option(
"--group-ids-config-path",
type=EXISTING_FILE_PATH,
default=utils.GROUP_IDS_PATH,
help="Path to density groups ids config",
show_default=True,
)
@log_args(L)
def cell_density(annotation_path, hierarchy_path, nissl_path, output_path, group_ids_config_path):
"""Compute and save the overall mouse brain cell density.
The input Nissl stain volume of AIBS is turned into an actual density field complying with
the cell counts of several regions.
Density is expressed as a number of cells per mm^3.
The output density field array is a float64 array of shape (W, H, D) where (W, H, D)
is the shape of the input annotated volume.
The computation of the overall cell density is based on:
\b
- the Nissl stain intensity, which is supposed to represent the overall cell density, up to
region-dependent constant scaling factors.
- cell counts from the scientific literature, which are used to determine a local
linear dependency factor for each region where a cell count is available.
- the optional soma radii, used to operate a correction.
"""
annotation = VoxelData.load_nrrd(annotation_path)
nissl = VoxelData.load_nrrd(nissl_path)
# Check nrrd metadata consistency
assert_properties([annotation, nissl])
region_map = RegionMap.load_json(hierarchy_path)
group_ids_config = utils.load_json(group_ids_config_path)
overall_cell_density = compute_cell_density(
region_map,
annotation.raw,
_get_voxel_volume_in_mm3(annotation),
nissl.raw,
group_ids_config=group_ids_config,
)
nissl.with_data(overall_cell_density).save_nrrd(output_path)
@app.command()
@common_atlas_options
@click.option(
"--cell-density-path",
type=EXISTING_FILE_PATH,
required=True,
help=("The path to the overall cell density nrrd file."),
)
@click.option(
"--glia-density-path",
type=EXISTING_FILE_PATH,
required=True,
help=("The path to the unconstrained overall glia cell density nrrd file."),
)
@click.option(
"--astrocyte-density-path",
type=EXISTING_FILE_PATH,
required=True,
help=("The path to the unconstrained astrocyte density nrrd file."),
)
@click.option(
"--oligodendrocyte-density-path",
type=EXISTING_FILE_PATH,
required=True,
help=("The path to the unconstrained oligodendrocyte density nrrd file."),
)
@click.option(
"--microglia-density-path",
type=EXISTING_FILE_PATH,
required=True,
help=("The path to the unconstrained microglia density nrrd file."),
)
@click.option(
"--glia-proportions-path",
type=EXISTING_FILE_PATH,
help="Path to the json file containing the different proportions of each glia type."
"This file must hold a dictionary of the following form: "
'{"astrocyte": <proportion>, "microglia": <proportion>, "oligodendrocyte": <proportion>,'
' "glia": 1.0}',
)
@click.option(
"--output-dir",
type=str,
required=True,
help="Path to the directory where to write the output cell density nrrd files."
" It will be created if it doesn't exist already.",
)
@click.option(
"--group-ids-config-path",
type=EXISTING_FILE_PATH,
default=utils.GROUP_IDS_PATH,
help="Path to density groups ids config",
show_default=True,
)
@log_args(L)
def glia_cell_densities(
annotation_path,
hierarchy_path,
cell_density_path,
glia_density_path,
astrocyte_density_path,
oligodendrocyte_density_path,
microglia_density_path,
glia_proportions_path,
output_dir,
group_ids_config_path,
): # pylint: disable=too-many-arguments, too-many-locals
"""Compute and save the glia cell densities.
Density is expressed as a number of cells per mm^3.
The output density field arrays are float64 arrays of shape (W, H, D) where (W, H, D)
is the shape of the input annotated volume.
The computation is based on:
\b
- an estimate of the overall cell density
- estimates of unconstrained densities for the different glia cell types
- glia cell counts from the scientific literature
The cell counts and the overall cell density are used to constrain the glia cell densities
so that:
\b
- they do not exceed voxel-wise the overall cell density
- the density sums multiplied by the voxel volume match the provided cell counts
An optimization process is responsible for enforcing these constraints while keeping
the output densities as close as possible to the unconstrained input densities.
Note: optimization is not fully implemented and the current process only returns a
feasible point.
The ouput glia densities are saved in the specified output directory under the following
names:
\b
- glia_density.nrrd (overall glia density)
- astrocyte_density.nrrd
- oligodendrocyte_density.nrrd
- microglia_density.nrrd
In addition, the overall neuron cell density is inferred from the overall cell density and
the glia cell density and saved in the same directory under the name:
\b
- neuron_density.nrrd
"""
L.info("Loading annotation ...")
annotation = VoxelData.load_nrrd(annotation_path)
L.info("Loading overall cell density ...")
overall_cell_density = VoxelData.load_nrrd(cell_density_path)
if np.any(overall_cell_density.raw < 0.0):
raise AtlasDensitiesError(f"Negative density value found in {cell_density_path}.")
L.info("Loading unconstrained glia cell densities ...")
glia_densities = {
"glia": VoxelData.load_nrrd(glia_density_path),
"astrocyte": VoxelData.load_nrrd(astrocyte_density_path),
"oligodendrocyte": VoxelData.load_nrrd(oligodendrocyte_density_path),
"microglia": VoxelData.load_nrrd(microglia_density_path),
}
atlases = list(glia_densities.values()) + [annotation, overall_cell_density]
L.info("Checking input files consistency ...")
assert_properties(atlases)
L.info("Loading hierarchy ...")
region_map = RegionMap.load_json(hierarchy_path)
glia_proportions = utils.load_json(glia_proportions_path)
glia_densities = {
glia_cell_type: voxel_data.raw for glia_cell_type, voxel_data in glia_densities.items()
}
group_ids_config = utils.load_json(group_ids_config_path)
L.info("Compute volumetric glia densities: started")
glia_densities = compute_glia_densities(
region_map,
annotation.raw,
_get_voxel_volume_in_mm3(annotation),
sum(glia_cell_counts().values()),
glia_densities,
overall_cell_density.raw,
glia_proportions,
copy=False,
group_ids_config=group_ids_config,
)
if not Path(output_dir).exists():
os.makedirs(output_dir)
L.info("Saving overall neuron density to file %s", str(Path(output_dir, "neuron_density.nrrd")))
neuron_density = overall_cell_density.raw - glia_densities["glia"]
_zero_negative_values(neuron_density)
annotation.with_data(np.asarray(neuron_density, dtype=float)).save_nrrd(
str(Path(output_dir, "neuron_density.nrrd"))
)
L.info("Saving glia densities to %s", str(Path(output_dir)))
for glia_type, density in glia_densities.items():
annotation.with_data(np.asarray(density, dtype=float)).save_nrrd(
str(Path(output_dir, f"{glia_type}_density.nrrd"))
)
@app.command()
@common_atlas_options
@click.option(
"--gad1-path",
type=EXISTING_FILE_PATH,
required=True,
help=("The path to the GAD marker nrrd file."),
)
@click.option(
"--nrn1-path",
type=EXISTING_FILE_PATH,
required=True,
help=("The path to Nrn1 marker nrrd file."),
)
@click.option(
"--neuron-density-path",
type=EXISTING_FILE_PATH,
required=True,
help=(
"The path to the overall neuron density nrrd file obtained as output of the command "
"`glia_cell_densities`."
),
)
@click.option(
"--inhibitory-neuron-counts-path",
type=EXISTING_FILE_PATH,
required=False,
default=Path(Path(__file__).parent, "data", "measurements", "mmc1.xlsx"),
help=(
"The path to the excel document mmc1.xlsx of the suplementary materials of "
'"Brain-wide Maps Reveal Stereotyped Cell-Type- Based Cortical Architecture '
'and Subcortical Sexual Dimorphism" by Kim et al., 2017. '
"https://ars.els-cdn.com/content/image/1-s2.0-S0092867417310693-mmc1.xlsx. "
"Defaults to `atlas_densities/app/data/measurements/mmc1.xlsx`."
),
)
@click.option(
"--output-dir",
type=str,
required=True,
help="Path to the directory where to write the output cell density nrrd files."
" It will be created if it doesn't exist already.",
)
@click.option(
"--group-ids-config-path",
type=EXISTING_FILE_PATH,
default=utils.GROUP_IDS_PATH,
help="Path to density groups ids config",
show_default=True,
)
@log_args(L)
def inhibitory_and_excitatory_neuron_densities(
annotation_path,
hierarchy_path,
gad1_path,
nrn1_path,
neuron_density_path,
inhibitory_neuron_counts_path,
output_dir,
group_ids_config_path,
): # pylint: disable=too-many-arguments
"""Compute and save the inhibitory and excitatory neuron densities.
Density is expressed as a number of cells per mm^3.
The output density field arrays are float64 arrays of shape (W, H, D) where (W, H, D)
is the shape of the input annotated volume.
The computation is based on:
\b
- an estimate of the overall neuron density
- estimates of unconstrained inhibitory and excitatory neuron densities provided by
the GAD1 and Nrn1 markers intensities respectively.
The overall neuron density and region-specific neuron counts from the scientific literature are
used to constrain the inhibitory and excitatory neuron densities so that:
\b
- they do not exceed voxel-wise the overall neuron cell density
- the ratio (inhibitory neuron count / excitatory neuron count) matches a prescribed value
wherever it is constrained.
An optimization process is responsible for enforcing these constraints while keeping
the output densities as close as possible to the unconstrained input densities.
Note: optimization is not fully implemented and the current process only returns a
feasible point.
The output densities are saved in the specified output directory under the following
names:
\b
- inhibitory_neuron_density.nrrd
- excitatory_neuron_density.nrrd
"""
annotation = VoxelData.load_nrrd(annotation_path)
neuron_density = VoxelData.load_nrrd(neuron_density_path)
assert_properties([annotation, neuron_density])
if np.any(neuron_density.raw < 0.0):
raise AtlasDensitiesError(f"Negative density value found in {neuron_density_path}.")
region_map = RegionMap.load_json(hierarchy_path)
inhibitory_df = extract_inhibitory_neurons_dataframe(inhibitory_neuron_counts_path)
group_ids_config = utils.load_json(group_ids_config_path)
inhibitory_neuron_density = compute_inhibitory_neuron_density(
region_map,
annotation.raw,
_get_voxel_volume_in_mm3(annotation),
VoxelData.load_nrrd(gad1_path).raw,
VoxelData.load_nrrd(nrn1_path).raw,
neuron_density.raw,
inhibitory_data=inhibitory_data(inhibitory_df),
group_ids_config=group_ids_config,
)
if not Path(output_dir).exists():
os.makedirs(output_dir)
annotation.with_data(np.asarray(inhibitory_neuron_density, dtype=float)).save_nrrd(
str(Path(output_dir, "inhibitory_neuron_density.nrrd"))
)
excitatory_neuron_density = neuron_density.raw - inhibitory_neuron_density
_zero_negative_values(excitatory_neuron_density)
annotation.with_data(np.asarray(excitatory_neuron_density, dtype=float)).save_nrrd(
str(Path(output_dir, "excitatory_neuron_density.nrrd"))
)
@app.command()
@click.option(
"--measurements-output-path",
required=True,
help="Path where the density-related measurement series will be written. CSV file whose columns"
" are described in the main help section.",
)
@click.option(
"--homogenous-regions-output-path",
required=True,
help="Path where the list of AIBS brain regions with homogenous neuron type (e.g., inhibitory"
' or excitatory) will be saved. CSV file with 2 columns: "brain_region" and "cell_type".',
)
@log_args(L)
def compile_measurements(
measurements_output_path,
homogenous_regions_output_path,
):
"""
Compile the cell density related measurements of mmc3.xlsx and `gaba_papers.xsls` into a CSV
file.
In addition to various measurements found in the scientific literature, a list of AIBS mouse
brain regions with homogenous neuron type is saved to `homogenous_regions_output_path`.
Two input excel files containing measurements are handled:
\b
- `mm3c.xls` from the supplementary materials of
'Brain-wide Maps Reveal Stereotyped Cell-Type-Based Cortical Architecture and Subcortical
Sexual Dimorphism' by Kim et al., 2017.
https://ars.els-cdn.com/content/image/1-s2.0-S0092867417310693-mmc3.xlsx
- `atlas_densities/app/data/measurements/gaba_papers.xlsx`, a compilation of measurements
from the scientific literature made by Rodarie Dimitri (BBP).
This command extracts measurements from the above two files and gathers them into a unique
CSV file with the following columns:
\b
- brain_region (str), a mouse brain region name, not necessarily compliant
with AIBS 1.json file. Thus some filtering must be done when working with AIBS
annotated files.
- ``cell type`` (str, e.g, 'PV+' for cells reacting to parvalbumin, 'inhibitory neuron'
for non-specific inhibitory neuron)
- ``measurement`` (float)
- ``standard_deviation`` (non-negative float)
- ``measurement_type`` (str), see measurement types below
- ``measurement_unit`` (str), see measurement units below
- ``comment`` (str), a comment on how the measurement has been obtained
- ``source_title`` (str), the title of the article where the measurement can be exracted
- ``specimen_age`` (str, e.g., '8 week old', 'P56', '3 month old'), age of the mice used to
obtain the measurement
The different measurement types are, for a given brain region R and a given cell type T:
\b
- ``cell density``, number of cells of type T per mm^3 in R
- ``cell count``, number of cells of type T in R
- ``neuron proportion``, number of cells of type T / number of neurons in R
(a cell of type T is assumed to be a neuron, e.g., T = GAD67+)
- ``cell proportion``, number of cells of type T / number of cells in R
- ``cell count per slice``, number of cells of type T per slice of R
Measurement units:
\b
- ``cell density``: 'number of cells per mm^3'
- ``neuron proportion``: None (empty)
- ``cell proportion``: None (empty)
- ``cell count per slice``: e.g, number of cells per 50-micrometer-thick slice
See `atlas_densities/densities/excel_reader.py` for more information.
Note: This function should be deprecated once its output has been stored permanently as the
unique source of density-related measurements for the AIBS mouse brain. New measurements
should be added to the stored file (Nexus).
"""
L.info("Loading hierarchy ...")
region_map = RegionMap.load_json(Path(DATA_PATH, "1.json")) # Unmodified AIBS 1.json
L.info("Loading excel files ...")
measurements = read_measurements(
region_map,
Path(DATA_PATH, "measurements", "mmc3.xlsx"),
Path(DATA_PATH, "measurements", "gaba_papers.xlsx"),
# The next measurement file has been obtained after manual extraction
# of non-density measurements from the worksheets PV-SST-VIP and 'GAD67 densities'
# of gaba_papers.xlsx.
Path(DATA_PATH, "measurements", "non_density_measurements.csv"),
)
homogenous_regions = read_homogenous_neuron_type_regions(
Path(DATA_PATH, "measurements", "gaba_papers.xlsx")
)
L.info("Saving to CSV files ...")
measurements.to_csv(measurements_output_path, index=False)
homogenous_regions.to_csv(homogenous_regions_output_path, index=False)
@app.command()
@common_atlas_options
@click.option(
"--region-name",
type=str,
default="root",
help="Name of the root region in the hierarchy",
)
@click.option(
"--cell-density-path",
type=EXISTING_FILE_PATH,
required=True,
help=("The path to the to the overall cell density nrrd file."),
)
@click.option(
"--neuron-density-path",
type=EXISTING_FILE_PATH,
required=True,
help=("The path to the overall neuron density nrrd file."),
)
@click.option(
"--measurements-path",
type=EXISTING_FILE_PATH,
required=True,
help=(
"The path to measurements.csv, the compilation of cell density "
"related measurements of Dimitri Rodarie (BBP)."
),
)
@click.option(
"--output-path",
type=str,
required=True,
help="Path where to write the output average cell densities (.csv file), that is, a data frame"
" of the same format as the input measurements file (see --measurements-path) but comprising "
"only measurements of type ``cell density``.",
)
@log_args(L)
def measurements_to_average_densities(
annotation_path,
hierarchy_path,
region_name,
cell_density_path,
neuron_density_path,
measurements_path,
output_path,
): # pylint: disable=too-many-arguments
"""Compute and save average cell densities based on measurements and AIBS region volumes.
Measurements from Dimitri Rodarie's compilation, together with volumes from the AIBS mouse brain
(`annotation`) and precomputed volumetric cell densities (`cell_density_path` and
`neuron_density_path`) are used to compute average cell densities in every AIBS region where
sufficient information is available.
Measurements from regions which are not in the provided brain region hierarchy or not in the
provided annotation volume will be ignored. A warning with all ignored lines from the
measurements file will be displayed.
The different cell types (e.g., PV+, SST+, VIP+ or overall inhibitory neurons) and
brain regions under consideration are prescribed by the input measurements.
Measurements can be cell densities in number of cells per mm^3 for instance.
If several cell density measurements are available for the same region, the output dataframe
records the average of these measurements.
Measurements can also be cell counts, in which case the AIBS brain model volumes
(`annotation_path`) are used in addition to compute average cell densities.
For measurements such as cell proportions, neuron proportions or cell counts per slice, the
brain-wide volumetric cell densities (`cell_density_path` or `neuron_density_path`) are used to
compute average cell densities.
If several combinations of measurements yield several average cell densities for the same
region, then the output data frame records the average of these measurements.
The output average cell densities are saved in under the CSV format as a dataframe with the same
columns as the input data frame specified via `--measurements-path`.
See :mod:`atlas_densities.app.densities.compile_measurements`.
All output measurements are average cell densities of various cell types over AIBS brain
regions expressed in number of cells per mm^3.
"""
L.info("Loading annotation ...")
annotation = VoxelData.load_nrrd(annotation_path)
L.info("Loading overall cell density ...")
overall_cell_density = VoxelData.load_nrrd(cell_density_path)
if np.any(overall_cell_density.raw < 0.0):
raise AtlasDensitiesError(f"Negative density value found in {cell_density_path}.")
L.info("Loading overall neuron density ...")
neuron_density = VoxelData.load_nrrd(neuron_density_path)
if np.any(neuron_density.raw < 0.0):
raise AtlasDensitiesError(f"Negative density value found in {neuron_density_path}.")
L.info("Checking input consistency ...")
assert_properties([annotation, overall_cell_density, neuron_density])
L.info("Loading hierarchy ...")
region_map = RegionMap.load_json(hierarchy_path)
L.info("Loading measurements ...")
measurements_df = pd.read_csv(measurements_path)
L.info("Measurement to average density: started")
average_cell_densities_df = measurement_to_average_density(
region_map,
annotation.raw,
annotation.voxel_dimensions,
_get_voxel_volume_in_mm3(annotation),
overall_cell_density.raw,
neuron_density.raw,
measurements_df,
region_name,
)
remove_non_density_measurements(average_cell_densities_df)
L.info("Saving average cell densities to file %s", output_path)
average_cell_densities_df.to_csv(
output_path,
index=False,
)
@app.command()
@common_atlas_options
@click.option(
"--region-name",
type=str,
default="root",
help="Name of the root region in the hierarchy",
)
@click.option(
"--neuron-density-path",
type=EXISTING_FILE_PATH,
required=True,
help=(
"Path to the overall neuron volumetric density nrrd file obtained as output of the command "
"`glia-cell-densities`."
),
)
@click.option(
"--average-densities-path",
required=True,
help="Path to the average densities data frame, i.e., the output of measurement-to-density."
"The format of this CSV file is described in the main help section of compile-measurements. It"
" contains only measurements of type ``cell density``.",
)
@click.option(
"--homogenous-regions-path",
type=EXISTING_FILE_PATH,
required=False,
help=f"Optional path to the CSV file containing names of regions whose neurons are "
f"either all inhibitory or all excitatory. Defaults to `{HOMOGENOUS_REGIONS_REL_PATH}`.",
default=HOMOGENOUS_REGIONS_PATH,
)
@click.option(
"--marker",
multiple=True,
type=str,
required=True,
help=("Marker information in the format `marker name`:`marker id`:`path/to/marker.nrrd`"),
)
@click.option(
"--realigned-slices-path",
type=EXISTING_FILE_PATH,
required=True,
help=("JSON file containing mapping of `marker id` to list of slices selected for that marker"),
)
@click.option(
"--cell-density-standard-deviations",
type=EXISTING_FILE_PATH,
required=True,
help=("Standard deviations for cells, in a CSV file"),
)
@click.option(
"--fitted-densities-output-path",
required=True,
help="Path where to write the data frame containing the average cell density of every region"
"found in the brain hierarchy (see --hierarchy-path option) for the marked cell types "
"The output file is a CSV file whose first column is a list of region names. The other columns"
" come in pairs for each cell type: ``<cell_type>`` and ``<cell_type>_standard_deviation``."
" Cell types are derived from marker names: ``<cell_type> = <marker>+``.",
)
@click.option(
"--fitting-maps-output-path",
required=False,
help="Path to the json file containing the fitting coefficients and standard deviations"
"for each region group and each cell type.",
)
@click.option(
"--group-ids-config-path",
type=EXISTING_FILE_PATH,
default=utils.GROUP_IDS_PATH,
help="Path to density groups ids config",
show_default=True,
)
@log_args(L)
def fit_average_densities(
hierarchy_path,
annotation_path,
region_name,
neuron_density_path,
marker,
realigned_slices_path,
cell_density_standard_deviations,
average_densities_path,
homogenous_regions_path,
fitted_densities_output_path,
fitting_maps_output_path,
group_ids_config_path,
): # pylint: disable=too-many-arguments, too-many-locals
"""
Estimate average cell densities of brain regions in `hierarchy_path` for the cell types
We perform a linear fitting based on average cell densities inferred from the scientific
literature (`average_densities_path`) to estimate average cell densities in regions where
no record is available.
In addition to the records from the scientific literature, we consider as input of our fitting
the average densities of the brain regions where neurons are either all inhibitory or all
excitatory. These regions are listed in `homogenous_regions_path`. The volumetric density
`neuron_density_path` is used to compute the average density of inhibitory neurons (a.k.a
gad67+) in every homogenous region of type "inhibitory".
Regions from the literature values and homogenous regions which are not in the provided brain
region hierarchy or not in the provided annotation volume will be ignored. A warning with all
ignored lines from the measurements file will be displayed.
Our linear fitting of density values relies on the assumption that the average cell density
(number of cells per mm^3) of a cell type T in a brain region R depends linearly on the
average intensity of a gene marker of T. The conversion factor is a constant which depends only
on T and on which of the following three groups R belongs to:
\b
- isocortex
- cerebellum
- the rest
(For each cell type T, there are hence three linear fittings.)
The cerebellum was singled out because of its very high cell densities wrt the rest of the
mouse brain. The isocortex was singled out because its layer densities and cell type
compositions are quite similar across its subregions.
Each fitted density value of `fitted_densities_output_path` comes along with standard
deviation.
The standard deviations are computed differently depending on whether the output value is a
record from the scientific literature or it has been produced by applying the fitted linear
map to an average gene marker intensity - we call it a fitted value.
In the first case, the deviation is the standard deviation attached to the initial and unchanged
density in `average_densities_path`. In the second case the deviation equals the fitted value
times the standard deviation of the linear fitting.
Notes:
\b
- 2D points for which the average marker intensity or the average cell density or is 0.0 are
filtered out before fitting.
- some regions can have NaN density values for one or more cell types because they are not
covered by the selected slices of the volumetric gene marker intensities.
"""
Path(fitted_densities_output_path).parent.mkdir(parents=True, exist_ok=True)
L.info("Loading annotation ...")
annotation = VoxelData.load_nrrd(annotation_path)
L.info("Loading neuron density ...")
neuron_density = VoxelData.load_nrrd(neuron_density_path)
if np.any(neuron_density.raw < 0.0):
raise AtlasDensitiesError(f"Negative density value found in {neuron_density_path}.")
L.info("Loading hierarchy ...")
region_map = RegionMap.load_json(hierarchy_path)
slices = utils.load_json(realigned_slices_path)
gene_marker_volumes = {}
for m in marker:
marker_name, marker_id, marker_path = m.split(":", 3)
gene_marker_volumes[marker_name] = {
"intensity": VoxelData.load_nrrd(marker_path),
"slices": slices[marker_id], # list of integer slice indices
}
assert_properties(
[annotation, neuron_density]
+ [intensity["intensity"] for intensity in gene_marker_volumes.values()]
)
for volume in gene_marker_volumes.values():
volume["intensity"] = volume["intensity"].raw
cell_density_stddev = utils.load_json(cell_density_standard_deviations)
cell_density_stddev = {
# Use the AIBS name attribute as key (this is a unique identifier in 1.json)
# (Ex: former key "|root|Basic cell groups and regions|Cerebrum" -> new key: "Cerebrum")
name.split("|")[-1]: stddev
for (name, stddev) in cell_density_stddev.items()
}
group_ids_config = utils.load_json(group_ids_config_path)
L.info("Loading average densities dataframe ...")
average_densities_df = pd.read_csv(average_densities_path)
homogenous_regions_df = pd.read_csv(homogenous_regions_path)
L.info("Fitting of average densities: started")
fitted_densities_df, fitting_maps = linear_fitting(
region_map,
annotation.raw,
neuron_density.raw,
gene_marker_volumes,
average_densities_df,
homogenous_regions_df,
cell_density_stddev,
region_name=region_name,
group_ids_config=group_ids_config,
)
# Turn index into column so as to ease off the save and load operations on csv files
fitted_densities_df["brain_region"] = fitted_densities_df.index
L.info("Saving fitted densities to file %s ...", fitted_densities_output_path)
fitted_densities_df.to_csv(fitted_densities_output_path, index=False)
if fitting_maps_output_path is not None:
L.info("Saving fitting maps to file %s ...", fitting_maps_output_path)
with open(fitting_maps_output_path, mode="w+", encoding="utf-8") as file_:
json.dump(fitting_maps, file_, indent=1, separators=(",", ": "))
@app.command()
@common_atlas_options
@click.option(
"--region-name",
type=str,
default="root",
help="Name of the root region in the hierarchy",
)
@click.option(
"--neuron-density-path",
type=EXISTING_FILE_PATH,
required=True,
help=(
"Path to the overall neuron volumetric density nrrd file obtained as output of the command "
"`glia-cell-densities`."
),
)
@click.option(
"--average-densities-path",
required=True,
help="Path to the average densities data frame, e.g., the output of fit-average-densities."
" The format of this CSV file is described in the main help section of fit-average-densities.",
)
@click.option(
"--algorithm",
type=click.Choice(list(ALGORITHMS)),
required=False,
default="linprog",
help=f"Algorithm to be used. Defaults to 'linprog'. "
f"See `{str(LINPROG_PATH)}` for a description of the linear program.",
)
@click.option(
"--output-dir",
required=True,
help="Path to the directory where the volumetric inhibitory neuron density files (nrrd) will"
" be saved. If it doesn't exist already, the directory will be created.",
)
@log_args(L)
def inhibitory_neuron_densities(
hierarchy_path,
annotation_path,
region_name,
neuron_density_path,
average_densities_path,
algorithm,
output_dir,
):
"""
Create volumetric cell densities of brain regions in `hierarchy_path` for the cell types
labelling the columns of the data frame stored in `average_densities_path`.