Repository navigation
Expand file tree
/
Copy pathconfig.py
More file actions
882 lines (813 loc) · 38.7 KB
/
Copy pathconfig.py
File metadata and controls
882 lines (813 loc) · 38.7 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
"""Validated Spiral configuration with built-in defaults."""
from __future__ import annotations
import json
import math
from pathlib import Path
DEFAULT_GAP_EXPANDER_CAPACITY = 144
def filter_known_config_keys(values, allowed, *, label, warn=print):
"""Copy client settings, reporting keys absent from the current schema."""
unknown = sorted(set(values) - set(allowed))
if unknown:
warn(f"Ignoring unknown {label} keys: {unknown}")
return {key: value for key, value in values.items() if key in allowed}
_ENUMS = {
"model_flow_integration_solver": ["rk4"],
"model_flow_field_type": ["cartesian", "cylindrical", "bspline", "bspline_cylindrical"],
"track_radius_target": ["mean", "median"],
"dense_spacing_mode": ["grad_mag", "winding_model"],
"dt_target_mode": ["strip_median", "whole_object_quantile"],
"spiral_outward_sense": ["CW", "ACW"],
}
_NULL_TYPES = {
"pcl_sampling_weights": "dictionary",
"track_length_bin_weights": "vector",
"track_max_tortuosity": "number",
"loss_start_track_dt": "integer",
"loss_start_unattached_pcl_dt": "integer",
"object_store_location": "string",
"object_store_author": "string",
"object_store_link_timeout_s": "integer",
"object_store_input_position": "mapping",
"object_store_patch_uuids": "strings",
"object_store_patch_tags": "strings",
"object_store_pcl_uuids": "strings",
"object_store_pcl_tags": "strings",
"object_store_fiber_uuids": "strings",
"object_store_fiber_tags": "strings",
"object_store_track_uuids": "strings",
"object_store_track_tags": "strings",
"object_store_normal_ct_path": "string",
"object_store_normal_ct_volume_uuid": "string",
"fit_volume_uuid": "string",
"spiral_outward_sense": "string",
}
# Settings host preparation consumes irreversibly: patch materialization
# erodes patches before they reach the resident pools, the patch-loss z
# margin decides which attachments materialization keeps (and is recorded in
# the input manifest), and the dense-spacing mode decides which dense stores
# are opened. Nothing retained by a resident session can re-derive their
# effect, so they demand a full rebuild.
_PREPARED_INPUT_FIELDS = {
"patch_erode_patches",
"patch_loss_z_margin",
"dense_spacing_mode",
}
# The discretised outer-shell lookup/atlas. These are ordinary run-boundary
# settings while the resident session only rebuilds its shell polar map from
# them, but a session whose tracks were filtered against the shell at load
# (a tracks store and an outer shell both present) consumed them irreversibly:
# apply_config refuses them there, and a model-only rebuild applies run
# changes through apply_config before it releases the model, so such a
# session needs the full rebuild (see rebuild_stage).
SHELL_ATLAS_KEYS = frozenset({
"shell_num_theta_bins",
"shell_table_smooth_sigma_z",
"shell_table_smooth_sigma_theta",
"shell_min_confidence",
})
_SCALE_WITH_Z_FIELDS = {
"sample_count_patches_per_step",
"sample_count_patches_per_step_for_dt",
"sample_count_relative_winding_pcls",
"sample_count_absolute_winding_pcls",
"sample_count_unattached_pcls_per_step",
"sample_count_tracks_per_step",
"sample_count_dense_normal_points",
"sample_count_fiber_direction_points",
"sample_count_regularisation_points",
"sample_count_winding_model_relative_pairs",
"sample_count_winding_model_density_pairs",
"sample_count_minimum_spacing_independent_samples",
"sample_count_shell_samples",
}
# z_begin/z_end are deliberately heavyweight settings, always classified as
# new-fit settings so nothing treats them as cheap run-boundary knobs:
# - host input filtering: patches, PCLs, unattached strips, and tracks are
# loaded/kept only where they intersect [z_begin, z_end);
# - dense-store coverage: the Lasagna normal/grad-mag brick pools are
# materialised for exactly this z window;
# - count scaling: every scale_with_z sample count is scaled by the number
# of slices in the range relative to the 9500-slice reference;
# - rendering/preview: the preview/export z window and the output-directory
# name derive from the range;
# - model/checkpoint-domain compatibility: the flow-field parameter shapes
# cover the range (plus margin), so resuming a checkpoint requires the
# optimisation range to lie within the checkpoint's stored model z-range.
_Z_RANGE_KEYS = frozenset({"z_begin", "z_end"})
# Participation toggles: whether a supervision source is loaded at all.
_INPUT_TOGGLE_KEYS = frozenset({
"input_use_verified_patches",
"input_use_tracks",
"input_use_fibers",
"input_use_fiber_directions",
"input_use_pcl_absolute",
"input_use_pcl_relative",
"input_use_pcl_same_winding",
"input_use_pcl_drawn_control_points",
"input_use_patch_overlaps",
"input_use_normals",
"input_use_gradient_magnitude",
"input_use_winding_inference",
"input_use_outer_shell",
})
# Human-readable descriptions of the configuration keys (and of VC3D's Spiral
# panel controls) live in descriptions.json beside this module, so the text
# can be edited without touching code; see config_descriptions().
DESCRIPTIONS_PATH = Path(__file__).with_name("descriptions.json")
def config_descriptions(path=DESCRIPTIONS_PATH, *, warn=print):
"""Return {config key: description} from the descriptions file.
The file is read on every call, so edited text reaches a client on its
next catalog fetch without restarting the service. Descriptions are
presentation only: a missing or malformed file leaves the catalog
undescribed instead of failing it.
"""
try:
section = json.loads(Path(path).read_text(encoding="utf-8"))["config"]
items = section.items()
except (OSError, ValueError, KeyError, TypeError, AttributeError) as error:
warn(f"Spiral configuration descriptions unavailable ({path}): {error}")
return {}
return {key: text.strip() for key, text in items
if isinstance(text, str) and text.strip()}
def describe_catalog(catalog, descriptions=None):
"""Return a copy of catalog whose field specs carry current descriptions."""
if descriptions is None:
descriptions = config_descriptions()
schema = dict(catalog["schema"])
for section in ("fields", "run_fields"):
described = {}
for key, spec in schema[section].items():
spec = {name: value for name, value in spec.items()
if name != "description"}
if key in descriptions:
spec["description"] = descriptions[key]
described[key] = spec
schema[section] = described
return {**catalog, "schema": schema}
# Configuration keys that shape the model's parameter tensors. A checkpoint
# whose stored value for any of them differs describes a different model, and
# is refused rather than reshaped: a domain/structure change is the explicit
# rebuild path's job. Configuration metadata, so it lives here beside the
# rest of it and is readable without importing the fitter.
# model_num_flow_integration_steps is deliberately absent: the RK4 step count
# changes the map a fixed parameter set produces (by the discretisation
# difference) but reshapes nothing, so it is a run-boundary setting and a
# checkpoint written under another count loads with a printed notice.
CHECKPOINT_MODEL_SHAPE_KEYS = (
"model_flow_integration_solver",
"model_num_flow_stages", "model_flow_bounds_z_margin",
"model_flow_bounds_radius", "model_flow_voxel_resolution",
"model_flow_field_type", "model_gap_expander_logit_resolution",
"model_gap_expander_capacity_windings",
"model_gap_expander_lr_scale",
"model_gap_expander_min_gap", "model_gap_expander_softplus_bias",
"model_initial_dr_per_winding", "model_linear_z_resolution",
"fit_volume_level",
)
# Configuration keys whose every consumer is built by
# FitContext._build_model_state(). A rebuild that changes only these can keep
# the host inputs and the dense stores and re-run the model stage alone; see
# rebuild_stage() below and FitContext.rebuild_model_state().
#
# This is an audited allowlist, not a prefix rule, because two model-shaped
# keys are read during host preparation:
# - model_flow_bounds_z_margin sizes the host-side ShellPolarMap that
# load_host_inputs() filters tracks with;
# - optimizer_random_seed seeds np.random and torch.random at the top of
# load_host_inputs() and the pool generators below it, so it reaches
# every RNG-order-sensitive host decision.
# Both are therefore absent, and a key nobody has audited is absent by
# construction — the safe answer.
MODEL_STAGE_KEYS = frozenset({
"model_flow_integration_solver",
"model_num_flow_stages",
"model_flow_bounds_radius",
"model_flow_voxel_resolution",
"model_flow_field_type",
"model_flow_field_direct_lr",
"model_gap_expander_logit_resolution",
"model_gap_expander_capacity_windings",
"model_gap_expander_min_gap",
"model_gap_expander_softplus_bias",
"model_gap_expander_lr_scale",
"model_linear_z_resolution",
"model_initial_dr_per_winding",
})
_MODEL_STRUCTURE_KEYS = frozenset({
"model_flow_integration_solver",
"model_num_flow_stages",
"model_flow_bounds_z_margin",
"model_flow_bounds_radius",
"model_flow_voxel_resolution",
"model_flow_field_type",
"model_flow_field_direct_lr",
"model_gap_expander_logit_resolution",
"model_gap_expander_capacity_windings",
"model_gap_expander_lr_scale",
"model_gap_expander_min_gap",
"model_gap_expander_softplus_bias",
"model_linear_z_resolution",
"model_initial_dr_per_winding",
})
_RUN_MUTABLE_MODEL_KEYS = frozenset({
"model_flow_field_low_res_lr_scale",
"model_num_flow_integration_steps",
"model_flow_field_high_res_lr_scale_initial",
"model_flow_field_high_res_lr_scale_final",
"model_flow_field_high_res_lr_ramp_start_step",
"model_flow_field_high_res_lr_ramp_steps",
"model_sym_dirichlet_finite_difference_epsilon",
"model_gap_expander_num_windings",
})
# input_ keys: hard participation gates deciding what host preparation loads
# and which dense stores open. Always a rebuild.
_INPUT_GATE_KEYS = _INPUT_TOGGLE_KEYS | {
"input_disable_patches",
}
# pcl_ settings that shape the frozen object-store inputs: the startup link
# refresh (object_store_inputs.refresh_links), materialization and the point
# views derived from them. A resident fit cannot re-derive them; they are a
# rebuild, and FitContext.apply_config refuses them.
OBJECT_STORE_PCL_KEYS = frozenset({
"pcl_link_distance_tolerance",
"pcl_link_window_points",
"pcl_link_window_min_points",
"pcl_fiber_min_point_spacing",
"pcl_patch_overlap_pair_spacing",
"pcl_patch_overlap_max_pairs",
"pcl_use_fiber_links",
"pcl_unattached_pcl_min_point_spacing",
})
_RUN_MUTABLE_PCL_KEYS = frozenset({
"pcl_vertical_fiber_radial_offset_enabled",
"pcl_vertical_fiber_radial_offset_voxels",
"pcl_rel_winding_adjacent_patches_only",
"pcl_stratified_pcl_sampling",
"pcl_sampling_weights",
})
# The object-store input selection and frames decide what host preparation
# reads, refreshes and materializes (object_store_inputs.load_fit_inputs).
OBJECT_STORE_INPUT_KEYS = frozenset({
"object_store_location",
"object_store_author",
"object_store_link_timeout_s",
"object_store_input_position",
"object_store_refresh_links",
"object_store_full_link_refresh",
"object_store_approved_links_only",
"object_store_allow_changed_inputs",
"object_store_patch_uuids",
"object_store_patch_tags",
"object_store_pcl_uuids",
"object_store_pcl_tags",
"object_store_fiber_uuids",
"object_store_fiber_tags",
"object_store_track_uuids",
"object_store_track_tags",
"object_store_normal_ct_path",
"object_store_normal_ct_level",
"object_store_normal_ct_downsampling",
"object_store_normal_ct_volume_uuid",
"fit_volume_uuid",
"fit_volume_level",
})
NEW_FIT_KEYS = frozenset(
set(_Z_RANGE_KEYS)
| _MODEL_STRUCTURE_KEYS
| OBJECT_STORE_INPUT_KEYS
| OBJECT_STORE_PCL_KEYS
| _INPUT_GATE_KEYS
| _PREPARED_INPUT_FIELDS
| {"optimizer_random_seed", "pcl_vertical_fiber_min_auto_certainty",
"pcl_vertical_fiber_min_z_fraction", "spiral_outward_sense"}
)
_AUDITED_PREFIXES = ("model_", "input_", "pcl_", "object_store_")
def _runtime_impact(key):
if key in NEW_FIT_KEYS:
return "new_fit"
if key in _RUN_MUTABLE_MODEL_KEYS or key in _RUN_MUTABLE_PCL_KEYS:
return "run_boundary"
if key.startswith(_AUDITED_PREFIXES):
return "new_fit"
return "run_boundary"
def unaudited_prefixed_keys(keys):
"""Keys with an audited prefix that no classification table names.
Such a key is reported as "new_fit" by construction; this exists so the
tests (and anyone adding a setting) can see the omission explicitly.
"""
tables = NEW_FIT_KEYS | _RUN_MUTABLE_MODEL_KEYS | _RUN_MUTABLE_PCL_KEYS
return sorted(key for key in keys
if key.startswith(_AUDITED_PREFIXES) and key not in tables)
_known_keys_cache = None
def known_config_keys():
"""Return every configuration key defined by Config."""
global _known_keys_cache
if _known_keys_cache is None:
_known_keys_cache = frozenset(Config().as_dict())
return _known_keys_cache
def rebuild_stage(changed_keys, *, shell_filtered_tracks=False):
"""Return the earliest rebuild stage required by changed settings.
``shell_filtered_tracks`` says whether the resident session filtered its
tracks against the outer shell when it loaded them; the shell atlas keys
are then a full rebuild rather than run-boundary settings.
"""
changed = set(changed_keys)
if shell_filtered_tracks and changed & SHELL_ATLAS_KEYS:
return "all"
known = known_config_keys()
demanding = {
key for key in changed
if key not in known or _runtime_impact(key) != "run_boundary"
}
return "model" if MODEL_STAGE_KEYS.issuperset(demanding) else "all"
def _field_spec(key, default):
nullable = key in _NULL_TYPES
if key in _ENUMS:
kind = "enum"
elif nullable:
kind = _NULL_TYPES[key]
elif type(default) is bool:
kind = "boolean"
elif type(default) is int:
kind = "integer"
elif type(default) is float:
kind = "number"
elif isinstance(default, list):
kind = "vector"
elif isinstance(default, dict):
kind = "dictionary"
else:
kind = "string"
spec = {
"type": kind,
"nullable": nullable,
"label": key.split("_", 1)[-1].replace("_", " ").title(),
"runtime_impact": _runtime_impact(key),
}
if spec["runtime_impact"] == "new_fit":
# The build stage a rebuild changing only this key needs.
spec["rebuild_stage"] = "model" if key in MODEL_STAGE_KEYS else "all"
if kind in ("integer", "number"):
spec.update(
minimum=(
1 if key in {
"output_num_slices_for_visualization",
"theta_crossing_map_update_interval",
"dt_target_update_interval",
"pcl_link_window_points",
"pcl_link_window_min_points",
"pcl_patch_overlap_max_pairs",
} else 0),
maximum=(1_000_000 if key == "output_num_slices_for_visualization"
else 16 if key == "fit_volume_level"
else 1_000_000_000),
step=1 if kind == "integer" else .01,
)
if kind == "number":
spec["precision"] = 6
elif kind == "enum":
spec["values"] = _ENUMS[key]
elif kind == "vector":
spec["length"] = 3 if key == "track_length_bin_weights" else len(default)
if key in _SCALE_WITH_Z_FIELDS:
spec["scale_with_z"] = True
if key in _Z_RANGE_KEYS:
# These values remain part of the resolved/checkpoint configuration,
# but interactive clients edit them through the run-level z controls,
# not as independent advanced-JSON settings.
spec["ui_owner"] = "run"
return spec
class Config:
def __init__(self, overrides=None):
# The optimisation z window (see _Z_RANGE_KEYS for the full effect
# list). Defaults match the historical fit_spiral module
# globals for the production PHercParis4 dataset.
self.z_begin = 4000
self.z_end = 17000
self.optimizer_random_seed = 1
self.optimizer_distributed_split_batch = True
self.optimizer_learning_rate = 3e-05
self.optimizer_exp_lr_schedule = True
self.optimizer_lr_final_factor = 0.3
self.optimizer_num_training_steps = 30000
# Flow-lattice gradient conditioning (see descriptions.json and
# README.md, "Flow-gradient conditioning").
# All are read live every step, so they apply at a run boundary
# without a rebuild. Off by default.
self.optimizer_flow_grad_smoothing = False
self.optimizer_flow_grad_smoothing_sigma_voxels = 32.0
self.optimizer_flow_grad_smoothing_across_sigma_voxels = 0.0
self.optimizer_flow_grad_smoothing_low_res_sigma_voxels = 0.0
self.optimizer_flow_lazy_moments = False
self.optimizer_flow_shared_second_moment = False
self.optimizer_flow_shared_second_moment_clip_quantile = 0.99
self.optimizer_flow_grad_clip_median_multiple = 0.0
self.model_num_flow_integration_steps = 3
self.model_flow_integration_solver = "rk4"
# Stationary velocity fields composed in sequence, held as the slabs
# of the flow lattices' leading axis (see transforms.SpiralAndTransform).
self.model_num_flow_stages = 2
self.model_flow_bounds_z_margin = 160
self.model_flow_bounds_radius = 3200
self.model_flow_voxel_resolution = 16
self.model_flow_field_type = "cartesian"
self.model_flow_field_high_res_lr_scale_initial = 0.2
self.model_flow_field_high_res_lr_scale_final = 0.2
self.model_flow_field_high_res_lr_ramp_start_step = 0
self.model_flow_field_high_res_lr_ramp_steps = 1
# Both flow lattices' LRs are optimizer_learning_rate times their
# scale; the low-resolution one had no scale of its own before.
self.model_flow_field_low_res_lr_scale = 1.0
self.model_flow_field_direct_lr = True
self.model_gap_expander_logit_resolution = 24
# The physical winding estimate and the allocated transform capacity
# are deliberately separate. shell_outer_winding_idx is the active
# hypothesis; num_windings remains the legacy/fallback physical
# estimate used by exporters, while capacity only shapes the lattice.
self.model_gap_expander_num_windings = 130
self.model_gap_expander_capacity_windings = \
DEFAULT_GAP_EXPANDER_CAPACITY
self.model_gap_expander_lr_scale = 0.3
self.model_gap_expander_min_gap = 1.0
self.model_gap_expander_softplus_bias = 4.0
self.model_linear_z_resolution = 48
self.model_initial_dr_per_winding = 16.0
# Patch/PCL theta=0 topology is transformed only on this cadence. Patch
# samples use cached node potentials; generic PCL/track walks gather
# cached signed crossings.
self.theta_crossing_map_update_interval = 100
self.patch_radius_loss_margin = 0.025
self.patch_radius_loss_inv = False
self.patch_loss_z_margin = 0
self.patch_dt_norm_p = 0.5
self.patch_dt_within_patch_norm_p = 3.0
self.patch_dt_loss_margin = 0.025
self.patch_radius_within_norm_p = 3.0
self.sample_count_patches_per_step = 360
self.sample_count_patches_per_step_for_dt = 240
self.sample_count_points_per_patch = 800
self.sample_count_relative_winding_pcls = 48
self.sample_count_relative_winding_patch_pairs_per_pcl = 4
self.sample_count_absolute_winding_pcls = 48
self.sample_count_absolute_winding_points_per_pcl = 4
self.sample_count_unattached_pcls_per_step = 84
self.sample_count_unattached_pcl_points_per_step = 32
self.sample_count_tracks_per_step = 48000
self.sample_count_track_points_per_step = 96
self.sample_count_dense_normal_points = 60000
self.sample_count_fiber_direction_points = 60000
self.sample_count_regularisation_points = 4500
self.sample_count_winding_model_relative_pairs = 128000
self.sample_count_winding_model_density_pairs = 128000
self.sample_count_minimum_spacing_independent_samples = 2000
self.sample_count_patch_dt_target_points = 256
self.sample_count_dt_target_points_per_strip = 512
self.sample_count_shell_samples = 24576
# Exponent applied to patch areas when building patch sampling
# probabilities: 0 = uniform, 1 = proportional to area.
self.patch_sampling_area_exponent = 0.5
self.patch_erode_patches = 1
# Rebuild-scoped supervision-source switches. A false value is a hard
# participation gate: the source is not loaded, prepared, sampled, or
# used by losses. Loss weights and sample counts remain unchanged so
# re-enabling a source restores its previous tuning.
self.input_use_verified_patches = True
self.input_use_tracks = False
self.input_use_fibers = True
self.input_use_fiber_directions = False
self.input_use_pcl_absolute = True
self.input_use_pcl_relative = True
self.input_use_pcl_same_winding = True
self.input_use_pcl_drawn_control_points = True
# Patch-overlap links: refreshed by the store's overlap detector and
# materialized as same-surface point pairs. Off skips both.
self.input_use_patch_overlaps = True
self.input_use_normals = True
self.input_use_gradient_magnitude = True
self.input_use_winding_inference = True
self.input_use_outer_shell = True
self.input_disable_patches = False
self.pcl_rel_winding_adjacent_patches_only = True
self.pcl_stratified_pcl_sampling = True
self.pcl_sampling_weights = None
self.pcl_fiber_min_point_spacing = 40.0
self.pcl_unattached_pcl_min_point_spacing = 16.0
# The object store's point-surface link detector, run by the startup
# link refresh (object_store_inputs.refresh_links; see
# descriptions.json): a point attaches to a patch surface within this
# many reference-volume voxels; window_min_points > 1 additionally
# requires that many of a centred window of window_points consecutive
# points to lie within tolerance of a candidate before it is eligible.
self.pcl_link_distance_tolerance = 2.5
self.pcl_link_window_points = 1
self.pcl_link_window_min_points = 1
# Cross-fiber links (the store's fiber_branch relationships):
# same-winding continuations between fibers, or adjacent-winding
# links between an H and a V fiber (the V one winding inside). When
# on, linked fibers
# merge into per-component cross-patch pcls with an explicit fiber
# graph (winding ties propagate through junctions whether or not the
# junction points attach to patches), and the unattached-strip loss
# samples chain walks that hop fibers at junctions. Pending branches
# are used unless object_store_approved_links_only.
self.pcl_use_fiber_links = True
# Object-store patch overlaps become same-surface point pairs sampled
# this many reference-volume voxels apart, at most max_pairs per edge
# (object_store_inputs.materialize_patch_overlaps). Each edge weighs
# the same in the 'patch_overlaps' sampling group however many pairs
# it yields.
self.pcl_patch_overlap_pair_spacing = 8.0
self.pcl_patch_overlap_max_pairs = 16
# Vertical/horizontal classification of fiber strips, used for radial
# offsets. VC3D's manual tag wins, then its automatic
# tag when the recorded certainty (0..1) reaches the threshold, then a
# geometric fallback: a strip whose z extent is at least this fraction
# of its path length is vertical.
self.pcl_vertical_fiber_min_z_fraction = 0.8
self.pcl_vertical_fiber_min_auto_certainty = 0.5
# Vertical fibers lie on the back face of the papyrus sheet (the face
# away from the umbilicus), a few voxels off the horizontal-fiber
# front face the fit targets, along the sheet normal in the
# increasing-winding direction of the fitted spiral (the scan-space
# winding gradient, not the line to the umbilicus). When enabled,
# vertical strips' radius and DT targets sit this many scroll voxels
# along that normal outside the winding instead of on it (positive =
# increasing winding), so a vertical fiber is satisfied where it
# physically is. The
# distance is physical: it is converted to spiral radius per point by
# the transform's local stretch along the normal (and, after a
# constraint bake, by the frozen stack's accumulated stretch), not
# applied as a constant in spiral space. Horizontal strips are
# untouched.
self.pcl_vertical_fiber_radial_offset_enabled = False
self.pcl_vertical_fiber_radial_offset_voxels = 4.0
self.track_min_sample_spacing = 20.0
self.track_max_sample_spacing = 60.0
self.track_length_bin_weights = [0.0, 0.15, 0.85]
self.track_max_tortuosity = None
self.track_crossing_precompute_max = 8
self.track_max_track_crossing_per_step = 2
self.track_exclusion_radius = 16.0
self.track_radius_target = "mean"
self.track_radius_loss_margin = 0.025
self.track_radius_within_norm_p = 6.0
self.track_dt_within_track_norm_p = 3.0
self.track_dt_norm_p = 0.5
self.track_dt_loss_margin = 0.025
self.dense_grad_mag_encode_scale = 1000.0
self.dense_grad_mag_factor = 0.25
self.dense_spacing_integration_steps = 8
self.dense_spacing_mode = "winding_model"
self.winding_model_relative_pair_delta = [3, 15]
self.winding_model_huber_delta = 0.5
self.loss_weight_min_spacing = 2.0
self.loss_weight_dense_spacing_density = 12.0
self.loss_weight_patch_radius = 8.0
self.loss_weight_patch_dt = 4.0
self.loss_weight_rel_winding = 5.0
self.loss_weight_abs_winding = 5.0
self.loss_weight_unattached_pcl_radius = 2.0
self.loss_weight_unattached_pcl_dt = 4.0
# Probability of hopping onto the linked fiber at each junction while
# sampling a chain walk through a link component in the
# unattached-strip loss.
self.loss_fiber_link_branch_probability = 0.5
self.loss_weight_track_radius = 50.0
self.loss_weight_track_dt = 10.0
self.loss_weight_sym_dirichlet = 10.0
self.loss_weight_dense_normals = 100.0
self.loss_weight_fiber_directions = 0.0
self.loss_weight_dense_spacing = 12.0
self.loss_weight_umbilicus = 1.25
self.loss_weight_shell_outer = 1.0
self.dense_min_spacing_d_min_wv = 6.0
self.dense_normals_finite_difference_epsilon = 8.0
self.fiber_directions_finite_difference_epsilon = 8.0
self.model_sym_dirichlet_finite_difference_epsilon = 4.0
self.optimizer_weight_decay_gap_expander = 0.01
self.optimizer_weight_decay_flow_field = 0.0
self.loss_start_patch_dt = 25000
self.loss_start_track_dt = 25000
# First iteration after which the unattached-PCL (fiber strip) DT snap
# acts. None follows loss_start_patch_dt, the historical coupling.
self.loss_start_unattached_pcl_dt = None
self.dt_target_mode = "strip_median"
self.dt_target_floating_threshold = 0.25
# Backward-compatible alias. FitContext phase-locks whole-object DT
# targets to theta_crossing_map_update_interval and keeps both values
# synchronized when either setting is changed.
self.dt_target_update_interval = 100
self.dt_target_max_stride = 128
self.output_first_winding = 10
self.output_winding_margin = 4
self.output_step_size = 20
self.shell_outer_winding_idx = 130
self.shell_outer_winding_margin = 10
self.shell_num_theta_bins = 720
self.shell_huber_delta = 16.0
self.shell_table_smooth_sigma_z = 4.0
self.shell_table_smooth_sigma_theta = 1.0
self.shell_min_confidence = 0.25
self.output_save_png_visualizations = False
self.output_num_slices_for_visualization = 20
# Patches, fibers, point collections and tracks come from one
# vc_object_store database (object_store_inputs.load_fit_inputs).
# Startup refreshes the store's automatic PCL-patch, fiber-patch,
# patch-overlap and track-crossing links, writing them back as
# object_store_author, unless refresh_links is off; approved_links_only
# consumes only approved relationships. A checkpoint resumes only
# against identical inputs and frames unless allow_changed_inputs.
# A store file on this host, or the endpoint of the vc_object_server
# serving one (object_store_inputs.open_store).
self.object_store_location = None
self.object_store_author = None
# How long one link_objects request of a served store may take: its
# detectors run on the server, which answers when they are done. Set
# with the server's --request-deadline; None is the default 300 s.
self.object_store_link_timeout_s = None
# The inputs are a position of the store ({store, generation,
# commit_sequence}). None links and pins the store's head at load
# (object_store_inputs.prepare_position); a position is read exactly
# as pinned, without linking. The service sets it from the position
# it prepared; a client never does.
self.object_store_input_position = None
self.object_store_refresh_links = True
# A refresh skips the detectors when the store's link marker shows the
# links current, and an input update links only new objects; true
# always runs every detector over the whole selection.
self.object_store_full_link_refresh = False
self.object_store_approved_links_only = False
self.object_store_allow_changed_inputs = False
# Object selection per kind: None selects every active object with
# geometry; otherwise the listed UUIDs plus objects carrying any
# listed tag (an empty list selects nothing).
self.object_store_patch_uuids = None
self.object_store_patch_tags = None
self.object_store_pcl_uuids = None
self.object_store_pcl_tags = None
self.object_store_fiber_uuids = None
self.object_store_fiber_tags = None
self.object_store_track_uuids = None
self.object_store_track_tags = None
# CT structure-tensor normals for fiber-patch link refresh when the fit
# loads no lasagna normals: a CT zarr whose group object_store_normal_
# ct_level is downsampled by object_store_normal_ct_downsampling
# relative to the fit volume, which object_store_normal_ct_volume_uuid
# must name.
self.object_store_normal_ct_path = None
self.object_store_normal_ct_level = 0
self.object_store_normal_ct_downsampling = 1.0
self.object_store_normal_ct_volume_uuid = None
# The reference volume every fit coordinate is a voxel of; None infers
# it from the selected objects' declared frames.
self.fit_volume_uuid = None
# The pyramid level of that volume the fit works in: a fit voxel is
# 2^level of its voxels. Inputs and scroll facts are converted to it,
# and exported surfaces back to the volume's own voxels.
self.fit_volume_level = 0
# The direction ("CW" or "ACW") the spiral winds outwards, where the
# fit volume's orientation (the catalog's z direction and handedness)
# does not decide it; one that disagrees with it is refused. Read off
# the CT by a person, or from an already-fitted spiral.
self.spiral_outward_sense = None
defaults = vars(self)
fields = {key: _field_spec(key, value)
for key, value in defaults.items()}
if isinstance(overrides, (str, Path)):
overrides = json.loads(Path(overrides).read_text())
overrides = overrides or {}
unknown = set(overrides) - set(defaults)
if unknown:
raise ValueError(f"Unknown Spiral config keys: {sorted(unknown)}")
values = defaults | overrides
for key, value in values.items():
spec = fields[key]
if value is None and spec["nullable"]:
continue
valid = {
"boolean": lambda: type(value) is bool,
"integer": lambda: type(value) is int,
"number": lambda: type(value) in (int, float),
"string": lambda: isinstance(value, str),
"enum": lambda: value in spec["values"],
"vector": lambda: isinstance(value, list),
"strings": lambda: isinstance(value, list) and all(
isinstance(item, str) for item in value),
"dictionary": lambda: isinstance(value, dict),
# Any JSON object (a store position).
"mapping": lambda: isinstance(value, dict),
}
if not valid[spec["type"]]():
raise ValueError(f"Invalid value for {key}")
if spec["type"] in ("integer", "number") and not (
spec["minimum"] <= value <= spec["maximum"]):
raise ValueError(f"Out-of-range value for {key}")
if spec["type"] == "vector" and len(value) != spec["length"]:
raise ValueError(f"Invalid vector length for {key}")
if spec["type"] == "vector" and any(
type(item) not in (int, float) for item in value):
raise ValueError(f"Invalid vector value for {key}")
if spec["type"] == "dictionary" and any(
not isinstance(item_key, str)
or type(item) not in (int, float)
for item_key, item in value.items()):
raise ValueError(f"Invalid dictionary value for {key}")
for key in ("object_store_normal_ct_downsampling",):
if values[key] is not None and not (
math.isfinite(values[key]) and values[key] > 0):
raise ValueError(f"{key} must be finite and positive")
if values["model_gap_expander_capacity_windings"] < 3:
raise ValueError(
"model_gap_expander_capacity_windings must be at least 3")
if not (0.0 < values["model_gap_expander_min_gap"]
< values["model_initial_dr_per_winding"]):
raise ValueError(
"model_gap_expander_min_gap must be positive and smaller "
"than model_initial_dr_per_winding")
for key, value in overrides.items():
setattr(self, key, value)
def as_dict(self):
return vars(self).copy()
@classmethod
def catalog(cls):
resolved_defaults = cls().as_dict()
run_owned = {"z_begin", "z_end"}
defaults = {
key: value for key, value in resolved_defaults.items()
if key not in run_owned
}
fields = {
key: _field_spec(key, value)
for key, value in defaults.items()
}
run_fields = {
key: _field_spec(key, resolved_defaults[key])
for key in sorted(run_owned)
}
presets = {
path.stem: {
key: value for key, value in cls(path).as_dict().items()
if key not in run_owned
}
for path in (Path(__file__).parent / "configs").glob("*.json")
}
return describe_catalog({
"defaults": defaults,
"schema": {
# No input path can be taken by a resident session: every path
# change implies a rebuild, which is the client's default for
# a path it finds no entry for.
"paths": {},
# The keys a rebuild can apply without reloading the session's
# inputs, advertised so a client can say in advance which kind
# of rebuild its pending changes would cause: the model-stage
# allowlist plus every run-boundary key, which rebuild_stage()
# ignores because a rebuild applies those through
# FitContext.apply_config first. Authoritative answers still
# come from the service (see rebuild_stage).
"model_stage_keys": sorted(
MODEL_STAGE_KEYS | {
key for key, spec in fields.items()
if spec["runtime_impact"] == "run_boundary"}),
"fields": fields,
# API run-block fields shown in the left-side dock. They are
# catalogued for clients but deliberately absent from the
# advanced configuration defaults, fields, and presets.
"run_fields": run_fields,
},
"presets": presets,
})
class FitConfig:
"""The one explicit fitter configuration: a resolved key -> value mapping.
A thin dict-style wrapper handed to FitContext, replacing the module
global `wandb.config` object the fitter used to read. Values must
already be fully resolved (Config defaults + overrides + any z-range
scaling); FitConfig performs no resolution of its own because the
resolution policies legitimately differ per entry point (the CLI
scales-and-splits for DDP, the interactive runtime round-trips
checkpoint counts, the headless fit driver scales without splitting).
Construction copies the mapping. update() mutates in place, so every
holder of the same FitConfig (the context, its losses call sites, a
driver that recorded it) observes run-boundary configuration changes,
matching the former shared-wandb.config semantics.
"""
def __init__(self, values):
self._values = dict(values)
def __getitem__(self, key):
return self._values[key]
def __contains__(self, key):
return key in self._values
def __iter__(self):
return iter(self._values)
def __len__(self):
return len(self._values)
def get(self, key, default=None):
return self._values.get(key, default)
def keys(self):
return self._values.keys()
def items(self):
return self._values.items()
def update(self, values):
self._values.update(values)
def __repr__(self):
return f"FitConfig({self._values!r})"