viewspec
tit.viewspec ¶
Declarative view specifications for the Viewer and the native TetraVox.
build_view(kind, ...) builds the single-subject, simulation, analysis,
group and custom layer stacks as a JSON-able ViewSpec
(contracts/openapi.yaml #/components/schemas/ViewSpec), and
:func:finish_spec attaches its scene: :func:to_tetravox_viewspec turns
the layers into the Tetravox ViewSpec v2 document
(contracts/tetravox-viewspec-v2.schema.json), which POST /api/view/open
re-roots onto host paths and writes as a .tetravox.json scene file for the
native TetraVox.
Regression notes, each pinned by a test in tests/test_viewspec.py:
- High-frequency magnE overlays are found with
:func:
tit.catalog.classify_sim_nifti(any conductivity, subject or MNI space), not with a fixed*_scalar_magnE.nii.gzglob that no real file matches. - A subject's
labeling.nii.gzatlas layer carries itslabeling_LUT.txt(VoxelAtlasManager.find_labeling_lut()). - A bundled MNI atlas's LUT is looked up as
<stem>_LUT.txtfirst, with the MASSP atlas'smassp2021_labels.txtas a special case. - The MNI atlas directory falls back to the checkout's
resources/atlasoutside the container (:func:tit.atlas.constants.mni_resources_dir). - Absolute
cal_min/cal_maxthresholds are kept whether or not a percentile window is set. kind="subject"withspace="mni"lists the MNI atlases (with their LUTs) and the subject'sT1_<id>_MNI.nii.gz.
Percentile thresholding: a heat-colormap layer built here (TI_max, magnE)
carries a percentile window ({"lo": 95, "hi": 99.9}) instead of a bare
cal_min/cal_max of None. resolve_percentiles fills in the
concrete absolute values by reading the NIfTI and computing
numpy.percentile over its non-zero voxels -- run by build_view
(GET /api/view/{kind}). A layer whose file cannot be read, or whose
voxels are all zero, keeps cal_min/cal_max as None.
See Also¶
tit.catalog : Discovery routines this module's helpers are shared with
(mni_resources_dir).
apply_scene_overrides ¶
scene, edited in place by overrides; unknown keys are ignored.
The accepted document::
{
"layers": {"<layerId>": {"visible": bool, "opacity": 0..1,
"colormap": str, "showIn3D": bool,
"showColorbar": bool, "contoursIn2D": bool,
"threshold": {"lo": num|null, "hi": num|null},
"colorMode": "tag|field|solid|label",
"clip": bool}},
"layout": "1x1|1+3|2x2|3d-only",
"camera": "A|P|L|R|S|I",
"radiological": bool,
"background": "dark|black|light" | [r, g, b, a]
}
Every value is validated against what the engine's own type accepts and
silently dropped otherwise: a stale preset from a saved selection must
not turn into a scene the app refuses to open. None returns scene
untouched, which is the whole compatibility guarantee -- absent
overrides means byte-identical output.
Source code in tit/viewspec.py
build_view ¶
build_view(kind: str, *, subject: str | None = None, simulation: str | None = None, space: str | None = None, field: str | None = None, analysis: str | None = None, atlas: str | None = None, roi: str | None = None, path: str | None = None, extras: list[str] | None = None, overrides: dict[str, Any] | None = None, files: list[str] | None = None) -> dict[str, Any] | None
Build a ViewSpec dict, or None when the request cannot resolve.
None means "unknown subject/simulation/analysis" (the route turns
that into a 404); an unrecognised kind also returns None.
extras and overrides are additive and optional (VM,
docs/dev/DECISIONS.md ยง 2026-09-06 (Native panes, job rows and notebooks)). With neither
given -- which is every caller that existed before them -- this function
returns exactly the document it returned before: extras adds no layer
and :func:apply_scene_overrides is not called at all. extras names
files to add to the layer list (:data:EXTRA_LAYERS); overrides
edits the finished scene (per-layer appearance, layout, camera,
convention, background) and is documented on
:func:apply_scene_overrides.
Source code in tit/viewspec.py
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 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 | |
viewer_candidates ¶
viewer_candidates(subject: str | None = None, simulation: str | None = None, space: str | None = None) -> list[dict[str, Any]]
Everything this subject (and simulation) offers the Viewer's "+ Addโฆ".
Grouped the way a person looks for a file -- the head model, the atlases,
the simulation's own outputs, its analyses -- and every entry is a real
file that exists right now, with its size, because the point of the list
is to choose without guessing. Files that a scene cannot use are not
listed at all: FreeSurfer .annot parcellations, .mat matrices,
logs and reports are not volumes or meshes.
This is a read: it opens nothing and writes nothing.
Source code in tit/viewspec.py
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 | |
viewer_tree ¶
viewer_tree(subject: str | None = None, space: str | None = None, simulations: list[str] | None = None) -> dict[str, Any]
What the Menu's composition tree draws, for one subject.
simulations is what the person has expanded/selected: analyses are listed only for those, because a subject with a dozen simulations has a dozen Analyses directories and listing all of them turns a menu into a file browser. Anatomy is always listed; it is what a scene starts from.
A read. It opens nothing, writes nothing and reads no voxels.
Source code in tit/viewspec.py
clear_percentile_cache ¶
Forget every memoised window. For tests; nothing in the server calls it.
Both in-process caches, because since the two percentile paths were joined
(:func:_resolve_layer_percentile) a window can be memoised in either: clearing only one
would leave a test asserting "this file is read again" passing for the wrong reason. The
on-disk sidecars are not removed -- they live in the project under test's own tmp directory
and are keyed by (size, mtime_ns), so they cannot leak between tests.
Source code in tit/viewspec.py
resolve_percentiles ¶
Resolve every layer's percentile window to concrete cal_min/
cal_max, in place, and return spec.
Layers are resolved concurrently on a small thread pool -- reading a
NIfTI and computing numpy.percentile both release the GIL for most
of their time, so a multi-layer ViewSpec (e.g. TI_max + a high-
frequency envelope) stays well under the ~2s budget for a 256^3 volume
even when several layers need a percentile scan at once.
Source code in tit/viewspec.py
stats_cache_dir ¶
stats_cache_dir() -> str | None
Where the on-disk statistics sidecars live, or None with no project open.
<project>/.ti-toolbox/cache/stats. In the project rather than in a home directory --
the project is the unit people copy and archive, and a window computed from a file belongs
with that file's project -- but hidden, because it is regenerable and not a result.
Source code in tit/viewspec.py
prefetch_volume_stats ¶
Warm :func:_volume_stats for paths concurrently.
Reading a NIfTI and computing a percentile both release the GIL for nearly all of their time, so the four volumes of a typical simulation scene cost about as long as the slowest one rather than the sum of all four. This is the cold half of the fix; the sidecar is the warm half.
Source code in tit/viewspec.py
to_tetravox_viewspec ¶
The real Tetravox ViewSpec (v2) for an already-resolved spec (pure I/O-wise).
Every file, LUT, colormap, opacity and visibility comes from
spec["layers"], which :func:build_view already decided -- this
function only reshapes that decision into the engine's vocabulary (plus
the read-only voxel-statistics lookups documented on
:func:_volume_stats/:func:_volume_scale). A hidden layer's dataset
carries no special "lazy" marker (unlike the retired TitScene): a
real DatasetRef has no such field, and the embed's own host code
decides when to fetch each dataset from ViewSpec.layers[].visible.
Validated against contracts/tetravox-viewspec-v2.schema.json (the
hand-written subset this function emits) by
tests/test_viewspec_scene.py.
Source code in tit/viewspec.py
2760 2761 2762 2763 2764 2765 2766 2767 2768 2769 2770 2771 2772 2773 2774 2775 2776 2777 2778 2779 2780 2781 2782 2783 2784 2785 2786 2787 2788 2789 2790 2791 2792 2793 2794 2795 2796 2797 2798 2799 2800 2801 2802 2803 2804 2805 2806 2807 2808 2809 2810 2811 2812 2813 2814 2815 2816 2817 2818 2819 2820 2821 2822 2823 2824 2825 2826 2827 2828 2829 2830 2831 2832 2833 2834 2835 2836 2837 2838 2839 2840 2841 2842 2843 2844 2845 2846 2847 2848 2849 2850 2851 2852 2853 2854 2855 2856 2857 2858 2859 2860 2861 2862 2863 2864 2865 2866 2867 2868 2869 2870 2871 2872 2873 2874 2875 2876 2877 2878 2879 2880 2881 2882 2883 2884 2885 2886 2887 2888 2889 2890 2891 2892 2893 2894 2895 2896 2897 2898 2899 2900 2901 2902 2903 2904 2905 2906 2907 2908 2909 2910 2911 2912 2913 2914 2915 2916 2917 2918 2919 2920 2921 2922 2923 2924 2925 2926 2927 2928 2929 2930 2931 2932 2933 2934 2935 2936 2937 2938 2939 2940 2941 2942 2943 2944 2945 2946 2947 2948 2949 2950 2951 2952 2953 2954 2955 2956 2957 2958 2959 2960 2961 2962 2963 2964 2965 2966 2967 2968 2969 2970 2971 2972 2973 2974 2975 2976 2977 2978 2979 2980 2981 2982 2983 2984 2985 2986 2987 2988 2989 2990 2991 2992 2993 2994 2995 2996 2997 2998 2999 3000 3001 3002 3003 3004 3005 3006 3007 3008 3009 3010 3011 3012 3013 3014 3015 3016 3017 3018 3019 3020 3021 3022 3023 3024 3025 3026 3027 3028 3029 3030 3031 3032 3033 3034 3035 3036 3037 3038 3039 3040 3041 3042 3043 3044 3045 3046 3047 3048 3049 3050 3051 3052 3053 3054 3055 3056 3057 3058 3059 3060 3061 3062 3063 3064 3065 3066 3067 3068 3069 3070 3071 3072 3073 3074 3075 3076 3077 3078 3079 3080 3081 3082 3083 3084 | |
finish_spec ¶
finish_spec(spec: dict[str, Any], *, title: str | None = None, cursor: list[float] | None = None) -> dict[str, Any]
Attach spec's scene (and its cursor, when given) in place.
The single place the scene is derived, so GET /api/view/{kind} (via
:func:_finish) and every other caller get it from the same rules.
title has nowhere to go in a real ViewSpec (unlike the retired
TitScene, it carries no title field) and is accepted only for call-
site compatibility with :func:build_view's existing _scene_title
plumbing; it is not part of the returned document.
Source code in tit/viewspec.py
jail_roots ¶
Directories a viewer/file path is allowed to resolve into.
Source code in tit/viewspec.py
raw_jail_roots ¶
Directories GET /api/files/raw may stream bytes out of.
The viewer jail, narrowed: the project plus the bundled atlas directory
only, not the whole resources/ tree :func:jail_roots allows. The
wider root exists so a launcher can hand Freeview any bundled reference
file; the raw route hands the browser bytes from the app's own origin,
and resources/ also holds patches and scripts that have no business
being fetchable there.
Source code in tit/viewspec.py
resolve_jailed ¶
raw_path resolved to an existing file inside :func:jail_roots, or None.
Pure and exception-free by design (domain layer, no HTTP concerns): a
caller that needs a 403/404 distinction wraps this; :func:build_view
just treats None the same as any other "can't resolve this" case.