From 09fcf7727e93f76c6502eade664519c9f5d7b8bc Mon Sep 17 00:00:00 2001 From: wallematthias Date: Mon, 22 Jun 2026 14:35:12 -0600 Subject: [PATCH 01/67] Add cleaned FEA CLI workflow --- ogo/cli/GenerateFEM.py | 462 +++++++++++++++++++++++++--- ogo/cli/ref/SpineCompressionFe.py | 65 +++- ogo/util/faim.py | 485 ++++++++++++++++++++++++++++++ tests/cli/test_GenerateFEM.py | 311 ++++++++++++++++--- tests/util/test_faim.py | 92 ++++++ 5 files changed, 1317 insertions(+), 98 deletions(-) create mode 100644 ogo/util/faim.py create mode 100644 tests/util/test_faim.py diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 2de9041..14ada50 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -1,56 +1,436 @@ +"""Generate finite-element models for spine vertebrae and hip femurs. + +This module is intentionally a thin orchestration layer. The scientific model +generation remains in ``ogo.cli.ref.SpineCompressionFe`` and +``ogo.cli.ref.SidewaysFallFe``; this command provides a clean user-facing entry +point for running one or many targets. +""" + import argparse +from collections import namedtuple +import importlib.util +from pathlib import Path import sys +from typing import Callable, List, Optional, Sequence + + +SpineTarget = namedtuple("SpineTarget", ["level", "body_label", "process_label"]) + +SPINE_PRESETS = { + "none": [], + "benchmark-linear": [ + "--fe_displacement", + "-0.2", + "--elastic_E_func", + "kopperdahl_trab_E", + "--cort_elastic_E_func", + "kopperdahl_trab_E", + ], + "benchmark-nonlinear": [ + "--fe_displacement", + "-2.0", + "--pmma_yield_compression", + "70.0", + "--pmma_yield_tension", + "70.0", + "--elastic_E_func", + "kopperdahl_trab_E", + "--yield_comp_func", + "kopperdahl_trab_yc", + "--yield_tens_func", + "kopperdahl_trab_yc", + "--cort_elastic_E_func", + "kopperdahl_trab_E", + "--cort_yield_comp_func", + "kopperdahl_trab_yc", + "--cort_yield_tens_func", + "kopperdahl_trab_yc", + ], +} + + +def remove_extension(path: Path) -> str: + """Remove common medical-image extensions while keeping the useful stem.""" + name = path.name + if name.endswith(".nii.gz"): + return name[:-7] + return path.stem + + +def parse_spine_target(value: str) -> SpineTarget: + """Parse ``LEVEL:BODY_LABEL:PROCESS_LABEL`` target syntax.""" + parts = [part.strip() for part in value.replace(",", ":").split(":")] + if len(parts) != 3 or not all(parts): + raise argparse.ArgumentTypeError( + "spine targets must use LEVEL:BODY_LABEL:PROCESS_LABEL, for example L1:2:1" + ) + level, body_label, process_label = parts + try: + return SpineTarget(level=level, body_label=int(body_label), process_label=int(process_label)) + except ValueError as exc: + raise argparse.ArgumentTypeError("body and process labels must be integers") from exc + + +def expand_sides(sides: Sequence[str]) -> List[str]: + """Expand the friendly hip side syntax into concrete left/right jobs.""" + expanded = [] # type: List[str] + for side in sides: + if side == "both": + expanded.extend(["left", "right"]) + elif side not in expanded: + expanded.append(side) + return expanded + + +def spine_preset_args(name: str) -> List[str]: + """Return a copy of the lower-level arguments for one spine preset.""" + return list(SPINE_PRESETS[name]) + + +def build_spine_command( + *, + calibrated_image: Path, + bone_mask: Path, + target: SpineTarget, + output_path: Optional[Path], + extra_args: Sequence[str], +) -> List[str]: + """Build the lower-level spine FE generator command for one vertebra.""" + cmd = [ + str(calibrated_image), + str(bone_mask), + "--mask_threshold", + str(target.body_label), + "--process_mask_threshold", + str(target.process_label), + "--appendix", + target.level, + ] + if output_path is not None: + cmd.extend(["--output_path", str(output_path)]) + cmd.extend(extra_args) + return cmd + + +def build_femur_command( + *, + calibrated_image: Path, + bone_mask: Path, + side: str, + output_path: Optional[Path], + extra_args: Sequence[str], +) -> List[str]: + """Build the lower-level femur FE generator command for one side.""" + femur_side = {"left": "1", "right": "2"}[side] + cmd = [ + str(calibrated_image), + str(bone_mask), + "--femur_side", + femur_side, + ] + if output_path is not None: + cmd.extend(["--output_path", str(output_path)]) + cmd.extend(extra_args) + return cmd + + +def expected_spine_model_path(calibrated_image: Path, output_path: Optional[Path], target: SpineTarget) -> Path: + """Predict the spine model path written by ``OgoSpineCompressionFe``.""" + output_dir = output_path if output_path is not None else calibrated_image.parent + return output_dir / "{}_vertebra_{}_{}.n88model".format( + remove_extension(calibrated_image), + target.body_label, + target.level, + ) + + +def expected_femur_model_path(calibrated_image: Path, output_path: Optional[Path], side: str) -> Path: + """Predict the femur model path written by ``OgoSidewaysFallFe``.""" + output_dir = output_path if output_path is not None else calibrated_image.parent + suffix = "_LT_FEMUR_SF" if side == "left" else "_RT_FEMUR_SF" + return output_dir / "{}{}.n88model".format(remove_extension(calibrated_image), suffix) + + +def option_value(argv: Sequence[str], option: str, default: Optional[str] = None) -> Optional[str]: + """Return the final value for an option from an argv-style list.""" + value = default + for index, token in enumerate(argv): + if token == option and index + 1 < len(argv): + value = argv[index + 1] + return value + + +def solve_model( + model_path: Path, + args: argparse.Namespace, + model_type: str, + generator_argv: Sequence[str], +) -> None: + """Run the generic FAIM adapter on one generated model.""" + try: + from ogo.util.faim import run_faim_pipeline + except ModuleNotFoundError: + # Support direct script use: python ogo/cli/GenerateFEM.py ... + module_path = Path(__file__).resolve().parents[1] / "util" / "faim.py" + spec = importlib.util.spec_from_file_location("ogo_util_faim", str(module_path)) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + run_faim_pipeline = module.run_faim_pipeline + + # Spine compression uses z reaction force; sideways-fall hip uses y. + if model_type == "spine": + analysis_var = "fz_ns1" + pistoia_vars = ["pis_fz_fail", "pis_stiffz"] + failure_axis = "z" + else: + analysis_var = "fy_ns1" + pistoia_vars = ["pis_fy_fail", "pis_stiffy"] + failure_axis = "y" + + applied_displacement = option_value(generator_argv, "--fe_displacement", "-1.0") + + run_faim_pipeline( + model_file=model_path, + output_prefix=model_path.with_suffix(""), + analysis_var=analysis_var, + pistoia_vars=pistoia_vars, + failure_axis=failure_axis, + threads=args.threads, + conda_env=args.faim_env, + conda_executable=args.conda_executable, + install_root=args.faim_install_root, + bin_dir=args.faim_bin_dir, + license_dir=args.faim_license_dir, + faim_command=args.faim_command, + n88modelinfo_command=args.n88modelinfo_command, + n88derivedfields_command=args.n88derivedfields_command, + n88postfaim_command=args.n88postfaim_command, + n88pistoia_command=args.n88pistoia_command, + n88tabulate_command=args.n88tabulate_command, + n88copymodel_command=args.n88copymodel_command, + critical_volume=args.critical_volume, + critical_strain=args.critical_strain, + exclude=args.exclude, + applied_displacement=applied_displacement, + target_displacement=args.target_displacement, + compress=not args.no_compress, + require_pistoia=args.require_pistoia, + dry_run=args.dry_run, + ) + + +def _call_cli(main_func: Callable[[], None], program: str, argv: Sequence[str]) -> None: + old_argv = sys.argv + try: + sys.argv = [program, *argv] + main_func() + finally: + sys.argv = old_argv + + +def run_spine_command(argv: Sequence[str]) -> None: + from ogo.cli.ref.SpineCompressionFe import main as spine_compression_main + + _call_cli(spine_compression_main, "OgoSpineCompressionFe", argv) + + +def run_femur_command(argv: Sequence[str]) -> None: + from ogo.cli.ref.SidewaysFallFe import main as sideways_fall_main + + _call_cli(sideways_fall_main, "OgoSidewaysFallFe", argv) + -from ogo.cli.ref.SidewaysFallFe import main as sideways_fall_main # Adjust import based on file structure -from ogo.cli.ref.SpineCompressionFe import main as spine_compression_main # Adjust import based on file structure - -def main(): - """ - Wrapper function to run the vertebra or femur FE model setup. - 'One day we will have a better way to do this.' - Matthias Walle - - - vertebra (OgoSpineCompressionFe): - Required Arguments: - - calibrated_image: Path to the *_K2HPO4.nii image file. - - bone_mask: Path to the *_MASK.nii bone mask image. - - --vertebral_body_label: Label for the vertebral body in the mask. - - --vertebral_process_label: Label for the vertebral process in the mask. - Optional Arguments: - - Refer to OgoSpineCompressionFe documentation for additional parameters. - - - femur (ogoSidewaysFallFe): - Required Arguments: - - calibrated_image: Path to the *_K2HPO4.nii image file. - - bone_mask: Path to the *_MASK.nii image file. - - --femur_side: 1 for left femur, 2 for right femur. - Optional Arguments: - - Refer to ogoSidewaysFallFe documentation for additional parameters. - """ +def print_dry_run(program: str, argv: Sequence[str]) -> None: + print(" ".join([program, *argv])) + + +def _add_common_image_args(parser: argparse.ArgumentParser) -> None: + parser.add_argument("calibrated_image", type=Path, help="Calibrated density image.") + parser.add_argument("bone_mask", type=Path, help="Bone mask or labelled bone mask.") + parser.add_argument( + "--output_path", + type=Path, + default=None, + help="Directory for generated .n88model files. Defaults to the input image directory.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Print generated lower-level commands without running model generation or solving.", + ) + parser.add_argument( + "--no-solve", + action="store_true", + help="Only generate the .n88model; skip FAIM solve and postprocessing.", + ) + parser.add_argument("--threads", type=int, default=4, help="FAIM solver thread count.") + parser.add_argument("--faim_env", default=None, help="Optional conda environment for FAIM/N88 tools.") + parser.add_argument("--conda_executable", default="conda", help="Conda executable for --faim_env.") + parser.add_argument("--faim_install_root", default=None, help="Optional FAIM install root.") + parser.add_argument("--faim_bin_dir", default=None, help="Optional directory containing FAIM/N88 commands.") + parser.add_argument("--faim_license_dir", default=None, help="Optional Numerics88 license directory.") + parser.add_argument("--faim_command", default=None, help="Override FAIM solver command.") + parser.add_argument("--n88modelinfo_command", default=None, help="Override n88modelinfo command.") + parser.add_argument("--n88derivedfields_command", default=None, help="Override n88derivedfields command.") + parser.add_argument("--n88postfaim_command", default=None, help="Override n88postfaim command.") + parser.add_argument("--n88pistoia_command", default=None, help="Override n88pistoia command.") + parser.add_argument("--n88tabulate_command", default=None, help="Override n88tabulate command.") + parser.add_argument("--n88copymodel_command", default=None, help="Override n88copymodel command.") + parser.add_argument( + "--critical_volume", + type=float, + default=2.0, + help="Pistoia critical volume percentage.", + ) + parser.add_argument( + "--critical_strain", + type=float, + default=0.007, + help="Pistoia critical EES strain.", + ) + parser.add_argument("--exclude", type=int, default=5000, help="Material ID excluded by Pistoia.") + parser.add_argument( + "--target_displacement", + type=float, + default=0.2, + help="Standard displacement threshold used for the rescaled load endpoint.", + ) + parser.add_argument( + "--require_pistoia", + action="store_true", + help="Fail if Pistoia postprocessing fails.", + ) + parser.add_argument( + "--no_compress", + action="store_true", + help="Skip n88copymodel --compress after solving.", + ) + + +def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( + prog="ogoGenerateFEM", description=( - "Wrapper to select vertebra or femur model setup.\n\n" - "Model Types:\n" - " vertebra: Run the OgoSpineCompressionFe script to set up an L4 vertebral compression FE model.\n" - " femur: Run the ogoSidewaysFallFe script to set up a sideways fall FE model for the femur." + "Generate N88 finite-element models for spine vertebrae and hip femurs. " + "Unknown options are forwarded to the selected lower-level FE generator." ), - formatter_class=argparse.RawTextHelpFormatter, ) - parser.add_argument( - "--model_type", - choices=["vertebra", "femur"], + subparsers = parser.add_subparsers(dest="model_type") + + spine = subparsers.add_parser( + "spine", + help="Generate one or more vertebral compression FE models.", + description=( + "Generate spine FE models. Repeat --vertebra for each level to run. " + "Each target uses LEVEL:BODY_LABEL:PROCESS_LABEL." + ), + ) + _add_common_image_args(spine) + spine.add_argument( + "--vertebra", + action="append", + type=parse_spine_target, required=True, - help="Specify the model type to run: 'vertebra' or 'femur'.", + metavar="LEVEL:BODY_LABEL:PROCESS_LABEL", + help="Vertebral target, for example L1:2:1. Repeat for all levels to process.", + ) + spine.add_argument( + "--preset", + choices=sorted(SPINE_PRESETS), + default="benchmark-linear", + help=( + "Spine FE parameter preset. The default benchmark-linear preset uses the " + "public spineFE-benchmark linear model settings; use none to pass only " + "explicit lower-level options." + ), ) - args, remaining_args = parser.parse_known_args() + hip = subparsers.add_parser( + "hip", + help="Generate hip sideways-fall FE models for left, right, or both femurs.", + ) + _add_common_image_args(hip) + hip.add_argument( + "--side", + action="append", + choices=["left", "right", "both"], + default=None, + help="Femur side to process. Repeatable; defaults to both.", + ) + return parser + + +def _run_legacy(argv: Sequence[str]) -> None: + """Keep the historical ``--model_type`` dispatch working.""" + parser = argparse.ArgumentParser(prog="ogoGenerateFEM") + parser.add_argument("--model_type", choices=["vertebra", "femur"], required=True) + args, remaining = parser.parse_known_args(argv) if args.model_type == "vertebra": - sys.argv = ["OgoSpineCompressionFe"] + remaining_args - spine_compression_main() - elif args.model_type == "femur": - sys.argv = ["OgoSidewaysFallFe"] + remaining_args - sideways_fall_main() + run_spine_command(remaining) + else: + run_femur_command(remaining) + + +def main(argv: Optional[Sequence[str]] = None) -> None: + argv = list(sys.argv[1:] if argv is None else argv) + if "--model_type" in argv: + _run_legacy(argv) + return + + parser = build_parser() + args, extra_args = parser.parse_known_args(argv) + + if args.model_type is None: + parser.error("model type is required: choose spine or hip") + + if args.model_type == "spine": + spine_extra_args = spine_preset_args(args.preset) + list(extra_args) + for target in args.vertebra: + cmd = build_spine_command( + calibrated_image=args.calibrated_image, + bone_mask=args.bone_mask, + target=target, + output_path=args.output_path, + extra_args=spine_extra_args, + ) + if args.dry_run: + print_dry_run("OgoSpineCompressionFe", cmd) + else: + run_spine_command(cmd) + if not args.no_solve: + solve_model( + expected_spine_model_path(args.calibrated_image, args.output_path, target), + args, + "spine", + cmd, + ) + return + + if args.model_type == "hip": + for side in expand_sides(args.side or ["both"]): + cmd = build_femur_command( + calibrated_image=args.calibrated_image, + bone_mask=args.bone_mask, + side=side, + output_path=args.output_path, + extra_args=extra_args, + ) + if args.dry_run: + print_dry_run("OgoSidewaysFallFe", cmd) + else: + run_femur_command(cmd) + if not args.no_solve: + solve_model( + expected_femur_model_path(args.calibrated_image, args.output_path, side), + args, + "hip", + cmd, + ) + return + + parser.error(f"Unsupported model type: {args.model_type}") if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/ogo/cli/ref/SpineCompressionFe.py b/ogo/cli/ref/SpineCompressionFe.py index 3b02d8a..a70d591 100644 --- a/ogo/cli/ref/SpineCompressionFe.py +++ b/ogo/cli/ref/SpineCompressionFe.py @@ -91,6 +91,22 @@ def print_matrix(matrix): # Send the row as a single message ogo.message(row_message) +def parse_scale_triplet(value, name="scale"): + if value is None: + return None + if isinstance(value, (int, float)): + return np.array([float(value)] * 3, dtype=float) + parts = [part.strip() for part in str(value).replace("x", ",").split(",") if part.strip()] + try: + values = [float(part) for part in parts] + except ValueError as exc: + raise ValueError(f"{name} must be a float or three comma-separated floats.") from exc + if len(values) == 1: + values = values * 3 + if len(values) != 3: + raise ValueError(f"{name} must be a float or three comma-separated floats.") + return np.array(values, dtype=float) + ###################################################################### HELPERS (VTK) ## VTK Helper functions: @@ -322,7 +338,13 @@ def perform_marching_cubes(body_output): raise ValueError("No data was generated by the Marching Cubes algorithm.") return mcubes -def get_icp_with_scaling(body, reference_path): +def get_icp_with_scaling( + body, + reference_path, + scale_factors=None, + min_scale=(0.8, 0.8, 0.75), + max_scale=(1.2, 1.2, 1.3), +): def get_principal_axes_lengths(polydata): points = vtk_to_numpy(polydata.GetPoints().GetData()) @@ -342,20 +364,22 @@ def get_principal_axes_lengths(polydata): reference_reader.Update() reference_polydata = reference_reader.GetOutput() - # Compute PCA-based axis lengths sample_lengths = get_principal_axes_lengths(mcubes_output) ref_lengths = get_principal_axes_lengths(reference_polydata) - scale_factors = sample_lengths / ref_lengths - - # Limit maximum scale factors - min_scale = np.array([0.8, 0.8, 0.75]) - max_scale = np.array([1.2, 1.2, 1.3]) - scale_factors = np.minimum(scale_factors, max_scale) - scale_factors = np.maximum(scale_factors, min_scale) + if scale_factors is None: + scale_factors = sample_lengths / ref_lengths + min_scale = parse_scale_triplet(min_scale, "registration_min_scale") + max_scale = parse_scale_triplet(max_scale, "registration_max_scale") + scale_factors = np.minimum(scale_factors, max_scale) + scale_factors = np.maximum(scale_factors, min_scale) + scale_source = "PCA axis lengths" + else: + scale_factors = parse_scale_triplet(scale_factors, "registration_scale") + scale_source = "manual override" ogo.message(f"PCA axis lengths (sample): {np.round(sample_lengths, 2)}") ogo.message(f"PCA axis lengths (reference): {np.round(ref_lengths, 2)}") - ogo.message(f"Scale factors applied to reference: {np.round(scale_factors, 3)}") + ogo.message(f"Scale factors applied to reference ({scale_source}): {np.round(scale_factors, 3)}") # Scale the reference to match the sample scale_transform = vtk.vtkTransform() @@ -1201,6 +1225,9 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, top_node_set_id = kwargs.get("top_node_set_id", 4) bottom_node_set_id = kwargs.get("bottom_node_set_id", 3) quality_control = kwargs.get("quality_control", True) + registration_scale = kwargs.get("registration_scale", None) + registration_min_scale = kwargs.get("registration_min_scale", "0.8,0.8,0.75") + registration_max_scale = kwargs.get("registration_max_scale", "1.2,1.2,1.3") #Read Image and Mask ogo.message(f"Reading image...: {input_image}") @@ -1224,8 +1251,14 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, isolated_vertebra, isolated_process, isolated_image = crop_and_transform(fullvertebra, body.GetOutput(), process.GetOutput(), image_reader.GetOutput()) # Marching Cubes and Registration - ogo.message(f"Starting ICP registration to refrence...: {reference_path} ") - icp = get_icp_with_scaling(body, reference_path) + ogo.message(f"Starting ICP registration to reference...: {reference_path} ") + icp = get_icp_with_scaling( + body, + reference_path, + scale_factors=registration_scale, + min_scale=registration_min_scale, + max_scale=registration_max_scale, + ) # Transform Images and resample at the same time (less interpolation) transformed_vertebra = transform_resample(isolated_vertebra.GetOutput(), icp.GetMatrix(), iso_resolution) @@ -1383,6 +1416,12 @@ def main(): help="Sets the applied displacement in [mm] to the FE model. (default: %(default)s [mm])") parser.add_argument("--reference_path", type=str, required=False, default=None, help="Path to the reference vtk file for ICP registration. (default: None)") + parser.add_argument("--registration_scale", type=str, default=None, + help="Manual affine scale applied to the reference body before ICP. Use a scalar or sx,sy,sz. If omitted, PCA-based scaling is used.") + parser.add_argument("--registration_min_scale", type=str, default="0.8,0.8,0.75", + help="Minimum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") + parser.add_argument("--registration_max_scale", type=str, default="1.2,1.2,1.3", + help="Maximum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") parser.add_argument("--top_node_set_id", type=int, default=4, help="ID for the top node set. (default: %(default)s)") parser.add_argument("--bottom_node_set_id", type=int, default=3, @@ -1472,4 +1511,4 @@ def main(): ###################################################################### MAIN if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/ogo/util/faim.py b/ogo/util/faim.py new file mode 100644 index 0000000..44713d8 --- /dev/null +++ b/ogo/util/faim.py @@ -0,0 +1,485 @@ +"""Small FAIM/N88 command adapter used by Ogo FE CLIs.""" + +import csv +import os +from pathlib import Path +import re +import subprocess +from typing import Dict, Iterable, List, Optional, Sequence + + +def _optional_path(value): + if value is None: + return None + text = str(value).strip() + if not text: + return None + return Path(text).expanduser() + + +def resolve_faim_bin_dir(install_root=None, bin_dir=None): + """Return the directory that likely contains the FAIM/N88 command-line tools.""" + explicit = _optional_path(bin_dir) + if explicit is not None: + return explicit + + root = _optional_path(install_root) + if root is None: + return None + + candidates = [ + root / "Contents" / "MacOS", + root / "bin" / "64bit", + root / "bin", + root, + ] + for candidate in candidates: + if candidate.exists(): + return candidate + return root + + +def resolve_faim_command(command_name, install_root=None, bin_dir=None, explicit_path=None): + """Resolve one FAIM/N88 command from an explicit path, install root, or PATH.""" + explicit = _optional_path(explicit_path) + if explicit is not None: + return str(explicit) + + resolved_bin_dir = resolve_faim_bin_dir(install_root=install_root, bin_dir=bin_dir) + if resolved_bin_dir is not None: + candidate = resolved_bin_dir / command_name + if candidate.exists(): + return str(candidate) + + return command_name + + +def resolve_faim_solver_command(install_root=None, bin_dir=None, explicit_path=None): + """Resolve the solver command, accepting either ``faim`` or the newer solver names.""" + explicit = _optional_path(explicit_path) + if explicit is not None: + return str(explicit) + + resolved_bin_dir = resolve_faim_bin_dir(install_root=install_root, bin_dir=bin_dir) + if resolved_bin_dir is not None: + for name in ("faim", "n88solver_slt", "n88solver_spt", "n88solver_sla"): + candidate = resolved_bin_dir / name + if candidate.exists(): + return str(candidate) + + return "faim" + + +def build_faim_env(bin_dir=None, license_dir=None, env_overrides=None): + """Build the environment used when launching FAIM/N88 tools.""" + env = os.environ.copy() + if env_overrides: + env.update({str(k): str(v) for k, v in env_overrides.items()}) + + resolved_bin_dir = _optional_path(bin_dir) + if resolved_bin_dir is not None: + existing_path = env.get("PATH", "") + env["PATH"] = os.pathsep.join( + [str(resolved_bin_dir)] + ([existing_path] if existing_path else []) + ) + + resolved_license_dir = _optional_path(license_dir) + if resolved_license_dir is not None: + env["NUMERICS88_LICENSE_DIR"] = str(resolved_license_dir) + + return env + + +def wrap_conda_command(cmd, conda_env=None, conda_executable="conda"): + """Wrap a command in ``conda run`` when the user requested a named environment.""" + if conda_env and str(conda_env).strip() and str(conda_env).strip() != "current": + return [conda_executable, "run", "-n", str(conda_env)] + list(cmd) + return list(cmd) + + +def run_command( + cmd, + conda_env=None, + conda_executable="conda", + env=None, + dry_run=False, + capture_output=False, + check=True, +): + """Run one external command and optionally return stdout.""" + full_cmd = wrap_conda_command(cmd, conda_env=conda_env, conda_executable=conda_executable) + if dry_run: + print("[dry-run] " + " ".join(str(part) for part in full_cmd)) + return "" + + stdout = subprocess.PIPE if capture_output else None + stderr = subprocess.PIPE if capture_output else None + completed = subprocess.run( + [str(part) for part in full_cmd], + stdout=stdout, + stderr=stderr, + universal_newlines=True, + env=env, + check=check, + ) + return completed.stdout if capture_output and completed.stdout is not None else "" + + +def _read_single_row_csv(path): + if path is None or not Path(path).exists(): + return {} + with open(str(path), newline="") as handle: + rows = list(csv.DictReader(handle)) + return rows[0] if rows else {} + + +def _safe_float(value): + if value in (None, ""): + return "" + try: + return float(value) + except (TypeError, ValueError): + return value + + +def parse_pistoia_text(path): + """Extract the main scalar values from a text Pistoia report.""" + if path is None or not Path(path).exists(): + return {} + + text = Path(path).read_text() + out = {} + patterns = { + "critical_volume_pct": r"Critical volume \(%\):\s+([-\d.E+]+)", + "critical_ees": r"Critical EES:\s+([-\d.E+]+)", + "ees_at_crit_vol": r"EES at vol_crit:\s+([-\d.E+]+)", + } + for key, pattern in patterns.items(): + match = re.search(pattern, text, flags=re.IGNORECASE) + if match: + out[key] = _safe_float(match.group(1)) + + match = re.search( + r"Failure load \(RF \* factor\):\s+([-\d.E+]+)\s+([-\d.E+]+)\s+([-\d.E+]+)", + text, + flags=re.IGNORECASE, + ) + if match: + out["failure_load_x_N"] = _safe_float(match.group(1)) + out["failure_load_y_N"] = _safe_float(match.group(2)) + out["failure_load_z_N"] = _safe_float(match.group(3)) + + match = re.search( + r"Axial stiffness:\s+([-\d.E+]+)\s+([-\d.E+]+)\s+([-\d.E+]+)", + text, + flags=re.IGNORECASE, + ) + if match: + out["stiffness_x_N_per_mm"] = _safe_float(match.group(1)) + out["stiffness_y_N_per_mm"] = _safe_float(match.group(2)) + out["stiffness_z_N_per_mm"] = _safe_float(match.group(3)) + + return out + + +def write_results_csv( + *, + output_file, + model_file, + analysis_file, + pistoia_file, + loads_csv, + pistoia_csv, + analysis_var, + pistoia_vars, + failure_axis, + applied_displacement=None, + target_displacement=0.2, + warnings=None, +): + """Write a compact, user-facing CSV with loads, stiffness, and Pistoia values.""" + pistoia_meta = parse_pistoia_text(pistoia_file) + loads = _read_single_row_csv(loads_csv) + pistoia = _read_single_row_csv(pistoia_csv) + + pistoia_vars = list(pistoia_vars or []) + failure_var = pistoia_vars[0] if pistoia_vars else "" + stiffness_var = pistoia_vars[1] if len(pistoia_vars) > 1 else "" + + failure_key = "failure_load_{}_N".format(failure_axis) + stiffness_key = "stiffness_{}_N_per_mm".format(failure_axis) + reaction_force = _safe_float(loads.get(analysis_var)) + pistoia_failure_load = _safe_float( + pistoia.get(failure_var) or pistoia_meta.get(failure_key) + ) + stiffness = _safe_float( + pistoia.get(stiffness_var) or pistoia_meta.get(stiffness_key) + ) + + # The benchmark endpoint is reported at 0.2. If the model was solved at a + # different prescribed displacement, scale the linear reaction force back to + # that standard point. + rescale_factor = "" + load_at_target = "" + try: + applied = abs(float(applied_displacement)) + target = abs(float(target_displacement)) + if applied > 0 and reaction_force != "": + rescale_factor = target / applied + load_at_target = abs(float(reaction_force)) * rescale_factor + except (TypeError, ValueError): + pass + + row = { + "model_file": str(model_file), + "analysis_file": str(analysis_file), + "pistoia_file": str(pistoia_file), + "loads_csv": str(loads_csv), + "pistoia_csv": str(pistoia_csv), + "analysis_var": analysis_var, + "pistoia_failure_var": failure_var, + "pistoia_stiffness_var": stiffness_var, + "applied_displacement": applied_displacement if applied_displacement is not None else "", + "target_displacement_percent": target_displacement, + "rescale_factor_to_target": rescale_factor, + "load_at_0p2_percent_N": load_at_target, + "load_at_0p2_percent_kN": load_at_target / 1000.0 if load_at_target != "" else "", + "reaction_force_N": reaction_force, + "pistoia_failure_load_N": pistoia_failure_load, + "stiffness_N_per_mm": stiffness, + "critical_volume_pct": pistoia_meta.get("critical_volume_pct", ""), + "critical_ees": pistoia_meta.get("critical_ees", ""), + "ees_at_crit_vol": pistoia_meta.get("ees_at_crit_vol", ""), + "warnings": " | ".join(warnings or []), + } + + output_file = Path(output_file) + output_file.parent.mkdir(parents=True, exist_ok=True) + with open(str(output_file), "w", newline="") as handle: + writer = csv.DictWriter(handle, fieldnames=list(row.keys())) + writer.writeheader() + writer.writerow(row) + return output_file + + +def run_faim_pipeline( + *, + model_file, + output_prefix=None, + analysis_var, + pistoia_vars, + failure_axis, + threads=4, + conda_env=None, + conda_executable="conda", + install_root=None, + bin_dir=None, + license_dir=None, + faim_command=None, + n88modelinfo_command=None, + n88derivedfields_command=None, + n88postfaim_command=None, + n88pistoia_command=None, + n88tabulate_command=None, + n88copymodel_command=None, + critical_volume=2.0, + critical_strain=0.007, + exclude=5000, + applied_displacement=None, + target_displacement=0.2, + compress=True, + require_pistoia=False, + dry_run=False, +): + """Solve one N88 model and collect standard FAIM/Pistoia output files.""" + model_file = Path(model_file) + output_prefix = Path(output_prefix) if output_prefix is not None else model_file.with_suffix("") + output_prefix.parent.mkdir(parents=True, exist_ok=True) + + resolved_bin_dir = resolve_faim_bin_dir(install_root=install_root, bin_dir=bin_dir) + env = build_faim_env(bin_dir=resolved_bin_dir, license_dir=license_dir) + + solver = resolve_faim_solver_command( + install_root=install_root, + bin_dir=resolved_bin_dir, + explicit_path=faim_command, + ) + modelinfo = resolve_faim_command( + "n88modelinfo", + install_root=install_root, + bin_dir=resolved_bin_dir, + explicit_path=n88modelinfo_command, + ) + derivedfields = resolve_faim_command( + "n88derivedfields", + install_root=install_root, + bin_dir=resolved_bin_dir, + explicit_path=n88derivedfields_command, + ) + postfaim = resolve_faim_command( + "n88postfaim", + install_root=install_root, + bin_dir=resolved_bin_dir, + explicit_path=n88postfaim_command, + ) + pistoia = resolve_faim_command( + "n88pistoia", + install_root=install_root, + bin_dir=resolved_bin_dir, + explicit_path=n88pistoia_command, + ) + tabulate = resolve_faim_command( + "n88tabulate", + install_root=install_root, + bin_dir=resolved_bin_dir, + explicit_path=n88tabulate_command, + ) + copymodel = resolve_faim_command( + "n88copymodel", + install_root=install_root, + bin_dir=resolved_bin_dir, + explicit_path=n88copymodel_command, + ) + + analysis_file = output_prefix.with_name(output_prefix.name + "_analysis.txt") + pistoia_file = output_prefix.with_name(output_prefix.name + "_pistoia.txt") + loads_csv = output_prefix.with_name(output_prefix.name + "_loads.csv") + pistoia_csv = output_prefix.with_name(output_prefix.name + "_pistoia.csv") + results_csv = output_prefix.with_name(output_prefix.name + "_results.csv") + log_file = output_prefix.with_name(output_prefix.name + ".log") + + warnings = [] + # Check whether the model already contains a solution so re-runs are cheap. + modelinfo_text = run_command( + [modelinfo, "--solutions", str(model_file)], + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + capture_output=True, + check=False, + ) + + if "Name :" not in modelinfo_text: + run_command( + [solver, "--engine=mt", "--threads={}".format(int(threads)), str(model_file)], + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + ) + + # Derived fields add quantities such as strain energy density to the model. + run_command( + [derivedfields, str(model_file)], + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + ) + run_command( + [postfaim, "--output_file", str(analysis_file), str(model_file)], + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + ) + + # Pistoia is useful but can fail for nonlinear or unusual models; make that + # behavior configurable so the main solve result can still be collected. + try: + run_command( + [ + pistoia, + str(model_file), + "--critical_volume", + str(critical_volume), + "--critical_strain", + str(critical_strain), + "--exclude", + str(exclude), + "--output_file", + str(pistoia_file), + ], + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + ) + except subprocess.CalledProcessError: + if require_pistoia: + raise + warnings.append("Pistoia postprocessing failed.") + + # Tabulate the reaction force and Pistoia values into small CSV files. + run_command( + [tabulate, "-H", "-d", ",", "-V", str(analysis_var), str(analysis_file), "-o", str(loads_csv)], + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + ) + + if pistoia_vars: + run_command( + [ + tabulate, + "-H", + "-d", + ",", + "-V", + ",".join(str(v) for v in pistoia_vars), + str(pistoia_file), + "-o", + str(pistoia_csv), + ], + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + ) + + if compress: + run_command( + [copymodel, "--compress", str(model_file), str(model_file)], + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + ) + + log_text = run_command( + [modelinfo, "--log", str(model_file)], + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + capture_output=True, + check=False, + ) + if not dry_run: + log_file.write_text(log_text) + write_results_csv( + output_file=results_csv, + model_file=model_file, + analysis_file=analysis_file, + pistoia_file=pistoia_file, + loads_csv=loads_csv, + pistoia_csv=pistoia_csv, + analysis_var=analysis_var, + pistoia_vars=pistoia_vars, + failure_axis=failure_axis, + applied_displacement=applied_displacement, + target_displacement=target_displacement, + warnings=warnings, + ) + + return { + "analysis_file": analysis_file, + "pistoia_file": pistoia_file, + "loads_csv": loads_csv, + "pistoia_csv": pistoia_csv, + "results_csv": results_csv, + "log_file": log_file, + "warnings": warnings, + } diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index a84e7fe..f63e725 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -1,44 +1,267 @@ -import unittest -from unittest import mock -from ogo.cli import GenerateFEM - - -class TestGenerateFEM(unittest.TestCase): - def setUp(self) -> None: - pass - - def tearDown(self) -> None: - pass - - @mock.patch("ogo.cli.GenerateFEM.spine_compression_main") - def test_dispatch_to_vertebra(self, mock_spine): - """Test that 'vertebra' dispatches to spine_compression_main""" - test_args = [ - "program", - "--model_type", "vertebra", - "--calibrated_image", "dummy_image.nii.gz", - "--bone_mask", "dummy_mask.nii.gz", - "--vertebral_body_label", "1", - "--vertebral_process_label", "2" - ] - with mock.patch("sys.argv", test_args): - GenerateFEM.main() - mock_spine.assert_called_once() - - @mock.patch("ogo.cli.GenerateFEM.sideways_fall_main") - def test_dispatch_to_femur(self, mock_femur): - """Test that 'femur' dispatches to sideways_fall_main""" - test_args = [ - "program", - "--model_type", "femur", - "--calibrated_image", "dummy_image.nii.gz", - "--bone_mask", "dummy_mask.nii.gz", - "--femur_side", "1" - ] - with mock.patch("sys.argv", test_args): - GenerateFEM.main() - mock_femur.assert_called_once() - - -if __name__ == "__main__": - unittest.main() +import importlib.util +from pathlib import Path + +import pytest + + +_MODULE_PATH = Path(__file__).resolve().parents[2] / "ogo" / "cli" / "GenerateFEM.py" +_SPEC = importlib.util.spec_from_file_location("GenerateFEM", str(_MODULE_PATH)) +GenerateFEM = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(GenerateFEM) + + +BENCHMARK_LINEAR_ARGS = [ + "--fe_displacement", + "-0.2", + "--elastic_E_func", + "kopperdahl_trab_E", + "--cort_elastic_E_func", + "kopperdahl_trab_E", +] + +BENCHMARK_NONLINEAR_ARGS = [ + "--fe_displacement", + "-2.0", + "--pmma_yield_compression", + "70.0", + "--pmma_yield_tension", + "70.0", + "--elastic_E_func", + "kopperdahl_trab_E", + "--yield_comp_func", + "kopperdahl_trab_yc", + "--yield_tens_func", + "kopperdahl_trab_yc", + "--cort_elastic_E_func", + "kopperdahl_trab_E", + "--cort_yield_comp_func", + "kopperdahl_trab_yc", + "--cort_yield_tens_func", + "kopperdahl_trab_yc", +] + + +@pytest.fixture +def solve_calls(monkeypatch): + calls = [] + monkeypatch.setattr( + GenerateFEM, + "solve_model", + lambda model_path, args, model_type, generator_argv: calls.append( + (Path(model_path), model_type, list(generator_argv)) + ), + ) + return calls + + +def test_spine_runs_each_requested_vertebra(monkeypatch, solve_calls): + calls = [] + monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "spine", + "density.nii.gz", + "spine_mask.nii.gz", + "--output_path", + "models", + "--vertebra", + "L1:2:1", + "--vertebra", + "T12:4:3", + "--iso_resolution", + "1.2", + ] + ) + + assert calls == [ + [ + "density.nii.gz", + "spine_mask.nii.gz", + "--mask_threshold", + "2", + "--process_mask_threshold", + "1", + "--appendix", + "L1", + "--output_path", + "models", + *BENCHMARK_LINEAR_ARGS, + "--iso_resolution", + "1.2", + ], + [ + "density.nii.gz", + "spine_mask.nii.gz", + "--mask_threshold", + "4", + "--process_mask_threshold", + "3", + "--appendix", + "T12", + "--output_path", + "models", + *BENCHMARK_LINEAR_ARGS, + "--iso_resolution", + "1.2", + ], + ] + assert solve_calls == [ + (Path("models/density_vertebra_2_L1.n88model"), "spine", calls[0]), + (Path("models/density_vertebra_4_T12.n88model"), "spine", calls[1]), + ] + + +def test_hip_defaults_to_both_sides(monkeypatch, solve_calls): + calls = [] + monkeypatch.setattr(GenerateFEM, "run_femur_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main(["hip", "density.nii.gz", "hip_mask.nii.gz", "--output_path", "models"]) + + assert calls == [ + ["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "1", "--output_path", "models"], + ["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "2", "--output_path", "models"], + ] + assert solve_calls == [ + (Path("models/density_LT_FEMUR_SF.n88model"), "hip", calls[0]), + (Path("models/density_RT_FEMUR_SF.n88model"), "hip", calls[1]), + ] + + +def test_hip_can_run_one_side(monkeypatch, solve_calls): + calls = [] + monkeypatch.setattr(GenerateFEM, "run_femur_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main(["hip", "density.nii.gz", "hip_mask.nii.gz", "--side", "right"]) + + assert calls == [["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "2"]] + assert solve_calls == [(Path("density_RT_FEMUR_SF.n88model"), "hip", calls[0])] + + +def test_dry_run_prints_commands_without_running(monkeypatch, capsys): + calls = [] + monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "spine", + "density.nii.gz", + "spine_mask.nii.gz", + "--vertebra", + "L2,6,5", + "--dry-run", + "--no-solve", + "--preset", + "none", + ] + ) + + assert calls == [] + assert ( + capsys.readouterr().out.strip() + == "OgoSpineCompressionFe density.nii.gz spine_mask.nii.gz " + "--mask_threshold 6 --process_mask_threshold 5 --appendix L2" + ) + + +def test_spine_preset_none_keeps_command_minimal(monkeypatch, solve_calls): + calls = [] + monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "spine", + "density.nii.gz", + "spine_mask.nii.gz", + "--vertebra", + "L3:8:7", + "--preset", + "none", + ] + ) + + assert calls == [ + [ + "density.nii.gz", + "spine_mask.nii.gz", + "--mask_threshold", + "8", + "--process_mask_threshold", + "7", + "--appendix", + "L3", + ] + ] + + +def test_spine_nonlinear_benchmark_preset(monkeypatch, solve_calls): + calls = [] + monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "spine", + "density.nii.gz", + "spine_mask.nii.gz", + "--vertebra", + "T10:3:2", + "--preset", + "benchmark-nonlinear", + ] + ) + + assert calls == [ + [ + "density.nii.gz", + "spine_mask.nii.gz", + "--mask_threshold", + "3", + "--process_mask_threshold", + "2", + "--appendix", + "T10", + *BENCHMARK_NONLINEAR_ARGS, + ] + ] + + +def test_spine_explicit_options_follow_preset_for_overrides(monkeypatch, solve_calls): + calls = [] + monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "spine", + "density.nii.gz", + "spine_mask.nii.gz", + "--vertebra", + "L4:2:1", + "--fe_displacement", + "-0.25", + ] + ) + + assert calls[0][-2:] == ["--fe_displacement", "-0.25"] + assert calls[0].count("--fe_displacement") == 2 + + +def test_legacy_model_type_dispatch_still_works(monkeypatch): + calls = [] + monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "--model_type", + "vertebra", + "density.nii.gz", + "mask.nii.gz", + "--mask_threshold", + "2", + "--process_mask_threshold", + "1", + ] + ) + + assert calls == [ + ["density.nii.gz", "mask.nii.gz", "--mask_threshold", "2", "--process_mask_threshold", "1"] + ] diff --git a/tests/util/test_faim.py b/tests/util/test_faim.py new file mode 100644 index 0000000..063d26e --- /dev/null +++ b/tests/util/test_faim.py @@ -0,0 +1,92 @@ +import csv +import importlib.util +from pathlib import Path + + +_MODULE_PATH = Path(__file__).resolve().parents[2] / "ogo" / "util" / "faim.py" +_SPEC = importlib.util.spec_from_file_location("faim", str(_MODULE_PATH)) +faim = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(faim) + + +def test_resolve_faim_command_uses_bin_64bit(tmp_path): + install_root = tmp_path / "Faim 9.0" + bin_dir = install_root / "bin" / "64bit" + bin_dir.mkdir(parents=True) + command = bin_dir / "n88derivedfields" + command.write_text("", encoding="utf-8") + + assert faim.resolve_faim_bin_dir(install_root=install_root) == bin_dir + assert faim.resolve_faim_command("n88derivedfields", install_root=install_root) == str(command) + + +def test_write_results_csv_collects_pistoia_and_loads(tmp_path): + analysis_file = tmp_path / "model_analysis.txt" + pistoia_file = tmp_path / "model_pistoia.txt" + loads_csv = tmp_path / "model_loads.csv" + pistoia_csv = tmp_path / "model_pistoia.csv" + results_csv = tmp_path / "model_results.csv" + + analysis_file.write_text("analysis", encoding="utf-8") + pistoia_file.write_text( + "\n".join( + [ + "Critical volume (%): 2.000", + "Critical EES: 7.0000E-03", + "EES at vol_crit: 3.5000E-03", + "Failure load (RF * factor): 1.0000E+02 2.0000E+02 3.0000E+02", + "Axial stiffness: 1.0000E+03 2.0000E+03 3.0000E+03", + ] + ), + encoding="utf-8", + ) + loads_csv.write_text("fz_ns1\n123.4\n", encoding="utf-8") + pistoia_csv.write_text("pis_fz_fail,pis_stiffz\n345.6,789.0\n", encoding="utf-8") + + faim.write_results_csv( + output_file=results_csv, + model_file=tmp_path / "model.n88model", + analysis_file=analysis_file, + pistoia_file=pistoia_file, + loads_csv=loads_csv, + pistoia_csv=pistoia_csv, + analysis_var="fz_ns1", + pistoia_vars=["pis_fz_fail", "pis_stiffz"], + failure_axis="z", + applied_displacement="-1.0", + target_displacement=0.2, + ) + + with results_csv.open(newline="") as handle: + row = next(csv.DictReader(handle)) + + assert row["reaction_force_N"] == "123.4" + assert row["load_at_0p2_percent_N"] == "24.680000000000003" + assert row["pistoia_failure_load_N"] == "345.6" + assert row["stiffness_N_per_mm"] == "789.0" + assert row["critical_volume_pct"] == "2.0" + assert row["rescale_factor_to_target"] == "0.2" + + +def test_run_faim_pipeline_dry_run_prints_full_workflow(tmp_path, capsys): + model = tmp_path / "model.n88model" + model.write_text("", encoding="utf-8") + + faim.run_faim_pipeline( + model_file=model, + analysis_var="fz_ns1", + pistoia_vars=["pis_fz_fail", "pis_stiffz"], + failure_axis="z", + dry_run=True, + threads=2, + compress=False, + ) + + out = capsys.readouterr().out + assert "n88modelinfo --solutions" in out + assert "faim --engine=mt --threads=2" in out + assert "n88derivedfields" in out + assert "n88postfaim --output_file" in out + assert "n88pistoia" in out + assert "n88tabulate -H -d , -V fz_ns1" in out + assert "n88tabulate -H -d , -V pis_fz_fail,pis_stiffz" in out From 3324685633eb9c2c5a9c063a8af7208b67baa3d7 Mon Sep 17 00:00:00 2001 From: wallematthias Date: Mon, 22 Jun 2026 23:24:08 -0600 Subject: [PATCH 02/67] Refactor FE helpers into domain package --- ogo/__init__.py | 14 +- ogo/cli/ref/SidewaysFallFe.py | 49 ++--- ogo/cli/ref/SpineCompressionFe.py | 170 ++--------------- ogo/fea/__init__.py | 2 + ogo/fea/femur.py | 46 +++++ ogo/fea/materials.py | 150 +++++++++++++++ ogo/fea/model.py | 173 ++++++++++++++++++ ogo/fea/spine.py | 117 ++++++++++++ pytest.ini | 3 + ...nefe_sub_verse004_vertebra21_metadata.json | 15 ++ tests/fea/test_femur.py | 29 +++ tests/fea/test_materials.py | 51 ++++++ tests/fea/test_spine.py | 15 ++ tests/fea/test_spinefe_benchmark.py | 98 ++++++++++ 14 files changed, 754 insertions(+), 178 deletions(-) create mode 100644 ogo/fea/__init__.py create mode 100644 ogo/fea/femur.py create mode 100644 ogo/fea/materials.py create mode 100644 ogo/fea/model.py create mode 100644 ogo/fea/spine.py create mode 100644 pytest.ini create mode 100644 tests/fea/fixtures/spinefe_sub_verse004_vertebra21_metadata.json create mode 100644 tests/fea/test_femur.py create mode 100644 tests/fea/test_materials.py create mode 100644 tests/fea/test_spine.py create mode 100644 tests/fea/test_spinefe_benchmark.py diff --git a/ogo/__init__.py b/ogo/__init__.py index 2d1ac89..318acf8 100644 --- a/ogo/__init__.py +++ b/ogo/__init__.py @@ -4,8 +4,12 @@ '__version__', 'version_info', ) -from pbr.version import VersionInfo - -_v = VersionInfo('ogo').semantic_version() -__version__ = _v.release_string() -version_info = _v.version_tuple() +try: + from pbr.version import VersionInfo +except ImportError: + __version__ = '0+unknown' + version_info = (0,) +else: + _v = VersionInfo('ogo').semantic_version() + __version__ = _v.release_string() + version_info = _v.version_tuple() diff --git a/ogo/cli/ref/SidewaysFallFe.py b/ogo/cli/ref/SidewaysFallFe.py index 56b0f13..361f15a 100644 --- a/ogo/cli/ref/SidewaysFallFe.py +++ b/ogo/cli/ref/SidewaysFallFe.py @@ -29,8 +29,10 @@ from collections import OrderedDict import vtk import vtkbone -import numpy as np +from ogo.fea.femur import side_rotation, sideways_fall_output_name, tilted_side_support_vector +from ogo.fea.materials import build_femur_material_table +from ogo.fea.model import write_model from ogo.util.echo_arguments import echo_arguments def remove_extension(filename): @@ -81,12 +83,12 @@ def sidewaysFallFe(args): script_name = sys.argv[0] - if femur_side == 1: - N88_fileName = output_file.replace('.n88model',"_LT_FEMUR_SF.n88model") - rot_z = 90 - elif femur_side == 2: - N88_fileName = output_file.replace('.n88model',"_RT_FEMUR_SF.n88model") - rot_z = -90 + try: + N88_fileName = sideways_fall_output_name(output_file, femur_side) + rot_z = side_rotation(femur_side) + except ValueError: + ogo.message("Femur side not recognized. Terminating...") + sys.exit() ## @@ -203,10 +205,19 @@ def sidewaysFallFe(args): ## # Set up the Material Table ogo.message("Setting up the Finite Element Material Table...") - material_table = vtkbone.vtkboneMaterialTable() - material_table = ogo.add_bone_material(material_table, bin_centers, elastic_Emax=elastic_Emax, elastic_exponent=elastic_exponent, - bone_yield_compression=bone_yield_compression, bone_yield_tension=bone_yield_tension, mu=poissons_ratio) - material_table = ogo.add_pmma_material(material_table, pmma_mat_id, pmma_E, pmma_v,pmma_yield_tension=pmma_yield_tension, pmma_yield_compression=pmma_yield_compression) + material_table = build_femur_material_table( + bin_centers, + elastic_Emax=elastic_Emax, + elastic_exponent=elastic_exponent, + bone_yield_compression=bone_yield_compression, + bone_yield_tension=bone_yield_tension, + poissons_ratio=poissons_ratio, + pmma_mat_id=pmma_mat_id, + pmma_E=pmma_E, + pmma_v=pmma_v, + pmma_yield_tension=pmma_yield_tension, + pmma_yield_compression=pmma_yield_compression, + ) bone_yield_compression = args.bone_yield_compression bone_yield_tension = args.bone_yield_tension @@ -227,16 +238,8 @@ def sidewaysFallFe(args): bottom_support_vector = (0, -1, 0) side_support_vector = (0, 0, -1) - # Define the rotation angle in degrees - theta = np.radians(-20) # Convert to radians - - # Define the rotation matrix around the x-axis - Rx = np.array([[1, 0, 0], - [0, np.cos(theta), -np.sin(theta)], - [0, np.sin(theta), np.cos(theta)]]) - - # Apply the rotation - side_support_vector = Rx @ side_support_vector + # Tilt the distal support to match the historical sideways-fall fixture. + side_support_vector = tilted_side_support_vector(-20) ogo.message("Determining Femoral Head ROI...") femoral_head_bounds = ( @@ -497,7 +500,7 @@ def sidewaysFallFe(args): ## # Write out n88model file ogo.message("Writing out n88model file: %s" % N88_fileName) - ogo.writeN88Model(model2, N88_fileName, image_pathname) + write_model(model2, N88_fileName) ## ogo.message("Done writing n88model.") @@ -611,4 +614,4 @@ def main(): sidewaysFallFe(args) if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/ogo/cli/ref/SpineCompressionFe.py b/ogo/cli/ref/SpineCompressionFe.py index a70d591..64b2f93 100644 --- a/ogo/cli/ref/SpineCompressionFe.py +++ b/ogo/cli/ref/SpineCompressionFe.py @@ -706,42 +706,31 @@ def convert_image_to_material(image, mask, n_bins=128, cort_mask=None): return binned_image, bin_centers def find_and_add_visible_nodes(model, bc_geometry, normal_vector, bone_material_id, node_set_name): - visibleNodesIds = vtk.vtkIdTypeArray() - vtkbone.vtkboneNodeSetsByGeometry.FindNodesOnVisibleSurface( - visibleNodesIds, bc_geometry, normal_vector, bone_material_id) - visibleNodesIds.SetName(node_set_name) - model.AddNodeSet(visibleNodesIds) - ogo.message(f"Found {visibleNodesIds.GetNumberOfTuples()} visible nodes for {node_set_name}.") - return model + """Compatibility wrapper for shared spine FE visible-node detection.""" + from ogo.fea.model import find_and_add_visible_nodes as _find_and_add_visible_nodes + + return _find_and_add_visible_nodes( + model, bc_geometry, normal_vector, bone_material_id, node_set_name + ) def resolve_func(func_or_name, module): - if callable(func_or_name): - return func_or_name - elif isinstance(func_or_name, str): - return getattr(module, func_or_name) - else: - return None + """Compatibility wrapper for material-law function resolution.""" + from ogo.fea.materials import resolve_material_func -def apply_boundary_conditions(model,**kwargs): + return resolve_material_func(func_or_name, module) - fe_displacement = kwargs.get("fe_displacement", -1.0) +def apply_boundary_conditions(model,**kwargs): + """Compatibility wrapper for shared spine compression boundary conditions.""" + from ogo.fea.model import apply_spine_boundary_conditions - ogo.message('Applying boundary conditions...') - model.ApplyBoundaryCondition("body_top", vtkbone.vtkboneConstraint.SENSE_Z, fe_displacement, "top_displacement") - model.ApplyBoundaryCondition("body_bottom", vtkbone.vtkboneConstraint.SENSE_Z, 0, "bottom_fixed_z") - return model + return apply_spine_boundary_conditions(model, **kwargs) def log_fe_arguments(**kwargs): - import inspect + """Compatibility wrapper for shared FE argument logging.""" + from ogo.fea.model import log_fe_arguments as _log_fe_arguments - ogo.message("========= FE MODEL ARGUMENTS =========") - for key, value in kwargs.items(): - if inspect.isfunction(value): - ogo.message(f"{key}: function -> {value.__name__}") - else: - ogo.message(f"{key}: {value}") - ogo.message("======================================") + return _log_fe_arguments(**kwargs) def create_microfe_model( image_with_pads, @@ -749,132 +738,13 @@ def create_microfe_model( bin_centers, **kwargs ): - import ogo.cli.ref.material_laws as material_laws + """Compatibility wrapper for shared spine compression model creation.""" + from ogo.fea.model import create_microfe_model as _create_microfe_model - # === Load defaults and dynamic functions === # - n_bins = 128 - poissons_ratio = kwargs.get("poissons_ratio", 0.3) - pmma_mat_id = kwargs.get("pmma_mat_id", 5000) - pmma_E = kwargs.get("pmma_E", 2500) - pmma_v = kwargs.get("pmma_v", 0.3) - top_displacement = kwargs.get("top_displacement", "top_displacement") - top_direction = kwargs.get("top_direction", (0, 0, 1)) - bottom_direction = kwargs.get("bottom_direction", (0, 0, -1)) - top_node_set_id = kwargs.get("top_node_set_id", 4) - bottom_node_set_id = kwargs.get("bottom_node_set_id", 3) - top_node_set_name = kwargs.get("top_node_set_name", "body_top") - bottom_node_set_name = kwargs.get("bottom_node_set_name", "body_bottom") - pmma_yield_compression = kwargs.get("pmma_yield_compression", None) - pmma_yield_tension = kwargs.get("pmma_yield_tension", None) - - # === Load material law functions from kwargs === # - elastic_E_func = resolve_func(kwargs.get("elastic_E_func"), material_laws) or default_E - yield_comp_func = resolve_func(kwargs.get("yield_comp_func"), material_laws) - yield_tens_func = resolve_func(kwargs.get("yield_tens_func"), material_laws) - - cort_elastic_E_func = resolve_func(kwargs.get("cort_elastic_E_func"), material_laws) or elastic_E_func - cort_yield_comp_func = resolve_func(kwargs.get("cort_yield_comp_func"), material_laws) or yield_comp_func - cort_yield_tens_func = resolve_func(kwargs.get("cort_yield_tens_func"), material_laws) or yield_tens_func - cort_poissons_ratio = poissons_ratio if kwargs.get('cort_poissons_ratio') is None else kwargs['cort_poissons_ratio'] - - log_fe_arguments( - n_bins=n_bins, - poissons_ratio=poissons_ratio, - pmma_mat_id=pmma_mat_id, - pmma_E=pmma_E, - pmma_v=pmma_v, - top_displacement=top_displacement, - top_direction=top_direction, - bottom_direction=bottom_direction, - top_node_set_id=top_node_set_id, - bottom_node_set_id=bottom_node_set_id, - top_node_set_name=top_node_set_name, - bottom_node_set_name=bottom_node_set_name, - pmma_yield_compression=pmma_yield_compression, - pmma_yield_tension=pmma_yield_tension, - elastic_E_func=elastic_E_func, - yield_comp_func=yield_comp_func, - yield_tens_func=yield_tens_func, - cort_elastic_E_func=cort_elastic_E_func, - cort_yield_comp_func=cort_yield_comp_func, - cort_yield_tens_func=cort_yield_tens_func, - cort_poissons_ratio=cort_poissons_ratio + return _create_microfe_model( + image_with_pads, boundary_masks_with_pads, bin_centers, **kwargs ) - # === Mesh images === # - ogo.message(f"Casting to Short Integer datatype...") - image_pads_short = ogo.cast2short(image_with_pads) - - ogo.message(f"Filtering connected components...") - conn = ogo.imageConnectivity(image_pads_short) - conn_bc = ogo.imageConnectivity(boundary_masks_with_pads) - - ogo.message(f"Meshing...") - mesh = ogo.Image2Mesh(conn) - temp_bc_mesh = ogo.Image2Mesh(conn_bc) - - # === Build material table === # - ogo.message("Setting up the Finite Element Material Table...") - material_table = vtkbone.vtkboneMaterialTable() - - # Trabecular material - material_table = ogo.add_bone_material( - material_table, bin_centers, - elastic_E_func=elastic_E_func, - mu=poissons_ratio, - yield_comp_func=yield_comp_func, - yield_tens_func=yield_tens_func, - bin_range=(0, n_bins), - material_name='TrabBone' - ) - - # Cortical material - material_table = ogo.add_bone_material( - material_table, bin_centers, - elastic_E_func=cort_elastic_E_func, - mu=cort_poissons_ratio, - yield_comp_func=cort_yield_comp_func, - yield_tens_func=cort_yield_tens_func, - bin_range=(n_bins, 2 * n_bins), - material_name='CortBone' - ) - - # PMMA caps - material_table = ogo.add_pmma_material( - material_table, - pmma_mat_id, - pmma_E, - pmma_v, - pmma_yield_tension=pmma_yield_tension, - pmma_yield_compression=pmma_yield_compression - ) - - # === Build and annotate model === # - ogo.message("Constructing the Finite Element Model...") - model = ogo.applyTestBase(mesh, material_table) - model.ComputeBounds() - - ogo.message(f"Identifying boundary nodes...") - model = find_and_add_visible_nodes(model, temp_bc_mesh, top_direction, top_node_set_id, top_node_set_name) - model = find_and_add_visible_nodes(model, temp_bc_mesh, bottom_direction, bottom_node_set_id, bottom_node_set_name) - model = apply_boundary_conditions(model, **kwargs) - - ogo.message(f"Setting convergence criteria...") - model.ConvergenceSetFromConstraint(top_displacement) - - # === Postprocessing sets === # - ogo.message('Postprocessing...') - info = model.GetInformation() - pp_node_sets_key = vtkbone.vtkboneSolverParameters.POST_PROCESSING_NODE_SETS() - pp_elem_sets_key = vtkbone.vtkboneSolverParameters.POST_PROCESSING_ELEMENT_SETS() - for setname in [top_node_set_name, bottom_node_set_name]: - pp_node_sets_key.Append(info, setname) - elementSet = model.GetAssociatedElementsFromNodeSet(setname) - model.AddElementSet(elementSet) - pp_elem_sets_key.Append(info, setname) - - return model - def create_microfe_model_depreciated( image_with_pads, boundary_masks_with_pads, diff --git a/ogo/fea/__init__.py b/ogo/fea/__init__.py new file mode 100644 index 0000000..90a2bc2 --- /dev/null +++ b/ogo/fea/__init__.py @@ -0,0 +1,2 @@ +"""Shared finite-element helpers used by Ogo FE command-line tools.""" + diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py new file mode 100644 index 0000000..d275d58 --- /dev/null +++ b/ogo/fea/femur.py @@ -0,0 +1,46 @@ +"""Femur-specific helpers for sideways-fall FE model generation.""" + + +LEFT_FEMUR = 1 +RIGHT_FEMUR = 2 + + +def side_suffix(femur_side): + """Return the legacy output suffix for a femur side.""" + if femur_side == LEFT_FEMUR: + return "_LT_FEMUR_SF" + if femur_side == RIGHT_FEMUR: + return "_RT_FEMUR_SF" + raise ValueError("femur_side must be 1 for left or 2 for right.") + + +def side_rotation(femur_side): + """Return the pre-alignment z rotation for a femur side.""" + if femur_side == LEFT_FEMUR: + return 90 + if femur_side == RIGHT_FEMUR: + return -90 + raise ValueError("femur_side must be 1 for left or 2 for right.") + + +def sideways_fall_output_name(output_file, femur_side): + """Apply the legacy left/right suffix to a sideways-fall output path.""" + suffix = side_suffix(femur_side) + if str(output_file).endswith(".n88model"): + return str(output_file).replace(".n88model", f"{suffix}.n88model") + return f"{output_file}{suffix}.n88model" + + +def tilted_side_support_vector(angle_degrees=-20): + """Return the distal support vector used for the sideways-fall fixture.""" + import numpy as np + + theta = np.radians(angle_degrees) + rotation_x = np.array( + [ + [1, 0, 0], + [0, np.cos(theta), -np.sin(theta)], + [0, np.sin(theta), np.cos(theta)], + ] + ) + return rotation_x @ np.array([0, 0, -1]) diff --git a/ogo/fea/materials.py b/ogo/fea/materials.py new file mode 100644 index 0000000..ec65325 --- /dev/null +++ b/ogo/fea/materials.py @@ -0,0 +1,150 @@ +"""Material table helpers for Ogo finite-element model generation.""" + +import inspect + +import ogo.cli.ref.material_laws as material_laws + + +def resolve_material_func(func_or_name, module=material_laws): + """Return a material-law callable from a function object or function name.""" + if callable(func_or_name): + return func_or_name + if isinstance(func_or_name, str): + return getattr(module, func_or_name) + return None + + +def describe_material_func(value): + """Return a compact display value for FE argument logging.""" + if inspect.isfunction(value): + return f"function -> {value.__name__}" + return value + + +def build_spine_material_table( + bin_centers, + *, + n_bins=128, + poissons_ratio=0.3, + pmma_mat_id=5000, + pmma_E=2500, + pmma_v=0.3, + pmma_yield_compression=None, + pmma_yield_tension=None, + elastic_E_func=None, + yield_comp_func=None, + yield_tens_func=None, + cort_elastic_E_func=None, + cort_yield_comp_func=None, + cort_yield_tens_func=None, + cort_poissons_ratio=None, +): + """Build trabecular, cortical, and PMMA materials for spine compression FE.""" + import vtkbone + + import ogo.util.Helper as ogo + + elastic_E_func = resolve_material_func(elastic_E_func) or material_laws.default_E + yield_comp_func = resolve_material_func(yield_comp_func) + yield_tens_func = resolve_material_func(yield_tens_func) + cort_elastic_E_func = resolve_material_func(cort_elastic_E_func) or elastic_E_func + cort_yield_comp_func = resolve_material_func(cort_yield_comp_func) or yield_comp_func + cort_yield_tens_func = resolve_material_func(cort_yield_tens_func) or yield_tens_func + cort_poissons_ratio = poissons_ratio if cort_poissons_ratio is None else cort_poissons_ratio + + material_table = vtkbone.vtkboneMaterialTable() + + # Material ids 0..127 are trabecular; 128..255 are cortical after binning. + material_table = ogo.add_bone_material( + material_table, + bin_centers, + elastic_E_func=elastic_E_func, + mu=poissons_ratio, + yield_comp_func=yield_comp_func, + yield_tens_func=yield_tens_func, + bin_range=(0, n_bins), + material_name="TrabBone", + ) + material_table = ogo.add_bone_material( + material_table, + bin_centers, + elastic_E_func=cort_elastic_E_func, + mu=cort_poissons_ratio, + yield_comp_func=cort_yield_comp_func, + yield_tens_func=cort_yield_tens_func, + bin_range=(n_bins, 2 * n_bins), + material_name="CortBone", + ) + material_table = ogo.add_pmma_material( + material_table, + pmma_mat_id, + pmma_E, + pmma_v, + pmma_yield_tension=pmma_yield_tension, + pmma_yield_compression=pmma_yield_compression, + ) + return material_table + + +def build_femur_material_table( + bin_centers, + *, + elastic_Emax=10500, + elastic_exponent=2.29, + poissons_ratio=0.3, + bone_yield_compression=None, + bone_yield_tension=None, + pmma_mat_id=5000, + pmma_E=2500, + pmma_v=0.3, + pmma_yield_compression=None, + pmma_yield_tension=None, +): + """Build bone and PMMA materials for sideways-fall femur FE.""" + import vtkbone + + import ogo.util.Helper as ogo + + material_table = vtkbone.vtkboneMaterialTable() + material_table = ogo.add_bone_material_depreciated( + material_table, + bin_centers, + elastic_Emax=elastic_Emax, + elastic_exponent=elastic_exponent, + bone_yield_compression=bone_yield_compression, + bone_yield_tension=bone_yield_tension, + mu=poissons_ratio, + bin_range=(0, len(bin_centers)), + material_name="Bone", + ) + material_table = ogo.add_pmma_material( + material_table, + pmma_mat_id, + pmma_E, + pmma_v, + pmma_yield_tension=pmma_yield_tension, + pmma_yield_compression=pmma_yield_compression, + ) + return material_table + + +def spine_material_log_values(kwargs): + """Resolve material-law arguments so FE logs show the callables being used.""" + poissons_ratio = kwargs.get("poissons_ratio", 0.3) + elastic_E_func = resolve_material_func(kwargs.get("elastic_E_func")) or material_laws.default_E + yield_comp_func = resolve_material_func(kwargs.get("yield_comp_func")) + yield_tens_func = resolve_material_func(kwargs.get("yield_tens_func")) + cort_elastic_E_func = resolve_material_func(kwargs.get("cort_elastic_E_func")) or elastic_E_func + cort_yield_comp_func = resolve_material_func(kwargs.get("cort_yield_comp_func")) or yield_comp_func + cort_yield_tens_func = resolve_material_func(kwargs.get("cort_yield_tens_func")) or yield_tens_func + cort_poissons_ratio = poissons_ratio if kwargs.get("cort_poissons_ratio") is None else kwargs["cort_poissons_ratio"] + return { + "elastic_E_func": elastic_E_func, + "yield_comp_func": yield_comp_func, + "yield_tens_func": yield_tens_func, + "cort_elastic_E_func": cort_elastic_E_func, + "cort_yield_comp_func": cort_yield_comp_func, + "cort_yield_tens_func": cort_yield_tens_func, + "cort_poissons_ratio": cort_poissons_ratio, + } + diff --git a/ogo/fea/model.py b/ogo/fea/model.py new file mode 100644 index 0000000..5597636 --- /dev/null +++ b/ogo/fea/model.py @@ -0,0 +1,173 @@ +"""Shared vtkbone model-building helpers for Ogo FE workflows.""" + +from ogo.fea.materials import describe_material_func, spine_material_log_values + + +def log_fe_arguments(**kwargs): + """Print FE model parameters in the same style as the legacy scripts.""" + import ogo.util.Helper as ogo + + ogo.message("========= FE MODEL ARGUMENTS =========") + for key, value in kwargs.items(): + ogo.message(f"{key}: {describe_material_func(value)}") + ogo.message("======================================") + + +def find_and_add_visible_nodes(model, bc_geometry, normal_vector, bone_material_id, node_set_name): + """Find exterior nodes facing a direction and attach them as a model node set.""" + import vtk + import vtkbone + + import ogo.util.Helper as ogo + + visible_nodes_ids = vtk.vtkIdTypeArray() + vtkbone.vtkboneNodeSetsByGeometry.FindNodesOnVisibleSurface( + visible_nodes_ids, bc_geometry, normal_vector, bone_material_id + ) + visible_nodes_ids.SetName(node_set_name) + model.AddNodeSet(visible_nodes_ids) + ogo.message(f"Found {visible_nodes_ids.GetNumberOfTuples()} visible nodes for {node_set_name}.") + return model + + +def apply_spine_boundary_conditions(model, **kwargs): + """Apply axial compression boundary conditions for the spine FE model.""" + import vtkbone + + import ogo.util.Helper as ogo + + fe_displacement = kwargs.get("fe_displacement", -1.0) + top_node_set_name = kwargs.get("top_node_set_name", "body_top") + bottom_node_set_name = kwargs.get("bottom_node_set_name", "body_bottom") + + ogo.message("Applying boundary conditions...") + model.ApplyBoundaryCondition( + top_node_set_name, + vtkbone.vtkboneConstraint.SENSE_Z, + fe_displacement, + "top_displacement", + ) + model.ApplyBoundaryCondition( + bottom_node_set_name, + vtkbone.vtkboneConstraint.SENSE_Z, + 0, + "bottom_fixed_z", + ) + return model + + +def append_postprocessing_sets(model, set_names): + """Mark node sets and associated element sets for N88 postprocessing.""" + import vtkbone + + info = model.GetInformation() + pp_node_sets_key = vtkbone.vtkboneSolverParameters.POST_PROCESSING_NODE_SETS() + pp_elem_sets_key = vtkbone.vtkboneSolverParameters.POST_PROCESSING_ELEMENT_SETS() + for set_name in set_names: + pp_node_sets_key.Append(info, set_name) + element_set = model.GetAssociatedElementsFromNodeSet(set_name) + model.AddElementSet(element_set) + pp_elem_sets_key.Append(info, set_name) + return model + + +def write_model(model, output_path): + """Write a vtkbone model to an ``.n88model`` file.""" + import vtkbone + + writer = vtkbone.vtkboneN88ModelWriter() + writer.SetInputData(model) + writer.SetFileName(str(output_path)) + writer.Update() + return output_path + + +def create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, **kwargs): + """Create the spine compression micro-FE model used by Ogo and the benchmark.""" + import ogo.util.Helper as ogo + + from ogo.fea.materials import build_spine_material_table + + n_bins = 128 + poissons_ratio = kwargs.get("poissons_ratio", 0.3) + pmma_mat_id = kwargs.get("pmma_mat_id", 5000) + pmma_E = kwargs.get("pmma_E", 2500) + pmma_v = kwargs.get("pmma_v", 0.3) + top_displacement = kwargs.get("top_displacement", "top_displacement") + top_direction = kwargs.get("top_direction", (0, 0, 1)) + bottom_direction = kwargs.get("bottom_direction", (0, 0, -1)) + top_node_set_id = kwargs.get("top_node_set_id", 4) + bottom_node_set_id = kwargs.get("bottom_node_set_id", 3) + top_node_set_name = kwargs.get("top_node_set_name", "body_top") + bottom_node_set_name = kwargs.get("bottom_node_set_name", "body_bottom") + pmma_yield_compression = kwargs.get("pmma_yield_compression", None) + pmma_yield_tension = kwargs.get("pmma_yield_tension", None) + material_log_values = spine_material_log_values(kwargs) + + log_fe_arguments( + n_bins=n_bins, + poissons_ratio=poissons_ratio, + pmma_mat_id=pmma_mat_id, + pmma_E=pmma_E, + pmma_v=pmma_v, + top_displacement=top_displacement, + top_direction=top_direction, + bottom_direction=bottom_direction, + top_node_set_id=top_node_set_id, + bottom_node_set_id=bottom_node_set_id, + top_node_set_name=top_node_set_name, + bottom_node_set_name=bottom_node_set_name, + pmma_yield_compression=pmma_yield_compression, + pmma_yield_tension=pmma_yield_tension, + **material_log_values, + ) + + ogo.message("Casting to Short Integer datatype...") + image_pads_short = ogo.cast2short(image_with_pads) + + ogo.message("Filtering connected components...") + conn = ogo.imageConnectivity(image_pads_short) + conn_bc = ogo.imageConnectivity(boundary_masks_with_pads) + + ogo.message("Meshing...") + mesh = ogo.Image2Mesh(conn) + temp_bc_mesh = ogo.Image2Mesh(conn_bc) + + ogo.message("Setting up the Finite Element Material Table...") + material_table = build_spine_material_table( + bin_centers, + n_bins=n_bins, + poissons_ratio=poissons_ratio, + pmma_mat_id=pmma_mat_id, + pmma_E=pmma_E, + pmma_v=pmma_v, + pmma_yield_compression=pmma_yield_compression, + pmma_yield_tension=pmma_yield_tension, + elastic_E_func=kwargs.get("elastic_E_func"), + yield_comp_func=kwargs.get("yield_comp_func"), + yield_tens_func=kwargs.get("yield_tens_func"), + cort_elastic_E_func=kwargs.get("cort_elastic_E_func"), + cort_yield_comp_func=kwargs.get("cort_yield_comp_func"), + cort_yield_tens_func=kwargs.get("cort_yield_tens_func"), + cort_poissons_ratio=kwargs.get("cort_poissons_ratio"), + ) + + ogo.message("Constructing the Finite Element Model...") + model = ogo.applyTestBase(mesh, material_table) + model.ComputeBounds() + + ogo.message("Identifying boundary nodes...") + model = find_and_add_visible_nodes( + model, temp_bc_mesh, top_direction, top_node_set_id, top_node_set_name + ) + model = find_and_add_visible_nodes( + model, temp_bc_mesh, bottom_direction, bottom_node_set_id, bottom_node_set_name + ) + model = apply_spine_boundary_conditions(model, **kwargs) + + ogo.message("Setting convergence criteria...") + model.ConvergenceSetFromConstraint(top_displacement) + + ogo.message("Postprocessing...") + return append_postprocessing_sets(model, [top_node_set_name, bottom_node_set_name]) + diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py new file mode 100644 index 0000000..7412321 --- /dev/null +++ b/ogo/fea/spine.py @@ -0,0 +1,117 @@ +"""Spine-specific FE helpers and spineFE benchmark presets.""" + +from pathlib import Path + + +def benchmark_linear_params(): + """Return the linear spineFE-benchmark model parameters.""" + return { + "poissons_ratio": 0.3, + "pmma_mat_id": 300, + "pmma_E": 2500, + "pmma_v": 0.3, + "top_node_set_id": 6, + "bottom_node_set_id": 5, + "top_direction": (0, 0, 1), + "bottom_direction": (0, 0, -1), + "fe_displacement": -0.2, + "pmma_yield_compression": None, + "pmma_yield_tension": None, + "cort_poissons_ratio": None, + "elastic_E_func": "kopperdahl_trab_E", + "yield_comp_func": None, + "yield_tens_func": None, + "cort_elastic_E_func": "kopperdahl_trab_E", + "cort_yield_comp_func": None, + "cort_yield_tens_func": None, + } + + +def benchmark_nonlinear_params(): + """Return the nonlinear spineFE-benchmark model parameters.""" + params = benchmark_linear_params() + params.update( + { + "fe_displacement": -2.0, + "pmma_yield_compression": 70.0, + "pmma_yield_tension": 70.0, + "yield_comp_func": "kopperdahl_trab_yc", + "yield_tens_func": "kopperdahl_trab_yc", + "cort_yield_comp_func": "kopperdahl_trab_yc", + "cort_yield_tens_func": "kopperdahl_trab_yc", + } + ) + return params + + +def label_mask_from_vtk(vtk_image, condition_fn): + """Create a binary VTK mask using a NumPy condition over voxel labels.""" + import numpy as np + import vtk + from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + + arr = vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape( + vtk_image.GetDimensions(), order="F" + ) + out_arr = condition_fn(arr).astype(np.uint8) + out_vtk = vtk_image.NewInstance() + out_vtk.DeepCopy(vtk_image) + out_vtk.GetPointData().SetScalars( + numpy_to_vtk(out_arr.ravel(order="F"), deep=True, array_type=vtk.VTK_UNSIGNED_CHAR) + ) + return out_vtk + + +def prepare_benchmark_images(input_image_path, input_mask_path, n_bins=128): + """Reproduce the spineFE-benchmark notebook image and mask preparation.""" + import numpy as np + + from ogo.cli.ref.SpineCompressionFe import convert_image_to_material, merge_vtk_images, read + + input_image = read(str(input_image_path)).GetOutput() + input_mask_with_disk = read(str(input_mask_path)).GetOutput() + + cortical_mask = label_mask_from_vtk(input_mask_with_disk, lambda x: np.isin(x, [2, 4])) + disk_mask = label_mask_from_vtk(input_mask_with_disk, lambda x: np.isin(x, [5, 6])) + vertebra_mask = label_mask_from_vtk( + input_mask_with_disk, + lambda x: np.where(np.isin(x, [1, 2]), 1, np.where(np.isin(x, [3, 4]), 2, 0)), + ) + + binned_image, bin_centers = convert_image_to_material( + input_image, vertebra_mask, n_bins=n_bins, cort_mask=cortical_mask + ) + image_with_disk = merge_vtk_images([binned_image, disk_mask], [None, 300]) + return image_with_disk, input_mask_with_disk, bin_centers + + +def build_benchmark_sample_model(input_image_path, input_mask_path, output_model_path, nonlinear=False): + """Build the public spineFE-benchmark sample model and write it to disk.""" + from ogo.fea.model import create_microfe_model, write_model + + image_with_disk, mask_with_disk, bin_centers = prepare_benchmark_images( + input_image_path, input_mask_path + ) + params = benchmark_nonlinear_params() if nonlinear else benchmark_linear_params() + model = create_microfe_model(image_with_disk, mask_with_disk, bin_centers, **params) + write_model(model, output_model_path) + return model + + +def find_spinefe_benchmark_dir(start_dir=None, env_var="SPINEFE_BENCHMARK_DIR"): + """Locate the optional public spineFE-benchmark checkout.""" + import os + + env_path = os.environ.get(env_var) + if env_path: + candidate = Path(env_path).expanduser() + if candidate.exists(): + return candidate + + base = Path(start_dir or Path.cwd()).resolve() + for parent in [base] + list(base.parents): + candidate = parent / "spineFE-benchmark" + if candidate.exists(): + return candidate + return None + diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..9e08fb8 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +markers = + integration: tests that require external sample data or solver/imaging runtimes diff --git a/tests/fea/fixtures/spinefe_sub_verse004_vertebra21_metadata.json b/tests/fea/fixtures/spinefe_sub_verse004_vertebra21_metadata.json new file mode 100644 index 0000000..b92b3e6 --- /dev/null +++ b/tests/fea/fixtures/spinefe_sub_verse004_vertebra21_metadata.json @@ -0,0 +1,15 @@ +{ + "linear": { + "node_sets": { + "body_top": 1432, + "body_bottom": 1413 + } + }, + "nonlinear": { + "node_sets": { + "body_top": 1432, + "body_bottom": 1413 + } + } +} + diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py new file mode 100644 index 0000000..17bec3d --- /dev/null +++ b/tests/fea/test_femur.py @@ -0,0 +1,29 @@ +import pytest + +from ogo.fea import femur + + +def test_side_suffix_and_rotation_match_legacy_outputs(): + assert femur.side_suffix(1) == "_LT_FEMUR_SF" + assert femur.side_suffix(2) == "_RT_FEMUR_SF" + assert femur.side_rotation(1) == 90 + assert femur.side_rotation(2) == -90 + + +def test_sideways_fall_output_name_preserves_legacy_pattern(): + assert femur.sideways_fall_output_name("density.n88model", 1) == "density_LT_FEMUR_SF.n88model" + assert femur.sideways_fall_output_name("density.n88model", 2) == "density_RT_FEMUR_SF.n88model" + + +def test_invalid_femur_side_is_rejected(): + with pytest.raises(ValueError): + femur.side_suffix(3) + + +def test_tilted_side_support_vector_has_unit_length(): + np = pytest.importorskip("numpy") + + vector = femur.tilted_side_support_vector(-20) + + assert np.linalg.norm(vector) == pytest.approx(1.0) + diff --git a/tests/fea/test_materials.py b/tests/fea/test_materials.py new file mode 100644 index 0000000..7be69d2 --- /dev/null +++ b/tests/fea/test_materials.py @@ -0,0 +1,51 @@ +import pytest + +from ogo.cli.ref import material_laws +from ogo.fea import materials + + +def test_resolve_material_func_accepts_callable_name_and_none(): + def custom_law(density): + return density + + assert materials.resolve_material_func(custom_law) is custom_law + assert materials.resolve_material_func("kopperdahl_trab_E") is material_laws.kopperdahl_trab_E + assert materials.resolve_material_func(None) is None + + +def test_spine_material_log_values_defaults_to_default_E(): + values = materials.spine_material_log_values({}) + + assert values["elastic_E_func"] is material_laws.default_E + assert values["cort_elastic_E_func"] is material_laws.default_E + assert values["yield_comp_func"] is None + assert values["cort_poissons_ratio"] == 0.3 + + +def test_spine_material_log_values_resolves_benchmark_laws(): + values = materials.spine_material_log_values( + { + "elastic_E_func": "kopperdahl_trab_E", + "yield_comp_func": "kopperdahl_trab_yc", + "cort_elastic_E_func": "kopperdahl_trab_E", + } + ) + + assert values["elastic_E_func"] is material_laws.kopperdahl_trab_E + assert values["yield_comp_func"] is material_laws.kopperdahl_trab_yc + assert values["cort_elastic_E_func"] is material_laws.kopperdahl_trab_E + + +def test_build_spine_material_table_requires_vtkbone_when_exercised(): + pytest.importorskip("vtkbone") + + table = materials.build_spine_material_table( + [100.0, 200.0], + n_bins=2, + pmma_mat_id=300, + elastic_E_func="kopperdahl_trab_E", + cort_elastic_E_func="kopperdahl_trab_E", + ) + + assert table.GetNumberOfMaterials() == 5 + diff --git a/tests/fea/test_spine.py b/tests/fea/test_spine.py new file mode 100644 index 0000000..250a9a1 --- /dev/null +++ b/tests/fea/test_spine.py @@ -0,0 +1,15 @@ +from ogo.fea import spine + + +def test_benchmark_presets_match_spinefe_notebook_settings(): + linear = spine.benchmark_linear_params() + nonlinear = spine.benchmark_nonlinear_params() + + assert linear["fe_displacement"] == -0.2 + assert linear["elastic_E_func"] == "kopperdahl_trab_E" + assert linear["yield_comp_func"] is None + assert linear["pmma_yield_compression"] is None + assert nonlinear["fe_displacement"] == -2.0 + assert nonlinear["yield_comp_func"] == "kopperdahl_trab_yc" + assert nonlinear["pmma_yield_compression"] == 70.0 + diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py new file mode 100644 index 0000000..e84783a --- /dev/null +++ b/tests/fea/test_spinefe_benchmark.py @@ -0,0 +1,98 @@ +import csv +import json +from pathlib import Path +import shutil + +import pytest + +from ogo.fea.spine import build_benchmark_sample_model, find_spinefe_benchmark_dir + + +def _node_set_count(model, name): + node_set = model.GetNodeSet(name) + if node_set is None: + raise AssertionError(f"Model is missing node set {name!r}") + return node_set.GetNumberOfTuples() + + +def _sample_paths(): + benchmark_dir = find_spinefe_benchmark_dir(Path(__file__).resolve().parents[2]) + if benchmark_dir is None: + pytest.skip("spineFE-benchmark checkout not found") + + image = benchmark_dir / "data" / "nii_files" / "sub-verse004_0000_vertebra_21_im.nii.gz" + mask = benchmark_dir / "data" / "nii_files" / "sub-verse004_0000_vertebra_21_seg.nii.gz" + if not image.exists() or not mask.exists(): + pytest.skip("spineFE-benchmark sample NIfTI files not found") + return image, mask + + +@pytest.mark.integration +@pytest.mark.parametrize("case_name, nonlinear", [("linear", False), ("nonlinear", True)]) +def test_spinefe_benchmark_sample_model_generation_matches_golden_metadata( + tmp_path, case_name, nonlinear +): + pytest.importorskip("vtk") + pytest.importorskip("vtkbone") + pytest.importorskip("SimpleITK") + + image, mask = _sample_paths() + expected_path = Path(__file__).parent / "fixtures" / "spinefe_sub_verse004_vertebra21_metadata.json" + expected = json.loads(expected_path.read_text(encoding="utf-8"))[case_name] + + model = build_benchmark_sample_model( + image, + mask, + tmp_path / f"sub-verse004_0000_vertebra_21_{case_name}.n88model", + nonlinear=nonlinear, + ) + + for node_set_name, expected_count in expected["node_sets"].items(): + assert _node_set_count(model, node_set_name) == expected_count + + +@pytest.mark.integration +def test_spinefe_benchmark_solve_outputs_match_golden_csv(tmp_path): + pytest.importorskip("vtk") + pytest.importorskip("vtkbone") + pytest.importorskip("SimpleITK") + + required_tools = [ + "n88modelinfo", + "n88derivedfields", + "n88postfaim", + "n88pistoia", + "n88tabulate", + "n88copymodel", + ] + if not (shutil.which("faim") or shutil.which("n88solver_slt")): + pytest.skip("FAIM/N88 solver command not found") + missing_tools = [tool for tool in required_tools if shutil.which(tool) is None] + if missing_tools: + pytest.skip("Incomplete FAIM/N88 postprocessing toolchain: " + ", ".join(missing_tools)) + + golden_csv = Path(__file__).parent / "fixtures" / "spinefe_sub_verse004_vertebra21_solve.csv" + if not golden_csv.exists(): + pytest.skip("Solve-level golden CSV has not been generated on a full FAIM/N88 install") + + from ogo.util.faim import run_faim_pipeline + + image, mask = _sample_paths() + model_path = tmp_path / "sub-verse004_0000_vertebra_21_linear.n88model" + build_benchmark_sample_model(image, mask, model_path, nonlinear=False) + outputs = run_faim_pipeline( + model_file=model_path, + analysis_var="fz_ns1", + pistoia_vars=["pis_fz_fail", "pis_stiffz"], + failure_axis="z", + applied_displacement=-0.2, + target_displacement=0.2, + ) + + with golden_csv.open(newline="") as handle: + expected = next(csv.DictReader(handle)) + with outputs["results_csv"].open(newline="") as handle: + actual = next(csv.DictReader(handle)) + + for key in ["load_at_0p2_percent_N", "pistoia_failure_load_N", "stiffness_N_per_mm"]: + assert float(actual[key]) == pytest.approx(float(expected[key]), rel=1e-4) From 51513dde313ff5b754914dd6912bb7e43f18d051 Mon Sep 17 00:00:00 2001 From: wallematthias Date: Tue, 23 Jun 2026 22:39:40 -0600 Subject: [PATCH 03/67] Streamline FEA generation and reporting --- ogo/cli/CheckFEModelBC.py | 497 +++++++++++++++ ogo/cli/GenerateFEM.py | 598 +++++++++++++++++- ogo/cli/ref/FePistoiaTabulate.py | 179 ------ ogo/cli/ref/Pistoia.py | 487 -------------- ogo/fea/README.md | 406 ++++++++++++ ogo/fea/boundary.py | 469 ++++++++++++++ ogo/fea/femur.py | 374 ++++++++++- ogo/{cli/ref => fea}/material_laws.py | 0 ogo/fea/materials.py | 243 +++++-- ogo/fea/model.py | 13 +- .../sideways_fall.py} | 391 ++++++++---- ogo/fea/spine.py | 70 +- .../spine_compression.py} | 413 ++++-------- ogo/util/Helper.py | 177 +++--- ogo/util/faim.py | 341 ++++++++-- ogo/util/vtk_image.py | 43 ++ setup.cfg | 3 +- tests/cli/test_GenerateFEM.py | 158 ++++- ...nefe_sub_verse004_vertebra21_metadata.json | 9 +- tests/fea/test_boundary.py | 188 ++++++ tests/fea/test_femur.py | 139 ++++ tests/fea/test_materials.py | 29 +- tests/fea/test_spinefe_benchmark.py | 22 +- tests/util/test_faim.py | 426 ++++++++++++- 24 files changed, 4327 insertions(+), 1348 deletions(-) create mode 100644 ogo/cli/CheckFEModelBC.py delete mode 100644 ogo/cli/ref/FePistoiaTabulate.py delete mode 100644 ogo/cli/ref/Pistoia.py create mode 100644 ogo/fea/README.md create mode 100644 ogo/fea/boundary.py rename ogo/{cli/ref => fea}/material_laws.py (100%) rename ogo/{cli/ref/SidewaysFallFe.py => fea/sideways_fall.py} (57%) rename ogo/{cli/ref/SpineCompressionFe.py => fea/spine_compression.py} (76%) create mode 100644 ogo/util/vtk_image.py create mode 100644 tests/fea/test_boundary.py diff --git a/ogo/cli/CheckFEModelBC.py b/ogo/cli/CheckFEModelBC.py new file mode 100644 index 0000000..19c14c6 --- /dev/null +++ b/ogo/cli/CheckFEModelBC.py @@ -0,0 +1,497 @@ +"""Audit FE model boundary-condition sets and displacement directions.""" + +import argparse +import csv +import json +from pathlib import Path +import subprocess + +import numpy as np +from netCDF4 import Dataset + +from ogo.fea.femur import ( + DISTAL_FEMUR_NODE_SET, + FEMORAL_HEAD_NODE_SET, + GREATER_TROCHANTER_NODE_SET, + SIDEWAYS_FALL_NODE_SETS, +) + + +SENSE_TO_AXIS = {1: "x", 2: "y", 3: "z"} + + +def _node_coordinates(root, node_numbers): + coords = root.groups["Parts"].groups["Part1"].variables["NodeCoordinates"][:] + return coords[np.asarray(node_numbers, dtype=np.int64) - 1] + + +def _bounds(points): + mins = points.min(axis=0) + maxs = points.max(axis=0) + return { + "x_min": float(mins[0]), + "x_max": float(maxs[0]), + "y_min": float(mins[1]), + "y_max": float(maxs[1]), + "z_min": float(mins[2]), + "z_max": float(maxs[2]), + } + + +def _centroid(points): + center = points.mean(axis=0) + return {"x": float(center[0]), "y": float(center[1]), "z": float(center[2])} + + +def read_node_sets(root): + out = {} + for name, group in root.groups["Sets"].groups["NodeSets"].groups.items(): + node_numbers = group.variables["NodeNumber"][:] + points = _node_coordinates(root, node_numbers) + out[name] = { + "count": int(len(node_numbers)), + "bounds": _bounds(points), + "centroid": _centroid(points), + } + return out + + +def read_constraints(root): + out = {} + for name, group in root.groups["Constraints"].groups.items(): + node_numbers = group.variables["NodeNumber"][:] + senses = np.asarray(group.variables["Sense"][:]) + values = np.asarray(group.variables["Value"][:], dtype=float) + points = _node_coordinates(root, node_numbers) + unique_senses = sorted(set(int(value) for value in senses.tolist())) + unique_values = sorted(set(float(value) for value in values.tolist())) + out[name] = { + "count": int(len(node_numbers)), + "axes": [SENSE_TO_AXIS.get(value, str(value)) for value in unique_senses], + "values": unique_values, + "bounds": _bounds(points), + "centroid": _centroid(points), + } + return out + + +def _single_axis(constraints, name): + axes = constraints.get(name, {}).get("axes", []) + return axes[0] if len(axes) == 1 else None + + +def _single_value(constraints, name): + values = constraints.get(name, {}).get("values", []) + return values[0] if len(values) == 1 else None + + +def audit_femur_sideways(node_sets, constraints, tolerance): + checks = [] + + for name in SIDEWAYS_FALL_NODE_SETS: + count = node_sets.get(name, {}).get("count", 0) + checks.append({"name": f"{name} exists", "passed": count > 0, "detail": str(count)}) + + fh = node_sets.get(FEMORAL_HEAD_NODE_SET) + gt = node_sets.get(GREATER_TROCHANTER_NODE_SET) + if fh and gt: + y_gap = gt["centroid"]["y"] - fh["centroid"]["y"] + applied = _single_value(constraints, "top_displacement") + axis = _single_axis(constraints, "top_displacement") + compressive = ( + axis == "y" + and applied is not None + and applied != 0 + and y_gap != 0 + and np.sign(applied) == np.sign(y_gap) + ) + checks.append({ + "name": "femoral head displacement moves toward fixed trochanter", + "passed": bool(compressive), + "detail": f"axis={axis}, value={applied}, gt_y_minus_fh_y={y_gap:.6g}", + }) + + distal = node_sets.get(DISTAL_FEMUR_NODE_SET) + if distal: + z_span = distal["bounds"]["z_max"] - distal["bounds"]["z_min"] + checks.append({ + "name": "distal shaft set is one flat z plane", + "passed": z_span <= tolerance, + "detail": f"z_span={z_span:.6g}", + }) + + expected_zero_constraints = { + "bottom_fixed_x": "x", + "bottom_fixed_z": "z", + "bottom_fixed_y_PMMA": "y", + } + for name, axis in expected_zero_constraints.items(): + actual_axis = _single_axis(constraints, name) + value = _single_value(constraints, name) + checks.append({ + "name": f"{name} fixes {axis}", + "passed": actual_axis == axis and value == 0.0, + "detail": f"axis={actual_axis}, value={value}", + }) + + return checks + + +def audit_spine_compression(node_sets, constraints, tolerance): + checks = [] + + required_sets = ["body_top", "body_bottom"] + for name in required_sets: + count = node_sets.get(name, {}).get("count", 0) + checks.append({"name": f"{name} exists", "passed": count > 0, "detail": str(count)}) + + top = node_sets.get("body_top") + bottom = node_sets.get("body_bottom") + if top and bottom: + z_gap = bottom["centroid"]["z"] - top["centroid"]["z"] + applied = _single_value(constraints, "top_displacement") + axis = _single_axis(constraints, "top_displacement") + compressive = ( + axis == "z" + and applied is not None + and applied != 0 + and z_gap != 0 + and np.sign(applied) == np.sign(z_gap) + ) + checks.append({ + "name": "top displacement moves toward bottom support", + "passed": bool(compressive), + "detail": f"axis={axis}, value={applied}, bottom_z_minus_top_z={z_gap:.6g}", + }) + + for set_name, set_data in (("body_top", top), ("body_bottom", bottom)): + z_span = set_data["bounds"]["z_max"] - set_data["bounds"]["z_min"] + checks.append({ + "name": f"{set_name} set is one flat z plane", + "passed": z_span <= tolerance, + "detail": f"z_span={z_span:.6g}", + }) + + axis = _single_axis(constraints, "bottom_fixed_z") + value = _single_value(constraints, "bottom_fixed_z") + checks.append({ + "name": "bottom_fixed_z fixes z", + "passed": axis == "z" and value == 0.0, + "detail": f"axis={axis}, value={value}", + }) + return checks + + +def audit_checks(model, node_sets, constraints, tolerance): + if model == "femur-sideways": + return audit_femur_sideways(node_sets, constraints, tolerance) + if model == "spine-compression": + return audit_spine_compression(node_sets, constraints, tolerance) + raise ValueError(f"Unsupported audit model: {model}") + + +def write_csv(path, node_sets, constraints, checks): + rows = [] + for kind, source in (("node_set", node_sets), ("constraint", constraints)): + for name, data in source.items(): + bounds = data["bounds"] + center = data["centroid"] + rows.append({ + "kind": kind, + "name": name, + "count": data["count"], + "axes": ",".join(data.get("axes", [])), + "values": ",".join(str(value) for value in data.get("values", [])), + "centroid_x": center["x"], + "centroid_y": center["y"], + "centroid_z": center["z"], + **bounds, + }) + + check_rows = [ + { + "kind": "check", + "name": check["name"], + "count": "", + "axes": "", + "values": "PASS" if check["passed"] else "FAIL", + "centroid_x": "", + "centroid_y": "", + "centroid_z": "", + "x_min": "", + "x_max": "", + "y_min": "", + "y_max": "", + "z_min": "", + "z_max": "", + } + for check in checks + ] + rows.extend(check_rows) + + with open(path, "w", newline="") as handle: + fieldnames = list(rows[0].keys()) if rows else [] + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + +def run_extract_sets(model_file, command): + subprocess.run([str(command), str(model_file)], check=True) + + +def read_all_node_coordinates(root): + return root.groups["Parts"].groups["Part1"].variables["NodeCoordinates"][:] + + +def read_node_set_points(root): + out = {} + for name, group in root.groups["Sets"].groups["NodeSets"].groups.items(): + out[name] = _node_coordinates(root, group.variables["NodeNumber"][:]) + return out + + +def _axis_range(coords, axis_index): + return float(coords[:, axis_index].min()), float(coords[:, axis_index].max()) + + +def _plot_projection(ax, coords, node_set_points, projection, displacement): + axis_a, axis_b, label_a, label_b = projection + colors = { + FEMORAL_HEAD_NODE_SET: "#d62728", + GREATER_TROCHANTER_NODE_SET: "#1f77b4", + DISTAL_FEMUR_NODE_SET: "#2ca02c", + "body_top": "#d62728", + "body_bottom": "#1f77b4", + } + labels = { + FEMORAL_HEAD_NODE_SET: "Head", + GREATER_TROCHANTER_NODE_SET: "GT", + DISTAL_FEMUR_NODE_SET: "Shaft", + "body_top": "Top", + "body_bottom": "Bottom", + } + + stride = max(1, int(np.ceil(coords.shape[0] / 30000.0))) + sample = coords[::stride] + ax.scatter(sample[:, axis_a], sample[:, axis_b], s=0.3, c="0.45", alpha=0.38, linewidths=0) + + for name, points in node_set_points.items(): + color = colors.get(name, "black") + label = labels.get(name, name.replace("_PMMA_Nodes", "").replace("_Nodes", "").replace("_", " ")) + ax.scatter(points[:, axis_a], points[:, axis_b], s=4, c=color, alpha=0.9, linewidths=0, label=label) + + if displacement is not None: + center = displacement["center"] + vector = displacement["vector"] + scale = 0.18 * max( + _axis_range(coords, axis_a)[1] - _axis_range(coords, axis_a)[0], + _axis_range(coords, axis_b)[1] - _axis_range(coords, axis_b)[0], + ) + vec2 = np.array([vector[axis_a], vector[axis_b]], dtype=float) + norm = float(np.linalg.norm(vec2)) + if norm > 0: + vec2 = vec2 / norm * scale + ax.arrow( + center[axis_a], + center[axis_b], + vec2[0], + vec2[1], + color="#d62728", + width=0.35, + head_width=2.5, + length_includes_head=True, + alpha=0.95, + ) + + amin, amax = _axis_range(coords, axis_a) + bmin, bmax = _axis_range(coords, axis_b) + apad = max((amax - amin) * 0.05, 1.0) + bpad = max((bmax - bmin) * 0.05, 1.0) + ax.set_xlim(amin - apad, amax + apad) + ax.set_ylim(bmin - bpad, bmax + bpad) + ax.set_xlabel(f"{label_a} (mm)") + ax.set_ylabel(f"{label_b} (mm)") + ax.set_aspect("equal", adjustable="box") + ax.grid(True, color="0.9", linewidth=0.4) + ax.tick_params(axis="both", which="major", labelsize=8, length=2.5, pad=1.5) + + +def _displacement_vector(node_sets, constraints): + top = constraints.get("top_displacement") + moving_set = node_sets.get(FEMORAL_HEAD_NODE_SET) or node_sets.get("body_top") + if not top or not moving_set: + return None + axis = _single_axis(constraints, "top_displacement") + value = _single_value(constraints, "top_displacement") + axis_index = {"x": 0, "y": 1, "z": 2}.get(axis) + if axis_index is None or value in (None, 0): + return None + vector = np.zeros(3, dtype=float) + vector[axis_index] = float(np.sign(value)) + center = np.array( + [moving_set["centroid"]["x"], moving_set["centroid"]["y"], moving_set["centroid"]["z"]], + dtype=float, + ) + return {"center": center, "vector": vector} + + +def write_png(path, coords, node_set_points, node_sets, constraints, checks): + import matplotlib + + matplotlib.use("Agg") + import matplotlib.pyplot as plt + from matplotlib.ticker import MaxNLocator + + plt.rcParams.update({ + "axes.labelsize": 8, + "axes.titlesize": 9, + "font.size": 8, + "legend.fontsize": 7, + }) + fig, axes = plt.subplots(2, 2, figsize=(7.0, 6.4), constrained_layout=True) + displacement = _displacement_vector(node_sets, constraints) + + projections = [ + (0, 1, "X", "Y"), + (0, 2, "X", "Z"), + (1, 2, "Y", "Z"), + ] + titles = ["X-Y", "X-Z", "Y-Z"] + for ax, projection, title in zip(axes.flat[:3], projections, titles): + _plot_projection(ax, coords, node_set_points, projection, displacement) + ax.set_title(title, pad=2) + ax.xaxis.set_major_locator(MaxNLocator(nbins=4)) + ax.yaxis.set_major_locator(MaxNLocator(nbins=4)) + + legend_handles, legend_labels = axes.flat[0].get_legend_handles_labels() + if legend_handles: + axes.flat[0].legend( + legend_handles, + legend_labels, + loc="upper left", + fontsize=7, + frameon=True, + borderpad=0.2, + labelspacing=0.25, + handlelength=0.8, + handletextpad=0.3, + markerscale=1.2, + ) + + text_ax = axes.flat[3] + text_ax.axis("off") + lines = [] + for check in checks: + status = "OK" if check["passed"] else "FAIL" + lines.append(f"{status}: {check['name']}") + text_ax.text( + 0.0, + 1.0, + "\n".join(lines), + va="top", + ha="left", + family="monospace", + fontsize=7, + linespacing=1.25, + transform=text_ax.transAxes, + ) + + fig.savefig(path, dpi=300) + plt.close(fig) + + +def audit_model( + model_file, + *, + model="femur-sideways", + output_prefix=None, + flat_tolerance=1.0e-4, + extract_sets=False, + n88extractsets_command="n88extractsets", + write_json=True, + write_csv_file=True, + write_plot=True, +): + model_file = Path(model_file) + output_prefix = Path(output_prefix) if output_prefix is not None else model_file.with_suffix("") + json_path = output_prefix.with_name(output_prefix.name + "_bc_audit.json") + csv_path = output_prefix.with_name(output_prefix.name + "_bc_audit.csv") + png_path = output_prefix.with_name(output_prefix.name + "_bc_audit.png") + + if extract_sets: + run_extract_sets(model_file, n88extractsets_command) + + with Dataset(str(model_file)) as root: + coords = read_all_node_coordinates(root) + node_set_points = read_node_set_points(root) + node_sets = read_node_sets(root) + constraints = read_constraints(root) + + checks = audit_checks(model, node_sets, constraints, flat_tolerance) + passed = all(check["passed"] for check in checks) + summary = { + "model_file": str(model_file), + "model": model, + "passed": passed, + "node_sets": node_sets, + "constraints": constraints, + "checks": checks, + } + + if write_json: + json_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n") + if write_csv_file: + write_csv(csv_path, node_sets, constraints, checks) + if write_plot: + write_png(png_path, coords, node_set_points, node_sets, constraints, checks) + return { + "passed": passed, + "json_path": json_path if write_json else None, + "csv_path": csv_path if write_csv_file else None, + "png_path": png_path if write_plot else None, + "checks": checks, + "summary": summary, + } + + +def main(): + parser = argparse.ArgumentParser( + prog="ogoCheckFEModelBC", + description="Audit n88model node sets, constraints, and loading direction.", + ) + parser.add_argument("model_file", type=Path) + parser.add_argument( + "--model", + choices=["femur-sideways", "spine-compression"], + default="femur-sideways", + ) + parser.add_argument("--output_prefix", type=Path, default=None) + parser.add_argument("--flat_tolerance", type=float, default=1.0e-4) + parser.add_argument("--extract_sets", action="store_true") + parser.add_argument("--n88extractsets_command", default="n88extractsets") + parser.add_argument("--no_png", action="store_true", help="Skip writing the PNG quick-look audit plot.") + args = parser.parse_args() + + result = audit_model( + args.model_file, + model=args.model, + output_prefix=args.output_prefix, + flat_tolerance=args.flat_tolerance, + extract_sets=args.extract_sets, + n88extractsets_command=args.n88extractsets_command, + write_plot=not args.no_png, + ) + + for check in result["checks"]: + status = "PASS" if check["passed"] else "FAIL" + print(f"{status}: {check['name']} ({check['detail']})") + print(f"Wrote {result['json_path']}") + print(f"Wrote {result['csv_path']}") + if result["png_path"] is not None: + print(f"Wrote {result['png_path']}") + raise SystemExit(0 if result["passed"] else 1) + + +if __name__ == "__main__": + main() diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 14ada50..d50eeca 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -1,18 +1,42 @@ -"""Generate finite-element models for spine vertebrae and hip femurs. +"""Generate and solve finite-element models for spine vertebrae and hip femurs. -This module is intentionally a thin orchestration layer. The scientific model -generation remains in ``ogo.cli.ref.SpineCompressionFe`` and -``ogo.cli.ref.SidewaysFallFe``; this command provides a clean user-facing entry -point for running one or many targets. +This module is intentionally a thin orchestration layer. The scientific model +generation lives in ``ogo.fea``; this command provides the maintained +user-facing entry point for running one or many targets. """ import argparse from collections import namedtuple import importlib.util +import json from pathlib import Path import sys from typing import Callable, List, Optional, Sequence +from ogo.fea.spine import ( + BENCHMARK_LINEAR_FE_DISPLACEMENT_MM, + BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM, + DEFAULT_SPINE_FE_DISPLACEMENT_MM, + DEFAULT_SPINE_ISO_RESOLUTION_MM, + DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + DEFAULT_SPINE_PMMA_THICKNESS_MM, + DEFAULT_SPINE_REGISTRATION_MAX_SCALE, + DEFAULT_SPINE_REGISTRATION_MIN_SCALE, + DEFAULT_SPINE_TARGET_DISPLACEMENT_PERCENT, + SPINE_ALIGNMENT_METHOD, + default_spine_reference_path, +) +from ogo.fea.femur import ( + DEFAULT_FEMUR_CUT_MODE, + DEFAULT_FEMUR_FE_DISPLACEMENT, + DEFAULT_FEMUR_ISO_RESOLUTION_MM, + DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + DEFAULT_FEMUR_TARGET_DISPLACEMENT_PERCENT, + DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, + DEFAULT_PMMA_INTRUSION_MM, + DEFAULT_PMMA_THICKNESS_MM, +) + SpineTarget = namedtuple("SpineTarget", ["level", "body_label", "process_label"]) @@ -20,7 +44,7 @@ "none": [], "benchmark-linear": [ "--fe_displacement", - "-0.2", + str(BENCHMARK_LINEAR_FE_DISPLACEMENT_MM), "--elastic_E_func", "kopperdahl_trab_E", "--cort_elastic_E_func", @@ -28,7 +52,7 @@ ], "benchmark-nonlinear": [ "--fe_displacement", - "-2.0", + str(BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM), "--pmma_yield_compression", "70.0", "--pmma_yield_tension", @@ -160,6 +184,77 @@ def option_value(argv: Sequence[str], option: str, default: Optional[str] = None return value +def option_float(argv: Sequence[str], option: str, default: float) -> float: + """Return the final numeric value for an argv-style option.""" + return float(option_value(argv, option, str(default))) + + +def option_optional_float(argv: Sequence[str], option: str) -> Optional[float]: + """Return the final numeric value for an optional argv-style option.""" + value = option_value(argv, option) + return None if value is None else float(value) + + +def option_int(argv: Sequence[str], option: str, default: int) -> int: + """Return the final integer value for an argv-style option.""" + return int(option_value(argv, option, str(default))) + + +def option_present(argv: Sequence[str], option: str) -> bool: + """Return whether a flag-like option is present.""" + return option in argv + + +def option_path(argv: Sequence[str], option: str, default: Optional[Path] = None) -> Optional[str]: + """Return the final path value for an option as a string.""" + value = option_value(argv, option, str(default) if default is not None else None) + return None if value is None else str(value) + + +def target_displacement_percent(args: argparse.Namespace, model_type: str) -> float: + """Return the maintained endpoint displacement percentage for this model.""" + if args.target_displacement is not None: + return args.target_displacement + if model_type == "spine": + return DEFAULT_SPINE_TARGET_DISPLACEMENT_PERCENT + return DEFAULT_FEMUR_TARGET_DISPLACEMENT_PERCENT + + +def percent_displacement_metadata( + model_path: Path, + *, + report_profile: str, + failure_axis: str, + target_percent: float, +) -> dict: + """Describe the solve displacement derived from model geometry.""" + metadata = { + "value_mm": None, + "target_displacement_percent": target_percent, + "characteristic_length_mm": None, + "value_source": "target_displacement_percent * characteristic_length_mm / 100", + } + try: + from ogo.util.faim import ( + infer_profile_characteristic_length_mm, + read_prescribed_displacement, + ) + + characteristic_length = infer_profile_characteristic_length_mm( + model_path, + report_profile, + failure_axis, + ) + current_value = read_prescribed_displacement(model_path, report_profile) + sign = -1.0 if current_value != "" and float(current_value) < 0 else 1.0 + target_mm = abs(float(target_percent)) * characteristic_length / 100.0 + metadata["value_mm"] = sign * target_mm + metadata["characteristic_length_mm"] = characteristic_length + except Exception as exc: + metadata["value_note"] = "available after model generation with netCDF4: {}".format(exc) + return metadata + + def solve_model( model_path: Path, args: argparse.Namespace, @@ -180,14 +275,25 @@ def solve_model( # Spine compression uses z reaction force; sideways-fall hip uses y. if model_type == "spine": analysis_var = "fz_ns1" - pistoia_vars = ["pis_fz_fail", "pis_stiffz"] + pistoia_vars = [] failure_axis = "z" else: analysis_var = "fy_ns1" pistoia_vars = ["pis_fy_fail", "pis_stiffy"] failure_axis = "y" - applied_displacement = option_value(generator_argv, "--fe_displacement", "-1.0") + default_applied_displacement = ( + str(DEFAULT_SPINE_FE_DISPLACEMENT_MM) + if model_type == "spine" + else str(DEFAULT_FEMUR_FE_DISPLACEMENT) + ) + applied_displacement = option_value( + generator_argv, + "--fe_displacement", + default_applied_displacement, + ) + target_displacement = target_displacement_percent(args, model_type) + report_profile = "spine" if model_type == "spine" else "femur" run_faim_pipeline( model_file=model_path, @@ -211,31 +317,439 @@ def solve_model( critical_volume=args.critical_volume, critical_strain=args.critical_strain, exclude=args.exclude, + run_pistoia=False, applied_displacement=applied_displacement, - target_displacement=args.target_displacement, + target_displacement=target_displacement, + report_profile=report_profile, + solve_displacement_percent=target_displacement, compress=not args.no_compress, require_pistoia=args.require_pistoia, dry_run=args.dry_run, ) +def write_modeling_metadata( + model_path: Path, + model_type: str, + generator_argv: Sequence[str], + args: argparse.Namespace, + bc_audit_summary: Optional[dict] = None, +) -> Optional[Path]: + """Write a traceable model-building record next to a generated n88model.""" + if args.dry_run: + return None + if not model_path.exists(): + return None + + output_path = model_path.with_name(model_path.with_suffix("").name + "_modeling.json") + common = { + "schema_version": 1, + "model_file": str(model_path), + "generator": { + "entry_point": "ogoFEA", + "model_type": model_type, + "lower_level_argv": list(generator_argv), + }, + "inputs": { + "calibrated_image": str(args.calibrated_image), + "bone_mask": str(args.bone_mask), + "output_path": str(args.output_path) if args.output_path is not None else str(args.calibrated_image.parent), + }, + "post_generation_validation": { + "bc_audit": { + "enabled": bc_audit_summary is not None, + "flat_tolerance": args.bc_audit_flat_tolerance, + "summary": bc_audit_summary, + "json_path": None, + "csv_path": None, + "png_path": None + if not args.debug + else str(model_path.with_name(model_path.with_suffix("").name + "_bc_audit.png")), + } + }, + "solve_and_reporting": { + "solve_requested": not args.no_solve, + "target_displacement_percent": target_displacement_percent(args, model_type), + "target_displacement_definition": "percent strain converted from model geometry for spine; percent of femur length for femur", + "run_pistoia": False, + "compress_solved_model": not args.no_compress, + }, + } + + if model_type == "spine": + body_label = option_int(generator_argv, "--mask_threshold", 0) + process_label = option_int(generator_argv, "--process_mask_threshold", 0) + appendix = option_value(generator_argv, "--appendix") + spine_displacement = percent_displacement_metadata( + model_path, + report_profile="spine", + failure_axis="z", + target_percent=common["solve_and_reporting"]["target_displacement_percent"], + ) + metadata = common.copy() + metadata.update({ + "model": "spine-compression", + "target": { + "vertebra": appendix, + "body_label": body_label, + "process_label": process_label, + }, + "alignment": { + "method": SPINE_ALIGNMENT_METHOD, + "reference_path": option_value( + generator_argv, + "--reference_path", + str(default_spine_reference_path()), + ), + "registration_scale": option_value(generator_argv, "--registration_scale", "auto"), + "registration_min_scale": option_value( + generator_argv, + "--registration_min_scale", + DEFAULT_SPINE_REGISTRATION_MIN_SCALE, + ), + "registration_max_scale": option_value( + generator_argv, + "--registration_max_scale", + DEFAULT_SPINE_REGISTRATION_MAX_SCALE, + ), + }, + "image_processing": { + "iso_resolution_mm": option_float(generator_argv, "--iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM), + "spatial_operations": "ICP transform and isotropic output spacing in one shared VTK reslice", + "image_interpolation": "cubic", + "label_interpolation": "nearest-neighbor", + "mask_smoothing": { + "operation": "binary close/open after ICP resampling", + "condition": "enabled only when any input spacing dimension exceeds threshold_mm", + "threshold_mm": option_float( + generator_argv, + "--mask_smoothing_spacing_threshold", + DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + ), + }, + "density_preprocessing": { + "connectivity_filter": True, + "bmd_preprocess_threshold": -31, + "density_binning": { + "n_bins": 128, + "background_material_id": 0, + "trabecular_material_ids": [1, 128], + "cortical_material_ids": [129, 256], + }, + }, + }, + "segmentation": { + "input_mask": str(args.bone_mask), + "labels": { + "vertebral_body": body_label, + "posterior_process": process_label, + }, + "derived_masks": { + "body": "threshold body label", + "process": "threshold process label", + "cortical": "density/surface-derived cortical shell generated after alignment", + "pmma_caps": "fit-to-body superior and inferior caps", + }, + }, + "materials": { + "builder": "ogo.fea.materials.build_spine_material_table", + "convention": "region material IDs; 0 background, 1..128 trabecular, 129..256 cortical, PMMA explicit", + "poissons_ratio": option_float(generator_argv, "--poissons_ratio", 0.3), + "trabecular": { + "elastic_E_func": option_value(generator_argv, "--elastic_E_func", "default_E"), + "yield_comp_func": option_value(generator_argv, "--yield_comp_func"), + "yield_tens_func": option_value(generator_argv, "--yield_tens_func"), + "material_id_range": [1, 128], + }, + "cortical": { + "elastic_E_func": option_value( + generator_argv, + "--cort_elastic_E_func", + option_value(generator_argv, "--elastic_E_func", "default_E"), + ), + "yield_comp_func": option_value( + generator_argv, + "--cort_yield_comp_func", + option_value(generator_argv, "--yield_comp_func"), + ), + "yield_tens_func": option_value( + generator_argv, + "--cort_yield_tens_func", + option_value(generator_argv, "--yield_tens_func"), + ), + "poissons_ratio": option_float( + generator_argv, + "--cort_poissons_ratio", + option_float(generator_argv, "--poissons_ratio", 0.3), + ), + "material_id_range": [129, 256], + }, + "pmma": { + "material_id": option_int(generator_argv, "--pmma_mat_id", 5000), + "elastic_E_MPa": option_float(generator_argv, "--pmma_E", 2500), + "poissons_ratio": option_float(generator_argv, "--pmma_v", 0.3), + "yield_compression_MPa": option_optional_float(generator_argv, "--pmma_yield_compression"), + "yield_tension_MPa": option_optional_float(generator_argv, "--pmma_yield_tension"), + }, + }, + "boundary_conditions": { + "fixture_geometry": { + "superior_cap": { + "label_id": option_int(generator_argv, "--top_node_set_id", 4), + "node_set": "body_top", + "shape": "fit to vertebral body surface", + "pmma_thickness_mm": option_float(generator_argv, "--pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM), + }, + "inferior_cap": { + "label_id": option_int(generator_argv, "--bottom_node_set_id", 3), + "node_set": "body_bottom", + "shape": "fit to vertebral body surface", + "pmma_thickness_mm": option_float(generator_argv, "--pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM), + }, + }, + "constraints": [ + { + "name": "top_displacement", + "node_set": "body_top", + "axis": "z", + **spine_displacement, + "meaning": "superior PMMA cap prescribed toward inferior cap", + }, + { + "name": "bottom_fixed", + "node_set": "body_bottom", + "axis": "z", + "value_mm": 0.0, + "meaning": "inferior PMMA cap fixed in compression direction", + }, + ], + }, + }) + else: + femur_side = option_value(generator_argv, "--femur_side", "1") + side = "left" if femur_side == "1" else "right" if femur_side == "2" else "unknown" + compartment_mask = option_path(generator_argv, "--compartment_mask") + femur_displacement = percent_displacement_metadata( + model_path, + report_profile="femur", + failure_axis="y", + target_percent=common["solve_and_reporting"]["target_displacement_percent"], + ) + metadata = common.copy() + metadata.update({ + "model": "femur-sideways", + "target": { + "side": side, + "femur_side_code": int(femur_side) if femur_side.isdigit() else femur_side, + "mask_threshold": option_int(generator_argv, "--mask_threshold", 1), + }, + "alignment": { + "method": "pre-rotation plus ICP to side-specific femur reference", + "reference_path": option_value(generator_argv, "--reference_path", "bundled side-specific femur reference"), + }, + "image_processing": { + "iso_resolution_mm": option_float(generator_argv, "--iso_resolution", DEFAULT_FEMUR_ISO_RESOLUTION_MM), + "spatial_operations": ( + "side pre-rotation reslice, then ICP transform and isotropic output spacing in one shared VTK reslice" + ), + "image_interpolation": "cubic", + "label_interpolation": "nearest-neighbor", + "mask_smoothing": { + "operation": "binary close/open after ICP resampling", + "condition": "enabled only when any input spacing dimension exceeds threshold_mm", + "threshold_mm": option_float( + generator_argv, + "--mask_smoothing_spacing_threshold", + DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + ), + "applies_to": [ + "whole-femur mask", + "derived cortical binary mask when compartment_mask is supplied", + ], + }, + "density_preprocessing": { + "connectivity_filter": True, + "bmd_preprocess_threshold": -31, + "density_binning": { + "n_bins": 128, + "background_material_id": 0, + "trabecular_material_ids": [1, 128], + "cortical_material_ids": [129, 256] if compartment_mask is not None else None, + }, + }, + }, + "segmentation": { + "input_mask": str(args.bone_mask), + "whole_bone_mask_threshold": option_int(generator_argv, "--mask_threshold", 1), + "compartment_mask": compartment_mask, + "compartment_labels": None + if compartment_mask is None + else { + "cortical": option_int(generator_argv, "--cortical_label", 1), + "trabecular": option_int(generator_argv, "--trabecular_label", 2), + }, + }, + "shaft_standardization": { + "cut_mode": option_value(generator_argv, "--femur_cut_mode", DEFAULT_FEMUR_CUT_MODE), + "fixed_length_mm": option_float(generator_argv, "--femur_shaft_length", 100.0), + "lesser_trochanter_distal_offset_mm": option_float( + generator_argv, + "--femur_lesser_trochanter_distal_offset", + DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, + ), + "cut_plane": "flat model-grid z plane", + "incomplete_fov_behavior": "fail model generation", + }, + "materials": { + "builder": "ogo.fea.materials.build_femur_material_table", + "convention": "region material IDs; 0 background, 1..128 trabecular, optional 129..256 cortical, PMMA explicit", + "include_cortical_region": compartment_mask is not None, + "poissons_ratio": option_float(generator_argv, "--poissons_ratio", 0.3), + "trabecular": { + "elastic_E_func": option_value(generator_argv, "--elastic_E_func", "default_E"), + "yield_comp_func": option_value(generator_argv, "--yield_comp_func"), + "yield_tens_func": option_value(generator_argv, "--yield_tens_func"), + "material_id_range": [1, 128], + }, + "cortical": None + if compartment_mask is None + else { + "elastic_E_func": option_value( + generator_argv, + "--cort_elastic_E_func", + option_value(generator_argv, "--elastic_E_func", "default_E"), + ), + "yield_comp_func": option_value( + generator_argv, + "--cort_yield_comp_func", + option_value(generator_argv, "--yield_comp_func"), + ), + "yield_tens_func": option_value( + generator_argv, + "--cort_yield_tens_func", + option_value(generator_argv, "--yield_tens_func"), + ), + "poissons_ratio": option_float( + generator_argv, + "--cort_poissons_ratio", + option_float(generator_argv, "--poissons_ratio", 0.3), + ), + "material_id_range": [129, 256], + }, + "pmma": { + "material_id": option_int(generator_argv, "--pmma_mat_id", 5000), + "elastic_E_MPa": option_float(generator_argv, "--pmma_E", 2500), + "poissons_ratio": option_float(generator_argv, "--pmma_v", 0.3), + "yield_compression_MPa": option_optional_float(generator_argv, "--pmma_yield_compression"), + "yield_tension_MPa": option_optional_float(generator_argv, "--pmma_yield_tension"), + }, + }, + "boundary_conditions": { + "fixture_geometry": { + "femoral_head": { + "node_set": "Femoral_Head_PMMA_Nodes", + "shape": "round fixture cap", + "pmma_thickness_mm": option_float(generator_argv, "--pmma_thick", DEFAULT_PMMA_THICKNESS_MM), + "pmma_intrusion_mm": option_float(generator_argv, "--pmma_intrusion", DEFAULT_PMMA_INTRUSION_MM), + "width_extension_mm": 10.0, + "long_axis_extension_mm": 80.0, + }, + "greater_trochanter": { + "node_set": "Greater_Trochanter_PMMA_Nodes", + "shape": "box fixture cap", + "pmma_thickness_mm": option_float(generator_argv, "--pmma_thick", DEFAULT_PMMA_THICKNESS_MM), + "pmma_intrusion_mm": option_float(generator_argv, "--pmma_intrusion", DEFAULT_PMMA_INTRUSION_MM), + }, + "distal_shaft": { + "node_set": "Distal_Femur_Nodes", + "support_surface": "flat distal shaft cut face", + }, + }, + "constraints": [ + { + "name": "top_displacement", + "node_set": "Femoral_Head_PMMA_Nodes", + "axis": "y", + **femur_displacement, + "meaning": "femoral head PMMA cap prescribed toward greater trochanter", + }, + { + "name": "bottom_fixed_y_PMMA", + "node_set": "Greater_Trochanter_PMMA_Nodes", + "axis": "y", + "value_mm": 0.0, + "meaning": "greater trochanter PMMA cap constrained in loading direction", + }, + { + "name": "bottom_fixed_x", + "node_set": "Distal_Femur_Nodes", + "axis": "x", + "value_mm": 0.0, + "meaning": "distal shaft rigid-body constraint", + }, + { + "name": "bottom_fixed_z", + "node_set": "Distal_Femur_Nodes", + "axis": "z", + "value_mm": 0.0, + "meaning": "distal shaft rigid-body constraint", + }, + ], + }, + }) + + output_path.write_text(json.dumps(metadata, indent=2, sort_keys=True) + "\n") + print(f"Wrote {output_path}") + return output_path + + +def audit_generated_model(model_path: Path, model_type: str, args: argparse.Namespace) -> Optional[dict]: + """Return BC audit summary and optionally write a debug PNG.""" + if args.skip_bc_audit or args.dry_run: + return None + if not model_path.exists(): + return None + + from ogo.cli.CheckFEModelBC import audit_model + + audit_kind = "spine-compression" if model_type == "spine" else "femur-sideways" + result = audit_model( + model_path, + model=audit_kind, + flat_tolerance=args.bc_audit_flat_tolerance, + write_json=False, + write_csv_file=False, + write_plot=args.debug, + ) + if result["png_path"] is not None: + print(f"Wrote {result['png_path']}") + if not result["passed"]: + failed = [check["name"] for check in result["checks"] if not check["passed"]] + raise RuntimeError("BC audit failed for {}: {}".format(model_path, "; ".join(failed))) + return result["summary"] + + def _call_cli(main_func: Callable[[], None], program: str, argv: Sequence[str]) -> None: old_argv = sys.argv try: sys.argv = [program, *argv] - main_func() + try: + main_func() + except SystemExit as exc: + if exc.code not in (None, 0): + raise finally: sys.argv = old_argv def run_spine_command(argv: Sequence[str]) -> None: - from ogo.cli.ref.SpineCompressionFe import main as spine_compression_main + from ogo.fea.spine_compression import main as spine_compression_main _call_cli(spine_compression_main, "OgoSpineCompressionFe", argv) def run_femur_command(argv: Sequence[str]) -> None: - from ogo.cli.ref.SidewaysFallFe import main as sideways_fall_main + from ogo.fea.sideways_fall import main as sideways_fall_main _call_cli(sideways_fall_main, "OgoSidewaysFallFe", argv) @@ -292,8 +806,11 @@ def _add_common_image_args(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--target_displacement", type=float, - default=0.2, - help="Standard displacement threshold used for the rescaled load endpoint.", + default=None, + help=( + "Profile reporting endpoint as percent strain. Defaults to 2.0 for spine " + "and 4.0 for femur; converted to mm from the generated model geometry." + ), ) parser.add_argument( "--require_pistoia", @@ -305,11 +822,27 @@ def _add_common_image_args(parser: argparse.ArgumentParser) -> None: action="store_true", help="Skip n88copymodel --compress after solving.", ) + parser.add_argument( + "--debug", + action="store_true", + help="Write debug sidecars such as BC audit PNGs and legacy spine QC quick looks.", + ) + parser.add_argument( + "--skip_bc_audit", + action="store_true", + help="Skip automatic boundary-condition audit files after model generation.", + ) + parser.add_argument( + "--bc_audit_flat_tolerance", + type=float, + default=1.0e-4, + help="Flat-plane tolerance for automatic boundary-condition audit checks.", + ) def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - prog="ogoGenerateFEM", + prog="ogoFEA", description=( "Generate N88 finite-element models for spine vertebrae and hip femurs. " "Unknown options are forwarded to the selected lower-level FE generator." @@ -361,22 +894,8 @@ def build_parser() -> argparse.ArgumentParser: return parser -def _run_legacy(argv: Sequence[str]) -> None: - """Keep the historical ``--model_type`` dispatch working.""" - parser = argparse.ArgumentParser(prog="ogoGenerateFEM") - parser.add_argument("--model_type", choices=["vertebra", "femur"], required=True) - args, remaining = parser.parse_known_args(argv) - if args.model_type == "vertebra": - run_spine_command(remaining) - else: - run_femur_command(remaining) - - def main(argv: Optional[Sequence[str]] = None) -> None: argv = list(sys.argv[1:] if argv is None else argv) - if "--model_type" in argv: - _run_legacy(argv) - return parser = build_parser() args, extra_args = parser.parse_known_args(argv) @@ -385,7 +904,10 @@ def main(argv: Optional[Sequence[str]] = None) -> None: parser.error("model type is required: choose spine or hip") if args.model_type == "spine": - spine_extra_args = spine_preset_args(args.preset) + list(extra_args) + spine_extra_args = spine_preset_args(args.preset) + list(extra_args) + [ + "--quality_control", + str(bool(args.debug)), + ] for target in args.vertebra: cmd = build_spine_command( calibrated_image=args.calibrated_image, @@ -398,6 +920,13 @@ def main(argv: Optional[Sequence[str]] = None) -> None: print_dry_run("OgoSpineCompressionFe", cmd) else: run_spine_command(cmd) + model_path = expected_spine_model_path(args.calibrated_image, args.output_path, target) + bc_audit_summary = audit_generated_model( + model_path, + "spine", + args, + ) + write_modeling_metadata(model_path, "spine", cmd, args, bc_audit_summary) if not args.no_solve: solve_model( expected_spine_model_path(args.calibrated_image, args.output_path, target), @@ -420,6 +949,13 @@ def main(argv: Optional[Sequence[str]] = None) -> None: print_dry_run("OgoSidewaysFallFe", cmd) else: run_femur_command(cmd) + model_path = expected_femur_model_path(args.calibrated_image, args.output_path, side) + bc_audit_summary = audit_generated_model( + model_path, + "hip", + args, + ) + write_modeling_metadata(model_path, "hip", cmd, args, bc_audit_summary) if not args.no_solve: solve_model( expected_femur_model_path(args.calibrated_image, args.output_path, side), diff --git a/ogo/cli/ref/FePistoiaTabulate.py b/ogo/cli/ref/FePistoiaTabulate.py deleted file mode 100644 index 67630db..0000000 --- a/ogo/cli/ref/FePistoiaTabulate.py +++ /dev/null @@ -1,179 +0,0 @@ -##### -# ogo_fe_pistoia_tabulate.py -# -# This script collects all the FE Pistoia results from the specified directory. -# -##### -# -# Andrew Michalski -# University of Calgary -# Biomedical Engineering Graduate Program -# September 6, 2019 -# Modified to Py3: March 25, 2020 -##### - -script_version = 1.0 - -## -# Import the required modules -import os -import sys -import glob -import argparse -import time -import pandas as pd -from datetime import date -from collections import OrderedDict - - -from ogo.util.echo_arguments import echo_arguments - -## -def fePistoiaTab(args): - - parser = argparse.ArgumentParser( - description="""This script collects all the FE Pistoia results from the specified directory. INPUT: Data_Directory; OUTPUT: TXT file of results""") - - parser.add_argument("analysis_directory", - help = "Path/To/Data/Directory") - - parser.add_argument("--model", type = int, - default = 1, - help = "Set the bone value to analyze. 1 = RT_FEMUR_SF;\n2 = RT_FEMUR_SLS;\n3 = LT_FEMUR_SF;\n4 = LT_FEMUR_SLS;\n6 = L4_SPINE_VC.\n(Default: %(default)s)") - - ## - # Collect the input arguments - args = parser.parse_args() - directory = args.analysis_directory - model = args.model - - ## - # Read the files in the Directory - files = [] - - if model == 1: - model_filename ='*_RT_FEMUR_SF_PISTOIA.txt' - model_filename2 ='_RT_FEMUR_SF_PISTOIA.txt' - fileName = "RT_FEMUR_SF_PISTOIA_Results.txt" - model_type = 'RT FEMUR SF Failure Load (N)' - elif model == 2: - model_filename = '*_RT_FEMUR_SLS_PISTOIA.txt' - model_filename2 = '_RT_FEMUR_SLS_PISTOIA.txt' - fileName = "RT_FEMUR_SLS_PISTOIA_Results.txt" - model_type = 'RT FEMUR SLS Failure Load (N)' - elif model == 3: - model_filename ='*_LT_FEMUR_SF_PISTOIA.txt' - model_filename2 ='_LT_FEMUR_SF_PISTOIA.txt' - fileName = "LT_FEMUR_SF_PISTOIA_Results.txt" - model_type = 'LT FEMUR SF Failure Load (N)' - elif model == 4: - model_filename = '*_LT_FEMUR_SLS_PISTOIA.txt' - model_filename2 = '_LT_FEMUR_SLS_PISTOIA.txt' - fileName = "LT_FEMUR_SLS_PISTOIA_Results.txt" - model_type = 'LT FEMUR SLS Failure Load (N)' - elif model == 6: - model_filename = '*_L4_FE_PISTOIA.txt' - model_filename2 = '_L4_FE_PISTOIA.txt' - fileName = "L4_SPINE_FE_PISTOIA_Results.txt" - model_type = 'L4 SPINE Failure Load (N)' - else: - print("Model value set is not defined. Ending script.") - sys.exit() - - ## - # Create dataframe for output - df = pd.DataFrame(columns = ['ID', model_type]) - - ## - # Read and extract the data - os.chdir(directory) - files = sorted(glob.glob(model_filename)) - k = 0 - for i in files: - ID = i.replace(model_filename2, "") - - # Check to see if the file is empty and add to talbe if needed - if os.stat(i).st_size == 0: - x_fl = "" - y_fl = "" - z_fl = "" - if model == 1: - df.loc[k] = [ID, y_fl] - if model == 2: - df.loc[k] = [ID, z_fl] - if model == 3: - df.loc[k] = [ID, y_fl] - if model == 4: - df.loc[k] = [ID, z_fl] - if model == 6: - df.loc[k] = [ID, z_fl] - k = k + 1 - continue - - # open each txt file and extract the failure loads - lines = [line.rstrip('\n') for line in open(i)] - lines = lines[11].replace(" ", "") - lines = lines.replace("Failureload(RF*factor):", "") - index_1 = lines.find("E") - index_2 = lines.find("E", index_1 + 1) - index_3 = lines.find("E", index_2 + 1) - indices = [index_1, index_2, index_3] - x_fl = lines[0:index_1+4] - y_fl = lines[index_1+4:index_2+4] - z_fl = lines[index_2+4:] - - - if model == 1: - df.loc[k] = [ID, y_fl] - if model == 2: - df.loc[k] = [ID, z_fl] - if model == 3: - df.loc[k] = [ID, y_fl] - if model == 4: - df.loc[k] = [ID, z_fl] - if model == 6: - df.loc[k] = [ID, z_fl] - k = k + 1 - - ## - # Write Output TXT file of dataframe - df.to_csv(fileName, sep = '\t', index = False, header = True) - - print("Script Complete.") - - -def main(): - description = ''' - This script collects all the FE Pistoia results from the specified directory. - - INPUT: Data_Directory; - OUTPUT: TXT file of results - - ''' - - - # Setup argument parsing - parser = argparse.ArgumentParser( - formatter_class=argparse.RawTextHelpFormatter, - prog="ogoFePistoiaTabulate", - description=description - ) - - parser.add_argument("analysis_directory", - help = "Path/To/Data/Directory") - - parser.add_argument("--model", type = int, - default = 1, - help = "Set the bone value to analyze. 1 = RT_FEMUR_SF;\n2 = RT_FEMUR_SLS;\n3 = LT_FEMUR_SF;\n4 = LT_FEMUR_SLS;\n6 = L4_SPINE_VC.\n(Default: %(default)s)") - - - # Parse and display - args = parser.parse_args() - - print(echo_arguments('fe_Pistoia_Tabulate', vars(args))) - - # Run program - fePistoiaTab(args) - -if __name__ == '__main__': - main() \ No newline at end of file diff --git a/ogo/cli/ref/Pistoia.py b/ogo/cli/ref/Pistoia.py deleted file mode 100644 index 5d33bf2..0000000 --- a/ogo/cli/ref/Pistoia.py +++ /dev/null @@ -1,487 +0,0 @@ -""" -pistoia.py - -Generate tables of standard post-processing quantities. - -Copyright (c) 2010-2016, Numerics88 Solutions Ltd. -http://www.numerics88.com/ -See LICENSE for details. -""" - - -import sys -#from N88ReportedError import N88ReportedError -from n88tools.N88ReportedError import N88ReportedError -import numpy -from numpy.core import * - - -def pistoia(): - - import os - import argparse - from collections import OrderedDict - import warnings - import numbers - from netCDF4 import Dataset - - global table_count - - # ------------------------------------------------------------------------ - # Parse options - - parser = argparse.ArgumentParser ( - description=""" - Calculate Pistoia yield critera. - - This is a method if estimating yield strength from linear solutions. - In general, it would be preferable to use a non-linear elastoplastic - model to calculate yield strengths. This utility is provided mostly - for comparing with older results. - """, - ) - - parser.add_argument ("--output_file", "-o", - help="Output file. If not specified, output will go to STDOUT.") - - parser.add_argument ("--constraint", "-n", default="top_displacement", - help="""Specify the constraint (i.e. boundary condition or applied load) - to use for analysis. This is the surface to which forces are applied. The default - ("top_displacement") will work for models generated with n88modelgenerator.""") - - parser.add_argument ("--rotation_center", "-c", - type = lambda x: numpy.fromstring(x, dtype=float, sep=","), - help="""Specify the spatial center used for calculation of angular - quantities. The argument must be given as a triplet of coordinates. - The default is read from the n88model. If not specified, either on the - command line or in the n88model file, no angular quantities will be - calculated.""") - - parser.add_argument ("--crit_vol", "-cv", type = float, default = 7, - help="Define the critical volume for the computed failure load.") - - parser.add_argument ("--crit_ees", "-cs", type = float, default = 0.011, - help="Define the critical strain for the computed failure load.") - - parser_sets = parser.add_mutually_exclusive_group() - - parser_sets.add_argument ("--include", "-i", - help="Only elements with the specified material IDs will be included in the calculation. Multiple IDs can be specified in a comma-delimited list (e.g. 100,101,105).") - - parser_sets.add_argument ("--exclude", "-e", - help="All elements with the specified material IDs will be excluded from the calculation. Multiple IDs can be specified in a comma-delimited list (e.g. 100,101,105).") - - parser.add_argument ("model_file") - - args = parser.parse_args() - crit_vol = args.crit_vol - crit_ees = args.crit_ees - - if args.rotation_center != None: - if len(args.rotation_center) != 3: - raise N88ReportedError ("ERROR: rotation_center must be a triplet of values.") - - # For now this is hard-wired. - args.twist_threshold = 1E-6 - - if args.output_file == None: - out = sys.stdout - else: - out = open (args.output_file, "wt") - - # ------------------------------------------------------------------------ - # Get information about the n88model file - - # Get the netCDF group handles from the n88model file. - - root = Dataset (args.model_file, "r") - activeSolution = root.groups["Solutions"].groups[root.ActiveSolution] - activeProblem = root.groups["Problems"].groups[activeSolution.Problem] - activePart = root.groups["Parts"].groups[activeProblem.Part] - hexahedrons = activePart.groups["Elements"].groups["Hexahedrons"] - nodeValues = activeSolution.groups["NodeValues"] - elementValues = activeSolution.groups["ElementValues"] - materialTable = activePart.groups["MaterialTable"] - materialDefinitions = root.groups["MaterialDefinitions"] - nodeSetsGroup = root.groups["Sets"].groups["NodeSets"] - elementSetsGroup = root.groups["Sets"].groups["ElementSets"] - - # Determine some constants to do with array sizes - - num = zeros (8,int) - num[:] = hexahedrons.variables["NodeNumbers"][0] - 1 - coord_var = activePart.variables["NodeCoordinates"] - spacing = array ([coord_var[num[1],0] - coord_var[num[0],0], - coord_var[num[2],1] - coord_var[num[0],1], - coord_var[num[4],2] - coord_var[num[0],2]]) - numberOfNodes = coord_var.shape[0] - dimensionality = coord_var.shape[1] - del num, coord_var - numberOfElements = hexahedrons.variables["NodeNumbers"].shape[0] - numberOfNodesPerElement = hexahedrons.variables["NodeNumbers"].shape[1] - sizeOfMaterialTable = materialTable.variables["ID"].shape[0] - nst = 6 - - if args.rotation_center == None: - try: - args.rotation_center = activeProblem.RotationCenter - except: - args.rotation_center = None - - # Array data that it is convenient to read now and keep around. - # Note that most array data will be read as needed and subsequently - # discarded for memory efficiency. - - materialIds = materialTable.variables["ID"][:] - elementMaterials = hexahedrons.variables["MaterialID"][:] - - constraints = root.groups["Constraints"] - try: - selected_constraint = constraints.groups[args.constraint] - except: - raise N88ReportedError ('ERROR: Specified constraint "%s" not found.' % args.constraint) - set_indices = selected_constraint.variables["NodeNumber"][:] - set_indices -= 1 # Convert to 0-indexed. - - - # ------------------------------------------------------------------------ - # Some constants used for formatting - - width = 76 - table_delimiter = "="*width + "\n" - section_delimiter = "-"*width + "\n" - subtable_delimiter = "."*width + "\n" - any_entry = "%%-32s%%%d" % (width-32) - text_entry = any_entry + "s\n" - integer_entry = any_entry + "d\n" - - - # ------------------------------------------------------------------------ - # Output functions - - - def CalculateStats (data, stats): - if "median" in stats: - # If sorting we make a copy so we don't modify the original data - if isinstance(data, numpy.ma.MaskedArray): - x = sort(data.compressed()) - else: - x = sort(data) - else: - if isinstance(data, numpy.ma.MaskedArray): - # If a MaskedArray, get a copy of the compressed array - may be faster - x = data.compressed() - else: - x = data - nan = float('nan') - n = len(x) - if n == 0: - total = 0 - average = nan - rms = nan - std_dev = nan - minimum = nan - maximum = nan - skewness = nan - kurtosis = nan - median = nan - perc05 = nan - perc25 = nan - perc75 = nan - perc95 = nan - else: - total = sum(x) - average = total/n - if "std_dev" in stats: - std_dev = sqrt(sum((x-average)**2)/n) - if "rms" in stats: - rms = sqrt(sum(x**2)/n) - if "minimum" in stats: - minimum = min(x) - if "maximum" in stats: - maximum = max(x) - # Might get warning if stddev is zero: ignore these - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - if "skewness" in stats: - skewness = sum(((x-average)/std_dev)**3)/n - if "kurtosis" in stats: - kurtosis = sum(((x-average)/std_dev)**4)/n - 3 - median = x[n//2] - perc05 = x[n//20] - perc25 = x[n//4] - perc75 = x[(3*n)//4] - perc95 = x[(19*n)//20] - values = OrderedDict() - for v in stats: - values[v] = locals()[v] - return values - - - def BeginTable (name): - global table_count - table_count += 1 - out.write ( - "\n" + table_delimiter - + "Table %d: %s\n" % (table_count, name) - + section_delimiter) - - - def StatsTable (data, stats, labels=None): - if len(data.shape) == 1: - num_col = 1 - else: - num_col = data.shape[1] - left_width = width - 11*6 - if labels: - out.write (" "*left_width + ("%11s"*num_col) % tuple(labels) + "\n") - int_row_format = "%%-%ds" % left_width + "%11d"*num_col + "\n" - float_row_format = "%%-%ds" % left_width + "%11.3E"*num_col + "\n" - if num_col == 1: - values = CalculateStats(data, stats) - for stat_name, stat_value in values.items(): - if isinstance(stat_value, numbers.Integral): - row_format = int_row_format - else: - row_format = float_row_format - out.write (row_format % (stat_name, stat_value)) - else: - values = [] - for i in range(num_col): - values.append (CalculateStats(data[:,i], stats)) - for v in list(values[0].keys()): - row_values = [] - for i in range(num_col): - row_values.append (values[i][v]) - if isinstance(row_values[0], numbers.Integral): - row_format = int_row_format - else: - row_format = float_row_format - out.write (row_format % (tuple([v] + row_values))) - return values - - - # ------------------------------------------------------------------------ - # Generate a mask - # Note that the mask is True (or value 1) for elements that are to be excluded. - - if args.exclude != None: - ids = numpy.unique(numpy.fromstring(args.exclude, sep=",", dtype=int)) - mask = zeros((numberOfElements,), bool) - for i in ids: - mask += (elementMaterials == i) - elif args.include != None: - ids = numpy.unique(numpy.fromstring(args.include, sep=",", dtype=int)) - mask = ones((numberOfElements,), bool) - for i in ids: - mask *= (elementMaterials != i) - else: - mask = None - - - # ------------------------------------------------------------------------ - # Calculations start here - - # Get average displacement of specified node set. - displacements = zeros (nodeValues.variables["Displacement"].shape, float64) - displacements[:] = nodeValues.variables["Displacement"][:] - set_data = displacements[set_indices] - u_ns1 = numpy.mean(set_data, axis=0) - - # Get total force on specified node set. - forces = zeros (nodeValues.variables["ReactionForce"].shape, float64) - forces[:] = nodeValues.variables["ReactionForce"][:] - set_data = forces[set_indices] - rf_ns1 = numpy.sum(set_data, axis=0) - - if args.rotation_center != None: - - # Calculate average angular rotation on specified node set. - p = zeros (activePart.variables["NodeCoordinates"].shape, float64) - p[:] = activePart.variables["NodeCoordinates"][:] - p -= args.rotation_center - p_dp = p + displacements - twist = numpy.arctan2 (numpy.roll(p_dp,1,axis=1), numpy.roll(p_dp,2,axis=1)) \ - - numpy.arctan2 (numpy.roll(p,1,axis=1), numpy.roll(p,2,axis=1)) - # Correct for going over branch cut - twist += 2*pi * (twist < pi) - twist -= 2*pi * (twist > pi) - # Mask out any points (initial or final) that are too close to the rotation center - # in the rotation plane. - mask = (numpy.roll(p_dp,1,axis=1)**2 + numpy.roll(p_dp,2,axis=1)**2 - > args.twist_threshold**2)[:, numpy.newaxis] - mask *= (numpy.roll(p,1,axis=1)**2 + numpy.roll(p,2,axis=1)**2 - > args.twist_threshold**2)[:, numpy.newaxis] - twist_masked = numpy.ma.MaskedArray (twist, numpy.logical_not(mask)) - set_data = twist_masked[set_indices] - rot_ns1 = set_data.mean(axis=0) - - # Calculate average torque on speficifed node set. - rf = zeros (nodeValues.variables["ReactionForce"].shape, float64) - rf[:] = nodeValues.variables["ReactionForce"][:] - torque = numpy.roll(rf,1,axis=1) * numpy.roll(p,2,axis=1) \ - - numpy.roll(rf,2,axis=1) * numpy.roll(p,1,axis=1) - set_data = torque[set_indices] - torque_ns1 = numpy.sum(set_data, axis=0) - - f = rf_ns1 - u = u_ns1 - if args.rotation_center != None: - t = torque_ns1 - rot = rot_ns1 - - fixed_critical_volume = crit_vol - fixed_critical_ees = crit_ees - - # Get typical Modulus - modulus = zeros(numberOfElements,float) - matNames = materialTable.variables['MaterialName'][:] - for id,matName in zip (materialIds, matNames): - material = materialDefinitions.groups[matName] - if hasattr(material, 'E'): - if material.Type == "LinearIsotropic": - modulus_value = material.E - elif material.Type == "LinearOrthotropic": - modulus_value = max(material.E) - else: - raise N88ReportedError ("ERROR: Unsupported material type for Pistoia calculation: %s" % material.Type) - indices = numpy.nonzero(elementMaterials == id) - modulus[indices] = modulus_value - else: - if material.Type != "LinearIsotropic": - raise N88ReportedError ("ERROR: Unsupported material type for Pistoia calculation: %s" % material.Type) - - modulus_values = numpy.array(material.variables['E'][:]) - for i, E in enumerate(modulus_values): - indices = numpy.nonzero(elementMaterials == i) - modulus[indices] = E - sed = zeros (elementValues.variables["StrainEnergyDensity"].shape, float64) - sed[:] = elementValues.variables["StrainEnergyDensity"][:] - if mask is not None: - sed = numpy.ma.MaskedArray (sed, mask) - modulus = numpy.ma.MaskedArray (modulus, mask) - ees = sqrt(2*sed/modulus) - del sed - if mask is None: - numberOfSelectedElements = numberOfElements - else: - numberOfSelectedElements = ees.count() - if numberOfSelectedElements == 0: - raise N88ReportedError ("ERROR: No elements in selection.") - if mask is None: - sort_indices = argsort (ees) - else: - sort_indices = ees.argsort(fill_value=inf)[:numberOfSelectedElements] - ees = ees[sort_indices] - el = int(numberOfSelectedElements*(100-fixed_critical_volume)/100) - ees_at_crit_vol = ees[el] - fixed_factor = fixed_critical_ees / ees_at_crit_vol - failure_load = f * fixed_factor - if args.rotation_center != None: - torsional_failure_load = t * fixed_factor - # Note that the use of numpy.float64 is to ensure IEEE math - # (i.e. result=NAN on divide by zero instead of exception) - # Might get warning if stddev is zero: ignore these - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - stiffness = f/u - if args.rotation_center != None: - torsional_stiffness = t/rot - - table_count = 0 - BeginTable ("Pistoia Failure Load Estimate") - out.write ("""Pistoia Criterion for Energy-Equivalent-Strain (EES) - (*) Results valid for linear, isotropic model only. - """) - if args.rotation_center != None: - out.write (" (*) Warning: Torsion failure load not validated. Caution!\n") - - out.write (subtable_delimiter) - float_entry = any_entry + ".3f\n" - out.write (float_entry % ("Critical volume (%):", fixed_critical_volume)) - float_entry = "%%-40s%%%d" % (width-40) + ".4E\n" - out.write (float_entry % ("Critical EES:", fixed_critical_ees)) - out.write (float_entry % ("EES at vol_crit:", ees_at_crit_vol)) - col_width = 12 - triple_entry = "%%-%ds" % (width-3*col_width) + ("%%%d.4E" % col_width)*3 + "\n" - triple_entry_text = "%%-%ds" % (width-3*col_width) + ("%%%ds" % col_width)*3 + "\n" - out.write (float_entry % ("Factor (from table):", fixed_factor)) - out.write (triple_entry % (("Failure load (RF * factor):",) + tuple(failure_load))) - if args.rotation_center != None: - out.write (triple_entry % (("Torsional failure load (T * factor):",) + tuple(torsional_failure_load))) - out.write (section_delimiter) - out.write ("Stiffness:\n") - out.write (subtable_delimiter) - out.write (triple_entry_text % ("", "x", "y", "z")) - out.write (triple_entry % (("RF (node set 1):",) + tuple(f))) - out.write (triple_entry % (("U (node set 1):",) + tuple(u))) - out.write (triple_entry % (("Axial stiffness:",) + tuple(stiffness))) - if args.rotation_center != None: - out.write (triple_entry % (("T (node set 1):",) + tuple(t))) - out.write (triple_entry % (("Rot (node set 1) [rad]:",) + tuple(rot))) - out.write (triple_entry % (("Torsional stiffness:",) + tuple(torsional_stiffness))) - - out.write (section_delimiter) - out.write ("Distribution of energy-equivalent-strain: EES = sqrt(2U/E).\n") - out.write (subtable_delimiter) - stats = ["average","std_dev","minimum","maximum","skewness","kurtosis","median"] - StatsTable (ees, stats) - - out.write (section_delimiter) - out.write ("Distribution of failed materials.\n") - out.write (subtable_delimiter) - g_mat_failed = elementMaterials[sort_indices[el:]] - unique_materials = numpy.unique(elementMaterials) - nMaterials = len(unique_materials) - nels_failed = zeros (nMaterials, int) - for i,m in enumerate(unique_materials): - nels_failed[i] = sum(g_mat_failed == m) - total = sum(nels_failed) - #g_mat_failed = elementMaterials[sort_indices[el:]] - #nels_failed = zeros (len(numpy.unique(elementMaterials)), int) - #for i,m in enumerate(numpy.unique(elementMaterials)): - # nels_failed[i] = sum (g_mat_failed == m) - #total = sum(nels_failed) - col_width = 12 - left_margin = (width - 3*col_width)//2 - heading_format = " "*left_margin + ("%%%ds" % col_width)*3 + "\n" - table_line = "-"*10 - row_format = " "*left_margin + "%%%ds" % col_width + "%%%dd" % col_width + "%%%d.2f" % col_width + "\n" - out.write (heading_format % ("material","# els","%")) - for i,m in enumerate(unique_materials): - out.write (row_format % (m, nels_failed[i], numpy.float64(100)*nels_failed[i]/total)) - out.write (heading_format % ((table_line,)*3)) - out.write (row_format % ("total",total,100)) - - out.write (section_delimiter) - out.write ("Factor table:\n") - crit_vol = arange(1,8,dtype=float) - # crit_ees = array((0.005, 0.006, 0.007, 0.008, 0.009)) - crit_ees = array((0.006, 0.007, 0.008, 0.009, 0.010, 0.011, 0.012, 0.013, 0.014, 0.015, 0.016, 0.017, 0.018, 0.019, 0.020)) - el = ((numberOfSelectedElements*(100-crit_vol)/100)).astype(int) - factors = crit_ees[numpy.newaxis,:] / ees[el][:,numpy.newaxis] - col_width = (width - 17)//5 - row_format = "%3d %10d |" + (" %%%d.3E" % col_width)*15 + "\n" - heading_format = "%3s %10s |" + (" %%%ds" % col_width)*5 + "\n" - out.write (heading_format % (("", "crit_vol", "crit_ees") + ("",)*4)) - heading_format = "%3s %10s |" + (" %%%ds" % col_width)*15 + "\n" - out.write (heading_format % (("%","# vol") + tuple(crit_ees))) - out.write (heading_format % (("--","-"*8) + ("-"*(col_width-2),)*15)) - for i in range(7): - out.write (row_format % ((crit_vol[i], int(crit_vol[i]*numberOfSelectedElements)) - + tuple(factors[i]))) - - out.write (table_delimiter) - - -def main(): - try: - pistoia() - except N88ReportedError as e: - sys.stderr.write ("Error: " + e.message) - sys.stderr.write ("\n") - sys.exit (e.value) - # Let other exceptions fall through to default python unhandled exception reporting. - -if __name__ == "__main__": - main() diff --git a/ogo/fea/README.md b/ogo/fea/README.md new file mode 100644 index 0000000..9b03bcc --- /dev/null +++ b/ogo/fea/README.md @@ -0,0 +1,406 @@ +# Ogo FEA Workflow + +This directory contains the maintained model-building code used by +`ogoFEA`. The user-facing entry point is the CLI in +`ogo/cli/GenerateFEM.py`; the lower-level spine and femur modules in this +directory should stay implementation details. + +## Inputs + +All FEA workflows expect a density-calibrated CT image and an aligned +segmentation image. + +The calibrated image should be a scalar image in K2HPO4-equivalent density +units, with the same image space as the segmentation. During model generation, +the image is cropped, aligned to the model reference frame, resampled to +isotropic spacing, thresholded at -31 mg/cc, and converted to material IDs. + +The segmentation depends on the model type: + +| Model | Required mask | Label convention | +| --- | --- | --- | +| Spine compression | Labelled vertebra mask | Provide the vertebral body label and posterior process label for each target as `LEVEL:BODY_LABEL:PROCESS_LABEL`. | +| Hip sideways fall | Whole-femur mask | Nonzero voxels are the femur by default. The wrapper chooses left/right from `--side`. | +| Hip with trab/cort compartments | Whole-femur mask plus `--compartment_mask` | Compartment mask defaults are cortical `1`, trabecular `2`; override with `--cortical_label` and `--trabecular_label` if needed. | + +The spine mask must contain both the body and posterior process labels. The +femur workflow currently requires a side-specific run because alignment uses +left and right reference models. + +## Basic Commands + +Generate and solve one L1 model: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --output_path derivatives/fea +``` + +Generate L1 and L2 in one command: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --vertebra L2:4:5 \ + --output_path derivatives/fea +``` + +Generate only the model files, without running FAIM: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --output_path derivatives/fea \ + --no-solve +``` + +Generate the left femur: + +```bash +ogoFEA hip \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-leftFemur_mask.nii.gz \ + --side left \ + --output_path derivatives/fea +``` + +Generate both femurs from a mask containing both sides: + +```bash +ogoFEA hip \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-femur_mask.nii.gz \ + --side both \ + --output_path derivatives/fea +``` + +Generate a femur model with an explicit trabecular/cortical compartment mask: + +```bash +ogoFEA hip \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-leftFemur_mask.nii.gz \ + --side left \ + --compartment_mask sub-001_desc-leftFemur_compartments.nii.gz \ + --output_path derivatives/fea +``` + +Use `--dry-run` to print the lower-level command without generating files. + +## Solver Settings + +By default, `ogoFEA` generates each `.n88model`, audits the boundary +conditions, writes a `_modeling.json` record, then runs the FAIM/N88 solve and +postprocessing. + +Control the FAIM thread count with `--threads`: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --threads 8 +``` + +This sets the FAIM solver command to use the multithreaded engine with +`--threads=8`. It is a per-solve thread count, not a request to run multiple +models in parallel. + +If Ogo and FAIM/N88 are installed in separate conda environments, activate the +Ogo environment first and pass the FAIM environment with `--faim_env`. Ogo +generates the `.n88model` in the current Python process, then launches each +FAIM/N88 subprocess through `conda run -n ENV_NAME`. + +For example, if Ogo is installed in `ogo-dev` and FAIM/N88 is installed in +`ogoloco-n88`: + +```bash +conda activate ogo-dev + +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --faim_env ogoloco-n88 \ + --threads 8 +``` + +The same pattern works for femur: + +```bash +ogoFEA hip \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-leftFemur_mask.nii.gz \ + --side left \ + --faim_env ogoloco-n88 \ + --threads 8 +``` + +Use `--conda_executable` if `conda` is not on `PATH` or if a specific conda +install should be used. For example: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --faim_env ogoloco-n88 \ + --conda_executable /path/to/conda +``` + +Other available solver-location options are `--faim_bin_dir`, +`--faim_install_root`, `--faim_license_dir`, and explicit command overrides +such as `--faim_command`, `--n88postfaim_command`, and `--n88tabulate_command`. + +FAIM/N88 does not interpret prescribed displacement as percent strain in these +models. The `.n88model` stores the applied displacement in millimeters. For +spine, Ogo converts Crawford `0.68%` strain to millimeters from the generated +vertebral height before solving, updates the model displacement, and reports +that solved load directly. Femur reporting still converts the `4%` endpoint +from generated model geometry for `_results.csv`. + +## Outputs + +The default output directory is the calibrated image directory. Use +`--output_path` for a dedicated derivatives folder. + +Expected default outputs are: + +| Output | Meaning | +| --- | --- | +| `.n88model` | The generated finite-element model. | +| `_modeling.json` | Traceable record of inputs, alignment, image processing, material laws, boundary conditions, audit summary, and reporting settings. | +| `_results.csv` | One-row summary written after solve with stiffness, reaction force, and profile-specific force endpoints. | +| Solve/postprocessing files | Written when `--no-solve` is not used. These come from FAIM/N88 postprocessing. | + +Use `--debug` to write visual sidecars such as the boundary-condition audit +PNG. These images are intended for manual model inspection and are not part of +the minimal output contract. + +## Results CSV + +After a successful solve, `ogoFEA` writes one compact `_results.csv` next to the +model. This is the file intended for downstream analysis tables. + +Maintained spine and femur models write the same columns: + +| Field | Meaning | +| --- | --- | +| `model_file` | Source `.n88model`. | +| `analysis_file` | FAIM/N88 postprocessing text file used to tabulate reaction force. | +| `analysis_var` | N88 reaction-force variable tabulated from `analysis_file`. | +| `applied_displacement` | Prescribed displacement in the solved `.n88model`. | +| `reaction_force_N` | Reaction force from the solved model at the prescribed displacement. | +| `stiffness_N_per_mm` | Reaction force divided by applied displacement when computable. | +| `characteristic_length_mm` | Spine: distance between `body_top` and `body_bottom` centroids along the loading axis. Femur: distance between femoral-head and greater-trochanter PMMA node-set centroids along the loading axis. | + +Spine models are solved directly at `0.68%` strain, following the vertebral +strength endpoint reported by Crawford et al. (2003). Femur models are solved +directly at `4%` displacement. The exact percent endpoint and converted +millimeter displacement are recorded in `_modeling.json`; the CSV stays compact +for downstream statistics. + +## Spine Model + +The spine workflow builds one compression model per `--vertebra` target: + +1. Threshold the labelled mask into vertebral body and posterior process. +2. Crop the image and masks to the vertebra. +3. Align the body to the bundled vertebral-body reference using scaled ICP. +4. Apply transform and isotropic resampling through the shared VTK reslice + helper. The default output spacing is `1.0 x 1.0 x 1.0 mm`. +5. Smooth body/process masks with one binary close/open pass only when at + least one input spacing dimension is coarser than 2 mm. +6. Generate superior and inferior PMMA caps fitted to the body surface. +7. Convert density to material IDs and construct the N88 model. +8. Apply axial compression boundary conditions. + +Default spine boundary conditions: + +| Region | Node set | Constraint | +| --- | --- | --- | +| Superior PMMA cap | `body_top` | Prescribed displacement in the compression direction. | +| Inferior PMMA cap | `body_bottom` | Fixed in the compression direction. | + +The default high-level spine preset is `benchmark-linear`. It applies the +spineFE benchmark linear material settings for model construction, then +`ogoFEA` updates the generated model to solve at Crawford `0.68%` strain. +The `0.68%` endpoint follows Crawford RP, Cann CE, Keaveny TM. Finite element +models predict in vitro vertebral body compressive strength better than +quantitative computed tomography. Bone. 2003;33(4):744-50. +doi:10.1016/s8756-3282(03)00210-2. + +Available spine presets: + +| Preset | Use | +| --- | --- | +| `benchmark-linear` | Default linear spineFE benchmark-style run. | +| `benchmark-nonlinear` | Nonlinear material/yield settings for benchmark-style runs. | +| `none` | Pass only explicit lower-level options. | + +## Femur Model + +The femur workflow builds one sideways-fall model per side: + +1. Read the calibrated image and whole-femur mask. +2. Pre-rotate the side to a stable starting orientation. +3. ICP-align to the bundled left or right femur reference. +4. Apply transform and isotropic resampling through the shared VTK reslice + helper. The default output spacing is `1.0 x 1.0 x 1.0 mm`. +5. Smooth the transformed femur mask with one binary close/open pass only when + at least one input spacing dimension is coarser than 2 mm. If a compartment + mask is supplied, the derived cortical binary mask follows the same rule. +6. Standardize the distal shaft with a flat model-grid cut. The default cut + mode detects the lesser trochanter and keeps `50 mm` distal to it. If the + required distal field of view is missing, model generation fails. +7. Generate PMMA fixtures for the femoral head and greater trochanter. +8. Convert density to material IDs and construct the N88 model. +9. Apply sideways-fall boundary conditions. + +Default femur boundary conditions: + +| Region | Node set | Constraint | +| --- | --- | --- | +| Femoral head PMMA | `Femoral_Head_PMMA_Nodes` | Prescribed displacement toward the greater trochanter. | +| Greater trochanter PMMA | `Greater_Trochanter_PMMA_Nodes` | Constrained in the loading direction. | +| Distal shaft cut face | `Distal_Femur_Nodes` | Constrained to remove rigid-body motion. | + +Femur reporting defaults to stiffness and force at `4%` displacement. Pistoia +postprocessing is not run for femur models by the high-level wrapper. + +## Materials + +Spine and femur use the same shared material-table builder in +`ogo.fea.materials`. + +Material ID convention: + +| Material | IDs | +| --- | --- | +| Background | `0` | +| Trabecular bone | `1..128` | +| Cortical bone | `129..256` when a cortical region is present | +| PMMA fixtures/caps | Default `5000` | + +The default material law is `default_E`, defined in `ogo.fea.material_laws`: + +```text +E = 10500 * rho^2.29 MPa +``` + +where `rho` is density in g/cc. Poisson's ratio defaults to `0.3` for bone. +PMMA defaults are `E = 2500 MPa`, Poisson's ratio `0.3`. + +The default spine preset overrides the elastic law to `kopperdahl_trab_E` for +both trabecular and cortical regions. The simple femur path uses `default_E` +unless a material-law override is supplied. If a femur compartment mask is +provided, cortical and trabecular regions use the same law unless cortical +overrides are supplied. + +### Kopperdahl Spine Presets + +The spine benchmark presets use the Kopperdahl trabecular density-modulus law: + +```text +E = 2980 * rho^1.05 MPa +``` + +where `rho` is density in g/cc. In code this is +`ogo.fea.material_laws.kopperdahl_trab_E`. + +The `benchmark-linear` preset uses this elastic law only. It does not assign a +bone yield criterion, so the solved model is linear elastic. The maintained +spine workflow does not run or report Pistoia postprocessing. + +The `benchmark-nonlinear` preset additionally assigns the Kopperdahl +compressive yield law: + +```text +sigma_y,c = 37.4 * rho^1.39 MPa +``` + +where `rho` is density in g/cc. In code this is +`ogo.fea.material_laws.kopperdahl_trab_yc`. The current nonlinear preset uses +the same function for compression and tension unless the user supplies separate +yield functions. When yield functions are present, Ogo creates +`vtkboneMohrCoulombIsotropicMaterial` entries with the resolved tensile and +compressive yield strengths. + +Material-law overrides can be passed through the high-level command because +unknown options are forwarded to the lower-level generator: + +```bash +ogoFEA hip \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-leftFemur_mask.nii.gz \ + --side left \ + --elastic_E_func kopperdahl_trab_E \ + --cort_elastic_E_func kopperdahl_trab_E +``` + +Supported named laws are defined in `ogo.fea.material_laws`, including +`default_E`, `kopperdahl_trab_E`, `kopperdahl_trab_yc`, `morgan_trab_E`, +`crawford_voxel_E`, and Bayraktar-style constants. + +## Quality Control + +Every generated model is audited by `ogo/cli/CheckFEModelBC.py` unless +`--skip_bc_audit` is used. The audit summary is embedded in `_modeling.json`. + +For manual inspection, run with `--debug`: + +```bash +ogoFEA hip \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-leftFemur_mask.nii.gz \ + --side left \ + --debug \ + --no-solve +``` + +This writes a boundary-condition audit PNG next to the model. For spine, debug +mode also enables legacy quick-look output from the lower-level generator. + +## Common Adjustments + +Change output resolution: + +```bash +ogoFEA spine image.nii.gz labels.nii.gz \ + --vertebra L1:2:3 \ + --iso_resolution 0.8 +``` + +Change femur shaft retention: + +```bash +ogoFEA hip image.nii.gz femur_mask.nii.gz \ + --side left \ + --femur_lesser_trochanter_distal_offset 70 +``` + +Change PMMA dimensions: + +```bash +ogoFEA hip image.nii.gz femur_mask.nii.gz \ + --side left \ + --pmma_thick 6 \ + --pmma_intrusion 6 +``` + +Run model generation only, inspect the `.n88model`, then solve later: + +```bash +ogoFEA hip image.nii.gz femur_mask.nii.gz \ + --side left \ + --no-solve \ + --debug +``` diff --git a/ogo/fea/boundary.py b/ogo/fea/boundary.py new file mode 100644 index 0000000..44e61aa --- /dev/null +++ b/ogo/fea/boundary.py @@ -0,0 +1,469 @@ +"""Shared boundary-condition geometry helpers for FE workflows.""" + +from ogo.util.vtk_image import numpy_to_vtk_image +from ogo.util.vtk_image import vtk_image_to_numpy + + +def axis_index(axis): + """Return the array axis index for a named anatomical/image axis.""" + axis_map = {"x": 0, "y": 1, "z": 2} + try: + return axis_map[str(axis).lower()] + except KeyError as exc: + raise ValueError("axis must be 'x', 'y', or 'z'.") from exc + + +def largest_connected_component(mask): + """Keep the largest connected component in a binary NumPy mask.""" + import numpy as np + from scipy.ndimage import label + + if not np.any(mask): + return mask.astype(bool) + labels, _ = label(mask) + sizes = np.bincount(labels.ravel()) + sizes[0] = 0 + return labels == int(np.argmax(sizes)) + + +def should_smooth_resampled_mask(spacing, threshold_mm): + """Return True when input spacing is coarse enough to justify mask smoothing.""" + return any(float(value) > float(threshold_mm) for value in spacing) + + +def smooth_binary_mask_vtk(mask_vtk, *, close_iter=1, open_iter=1): + """Apply light binary closing/opening to a VTK mask and keep the main component.""" + import numpy as np + import vtk + from scipy.ndimage import binary_closing, binary_dilation, binary_erosion + + mask = vtk_image_to_numpy(mask_vtk, processing_order=True) > 0 + if close_iter > 0: + mask = binary_closing(mask, structure=np.ones((3, 3, 3), dtype=bool), iterations=int(close_iter)) + if open_iter > 0: + mask = binary_erosion(mask, iterations=int(open_iter)) + mask = binary_dilation(mask, iterations=int(open_iter)) + mask = largest_connected_component(mask) + return numpy_to_vtk_image( + mask.astype(np.uint8), + mask_vtk, + vtk.VTK_UNSIGNED_CHAR, + processing_order=True, + ) + + +def _bounding_box(mask): + import numpy as np + + coords = np.array(np.where(mask)) + if coords.size == 0: + raise ValueError("Cannot generate a cap from an empty mask.") + min_coords = coords.min(axis=1) + max_coords = coords.max(axis=1) + 1 + return tuple(slice(min_coords[i], max_coords[i]) for i in range(mask.ndim)) + + +def _expanded_bbox(mask, extrusion_axis, direction, thickness): + bb = _bounding_box(mask) + bb_list = list(bb) + if direction == "up": + bb_list[extrusion_axis] = slice( + bb[extrusion_axis].start, + min(bb[extrusion_axis].stop + thickness, mask.shape[extrusion_axis]), + ) + elif direction == "down": + bb_list[extrusion_axis] = slice( + max(bb[extrusion_axis].start - thickness, 0), + bb[extrusion_axis].stop, + ) + else: + raise ValueError("direction must be 'up' or 'down'.") + return tuple(bb_list), bb + + +def _contact_surface(mask, extrusion_axis, direction): + import numpy as np + + surface = np.zeros_like(mask, dtype=bool) + dims = mask.shape + if extrusion_axis == 0: + for j in range(dims[1]): + for k in range(dims[2]): + hit = np.flatnonzero(mask[:, j, k]) + if hit.size: + surface[int(hit.max() if direction == "up" else hit.min()), j, k] = True + elif extrusion_axis == 1: + for i in range(dims[0]): + for k in range(dims[2]): + hit = np.flatnonzero(mask[i, :, k]) + if hit.size: + surface[i, int(hit.max() if direction == "up" else hit.min()), k] = True + else: + for i in range(dims[0]): + for j in range(dims[1]): + hit = np.flatnonzero(mask[i, j, :]) + if hit.size: + surface[i, j, int(hit.max() if direction == "up" else hit.min())] = True + return surface + + +def _shift_surface(surface, extrusion_axis, direction, thickness): + import numpy as np + + cap = np.zeros_like(surface, dtype=bool) + axis_len = int(surface.shape[extrusion_axis]) + step_sign = 1 if direction == "up" else -1 + for step in range(1, max(int(thickness), 0) + 1): + shifted = np.zeros_like(surface, dtype=bool) + if step_sign > 0: + src = slice(0, axis_len - step) + dst = slice(step, axis_len) + else: + src = slice(step, axis_len) + dst = slice(0, axis_len - step) + if extrusion_axis == 0: + shifted[dst, :, :] = surface[src, :, :] + elif extrusion_axis == 1: + shifted[:, dst, :] = surface[:, src, :] + else: + shifted[:, :, dst] = surface[:, :, src] + cap |= shifted + return cap + + +def _fit_cap(mask, cap, extrusion_axis, direction, thickness): + import numpy as np + + flat_cap = np.zeros_like(cap, dtype=bool) + cap = largest_connected_component(cap) + if not np.any(cap): + return flat_cap + + thickness_records = [] + if extrusion_axis == 0: + outer = int(np.max(np.where(cap)[0])) if direction == "up" else int(np.min(np.where(cap)[0])) + for j in range(mask.shape[1]): + for k in range(mask.shape[2]): + hit = np.flatnonzero(mask[:, j, k]) + cap_hit = np.flatnonzero(cap[:, j, k]) + if hit.size == 0 or cap_hit.size == 0: + continue + contact = int(hit.max() if direction == "up" else hit.min()) + start, stop = (contact + 1, outer + 1) if direction == "up" else (outer, contact) + if start < stop: + flat_cap[start:stop, j, k] = True + thickness_records.append(((j, k), int(stop - start))) + elif extrusion_axis == 1: + outer = int(np.max(np.where(cap)[1])) if direction == "up" else int(np.min(np.where(cap)[1])) + for i in range(mask.shape[0]): + for k in range(mask.shape[2]): + hit = np.flatnonzero(mask[i, :, k]) + cap_hit = np.flatnonzero(cap[i, :, k]) + if hit.size == 0 or cap_hit.size == 0: + continue + contact = int(hit.max() if direction == "up" else hit.min()) + start, stop = (contact + 1, outer + 1) if direction == "up" else (outer, contact) + if start < stop: + flat_cap[i, start:stop, k] = True + thickness_records.append(((i, k), int(stop - start))) + else: + outer = int(np.max(np.where(cap)[2])) if direction == "up" else int(np.min(np.where(cap)[2])) + for i in range(mask.shape[0]): + for j in range(mask.shape[1]): + hit = np.flatnonzero(mask[i, j, :]) + cap_hit = np.flatnonzero(cap[i, j, :]) + if hit.size == 0 or cap_hit.size == 0: + continue + contact = int(hit.max() if direction == "up" else hit.min()) + start, stop = (contact + 1, outer + 1) if direction == "up" else (outer, contact) + if start < stop: + flat_cap[i, j, start:stop] = True + thickness_records.append(((i, j), int(stop - start))) + + if thickness_records: + values = np.asarray([thick for _, thick in thickness_records], dtype=float) + upper = max(float(thickness * 2), 2.0 * float(np.mean(values))) + for coords, thick in thickness_records: + if float(thick) <= upper: + continue + if extrusion_axis == 0: + j, k = coords + flat_cap[:, j, k] = False + elif extrusion_axis == 1: + i, k = coords + flat_cap[i, :, k] = False + else: + i, j = coords + flat_cap[i, j, :] = False + return flat_cap + + +def _projected_footprint(surface, extrusion_axis, shape): + import numpy as np + from scipy.ndimage import binary_fill_holes + + footprint = surface.any(axis=extrusion_axis) + if not np.any(footprint): + return footprint + + coords = np.array(np.where(footprint)) + mins = coords.min(axis=1) + maxs = coords.max(axis=1) + if shape == "box": + out = np.zeros_like(footprint, dtype=bool) + out[tuple(slice(mins[i], maxs[i] + 1) for i in range(2))] = True + return out + if shape == "round": + grids = np.ogrid[tuple(slice(0, size) for size in footprint.shape)] + center = (mins + maxs) / 2.0 + radii = np.maximum((maxs - mins + 1) / 2.0, 0.5) + distance = sum(((grids[i] - center[i]) / radii[i]) ** 2 for i in range(2)) + return distance <= 1.0 + if shape == "fit": + return binary_fill_holes(footprint) + raise ValueError("shape must be 'fit', 'box', or 'round'.") + + +def _projected_contact_within_depth(mask, extrusion_axis, direction, depth): + """Project only columns with bone near the flat fixture contact plane.""" + import numpy as np + + hit = np.where(mask)[extrusion_axis] + contact = int(hit.max() if direction == "up" else hit.min()) + depth = max(int(depth), 1) + if direction == "up": + support_slice = slice(max(contact - depth + 1, 0), contact + 1) + elif direction == "down": + support_slice = slice(contact, min(contact + depth, mask.shape[extrusion_axis])) + else: + raise ValueError("direction must be 'up' or 'down'.") + + if extrusion_axis == 0: + return mask[support_slice, :, :].any(axis=0) + if extrusion_axis == 1: + return mask[:, support_slice, :].any(axis=1) + return mask[:, :, support_slice].any(axis=2) + + +def _shaped_contact_cap(mask, surface, extrusion_axis, direction, thickness, shape): + import numpy as np + + footprint = _projected_footprint(surface, extrusion_axis, shape) + cap = np.zeros_like(mask, dtype=bool) + hit = np.where(mask)[extrusion_axis] + outer = int(hit.max() + thickness if direction == "up" else hit.min() - thickness) + outer = int(np.clip(outer, 0, mask.shape[extrusion_axis] - 1)) + + footprint_coords = np.array(np.where(footprint)).T + for coord in footprint_coords: + if extrusion_axis == 0: + j, k = coord + ray_hit = np.flatnonzero(mask[:, j, k]) + elif extrusion_axis == 1: + i, k = coord + ray_hit = np.flatnonzero(mask[i, :, k]) + else: + i, j = coord + ray_hit = np.flatnonzero(mask[i, j, :]) + if ray_hit.size == 0: + continue + contact = int(ray_hit.max() if direction == "up" else ray_hit.min()) + start, stop = (contact + 1, outer + 1) if direction == "up" else (outer, contact) + if start >= stop: + continue + if extrusion_axis == 0: + cap[start:stop, j, k] = True + elif extrusion_axis == 1: + cap[i, start:stop, k] = True + else: + cap[i, j, start:stop] = True + return cap + + +def _full_fixture_cap( + mask, + surface, + extrusion_axis, + direction, + thickness, + shape, + intrusion=0, + crop_to_contact=False, +): + import numpy as np + + footprint = _projected_footprint(surface, extrusion_axis, shape) + if crop_to_contact: + footprint &= _projected_contact_within_depth(mask, extrusion_axis, direction, intrusion) + cap = np.zeros_like(mask, dtype=bool) + hit = np.where(mask)[extrusion_axis] + contact = int(hit.min() if direction == "down" else hit.max()) + intrusion = max(int(intrusion), 0) + if direction == "up": + slab = slice( + max(contact - intrusion + 1, 0), + min(contact + thickness + 1, mask.shape[extrusion_axis]), + ) + else: + slab = slice( + max(contact - thickness, 0), + min(contact + intrusion, mask.shape[extrusion_axis]), + ) + if extrusion_axis == 0: + cap[slab, :, :] = footprint + elif extrusion_axis == 1: + cap[:, slab, :] = footprint[:, None, :] + else: + cap[:, :, slab] = footprint[:, :, None] + return cap + + +def generate_bone_cap_mask(mask, *, axis="x", direction="up", thickness=5, shape="fit"): + """Generate a one-sided cap mask from a binary bone/contact mask.""" + import numpy as np + + shape = str(shape).lower() + thickness = int(thickness) + extrusion_axis = axis_index(axis) + work_bb, _ = _expanded_bbox(mask, extrusion_axis, direction, thickness) + cropped = mask[work_bb].astype(bool) + surface = _contact_surface(cropped, extrusion_axis, direction) + + if shape == "fit": + cap = _fit_cap(cropped, _shift_surface(surface, extrusion_axis, direction, thickness), extrusion_axis, direction, thickness) + else: + cap = _shaped_contact_cap(cropped, surface, extrusion_axis, direction, thickness, shape) + + output = np.zeros_like(mask, dtype=bool) + output[work_bb] = cap + output[mask] = False + return largest_connected_component(output) + + +def generate_fixture_cap_mask( + mask, + *, + axis="x", + direction="up", + thickness=5, + shape="box", + intrusion=0, + crop_to_contact=False, +): + """Generate a full geometric fixture cap from a binary ROI/contact mask. + + Unlike a bone-fit cap, fixture caps keep their full square/round footprint. + They may overlap the bone mask; the later image-combine step preserves bone + material IDs where PMMA and bone overlap. + """ + import numpy as np + + shape = str(shape).lower() + if shape not in {"box", "round"}: + raise ValueError("fixture cap shape must be 'box' or 'round'.") + thickness = int(thickness) + extrusion_axis = axis_index(axis) + work_bb, _ = _expanded_bbox(mask, extrusion_axis, direction, thickness) + cropped = mask[work_bb].astype(bool) + surface = _contact_surface(cropped, extrusion_axis, direction) + cap = _full_fixture_cap( + cropped, + surface, + extrusion_axis, + direction, + thickness, + shape, + intrusion=intrusion, + crop_to_contact=crop_to_contact, + ) + + output = np.zeros_like(mask, dtype=bool) + output[work_bb] = cap + return largest_connected_component(output) + + +def label_foreground_in_bounds_vtk(vtk_image, bounds, *, label_value=1): + """Label nonzero voxels inside physical bounds for contact-cap generation.""" + import numpy as np + import vtk + + dims = vtk_image.GetDimensions() + extent = vtk_image.GetExtent() + spacing = vtk_image.GetSpacing() + origin = vtk_image.GetOrigin() + data = vtk_image_to_numpy(vtk_image) + + x = origin[0] + (np.arange(dims[0]) + extent[0]) * spacing[0] + y = origin[1] + (np.arange(dims[1]) + extent[2]) * spacing[1] + z = origin[2] + (np.arange(dims[2]) + extent[4]) * spacing[2] + in_bounds = ( + (x[:, None, None] >= bounds[0]) + & (x[:, None, None] <= bounds[1]) + & (y[None, :, None] >= bounds[2]) + & (y[None, :, None] <= bounds[3]) + & (z[None, None, :] >= bounds[4]) + & (z[None, None, :] <= bounds[5]) + ) + + output_data = np.where((data != 0) & in_bounds, label_value, 0).astype(np.uint16) + return numpy_to_vtk_image(output_data, vtk_image, vtk_array_type=vtk.VTK_UNSIGNED_SHORT) + + +def generate_bone_cap_vtk( + labelled_vtk_image, + *, + label_value, + axis="x", + direction="up", + thickness=5, + shape="fit", + output_value=1, +): + """Generate a VTK PMMA/support cap from a labelled bone mask.""" + import numpy as np + + labels = vtk_image_to_numpy(labelled_vtk_image, processing_order=True) + cap = generate_bone_cap_mask( + labels == label_value, + axis=axis, + direction=direction, + thickness=thickness, + shape=shape, + ) + return numpy_to_vtk_image( + (cap.astype(np.uint16) * int(output_value)), + labelled_vtk_image, + processing_order=True, + ) + + +def generate_fixture_cap_vtk( + labelled_vtk_image, + *, + label_value, + axis="x", + direction="up", + thickness=5, + shape="box", + intrusion=0, + crop_to_contact=False, + output_value=1, +): + """Generate a full geometric VTK fixture cap from a labelled ROI mask.""" + import numpy as np + + labels = vtk_image_to_numpy(labelled_vtk_image, processing_order=True) + cap = generate_fixture_cap_mask( + labels == label_value, + axis=axis, + direction=direction, + thickness=thickness, + shape=shape, + intrusion=intrusion, + crop_to_contact=crop_to_contact, + ) + return numpy_to_vtk_image( + (cap.astype(np.uint16) * int(output_value)), + labelled_vtk_image, + processing_order=True, + ) diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index d275d58..21455e9 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -1,9 +1,64 @@ -"""Femur-specific helpers for sideways-fall FE model generation.""" +"""Femur-specific defaults and helpers for sideways-fall FE generation. + +Default femur workflow: +1. Read the calibrated density image and whole-femur mask. The public wrapper + chooses left/right from ``--side`` and the lower-level script uses + ``femur_side=1`` for left and ``2`` for right. +2. Pre-rotate the femur by side to provide a stable starting orientation for + ICP. This is a VTK reslice in the native input grid. +3. Run ICP to the bundled side-specific femur reference, then apply the ICP + transform and set the final output spacing to 1.0 x 1.0 x 1.0 mm in one + shared VTK reslice helper. Image data use cubic interpolation; bone and + compartment labels use nearest-neighbor interpolation. +4. Smooth the transformed femur mask with one binary close/open pass only when + at least one input spacing dimension is coarser than 2 mm. If a compartment + mask is supplied, the derived cortical binary mask follows the same rule. +5. Standardize the distal shaft by cutting on a flat model-grid z plane. The + default cut mode detects the lesser-trochanter cross-section peak and keeps + the femur to 50 mm distal to that landmark. If the scan does not include the + required distal field of view, model generation fails instead of silently + using a shorter shaft. ``fixed_length`` mode is available for debugging. +6. Generate two geometric PMMA fixtures: a round femoral-head loading fixture + and a box-like greater-trochanter contact fixture. Defaults are 6 mm PMMA + thickness and 6 mm PMMA intrusion toward bone before material merging. The + femoral-head footprint is widened by 10 mm and lengthened by 80 mm so the + anatomical cropping determines the actual contact. +7. Apply sideways-fall boundary conditions: prescribed displacement at the + femoral-head PMMA cap toward the greater trochanter, loading-direction + constraint at the greater-trochanter PMMA cap, and distal shaft constraints + to remove rigid-body motion. +8. Build materials with the same shared bone/PMMA material-table helper used by + the spine workflow. If no compartment mask is supplied, femur bone is one + trabecular-style region. If supplied, cortical=1 and trabecular=2 by default. +""" LEFT_FEMUR = 1 RIGHT_FEMUR = 2 +DEFAULT_FEMUR_ISO_RESOLUTION_MM = 1.0 +DEFAULT_FEMUR_FE_DISPLACEMENT = -4.0 +DEFAULT_FEMUR_TARGET_DISPLACEMENT_PERCENT = 4.0 +DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM = 2.0 +FEMORAL_HEAD_FIXTURE_WIDTH_EXTENSION_MM = 10.0 +FEMORAL_HEAD_FIXTURE_LONG_AXIS_EXTENSION_MM = 80.0 +DEFAULT_PMMA_THICKNESS_MM = 6.0 +DEFAULT_PMMA_INTRUSION_MM = 6.0 +DEFAULT_FEMUR_SHAFT_LENGTH_MM = 100.0 +DEFAULT_FEMUR_CUT_MODE = "lesser_trochanter" +DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM = 50.0 +DEFAULT_CORTICAL_LABEL = 1 +DEFAULT_TRABECULAR_LABEL = 2 + +FEMORAL_HEAD_NODE_SET = "Femoral_Head_PMMA_Nodes" +GREATER_TROCHANTER_NODE_SET = "Greater_Trochanter_PMMA_Nodes" +DISTAL_FEMUR_NODE_SET = "Distal_Femur_Nodes" +SIDEWAYS_FALL_NODE_SETS = [ + FEMORAL_HEAD_NODE_SET, + GREATER_TROCHANTER_NODE_SET, + DISTAL_FEMUR_NODE_SET, +] + def side_suffix(femur_side): """Return the legacy output suffix for a femur side.""" @@ -44,3 +99,320 @@ def tilted_side_support_vector(angle_degrees=-20): ] ) return rotation_x @ np.array([0, 0, -1]) + + +def cortical_compartment_mask(compartment_vtk, *, cortical_label=1, trabecular_label=2): + """Return a binary VTK mask for cortical voxels from a trab/cort label image.""" + import numpy as np + import vtk + + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + + data = np.rint(vtk_image_to_numpy(compartment_vtk)).astype(np.int32) + cortical_label = int(cortical_label) + trabecular_label = int(trabecular_label) + present = set(int(value) for value in np.unique(data) if int(value) != 0) + missing = [label for label in (cortical_label, trabecular_label) if label not in present] + if missing: + raise ValueError( + "Compartment mask is missing required label(s): {}. " + "Expected cortical={} and trabecular={}.".format( + ", ".join(str(label) for label in missing), + cortical_label, + trabecular_label, + ) + ) + return numpy_to_vtk_image( + (data == cortical_label).astype(np.uint8), + compartment_vtk, + vtk_array_type=vtk.VTK_UNSIGNED_CHAR, + ) + + +def distal_cut_z_from_reference(reference_bounds, retained_length_mm): + """Return a flat distal cut plane in aligned reference coordinates.""" + retained_length_mm = float(retained_length_mm) + if retained_length_mm <= 0: + raise ValueError("retained_length_mm must be positive.") + return float(reference_bounds[5]) - retained_length_mm + + +def femur_z_coverage(vtk_mask): + """Return physical z coverage for nonzero voxels in a VTK femur mask.""" + import numpy as np + from ogo.util.vtk_image import vtk_image_to_numpy + + dims = vtk_mask.GetDimensions() + extent = vtk_mask.GetExtent() + spacing = vtk_mask.GetSpacing() + origin = vtk_mask.GetOrigin() + data = vtk_image_to_numpy(vtk_mask) + occupied = np.where(np.any(data != 0, axis=(0, 1)))[0] + if occupied.size == 0: + raise ValueError("Cannot standardize femur shaft length from an empty mask.") + z = origin[2] + (occupied + extent[4]) * spacing[2] + return float(z.min()), float(z.max()) + + +def femur_z_profile(vtk_mask): + """Return per-z cross-section profile arrays for a VTK femur mask.""" + import numpy as np + from ogo.util.vtk_image import vtk_image_to_numpy + + dims = vtk_mask.GetDimensions() + extent = vtk_mask.GetExtent() + spacing = vtk_mask.GetSpacing() + origin = vtk_mask.GetOrigin() + data = vtk_image_to_numpy(vtk_mask) != 0 + z_coords = origin[2] + (np.arange(dims[2]) + extent[4]) * spacing[2] + y_coords = origin[1] + (np.arange(dims[1]) + extent[2]) * spacing[1] + + rows = [] + for z_index, z_value in enumerate(z_coords): + plane = data[:, :, z_index] + if not plane.any(): + continue + y_indices = np.where(plane)[1] + rows.append( + ( + float(z_value), + float(plane.sum()), + float(y_coords[y_indices].min()), + float(y_coords[y_indices].max()), + ) + ) + if not rows: + raise ValueError("Cannot compute femur profile from an empty mask.") + + profile = np.asarray(rows, dtype=float) + return { + "z": profile[:, 0], + "area": profile[:, 1], + "y_min": profile[:, 2], + "y_max": profile[:, 3], + "width_y": profile[:, 3] - profile[:, 2], + } + + +def _smooth_profile(values, window=7): + """Smooth one profile with an odd moving-average window.""" + import numpy as np + + values = np.asarray(values, dtype=float) + window = max(1, int(window)) + if window % 2 == 0: + window += 1 + if values.size < window or window == 1: + return values.copy() + pad = window // 2 + padded = np.pad(values, (pad, pad), mode="edge") + return np.convolve(padded, np.ones(window, dtype=float) / float(window), mode="valid") + + +def detect_lesser_trochanter_cut_z( + vtk_mask, + *, + distal_offset_mm=0.0, + min_distal_to_greater_mm=8.0, + max_distal_to_greater_mm=45.0, + min_distal_coverage_mm=0.0, + distal_fov_tolerance_mm=2.0, +): + """Detect a lesser-trochanter-based flat distal cut plane. + + The aligned sideways-fall reference frame has femur length along z and the + lateral greater-trochanter side at high y. The greater trochanter is found + from the smoothed high-y profile. The lesser-trochanter/transition level is + then the dominant smoothed cross-sectional area peak distal to that point. + """ + import numpy as np + + profile = femur_z_profile(vtk_mask) + z = profile["z"] + area = _smooth_profile(profile["area"], window=7) + y_max = _smooth_profile(profile["y_max"], window=7) + + z_min = float(z.min()) + z_max = float(z.max()) + if z_max - z_min < max_distal_to_greater_mm: + raise ValueError( + "Femur scan does not include enough proximal-distal coverage to identify " + "the lesser trochanter." + ) + + proximal_mask = z >= (z_min + 0.55 * (z_max - z_min)) + if not np.any(proximal_mask): + raise ValueError("Cannot identify greater trochanter from femur profile.") + proximal_indices = np.where(proximal_mask)[0] + greater_index = int(proximal_indices[np.argmax(y_max[proximal_indices])]) + greater_z = float(z[greater_index]) + + distal_mask = ( + (z <= greater_z - float(min_distal_to_greater_mm)) + & (z >= greater_z - float(max_distal_to_greater_mm)) + ) + distal_indices = np.where(distal_mask)[0] + if distal_indices.size < 5: + raise ValueError( + "Femur scan does not include the distal profile needed to identify " + "the lesser trochanter." + ) + + lesser_index = int(distal_indices[np.argmax(area[distal_indices])]) + lesser_z = float(z[lesser_index]) + cut_z = lesser_z - float(distal_offset_mm) + + required_z_min = cut_z - float(min_distal_coverage_mm) + if z_min > required_z_min: + missing = z_min - required_z_min + if missing <= float(distal_fov_tolerance_mm): + cut_z = z_min + float(min_distal_coverage_mm) + else: + available = max(0.0, cut_z - z_min) + raise ValueError( + "Femur scan does not extend distal to the lesser-trochanter cut plane " + f"(required {float(min_distal_coverage_mm):.1f} mm, available {available:.1f} mm)." + ) + + shaft_area = float(np.median(area[z < lesser_z - 10.0])) if np.any(z < lesser_z - 10.0) else 0.0 + if shaft_area > 0 and float(area[lesser_index]) < 1.08 * shaft_area: + raise ValueError( + "Could not identify a clear lesser-trochanter cross-section peak; " + "the femur field of view is likely incomplete or the alignment failed." + ) + + return { + "cut_z": float(cut_z), + "lesser_trochanter_z": lesser_z, + "greater_trochanter_z": greater_z, + "mask_z_min": z_min, + "mask_z_max": z_max, + "retained_length_mm": float(z_max - cut_z), + "distal_offset_mm": float(distal_offset_mm), + "lesser_area": float(area[lesser_index]), + "shaft_area_median": shaft_area, + } + + +def flat_crop_vtk_image_below_z(vtk_image, cut_z): + """Zero voxels below a physical z cut plane while preserving image metadata.""" + import numpy as np + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + + dims = vtk_image.GetDimensions() + extent = vtk_image.GetExtent() + spacing = vtk_image.GetSpacing() + origin = vtk_image.GetOrigin() + data = vtk_image_to_numpy(vtk_image, copy=True) + z = origin[2] + (np.arange(dims[2]) + extent[4]) * spacing[2] + data[:, :, z < float(cut_z)] = 0 + + return numpy_to_vtk_image(data, vtk_image) + + +def standardize_femur_shaft_length( + image_vtk, + mask_vtk, + *, + reference_bounds, + retained_length_mm=None, + cut_mode="lesser_trochanter", + lesser_trochanter_distal_offset_mm=50.0, +): + """Apply a flat distal crop to aligned femur image and mask. + + In the default mode, the cut plane is detected from the lesser-trochanter + cross-section profile. A fixed retained length can be requested explicitly + for debugging or legacy comparisons. + """ + cut_mode = str(cut_mode).lower() + if cut_mode == "lesser_trochanter": + meta = detect_lesser_trochanter_cut_z( + mask_vtk, + distal_offset_mm=lesser_trochanter_distal_offset_mm, + ) + cut_z = meta["cut_z"] + retained_length_mm = meta["retained_length_mm"] + elif cut_mode == "fixed_length": + if retained_length_mm is None: + raise ValueError("retained_length_mm is required for fixed_length femur cuts.") + cut_z = distal_cut_z_from_reference(reference_bounds, retained_length_mm) + meta = { + "cut_z": cut_z, + "retained_length_mm": float(retained_length_mm), + } + else: + raise ValueError("cut_mode must be 'lesser_trochanter' or 'fixed_length'.") + + z_min, z_max = femur_z_coverage(mask_vtk) + if z_min > cut_z: + available = max(0.0, float(reference_bounds[5]) - z_min) + raise ValueError( + "Femur scan does not include enough distal shaft for the standard crop " + f"(required retained length {float(retained_length_mm):.1f} mm, " + f"available approximately {available:.1f} mm)." + ) + meta.update( + { + "cut_z": cut_z, + "mask_z_min": z_min, + "mask_z_max": z_max, + "retained_length_mm": float(retained_length_mm), + "cut_mode": cut_mode, + } + ) + return ( + flat_crop_vtk_image_below_z(image_vtk, cut_z), + flat_crop_vtk_image_below_z(mask_vtk, cut_z), + meta, + ) + + +def swap_xz_footprint(bounds): + """Swap x/z footprint dimensions of physical bounds around the same center.""" + x_center = (float(bounds[0]) + float(bounds[1])) / 2.0 + z_center = (float(bounds[4]) + float(bounds[5])) / 2.0 + x_length = float(bounds[1]) - float(bounds[0]) + z_length = float(bounds[5]) - float(bounds[4]) + return ( + x_center - z_length / 2.0, + x_center + z_length / 2.0, + float(bounds[2]), + float(bounds[3]), + z_center - x_length / 2.0, + z_center + x_length / 2.0, + ) + + +def expand_z_footprint(bounds, extension_mm): + """Expand physical bounds along z around the same center.""" + z_center = (float(bounds[4]) + float(bounds[5])) / 2.0 + z_length = float(bounds[5]) - float(bounds[4]) + float(extension_mm) + if z_length <= 0: + raise ValueError("Expanded z footprint length must be positive.") + return ( + float(bounds[0]), + float(bounds[1]), + float(bounds[2]), + float(bounds[3]), + z_center - z_length / 2.0, + z_center + z_length / 2.0, + ) + + +def expand_xz_footprint(bounds, *, x_extension_mm=0.0, z_extension_mm=0.0): + """Expand physical bounds along x and z around the same center.""" + x_center = (float(bounds[0]) + float(bounds[1])) / 2.0 + z_center = (float(bounds[4]) + float(bounds[5])) / 2.0 + x_length = float(bounds[1]) - float(bounds[0]) + float(x_extension_mm) + z_length = float(bounds[5]) - float(bounds[4]) + float(z_extension_mm) + if x_length <= 0 or z_length <= 0: + raise ValueError("Expanded x/z footprint lengths must be positive.") + return ( + x_center - x_length / 2.0, + x_center + x_length / 2.0, + float(bounds[2]), + float(bounds[3]), + z_center - z_length / 2.0, + z_center + z_length / 2.0, + ) diff --git a/ogo/cli/ref/material_laws.py b/ogo/fea/material_laws.py similarity index 100% rename from ogo/cli/ref/material_laws.py rename to ogo/fea/material_laws.py diff --git a/ogo/fea/materials.py b/ogo/fea/materials.py index ec65325..daaec71 100644 --- a/ogo/fea/materials.py +++ b/ogo/fea/materials.py @@ -1,8 +1,16 @@ -"""Material table helpers for Ogo finite-element model generation.""" +"""Material table helpers for Ogo finite-element model generation. + +Spine and femur models use the same material-id convention. Trabecular bone +uses material IDs 1..n_bins. If a cortical compartment is present, cortical bone +uses n_bins+1..2*n_bins. PMMA caps/supports use their own explicit material ID. +The public spine/femur builders below only choose workflow defaults; material +table construction itself is shared in ``build_bone_pmma_material_table``. +""" import inspect +from collections import namedtuple -import ogo.cli.ref.material_laws as material_laws +import ogo.fea.material_laws as material_laws def resolve_material_func(func_or_name, module=material_laws): @@ -21,16 +29,93 @@ def describe_material_func(value): return value -def build_spine_material_table( +_BoneMaterialRegion = namedtuple( + "BoneMaterialRegion", + ["name", "bin_range", "elastic_E_func", "yield_comp_func", "yield_tens_func", "poissons_ratio"], +) + + +class BoneMaterialRegion(_BoneMaterialRegion): + """One contiguous material-id range with one set of bone material laws.""" + + __slots__ = () + + def __new__( + cls, + name, + bin_range, + elastic_E_func=None, + yield_comp_func=None, + yield_tens_func=None, + poissons_ratio=0.3, + ): + return super(BoneMaterialRegion, cls).__new__( + cls, + name, + bin_range, + elastic_E_func, + yield_comp_func, + yield_tens_func, + poissons_ratio, + ) + + +def build_bone_pmma_material_table( bin_centers, *, - n_bins=128, - poissons_ratio=0.3, + bone_regions, pmma_mat_id=5000, pmma_E=2500, pmma_v=0.3, pmma_yield_compression=None, pmma_yield_tension=None, +): + """Build one material table from explicit bone regions plus PMMA.""" + import vtkbone + + import ogo.util.Helper as ogo + + material_table = vtkbone.vtkboneMaterialTable() + for region in bone_regions: + start, stop = region.bin_range + width = int(stop) - int(start) + if width <= 0: + raise ValueError(f"Invalid bin range for {region.name}: {region.bin_range}") + if len(bin_centers) >= 2 * width and int(start) > width: + region_bin_centers = bin_centers[width:2 * width] + else: + region_bin_centers = bin_centers[:width] + if len(region_bin_centers) != width: + raise ValueError( + f"Material region {region.name} needs {width} bin centers, " + f"but only {len(region_bin_centers)} are available." + ) + elastic_E_func = resolve_material_func(region.elastic_E_func) or material_laws.default_E + yield_comp_func = resolve_material_func(region.yield_comp_func) + yield_tens_func = resolve_material_func(region.yield_tens_func) + material_table = ogo.add_bone_material( + material_table, + region_bin_centers, + elastic_E_func=elastic_E_func, + mu=region.poissons_ratio, + yield_comp_func=yield_comp_func, + yield_tens_func=yield_tens_func, + bin_range=region.bin_range, + material_name=region.name, + ) + return ogo.add_pmma_material( + material_table, + pmma_mat_id=pmma_mat_id, + pmma_E=pmma_E, + pmma_v=pmma_v, + pmma_yield_tension=pmma_yield_tension, + pmma_yield_compression=pmma_yield_compression, + ) + + +def _bone_regions( + n_bins=128, + poissons_ratio=0.3, elastic_E_func=None, yield_comp_func=None, yield_tens_func=None, @@ -38,12 +123,9 @@ def build_spine_material_table( cort_yield_comp_func=None, cort_yield_tens_func=None, cort_poissons_ratio=None, + include_cortical=True, ): - """Build trabecular, cortical, and PMMA materials for spine compression FE.""" - import vtkbone - - import ogo.util.Helper as ogo - + """Return bone material regions using the shared trab/cort convention.""" elastic_E_func = resolve_material_func(elastic_E_func) or material_laws.default_E yield_comp_func = resolve_material_func(yield_comp_func) yield_tens_func = resolve_material_func(yield_tens_func) @@ -52,80 +134,116 @@ def build_spine_material_table( cort_yield_tens_func = resolve_material_func(cort_yield_tens_func) or yield_tens_func cort_poissons_ratio = poissons_ratio if cort_poissons_ratio is None else cort_poissons_ratio - material_table = vtkbone.vtkboneMaterialTable() + regions = [ + BoneMaterialRegion( + "TrabBone", + (1, n_bins + 1), + elastic_E_func=elastic_E_func, + yield_comp_func=yield_comp_func, + yield_tens_func=yield_tens_func, + poissons_ratio=poissons_ratio, + ) + ] + if include_cortical: + regions.append( + BoneMaterialRegion( + "CortBone", + (n_bins + 1, 2 * n_bins + 1), + elastic_E_func=cort_elastic_E_func, + yield_comp_func=cort_yield_comp_func, + yield_tens_func=cort_yield_tens_func, + poissons_ratio=cort_poissons_ratio, + ), + ) + return regions - # Material ids 0..127 are trabecular; 128..255 are cortical after binning. - material_table = ogo.add_bone_material( - material_table, - bin_centers, - elastic_E_func=elastic_E_func, - mu=poissons_ratio, - yield_comp_func=yield_comp_func, - yield_tens_func=yield_tens_func, - bin_range=(0, n_bins), - material_name="TrabBone", - ) - material_table = ogo.add_bone_material( - material_table, + +def build_spine_material_table( + bin_centers, + *, + n_bins=128, + poissons_ratio=0.3, + pmma_mat_id=5000, + pmma_E=2500, + pmma_v=0.3, + pmma_yield_compression=None, + pmma_yield_tension=None, + elastic_E_func=None, + yield_comp_func=None, + yield_tens_func=None, + cort_elastic_E_func=None, + cort_yield_comp_func=None, + cort_yield_tens_func=None, + cort_poissons_ratio=None, +): + """Build spine materials with the shared trabecular/cortical convention.""" + return build_bone_pmma_material_table( bin_centers, - elastic_E_func=cort_elastic_E_func, - mu=cort_poissons_ratio, - yield_comp_func=cort_yield_comp_func, - yield_tens_func=cort_yield_tens_func, - bin_range=(n_bins, 2 * n_bins), - material_name="CortBone", - ) - material_table = ogo.add_pmma_material( - material_table, - pmma_mat_id, - pmma_E, - pmma_v, + bone_regions=_bone_regions( + n_bins=n_bins, + poissons_ratio=poissons_ratio, + elastic_E_func=elastic_E_func, + yield_comp_func=yield_comp_func, + yield_tens_func=yield_tens_func, + cort_elastic_E_func=cort_elastic_E_func, + cort_yield_comp_func=cort_yield_comp_func, + cort_yield_tens_func=cort_yield_tens_func, + cort_poissons_ratio=cort_poissons_ratio, + include_cortical=True, + ), + pmma_mat_id=pmma_mat_id, + pmma_E=pmma_E, + pmma_v=pmma_v, pmma_yield_tension=pmma_yield_tension, pmma_yield_compression=pmma_yield_compression, ) - return material_table def build_femur_material_table( bin_centers, *, - elastic_Emax=10500, - elastic_exponent=2.29, + n_bins=128, poissons_ratio=0.3, - bone_yield_compression=None, - bone_yield_tension=None, pmma_mat_id=5000, pmma_E=2500, pmma_v=0.3, pmma_yield_compression=None, pmma_yield_tension=None, + elastic_E_func=None, + yield_comp_func=None, + yield_tens_func=None, + cort_elastic_E_func=None, + cort_yield_comp_func=None, + cort_yield_tens_func=None, + cort_poissons_ratio=None, + include_cortical=False, ): - """Build bone and PMMA materials for sideways-fall femur FE.""" - import vtkbone - - import ogo.util.Helper as ogo + """Build femur materials with the same region convention as spine. - material_table = vtkbone.vtkboneMaterialTable() - material_table = ogo.add_bone_material_depreciated( - material_table, + ``include_cortical`` is False for the simple whole-bone femur path and True + when the user supplies a compartment mask. The material IDs stay compatible + either way. + """ + return build_bone_pmma_material_table( bin_centers, - elastic_Emax=elastic_Emax, - elastic_exponent=elastic_exponent, - bone_yield_compression=bone_yield_compression, - bone_yield_tension=bone_yield_tension, - mu=poissons_ratio, - bin_range=(0, len(bin_centers)), - material_name="Bone", - ) - material_table = ogo.add_pmma_material( - material_table, - pmma_mat_id, - pmma_E, - pmma_v, + bone_regions=_bone_regions( + n_bins=n_bins, + poissons_ratio=poissons_ratio, + elastic_E_func=elastic_E_func, + yield_comp_func=yield_comp_func, + yield_tens_func=yield_tens_func, + cort_elastic_E_func=cort_elastic_E_func, + cort_yield_comp_func=cort_yield_comp_func, + cort_yield_tens_func=cort_yield_tens_func, + cort_poissons_ratio=cort_poissons_ratio, + include_cortical=include_cortical, + ), + pmma_mat_id=pmma_mat_id, + pmma_E=pmma_E, + pmma_v=pmma_v, pmma_yield_tension=pmma_yield_tension, pmma_yield_compression=pmma_yield_compression, ) - return material_table def spine_material_log_values(kwargs): @@ -147,4 +265,3 @@ def spine_material_log_values(kwargs): "cort_yield_tens_func": cort_yield_tens_func, "cort_poissons_ratio": cort_poissons_ratio, } - diff --git a/ogo/fea/model.py b/ogo/fea/model.py index 5597636..901cdad 100644 --- a/ogo/fea/model.py +++ b/ogo/fea/model.py @@ -71,6 +71,18 @@ def append_postprocessing_sets(model, set_names): return model +def find_nodes_on_coordinate_plane(model, axis, value, *, tolerance=1.0e-5): + """Return model point IDs that lie on one coordinate plane.""" + import vtk + + axis_index = {"x": 0, "y": 1, "z": 2}[str(axis).lower()] + node_ids = vtk.vtkIdTypeArray() + for point_id in range(model.GetNumberOfPoints()): + if abs(model.GetPoint(point_id)[axis_index] - value) <= tolerance: + node_ids.InsertNextValue(point_id) + return node_ids + + def write_model(model, output_path): """Write a vtkbone model to an ``.n88model`` file.""" import vtkbone @@ -170,4 +182,3 @@ def create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, ogo.message("Postprocessing...") return append_postprocessing_sets(model, [top_node_set_name, bottom_node_set_name]) - diff --git a/ogo/cli/ref/SidewaysFallFe.py b/ogo/fea/sideways_fall.py similarity index 57% rename from ogo/cli/ref/SidewaysFallFe.py rename to ogo/fea/sideways_fall.py index 361f15a..3660a41 100644 --- a/ogo/cli/ref/SidewaysFallFe.py +++ b/ogo/fea/sideways_fall.py @@ -21,6 +21,7 @@ ## # Import the required modules import ogo.util.Helper as ogo +import math import os import sys import argparse @@ -30,11 +31,43 @@ import vtk import vtkbone -from ogo.fea.femur import side_rotation, sideways_fall_output_name, tilted_side_support_vector +from ogo.fea.boundary import ( + generate_fixture_cap_vtk, + label_foreground_in_bounds_vtk, + should_smooth_resampled_mask, + smooth_binary_mask_vtk, +) +from ogo.fea.femur import ( + DEFAULT_CORTICAL_LABEL, + DEFAULT_FEMUR_FE_DISPLACEMENT, + DEFAULT_FEMUR_CUT_MODE, + DEFAULT_FEMUR_ISO_RESOLUTION_MM, + DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + DEFAULT_FEMUR_SHAFT_LENGTH_MM, + DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, + DEFAULT_PMMA_INTRUSION_MM, + DEFAULT_PMMA_THICKNESS_MM, + DEFAULT_TRABECULAR_LABEL, + DISTAL_FEMUR_NODE_SET, + FEMORAL_HEAD_FIXTURE_LONG_AXIS_EXTENSION_MM, + FEMORAL_HEAD_FIXTURE_WIDTH_EXTENSION_MM, + FEMORAL_HEAD_NODE_SET, + GREATER_TROCHANTER_NODE_SET, + SIDEWAYS_FALL_NODE_SETS, + cortical_compartment_mask, + expand_xz_footprint, + femur_z_coverage, + flat_crop_vtk_image_below_z, + side_rotation, + sideways_fall_output_name, + standardize_femur_shaft_length, + swap_xz_footprint, +) from ogo.fea.materials import build_femur_material_table -from ogo.fea.model import write_model +from ogo.fea.model import append_postprocessing_sets, find_nodes_on_coordinate_plane, write_model from ogo.util.echo_arguments import echo_arguments + def remove_extension(filename): while True: filename, ext = os.path.splitext(filename) @@ -52,25 +85,29 @@ def sidewaysFallFe(args): # Collect the input arguments image = args.calibrated_image mask = args.bone_mask + compartment_mask = args.compartment_mask mask_threshold = args.mask_threshold iso_resolution = args.iso_resolution femur_side = args.femur_side - elastic_exponent = args.elastic_exponent - elastic_Emax = args.elastic_Emax poissons_ratio = args.poissons_ratio pmma_E = args.pmma_E pmma_v = args.pmma_v pmma_thick = args.pmma_thick + pmma_intrusion = args.pmma_intrusion pmma_mat_id = args.pmma_mat_id fe_displacement = args.fe_displacement left_femur_reference = args.left_femur_reference right_femur_reference = args.right_femur_reference output_file = args.output_file - bone_yield_compression = args.bone_yield_compression - bone_yield_tension = args.bone_yield_tension pmma_yield_compression = args.pmma_yield_compression pmma_yield_tension = args.pmma_yield_tension + femur_shaft_length = args.femur_shaft_length + femur_cut_mode = args.femur_cut_mode + femur_lesser_trochanter_distal_offset = args.femur_lesser_trochanter_distal_offset + cortical_label = args.cortical_label + trabecular_label = args.trabecular_label + mask_smoothing_spacing_threshold = args.mask_smoothing_spacing_threshold @@ -105,35 +142,50 @@ def sidewaysFallFe(args): else: ogo.message("Femur side not recognized. Terminating...") sys.exit() - ogo.message("Bone Power Law Exponent: %8.4f" % elastic_exponent) - ogo.message("Bone Power Law Constant [MPa]: %8.4f" % elastic_Emax) + ogo.message("Trabecular Elastic Function: %s" % (args.elastic_E_func or "default_E")) + ogo.message("Cortical Elastic Function: %s" % (args.cort_elastic_E_func or args.elastic_E_func or "default_E")) ogo.message("Bone Poissons Ratio: %1.1f" % poissons_ratio) ogo.message("PMMA Elastic Modulus [MPa]: %8.4f" % pmma_E) ogo.message("PMMA Poissons Ration: %1.1f" % pmma_v) ogo.message("PMMA Thickness [mm]: %8.4f" % pmma_thick) + ogo.message("PMMA Intrusion [mm]: %8.4f" % pmma_intrusion) ogo.message("PMMA Material ID: %d" % pmma_mat_id) ogo.message("Applied Displacement [mm]: %8.4f" % fe_displacement) + ogo.message("Femur Cut Mode: %s" % femur_cut_mode) + if compartment_mask is not None: + ogo.message("Compartment Mask: %s" % compartment_mask) + ogo.message("Cortical Label: %d" % cortical_label) + ogo.message("Trabecular Label: %d" % trabecular_label) + if femur_cut_mode == "fixed_length": + ogo.message("Retained Proximal Femur Length [mm]: %8.4f" % femur_shaft_length) + else: + ogo.message( + "Lesser Trochanter Distal Cut Offset [mm]: %8.4f" + % femur_lesser_trochanter_distal_offset + ) ## # Read input image ogo.message("Reading calibrated image...") imageData = ogo.readNii(image) + input_spacing = imageData.GetSpacing() ## # Read bone mask ogo.message("Reading bone mask...") maskData = ogo.readNii(mask) maskThres = ogo.maskThreshold(maskData, mask_threshold) - - ## - # Resampling the images to isotropic voxel size - ogo.message("Resampling the Input image and bone mask to isotropic...") - image_resample = ogo.imageResample(imageData, iso_resolution) - mask_resample = ogo.imageResample(maskThres, iso_resolution) + compartmentData = None + if compartment_mask is not None: + ogo.message("Reading trabecular/cortical compartment mask...") + compartmentData = ogo.readNii(compartment_mask) ## # Pre-rotate the image and mask for better alignment - image_rot, mask_rot = ogo.preRotateImage(image_resample, mask_resample, rot_z) + image_rot, mask_rot = ogo.preRotateImage(imageData, maskThres, rot_z) + compartment_rot = None + if compartmentData is not None: + _, compartment_rot = ogo.preRotateImage(imageData, compartmentData, rot_z) ## # Align the input femur with the reference model @@ -150,9 +202,60 @@ def sidewaysFallFe(args): icp = ogo.iterativeClosestPoint(ref_poly, mask_surface) # ogo.message("Transform:\n %s" % str(icp)) - ogo.message("Applying the transformation to the image and mask...") - image_trans = ogo.applyTransform(image_rot, icp) - mask_trans = ogo.applyTransform(mask_rot, icp) + ogo.message("Applying the transformation and isotropic resampling to the image and mask...") + image_trans = ogo.transformResample(image_rot, icp, iso_resolution) + mask_trans = ogo.labelTransformResample(mask_rot, icp, iso_resolution) + compartment_trans = ( + ogo.labelTransformResample(compartment_rot, icp, iso_resolution) + if compartment_rot is not None + else None + ) + smooth_resampled_masks = should_smooth_resampled_mask( + input_spacing, + mask_smoothing_spacing_threshold, + ) + if smooth_resampled_masks: + ogo.message( + "smoothing resampled femur mask because input spacing " + f"{input_spacing} exceeds {mask_smoothing_spacing_threshold} mm..." + ) + mask_trans = smooth_binary_mask_vtk(mask_trans, close_iter=1, open_iter=1) + else: + ogo.message( + "skipping femur mask smoothing because input spacing " + f"{input_spacing} is <= {mask_smoothing_spacing_threshold} mm in all dimensions..." + ) + + ogo.message("Applying flat distal femur crop in reference coordinates...") + try: + image_trans, mask_trans, shaft_crop = standardize_femur_shaft_length( + image_trans, + mask_trans, + reference_bounds=ref_poly.GetBounds(), + retained_length_mm=femur_shaft_length, + cut_mode=femur_cut_mode, + lesser_trochanter_distal_offset_mm=femur_lesser_trochanter_distal_offset, + ) + except ValueError as exc: + ogo.message(str(exc)) + sys.exit(1) + retained_length_mm = shaft_crop["retained_length_mm"] + ogo.message( + "Distal cut z=%8.4f; input mask z coverage [%8.4f, %8.4f]; retained length=%8.4f" + % ( + shaft_crop["cut_z"], + shaft_crop["mask_z_min"], + shaft_crop["mask_z_max"], + retained_length_mm, + ) + ) + if femur_cut_mode == "lesser_trochanter": + ogo.message( + "Detected greater trochanter z=%8.4f; lesser trochanter z=%8.4f" + % (shaft_crop["greater_trochanter_z"], shaft_crop["lesser_trochanter_z"]) + ) + if compartment_trans is not None: + compartment_trans = flat_crop_vtk_image_below_z(compartment_trans, shaft_crop["cut_z"]) # ogo.message("Writing out temp images...") # ogo.writeNii(image_trans, "temp_image.nii", image_pathname) @@ -176,8 +279,22 @@ def sidewaysFallFe(args): # K2HPO4 den = -31 mg/cc => Ash den = 6 mg/cc => E = 0.1 MPa image_thres = ogo.bmd_preprocess(image_trans, -31) image_ash = ogo.bmd_K2hpo4ToAsh(image_thres) - - binned_image, bin_centers = ogo.density2materialID(image_ash, n_bins=128) + + cortical_mask = None + if compartment_trans is not None: + cortical_mask = cortical_compartment_mask( + compartment_trans, + cortical_label=cortical_label, + trabecular_label=trabecular_label, + ) + if smooth_resampled_masks: + ogo.message("smoothing derived cortical compartment mask...") + cortical_mask = smooth_binary_mask_vtk(cortical_mask, close_iter=1, open_iter=1) + binned_image, bin_centers = ogo.density2materialID( + image_ash, + n_bins=128, + cort_mask=cortical_mask, + ) ## # Set up the FE model. @@ -197,6 +314,23 @@ def sidewaysFallFe(args): change = ogo.changeInfo(conn) + model_z_min, model_z_max = femur_z_coverage(change) + model_cut_z = model_z_max - retained_length_mm + if model_z_min > model_cut_z: + if model_z_min - model_cut_z <= 2.0: + model_cut_z = model_z_min + else: + ogo.message( + "Femur scan does not include enough distal shaft after model-grid alignment " + "(required retained length %.1f mm, available approximately %.1f mm)." + % (retained_length_mm, max(0.0, model_z_max - model_z_min)) + ) + sys.exit(1) + change = flat_crop_vtk_image_below_z(change, model_cut_z) + ogo.message( + "Applied model-grid distal shaft cut z=%8.4f; model mask z coverage [%8.4f, %8.4f]" + % (model_cut_z, model_z_min, model_z_max) + ) # Convert image data to hexahedral elements ogo.message("Meshing image data to elements...") @@ -207,23 +341,22 @@ def sidewaysFallFe(args): ogo.message("Setting up the Finite Element Material Table...") material_table = build_femur_material_table( bin_centers, - elastic_Emax=elastic_Emax, - elastic_exponent=elastic_exponent, - bone_yield_compression=bone_yield_compression, - bone_yield_tension=bone_yield_tension, + n_bins=128, + elastic_E_func=args.elastic_E_func, + yield_comp_func=args.yield_comp_func, + yield_tens_func=args.yield_tens_func, + cort_elastic_E_func=args.cort_elastic_E_func, + cort_yield_comp_func=args.cort_yield_comp_func, + cort_yield_tens_func=args.cort_yield_tens_func, + cort_poissons_ratio=args.cort_poissons_ratio, poissons_ratio=poissons_ratio, pmma_mat_id=pmma_mat_id, pmma_E=pmma_E, pmma_v=pmma_v, pmma_yield_tension=pmma_yield_tension, pmma_yield_compression=pmma_yield_compression, + include_cortical=cortical_mask is not None, ) - - bone_yield_compression = args.bone_yield_compression - bone_yield_tension = args.bone_yield_tension - pmma_yield_compression = args.pmma_yield_compression - pmma_yield_tension = args.pmma_yield_tension - ## # Create the preliminary FE model ogo.message("Constructing the Finite Element Model...") @@ -238,8 +371,8 @@ def sidewaysFallFe(args): bottom_support_vector = (0, -1, 0) side_support_vector = (0, 0, -1) - # Tilt the distal support to match the historical sideways-fall fixture. - side_support_vector = tilted_side_support_vector(-20) + # The shaft is cut on a flat z plane, so use that cut-plane normal for the distal BC. + side_support_vector = (0, 0, -1) ogo.message("Determining Femoral Head ROI...") femoral_head_bounds = ( @@ -247,7 +380,7 @@ def sidewaysFallFe(args): float(model_bounds[1]), float(model_bounds[2]), float(model_bounds[2] + (model_bounds[3]-model_bounds[2]) / 5), - float(model_bounds[4] + (model_bounds[5]-model_bounds[4]) / 2), + float(model_bounds[4]), float(model_bounds[5]) ) ogo.message("Extracted Femoral Head Bounds: %s" % str(femoral_head_bounds)) @@ -300,45 +433,51 @@ def sidewaysFallFe(args): greater_trochanter_model_bounds[3] - greater_trochanter_model_bounds[2], greater_trochanter_model_bounds[5] - greater_trochanter_model_bounds[4] ) - ## - # Define values for adding PMMA blocks - inval = pmma_mat_id - outval = 0 # Thickness to be added to PMMA cap. - origin = image_resample.GetOrigin() - spacing = image_resample.GetSpacing() - thickness = int(pmma_thick / spacing[1]) + thickness = max(1, int(math.ceil(pmma_thick / change.GetSpacing()[1]))) + intrusion = max(0, int(math.ceil(pmma_intrusion / change.GetSpacing()[1]))) ## # Creates the femoral head pmma cap - ogo.message("Creating Femoral Head PMMA Cap...") - femoralHeadPMMA = ogo.femoralHeadPMMA( - femoral_head_model_bounds, - spacing, - origin, - inval, - outval, - thickness, - pmma_mat_id + ogo.message("Creating Femoral Head PMMA Cap from contact ROI...") + femoral_head_fixture_bounds = expand_xz_footprint( + swap_xz_footprint(femoral_head_model_bounds), + x_extension_mm=FEMORAL_HEAD_FIXTURE_WIDTH_EXTENSION_MM, + z_extension_mm=FEMORAL_HEAD_FIXTURE_LONG_AXIS_EXTENSION_MM, + ) + ogo.message("Femoral Head PMMA Fixture Bounds: %s" % str(femoral_head_fixture_bounds)) + femoral_head_contact = label_foreground_in_bounds_vtk(change, femoral_head_fixture_bounds, label_value=1) + femoralHeadPMMA = generate_fixture_cap_vtk( + femoral_head_contact, + label_value=1, + axis="y", + direction="down", + thickness=thickness, + shape="box", + intrusion=intrusion, + crop_to_contact=True, + output_value=pmma_mat_id, ) ## # Creates the greater trochanter pmma cap - ogo.message("Creating Greater Trochanter PMMA Cap...") - greaterTrochanterPMMA = ogo.greaterTrochanterPMMA( - greater_trochanter_model_bounds, - spacing, - origin, - inval, - outval, - thickness, - pmma_mat_id + ogo.message("Creating Greater Trochanter PMMA Cap from contact ROI...") + greater_trochanter_contact = label_foreground_in_bounds_vtk(change, greater_trochanter_model_bounds, label_value=1) + greaterTrochanterPMMA = generate_fixture_cap_vtk( + greater_trochanter_contact, + label_value=1, + axis="y", + direction="up", + thickness=thickness, + shape="round", + intrusion=intrusion, + output_value=pmma_mat_id, ) ## # combines the pmma cap images with the model image. ogo.message("Combine PMMA Cap Images with Model Image...") - combinedImage = ogo.combineImageData_SF(conn, femoralHeadPMMA, greaterTrochanterPMMA, pmma_mat_id) + combinedImage = ogo.combineImageData_SF(change, femoralHeadPMMA, greaterTrochanterPMMA, pmma_mat_id) ## # Mesh the final image and create Finite Element Model @@ -386,7 +525,7 @@ def sidewaysFallFe(args): ogo.message("-- found %d visible exterior nodes on Femoral Head PMMA Cap." % fh_pmma_visible_node_IDS.GetNumberOfTuples()) - fh_pmma_visible_node_IDS.SetName("Femoral_Head_PMMA_Nodes") + fh_pmma_visible_node_IDS.SetName(FEMORAL_HEAD_NODE_SET) model2.AddNodeSet(fh_pmma_visible_node_IDS) ## @@ -417,65 +556,46 @@ def sidewaysFallFe(args): ogo.message("-- found %d visible exterior nodes on Greater Trochanter PMMA Cap." % gt_pmma_visible_node_IDS.GetNumberOfTuples()) - gt_pmma_visible_node_IDS.SetName("Greater_Trochanter_PMMA_Nodes") + gt_pmma_visible_node_IDS.SetName(GREATER_TROCHANTER_NODE_SET) model2.AddNodeSet(gt_pmma_visible_node_IDS) ## - # Distal Femur (df) supprt nodes + # Distal Femur (df) support nodes. The shaft is cut on a flat z plane, so + # the fixed support is the cut face itself rather than a thick distal band. ogo.message("Determining distal femur nodes...") - distal_femur_bounds = ( - model2_bounds[0], - model2_bounds[1], - model2_bounds[2], - model2_bounds[3], - model2_bounds[4], - model2_bounds[4]+10 #This was 3, the depth of the BC search but does not capture with the slanted approach - ) - ogo.message("Distal Femur Bounds: %s" % str(distal_femur_bounds)) - df_model = ogo.extractBox(distal_femur_bounds, model2) - df_model_bounds = df_model.GetBounds() - ogo.message("Distal Femur Model Bounds: %s" % str(df_model_bounds)) - - # Get the visible nodes of the distal femur - df_visible_node_IDS = vtk.vtkIdTypeArray() - vtkbone.vtkboneNodeSetsByGeometry.FindNodesOnVisibleSurface( - df_visible_node_IDS, - df_model, - side_support_vector, - -1) + distal_cut_z = model2_bounds[4] + ogo.message("Distal Femur Cut Plane z: %8.4f" % distal_cut_z) + df_visible_node_IDS = find_nodes_on_coordinate_plane(model2, "z", distal_cut_z) ogo.message("-- found %d visible exterior nodes on Distal Femur." % df_visible_node_IDS.GetNumberOfTuples()) - df_visible_node_IDS.SetName("Distal_Femur_Nodes") + df_visible_node_IDS.SetName(DISTAL_FEMUR_NODE_SET) model2.AddNodeSet(df_visible_node_IDS) ## # Apply Boundary conditions to PMMA caps at specific sites - # - Femoral Head PMMA Cap BCs - ogo.message("Applying boundary conditions to Femoral Head PMMA cap...") + ogo.message("Applying displacement boundary condition to Femoral Head PMMA cap...") model2.ApplyBoundaryCondition( - "Femoral_Head_PMMA_Nodes", + FEMORAL_HEAD_NODE_SET, vtkbone.vtkboneConstraint.SENSE_Y, -fe_displacement, - "top_displacement") #switched from bottom_fixed_z_PMMA - - dirs = [vtkbone.vtkboneConstraint.SENSE_Y, vtkbone.vtkboneConstraint.SENSE_X, vtkbone.vtkboneConstraint.SENSE_Z] - labels = ['y','x','z'] + "top_displacement") - for SENSE, label in zip(dirs,labels): - # - Greater Trochanter PMMA Cap BCs - ogo.message("Applying boundary conditions to Greater Trochanter PMMA cap...") - model2.ApplyBoundaryCondition( - "Greater_Trochanter_PMMA_Nodes", - SENSE, - 0, - f"bottom_fixed_{label}_PMMA") #switched from top_displacement - - # - Distal Femur fixed in z-axis - ogo.message("Applying boundary conditions to Distal Femur...") + ogo.message("Constraining Greater Trochanter PMMA cap in loading direction...") + model2.ApplyBoundaryCondition( + GREATER_TROCHANTER_NODE_SET, + vtkbone.vtkboneConstraint.SENSE_Y, + 0, + "bottom_fixed_y_PMMA") + + for SENSE, label in ( + (vtkbone.vtkboneConstraint.SENSE_X, "x"), + (vtkbone.vtkboneConstraint.SENSE_Z, "z"), + ): + ogo.message("Constraining distal femur rigid-body motion...") model2.ApplyBoundaryCondition( - "Distal_Femur_Nodes", + DISTAL_FEMUR_NODE_SET, SENSE, 0, f"bottom_fixed_{label}") @@ -483,16 +603,10 @@ def sidewaysFallFe(args): ## # Post Processing parameters ogo.message("Setting up Post Processing Parameters...") - info = model2.GetInformation() - pp_node_sets_key = vtkbone.vtkboneSolverParameters.POST_PROCESSING_NODE_SETS() - pp_elem_sets_key = vtkbone.vtkboneSolverParameters.POST_PROCESSING_ELEMENT_SETS() - - for setname in ["Femoral_Head_PMMA_Nodes", "Greater_Trochanter_PMMA_Nodes", "Distal_Femur_Nodes"]: - # Specify that these sets should be used for post-processing - pp_node_sets_key.Append(info, setname) - elementSet = model2.GetAssociatedElementsFromNodeSet(setname) - model2.AddElementSet(elementSet) - pp_elem_sets_key.Append(info, setname) + append_postprocessing_sets( + model2, + SIDEWAYS_FALL_NODE_SETS, + ) model2.AppendHistory( "Created by %s version %s." % (script_name, script_version)) @@ -548,48 +662,67 @@ def main(): parser.add_argument("--mask_threshold", type = int, default = 1, help = "Set the threshold value to extract the bone of interest from the mask. (Default: %(default)s)") - parser.add_argument("--iso_resolution", type = float, default = 1.0, + parser.add_argument("--iso_resolution", type = float, default = DEFAULT_FEMUR_ISO_RESOLUTION_MM, help = "Set the isotropic voxel size [in mm]. (default: %(default)s [mm])") + parser.add_argument("--mask_smoothing_spacing_threshold", type=float, default=DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + help="Smooth resampled femur masks only when an input spacing dimension exceeds this value. (default: %(default)s [mm])") parser.add_argument("--femur_side", type = int, default = 1, help = "Set whether the left or right femur is to be analyzed. 1 = Left; 2 = Right. (default: %(default)s)") parser.add_argument("--output_path", type=str, default=None, help="Set output path for the N88 model file. (default: same as input image)") - parser.add_argument("--elastic_exponent", type=float, default=2.29, - help="Sets the exponent (b) for power law: E=A(den)^b, used in converting material IDS/density to Elastic Moduli value. (default: %(default)s)") - parser.add_argument("--elastic_Emax", type=float, default=10500, - help="Sets the coefficient (A) Elastic Modulus value for the power law: E=A(den)^b. (default: %(default)s [MPa])") parser.add_argument("--poissons_ratio", type=float, default=0.3, help="Sets the Poisson's ratio for the material(s) in the FE model. (default: %(default)s)") + parser.add_argument("--elastic_E_func", type=str, default=None, + help="Function name for trabecular bone Young's modulus. (default: default_E)") + parser.add_argument("--yield_comp_func", type=str, default=None, + help="Function name for trabecular compression yield. (default: none)") + parser.add_argument("--yield_tens_func", type=str, default=None, + help="Function name for trabecular tension yield. (default: none)") + parser.add_argument("--cort_elastic_E_func", type=str, default=None, + help="Function name for cortical bone Young's modulus. Defaults to --elastic_E_func.") + parser.add_argument("--cort_yield_comp_func", type=str, default=None, + help="Function name for cortical compression yield. Defaults to --yield_comp_func.") + parser.add_argument("--cort_yield_tens_func", type=str, default=None, + help="Function name for cortical tension yield. Defaults to --yield_tens_func.") + parser.add_argument("--cort_poissons_ratio", type=float, default=None, + help="Poisson's ratio for cortical bone. Defaults to --poissons_ratio.") parser.add_argument("--pmma_E", type=float, default=2500, help="Sets the Elastic Modulus for PMMA caps in the FE model. (default: %(default)s [MPa])") parser.add_argument("--pmma_v", type=float, default=0.3, help="Sets the Poisson's ratio for the PMMA material(s) in the FE model. (default: %(default)s)") - parser.add_argument("--pmma_thick", type=float, default=2.0, + parser.add_argument("--pmma_thick", type=float, default=DEFAULT_PMMA_THICKNESS_MM, help="Sets the minimum thickness for PMMA caps in the FE model. Default value (6 [mm]) based on observed measurement of Keaveny BCT FE modeling of the femur. (default: %(default)s [mm])") + parser.add_argument("--pmma_intrusion", type=float, default=DEFAULT_PMMA_INTRUSION_MM, + help="Sets how far PMMA fixtures intrude toward the bone side before bone overlap is preserved during material combination. (default: %(default)s [mm])") parser.add_argument("--pmma_mat_id", type=int, default=5000, help="Sets the material ID for the PMMA blocks. (default: %(default)s)") - parser.add_argument("--fe_displacement", type=float, default=-1.0, - help="Sets the applied displacement in [mm] to the FE model. (default: %(default)s [mm])") + parser.add_argument("--fe_displacement", type=float, default=DEFAULT_FEMUR_FE_DISPLACEMENT, + help="Sets the applied displacement endpoint for the sideways-fall model. The default reports the force at 4%% displacement. (default: %(default)s)") + parser.add_argument("--femur_shaft_length", type=float, default=DEFAULT_FEMUR_SHAFT_LENGTH_MM, + help="Retained proximal femur length [mm] for --femur_cut_mode fixed_length. (default: %(default)s [mm])") + parser.add_argument("--femur_cut_mode", choices=["lesser_trochanter", "fixed_length"], + default=DEFAULT_FEMUR_CUT_MODE, + help="Set the distal femur crop. Default detects the lesser trochanter and fails if the field of view is incomplete. (default: %(default)s)") + parser.add_argument("--femur_lesser_trochanter_distal_offset", type=float, default=DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, + help="Cut this many mm distal to the detected lesser trochanter in lesser_trochanter mode. (default: %(default)s [mm])") + parser.add_argument("--compartment_mask", type=str, default=None, + help="Optional trabecular/cortical compartment mask aligned with the bone mask. Defaults: cortical=1, trabecular=2.") + parser.add_argument("--cortical_label", type=int, default=DEFAULT_CORTICAL_LABEL, + help="Label value for cortical bone in --compartment_mask. (default: %(default)s)") + parser.add_argument("--trabecular_label", type=int, default=DEFAULT_TRABECULAR_LABEL, + help="Label value for trabecular bone in --compartment_mask. (default: %(default)s)") parser.add_argument("--reference_path", type=str, required=False, default=None, help="Path to the reference vtk file for ICP registration. (default: None)") parser.add_argument("--pmma_yield_compression", type=float, default=None, help="Sets the yield strength in compression for PMMA material in the FE model. (default: %(default)s [MPa])") parser.add_argument("--pmma_yield_tension", type=float, default=None, help="Sets the yield strength in tension for PMMA material in the FE model. (default: %(default)s [MPa])") - parser.add_argument("--bone_yield_compression", type=float, default=None, - help="Sets the yield strength in compression for bone material in the FE model. (default: %(default)s [MPa])") - parser.add_argument("--bone_yield_tension", type=float, default=None, - help="Sets the yield strength in tension for bone material in the FE model. (default: %(default)s [MPa])") - - # Parse and display args = parser.parse_args() # Set default reference paths - # This needs to be aligned to be a "folder" level above between Spine and Femur if args.reference_path is None: - script_dir = os.path.dirname(os.path.abspath(__file__)) - data_dir = os.path.join(script_dir, "../../dat") + data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "dat") args.left_femur_reference = os.path.join(data_dir, "LT_FEMUR_SIDEWAYS_FALL_REF.vtk") args.right_femur_reference = os.path.join(data_dir, "RT_FEMUR_SIDEWAYS_FALL_REF.vtk") else: diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 7412321..92ad93d 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -1,20 +1,73 @@ -"""Spine-specific FE helpers and spineFE benchmark presets.""" +"""Spine-specific defaults, helpers, and spineFE benchmark presets. + +Default spine workflow: +1. Read the calibrated density image and labelled vertebra mask. The high-level + wrapper uses ``--vertebra LEVEL:BODY_LABEL:PROCESS_LABEL`` so body and + posterior-process labels are explicit and traceable. +2. Crop the vertebral body, posterior process, calibrated image, and combined + vertebra mask to the same bounding box. +3. ICP-align the vertebral body to the bundled L4 compression reference. By + default the reference is PCA-scaled within 0.8,0.8,0.75 to 1.2,1.2,1.3 before + rigid ICP; users can override the scale/reference explicitly. +4. Apply the ICP transform and set the final output spacing to + 1.0 x 1.0 x 1.0 mm in one shared VTK reslice helper. Image data use cubic + interpolation; body/process labels use nearest-neighbor interpolation. +5. Smooth the transformed body/process masks with one binary close/open pass + only when at least one input spacing dimension is coarser than 2 mm. Then + generate fit-to-body PMMA caps on the superior and inferior body surfaces. + The default cap thickness is 3 mm. +6. Build materials with the same shared bone/PMMA material-table helper used by + the femur workflow. The spine convention is trabecular material IDs 1..128 + and cortical IDs 129..256; PMMA is a separate material ID. +7. Apply axial compression boundary conditions: prescribed displacement at the + superior PMMA cap toward the inferior cap and fixed displacement at the + inferior cap. The wrapper solves and reports at 0.68 percent strain by + default. +""" from pathlib import Path +DEFAULT_SPINE_ISO_RESOLUTION_MM = 1.0 +DEFAULT_SPINE_PMMA_THICKNESS_MM = 3 +DEFAULT_SPINE_PMMA_MATERIAL_ID = 5000 +DEFAULT_SPINE_POISSONS_RATIO = 0.3 +DEFAULT_SPINE_PMMA_E_MPA = 2500 +DEFAULT_SPINE_PMMA_POISSONS_RATIO = 0.3 +DEFAULT_SPINE_FE_DISPLACEMENT_MM = -1.0 +DEFAULT_SPINE_TARGET_DISPLACEMENT_PERCENT = 0.68 +DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM = 2.0 +DEFAULT_SPINE_TOP_NODE_SET_ID = 4 +DEFAULT_SPINE_BOTTOM_NODE_SET_ID = 3 +DEFAULT_SPINE_REGISTRATION_SCALE = None +DEFAULT_SPINE_REGISTRATION_MIN_SCALE = "0.8,0.8,0.75" +DEFAULT_SPINE_REGISTRATION_MAX_SCALE = "1.2,1.2,1.3" +DEFAULT_SPINE_REFERENCE_FILENAME = "L4_BODY_SPINE_COMPRESSION_REF.vtk" + +SPINE_ALIGNMENT_METHOD = "scaled ICP to reference vertebral body" + +BENCHMARK_LINEAR_FE_DISPLACEMENT_MM = -0.2 +BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM = -2.0 +BENCHMARK_PMMA_MATERIAL_ID = 300 + + +def default_spine_reference_path(): + """Return the bundled reference body used for spine ICP alignment.""" + return Path(__file__).resolve().parents[1] / "dat" / DEFAULT_SPINE_REFERENCE_FILENAME + + def benchmark_linear_params(): """Return the linear spineFE-benchmark model parameters.""" return { - "poissons_ratio": 0.3, - "pmma_mat_id": 300, - "pmma_E": 2500, - "pmma_v": 0.3, + "poissons_ratio": DEFAULT_SPINE_POISSONS_RATIO, + "pmma_mat_id": BENCHMARK_PMMA_MATERIAL_ID, + "pmma_E": DEFAULT_SPINE_PMMA_E_MPA, + "pmma_v": DEFAULT_SPINE_PMMA_POISSONS_RATIO, "top_node_set_id": 6, "bottom_node_set_id": 5, "top_direction": (0, 0, 1), "bottom_direction": (0, 0, -1), - "fe_displacement": -0.2, + "fe_displacement": BENCHMARK_LINEAR_FE_DISPLACEMENT_MM, "pmma_yield_compression": None, "pmma_yield_tension": None, "cort_poissons_ratio": None, @@ -32,7 +85,7 @@ def benchmark_nonlinear_params(): params = benchmark_linear_params() params.update( { - "fe_displacement": -2.0, + "fe_displacement": BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM, "pmma_yield_compression": 70.0, "pmma_yield_tension": 70.0, "yield_comp_func": "kopperdahl_trab_yc", @@ -66,7 +119,7 @@ def prepare_benchmark_images(input_image_path, input_mask_path, n_bins=128): """Reproduce the spineFE-benchmark notebook image and mask preparation.""" import numpy as np - from ogo.cli.ref.SpineCompressionFe import convert_image_to_material, merge_vtk_images, read + from ogo.fea.spine_compression import convert_image_to_material, merge_vtk_images, read input_image = read(str(input_image_path)).GetOutput() input_mask_with_disk = read(str(input_mask_path)).GetOutput() @@ -114,4 +167,3 @@ def find_spinefe_benchmark_dir(start_dir=None, env_var="SPINEFE_BENCHMARK_DIR"): if candidate.exists(): return candidate return None - diff --git a/ogo/cli/ref/SpineCompressionFe.py b/ogo/fea/spine_compression.py similarity index 76% rename from ogo/cli/ref/SpineCompressionFe.py rename to ogo/fea/spine_compression.py index 64b2f93..9dee1aa 100644 --- a/ogo/cli/ref/SpineCompressionFe.py +++ b/ogo/fea/spine_compression.py @@ -31,7 +31,7 @@ import argparse import os from glob import glob -import ogo.cli.ref.material_laws +import ogo.fea.material_laws import matplotlib.pyplot as plt import numpy as np @@ -41,7 +41,6 @@ from scipy.ndimage import ( binary_dilation, binary_erosion, - binary_fill_holes, find_objects, label, ) @@ -50,6 +49,27 @@ import vtkbone from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy +from ogo.fea.boundary import ( + generate_bone_cap_vtk, + should_smooth_resampled_mask, + smooth_binary_mask_vtk, +) +from ogo.fea.spine import ( + DEFAULT_SPINE_BOTTOM_NODE_SET_ID, + DEFAULT_SPINE_FE_DISPLACEMENT_MM, + DEFAULT_SPINE_ISO_RESOLUTION_MM, + DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + DEFAULT_SPINE_PMMA_E_MPA, + DEFAULT_SPINE_PMMA_MATERIAL_ID, + DEFAULT_SPINE_PMMA_POISSONS_RATIO, + DEFAULT_SPINE_PMMA_THICKNESS_MM, + DEFAULT_SPINE_POISSONS_RATIO, + DEFAULT_SPINE_REGISTRATION_MAX_SCALE, + DEFAULT_SPINE_REGISTRATION_MIN_SCALE, + DEFAULT_SPINE_REGISTRATION_SCALE, + DEFAULT_SPINE_TOP_NODE_SET_ID, + default_spine_reference_path, +) import ogo.util.Helper as ogo from ogo.util.echo_arguments import echo_arguments from scipy.ndimage import gaussian_filter @@ -57,26 +77,22 @@ vtk.vtkObject.GlobalWarningDisplayOff() + +def _as_bool(value): + if isinstance(value, bool): + return value + text = str(value).strip().lower() + if text in {"1", "true", "t", "yes", "y", "on"}: + return True + if text in {"0", "false", "f", "no", "n", "off"}: + return False + raise argparse.ArgumentTypeError(f"Invalid boolean value: {value}") + + ###################################################################### HELPERS (Python) ## General helper functions: ###################################################################### HELPERS (Python) -def get_bounding_box(binary_mask): - """ - Get the bounding box of a binary mask. - Returns a tuple of slices defining the bounding box. - """ - coords = np.array(np.where(binary_mask)) - min_coords = coords.min(axis=1) - max_coords = coords.max(axis=1) + 1 - return tuple(slice(min_coords[i], max_coords[i]) for i in range(binary_mask.ndim)) - -def getLargestCC(segmentation): - labels, _ = label(segmentation) - assert( labels.max() != 0 ) # assume at least 1 CC - largestCC = labels == np.argmax(np.bincount(labels.flat)[1:])+1 - return largestCC - def remove_extension(filename): while True: filename, ext = os.path.splitext(filename) @@ -91,6 +107,7 @@ def print_matrix(matrix): # Send the row as a single message ogo.message(row_message) + def parse_scale_triplet(value, name="scale"): if value is None: return None @@ -434,36 +451,11 @@ def get_icp(body, reference_path): print_matrix(icp.GetMatrix()) return icp -def transform_resample(image, matrix, iso_resolution): - transform = vtk.vtkTransform() - transform.SetMatrix(matrix) - transform.Update() - - reslice = vtk.vtkImageReslice() - reslice.SetInputData(image) - reslice.SetInterpolationModeToCubic() - reslice.SetResliceTransform(transform) - reslice.AutoCropOutputOn() - reslice.Update() - - # After reslice - if reslice.GetOutput().GetNumberOfPoints() == 0: +def transform_resample(image, matrix, iso_resolution, interpolation='cubic'): + output = ogo.transformResample(image, matrix, iso_resolution, interpolation=interpolation) + if output.GetNumberOfPoints() == 0: ogo.message("Reslice output contains no points. Check the input and transformation.") - - image_resample = vtk.vtkImageResample() - image_resample.SetInputData(reslice.GetOutput()) - image_resample.SetInterpolationModeToCubic() - image_resample.SetDimensionality(3) - image_resample.SetAxisOutputSpacing(0, iso_resolution) - image_resample.SetAxisOutputSpacing(1, iso_resolution) - image_resample.SetAxisOutputSpacing(2, iso_resolution) - image_resample.Update() - - # After reslice - if image_resample.GetOutput().GetNumberOfPoints() == 0: - ogo.message("Resample output contains no points. Check the input and transformation.") - - return image_resample + return output ###################################################################### DISK GENERATION ## Functions related to disk generation (image processing) @@ -527,104 +519,6 @@ def identify_boundary_surface(input_vtk_image, top_value, bottom_value): return output_vtk_image -def extrude_disk(boundary_labelled_vtk_image, value, pmma_thick=5, direction='up', axis='x'): - """ - Extend the surface along the specified axis by projecting the surface in the - remaining two axes and filling the bounding box for each slice along the chosen axis. - Values in the original image are masked out. - - Parameters: - - boundary_labelled_vtk_image: VTK image with labeled boundary surface. - - value: Integer value representing the labeled surface to extrude. - - pmma_thick: Number of slices to extrude. - - direction: Direction of extrusion ('up' or 'down'). - - axis: Axis along which to extrude ('x', 'y', 'z'). - - Returns: - - extruded_vtk_image: VTK image with the extruded and bounded surface. - """ - # Convert VTK image data to NumPy array - input_array = vtk_to_numpy(boundary_labelled_vtk_image.GetPointData().GetScalars()).reshape( - boundary_labelled_vtk_image.GetDimensions(), order='F' - ) - input_array = np.swapaxes(input_array, 0, 2) # Swap axes for correct orientation - - # Create a binary mask for the labeled surface - binary_mask = input_array == value - - # Determine bounding box of the binary mask - bb = get_bounding_box(binary_mask) - - # Adjust the bounding box based on direction and pmma_thick - axis_map = {'x': 0, 'y': 1, 'z': 2} - extrusion_axis = axis_map[axis] - bb_list = list(bb) # Convert tuple to list for modification - - if direction == 'up': - bb_list[extrusion_axis] = slice( - bb[extrusion_axis].start, - min(bb[extrusion_axis].stop + pmma_thick, binary_mask.shape[extrusion_axis]) - ) - elif direction == 'down': - bb_list[extrusion_axis] = slice( - max(bb[extrusion_axis].start - pmma_thick, 0), - bb[extrusion_axis].stop - ) - else: - raise ValueError("Direction must be 'up' or 'down'.") - - # Apply the adjusted bounding box - bb = tuple(bb_list) - ogo.message(f"Adjusted bounding box: {bb}") - - # Crop and project the mask - cropped_mask = binary_mask[bb] - cross_section = cropped_mask.any(axis=extrusion_axis) - - # Fill each slice in the cropped mask with the projection - for idx in range(cropped_mask.shape[extrusion_axis]): - if extrusion_axis == 0: # Extrusion along x - cropped_mask[idx, :, :] = cross_section - elif extrusion_axis == 1: # Extrusion along y - cropped_mask[:, idx, :] = cross_section - elif extrusion_axis == 2: # Extrusion along z - cropped_mask[:, :, idx] = cross_section - - # fill holes - pad_width = [(1, 1) if i == extrusion_axis else (0, 0) for i in range(3)] - - # Pad the binary mask with True (1) to simulate closed boundaries - padded_mask = np.pad(cropped_mask, pad_width=pad_width, mode='constant', constant_values=1) - # Fill holes in the padded mask - filled_padded = binary_fill_holes(padded_mask) - # Remove the padding to return to original size - cropped_mask = filled_padded[tuple(slice(1, -1) if i == extrusion_axis else slice(None) for i in range(3))] - - # Create the output array and mask out original values - output_array = np.zeros_like(input_array, dtype=np.uint16) - output_array[bb] = cropped_mask - output_array[input_array != 0] = False - largestcc = getLargestCC(output_array) - # Convert back to VTK format - final_array = np.swapaxes(largestcc, 0, 2) - - # Adjust VTK metadata - origin = list(boundary_labelled_vtk_image.GetOrigin()) - spacing = boundary_labelled_vtk_image.GetSpacing() - dimensions = list(final_array.shape) - - vtk_array = numpy_to_vtk(final_array.ravel(order='F').astype(np.uint16), deep=True) - - - # Convert back to VTK format - extruded_vtk_image = boundary_labelled_vtk_image.NewInstance() - extruded_vtk_image.SetDimensions(dimensions) - extruded_vtk_image.SetOrigin(origin) - extruded_vtk_image.SetSpacing(spacing) - extruded_vtk_image.GetPointData().SetScalars(vtk_array) - - return extruded_vtk_image - ###################################################################### MICRO-FE (FAIM) ## Functions related to micro-FE model ###################################################################### MICRO-FE (FAIM) @@ -695,13 +589,15 @@ def generate_cortical_mask(image_vtk, mask_vtk, threshold=0.2, min_th=1, max_th= def convert_image_to_material(image, mask, n_bins=128, cort_mask=None): change = ogo.changeInfo(image) + mask_change = ogo.changeInfo(mask) + cort_mask_change = ogo.changeInfo(cort_mask) if cort_mask is not None else None connected_image = ogo.imageConnectivity(change) thr_image = ogo.bmd_preprocess(connected_image, -31) #this was done previously - but if someone wants this they should just do it in the calib functions #ash_image = ogo.bmd_K2hpo4ToAsh(thr_image) cast_image = ogo.cast2short(thr_image) - bone_image = ogo.applyMask(cast_image, mask) - binned_image, bin_centers = ogo.density2materialID(bone_image, n_bins=n_bins, cort_mask=cort_mask) + bone_image = ogo.applyMaskByArray(cast_image, mask_change) + binned_image, bin_centers = ogo.density2materialID(bone_image, n_bins=n_bins, cort_mask=cort_mask_change) return binned_image, bin_centers @@ -745,118 +641,6 @@ def create_microfe_model( image_with_pads, boundary_masks_with_pads, bin_centers, **kwargs ) -def create_microfe_model_depreciated( - image_with_pads, - boundary_masks_with_pads, - bin_centers, - **kwargs -): - """ - Creates a micro-finite element model with optional customization via kwargs. - - Parameters: - image_with_pads (array): Padded image input. - boundary_masks_with_pads (array): Padded boundary masks. - **kwargs: Optional finite element parameters with defaults: - poissons_ratio (float): Poisson's ratio. Default is 0.3. - elastic_Emax (float): Maximum elastic modulus. Default is 10500. - elastic_exponent (float): Elastic exponent. Default is 2.29. - pmma_mat_id (int): Material ID for PMMA. Default is 5000. - pmma_E (float): Elastic modulus of PMMA. Default is 2500. - pmma_v (float): Poisson's ratio of PMMA. Default is 0.3. - top_displacement (str): Constraint name for top displacement. Default is "top_displacement". - top_direction (tuple): Direction for top node visibility. Default is (0, 0, 1). - bottom_direction (tuple): Direction for bottom node visibility. Default is (0, 0, -1). - top_node_set_id (int): Node set ID for the top. Default is 4. - bottom_node_set_id (int): Node set ID for the bottom. Default is 3. - top_node_set_name (str): Name of the top node set. Default is "body_top". - bottom_node_set_name (str): Name of the bottom node set. Default is "body_bottom". - - Returns: - model: The constructed finite element model. - """ - # Default parameters - poissons_ratio = kwargs.get("poissons_ratio", 0.3) - elastic_Emax = kwargs.get("elastic_Emax", 10500) - elastic_exponent = kwargs.get("elastic_exponent", 2.29) - pmma_mat_id = kwargs.get("pmma_mat_id", 5000) - pmma_E = kwargs.get("pmma_E", 2500) - pmma_v = kwargs.get("pmma_v", 0.3) - top_displacement = kwargs.get("top_displacement", "top_displacement") - top_direction = kwargs.get("top_direction", (0, 0, 1)) - bottom_direction = kwargs.get("bottom_direction", (0, 0, -1)) - top_node_set_id = kwargs.get("top_node_set_id", 4) - bottom_node_set_id = kwargs.get("bottom_node_set_id", 3) - top_node_set_name = kwargs.get("top_node_set_name", "body_top") - bottom_node_set_name = kwargs.get("bottom_node_set_name", "body_bottom") - bone_yield_compression = kwargs.get("bone_yield_compression", None) - bone_yield_tension = kwargs.get("bone_yield_tension", None) - pmma_yield_compression = kwargs.get("pmma_yield_compression", None) - pmma_yield_tension = kwargs.get("pmma_yield_tension", None) - cort_elastic_Emax = elastic_Emax if kwargs.get('cort_elastic_Emax') is None else kwargs['cort_elastic_Emax'] - cort_elastic_exponent = elastic_exponent if kwargs.get('cort_elastic_exponent') is None else kwargs['cort_elastic_exponent'] - cort_poissons_ratio = poissons_ratio if kwargs.get('cort_poissons_ratio') is None else kwargs['cort_poissons_ratio'] - cort_yield_compression = bone_yield_compression if kwargs.get('cort_yield_compression') is None else kwargs['cort_yield_compression'] - cort_yield_tension = bone_yield_tension if kwargs.get('cort_yield_tension') is None else kwargs['cort_yield_tension'] - - n_bins = 128 - print(kwargs) - - print(cort_elastic_exponent) - - # Main logic - ogo.message(f"Casting to Short Integer datatype...") - image_pads_short = ogo.cast2short(image_with_pads) - - ogo.message(f"Filtering connected components...") - conn = ogo.imageConnectivity(image_pads_short) - conn_bc = ogo.imageConnectivity(boundary_masks_with_pads) - - ogo.message(f"Meshing...") - mesh = ogo.Image2Mesh(conn) - temp_bc_mesh = ogo.Image2Mesh(conn_bc) - - ogo.message("Setting up the Finite Element Material Table...") - material_table = vtkbone.vtkboneMaterialTable() - - material_table = ogo.add_bone_material(material_table, bin_centers, elastic_Emax=elastic_Emax, elastic_exponent=elastic_exponent, - mu=poissons_ratio,bone_yield_compression=bone_yield_compression, bone_yield_tension=bone_yield_tension, bin_range=(0,n_bins), material_name='TrabBone') - - - ogo.message('Cortical Bone Material Table...') - material_table = ogo.add_bone_material(material_table, bin_centers, elastic_Emax=cort_elastic_Emax, elastic_exponent=cort_elastic_exponent, - mu=cort_poissons_ratio,bone_yield_compression=cort_yield_compression, bone_yield_tension=cort_yield_tension, bin_range=(n_bins, 2*n_bins), material_name='CortBone') - - - - material_table = ogo.add_pmma_material(material_table, pmma_mat_id, pmma_E, pmma_v,pmma_yield_tension=pmma_yield_tension, pmma_yield_compression=pmma_yield_compression) - - - ogo.message("Constructing the Finite Element Model...") - model = ogo.applyTestBase(mesh, material_table) - model.ComputeBounds() - - ogo.message(f"Identifying boundary nodes...") - model = find_and_add_visible_nodes(model, temp_bc_mesh, top_direction, top_node_set_id, top_node_set_name) - model = find_and_add_visible_nodes(model, temp_bc_mesh, bottom_direction, bottom_node_set_id, bottom_node_set_name) - model = apply_boundary_conditions(model,**kwargs) - - ogo.message(f"Setting convergence criteria...") - model.ConvergenceSetFromConstraint(top_displacement) - - ogo.message('Postprocessing...') - info = model.GetInformation() - pp_node_sets_key = vtkbone.vtkboneSolverParameters.POST_PROCESSING_NODE_SETS() - pp_elem_sets_key = vtkbone.vtkboneSolverParameters.POST_PROCESSING_ELEMENT_SETS() - for setname in [top_node_set_name, bottom_node_set_name]: - pp_node_sets_key.Append(info, setname) - elementSet = model.GetAssociatedElementsFromNodeSet(setname) - model.AddElementSet(elementSet) - pp_elem_sets_key.Append(info, setname) - - return model - - ###################################################################### QUALITY CONTROL ## Functions to check image and boundary conditions ###################################################################### QUALITY CONTROL @@ -913,14 +697,16 @@ def visualize_slice(image, filepath): normalized_slice = (clipped_slice - clipped_slice.min()) / (clipped_slice.max() - clipped_slice.min() + 1e-8) # Plot the normalized slice - plt.figure(figsize=(10, 10)) + plt.figure(figsize=(4.0, 4.0)) plt.imshow(normalized_slice, cmap='viridis', origin="lower") - plt.title('Middle X Slice (Clipped and Normalized)') - plt.xlabel('Y axis') - plt.ylabel('Z axis') + plt.title('Middle X', fontsize=9, pad=2) + plt.xlabel('Y', fontsize=8) + plt.ylabel('Z', fontsize=8) + plt.tick_params(axis='both', which='major', labelsize=8, length=2.5, pad=1.5) # Save the output image - plt.savefig(filepath) + plt.tight_layout(pad=0.2) + plt.savefig(filepath, dpi=300) plt.close() ogo.message(f"Slice image saved to {filepath}") @@ -1090,20 +876,25 @@ def get_mask_np(vtk_image): def process_vertebra(input_mask, input_image, n88model_output_path, body_label, process_label, reference_path, **kwargs): pmma_mat_id = kwargs.get("pmma_mat_id", 5000) - iso_resolution = kwargs.get("iso_resolution", 1.0) - pmma_thick = kwargs.get("pmma_thick", 5) - top_node_set_id = kwargs.get("top_node_set_id", 4) - bottom_node_set_id = kwargs.get("bottom_node_set_id", 3) + iso_resolution = kwargs.get("iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM) + pmma_thick = kwargs.get("pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM) + top_node_set_id = kwargs.get("top_node_set_id", DEFAULT_SPINE_TOP_NODE_SET_ID) + bottom_node_set_id = kwargs.get("bottom_node_set_id", DEFAULT_SPINE_BOTTOM_NODE_SET_ID) quality_control = kwargs.get("quality_control", True) - registration_scale = kwargs.get("registration_scale", None) - registration_min_scale = kwargs.get("registration_min_scale", "0.8,0.8,0.75") - registration_max_scale = kwargs.get("registration_max_scale", "1.2,1.2,1.3") + registration_scale = kwargs.get("registration_scale", DEFAULT_SPINE_REGISTRATION_SCALE) + registration_min_scale = kwargs.get("registration_min_scale", DEFAULT_SPINE_REGISTRATION_MIN_SCALE) + registration_max_scale = kwargs.get("registration_max_scale", DEFAULT_SPINE_REGISTRATION_MAX_SCALE) + mask_smoothing_spacing_threshold = kwargs.get( + "mask_smoothing_spacing_threshold", + DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + ) #Read Image and Mask ogo.message(f"Reading image...: {input_image}") image_reader = read(input_image) ogo.message(f"Reading mask...: {input_mask}") mask_reader = resample_to_match(image_reader.GetOutput(), read(input_mask).GetOutput()) + input_spacing = image_reader.GetOutput().GetSpacing() # Get and Check labels ogo.message(f"Checking if labels {body_label} (body) and {process_label} (process) are present...") @@ -1131,14 +922,37 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, ) # Transform Images and resample at the same time (less interpolation) - transformed_vertebra = transform_resample(isolated_vertebra.GetOutput(), icp.GetMatrix(), iso_resolution) - transformed_process = transform_resample(isolated_process.GetOutput(), icp.GetMatrix(), iso_resolution) + transformed_vertebra = transform_resample( + isolated_vertebra.GetOutput(), + icp.GetMatrix(), + iso_resolution, + interpolation='nearest', + ) + transformed_process = transform_resample( + isolated_process.GetOutput(), + icp.GetMatrix(), + iso_resolution, + interpolation='nearest', + ) transformed_image = transform_resample(isolated_image.GetOutput(), icp.GetMatrix(), iso_resolution) + if should_smooth_resampled_mask(input_spacing, mask_smoothing_spacing_threshold): + ogo.message( + "smoothing resampled body/process masks because input spacing " + f"{input_spacing} exceeds {mask_smoothing_spacing_threshold} mm..." + ) + transformed_vertebra = smooth_binary_mask_vtk(transformed_vertebra, close_iter=1, open_iter=1) + transformed_process = smooth_binary_mask_vtk(transformed_process, close_iter=1, open_iter=1) + else: + ogo.message( + "skipping body/process mask smoothing because input spacing " + f"{input_spacing} is <= {mask_smoothing_spacing_threshold} mm in all dimensions..." + ) + ogo.message(f"relabelling mask and identifying boundary surfaces...") # Assuming mask1_data and mask2_data are your initial binary masks - mask1_labeled = label_mask(transformed_vertebra.GetOutput(), 2) # Apply label 1 to the first mask - mask2_labeled = label_mask(transformed_process.GetOutput(), 1) # Apply label 2 to the second mask + mask1_labeled = label_mask(transformed_vertebra, 2) # Apply label 1 to the first mask + mask2_labeled = label_mask(transformed_process, 1) # Apply label 2 to the second mask # Combine masks here (otherwise it does weird interpolations) combined_masks = add_masks(mask1_labeled.GetOutput(), mask2_labeled.GetOutput()) # Combine the labeled masks @@ -1149,7 +963,7 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, ogo.message("creating cortical mask....") #1 - 3 alterantive (to create a continous cortical shell - changed to 0 5 ) - cort_mask = generate_cortical_mask(transformed_image.GetOutput(), combined_masks.GetOutput(), threshold=1, min_th=2, max_th=5) + cort_mask = generate_cortical_mask(transformed_image, combined_masks.GetOutput(), threshold=1, min_th=2, max_th=5) #writer = vtk.vtkXMLImageDataWriter() #writer.SetFileName('/home/matthias.walle/work/fem/CT_FE_TEMPLATE/MODELS/10001_QCT_vertebra_20_cortmask.vti') @@ -1159,21 +973,24 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, ogo.message(f"converting image to material ID...") # This converts the raw image to a material ID mapped image n_bins = 128 - bone_image, bin_centers = convert_image_to_material(transformed_image.GetOutput(), combined_masks.GetOutput(), n_bins=n_bins, cort_mask = cort_mask) + bone_image, bin_centers = convert_image_to_material(transformed_image, combined_masks.GetOutput(), n_bins=n_bins, cort_mask = cort_mask) ogo.message(f"padding images...") # Pad images before we add the disks (otherwise they may not have right pmma_thick) padded_image = pad_vtk_image(bone_image, axis='x', pmma_thick=pmma_thick, pad_value=0) - padded_transformed_image = pad_vtk_image(transformed_image.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) + padded_transformed_image = pad_vtk_image(transformed_image, axis='x', pmma_thick=pmma_thick, pad_value=0) padded_mask = pad_vtk_image(boundary_masks, axis='x', pmma_thick=pmma_thick, pad_value=0) padded_cort_mask = pad_vtk_image(cort_mask, axis='x', pmma_thick=pmma_thick, pad_value=0) padded_mask1 = pad_vtk_image(mask1_labeled.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) padded_mask2 = pad_vtk_image(mask2_labeled.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) - ogo.message(f"extruding boundary diks (pmma_thick > {pmma_thick} mm)...") - # Generate disks for the top and bottom surfaces (this could be easily changed to have no disk) - inferior_disk = extrude_disk(padded_mask, value=bottom_node_set_id, pmma_thick=pmma_thick, direction='down', axis='x') - superior_disk = extrude_disk(padded_mask, value=top_node_set_id, pmma_thick=pmma_thick, direction='up', axis='x') + ogo.message(f"extruding body-cap disks (pmma_thick > {pmma_thick} mm)...") + inferior_disk = generate_bone_cap_vtk( + padded_mask1, label_value=2, thickness=pmma_thick, direction='down', axis='x', shape='fit' + ) + superior_disk = generate_bone_cap_vtk( + padded_mask1, label_value=2, thickness=pmma_thick, direction='up', axis='x', shape='fit' + ) ogo.message(f"merging disks with image...") # Now we create the image with the disks and the boundary image as well @@ -1264,37 +1081,39 @@ def main(): help="Label value for the vertebral process in the bone mask.") parser.add_argument("--output_path", type=str, default=None, help="Set output path for the N88 model file. (default: same as input image)") - parser.add_argument("--quality_control", type=bool, default=True, + parser.add_argument("--quality_control", type=_as_bool, default=True, help="Set quality control flag (visualise output and add volume checks). (default: %(default)s)") - parser.add_argument("--iso_resolution", type=float, default=1.0, + parser.add_argument("--iso_resolution", type=float, default=DEFAULT_SPINE_ISO_RESOLUTION_MM, help="Set the isotropic voxel size [in mm]. (default: %(default)s [mm])") + parser.add_argument("--mask_smoothing_spacing_threshold", type=float, default=DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + help="Smooth resampled body/process masks only when an input spacing dimension exceeds this value. (default: %(default)s [mm])") #parser.add_argument("--elastic_exponent", type=float, default=2.29, # help="Sets the exponent (b) for power law: E=A(den)^b. (default: %(default)s)") #parser.add_argument("--elastic_Emax", type=float, default=10500, # help="Sets the coefficient (A) Elastic Modulus value for the power law: E=A(den)^b. (default: %(default)s [MPa])") - parser.add_argument("--poissons_ratio", type=float, default=0.3, + parser.add_argument("--poissons_ratio", type=float, default=DEFAULT_SPINE_POISSONS_RATIO, help="Sets the Poisson's ratio for the material(s) in the FE model. (default: %(default)s)") - parser.add_argument("--pmma_E", type=float, default=2500, + parser.add_argument("--pmma_E", type=float, default=DEFAULT_SPINE_PMMA_E_MPA, help="Sets the Elastic Modulus for PMMA caps in the FE model. (default: %(default)s [MPa])") - parser.add_argument("--pmma_v", type=float, default=0.3, + parser.add_argument("--pmma_v", type=float, default=DEFAULT_SPINE_PMMA_POISSONS_RATIO, help="Sets the Poisson's ratio for the PMMA material(s) in the FE model. (default: %(default)s)") - parser.add_argument("--pmma_thick", type=int, default=3, + parser.add_argument("--pmma_thick", type=int, default=DEFAULT_SPINE_PMMA_THICKNESS_MM, help="Sets the minimum pmma_thick for PMMA caps in the FE model. (default: %(default)s [mm])") - parser.add_argument("--pmma_mat_id", type=int, default=5000, + parser.add_argument("--pmma_mat_id", type=int, default=DEFAULT_SPINE_PMMA_MATERIAL_ID, help="Sets the material ID for the PMMA blocks. (default: %(default)s)") - parser.add_argument("--fe_displacement", type=float, default=-1.0, + parser.add_argument("--fe_displacement", type=float, default=DEFAULT_SPINE_FE_DISPLACEMENT_MM, help="Sets the applied displacement in [mm] to the FE model. (default: %(default)s [mm])") parser.add_argument("--reference_path", type=str, required=False, default=None, help="Path to the reference vtk file for ICP registration. (default: None)") - parser.add_argument("--registration_scale", type=str, default=None, + parser.add_argument("--registration_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_SCALE, help="Manual affine scale applied to the reference body before ICP. Use a scalar or sx,sy,sz. If omitted, PCA-based scaling is used.") - parser.add_argument("--registration_min_scale", type=str, default="0.8,0.8,0.75", + parser.add_argument("--registration_min_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_MIN_SCALE, help="Minimum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") - parser.add_argument("--registration_max_scale", type=str, default="1.2,1.2,1.3", + parser.add_argument("--registration_max_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_MAX_SCALE, help="Maximum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") - parser.add_argument("--top_node_set_id", type=int, default=4, + parser.add_argument("--top_node_set_id", type=int, default=DEFAULT_SPINE_TOP_NODE_SET_ID, help="ID for the top node set. (default: %(default)s)") - parser.add_argument("--bottom_node_set_id", type=int, default=3, + parser.add_argument("--bottom_node_set_id", type=int, default=DEFAULT_SPINE_BOTTOM_NODE_SET_ID, help="ID for the bottom node set. (default: %(default)s)") parser.add_argument("--pmma_yield_compression", type=float, default=None, help="Sets the yield strength in compression for PMMA material in the FE model. (default: %(default)s [MPa])") @@ -1356,9 +1175,7 @@ def main(): # Set default reference path if not provided reference_path = args.reference_path if reference_path is None: - script_dir = os.path.dirname(os.path.abspath(__file__)) - data_dir = os.path.join(script_dir, "../../dat") - reference_path = os.path.join(data_dir, "L4_BODY_SPINE_COMPRESSION_REF.vtk") + reference_path = str(default_spine_reference_path()) # Extract all kwargs kwargs = vars(args).copy() # Convert parsed arguments to a dictionary diff --git a/ogo/util/Helper.py b/ogo/util/Helper.py index 38190da..9045dc1 100644 --- a/ogo/util/Helper.py +++ b/ogo/util/Helper.py @@ -30,7 +30,7 @@ from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtk from collections import OrderedDict import ogo.dat.OgoMasterLabels as lb -import ogo.cli.ref.material_laws +import ogo.fea.material_laws from ogo.calib.internal_calibration import InternalCalibration start_time = time.time() @@ -414,6 +414,21 @@ def applyMask(imageData, maskData): return mask.GetOutput() +def applyMaskByArray(imageData, maskData): + """Apply a mask by scalar array index for images already on the same grid.""" + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + + image_scalars = vtk_image_to_numpy(imageData).ravel(order="F") + mask_scalars = vtk_image_to_numpy(maskData).ravel(order="F") + if image_scalars.shape != mask_scalars.shape: + raise ValueError( + "Cannot apply array mask because image and mask scalar arrays have different sizes " + f"({image_scalars.shape} vs {mask_scalars.shape})." + ) + masked_values = np.where(mask_scalars > 0, image_scalars, 0) + return numpy_to_vtk_image(masked_values, imageData) + + def applyTestBase(mesh, material_table): """Constructs to the FEM object. The first argument is the Image Mesh. @@ -428,7 +443,7 @@ def applyTestBase(mesh, material_table): return generator.GetOutput() -def applyTransform(vtk_image, matrix): +def applyTransform(vtk_image, matrix, interpolation="cubic"): """Applies the transform matrix to the image. The first argument is the vtk image data. The second argument is the 4x4 rotation matrix. @@ -440,7 +455,10 @@ def applyTransform(vtk_image, matrix): reslice = vtk.vtkImageReslice() reslice.SetInputData(vtk_image) - reslice.SetInterpolationModeToCubic() + if interpolation == "nearest": + reslice.SetInterpolationModeToNearestNeighbor() + else: + reslice.SetInterpolationModeToCubic() reslice.SetResliceTransform(transform) reslice.AutoCropOutputOn() reslice.Update() @@ -448,6 +466,44 @@ def applyTransform(vtk_image, matrix): return reslice.GetOutput() +def applyLabelTransform(vtk_image, matrix): + """Apply a transform to a label image using nearest-neighbor interpolation.""" + return applyTransform(vtk_image, matrix, interpolation="nearest") + + +def transformResample(vtk_image, matrix, isotropic_voxel_size, interpolation="cubic"): + """Apply a transform and set isotropic output spacing in one VTK reslice.""" + transform = vtk.vtkTransform() + transform.SetMatrix(matrix) + transform.Update() + + reslice = vtk.vtkImageReslice() + reslice.SetInputData(vtk_image) + if interpolation == "nearest": + reslice.SetInterpolationModeToNearestNeighbor() + else: + reslice.SetInterpolationModeToCubic() + reslice.SetResliceTransform(transform) + reslice.SetOutputSpacing( + float(isotropic_voxel_size), + float(isotropic_voxel_size), + float(isotropic_voxel_size), + ) + reslice.AutoCropOutputOn() + reslice.Update() + return reslice.GetOutput() + + +def labelTransformResample(vtk_image, matrix, isotropic_voxel_size): + """Apply a transform and isotropic resampling to a label image.""" + return transformResample( + vtk_image, + matrix, + isotropic_voxel_size, + interpolation="nearest", + ) + + def bmd_K2hpo4ToAsh(vtk_image): """Converts K2HPO4 density to ash density using equation from: K2HPO4 density to ASH density relationship from Keyak et al. J Biomed Mater Res @@ -649,6 +705,15 @@ def cast2unsignchar(vtk_image): return cast.GetOutput() +def cast2sameScalarType(vtk_image, template_image): + """Cast image data to match a template image scalar type.""" + cast = vtk.vtkImageCast() + cast.SetInputData(vtk_image) + cast.SetOutputScalarType(template_image.GetScalarType()) + cast.Update() + return cast.GetOutput() + + def changeInfo(vtk_image): """Changes the origin of image to 0,0,0""" change = vtk.vtkImageChangeInformation() @@ -673,23 +738,25 @@ def combineImageData_SF(image, fh_pmma_id_pad, gt_pmma_id_pad, pmma_mat_id): fh_pad.SetOutputWholeExtent(image.GetExtent()) fh_pad.SetConstant(0) fh_pad.Update() + fh_pad_image = cast2sameScalarType(fh_pad.GetOutput(), image) gt_pad = vtk.vtkImageConstantPad() gt_pad.SetInputData(gt_pmma_id_pad) gt_pad.SetOutputWholeExtent(image.GetExtent()) gt_pad.SetConstant(0) gt_pad.Update() + gt_pad_image = cast2sameScalarType(gt_pad.GetOutput(), image) message("Combining PMMA Caps with Image Data...") fh_logic = vtk.vtkImageLogic() - fh_logic.SetInput1Data(fh_pad.GetOutput()) + fh_logic.SetInput1Data(fh_pad_image) fh_logic.SetInput2Data(image) fh_logic.SetOperationToAnd() fh_logic.SetOutputTrueValue(pmma_mat_id) fh_logic.Update() fh_math = vtk.vtkImageMathematics() - fh_math.SetInput1Data(fh_pad.GetOutput()) + fh_math.SetInput1Data(fh_pad_image) fh_math.SetInput2Data(fh_logic.GetOutput()) fh_math.SetOperationToSubtract() fh_math.Update() @@ -701,14 +768,14 @@ def combineImageData_SF(image, fh_pmma_id_pad, gt_pmma_id_pad, pmma_mat_id): combo_image.Update() gt_logic = vtk.vtkImageLogic() - gt_logic.SetInput1Data(gt_pad.GetOutput()) + gt_logic.SetInput1Data(gt_pad_image) gt_logic.SetInput2Data(image) gt_logic.SetOperationToAnd() gt_logic.SetOutputTrueValue(pmma_mat_id) gt_logic.Update() gt_math = vtk.vtkImageMathematics() - gt_math.SetInput1Data(gt_pad.GetOutput()) + gt_math.SetInput1Data(gt_pad_image) gt_math.SetInput2Data(gt_logic.GetOutput()) gt_math.SetOperationToSubtract() gt_math.Update() @@ -748,23 +815,25 @@ def combineImageData_VC(image, sup_pmma_id_pad, inf_pmma_id_pad, pmma_mat_id): sup_pad.SetOutputWholeExtent(image.GetExtent()) sup_pad.SetConstant(0) sup_pad.Update() + sup_pad_image = cast2sameScalarType(sup_pad.GetOutput(), image) inf_pad = vtk.vtkImageConstantPad() inf_pad.SetInputData(inf_pmma_id_pad) inf_pad.SetOutputWholeExtent(image.GetExtent()) inf_pad.SetConstant(0) inf_pad.Update() + inf_pad_image = cast2sameScalarType(inf_pad.GetOutput(), image) message("Combining PMMA Caps with Image Data...") sup_logic = vtk.vtkImageLogic() - sup_logic.SetInput1Data(sup_pad.GetOutput()) + sup_logic.SetInput1Data(sup_pad_image) sup_logic.SetInput2Data(image) sup_logic.SetOperationToAnd() sup_logic.SetOutputTrueValue(pmma_mat_id) sup_logic.Update() sup_math = vtk.vtkImageMathematics() - sup_math.SetInput1Data(sup_pad.GetOutput()) + sup_math.SetInput1Data(sup_pad_image) sup_math.SetInput2Data(sup_logic.GetOutput()) sup_math.SetOperationToSubtract() sup_math.Update() @@ -776,14 +845,14 @@ def combineImageData_VC(image, sup_pmma_id_pad, inf_pmma_id_pad, pmma_mat_id): combo_image.Update() inf_logic = vtk.vtkImageLogic() - inf_logic.SetInput1Data(inf_pad.GetOutput()) + inf_logic.SetInput1Data(inf_pad_image) inf_logic.SetInput2Data(image) inf_logic.SetOperationToAnd() inf_logic.SetOutputTrueValue(pmma_mat_id) inf_logic.Update() inf_math = vtk.vtkImageMathematics() - inf_math.SetInput1Data(inf_pad.GetOutput()) + inf_math.SetInput1Data(inf_pad_image) inf_math.SetInput2Data(inf_logic.GetOutput()) inf_math.SetOperationToSubtract() inf_math.Update() @@ -1179,7 +1248,7 @@ def imageConnectivity(vtk_image): return conn.GetOutput() -def imageResample(vtk_image, isotropic_voxel_size): +def imageResample(vtk_image, isotropic_voxel_size, interpolation="cubic"): """Resample the input vtk image to isotropic voxel size as specified. The first argument is the input vtk Image Data. The second argument is the isotropic output voxel size. @@ -1187,7 +1256,10 @@ def imageResample(vtk_image, isotropic_voxel_size): """ image_resample = vtk.vtkImageResample() image_resample.SetInputData(vtk_image) - image_resample.SetInterpolationModeToCubic() + if interpolation == "nearest": + image_resample.SetInterpolationModeToNearestNeighbor() + else: + image_resample.SetInterpolationModeToCubic() image_resample.SetDimensionality(3) image_resample.SetAxisOutputSpacing(0, isotropic_voxel_size) image_resample.SetAxisOutputSpacing(1, isotropic_voxel_size) @@ -1196,6 +1268,11 @@ def imageResample(vtk_image, isotropic_voxel_size): return image_resample.GetOutput() +def labelImageResample(vtk_image, isotropic_voxel_size): + """Resample a label image using nearest-neighbor interpolation.""" + return imageResample(vtk_image, isotropic_voxel_size, interpolation="nearest") + + def isotropicResampling(image, iso_resolution, image_type): """An alternative to imageResample that uses SimpleITK and avoids aliasing""" @@ -1477,13 +1554,18 @@ def density2materialID(density_image, n_bins=128, cort_mask=None): vtkImageData: Binned image with material IDs (0 for background, 1 to n_bins for other densities). np.ndarray: Bin center values representing the densities for each bin. """ + from ogo.util.vtk_image import numpy_to_vtk_image + from ogo.util.vtk_image import vtk_image_to_numpy + # Extract the density values as a numpy array - density_array = vtk_to_numpy(density_image.GetPointData().GetScalars()) + density_array = vtk_image_to_numpy(density_image).ravel(order="F") # Preserve the background (values equal to 0) background_mask = density_array == 0 # Determine the density range, excluding background density_nonzero = density_array[~background_mask] + if density_nonzero.size == 0: + raise ValueError("Cannot bin material IDs because the masked density image is empty.") density_min = np.min(density_nonzero) density_max = np.max(density_nonzero) @@ -1499,15 +1581,12 @@ def density2materialID(density_image, n_bins=128, cort_mask=None): binned_values[background_mask] = 0 if cort_mask is not None: - cort_array = vtk_to_numpy(cort_mask.GetPointData().GetScalars()) + cort_array = vtk_image_to_numpy(cort_mask).ravel(order="F") binned_values[cort_array > 0] += n_bins bin_centers = np.concatenate([bin_centers, bin_centers]) # Duplicate bin centers for cortical region - # Convert the binned values back to a VTK array - binned_vtk = numpy_to_vtk(binned_values, deep=True) - binned_image = vtk.vtkImageData() - binned_image.DeepCopy(density_image) - binned_image.GetPointData().SetScalars(binned_vtk) + # Convert the binned values back to a VTK image while preserving geometry. + binned_image = numpy_to_vtk_image(binned_values, density_image) return binned_image, bin_centers @@ -1596,64 +1675,6 @@ def add_bone_material( f"Yield Strength (Comp):{yc} MPa; Yield Strength (Tens):{yt} MPa") return material_table -def add_bone_material_depreciated(material_table, bin_centers, elastic_Emax=10500, elastic_exponent=2.29, mu=0.3, - bone_yield_compression=None, bone_yield_tension=None, bin_range=None, material_name='BoneMat'): - """ - Adds Mohr-Coulomb elastoplastic bone material properties to a VTK material table based on density bins. - - Parameters: - material_table (vtkbone.vtkboneMaterialTable): The material table to populate. - bin_centers (np.ndarray): Array of density values (in mg/ccm) for each bin. - elastic_Emax (float): Maximum elastic modulus (MPa). - elastic_exponent (float): Exponent for power-law calculation of modulus. - mu (float): Constant Poisson's ratio for all materials. - yield_strength_compressive (float): Yield strength (MPa) for compression. - yield_strength_tensile (float): Yield strength (MPa) for tension. - - Returns: - vtkbone.vtkboneMaterialTable: The updated material table with Mohr-Coulomb bone properties. - """ - - # Apply bin range filtering if provided - if bin_range is not None: - if len(bin_range) != 2: - raise ValueError("bin_range must be a list or tuple with exactly two values [min_bin_idx, max_bin_idx].") - - min_bin_idx, max_bin_idx = bin_range - if min_bin_idx < 0 or max_bin_idx > len(bin_centers): - raise ValueError(f"bin_range indices must be within [0, {len(bin_centers)}]") - - # Convert to zero-based indexing for slicing - bin_centers = bin_centers[min_bin_idx : max_bin_idx] - - # Loop through each density bin - for i, density in enumerate(bin_centers, start=1): - # Calculate Young's modulus using the power-law formula (Converting density to g/ccm) - E = elastic_Emax * ((density / 1000) ** elastic_exponent) - - print(f"ID:{i+bin_range[0]}; Density(g/cc):{density/1000:.4f}; Modulus(MPa):{E:.4f}; " - f"Yield Strength (Comp):{bone_yield_compression} MPa; Yield Strength (Tens):{bone_yield_tension} MPa") - - # Create a Mohr-Coulomb elastoplastic material - if bone_yield_tension is not None and bone_yield_compression is not None: - bone_material = vtkbone.vtkboneMohrCoulombIsotropicMaterial() - bone_material.SetYieldStrengths(bone_yield_tension, bone_yield_compression) - bone_material.SetName(f"MohrCoulomb_{material_name}{i}") - else: - bone_material = vtkbone.vtkboneLinearIsotropicMaterial() - bone_material.SetName(f"LinearIsotropic_{material_name}{i}") - - bone_material.SetYoungsModulus(E) - bone_material.SetPoissonsRatio(mu) - - # Set a dynamic name for the material - - # Add material to the table with an ID corresponding to the bin index - material_table.AddMaterial(i+bin_range[0], bone_material) - - return material_table - - def add_pmma_material(material_table, pmma_mat_id, pmma_E, pmma_v, pmma_yield_tension=None, pmma_yield_compression=None): """ Adds a PMMA material to the provided material table. diff --git a/ogo/util/faim.py b/ogo/util/faim.py index 44713d8..817aefd 100644 --- a/ogo/util/faim.py +++ b/ogo/util/faim.py @@ -8,6 +8,9 @@ from typing import Dict, Iterable, List, Optional, Sequence +_AXIS_INDEX = {"x": 0, "y": 1, "z": 2} + + def _optional_path(value): if value is None: return None @@ -182,6 +185,167 @@ def parse_pistoia_text(path): return out +def _read_n88_node_coordinates_and_sets(model_file): + """Read node coordinates and named node sets from an n88model file.""" + try: + from netCDF4 import Dataset + except ImportError as exc: + raise RuntimeError( + "netCDF4 is required to convert profile target displacements from percent to mm." + ) from exc + + with Dataset(str(model_file), "r") as root: + coords = root.groups["Parts"].groups["Part1"].variables["NodeCoordinates"][:] + node_sets = {} + sets_group = root.groups.get("Sets") + if sets_group is not None and "NodeSets" in sets_group.groups: + node_sets_group = sets_group.groups["NodeSets"] + node_sets.update( + { + name: group.variables["NodeNumber"][:] + for name, group in node_sets_group.groups.items() + if "NodeNumber" in group.variables + } + ) + constraints_group = root.groups.get("Constraints") + if constraints_group is not None: + node_sets.update( + { + name: group.variables["NodeNumber"][:] + for name, group in constraints_group.groups.items() + if "NodeNumber" in group.variables + } + ) + return coords, node_sets + + +def _first_available_node_set_centroid(coords, node_sets, set_names): + """Return the centroid for the first available node-set or constraint name.""" + for set_name in set_names: + if set_name in node_sets: + return _node_set_centroid(coords, node_sets, set_name) + raise ValueError( + "None of the expected node sets were found in the n88model: {}".format( + ", ".join(set_names) + ) + ) + + +def _node_set_centroid(coords, node_sets, set_name): + if set_name not in node_sets: + raise ValueError(f"Node set '{set_name}' was not found in the n88model.") + + node_numbers = [int(node_number) for node_number in node_sets[set_name]] + if not node_numbers: + raise ValueError(f"Node set '{set_name}' is empty.") + + totals = [0.0, 0.0, 0.0] + for node_number in node_numbers: + coord = coords[node_number - 1] + for axis_index in range(3): + totals[axis_index] += float(coord[axis_index]) + return [total / len(node_numbers) for total in totals] + + +def infer_profile_characteristic_length_mm(model_file, report_profile, failure_axis): + """Infer the physical length used to convert percent strain endpoints to mm.""" + profile = str(report_profile or "generic").strip().lower() + axis_index = _AXIS_INDEX[str(failure_axis).lower()] + coords, node_sets = _read_n88_node_coordinates_and_sets(model_file) + + if profile == "spine": + first = _first_available_node_set_centroid( + coords, node_sets, ["body_top", "top_displacement", "convergence_set"] + ) + second = _first_available_node_set_centroid( + coords, node_sets, ["body_bottom", "bottom_fixed_z"] + ) + elif profile == "femur": + from ogo.fea.femur import FEMORAL_HEAD_NODE_SET, GREATER_TROCHANTER_NODE_SET + + first = _first_available_node_set_centroid( + coords, node_sets, [FEMORAL_HEAD_NODE_SET, "top_displacement", "convergence_set"] + ) + second = _first_available_node_set_centroid( + coords, node_sets, [GREATER_TROCHANTER_NODE_SET, "bottom_fixed_y_PMMA"] + ) + else: + return "" + + length = abs(first[axis_index] - second[axis_index]) + if length <= 0: + raise ValueError( + f"Could not infer a positive {profile} characteristic length along {failure_axis}." + ) + return length + + +def _percent_to_mm(percent, characteristic_length_mm): + return abs(float(percent)) * float(characteristic_length_mm) / 100.0 + + +def _first_available_constraint_group(root, names): + constraints = root.groups.get("Constraints") + if constraints is None: + return None + for name in names: + group = constraints.groups.get(name) + if group is not None and "Value" in group.variables: + return group + return None + + +def read_prescribed_displacement(model_file, report_profile="generic"): + """Read the active prescribed displacement from the model constraints.""" + try: + from netCDF4 import Dataset + except ImportError as exc: + raise RuntimeError("netCDF4 is required to read n88model constraints.") from exc + + profile = str(report_profile or "generic").strip().lower() + names = ["top_displacement", "convergence_set"] + with Dataset(str(model_file), "r") as root: + group = _first_available_constraint_group(root, names) + if group is None: + return "" + values = group.variables["Value"][:] + return float(values[0]) if len(values) else "" + + +def set_prescribed_displacement_from_percent( + model_file, + *, + report_profile, + failure_axis, + target_displacement_percent, + displacement_sign=-1.0, +): + """Set model displacement constraints to a profile percent endpoint.""" + try: + from netCDF4 import Dataset + except ImportError as exc: + raise RuntimeError("netCDF4 is required to update n88model constraints.") from exc + + characteristic_length = infer_profile_characteristic_length_mm( + model_file, report_profile, failure_axis + ) + target_displacement_mm = _percent_to_mm(target_displacement_percent, characteristic_length) + signed_displacement = float(displacement_sign) * target_displacement_mm + + with Dataset(str(model_file), "r+") as root: + for name in ("top_displacement", "convergence_set"): + group = _first_available_constraint_group(root, [name]) + if group is not None: + values = group.variables["Value"] + values[:] = signed_displacement + + return { + "characteristic_length_mm": characteristic_length, + "target_displacement_mm": target_displacement_mm, + "applied_displacement_mm": signed_displacement, + } + + def write_results_csv( *, output_file, @@ -195,9 +359,10 @@ def write_results_csv( failure_axis, applied_displacement=None, target_displacement=0.2, + report_profile="generic", warnings=None, ): - """Write a compact, user-facing CSV with loads, stiffness, and Pistoia values.""" + """Write a compact, user-facing CSV with loads and stiffness.""" pistoia_meta = parse_pistoia_text(pistoia_file) loads = _read_single_row_csv(loads_csv) pistoia = _read_single_row_csv(pistoia_csv) @@ -215,43 +380,84 @@ def write_results_csv( stiffness = _safe_float( pistoia.get(stiffness_var) or pistoia_meta.get(stiffness_key) ) + pistoia_failure_load_magnitude = ( + abs(float(pistoia_failure_load)) if pistoia_failure_load != "" else "" + ) - # The benchmark endpoint is reported at 0.2. If the model was solved at a - # different prescribed displacement, scale the linear reaction force back to - # that standard point. + # If the linear model was solved at a different prescribed displacement, + # scale the reaction force to the requested reporting endpoint. + profile = str(report_profile or "generic").strip().lower() + characteristic_length = "" + target_displacement_mm = "" rescale_factor = "" load_at_target = "" + computed_stiffness = "" try: applied = abs(float(applied_displacement)) target = abs(float(target_displacement)) + except (TypeError, ValueError): + applied = "" + target = "" + + if applied != "" and target != "": + if profile in ("femur", "spine"): + characteristic_length = infer_profile_characteristic_length_mm( + model_file, profile, failure_axis + ) + target_displacement_mm = _percent_to_mm(target, characteristic_length) + else: + target_displacement_mm = target if applied > 0 and reaction_force != "": - rescale_factor = target / applied + rescale_factor = target_displacement_mm / applied load_at_target = abs(float(reaction_force)) * rescale_factor - except (TypeError, ValueError): - pass + computed_stiffness = abs(float(reaction_force)) / applied - row = { + common = { "model_file": str(model_file), "analysis_file": str(analysis_file), - "pistoia_file": str(pistoia_file), - "loads_csv": str(loads_csv), - "pistoia_csv": str(pistoia_csv), "analysis_var": analysis_var, - "pistoia_failure_var": failure_var, - "pistoia_stiffness_var": stiffness_var, "applied_displacement": applied_displacement if applied_displacement is not None else "", - "target_displacement_percent": target_displacement, - "rescale_factor_to_target": rescale_factor, - "load_at_0p2_percent_N": load_at_target, - "load_at_0p2_percent_kN": load_at_target / 1000.0 if load_at_target != "" else "", "reaction_force_N": reaction_force, - "pistoia_failure_load_N": pistoia_failure_load, - "stiffness_N_per_mm": stiffness, - "critical_volume_pct": pistoia_meta.get("critical_volume_pct", ""), - "critical_ees": pistoia_meta.get("critical_ees", ""), - "ees_at_crit_vol": pistoia_meta.get("ees_at_crit_vol", ""), - "warnings": " | ".join(warnings or []), + "stiffness_N_per_mm": computed_stiffness if computed_stiffness != "" else stiffness, } + if profile == "femur": + row = { + "model_file": common["model_file"], + "analysis_file": common["analysis_file"], + "analysis_var": common["analysis_var"], + "applied_displacement": common["applied_displacement"], + "reaction_force_N": common["reaction_force_N"], + "stiffness_N_per_mm": common["stiffness_N_per_mm"], + "characteristic_length_mm": characteristic_length, + } + elif profile == "spine": + row = { + "model_file": common["model_file"], + "analysis_file": common["analysis_file"], + "analysis_var": common["analysis_var"], + "applied_displacement": common["applied_displacement"], + "reaction_force_N": common["reaction_force_N"], + "stiffness_N_per_mm": common["stiffness_N_per_mm"], + "characteristic_length_mm": characteristic_length, + } + else: + row = { + **common, + "loads_csv": str(loads_csv), + "pistoia_file": str(pistoia_file), + "pistoia_csv": str(pistoia_csv), + "pistoia_failure_var": failure_var, + "pistoia_stiffness_var": stiffness_var, + "target_displacement_percent": target_displacement, + "rescale_factor_to_target": rescale_factor, + "load_at_target_displacement_N": load_at_target, + "load_at_target_displacement_kN": load_at_target / 1000.0 if load_at_target != "" else "", + "pistoia_failure_load_N": pistoia_failure_load, + "critical_volume_pct": pistoia_meta.get("critical_volume_pct", ""), + "critical_ees": pistoia_meta.get("critical_ees", ""), + "ees_at_crit_vol": pistoia_meta.get("ees_at_crit_vol", ""), + "warnings": " | ".join(warnings or []), + } output_file = Path(output_file) output_file.parent.mkdir(parents=True, exist_ok=True) @@ -285,8 +491,11 @@ def run_faim_pipeline( critical_volume=2.0, critical_strain=0.007, exclude=5000, + run_pistoia=True, applied_displacement=None, target_displacement=0.2, + report_profile="generic", + solve_displacement_percent=None, compress=True, require_pistoia=False, dry_run=False, @@ -360,6 +569,36 @@ def run_faim_pipeline( check=False, ) + if solve_displacement_percent is not None and not dry_run: + if "Name :" in modelinfo_text: + warnings.append( + "Existing solution found; prescribed displacement was not updated before solve." + ) + current_displacement = read_prescribed_displacement(model_file, report_profile) + if current_displacement != "": + applied_displacement = current_displacement + else: + current_displacement = read_prescribed_displacement(model_file, report_profile) + try: + sign_source = ( + current_displacement + if current_displacement != "" + else applied_displacement + if applied_displacement is not None + else -1.0 + ) + displacement_sign = -1.0 if float(sign_source) < 0 else 1.0 + except (TypeError, ValueError): + displacement_sign = -1.0 + update = set_prescribed_displacement_from_percent( + model_file, + report_profile=report_profile, + failure_axis=failure_axis, + target_displacement_percent=solve_displacement_percent, + displacement_sign=displacement_sign, + ) + applied_displacement = update["applied_displacement_mm"] + if "Name :" not in modelinfo_text: run_command( [solver, "--engine=mt", "--threads={}".format(int(threads)), str(model_file)], @@ -387,29 +626,30 @@ def run_faim_pipeline( # Pistoia is useful but can fail for nonlinear or unusual models; make that # behavior configurable so the main solve result can still be collected. - try: - run_command( - [ - pistoia, - str(model_file), - "--critical_volume", - str(critical_volume), - "--critical_strain", - str(critical_strain), - "--exclude", - str(exclude), - "--output_file", - str(pistoia_file), - ], - conda_env=conda_env, - conda_executable=conda_executable, - env=env, - dry_run=dry_run, - ) - except subprocess.CalledProcessError: - if require_pistoia: - raise - warnings.append("Pistoia postprocessing failed.") + if run_pistoia: + try: + run_command( + [ + pistoia, + str(model_file), + "--critical_volume", + str(critical_volume), + "--critical_strain", + str(critical_strain), + "--exclude", + str(exclude), + "--output_file", + str(pistoia_file), + ], + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + ) + except subprocess.CalledProcessError: + if require_pistoia: + raise + warnings.append("Pistoia postprocessing failed.") # Tabulate the reaction force and Pistoia values into small CSV files. run_command( @@ -420,7 +660,7 @@ def run_faim_pipeline( dry_run=dry_run, ) - if pistoia_vars: + if run_pistoia and pistoia_vars: run_command( [ tabulate, @@ -471,8 +711,15 @@ def run_faim_pipeline( failure_axis=failure_axis, applied_displacement=applied_displacement, target_displacement=target_displacement, + report_profile=report_profile, warnings=warnings, ) + if str(report_profile or "").strip().lower() in ("spine", "femur"): + for intermediate_file in (loads_csv, pistoia_csv): + try: + Path(intermediate_file).unlink() + except FileNotFoundError: + pass return { "analysis_file": analysis_file, diff --git a/ogo/util/vtk_image.py b/ogo/util/vtk_image.py new file mode 100644 index 0000000..ebfe1e5 --- /dev/null +++ b/ogo/util/vtk_image.py @@ -0,0 +1,43 @@ +"""Small VTK image/NumPy conversion helpers used by FE workflows.""" + + +def vtk_image_to_numpy(vtk_image, *, processing_order=False, copy=False): + """Return image scalars as a NumPy array. + + By default the returned array is in VTK image dimension order. Set + ``processing_order`` when using FE boundary masks that are processed as + z/y/x NumPy arrays. + """ + import numpy as np + from vtk.util.numpy_support import vtk_to_numpy + + scalars = vtk_image.GetPointData().GetScalars() + try: + array = vtk_to_numpy(scalars) + except ValueError: + array = np.asarray([scalars.GetTuple1(i) for i in range(scalars.GetNumberOfTuples())]) + array = array.reshape(vtk_image.GetDimensions(), order="F") + if processing_order: + array = np.swapaxes(array, 0, 2) + if copy: + array = array.copy() + return array + + +def numpy_to_vtk_image(array, template_vtk_image, vtk_array_type=None, *, processing_order=False): + """Create a VTK image from an array and preserve the template image geometry.""" + import numpy as np + from vtk.util.numpy_support import numpy_to_vtk + + final_array = np.swapaxes(array, 0, 2) if processing_order else array + if vtk_array_type is None: + vtk_array_type = template_vtk_image.GetScalarType() + vtk_array = numpy_to_vtk(final_array.ravel(order="F"), deep=True, array_type=vtk_array_type) + + output = template_vtk_image.NewInstance() + output.DeepCopy(template_vtk_image) + output.SetExtent(template_vtk_image.GetExtent()) + output.SetOrigin(template_vtk_image.GetOrigin()) + output.SetSpacing(template_vtk_image.GetSpacing()) + output.GetPointData().SetScalars(vtk_array) + return output diff --git a/setup.cfg b/setup.cfg index 86ab678..95fde32 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,7 +29,7 @@ console_scripts = ogoPrintLabels = ogo.cli.PrintLabels:main ogoReplaceLabels = ogo.cli.ReplaceLabels:main ogoProcrustes = ogo.cli.Procrustes:main - ogoGenerateFEM = ogo.cli.GenerateFEM:main + ogoFEA = ogo.cli.GenerateFEM:main ogoRepairNIfTI = ogo.cli.RepairNifti:main ogoVisualize = ogo.cli.Visualize:main ogoValidate = ogo.cli.Validate:main @@ -51,4 +51,3 @@ console_scripts = [pbr] skip_changelog = 1 skip_authors = 1 - diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index f63e725..f5b1546 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -1,5 +1,7 @@ import importlib.util +import json from pathlib import Path +from types import SimpleNamespace import pytest @@ -40,6 +42,8 @@ "kopperdahl_trab_yc", ] +SPINE_DEFAULT_QC_ARGS = ["--quality_control", "False"] + @pytest.fixture def solve_calls(monkeypatch): @@ -89,6 +93,7 @@ def test_spine_runs_each_requested_vertebra(monkeypatch, solve_calls): *BENCHMARK_LINEAR_ARGS, "--iso_resolution", "1.2", + *SPINE_DEFAULT_QC_ARGS, ], [ "density.nii.gz", @@ -104,6 +109,7 @@ def test_spine_runs_each_requested_vertebra(monkeypatch, solve_calls): *BENCHMARK_LINEAR_ARGS, "--iso_resolution", "1.2", + *SPINE_DEFAULT_QC_ARGS, ], ] assert solve_calls == [ @@ -160,7 +166,8 @@ def test_dry_run_prints_commands_without_running(monkeypatch, capsys): assert ( capsys.readouterr().out.strip() == "OgoSpineCompressionFe density.nii.gz spine_mask.nii.gz " - "--mask_threshold 6 --process_mask_threshold 5 --appendix L2" + "--mask_threshold 6 --process_mask_threshold 5 --appendix L2 " + "--quality_control False" ) @@ -190,6 +197,7 @@ def test_spine_preset_none_keeps_command_minimal(monkeypatch, solve_calls): "7", "--appendix", "L3", + *SPINE_DEFAULT_QC_ARGS, ] ] @@ -221,6 +229,7 @@ def test_spine_nonlinear_benchmark_preset(monkeypatch, solve_calls): "--appendix", "T10", *BENCHMARK_NONLINEAR_ARGS, + *SPINE_DEFAULT_QC_ARGS, ] ] @@ -241,27 +250,142 @@ def test_spine_explicit_options_follow_preset_for_overrides(monkeypatch, solve_c ] ) - assert calls[0][-2:] == ["--fe_displacement", "-0.25"] + explicit_index = len(calls[0]) - len(SPINE_DEFAULT_QC_ARGS) - 2 + assert calls[0][explicit_index:explicit_index + 2] == ["--fe_displacement", "-0.25"] assert calls[0].count("--fe_displacement") == 2 -def test_legacy_model_type_dispatch_still_works(monkeypatch): +def _metadata_args(tmp_path, no_solve=True): + return type( + "Args", + (), + { + "dry_run": False, + "calibrated_image": tmp_path / "density.nii.gz", + "bone_mask": tmp_path / "mask.nii.gz", + "output_path": tmp_path, + "skip_bc_audit": False, + "bc_audit_flat_tolerance": 1.0e-4, + "debug": False, + "no_solve": no_solve, + "target_displacement": None, + "exclude": 5000, + "critical_volume": 2.0, + "critical_strain": 0.007, + "no_compress": False, + }, + )() + + +def _solve_args(**overrides): + data = { + "threads": 4, + "faim_env": None, + "conda_executable": "conda", + "faim_install_root": None, + "faim_bin_dir": None, + "faim_license_dir": None, + "faim_command": None, + "n88modelinfo_command": None, + "n88derivedfields_command": None, + "n88postfaim_command": None, + "n88pistoia_command": None, + "n88tabulate_command": None, + "n88copymodel_command": None, + "critical_volume": 2.0, + "critical_strain": 0.007, + "exclude": 5000, + "no_compress": False, + "require_pistoia": False, + "dry_run": False, + "target_displacement": None, + } + data.update(overrides) + return SimpleNamespace(**data) + + +def test_solve_model_sets_femur_displacement_from_percent(monkeypatch, tmp_path): + import ogo.util.faim as faim_module + calls = [] - monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + monkeypatch.setattr(faim_module, "run_faim_pipeline", lambda **kwargs: calls.append(kwargs)) - GenerateFEM.main( - [ - "--model_type", - "vertebra", - "density.nii.gz", - "mask.nii.gz", - "--mask_threshold", - "2", - "--process_mask_threshold", - "1", - ] + GenerateFEM.solve_model( + tmp_path / "density_LT_FEMUR_SF.n88model", + _solve_args(), + "hip", + ["density.nii.gz", "mask.nii.gz", "--fe_displacement", "-4.0"], ) - assert calls == [ - ["density.nii.gz", "mask.nii.gz", "--mask_threshold", "2", "--process_mask_threshold", "1"] + assert calls[0]["report_profile"] == "femur" + assert calls[0]["target_displacement"] == 4.0 + assert calls[0]["solve_displacement_percent"] == 4.0 + + +def test_spine_modeling_metadata_records_materials_and_bcs(tmp_path): + model = tmp_path / "density_vertebra_2_L1.n88model" + model.write_text("dummy") + argv = [ + "density.nii.gz", + "mask.nii.gz", + "--mask_threshold", + "2", + "--process_mask_threshold", + "3", + "--appendix", + "L1", + "--elastic_E_func", + "kopperdahl_trab_E", + "--cort_elastic_E_func", + "kopperdahl_trab_E", + "--fe_displacement", + "-0.2", ] + + path = GenerateFEM.write_modeling_metadata(model, "spine", argv, _metadata_args(tmp_path)) + data = json.loads(path.read_text()) + + assert data["target"] == {"vertebra": "L1", "body_label": 2, "process_label": 3} + assert data["materials"]["trabecular"]["elastic_E_func"] == "kopperdahl_trab_E" + assert data["materials"]["cortical"]["material_id_range"] == [129, 256] + assert data["materials"]["pmma"]["material_id"] == 5000 + assert data["boundary_conditions"]["constraints"][0]["node_set"] == "body_top" + assert "initial_generator_value_mm" not in data["boundary_conditions"]["constraints"][0] + assert data["boundary_conditions"]["constraints"][0]["target_displacement_percent"] == 0.68 + assert data["boundary_conditions"]["constraints"][0]["value_source"].startswith("target_displacement_percent") + assert data["solve_and_reporting"]["target_displacement_percent"] == 0.68 + assert data["solve_and_reporting"]["run_pistoia"] is False + + +def test_femur_modeling_metadata_records_materials_shaft_and_bcs(tmp_path): + model = tmp_path / "density_LT_FEMUR_SF.n88model" + model.write_text("dummy") + argv = [ + "density.nii.gz", + "mask.nii.gz", + "--femur_side", + "1", + "--pmma_thick", + "6", + "--pmma_intrusion", + "6", + "--femur_lesser_trochanter_distal_offset", + "50", + "--compartment_mask", + "trab_cort.nii.gz", + ] + + path = GenerateFEM.write_modeling_metadata(model, "hip", argv, _metadata_args(tmp_path)) + data = json.loads(path.read_text()) + + assert data["target"]["side"] == "left" + assert data["segmentation"]["compartment_labels"] == {"cortical": 1, "trabecular": 2} + assert data["shaft_standardization"]["lesser_trochanter_distal_offset_mm"] == 50.0 + assert data["materials"]["include_cortical_region"] is True + assert data["materials"]["cortical"]["material_id_range"] == [129, 256] + assert data["boundary_conditions"]["fixture_geometry"]["femoral_head"]["pmma_intrusion_mm"] == 6.0 + assert data["boundary_conditions"]["constraints"][0]["node_set"] == "Femoral_Head_PMMA_Nodes" + assert "initial_generator_value_mm" not in data["boundary_conditions"]["constraints"][0] + assert data["boundary_conditions"]["constraints"][0]["target_displacement_percent"] == 4.0 + assert data["boundary_conditions"]["constraints"][0]["value_source"].startswith("target_displacement_percent") + assert data["solve_and_reporting"]["target_displacement_percent"] == 4.0 diff --git a/tests/fea/fixtures/spinefe_sub_verse004_vertebra21_metadata.json b/tests/fea/fixtures/spinefe_sub_verse004_vertebra21_metadata.json index b92b3e6..66646ac 100644 --- a/tests/fea/fixtures/spinefe_sub_verse004_vertebra21_metadata.json +++ b/tests/fea/fixtures/spinefe_sub_verse004_vertebra21_metadata.json @@ -1,15 +1,22 @@ { "linear": { + "mesh": { + "elements": 67137, + "nodes": 77892 + }, "node_sets": { "body_top": 1432, "body_bottom": 1413 } }, "nonlinear": { + "mesh": { + "elements": 67137, + "nodes": 77892 + }, "node_sets": { "body_top": 1432, "body_bottom": 1413 } } } - diff --git a/tests/fea/test_boundary.py b/tests/fea/test_boundary.py new file mode 100644 index 0000000..1bce348 --- /dev/null +++ b/tests/fea/test_boundary.py @@ -0,0 +1,188 @@ +import pytest + +np = pytest.importorskip("numpy") + +from ogo.fea.boundary import generate_bone_cap_mask, generate_fixture_cap_mask + + +def test_fit_cap_follows_each_contact_ray(): + mask = np.zeros((12, 8, 8), dtype=bool) + mask[4:7, 2:6, 2:6] = True + mask[5, 1, 3] = True + + cap = generate_bone_cap_mask(mask, axis="x", direction="down", thickness=3, shape="fit") + + assert cap.any() + assert not np.any(cap & mask) + for y in range(mask.shape[1]): + for z in range(mask.shape[2]): + body_x = np.flatnonzero(mask[:, y, z]) + cap_x = np.flatnonzero(cap[:, y, z]) + if cap_x.size == 0: + continue + assert body_x.size > 0 + assert cap_x.max() < body_x.min() + + +def test_box_cap_uses_projected_contact_extent(): + mask = np.zeros((12, 8, 8), dtype=bool) + mask[4:7, 2:6, 2:6] = True + + cap = generate_bone_cap_mask(mask, axis="x", direction="up", thickness=2, shape="box") + + assert cap.sum() == 2 * 4 * 4 + assert np.all(cap[7:9, 2:6, 2:6]) + assert not np.any(cap & mask) + + +@pytest.mark.parametrize("shape", ["box", "round"]) +def test_shaped_caps_touch_irregular_bone_surface(shape): + mask = np.zeros((14, 9, 9), dtype=bool) + mask[3:6, 2:7, 2:7] = True + mask[6, 4, 4] = True + mask[7, 5, 4] = True + + cap = generate_bone_cap_mask(mask, axis="x", direction="up", thickness=3, shape=shape) + + assert cap.any() + assert not np.any(cap & mask) + for y in range(mask.shape[1]): + for z in range(mask.shape[2]): + cap_x = np.flatnonzero(cap[:, y, z]) + if cap_x.size == 0: + continue + body_x = np.flatnonzero(mask[:, y, z]) + assert body_x.size > 0 + assert cap_x.min() == body_x.max() + 1 + + +def test_round_cap_stays_inside_projected_box(): + mask = np.zeros((12, 8, 8), dtype=bool) + mask[4:7, 2:6, 2:6] = True + + round_cap = generate_bone_cap_mask(mask, axis="x", direction="up", thickness=2, shape="round") + box_cap = generate_bone_cap_mask(mask, axis="x", direction="up", thickness=2, shape="box") + + assert round_cap.any() + assert round_cap.sum() < box_cap.sum() + assert np.all(round_cap <= box_cap) + assert not np.any(round_cap & mask) + + +def test_fixture_box_cap_keeps_full_footprint_over_irregular_surface(): + mask = np.zeros((14, 9, 9), dtype=bool) + mask[3:6, 2:7, 2:7] = True + mask[6, 4, 4] = True + mask[7, 5, 4] = True + + cap = generate_fixture_cap_mask(mask, axis="x", direction="up", thickness=3, shape="box") + + assert np.all(cap[8:11, 2:7, 2:7]) + assert cap.sum() == 3 * 5 * 5 + + +def test_fixture_cap_intrudes_across_contact_plane(): + mask = np.zeros((14, 9, 9), dtype=bool) + mask[3:6, 2:7, 2:7] = True + + cap = generate_fixture_cap_mask( + mask, + axis="x", + direction="up", + thickness=3, + shape="box", + intrusion=2, + ) + + assert np.all(cap[4:9, 2:7, 2:7]) + assert not np.any(cap[:4]) + assert not np.any(cap[9:]) + + +def test_fixture_contact_crop_removes_unsupported_columns(): + mask = np.zeros((14, 9, 9), dtype=bool) + mask[3:6, 2:7, 4] = True + mask[3:6, 4, 2:7] = True + mask[0, 3, 3] = True + + full = generate_fixture_cap_mask(mask, axis="x", direction="up", thickness=2, shape="box") + cropped = generate_fixture_cap_mask( + mask, + axis="x", + direction="up", + thickness=2, + shape="box", + intrusion=2, + crop_to_contact=True, + ) + + assert full[:, 3, 3].any() + assert not cropped[:, 3, 3].any() + assert cropped[:, 4, 3].any() + + +def test_fixture_round_cap_is_not_contact_cropped(): + mask = np.zeros((14, 11, 11), dtype=bool) + mask[3:6, 2:9, 5] = True + mask[3:6, 5, 2:9] = True + + fixture = generate_fixture_cap_mask(mask, axis="x", direction="up", thickness=2, shape="round") + contact = generate_bone_cap_mask(mask, axis="x", direction="up", thickness=2, shape="round") + + assert fixture[:, 3, 3].any() + assert not contact[:, 3, 3].any() + assert not np.any(contact & mask) + + +def test_vtk_roi_cap_uses_requested_output_value(): + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + + from ogo.fea.boundary import generate_bone_cap_vtk, label_foreground_in_bounds_vtk + + labels = np.zeros((12, 8, 8), dtype=np.uint16) + labels[4:7, 2:6, 2:6] = 12 + vtk_arr = numpy_to_vtk(np.swapaxes(labels, 0, 2).ravel(order="F"), deep=True) + image = vtk.vtkImageData() + image.SetDimensions(list(np.swapaxes(labels, 0, 2).shape)) + image.SetSpacing((1, 1, 1)) + image.SetOrigin((0, 0, 0)) + image.GetPointData().SetScalars(vtk_arr) + + roi = label_foreground_in_bounds_vtk(image, (0, 11, 2, 5, 2, 5), label_value=1) + cap = generate_bone_cap_vtk( + roi, label_value=1, axis="x", direction="up", thickness=2, shape="box", output_value=5000 + ) + cap_np = vtk_to_numpy(cap.GetPointData().GetScalars()).reshape(cap.GetDimensions(), order="F") + + assert set(np.unique(cap_np)) == {0, 5000} + + +def test_vtk_conversion_preserves_template_extent(): + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + from ogo.fea.boundary import generate_fixture_cap_vtk + + labels = np.zeros((6, 5, 4), dtype=np.uint16) + labels[2:4, 1:4, 1:3] = 1 + vtk_arr = numpy_to_vtk(np.swapaxes(labels, 0, 2).ravel(order="F"), deep=True) + image = vtk.vtkImageData() + image.SetExtent(10, 13, 20, 24, 30, 35) + image.SetSpacing((0.7, 0.8, 0.9)) + image.SetOrigin((1.0, 2.0, 3.0)) + image.GetPointData().SetScalars(vtk_arr) + + cap = generate_fixture_cap_vtk( + image, + label_value=1, + axis="x", + direction="up", + thickness=1, + shape="box", + output_value=5000, + ) + + assert cap.GetExtent() == image.GetExtent() + assert cap.GetSpacing() == image.GetSpacing() + assert cap.GetOrigin() == image.GetOrigin() diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 17bec3d..832848a 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -27,3 +27,142 @@ def test_tilted_side_support_vector_has_unit_length(): assert np.linalg.norm(vector) == pytest.approx(1.0) + +def test_distal_cut_z_from_reference_uses_superior_bound(): + assert femur.distal_cut_z_from_reference((0, 1, 0, 1, -20, 120), 100) == pytest.approx(20) + + +def test_swap_xz_footprint_preserves_center_and_swaps_lengths(): + swapped = femur.swap_xz_footprint((10, 50, 1, 3, 100, 120)) + + assert swapped == pytest.approx((20, 40, 1, 3, 90, 130)) + + +def test_expand_z_footprint_preserves_center(): + expanded = femur.expand_z_footprint((20, 40, 1, 3, 90, 130), 20) + + assert expanded == pytest.approx((20, 40, 1, 3, 80, 140)) + + +def test_expand_xz_footprint_preserves_center(): + expanded = femur.expand_xz_footprint( + (20, 40, 1, 3, 90, 130), + x_extension_mm=10, + z_extension_mm=20, + ) + + assert expanded == pytest.approx((15, 45, 1, 3, 80, 140)) + + +def test_flat_femur_shaft_crop_rejects_short_distal_coverage(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + data = np.zeros((4, 4, 4), dtype=np.uint8) + data[:, :, 2:] = 1 + image = vtk.vtkImageData() + image.SetDimensions(4, 4, 4) + image.SetOrigin(0, 0, 0) + image.SetSpacing(1, 1, 1) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + + with pytest.raises(ValueError, match="does not include enough distal shaft"): + femur.standardize_femur_shaft_length( + image, + image, + reference_bounds=(0, 0, 0, 0, 0, 10), + retained_length_mm=9, + cut_mode="fixed_length", + ) + + +def test_flat_femur_shaft_crop_zeroes_voxels_below_cut(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + + data = np.ones((3, 3, 5), dtype=np.uint8) + image = vtk.vtkImageData() + image.SetDimensions(3, 3, 5) + image.SetOrigin(0, 0, 0) + image.SetSpacing(1, 1, 1) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + + _, cropped, meta = femur.standardize_femur_shaft_length( + image, + image, + reference_bounds=(0, 0, 0, 0, 0, 4), + retained_length_mm=2, + cut_mode="fixed_length", + ) + out = vtk_to_numpy(cropped.GetPointData().GetScalars()).reshape((3, 3, 5), order="F") + + assert meta["cut_z"] == pytest.approx(2) + assert not np.any(out[:, :, :2]) + assert np.all(out[:, :, 2:]) + + +def test_lesser_trochanter_cut_uses_distal_area_peak(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + data = np.zeros((60, 80, 90), dtype=np.uint8) + x = np.arange(data.shape[0])[:, None] + y = np.arange(data.shape[1])[None, :] + for z in range(10, 86): + radius = 10 + y_center = 35 + if 58 <= z <= 64: + radius = 19 + if 73 <= z <= 79: + y_center = 50 + radius = 12 + section = ((x - 30) ** 2 + (y - y_center) ** 2) <= radius**2 + data[:, :, z] = section.astype(np.uint8) + + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(0, 0, 0) + image.SetSpacing(1, 1, 1) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + + meta = femur.detect_lesser_trochanter_cut_z(image) + + assert meta["greater_trochanter_z"] == pytest.approx(76) + assert meta["lesser_trochanter_z"] == pytest.approx(61) + assert meta["cut_z"] == pytest.approx(61) + + +def test_cortical_compartment_mask_uses_default_labels(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + + data = np.zeros((3, 3, 3), dtype=np.uint8) + data[0, :, :] = 1 + data[1:, :, :] = 2 + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + + cortical = femur.cortical_compartment_mask(image) + out = vtk_to_numpy(cortical.GetPointData().GetScalars()).reshape(data.shape, order="F") + + assert np.all(out[0, :, :] == 1) + assert not np.any(out[1:, :, :]) + + +def test_cortical_compartment_mask_requires_trab_and_cort_labels(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + data = np.ones((3, 3, 3), dtype=np.uint8) + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + + with pytest.raises(ValueError, match="missing required label"): + femur.cortical_compartment_mask(image) diff --git a/tests/fea/test_materials.py b/tests/fea/test_materials.py index 7be69d2..ed2a679 100644 --- a/tests/fea/test_materials.py +++ b/tests/fea/test_materials.py @@ -1,6 +1,6 @@ import pytest -from ogo.cli.ref import material_laws +from ogo.fea import material_laws from ogo.fea import materials @@ -49,3 +49,30 @@ def test_build_spine_material_table_requires_vtkbone_when_exercised(): assert table.GetNumberOfMaterials() == 5 + +def test_build_femur_material_table_can_use_single_trabecular_region(): + pytest.importorskip("vtkbone") + + table = materials.build_femur_material_table( + [100.0, 200.0], + n_bins=2, + pmma_mat_id=300, + elastic_E_func="kopperdahl_trab_E", + ) + + assert table.GetNumberOfMaterials() == 3 + + +def test_build_femur_material_table_can_use_trab_cort_regions(): + pytest.importorskip("vtkbone") + + table = materials.build_femur_material_table( + [100.0, 200.0, 100.0, 200.0], + n_bins=2, + pmma_mat_id=300, + elastic_E_func="kopperdahl_trab_E", + cort_elastic_E_func="bayraktar_cort_E", + include_cortical=True, + ) + + assert table.GetNumberOfMaterials() == 5 diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py index e84783a..542b7ae 100644 --- a/tests/fea/test_spinefe_benchmark.py +++ b/tests/fea/test_spinefe_benchmark.py @@ -15,6 +15,14 @@ def _node_set_count(model, name): return node_set.GetNumberOfTuples() +def _model_count(model, method_names): + for method_name in method_names: + method = getattr(model, method_name, None) + if method is not None: + return method() + raise AssertionError(f"Model does not expose any of {method_names!r}") + + def _sample_paths(): benchmark_dir = find_spinefe_benchmark_dir(Path(__file__).resolve().parents[2]) if benchmark_dir is None: @@ -47,6 +55,12 @@ def test_spinefe_benchmark_sample_model_generation_matches_golden_metadata( nonlinear=nonlinear, ) + assert _model_count(model, ["GetNumberOfCells", "GetNumberOfElements"]) == expected["mesh"][ + "elements" + ] + assert _model_count(model, ["GetNumberOfPoints", "GetNumberOfNodes"]) == expected["mesh"][ + "nodes" + ] for node_set_name, expected_count in expected["node_sets"].items(): assert _node_set_count(model, node_set_name) == expected_count @@ -86,7 +100,8 @@ def test_spinefe_benchmark_solve_outputs_match_golden_csv(tmp_path): pistoia_vars=["pis_fz_fail", "pis_stiffz"], failure_axis="z", applied_displacement=-0.2, - target_displacement=0.2, + target_displacement=0.68, + report_profile="spine", ) with golden_csv.open(newline="") as handle: @@ -94,5 +109,8 @@ def test_spinefe_benchmark_solve_outputs_match_golden_csv(tmp_path): with outputs["results_csv"].open(newline="") as handle: actual = next(csv.DictReader(handle)) - for key in ["load_at_0p2_percent_N", "pistoia_failure_load_N", "stiffness_N_per_mm"]: + keys = ["reaction_force_N", "stiffness_N_per_mm"] + if any(key not in expected for key in keys): + pytest.skip("Solve-level golden CSV uses the old result schema") + for key in keys: assert float(actual[key]) == pytest.approx(float(expected[key]), rel=1e-4) diff --git a/tests/util/test_faim.py b/tests/util/test_faim.py index 063d26e..9984d96 100644 --- a/tests/util/test_faim.py +++ b/tests/util/test_faim.py @@ -2,6 +2,8 @@ import importlib.util from pathlib import Path +import pytest + _MODULE_PATH = Path(__file__).resolve().parents[2] / "ogo" / "util" / "faim.py" _SPEC = importlib.util.spec_from_file_location("faim", str(_MODULE_PATH)) @@ -9,6 +11,57 @@ _SPEC.loader.exec_module(faim) +def write_minimal_n88_model(path, coords, node_sets): + netCDF4 = pytest.importorskip("netCDF4") + + with netCDF4.Dataset(str(path), "w") as root: + parts = root.createGroup("Parts") + part = parts.createGroup("Part1") + part.createDimension("NumberOfNodes", len(coords)) + part.createDimension("NumberOfCoordinates", 3) + coordinates = part.createVariable( + "NodeCoordinates", + "f8", + ("NumberOfNodes", "NumberOfCoordinates"), + ) + coordinates[:] = coords + + sets = root.createGroup("Sets") + node_sets_group = sets.createGroup("NodeSets") + for name, node_numbers in node_sets.items(): + node_set = node_sets_group.createGroup(name) + node_set.createDimension("NumberOfNodes", len(node_numbers)) + variable = node_set.createVariable("NodeNumber", "i4", ("NumberOfNodes",)) + variable[:] = node_numbers + + +def write_minimal_n88_model_with_constraints(path, coords, constraints): + netCDF4 = pytest.importorskip("netCDF4") + + with netCDF4.Dataset(str(path), "w") as root: + parts = root.createGroup("Parts") + part = parts.createGroup("Part1") + part.createDimension("NumberOfNodes", len(coords)) + part.createDimension("NumberOfCoordinates", 3) + coordinates = part.createVariable( + "NodeCoordinates", + "f8", + ("NumberOfNodes", "NumberOfCoordinates"), + ) + coordinates[:] = coords + + constraints_group = root.createGroup("Constraints") + for name, node_numbers in constraints.items(): + constraint = constraints_group.createGroup(name) + constraint.createDimension("NumberOfValues", len(node_numbers)) + node_number = constraint.createVariable("NodeNumber", "i4", ("NumberOfValues",)) + node_number[:] = node_numbers + sense = constraint.createVariable("Sense", "i4", ("NumberOfValues",)) + sense[:] = [3] * len(node_numbers) + value = constraint.createVariable("Value", "f8", ("NumberOfValues",)) + value[:] = [0.0] * len(node_numbers) + + def test_resolve_faim_command_uses_bin_64bit(tmp_path): install_root = tmp_path / "Faim 9.0" bin_dir = install_root / "bin" / "64bit" @@ -61,13 +114,362 @@ def test_write_results_csv_collects_pistoia_and_loads(tmp_path): row = next(csv.DictReader(handle)) assert row["reaction_force_N"] == "123.4" - assert row["load_at_0p2_percent_N"] == "24.680000000000003" + assert row["load_at_target_displacement_N"] == "24.680000000000003" + assert "load_at_0p68_percent_N" not in row assert row["pistoia_failure_load_N"] == "345.6" - assert row["stiffness_N_per_mm"] == "789.0" + assert row["stiffness_N_per_mm"] == "123.4" assert row["critical_volume_pct"] == "2.0" assert row["rescale_factor_to_target"] == "0.2" +def test_write_results_csv_reports_generic_target_without_0p68_alias(tmp_path): + analysis_file = tmp_path / "model_analysis.txt" + pistoia_file = tmp_path / "model_pistoia.txt" + loads_csv = tmp_path / "model_loads.csv" + pistoia_csv = tmp_path / "model_pistoia.csv" + results_csv = tmp_path / "model_results.csv" + + analysis_file.write_text("analysis", encoding="utf-8") + pistoia_file.write_text("", encoding="utf-8") + loads_csv.write_text("fy_ns1\n100.0\n", encoding="utf-8") + pistoia_csv.write_text("pis_fy_fail,pis_stiffy\n50.0,25.0\n", encoding="utf-8") + + faim.write_results_csv( + output_file=results_csv, + model_file=tmp_path / "model.n88model", + analysis_file=analysis_file, + pistoia_file=pistoia_file, + loads_csv=loads_csv, + pistoia_csv=pistoia_csv, + analysis_var="fy_ns1", + pistoia_vars=["pis_fy_fail", "pis_stiffy"], + failure_axis="y", + applied_displacement="1.0", + target_displacement=4.0, + ) + + with results_csv.open(newline="") as handle: + row = next(csv.DictReader(handle)) + + assert row["load_at_target_displacement_N"] == "400.0" + assert row["load_at_target_displacement_kN"] == "0.4" + assert "load_at_0p68_percent_N" not in row + + +def test_write_results_csv_femur_profile_reports_4_percent_and_stiffness(tmp_path): + analysis_file = tmp_path / "model_analysis.txt" + model_file = tmp_path / "model.n88model" + pistoia_file = tmp_path / "model_pistoia.txt" + loads_csv = tmp_path / "model_loads.csv" + pistoia_csv = tmp_path / "model_pistoia.csv" + results_csv = tmp_path / "model_results.csv" + + analysis_file.write_text("analysis", encoding="utf-8") + pistoia_file.write_text("", encoding="utf-8") + loads_csv.write_text("fy_ns1\n100.0\n", encoding="utf-8") + pistoia_csv.write_text("", encoding="utf-8") + write_minimal_n88_model( + model_file, + coords=[ + [0.0, 0.0, 0.0], + [0.0, 100.0, 0.0], + ], + node_sets={ + "Femoral_Head_PMMA_Nodes": [1], + "Greater_Trochanter_PMMA_Nodes": [2], + }, + ) + + faim.write_results_csv( + output_file=results_csv, + model_file=model_file, + analysis_file=analysis_file, + pistoia_file=pistoia_file, + loads_csv=loads_csv, + pistoia_csv=pistoia_csv, + analysis_var="fy_ns1", + pistoia_vars=[], + failure_axis="y", + applied_displacement="1.0", + target_displacement=4.0, + report_profile="femur", + ) + + with results_csv.open(newline="") as handle: + row = next(csv.DictReader(handle)) + + assert list(row) == [ + "model_file", + "analysis_file", + "analysis_var", + "applied_displacement", + "reaction_force_N", + "stiffness_N_per_mm", + "characteristic_length_mm", + ] + assert row["analysis_var"] == "fy_ns1" + assert row["applied_displacement"] == "1.0" + assert row["reaction_force_N"] == "100.0" + assert row["characteristic_length_mm"] == "100.0" + assert row["stiffness_N_per_mm"] == "100.0" + + +def test_write_results_csv_spine_profile_reports_0p68_percent(tmp_path): + analysis_file = tmp_path / "model_analysis.txt" + model_file = tmp_path / "model.n88model" + pistoia_file = tmp_path / "model_pistoia.txt" + loads_csv = tmp_path / "model_loads.csv" + pistoia_csv = tmp_path / "model_pistoia.csv" + results_csv = tmp_path / "model_results.csv" + + analysis_file.write_text("analysis", encoding="utf-8") + pistoia_file.write_text("", encoding="utf-8") + loads_csv.write_text("fz_ns1\n200.0\n", encoding="utf-8") + pistoia_csv.write_text("", encoding="utf-8") + write_minimal_n88_model( + model_file, + coords=[ + [0.0, 0.0, 100.0], + [0.0, 0.0, 0.0], + ], + node_sets={ + "body_top": [1], + "body_bottom": [2], + }, + ) + + faim.write_results_csv( + output_file=results_csv, + model_file=model_file, + analysis_file=analysis_file, + pistoia_file=pistoia_file, + loads_csv=loads_csv, + pistoia_csv=pistoia_csv, + analysis_var="fz_ns1", + pistoia_vars=[], + failure_axis="z", + applied_displacement="-2.0", + target_displacement=0.68, + report_profile="spine", + ) + + with results_csv.open(newline="") as handle: + row = next(csv.DictReader(handle)) + + assert list(row) == [ + "model_file", + "analysis_file", + "analysis_var", + "applied_displacement", + "reaction_force_N", + "stiffness_N_per_mm", + "characteristic_length_mm", + ] + assert row["analysis_var"] == "fz_ns1" + assert row["applied_displacement"] == "-2.0" + assert row["reaction_force_N"] == "200.0" + assert row["characteristic_length_mm"] == "100.0" + assert row["stiffness_N_per_mm"] == "100.0" + + +def test_write_results_csv_spine_profile_converts_percent_from_geometry(tmp_path): + analysis_file = tmp_path / "model_analysis.txt" + model_file = tmp_path / "model.n88model" + pistoia_file = tmp_path / "model_pistoia.txt" + loads_csv = tmp_path / "model_loads.csv" + pistoia_csv = tmp_path / "model_pistoia.csv" + results_csv = tmp_path / "model_results.csv" + + analysis_file.write_text("analysis", encoding="utf-8") + pistoia_file.write_text("", encoding="utf-8") + loads_csv.write_text("fz_ns1\n200.0\n", encoding="utf-8") + pistoia_csv.write_text("", encoding="utf-8") + write_minimal_n88_model( + model_file, + coords=[ + [0.0, 0.0, 50.0], + [0.0, 0.0, 0.0], + ], + node_sets={ + "body_top": [1], + "body_bottom": [2], + }, + ) + + faim.write_results_csv( + output_file=results_csv, + model_file=model_file, + analysis_file=analysis_file, + pistoia_file=pistoia_file, + loads_csv=loads_csv, + pistoia_csv=pistoia_csv, + analysis_var="fz_ns1", + pistoia_vars=[], + failure_axis="z", + applied_displacement="-2.0", + target_displacement=0.68, + report_profile="spine", + ) + + with results_csv.open(newline="") as handle: + row = next(csv.DictReader(handle)) + + assert row["characteristic_length_mm"] == "50.0" + assert row["reaction_force_N"] == "200.0" + assert row["stiffness_N_per_mm"] == "100.0" + + +def test_write_results_csv_spine_profile_excludes_pistoia_fields(tmp_path): + analysis_file = tmp_path / "model_analysis.txt" + model_file = tmp_path / "model.n88model" + pistoia_file = tmp_path / "model_pistoia.txt" + loads_csv = tmp_path / "model_loads.csv" + pistoia_csv = tmp_path / "model_pistoia.csv" + results_csv = tmp_path / "model_results.csv" + + analysis_file.write_text("analysis", encoding="utf-8") + pistoia_file.write_text("Critical volume (%): 2.000\n", encoding="utf-8") + loads_csv.write_text("fz_ns1\n200.0\n", encoding="utf-8") + pistoia_csv.write_text("pis_fz_fail,pis_stiffz\n-300.0,120.0\n", encoding="utf-8") + write_minimal_n88_model( + model_file, + coords=[ + [0.0, 0.0, 100.0], + [0.0, 0.0, 0.0], + ], + node_sets={ + "body_top": [1], + "body_bottom": [2], + }, + ) + + faim.write_results_csv( + output_file=results_csv, + model_file=model_file, + analysis_file=analysis_file, + pistoia_file=pistoia_file, + loads_csv=loads_csv, + pistoia_csv=pistoia_csv, + analysis_var="fz_ns1", + pistoia_vars=["pis_fz_fail", "pis_stiffz"], + failure_axis="z", + applied_displacement="-2.0", + target_displacement=0.68, + report_profile="spine", + ) + + with results_csv.open(newline="") as handle: + row = next(csv.DictReader(handle)) + + assert "pistoia_failure_load_N" not in row + assert "pistoia_failure_load_kN" not in row + assert "pistoia_stiffness_N_per_mm" not in row + + +def test_write_results_csv_spine_profile_can_use_constraint_groups(tmp_path): + analysis_file = tmp_path / "model_analysis.txt" + model_file = tmp_path / "model.n88model" + pistoia_file = tmp_path / "model_pistoia.txt" + loads_csv = tmp_path / "model_loads.csv" + pistoia_csv = tmp_path / "model_pistoia.csv" + results_csv = tmp_path / "model_results.csv" + + analysis_file.write_text("analysis", encoding="utf-8") + pistoia_file.write_text("", encoding="utf-8") + loads_csv.write_text("fz_ns1\n200.0\n", encoding="utf-8") + pistoia_csv.write_text("", encoding="utf-8") + write_minimal_n88_model_with_constraints( + model_file, + coords=[ + [0.0, 0.0, 100.0], + [0.0, 0.0, 0.0], + ], + constraints={ + "top_displacement": [1], + "bottom_fixed_z": [2], + }, + ) + + faim.write_results_csv( + output_file=results_csv, + model_file=model_file, + analysis_file=analysis_file, + pistoia_file=pistoia_file, + loads_csv=loads_csv, + pistoia_csv=pistoia_csv, + analysis_var="fz_ns1", + pistoia_vars=[], + failure_axis="z", + applied_displacement="-2.0", + target_displacement=0.68, + report_profile="spine", + ) + + with results_csv.open(newline="") as handle: + row = next(csv.DictReader(handle)) + + assert row["characteristic_length_mm"] == "100.0" + assert row["reaction_force_N"] == "200.0" + + +def test_set_prescribed_displacement_from_percent_updates_constraints(tmp_path): + model_file = tmp_path / "model.n88model" + write_minimal_n88_model_with_constraints( + model_file, + coords=[ + [0.0, 0.0, 100.0], + [0.0, 0.0, 0.0], + ], + constraints={ + "top_displacement": [1], + "bottom_fixed_z": [2], + "convergence_set": [1], + }, + ) + + update = faim.set_prescribed_displacement_from_percent( + model_file, + report_profile="spine", + failure_axis="z", + target_displacement_percent=0.68, + displacement_sign=-1.0, + ) + + assert update["characteristic_length_mm"] == 100.0 + assert update["target_displacement_mm"] == 0.68 + assert update["applied_displacement_mm"] == -0.68 + assert faim.read_prescribed_displacement(model_file, "spine") == -0.68 + + +def test_set_prescribed_displacement_from_percent_updates_femur_constraints(tmp_path): + model_file = tmp_path / "model.n88model" + write_minimal_n88_model_with_constraints( + model_file, + coords=[ + [0.0, 100.0, 0.0], + [0.0, 0.0, 0.0], + ], + constraints={ + "top_displacement": [1], + "bottom_fixed_y_PMMA": [2], + "convergence_set": [1], + }, + ) + + update = faim.set_prescribed_displacement_from_percent( + model_file, + report_profile="femur", + failure_axis="y", + target_displacement_percent=4.0, + displacement_sign=1.0, + ) + + assert update["characteristic_length_mm"] == 100.0 + assert update["target_displacement_mm"] == 4.0 + assert update["applied_displacement_mm"] == 4.0 + assert faim.read_prescribed_displacement(model_file, "femur") == 4.0 + + def test_run_faim_pipeline_dry_run_prints_full_workflow(tmp_path, capsys): model = tmp_path / "model.n88model" model.write_text("", encoding="utf-8") @@ -90,3 +492,23 @@ def test_run_faim_pipeline_dry_run_prints_full_workflow(tmp_path, capsys): assert "n88pistoia" in out assert "n88tabulate -H -d , -V fz_ns1" in out assert "n88tabulate -H -d , -V pis_fz_fail,pis_stiffz" in out + + +def test_run_faim_pipeline_can_skip_pistoia(tmp_path, capsys): + model = tmp_path / "model.n88model" + model.write_text("", encoding="utf-8") + + faim.run_faim_pipeline( + model_file=model, + analysis_var="fy_ns1", + pistoia_vars=["pis_fy_fail", "pis_stiffy"], + failure_axis="y", + run_pistoia=False, + dry_run=True, + compress=False, + ) + + out = capsys.readouterr().out + assert "n88pistoia" not in out + assert "pis_fy_fail,pis_stiffy" not in out + assert "n88tabulate -H -d , -V fy_ns1" in out From c784eb4337340b3869cef21c3a4255ef6ca88176 Mon Sep 17 00:00:00 2001 From: wallematthias Date: Thu, 25 Jun 2026 10:17:55 -0600 Subject: [PATCH 04/67] Clean FEA output naming and references --- ogo/cli/GenerateFEM.py | 10 ++---- ogo/fea/README.md | 66 +++++++++++++++++++++++++++++++----- ogo/fea/femur.py | 44 +++++++++++++++++++----- ogo/fea/sideways_fall.py | 10 +++++- ogo/fea/spine_compression.py | 4 +-- tests/fea/test_femur.py | 12 +++---- 6 files changed, 113 insertions(+), 33 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index d50eeca..d27b0f6 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -161,18 +161,14 @@ def build_femur_command( def expected_spine_model_path(calibrated_image: Path, output_path: Optional[Path], target: SpineTarget) -> Path: """Predict the spine model path written by ``OgoSpineCompressionFe``.""" output_dir = output_path if output_path is not None else calibrated_image.parent - return output_dir / "{}_vertebra_{}_{}.n88model".format( - remove_extension(calibrated_image), - target.body_label, - target.level, - ) + return output_dir / "{}_{}.n88model".format(remove_extension(calibrated_image), target.level) def expected_femur_model_path(calibrated_image: Path, output_path: Optional[Path], side: str) -> Path: """Predict the femur model path written by ``OgoSidewaysFallFe``.""" output_dir = output_path if output_path is not None else calibrated_image.parent - suffix = "_LT_FEMUR_SF" if side == "left" else "_RT_FEMUR_SF" - return output_dir / "{}{}.n88model".format(remove_extension(calibrated_image), suffix) + stem = "LF" if side == "left" else "RF" + return output_dir / "{}_{}.n88model".format(remove_extension(calibrated_image), stem) def option_value(argv: Sequence[str], option: str, default: Optional[str] = None) -> Optional[str]: diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 9b03bcc..114ad73 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -24,8 +24,8 @@ The segmentation depends on the model type: | Hip with trab/cort compartments | Whole-femur mask plus `--compartment_mask` | Compartment mask defaults are cortical `1`, trabecular `2`; override with `--cortical_label` and `--trabecular_label` if needed. | The spine mask must contain both the body and posterior process labels. The -femur workflow currently requires a side-specific run because alignment uses -left and right reference models. +femur workflow currently requires a side-specific run because alignment uses a +side-specific femur reference frame. ## Basic Commands @@ -202,11 +202,59 @@ Maintained spine and femur models write the same columns: | `stiffness_N_per_mm` | Reaction force divided by applied displacement when computable. | | `characteristic_length_mm` | Spine: distance between `body_top` and `body_bottom` centroids along the loading axis. Femur: distance between femoral-head and greater-trochanter PMMA node-set centroids along the loading axis. | -Spine models are solved directly at `0.68%` strain, following the vertebral -strength endpoint reported by Crawford et al. (2003). Femur models are solved -directly at `4%` displacement. The exact percent endpoint and converted -millimeter displacement are recorded in `_modeling.json`; the CSV stays compact -for downstream statistics. +## Reaction Force and Displacement Endpoint + +The reported `reaction_force_N` is the net support reaction from the solved +model at the prescribed displacement endpoint. For spine compression this is +tabulated from the axial `fz_ns1` reaction force. For hip sideways fall this is +tabulated from the lateral `fy_ns1` reaction force. The sign depends on the +model coordinate convention, so the compact CSV reports the force magnitude +used for stiffness and downstream comparisons. + +The reaction force and stiffness in `_results.csv` should be interpreted at +that endpoint. They are the relevant summary values for the chosen model +protocol: the force is the load carried by the model at the target normalized +displacement, and stiffness is the secant stiffness from zero displacement to +that same endpoint. Changing the endpoint can change both values, especially +for nonlinear material settings, so comparisons should use the same model type, +material law, boundary conditions, and displacement endpoint. + +Ogo chooses percent displacement endpoints so models of different physical +size are compared at a similar normalized deformation: + +| Model | Default endpoint | Length used for conversion | Reason | +| --- | --- | --- | --- | +| Spine compression | `0.68%` strain | Distance between `body_top` and `body_bottom` centroids along the loading axis | Matches the vertebral strength endpoint reported by Crawford et al. (2003), and keeps the solved displacement scaled to each vertebral body height. | +| Hip sideways fall | `4%` displacement | Distance between femoral-head and greater-trochanter PMMA node-set centroids along the loading axis | Keeps the sideways-fall load endpoint scaled to each generated femur model instead of using one fixed millimeter displacement for all femora. | + +FAIM/N88 stores prescribed displacement in millimeters, not percent strain. +Before solving, `ogoFEA` converts the percent endpoint to millimeters from the +generated model geometry and updates the `.n88model`. The exact percent +endpoint, converted millimeter displacement, and characteristic length are +recorded in `_modeling.json`; `_results.csv` stays compact for downstream +statistics. `stiffness_N_per_mm` is computed as +`reaction_force_N / applied_displacement`. + +## Reference Models + +The reference models define the common coordinate frame used before meshing and +boundary-condition placement. They are alignment targets, not subject-specific +geometry copied into the final model. + +For spine compression, the bundled vertebral-body reference is an L4 body. The +same reference is used for all requested vertebrae. Before ICP, Ogo estimates +principal-axis lengths for the segmented body and for the L4 reference, scales +the reference to match the target vertebra within the configured scale limits, +and then runs rigid ICP. This lets one L4 reference provide a stable model frame +for other vertebral levels while still adapting to their size. + +For hip sideways fall, the canonical bundled reference is a left femur. Left +femur models ICP-align directly to that reference. Right femur models use +`RT_FEMUR_SIDEWAYS_FALL_REF.vtk` when it is present; otherwise Ogo mirrors the +left femur reference across the x direction in memory and uses that mirrored +surface as the right-side ICP target. The mirrored reference keeps left and +right outputs in matching sideways-fall coordinate frames without requiring a +separate right reference file. ## Spine Model @@ -214,7 +262,7 @@ The spine workflow builds one compression model per `--vertebra` target: 1. Threshold the labelled mask into vertebral body and posterior process. 2. Crop the image and masks to the vertebra. -3. Align the body to the bundled vertebral-body reference using scaled ICP. +3. Align the body to the scaled L4 vertebral-body reference using ICP. 4. Apply transform and isotropic resampling through the shared VTK reslice helper. The default output spacing is `1.0 x 1.0 x 1.0 mm`. 5. Smooth body/process masks with one binary close/open pass only when at @@ -252,7 +300,7 @@ The femur workflow builds one sideways-fall model per side: 1. Read the calibrated image and whole-femur mask. 2. Pre-rotate the side to a stable starting orientation. -3. ICP-align to the bundled left or right femur reference. +3. ICP-align to the left femur reference or the mirrored right reference. 4. Apply transform and isotropic resampling through the shared VTK reslice helper. The default output spacing is `1.0 x 1.0 x 1.0 mm`. 5. Smooth the transformed femur mask with one binary close/open pass only when diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 21455e9..61d76dc 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -32,6 +32,8 @@ trabecular-style region. If supplied, cortical=1 and trabecular=2 by default. """ +from pathlib import Path + LEFT_FEMUR = 1 RIGHT_FEMUR = 2 @@ -61,11 +63,11 @@ def side_suffix(femur_side): - """Return the legacy output suffix for a femur side.""" + """Return the compact output stem for a femur side.""" if femur_side == LEFT_FEMUR: - return "_LT_FEMUR_SF" + return "LF" if femur_side == RIGHT_FEMUR: - return "_RT_FEMUR_SF" + return "RF" raise ValueError("femur_side must be 1 for left or 2 for right.") @@ -79,11 +81,37 @@ def side_rotation(femur_side): def sideways_fall_output_name(output_file, femur_side): - """Apply the legacy left/right suffix to a sideways-fall output path.""" - suffix = side_suffix(femur_side) - if str(output_file).endswith(".n88model"): - return str(output_file).replace(".n88model", f"{suffix}.n88model") - return f"{output_file}{suffix}.n88model" + """Return the compact side-specific sideways-fall output path.""" + output_path = Path(output_file) + return str(output_path.with_name(f"{output_path.stem}_{side_suffix(femur_side)}.n88model")) + + +def mirror_polydata_x(polydata): + """Return a left/right mirrored copy of polydata around its x-bounds center.""" + import vtk + + bounds = polydata.GetBounds() + center_x = (bounds[0] + bounds[1]) / 2.0 + + transform = vtk.vtkTransform() + transform.Translate(center_x, 0.0, 0.0) + transform.Scale(-1.0, 1.0, 1.0) + transform.Translate(-center_x, 0.0, 0.0) + + transform_filter = vtk.vtkTransformPolyDataFilter() + transform_filter.SetInputData(polydata) + transform_filter.SetTransform(transform) + transform_filter.Update() + + reverse = vtk.vtkReverseSense() + reverse.SetInputData(transform_filter.GetOutput()) + reverse.ReverseCellsOn() + reverse.ReverseNormalsOn() + reverse.Update() + + mirrored = vtk.vtkPolyData() + mirrored.DeepCopy(reverse.GetOutput()) + return mirrored def tilted_side_support_vector(angle_degrees=-20): diff --git a/ogo/fea/sideways_fall.py b/ogo/fea/sideways_fall.py index 3660a41..1a901ec 100644 --- a/ogo/fea/sideways_fall.py +++ b/ogo/fea/sideways_fall.py @@ -58,6 +58,7 @@ expand_xz_footprint, femur_z_coverage, flat_crop_vtk_image_below_z, + mirror_polydata_x, side_rotation, sideways_fall_output_name, standardize_femur_shaft_length, @@ -194,7 +195,14 @@ def sidewaysFallFe(args): if femur_side == 1: ref_poly = ogo.readPolyData(left_femur_reference) elif femur_side == 2: - ref_poly = ogo.readPolyData(right_femur_reference) + if os.path.exists(right_femur_reference): + ref_poly = ogo.readPolyData(right_femur_reference) + else: + ogo.message( + "Right femur reference not found; mirroring left reference in x:", + right_femur_reference, + ) + ref_poly = mirror_polydata_x(ogo.readPolyData(left_femur_reference)) else: print("Error: Femur Side not defined. Terminating...") sys.exit() diff --git a/ogo/fea/spine_compression.py b/ogo/fea/spine_compression.py index 9dee1aa..e4aa281 100644 --- a/ogo/fea/spine_compression.py +++ b/ogo/fea/spine_compression.py @@ -1167,9 +1167,9 @@ def main(): output_dir = os.path.abspath(output_dir) if args.appendix is None: - output_file = os.path.join(output_dir, f"{basename}_vertebra_{args.mask_threshold}.n88model") + output_file = os.path.join(output_dir, f"{basename}_{args.mask_threshold}.n88model") else: - output_file = os.path.join(output_dir, f"{basename}_vertebra_{args.mask_threshold}_{args.appendix}.n88model") + output_file = os.path.join(output_dir, f"{basename}_{args.appendix}.n88model") ogo.message(f'N88Model File path: {output_file}') # Set default reference path if not provided diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 832848a..de5abca 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -3,16 +3,16 @@ from ogo.fea import femur -def test_side_suffix_and_rotation_match_legacy_outputs(): - assert femur.side_suffix(1) == "_LT_FEMUR_SF" - assert femur.side_suffix(2) == "_RT_FEMUR_SF" +def test_side_suffix_and_rotation_match_compact_outputs(): + assert femur.side_suffix(1) == "LF" + assert femur.side_suffix(2) == "RF" assert femur.side_rotation(1) == 90 assert femur.side_rotation(2) == -90 -def test_sideways_fall_output_name_preserves_legacy_pattern(): - assert femur.sideways_fall_output_name("density.n88model", 1) == "density_LT_FEMUR_SF.n88model" - assert femur.sideways_fall_output_name("density.n88model", 2) == "density_RT_FEMUR_SF.n88model" +def test_sideways_fall_output_name_uses_compact_side_suffix(): + assert femur.sideways_fall_output_name("density.n88model", 1) == "density_LF.n88model" + assert femur.sideways_fall_output_name("density.n88model", 2) == "density_RF.n88model" def test_invalid_femur_side_is_rejected(): From 689df8faeb69810165e1bf35b0963c10647ba935 Mon Sep 17 00:00:00 2001 From: wallematthias Date: Thu, 25 Jun 2026 10:34:29 -0600 Subject: [PATCH 05/67] Update FEA CLI naming test expectations --- tests/cli/test_GenerateFEM.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index f5b1546..9507b35 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -113,8 +113,8 @@ def test_spine_runs_each_requested_vertebra(monkeypatch, solve_calls): ], ] assert solve_calls == [ - (Path("models/density_vertebra_2_L1.n88model"), "spine", calls[0]), - (Path("models/density_vertebra_4_T12.n88model"), "spine", calls[1]), + (Path("models/density_L1.n88model"), "spine", calls[0]), + (Path("models/density_T12.n88model"), "spine", calls[1]), ] @@ -129,8 +129,8 @@ def test_hip_defaults_to_both_sides(monkeypatch, solve_calls): ["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "2", "--output_path", "models"], ] assert solve_calls == [ - (Path("models/density_LT_FEMUR_SF.n88model"), "hip", calls[0]), - (Path("models/density_RT_FEMUR_SF.n88model"), "hip", calls[1]), + (Path("models/density_LF.n88model"), "hip", calls[0]), + (Path("models/density_RF.n88model"), "hip", calls[1]), ] @@ -141,7 +141,7 @@ def test_hip_can_run_one_side(monkeypatch, solve_calls): GenerateFEM.main(["hip", "density.nii.gz", "hip_mask.nii.gz", "--side", "right"]) assert calls == [["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "2"]] - assert solve_calls == [(Path("density_RT_FEMUR_SF.n88model"), "hip", calls[0])] + assert solve_calls == [(Path("density_RF.n88model"), "hip", calls[0])] def test_dry_run_prints_commands_without_running(monkeypatch, capsys): From 8b0f4ca5e58504a3f05f450f80db5b500b04a47c Mon Sep 17 00:00:00 2001 From: wallematthias Date: Thu, 25 Jun 2026 11:07:41 -0600 Subject: [PATCH 06/67] Improve femur crop safety and CI test runner --- .github/workflows/main.yml | 4 +- ogo/fea/femur.py | 158 ++++++++++++++++++++++++++++++++++++- ogo/fea/sideways_fall.py | 36 +++++++++ tests/fea/test_femur.py | 116 +++++++++++++++++++++++++++ 4 files changed, 310 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3f809bb..f7eede8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -41,9 +41,9 @@ jobs: auto-activate-base: false - name: Create and install dependencies run: | - conda install -n test n88tools matplotlib pbr nose six simpleitk pydicom gdcm pandas + conda install -n test n88tools matplotlib pbr pytest six simpleitk pydicom gdcm pandas conda activate test pip install -e . - name: Run tests run: | - nosetests tests + python -m pytest tests diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 61d76dc..27cf126 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -46,6 +46,7 @@ FEMORAL_HEAD_FIXTURE_LONG_AXIS_EXTENSION_MM = 80.0 DEFAULT_PMMA_THICKNESS_MM = 6.0 DEFAULT_PMMA_INTRUSION_MM = 6.0 +DEFAULT_FEMUR_INPUT_MARGIN_MM = DEFAULT_PMMA_THICKNESS_MM + DEFAULT_PMMA_INTRUSION_MM DEFAULT_FEMUR_SHAFT_LENGTH_MM = 100.0 DEFAULT_FEMUR_CUT_MODE = "lesser_trochanter" DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM = 50.0 @@ -222,6 +223,114 @@ def femur_z_profile(vtk_mask): } +def femoral_head_side_z_bounds(vtk_mask, *, y_fraction=0.2): + """Estimate the z extent of the femoral-head-side y band in an aligned femur mask.""" + import numpy as np + + from ogo.util.vtk_image import vtk_image_to_numpy + + data = vtk_image_to_numpy(vtk_mask) != 0 + coords = np.array(np.where(data)) + if coords.size == 0: + raise ValueError("Cannot estimate femoral-head-side bounds from an empty mask.") + + dims = vtk_mask.GetDimensions() + extent = vtk_mask.GetExtent() + spacing = vtk_mask.GetSpacing() + origin = vtk_mask.GetOrigin() + y = origin[1] + (np.arange(dims[1]) + extent[2]) * spacing[1] + z = origin[2] + (np.arange(dims[2]) + extent[4]) * spacing[2] + + y_min = float(y[coords[1].min()]) + y_max = float(y[coords[1].max()]) + y_limit = y_min + (y_max - y_min) * float(y_fraction) + head_side = data & (y[None, :, None] >= y_min) & (y[None, :, None] <= y_limit) + head_coords = np.array(np.where(head_side)) + if head_coords.size == 0: + raise ValueError("Cannot estimate femoral-head-side bounds from an empty y band.") + + return { + "y_min": y_min, + "y_max": y_limit, + "z_min": float(z[head_coords[2].min()]), + "z_max": float(z[head_coords[2].max()]), + } + + +def pad_vtk_images_to_foreground_margin( + vtk_images, + vtk_mask, + *, + margin_mm=DEFAULT_FEMUR_INPUT_MARGIN_MM, + constants=None, +): + """Pad images so femur foreground has a physical margin to every extent face.""" + import math + import numpy as np + import vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + images = list(vtk_images) + constants = [0] * len(images) if constants is None else list(constants) + if len(constants) != len(images): + raise ValueError("constants must match vtk_images length.") + + margin_mm = max(0.0, float(margin_mm)) + if margin_mm == 0.0: + return images, {"lower": (0, 0, 0), "upper": (0, 0, 0), "margin_mm": 0.0} + + mask = vtk_image_to_numpy(vtk_mask) != 0 + coords = np.array(np.where(mask)) + if coords.size == 0: + raise ValueError("Cannot pad femur images from an empty mask.") + + dims = np.array(mask.shape, dtype=int) + mins = coords.min(axis=1) + maxs = coords.max(axis=1) + spacing = vtk_mask.GetSpacing() + margin_voxels = np.array( + [max(0, int(math.ceil(margin_mm / max(float(value), 1.0e-6)))) for value in spacing], + dtype=int, + ) + lower = np.maximum(0, margin_voxels - mins) + upper = np.maximum(0, margin_voxels - ((dims - 1) - maxs)) + if not np.any(lower) and not np.any(upper): + return images, { + "lower": tuple(int(v) for v in lower), + "upper": tuple(int(v) for v in upper), + "margin_mm": margin_mm, + "margin_voxels": tuple(int(v) for v in margin_voxels), + } + + extent = vtk_mask.GetExtent() + output_extent = ( + int(extent[0] - lower[0]), + int(extent[1] + upper[0]), + int(extent[2] - lower[1]), + int(extent[3] + upper[1]), + int(extent[4] - lower[2]), + int(extent[5] + upper[2]), + ) + padded = [] + for image, constant in zip(images, constants): + pad = vtk.vtkImageConstantPad() + pad.SetInputData(image) + pad.SetOutputWholeExtent(output_extent) + pad.SetConstant(float(constant)) + pad.Update() + out = vtk.vtkImageData() + out.DeepCopy(pad.GetOutput()) + padded.append(out) + + return padded, { + "lower": tuple(int(v) for v in lower), + "upper": tuple(int(v) for v in upper), + "margin_mm": margin_mm, + "margin_voxels": tuple(int(v) for v in margin_voxels), + } + + def _smooth_profile(values, window=7): """Smooth one profile with an odd moving-average window.""" import numpy as np @@ -237,10 +346,31 @@ def _smooth_profile(values, window=7): return np.convolve(padded, np.ones(window, dtype=float) / float(window), mode="valid") +def _peak_center_z(z, values, indices, *, relative_height=0.95): + """Return the center of the high plateau around a dominant local peak.""" + import numpy as np + + peak_index = int(indices[np.argmax(values[indices])]) + threshold = float(values[peak_index]) * float(relative_height) + left = peak_index + right = peak_index + valid = set(int(i) for i in indices) + while left - 1 in valid and values[left - 1] >= threshold: + left -= 1 + while right + 1 in valid and values[right + 1] >= threshold: + right += 1 + plateau = np.arange(left, right + 1, dtype=int) + weights = np.maximum(values[plateau], 0.0) + if float(weights.sum()) <= 0.0: + return peak_index, float(z[peak_index]) + return peak_index, float(np.average(z[plateau], weights=weights)) + + def detect_lesser_trochanter_cut_z( vtk_mask, *, distal_offset_mm=0.0, + distal_offset_percent=None, min_distal_to_greater_mm=8.0, max_distal_to_greater_mm=45.0, min_distal_coverage_mm=0.0, @@ -286,8 +416,9 @@ def detect_lesser_trochanter_cut_z( "the lesser trochanter." ) - lesser_index = int(distal_indices[np.argmax(area[distal_indices])]) - lesser_z = float(z[lesser_index]) + lesser_index, lesser_z = _peak_center_z(z, area, distal_indices) + if distal_offset_percent is not None: + distal_offset_mm = abs(greater_z - lesser_z) * float(distal_offset_percent) / 100.0 cut_z = lesser_z - float(distal_offset_mm) required_z_min = cut_z - float(min_distal_coverage_mm) @@ -317,6 +448,7 @@ def detect_lesser_trochanter_cut_z( "mask_z_max": z_max, "retained_length_mm": float(z_max - cut_z), "distal_offset_mm": float(distal_offset_mm), + "distal_offset_percent": None if distal_offset_percent is None else float(distal_offset_percent), "lesser_area": float(area[lesser_index]), "shaft_area_median": shaft_area, } @@ -346,6 +478,7 @@ def standardize_femur_shaft_length( retained_length_mm=None, cut_mode="lesser_trochanter", lesser_trochanter_distal_offset_mm=50.0, + lesser_trochanter_distal_offset_percent=None, ): """Apply a flat distal crop to aligned femur image and mask. @@ -358,6 +491,7 @@ def standardize_femur_shaft_length( meta = detect_lesser_trochanter_cut_z( mask_vtk, distal_offset_mm=lesser_trochanter_distal_offset_mm, + distal_offset_percent=lesser_trochanter_distal_offset_percent, ) cut_z = meta["cut_z"] retained_length_mm = meta["retained_length_mm"] @@ -373,6 +507,25 @@ def standardize_femur_shaft_length( raise ValueError("cut_mode must be 'lesser_trochanter' or 'fixed_length'.") z_min, z_max = femur_z_coverage(mask_vtk) + warnings = [] + try: + head_side_bounds = femoral_head_side_z_bounds(mask_vtk) + except ValueError: + head_side_bounds = None + if head_side_bounds is not None: + head_side_z_min = float(head_side_bounds["z_min"]) + if float(cut_z) > head_side_z_min + 2.0: + warnings.append( + "distal cut plane appears to enter the femoral-head-side region " + f"(cut_z={float(cut_z):.3f}, head_side_z_min={head_side_z_min:.3f})" + ) + meta["femoral_head_side_z_min"] = head_side_z_min + meta["femoral_head_side_z_max"] = float(head_side_bounds["z_max"]) + if retained_length_mm is not None and float(retained_length_mm) < 40.0: + warnings.append( + "retained proximal femur length is unusually short " + f"({float(retained_length_mm):.3f} mm)" + ) if z_min > cut_z: available = max(0.0, float(reference_bounds[5]) - z_min) raise ValueError( @@ -387,6 +540,7 @@ def standardize_femur_shaft_length( "mask_z_max": z_max, "retained_length_mm": float(retained_length_mm), "cut_mode": cut_mode, + "warnings": warnings, } ) return ( diff --git a/ogo/fea/sideways_fall.py b/ogo/fea/sideways_fall.py index 1a901ec..b0623b8 100644 --- a/ogo/fea/sideways_fall.py +++ b/ogo/fea/sideways_fall.py @@ -41,6 +41,7 @@ DEFAULT_CORTICAL_LABEL, DEFAULT_FEMUR_FE_DISPLACEMENT, DEFAULT_FEMUR_CUT_MODE, + DEFAULT_FEMUR_INPUT_MARGIN_MM, DEFAULT_FEMUR_ISO_RESOLUTION_MM, DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, DEFAULT_FEMUR_SHAFT_LENGTH_MM, @@ -59,6 +60,7 @@ femur_z_coverage, flat_crop_vtk_image_below_z, mirror_polydata_x, + pad_vtk_images_to_foreground_margin, side_rotation, sideways_fall_output_name, standardize_femur_shaft_length, @@ -106,6 +108,8 @@ def sidewaysFallFe(args): femur_shaft_length = args.femur_shaft_length femur_cut_mode = args.femur_cut_mode femur_lesser_trochanter_distal_offset = args.femur_lesser_trochanter_distal_offset + femur_lesser_trochanter_distal_offset_percent = args.femur_lesser_trochanter_distal_offset_percent + femur_input_margin = args.femur_input_margin cortical_label = args.cortical_label trabecular_label = args.trabecular_label mask_smoothing_spacing_threshold = args.mask_smoothing_spacing_threshold @@ -159,11 +163,17 @@ def sidewaysFallFe(args): ogo.message("Trabecular Label: %d" % trabecular_label) if femur_cut_mode == "fixed_length": ogo.message("Retained Proximal Femur Length [mm]: %8.4f" % femur_shaft_length) + elif femur_lesser_trochanter_distal_offset_percent is not None: + ogo.message( + "Lesser Trochanter Distal Cut Offset [%% greater-lesser distance]: %8.4f" + % femur_lesser_trochanter_distal_offset_percent + ) else: ogo.message( "Lesser Trochanter Distal Cut Offset [mm]: %8.4f" % femur_lesser_trochanter_distal_offset ) + ogo.message("Input Foreground Safety Margin [mm]: %8.4f" % femur_input_margin) ## # Read input image @@ -181,6 +191,25 @@ def sidewaysFallFe(args): ogo.message("Reading trabecular/cortical compartment mask...") compartmentData = ogo.readNii(compartment_mask) + images_to_pad = [imageData, maskThres] + ([compartmentData] if compartmentData is not None else []) + padded_images, padding = pad_vtk_images_to_foreground_margin( + images_to_pad, + maskThres, + margin_mm=femur_input_margin, + constants=[0] * len(images_to_pad), + ) + imageData = padded_images[0] + maskThres = padded_images[1] + if compartmentData is not None: + compartmentData = padded_images[2] + if any(padding["lower"]) or any(padding["upper"]): + ogo.message( + "Padded input image extent by lower=%s upper=%s voxels for FE transform safety." + % (padding["lower"], padding["upper"]) + ) + else: + ogo.message("Input image already has sufficient foreground safety margin.") + ## # Pre-rotate the image and mask for better alignment image_rot, mask_rot = ogo.preRotateImage(imageData, maskThres, rot_z) @@ -243,6 +272,7 @@ def sidewaysFallFe(args): retained_length_mm=femur_shaft_length, cut_mode=femur_cut_mode, lesser_trochanter_distal_offset_mm=femur_lesser_trochanter_distal_offset, + lesser_trochanter_distal_offset_percent=femur_lesser_trochanter_distal_offset_percent, ) except ValueError as exc: ogo.message(str(exc)) @@ -257,6 +287,8 @@ def sidewaysFallFe(args): retained_length_mm, ) ) + for warning in shaft_crop.get("warnings", []): + ogo.message("WARNING: Femur crop QC: %s" % warning) if femur_cut_mode == "lesser_trochanter": ogo.message( "Detected greater trochanter z=%8.4f; lesser trochanter z=%8.4f" @@ -713,6 +745,10 @@ def main(): help="Set the distal femur crop. Default detects the lesser trochanter and fails if the field of view is incomplete. (default: %(default)s)") parser.add_argument("--femur_lesser_trochanter_distal_offset", type=float, default=DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, help="Cut this many mm distal to the detected lesser trochanter in lesser_trochanter mode. (default: %(default)s [mm])") + parser.add_argument("--femur_lesser_trochanter_distal_offset_percent", type=float, default=None, + help="Optional percentage of the detected greater-to-lesser trochanter z distance to cut distal to the lesser trochanter. Overrides --femur_lesser_trochanter_distal_offset when set. (default: %(default)s)") + parser.add_argument("--femur_input_margin", type=float, default=DEFAULT_FEMUR_INPUT_MARGIN_MM, + help="Pad the input image/mask as needed so femur foreground has this margin before pre-rotation and ICP. (default: %(default)s [mm])") parser.add_argument("--compartment_mask", type=str, default=None, help="Optional trabecular/cortical compartment mask aligned with the bone mask. Defaults: cortical=1, trabecular=2.") parser.add_argument("--cortical_label", type=int, default=DEFAULT_CORTICAL_LABEL, diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index de5abca..1d3af5c 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -99,10 +99,89 @@ def test_flat_femur_shaft_crop_zeroes_voxels_below_cut(): out = vtk_to_numpy(cropped.GetPointData().GetScalars()).reshape((3, 3, 5), order="F") assert meta["cut_z"] == pytest.approx(2) + assert not any("femoral-head-side region" in warning for warning in meta["warnings"]) assert not np.any(out[:, :, :2]) assert np.all(out[:, :, 2:]) +def test_femur_crop_warns_when_cut_enters_head_side_region(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + data = np.zeros((40, 40, 100), dtype=np.uint8) + data[:, 0:8, 20:70] = 1 + data[:, 8:25, 10:95] = 1 + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(0, 0, 0) + image.SetSpacing(1, 1, 1) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + + _, _, meta = femur.standardize_femur_shaft_length( + image, + image, + reference_bounds=(0, 0, 0, 0, 0, 90), + retained_length_mm=30, + cut_mode="fixed_length", + ) + + assert any("femoral-head-side region" in warning for warning in meta["warnings"]) + assert any("unusually short" in warning for warning in meta["warnings"]) + + +def test_femur_crop_does_not_warn_short_length_above_threshold(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + data = np.zeros((40, 40, 100), dtype=np.uint8) + data[:, 10:30, 20:95] = 1 + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(0, 0, 0) + image.SetSpacing(1, 1, 1) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + + _, _, meta = femur.standardize_femur_shaft_length( + image, + image, + reference_bounds=(0, 0, 0, 0, 0, 90), + retained_length_mm=56, + cut_mode="fixed_length", + ) + + assert not any("unusually short" in warning for warning in meta["warnings"]) + + +def test_femur_input_padding_adds_only_missing_foreground_margin(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + + data = np.zeros((5, 6, 7), dtype=np.uint8) + data[0:3, 2:5, 0:4] = 1 + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(0, 0, 0) + image.SetSpacing(1, 2, 1) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + + padded, meta = femur.pad_vtk_images_to_foreground_margin([image], image, margin_mm=2) + out = vtk_to_numpy(padded[0].GetPointData().GetScalars()).reshape( + padded[0].GetDimensions(), + order="F", + ) + coords = np.array(np.where(out != 0)) + + assert meta["lower"] == (2, 0, 2) + assert meta["upper"] == (0, 0, 0) + assert tuple(coords.min(axis=1)) == (2, 2, 2) + assert out.shape[0] - 1 - coords.max(axis=1)[0] >= 2 + assert out.shape[1] - 1 - coords.max(axis=1)[1] >= 1 + assert out.shape[2] - 1 - coords.max(axis=1)[2] >= 2 + + def test_lesser_trochanter_cut_uses_distal_area_peak(): np = pytest.importorskip("numpy") vtk = pytest.importorskip("vtk") @@ -135,6 +214,43 @@ def test_lesser_trochanter_cut_uses_distal_area_peak(): assert meta["cut_z"] == pytest.approx(61) +def test_lesser_trochanter_cut_uses_peak_plateau_center_and_percent_offset(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + data = np.zeros((80, 90, 100), dtype=np.uint8) + x = np.arange(data.shape[0])[:, None] + y = np.arange(data.shape[1])[None, :] + for z in range(10, 96): + radius = 10 + y_center = 35 + if 54 <= z <= 62: + radius = 20 + if 78 <= z <= 82: + y_center = 58 + radius = 12 + section = ((x - 40) ** 2 + (y - y_center) ** 2) <= radius**2 + data[:, :, z] = section.astype(np.uint8) + + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(0, 0, 0) + image.SetSpacing(1, 1, 1) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + + meta = femur.detect_lesser_trochanter_cut_z( + image, + distal_offset_percent=50.0, + max_distal_to_greater_mm=45.0, + ) + + assert meta["greater_trochanter_z"] == pytest.approx(80) + assert meta["lesser_trochanter_z"] == pytest.approx(58) + assert meta["distal_offset_mm"] == pytest.approx(11) + assert meta["cut_z"] == pytest.approx(47) + + def test_cortical_compartment_mask_uses_default_labels(): np = pytest.importorskip("numpy") vtk = pytest.importorskip("vtk") From 1899c89315f1c3dd6a23f12ff4bae83aa29549bb Mon Sep 17 00:00:00 2001 From: wallematthias Date: Thu, 25 Jun 2026 11:12:59 -0600 Subject: [PATCH 07/67] Cache CI conda environment --- .github/workflows/main.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7eede8..806d904 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,9 +39,17 @@ jobs: activate-environment: test channels: numerics88,simpleitk,conda-forge auto-activate-base: false + - uses: actions/cache@v3 + id: conda-env-cache + with: + path: ${{ env.CONDA }}/envs/test + key: conda-env-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.github/workflows/main.yml') }} - name: Create and install dependencies + if: steps.conda-env-cache.outputs.cache-hit != 'true' run: | conda install -n test n88tools matplotlib pbr pytest six simpleitk pydicom gdcm pandas + - name: Install package + run: | conda activate test pip install -e . - name: Run tests From 1344e0cd7593d67eede270c02c7009356608d196 Mon Sep 17 00:00:00 2001 From: wallematthias Date: Thu, 25 Jun 2026 11:15:40 -0600 Subject: [PATCH 08/67] Revert "Cache CI conda environment" This reverts commit 1899c89315f1c3dd6a23f12ff4bae83aa29549bb. --- .github/workflows/main.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 806d904..f7eede8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,17 +39,9 @@ jobs: activate-environment: test channels: numerics88,simpleitk,conda-forge auto-activate-base: false - - uses: actions/cache@v3 - id: conda-env-cache - with: - path: ${{ env.CONDA }}/envs/test - key: conda-env-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.github/workflows/main.yml') }} - name: Create and install dependencies - if: steps.conda-env-cache.outputs.cache-hit != 'true' run: | conda install -n test n88tools matplotlib pbr pytest six simpleitk pydicom gdcm pandas - - name: Install package - run: | conda activate test pip install -e . - name: Run tests From cd6b158190c48e599d1c69b0407c0ec2f0b5f0ad Mon Sep 17 00:00:00 2001 From: wallematthias Date: Thu, 25 Jun 2026 12:09:52 -0600 Subject: [PATCH 09/67] Use cached conda OpenCV CI setup --- .github/workflows/main.yml | 10 +++++++++- requirements.txt | 3 +-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f7eede8..9d1cf13 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,9 +39,17 @@ jobs: activate-environment: test channels: numerics88,simpleitk,conda-forge auto-activate-base: false + - uses: actions/cache@v3 + id: conda-env-cache + with: + path: ${{ env.CONDA }}/envs/test + key: conda-env-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('.github/workflows/main.yml') }} - name: Create and install dependencies + if: steps.conda-env-cache.outputs.cache-hit != 'true' + run: | + conda install -n test n88tools matplotlib opencv pbr pytest six simpleitk pydicom gdcm pandas + - name: Install package run: | - conda install -n test n88tools matplotlib pbr pytest six simpleitk pydicom gdcm pandas conda activate test pip install -e . - name: Run tests diff --git a/requirements.txt b/requirements.txt index 0d4e223..d7e49dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,6 @@ click # Image processing simpleitk numpy<2.0 -opencv-python<4.12 matplotlib # Statistics @@ -19,4 +18,4 @@ Sphinx # Testings nose -flake8 \ No newline at end of file +flake8 From 7449c8bdeab3c0eb24e7408f179d9169c9006725 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Sun, 28 Jun 2026 20:32:53 -0600 Subject: [PATCH 10/67] Implement fixed-thickness spine cap workflow --- ogo/cli/GenerateFEM.py | 37 ++- ogo/fea/README.md | 19 ++ ogo/fea/boundary.py | 355 ++++++++++++++++++++++++++-- ogo/fea/femur.py | 8 +- ogo/fea/model.py | 78 ++++++ ogo/fea/sideways_fall.py | 4 +- ogo/fea/spine.py | 15 +- ogo/fea/spine_compression.py | 47 +++- tests/cli/test_GenerateFEM.py | 5 + tests/fea/test_boundary.py | 158 ++++++++++++- tests/fea/test_spine.py | 7 +- tests/fea/test_spinefe_benchmark.py | 67 ++++++ 12 files changed, 761 insertions(+), 39 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index d27b0f6..0be6c6f 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -19,9 +19,11 @@ DEFAULT_SPINE_FE_DISPLACEMENT_MM, DEFAULT_SPINE_ISO_RESOLUTION_MM, DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + DEFAULT_SPINE_PMMA_INTRUSION_MM, DEFAULT_SPINE_PMMA_THICKNESS_MM, DEFAULT_SPINE_REGISTRATION_MAX_SCALE, DEFAULT_SPINE_REGISTRATION_MIN_SCALE, + DEFAULT_SPINE_REGISTRATION_BACKEND, DEFAULT_SPINE_TARGET_DISPLACEMENT_PERCENT, SPINE_ALIGNMENT_METHOD, default_spine_reference_path, @@ -408,6 +410,11 @@ def write_modeling_metadata( "--registration_max_scale", DEFAULT_SPINE_REGISTRATION_MAX_SCALE, ), + "registration_backend": option_value( + generator_argv, + "--registration_backend", + DEFAULT_SPINE_REGISTRATION_BACKEND, + ), }, "image_processing": { "iso_resolution_mm": option_float(generator_argv, "--iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM), @@ -444,7 +451,7 @@ def write_modeling_metadata( "body": "threshold body label", "process": "threshold process label", "cortical": "density/surface-derived cortical shell generated after alignment", - "pmma_caps": "fit-to-body superior and inferior caps", + "pmma_caps": "fixed-thickness anatomy superior and inferior caps", }, }, "materials": { @@ -493,14 +500,34 @@ def write_modeling_metadata( "superior_cap": { "label_id": option_int(generator_argv, "--top_node_set_id", 4), "node_set": "body_top", - "shape": "fit to vertebral body surface", - "pmma_thickness_mm": option_float(generator_argv, "--pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM), + "shape": "fixed-thickness anatomy cap", + "pmma_thickness_mm": option_float( + generator_argv, "--pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM + ), + "pmma_intrusion_mm": option_float( + generator_argv, "--pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM + ), + "meaning": ( + "fixed-thickness anatomy cap: pmma_thickness_mm is total cap thickness; " + "pmma_intrusion_mm controls how far anatomy can occupy that fixed " + "thickness without overwriting body bone" + ), }, "inferior_cap": { "label_id": option_int(generator_argv, "--bottom_node_set_id", 3), "node_set": "body_bottom", - "shape": "fit to vertebral body surface", - "pmma_thickness_mm": option_float(generator_argv, "--pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM), + "shape": "fixed-thickness anatomy cap", + "pmma_thickness_mm": option_float( + generator_argv, "--pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM + ), + "pmma_intrusion_mm": option_float( + generator_argv, "--pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM + ), + "meaning": ( + "fixed-thickness anatomy cap: pmma_thickness_mm is total cap thickness; " + "pmma_intrusion_mm controls how far anatomy can occupy that fixed " + "thickness without overwriting body bone" + ), }, }, "constraints": [ diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 114ad73..4b66c2a 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -444,6 +444,25 @@ ogoFEA hip image.nii.gz femur_mask.nii.gz \ --pmma_intrusion 6 ``` +For spine compression, `--pmma_thick` controls the total generated disk +thickness and `--pmma_intrusion` controls how far anatomy can occupy that fixed +thickness. The maintained spine defaults are 10 mm thickness and 6 mm intrusion. +Intrusion does not overwrite vertebral-body voxels; it limits how far from the +superior/inferior body surface the cap is allowed to search for supporting +anatomy before generating the flat PMMA disk. + +After vtkbone identifies visible cap nodes, spine compression filters each cap +node set to the dominant coordinate plane along the load axis. This removes +small rim/contact-side nodes from the jelly-bean disk surface while keeping the +full disk material geometry in the solved model. + +```bash +ogoFEA spine image.nii.gz labels.nii.gz \ + --vertebra L1:20:48 \ + --pmma_thick 10 \ + --pmma_intrusion 6 +``` + Run model generation only, inspect the `.n88model`, then solve later: ```bash diff --git a/ogo/fea/boundary.py b/ogo/fea/boundary.py index 44e61aa..ff4c9ae 100644 --- a/ogo/fea/boundary.py +++ b/ogo/fea/boundary.py @@ -81,6 +81,18 @@ def _expanded_bbox(mask, extrusion_axis, direction, thickness): return tuple(bb_list), bb +def _expanded_bbox_for_fixed_thickness_fixture(mask, extrusion_axis, thickness, intrusion): + """Return a crop wide enough for a fixture that may straddle the bone surface.""" + bb = _bounding_box(mask) + bb_list = list(bb) + margin = max(int(thickness), int(intrusion), 0) + bb_list[extrusion_axis] = slice( + max(bb[extrusion_axis].start - margin, 0), + min(bb[extrusion_axis].stop + margin, mask.shape[extrusion_axis]), + ) + return tuple(bb_list), bb + + def _contact_surface(mask, extrusion_axis, direction): import numpy as np @@ -224,6 +236,272 @@ def _projected_footprint(surface, extrusion_axis, shape): raise ValueError("shape must be 'fit', 'box', or 'round'.") +def _selector_for_material_column(lateral, extrusion_axis): + """Build an index selector for one column running along the extrusion axis.""" + selector = [slice(None), slice(None), slice(None)] + lateral_iter = iter(lateral) + for axis in range(3): + if axis != extrusion_axis: + selector[axis] = next(lateral_iter) + return selector + + +def _largest_planar_component(mask): + """Keep the largest connected region in a 2D projected footprint.""" + import numpy as np + + values = np.asarray(mask, dtype=bool) + visited = np.zeros(values.shape, dtype=bool) + best = [] + for start_array in np.argwhere(values): + start = tuple(int(v) for v in start_array) + if visited[start]: + continue + component = [] + queue = [start] + visited[start] = True + while queue: + coord = queue.pop(0) + component.append(coord) + for axis in range(2): + for offset in (-1, 1): + neighbor = [coord[0], coord[1]] + neighbor[axis] += offset + if neighbor[axis] < 0 or neighbor[axis] >= values.shape[axis]: + continue + token = tuple(neighbor) + if visited[token] or not values[token]: + continue + visited[token] = True + queue.append(token) + if len(component) > len(best): + best = component + out = np.zeros(values.shape, dtype=bool) + if best: + coords = tuple(np.asarray(best, dtype=np.int64).T) + out[coords] = True + return out + + +def _fill_short_boolean_gaps_in_line(line, max_gap): + """Fill short False runs when they are bracketed by True values.""" + import numpy as np + + out = np.asarray(line, dtype=bool).copy() + false_runs = np.flatnonzero(~out) + if false_runs.size == 0: + return out + start = 0 + while start < false_runs.size: + stop = start + 1 + while stop < false_runs.size and false_runs[stop] == false_runs[stop - 1] + 1: + stop += 1 + run = false_runs[start:stop] + if ( + run.size <= int(max_gap) + and int(run[0]) > 0 + and int(run[-1]) < out.size - 1 + and out[int(run[0]) - 1] + and out[int(run[-1]) + 1] + ): + out[run] = True + start = stop + return out + + +def _fill_short_boolean_gaps_in_footprint(values, max_gap=2): + """Fill small row-wise and column-wise holes in a projected footprint.""" + import numpy as np + + out = np.asarray(values, dtype=bool).copy() + for row in range(out.shape[0]): + out[row, :] = _fill_short_boolean_gaps_in_line(out[row, :], max_gap) + for col in range(out.shape[1]): + out[:, col] = _fill_short_boolean_gaps_in_line(out[:, col], max_gap) + return out + + +def _clean_projected_footprint(mask): + """Clean a candidate cap footprint before it is extruded into a disk.""" + import numpy as np + from scipy.ndimage import binary_fill_holes + + values = np.asarray(mask, dtype=bool) + values = _fill_short_boolean_gaps_in_footprint(values, max_gap=2) + values = binary_fill_holes(values).astype(bool) + return _largest_planar_component(values) + + +def _apply_requested_footprint_shape(mask, *, shape): + """Convert a projected support mask to the requested disk footprint.""" + import numpy as np + + values = np.asarray(mask, dtype=bool) + if not np.any(values): + return values + mode = str(shape).strip().lower() + if mode in {"", "anatomy", "fit", "projected", "mask"}: + return values + coords = np.argwhere(values) + lo = coords.min(axis=0) + hi = coords.max(axis=0) + yy, xx = np.indices(values.shape, dtype=np.float64) + center = (lo + hi) / 2.0 + half = np.maximum((hi - lo + 1) / 2.0, 0.5) + if mode in {"box", "square"}: + shaped = np.ones(values.shape, dtype=bool) + elif mode in {"round", "circle", "circular"}: + norm_y = (yy - center[0]) / half[0] + norm_x = (xx - center[1]) / half[1] + shaped = (norm_y * norm_y + norm_x * norm_x) <= 1.0 + elif mode in {"hex", "hexagon", "hexagonal"}: + norm_y = np.abs((yy - center[0]) / half[0]) + norm_x = np.abs((xx - center[1]) / half[1]) + shaped = (norm_x <= 1.0) & (norm_y <= 1.0) & (norm_x + norm_y / 2.0 <= 1.0) + else: + raise ValueError("disk shape must be one of anatomy, square, round, or hex") + return shaped + + +def _generate_projected_bone_caps_from_mask(mask, *, extrusion_axis, thickness, intrusion_depth, shape): + """Generate inferior and superior bone caps from projected surface footprints. + + This is the older Ogo-style cap builder used for non-``anatomy`` shapes. It + looks at each material column, records the first and last bone voxel, then + builds a flat cap from columns close enough to the global inferior/superior + surfaces. + """ + import numpy as np + + thickness = max(1, int(thickness)) + intrusion = max( + 0, + int(round(thickness * 2.5)) if intrusion_depth is None else int(intrusion_depth), + ) + inferior = np.zeros(mask.shape, dtype=bool) + superior = np.zeros(mask.shape, dtype=bool) + lateral_shape = tuple(mask.shape[idx] for idx in range(3) if idx != extrusion_axis) + inferior_surface_by_column = np.full(lateral_shape, -1, dtype=np.int32) + superior_surface_by_column = np.full(lateral_shape, -1, dtype=np.int32) + for lateral in np.ndindex(lateral_shape): + selector = _selector_for_material_column(lateral, extrusion_axis) + occupied = np.flatnonzero(mask[tuple(selector)]) + if occupied.size == 0: + continue + inferior_surface_by_column[lateral] = int(occupied.min()) + superior_surface_by_column[lateral] = int(occupied.max()) + has_bone_in_column = inferior_surface_by_column >= 0 + if not np.any(has_bone_in_column): + return inferior, superior + + global_inferior_surface = int(np.min(inferior_surface_by_column[has_bone_in_column])) + global_superior_surface = int(np.max(superior_surface_by_column[has_bone_in_column])) + inferior_depth_limit = min( + global_inferior_surface + intrusion, + int(np.max(inferior_surface_by_column[has_bone_in_column])), + ) + superior_depth_limit = max( + global_superior_surface - intrusion, + int(np.min(superior_surface_by_column[has_bone_in_column])), + ) + inferior_footprint = _apply_requested_footprint_shape( + _clean_projected_footprint( + has_bone_in_column & (inferior_surface_by_column <= inferior_depth_limit) + ), + shape=shape, + ) + superior_footprint = _apply_requested_footprint_shape( + _clean_projected_footprint( + has_bone_in_column & (superior_surface_by_column >= superior_depth_limit) + ), + shape=shape, + ) + + inferior_start = max(0, global_inferior_surface + intrusion - thickness) + inferior_stop = min(mask.shape[extrusion_axis], global_inferior_surface + intrusion) + superior_start = max(0, global_superior_surface - intrusion + 1) + superior_stop = min(mask.shape[extrusion_axis], superior_start + thickness) + + for lateral in np.ndindex(lateral_shape): + if inferior_footprint[lateral]: + selector = _selector_for_material_column(lateral, extrusion_axis) + selector[extrusion_axis] = slice(inferior_start, inferior_stop) + inferior[tuple(selector)] = True + if superior_footprint[lateral]: + selector = _selector_for_material_column(lateral, extrusion_axis) + selector[extrusion_axis] = slice(superior_start, superior_stop) + superior[tuple(selector)] = True + return inferior, superior + + +def _generate_axis_aligned_anatomy_cap_from_mask(mask, *, extrusion_axis, direction, thickness, intrusion_depth): + """Generate an axis-aligned cap from the local anatomy surface. + + The cap has a fixed total thickness. ``intrusion_depth`` does not mean + "overwrite this much bone". It means that the anatomy is allowed to occupy + that much of the fixed cap thickness before the PMMA part starts. After the + cap is built, any overlap with bone is cleared so bone material is preserved. + """ + import numpy as np + + thickness = max(1, int(thickness)) + intrusion = max(0, int(intrusion_depth)) + cap = np.zeros(mask.shape, dtype=bool) + lateral_shape = tuple(mask.shape[idx] for idx in range(3) if idx != extrusion_axis) + surface_position_by_column = np.full(lateral_shape, -1, dtype=np.int32) + + for lateral in np.ndindex(lateral_shape): + selector = _selector_for_material_column(lateral, extrusion_axis) + occupied = np.flatnonzero(mask[tuple(selector)]) + if occupied.size == 0: + continue + surface_position_by_column[lateral] = int(occupied.max() if direction == "up" else occupied.min()) + + has_bone_in_column = surface_position_by_column >= 0 + if not np.any(has_bone_in_column): + return cap + + # Keep columns whose surface is close enough to the outermost contact + # surface. This preserves a flat outer cap while allowing the inner side to + # follow nearby anatomy. + maximum_supported_surface_depth = thickness + intrusion + if direction == "up": + flat_contact_position = int(np.max(surface_position_by_column[has_bone_in_column])) + included_columns = has_bone_in_column & ( + surface_position_by_column >= flat_contact_position - maximum_supported_surface_depth + ) + outer_cap_position = flat_contact_position - intrusion + thickness + for lateral in np.argwhere(included_columns): + lateral = tuple(int(value) for value in lateral) + bone_surface_position = int(surface_position_by_column[lateral]) + selector = _selector_for_material_column(lateral, extrusion_axis) + selector[extrusion_axis] = slice( + max(bone_surface_position + 1, 0), + min(outer_cap_position + 1, mask.shape[extrusion_axis]), + ) + cap[tuple(selector)] = True + elif direction == "down": + flat_contact_position = int(np.min(surface_position_by_column[has_bone_in_column])) + included_columns = has_bone_in_column & ( + surface_position_by_column <= flat_contact_position + maximum_supported_surface_depth + ) + outer_cap_position = flat_contact_position + intrusion - thickness + for lateral in np.argwhere(included_columns): + lateral = tuple(int(value) for value in lateral) + bone_surface_position = int(surface_position_by_column[lateral]) + selector = _selector_for_material_column(lateral, extrusion_axis) + selector[extrusion_axis] = slice( + max(outer_cap_position, 0), + min(bone_surface_position, mask.shape[extrusion_axis]), + ) + cap[tuple(selector)] = True + else: + raise ValueError("direction must be 'up' or 'down'.") + + cap[mask] = False + return cap + + def _projected_contact_within_depth(mask, extrusion_axis, direction, depth): """Project only columns with bone near the flat fixture contact plane.""" import numpy as np @@ -298,17 +576,20 @@ def _full_fixture_cap( cap = np.zeros_like(mask, dtype=bool) hit = np.where(mask)[extrusion_axis] contact = int(hit.min() if direction == "down" else hit.max()) + thickness = max(int(thickness), 0) intrusion = max(int(intrusion), 0) if direction == "up": - slab = slice( - max(contact - intrusion + 1, 0), - min(contact + thickness + 1, mask.shape[extrusion_axis]), - ) + slab_start = contact - intrusion + 1 + slab_stop = slab_start + thickness + elif direction == "down": + slab_stop = contact + intrusion + slab_start = slab_stop - thickness else: - slab = slice( - max(contact - thickness, 0), - min(contact + intrusion, mask.shape[extrusion_axis]), - ) + raise ValueError("direction must be 'up' or 'down'.") + slab = slice( + max(slab_start, 0), + min(slab_stop, mask.shape[extrusion_axis]), + ) if extrusion_axis == 0: cap[slab, :, :] = footprint elif extrusion_axis == 1: @@ -318,13 +599,52 @@ def _full_fixture_cap( return cap -def generate_bone_cap_mask(mask, *, axis="x", direction="up", thickness=5, shape="fit"): - """Generate a one-sided cap mask from a binary bone/contact mask.""" +def generate_bone_cap_mask(mask, *, axis="x", direction="up", thickness=5, shape="fit", intrusion=None): + """Generate a one-sided cap mask from a binary bone/contact mask. + + Without ``intrusion`` this uses the historical Ogo surface-projection path. + With ``intrusion`` it uses the newer fixed-thickness cap convention: + + * ``thickness`` is the total requested cap thickness. + * ``intrusion`` is how far the anatomy is allowed to occupy that thickness. + * generated PMMA cap voxels are removed anywhere they overlap bone. + + The spine workflow calls this with ``shape="anatomy"`` to build a flat + outside face whose inner side follows nearby body anatomy. + """ import numpy as np shape = str(shape).lower() thickness = int(thickness) extrusion_axis = axis_index(axis) + if intrusion is not None: + work_bb, _ = _expanded_bbox(mask, extrusion_axis, direction, thickness + int(intrusion)) + cropped = mask[work_bb].astype(bool) + if shape in {"anatomy", "interactive", "interactive_anatomy"}: + cap = _generate_axis_aligned_anatomy_cap_from_mask( + cropped, + extrusion_axis=extrusion_axis, + direction=direction, + thickness=thickness, + intrusion_depth=int(intrusion), + ) + output = np.zeros_like(mask, dtype=bool) + output[work_bb] = cap + output[mask] = False + return output + inferior, superior = _generate_projected_bone_caps_from_mask( + cropped, + extrusion_axis=extrusion_axis, + thickness=thickness, + intrusion_depth=int(intrusion), + shape=shape, + ) + cap = superior if direction == "up" else inferior + output = np.zeros_like(mask, dtype=bool) + output[work_bb] = cap + output[mask] = False + return largest_connected_component(output) + work_bb, _ = _expanded_bbox(mask, extrusion_axis, direction, thickness) cropped = mask[work_bb].astype(bool) surface = _contact_surface(cropped, extrusion_axis, direction) @@ -352,9 +672,14 @@ def generate_fixture_cap_mask( ): """Generate a full geometric fixture cap from a binary ROI/contact mask. - Unlike a bone-fit cap, fixture caps keep their full square/round footprint. - They may overlap the bone mask; the later image-combine step preserves bone - material IDs where PMMA and bone overlap. + Fixture caps are used for hip/sideways-fall style contact fixtures. They + keep the full square/round footprint instead of trimming to a bone-fit + surface. + + ``thickness`` is the total fixture thickness. ``intrusion`` shifts that + fixed-thickness slab toward the anatomy; it does not request a thicker cap. + The cap mask may overlap bone, so callers that merge it into a material + image should preserve existing bone material IDs. """ import numpy as np @@ -363,7 +688,7 @@ def generate_fixture_cap_mask( raise ValueError("fixture cap shape must be 'box' or 'round'.") thickness = int(thickness) extrusion_axis = axis_index(axis) - work_bb, _ = _expanded_bbox(mask, extrusion_axis, direction, thickness) + work_bb, _ = _expanded_bbox_for_fixed_thickness_fixture(mask, extrusion_axis, thickness, intrusion) cropped = mask[work_bb].astype(bool) surface = _contact_surface(cropped, extrusion_axis, direction) cap = _full_fixture_cap( @@ -417,6 +742,7 @@ def generate_bone_cap_vtk( direction="up", thickness=5, shape="fit", + intrusion=None, output_value=1, ): """Generate a VTK PMMA/support cap from a labelled bone mask.""" @@ -429,6 +755,7 @@ def generate_bone_cap_vtk( direction=direction, thickness=thickness, shape=shape, + intrusion=intrusion, ) return numpy_to_vtk_image( (cap.astype(np.uint16) * int(output_value)), diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 27cf126..b6ce94f 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -20,9 +20,11 @@ using a shorter shaft. ``fixed_length`` mode is available for debugging. 6. Generate two geometric PMMA fixtures: a round femoral-head loading fixture and a box-like greater-trochanter contact fixture. Defaults are 6 mm PMMA - thickness and 6 mm PMMA intrusion toward bone before material merging. The - femoral-head footprint is widened by 10 mm and lengthened by 80 mm so the - anatomical cropping determines the actual contact. + thickness and 6 mm intrusion through that fixed thickness. The fixture masks + themselves do not overwrite bone voxels; intrusion only determines + which anatomy is close enough to support the cap. The femoral-head footprint + is widened by 10 mm and lengthened by 80 mm so the anatomical cropping + determines the actual contact. 7. Apply sideways-fall boundary conditions: prescribed displacement at the femoral-head PMMA cap toward the greater trochanter, loading-direction constraint at the greater-trochanter PMMA cap, and distal shaft constraints diff --git a/ogo/fea/model.py b/ogo/fea/model.py index 901cdad..5e184e4 100644 --- a/ogo/fea/model.py +++ b/ogo/fea/model.py @@ -30,6 +30,65 @@ def find_and_add_visible_nodes(model, bc_geometry, normal_vector, bone_material_ return model +def filter_node_set_to_dominant_coordinate_plane( + model, + node_set_name, + *, + axis="z", + tolerance=1.0e-5, +): + """Keep only the main flat face of a boundary-condition node set. + + vtkbone first finds all visible nodes on the PMMA contact geometry. For a + thick disk that can include small rim or side-wall nodes. In the spine + workflow the boundary condition belongs on the flat outside face, so Ogo + filters the visible node set down to the coordinate plane with the most + nodes. + """ + import vtk + + import ogo.util.Helper as ogo + + axis_index = {"x": 0, "y": 1, "z": 2}[str(axis).lower()] + node_ids = model.GetNodeSet(node_set_name) + if node_ids is None or node_ids.GetNumberOfTuples() == 0: + ogo.message(f"No nodes found for {node_set_name}; skipping coordinate-plane filter.") + return model + + nodes_by_coordinate_plane = {} + for idx in range(node_ids.GetNumberOfTuples()): + point_id = int(node_ids.GetValue(idx)) + coordinate = float(model.GetPoint(point_id)[axis_index]) + plane_key = round(coordinate / float(tolerance)) + nodes_by_coordinate_plane.setdefault(plane_key, []).append((point_id, coordinate)) + + dominant_plane_nodes = [] + dominant_plane_score = None + for candidate_nodes in nodes_by_coordinate_plane.values(): + average_coordinate = sum(coordinate for _, coordinate in candidate_nodes) / len(candidate_nodes) + # Prefer the plane with the most nodes. The coordinate tie-breaker keeps + # the result deterministic when a synthetic test has two equal planes. + candidate_score = (len(candidate_nodes), average_coordinate) + if dominant_plane_score is None or candidate_score > dominant_plane_score: + dominant_plane_nodes = candidate_nodes + dominant_plane_score = candidate_score + + target_coordinate = sum(coordinate for _, coordinate in dominant_plane_nodes) / len(dominant_plane_nodes) + + filtered = vtk.vtkIdTypeArray() + filtered.SetName(node_set_name) + for point_id, coordinate in dominant_plane_nodes: + if abs(coordinate - target_coordinate) <= float(tolerance): + filtered.InsertNextValue(point_id) + + model.AddNodeSet(filtered) + ogo.message( + f"Filtered {node_set_name} to dominant {axis}-plane {target_coordinate:g}: " + f"{filtered.GetNumberOfTuples()} of {node_ids.GetNumberOfTuples()} nodes retained." + ) + return model + + def apply_spine_boundary_conditions(model, **kwargs): """Apply axial compression boundary conditions for the spine FE model.""" import vtkbone @@ -112,6 +171,9 @@ def create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, bottom_node_set_id = kwargs.get("bottom_node_set_id", 3) top_node_set_name = kwargs.get("top_node_set_name", "body_top") bottom_node_set_name = kwargs.get("bottom_node_set_name", "body_bottom") + filter_bc_node_sets = kwargs.get("filter_bc_node_sets", True) + bc_filter_axis = kwargs.get("bc_filter_axis", "z") + bc_filter_tolerance = kwargs.get("bc_filter_tolerance", 1.0e-5) pmma_yield_compression = kwargs.get("pmma_yield_compression", None) pmma_yield_tension = kwargs.get("pmma_yield_tension", None) material_log_values = spine_material_log_values(kwargs) @@ -129,6 +191,9 @@ def create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, bottom_node_set_id=bottom_node_set_id, top_node_set_name=top_node_set_name, bottom_node_set_name=bottom_node_set_name, + filter_bc_node_sets=filter_bc_node_sets, + bc_filter_axis=bc_filter_axis, + bc_filter_tolerance=bc_filter_tolerance, pmma_yield_compression=pmma_yield_compression, pmma_yield_tension=pmma_yield_tension, **material_log_values, @@ -175,6 +240,19 @@ def create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, model = find_and_add_visible_nodes( model, temp_bc_mesh, bottom_direction, bottom_node_set_id, bottom_node_set_name ) + if filter_bc_node_sets: + model = filter_node_set_to_dominant_coordinate_plane( + model, + top_node_set_name, + axis=bc_filter_axis, + tolerance=bc_filter_tolerance, + ) + model = filter_node_set_to_dominant_coordinate_plane( + model, + bottom_node_set_name, + axis=bc_filter_axis, + tolerance=bc_filter_tolerance, + ) model = apply_spine_boundary_conditions(model, **kwargs) ogo.message("Setting convergence criteria...") diff --git a/ogo/fea/sideways_fall.py b/ogo/fea/sideways_fall.py index b0623b8..c590890 100644 --- a/ogo/fea/sideways_fall.py +++ b/ogo/fea/sideways_fall.py @@ -496,6 +496,7 @@ def sidewaysFallFe(args): shape="box", intrusion=intrusion, crop_to_contact=True, + clear_contact_mask=True, output_value=pmma_mat_id, ) @@ -511,6 +512,7 @@ def sidewaysFallFe(args): thickness=thickness, shape="round", intrusion=intrusion, + clear_contact_mask=True, output_value=pmma_mat_id, ) @@ -733,7 +735,7 @@ def main(): parser.add_argument("--pmma_thick", type=float, default=DEFAULT_PMMA_THICKNESS_MM, help="Sets the minimum thickness for PMMA caps in the FE model. Default value (6 [mm]) based on observed measurement of Keaveny BCT FE modeling of the femur. (default: %(default)s [mm])") parser.add_argument("--pmma_intrusion", type=float, default=DEFAULT_PMMA_INTRUSION_MM, - help="Sets how far PMMA fixtures intrude toward the bone side before bone overlap is preserved during material combination. (default: %(default)s [mm])") + help="Sets how far anatomy can occupy the fixed PMMA fixture thickness before bone overlap is preserved during material combination. (default: %(default)s [mm])") parser.add_argument("--pmma_mat_id", type=int, default=5000, help="Sets the material ID for the PMMA blocks. (default: %(default)s)") parser.add_argument("--fe_displacement", type=float, default=DEFAULT_FEMUR_FE_DISPLACEMENT, diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 92ad93d..708f4d7 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -14,8 +14,9 @@ interpolation; body/process labels use nearest-neighbor interpolation. 5. Smooth the transformed body/process masks with one binary close/open pass only when at least one input spacing dimension is coarser than 2 mm. Then - generate fit-to-body PMMA caps on the superior and inferior body surfaces. - The default cap thickness is 3 mm. + generate fixed-thickness anatomy PMMA caps on the superior and inferior body + surfaces. The maintained cap thickness is 10 mm and the intrusion depth is + 6 mm. 6. Build materials with the same shared bone/PMMA material-table helper used by the femur workflow. The spine convention is trabecular material IDs 1..128 and cortical IDs 129..256; PMMA is a separate material ID. @@ -29,7 +30,8 @@ DEFAULT_SPINE_ISO_RESOLUTION_MM = 1.0 -DEFAULT_SPINE_PMMA_THICKNESS_MM = 3 +DEFAULT_SPINE_PMMA_THICKNESS_MM = 10 +DEFAULT_SPINE_PMMA_INTRUSION_MM = 6 DEFAULT_SPINE_PMMA_MATERIAL_ID = 5000 DEFAULT_SPINE_POISSONS_RATIO = 0.3 DEFAULT_SPINE_PMMA_E_MPA = 2500 @@ -42,6 +44,7 @@ DEFAULT_SPINE_REGISTRATION_SCALE = None DEFAULT_SPINE_REGISTRATION_MIN_SCALE = "0.8,0.8,0.75" DEFAULT_SPINE_REGISTRATION_MAX_SCALE = "1.2,1.2,1.3" +DEFAULT_SPINE_REGISTRATION_BACKEND = "vtk" DEFAULT_SPINE_REFERENCE_FILENAME = "L4_BODY_SPINE_COMPRESSION_REF.vtk" SPINE_ALIGNMENT_METHOD = "scaled ICP to reference vertebral body" @@ -134,7 +137,11 @@ def prepare_benchmark_images(input_image_path, input_mask_path, n_bins=128): binned_image, bin_centers = convert_image_to_material( input_image, vertebra_mask, n_bins=n_bins, cort_mask=cortical_mask ) - image_with_disk = merge_vtk_images([binned_image, disk_mask], [None, 300]) + image_with_disk = merge_vtk_images( + [binned_image, disk_mask], + [None, 300], + overwrite_existing=False, + ) return image_with_disk, input_mask_with_disk, bin_centers diff --git a/ogo/fea/spine_compression.py b/ogo/fea/spine_compression.py index e4aa281..840e43b 100644 --- a/ogo/fea/spine_compression.py +++ b/ogo/fea/spine_compression.py @@ -60,12 +60,14 @@ DEFAULT_SPINE_ISO_RESOLUTION_MM, DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, DEFAULT_SPINE_PMMA_E_MPA, + DEFAULT_SPINE_PMMA_INTRUSION_MM, DEFAULT_SPINE_PMMA_MATERIAL_ID, DEFAULT_SPINE_PMMA_POISSONS_RATIO, DEFAULT_SPINE_PMMA_THICKNESS_MM, DEFAULT_SPINE_POISSONS_RATIO, DEFAULT_SPINE_REGISTRATION_MAX_SCALE, DEFAULT_SPINE_REGISTRATION_MIN_SCALE, + DEFAULT_SPINE_REGISTRATION_BACKEND, DEFAULT_SPINE_REGISTRATION_SCALE, DEFAULT_SPINE_TOP_NODE_SET_ID, default_spine_reference_path, @@ -281,7 +283,7 @@ def pad_vtk_image(vtk_image, axis='x', pmma_thick=5, pad_value=0): return padded_vtk_image -def merge_vtk_images(image_list, label_list=None): +def merge_vtk_images(image_list, label_list=None, overwrite_existing=True): """ Merges an arbitrary list of vtkImageData objects into a single vtkImageData. Optionally assigns specific label values for each image. @@ -290,6 +292,8 @@ def merge_vtk_images(image_list, label_list=None): - image_list: List of vtkImageData objects to merge. - label_list: Optional list of label values corresponding to each image. If None or an entry in the list is None, uses the original values in the image. + - overwrite_existing: If False, labeled images only write into zero-valued voxels already + present in the merged image. Returns: - merged_image: vtkImageData containing the merged result. @@ -315,7 +319,10 @@ def merge_vtk_images(image_list, label_list=None): if label_list is not None and label_list[idx] is not None: label_value = label_list[idx] # Replace non-zero values in the image with the label value - merged_array[image_array > 0] = label_value + write_mask = image_array > 0 + if not overwrite_existing: + write_mask &= merged_array == 0 + merged_array[write_mask] = label_value else: # Add the original values of the image where label is None merged_array += image_array @@ -355,6 +362,7 @@ def perform_marching_cubes(body_output): raise ValueError("No data was generated by the Marching Cubes algorithm.") return mcubes + def get_icp_with_scaling( body, reference_path, @@ -878,6 +886,7 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, pmma_mat_id = kwargs.get("pmma_mat_id", 5000) iso_resolution = kwargs.get("iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM) pmma_thick = kwargs.get("pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM) + pmma_intrusion = kwargs.get("pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM) top_node_set_id = kwargs.get("top_node_set_id", DEFAULT_SPINE_TOP_NODE_SET_ID) bottom_node_set_id = kwargs.get("bottom_node_set_id", DEFAULT_SPINE_BOTTOM_NODE_SET_ID) quality_control = kwargs.get("quality_control", True) @@ -984,18 +993,36 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, padded_mask1 = pad_vtk_image(mask1_labeled.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) padded_mask2 = pad_vtk_image(mask2_labeled.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) - ogo.message(f"extruding body-cap disks (pmma_thick > {pmma_thick} mm)...") + ogo.message( + "extruding fixed-thickness anatomy body-cap disks " + f"(pmma_thick = {pmma_thick} mm total, pmma_intrusion = {pmma_intrusion} mm)..." + ) inferior_disk = generate_bone_cap_vtk( - padded_mask1, label_value=2, thickness=pmma_thick, direction='down', axis='x', shape='fit' + padded_mask1, + label_value=2, + thickness=pmma_thick, + intrusion=pmma_intrusion, + direction='down', + axis='x', + shape='anatomy', ) superior_disk = generate_bone_cap_vtk( - padded_mask1, label_value=2, thickness=pmma_thick, direction='up', axis='x', shape='fit' + padded_mask1, + label_value=2, + thickness=pmma_thick, + intrusion=pmma_intrusion, + direction='up', + axis='x', + shape='anatomy', ) ogo.message(f"merging disks with image...") # Now we create the image with the disks and the boundary image as well - image_with_pads = merge_vtk_images([ - padded_image, superior_disk, inferior_disk], [None, pmma_mat_id,pmma_mat_id]) + image_with_pads = merge_vtk_images( + [padded_image, superior_disk, inferior_disk], + [None, pmma_mat_id, pmma_mat_id], + overwrite_existing=False, + ) boundary_masks_with_pads = merge_vtk_images([ padded_mask, inferior_disk, superior_disk], [None, bottom_node_set_id, top_node_set_id]) @@ -1098,7 +1125,9 @@ def main(): parser.add_argument("--pmma_v", type=float, default=DEFAULT_SPINE_PMMA_POISSONS_RATIO, help="Sets the Poisson's ratio for the PMMA material(s) in the FE model. (default: %(default)s)") parser.add_argument("--pmma_thick", type=int, default=DEFAULT_SPINE_PMMA_THICKNESS_MM, - help="Sets the minimum pmma_thick for PMMA caps in the FE model. (default: %(default)s [mm])") + help="Sets the total PMMA cap thickness in the FE model. (default: %(default)s [mm])") + parser.add_argument("--pmma_intrusion", type=int, default=DEFAULT_SPINE_PMMA_INTRUSION_MM, + help="Sets how far anatomy can occupy the fixed PMMA cap thickness. (default: %(default)s [mm])") parser.add_argument("--pmma_mat_id", type=int, default=DEFAULT_SPINE_PMMA_MATERIAL_ID, help="Sets the material ID for the PMMA blocks. (default: %(default)s)") parser.add_argument("--fe_displacement", type=float, default=DEFAULT_SPINE_FE_DISPLACEMENT_MM, @@ -1111,6 +1140,8 @@ def main(): help="Minimum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") parser.add_argument("--registration_max_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_MAX_SCALE, help="Maximum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") + parser.add_argument("--registration_backend", choices=("vtk",), default=DEFAULT_SPINE_REGISTRATION_BACKEND, + help="Compatibility option; spine reference alignment uses VTK ICP. (default: %(default)s)") parser.add_argument("--top_node_set_id", type=int, default=DEFAULT_SPINE_TOP_NODE_SET_ID, help="ID for the top node set. (default: %(default)s)") parser.add_argument("--bottom_node_set_id", type=int, default=DEFAULT_SPINE_BOTTOM_NODE_SET_ID, diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index 9507b35..c1e4a11 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -340,15 +340,20 @@ def test_spine_modeling_metadata_records_materials_and_bcs(tmp_path): "kopperdahl_trab_E", "--fe_displacement", "-0.2", + "--pmma_intrusion", + "3", ] path = GenerateFEM.write_modeling_metadata(model, "spine", argv, _metadata_args(tmp_path)) data = json.loads(path.read_text()) assert data["target"] == {"vertebra": "L1", "body_label": 2, "process_label": 3} + assert data["alignment"]["registration_backend"] == "vtk" assert data["materials"]["trabecular"]["elastic_E_func"] == "kopperdahl_trab_E" assert data["materials"]["cortical"]["material_id_range"] == [129, 256] assert data["materials"]["pmma"]["material_id"] == 5000 + assert data["boundary_conditions"]["fixture_geometry"]["superior_cap"]["pmma_intrusion_mm"] == 3.0 + assert data["boundary_conditions"]["fixture_geometry"]["inferior_cap"]["pmma_intrusion_mm"] == 3.0 assert data["boundary_conditions"]["constraints"][0]["node_set"] == "body_top" assert "initial_generator_value_mm" not in data["boundary_conditions"]["constraints"][0] assert data["boundary_conditions"]["constraints"][0]["target_displacement_percent"] == 0.68 diff --git a/tests/fea/test_boundary.py b/tests/fea/test_boundary.py index 1bce348..d8e369b 100644 --- a/tests/fea/test_boundary.py +++ b/tests/fea/test_boundary.py @@ -2,7 +2,10 @@ np = pytest.importorskip("numpy") -from ogo.fea.boundary import generate_bone_cap_mask, generate_fixture_cap_mask +from ogo.fea.boundary import ( + generate_bone_cap_mask, + generate_fixture_cap_mask, +) def test_fit_cap_follows_each_contact_ray(): @@ -69,6 +72,126 @@ def test_round_cap_stays_inside_projected_box(): assert not np.any(round_cap & mask) +def test_bone_cap_intrusion_limits_global_surface_depth(): + mask = np.zeros((14, 9, 9), dtype=bool) + mask[4:6, 2:7, 2:7] = True + mask[7, 4:6, 4:6] = True + + cap = generate_bone_cap_mask( + mask, + axis="x", + direction="up", + thickness=2, + shape="fit", + intrusion=1, + ) + + assert cap.any() + assert not np.any(cap & mask) + cap_x = np.where(cap)[0] + assert cap_x.min() >= 7 + assert cap_x.max() <= 9 + assert cap[:, 2, 2].sum() == 0 + assert cap[:, 4, 4].sum() > 0 + + +def test_bone_cap_zero_intrusion_stays_outside_global_surface(): + mask = np.zeros((12, 8, 8), dtype=bool) + mask[4:7, 2:6, 2:6] = True + + cap = generate_bone_cap_mask( + mask, + axis="x", + direction="up", + thickness=2, + shape="box", + intrusion=0, + ) + + assert np.all(cap[7:9, 2:6, 2:6]) + assert not np.any(cap[:7]) + assert not np.any(cap & mask) + + +def test_projected_bone_cap_closes_two_voxel_footprint_gaps(): + mask = np.zeros((12, 12, 12), dtype=bool) + mask[5:7, 2:5, 2:10] = True + mask[5:7, 7:10, 2:10] = True + + cap = generate_bone_cap_mask( + mask, + axis="x", + direction="up", + thickness=4, + shape="fit", + intrusion=2, + ) + + assert cap.any() + assert not np.any(cap & mask) + assert np.all(cap[7:9, 5:7, 2:10]) + cap_x = np.where(cap)[0] + assert cap_x.max() - cap_x.min() + 1 <= 4 + + +def test_projected_bone_cap_intrusion_keeps_requested_total_thickness(): + mask = np.zeros((24, 12, 12), dtype=bool) + mask[8:10, 2:10, 2:10] = True + mask[14:16, 5:7, 5:7] = True + + cap = generate_bone_cap_mask( + mask, + axis="x", + direction="up", + thickness=6, + shape="fit", + intrusion=5, + ) + + assert cap.any() + assert not np.any(cap & mask) + cap_x = np.where(cap)[0] + assert cap_x.max() - cap_x.min() + 1 <= 6 + + +def test_anatomy_bone_cap_uses_axis_aligned_surface_depth(): + mask = np.zeros((16, 9, 9), dtype=bool) + mask[7:9, 2:6, 2:6] = True + mask[2:4, 6, 3] = True + + cap = generate_bone_cap_mask( + mask, + axis="x", + direction="up", + thickness=5, + shape="anatomy", + intrusion=3, + ) + + assert not np.any(cap & mask) + assert np.flatnonzero(cap[:, 3, 3]).tolist() == [9, 10] + assert np.flatnonzero(cap[:, 6, 3]).tolist() == [4, 5, 6, 7, 8, 9, 10] + + +def test_anatomy_bone_cap_uses_axis_aligned_surface_depth_inferior(): + mask = np.zeros((18, 9, 9), dtype=bool) + mask[7:9, 2:6, 2:6] = True + mask[12:14, 6, 3] = True + + cap = generate_bone_cap_mask( + mask, + axis="x", + direction="down", + thickness=5, + shape="anatomy", + intrusion=3, + ) + + assert not np.any(cap & mask) + assert np.flatnonzero(cap[:, 3, 3]).tolist() == [5, 6] + assert np.flatnonzero(cap[:, 6, 3]).tolist() == [5, 6, 7, 8, 9, 10, 11] + + def test_fixture_box_cap_keeps_full_footprint_over_irregular_surface(): mask = np.zeros((14, 9, 9), dtype=bool) mask[3:6, 2:7, 2:7] = True @@ -94,9 +217,38 @@ def test_fixture_cap_intrudes_across_contact_plane(): intrusion=2, ) - assert np.all(cap[4:9, 2:7, 2:7]) + assert np.all(cap[4:7, 2:7, 2:7]) assert not np.any(cap[:4]) - assert not np.any(cap[9:]) + assert not np.any(cap[7:]) + + +def test_fixture_cap_intrusion_keeps_total_thickness(): + mask = np.zeros((18, 9, 9), dtype=bool) + mask[8:10, 2:7, 2:7] = True + + half_intrusion = generate_fixture_cap_mask( + mask, + axis="x", + direction="up", + thickness=6, + shape="box", + intrusion=3, + ) + full_intrusion = generate_fixture_cap_mask( + mask, + axis="x", + direction="up", + thickness=6, + shape="box", + intrusion=6, + ) + + half_x = np.flatnonzero(half_intrusion[:, 3, 3]) + full_x = np.flatnonzero(full_intrusion[:, 3, 3]) + assert half_x.tolist() == [7, 8, 9, 10, 11, 12] + assert full_x.tolist() == [4, 5, 6, 7, 8, 9] + assert half_x.size == 6 + assert full_x.size == 6 def test_fixture_contact_crop_removes_unsupported_columns(): diff --git a/tests/fea/test_spine.py b/tests/fea/test_spine.py index 250a9a1..4fc0dbd 100644 --- a/tests/fea/test_spine.py +++ b/tests/fea/test_spine.py @@ -1,6 +1,12 @@ from ogo.fea import spine +def test_default_spine_cap_geometry_matches_maintained_settings(): + assert spine.DEFAULT_SPINE_PMMA_THICKNESS_MM == 10 + assert spine.DEFAULT_SPINE_PMMA_INTRUSION_MM == 6 + assert spine.DEFAULT_SPINE_REGISTRATION_BACKEND == "vtk" + + def test_benchmark_presets_match_spinefe_notebook_settings(): linear = spine.benchmark_linear_params() nonlinear = spine.benchmark_nonlinear_params() @@ -12,4 +18,3 @@ def test_benchmark_presets_match_spinefe_notebook_settings(): assert nonlinear["fe_displacement"] == -2.0 assert nonlinear["yield_comp_func"] == "kopperdahl_trab_yc" assert nonlinear["pmma_yield_compression"] == 70.0 - diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py index 542b7ae..c7128fd 100644 --- a/tests/fea/test_spinefe_benchmark.py +++ b/tests/fea/test_spinefe_benchmark.py @@ -5,9 +5,44 @@ import pytest +from ogo.fea.model import filter_node_set_to_dominant_coordinate_plane from ogo.fea.spine import build_benchmark_sample_model, find_spinefe_benchmark_dir +def test_filter_node_set_to_dominant_coordinate_plane_removes_small_rim(): + vtk = pytest.importorskip("vtk") + + class FakeModel: + def __init__(self): + self.points = [ + (0.0, 0.0, 2.0), + (1.0, 0.0, 2.0), + (0.0, 1.0, 2.0), + (1.0, 1.0, 2.0), + (0.0, 0.0, 1.0), + ] + self.node_sets = {"cap": vtk.vtkIdTypeArray()} + self.node_sets["cap"].SetName("cap") + for point_id in range(len(self.points)): + self.node_sets["cap"].InsertNextValue(point_id) + + def GetNodeSet(self, name): + return self.node_sets.get(name) + + def AddNodeSet(self, node_ids): + self.node_sets[node_ids.GetName()] = node_ids + + def GetPoint(self, point_id): + return self.points[point_id] + + model = FakeModel() + filter_node_set_to_dominant_coordinate_plane(model, "cap", axis="z") + + filtered = model.GetNodeSet("cap") + assert filtered.GetNumberOfTuples() == 4 + assert [filtered.GetValue(i) for i in range(filtered.GetNumberOfTuples())] == [0, 1, 2, 3] + + def _node_set_count(model, name): node_set = model.GetNodeSet(name) if node_set is None: @@ -23,6 +58,38 @@ def _model_count(model, method_names): raise AssertionError(f"Model does not expose any of {method_names!r}") +def test_merge_vtk_images_can_preserve_existing_material_voxels(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + + from ogo.fea.spine_compression import merge_vtk_images + + material = np.zeros((3, 3, 3), dtype=np.float32) + material[1, 1, 1] = 42 + disk = np.zeros_like(material) + disk[1, 1, 1] = 1 + disk[2, 1, 1] = 1 + + def vtk_image(array): + image = vtk.vtkImageData() + image.SetDimensions(array.shape) + image.SetOrigin(0, 0, 0) + image.SetSpacing(1, 1, 1) + image.GetPointData().SetScalars(numpy_to_vtk(array.ravel(order="F"), deep=True)) + return image + + merged = merge_vtk_images( + [vtk_image(material), vtk_image(disk)], + [None, 5000], + overwrite_existing=False, + ) + + out = vtk_to_numpy(merged.GetPointData().GetScalars()).reshape(material.shape, order="F") + assert out[1, 1, 1] == 42 + assert out[2, 1, 1] == 5000 + + def _sample_paths(): benchmark_dir = find_spinefe_benchmark_dir(Path(__file__).resolve().parents[2]) if benchmark_dir is None: From 82d613ce6965835d1e80bb119bd86a3bc9bf2869 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:54:44 -0600 Subject: [PATCH 11/67] Refine FE workflow generation --- ogo/cli/CheckFEModelBC.py | 41 +- ogo/cli/GenerateFEM.py | 91 ++- ogo/fea/README.md | 17 +- ogo/fea/alignment.py | 376 ++++++++++ ogo/fea/boundary.py | 871 +++++++++++++++++----- ogo/fea/femur.py | 1056 ++++++++++++++++++++++++++- ogo/fea/materials.py | 4 +- ogo/fea/model.py | 266 ++++++- ogo/fea/sideways_fall.py | 661 +++++++++++------ ogo/fea/spine.py | 5 + ogo/fea/spine_compression.py | 374 +++++++--- ogo/util/Helper.py | 46 +- tests/cli/test_CheckFEModelBC.py | 37 + tests/cli/test_GenerateFEM.py | 9 + tests/fea/test_alignment.py | 137 ++++ tests/fea/test_boundary.py | 396 ++++++---- tests/fea/test_femur.py | 376 +++++++++- tests/fea/test_model.py | 137 ++++ tests/fea/test_sideways_fall.py | 46 ++ tests/fea/test_spinefe_benchmark.py | 128 ++++ tests/util/test_Helper.py | 200 +++++ 21 files changed, 4565 insertions(+), 709 deletions(-) create mode 100644 ogo/fea/alignment.py create mode 100644 tests/cli/test_CheckFEModelBC.py create mode 100644 tests/fea/test_alignment.py create mode 100644 tests/fea/test_model.py create mode 100644 tests/fea/test_sideways_fall.py diff --git a/ogo/cli/CheckFEModelBC.py b/ogo/cli/CheckFEModelBC.py index 19c14c6..952008d 100644 --- a/ogo/cli/CheckFEModelBC.py +++ b/ogo/cli/CheckFEModelBC.py @@ -43,6 +43,19 @@ def _centroid(points): return {"x": float(center[0]), "y": float(center[1]), "z": float(center[2])} +def _planarity(points): + if len(points) < 3: + return {"rms_mm": 0.0, "normal": [0.0, 0.0, 1.0]} + centered = points - points.mean(axis=0) + _u, _s, vh = np.linalg.svd(centered, full_matrices=False) + normal = vh[-1] + distances = centered @ normal + return { + "rms_mm": float(np.sqrt(np.mean(distances**2))), + "normal": [float(value) for value in normal], + } + + def read_node_sets(root): out = {} for name, group in root.groups["Sets"].groups["NodeSets"].groups.items(): @@ -52,6 +65,7 @@ def read_node_sets(root): "count": int(len(node_numbers)), "bounds": _bounds(points), "centroid": _centroid(points), + "planarity": _planarity(points), } return out @@ -113,11 +127,11 @@ def audit_femur_sideways(node_sets, constraints, tolerance): distal = node_sets.get(DISTAL_FEMUR_NODE_SET) if distal: - z_span = distal["bounds"]["z_max"] - distal["bounds"]["z_min"] + rms = distal.get("planarity", {}).get("rms_mm", 0.0) checks.append({ - "name": "distal shaft set is one flat z plane", - "passed": z_span <= tolerance, - "detail": f"z_span={z_span:.6g}", + "name": "distal shaft set is a planar support surface", + "passed": rms <= max(float(tolerance), 1.5), + "detail": f"planarity_rms_mm={rms:.6g}", }) expected_zero_constraints = { @@ -172,13 +186,18 @@ def audit_spine_compression(node_sets, constraints, tolerance): "detail": f"z_span={z_span:.6g}", }) - axis = _single_axis(constraints, "bottom_fixed_z") - value = _single_value(constraints, "bottom_fixed_z") - checks.append({ - "name": "bottom_fixed_z fixes z", - "passed": axis == "z" and value == 0.0, - "detail": f"axis={axis}, value={value}", - }) + for name, expected_axis in ( + ("bottom_fixed_x", "x"), + ("bottom_fixed_y", "y"), + ("bottom_fixed_z", "z"), + ): + axis = _single_axis(constraints, name) + value = _single_value(constraints, name) + checks.append({ + "name": f"{name} fixes {expected_axis}", + "passed": axis == expected_axis and value == 0.0, + "detail": f"axis={axis}, value={value}", + }) return checks diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 0be6c6f..6214a95 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -29,6 +29,8 @@ default_spine_reference_path, ) from ogo.fea.femur import ( + DEFAULT_FEMUR_BBOX_CROP_FROM, + DEFAULT_FEMUR_BBOX_RATIO, DEFAULT_FEMUR_CUT_MODE, DEFAULT_FEMUR_FE_DISPLACEMENT, DEFAULT_FEMUR_ISO_RESOLUTION_MM, @@ -37,6 +39,11 @@ DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, DEFAULT_PMMA_INTRUSION_MM, DEFAULT_PMMA_THICKNESS_MM, + DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, + DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, + FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, + GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, + SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, ) @@ -209,6 +216,19 @@ def option_path(argv: Sequence[str], option: str, default: Optional[Path] = None return None if value is None else str(value) +def option_n_values(argv: Sequence[str], option: str, count: int, default: Sequence) -> list: + """Return a fixed-width option value list from an argv-style list.""" + values = list(default) + for index, token in enumerate(argv): + if token == option and index + count < len(argv): + values = list(argv[index + 1 : index + 1 + count]) + parsed = [] + for value in values: + token = str(value).strip().lower() if value is not None else "none" + parsed.append(None if token in {"", "none", "null", "auto"} else value) + return parsed + + def target_displacement_percent(args: argparse.Namespace, model_type: str) -> float: """Return the maintained endpoint displacement percentage for this model.""" if args.target_displacement is not None: @@ -353,6 +373,11 @@ def write_modeling_metadata( "bone_mask": str(args.bone_mask), "output_path": str(args.output_path) if args.output_path is not None else str(args.calibrated_image.parent), }, + "geometry": { + "model_coordinates": "preprocessed_image_physical_space", + "origin_policy": "cropping and resampling update the image origin; FE meshing preserves that origin", + "boundary_condition_coordinates": "defined from the generated model bounding box in the same physical space", + }, "post_generation_validation": { "bc_audit": { "enabled": bc_audit_summary is not None, @@ -541,9 +566,9 @@ def write_modeling_metadata( { "name": "bottom_fixed", "node_set": "body_bottom", - "axis": "z", + "axes": ["x", "y", "z"], "value_mm": 0.0, - "meaning": "inferior PMMA cap fixed in compression direction", + "meaning": "inferior PMMA cap fixed in all displacement directions", }, ], }, @@ -552,6 +577,14 @@ def write_modeling_metadata( femur_side = option_value(generator_argv, "--femur_side", "1") side = "left" if femur_side == "1" else "right" if femur_side == "2" else "unknown" compartment_mask = option_path(generator_argv, "--compartment_mask") + femur_cut_mode = option_value(generator_argv, "--femur_cut_mode", DEFAULT_FEMUR_CUT_MODE) + femur_bbox_ratio = option_n_values(generator_argv, "--femur_bbox_ratio", 3, DEFAULT_FEMUR_BBOX_RATIO) + femur_bbox_crop_from = option_n_values( + generator_argv, + "--femur_bbox_crop_from", + 3, + DEFAULT_FEMUR_BBOX_CROP_FROM, + ) femur_displacement = percent_displacement_metadata( model_path, report_profile="femur", @@ -567,13 +600,13 @@ def write_modeling_metadata( "mask_threshold": option_int(generator_argv, "--mask_threshold", 1), }, "alignment": { - "method": "pre-rotation plus ICP to side-specific femur reference", + "method": "ICP to side-specific femur reference from cropped/padded input geometry", "reference_path": option_value(generator_argv, "--reference_path", "bundled side-specific femur reference"), }, "image_processing": { "iso_resolution_mm": option_float(generator_argv, "--iso_resolution", DEFAULT_FEMUR_ISO_RESOLUTION_MM), "spatial_operations": ( - "side pre-rotation reslice, then ICP transform and isotropic output spacing in one shared VTK reslice" + "ICP transform and isotropic output spacing in one shared VTK reslice" ), "image_interpolation": "cubic", "label_interpolation": "nearest-neighbor", @@ -613,14 +646,20 @@ def write_modeling_metadata( }, }, "shaft_standardization": { - "cut_mode": option_value(generator_argv, "--femur_cut_mode", DEFAULT_FEMUR_CUT_MODE), + "cut_mode": femur_cut_mode, "fixed_length_mm": option_float(generator_argv, "--femur_shaft_length", 100.0), "lesser_trochanter_distal_offset_mm": option_float( generator_argv, "--femur_lesser_trochanter_distal_offset", DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, ), - "cut_plane": "flat model-grid z plane", + "bbox_ratio": femur_bbox_ratio, + "bbox_crop_from": femur_bbox_crop_from, + "cut_plane": ( + "input bbox-ratio crop face transformed with the femur" + if femur_cut_mode == "bbox_ratio" + else "flat model-grid z plane" + ), "incomplete_fov_behavior": "fail model generation", }, "materials": { @@ -671,21 +710,49 @@ def write_modeling_metadata( "fixture_geometry": { "femoral_head": { "node_set": "Femoral_Head_PMMA_Nodes", - "shape": "round fixture cap", + "shape": "rectangle fixture cap", + "relative_to": "model_bbox", + "center_fraction": list(FEMORAL_HEAD_FIXTURE_CENTER_FRACTION), + "size_fraction": list(SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION), + "projection_axis": "y", "pmma_thickness_mm": option_float(generator_argv, "--pmma_thick", DEFAULT_PMMA_THICKNESS_MM), "pmma_intrusion_mm": option_float(generator_argv, "--pmma_intrusion", DEFAULT_PMMA_INTRUSION_MM), - "width_extension_mm": 10.0, - "long_axis_extension_mm": 80.0, + "meaning": "bbox-scaled high-y contact fixture with unsupported columns cropped", }, "greater_trochanter": { "node_set": "Greater_Trochanter_PMMA_Nodes", - "shape": "box fixture cap", + "shape": "rectangle fixture cap", + "relative_to": "model_bbox", + "center_fraction": list(GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION), + "size_fraction": list(SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION), + "projection_axis": "y", "pmma_thickness_mm": option_float(generator_argv, "--pmma_thick", DEFAULT_PMMA_THICKNESS_MM), "pmma_intrusion_mm": option_float(generator_argv, "--pmma_intrusion", DEFAULT_PMMA_INTRUSION_MM), + "meaning": "bbox-scaled low-y contact fixture with unsupported columns cropped", }, "distal_shaft": { "node_set": "Distal_Femur_Nodes", - "support_surface": "flat distal shaft cut face", + "support_surface": ( + "finite bbox-relative patch projected onto the transformed oblique shaft surface" + if femur_cut_mode == "bbox_ratio" + else "flat distal shaft cut face" + ), + "relative_to": "model_bbox" if femur_cut_mode == "bbox_ratio" else None, + "center_fraction": ( + list(DISTAL_SHAFT_FIXTURE_CENTER_FRACTION) + if femur_cut_mode == "bbox_ratio" + else None + ), + "size_fraction": ( + list(DISTAL_SHAFT_FIXTURE_SIZE_FRACTION) + if femur_cut_mode == "bbox_ratio" + else None + ), + "normal_source": ( + "transformed input bbox-ratio crop face" + if femur_cut_mode == "bbox_ratio" + else "model-grid distal z face" + ), }, }, "constraints": [ @@ -848,7 +915,7 @@ def _add_common_image_args(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--debug", action="store_true", - help="Write debug sidecars such as BC audit PNGs and legacy spine QC quick looks.", + help="Write debug sidecars such as BC audit PNGs and spine QC quick looks.", ) parser.add_argument( "--skip_bc_audit", diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 4b66c2a..a412bd4 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -276,7 +276,7 @@ Default spine boundary conditions: | Region | Node set | Constraint | | --- | --- | --- | | Superior PMMA cap | `body_top` | Prescribed displacement in the compression direction. | -| Inferior PMMA cap | `body_bottom` | Fixed in the compression direction. | +| Inferior PMMA cap | `body_bottom` | Fixed in all displacement directions. | The default high-level spine preset is `benchmark-linear`. It applies the spineFE benchmark linear material settings for model construction, then @@ -309,7 +309,10 @@ The femur workflow builds one sideways-fall model per side: 6. Standardize the distal shaft with a flat model-grid cut. The default cut mode detects the lesser trochanter and keeps `50 mm` distal to it. If the required distal field of view is missing, model generation fails. -7. Generate PMMA fixtures for the femoral head and greater trochanter. +7. Generate bbox-relative rectangular PMMA fixtures for the femoral head and + greater trochanter. The femoral-head fixture is placed on the high-y side, + the greater-trochanter fixture is placed on the low-y side, and both + footprints scale with the generated model bbox. 8. Convert density to material IDs and construct the N88 model. 9. Apply sideways-fall boundary conditions. @@ -415,7 +418,7 @@ ogoFEA hip \ ``` This writes a boundary-condition audit PNG next to the model. For spine, debug -mode also enables legacy quick-look output from the lower-level generator. +mode also enables quick-look output from the lower-level generator. ## Common Adjustments @@ -444,6 +447,14 @@ ogoFEA hip image.nii.gz femur_mask.nii.gz \ --pmma_intrusion 6 ``` +For hip sideways fall, both PMMA fixtures use bbox-relative contact planes by +default. The maintained footprint is rectangular, centered from stored bbox +fractions, and scaled independently to `1.1 x 1.1` of the model bbox in the two +authored in-plane axes. `--pmma_thick` controls total fixture thickness; +`--pmma_intrusion` controls how far anatomy can occupy that fixed thickness +before unsupported contact columns are removed. The PMMA labels do not replace +bone voxels. + For spine compression, `--pmma_thick` controls the total generated disk thickness and `--pmma_intrusion` controls how far anatomy can occupy that fixed thickness. The maintained spine defaults are 10 mm thickness and 6 mm intrusion. diff --git a/ogo/fea/alignment.py b/ogo/fea/alignment.py new file mode 100644 index 0000000..40092cc --- /dev/null +++ b/ogo/fea/alignment.py @@ -0,0 +1,376 @@ +"""Shared point-cloud helpers for reference-frame FE alignment.""" + + +def polydata_points(polydata): + """Return VTK polydata point coordinates as an ``(n, 3)`` NumPy array.""" + import numpy as np + from vtk.util.numpy_support import vtk_to_numpy + + points = polydata.GetPoints() + if points is None or points.GetNumberOfPoints() == 0: + raise ValueError("Polydata contains no points.") + coordinates = np.asarray(vtk_to_numpy(points.GetData()), dtype=float) + if coordinates.ndim != 2 or coordinates.shape[1] != 3: + raise ValueError("Polydata points must have shape (n, 3).") + return coordinates[np.all(np.isfinite(coordinates), axis=1)] + + +def polydata_from_points(points): + """Create a vertex-only VTK polydata from an ``(n, 3)`` point cloud.""" + import numpy as np + import vtk + + coordinates = np.asarray(points, dtype=float) + if coordinates.ndim != 2 or coordinates.shape[1] != 3 or coordinates.shape[0] < 3: + raise ValueError("points must have shape (n, 3) with at least three rows.") + + vtk_points = vtk.vtkPoints() + vertices = vtk.vtkCellArray() + for point in coordinates: + point_id = vtk_points.InsertNextPoint(float(point[0]), float(point[1]), float(point[2])) + vertices.InsertNextCell(1) + vertices.InsertCellPoint(point_id) + + polydata = vtk.vtkPolyData() + polydata.SetPoints(vtk_points) + polydata.SetVerts(vertices) + return polydata + + +def sample_points(points, *, max_points=None, mode="linspace", offset=0): + """Sample a point cloud using deterministic stride or linspace modes.""" + import numpy as np + + coordinates = np.asarray(points, dtype=float) + if max_points is None or int(max_points) <= 0 or coordinates.shape[0] <= int(max_points): + return coordinates + + token = str(mode).strip().lower() + if token == "stride": + step = max(1, int(np.ceil(coordinates.shape[0] / int(max_points)))) + start = int(offset) % step + indices = np.arange(start, coordinates.shape[0], step, dtype=int) + if indices.size < int(max_points): + fallback = np.linspace(0, coordinates.shape[0] - 1, int(max_points), dtype=int) + indices = np.unique(np.concatenate([indices, fallback])) + indices = indices[: int(max_points)] + else: + indices = np.linspace(0, coordinates.shape[0] - 1, int(max_points), dtype=int) + return coordinates[indices] + + +def _binary_erosion_6(mask): + import numpy as np + + padded = np.pad(mask, 1, mode="constant", constant_values=False) + center = padded[1:-1, 1:-1, 1:-1] + eroded = center.copy() + eroded &= padded[:-2, 1:-1, 1:-1] + eroded &= padded[2:, 1:-1, 1:-1] + eroded &= padded[1:-1, :-2, 1:-1] + eroded &= padded[1:-1, 2:, 1:-1] + eroded &= padded[1:-1, 1:-1, :-2] + eroded &= padded[1:-1, 1:-1, 2:] + return eroded + + +def surface_points_from_vtk_mask( + vtk_mask, + *, + max_points=8000, + sample_mode="stride", + sample_offset=0, +): + """Return physical x/y/z points from 6-connected voxel surface centers.""" + import numpy as np + + from ogo.util.vtk_image import vtk_image_to_numpy + + mask = np.asarray(vtk_image_to_numpy(vtk_mask), dtype=bool) + if not np.any(mask): + raise ValueError("mask contains no foreground voxels.") + + surface = mask & ~_binary_erosion_6(mask) + surface_zyx = np.transpose(surface if np.any(surface) else mask, (2, 1, 0)) + coords_xyz = np.argwhere(surface_zyx)[:, [2, 1, 0]].astype(float) + points = np.asarray(vtk_mask.GetOrigin(), dtype=float) + coords_xyz * np.asarray( + vtk_mask.GetSpacing(), + dtype=float, + ) + return sample_points( + points, + max_points=max_points, + mode=sample_mode, + offset=sample_offset, + ) + + +def point_cloud_axis_lengths(points): + """Return approximate principal-axis diameters for an ``(n, 3)`` point cloud.""" + import numpy as np + + coordinates = np.asarray(points, dtype=float) + if coordinates.ndim != 2 or coordinates.shape[1] != 3 or coordinates.shape[0] < 3: + raise ValueError("points must have shape (n, 3) with at least three rows.") + centered = coordinates - coordinates.mean(axis=0) + covariance = np.cov(centered.T) + eigvals = np.linalg.eigvalsh(covariance) + return np.sqrt(np.maximum(eigvals, 0.0)) * 2.0 + + +def estimate_rigid_icp( + *, + moving_points, + fixed_points, + iterations=50, + tolerance=1.0e-4, + start_by_matching_centroids_only=False, + convergence="delta", + distance_mode="mean", +): + """Estimate a rigid transform from ``moving_points`` to ``fixed_points``.""" + import numpy as np + + moving = _points_array(moving_points, "moving_points") + fixed = _points_array(fixed_points, "fixed_points") + if start_by_matching_centroids_only: + rotation = np.eye(3) + translation = fixed.mean(axis=0) - moving.mean(axis=0) + else: + rotation, translation = _best_initial_transform(moving, fixed) + + previous_error = np.inf + used_iterations = 0 + convergence_token = str(convergence).strip().lower() + distance_token = str(distance_mode).strip().lower() + for used_iterations in range(1, max(1, int(iterations)) + 1): + transformed = moving @ rotation.T + translation + matched = fixed[_nearest_indices(transformed, fixed)] + step_rotation, step_translation = _kabsch(transformed, matched) + rotation = step_rotation @ rotation + translation = step_rotation @ translation + step_translation + distances = np.linalg.norm(transformed - matched, axis=1) + if distance_token == "mean": + error = float(np.mean(distances)) + elif distance_token == "rms": + error = float(np.sqrt(np.mean(distances * distances))) + else: + raise ValueError("distance_mode must be 'mean' or 'rms'.") + + if convergence_token == "delta": + converged = abs(previous_error - error) <= float(tolerance) + elif convergence_token in {"absolute", "abs"}: + converged = error <= float(tolerance) + else: + raise ValueError("convergence must be 'delta' or 'absolute'.") + if converged: + previous_error = error + break + previous_error = error + + return { + "rotation": rotation, + "translation": translation, + "iterations": used_iterations, + "mean_distance": previous_error, + } + + +def invert_point_transform(rotation, translation): + """Invert ``points @ rotation.T + translation`` row-vector transforms.""" + import numpy as np + + rotation_arr = np.asarray(rotation, dtype=float) + translation_arr = np.asarray(translation, dtype=float) + if rotation_arr.shape != (3, 3): + raise ValueError("rotation must have shape (3, 3).") + if translation_arr.shape != (3,): + raise ValueError("translation must contain three values.") + inverse_rotation = rotation_arr.T + inverse_translation = -translation_arr @ rotation_arr + return inverse_rotation, inverse_translation + + +def output_grid_for_point_transform( + points, + *, + rotation, + translation, + spacing, + margin_voxels=0, +): + """Return an explicit output image grid for transformed physical points.""" + import numpy as np + + coordinates = _points_array(points, "points") + rotation_arr = np.asarray(rotation, dtype=float) + translation_arr = np.asarray(translation, dtype=float) + spacing_arr = np.asarray(spacing, dtype=float) + if rotation_arr.shape != (3, 3): + raise ValueError("rotation must have shape (3, 3).") + if translation_arr.shape != (3,): + raise ValueError("translation must contain three values.") + if spacing_arr.shape != (3,) or np.any(spacing_arr <= 0.0): + raise ValueError("spacing must contain three positive values.") + + transformed = coordinates @ rotation_arr.T + translation_arr + margin = int(max(0, margin_voxels)) + lower = transformed.min(axis=0) - margin * spacing_arr + upper = transformed.max(axis=0) + margin * spacing_arr + size = np.maximum(1, np.ceil((upper - lower) / spacing_arr).astype(int) + 1) + return tuple(float(value) for value in lower), tuple(int(value) for value in size) + + +def resample_vtk_image_with_point_transform( + vtk_image, + *, + rotation, + translation, + output_origin, + output_size, + output_spacing, + interpolation="nearest", +): + """Resample a VTK image onto an explicit grid after a point transform.""" + import numpy as np + import SimpleITK as sitk + import vtk + from vtk.util.numpy_support import numpy_to_vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + rotation_arr = np.asarray(rotation, dtype=float) + translation_arr = np.asarray(translation, dtype=float) + if rotation_arr.shape != (3, 3): + raise ValueError("rotation must have shape (3, 3).") + if translation_arr.shape != (3,): + raise ValueError("translation must contain three values.") + + array_xyz = vtk_image_to_numpy(vtk_image) + image = sitk.GetImageFromArray(np.transpose(array_xyz, (2, 1, 0))) + image.SetSpacing(tuple(float(value) for value in vtk_image.GetSpacing())) + image.SetOrigin(tuple(float(value) for value in vtk_image.GetOrigin())) + + transform = sitk.AffineTransform(3) + transform.SetMatrix(tuple(float(value) for value in rotation_arr.reshape(-1))) + transform.SetTranslation(tuple(float(value) for value in translation_arr)) + + resampler = sitk.ResampleImageFilter() + resampler.SetReferenceImage(image) + resampler.SetSize([int(value) for value in output_size]) + resampler.SetOutputSpacing(tuple(float(value) for value in output_spacing)) + resampler.SetOutputOrigin(tuple(float(value) for value in output_origin)) + resampler.SetOutputDirection((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) + resampler.SetTransform(transform.GetInverse()) + mode = str(interpolation).strip().lower() + if mode == "nearest": + resampler.SetInterpolator(sitk.sitkNearestNeighbor) + elif mode in {"linear", "trilinear"}: + resampler.SetInterpolator(sitk.sitkLinear) + elif mode in {"cubic", "bspline", "spline"}: + resampler.SetInterpolator(sitk.sitkBSpline) + else: + raise ValueError("interpolation must be nearest, linear, or bspline.") + resampler.SetDefaultPixelValue(0) + resampled_zyx = sitk.GetArrayFromImage(resampler.Execute(image)) + resampled_xyz = np.transpose(resampled_zyx, (2, 1, 0)) + + output = vtk.vtkImageData() + output.SetDimensions(tuple(int(value) for value in output_size)) + output.SetSpacing(tuple(float(value) for value in output_spacing)) + output.SetOrigin(tuple(float(value) for value in output_origin)) + output.GetPointData().SetScalars( + numpy_to_vtk(resampled_xyz.ravel(order="F"), deep=True) + ) + return output + + +def _points_array(points, name): + import numpy as np + + array = np.asarray(points, dtype=float) + if array.ndim != 2 or array.shape[1] != 3: + raise ValueError(f"{name} must have shape (n, 3).") + if array.shape[0] < 3: + raise ValueError(f"{name} must contain at least three points.") + return array[np.all(np.isfinite(array), axis=1)] + + +def _best_initial_transform(moving, fixed): + import numpy as np + + moving_center = moving.mean(axis=0) + fixed_center = fixed.mean(axis=0) + candidates = [ + (np.eye(3), fixed_center - moving_center), + _initial_pca_transform(moving, fixed), + ] + return min(candidates, key=lambda item: _nearest_mean_distance(moving, fixed, *item)) + + +def _initial_pca_transform(moving, fixed): + import numpy as np + + moving_center = moving.mean(axis=0) + fixed_center = fixed.mean(axis=0) + moving_axes = _principal_axes(moving - moving_center) + fixed_axes = _principal_axes(fixed - fixed_center) + rotation = fixed_axes @ moving_axes.T + if np.linalg.det(rotation) < 0: + fixed_axes[:, -1] *= -1 + rotation = fixed_axes @ moving_axes.T + translation = fixed_center - rotation @ moving_center + return rotation, translation + + +def _principal_axes(points): + import numpy as np + + _, _, vh = np.linalg.svd(points, full_matrices=False) + axes = vh.T + if np.linalg.det(axes) < 0: + axes[:, -1] *= -1 + return axes + + +def _nearest_indices(query, target): + try: + from scipy.spatial import cKDTree + except ImportError: + return _nearest_indices_numpy(query, target) + return cKDTree(target).query(query, workers=-1)[1] + + +def _nearest_indices_numpy(query, target): + import numpy as np + + out = np.empty((query.shape[0],), dtype=int) + chunk = 512 + for start in range(0, query.shape[0], chunk): + stop = min(start + chunk, query.shape[0]) + diff = query[start:stop, None, :] - target[None, :, :] + out[start:stop] = np.argmin(np.sum(diff * diff, axis=2), axis=1) + return out + + +def _nearest_mean_distance(moving, fixed, rotation, translation): + import numpy as np + + transformed = moving @ rotation.T + translation + matched = fixed[_nearest_indices(transformed, fixed)] + return float(np.mean(np.linalg.norm(transformed - matched, axis=1))) + + +def _kabsch(moving, fixed): + import numpy as np + + moving_center = moving.mean(axis=0) + fixed_center = fixed.mean(axis=0) + h = (moving - moving_center).T @ (fixed - fixed_center) + u, _, vt = np.linalg.svd(h) + rotation = vt.T @ u.T + if np.linalg.det(rotation) < 0: + vt[-1, :] *= -1 + rotation = vt.T @ u.T + translation = fixed_center - rotation @ moving_center + return rotation, translation diff --git a/ogo/fea/boundary.py b/ogo/fea/boundary.py index ff4c9ae..179fd06 100644 --- a/ogo/fea/boundary.py +++ b/ogo/fea/boundary.py @@ -13,6 +13,186 @@ def axis_index(axis): raise ValueError("axis must be 'x', 'y', or 'z'.") from exc +def _axis_bounds(bounds, axis): + index = 2 * int(axis) + return float(bounds[index]), float(bounds[index + 1]) + + +def _axis_extent_from_vector(bounds, vector): + import numpy as np + + spans = np.asarray( + [ + float(bounds[1]) - float(bounds[0]), + float(bounds[3]) - float(bounds[2]), + float(bounds[5]) - float(bounds[4]), + ], + dtype=float, + ) + return float(np.sum(np.abs(np.asarray(vector, dtype=float)) * spans)) + + +def _contact_plane_axes(projection_axis, normal_sign): + """Return stable in-plane axes for a coordinate-axis contact plane.""" + axis = axis_index(projection_axis) + sign = 1.0 if float(normal_sign) >= 0.0 else -1.0 + if axis == 0: + return (0.0, 0.0, sign), (0.0, -1.0, 0.0) + if axis == 1: + return (0.0, 0.0, -sign), (-1.0, 0.0, 0.0) + return (1.0, 0.0, 0.0), (0.0, sign, 0.0) + + +def bbox_relative_contact_bounds( + model_bounds, + *, + center_fraction, + size_fraction, + projection_axis="y", + shape="rectangle", +): + """Return physical contact ROI bounds from bbox-relative plane values. + + ``center_fraction`` is authored in x/y/z order against voxel-center bounds. + The two ``size_fraction`` values scale the lateral axes of the projection + plane in coordinate order. The projection axis itself spans the full input + bounds so downstream geometry can find the nearest supported anatomy. + """ + if len(model_bounds) != 6: + raise ValueError("model_bounds must contain x/y/z min/max values.") + if len(center_fraction) != 3: + raise ValueError("center_fraction must contain x/y/z fractions.") + if len(size_fraction) != 2: + raise ValueError("size_fraction must contain the two in-plane scale factors.") + + projection_index = axis_index(projection_axis) + out = [float(value) for value in model_bounds] + lateral_axes = [axis for axis in (0, 1, 2) if axis != projection_index] + lateral_lengths = [] + for size_value, axis in zip(size_fraction, lateral_axes): + lo, hi = _axis_bounds(model_bounds, axis) + span = hi - lo + if span <= 0.0: + raise ValueError("model_bounds must have positive span on every lateral axis.") + length = span * float(size_value) + if length <= 0.0: + raise ValueError("size_fraction values must be positive.") + lateral_lengths.append(length) + + if str(shape).strip().lower() == "square": + lateral_lengths = [min(lateral_lengths)] * len(lateral_lengths) + + for length, axis in zip(lateral_lengths, lateral_axes): + lo, hi = _axis_bounds(model_bounds, axis) + span = hi - lo + center = lo + float(center_fraction[axis]) * span + out[2 * axis] = center - length / 2.0 + out[2 * axis + 1] = center + length / 2.0 + + lo, hi = _axis_bounds(model_bounds, projection_index) + out[2 * projection_index] = lo + out[2 * projection_index + 1] = hi + return tuple(out) + + +def bbox_relative_contact_direction(center_fraction, *, projection_axis="y"): + """Return the cap side implied by a bbox-relative plane fraction.""" + if len(center_fraction) != 3: + raise ValueError("center_fraction must contain x/y/z fractions.") + return "up" if float(center_fraction[axis_index(projection_axis)]) >= 0.5 else "down" + + +def bbox_relative_contact_plane( + model_bounds, + *, + center_fraction, + size_fraction, + projection_axis="y", + shape="square", +): + """Return a physical contact-plane definition from bbox-relative values.""" + if len(model_bounds) != 6: + raise ValueError("model_bounds must contain x/y/z min/max values.") + if len(center_fraction) != 3: + raise ValueError("center_fraction must contain x/y/z fractions.") + if len(size_fraction) != 2: + raise ValueError("size_fraction must contain the two in-plane scale factors.") + + projection_index = axis_index(projection_axis) + center = [] + for axis in range(3): + lo, hi = _axis_bounds(model_bounds, axis) + center.append(lo + float(center_fraction[axis]) * (hi - lo)) + + normal_sign = -1.0 if float(center_fraction[projection_index]) >= 0.5 else 1.0 + normal = [0.0, 0.0, 0.0] + normal[projection_index] = normal_sign + u_axis, v_axis = _contact_plane_axes(projection_axis, normal_sign) + size = ( + _axis_extent_from_vector(model_bounds, u_axis) * float(size_fraction[0]), + _axis_extent_from_vector(model_bounds, v_axis) * float(size_fraction[1]), + ) + return { + "center": tuple(float(value) for value in center), + "normal": tuple(float(value) for value in normal), + "u_axis": tuple(float(value) for value in u_axis), + "v_axis": tuple(float(value) for value in v_axis), + "size": tuple(float(value) for value in size), + "shape": str(shape).strip().lower(), + } + + +def bounds_with_reference_extent(current_bounds, reference_bounds): + """Return bounds centered on ``current_bounds`` with ``reference_bounds`` extents.""" + import numpy as np + + current = np.asarray(current_bounds, dtype=float) + reference = np.asarray(reference_bounds, dtype=float) + if current.shape != (6,) or reference.shape != (6,): + raise ValueError("bounds must contain x/y/z min/max values.") + out = [] + for axis in range(3): + current_lo, current_hi = current[2 * axis], current[2 * axis + 1] + reference_lo, reference_hi = reference[2 * axis], reference[2 * axis + 1] + center = (current_lo + current_hi) / 2.0 + extent = reference_hi - reference_lo + out.extend([center - extent / 2.0, center + extent / 2.0]) + return tuple(float(value) for value in out) + + +def foreground_voxel_center_bounds_from_mask(mask, *, origin, spacing): + """Return x/y/z physical bounds of nonzero voxel centers.""" + import numpy as np + + active = np.asarray(mask) != 0 + coords = np.argwhere(active) + if coords.size == 0: + raise ValueError("Cannot compute foreground bounds from an empty mask.") + origin = np.asarray(origin, dtype=np.float64) + spacing = np.asarray(spacing, dtype=np.float64) + if origin.shape != (3,) or spacing.shape != (3,): + raise ValueError("origin and spacing must contain three values.") + lo = origin + coords.min(axis=0).astype(np.float64) * spacing + hi = origin + coords.max(axis=0).astype(np.float64) * spacing + return ( + float(lo[0]), + float(hi[0]), + float(lo[1]), + float(hi[1]), + float(lo[2]), + float(hi[2]), + ) + + +def foreground_voxel_center_bounds(vtk_image): + """Return physical foreground bounds for a VTK image using voxel centers.""" + return foreground_voxel_center_bounds_from_mask( + vtk_image_to_numpy(vtk_image), + origin=vtk_image.GetOrigin(), + spacing=vtk_image.GetSpacing(), + ) + + def largest_connected_component(mask): """Keep the largest connected component in a binary NumPy mask.""" import numpy as np @@ -52,6 +232,47 @@ def smooth_binary_mask_vtk(mask_vtk, *, close_iter=1, open_iter=1): ) +def smooth_label_mask_vtk(label_vtk, *, sigma_mm=1.0, threshold=0.5): + """Smooth a multi-label mask by smoothing each label probability separately.""" + import numpy as np + import SimpleITK as sitk + import vtk + + labels_xyz = np.asarray(vtk_image_to_numpy(label_vtk)) + unique_labels = [int(value) for value in np.unique(labels_xyz) if int(value) != 0] + if not unique_labels: + return numpy_to_vtk_image( + labels_xyz.astype(np.uint8, copy=False), + label_vtk, + vtk.VTK_UNSIGNED_CHAR, + ) + + labels_zyx = np.transpose(labels_xyz, (2, 1, 0)) + scores = [] + for label_value in unique_labels: + image = sitk.GetImageFromArray((labels_zyx == label_value).astype(np.float32)) + image.SetSpacing(tuple(float(value) for value in label_vtk.GetSpacing())) + smoothed = sitk.SmoothingRecursiveGaussian(image, float(sigma_mm)) + scores.append(sitk.GetArrayFromImage(smoothed)) + stacked = np.stack(scores, axis=0) + best_index = np.argmax(stacked, axis=0) + best_score = np.take_along_axis( + stacked, + best_index[np.newaxis, ...], + axis=0, + )[0] + out_zyx = np.zeros(labels_zyx.shape, dtype=labels_xyz.dtype) + label_values = np.asarray(unique_labels, dtype=labels_xyz.dtype) + active = best_score >= float(threshold) + out_zyx[active] = label_values[best_index[active]] + out_xyz = np.transpose(out_zyx, (2, 1, 0)) + return numpy_to_vtk_image( + out_xyz.astype(np.uint8, copy=False), + label_vtk, + vtk.VTK_UNSIGNED_CHAR, + ) + + def _bounding_box(mask): import numpy as np @@ -81,18 +302,6 @@ def _expanded_bbox(mask, extrusion_axis, direction, thickness): return tuple(bb_list), bb -def _expanded_bbox_for_fixed_thickness_fixture(mask, extrusion_axis, thickness, intrusion): - """Return a crop wide enough for a fixture that may straddle the bone surface.""" - bb = _bounding_box(mask) - bb_list = list(bb) - margin = max(int(thickness), int(intrusion), 0) - bb_list[extrusion_axis] = slice( - max(bb[extrusion_axis].start - margin, 0), - min(bb[extrusion_axis].stop + margin, mask.shape[extrusion_axis]), - ) - return tuple(bb_list), bb - - def _contact_surface(mask, extrusion_axis, direction): import numpy as np @@ -348,7 +557,7 @@ def _apply_requested_footprint_shape(mask, *, shape): yy, xx = np.indices(values.shape, dtype=np.float64) center = (lo + hi) / 2.0 half = np.maximum((hi - lo + 1) / 2.0, 0.5) - if mode in {"box", "square"}: + if mode in {"box", "rectangle", "rectangular", "square"}: shaped = np.ones(values.shape, dtype=bool) elif mode in {"round", "circle", "circular"}: norm_y = (yy - center[0]) / half[0] @@ -359,17 +568,426 @@ def _apply_requested_footprint_shape(mask, *, shape): norm_x = np.abs((xx - center[1]) / half[1]) shaped = (norm_x <= 1.0) & (norm_y <= 1.0) & (norm_x + norm_y / 2.0 <= 1.0) else: - raise ValueError("disk shape must be one of anatomy, square, round, or hex") + raise ValueError("disk shape must be one of anatomy, rectangle, square, round, or hex") return shaped +def _unit_vector(vector, name): + """Return a unit vector from a physical-space direction.""" + import numpy as np + + values = np.asarray(vector, dtype=float) + if values.shape != (3,): + raise ValueError(f"{name} must contain three values.") + norm = float(np.linalg.norm(values)) + if norm <= 1.0e-12: + raise ValueError(f"{name} must have nonzero length.") + return values / norm + + +def _plane_axes_from_normal(normal): + """Return a stable orthonormal in-plane basis for a plane normal.""" + import numpy as np + + helper = np.asarray([1.0, 0.0, 0.0], dtype=float) + if abs(float(np.dot(normal, helper))) > 0.85: + helper = np.asarray([0.0, 1.0, 0.0], dtype=float) + u_axis = _unit_vector(np.cross(normal, helper), "u_axis") + v_axis = _unit_vector(np.cross(normal, u_axis), "v_axis") + return u_axis, v_axis + + +def pad_vtk_images_to_physical_bounds(vtk_images, *, desired_bounds, constants=None): + """Pad aligned VTK images so the requested physical x/y/z bounds fit.""" + import numpy as np + import vtk + + images = list(vtk_images) + if not images: + return [], {"lower": (0, 0, 0), "upper": (0, 0, 0)} + constants = [0] * len(images) if constants is None else list(constants) + if len(constants) != len(images): + raise ValueError("constants must match vtk_images length.") + if len(desired_bounds) != 6: + raise ValueError("desired_bounds must contain x/y/z min/max values.") + + reference = images[0] + spacing = np.asarray(reference.GetSpacing(), dtype=float) + if np.any(spacing <= 0.0): + raise ValueError("VTK image spacing must be positive.") + extent = reference.GetExtent() + current_min = np.asarray( + [ + reference.GetOrigin()[0] + float(extent[0]) * spacing[0], + reference.GetOrigin()[1] + float(extent[2]) * spacing[1], + reference.GetOrigin()[2] + float(extent[4]) * spacing[2], + ], + dtype=float, + ) + current_max = np.asarray( + [ + reference.GetOrigin()[0] + float(extent[1]) * spacing[0], + reference.GetOrigin()[1] + float(extent[3]) * spacing[1], + reference.GetOrigin()[2] + float(extent[5]) * spacing[2], + ], + dtype=float, + ) + desired = np.asarray(desired_bounds, dtype=float) + desired_min = desired[[0, 2, 4]] + desired_max = desired[[1, 3, 5]] + lower = np.maximum(0, np.ceil((current_min - desired_min) / spacing).astype(int)) + upper = np.maximum(0, np.ceil((desired_max - current_max) / spacing).astype(int)) + + if not np.any(lower) and not np.any(upper): + return images, { + "lower": tuple(int(value) for value in lower), + "upper": tuple(int(value) for value in upper), + } + + output_extent = ( + int(extent[0] - lower[0]), + int(extent[1] + upper[0]), + int(extent[2] - lower[1]), + int(extent[3] + upper[1]), + int(extent[4] - lower[2]), + int(extent[5] + upper[2]), + ) + padded = [] + for image, constant in zip(images, constants, strict=True): + pad = vtk.vtkImageConstantPad() + pad.SetInputData(image) + pad.SetOutputWholeExtent(output_extent) + pad.SetConstant(float(constant)) + pad.Update() + out = vtk.vtkImageData() + out.DeepCopy(pad.GetOutput()) + dims_out = out.GetDimensions() + image_origin = image.GetOrigin() + image_spacing = out.GetSpacing() + out.SetOrigin( + float(image_origin[0]) + float(output_extent[0]) * float(image_spacing[0]), + float(image_origin[1]) + float(output_extent[2]) * float(image_spacing[1]), + float(image_origin[2]) + float(output_extent[4]) * float(image_spacing[2]), + ) + out.SetExtent(0, dims_out[0] - 1, 0, dims_out[1] - 1, 0, dims_out[2] - 1) + padded.append(out) + + return padded, { + "lower": tuple(int(value) for value in lower), + "upper": tuple(int(value) for value in upper), + } + + +def projected_material_disk_required_bounds( + surface_vtk_image, + *, + center, + normal, + u_axis=None, + v_axis=None, + size=(24.0, 24.0), + shape="anatomy", + thickness=3.0, + intrusion=2.0, +): + """Return conservative physical bounds needed for a plane-projected disk.""" + import numpy as np + + active = vtk_image_to_numpy(surface_vtk_image) != 0 + if not np.any(active): + return None + + spacing = tuple(float(value) for value in surface_vtk_image.GetSpacing()) + origin = tuple(float(value) for value in surface_vtk_image.GetOrigin()) + extent = surface_vtk_image.GetExtent() + extent_start = (extent[0], extent[2], extent[4]) + center = np.asarray(center, dtype=float) + if center.shape != (3,): + raise ValueError("center must contain three values.") + normal = _unit_vector(normal, "normal") + if u_axis is None or v_axis is None: + u_axis, v_axis = _plane_axes_from_normal(normal) + else: + u_axis = _unit_vector(u_axis, "u_axis") + v_axis = _unit_vector(v_axis, "v_axis") + if len(size) != 2: + raise ValueError("size must contain two in-plane lengths.") + + half_u = max(float(size[0]) / 2.0, 0.0) + half_v = max(float(size[1]) / 2.0, 0.0) + active_indices = np.argwhere(active) + active_points = _physical_points_from_indices( + active_indices, + spacing=spacing, + origin=origin, + extent_start=extent_start, + ) + rel = active_points - center + distance = rel @ normal + u = rel @ u_axis + v = rel @ v_axis + tolerance = max(min(spacing) * 0.75, 1.0e-6) + inside = _inside_projected_shape( + shape, + u, + v, + half_u, + half_v, + tolerance=tolerance, + ) + forward = distance >= -tolerance + if not np.any(inside & forward): + return None + + surface_distance = float(np.min(distance[inside & forward])) + cap_inner_distance = surface_distance + max(float(intrusion), 0.0) + cap_outer_distance = cap_inner_distance - max(float(thickness), 0.0) + d_min = min(cap_inner_distance, cap_outer_distance) + d_max = max(cap_inner_distance, cap_outer_distance) + corners = [] + for u_value in (-half_u, half_u): + for v_value in (-half_v, half_v): + for d_value in (d_min, d_max): + corners.append(center + u_value * u_axis + v_value * v_axis + d_value * normal) + points = np.vstack(corners) + margin = np.asarray(spacing, dtype=float) + lower = points.min(axis=0) - margin + upper = points.max(axis=0) + margin + return ( + float(lower[0]), + float(upper[0]), + float(lower[1]), + float(upper[1]), + float(lower[2]), + float(upper[2]), + ) + + +def _physical_points_from_indices(indices, *, spacing, origin, extent_start=(0, 0, 0)): + """Convert x/y/z array indices to physical image coordinates.""" + import numpy as np + + idx = np.asarray(indices, dtype=float) + return np.asarray(origin, dtype=float) + ( + idx + np.asarray(extent_start, dtype=float) + ) * np.asarray(spacing, dtype=float) + + +def _projected_bucket_key(u, v, *, spacing): + resolution = max(min(float(value) for value in spacing), 1.0e-6) + return int(round(float(u) / resolution)), int(round(float(v) / resolution)) + + +def _inside_projected_shape(shape, u, v, half_u, half_v, *, tolerance): + import numpy as np + + token = str(shape).strip().lower() + if token in {"anatomy", "rectangle", "rectangular", "box"}: + return (np.abs(u) <= half_u + tolerance) & (np.abs(v) <= half_v + tolerance) + if token == "square": + half = min(float(half_u), float(half_v)) + return (np.abs(u) <= half + tolerance) & (np.abs(v) <= half + tolerance) + if token in {"round", "circle", "circular", "oval"}: + half_u = max(float(half_u) + tolerance, 1.0e-9) + half_v = max(float(half_v) + tolerance, 1.0e-9) + return ((u / half_u) ** 2 + (v / half_v) ** 2) <= 1.0 + if token in {"hex", "hexagon", "hexagonal"}: + half = max(min(float(half_u), float(half_v)), 1.0e-9) + uu = u / half + vv = v / half + hex_tol = tolerance / half + return ( + (np.abs(uu) <= 1.0 + hex_tol) + & (np.abs(0.5 * uu + 0.8660254 * vv) <= 1.0 + hex_tol) + & (np.abs(0.5 * uu - 0.8660254 * vv) <= 1.0 + hex_tol) + ) + raise ValueError("disk shape must be one of anatomy, rectangle, square, round, or hex.") + + +def _surface_distance_by_projected_bucket( + indices, + distance, + u, + v, + *, + spacing, +): + import numpy as np + + best = {} + for voxel, dist, uu, vv in zip(indices, distance, u, v, strict=True): + if float(dist) < 0.0: + continue + key = _projected_bucket_key(float(uu), float(vv), spacing=spacing) + current = best.get(key) + if current is None or float(dist) < current[0]: + best[key] = (float(dist), np.asarray(voxel, dtype=np.int64)) + if not best: + return 0.0, {} + first_distance = min(value[0] for value in best.values()) + return float(first_distance), {key: float(value[0]) for key, value in best.items()} + + +def generate_projected_material_disk_mask( + active_mask, + *, + spacing, + origin, + center, + normal, + u_axis=None, + v_axis=None, + size=(24.0, 24.0), + shape="square", + thickness=3.0, + intrusion=2.0, + anatomy_constrained=True, + material_mask=None, + extent_start=(0, 0, 0), +): + """Generate a physical-space material disk from an interactive contact plane. + + The plane normal points from the authored plane toward the anatomy. The + disk has a fixed total ``thickness`` in physical units. ``intrusion`` lets + the local anatomy occupy that much of the fixed disk thickness; generated + disk voxels are then cleared anywhere material already exists, so bone is + preserved. + """ + import numpy as np + + active = np.asarray(active_mask, dtype=bool) + if active.ndim != 3: + raise ValueError("active_mask must be a 3D x/y/z array.") + material = active if material_mask is None else np.asarray(material_mask) > 0 + if material.shape != active.shape: + raise ValueError("material_mask must match active_mask shape.") + if not np.any(active): + return np.zeros(active.shape, dtype=bool) + + spacing = tuple(float(value) for value in spacing) + origin = tuple(float(value) for value in origin) + center = np.asarray(center, dtype=float) + if center.shape != (3,): + raise ValueError("center must contain three values.") + normal = _unit_vector(normal, "normal") + if u_axis is None or v_axis is None: + u_axis, v_axis = _plane_axes_from_normal(normal) + else: + u_axis = _unit_vector(u_axis, "u_axis") + v_axis = _unit_vector(v_axis, "v_axis") + if len(size) != 2: + raise ValueError("size must contain two in-plane lengths.") + half_u = max(float(size[0]) / 2.0, 0.5) + half_v = max(float(size[1]) / 2.0, 0.5) + thickness = max(float(thickness), 0.0) + intrusion = max(float(intrusion), 0.0) + if thickness <= 0.0: + return np.zeros(active.shape, dtype=bool) + + active_indices = np.argwhere(active) + active_points = _physical_points_from_indices( + active_indices, + spacing=spacing, + origin=origin, + extent_start=extent_start, + ) + active_rel = active_points - center + active_distance = active_rel @ normal + active_u = active_rel @ u_axis + active_v = active_rel @ v_axis + tolerance = max(min(spacing) * 0.75, 1.0e-6) + active_inside = _inside_projected_shape( + shape, + active_u, + active_v, + half_u, + half_v, + tolerance=tolerance, + ) + forward = active_distance >= -tolerance + candidate = active_inside & forward + if not np.any(candidate): + return np.zeros(active.shape, dtype=bool) + + first_distance, distance_by_key = _surface_distance_by_projected_bucket( + active_indices[candidate], + active_distance[candidate], + active_u[candidate], + active_v[candidate], + spacing=spacing, + ) + if not distance_by_key: + return np.zeros(active.shape, dtype=bool) + + if anatomy_constrained: + depth_limit = first_distance + intrusion + tolerance + distance_by_key = { + key: distance + for key, distance in distance_by_key.items() + if distance <= depth_limit + } + if not distance_by_key: + return np.zeros(active.shape, dtype=bool) + + cap_inner_distance = first_distance + intrusion + cap_outer_distance = cap_inner_distance - thickness + full_indices = np.argwhere(np.ones(active.shape, dtype=bool)) + full_points = _physical_points_from_indices( + full_indices, + spacing=spacing, + origin=origin, + extent_start=extent_start, + ) + rel = full_points - center + distance = rel @ normal + u = rel @ u_axis + v = rel @ v_axis + inside = _inside_projected_shape( + shape, + u, + v, + half_u, + half_v, + tolerance=tolerance, + ) + empty = ~material[tuple(full_indices.T)] + + if anatomy_constrained: + keys = [ + _projected_bucket_key(float(uu), float(vv), spacing=spacing) + for uu, vv in zip(u, v, strict=True) + ] + local_surface = np.asarray( + [distance_by_key.get(key, np.nan) for key in keys], + dtype=float, + ) + bucket_mask = np.isfinite(local_surface) + flat_outer = cap_outer_distance + if flat_outer >= first_distance: + flat_outer = first_distance - thickness + local_min = np.minimum(flat_outer, local_surface) + local_max = np.maximum(flat_outer, local_surface) + depth_ok = (distance >= local_min - tolerance) & (distance <= local_max + tolerance) + else: + bucket_mask = np.ones(full_indices.shape[0], dtype=bool) + depth_ok = ( + (distance >= cap_outer_distance - tolerance) + & (distance <= cap_inner_distance + tolerance) + ) + + keep = inside & depth_ok & empty & bucket_mask + disk = np.zeros(active.shape, dtype=bool) + if np.any(keep): + disk[tuple(full_indices[keep].T)] = True + return disk + + def _generate_projected_bone_caps_from_mask(mask, *, extrusion_axis, thickness, intrusion_depth, shape): """Generate inferior and superior bone caps from projected surface footprints. - This is the older Ogo-style cap builder used for non-``anatomy`` shapes. It - looks at each material column, records the first and last bone voxel, then - builds a flat cap from columns close enough to the global inferior/superior - surfaces. + The builder looks at each material column, records the first and last bone + voxel, then builds a flat cap from columns close enough to the global + inferior/superior surfaces. """ import numpy as np @@ -462,9 +1080,9 @@ def _generate_axis_aligned_anatomy_cap_from_mask(mask, *, extrusion_axis, direct return cap # Keep columns whose surface is close enough to the outermost contact - # surface. This preserves a flat outer cap while allowing the inner side to - # follow nearby anatomy. - maximum_supported_surface_depth = thickness + intrusion + # surface. Intrusion is the amount of the fixed cap thickness that anatomy + # may occupy; deeper columns would make the PMMA thicker than requested. + maximum_supported_surface_depth = intrusion if direction == "up": flat_contact_position = int(np.max(surface_position_by_column[has_bone_in_column])) included_columns = has_bone_in_column & ( @@ -502,27 +1120,6 @@ def _generate_axis_aligned_anatomy_cap_from_mask(mask, *, extrusion_axis, direct return cap -def _projected_contact_within_depth(mask, extrusion_axis, direction, depth): - """Project only columns with bone near the flat fixture contact plane.""" - import numpy as np - - hit = np.where(mask)[extrusion_axis] - contact = int(hit.max() if direction == "up" else hit.min()) - depth = max(int(depth), 1) - if direction == "up": - support_slice = slice(max(contact - depth + 1, 0), contact + 1) - elif direction == "down": - support_slice = slice(contact, min(contact + depth, mask.shape[extrusion_axis])) - else: - raise ValueError("direction must be 'up' or 'down'.") - - if extrusion_axis == 0: - return mask[support_slice, :, :].any(axis=0) - if extrusion_axis == 1: - return mask[:, support_slice, :].any(axis=1) - return mask[:, :, support_slice].any(axis=2) - - def _shaped_contact_cap(mask, surface, extrusion_axis, direction, thickness, shape): import numpy as np @@ -558,52 +1155,11 @@ def _shaped_contact_cap(mask, surface, extrusion_axis, direction, thickness, sha return cap -def _full_fixture_cap( - mask, - surface, - extrusion_axis, - direction, - thickness, - shape, - intrusion=0, - crop_to_contact=False, -): - import numpy as np - - footprint = _projected_footprint(surface, extrusion_axis, shape) - if crop_to_contact: - footprint &= _projected_contact_within_depth(mask, extrusion_axis, direction, intrusion) - cap = np.zeros_like(mask, dtype=bool) - hit = np.where(mask)[extrusion_axis] - contact = int(hit.min() if direction == "down" else hit.max()) - thickness = max(int(thickness), 0) - intrusion = max(int(intrusion), 0) - if direction == "up": - slab_start = contact - intrusion + 1 - slab_stop = slab_start + thickness - elif direction == "down": - slab_stop = contact + intrusion - slab_start = slab_stop - thickness - else: - raise ValueError("direction must be 'up' or 'down'.") - slab = slice( - max(slab_start, 0), - min(slab_stop, mask.shape[extrusion_axis]), - ) - if extrusion_axis == 0: - cap[slab, :, :] = footprint - elif extrusion_axis == 1: - cap[:, slab, :] = footprint[:, None, :] - else: - cap[:, :, slab] = footprint[:, :, None] - return cap - - def generate_bone_cap_mask(mask, *, axis="x", direction="up", thickness=5, shape="fit", intrusion=None): """Generate a one-sided cap mask from a binary bone/contact mask. - Without ``intrusion`` this uses the historical Ogo surface-projection path. - With ``intrusion`` it uses the newer fixed-thickness cap convention: + Without ``intrusion`` this uses the surface-projection path. With + ``intrusion`` it uses the fixed-thickness cap convention: * ``thickness`` is the total requested cap thickness. * ``intrusion`` is how far the anatomy is allowed to occupy that thickness. @@ -660,80 +1216,6 @@ def generate_bone_cap_mask(mask, *, axis="x", direction="up", thickness=5, shape return largest_connected_component(output) -def generate_fixture_cap_mask( - mask, - *, - axis="x", - direction="up", - thickness=5, - shape="box", - intrusion=0, - crop_to_contact=False, -): - """Generate a full geometric fixture cap from a binary ROI/contact mask. - - Fixture caps are used for hip/sideways-fall style contact fixtures. They - keep the full square/round footprint instead of trimming to a bone-fit - surface. - - ``thickness`` is the total fixture thickness. ``intrusion`` shifts that - fixed-thickness slab toward the anatomy; it does not request a thicker cap. - The cap mask may overlap bone, so callers that merge it into a material - image should preserve existing bone material IDs. - """ - import numpy as np - - shape = str(shape).lower() - if shape not in {"box", "round"}: - raise ValueError("fixture cap shape must be 'box' or 'round'.") - thickness = int(thickness) - extrusion_axis = axis_index(axis) - work_bb, _ = _expanded_bbox_for_fixed_thickness_fixture(mask, extrusion_axis, thickness, intrusion) - cropped = mask[work_bb].astype(bool) - surface = _contact_surface(cropped, extrusion_axis, direction) - cap = _full_fixture_cap( - cropped, - surface, - extrusion_axis, - direction, - thickness, - shape, - intrusion=intrusion, - crop_to_contact=crop_to_contact, - ) - - output = np.zeros_like(mask, dtype=bool) - output[work_bb] = cap - return largest_connected_component(output) - - -def label_foreground_in_bounds_vtk(vtk_image, bounds, *, label_value=1): - """Label nonzero voxels inside physical bounds for contact-cap generation.""" - import numpy as np - import vtk - - dims = vtk_image.GetDimensions() - extent = vtk_image.GetExtent() - spacing = vtk_image.GetSpacing() - origin = vtk_image.GetOrigin() - data = vtk_image_to_numpy(vtk_image) - - x = origin[0] + (np.arange(dims[0]) + extent[0]) * spacing[0] - y = origin[1] + (np.arange(dims[1]) + extent[2]) * spacing[1] - z = origin[2] + (np.arange(dims[2]) + extent[4]) * spacing[2] - in_bounds = ( - (x[:, None, None] >= bounds[0]) - & (x[:, None, None] <= bounds[1]) - & (y[None, :, None] >= bounds[2]) - & (y[None, :, None] <= bounds[3]) - & (z[None, None, :] >= bounds[4]) - & (z[None, None, :] <= bounds[5]) - ) - - output_data = np.where((data != 0) & in_bounds, label_value, 0).astype(np.uint16) - return numpy_to_vtk_image(output_data, vtk_image, vtk_array_type=vtk.VTK_UNSIGNED_SHORT) - - def generate_bone_cap_vtk( labelled_vtk_image, *, @@ -764,33 +1246,64 @@ def generate_bone_cap_vtk( ) -def generate_fixture_cap_vtk( - labelled_vtk_image, +def generate_projected_material_disk_vtk( + material_vtk_image, *, - label_value, - axis="x", - direction="up", - thickness=5, - shape="box", - intrusion=0, - crop_to_contact=False, + surface_vtk_image=None, + exclusion_vtk_image=None, + center, + normal, + u_axis=None, + v_axis=None, + size=(24.0, 24.0), + shape="square", + thickness=3.0, + intrusion=2.0, + anatomy_constrained=True, output_value=1, ): - """Generate a full geometric VTK fixture cap from a labelled ROI mask.""" + """Generate a VTK material disk from a physical-space contact plane. + + ``surface_vtk_image`` can be supplied when the anatomical surface should be + traced from a mask rather than from the binned material image. Nonzero + voxels in ``exclusion_vtk_image`` are treated as protected anatomy that the + generated disk must never occupy. + """ import numpy as np + import vtk - labels = vtk_image_to_numpy(labelled_vtk_image, processing_order=True) - cap = generate_fixture_cap_mask( - labels == label_value, - axis=axis, - direction=direction, - thickness=thickness, + material = vtk_image_to_numpy(material_vtk_image) + surface = ( + material + if surface_vtk_image is None + else vtk_image_to_numpy(surface_vtk_image) + ) + exclusion = material > 0 + if exclusion_vtk_image is not None: + exclusion = exclusion | (vtk_image_to_numpy(exclusion_vtk_image) > 0) + if surface.shape != material.shape: + raise ValueError("surface_vtk_image must match material_vtk_image shape.") + if exclusion.shape != material.shape: + raise ValueError("exclusion_vtk_image must match material_vtk_image shape.") + extent = material_vtk_image.GetExtent() + disk = generate_projected_material_disk_mask( + surface > 0, + spacing=material_vtk_image.GetSpacing(), + origin=material_vtk_image.GetOrigin(), + center=center, + normal=normal, + u_axis=u_axis, + v_axis=v_axis, + size=size, shape=shape, + thickness=thickness, intrusion=intrusion, - crop_to_contact=crop_to_contact, + anatomy_constrained=anatomy_constrained, + material_mask=exclusion, + extent_start=(extent[0], extent[2], extent[4]), ) return numpy_to_vtk_image( - (cap.astype(np.uint16) * int(output_value)), - labelled_vtk_image, - processing_order=True, + disk.astype(np.uint16) * int(output_value), + material_vtk_image, + vtk_array_type=vtk.VTK_UNSIGNED_SHORT, ) diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index b6ce94f..e150adc 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -4,32 +4,30 @@ 1. Read the calibrated density image and whole-femur mask. The public wrapper chooses left/right from ``--side`` and the lower-level script uses ``femur_side=1`` for left and ``2`` for right. -2. Pre-rotate the femur by side to provide a stable starting orientation for - ICP. This is a VTK reslice in the native input grid. -3. Run ICP to the bundled side-specific femur reference, then apply the ICP - transform and set the final output spacing to 1.0 x 1.0 x 1.0 mm in one - shared VTK reslice helper. Image data use cubic interpolation; bone and - compartment labels use nearest-neighbor interpolation. -4. Smooth the transformed femur mask with one binary close/open pass only when +2. Run ICP to the bundled side-specific femur reference from the cropped and + padded input geometry, then build an explicit reference-frame output grid + from the transformed femur surface. Density is resampled with B-spline + interpolation; bone, crop-face, and compartment labels use nearest-neighbor + interpolation on the same grid. +3. Smooth the transformed femur mask with one binary close/open pass only when at least one input spacing dimension is coarser than 2 mm. If a compartment mask is supplied, the derived cortical binary mask follows the same rule. -5. Standardize the distal shaft by cutting on a flat model-grid z plane. The +4. Standardize the distal shaft by cutting on a flat model-grid z plane. The default cut mode detects the lesser-trochanter cross-section peak and keeps the femur to 50 mm distal to that landmark. If the scan does not include the required distal field of view, model generation fails instead of silently using a shorter shaft. ``fixed_length`` mode is available for debugging. -6. Generate two geometric PMMA fixtures: a round femoral-head loading fixture - and a box-like greater-trochanter contact fixture. Defaults are 6 mm PMMA - thickness and 6 mm intrusion through that fixed thickness. The fixture masks - themselves do not overwrite bone voxels; intrusion only determines - which anatomy is close enough to support the cap. The femoral-head footprint - is widened by 10 mm and lengthened by 80 mm so the anatomical cropping - determines the actual contact. -7. Apply sideways-fall boundary conditions: prescribed displacement at the +5. Generate two geometric PMMA fixtures from bbox-relative contact planes: + a femoral-head loading fixture on the high-y side and a greater-trochanter + contact fixture on the low-y side. Defaults are 10 mm PMMA thickness and + 6 mm intrusion through that fixed thickness. The square fixture footprints + scale with the generated model bbox, and the fixture masks themselves do + not overwrite bone voxels. +6. Apply sideways-fall boundary conditions: prescribed displacement at the femoral-head PMMA cap toward the greater trochanter, loading-direction constraint at the greater-trochanter PMMA cap, and distal shaft constraints to remove rigid-body motion. -8. Build materials with the same shared bone/PMMA material-table helper used by +7. Build materials with the same shared bone/PMMA material-table helper used by the spine workflow. If no compartment mask is supplied, femur bone is one trabecular-style region. If supplied, cortical=1 and trabecular=2 by default. """ @@ -46,11 +44,20 @@ DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM = 2.0 FEMORAL_HEAD_FIXTURE_WIDTH_EXTENSION_MM = 10.0 FEMORAL_HEAD_FIXTURE_LONG_AXIS_EXTENSION_MM = 80.0 -DEFAULT_PMMA_THICKNESS_MM = 6.0 +SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION = (1.1, 1.1) +FEMORAL_HEAD_FIXTURE_CENTER_FRACTION = (0.5, 1.0278767152, 0.9650933279) +GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION = (0.5, 0.0151983839, 0.6551527108) +DISTAL_SHAFT_FIXTURE_CENTER_FRACTION = (0.1958514895, 0.5708725889, 0.0601674635) +DISTAL_SHAFT_FIXTURE_SIZE_FRACTION = (0.3640088821, 0.6351022953) +DEFAULT_FEMUR_BBOX_RATIO = (1.0, 1.2, None) +DEFAULT_FEMUR_BBOX_CROP_FROM = (None, "min", None) +DEFAULT_FEMUR_REFERENCE_MIN_SCALE = (0.8, 0.8, 0.75) +DEFAULT_FEMUR_REFERENCE_MAX_SCALE = (1.2, 1.2, 1.3) +DEFAULT_PMMA_THICKNESS_MM = 10.0 DEFAULT_PMMA_INTRUSION_MM = 6.0 DEFAULT_FEMUR_INPUT_MARGIN_MM = DEFAULT_PMMA_THICKNESS_MM + DEFAULT_PMMA_INTRUSION_MM DEFAULT_FEMUR_SHAFT_LENGTH_MM = 100.0 -DEFAULT_FEMUR_CUT_MODE = "lesser_trochanter" +DEFAULT_FEMUR_CUT_MODE = "bbox_ratio" DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM = 50.0 DEFAULT_CORTICAL_LABEL = 1 DEFAULT_TRABECULAR_LABEL = 2 @@ -74,21 +81,1003 @@ def side_suffix(femur_side): raise ValueError("femur_side must be 1 for left or 2 for right.") -def side_rotation(femur_side): - """Return the pre-alignment z rotation for a femur side.""" - if femur_side == LEFT_FEMUR: - return 90 - if femur_side == RIGHT_FEMUR: - return -90 - raise ValueError("femur_side must be 1 for left or 2 for right.") - - def sideways_fall_output_name(output_file, femur_side): """Return the compact side-specific sideways-fall output path.""" output_path = Path(output_file) return str(output_path.with_name(f"{output_path.stem}_{side_suffix(femur_side)}.n88model")) +def _axis_index(axis): + axis_map = {"x": 0, "y": 1, "z": 2} + try: + return axis_map[str(axis).lower()] + except KeyError as exc: + raise ValueError("projection_axis must be 'x', 'y', or 'z'.") from exc + + +def _axis_bounds(model_bounds, axis): + index = 2 * int(axis) + return float(model_bounds[index]), float(model_bounds[index + 1]) + + +def foreground_voxel_center_bounds_from_mask(mask, *, origin, spacing): + """Return x/y/z physical bounds of nonzero voxel centers. + + Bbox-relative workflow fixtures are authored against the occupied voxel + centers, matching the image-space convention used by interactive workflow + replay. FE mesh/node bounds are wider by the element faces and should not + be used to resolve those recipe planes. + """ + import numpy as np + + active = np.asarray(mask) != 0 + coords = np.argwhere(active) + if coords.size == 0: + raise ValueError("Cannot compute foreground bounds from an empty mask.") + origin = np.asarray(origin, dtype=np.float64) + spacing = np.asarray(spacing, dtype=np.float64) + if origin.shape != (3,) or spacing.shape != (3,): + raise ValueError("origin and spacing must contain three values.") + lo = origin + coords.min(axis=0).astype(np.float64) * spacing + hi = origin + coords.max(axis=0).astype(np.float64) * spacing + return ( + float(lo[0]), + float(hi[0]), + float(lo[1]), + float(hi[1]), + float(lo[2]), + float(hi[2]), + ) + + +def foreground_voxel_center_bounds(vtk_image): + """Return physical foreground bounds for a VTK image using voxel centers.""" + from ogo.util.vtk_image import vtk_image_to_numpy + + return foreground_voxel_center_bounds_from_mask( + vtk_image_to_numpy(vtk_image), + origin=vtk_image.GetOrigin(), + spacing=vtk_image.GetSpacing(), + ) + + +def matrix4x4_to_numpy(matrix): + """Return a 4 x 4 NumPy matrix from a VTK matrix or array-like value.""" + import numpy as np + + if hasattr(matrix, "GetElement"): + return np.asarray( + [[float(matrix.GetElement(row, col)) for col in range(4)] for row in range(4)], + dtype=np.float64, + ) + values = np.asarray(matrix, dtype=np.float64) + if values.shape != (4, 4): + raise ValueError("transform matrix must have shape 4 x 4.") + return values + + +def reference_grid_from_output_to_input_matrix( + points_xyz, + output_to_input_matrix, + *, + spacing, + margin_voxels=4, +): + """Return an explicit output grid for a transformed surface point cloud. + + Registration matrices used by image resampling map output coordinates back + to input coordinates. To build the output lattice, first map input surface + points through the inverse matrix, then pad the transformed bounds by a + fixed number of voxels. This keeps the final model grid stable and + independent of toolkit-specific automatic crop rules. + """ + import numpy as np + + points = np.asarray(points_xyz, dtype=np.float64) + if points.ndim != 2 or points.shape[1] != 3 or points.shape[0] == 0: + raise ValueError("points_xyz must have shape (n, 3) with at least one point.") + matrix = matrix4x4_to_numpy(output_to_input_matrix) + inverse = np.linalg.inv(matrix) + homogeneous = np.column_stack([points, np.ones(points.shape[0], dtype=np.float64)]) + transformed = (homogeneous @ inverse.T)[:, :3] + spacing_arr = np.asarray(spacing, dtype=np.float64) + if spacing_arr.shape != (3,) or np.any(spacing_arr <= 0.0): + raise ValueError("spacing must contain three positive values.") + margin = max(0, int(margin_voxels)) + lower = transformed.min(axis=0) - margin * spacing_arr + upper = transformed.max(axis=0) + margin * spacing_arr + size = np.maximum(1, np.ceil((upper - lower) / spacing_arr).astype(int) + 1) + return tuple(float(value) for value in lower), tuple(int(value) for value in size) + + +def reference_grid_from_vtk_mask( + vtk_mask, + output_to_input_matrix, + *, + margin_voxels=4, +): + """Return an explicit reference-frame output grid for a transformed mask.""" + points = surface_points_from_vtk_mask(vtk_mask, max_points=None) + return reference_grid_from_output_to_input_matrix( + points, + output_to_input_matrix, + spacing=vtk_mask.GetSpacing(), + margin_voxels=margin_voxels, + ) + + +def transform_resample_vtk_image_to_reference_grid( + vtk_image, + output_to_input_matrix, + *, + output_origin, + output_size, + output_spacing, + interpolation="nearest", +): + """Resample a VTK image onto an explicit reference-frame grid.""" + import numpy as np + import SimpleITK as sitk + import vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + matrix = matrix4x4_to_numpy(output_to_input_matrix) + array_zyx = vtk_image_to_numpy(vtk_image, processing_order=True) + image = sitk.GetImageFromArray(array_zyx) + image.SetSpacing(tuple(float(value) for value in vtk_image.GetSpacing())) + image.SetOrigin(tuple(float(value) for value in vtk_image.GetOrigin())) + image.SetDirection((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) + + transform = sitk.AffineTransform(3) + transform.SetMatrix(tuple(float(value) for value in matrix[:3, :3].reshape(-1))) + transform.SetTranslation(tuple(float(value) for value in matrix[:3, 3])) + + resampler = sitk.ResampleImageFilter() + resampler.SetSize([int(value) for value in output_size]) + resampler.SetOutputSpacing(tuple(float(value) for value in output_spacing)) + resampler.SetOutputOrigin(tuple(float(value) for value in output_origin)) + resampler.SetOutputDirection((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) + resampler.SetTransform(transform) + resampler.SetDefaultPixelValue(0) + resampler.SetInterpolator( + sitk.sitkNearestNeighbor + if interpolation == "nearest" + else sitk.sitkBSpline + if interpolation == "bspline" + else sitk.sitkLinear + ) + out_zyx = sitk.GetArrayFromImage(resampler.Execute(image)) + out_xyz = np.transpose(out_zyx, (2, 1, 0)) + vtk_type = vtk.VTK_UNSIGNED_CHAR if interpolation == "nearest" else vtk_image.GetScalarType() + out = _vtk_image_from_array( + out_xyz, + vtk_image, + origin=tuple(float(value) for value in output_origin), + vtk_array_type=vtk_type, + ) + out.SetSpacing(tuple(float(value) for value in output_spacing)) + return out + + +def bbox_relative_fixture_bounds( + model_bounds, + *, + center_fraction, + size_fraction, + projection_axis="y", + shape="rectangle", +): + """Return physical contact ROI bounds from model-bbox-relative plane values. + + ``center_fraction`` has x/y/z fractions relative to the model bbox. The two + ``size_fraction`` values scale the lateral axes of the projection plane in + coordinate order. When ``shape="square"``, the smaller scaled lateral + length is used on both axes. The projection axis itself spans the whole + model bbox so the cap builder can find the nearest supported anatomy + column. + """ + from ogo.fea.boundary import bbox_relative_contact_bounds + + return bbox_relative_contact_bounds( + model_bounds, + center_fraction=center_fraction, + size_fraction=size_fraction, + projection_axis=projection_axis, + shape=shape, + ) + + +def bbox_relative_fixture_direction(center_fraction, *, projection_axis="y"): + """Return the cap side implied by a bbox-relative plane fraction.""" + from ogo.fea.boundary import bbox_relative_contact_direction + + return bbox_relative_contact_direction( + center_fraction, + projection_axis=projection_axis, + ) + + +def _axis_extent_from_vector(model_bounds, vector): + import numpy as np + + spans = np.asarray( + [ + float(model_bounds[1]) - float(model_bounds[0]), + float(model_bounds[3]) - float(model_bounds[2]), + float(model_bounds[5]) - float(model_bounds[4]), + ], + dtype=float, + ) + return float(np.sum(np.abs(np.asarray(vector, dtype=float)) * spans)) + + +def _fixture_plane_axes(projection_axis, normal_sign): + """Return stable in-plane axes for bbox-relative fixture planes.""" + axis = _axis_index(projection_axis) + sign = 1.0 if float(normal_sign) >= 0.0 else -1.0 + if axis == 0: + return (0.0, 0.0, sign), (0.0, -1.0, 0.0) + if axis == 1: + return (0.0, 0.0, -sign), (-1.0, 0.0, 0.0) + return (1.0, 0.0, 0.0), (0.0, sign, 0.0) + + +def bbox_relative_fixture_plane( + model_bounds, + *, + center_fraction, + size_fraction, + projection_axis="y", + shape="square", +): + """Return a physical contact-plane definition from bbox-relative values.""" + from ogo.fea.boundary import bbox_relative_contact_plane + + return bbox_relative_contact_plane( + model_bounds, + center_fraction=center_fraction, + size_fraction=size_fraction, + projection_axis=projection_axis, + shape=shape, + ) + + +def _scale_triplet(values, name): + import numpy as np + + parsed = np.asarray(values, dtype=float) + if parsed.shape != (3,): + raise ValueError(f"{name} must contain three x/y/z values.") + return parsed + + +def principal_axis_lengths(polydata): + """Return approximate principal-axis diameters for a VTK polydata surface.""" + import numpy as np + from vtk.util.numpy_support import vtk_to_numpy + + points = polydata.GetPoints() + if points is None or points.GetNumberOfPoints() == 0: + raise ValueError("Cannot measure principal axes for empty polydata.") + coordinates = np.asarray(vtk_to_numpy(points.GetData()), dtype=float) + centered = coordinates - coordinates.mean(axis=0) + covariance = np.cov(centered.T) + eigvals = np.linalg.eigvalsh(covariance) + return np.sqrt(np.maximum(eigvals, 0.0)) * 2.0 + + +def polydata_points(polydata): + """Return VTK polydata point coordinates as an ``(n, 3)`` NumPy array.""" + import numpy as np + from vtk.util.numpy_support import vtk_to_numpy + + points = polydata.GetPoints() + if points is None or points.GetNumberOfPoints() == 0: + raise ValueError("Polydata contains no points.") + coordinates = np.asarray(vtk_to_numpy(points.GetData()), dtype=float) + if coordinates.ndim != 2 or coordinates.shape[1] != 3: + raise ValueError("Polydata points must have shape (n, 3).") + return coordinates[np.all(np.isfinite(coordinates), axis=1)] + + +def polydata_from_points(points): + """Create a vertex-only VTK polydata from an ``(n, 3)`` point cloud.""" + import numpy as np + import vtk + + coordinates = np.asarray(points, dtype=float) + if coordinates.ndim != 2 or coordinates.shape[1] != 3 or coordinates.shape[0] < 3: + raise ValueError("points must have shape (n, 3) with at least three rows.") + + vtk_points = vtk.vtkPoints() + vertices = vtk.vtkCellArray() + for point in coordinates: + point_id = vtk_points.InsertNextPoint(float(point[0]), float(point[1]), float(point[2])) + vertices.InsertNextCell(1) + vertices.InsertCellPoint(point_id) + + polydata = vtk.vtkPolyData() + polydata.SetPoints(vtk_points) + polydata.SetVerts(vertices) + return polydata + + +def sample_points(points, *, max_points=None, mode="linspace", offset=0): + """Sample a point cloud using the same small deterministic modes as workflows.""" + import numpy as np + + coordinates = np.asarray(points, dtype=float) + if max_points is None or int(max_points) <= 0 or coordinates.shape[0] <= int(max_points): + return coordinates + + token = str(mode).strip().lower() + if token == "stride": + step = max(1, int(np.ceil(coordinates.shape[0] / int(max_points)))) + start = int(offset) % step + indices = np.arange(start, coordinates.shape[0], step, dtype=int) + if indices.size < int(max_points): + fallback = np.linspace(0, coordinates.shape[0] - 1, int(max_points), dtype=int) + indices = np.unique(np.concatenate([indices, fallback])) + indices = indices[: int(max_points)] + else: + indices = np.linspace(0, coordinates.shape[0] - 1, int(max_points), dtype=int) + return coordinates[indices] + + +def _binary_erosion_6(mask): + import numpy as np + + padded = np.pad(mask, 1, mode="constant", constant_values=False) + center = padded[1:-1, 1:-1, 1:-1] + eroded = center.copy() + eroded &= padded[:-2, 1:-1, 1:-1] + eroded &= padded[2:, 1:-1, 1:-1] + eroded &= padded[1:-1, :-2, 1:-1] + eroded &= padded[1:-1, 2:, 1:-1] + eroded &= padded[1:-1, 1:-1, :-2] + eroded &= padded[1:-1, 1:-1, 2:] + return eroded + + +def surface_points_from_vtk_mask( + vtk_mask, + *, + max_points=8000, + sample_mode="stride", + sample_offset=0, +): + """Return physical x/y/z points from the 6-connected voxel mask surface.""" + import numpy as np + from ogo.util.vtk_image import vtk_image_to_numpy + + mask = np.asarray(vtk_image_to_numpy(vtk_mask), dtype=bool) + if not np.any(mask): + raise ValueError("mask contains no foreground voxels.") + + surface = mask & ~_binary_erosion_6(mask) + # Match the workflow-replay path: enumerate the voxel surface in z/y/x + # NumPy order, then convert those indices back to physical x/y/z points. + surface_zyx = np.transpose(surface if np.any(surface) else mask, (2, 1, 0)) + coords_xyz = np.argwhere(surface_zyx)[:, [2, 1, 0]].astype(float) + points = np.asarray(vtk_mask.GetOrigin(), dtype=float) + coords_xyz * np.asarray( + vtk_mask.GetSpacing(), + dtype=float, + ) + return sample_points( + points, + max_points=max_points, + mode=sample_mode, + offset=sample_offset, + ) + + +def point_cloud_axis_lengths(points): + """Return approximate principal-axis diameters for an ``(n, 3)`` point cloud.""" + import numpy as np + + coordinates = np.asarray(points, dtype=float) + if coordinates.ndim != 2 or coordinates.shape[1] != 3 or coordinates.shape[0] < 3: + raise ValueError("points must have shape (n, 3) with at least three rows.") + centered = coordinates - coordinates.mean(axis=0) + covariance = np.cov(centered.T) + eigvals = np.linalg.eigvalsh(covariance) + return np.sqrt(np.maximum(eigvals, 0.0)) * 2.0 + + +def scale_reference_point_cloud_to_sample( + reference_polydata, + sample_surface_points, + *, + max_points=8000, + reference_sample_mode="linspace", + min_scale=DEFAULT_FEMUR_REFERENCE_MIN_SCALE, + max_scale=DEFAULT_FEMUR_REFERENCE_MAX_SCALE, +): + """Scale sampled reference points to the sampled voxel-surface point cloud.""" + import numpy as np + + reference_points = sample_points( + polydata_points(reference_polydata), + max_points=max_points, + mode=reference_sample_mode, + ) + sample_points_array = np.asarray(sample_surface_points, dtype=float) + reference_lengths = point_cloud_axis_lengths(reference_points) + sample_lengths = point_cloud_axis_lengths(sample_points_array) + scale = sample_lengths / np.maximum(reference_lengths, 1.0e-6) + min_values = _scale_triplet(min_scale, "min_scale") + max_values = _scale_triplet(max_scale, "max_scale") + scale = np.clip(scale, min_values, max_values) + + scaled_points = reference_points * scale + return polydata_from_points(scaled_points), { + "source": "voxel_surface_point_cloud", + "reference_axis_lengths": reference_lengths.tolist(), + "sample_axis_lengths": sample_lengths.tolist(), + "scale_factors": scale.tolist(), + "min_scale": min_values.tolist(), + "max_scale": max_values.tolist(), + } + + +def scale_reference_to_sample_principal_lengths( + reference_polydata, + sample_polydata, + *, + min_scale=DEFAULT_FEMUR_REFERENCE_MIN_SCALE, + max_scale=DEFAULT_FEMUR_REFERENCE_MAX_SCALE, +): + """Scale a femur reference so its principal lengths match the sample.""" + import numpy as np + import vtk + + reference_lengths = principal_axis_lengths(reference_polydata) + sample_lengths = principal_axis_lengths(sample_polydata) + scale = sample_lengths / np.maximum(reference_lengths, 1.0e-6) + min_values = _scale_triplet(min_scale, "min_scale") + max_values = _scale_triplet(max_scale, "max_scale") + scale = np.clip(scale, min_values, max_values) + + transform = vtk.vtkTransform() + transform.Scale(float(scale[0]), float(scale[1]), float(scale[2])) + + transform_filter = vtk.vtkTransformPolyDataFilter() + transform_filter.SetInputData(reference_polydata) + transform_filter.SetTransform(transform) + transform_filter.Update() + + return transform_filter.GetOutput(), { + "reference_axis_lengths": reference_lengths.tolist(), + "sample_axis_lengths": sample_lengths.tolist(), + "scale_factors": scale.tolist(), + "min_scale": min_values.tolist(), + "max_scale": max_values.tolist(), + } + + +def _parse_bbox_ratio_recipe(values): + """Return bbox-ratio values in Ogo's x/y/z image order. + + Saved workflows store bbox ratios in recipe order: + ``reference, constrained, free``. For the hip sideways-fall recipe the + reference axis is y, the constrained crop axis is z, and x is left free. + Ogo image arrays are x/y/z, so the mapped order is ``free, reference, + constrained``. + """ + if len(values) != 3: + raise ValueError("bbox_ratio must contain reference/constrained/free values.") + parsed = [] + for item in values: + if item is None: + parsed.append(None) + continue + token = str(item).strip().lower() + if token in {"", "none", "null", "auto"}: + parsed.append(None) + continue + value = float(item) + if value <= 0: + raise ValueError("bbox_ratio values must be positive or null.") + parsed.append(value) + reference, constrained, free = parsed + return free, reference, constrained + + +def _crop_from_value(value): + if value is None: + return None + token = str(value).strip().lower() + if token in {"", "none", "null", "auto", "center", "centre"}: + return None + if token in {"min", "low", "lo", "start"}: + return "min" + if token in {"max", "high", "hi", "end"}: + return "max" + raise ValueError("bbox_crop_from values must be min, max, center, or null.") + + +def _parse_bbox_crop_from_recipe(values): + """Return bbox crop-end values in Ogo's x/y/z image order. + + The recipe uses the same reference/constrained/free convention as the + Slicer-authored workflow. For the hip sideways-fall recipe the mapped order + is ``free, reference, constrained`` in Ogo's x/y/z image axes. + """ + if values is None: + return None, None, None + if len(values) != 3: + raise ValueError("bbox_crop_from must contain reference/constrained/free values.") + reference, constrained, free = (_crop_from_value(item) for item in values) + return free, reference, constrained + + +def _vtk_image_from_array(array, template_vtk_image, *, origin, vtk_array_type=None): + """Create a zero-based VTK image from an x/y/z NumPy array.""" + import numpy as np + import vtk + from vtk.util.numpy_support import numpy_to_vtk + + data = np.ascontiguousarray(array) + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(*origin) + image.SetSpacing(*template_vtk_image.GetSpacing()) + if vtk_array_type is None: + vtk_array_type = template_vtk_image.GetScalarType() + scalars = numpy_to_vtk(data.ravel(order="F"), deep=True, array_type=vtk_array_type) + image.GetPointData().SetScalars(scalars) + return image + + +def resample_vtk_image_like_workflow(vtk_image, target_spacing_mm, *, interpolation="nearest"): + """Resample a VTK image with the same output-size rule as workflow replay.""" + import numpy as np + import SimpleITK as sitk + import vtk + from ogo.util.vtk_image import vtk_image_to_numpy + + target = float(target_spacing_mm) + target_spacing = (target, target, target) + array_zyx = vtk_image_to_numpy(vtk_image, processing_order=True) + image = sitk.GetImageFromArray(array_zyx) + image.SetSpacing(tuple(float(value) for value in vtk_image.GetSpacing())) + image.SetOrigin(tuple(float(value) for value in vtk_image.GetOrigin())) + original_size = np.asarray(image.GetSize(), dtype=np.int64) + original_spacing = np.asarray(image.GetSpacing(), dtype=np.float64) + new_spacing = np.asarray(target_spacing, dtype=np.float64) + new_size = np.maximum(1, np.round(original_size * original_spacing / new_spacing)).astype(int) + + resampler = sitk.ResampleImageFilter() + resampler.SetOutputSpacing(target_spacing) + resampler.SetSize([int(value) for value in new_size]) + resampler.SetOutputOrigin(image.GetOrigin()) + resampler.SetOutputDirection(image.GetDirection()) + resampler.SetDefaultPixelValue(0) + resampler.SetInterpolator( + sitk.sitkNearestNeighbor + if interpolation == "nearest" + else sitk.sitkBSpline + if interpolation == "bspline" + else sitk.sitkLinear + ) + out_zyx = sitk.GetArrayFromImage(resampler.Execute(image)) + out_xyz = np.transpose(out_zyx, (2, 1, 0)) + vtk_type = vtk.VTK_UNSIGNED_CHAR if interpolation == "nearest" else vtk_image.GetScalarType() + out = _vtk_image_from_array( + out_xyz, + vtk_image, + origin=vtk_image.GetOrigin(), + vtk_array_type=vtk_type, + ) + out.SetSpacing(*target_spacing) + return out + + +def crop_vtk_images_to_bbox_ratio( + vtk_images, + vtk_mask, + *, + bbox_ratio=DEFAULT_FEMUR_BBOX_RATIO, + bbox_crop_from=None, + labels=None, +): + """Crop VTK images to a mask-bbox ratio and return the retained crop face. + + Ratios are interpreted in Ogo's x/y/z VTK image order. The returned + crop-face image has value 1 on the newly exposed one-sided crop surface. + For the hip recipe this is the z-min face created by + ``bbox_ratio=(1, 1.2, None)`` and ``bbox_crop_from=(None, "min", None)``. + """ + import numpy as np + import vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + images = list(vtk_images) + mask_data = vtk_image_to_numpy(vtk_mask) + if labels: + active = np.isin(mask_data, sorted(int(label) for label in labels)) + else: + active = mask_data != 0 + if not np.any(active): + raise ValueError("Cannot apply bbox-ratio crop to an empty femur mask.") + + ratio_xyz = _parse_bbox_ratio_recipe(bbox_ratio) + crop_from_xyz = _parse_bbox_crop_from_recipe(bbox_crop_from) + numeric_axes = [axis for axis, value in enumerate(ratio_xyz) if value is not None] + if not numeric_axes: + empty_face = np.zeros(mask_data.shape, dtype=np.uint8) + return images, _vtk_image_from_array(empty_face, vtk_mask, origin=vtk_mask.GetOrigin(), vtk_array_type=vtk.VTK_UNSIGNED_CHAR), { + "enabled": False, + "ratio_recipe": tuple(bbox_ratio), + "ratio_xyz": ratio_xyz, + "crop_from_xyz": crop_from_xyz, + } + + reference_axes = [axis for axis in numeric_axes if np.isclose(float(ratio_xyz[axis]), 1.0)] + if not reference_axes: + raise ValueError("bbox_ratio must contain one preserved axis with value 1.") + + coords = np.argwhere(active) + lo = coords.min(axis=0).astype(np.int64) + hi = (coords.max(axis=0) + 1).astype(np.int64) + size = hi - lo + spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) + physical_size = size.astype(np.float64) * spacing + reference_axis = min(reference_axes, key=lambda axis: float(physical_size[axis])) + reference_length_mm = float(size[reference_axis]) * float(spacing[reference_axis]) + + out_lo = lo.copy() + out_hi = hi.copy() + crop_surface = None + for axis, axis_ratio in enumerate(ratio_xyz): + if axis_ratio is None: + continue + target_mm = reference_length_mm * float(axis_ratio) + target_voxels = max(1, int(round(target_mm / float(spacing[axis])))) + target_voxels = min(int(size[axis]), target_voxels) + mode = crop_from_xyz[axis] + if mode == "min": + start = int(hi[axis]) - target_voxels + crop_surface = {"axis": axis, "side": "min", "local_index": 0, "normal_sign": -1.0} + elif mode == "max": + start = int(lo[axis]) + crop_surface = { + "axis": axis, + "side": "max", + "local_index": target_voxels - 1, + "normal_sign": 1.0, + } + else: + center = 0.5 * (float(lo[axis]) + float(hi[axis])) + start = int(round(center - 0.5 * float(target_voxels))) + start = max(int(lo[axis]), min(start, int(hi[axis]) - target_voxels)) + out_lo[axis] = start + out_hi[axis] = start + target_voxels + + slices = tuple(slice(int(out_lo[axis]), int(out_hi[axis])) for axis in range(3)) + origin = tuple( + float(vtk_mask.GetOrigin()[axis]) + float(out_lo[axis]) * float(spacing[axis]) + for axis in range(3) + ) + cropped_images = [ + _vtk_image_from_array(vtk_image_to_numpy(image)[slices], image, origin=origin) + for image in images + ] + + cropped_active = active[slices] + crop_face = np.zeros(cropped_active.shape, dtype=np.uint8) + if crop_surface is not None: + axis = int(crop_surface["axis"]) + index = int(crop_surface["local_index"]) + face_slice = [slice(None), slice(None), slice(None)] + face_slice[axis] = index + crop_face[tuple(face_slice)] = cropped_active[tuple(face_slice)].astype(np.uint8) + + crop_face_image = _vtk_image_from_array( + crop_face, + vtk_mask, + origin=origin, + vtk_array_type=vtk.VTK_UNSIGNED_CHAR, + ) + meta = { + "enabled": True, + "ratio_recipe": tuple(bbox_ratio), + "crop_from_recipe": tuple(bbox_crop_from) if bbox_crop_from is not None else None, + "ratio_xyz": ratio_xyz, + "crop_from_xyz": crop_from_xyz, + "reference_axis": "xyz"[reference_axis], + "reference_length_mm": reference_length_mm, + "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), + "crop_slices_xyz": tuple((int(out_lo[axis]), int(out_hi[axis])) for axis in range(3)), + "output_shape_xyz": tuple(int(value) for value in cropped_active.shape), + "output_origin": origin, + "crop_surface": None + if crop_surface is None + else { + "axis": "xyz"[int(crop_surface["axis"])], + "side": crop_surface["side"], + "normal_sign": crop_surface["normal_sign"], + }, + "crop_face_voxels": int(crop_face.sum()), + } + return cropped_images, crop_face_image, meta + + +def crop_face_support_vector(crop_face_vtk, mask_vtk): + """Estimate the outward normal of a transformed crop-face label image.""" + import numpy as np + + from ogo.util.vtk_image import vtk_image_to_numpy + + face = vtk_image_to_numpy(crop_face_vtk) != 0 + bone = vtk_image_to_numpy(mask_vtk) != 0 + if not np.any(face): + raise ValueError("Cannot estimate crop-face support vector from an empty face mask.") + if not np.any(bone): + raise ValueError("Cannot orient crop-face support vector from an empty femur mask.") + + spacing = np.asarray(crop_face_vtk.GetSpacing(), dtype=np.float64) + origin = np.asarray(crop_face_vtk.GetOrigin(), dtype=np.float64) + face_coords = np.argwhere(face).astype(np.float64) + bone_coords = np.argwhere(bone).astype(np.float64) + face_points = origin + face_coords * spacing + bone_points = origin + bone_coords * spacing + if face_points.shape[0] < 3: + raise ValueError("At least three crop-face voxels are required to estimate a surface normal.") + + centered = face_points - face_points.mean(axis=0) + _u, _s, vh = np.linalg.svd(centered, full_matrices=False) + normal = np.asarray(vh[-1], dtype=np.float64) + norm = float(np.linalg.norm(normal)) + if norm <= 0.0: + raise ValueError("Cannot estimate crop-face support vector from degenerate face points.") + normal /= norm + + # The vector should point out of the retained bone, toward the cropped-away + # side. Orient it by comparing the face centroid to the retained-bone + # centroid. + into_bone = bone_points.mean(axis=0) - face_points.mean(axis=0) + if float(np.dot(normal, into_bone)) > 0.0: + normal = -normal + return tuple(float(value) for value in normal) + + +def _image_index_points(vtk_image, indices): + """Return physical center coordinates for VTK image array indices.""" + import numpy as np + + extent = vtk_image.GetExtent() + offset = np.asarray([extent[0], extent[2], extent[4]], dtype=np.float64) + spacing = np.asarray(vtk_image.GetSpacing(), dtype=np.float64) + origin = np.asarray(vtk_image.GetOrigin(), dtype=np.float64) + return origin + (np.asarray(indices, dtype=np.float64) + offset) * spacing + + +def _unit_vector(vector, name): + import numpy as np + + values = np.asarray(vector, dtype=np.float64) + if values.shape != (3,) or not np.all(np.isfinite(values)): + raise ValueError(f"{name} must be a finite 3-vector.") + norm = float(np.linalg.norm(values)) + if norm <= 0.0: + raise ValueError(f"{name} must be non-zero.") + return values / norm + + +def bbox_relative_oriented_contact_plane( + model_bounds, + *, + center_fraction, + size_fraction, + normal, + width_axis=(-1.0, 0.0, 0.0), + shape="anatomy", +): + """Return a bbox-relative plane that keeps a measured contact normal. + + The center and footprint scale with the generated model bounding box. The + normal comes from measured geometry, such as a transformed crop face, so the + plane can follow an oblique distal shaft cut while still using a stable + recipe-sized contact patch. + """ + import numpy as np + + if len(model_bounds) != 6: + raise ValueError("model_bounds must contain x/y/z min/max values.") + if len(center_fraction) != 3: + raise ValueError("center_fraction must contain x/y/z fractions.") + if len(size_fraction) != 2: + raise ValueError("size_fraction must contain the two in-plane scale factors.") + + normal_vec = _unit_vector(normal, "contact plane normal") + width_vec = np.asarray(width_axis, dtype=np.float64) + if width_vec.shape != (3,) or not np.all(np.isfinite(width_vec)): + raise ValueError("width_axis must be a finite 3-vector.") + + width_vec = width_vec - normal_vec * float(np.dot(width_vec, normal_vec)) + if float(np.linalg.norm(width_vec)) <= 1.0e-8: + fallback_axes = np.eye(3, dtype=np.float64) + width_vec = max( + (axis - normal_vec * float(np.dot(axis, normal_vec)) for axis in fallback_axes), + key=lambda candidate: float(np.linalg.norm(candidate)), + ) + v_axis = _unit_vector(width_vec, "contact plane width axis") + u_axis = _unit_vector(np.cross(v_axis, normal_vec), "contact plane long axis") + + center = [] + for axis in range(3): + lo, hi = _axis_bounds(model_bounds, axis) + center.append(lo + float(center_fraction[axis]) * (hi - lo)) + + size = ( + _axis_extent_from_vector(model_bounds, u_axis) * float(size_fraction[0]), + _axis_extent_from_vector(model_bounds, v_axis) * float(size_fraction[1]), + ) + if size[0] <= 0.0 or size[1] <= 0.0: + raise ValueError("size_fraction values must produce positive plane dimensions.") + + return { + "center": tuple(float(value) for value in center), + "normal": tuple(float(value) for value in normal_vec), + "outward_normal": tuple(float(value) for value in -normal_vec), + "u_axis": tuple(float(value) for value in u_axis), + "v_axis": tuple(float(value) for value in v_axis), + "size": tuple(float(value) for value in size), + "shape": str(shape).strip().lower(), + } + + +def _inside_plane_shape(shape, u_values, v_values, half_u, half_v, *, tolerance): + import numpy as np + + token = str(shape).strip().lower() + if token == "square": + half = min(float(half_u), float(half_v)) + return (np.abs(u_values) <= half + tolerance) & (np.abs(v_values) <= half + tolerance) + if token in {"round", "circle", "circular", "oval"}: + safe_u = max(float(half_u) + tolerance, 1.0e-9) + safe_v = max(float(half_v) + tolerance, 1.0e-9) + return ((u_values / safe_u) ** 2 + (v_values / safe_v) ** 2) <= 1.0 + return (np.abs(u_values) <= float(half_u) + tolerance) & ( + np.abs(v_values) <= float(half_v) + tolerance + ) + + +def _plane_bucket_key(u_value, v_value, *, spacing): + import math + + resolution = max(min(float(value) for value in spacing), 1.0e-6) + return ( + int(math.floor(float(u_value) / resolution + 0.5)), + int(math.floor(float(v_value) / resolution + 0.5)), + ) + + +def crop_face_contact_plane(crop_face_vtk, mask_vtk): + """Fit the transformed distal crop face as a contact plane. + + The returned ``normal`` points from the crop face into the retained femur. + ``outward_normal`` points toward the cropped-away shaft. Both are derived + from the transformed crop-face label, so the shaft support angle follows the + same registration transform as the femur. + """ + import numpy as np + + from ogo.util.vtk_image import vtk_image_to_numpy + + face = vtk_image_to_numpy(crop_face_vtk) != 0 + bone = vtk_image_to_numpy(mask_vtk) != 0 + if not np.any(face): + raise ValueError("Cannot fit a contact plane from an empty crop-face mask.") + if not np.any(bone): + raise ValueError("Cannot orient a crop-face contact plane from an empty femur mask.") + + face_indices = np.argwhere(face) + if face_indices.shape[0] < 3: + raise ValueError("At least three crop-face voxels are required to fit a contact plane.") + face_points = _image_index_points(crop_face_vtk, face_indices) + bone_points = _image_index_points(mask_vtk, np.argwhere(bone)) + + center = face_points.mean(axis=0) + centered = face_points - center + _u_svd, _s_svd, vh = np.linalg.svd(centered, full_matrices=False) + u_axis = _unit_vector(vh[0], "crop-face u axis") + v_axis = _unit_vector(vh[1], "crop-face v axis") + normal = _unit_vector(vh[-1], "crop-face normal") + + into_bone = bone_points.mean(axis=0) - center + if float(np.dot(normal, into_bone)) < 0.0: + normal = -normal + if float(np.dot(np.cross(u_axis, v_axis), normal)) < 0.0: + v_axis = -v_axis + + u_values = centered @ u_axis + v_values = centered @ v_axis + min_spacing = min(float(value) for value in crop_face_vtk.GetSpacing()) + size = ( + max(float(np.ptp(u_values)) + min_spacing, min_spacing), + max(float(np.ptp(v_values)) + min_spacing, min_spacing), + ) + return { + "center": tuple(float(value) for value in center), + "normal": tuple(float(value) for value in normal), + "outward_normal": tuple(float(value) for value in -normal), + "u_axis": tuple(float(value) for value in u_axis), + "v_axis": tuple(float(value) for value in v_axis), + "size": tuple(float(value) for value in size), + "shape": "anatomy", + } + + +def projected_crop_face_surface_vtk(material_vtk, plane, *, intrusion=0.0, output_value=1): + """Project a fitted crop-face plane onto the first active femur surface.""" + import numpy as np + import vtk + + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + + active = vtk_image_to_numpy(material_vtk) != 0 + out = np.zeros(active.shape, dtype=np.uint8) + if not np.any(active): + return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) + + center = np.asarray(plane["center"], dtype=np.float64) + normal = _unit_vector(plane["normal"], "plane normal") + u_axis = _unit_vector(plane["u_axis"], "plane u axis") + v_axis = _unit_vector(plane["v_axis"], "plane v axis") + size = plane.get("size", (0.0, 0.0)) + half_u = max(float(size[0]) / 2.0, 0.5) + half_v = max(float(size[1]) / 2.0, 0.5) + spacing = tuple(float(value) for value in material_vtk.GetSpacing()) + tolerance = max(min(spacing) * 0.75, 1.0e-6) + + indices = np.argwhere(active) + points = _image_index_points(material_vtk, indices) + relative = points - center + distances = relative @ normal + u_values = relative @ u_axis + v_values = relative @ v_axis + inside = _inside_plane_shape( + plane.get("shape", "anatomy"), + u_values, + v_values, + half_u, + half_v, + tolerance=tolerance, + ) + candidates = inside & (distances >= -tolerance) + if not np.any(candidates): + return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) + + best = {} + for voxel, distance, u_value, v_value in zip( + indices[candidates], + distances[candidates], + u_values[candidates], + v_values[candidates], + strict=True, + ): + if float(distance) < -tolerance: + continue + key = _plane_bucket_key(u_value, v_value, spacing=spacing) + current = best.get(key) + if current is None or float(distance) < current[0]: + best[key] = (float(distance), np.asarray(voxel, dtype=np.int64)) + if not best: + return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) + + first_distance = min(value[0] for value in best.values()) + max_distance = first_distance + max(float(intrusion), 0.0) + tolerance + kept = [voxel for distance, voxel in best.values() if distance <= max_distance] + if kept: + kept_indices = np.stack(kept, axis=0) + out[tuple(kept_indices.T)] = np.uint8(output_value) + return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) + + def mirror_polydata_x(polydata): """Return a left/right mirrored copy of polydata around its x-bounds center.""" import vtk @@ -323,6 +1312,15 @@ def pad_vtk_images_to_foreground_margin( pad.Update() out = vtk.vtkImageData() out.DeepCopy(pad.GetOutput()) + dims_out = out.GetDimensions() + spacing_out = out.GetSpacing() + origin = image.GetOrigin() + out.SetOrigin( + float(origin[0]) + float(output_extent[0]) * float(spacing_out[0]), + float(origin[1]) + float(output_extent[2]) * float(spacing_out[1]), + float(origin[2]) + float(output_extent[4]) * float(spacing_out[2]), + ) + out.SetExtent(0, dims_out[0] - 1, 0, dims_out[1] - 1, 0, dims_out[2] - 1) padded.append(out) return padded, { @@ -486,7 +1484,7 @@ def standardize_femur_shaft_length( In the default mode, the cut plane is detected from the lesser-trochanter cross-section profile. A fixed retained length can be requested explicitly - for debugging or legacy comparisons. + for controlled debugging runs. """ cut_mode = str(cut_mode).lower() if cut_mode == "lesser_trochanter": diff --git a/ogo/fea/materials.py b/ogo/fea/materials.py index daaec71..33de624 100644 --- a/ogo/fea/materials.py +++ b/ogo/fea/materials.py @@ -221,8 +221,8 @@ def build_femur_material_table( """Build femur materials with the same region convention as spine. ``include_cortical`` is False for the simple whole-bone femur path and True - when the user supplies a compartment mask. The material IDs stay compatible - either way. + when the user supplies a compartment mask. The material IDs keep the same + region layout either way. """ return build_bone_pmma_material_table( bin_centers, diff --git a/ogo/fea/model.py b/ogo/fea/model.py index 5e184e4..de08786 100644 --- a/ogo/fea/model.py +++ b/ogo/fea/model.py @@ -4,7 +4,7 @@ def log_fe_arguments(**kwargs): - """Print FE model parameters in the same style as the legacy scripts.""" + """Print FE model parameters in Ogo's standard command-line format.""" import ogo.util.Helper as ogo ogo.message("========= FE MODEL ARGUMENTS =========") @@ -98,6 +98,15 @@ def apply_spine_boundary_conditions(model, **kwargs): fe_displacement = kwargs.get("fe_displacement", -1.0) top_node_set_name = kwargs.get("top_node_set_name", "body_top") bottom_node_set_name = kwargs.get("bottom_node_set_name", "body_bottom") + bottom_fixed_senses = tuple(str(axis).lower() for axis in kwargs.get("bottom_fixed_senses", ("x", "y", "z"))) + sense_by_axis = { + "x": vtkbone.vtkboneConstraint.SENSE_X, + "y": vtkbone.vtkboneConstraint.SENSE_Y, + "z": vtkbone.vtkboneConstraint.SENSE_Z, + } + for axis in bottom_fixed_senses: + if axis not in sense_by_axis: + raise ValueError("bottom_fixed_senses must contain only 'x', 'y', and 'z'.") ogo.message("Applying boundary conditions...") model.ApplyBoundaryCondition( @@ -106,12 +115,13 @@ def apply_spine_boundary_conditions(model, **kwargs): fe_displacement, "top_displacement", ) - model.ApplyBoundaryCondition( - bottom_node_set_name, - vtkbone.vtkboneConstraint.SENSE_Z, - 0, - "bottom_fixed_z", - ) + for axis in bottom_fixed_senses: + model.ApplyBoundaryCondition( + bottom_node_set_name, + sense_by_axis[axis], + 0, + f"bottom_fixed_{axis}", + ) return model @@ -142,6 +152,199 @@ def find_nodes_on_coordinate_plane(model, axis, value, *, tolerance=1.0e-5): return node_ids +def interface_node_ids_from_voxel_mask( + model, + selected_vtk, + material_vtk, + *, + name=None, + direction=None, + direction_component_threshold=0.1, + tolerance=1.0e-4, +): + """Return model node IDs on the interface between selected and material voxels.""" + import itertools + + import numpy as np + import vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + selected = vtk_image_to_numpy(selected_vtk) != 0 + material = vtk_image_to_numpy(material_vtk) != 0 + if selected.shape != material.shape: + raise ValueError("selected and material masks must have the same shape.") + + spacing = np.asarray(material_vtk.GetSpacing(), dtype=np.float64) + origin = np.asarray(material_vtk.GetOrigin(), dtype=np.float64) + extent = material_vtk.GetExtent() + offset = np.asarray([extent[0], extent[2], extent[4]], dtype=np.float64) + tolerance = float(tolerance) + if tolerance <= 0.0: + raise ValueError("tolerance must be positive.") + if direction is None: + allowed_faces = None + else: + vector = np.asarray(direction, dtype=np.float64) + if vector.shape != (3,) or not np.all(np.isfinite(vector)) or not np.any(vector): + raise ValueError("direction must be a finite non-zero 3-vector.") + vector = vector / float(np.linalg.norm(vector)) + threshold = max(float(direction_component_threshold), 0.0) + allowed_faces = { + (axis, side) + for axis in range(3) + for side in (-1, 1) + if side * float(vector[axis]) >= threshold + } + + def key(point): + return tuple(int(round(float(value) / tolerance)) for value in point) + + model_point_ids = {} + for point_id in range(model.GetNumberOfPoints()): + model_point_ids.setdefault(key(model.GetPoint(point_id)), int(point_id)) + + node_ids = vtk.vtkIdTypeArray() + if name is not None: + node_ids.SetName(str(name)) + + chosen = set() + shape = np.asarray(material.shape, dtype=np.int64) + for voxel_array in np.argwhere(selected): + voxel = np.asarray(voxel_array, dtype=np.int64) + center = origin + (voxel.astype(np.float64) + offset) * spacing + for axis in range(3): + for side in (-1, 1): + if allowed_faces is not None and (axis, side) not in allowed_faces: + continue + neighbor = voxel.copy() + neighbor[axis] += side + if np.any(neighbor < 0) or np.any(neighbor >= shape): + continue + neighbor_tuple = tuple(int(value) for value in neighbor) + if not bool(material[neighbor_tuple]) or bool(selected[neighbor_tuple]): + continue + + face_center = center.copy() + face_center[axis] += side * spacing[axis] / 2.0 + lateral_axes = [idx for idx in range(3) if idx != axis] + for offsets in itertools.product((-0.5, 0.5), repeat=2): + point = face_center.copy() + for lateral_axis, offset_value in zip(lateral_axes, offsets, strict=True): + point[lateral_axis] += offset_value * spacing[lateral_axis] + point_id = model_point_ids.get(key(point)) + if point_id is not None: + chosen.add(point_id) + + for point_id in sorted(chosen): + node_ids.InsertNextValue(int(point_id)) + return node_ids + + +def directional_face_node_ids_from_voxel_mask( + model, + selected_vtk, + *, + direction, + name=None, + tolerance=1.0e-4, +): + """Return model node IDs on the exposed face of a selected voxel mask.""" + import itertools + + import numpy as np + import vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + selected = vtk_image_to_numpy(selected_vtk) != 0 + spacing = np.asarray(selected_vtk.GetSpacing(), dtype=np.float64) + origin = np.asarray(selected_vtk.GetOrigin(), dtype=np.float64) + extent = selected_vtk.GetExtent() + offset = np.asarray([extent[0], extent[2], extent[4]], dtype=np.float64) + tolerance = float(tolerance) + if tolerance <= 0.0: + raise ValueError("tolerance must be positive.") + + vector = np.asarray(direction, dtype=np.float64) + if vector.shape != (3,) or not np.all(np.isfinite(vector)) or not np.any(vector): + raise ValueError("direction must be a finite non-zero 3-vector.") + vector = vector / float(np.linalg.norm(vector)) + selected_faces = [ + (axis, side) + for axis in range(3) + for side in (-1, 1) + if side * float(vector[axis]) > 0.0 + ] + + def key(point): + return tuple(int(round(float(value) / tolerance)) for value in point) + + model_point_ids = {} + for point_id in range(model.GetNumberOfPoints()): + model_point_ids.setdefault(key(model.GetPoint(point_id)), int(point_id)) + + node_ids = vtk.vtkIdTypeArray() + if name is not None: + node_ids.SetName(str(name)) + + chosen = set() + shape = np.asarray(selected.shape, dtype=np.int64) + for voxel_array in np.argwhere(selected): + voxel = np.asarray(voxel_array, dtype=np.int64) + center = origin + (voxel.astype(np.float64) + offset) * spacing + for axis, side in selected_faces: + neighbor = voxel.copy() + neighbor[axis] += side + outside = bool(np.any(neighbor < 0) or np.any(neighbor >= shape)) + if not outside and bool(selected[tuple(int(value) for value in neighbor)]): + continue + + face_center = center.copy() + face_center[axis] += side * spacing[axis] / 2.0 + lateral_axes = [idx for idx in range(3) if idx != axis] + for offsets in itertools.product((-0.5, 0.5), repeat=2): + point = face_center.copy() + for lateral_axis, offset_value in zip(lateral_axes, offsets, strict=True): + point[lateral_axis] += offset_value * spacing[lateral_axis] + point_id = model_point_ids.get(key(point)) + if point_id is not None: + chosen.add(point_id) + + for point_id in sorted(chosen): + node_ids.InsertNextValue(int(point_id)) + return node_ids + + +def directional_face_node_ids_from_label_image( + model, + labels_vtk, + *, + label_value, + direction, + name=None, + tolerance=1.0e-4, +): + """Return model node IDs on the outer face of one label image value.""" + import vtk + + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + + selected = vtk_image_to_numpy(labels_vtk) == int(label_value) + selected_vtk = numpy_to_vtk_image( + selected.astype("uint8"), + labels_vtk, + vtk_array_type=vtk.VTK_UNSIGNED_CHAR, + ) + return directional_face_node_ids_from_voxel_mask( + model, + selected_vtk, + direction=direction, + name=name, + tolerance=tolerance, + ) + + def write_model(model, output_path): """Write a vtkbone model to an ``.n88model`` file.""" import vtkbone @@ -174,6 +377,10 @@ def create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, filter_bc_node_sets = kwargs.get("filter_bc_node_sets", True) bc_filter_axis = kwargs.get("bc_filter_axis", "z") bc_filter_tolerance = kwargs.get("bc_filter_tolerance", 1.0e-5) + top_boundary_mask_image = kwargs.get("top_boundary_mask_image") + bottom_boundary_mask_image = kwargs.get("bottom_boundary_mask_image") + top_boundary_mask_label = int(kwargs.get("top_boundary_mask_label", 1)) + bottom_boundary_mask_label = int(kwargs.get("bottom_boundary_mask_label", 1)) pmma_yield_compression = kwargs.get("pmma_yield_compression", None) pmma_yield_tension = kwargs.get("pmma_yield_tension", None) material_log_values = spine_material_log_values(kwargs) @@ -194,6 +401,8 @@ def create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, filter_bc_node_sets=filter_bc_node_sets, bc_filter_axis=bc_filter_axis, bc_filter_tolerance=bc_filter_tolerance, + top_boundary_mask_image=top_boundary_mask_image is not None, + bottom_boundary_mask_image=bottom_boundary_mask_image is not None, pmma_yield_compression=pmma_yield_compression, pmma_yield_tension=pmma_yield_tension, **material_log_values, @@ -234,13 +443,42 @@ def create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, model.ComputeBounds() ogo.message("Identifying boundary nodes...") - model = find_and_add_visible_nodes( - model, temp_bc_mesh, top_direction, top_node_set_id, top_node_set_name - ) - model = find_and_add_visible_nodes( - model, temp_bc_mesh, bottom_direction, bottom_node_set_id, bottom_node_set_name - ) - if filter_bc_node_sets: + if top_boundary_mask_image is not None and bottom_boundary_mask_image is not None: + top_nodes = directional_face_node_ids_from_label_image( + model, + top_boundary_mask_image, + label_value=top_boundary_mask_label, + direction=top_direction, + name=top_node_set_name, + tolerance=bc_filter_tolerance, + ) + bottom_nodes = directional_face_node_ids_from_label_image( + model, + bottom_boundary_mask_image, + label_value=bottom_boundary_mask_label, + direction=bottom_direction, + name=bottom_node_set_name, + tolerance=bc_filter_tolerance, + ) + model.AddNodeSet(top_nodes) + model.AddNodeSet(bottom_nodes) + ogo.message( + f"Found {top_nodes.GetNumberOfTuples()} outer-face nodes for {top_node_set_name}." + ) + ogo.message( + f"Found {bottom_nodes.GetNumberOfTuples()} outer-face nodes for {bottom_node_set_name}." + ) + else: + model = find_and_add_visible_nodes( + model, temp_bc_mesh, top_direction, top_node_set_id, top_node_set_name + ) + model = find_and_add_visible_nodes( + model, temp_bc_mesh, bottom_direction, bottom_node_set_id, bottom_node_set_name + ) + + if filter_bc_node_sets and ( + top_boundary_mask_image is None or bottom_boundary_mask_image is None + ): model = filter_node_set_to_dominant_coordinate_plane( model, top_node_set_name, diff --git a/ogo/fea/sideways_fall.py b/ogo/fea/sideways_fall.py index c590890..2ef7717 100644 --- a/ogo/fea/sideways_fall.py +++ b/ogo/fea/sideways_fall.py @@ -21,7 +21,6 @@ ## # Import the required modules import ogo.util.Helper as ogo -import math import os import sys import argparse @@ -32,42 +31,64 @@ import vtkbone from ogo.fea.boundary import ( - generate_fixture_cap_vtk, - label_foreground_in_bounds_vtk, + generate_projected_material_disk_vtk, should_smooth_resampled_mask, smooth_binary_mask_vtk, ) from ogo.fea.femur import ( DEFAULT_CORTICAL_LABEL, + DEFAULT_FEMUR_BBOX_CROP_FROM, + DEFAULT_FEMUR_BBOX_RATIO, DEFAULT_FEMUR_FE_DISPLACEMENT, DEFAULT_FEMUR_CUT_MODE, DEFAULT_FEMUR_INPUT_MARGIN_MM, DEFAULT_FEMUR_ISO_RESOLUTION_MM, DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + DEFAULT_FEMUR_REFERENCE_MAX_SCALE, + DEFAULT_FEMUR_REFERENCE_MIN_SCALE, DEFAULT_FEMUR_SHAFT_LENGTH_MM, DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, DEFAULT_PMMA_INTRUSION_MM, DEFAULT_PMMA_THICKNESS_MM, DEFAULT_TRABECULAR_LABEL, + DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, + DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, DISTAL_FEMUR_NODE_SET, - FEMORAL_HEAD_FIXTURE_LONG_AXIS_EXTENSION_MM, - FEMORAL_HEAD_FIXTURE_WIDTH_EXTENSION_MM, + FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, FEMORAL_HEAD_NODE_SET, + GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, GREATER_TROCHANTER_NODE_SET, SIDEWAYS_FALL_NODE_SETS, + SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, + bbox_relative_fixture_direction, + bbox_relative_fixture_plane, + bbox_relative_oriented_contact_plane, cortical_compartment_mask, - expand_xz_footprint, + crop_face_contact_plane, + crop_vtk_images_to_bbox_ratio, femur_z_coverage, flat_crop_vtk_image_below_z, + foreground_voxel_center_bounds, mirror_polydata_x, pad_vtk_images_to_foreground_margin, - side_rotation, + polydata_from_points, + reference_grid_from_vtk_mask, + projected_crop_face_surface_vtk, + resample_vtk_image_like_workflow, + scale_reference_point_cloud_to_sample, sideways_fall_output_name, standardize_femur_shaft_length, - swap_xz_footprint, + surface_points_from_vtk_mask, + transform_resample_vtk_image_to_reference_grid, ) from ogo.fea.materials import build_femur_material_table -from ogo.fea.model import append_postprocessing_sets, find_nodes_on_coordinate_plane, write_model +from ogo.fea.model import ( + append_postprocessing_sets, + directional_face_node_ids_from_voxel_mask, + find_nodes_on_coordinate_plane, + interface_node_ids_from_voxel_mask, + write_model, +) from ogo.util.echo_arguments import echo_arguments @@ -107,6 +128,8 @@ def sidewaysFallFe(args): pmma_yield_tension = args.pmma_yield_tension femur_shaft_length = args.femur_shaft_length femur_cut_mode = args.femur_cut_mode + femur_bbox_ratio = args.femur_bbox_ratio + femur_bbox_crop_from = args.femur_bbox_crop_from femur_lesser_trochanter_distal_offset = args.femur_lesser_trochanter_distal_offset femur_lesser_trochanter_distal_offset_percent = args.femur_lesser_trochanter_distal_offset_percent femur_input_margin = args.femur_input_margin @@ -127,7 +150,6 @@ def sidewaysFallFe(args): try: N88_fileName = sideways_fall_output_name(output_file, femur_side) - rot_z = side_rotation(femur_side) except ValueError: ogo.message("Femur side not recognized. Terminating...") sys.exit() @@ -157,6 +179,9 @@ def sidewaysFallFe(args): ogo.message("PMMA Material ID: %d" % pmma_mat_id) ogo.message("Applied Displacement [mm]: %8.4f" % fe_displacement) ogo.message("Femur Cut Mode: %s" % femur_cut_mode) + if femur_cut_mode == "bbox_ratio": + ogo.message("Femur BBox Ratio [reference, constrained, free]: %s" % str(femur_bbox_ratio)) + ogo.message("Femur BBox Crop From [reference, constrained, free]: %s" % str(femur_bbox_crop_from)) if compartment_mask is not None: ogo.message("Compartment Mask: %s" % compartment_mask) ogo.message("Cortical Label: %d" % cortical_label) @@ -191,36 +216,101 @@ def sidewaysFallFe(args): ogo.message("Reading trabecular/cortical compartment mask...") compartmentData = ogo.readNii(compartment_mask) - images_to_pad = [imageData, maskThres] + ([compartmentData] if compartmentData is not None else []) + distal_crop_face = None + bbox_crop_meta = None + if femur_cut_mode == "bbox_ratio": + ogo.message("Applying bbox-ratio femur crop in input image coordinates...") + images_to_crop = [imageData, maskThres] + ([compartmentData] if compartmentData is not None else []) + cropped_images, distal_crop_face, bbox_crop_meta = crop_vtk_images_to_bbox_ratio( + images_to_crop, + maskThres, + bbox_ratio=femur_bbox_ratio, + bbox_crop_from=femur_bbox_crop_from, + labels={1}, + ) + imageData = cropped_images[0] + maskThres = cropped_images[1] + if compartmentData is not None: + compartmentData = cropped_images[2] + ogo.message( + "BBox-ratio crop slices xyz=%s; output shape xyz=%s; crop face voxels=%d." + % ( + bbox_crop_meta["crop_slices_xyz"], + bbox_crop_meta["output_shape_xyz"], + bbox_crop_meta["crop_face_voxels"], + ) + ) + + ogo.message("Resampling cropped femur inputs to isotropic spacing before ICP...") + imageData = resample_vtk_image_like_workflow( + imageData, + iso_resolution, + interpolation="bspline", + ) + maskThres = resample_vtk_image_like_workflow( + maskThres, + iso_resolution, + interpolation="nearest", + ) + if distal_crop_face is not None: + distal_crop_face = resample_vtk_image_like_workflow( + distal_crop_face, + iso_resolution, + interpolation="nearest", + ) + if compartmentData is not None: + compartmentData = resample_vtk_image_like_workflow( + compartmentData, + iso_resolution, + interpolation="nearest", + ) + registration_mask = maskThres + + images_to_pad = [imageData, maskThres] + if distal_crop_face is not None: + images_to_pad.append(distal_crop_face) + if compartmentData is not None: + images_to_pad.append(compartmentData) + pad_constants = [0] * len(images_to_pad) padded_images, padding = pad_vtk_images_to_foreground_margin( images_to_pad, maskThres, margin_mm=femur_input_margin, - constants=[0] * len(images_to_pad), + constants=pad_constants, ) imageData = padded_images[0] maskThres = padded_images[1] + next_padded_index = 2 + if distal_crop_face is not None: + distal_crop_face = padded_images[next_padded_index] + next_padded_index += 1 if compartmentData is not None: - compartmentData = padded_images[2] + compartmentData = padded_images[next_padded_index] if any(padding["lower"]) or any(padding["upper"]): ogo.message( - "Padded input image extent by lower=%s upper=%s voxels for FE transform safety." + "Padded isotropic input image extent by lower=%s upper=%s voxels for FE transform safety." % (padding["lower"], padding["upper"]) ) else: - ogo.message("Input image already has sufficient foreground safety margin.") + ogo.message("Isotropic input image already has sufficient foreground safety margin.") ## - # Pre-rotate the image and mask for better alignment - image_rot, mask_rot = ogo.preRotateImage(imageData, maskThres, rot_z) - compartment_rot = None - if compartmentData is not None: - _, compartment_rot = ogo.preRotateImage(imageData, compartmentData, rot_z) + # Use the cropped, isotropic input geometry directly for ICP. + image_rot = imageData + mask_rot = maskThres + distal_crop_face_rot = distal_crop_face + compartment_rot = compartmentData ## # Align the input femur with the reference model ogo.message("Aligning input with reference model...") - mask_surface = ogo.marchingCubes(mask_rot) + sample_surface_points = surface_points_from_vtk_mask( + registration_mask, + max_points=8000, + sample_mode="stride", + sample_offset=0, + ) + mask_surface = polydata_from_points(sample_surface_points) if femur_side == 1: ref_poly = ogo.readPolyData(left_femur_reference) elif femur_side == 2: @@ -236,14 +326,69 @@ def sidewaysFallFe(args): print("Error: Femur Side not defined. Terminating...") sys.exit() + ref_poly, reference_scale = scale_reference_point_cloud_to_sample( + ref_poly, + sample_surface_points, + max_points=8000, + reference_sample_mode="linspace", + min_scale=DEFAULT_FEMUR_REFERENCE_MIN_SCALE, + max_scale=DEFAULT_FEMUR_REFERENCE_MAX_SCALE, + ) + ogo.message("Femur reference axis lengths: %s" % str(reference_scale["reference_axis_lengths"])) + ogo.message("Sample femur axis lengths: %s" % str(reference_scale["sample_axis_lengths"])) + ogo.message("Femur reference scale factors: %s" % str(reference_scale["scale_factors"])) + icp = ogo.iterativeClosestPoint(ref_poly, mask_surface) # ogo.message("Transform:\n %s" % str(icp)) ogo.message("Applying the transformation and isotropic resampling to the image and mask...") - image_trans = ogo.transformResample(image_rot, icp, iso_resolution) - mask_trans = ogo.labelTransformResample(mask_rot, icp, iso_resolution) + output_origin, output_size = reference_grid_from_vtk_mask( + mask_rot, + icp, + margin_voxels=int(round(femur_input_margin / max(iso_resolution, 1.0e-6))), + ) + output_spacing = (float(iso_resolution), float(iso_resolution), float(iso_resolution)) + ogo.message( + "Reference-frame output grid: origin=%s size=%s spacing=%s" + % (output_origin, output_size, output_spacing) + ) + image_trans = transform_resample_vtk_image_to_reference_grid( + image_rot, + icp, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="bspline", + ) + mask_trans = transform_resample_vtk_image_to_reference_grid( + mask_rot, + icp, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", + ) + distal_crop_face_trans = ( + transform_resample_vtk_image_to_reference_grid( + distal_crop_face_rot, + icp, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", + ) + if distal_crop_face_rot is not None + else None + ) compartment_trans = ( - ogo.labelTransformResample(compartment_rot, icp, iso_resolution) + transform_resample_vtk_image_to_reference_grid( + compartment_rot, + icp, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", + ) if compartment_rot is not None else None ) @@ -263,39 +408,61 @@ def sidewaysFallFe(args): f"{input_spacing} is <= {mask_smoothing_spacing_threshold} mm in all dimensions..." ) - ogo.message("Applying flat distal femur crop in reference coordinates...") - try: - image_trans, mask_trans, shaft_crop = standardize_femur_shaft_length( - image_trans, - mask_trans, - reference_bounds=ref_poly.GetBounds(), - retained_length_mm=femur_shaft_length, - cut_mode=femur_cut_mode, - lesser_trochanter_distal_offset_mm=femur_lesser_trochanter_distal_offset, - lesser_trochanter_distal_offset_percent=femur_lesser_trochanter_distal_offset_percent, - ) - except ValueError as exc: - ogo.message(str(exc)) - sys.exit(1) - retained_length_mm = shaft_crop["retained_length_mm"] - ogo.message( - "Distal cut z=%8.4f; input mask z coverage [%8.4f, %8.4f]; retained length=%8.4f" - % ( - shaft_crop["cut_z"], - shaft_crop["mask_z_min"], - shaft_crop["mask_z_max"], - retained_length_mm, + if femur_cut_mode == "bbox_ratio": + ogo.message("Using transformed bbox-ratio crop face for distal shaft standardization.") + try: + mask_z_min, mask_z_max = femur_z_coverage(mask_trans) + except ValueError as exc: + ogo.message(str(exc)) + sys.exit(1) + retained_length_mm = mask_z_max - mask_z_min + shaft_crop = { + "cut_mode": "bbox_ratio", + "cut_plane": "transformed input crop face", + "bbox_ratio": bbox_crop_meta, + "mask_z_min": mask_z_min, + "mask_z_max": mask_z_max, + "retained_length_mm": retained_length_mm, + "warnings": [], + } + ogo.message( + "Transformed bbox-ratio mask z coverage [%8.4f, %8.4f]; retained z-span=%8.4f" + % (mask_z_min, mask_z_max, retained_length_mm) ) - ) - for warning in shaft_crop.get("warnings", []): - ogo.message("WARNING: Femur crop QC: %s" % warning) - if femur_cut_mode == "lesser_trochanter": + else: + ogo.message("Applying flat distal femur crop in reference coordinates...") + try: + image_trans, mask_trans, shaft_crop = standardize_femur_shaft_length( + image_trans, + mask_trans, + reference_bounds=ref_poly.GetBounds(), + retained_length_mm=femur_shaft_length, + cut_mode=femur_cut_mode, + lesser_trochanter_distal_offset_mm=femur_lesser_trochanter_distal_offset, + lesser_trochanter_distal_offset_percent=femur_lesser_trochanter_distal_offset_percent, + ) + except ValueError as exc: + ogo.message(str(exc)) + sys.exit(1) + retained_length_mm = shaft_crop["retained_length_mm"] ogo.message( - "Detected greater trochanter z=%8.4f; lesser trochanter z=%8.4f" - % (shaft_crop["greater_trochanter_z"], shaft_crop["lesser_trochanter_z"]) + "Distal cut z=%8.4f; input mask z coverage [%8.4f, %8.4f]; retained length=%8.4f" + % ( + shaft_crop["cut_z"], + shaft_crop["mask_z_min"], + shaft_crop["mask_z_max"], + retained_length_mm, + ) ) - if compartment_trans is not None: - compartment_trans = flat_crop_vtk_image_below_z(compartment_trans, shaft_crop["cut_z"]) + for warning in shaft_crop.get("warnings", []): + ogo.message("WARNING: Femur crop QC: %s" % warning) + if femur_cut_mode == "lesser_trochanter": + ogo.message( + "Detected greater trochanter z=%8.4f; lesser trochanter z=%8.4f" + % (shaft_crop["greater_trochanter_z"], shaft_crop["lesser_trochanter_z"]) + ) + if compartment_trans is not None: + compartment_trans = flat_crop_vtk_image_below_z(compartment_trans, shaft_crop["cut_z"]) # ogo.message("Writing out temp images...") # ogo.writeNii(image_trans, "temp_image.nii", image_pathname) @@ -353,24 +520,37 @@ def sidewaysFallFe(args): conn = ogo.imageConnectivity(bone_image) - change = ogo.changeInfo(conn) - model_z_min, model_z_max = femur_z_coverage(change) - model_cut_z = model_z_max - retained_length_mm - if model_z_min > model_cut_z: - if model_z_min - model_cut_z <= 2.0: - model_cut_z = model_z_min - else: - ogo.message( - "Femur scan does not include enough distal shaft after model-grid alignment " - "(required retained length %.1f mm, available approximately %.1f mm)." - % (retained_length_mm, max(0.0, model_z_max - model_z_min)) - ) - sys.exit(1) - change = flat_crop_vtk_image_below_z(change, model_cut_z) - ogo.message( - "Applied model-grid distal shaft cut z=%8.4f; model mask z coverage [%8.4f, %8.4f]" - % (model_cut_z, model_z_min, model_z_max) + change = ogo.prepareFiniteElementImage(conn) + femur_mask_on_model_grid = ogo.prepareFiniteElementImage(cast_mask) + distal_crop_face_change = ( + ogo.prepareFiniteElementImage(distal_crop_face_trans) if distal_crop_face_trans is not None else None ) + if distal_crop_face_change is not None: + distal_crop_face_change = ogo.applyMask(distal_crop_face_change, ogo.cast2unsignchar(change)) + if femur_cut_mode == "bbox_ratio": + ogo.message("Skipping model-grid distal shaft cut; bbox-ratio crop face is already transformed.") + else: + model_z_min, model_z_max = femur_z_coverage(change) + model_cut_z = model_z_max - retained_length_mm + if model_z_min > model_cut_z: + if model_z_min - model_cut_z <= 2.0: + model_cut_z = model_z_min + else: + ogo.message( + "Femur scan does not include enough distal shaft after model-grid alignment " + "(required retained length %.1f mm, available approximately %.1f mm)." + % (retained_length_mm, max(0.0, model_z_max - model_z_min)) + ) + sys.exit(1) + change = flat_crop_vtk_image_below_z(change, model_cut_z) + femur_mask_on_model_grid = flat_crop_vtk_image_below_z( + femur_mask_on_model_grid, + model_cut_z, + ) + ogo.message( + "Applied model-grid distal shaft cut z=%8.4f; model mask z coverage [%8.4f, %8.4f]" + % (model_cut_z, model_z_min, model_z_max) + ) # Convert image data to hexahedral elements ogo.message("Meshing image data to elements...") @@ -402,117 +582,94 @@ def sidewaysFallFe(args): ogo.message("Constructing the Finite Element Model...") model = ogo.applyTestBase(mesh, material_table) model.ComputeBounds() - model_bounds = model.GetBounds() - ogo.message("Model Bounds: %s" % str(model_bounds)) + mesh_model_bounds = model.GetBounds() + model_bounds = foreground_voxel_center_bounds(femur_mask_on_model_grid) + ogo.message("Model Mesh Bounds: %s" % str(mesh_model_bounds)) + ogo.message("Model Foreground Voxel-Center Bounds: %s" % str(model_bounds)) - # Define support vectors for later application - z_center = (model_bounds[4] + model_bounds[5] / 2) + # Define support vectors for the two PMMA contact fixtures. top_support_vector = (0, 1, 0) bottom_support_vector = (0, -1, 0) - side_support_vector = (0, 0, -1) - - # The shaft is cut on a flat z plane, so use that cut-plane normal for the distal BC. - side_support_vector = (0, 0, -1) - - ogo.message("Determining Femoral Head ROI...") - femoral_head_bounds = ( - float(model_bounds[0]), - float(model_bounds[1]), - float(model_bounds[2]), - float(model_bounds[2] + (model_bounds[3]-model_bounds[2]) / 5), - float(model_bounds[4]), - float(model_bounds[5]) - ) - ogo.message("Extracted Femoral Head Bounds: %s" % str(femoral_head_bounds)) - femoral_head_model = ogo.extractBox(femoral_head_bounds, model) - femoral_head_model_bounds = femoral_head_model.GetBounds() - ogo.message("Extracted Femoral Head Model Bounds: %s" % str(femoral_head_model_bounds)) - femoral_head_model_bounds = ( - femoral_head_model_bounds[0], - femoral_head_model_bounds[1] - 1, - femoral_head_model_bounds[2], - femoral_head_model_bounds[3] - 1, - femoral_head_model_bounds[4], - femoral_head_model_bounds[5] - 1 - ) - - femoral_head_element_dims = ( - femoral_head_model_bounds[1] - femoral_head_model_bounds[0], - femoral_head_model_bounds[3] - femoral_head_model_bounds[2], - femoral_head_model_bounds[5] - femoral_head_model_bounds[4] - ) ## - # Extract Greater Trochater ROI - ogo.message("Determining Greater Trochater ROI...") - greater_trochanter_bounds = ( - float(model_bounds[0]), - float(model_bounds[1]), - float(model_bounds[3]-(model_bounds[3]-model_bounds[2])/20), - float(model_bounds[3]), - float(model_bounds[4]), - float(model_bounds[5]) - ) - ogo.message("Extracted Greater Trochanter Bounds: %s" % str(greater_trochanter_bounds)) - greater_trochanter_model = ogo.extractBox(greater_trochanter_bounds, model) - greater_trochanter_model_bounds = greater_trochanter_model.GetBounds() - ogo.message("Extracted Greater Trochanter Model Bounds: %s" - % str(greater_trochanter_model_bounds)) - - greater_trochanter_model_bounds = ( - greater_trochanter_model_bounds[0], - greater_trochanter_model_bounds[1] - 1, - greater_trochanter_model_bounds[2], - greater_trochanter_model_bounds[3] - 1, - greater_trochanter_model_bounds[4], - greater_trochanter_model_bounds[5] - 1 + # Create PMMA disks from bbox-relative contact planes. The disk normal + # points from the contact plane toward the anatomy; the generated PMMA + # image clears any femur-mask voxels so PMMA never occupies the segmentation. + femoral_head_plane = bbox_relative_fixture_plane( + model_bounds, + center_fraction=FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, + size_fraction=SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, + projection_axis="y", + shape="rectangle", ) - - greater_trochanter_element_dims = ( - greater_trochanter_model_bounds[1] - greater_trochanter_model_bounds[0], - greater_trochanter_model_bounds[3] - greater_trochanter_model_bounds[2], greater_trochanter_model_bounds[5] - greater_trochanter_model_bounds[4] + femoral_head_cap_direction = bbox_relative_fixture_direction( + FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, + projection_axis="y", ) - - # Thickness to be added to PMMA cap. - thickness = max(1, int(math.ceil(pmma_thick / change.GetSpacing()[1]))) - intrusion = max(0, int(math.ceil(pmma_intrusion / change.GetSpacing()[1]))) - - ## - # Creates the femoral head pmma cap - ogo.message("Creating Femoral Head PMMA Cap from contact ROI...") - femoral_head_fixture_bounds = expand_xz_footprint( - swap_xz_footprint(femoral_head_model_bounds), - x_extension_mm=FEMORAL_HEAD_FIXTURE_WIDTH_EXTENSION_MM, - z_extension_mm=FEMORAL_HEAD_FIXTURE_LONG_AXIS_EXTENSION_MM, + ogo.message( + "Femoral Head PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" + % ( + femoral_head_plane["center"], + femoral_head_plane["normal"], + femoral_head_plane["u_axis"], + femoral_head_plane["v_axis"], + femoral_head_plane["size"], + ) ) - ogo.message("Femoral Head PMMA Fixture Bounds: %s" % str(femoral_head_fixture_bounds)) - femoral_head_contact = label_foreground_in_bounds_vtk(change, femoral_head_fixture_bounds, label_value=1) - femoralHeadPMMA = generate_fixture_cap_vtk( - femoral_head_contact, - label_value=1, - axis="y", - direction="down", - thickness=thickness, - shape="box", - intrusion=intrusion, - crop_to_contact=True, - clear_contact_mask=True, + ogo.message("Femoral Head PMMA cap direction: %s" % femoral_head_cap_direction) + femoralHeadPMMA = generate_projected_material_disk_vtk( + change, + surface_vtk_image=femur_mask_on_model_grid, + exclusion_vtk_image=femur_mask_on_model_grid, + center=femoral_head_plane["center"], + normal=femoral_head_plane["normal"], + u_axis=femoral_head_plane["u_axis"], + v_axis=femoral_head_plane["v_axis"], + size=femoral_head_plane["size"], + shape=femoral_head_plane["shape"], + thickness=pmma_thick, + intrusion=pmma_intrusion, + anatomy_constrained=True, output_value=pmma_mat_id, ) ## # Creates the greater trochanter pmma cap - ogo.message("Creating Greater Trochanter PMMA Cap from contact ROI...") - greater_trochanter_contact = label_foreground_in_bounds_vtk(change, greater_trochanter_model_bounds, label_value=1) - greaterTrochanterPMMA = generate_fixture_cap_vtk( - greater_trochanter_contact, - label_value=1, - axis="y", - direction="up", - thickness=thickness, - shape="round", - intrusion=intrusion, - clear_contact_mask=True, + greater_trochanter_plane = bbox_relative_fixture_plane( + model_bounds, + center_fraction=GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, + size_fraction=SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, + projection_axis="y", + shape="rectangle", + ) + greater_trochanter_cap_direction = bbox_relative_fixture_direction( + GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, + projection_axis="y", + ) + ogo.message( + "Greater Trochanter PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" + % ( + greater_trochanter_plane["center"], + greater_trochanter_plane["normal"], + greater_trochanter_plane["u_axis"], + greater_trochanter_plane["v_axis"], + greater_trochanter_plane["size"], + ) + ) + ogo.message("Greater Trochanter PMMA cap direction: %s" % greater_trochanter_cap_direction) + greaterTrochanterPMMA = generate_projected_material_disk_vtk( + change, + surface_vtk_image=femur_mask_on_model_grid, + exclusion_vtk_image=femur_mask_on_model_grid, + center=greater_trochanter_plane["center"], + normal=greater_trochanter_plane["normal"], + u_axis=greater_trochanter_plane["u_axis"], + v_axis=greater_trochanter_plane["v_axis"], + size=greater_trochanter_plane["size"], + shape=greater_trochanter_plane["shape"], + thickness=pmma_thick, + intrusion=pmma_intrusion, + anatomy_constrained=True, output_value=pmma_mat_id, ) @@ -538,78 +695,128 @@ def sidewaysFallFe(args): model2.ComputeBounds() model2_bounds = model2.GetBounds() ogo.message("Model 2 Bounds: %s" % str(model2_bounds)) - z_center2 = (model2_bounds[4] + model2_bounds[5] / 2) ## # Determine Femoral Head PMMA Cap support nodes. ogo.message("Determining Femoral Head PMMA Cap nodes...") - fh_pmma_bounds = ( - model2_bounds[0], - model2_bounds[1], - model2_bounds[2], - model2_bounds[2] + 1, - model2_bounds[4], - model2_bounds[5] - ) - ogo.message("Femoral Head PMMA Cap Bounds: %s" % str(fh_pmma_bounds)) - fh_pmma_model = ogo.extractBox(fh_pmma_bounds, model2) - fh_pmma_model_bounds = fh_pmma_model.GetBounds() - ogo.message("Femoral Head PMMA Cap Model Bounds: %s" % str(fh_pmma_model_bounds)) - - # Get the visible nodes of the femoral head pmma cap - fh_pmma_visible_node_IDS = vtk.vtkIdTypeArray() - vtkbone.vtkboneNodeSetsByGeometry.FindNodesOnVisibleSurface( - fh_pmma_visible_node_IDS, - fh_pmma_model, - bottom_support_vector, - -1 + if femoral_head_cap_direction == "up": + fh_pmma_bounds = ( + model2_bounds[0], + model2_bounds[1], + model2_bounds[3] - 1, + model2_bounds[3], + model2_bounds[4], + model2_bounds[5], + ) + femoral_head_support_vector = top_support_vector + else: + fh_pmma_bounds = ( + model2_bounds[0], + model2_bounds[1], + model2_bounds[2], + model2_bounds[2] + 1, + model2_bounds[4], + model2_bounds[5], + ) + femoral_head_support_vector = bottom_support_vector + fh_pmma_visible_node_IDS = directional_face_node_ids_from_voxel_mask( + model2, + femoralHeadPMMA, + direction=femoral_head_support_vector, + name=FEMORAL_HEAD_NODE_SET, ) - ogo.message("-- found %d visible exterior nodes on Femoral Head PMMA Cap." + ogo.message("-- found %d outer-face nodes on Femoral Head PMMA Cap." % fh_pmma_visible_node_IDS.GetNumberOfTuples()) - fh_pmma_visible_node_IDS.SetName(FEMORAL_HEAD_NODE_SET) model2.AddNodeSet(fh_pmma_visible_node_IDS) ## # Determine Greater Trochanter PMMA Cap support nodes ogo.message("Determining Greater Trochanter PMMA Cap support nodes...") - gt_pmma_bounds = ( - model2_bounds[0], - model2_bounds[1], - model2_bounds[3] - 1, - model2_bounds[3], - model2_bounds[4], - model2_bounds[5] - ) - ogo.message("Greater Trochanter PMMA Cap Bounds: %s" % str(gt_pmma_bounds)) - gt_pmma_model = ogo.extractBox(gt_pmma_bounds, model2) - gt_pmma_model_bounds = gt_pmma_model.GetBounds() - ogo.message("Greater Trochanter PMMA Cap Model Bounds: %s" % str(gt_pmma_model_bounds)) - - # Get the visible nodes on the Greater Trochanter PMMA Cap - gt_pmma_visible_node_IDS = vtk.vtkIdTypeArray() - vtkbone.vtkboneNodeSetsByGeometry.FindNodesOnVisibleSurface( - gt_pmma_visible_node_IDS, - gt_pmma_model, - top_support_vector, - -1 + if greater_trochanter_cap_direction == "up": + gt_pmma_bounds = ( + model2_bounds[0], + model2_bounds[1], + model2_bounds[3] - 1, + model2_bounds[3], + model2_bounds[4], + model2_bounds[5], + ) + greater_trochanter_support_vector = top_support_vector + else: + gt_pmma_bounds = ( + model2_bounds[0], + model2_bounds[1], + model2_bounds[2], + model2_bounds[2] + 1, + model2_bounds[4], + model2_bounds[5], + ) + greater_trochanter_support_vector = bottom_support_vector + gt_pmma_visible_node_IDS = directional_face_node_ids_from_voxel_mask( + model2, + greaterTrochanterPMMA, + direction=greater_trochanter_support_vector, + name=GREATER_TROCHANTER_NODE_SET, ) - ogo.message("-- found %d visible exterior nodes on Greater Trochanter PMMA Cap." + ogo.message("-- found %d outer-face nodes on Greater Trochanter PMMA Cap." % gt_pmma_visible_node_IDS.GetNumberOfTuples()) - gt_pmma_visible_node_IDS.SetName(GREATER_TROCHANTER_NODE_SET) model2.AddNodeSet(gt_pmma_visible_node_IDS) ## - # Distal Femur (df) support nodes. The shaft is cut on a flat z plane, so - # the fixed support is the cut face itself rather than a thick distal band. + # Distal Femur (df) support nodes. In bbox-ratio mode the distal shaft cut + # was made before alignment, so its transformed crop-face label carries the + # correct oblique support angle in model coordinates. ogo.message("Determining distal femur nodes...") - distal_cut_z = model2_bounds[4] - ogo.message("Distal Femur Cut Plane z: %8.4f" % distal_cut_z) - df_visible_node_IDS = find_nodes_on_coordinate_plane(model2, "z", distal_cut_z) + if femur_cut_mode == "bbox_ratio": + if distal_crop_face_change is None: + ogo.message("BBox-ratio cut mode requires a transformed distal crop-face mask.") + sys.exit(1) + try: + distal_crop_face_plane = crop_face_contact_plane(distal_crop_face_change, change) + distal_plane = bbox_relative_oriented_contact_plane( + model_bounds, + center_fraction=DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, + size_fraction=DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, + normal=distal_crop_face_plane["normal"], + shape="anatomy", + ) + except ValueError as exc: + ogo.message(str(exc)) + sys.exit(1) + ogo.message( + "Distal Femur bbox-relative oblique support plane: center=%s normal=%s outward=%s size=%s" + % ( + distal_plane["center"], + distal_plane["normal"], + distal_plane["outward_normal"], + distal_plane["size"], + ) + ) + distal_surface = projected_crop_face_surface_vtk( + change, + distal_plane, + intrusion=pmma_intrusion, + output_value=1, + ) + df_visible_node_IDS = interface_node_ids_from_voxel_mask( + model2, + distal_surface, + change, + name=DISTAL_FEMUR_NODE_SET, + direction=distal_plane["normal"], + ) + if df_visible_node_IDS.GetNumberOfTuples() == 0: + ogo.message("No distal femur nodes found on the bbox-relative oblique shaft support surface.") + sys.exit(1) + else: + distal_cut_z = model2_bounds[4] + ogo.message("Distal Femur Cut Plane z: %8.4f" % distal_cut_z) + df_visible_node_IDS = find_nodes_on_coordinate_plane(model2, "z", distal_cut_z) - ogo.message("-- found %d visible exterior nodes on Distal Femur." + ogo.message("-- found %d distal shaft interface nodes." % df_visible_node_IDS.GetNumberOfTuples()) df_visible_node_IDS.SetName(DISTAL_FEMUR_NODE_SET) @@ -621,7 +828,7 @@ def sidewaysFallFe(args): model2.ApplyBoundaryCondition( FEMORAL_HEAD_NODE_SET, vtkbone.vtkboneConstraint.SENSE_Y, - -fe_displacement, + fe_displacement, "top_displacement") ogo.message("Constraining Greater Trochanter PMMA cap in loading direction...") @@ -733,7 +940,7 @@ def main(): parser.add_argument("--pmma_v", type=float, default=0.3, help="Sets the Poisson's ratio for the PMMA material(s) in the FE model. (default: %(default)s)") parser.add_argument("--pmma_thick", type=float, default=DEFAULT_PMMA_THICKNESS_MM, - help="Sets the minimum thickness for PMMA caps in the FE model. Default value (6 [mm]) based on observed measurement of Keaveny BCT FE modeling of the femur. (default: %(default)s [mm])") + help="Sets the fixed thickness for PMMA caps in the FE model. (default: %(default)s [mm])") parser.add_argument("--pmma_intrusion", type=float, default=DEFAULT_PMMA_INTRUSION_MM, help="Sets how far anatomy can occupy the fixed PMMA fixture thickness before bone overlap is preserved during material combination. (default: %(default)s [mm])") parser.add_argument("--pmma_mat_id", type=int, default=5000, @@ -742,15 +949,19 @@ def main(): help="Sets the applied displacement endpoint for the sideways-fall model. The default reports the force at 4%% displacement. (default: %(default)s)") parser.add_argument("--femur_shaft_length", type=float, default=DEFAULT_FEMUR_SHAFT_LENGTH_MM, help="Retained proximal femur length [mm] for --femur_cut_mode fixed_length. (default: %(default)s [mm])") - parser.add_argument("--femur_cut_mode", choices=["lesser_trochanter", "fixed_length"], + parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "lesser_trochanter", "fixed_length"], default=DEFAULT_FEMUR_CUT_MODE, - help="Set the distal femur crop. Default detects the lesser trochanter and fails if the field of view is incomplete. (default: %(default)s)") + help="Set the distal femur crop. bbox_ratio crops in input image coordinates and uses the transformed crop face for distal support. (default: %(default)s)") + parser.add_argument("--femur_bbox_ratio", nargs=3, default=DEFAULT_FEMUR_BBOX_RATIO, + help="BBox-ratio crop in recipe order: reference constrained free. Use 'none' for a free axis. (default: %(default)s)") + parser.add_argument("--femur_bbox_crop_from", nargs=3, default=DEFAULT_FEMUR_BBOX_CROP_FROM, + help="BBox crop side in recipe order: reference constrained free. Values are min, max, center/null. (default: %(default)s)") parser.add_argument("--femur_lesser_trochanter_distal_offset", type=float, default=DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, help="Cut this many mm distal to the detected lesser trochanter in lesser_trochanter mode. (default: %(default)s [mm])") parser.add_argument("--femur_lesser_trochanter_distal_offset_percent", type=float, default=None, help="Optional percentage of the detected greater-to-lesser trochanter z distance to cut distal to the lesser trochanter. Overrides --femur_lesser_trochanter_distal_offset when set. (default: %(default)s)") parser.add_argument("--femur_input_margin", type=float, default=DEFAULT_FEMUR_INPUT_MARGIN_MM, - help="Pad the input image/mask as needed so femur foreground has this margin before pre-rotation and ICP. (default: %(default)s [mm])") + help="Pad the input image/mask as needed so femur foreground has this margin before ICP. (default: %(default)s [mm])") parser.add_argument("--compartment_mask", type=str, default=None, help="Optional trabecular/cortical compartment mask aligned with the bone mask. Defaults: cortical=1, trabecular=2.") parser.add_argument("--cortical_label", type=int, default=DEFAULT_CORTICAL_LABEL, diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 708f4d7..83cc4f6 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -38,6 +38,8 @@ DEFAULT_SPINE_PMMA_POISSONS_RATIO = 0.3 DEFAULT_SPINE_FE_DISPLACEMENT_MM = -1.0 DEFAULT_SPINE_TARGET_DISPLACEMENT_PERCENT = 0.68 +DEFAULT_SPINE_LABEL_SMOOTHING_SIGMA_MM = 1.0 +DEFAULT_SPINE_LABEL_SMOOTHING_THRESHOLD = 0.5 DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM = 2.0 DEFAULT_SPINE_TOP_NODE_SET_ID = 4 DEFAULT_SPINE_BOTTOM_NODE_SET_ID = 3 @@ -46,6 +48,9 @@ DEFAULT_SPINE_REGISTRATION_MAX_SCALE = "1.2,1.2,1.3" DEFAULT_SPINE_REGISTRATION_BACKEND = "vtk" DEFAULT_SPINE_REFERENCE_FILENAME = "L4_BODY_SPINE_COMPRESSION_REF.vtk" +SPINE_CONTACT_SIZE_FRACTION = (1.6, 1.6) +SPINE_SUPERIOR_CONTACT_CENTER_FRACTION = (0.5, 0.5, 1.05) +SPINE_INFERIOR_CONTACT_CENTER_FRACTION = (0.5, 0.5, -0.05) SPINE_ALIGNMENT_METHOD = "scaled ICP to reference vertebral body" diff --git a/ogo/fea/spine_compression.py b/ogo/fea/spine_compression.py index 840e43b..fc0fdde 100644 --- a/ogo/fea/spine_compression.py +++ b/ogo/fea/spine_compression.py @@ -49,15 +49,32 @@ import vtkbone from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy +from ogo.fea.alignment import ( + estimate_rigid_icp, + invert_point_transform, + output_grid_for_point_transform, + point_cloud_axis_lengths, + polydata_points, + resample_vtk_image_with_point_transform, + sample_points, + surface_points_from_vtk_mask, +) from ogo.fea.boundary import ( - generate_bone_cap_vtk, + bbox_relative_contact_plane, + foreground_voxel_center_bounds, + generate_projected_material_disk_vtk, + pad_vtk_images_to_physical_bounds, + projected_material_disk_required_bounds, should_smooth_resampled_mask, smooth_binary_mask_vtk, + smooth_label_mask_vtk, ) from ogo.fea.spine import ( DEFAULT_SPINE_BOTTOM_NODE_SET_ID, DEFAULT_SPINE_FE_DISPLACEMENT_MM, DEFAULT_SPINE_ISO_RESOLUTION_MM, + DEFAULT_SPINE_LABEL_SMOOTHING_SIGMA_MM, + DEFAULT_SPINE_LABEL_SMOOTHING_THRESHOLD, DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, DEFAULT_SPINE_PMMA_E_MPA, DEFAULT_SPINE_PMMA_INTRUSION_MM, @@ -70,6 +87,9 @@ DEFAULT_SPINE_REGISTRATION_BACKEND, DEFAULT_SPINE_REGISTRATION_SCALE, DEFAULT_SPINE_TOP_NODE_SET_ID, + SPINE_CONTACT_SIZE_FRACTION, + SPINE_INFERIOR_CONTACT_CENTER_FRACTION, + SPINE_SUPERIOR_CONTACT_CENTER_FRACTION, default_spine_reference_path, ) import ogo.util.Helper as ogo @@ -132,11 +152,18 @@ def parse_scale_triplet(value, name="scale"): ###################################################################### HELPERS (VTK) +class _NiftiImageHandle: + """Reader-like wrapper for NIfTI images loaded through Ogo's orientation path.""" + + def __init__(self, image): + self._image = image + + def GetOutput(self): + return self._image + + def read(input_mask): - mask_reader = vtk.vtkNIFTIImageReader() - mask_reader.SetFileName(input_mask) - mask_reader.Update() - return mask_reader + return _NiftiImageHandle(ogo.readNii(input_mask)) def check_vertebra_presence(mask_reader, vertebra): if vertebra not in vtk_to_numpy(mask_reader.GetOutput().GetPointData().GetScalars()).ravel(): @@ -232,6 +259,17 @@ def crop_to_bounding_box(image_data, bb=None): if crop.GetOutput().GetNumberOfPoints() == 0: raise ValueError("Cropping resulted in an empty image. Extents might be incorrect.") + output = crop.GetOutput() + spacing = output.GetSpacing() + origin = image_data.GetOrigin() + output.SetOrigin( + float(origin[0]) + float(min_x) * float(spacing[0]), + float(origin[1]) + float(min_y) * float(spacing[1]), + float(origin[2]) + float(min_z) * float(spacing[2]), + ) + dims = output.GetDimensions() + output.SetExtent(0, dims[0] - 1, 0, dims[1] - 1, 0, dims[2] - 1) + return crop, bb def pad_vtk_image(vtk_image, axis='x', pmma_thick=5, pad_value=0): @@ -371,33 +409,27 @@ def get_icp_with_scaling( max_scale=(1.2, 1.2, 1.3), ): - def get_principal_axes_lengths(polydata): - points = vtk_to_numpy(polydata.GetPoints().GetData()) - centered = points - np.mean(points, axis=0) - cov = np.cov(centered.T) - eigvals, eigvecs = np.linalg.eigh(cov) - lengths = np.sqrt(eigvals) * 2 # Approx. diameter along each PCA axis - return lengths - - # Get sample surface from marching cubes - mcubes = perform_marching_cubes(body) - mcubes_output = mcubes.GetOutput() - - # Load reference polydata - reference_reader = vtk.vtkPolyDataReader() - reference_reader.SetFileName(reference_path) - reference_reader.Update() - reference_polydata = reference_reader.GetOutput() + sample_surface_points = surface_points_from_vtk_mask( + body.GetOutput(), + max_points=8000, + sample_mode="stride", + ) + reference_polydata = ogo.readPolyData(reference_path) + reference_points = sample_points( + polydata_points(reference_polydata), + max_points=8000, + mode="linspace", + ) - sample_lengths = get_principal_axes_lengths(mcubes_output) - ref_lengths = get_principal_axes_lengths(reference_polydata) + sample_lengths = point_cloud_axis_lengths(sample_surface_points) + ref_lengths = point_cloud_axis_lengths(reference_points) if scale_factors is None: scale_factors = sample_lengths / ref_lengths min_scale = parse_scale_triplet(min_scale, "registration_min_scale") max_scale = parse_scale_triplet(max_scale, "registration_max_scale") scale_factors = np.minimum(scale_factors, max_scale) scale_factors = np.maximum(scale_factors, min_scale) - scale_source = "PCA axis lengths" + scale_source = "voxel-surface PCA axis lengths" else: scale_factors = parse_scale_triplet(scale_factors, "registration_scale") scale_source = "manual override" @@ -406,45 +438,45 @@ def get_principal_axes_lengths(polydata): ogo.message(f"PCA axis lengths (reference): {np.round(ref_lengths, 2)}") ogo.message(f"Scale factors applied to reference ({scale_source}): {np.round(scale_factors, 3)}") - # Scale the reference to match the sample - scale_transform = vtk.vtkTransform() - scale_transform.Scale(*scale_factors) - - transform_filter = vtk.vtkTransformPolyDataFilter() - transform_filter.SetInputData(reference_polydata) - transform_filter.SetTransform(scale_transform) - transform_filter.Update() - scaled_reference = transform_filter.GetOutput() - - # ICP with scaled reference - icp = vtk.vtkIterativeClosestPointTransform() - icp.SetTarget(mcubes_output) - icp.SetSource(scaled_reference) - icp.StartByMatchingCentroidsOn() - icp.GetLandmarkTransform().SetModeToRigidBody() - icp.SetMeanDistanceModeToRMS() - icp.SetMaximumMeanDistance(0.05) - icp.CheckMeanDistanceOn() - icp.SetMaximumNumberOfLandmarks(250) - icp.SetMaximumNumberOfIterations(75) - icp.Update() + transform = estimate_rigid_icp( + moving_points=reference_points * scale_factors, + fixed_points=sample_surface_points, + iterations=50, + tolerance=1.0e-4, + start_by_matching_centroids_only=False, + convergence="delta", + distance_mode="mean", + ) - ogo.message("ICP (with scaled reference) Matrix:") - print_matrix(icp.GetMatrix()) + matrix = vtk.vtkMatrix4x4() + matrix.Identity() + rotation = transform["rotation"] + translation = transform["translation"] + for row in range(3): + for col in range(3): + matrix.SetElement(row, col, float(rotation[row, col])) + matrix.SetElement(row, 3, float(translation[row])) + + vtk_transform = vtk.vtkTransform() + vtk_transform.SetMatrix(matrix) + ogo.message( + "ICP (with scaled reference) iterations=%d mean_distance=%0.4f" + % (transform["iterations"], transform["mean_distance"]) + ) + ogo.message("ICP reference-to-sample Matrix:") + print_matrix(vtk_transform.GetMatrix()) - return icp + return vtk_transform def get_icp(body, reference_path): mcubes = perform_marching_cubes(body) mcubes_output = mcubes.GetOutput() - reference_bone_reader = vtk.vtkPolyDataReader() - reference_bone_reader.SetFileName(reference_path) - reference_bone_reader.Update() + reference_bone = ogo.readPolyData(reference_path) icp = vtk.vtkIterativeClosestPointTransform() icp.SetTarget(mcubes_output) - icp.SetSource(reference_bone_reader.GetOutput()) + icp.SetSource(reference_bone) icp.StartByMatchingCentroidsOn() icp.GetLandmarkTransform().SetModeToRigidBody() @@ -465,6 +497,15 @@ def transform_resample(image, matrix, iso_resolution, interpolation='cubic'): ogo.message("Reslice output contains no points. Check the input and transformation.") return output + +def _matrix_rotation_translation(matrix): + rotation = np.asarray( + [[matrix.GetElement(row, col) for col in range(3)] for row in range(3)], + dtype=float, + ) + translation = np.asarray([matrix.GetElement(row, 3) for row in range(3)], dtype=float) + return rotation, translation + ###################################################################### DISK GENERATION ## Functions related to disk generation (image processing) ###################################################################### DISK GENERATION @@ -596,9 +637,9 @@ def generate_cortical_mask(image_vtk, mask_vtk, threshold=0.2, min_th=1, max_th= def convert_image_to_material(image, mask, n_bins=128, cort_mask=None): - change = ogo.changeInfo(image) - mask_change = ogo.changeInfo(mask) - cort_mask_change = ogo.changeInfo(cort_mask) if cort_mask is not None else None + change = ogo.prepareFiniteElementImage(image) + mask_change = ogo.prepareFiniteElementImage(mask) + cort_mask_change = ogo.prepareFiniteElementImage(cort_mask) if cort_mask is not None else None connected_image = ogo.imageConnectivity(change) thr_image = ogo.bmd_preprocess(connected_image, -31) #this was done previously - but if someone wants this they should just do it in the calib functions @@ -610,7 +651,7 @@ def convert_image_to_material(image, mask, n_bins=128, cort_mask=None): return binned_image, bin_centers def find_and_add_visible_nodes(model, bc_geometry, normal_vector, bone_material_id, node_set_name): - """Compatibility wrapper for shared spine FE visible-node detection.""" + """Delegate visible-node detection to the shared FE model helper.""" from ogo.fea.model import find_and_add_visible_nodes as _find_and_add_visible_nodes return _find_and_add_visible_nodes( @@ -618,20 +659,20 @@ def find_and_add_visible_nodes(model, bc_geometry, normal_vector, bone_material_ ) def resolve_func(func_or_name, module): - """Compatibility wrapper for material-law function resolution.""" + """Resolve a material-law function by object or configured name.""" from ogo.fea.materials import resolve_material_func return resolve_material_func(func_or_name, module) def apply_boundary_conditions(model,**kwargs): - """Compatibility wrapper for shared spine compression boundary conditions.""" + """Apply the maintained spine compression boundary conditions.""" from ogo.fea.model import apply_spine_boundary_conditions return apply_spine_boundary_conditions(model, **kwargs) def log_fe_arguments(**kwargs): - """Compatibility wrapper for shared FE argument logging.""" + """Print model-building arguments through the shared FE logger.""" from ogo.fea.model import log_fe_arguments as _log_fe_arguments return _log_fe_arguments(**kwargs) @@ -642,7 +683,7 @@ def create_microfe_model( bin_centers, **kwargs ): - """Compatibility wrapper for shared spine compression model creation.""" + """Create a spine compression micro-FE model from material and BC masks.""" from ogo.fea.model import create_microfe_model as _create_microfe_model return _create_microfe_model( @@ -897,12 +938,30 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, "mask_smoothing_spacing_threshold", DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, ) + label_smoothing_sigma_mm = float( + kwargs.get("label_smoothing_sigma_mm", DEFAULT_SPINE_LABEL_SMOOTHING_SIGMA_MM) + ) + label_smoothing_threshold = float( + kwargs.get("label_smoothing_threshold", DEFAULT_SPINE_LABEL_SMOOTHING_THRESHOLD) + ) #Read Image and Mask ogo.message(f"Reading image...: {input_image}") image_reader = read(input_image) ogo.message(f"Reading mask...: {input_mask}") mask_reader = resample_to_match(image_reader.GetOutput(), read(input_mask).GetOutput()) + if label_smoothing_sigma_mm > 0: + ogo.message( + "smoothing input label mask " + f"(sigma={label_smoothing_sigma_mm} mm, threshold={label_smoothing_threshold})..." + ) + mask_reader = _NiftiImageHandle( + smooth_label_mask_vtk( + mask_reader.GetOutput(), + sigma_mm=label_smoothing_sigma_mm, + threshold=label_smoothing_threshold, + ) + ) input_spacing = image_reader.GetOutput().GetSpacing() # Get and Check labels @@ -931,19 +990,56 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, ) # Transform Images and resample at the same time (less interpolation) - transformed_vertebra = transform_resample( + reference_to_sample_rotation, reference_to_sample_translation = _matrix_rotation_translation( + icp.GetMatrix() + ) + sample_to_reference_rotation, sample_to_reference_translation = invert_point_transform( + reference_to_sample_rotation, + reference_to_sample_translation, + ) + output_spacing = (float(iso_resolution), float(iso_resolution), float(iso_resolution)) + isolated_model_mask = combine_mask( isolated_vertebra.GetOutput(), - icp.GetMatrix(), - iso_resolution, - interpolation='nearest', + isolated_process.GetOutput(), + ) + model_surface_points = surface_points_from_vtk_mask( + isolated_model_mask.GetOutput(), + max_points=None, + ) + output_origin, output_size = output_grid_for_point_transform( + model_surface_points, + rotation=sample_to_reference_rotation, + translation=sample_to_reference_translation, + spacing=output_spacing, + margin_voxels=int(kwargs.get("registration_margin_voxels", 4)), + ) + transformed_vertebra = resample_vtk_image_with_point_transform( + isolated_vertebra.GetOutput(), + rotation=sample_to_reference_rotation, + translation=sample_to_reference_translation, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", ) - transformed_process = transform_resample( + transformed_process = resample_vtk_image_with_point_transform( isolated_process.GetOutput(), - icp.GetMatrix(), - iso_resolution, - interpolation='nearest', + rotation=sample_to_reference_rotation, + translation=sample_to_reference_translation, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", + ) + transformed_image = resample_vtk_image_with_point_transform( + isolated_image.GetOutput(), + rotation=sample_to_reference_rotation, + translation=sample_to_reference_translation, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="bspline", ) - transformed_image = transform_resample(isolated_image.GetOutput(), icp.GetMatrix(), iso_resolution) if should_smooth_resampled_mask(input_spacing, mask_smoothing_spacing_threshold): ogo.message( @@ -994,26 +1090,123 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, padded_mask2 = pad_vtk_image(mask2_labeled.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) ogo.message( - "extruding fixed-thickness anatomy body-cap disks " + "generating fixed-thickness anatomy body-cap disks " f"(pmma_thick = {pmma_thick} mm total, pmma_intrusion = {pmma_intrusion} mm)..." ) - inferior_disk = generate_bone_cap_vtk( - padded_mask1, - label_value=2, + transformed_body_bounds = foreground_voxel_center_bounds(padded_mask1) + body_bounds = transformed_body_bounds + inferior_plane = bbox_relative_contact_plane( + body_bounds, + center_fraction=SPINE_INFERIOR_CONTACT_CENTER_FRACTION, + size_fraction=SPINE_CONTACT_SIZE_FRACTION, + projection_axis="z", + shape="anatomy", + ) + superior_plane = bbox_relative_contact_plane( + body_bounds, + center_fraction=SPINE_SUPERIOR_CONTACT_CENTER_FRACTION, + size_fraction=SPINE_CONTACT_SIZE_FRACTION, + projection_axis="z", + shape="anatomy", + ) + ogo.message( + "Inferior PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" + % ( + inferior_plane["center"], + inferior_plane["normal"], + inferior_plane["u_axis"], + inferior_plane["v_axis"], + inferior_plane["size"], + ) + ) + ogo.message( + "Superior PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" + % ( + superior_plane["center"], + superior_plane["normal"], + superior_plane["u_axis"], + superior_plane["v_axis"], + superior_plane["size"], + ) + ) + required_bounds = [ + projected_material_disk_required_bounds( + padded_mask1, + center=plane["center"], + normal=plane["normal"], + u_axis=plane["u_axis"], + v_axis=plane["v_axis"], + size=plane["size"], + shape=plane["shape"], + thickness=pmma_thick, + intrusion=pmma_intrusion, + ) + for plane in (inferior_plane, superior_plane) + ] + required_bounds = [bounds for bounds in required_bounds if bounds is not None] + if required_bounds: + bounds_array = np.asarray(required_bounds, dtype=float) + desired_bounds = ( + float(bounds_array[:, 0].min()), + float(bounds_array[:, 1].max()), + float(bounds_array[:, 2].min()), + float(bounds_array[:, 3].max()), + float(bounds_array[:, 4].min()), + float(bounds_array[:, 5].max()), + ) + ( + padded_image, + padded_transformed_image, + padded_mask, + padded_cort_mask, + padded_mask1, + padded_mask2, + ), contact_padding = pad_vtk_images_to_physical_bounds( + [ + padded_image, + padded_transformed_image, + padded_mask, + padded_cort_mask, + padded_mask1, + padded_mask2, + ], + desired_bounds=desired_bounds, + constants=[0, 0, 0, 0, 0, 0], + ) + if any(contact_padding["lower"]) or any(contact_padding["upper"]): + ogo.message( + "expanded image canvas for projected disks: lower=%s upper=%s" + % (contact_padding["lower"], contact_padding["upper"]) + ) + inferior_disk = generate_projected_material_disk_vtk( + padded_image, + surface_vtk_image=padded_mask1, + exclusion_vtk_image=padded_mask1, + center=inferior_plane["center"], + normal=inferior_plane["normal"], + u_axis=inferior_plane["u_axis"], + v_axis=inferior_plane["v_axis"], + size=inferior_plane["size"], + shape=inferior_plane["shape"], thickness=pmma_thick, intrusion=pmma_intrusion, - direction='down', - axis='x', - shape='anatomy', + anatomy_constrained=True, + output_value=1, ) - superior_disk = generate_bone_cap_vtk( - padded_mask1, - label_value=2, + superior_disk = generate_projected_material_disk_vtk( + padded_image, + surface_vtk_image=padded_mask1, + exclusion_vtk_image=padded_mask1, + center=superior_plane["center"], + normal=superior_plane["normal"], + u_axis=superior_plane["u_axis"], + v_axis=superior_plane["v_axis"], + size=superior_plane["size"], + shape=superior_plane["shape"], thickness=pmma_thick, intrusion=pmma_intrusion, - direction='up', - axis='x', - shape='anatomy', + anatomy_constrained=True, + output_value=1, ) ogo.message(f"merging disks with image...") @@ -1030,7 +1223,16 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, # This is the final model that we will use for the FEA. # For the future --> pull out material table to have option without disks ogo.message("generating n88model file...") - model = create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, **kwargs) + model = create_microfe_model( + image_with_pads, + boundary_masks_with_pads, + bin_centers, + top_boundary_mask_image=superior_disk, + bottom_boundary_mask_image=inferior_disk, + top_boundary_mask_label=1, + bottom_boundary_mask_label=1, + **kwargs, + ) if kwargs.get("export_nifti", False): ogo.message("generating nifti files...") @@ -1141,7 +1343,7 @@ def main(): parser.add_argument("--registration_max_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_MAX_SCALE, help="Maximum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") parser.add_argument("--registration_backend", choices=("vtk",), default=DEFAULT_SPINE_REGISTRATION_BACKEND, - help="Compatibility option; spine reference alignment uses VTK ICP. (default: %(default)s)") + help="Spine reference alignment backend. (default: %(default)s)") parser.add_argument("--top_node_set_id", type=int, default=DEFAULT_SPINE_TOP_NODE_SET_ID, help="ID for the top node set. (default: %(default)s)") parser.add_argument("--bottom_node_set_id", type=int, default=DEFAULT_SPINE_BOTTOM_NODE_SET_ID, diff --git a/ogo/util/Helper.py b/ogo/util/Helper.py index 9045dc1..67ad5de 100644 --- a/ogo/util/Helper.py +++ b/ogo/util/Helper.py @@ -714,11 +714,10 @@ def cast2sameScalarType(vtk_image, template_image): return cast.GetOutput() -def changeInfo(vtk_image): - """Changes the origin of image to 0,0,0""" +def prepareFiniteElementImage(vtk_image): + """Prepare image data for FE meshing while preserving physical metadata.""" change = vtk.vtkImageChangeInformation() change.SetInputData(vtk_image) - change.SetOutputOrigin(0, 0, 0) change.Update() return change.GetOutput() @@ -1410,11 +1409,11 @@ def iterativeClosestPoint(source, target): icp.SetTarget(target) icp.StartByMatchingCentroidsOn() icp.GetLandmarkTransform().SetModeToRigidBody() - icp.SetMeanDistanceModeToRMS() + icp.SetMeanDistanceModeToAbsoluteValue() icp.SetMaximumMeanDistance(0.05) icp.CheckMeanDistanceOn() - icp.SetMaximumNumberOfLandmarks(250) - icp.SetMaximumNumberOfIterations(75) + icp.SetMaximumNumberOfLandmarks(8000) + icp.SetMaximumNumberOfIterations(50) icp.Update() return icp.GetMatrix() @@ -1748,7 +1747,7 @@ def point2cellData(vtk_image): def preRotateImage(image, mask, z_rotation): - """Pre-rotation the image for ICP alignment.""" + """Rotate an image/mask pair around the image z axis for ICP initialization.""" message("Pre-rotating image...") spacing = image.GetSpacing() origin = image.GetOrigin() @@ -1761,7 +1760,6 @@ def preRotateImage(image, mask, z_rotation): transform = vtk.vtkTransform() transform.Translate(center[0], center[1], center[2]) - transform.RotateY(180) transform.RotateZ(z_rotation) transform.Translate(-center[0], -center[1], -center[2]) @@ -1784,14 +1782,17 @@ def preRotateImage(image, mask, z_rotation): def readPolyData(vtk_poly): - """Reads a VTK Legacy file in PolyData Format. - The first argument is the filename. - Returns the Poly Data reader output. - """ + """Read VTK polydata as RAS physical reference geometry.""" poly = vtk.vtkPolyDataReader() poly.SetFileName(vtk_poly) poly.Update() - return poly.GetOutput() + transform = vtk.vtkTransform() + transform.Scale(-1.0, -1.0, 1.0) + transform_filter = vtk.vtkTransformPolyDataFilter() + transform_filter.SetInputData(poly.GetOutput()) + transform_filter.SetTransform(transform) + transform_filter.Update() + return transform_filter.GetOutput() def readTransform(transform_file): @@ -2655,11 +2656,14 @@ def readDCM(fileDir): return flip2.GetOutput() # def readNii(filename): - """Reads a NIFTI image. - The first argument is the image filename. - Returns the Image as vtk Output Data - """ - image = vtk.vtkNIFTIImageReader() - image.SetFileName(filename) - image.Update() - return image.GetOutput() + """Read a NIfTI image into a RAS physical-space VTK image.""" + image = sitk.DICOMOrient(sitk.ReadImage(str(filename)), "RAS") + array_xyz = np.ascontiguousarray(np.transpose(sitk.GetArrayFromImage(image), (2, 1, 0))) + + vtk_image = vtk.vtkImageData() + vtk_image.SetDimensions(array_xyz.shape) + vtk_image.SetSpacing(tuple(float(value) for value in image.GetSpacing())) + origin_lps = image.GetOrigin() + vtk_image.SetOrigin(-float(origin_lps[0]), -float(origin_lps[1]), float(origin_lps[2])) + vtk_image.GetPointData().SetScalars(numpy_to_vtk(array_xyz.ravel(order="F"), deep=True)) + return vtk_image diff --git a/tests/cli/test_CheckFEModelBC.py b/tests/cli/test_CheckFEModelBC.py new file mode 100644 index 0000000..2c34cc2 --- /dev/null +++ b/tests/cli/test_CheckFEModelBC.py @@ -0,0 +1,37 @@ +from ogo.cli.CheckFEModelBC import audit_spine_compression + + +def _flat_set(z): + return { + "count": 4, + "bounds": { + "x_min": 0.0, + "x_max": 1.0, + "y_min": 0.0, + "y_max": 1.0, + "z_min": float(z), + "z_max": float(z), + }, + "centroid": {"x": 0.5, "y": 0.5, "z": float(z)}, + "planarity": {"rms_mm": 0.0, "normal": [0.0, 0.0, 1.0]}, + } + + +def test_spine_bc_audit_requires_all_inferior_fixture_axes(): + node_sets = { + "body_top": _flat_set(10.0), + "body_bottom": _flat_set(0.0), + } + constraints = { + "top_displacement": {"axes": ["z"], "values": [-0.1]}, + "bottom_fixed_x": {"axes": ["x"], "values": [0.0]}, + "bottom_fixed_y": {"axes": ["y"], "values": [0.0]}, + "bottom_fixed_z": {"axes": ["z"], "values": [0.0]}, + } + + checks = audit_spine_compression(node_sets, constraints, tolerance=1.0e-5) + by_name = {check["name"]: check for check in checks} + + assert by_name["bottom_fixed_x fixes x"]["passed"] is True + assert by_name["bottom_fixed_y fixes y"]["passed"] is True + assert by_name["bottom_fixed_z fixes z"]["passed"] is True diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index c1e4a11..d149ecc 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -349,6 +349,7 @@ def test_spine_modeling_metadata_records_materials_and_bcs(tmp_path): assert data["target"] == {"vertebra": "L1", "body_label": 2, "process_label": 3} assert data["alignment"]["registration_backend"] == "vtk" + assert data["geometry"]["model_coordinates"] == "preprocessed_image_physical_space" assert data["materials"]["trabecular"]["elastic_E_func"] == "kopperdahl_trab_E" assert data["materials"]["cortical"]["material_id_range"] == [129, 256] assert data["materials"]["pmma"]["material_id"] == 5000 @@ -358,6 +359,9 @@ def test_spine_modeling_metadata_records_materials_and_bcs(tmp_path): assert "initial_generator_value_mm" not in data["boundary_conditions"]["constraints"][0] assert data["boundary_conditions"]["constraints"][0]["target_displacement_percent"] == 0.68 assert data["boundary_conditions"]["constraints"][0]["value_source"].startswith("target_displacement_percent") + assert data["boundary_conditions"]["constraints"][1]["node_set"] == "body_bottom" + assert data["boundary_conditions"]["constraints"][1]["axes"] == ["x", "y", "z"] + assert data["boundary_conditions"]["constraints"][1]["value_mm"] == 0.0 assert data["solve_and_reporting"]["target_displacement_percent"] == 0.68 assert data["solve_and_reporting"]["run_pistoia"] is False @@ -384,10 +388,15 @@ def test_femur_modeling_metadata_records_materials_shaft_and_bcs(tmp_path): data = json.loads(path.read_text()) assert data["target"]["side"] == "left" + assert data["geometry"]["model_coordinates"] == "preprocessed_image_physical_space" assert data["segmentation"]["compartment_labels"] == {"cortical": 1, "trabecular": 2} assert data["shaft_standardization"]["lesser_trochanter_distal_offset_mm"] == 50.0 assert data["materials"]["include_cortical_region"] is True assert data["materials"]["cortical"]["material_id_range"] == [129, 256] + assert data["boundary_conditions"]["fixture_geometry"]["femoral_head"]["relative_to"] == "model_bbox" + assert data["boundary_conditions"]["fixture_geometry"]["femoral_head"]["shape"] == "rectangle fixture cap" + assert data["boundary_conditions"]["fixture_geometry"]["greater_trochanter"]["relative_to"] == "model_bbox" + assert data["boundary_conditions"]["fixture_geometry"]["greater_trochanter"]["shape"] == "rectangle fixture cap" assert data["boundary_conditions"]["fixture_geometry"]["femoral_head"]["pmma_intrusion_mm"] == 6.0 assert data["boundary_conditions"]["constraints"][0]["node_set"] == "Femoral_Head_PMMA_Nodes" assert "initial_generator_value_mm" not in data["boundary_conditions"]["constraints"][0] diff --git a/tests/fea/test_alignment.py b/tests/fea/test_alignment.py new file mode 100644 index 0000000..9ffbb56 --- /dev/null +++ b/tests/fea/test_alignment.py @@ -0,0 +1,137 @@ +import pytest + + +def _vtk_image_from_array(data, *, origin=(0, 0, 0), spacing=(1, 1, 1)): + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(*origin) + image.SetSpacing(*spacing) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + return image + + +def test_surface_points_from_vtk_mask_uses_voxel_surface_centers(): + np = pytest.importorskip("numpy") + + from ogo.fea.alignment import surface_points_from_vtk_mask + + mask = np.ones((3, 3, 3), dtype=np.uint8) + image = _vtk_image_from_array(mask, origin=(10, 20, 30), spacing=(2, 3, 4)) + + full = surface_points_from_vtk_mask(image, max_points=None) + sampled = surface_points_from_vtk_mask( + image, + max_points=5, + sample_mode="stride", + ) + + assert full.shape == (26, 3) + assert not np.any(np.all(np.isclose(full, (12, 23, 34)), axis=1)) + assert sampled == pytest.approx(full[[0, 6, 12, 18, 24]]) + + +def test_estimate_rigid_icp_recovers_simple_translation(): + np = pytest.importorskip("numpy") + + from ogo.fea.alignment import estimate_rigid_icp + + moving = np.asarray( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) + fixed = moving + np.asarray([3.0, -2.0, 5.0]) + + transform = estimate_rigid_icp( + moving_points=moving, + fixed_points=fixed, + iterations=5, + tolerance=1.0e-9, + ) + + assert transform["rotation"] == pytest.approx(np.eye(3)) + assert transform["translation"] == pytest.approx((3.0, -2.0, 5.0)) + + +def test_invert_point_transform_reverses_row_vector_transform(): + np = pytest.importorskip("numpy") + + from ogo.fea.alignment import invert_point_transform + + rotation = np.asarray( + [ + [0.0, -1.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 0.0, 1.0], + ] + ) + translation = np.asarray([3.0, -2.0, 5.0]) + points = np.asarray([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) + + inverse_rotation, inverse_translation = invert_point_transform(rotation, translation) + transformed = points @ rotation.T + translation + restored = transformed @ inverse_rotation.T + inverse_translation + + assert restored == pytest.approx(points) + + +def test_output_grid_for_point_transform_uses_transformed_voxel_centers(): + np = pytest.importorskip("numpy") + + from ogo.fea.alignment import output_grid_for_point_transform + + points = np.asarray( + [ + [10.0, 20.0, 30.0], + [14.0, 20.0, 30.0], + [10.0, 26.0, 30.0], + [10.0, 20.0, 38.0], + ] + ) + rotation = np.eye(3) + translation = np.asarray([1.0, -2.0, 3.0]) + + origin, size = output_grid_for_point_transform( + points, + rotation=rotation, + translation=translation, + spacing=(2.0, 3.0, 4.0), + margin_voxels=1, + ) + + assert origin == pytest.approx((9.0, 15.0, 29.0)) + assert size == (5, 5, 5) + + +def test_resample_vtk_image_with_point_transform_uses_explicit_output_grid(): + np = pytest.importorskip("numpy") + + from ogo.fea.alignment import resample_vtk_image_with_point_transform + from ogo.util.vtk_image import vtk_image_to_numpy + + data = np.zeros((3, 3, 3), dtype=np.uint8) + data[1, 1, 1] = 7 + image = _vtk_image_from_array(data, origin=(0, 0, 0), spacing=(1, 1, 1)) + + shifted = resample_vtk_image_with_point_transform( + image, + rotation=np.eye(3), + translation=np.asarray([2.0, 0.0, 0.0]), + output_origin=(0.0, 0.0, 0.0), + output_size=(5, 3, 3), + output_spacing=(1.0, 1.0, 1.0), + interpolation="nearest", + ) + + shifted_array = vtk_image_to_numpy(shifted) + assert shifted.GetOrigin() == pytest.approx((0.0, 0.0, 0.0)) + assert shifted.GetSpacing() == pytest.approx((1.0, 1.0, 1.0)) + assert shifted.GetDimensions() == (5, 3, 3) + assert shifted_array[3, 1, 1] == 7 + assert int(np.count_nonzero(shifted_array)) == 1 diff --git a/tests/fea/test_boundary.py b/tests/fea/test_boundary.py index d8e369b..b069d3a 100644 --- a/tests/fea/test_boundary.py +++ b/tests/fea/test_boundary.py @@ -2,12 +2,171 @@ np = pytest.importorskip("numpy") -from ogo.fea.boundary import ( +from ogo.fea.boundary import ( # noqa: E402 + bbox_relative_contact_bounds, + bbox_relative_contact_direction, + bbox_relative_contact_plane, + bounds_with_reference_extent, + foreground_voxel_center_bounds_from_mask, generate_bone_cap_mask, - generate_fixture_cap_mask, + generate_projected_material_disk_mask, + pad_vtk_images_to_physical_bounds, + projected_material_disk_required_bounds, + smooth_label_mask_vtk, ) +def test_bbox_relative_contact_plane_uses_authored_axis_convention(): + bounds = (10, 50, -20, 80, 100, 200) + + low_y_plane = bbox_relative_contact_plane( + bounds, + center_fraction=(0.5, 0.02, 0.25), + size_fraction=(1.1, 0.5), + projection_axis="y", + shape="rectangle", + ) + high_y_plane = bbox_relative_contact_plane( + bounds, + center_fraction=(0.5, 1.02, 0.25), + size_fraction=(1.1, 0.5), + projection_axis="y", + shape="rectangle", + ) + + assert bbox_relative_contact_direction((0.5, 0.02, 0.25), projection_axis="y") == "down" + assert bbox_relative_contact_direction((0.5, 1.02, 0.25), projection_axis="y") == "up" + assert low_y_plane["center"] == pytest.approx((30, -18, 125)) + assert low_y_plane["normal"] == pytest.approx((0, 1, 0)) + assert low_y_plane["u_axis"] == pytest.approx((0, 0, -1)) + assert low_y_plane["v_axis"] == pytest.approx((-1, 0, 0)) + assert low_y_plane["size"] == pytest.approx((110, 20)) + assert low_y_plane["shape"] == "rectangle" + assert high_y_plane["normal"] == pytest.approx((0, -1, 0)) + assert high_y_plane["u_axis"] == pytest.approx((0, 0, 1)) + assert high_y_plane["v_axis"] == pytest.approx((-1, 0, 0)) + + +def test_bbox_relative_contact_bounds_can_enforce_square_footprint(): + bounds = (10, 50, -20, 80, 100, 200) + + contact_bounds = bbox_relative_contact_bounds( + bounds, + center_fraction=(0.5, 1.1, 0.25), + size_fraction=(1.1, 0.5), + projection_axis="y", + shape="square", + ) + + assert contact_bounds == pytest.approx((8, 52, -20, 80, 103, 147)) + + +def test_bounds_with_reference_extent_preserves_current_center(): + current_bounds = (10, 20, -5, 35, 100, 133) + reference_bounds = (0, 49, 0, 43, 0, 34) + + bounds = bounds_with_reference_extent(current_bounds, reference_bounds) + + assert bounds == pytest.approx((-9.5, 39.5, -6.5, 36.5, 99.5, 133.5)) + + +def test_foreground_voxel_center_bounds_from_mask_uses_xyz_array_order(): + mask = np.zeros((8, 9, 10), dtype=np.uint8) + mask[2:6, 3:8, 1:7] = 1 + + bounds = foreground_voxel_center_bounds_from_mask( + mask, + origin=(10, 20, 30), + spacing=(2, 3, 4), + ) + + assert bounds == pytest.approx((14, 20, 29, 41, 34, 54)) + + +def test_pad_vtk_images_to_physical_bounds_preserves_world_coordinates(): + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + values = np.zeros((3, 3, 3), dtype=np.uint8) + values[0, 0, 0] = 7 + image = vtk.vtkImageData() + image.SetDimensions(values.shape) + image.SetOrigin(10.0, 20.0, 30.0) + image.SetSpacing(2.0, 1.0, 5.0) + image.GetPointData().SetScalars(numpy_to_vtk(values.ravel(order="F"), deep=True)) + + padded, padding = pad_vtk_images_to_physical_bounds( + [image], + desired_bounds=(6.0, 14.0, 20.0, 24.0, 30.0, 40.0), + constants=[3], + ) + + out = padded[0] + data = vtk_image_to_numpy(out) + + assert out.GetDimensions() == (5, 5, 3) + assert out.GetOrigin() == pytest.approx((6.0, 20.0, 30.0)) + assert padding["lower"] == (2, 0, 0) + assert padding["upper"] == (0, 2, 0) + assert data[2, 0, 0] == 7 + assert data[0, 4, 0] == 3 + + +def test_projected_material_disk_required_bounds_includes_lateral_plane_extent(): + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + mask = np.zeros((5, 5, 5), dtype=np.uint8) + mask[2, 2, 2] = 1 + image = vtk.vtkImageData() + image.SetDimensions(mask.shape) + image.SetOrigin(0.0, 0.0, 0.0) + image.SetSpacing(1.0, 1.0, 1.0) + image.GetPointData().SetScalars(numpy_to_vtk(mask.ravel(order="F"), deep=True)) + + bounds = projected_material_disk_required_bounds( + image, + center=(2.0, 2.0, 6.0), + normal=(0.0, 0.0, -1.0), + u_axis=(1.0, 0.0, 0.0), + v_axis=(0.0, 1.0, 0.0), + size=(8.0, 6.0), + thickness=4.0, + intrusion=2.0, + ) + + assert bounds == pytest.approx((-3.0, 7.0, -2.0, 6.0, -1.0, 5.0)) + + +def test_smooth_label_mask_vtk_preserves_likely_label_regions(): + vtk = pytest.importorskip("vtk") + + labels = np.zeros((9, 9, 9), dtype=np.uint8) + labels[2:7, 2:7, 2:7] = 1 + labels[4:7, 4:7, 4:7] = 2 + + from vtk.util.numpy_support import numpy_to_vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + image = vtk.vtkImageData() + image.SetDimensions(labels.shape) + image.SetSpacing(1.0, 1.0, 1.0) + image.SetOrigin(0.0, 0.0, 0.0) + image.GetPointData().SetScalars( + numpy_to_vtk(labels.ravel(order="F"), deep=True) + ) + + smoothed = smooth_label_mask_vtk(image, sigma_mm=1.0, threshold=0.5) + values = vtk_image_to_numpy(smoothed) + + assert set(np.unique(values)) <= {0, 1, 2} + assert values[3, 3, 3] == 1 + assert values[5, 5, 5] == 2 + + def test_fit_cap_follows_each_contact_ray(): mask = np.zeros((12, 8, 8), dtype=bool) mask[4:7, 2:6, 2:6] = True @@ -157,7 +316,8 @@ def test_projected_bone_cap_intrusion_keeps_requested_total_thickness(): def test_anatomy_bone_cap_uses_axis_aligned_surface_depth(): mask = np.zeros((16, 9, 9), dtype=bool) mask[7:9, 2:6, 2:6] = True - mask[2:4, 6, 3] = True + mask[5:7, 6, 3] = True + mask[2:4, 7, 3] = True cap = generate_bone_cap_mask( mask, @@ -170,13 +330,18 @@ def test_anatomy_bone_cap_uses_axis_aligned_surface_depth(): assert not np.any(cap & mask) assert np.flatnonzero(cap[:, 3, 3]).tolist() == [9, 10] - assert np.flatnonzero(cap[:, 6, 3]).tolist() == [4, 5, 6, 7, 8, 9, 10] + assert np.flatnonzero(cap[:, 6, 3]).tolist() == [7, 8, 9, 10] + assert np.flatnonzero(cap[:, 7, 3]).tolist() == [] + for y in range(mask.shape[1]): + for z in range(mask.shape[2]): + assert np.flatnonzero(cap[:, y, z]).size <= 5 def test_anatomy_bone_cap_uses_axis_aligned_surface_depth_inferior(): mask = np.zeros((18, 9, 9), dtype=bool) mask[7:9, 2:6, 2:6] = True - mask[12:14, 6, 3] = True + mask[9:11, 6, 3] = True + mask[12:14, 7, 3] = True cap = generate_bone_cap_mask( mask, @@ -189,108 +354,18 @@ def test_anatomy_bone_cap_uses_axis_aligned_surface_depth_inferior(): assert not np.any(cap & mask) assert np.flatnonzero(cap[:, 3, 3]).tolist() == [5, 6] - assert np.flatnonzero(cap[:, 6, 3]).tolist() == [5, 6, 7, 8, 9, 10, 11] - - -def test_fixture_box_cap_keeps_full_footprint_over_irregular_surface(): - mask = np.zeros((14, 9, 9), dtype=bool) - mask[3:6, 2:7, 2:7] = True - mask[6, 4, 4] = True - mask[7, 5, 4] = True - - cap = generate_fixture_cap_mask(mask, axis="x", direction="up", thickness=3, shape="box") - - assert np.all(cap[8:11, 2:7, 2:7]) - assert cap.sum() == 3 * 5 * 5 - - -def test_fixture_cap_intrudes_across_contact_plane(): - mask = np.zeros((14, 9, 9), dtype=bool) - mask[3:6, 2:7, 2:7] = True - - cap = generate_fixture_cap_mask( - mask, - axis="x", - direction="up", - thickness=3, - shape="box", - intrusion=2, - ) - - assert np.all(cap[4:7, 2:7, 2:7]) - assert not np.any(cap[:4]) - assert not np.any(cap[7:]) - - -def test_fixture_cap_intrusion_keeps_total_thickness(): - mask = np.zeros((18, 9, 9), dtype=bool) - mask[8:10, 2:7, 2:7] = True - - half_intrusion = generate_fixture_cap_mask( - mask, - axis="x", - direction="up", - thickness=6, - shape="box", - intrusion=3, - ) - full_intrusion = generate_fixture_cap_mask( - mask, - axis="x", - direction="up", - thickness=6, - shape="box", - intrusion=6, - ) - - half_x = np.flatnonzero(half_intrusion[:, 3, 3]) - full_x = np.flatnonzero(full_intrusion[:, 3, 3]) - assert half_x.tolist() == [7, 8, 9, 10, 11, 12] - assert full_x.tolist() == [4, 5, 6, 7, 8, 9] - assert half_x.size == 6 - assert full_x.size == 6 - - -def test_fixture_contact_crop_removes_unsupported_columns(): - mask = np.zeros((14, 9, 9), dtype=bool) - mask[3:6, 2:7, 4] = True - mask[3:6, 4, 2:7] = True - mask[0, 3, 3] = True - - full = generate_fixture_cap_mask(mask, axis="x", direction="up", thickness=2, shape="box") - cropped = generate_fixture_cap_mask( - mask, - axis="x", - direction="up", - thickness=2, - shape="box", - intrusion=2, - crop_to_contact=True, - ) - - assert full[:, 3, 3].any() - assert not cropped[:, 3, 3].any() - assert cropped[:, 4, 3].any() - - -def test_fixture_round_cap_is_not_contact_cropped(): - mask = np.zeros((14, 11, 11), dtype=bool) - mask[3:6, 2:9, 5] = True - mask[3:6, 5, 2:9] = True - - fixture = generate_fixture_cap_mask(mask, axis="x", direction="up", thickness=2, shape="round") - contact = generate_bone_cap_mask(mask, axis="x", direction="up", thickness=2, shape="round") - - assert fixture[:, 3, 3].any() - assert not contact[:, 3, 3].any() - assert not np.any(contact & mask) + assert np.flatnonzero(cap[:, 6, 3]).tolist() == [5, 6, 7, 8] + assert np.flatnonzero(cap[:, 7, 3]).tolist() == [] + for y in range(mask.shape[1]): + for z in range(mask.shape[2]): + assert np.flatnonzero(cap[:, y, z]).size <= 5 -def test_vtk_roi_cap_uses_requested_output_value(): +def test_vtk_bone_cap_uses_requested_output_value(): vtk = pytest.importorskip("vtk") from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy - from ogo.fea.boundary import generate_bone_cap_vtk, label_foreground_in_bounds_vtk + from ogo.fea.boundary import generate_bone_cap_vtk labels = np.zeros((12, 8, 8), dtype=np.uint16) labels[4:7, 2:6, 2:6] = 12 @@ -301,40 +376,113 @@ def test_vtk_roi_cap_uses_requested_output_value(): image.SetOrigin((0, 0, 0)) image.GetPointData().SetScalars(vtk_arr) - roi = label_foreground_in_bounds_vtk(image, (0, 11, 2, 5, 2, 5), label_value=1) cap = generate_bone_cap_vtk( - roi, label_value=1, axis="x", direction="up", thickness=2, shape="box", output_value=5000 + image, label_value=12, axis="x", direction="up", thickness=2, shape="box", output_value=5000 ) cap_np = vtk_to_numpy(cap.GetPointData().GetScalars()).reshape(cap.GetDimensions(), order="F") assert set(np.unique(cap_np)) == {0, 5000} -def test_vtk_conversion_preserves_template_extent(): +def test_projected_material_disk_uses_fixed_thickness_and_anatomy_intrusion(): + active = np.zeros((14, 14, 14), dtype=bool) + active[4:10, 6:9, 4:10] = True + active[7, 6:8, 7] = False + active[7, 8:10, 7] = True + active[8, 6:10, 7] = False + active[8, 10:12, 7] = True + + disk = generate_projected_material_disk_mask( + active, + spacing=(1, 1, 1), + origin=(0, 0, 0), + center=(7, 1, 7), + normal=(0, 1, 0), + u_axis=(1, 0, 0), + v_axis=(0, 0, 1), + size=(8, 8), + shape="square", + thickness=6, + intrusion=3, + anatomy_constrained=True, + ) + + assert np.flatnonzero(disk[5, :, 5]).tolist() == [3, 4, 5] + assert np.flatnonzero(disk[7, :, 7]).tolist() == [3, 4, 5, 6, 7] + assert not np.any(disk[8, :, 7]) + assert not np.any(disk & active) + + +def test_projected_material_disk_vtk_preserves_geometry_and_output_value(): vtk = pytest.importorskip("vtk") - from vtk.util.numpy_support import numpy_to_vtk + from ogo.fea.boundary import generate_projected_material_disk_vtk + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy - from ogo.fea.boundary import generate_fixture_cap_vtk + material = np.zeros((10, 10, 10), dtype=np.uint16) + material[3:7, 5:7, 3:7] = 12 - labels = np.zeros((6, 5, 4), dtype=np.uint16) - labels[2:4, 1:4, 1:3] = 1 - vtk_arr = numpy_to_vtk(np.swapaxes(labels, 0, 2).ravel(order="F"), deep=True) - image = vtk.vtkImageData() - image.SetExtent(10, 13, 20, 24, 30, 35) - image.SetSpacing((0.7, 0.8, 0.9)) - image.SetOrigin((1.0, 2.0, 3.0)) - image.GetPointData().SetScalars(vtk_arr) + template = vtk.vtkImageData() + template.SetDimensions(material.shape) + template.SetOrigin(-2, 4, 8) + template.SetSpacing(1, 1, 1) + image = numpy_to_vtk_image(material, template, vtk_array_type=vtk.VTK_UNSIGNED_SHORT) - cap = generate_fixture_cap_vtk( + disk = generate_projected_material_disk_vtk( image, - label_value=1, - axis="x", - direction="up", - thickness=1, - shape="box", + center=(2, 12, 12), + normal=(0, -1, 0), + u_axis=(1, 0, 0), + v_axis=(0, 0, 1), + size=(8, 8), + shape="square", + thickness=4, + intrusion=2, + anatomy_constrained=True, + output_value=5000, + ) + + disk_data = vtk_image_to_numpy(disk) + assert disk.GetExtent() == image.GetExtent() + assert disk.GetSpacing() == image.GetSpacing() + assert disk.GetOrigin() == image.GetOrigin() + assert set(np.unique(disk_data)) == {0, 5000} + assert not np.any((disk_data != 0) & (material != 0)) + + +def test_projected_material_disk_vtk_keeps_pmma_out_of_protected_mask(): + vtk = pytest.importorskip("vtk") + from ogo.fea.boundary import generate_projected_material_disk_vtk + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + + material = np.zeros((10, 10, 10), dtype=np.uint16) + material[3:7, 5:7, 3:7] = 12 + + femur_mask = np.zeros_like(material) + femur_mask[3:7, 4:7, 3:7] = 1 + + template = vtk.vtkImageData() + template.SetDimensions(material.shape) + template.SetOrigin(-2, 4, 8) + template.SetSpacing(1, 1, 1) + material_image = numpy_to_vtk_image(material, template, vtk_array_type=vtk.VTK_UNSIGNED_SHORT) + mask_image = numpy_to_vtk_image(femur_mask, template, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) + + disk = generate_projected_material_disk_vtk( + material_image, + surface_vtk_image=mask_image, + exclusion_vtk_image=mask_image, + center=(2, 12, 12), + normal=(0, -1, 0), + u_axis=(1, 0, 0), + v_axis=(0, 0, 1), + size=(8, 8), + shape="square", + thickness=4, + intrusion=2, + anatomy_constrained=True, output_value=5000, ) - assert cap.GetExtent() == image.GetExtent() - assert cap.GetSpacing() == image.GetSpacing() - assert cap.GetOrigin() == image.GetOrigin() + disk_data = vtk_image_to_numpy(disk) + assert np.any(disk_data != 0) + assert not np.any((disk_data != 0) & (femur_mask != 0)) diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 1d3af5c..39cf52b 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -3,11 +3,37 @@ from ogo.fea import femur -def test_side_suffix_and_rotation_match_compact_outputs(): +def _vtk_image_from_array(data, *, origin=(0, 0, 0), spacing=(1, 1, 1)): + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(*origin) + image.SetSpacing(*spacing) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + return image + + +def _polydata_from_points(points): + vtk = pytest.importorskip("vtk") + + vtk_points = vtk.vtkPoints() + vertices = vtk.vtkCellArray() + for point in points: + point_id = vtk_points.InsertNextPoint(*point) + vertices.InsertNextCell(1) + vertices.InsertCellPoint(point_id) + + polydata = vtk.vtkPolyData() + polydata.SetPoints(vtk_points) + polydata.SetVerts(vertices) + return polydata + + +def test_side_suffix_matches_compact_outputs(): assert femur.side_suffix(1) == "LF" assert femur.side_suffix(2) == "RF" - assert femur.side_rotation(1) == 90 - assert femur.side_rotation(2) == -90 def test_sideways_fall_output_name_uses_compact_side_suffix(): @@ -20,6 +46,11 @@ def test_invalid_femur_side_is_rejected(): femur.side_suffix(3) +def test_hip_fixture_defaults_use_fixed_thickness_and_intrusion(): + assert femur.DEFAULT_PMMA_THICKNESS_MM == pytest.approx(10.0) + assert femur.DEFAULT_PMMA_INTRUSION_MM == pytest.approx(6.0) + + def test_tilted_side_support_vector_has_unit_length(): np = pytest.importorskip("numpy") @@ -54,6 +85,343 @@ def test_expand_xz_footprint_preserves_center(): assert expanded == pytest.approx((15, 45, 1, 3, 80, 140)) +def test_bbox_relative_fixture_bounds_scale_lateral_axes_to_model_bbox(): + bounds = (10, 50, -20, 80, 100, 200) + + fixture_bounds = femur.bbox_relative_fixture_bounds( + bounds, + center_fraction=(0.5, 1.1, 0.25), + size_fraction=(1.1, 0.5), + projection_axis="y", + ) + + assert fixture_bounds == pytest.approx((8, 52, -20, 80, 100, 150)) + + +def test_bbox_relative_fixture_bounds_can_enforce_square_footprint(): + bounds = (10, 50, -20, 80, 100, 200) + + fixture_bounds = femur.bbox_relative_fixture_bounds( + bounds, + center_fraction=(0.5, 1.1, 0.25), + size_fraction=(1.1, 0.5), + projection_axis="y", + shape="square", + ) + + assert fixture_bounds == pytest.approx((8, 52, -20, 80, 103, 147)) + + +def test_foreground_voxel_center_bounds_match_workflow_bbox_convention(): + np = pytest.importorskip("numpy") + + mask = np.zeros((8, 9, 10), dtype=np.uint8) + mask[2:6, 3:8, 1:7] = 1 + + bounds = femur.foreground_voxel_center_bounds_from_mask( + mask, + origin=(10, 20, 30), + spacing=(2, 3, 4), + ) + + assert bounds == pytest.approx((14, 20, 29, 41, 34, 54)) + + +def test_reference_grid_from_output_to_input_matrix_uses_transformed_surface_bounds(): + np = pytest.importorskip("numpy") + + points = np.asarray( + [ + [10.2, -4.0, 2.5], + [14.7, 6.1, 8.2], + [12.0, 0.5, 5.0], + ], + dtype=float, + ) + matrix = np.eye(4) + matrix[:3, 3] = [2.0, -3.0, 1.0] + + origin, size = femur.reference_grid_from_output_to_input_matrix( + points, + matrix, + spacing=(1.0, 1.0, 1.0), + margin_voxels=2, + ) + + # The matrix maps output coordinates to input coordinates, so the grid is + # built from the inverse-transformed input points. + expected = points - np.asarray([2.0, -3.0, 1.0]) + expected_lo = expected.min(axis=0) - 2.0 + expected_hi = expected.max(axis=0) + 2.0 + assert origin == pytest.approx(tuple(expected_lo)) + assert size == tuple((np.ceil(expected_hi - expected_lo).astype(int) + 1).tolist()) + + +def test_bbox_relative_fixture_direction_uses_plane_side_fraction(): + assert femur.bbox_relative_fixture_direction((0.5, 0.02, 0.5), projection_axis="y") == "down" + assert femur.bbox_relative_fixture_direction((0.5, 1.02, 0.5), projection_axis="y") == "up" + + +def test_bbox_relative_fixture_plane_uses_authored_y_plane_axes(): + bounds = (10, 50, -20, 80, 100, 200) + + low_y_plane = femur.bbox_relative_fixture_plane( + bounds, + center_fraction=(0.5, 0.02, 0.25), + size_fraction=(1.1, 0.5), + projection_axis="y", + shape="rectangle", + ) + high_y_plane = femur.bbox_relative_fixture_plane( + bounds, + center_fraction=(0.5, 1.02, 0.25), + size_fraction=(1.1, 0.5), + projection_axis="y", + shape="rectangle", + ) + + assert low_y_plane["center"] == pytest.approx((30, -18, 125)) + assert low_y_plane["normal"] == pytest.approx((0, 1, 0)) + assert low_y_plane["u_axis"] == pytest.approx((0, 0, -1)) + assert low_y_plane["v_axis"] == pytest.approx((-1, 0, 0)) + assert low_y_plane["size"] == pytest.approx((110, 20)) + assert low_y_plane["shape"] == "rectangle" + assert high_y_plane["normal"] == pytest.approx((0, -1, 0)) + assert high_y_plane["u_axis"] == pytest.approx((0, 0, 1)) + assert high_y_plane["v_axis"] == pytest.approx((-1, 0, 0)) + + +def test_bbox_relative_oriented_contact_plane_keeps_measured_normal(): + np = pytest.importorskip("numpy") + bounds = (10, 50, -20, 80, 100, 200) + + plane = femur.bbox_relative_oriented_contact_plane( + bounds, + center_fraction=(0.25, 0.5, 0.1), + size_fraction=(0.4, 0.5), + normal=(0.0, -0.5, 0.8660254038), + shape="anatomy", + ) + + assert plane["center"] == pytest.approx((20.0, 30.0, 110.0)) + assert plane["normal"] == pytest.approx((0.0, -0.5, 0.8660254038)) + assert plane["outward_normal"] == pytest.approx((0.0, 0.5, -0.8660254038)) + assert plane["v_axis"] == pytest.approx((-1.0, 0.0, 0.0)) + assert np.cross(plane["u_axis"], plane["v_axis"]) == pytest.approx(plane["normal"]) + assert plane["size"] == pytest.approx((54.641016152, 20.0)) + assert plane["shape"] == "anatomy" + + +def test_scale_reference_to_sample_principal_lengths_clips_and_scales_origin(): + reference = _polydata_from_points( + [ + (-1.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + (0.0, -2.0, 0.0), + (0.0, 2.0, 0.0), + (0.0, 0.0, -3.0), + (0.0, 0.0, 3.0), + ] + ) + sample = _polydata_from_points( + [ + (-0.5, 0.0, 0.0), + (0.5, 0.0, 0.0), + (0.0, -4.0, 0.0), + (0.0, 4.0, 0.0), + (0.0, 0.0, -6.0), + (0.0, 0.0, 6.0), + ] + ) + + scaled, metadata = femur.scale_reference_to_sample_principal_lengths( + reference, + sample, + min_scale=(0.8, 0.8, 0.75), + max_scale=(1.2, 1.2, 1.3), + ) + + assert metadata["scale_factors"] == pytest.approx([0.8, 1.2, 1.3]) + assert scaled.GetBounds() == pytest.approx((-0.8, 0.8, -2.4, 2.4, -3.9, 3.9)) + + +def test_surface_points_from_vtk_mask_uses_voxel_surface_and_stride_sampling(): + np = pytest.importorskip("numpy") + + mask = np.ones((3, 3, 3), dtype=np.uint8) + image = _vtk_image_from_array(mask, origin=(10, 20, 30), spacing=(2, 3, 4)) + + full = femur.surface_points_from_vtk_mask(image, max_points=None) + sampled = femur.surface_points_from_vtk_mask( + image, + max_points=5, + sample_mode="stride", + ) + + assert full.shape == (26, 3) + assert not np.any(np.all(np.isclose(full, (12, 23, 34)), axis=1)) + assert sampled == pytest.approx(full[[0, 6, 12, 18, 24]]) + + +def test_scale_reference_point_cloud_to_sample_matches_voxel_surface_lengths(): + np = pytest.importorskip("numpy") + + reference = _polydata_from_points( + [ + (-1.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + (0.0, -2.0, 0.0), + (0.0, 2.0, 0.0), + (0.0, 0.0, -3.0), + (0.0, 0.0, 3.0), + ] + ) + sample_points = np.asarray( + [ + (-0.5, 0.0, 0.0), + (0.5, 0.0, 0.0), + (0.0, -4.0, 0.0), + (0.0, 4.0, 0.0), + (0.0, 0.0, -6.0), + (0.0, 0.0, 6.0), + ] + ) + + scaled, metadata = femur.scale_reference_point_cloud_to_sample( + reference, + sample_points, + min_scale=(0.8, 0.8, 0.75), + max_scale=(1.2, 1.2, 1.3), + ) + + assert metadata["source"] == "voxel_surface_point_cloud" + assert metadata["scale_factors"] == pytest.approx([0.8, 1.2, 1.3]) + assert scaled.GetBounds() == pytest.approx((-0.8, 0.8, -2.4, 2.4, -3.9, 3.9)) + + +def test_bbox_ratio_crop_matches_recipe_order_and_tracks_cut_face(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + density = np.zeros((24, 20, 24), dtype=np.float32) + mask = np.zeros_like(density, dtype=np.uint8) + density[2:22, 4:14, 2:18] = 700.0 + mask[2:22, 4:14, 2:18] = 2 + + cropped, crop_face, meta = femur.crop_vtk_images_to_bbox_ratio( + [_vtk_image_from_array(density)], + _vtk_image_from_array(mask), + bbox_ratio=(1.0, 1.2, None), + bbox_crop_from=femur.DEFAULT_FEMUR_BBOX_CROP_FROM, + labels={2}, + ) + + cropped_density = vtk_image_to_numpy(cropped[0]) + crop_face_data = vtk_image_to_numpy(crop_face) + + assert cropped_density.shape == (20, 10, 12) + assert cropped[0].GetOrigin() == pytest.approx((2, 4, 6)) + assert meta["ratio_xyz"] == (None, 1.0, 1.2) + assert meta["crop_from_xyz"] == (None, None, "min") + assert meta["crop_slices_xyz"] == ((2, 22), (4, 14), (6, 18)) + assert crop_face_data.sum() == 20 * 10 + assert np.all(crop_face_data[:, :, 0] == 1) + assert not np.any(crop_face_data[:, :, 1:]) + + +def test_bbox_ratio_crop_uses_shortest_unit_axis_as_reference(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + density = np.zeros((24, 20, 24), dtype=np.float32) + mask = np.zeros_like(density, dtype=np.uint8) + density[2:22, 4:14, 2:18] = 700.0 + mask[2:22, 4:14, 2:18] = 2 + + cropped, _crop_face, meta = femur.crop_vtk_images_to_bbox_ratio( + [_vtk_image_from_array(density)], + _vtk_image_from_array(mask), + bbox_ratio=(1.0, 1.0, 1.0), + labels={2}, + ) + + assert vtk_image_to_numpy(cropped[0]).shape == (10, 10, 10) + assert cropped[0].GetOrigin() == pytest.approx((7, 4, 5)) + assert meta["reference_axis"] == "y" + assert meta["reference_length_mm"] == pytest.approx(10.0) + + +def test_crop_face_support_vector_points_away_from_retained_bone(): + np = pytest.importorskip("numpy") + + mask = np.zeros((10, 10, 10), dtype=np.uint8) + crop_face = np.zeros_like(mask) + for x in range(2, 8): + for y in range(2, 8): + z = 2 + (x - 2) // 3 + crop_face[x, y, z] = 1 + mask[x, y, z:9] = 1 + + vector = femur.crop_face_support_vector( + _vtk_image_from_array(crop_face), + _vtk_image_from_array(mask), + ) + + assert np.linalg.norm(vector) == pytest.approx(1.0) + assert vector[2] < -0.6 + + +def test_crop_face_contact_plane_uses_transformed_crop_face_angle(): + np = pytest.importorskip("numpy") + + mask = np.zeros((12, 12, 12), dtype=np.uint8) + crop_face = np.zeros_like(mask) + for x in range(3, 9): + for y in range(3, 9): + z = 2 + (x - 3) // 3 + crop_face[x, y, z] = 1 + mask[x, y, z:10] = 1 + + plane = femur.crop_face_contact_plane( + _vtk_image_from_array(crop_face), + _vtk_image_from_array(mask), + ) + + assert np.linalg.norm(plane["normal"]) == pytest.approx(1.0) + assert plane["normal"][2] > 0.6 + assert plane["normal"][0] < -0.2 + assert plane["outward_normal"][2] < -0.6 + assert plane["size"][0] > 0 + assert plane["size"][1] > 0 + + +def test_projected_crop_face_surface_mask_keeps_first_contact_layer(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + active = np.zeros((10, 10, 10), dtype=np.uint8) + active[3:7, 3:7, 4:8] = 1 + plane = { + "center": (4.5, 4.5, 3.0), + "normal": (0.0, 0.0, 1.0), + "u_axis": (1.0, 0.0, 0.0), + "v_axis": (0.0, 1.0, 0.0), + "size": (4.0, 4.0), + "shape": "anatomy", + } + + surface = femur.projected_crop_face_surface_vtk( + _vtk_image_from_array(active), + plane, + intrusion=6.0, + ) + surface_data = vtk_image_to_numpy(surface) != 0 + + assert surface_data.sum() == 16 + assert np.all(surface_data[3:7, 3:7, 4]) + assert not np.any(surface_data[:, :, 5:]) + + def test_flat_femur_shaft_crop_rejects_short_distal_coverage(): np = pytest.importorskip("numpy") vtk = pytest.importorskip("vtk") @@ -176,6 +544,8 @@ def test_femur_input_padding_adds_only_missing_foreground_margin(): assert meta["lower"] == (2, 0, 2) assert meta["upper"] == (0, 0, 0) + assert padded[0].GetOrigin() == pytest.approx((-2, 0, -2)) + assert padded[0].GetExtent() == (0, 6, 0, 5, 0, 8) assert tuple(coords.min(axis=1)) == (2, 2, 2) assert out.shape[0] - 1 - coords.max(axis=1)[0] >= 2 assert out.shape[1] - 1 - coords.max(axis=1)[1] >= 1 diff --git a/tests/fea/test_model.py b/tests/fea/test_model.py new file mode 100644 index 0000000..06b75c5 --- /dev/null +++ b/tests/fea/test_model.py @@ -0,0 +1,137 @@ +import pytest + + +def _vtk_image_from_array(data, *, origin=(0, 0, 0), spacing=(1, 1, 1)): + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(*origin) + image.SetSpacing(*spacing) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + return image + + +class _GridPointModel: + def __init__(self, shape, *, origin=(0, 0, 0), spacing=(1, 1, 1)): + self.points = [] + for x in range(shape[0] + 1): + for y in range(shape[1] + 1): + for z in range(shape[2] + 1): + self.points.append( + ( + origin[0] + (x - 0.5) * spacing[0], + origin[1] + (y - 0.5) * spacing[1], + origin[2] + (z - 0.5) * spacing[2], + ) + ) + + def GetNumberOfPoints(self): + return len(self.points) + + def GetPoint(self, point_id): + return self.points[point_id] + + +def test_interface_node_ids_from_voxel_mask_uses_material_interface_only(): + np = pytest.importorskip("numpy") + + from ogo.fea.model import interface_node_ids_from_voxel_mask + + material = np.ones((2, 2, 2), dtype=np.uint8) + selected = np.zeros_like(material) + selected[:, :, 0] = 1 + model = _GridPointModel(material.shape) + + node_ids = interface_node_ids_from_voxel_mask( + model, + _vtk_image_from_array(selected), + _vtk_image_from_array(material), + name="shaft", + ) + points = { + model.GetPoint(node_ids.GetValue(index)) + for index in range(node_ids.GetNumberOfTuples()) + } + + assert node_ids.GetName() == "shaft" + assert node_ids.GetNumberOfTuples() == 9 + assert {point[2] for point in points} == {0.5} + + +def test_interface_node_ids_from_voxel_mask_can_follow_direction(): + np = pytest.importorskip("numpy") + + from ogo.fea.model import interface_node_ids_from_voxel_mask + + material = np.ones((2, 2, 2), dtype=np.uint8) + selected = np.zeros_like(material) + selected[0, 0, 0] = 1 + model = _GridPointModel(material.shape) + + node_ids = interface_node_ids_from_voxel_mask( + model, + _vtk_image_from_array(selected), + _vtk_image_from_array(material), + name="shaft", + direction=(0.0, 0.0, 1.0), + ) + points = { + model.GetPoint(node_ids.GetValue(index)) + for index in range(node_ids.GetNumberOfTuples()) + } + + assert node_ids.GetNumberOfTuples() == 4 + assert {point[2] for point in points} == {0.5} + + +def test_directional_face_node_ids_from_voxel_mask_uses_outer_face(): + np = pytest.importorskip("numpy") + + from ogo.fea.model import directional_face_node_ids_from_voxel_mask + + selected = np.ones((2, 2, 2), dtype=np.uint8) + model = _GridPointModel(selected.shape) + + node_ids = directional_face_node_ids_from_voxel_mask( + model, + _vtk_image_from_array(selected), + direction=(0.0, 1.0, 0.0), + name="cap", + ) + points = { + model.GetPoint(node_ids.GetValue(index)) + for index in range(node_ids.GetNumberOfTuples()) + } + + assert node_ids.GetName() == "cap" + assert node_ids.GetNumberOfTuples() == 9 + assert {point[1] for point in points} == {1.5} + + +def test_directional_face_node_ids_from_label_image_uses_requested_label_only(): + np = pytest.importorskip("numpy") + + from ogo.fea.model import directional_face_node_ids_from_label_image + + labels = np.zeros((2, 2, 3), dtype=np.uint8) + labels[:, :, 1] = 4 + labels[:, :, 2] = 3 + model = _GridPointModel(labels.shape) + + node_ids = directional_face_node_ids_from_label_image( + model, + _vtk_image_from_array(labels), + label_value=4, + direction=(0.0, 0.0, 1.0), + name="top_cap", + ) + points = { + model.GetPoint(node_ids.GetValue(index)) + for index in range(node_ids.GetNumberOfTuples()) + } + + assert node_ids.GetName() == "top_cap" + assert node_ids.GetNumberOfTuples() == 9 + assert {point[2] for point in points} == {1.5} diff --git a/tests/fea/test_sideways_fall.py b/tests/fea/test_sideways_fall.py new file mode 100644 index 0000000..95ae34b --- /dev/null +++ b/tests/fea/test_sideways_fall.py @@ -0,0 +1,46 @@ +import ast +import inspect +import textwrap + +import pytest + + +def test_sideways_fall_icp_path_does_not_pre_rotate_input_images(): + pytest.importorskip("vtk") + pytest.importorskip("vtkbone") + + from ogo.fea import sideways_fall + + source = textwrap.dedent(inspect.getsource(sideways_fall.sidewaysFallFe)) + tree = ast.parse(source) + pre_rotation_calls = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.Attribute) and node.attr == "preRotateImage" + ] + + assert pre_rotation_calls == [] + + +def test_sideways_fall_icp_path_uses_voxel_surface_points(): + pytest.importorskip("vtk") + pytest.importorskip("vtkbone") + + from ogo.fea import sideways_fall + + source = textwrap.dedent(inspect.getsource(sideways_fall.sidewaysFallFe)) + tree = ast.parse(source) + called_names = { + node.func.id + for node in ast.walk(tree) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) + } + marching_cube_calls = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.Attribute) and node.attr == "marchingCubes" + ] + + assert "surface_points_from_vtk_mask" in called_names + assert "scale_reference_point_cloud_to_sample" in called_names + assert marching_cube_calls == [] diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py index c7128fd..085f169 100644 --- a/tests/fea/test_spinefe_benchmark.py +++ b/tests/fea/test_spinefe_benchmark.py @@ -1,7 +1,10 @@ import csv +import ast +import inspect import json from pathlib import Path import shutil +import textwrap import pytest @@ -90,6 +93,131 @@ def vtk_image(array): assert out[2, 1, 1] == 5000 +def test_spine_workflow_uses_projected_contact_disks(): + pytest.importorskip("vtk") + + from ogo.fea import spine_compression + + source = textwrap.dedent(inspect.getsource(spine_compression.process_vertebra)) + tree = ast.parse(source) + called_names = { + node.func.id + for node in ast.walk(tree) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) + } + + assert "generate_projected_material_disk_vtk" in called_names + assert "generate_bone_cap_vtk" not in called_names + + +def test_spine_registration_scaling_uses_voxel_surface_points(): + pytest.importorskip("vtk") + + from ogo.fea import spine_compression + + source = textwrap.dedent(inspect.getsource(spine_compression.get_icp_with_scaling)) + + assert "surface_points_from_vtk_mask" in source + assert "point_cloud_axis_lengths" in source + assert "perform_marching_cubes(body)" not in source + + +def test_spine_workflow_resamples_to_explicit_reference_grid(): + pytest.importorskip("vtk") + + from ogo.fea import spine_compression + + source = textwrap.dedent(inspect.getsource(spine_compression.process_vertebra)) + + assert "output_grid_for_point_transform" in source + assert "resample_vtk_image_with_point_transform" in source + assert "transform_resample(" not in source + + +def test_spine_contact_planes_use_generated_model_bbox(): + pytest.importorskip("vtk") + + from ogo.fea import spine_compression + + source = textwrap.dedent(inspect.getsource(spine_compression.process_vertebra)) + + assert "bounds_with_reference_extent" not in source + assert "body_bounds = transformed_body_bounds" in source + + +def test_spine_projected_disks_pad_to_contact_plane_bounds(): + pytest.importorskip("vtk") + + from ogo.fea import spine_compression + + source = textwrap.dedent(inspect.getsource(spine_compression.process_vertebra)) + + assert "projected_material_disk_required_bounds" in source + assert "pad_vtk_images_to_physical_bounds" in source + + +def test_spine_boundary_conditions_fix_inferior_cap_in_all_directions(): + vtkbone = pytest.importorskip("vtkbone") + + from ogo.fea.model import apply_spine_boundary_conditions + + class RecordingModel: + def __init__(self): + self.calls = [] + + def ApplyBoundaryCondition(self, *args): + self.calls.append(args) + + model = RecordingModel() + apply_spine_boundary_conditions(model, fe_displacement=-0.2924) + + assert model.calls == [ + ("body_top", vtkbone.vtkboneConstraint.SENSE_Z, -0.2924, "top_displacement"), + ("body_bottom", vtkbone.vtkboneConstraint.SENSE_X, 0, "bottom_fixed_x"), + ("body_bottom", vtkbone.vtkboneConstraint.SENSE_Y, 0, "bottom_fixed_y"), + ("body_bottom", vtkbone.vtkboneConstraint.SENSE_Z, 0, "bottom_fixed_z"), + ] + + +def test_spine_crop_to_bounding_box_updates_origin_and_extent(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + from ogo.fea.spine_compression import crop_to_bounding_box + + values = np.zeros((6, 7, 8), dtype=np.uint8) + values[2:5, 3:6, 4:7] = 1 + image = vtk.vtkImageData() + image.SetDimensions(values.shape) + image.SetOrigin(10.0, 20.0, 30.0) + image.SetSpacing(2.0, 3.0, 4.0) + image.GetPointData().SetScalars(numpy_to_vtk(values.ravel(order="F"), deep=True)) + + cropped, bounds = crop_to_bounding_box(image) + output = cropped.GetOutput() + + assert bounds == [2, 4, 3, 5, 4, 6] + assert output.GetExtent() == (0, 2, 0, 2, 0, 2) + assert output.GetOrigin() == pytest.approx((14.0, 29.0, 46.0)) + + +def test_spine_compression_read_preserves_reader_get_output_contract(tmp_path): + np = pytest.importorskip("numpy") + sitk = pytest.importorskip("SimpleITK") + pytest.importorskip("vtk") + + from ogo.fea.spine_compression import read + + image_path = tmp_path / "image.nii.gz" + image = sitk.GetImageFromArray(np.ones((2, 3, 4), dtype=np.uint8)) + sitk.WriteImage(image, str(image_path)) + + reader = read(str(image_path)) + + assert reader.GetOutput().GetDimensions() == (4, 3, 2) + + def _sample_paths(): benchmark_dir = find_spinefe_benchmark_dir(Path(__file__).resolve().parents[2]) if benchmark_dir is None: diff --git a/tests/util/test_Helper.py b/tests/util/test_Helper.py index 08bf619..7791259 100644 --- a/tests/util/test_Helper.py +++ b/tests/util/test_Helper.py @@ -1,5 +1,8 @@ import unittest +import pytest + +import ogo.util.Helper as helper from ogo.util.Helper import get_phantom @@ -20,3 +23,200 @@ def test_placeholder(self) -> None: # There are many more functions in util.Helper that I did not write tests for. # It's not clear that these functions are actually used, so I didn't bother. + + +def test_prepare_finite_element_image_preserves_physical_metadata(): + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + image = vtk.vtkImageData() + image.SetDimensions(3, 4, 5) + image.SetOrigin(11.0, -22.0, 33.0) + image.SetSpacing(0.7, 0.8, 0.9) + values = numpy_to_vtk([1] * (3 * 4 * 5), deep=True) + image.GetPointData().SetScalars(values) + + assert hasattr(helper, "prepareFiniteElementImage") + + prepared = helper.prepareFiniteElementImage(image) + + assert prepared.GetOrigin() == pytest.approx((11.0, -22.0, 33.0)) + assert prepared.GetSpacing() == pytest.approx((0.7, 0.8, 0.9)) + assert prepared.GetDimensions() == (3, 4, 5) + + +def test_prepare_finite_element_image_mesh_uses_physical_coordinates(): + vtk = pytest.importorskip("vtk") + pytest.importorskip("vtkbone") + from vtk.util.numpy_support import numpy_to_vtk + + image = vtk.vtkImageData() + image.SetDimensions(2, 2, 2) + image.SetOrigin(11.0, -22.0, 33.0) + image.SetSpacing(1.0, 1.0, 1.0) + image.GetPointData().SetScalars(numpy_to_vtk([1] * 8, deep=True)) + + mesh = helper.Image2Mesh(helper.prepareFiniteElementImage(image)) + + assert mesh.GetBounds() == pytest.approx((10.5, 12.5, -22.5, -20.5, 32.5, 34.5)) + + +def test_sideways_fall_combiner_preserves_existing_material_voxels(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + + pmma_mat_id = 5000 + material = np.zeros((4, 4, 4), dtype=np.int16) + material[1, 1, 1] = 1 + material[2, 2, 2] = 77 + + femoral_head_cap = np.zeros_like(material) + femoral_head_cap[1, 1, 1] = pmma_mat_id + femoral_head_cap[0, 1, 1] = pmma_mat_id + + greater_trochanter_cap = np.zeros_like(material) + greater_trochanter_cap[2, 2, 2] = pmma_mat_id + greater_trochanter_cap[3, 2, 2] = pmma_mat_id + + def vtk_image(array): + image = vtk.vtkImageData() + image.SetDimensions(array.shape) + image.SetOrigin(0.0, 0.0, 0.0) + image.SetSpacing(1.0, 1.0, 1.0) + image.GetPointData().SetScalars(numpy_to_vtk(array.ravel(order="F"), deep=True)) + return image + + combined = helper.combineImageData_SF( + vtk_image(material), + vtk_image(femoral_head_cap), + vtk_image(greater_trochanter_cap), + pmma_mat_id, + ) + + out = vtk_to_numpy(combined.GetPointData().GetScalars()).reshape(material.shape, order="F") + assert out[1, 1, 1] == 1 + assert out[2, 2, 2] == 77 + assert out[0, 1, 1] == pmma_mat_id + assert out[3, 2, 2] == pmma_mat_id + + +def test_iterative_closest_point_uses_current_fea_defaults(monkeypatch): + calls = {} + + class FakeLandmarkTransform: + def SetModeToRigidBody(self): + calls["rigid_body"] = True + + class FakeIcp: + def SetSource(self, source): + calls["source"] = source + + def SetTarget(self, target): + calls["target"] = target + + def StartByMatchingCentroidsOn(self): + calls["centroid_start"] = True + + def GetLandmarkTransform(self): + return FakeLandmarkTransform() + + def SetMeanDistanceModeToAbsoluteValue(self): + calls["mean_distance_mode"] = "absolute" + + def SetMeanDistanceModeToRMS(self): + calls["mean_distance_mode"] = "rms" + + def SetMaximumMeanDistance(self, value): + calls["maximum_mean_distance"] = value + + def CheckMeanDistanceOn(self): + calls["check_mean_distance"] = True + + def SetMaximumNumberOfLandmarks(self, value): + calls["maximum_landmarks"] = value + + def SetMaximumNumberOfIterations(self, value): + calls["maximum_iterations"] = value + + def Update(self): + calls["updated"] = True + + def GetMatrix(self): + return "matrix" + + monkeypatch.setattr(helper.vtk, "vtkIterativeClosestPointTransform", FakeIcp) + + matrix = helper.iterativeClosestPoint("source", "target") + + assert matrix == "matrix" + assert calls["source"] == "source" + assert calls["target"] == "target" + assert calls["centroid_start"] is True + assert calls["rigid_body"] is True + assert calls["mean_distance_mode"] == "absolute" + assert calls["maximum_landmarks"] == 8000 + assert calls["maximum_iterations"] == 50 + + +def test_pre_rotate_image_zero_degrees_preserves_image_orientation(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + + image_array = np.zeros((5, 5, 5), dtype=np.float32) + mask_array = np.zeros((5, 5, 5), dtype=np.uint8) + image_array[1, 2, 3] = 99.0 + mask_array[1, 2, 3] = 1 + + template = vtk.vtkImageData() + template.SetDimensions(image_array.shape) + template.SetOrigin(10.0, -20.0, 30.0) + template.SetSpacing(1.0, 1.0, 1.0) + image = numpy_to_vtk_image(image_array, template, vtk_array_type=vtk.VTK_FLOAT) + mask = numpy_to_vtk_image(mask_array, template, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) + + image_rot, mask_rot = helper.preRotateImage(image, mask, 0) + + assert vtk_image_to_numpy(image_rot) == pytest.approx(image_array) + assert np.array_equal(vtk_image_to_numpy(mask_rot), mask_array) + + +def test_read_nii_returns_ras_physical_vtk_image(tmp_path): + np = pytest.importorskip("numpy") + sitk = pytest.importorskip("SimpleITK") + + array_zyx = np.arange(2 * 3 * 4, dtype=np.float32).reshape((2, 3, 4)) + image = sitk.GetImageFromArray(array_zyx) + image.SetSpacing((0.5, 0.75, 1.25)) + image.SetOrigin((10.0, 20.0, 30.0)) + image.SetDirection((1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0)) + path = tmp_path / "image.nii.gz" + sitk.WriteImage(image, str(path)) + + expected = sitk.DICOMOrient(sitk.ReadImage(str(path)), "RAS") + vtk_image = helper.readNii(str(path)) + + assert vtk_image.GetDimensions() == expected.GetSize() + assert vtk_image.GetSpacing() == pytest.approx(expected.GetSpacing()) + assert vtk_image.GetOrigin() == pytest.approx( + (-expected.GetOrigin()[0], -expected.GetOrigin()[1], expected.GetOrigin()[2]) + ) + + +def test_read_poly_data_returns_ras_reference_points(tmp_path): + vtk = pytest.importorskip("vtk") + + points = vtk.vtkPoints() + points.InsertNextPoint(1.0, 2.0, 3.0) + polydata = vtk.vtkPolyData() + polydata.SetPoints(points) + path = tmp_path / "reference.vtk" + writer = vtk.vtkPolyDataWriter() + writer.SetFileName(str(path)) + writer.SetInputData(polydata) + writer.Update() + + reference = helper.readPolyData(str(path)) + + assert reference.GetPoint(0) == pytest.approx((-1.0, -2.0, 3.0)) From 10928e199d4e1ea6f919d999264576ec02312d52 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 14 Jul 2026 00:10:36 -0600 Subject: [PATCH 12/67] Harden FE workflow execution --- ogo/cli/GenerateFEM.py | 9 ++++++++ ogo/fea/femur.py | 3 +-- ogo/fea/spine_compression.py | 10 +++++++-- pytest.ini | 4 ++++ tests/cli/test_GenerateFEM.py | 32 +++++++++++++++++++++++++++-- tests/fea/test_spinefe_benchmark.py | 14 +++++++++++++ 6 files changed, 66 insertions(+), 6 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 6214a95..2c65a93 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -180,6 +180,12 @@ def expected_femur_model_path(calibrated_image: Path, output_path: Optional[Path return output_dir / "{}_{}.n88model".format(remove_extension(calibrated_image), stem) +def ensure_output_directory(output_path: Optional[Path]) -> None: + """Create an explicit model output directory before generation starts.""" + if output_path is not None: + output_path.mkdir(parents=True, exist_ok=True) + + def option_value(argv: Sequence[str], option: str, default: Optional[str] = None) -> Optional[str]: """Return the final value for an option from an argv-style list.""" value = default @@ -993,6 +999,9 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if args.model_type is None: parser.error("model type is required: choose spine or hip") + if not args.dry_run: + ensure_output_directory(args.output_path) + if args.model_type == "spine": spine_extra_args = spine_preset_args(args.preset) + list(extra_args) + [ "--quality_control", diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index e150adc..5cf73b8 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -1402,8 +1402,7 @@ def detect_lesser_trochanter_cut_z( if not np.any(proximal_mask): raise ValueError("Cannot identify greater trochanter from femur profile.") proximal_indices = np.where(proximal_mask)[0] - greater_index = int(proximal_indices[np.argmax(y_max[proximal_indices])]) - greater_z = float(z[greater_index]) + greater_index, greater_z = _peak_center_z(z, y_max, proximal_indices) distal_mask = ( (z <= greater_z - float(min_distal_to_greater_mm)) diff --git a/ogo/fea/spine_compression.py b/ogo/fea/spine_compression.py index fc0fdde..6ee40b3 100644 --- a/ogo/fea/spine_compression.py +++ b/ogo/fea/spine_compression.py @@ -722,9 +722,15 @@ def check_values(features, checks): return results def parse_filename(filepath): - base_name = os.path.basename(filepath) + base_name = os.path.splitext(os.path.basename(filepath))[0] parts = base_name.split('_') - return {'ID': parts[0], 'TREATMENT': parts[1], 'LOCATION': parts[2], 'NUMBER': parts[4], 'filename':'_'.join(parts[:-3])} + return { + 'ID': parts[0] if len(parts) > 0 else '', + 'TREATMENT': parts[1] if len(parts) > 1 else '', + 'LOCATION': parts[2] if len(parts) > 2 else '', + 'NUMBER': parts[4] if len(parts) > 4 else '', + 'filename': '_'.join(parts[:-3]) if len(parts) > 3 else base_name, + } def visualize_slice(image, filepath): # Get the dimensions of the vtkImageData object diff --git a/pytest.ini b/pytest.ini index 9e08fb8..9ba0772 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,7 @@ [pytest] +testpaths = + tests +norecursedirs = + archive markers = integration: tests that require external sample data or solver/imaging runtimes diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index d149ecc..6db4138 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -58,7 +58,8 @@ def solve_calls(monkeypatch): return calls -def test_spine_runs_each_requested_vertebra(monkeypatch, solve_calls): +def test_spine_runs_each_requested_vertebra(monkeypatch, tmp_path, solve_calls): + monkeypatch.chdir(tmp_path) calls = [] monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) @@ -118,7 +119,8 @@ def test_spine_runs_each_requested_vertebra(monkeypatch, solve_calls): ] -def test_hip_defaults_to_both_sides(monkeypatch, solve_calls): +def test_hip_defaults_to_both_sides(monkeypatch, tmp_path, solve_calls): + monkeypatch.chdir(tmp_path) calls = [] monkeypatch.setattr(GenerateFEM, "run_femur_command", lambda argv: calls.append(list(argv))) @@ -134,6 +136,32 @@ def test_hip_defaults_to_both_sides(monkeypatch, solve_calls): ] +def test_output_path_is_created_before_generator_runs(monkeypatch, tmp_path): + output_path = tmp_path / "new" / "models" + calls = [] + + def record_run(argv): + assert output_path.is_dir() + calls.append(list(argv)) + + monkeypatch.setattr(GenerateFEM, "run_spine_command", record_run) + + GenerateFEM.main( + [ + "spine", + "density.nii.gz", + "spine_mask.nii.gz", + "--output_path", + str(output_path), + "--vertebra", + "L1:2:1", + "--no-solve", + ] + ) + + assert len(calls) == 1 + + def test_hip_can_run_one_side(monkeypatch, solve_calls): calls = [] monkeypatch.setattr(GenerateFEM, "run_femur_command", lambda argv: calls.append(list(argv))) diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py index 085f169..5d09722 100644 --- a/tests/fea/test_spinefe_benchmark.py +++ b/tests/fea/test_spinefe_benchmark.py @@ -46,6 +46,20 @@ def GetPoint(self, point_id): assert [filtered.GetValue(i) for i in range(filtered.GetNumberOfTuples())] == [0, 1, 2, 3] +def test_spine_qc_filename_parser_handles_compact_generated_names(): + from ogo.fea.spine_compression import parse_filename + + metadata = parse_filename("/tmp/density_L4_BCcheck.csv") + + assert metadata == { + "ID": "density", + "TREATMENT": "L4", + "LOCATION": "BCcheck", + "NUMBER": "", + "filename": "density_L4_BCcheck", + } + + def _node_set_count(model, name): node_set = model.GetNodeSet(name) if node_set is None: From f6b7650792b15f5ed8c3e6a35610e0b806f7b641 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 14 Jul 2026 07:20:07 -0600 Subject: [PATCH 13/67] Harden FE helper compatibility --- ogo/fea/boundary.py | 18 ++++++++++------ ogo/fea/femur.py | 30 ++++++++++++++++++-------- ogo/fea/model.py | 4 ++-- tests/fea/test_python_compatibility.py | 18 ++++++++++++++++ 4 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 tests/fea/test_python_compatibility.py diff --git a/ogo/fea/boundary.py b/ogo/fea/boundary.py index 179fd06..0d33520 100644 --- a/ogo/fea/boundary.py +++ b/ogo/fea/boundary.py @@ -652,8 +652,11 @@ def pad_vtk_images_to_physical_bounds(vtk_images, *, desired_bounds, constants=N int(extent[4] - lower[2]), int(extent[5] + upper[2]), ) + if len(images) != len(constants): + raise ValueError("images and constants must contain the same number of items.") + padded = [] - for image, constant in zip(images, constants, strict=True): + for image, constant in zip(images, constants): pad = vtk.vtkImageConstantPad() pad.SetInputData(image) pad.SetOutputWholeExtent(output_extent) @@ -815,7 +818,11 @@ def _surface_distance_by_projected_bucket( import numpy as np best = {} - for voxel, dist, uu, vv in zip(indices, distance, u, v, strict=True): + lengths = {len(indices), len(distance), len(u), len(v)} + if len(lengths) != 1: + raise ValueError("indices, distance, u, and v must have the same length.") + + for voxel, dist, uu, vv in zip(indices, distance, u, v): if float(dist) < 0.0: continue key = _projected_bucket_key(float(uu), float(vv), spacing=spacing) @@ -953,10 +960,9 @@ def generate_projected_material_disk_mask( empty = ~material[tuple(full_indices.T)] if anatomy_constrained: - keys = [ - _projected_bucket_key(float(uu), float(vv), spacing=spacing) - for uu, vv in zip(u, v, strict=True) - ] + if len(u) != len(v): + raise ValueError("u and v must have the same length.") + keys = [_projected_bucket_key(float(uu), float(vv), spacing=spacing) for uu, vv in zip(u, v)] local_surface = np.asarray( [distance_by_key.get(key, np.nan) for key in keys], dtype=float, diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 5cf73b8..3658242 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -104,9 +104,9 @@ def foreground_voxel_center_bounds_from_mask(mask, *, origin, spacing): """Return x/y/z physical bounds of nonzero voxel centers. Bbox-relative workflow fixtures are authored against the occupied voxel - centers, matching the image-space convention used by interactive workflow - replay. FE mesh/node bounds are wider by the element faces and should not - be used to resolve those recipe planes. + centers, matching the image-space convention used by the FE recipe + parameters. FE mesh/node bounds are wider by the element faces and should + not be used to resolve those contact planes. """ import numpy as np @@ -631,7 +631,7 @@ def _vtk_image_from_array(array, template_vtk_image, *, origin, vtk_array_type=N def resample_vtk_image_like_workflow(vtk_image, target_spacing_mm, *, interpolation="nearest"): - """Resample a VTK image with the same output-size rule as workflow replay.""" + """Resample a VTK image using the FE reference-grid output-size rule.""" import numpy as np import SimpleITK as sitk import vtk @@ -1053,12 +1053,24 @@ def projected_crop_face_surface_vtk(material_vtk, plane, *, intrusion=0.0, outpu return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) best = {} + candidate_indices = indices[candidates] + candidate_distances = distances[candidates] + candidate_u_values = u_values[candidates] + candidate_v_values = v_values[candidates] + candidate_lengths = { + len(candidate_indices), + len(candidate_distances), + len(candidate_u_values), + len(candidate_v_values), + } + if len(candidate_lengths) != 1: + raise ValueError("candidate indices, distances, and plane coordinates must have the same length.") + for voxel, distance, u_value, v_value in zip( - indices[candidates], - distances[candidates], - u_values[candidates], - v_values[candidates], - strict=True, + candidate_indices, + candidate_distances, + candidate_u_values, + candidate_v_values, ): if float(distance) < -tolerance: continue diff --git a/ogo/fea/model.py b/ogo/fea/model.py index de08786..713ab86 100644 --- a/ogo/fea/model.py +++ b/ogo/fea/model.py @@ -230,7 +230,7 @@ def key(point): lateral_axes = [idx for idx in range(3) if idx != axis] for offsets in itertools.product((-0.5, 0.5), repeat=2): point = face_center.copy() - for lateral_axis, offset_value in zip(lateral_axes, offsets, strict=True): + for lateral_axis, offset_value in zip(lateral_axes, offsets): point[lateral_axis] += offset_value * spacing[lateral_axis] point_id = model_point_ids.get(key(point)) if point_id is not None: @@ -305,7 +305,7 @@ def key(point): lateral_axes = [idx for idx in range(3) if idx != axis] for offsets in itertools.product((-0.5, 0.5), repeat=2): point = face_center.copy() - for lateral_axis, offset_value in zip(lateral_axes, offsets, strict=True): + for lateral_axis, offset_value in zip(lateral_axes, offsets): point[lateral_axis] += offset_value * spacing[lateral_axis] point_id = model_point_ids.get(key(point)) if point_id is not None: diff --git a/tests/fea/test_python_compatibility.py b/tests/fea/test_python_compatibility.py new file mode 100644 index 0000000..a9d34a7 --- /dev/null +++ b/tests/fea/test_python_compatibility.py @@ -0,0 +1,18 @@ +import ast +from pathlib import Path + + +def test_fe_code_does_not_use_python_310_zip_strict_keyword(): + """The project CI still runs FE tests on Python 3.7.""" + repo_root = Path(__file__).resolve().parents[2] + for path in repo_root.joinpath("ogo", "fea").glob("*.py"): + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + for node in ast.walk(tree): + if not isinstance(node, ast.Call): + continue + if not isinstance(node.func, ast.Name) or node.func.id != "zip": + continue + assert all(keyword.arg != "strict" for keyword in node.keywords), ( + f"{path.relative_to(repo_root)} uses zip(..., strict=...), " + "which is not available on Python 3.7." + ) From f0aa63865a1578ac2a748b41dc2106e1f65fc4f7 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 14 Jul 2026 08:35:31 -0600 Subject: [PATCH 14/67] Lock maintained FE workflow defaults --- .../fixtures/fe_workflow_defaults_locked.json | 120 ++++++++++++++++++ tests/fea/test_workflow_locked_defaults.py | 100 +++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 tests/fea/fixtures/fe_workflow_defaults_locked.json create mode 100644 tests/fea/test_workflow_locked_defaults.py diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json new file mode 100644 index 0000000..4f330cf --- /dev/null +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -0,0 +1,120 @@ +{ + "schema_version": 1, + "workflows": { + "hip-sideways-fall": { + "bbox_crop_from": [ + null, + "min", + null + ], + "bbox_ratio": [ + 1.0, + 1.2, + null + ], + "cortical_label": 1, + "cut_mode": "bbox_ratio", + "distal_shaft_center_fraction": [ + 0.1958514895, + 0.5708725889, + 0.0601674635 + ], + "distal_shaft_size_fraction": [ + 0.3640088821, + 0.6351022953 + ], + "fe_displacement_mm": -4.0, + "femoral_head_center_fraction": [ + 0.5, + 1.0278767152, + 0.9650933279 + ], + "fixture_size_fraction": [ + 1.1, + 1.1 + ], + "greater_trochanter_center_fraction": [ + 0.5, + 0.0151983839, + 0.6551527108 + ], + "input_margin_mm": 16.0, + "iso_resolution_mm": 1.0, + "left_femur_code": 1, + "lesser_trochanter_distal_offset_mm": 50.0, + "mask_smoothing_spacing_threshold_mm": 2.0, + "node_sets": [ + "Femoral_Head_PMMA_Nodes", + "Greater_Trochanter_PMMA_Nodes", + "Distal_Femur_Nodes" + ], + "pmma_intrusion_mm": 6.0, + "pmma_thickness_mm": 10.0, + "reference_max_scale": [ + 1.2, + 1.2, + 1.3 + ], + "reference_min_scale": [ + 0.8, + 0.8, + 0.75 + ], + "right_femur_code": 2, + "shaft_length_mm": 100.0, + "target_displacement_percent": 4.0, + "trabecular_label": 2 + }, + "spine-compression": { + "benchmark_linear": { + "bottom_direction": [ + 0, + 0, + -1 + ], + "bottom_node_set_id": 5, + "cort_elastic_E_func": "kopperdahl_trab_E", + "elastic_E_func": "kopperdahl_trab_E", + "fe_displacement": -0.2, + "pmma_E": 2500, + "pmma_mat_id": 300, + "pmma_v": 0.3, + "top_direction": [ + 0, + 0, + 1 + ], + "top_node_set_id": 6 + }, + "bottom_node_set_id": 3, + "contact_size_fraction": [ + 1.6, + 1.6 + ], + "inferior_contact_center_fraction": [ + 0.5, + 0.5, + -0.05 + ], + "iso_resolution_mm": 1.0, + "label_smoothing_sigma_mm": 1.0, + "label_smoothing_threshold": 0.5, + "mask_smoothing_spacing_threshold_mm": 2.0, + "pmma_e_mpa": 2500, + "pmma_intrusion_mm": 6, + "pmma_material_id": 5000, + "pmma_thickness_mm": 10, + "reference_filename": "L4_BODY_SPINE_COMPRESSION_REF.vtk", + "registration_backend": "vtk", + "registration_max_scale": "1.2,1.2,1.3", + "registration_min_scale": "0.8,0.8,0.75", + "superior_contact_center_fraction": [ + 0.5, + 0.5, + 1.05 + ], + "target_displacement_percent": 0.68, + "top_node_set_id": 4 + } + } +} diff --git a/tests/fea/test_workflow_locked_defaults.py b/tests/fea/test_workflow_locked_defaults.py new file mode 100644 index 0000000..9370281 --- /dev/null +++ b/tests/fea/test_workflow_locked_defaults.py @@ -0,0 +1,100 @@ +import json +from pathlib import Path + +from ogo.fea import femur +from ogo.fea import spine + + +def test_maintained_fe_workflow_defaults_match_locked_fixture(): + expected_path = Path(__file__).parent / "fixtures" / "fe_workflow_defaults_locked.json" + expected = json.loads(expected_path.read_text(encoding="utf-8")) + + assert _locked_workflow_defaults() == expected + + +def _locked_workflow_defaults(): + return { + "schema_version": 1, + "workflows": { + "spine-compression": { + "iso_resolution_mm": spine.DEFAULT_SPINE_ISO_RESOLUTION_MM, + "pmma_thickness_mm": spine.DEFAULT_SPINE_PMMA_THICKNESS_MM, + "pmma_intrusion_mm": spine.DEFAULT_SPINE_PMMA_INTRUSION_MM, + "pmma_material_id": spine.DEFAULT_SPINE_PMMA_MATERIAL_ID, + "pmma_e_mpa": spine.DEFAULT_SPINE_PMMA_E_MPA, + "target_displacement_percent": spine.DEFAULT_SPINE_TARGET_DISPLACEMENT_PERCENT, + "label_smoothing_sigma_mm": spine.DEFAULT_SPINE_LABEL_SMOOTHING_SIGMA_MM, + "label_smoothing_threshold": spine.DEFAULT_SPINE_LABEL_SMOOTHING_THRESHOLD, + "mask_smoothing_spacing_threshold_mm": spine.DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + "top_node_set_id": spine.DEFAULT_SPINE_TOP_NODE_SET_ID, + "bottom_node_set_id": spine.DEFAULT_SPINE_BOTTOM_NODE_SET_ID, + "registration_backend": spine.DEFAULT_SPINE_REGISTRATION_BACKEND, + "registration_min_scale": spine.DEFAULT_SPINE_REGISTRATION_MIN_SCALE, + "registration_max_scale": spine.DEFAULT_SPINE_REGISTRATION_MAX_SCALE, + "reference_filename": spine.DEFAULT_SPINE_REFERENCE_FILENAME, + "contact_size_fraction": list(spine.SPINE_CONTACT_SIZE_FRACTION), + "superior_contact_center_fraction": list( + spine.SPINE_SUPERIOR_CONTACT_CENTER_FRACTION + ), + "inferior_contact_center_fraction": list( + spine.SPINE_INFERIOR_CONTACT_CENTER_FRACTION + ), + "benchmark_linear": _selected_spine_benchmark( + spine.benchmark_linear_params() + ), + }, + "hip-sideways-fall": { + "left_femur_code": femur.LEFT_FEMUR, + "right_femur_code": femur.RIGHT_FEMUR, + "iso_resolution_mm": femur.DEFAULT_FEMUR_ISO_RESOLUTION_MM, + "fe_displacement_mm": femur.DEFAULT_FEMUR_FE_DISPLACEMENT, + "target_displacement_percent": femur.DEFAULT_FEMUR_TARGET_DISPLACEMENT_PERCENT, + "mask_smoothing_spacing_threshold_mm": femur.DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + "fixture_size_fraction": list(femur.SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION), + "femoral_head_center_fraction": list( + femur.FEMORAL_HEAD_FIXTURE_CENTER_FRACTION + ), + "greater_trochanter_center_fraction": list( + femur.GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION + ), + "distal_shaft_center_fraction": list( + femur.DISTAL_SHAFT_FIXTURE_CENTER_FRACTION + ), + "distal_shaft_size_fraction": list( + femur.DISTAL_SHAFT_FIXTURE_SIZE_FRACTION + ), + "bbox_ratio": list(femur.DEFAULT_FEMUR_BBOX_RATIO), + "bbox_crop_from": list(femur.DEFAULT_FEMUR_BBOX_CROP_FROM), + "reference_min_scale": list(femur.DEFAULT_FEMUR_REFERENCE_MIN_SCALE), + "reference_max_scale": list(femur.DEFAULT_FEMUR_REFERENCE_MAX_SCALE), + "pmma_thickness_mm": femur.DEFAULT_PMMA_THICKNESS_MM, + "pmma_intrusion_mm": femur.DEFAULT_PMMA_INTRUSION_MM, + "input_margin_mm": femur.DEFAULT_FEMUR_INPUT_MARGIN_MM, + "cut_mode": femur.DEFAULT_FEMUR_CUT_MODE, + "shaft_length_mm": femur.DEFAULT_FEMUR_SHAFT_LENGTH_MM, + "lesser_trochanter_distal_offset_mm": femur.DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, + "cortical_label": femur.DEFAULT_CORTICAL_LABEL, + "trabecular_label": femur.DEFAULT_TRABECULAR_LABEL, + "node_sets": list(femur.SIDEWAYS_FALL_NODE_SETS), + }, + }, + } + + +def _selected_spine_benchmark(params): + keys = ( + "pmma_mat_id", + "pmma_E", + "pmma_v", + "top_node_set_id", + "bottom_node_set_id", + "top_direction", + "bottom_direction", + "fe_displacement", + "elastic_E_func", + "cort_elastic_E_func", + ) + return { + key: list(params[key]) if isinstance(params[key], tuple) else params[key] + for key in keys + } From aa8f75ef790755a0ad9af9c30c175fd54f634e2b Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 14 Jul 2026 08:48:46 -0600 Subject: [PATCH 15/67] Stabilize editable package installs --- pyproject.toml | 7 +++++++ setup.py | 1 - tests/test_packaging_compatibility.py | 28 +++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 pyproject.toml create mode 100644 tests/test_packaging_compatibility.py diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..af4475e --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,7 @@ +[build-system] +requires = [ + "pbr", + "setuptools>=64,<69", + "wheel", +] +build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index aa2d8a0..f9cf62b 100644 --- a/setup.py +++ b/setup.py @@ -3,6 +3,5 @@ from setuptools import setup setup( - setup_requires=['pbr'], pbr=True, ) diff --git a/tests/test_packaging_compatibility.py b/tests/test_packaging_compatibility.py new file mode 100644 index 0000000..b8b0f4a --- /dev/null +++ b/tests/test_packaging_compatibility.py @@ -0,0 +1,28 @@ +import ast +from pathlib import Path + + +def test_setup_py_does_not_bootstrap_setup_dependencies(): + """CI installs build dependencies before installing this package.""" + repo_root = Path(__file__).resolve().parents[1] + setup_py = repo_root / "setup.py" + tree = ast.parse(setup_py.read_text(encoding="utf-8"), filename=str(setup_py)) + + for node in ast.walk(tree): + if not isinstance(node, ast.keyword): + continue + assert node.arg != "setup_requires", ( + "setup.py should not use setup_requires; that setuptools bootstrap path " + "is fragile in the Python 3.7 CI environment." + ) + + +def test_build_system_declares_pbr_dependency(): + """Editable installs need pbr before setup.py metadata is evaluated.""" + repo_root = Path(__file__).resolve().parents[1] + pyproject_toml = repo_root / "pyproject.toml" + + assert pyproject_toml.exists(), "pyproject.toml should declare build requirements." + text = pyproject_toml.read_text(encoding="utf-8") + assert "[build-system]" in text + assert '"pbr' in text From 2703ddd2567b6d4627a902c552b28ffcfeb2833a Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:04:50 -0600 Subject: [PATCH 16/67] Make CI dependency installs non-interactive --- .github/workflows/main.yml | 2 +- tests/test_packaging_compatibility.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2d925fd..c782a75 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -47,7 +47,7 @@ jobs: - name: Create and install dependencies if: steps.conda-env-cache.outputs.cache-hit != 'true' run: | - conda install -n test n88tools click flake8 matplotlib nose numpy opencv pandas pbr pytest scipy simpleitk six sphinx pydicom gdcm + conda install -y -n test n88tools click flake8 matplotlib nose numpy opencv pandas pbr pytest scipy simpleitk six sphinx pydicom gdcm - name: Install package run: | conda activate test diff --git a/tests/test_packaging_compatibility.py b/tests/test_packaging_compatibility.py index b8b0f4a..bce84f2 100644 --- a/tests/test_packaging_compatibility.py +++ b/tests/test_packaging_compatibility.py @@ -1,4 +1,5 @@ import ast +import re from pathlib import Path @@ -26,3 +27,18 @@ def test_build_system_declares_pbr_dependency(): text = pyproject_toml.read_text(encoding="utf-8") assert "[build-system]" in text assert '"pbr' in text + + +def test_ci_conda_install_is_non_interactive(): + """Cache misses in CI should not wait for an install confirmation prompt.""" + repo_root = Path(__file__).resolve().parents[1] + workflow = repo_root / ".github" / "workflows" / "main.yml" + text = workflow.read_text(encoding="utf-8") + + install_lines = re.findall(r"^\s*conda install\b.*$", text, flags=re.MULTILINE) + assert install_lines, "The CI workflow should install conda dependencies." + for line in install_lines: + assert " -y " in " {0} ".format(line), ( + "conda install commands in CI should include -y so cache misses " + "cannot block on confirmation." + ) From fabc8f07f780ff907bb6e53ce029075d101b0e1d Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:44:04 -0600 Subject: [PATCH 17/67] Speed up CI dependency resolution --- .github/workflows/main.yml | 3 ++- tests/test_packaging_compatibility.py | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c782a75..c67a5a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,6 +36,7 @@ jobs: with: python-version: ${{ matrix.python-version }} miniconda-version: "latest" + mamba-version: "*" activate-environment: test channels: numerics88,simpleitk,conda-forge auto-activate-base: false @@ -47,7 +48,7 @@ jobs: - name: Create and install dependencies if: steps.conda-env-cache.outputs.cache-hit != 'true' run: | - conda install -y -n test n88tools click flake8 matplotlib nose numpy opencv pandas pbr pytest scipy simpleitk six sphinx pydicom gdcm + mamba install -y -n test n88tools click flake8 matplotlib nose numpy opencv pandas pbr pytest scipy simpleitk six sphinx pydicom gdcm - name: Install package run: | conda activate test diff --git a/tests/test_packaging_compatibility.py b/tests/test_packaging_compatibility.py index bce84f2..a12626c 100644 --- a/tests/test_packaging_compatibility.py +++ b/tests/test_packaging_compatibility.py @@ -35,10 +35,20 @@ def test_ci_conda_install_is_non_interactive(): workflow = repo_root / ".github" / "workflows" / "main.yml" text = workflow.read_text(encoding="utf-8") - install_lines = re.findall(r"^\s*conda install\b.*$", text, flags=re.MULTILINE) - assert install_lines, "The CI workflow should install conda dependencies." + install_lines = re.findall(r"^\s*(?:conda|mamba) install\b.*$", text, flags=re.MULTILINE) + assert install_lines, "The CI workflow should install dependencies." for line in install_lines: assert " -y " in " {0} ".format(line), ( - "conda install commands in CI should include -y so cache misses " + "CI install commands should include -y so cache misses " "cannot block on confirmation." ) + + +def test_ci_uses_mamba_for_dependency_resolution(): + """Cold CI environments should use the faster conda-compatible solver.""" + repo_root = Path(__file__).resolve().parents[1] + workflow = repo_root / ".github" / "workflows" / "main.yml" + text = workflow.read_text(encoding="utf-8") + + assert "mamba-version:" in text + assert re.search(r"^\s*mamba install\b", text, flags=re.MULTILINE) From c172f93ceed8383335ac6ff872fac6ba3e9a4d35 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 14 Jul 2026 09:58:22 -0600 Subject: [PATCH 18/67] Clarify FE helper module boundaries --- ogo/fea/femur.py | 294 ++++------------------------------- ogo/fea/spine_compression.py | 40 +---- 2 files changed, 34 insertions(+), 300 deletions(-) diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 3658242..7bd0e07 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -1,5 +1,10 @@ """Femur-specific defaults and helpers for sideways-fall FE generation. +Shared point-cloud and boundary-contact primitives live in ``alignment`` and +``boundary``. They are re-exported here so older femur workflow imports keep +working while this module stays focused on femur defaults and femur-only +geometry. + Default femur workflow: 1. Read the calibrated density image and whole-femur mask. The public wrapper chooses left/right from ``--side`` and the lower-level script uses @@ -34,6 +39,23 @@ from pathlib import Path +from ogo.fea.alignment import ( + point_cloud_axis_lengths, + polydata_from_points, + polydata_points, + sample_points, + surface_points_from_vtk_mask, +) +from ogo.fea.boundary import ( + _axis_bounds, + _axis_extent_from_vector, + bbox_relative_contact_bounds as bbox_relative_fixture_bounds, + bbox_relative_contact_direction as bbox_relative_fixture_direction, + bbox_relative_contact_plane as bbox_relative_fixture_plane, + foreground_voxel_center_bounds, + foreground_voxel_center_bounds_from_mask, +) + LEFT_FEMUR = 1 RIGHT_FEMUR = 2 @@ -87,60 +109,6 @@ def sideways_fall_output_name(output_file, femur_side): return str(output_path.with_name(f"{output_path.stem}_{side_suffix(femur_side)}.n88model")) -def _axis_index(axis): - axis_map = {"x": 0, "y": 1, "z": 2} - try: - return axis_map[str(axis).lower()] - except KeyError as exc: - raise ValueError("projection_axis must be 'x', 'y', or 'z'.") from exc - - -def _axis_bounds(model_bounds, axis): - index = 2 * int(axis) - return float(model_bounds[index]), float(model_bounds[index + 1]) - - -def foreground_voxel_center_bounds_from_mask(mask, *, origin, spacing): - """Return x/y/z physical bounds of nonzero voxel centers. - - Bbox-relative workflow fixtures are authored against the occupied voxel - centers, matching the image-space convention used by the FE recipe - parameters. FE mesh/node bounds are wider by the element faces and should - not be used to resolve those contact planes. - """ - import numpy as np - - active = np.asarray(mask) != 0 - coords = np.argwhere(active) - if coords.size == 0: - raise ValueError("Cannot compute foreground bounds from an empty mask.") - origin = np.asarray(origin, dtype=np.float64) - spacing = np.asarray(spacing, dtype=np.float64) - if origin.shape != (3,) or spacing.shape != (3,): - raise ValueError("origin and spacing must contain three values.") - lo = origin + coords.min(axis=0).astype(np.float64) * spacing - hi = origin + coords.max(axis=0).astype(np.float64) * spacing - return ( - float(lo[0]), - float(hi[0]), - float(lo[1]), - float(hi[1]), - float(lo[2]), - float(hi[2]), - ) - - -def foreground_voxel_center_bounds(vtk_image): - """Return physical foreground bounds for a VTK image using voxel centers.""" - from ogo.util.vtk_image import vtk_image_to_numpy - - return foreground_voxel_center_bounds_from_mask( - vtk_image_to_numpy(vtk_image), - origin=vtk_image.GetOrigin(), - spacing=vtk_image.GetSpacing(), - ) - - def matrix4x4_to_numpy(matrix): """Return a 4 x 4 NumPy matrix from a VTK matrix or array-like value.""" import numpy as np @@ -260,89 +228,6 @@ def transform_resample_vtk_image_to_reference_grid( return out -def bbox_relative_fixture_bounds( - model_bounds, - *, - center_fraction, - size_fraction, - projection_axis="y", - shape="rectangle", -): - """Return physical contact ROI bounds from model-bbox-relative plane values. - - ``center_fraction`` has x/y/z fractions relative to the model bbox. The two - ``size_fraction`` values scale the lateral axes of the projection plane in - coordinate order. When ``shape="square"``, the smaller scaled lateral - length is used on both axes. The projection axis itself spans the whole - model bbox so the cap builder can find the nearest supported anatomy - column. - """ - from ogo.fea.boundary import bbox_relative_contact_bounds - - return bbox_relative_contact_bounds( - model_bounds, - center_fraction=center_fraction, - size_fraction=size_fraction, - projection_axis=projection_axis, - shape=shape, - ) - - -def bbox_relative_fixture_direction(center_fraction, *, projection_axis="y"): - """Return the cap side implied by a bbox-relative plane fraction.""" - from ogo.fea.boundary import bbox_relative_contact_direction - - return bbox_relative_contact_direction( - center_fraction, - projection_axis=projection_axis, - ) - - -def _axis_extent_from_vector(model_bounds, vector): - import numpy as np - - spans = np.asarray( - [ - float(model_bounds[1]) - float(model_bounds[0]), - float(model_bounds[3]) - float(model_bounds[2]), - float(model_bounds[5]) - float(model_bounds[4]), - ], - dtype=float, - ) - return float(np.sum(np.abs(np.asarray(vector, dtype=float)) * spans)) - - -def _fixture_plane_axes(projection_axis, normal_sign): - """Return stable in-plane axes for bbox-relative fixture planes.""" - axis = _axis_index(projection_axis) - sign = 1.0 if float(normal_sign) >= 0.0 else -1.0 - if axis == 0: - return (0.0, 0.0, sign), (0.0, -1.0, 0.0) - if axis == 1: - return (0.0, 0.0, -sign), (-1.0, 0.0, 0.0) - return (1.0, 0.0, 0.0), (0.0, sign, 0.0) - - -def bbox_relative_fixture_plane( - model_bounds, - *, - center_fraction, - size_fraction, - projection_axis="y", - shape="square", -): - """Return a physical contact-plane definition from bbox-relative values.""" - from ogo.fea.boundary import bbox_relative_contact_plane - - return bbox_relative_contact_plane( - model_bounds, - center_fraction=center_fraction, - size_fraction=size_fraction, - projection_axis=projection_axis, - shape=shape, - ) - - def _scale_triplet(values, name): import numpy as np @@ -354,135 +239,12 @@ def _scale_triplet(values, name): def principal_axis_lengths(polydata): """Return approximate principal-axis diameters for a VTK polydata surface.""" - import numpy as np - from vtk.util.numpy_support import vtk_to_numpy - - points = polydata.GetPoints() - if points is None or points.GetNumberOfPoints() == 0: - raise ValueError("Cannot measure principal axes for empty polydata.") - coordinates = np.asarray(vtk_to_numpy(points.GetData()), dtype=float) - centered = coordinates - coordinates.mean(axis=0) - covariance = np.cov(centered.T) - eigvals = np.linalg.eigvalsh(covariance) - return np.sqrt(np.maximum(eigvals, 0.0)) * 2.0 - - -def polydata_points(polydata): - """Return VTK polydata point coordinates as an ``(n, 3)`` NumPy array.""" - import numpy as np - from vtk.util.numpy_support import vtk_to_numpy - - points = polydata.GetPoints() - if points is None or points.GetNumberOfPoints() == 0: - raise ValueError("Polydata contains no points.") - coordinates = np.asarray(vtk_to_numpy(points.GetData()), dtype=float) - if coordinates.ndim != 2 or coordinates.shape[1] != 3: - raise ValueError("Polydata points must have shape (n, 3).") - return coordinates[np.all(np.isfinite(coordinates), axis=1)] - - -def polydata_from_points(points): - """Create a vertex-only VTK polydata from an ``(n, 3)`` point cloud.""" - import numpy as np - import vtk - - coordinates = np.asarray(points, dtype=float) - if coordinates.ndim != 2 or coordinates.shape[1] != 3 or coordinates.shape[0] < 3: - raise ValueError("points must have shape (n, 3) with at least three rows.") - - vtk_points = vtk.vtkPoints() - vertices = vtk.vtkCellArray() - for point in coordinates: - point_id = vtk_points.InsertNextPoint(float(point[0]), float(point[1]), float(point[2])) - vertices.InsertNextCell(1) - vertices.InsertCellPoint(point_id) - - polydata = vtk.vtkPolyData() - polydata.SetPoints(vtk_points) - polydata.SetVerts(vertices) - return polydata - - -def sample_points(points, *, max_points=None, mode="linspace", offset=0): - """Sample a point cloud using the same small deterministic modes as workflows.""" - import numpy as np - - coordinates = np.asarray(points, dtype=float) - if max_points is None or int(max_points) <= 0 or coordinates.shape[0] <= int(max_points): - return coordinates - - token = str(mode).strip().lower() - if token == "stride": - step = max(1, int(np.ceil(coordinates.shape[0] / int(max_points)))) - start = int(offset) % step - indices = np.arange(start, coordinates.shape[0], step, dtype=int) - if indices.size < int(max_points): - fallback = np.linspace(0, coordinates.shape[0] - 1, int(max_points), dtype=int) - indices = np.unique(np.concatenate([indices, fallback])) - indices = indices[: int(max_points)] - else: - indices = np.linspace(0, coordinates.shape[0] - 1, int(max_points), dtype=int) - return coordinates[indices] - - -def _binary_erosion_6(mask): - import numpy as np - - padded = np.pad(mask, 1, mode="constant", constant_values=False) - center = padded[1:-1, 1:-1, 1:-1] - eroded = center.copy() - eroded &= padded[:-2, 1:-1, 1:-1] - eroded &= padded[2:, 1:-1, 1:-1] - eroded &= padded[1:-1, :-2, 1:-1] - eroded &= padded[1:-1, 2:, 1:-1] - eroded &= padded[1:-1, 1:-1, :-2] - eroded &= padded[1:-1, 1:-1, 2:] - return eroded - - -def surface_points_from_vtk_mask( - vtk_mask, - *, - max_points=8000, - sample_mode="stride", - sample_offset=0, -): - """Return physical x/y/z points from the 6-connected voxel mask surface.""" - import numpy as np - from ogo.util.vtk_image import vtk_image_to_numpy - - mask = np.asarray(vtk_image_to_numpy(vtk_mask), dtype=bool) - if not np.any(mask): - raise ValueError("mask contains no foreground voxels.") - - surface = mask & ~_binary_erosion_6(mask) - # Match the workflow-replay path: enumerate the voxel surface in z/y/x - # NumPy order, then convert those indices back to physical x/y/z points. - surface_zyx = np.transpose(surface if np.any(surface) else mask, (2, 1, 0)) - coords_xyz = np.argwhere(surface_zyx)[:, [2, 1, 0]].astype(float) - points = np.asarray(vtk_mask.GetOrigin(), dtype=float) + coords_xyz * np.asarray( - vtk_mask.GetSpacing(), - dtype=float, - ) - return sample_points( - points, - max_points=max_points, - mode=sample_mode, - offset=sample_offset, - ) - - -def point_cloud_axis_lengths(points): - """Return approximate principal-axis diameters for an ``(n, 3)`` point cloud.""" - import numpy as np - - coordinates = np.asarray(points, dtype=float) - if coordinates.ndim != 2 or coordinates.shape[1] != 3 or coordinates.shape[0] < 3: - raise ValueError("points must have shape (n, 3) with at least three rows.") - centered = coordinates - coordinates.mean(axis=0) - covariance = np.cov(centered.T) - eigvals = np.linalg.eigvalsh(covariance) - return np.sqrt(np.maximum(eigvals, 0.0)) * 2.0 + try: + return point_cloud_axis_lengths(polydata_points(polydata)) + except ValueError as exc: + if "contains no points" in str(exc): + raise ValueError("Cannot measure principal axes for empty polydata.") from exc + raise def scale_reference_point_cloud_to_sample( diff --git a/ogo/fea/spine_compression.py b/ogo/fea/spine_compression.py index 6ee40b3..1fe8028 100644 --- a/ogo/fea/spine_compression.py +++ b/ogo/fea/spine_compression.py @@ -92,6 +92,12 @@ SPINE_SUPERIOR_CONTACT_CENTER_FRACTION, default_spine_reference_path, ) +from ogo.fea.model import ( + apply_spine_boundary_conditions as apply_boundary_conditions, + create_microfe_model, + find_and_add_visible_nodes, + log_fe_arguments, +) import ogo.util.Helper as ogo from ogo.util.echo_arguments import echo_arguments from scipy.ndimage import gaussian_filter @@ -650,46 +656,12 @@ def convert_image_to_material(image, mask, n_bins=128, cort_mask=None): return binned_image, bin_centers -def find_and_add_visible_nodes(model, bc_geometry, normal_vector, bone_material_id, node_set_name): - """Delegate visible-node detection to the shared FE model helper.""" - from ogo.fea.model import find_and_add_visible_nodes as _find_and_add_visible_nodes - - return _find_and_add_visible_nodes( - model, bc_geometry, normal_vector, bone_material_id, node_set_name - ) - def resolve_func(func_or_name, module): """Resolve a material-law function by object or configured name.""" from ogo.fea.materials import resolve_material_func return resolve_material_func(func_or_name, module) -def apply_boundary_conditions(model,**kwargs): - """Apply the maintained spine compression boundary conditions.""" - from ogo.fea.model import apply_spine_boundary_conditions - - return apply_spine_boundary_conditions(model, **kwargs) - - -def log_fe_arguments(**kwargs): - """Print model-building arguments through the shared FE logger.""" - from ogo.fea.model import log_fe_arguments as _log_fe_arguments - - return _log_fe_arguments(**kwargs) - -def create_microfe_model( - image_with_pads, - boundary_masks_with_pads, - bin_centers, - **kwargs -): - """Create a spine compression micro-FE model from material and BC masks.""" - from ogo.fea.model import create_microfe_model as _create_microfe_model - - return _create_microfe_model( - image_with_pads, boundary_masks_with_pads, bin_centers, **kwargs - ) - ###################################################################### QUALITY CONTROL ## Functions to check image and boundary conditions ###################################################################### QUALITY CONTROL From b623c3a5d981e6836b035f2d1e2deb677d74195e Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 14 Jul 2026 10:11:22 -0600 Subject: [PATCH 19/67] Fold FE builders into domain modules --- ogo/cli/GenerateFEM.py | 16 +- ogo/fea/README.md | 9 + ogo/fea/femur.py | 967 ++++++++++++++++++ ogo/fea/sideways_fall.py | 1007 ------------------- ogo/fea/spine.py | 1368 +++++++++++++++++++++++++- ogo/fea/spine_compression.py | 1412 --------------------------- tests/cli/test_GenerateFEM.py | 2 +- tests/fea/test_module_layout.py | 20 + tests/fea/test_sideways_fall.py | 8 +- tests/fea/test_spinefe_benchmark.py | 28 +- 10 files changed, 2389 insertions(+), 2448 deletions(-) delete mode 100644 ogo/fea/sideways_fall.py delete mode 100644 ogo/fea/spine_compression.py create mode 100644 tests/fea/test_module_layout.py diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 2c65a93..be3150b 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -168,13 +168,13 @@ def build_femur_command( def expected_spine_model_path(calibrated_image: Path, output_path: Optional[Path], target: SpineTarget) -> Path: - """Predict the spine model path written by ``OgoSpineCompressionFe``.""" + """Predict the spine model path written by the spine builder.""" output_dir = output_path if output_path is not None else calibrated_image.parent return output_dir / "{}_{}.n88model".format(remove_extension(calibrated_image), target.level) def expected_femur_model_path(calibrated_image: Path, output_path: Optional[Path], side: str) -> Path: - """Predict the femur model path written by ``OgoSidewaysFallFe``.""" + """Predict the femur model path written by the femur builder.""" output_dir = output_path if output_path is not None else calibrated_image.parent stem = "LF" if side == "left" else "RF" return output_dir / "{}_{}.n88model".format(remove_extension(calibrated_image), stem) @@ -839,15 +839,15 @@ def _call_cli(main_func: Callable[[], None], program: str, argv: Sequence[str]) def run_spine_command(argv: Sequence[str]) -> None: - from ogo.fea.spine_compression import main as spine_compression_main + from ogo.fea.spine import main as spine_main - _call_cli(spine_compression_main, "OgoSpineCompressionFe", argv) + _call_cli(spine_main, "ogoFEA-spine-builder", argv) def run_femur_command(argv: Sequence[str]) -> None: - from ogo.fea.sideways_fall import main as sideways_fall_main + from ogo.fea.femur import main as femur_main - _call_cli(sideways_fall_main, "OgoSidewaysFallFe", argv) + _call_cli(femur_main, "ogoFEA-hip-builder", argv) def print_dry_run(program: str, argv: Sequence[str]) -> None: @@ -1016,7 +1016,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: extra_args=spine_extra_args, ) if args.dry_run: - print_dry_run("OgoSpineCompressionFe", cmd) + print_dry_run("ogoFEA-spine-builder", cmd) else: run_spine_command(cmd) model_path = expected_spine_model_path(args.calibrated_image, args.output_path, target) @@ -1045,7 +1045,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: extra_args=extra_args, ) if args.dry_run: - print_dry_run("OgoSidewaysFallFe", cmd) + print_dry_run("ogoFEA-hip-builder", cmd) else: run_femur_command(cmd) model_path = expected_femur_model_path(args.calibrated_image, args.output_path, side) diff --git a/ogo/fea/README.md b/ogo/fea/README.md index a412bd4..624fa84 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -5,6 +5,15 @@ This directory contains the maintained model-building code used by `ogo/cli/GenerateFEM.py`; the lower-level spine and femur modules in this directory should stay implementation details. +The workflow builders are organized by anatomy: + +- `spine.py` owns spine-compression defaults, preprocessing, model building, + and the lower-level spine command parser. +- `femur.py` owns hip sideways-fall defaults, preprocessing, model building, + and the lower-level femur command parser. +- Shared helper modules such as `alignment.py`, `boundary.py`, `materials.py`, + and `model.py` contain reusable mechanics used by both anatomy workflows. + ## Inputs All FEA workflows expect a density-calibrated CT image and an aligned diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 7bd0e07..03b0220 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -1371,3 +1371,970 @@ def expand_xz_footprint(bounds, *, x_extension_mm=0.0, z_extension_mm=0.0): z_center - z_length / 2.0, z_center + z_length / 2.0, ) + + +# ----------------------------------------------------------------------------- +# Hip sideways-fall workflow builder +# ----------------------------------------------------------------------------- + +##### +# Hip workflow builder +# +# This script sets up the sideways fall FE model on the hip from the density (K2HPO4) +# calibrated image. This script sets up the model for either a left or right femur, as +# specified by the user. The analysis resamples the image to isotropic voxels, transforms +# the image, applies the bone mask and bins the data. It then creates the FE model for +# solving using FAIM (>v8.0, Numerics Solutions Ltd, Calgary, Canada - Steven Boyd). +# +##### +# +# Andrew Michalski +# University of Calgary +# Biomedical Engineering Graduate Program +# April 29, 2019 +# Modified to Py3: March 25, 2020 +##### + +script_version = 1.0 + +## +# Import the required modules +import ogo.util.Helper as ogo +import os +import sys +import argparse +import time +from datetime import date +from collections import OrderedDict +import vtk +import vtkbone + +from ogo.fea.boundary import ( + generate_projected_material_disk_vtk, + should_smooth_resampled_mask, + smooth_binary_mask_vtk, +) +from ogo.fea.materials import build_femur_material_table +from ogo.fea.model import ( + append_postprocessing_sets, + directional_face_node_ids_from_voxel_mask, + find_nodes_on_coordinate_plane, + interface_node_ids_from_voxel_mask, + write_model, +) +from ogo.util.echo_arguments import echo_arguments + + +def remove_extension(filename): + while True: + filename, ext = os.path.splitext(filename) + if not ext: + break + return filename + + +## +# Start script +def sidewaysFallFe(args): + ogo.message("Start of Script...") + + ## + # Collect the input arguments + image = args.calibrated_image + mask = args.bone_mask + compartment_mask = args.compartment_mask + + mask_threshold = args.mask_threshold + iso_resolution = args.iso_resolution + femur_side = args.femur_side + poissons_ratio = args.poissons_ratio + pmma_E = args.pmma_E + pmma_v = args.pmma_v + pmma_thick = args.pmma_thick + pmma_intrusion = args.pmma_intrusion + pmma_mat_id = args.pmma_mat_id + fe_displacement = args.fe_displacement + left_femur_reference = args.left_femur_reference + right_femur_reference = args.right_femur_reference + output_file = args.output_file + pmma_yield_compression = args.pmma_yield_compression + pmma_yield_tension = args.pmma_yield_tension + femur_shaft_length = args.femur_shaft_length + femur_cut_mode = args.femur_cut_mode + femur_bbox_ratio = args.femur_bbox_ratio + femur_bbox_crop_from = args.femur_bbox_crop_from + femur_lesser_trochanter_distal_offset = args.femur_lesser_trochanter_distal_offset + femur_lesser_trochanter_distal_offset_percent = args.femur_lesser_trochanter_distal_offset_percent + femur_input_margin = args.femur_input_margin + cortical_label = args.cortical_label + trabecular_label = args.trabecular_label + mask_smoothing_spacing_threshold = args.mask_smoothing_spacing_threshold + + + + ## + # Determine image locations and names of files + image_pathname = os.path.dirname(image) + image_basename = os.path.basename(image) + mask_pathname = os.path.dirname(mask) + mask_basename = os.path.basename(mask) + script_name = sys.argv[0] + + + try: + N88_fileName = sideways_fall_output_name(output_file, femur_side) + except ValueError: + ogo.message("Femur side not recognized. Terminating...") + sys.exit() + + + ## + # Message the input parameters to the terminal + ogo.message("Image Path: %s" % image_pathname) + ogo.message("Image File: %s" % image_basename) + ogo.message("Mask Path: %s" % mask_pathname) + ogo.message("Mask File: %s" % mask_basename) + ogo.message("Isotropic Voxel Size: %8.4f" % iso_resolution) + if femur_side == 1: + ogo.message("Femur Side for Model: Left") + elif femur_side == 2: + ogo.message("Femur Side for Model: Right") + else: + ogo.message("Femur side not recognized. Terminating...") + sys.exit() + ogo.message("Trabecular Elastic Function: %s" % (args.elastic_E_func or "default_E")) + ogo.message("Cortical Elastic Function: %s" % (args.cort_elastic_E_func or args.elastic_E_func or "default_E")) + ogo.message("Bone Poissons Ratio: %1.1f" % poissons_ratio) + ogo.message("PMMA Elastic Modulus [MPa]: %8.4f" % pmma_E) + ogo.message("PMMA Poissons Ration: %1.1f" % pmma_v) + ogo.message("PMMA Thickness [mm]: %8.4f" % pmma_thick) + ogo.message("PMMA Intrusion [mm]: %8.4f" % pmma_intrusion) + ogo.message("PMMA Material ID: %d" % pmma_mat_id) + ogo.message("Applied Displacement [mm]: %8.4f" % fe_displacement) + ogo.message("Femur Cut Mode: %s" % femur_cut_mode) + if femur_cut_mode == "bbox_ratio": + ogo.message("Femur BBox Ratio [reference, constrained, free]: %s" % str(femur_bbox_ratio)) + ogo.message("Femur BBox Crop From [reference, constrained, free]: %s" % str(femur_bbox_crop_from)) + if compartment_mask is not None: + ogo.message("Compartment Mask: %s" % compartment_mask) + ogo.message("Cortical Label: %d" % cortical_label) + ogo.message("Trabecular Label: %d" % trabecular_label) + if femur_cut_mode == "fixed_length": + ogo.message("Retained Proximal Femur Length [mm]: %8.4f" % femur_shaft_length) + elif femur_lesser_trochanter_distal_offset_percent is not None: + ogo.message( + "Lesser Trochanter Distal Cut Offset [%% greater-lesser distance]: %8.4f" + % femur_lesser_trochanter_distal_offset_percent + ) + else: + ogo.message( + "Lesser Trochanter Distal Cut Offset [mm]: %8.4f" + % femur_lesser_trochanter_distal_offset + ) + ogo.message("Input Foreground Safety Margin [mm]: %8.4f" % femur_input_margin) + + ## + # Read input image + ogo.message("Reading calibrated image...") + imageData = ogo.readNii(image) + input_spacing = imageData.GetSpacing() + + ## + # Read bone mask + ogo.message("Reading bone mask...") + maskData = ogo.readNii(mask) + maskThres = ogo.maskThreshold(maskData, mask_threshold) + compartmentData = None + if compartment_mask is not None: + ogo.message("Reading trabecular/cortical compartment mask...") + compartmentData = ogo.readNii(compartment_mask) + + distal_crop_face = None + bbox_crop_meta = None + if femur_cut_mode == "bbox_ratio": + ogo.message("Applying bbox-ratio femur crop in input image coordinates...") + images_to_crop = [imageData, maskThres] + ([compartmentData] if compartmentData is not None else []) + cropped_images, distal_crop_face, bbox_crop_meta = crop_vtk_images_to_bbox_ratio( + images_to_crop, + maskThres, + bbox_ratio=femur_bbox_ratio, + bbox_crop_from=femur_bbox_crop_from, + labels={1}, + ) + imageData = cropped_images[0] + maskThres = cropped_images[1] + if compartmentData is not None: + compartmentData = cropped_images[2] + ogo.message( + "BBox-ratio crop slices xyz=%s; output shape xyz=%s; crop face voxels=%d." + % ( + bbox_crop_meta["crop_slices_xyz"], + bbox_crop_meta["output_shape_xyz"], + bbox_crop_meta["crop_face_voxels"], + ) + ) + + ogo.message("Resampling cropped femur inputs to isotropic spacing before ICP...") + imageData = resample_vtk_image_like_workflow( + imageData, + iso_resolution, + interpolation="bspline", + ) + maskThres = resample_vtk_image_like_workflow( + maskThres, + iso_resolution, + interpolation="nearest", + ) + if distal_crop_face is not None: + distal_crop_face = resample_vtk_image_like_workflow( + distal_crop_face, + iso_resolution, + interpolation="nearest", + ) + if compartmentData is not None: + compartmentData = resample_vtk_image_like_workflow( + compartmentData, + iso_resolution, + interpolation="nearest", + ) + registration_mask = maskThres + + images_to_pad = [imageData, maskThres] + if distal_crop_face is not None: + images_to_pad.append(distal_crop_face) + if compartmentData is not None: + images_to_pad.append(compartmentData) + pad_constants = [0] * len(images_to_pad) + padded_images, padding = pad_vtk_images_to_foreground_margin( + images_to_pad, + maskThres, + margin_mm=femur_input_margin, + constants=pad_constants, + ) + imageData = padded_images[0] + maskThres = padded_images[1] + next_padded_index = 2 + if distal_crop_face is not None: + distal_crop_face = padded_images[next_padded_index] + next_padded_index += 1 + if compartmentData is not None: + compartmentData = padded_images[next_padded_index] + if any(padding["lower"]) or any(padding["upper"]): + ogo.message( + "Padded isotropic input image extent by lower=%s upper=%s voxels for FE transform safety." + % (padding["lower"], padding["upper"]) + ) + else: + ogo.message("Isotropic input image already has sufficient foreground safety margin.") + + ## + # Use the cropped, isotropic input geometry directly for ICP. + image_rot = imageData + mask_rot = maskThres + distal_crop_face_rot = distal_crop_face + compartment_rot = compartmentData + + ## + # Align the input femur with the reference model + ogo.message("Aligning input with reference model...") + sample_surface_points = surface_points_from_vtk_mask( + registration_mask, + max_points=8000, + sample_mode="stride", + sample_offset=0, + ) + mask_surface = polydata_from_points(sample_surface_points) + if femur_side == 1: + ref_poly = ogo.readPolyData(left_femur_reference) + elif femur_side == 2: + if os.path.exists(right_femur_reference): + ref_poly = ogo.readPolyData(right_femur_reference) + else: + ogo.message( + "Right femur reference not found; mirroring left reference in x:", + right_femur_reference, + ) + ref_poly = mirror_polydata_x(ogo.readPolyData(left_femur_reference)) + else: + print("Error: Femur Side not defined. Terminating...") + sys.exit() + + ref_poly, reference_scale = scale_reference_point_cloud_to_sample( + ref_poly, + sample_surface_points, + max_points=8000, + reference_sample_mode="linspace", + min_scale=DEFAULT_FEMUR_REFERENCE_MIN_SCALE, + max_scale=DEFAULT_FEMUR_REFERENCE_MAX_SCALE, + ) + ogo.message("Femur reference axis lengths: %s" % str(reference_scale["reference_axis_lengths"])) + ogo.message("Sample femur axis lengths: %s" % str(reference_scale["sample_axis_lengths"])) + ogo.message("Femur reference scale factors: %s" % str(reference_scale["scale_factors"])) + + icp = ogo.iterativeClosestPoint(ref_poly, mask_surface) + # ogo.message("Transform:\n %s" % str(icp)) + + ogo.message("Applying the transformation and isotropic resampling to the image and mask...") + output_origin, output_size = reference_grid_from_vtk_mask( + mask_rot, + icp, + margin_voxels=int(round(femur_input_margin / max(iso_resolution, 1.0e-6))), + ) + output_spacing = (float(iso_resolution), float(iso_resolution), float(iso_resolution)) + ogo.message( + "Reference-frame output grid: origin=%s size=%s spacing=%s" + % (output_origin, output_size, output_spacing) + ) + image_trans = transform_resample_vtk_image_to_reference_grid( + image_rot, + icp, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="bspline", + ) + mask_trans = transform_resample_vtk_image_to_reference_grid( + mask_rot, + icp, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", + ) + distal_crop_face_trans = ( + transform_resample_vtk_image_to_reference_grid( + distal_crop_face_rot, + icp, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", + ) + if distal_crop_face_rot is not None + else None + ) + compartment_trans = ( + transform_resample_vtk_image_to_reference_grid( + compartment_rot, + icp, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", + ) + if compartment_rot is not None + else None + ) + smooth_resampled_masks = should_smooth_resampled_mask( + input_spacing, + mask_smoothing_spacing_threshold, + ) + if smooth_resampled_masks: + ogo.message( + "smoothing resampled femur mask because input spacing " + f"{input_spacing} exceeds {mask_smoothing_spacing_threshold} mm..." + ) + mask_trans = smooth_binary_mask_vtk(mask_trans, close_iter=1, open_iter=1) + else: + ogo.message( + "skipping femur mask smoothing because input spacing " + f"{input_spacing} is <= {mask_smoothing_spacing_threshold} mm in all dimensions..." + ) + + if femur_cut_mode == "bbox_ratio": + ogo.message("Using transformed bbox-ratio crop face for distal shaft standardization.") + try: + mask_z_min, mask_z_max = femur_z_coverage(mask_trans) + except ValueError as exc: + ogo.message(str(exc)) + sys.exit(1) + retained_length_mm = mask_z_max - mask_z_min + shaft_crop = { + "cut_mode": "bbox_ratio", + "cut_plane": "transformed input crop face", + "bbox_ratio": bbox_crop_meta, + "mask_z_min": mask_z_min, + "mask_z_max": mask_z_max, + "retained_length_mm": retained_length_mm, + "warnings": [], + } + ogo.message( + "Transformed bbox-ratio mask z coverage [%8.4f, %8.4f]; retained z-span=%8.4f" + % (mask_z_min, mask_z_max, retained_length_mm) + ) + else: + ogo.message("Applying flat distal femur crop in reference coordinates...") + try: + image_trans, mask_trans, shaft_crop = standardize_femur_shaft_length( + image_trans, + mask_trans, + reference_bounds=ref_poly.GetBounds(), + retained_length_mm=femur_shaft_length, + cut_mode=femur_cut_mode, + lesser_trochanter_distal_offset_mm=femur_lesser_trochanter_distal_offset, + lesser_trochanter_distal_offset_percent=femur_lesser_trochanter_distal_offset_percent, + ) + except ValueError as exc: + ogo.message(str(exc)) + sys.exit(1) + retained_length_mm = shaft_crop["retained_length_mm"] + ogo.message( + "Distal cut z=%8.4f; input mask z coverage [%8.4f, %8.4f]; retained length=%8.4f" + % ( + shaft_crop["cut_z"], + shaft_crop["mask_z_min"], + shaft_crop["mask_z_max"], + retained_length_mm, + ) + ) + for warning in shaft_crop.get("warnings", []): + ogo.message("WARNING: Femur crop QC: %s" % warning) + if femur_cut_mode == "lesser_trochanter": + ogo.message( + "Detected greater trochanter z=%8.4f; lesser trochanter z=%8.4f" + % (shaft_crop["greater_trochanter_z"], shaft_crop["lesser_trochanter_z"]) + ) + if compartment_trans is not None: + compartment_trans = flat_crop_vtk_image_below_z(compartment_trans, shaft_crop["cut_z"]) + + # ogo.message("Writing out temp images...") + # ogo.writeNii(image_trans, "temp_image.nii", image_pathname) + # ogo.writeNii(mask_trans, "temp_mask.nii", image_pathname) + # + # left_femur_reference_image = "/Users/andrew.michalski/Development/Ogo/LT_FEMUR_SIDEWAYS_FALL_REF.nii" + # right_femur_reference_image = "/Users/andrew.michalski/Development/Ogo/RT_FEMUR_SIDEWAYS_FALL_REF.nii" + # ogo.message("Applying SimpleITK registration of the images...") + # if femur_side == 1: + # ogo.finalRegistration(left_femur_reference_image) + # elif femur_side == 2: + # ogo.finalRegistration(right_femur_reference_image) + # ogo.message("Reading temp images...") + # image_trans = ogo.readNii("temp_image2.nii") + # mask_trans = ogo.readNii("temp_mask2.nii") + # os.remove("temp_image2.nii") + # os.remove("temp_mask2.nii") + + # replace any negative values less than -31 to be equivalent to -31. + # -31 is used as that converts to a minumum elastic modulus value of 0.1 MPa + # K2HPO4 den = -31 mg/cc => Ash den = 6 mg/cc => E = 0.1 MPa + image_thres = ogo.bmd_preprocess(image_trans, -31) + image_ash = ogo.bmd_K2hpo4ToAsh(image_thres) + + cortical_mask = None + if compartment_trans is not None: + cortical_mask = cortical_compartment_mask( + compartment_trans, + cortical_label=cortical_label, + trabecular_label=trabecular_label, + ) + if smooth_resampled_masks: + ogo.message("smoothing derived cortical compartment mask...") + cortical_mask = smooth_binary_mask_vtk(cortical_mask, close_iter=1, open_iter=1) + binned_image, bin_centers = ogo.density2materialID( + image_ash, + n_bins=128, + cort_mask=cortical_mask, + ) + + ## + # Set up the FE model. + ogo.message("Setting up the Finite Element Model...") + # Cast the image to Short to "round" float values to nearest whole number + cast_image = ogo.cast2short(binned_image) + cast_mask = ogo.cast2unsignchar(mask_trans) + + + # Apply the mask to the bone + ogo.message("Applying the bone mask to the image...") + bone_image = ogo.applyMask(cast_image, cast_mask) + + # Ensure connectivity + ogo.message("Performing Image connectivity...") + conn = ogo.imageConnectivity(bone_image) + + + change = ogo.prepareFiniteElementImage(conn) + femur_mask_on_model_grid = ogo.prepareFiniteElementImage(cast_mask) + distal_crop_face_change = ( + ogo.prepareFiniteElementImage(distal_crop_face_trans) if distal_crop_face_trans is not None else None + ) + if distal_crop_face_change is not None: + distal_crop_face_change = ogo.applyMask(distal_crop_face_change, ogo.cast2unsignchar(change)) + if femur_cut_mode == "bbox_ratio": + ogo.message("Skipping model-grid distal shaft cut; bbox-ratio crop face is already transformed.") + else: + model_z_min, model_z_max = femur_z_coverage(change) + model_cut_z = model_z_max - retained_length_mm + if model_z_min > model_cut_z: + if model_z_min - model_cut_z <= 2.0: + model_cut_z = model_z_min + else: + ogo.message( + "Femur scan does not include enough distal shaft after model-grid alignment " + "(required retained length %.1f mm, available approximately %.1f mm)." + % (retained_length_mm, max(0.0, model_z_max - model_z_min)) + ) + sys.exit(1) + change = flat_crop_vtk_image_below_z(change, model_cut_z) + femur_mask_on_model_grid = flat_crop_vtk_image_below_z( + femur_mask_on_model_grid, + model_cut_z, + ) + ogo.message( + "Applied model-grid distal shaft cut z=%8.4f; model mask z coverage [%8.4f, %8.4f]" + % (model_cut_z, model_z_min, model_z_max) + ) + + # Convert image data to hexahedral elements + ogo.message("Meshing image data to elements...") + mesh = ogo.Image2Mesh(change) + + ## + # Set up the Material Table + ogo.message("Setting up the Finite Element Material Table...") + material_table = build_femur_material_table( + bin_centers, + n_bins=128, + elastic_E_func=args.elastic_E_func, + yield_comp_func=args.yield_comp_func, + yield_tens_func=args.yield_tens_func, + cort_elastic_E_func=args.cort_elastic_E_func, + cort_yield_comp_func=args.cort_yield_comp_func, + cort_yield_tens_func=args.cort_yield_tens_func, + cort_poissons_ratio=args.cort_poissons_ratio, + poissons_ratio=poissons_ratio, + pmma_mat_id=pmma_mat_id, + pmma_E=pmma_E, + pmma_v=pmma_v, + pmma_yield_tension=pmma_yield_tension, + pmma_yield_compression=pmma_yield_compression, + include_cortical=cortical_mask is not None, + ) + ## + # Create the preliminary FE model + ogo.message("Constructing the Finite Element Model...") + model = ogo.applyTestBase(mesh, material_table) + model.ComputeBounds() + mesh_model_bounds = model.GetBounds() + model_bounds = foreground_voxel_center_bounds(femur_mask_on_model_grid) + ogo.message("Model Mesh Bounds: %s" % str(mesh_model_bounds)) + ogo.message("Model Foreground Voxel-Center Bounds: %s" % str(model_bounds)) + + # Define support vectors for the two PMMA contact fixtures. + top_support_vector = (0, 1, 0) + bottom_support_vector = (0, -1, 0) + + ## + # Create PMMA disks from bbox-relative contact planes. The disk normal + # points from the contact plane toward the anatomy; the generated PMMA + # image clears any femur-mask voxels so PMMA never occupies the segmentation. + femoral_head_plane = bbox_relative_fixture_plane( + model_bounds, + center_fraction=FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, + size_fraction=SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, + projection_axis="y", + shape="rectangle", + ) + femoral_head_cap_direction = bbox_relative_fixture_direction( + FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, + projection_axis="y", + ) + ogo.message( + "Femoral Head PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" + % ( + femoral_head_plane["center"], + femoral_head_plane["normal"], + femoral_head_plane["u_axis"], + femoral_head_plane["v_axis"], + femoral_head_plane["size"], + ) + ) + ogo.message("Femoral Head PMMA cap direction: %s" % femoral_head_cap_direction) + femoralHeadPMMA = generate_projected_material_disk_vtk( + change, + surface_vtk_image=femur_mask_on_model_grid, + exclusion_vtk_image=femur_mask_on_model_grid, + center=femoral_head_plane["center"], + normal=femoral_head_plane["normal"], + u_axis=femoral_head_plane["u_axis"], + v_axis=femoral_head_plane["v_axis"], + size=femoral_head_plane["size"], + shape=femoral_head_plane["shape"], + thickness=pmma_thick, + intrusion=pmma_intrusion, + anatomy_constrained=True, + output_value=pmma_mat_id, + ) + + ## + # Creates the greater trochanter pmma cap + greater_trochanter_plane = bbox_relative_fixture_plane( + model_bounds, + center_fraction=GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, + size_fraction=SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, + projection_axis="y", + shape="rectangle", + ) + greater_trochanter_cap_direction = bbox_relative_fixture_direction( + GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, + projection_axis="y", + ) + ogo.message( + "Greater Trochanter PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" + % ( + greater_trochanter_plane["center"], + greater_trochanter_plane["normal"], + greater_trochanter_plane["u_axis"], + greater_trochanter_plane["v_axis"], + greater_trochanter_plane["size"], + ) + ) + ogo.message("Greater Trochanter PMMA cap direction: %s" % greater_trochanter_cap_direction) + greaterTrochanterPMMA = generate_projected_material_disk_vtk( + change, + surface_vtk_image=femur_mask_on_model_grid, + exclusion_vtk_image=femur_mask_on_model_grid, + center=greater_trochanter_plane["center"], + normal=greater_trochanter_plane["normal"], + u_axis=greater_trochanter_plane["u_axis"], + v_axis=greater_trochanter_plane["v_axis"], + size=greater_trochanter_plane["size"], + shape=greater_trochanter_plane["shape"], + thickness=pmma_thick, + intrusion=pmma_intrusion, + anatomy_constrained=True, + output_value=pmma_mat_id, + ) + + ## + # combines the pmma cap images with the model image. + ogo.message("Combine PMMA Cap Images with Model Image...") + combinedImage = ogo.combineImageData_SF(change, femoralHeadPMMA, greaterTrochanterPMMA, pmma_mat_id) + + ## + # Mesh the final image and create Finite Element Model + # Ensure connectivity + ogo.message("Performing Image connectivity...") + conn2 = ogo.imageConnectivity(combinedImage) + + # Convert image data to hexahedral elements + ogo.message("Meshing image data to elements...") + mesh2 = ogo.Image2Mesh(conn2) + + ## + # Create the final FE model + ogo.message("Constructing the Finite Element Model...") + model2 = ogo.applyTestBase(mesh2, material_table) + model2.ComputeBounds() + model2_bounds = model2.GetBounds() + ogo.message("Model 2 Bounds: %s" % str(model2_bounds)) + + ## + # Determine Femoral Head PMMA Cap support nodes. + ogo.message("Determining Femoral Head PMMA Cap nodes...") + if femoral_head_cap_direction == "up": + fh_pmma_bounds = ( + model2_bounds[0], + model2_bounds[1], + model2_bounds[3] - 1, + model2_bounds[3], + model2_bounds[4], + model2_bounds[5], + ) + femoral_head_support_vector = top_support_vector + else: + fh_pmma_bounds = ( + model2_bounds[0], + model2_bounds[1], + model2_bounds[2], + model2_bounds[2] + 1, + model2_bounds[4], + model2_bounds[5], + ) + femoral_head_support_vector = bottom_support_vector + fh_pmma_visible_node_IDS = directional_face_node_ids_from_voxel_mask( + model2, + femoralHeadPMMA, + direction=femoral_head_support_vector, + name=FEMORAL_HEAD_NODE_SET, + ) + + ogo.message("-- found %d outer-face nodes on Femoral Head PMMA Cap." + % fh_pmma_visible_node_IDS.GetNumberOfTuples()) + model2.AddNodeSet(fh_pmma_visible_node_IDS) + + ## + # Determine Greater Trochanter PMMA Cap support nodes + ogo.message("Determining Greater Trochanter PMMA Cap support nodes...") + if greater_trochanter_cap_direction == "up": + gt_pmma_bounds = ( + model2_bounds[0], + model2_bounds[1], + model2_bounds[3] - 1, + model2_bounds[3], + model2_bounds[4], + model2_bounds[5], + ) + greater_trochanter_support_vector = top_support_vector + else: + gt_pmma_bounds = ( + model2_bounds[0], + model2_bounds[1], + model2_bounds[2], + model2_bounds[2] + 1, + model2_bounds[4], + model2_bounds[5], + ) + greater_trochanter_support_vector = bottom_support_vector + gt_pmma_visible_node_IDS = directional_face_node_ids_from_voxel_mask( + model2, + greaterTrochanterPMMA, + direction=greater_trochanter_support_vector, + name=GREATER_TROCHANTER_NODE_SET, + ) + + ogo.message("-- found %d outer-face nodes on Greater Trochanter PMMA Cap." + % gt_pmma_visible_node_IDS.GetNumberOfTuples()) + + model2.AddNodeSet(gt_pmma_visible_node_IDS) + + ## + # Distal Femur (df) support nodes. In bbox-ratio mode the distal shaft cut + # was made before alignment, so its transformed crop-face label carries the + # correct oblique support angle in model coordinates. + ogo.message("Determining distal femur nodes...") + if femur_cut_mode == "bbox_ratio": + if distal_crop_face_change is None: + ogo.message("BBox-ratio cut mode requires a transformed distal crop-face mask.") + sys.exit(1) + try: + distal_crop_face_plane = crop_face_contact_plane(distal_crop_face_change, change) + distal_plane = bbox_relative_oriented_contact_plane( + model_bounds, + center_fraction=DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, + size_fraction=DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, + normal=distal_crop_face_plane["normal"], + shape="anatomy", + ) + except ValueError as exc: + ogo.message(str(exc)) + sys.exit(1) + ogo.message( + "Distal Femur bbox-relative oblique support plane: center=%s normal=%s outward=%s size=%s" + % ( + distal_plane["center"], + distal_plane["normal"], + distal_plane["outward_normal"], + distal_plane["size"], + ) + ) + distal_surface = projected_crop_face_surface_vtk( + change, + distal_plane, + intrusion=pmma_intrusion, + output_value=1, + ) + df_visible_node_IDS = interface_node_ids_from_voxel_mask( + model2, + distal_surface, + change, + name=DISTAL_FEMUR_NODE_SET, + direction=distal_plane["normal"], + ) + if df_visible_node_IDS.GetNumberOfTuples() == 0: + ogo.message("No distal femur nodes found on the bbox-relative oblique shaft support surface.") + sys.exit(1) + else: + distal_cut_z = model2_bounds[4] + ogo.message("Distal Femur Cut Plane z: %8.4f" % distal_cut_z) + df_visible_node_IDS = find_nodes_on_coordinate_plane(model2, "z", distal_cut_z) + + ogo.message("-- found %d distal shaft interface nodes." + % df_visible_node_IDS.GetNumberOfTuples()) + + df_visible_node_IDS.SetName(DISTAL_FEMUR_NODE_SET) + model2.AddNodeSet(df_visible_node_IDS) + + ## + # Apply Boundary conditions to PMMA caps at specific sites + ogo.message("Applying displacement boundary condition to Femoral Head PMMA cap...") + model2.ApplyBoundaryCondition( + FEMORAL_HEAD_NODE_SET, + vtkbone.vtkboneConstraint.SENSE_Y, + fe_displacement, + "top_displacement") + + ogo.message("Constraining Greater Trochanter PMMA cap in loading direction...") + model2.ApplyBoundaryCondition( + GREATER_TROCHANTER_NODE_SET, + vtkbone.vtkboneConstraint.SENSE_Y, + 0, + "bottom_fixed_y_PMMA") + + for SENSE, label in ( + (vtkbone.vtkboneConstraint.SENSE_X, "x"), + (vtkbone.vtkboneConstraint.SENSE_Z, "z"), + ): + ogo.message("Constraining distal femur rigid-body motion...") + model2.ApplyBoundaryCondition( + DISTAL_FEMUR_NODE_SET, + SENSE, + 0, + f"bottom_fixed_{label}") + + ## + # Post Processing parameters + ogo.message("Setting up Post Processing Parameters...") + append_postprocessing_sets( + model2, + SIDEWAYS_FALL_NODE_SETS, + ) + + model2.AppendHistory( + "Created by %s version %s." % (script_name, script_version)) + + ## + # Write out n88model file + ogo.message("Writing out n88model file: %s" % N88_fileName) + write_model(model2, N88_fileName) + + ## + ogo.message("Done writing n88model.") + + ## + # End of script + ogo.message("End of Script.") + sys.exit() + + +def main(): + # Setup description + description=''' +This script sets up the sideways fall FE model on the hip from the + density (K2HPO4) calibrated image. This script sets up the model for either a left or + right femur, as specified by the user. The analysis resamples the image to isotropic + voxels, transforms the image, applies the bone mask and bins the data. It then + creates the FE model for solving using FAIM (v8.1, Numerics Solutions Ltd, Calgary, + Canada - Steven Boyd). + + Input: Calibrated K2HPO4 Image (*.nii), Bone Mask (*_MASK.nii) + + Optional Parameters: + 1) Isotropic resample voxel size + 2) Left or Right Femur Bone Mask + + Output: N88 Model (*.n88model) + +''' + + + # Setup argument parsing + parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, + prog="ogoFEA-hip-builder", + description=description + ) + + + parser.add_argument("calibrated_image", + help = "*_K2HPO4.nii image file") + parser.add_argument("bone_mask", + help = "*_MASK.nii mask image of bone") + + parser.add_argument("--mask_threshold", type = int, + default = 1, + help = "Set the threshold value to extract the bone of interest from the mask. (Default: %(default)s)") + parser.add_argument("--iso_resolution", type = float, default = DEFAULT_FEMUR_ISO_RESOLUTION_MM, + help = "Set the isotropic voxel size [in mm]. (default: %(default)s [mm])") + parser.add_argument("--mask_smoothing_spacing_threshold", type=float, default=DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + help="Smooth resampled femur masks only when an input spacing dimension exceeds this value. (default: %(default)s [mm])") + parser.add_argument("--femur_side", type = int, default = 1, + help = "Set whether the left or right femur is to be analyzed. 1 = Left; 2 = Right. (default: %(default)s)") + parser.add_argument("--output_path", type=str, default=None, + help="Set output path for the N88 model file. (default: same as input image)") + parser.add_argument("--poissons_ratio", type=float, default=0.3, + help="Sets the Poisson's ratio for the material(s) in the FE model. (default: %(default)s)") + parser.add_argument("--elastic_E_func", type=str, default=None, + help="Function name for trabecular bone Young's modulus. (default: default_E)") + parser.add_argument("--yield_comp_func", type=str, default=None, + help="Function name for trabecular compression yield. (default: none)") + parser.add_argument("--yield_tens_func", type=str, default=None, + help="Function name for trabecular tension yield. (default: none)") + parser.add_argument("--cort_elastic_E_func", type=str, default=None, + help="Function name for cortical bone Young's modulus. Defaults to --elastic_E_func.") + parser.add_argument("--cort_yield_comp_func", type=str, default=None, + help="Function name for cortical compression yield. Defaults to --yield_comp_func.") + parser.add_argument("--cort_yield_tens_func", type=str, default=None, + help="Function name for cortical tension yield. Defaults to --yield_tens_func.") + parser.add_argument("--cort_poissons_ratio", type=float, default=None, + help="Poisson's ratio for cortical bone. Defaults to --poissons_ratio.") + parser.add_argument("--pmma_E", type=float, default=2500, + help="Sets the Elastic Modulus for PMMA caps in the FE model. (default: %(default)s [MPa])") + parser.add_argument("--pmma_v", type=float, default=0.3, + help="Sets the Poisson's ratio for the PMMA material(s) in the FE model. (default: %(default)s)") + parser.add_argument("--pmma_thick", type=float, default=DEFAULT_PMMA_THICKNESS_MM, + help="Sets the fixed thickness for PMMA caps in the FE model. (default: %(default)s [mm])") + parser.add_argument("--pmma_intrusion", type=float, default=DEFAULT_PMMA_INTRUSION_MM, + help="Sets how far anatomy can occupy the fixed PMMA fixture thickness before bone overlap is preserved during material combination. (default: %(default)s [mm])") + parser.add_argument("--pmma_mat_id", type=int, default=5000, + help="Sets the material ID for the PMMA blocks. (default: %(default)s)") + parser.add_argument("--fe_displacement", type=float, default=DEFAULT_FEMUR_FE_DISPLACEMENT, + help="Sets the applied displacement endpoint for the sideways-fall model. The default reports the force at 4%% displacement. (default: %(default)s)") + parser.add_argument("--femur_shaft_length", type=float, default=DEFAULT_FEMUR_SHAFT_LENGTH_MM, + help="Retained proximal femur length [mm] for --femur_cut_mode fixed_length. (default: %(default)s [mm])") + parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "lesser_trochanter", "fixed_length"], + default=DEFAULT_FEMUR_CUT_MODE, + help="Set the distal femur crop. bbox_ratio crops in input image coordinates and uses the transformed crop face for distal support. (default: %(default)s)") + parser.add_argument("--femur_bbox_ratio", nargs=3, default=DEFAULT_FEMUR_BBOX_RATIO, + help="BBox-ratio crop in recipe order: reference constrained free. Use 'none' for a free axis. (default: %(default)s)") + parser.add_argument("--femur_bbox_crop_from", nargs=3, default=DEFAULT_FEMUR_BBOX_CROP_FROM, + help="BBox crop side in recipe order: reference constrained free. Values are min, max, center/null. (default: %(default)s)") + parser.add_argument("--femur_lesser_trochanter_distal_offset", type=float, default=DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, + help="Cut this many mm distal to the detected lesser trochanter in lesser_trochanter mode. (default: %(default)s [mm])") + parser.add_argument("--femur_lesser_trochanter_distal_offset_percent", type=float, default=None, + help="Optional percentage of the detected greater-to-lesser trochanter z distance to cut distal to the lesser trochanter. Overrides --femur_lesser_trochanter_distal_offset when set. (default: %(default)s)") + parser.add_argument("--femur_input_margin", type=float, default=DEFAULT_FEMUR_INPUT_MARGIN_MM, + help="Pad the input image/mask as needed so femur foreground has this margin before ICP. (default: %(default)s [mm])") + parser.add_argument("--compartment_mask", type=str, default=None, + help="Optional trabecular/cortical compartment mask aligned with the bone mask. Defaults: cortical=1, trabecular=2.") + parser.add_argument("--cortical_label", type=int, default=DEFAULT_CORTICAL_LABEL, + help="Label value for cortical bone in --compartment_mask. (default: %(default)s)") + parser.add_argument("--trabecular_label", type=int, default=DEFAULT_TRABECULAR_LABEL, + help="Label value for trabecular bone in --compartment_mask. (default: %(default)s)") + parser.add_argument("--reference_path", type=str, required=False, default=None, + help="Path to the reference vtk file for ICP registration. (default: None)") + parser.add_argument("--pmma_yield_compression", type=float, default=None, + help="Sets the yield strength in compression for PMMA material in the FE model. (default: %(default)s [MPa])") + parser.add_argument("--pmma_yield_tension", type=float, default=None, + help="Sets the yield strength in tension for PMMA material in the FE model. (default: %(default)s [MPa])") + # Parse and display + args = parser.parse_args() + + # Set default reference paths + if args.reference_path is None: + data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "dat") + args.left_femur_reference = os.path.join(data_dir, "LT_FEMUR_SIDEWAYS_FALL_REF.vtk") + args.right_femur_reference = os.path.join(data_dir, "RT_FEMUR_SIDEWAYS_FALL_REF.vtk") + else: + args.left_femur_reference = os.path.join(args.reference_path, "LT_FEMUR_SIDEWAYS_FALL_REF.vtk") + args.right_femur_reference = os.path.join(args.reference_path, "RT_FEMUR_SIDEWAYS_FALL_REF.vtk") + + + basename = remove_extension(os.path.basename(args.calibrated_image)) + + if args.output_path is None: + output_dir = os.path.dirname(args.calibrated_image) + else: + output_dir = args.output_path + + output_dir = os.path.abspath(output_dir) + + args.output_file = os.path.join(output_dir, f"{basename}.n88model") + + print(echo_arguments("ogoFEA-hip-builder", vars(args))) + + # Run program + sidewaysFallFe(args) + +if __name__ == '__main__': + main() diff --git a/ogo/fea/sideways_fall.py b/ogo/fea/sideways_fall.py deleted file mode 100644 index 2ef7717..0000000 --- a/ogo/fea/sideways_fall.py +++ /dev/null @@ -1,1007 +0,0 @@ -##### -# ogo_sideways_fall_fe.py -# -# This script sets up the sideways fall FE model on the hip from the density (K2HPO4) -# calibrated image. This script sets up the model for either a left or right femur, as -# specified by the user. The analysis resamples the image to isotropic voxels, transforms -# the image, applies the bone mask and bins the data. It then creates the FE model for -# solving using FAIM (>v8.0, Numerics Solutions Ltd, Calgary, Canada - Steven Boyd). -# -##### -# -# Andrew Michalski -# University of Calgary -# Biomedical Engineering Graduate Program -# April 29, 2019 -# Modified to Py3: March 25, 2020 -##### - -script_version = 1.0 - -## -# Import the required modules -import ogo.util.Helper as ogo -import os -import sys -import argparse -import time -from datetime import date -from collections import OrderedDict -import vtk -import vtkbone - -from ogo.fea.boundary import ( - generate_projected_material_disk_vtk, - should_smooth_resampled_mask, - smooth_binary_mask_vtk, -) -from ogo.fea.femur import ( - DEFAULT_CORTICAL_LABEL, - DEFAULT_FEMUR_BBOX_CROP_FROM, - DEFAULT_FEMUR_BBOX_RATIO, - DEFAULT_FEMUR_FE_DISPLACEMENT, - DEFAULT_FEMUR_CUT_MODE, - DEFAULT_FEMUR_INPUT_MARGIN_MM, - DEFAULT_FEMUR_ISO_RESOLUTION_MM, - DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - DEFAULT_FEMUR_REFERENCE_MAX_SCALE, - DEFAULT_FEMUR_REFERENCE_MIN_SCALE, - DEFAULT_FEMUR_SHAFT_LENGTH_MM, - DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, - DEFAULT_PMMA_INTRUSION_MM, - DEFAULT_PMMA_THICKNESS_MM, - DEFAULT_TRABECULAR_LABEL, - DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, - DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, - DISTAL_FEMUR_NODE_SET, - FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, - FEMORAL_HEAD_NODE_SET, - GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, - GREATER_TROCHANTER_NODE_SET, - SIDEWAYS_FALL_NODE_SETS, - SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, - bbox_relative_fixture_direction, - bbox_relative_fixture_plane, - bbox_relative_oriented_contact_plane, - cortical_compartment_mask, - crop_face_contact_plane, - crop_vtk_images_to_bbox_ratio, - femur_z_coverage, - flat_crop_vtk_image_below_z, - foreground_voxel_center_bounds, - mirror_polydata_x, - pad_vtk_images_to_foreground_margin, - polydata_from_points, - reference_grid_from_vtk_mask, - projected_crop_face_surface_vtk, - resample_vtk_image_like_workflow, - scale_reference_point_cloud_to_sample, - sideways_fall_output_name, - standardize_femur_shaft_length, - surface_points_from_vtk_mask, - transform_resample_vtk_image_to_reference_grid, -) -from ogo.fea.materials import build_femur_material_table -from ogo.fea.model import ( - append_postprocessing_sets, - directional_face_node_ids_from_voxel_mask, - find_nodes_on_coordinate_plane, - interface_node_ids_from_voxel_mask, - write_model, -) -from ogo.util.echo_arguments import echo_arguments - - -def remove_extension(filename): - while True: - filename, ext = os.path.splitext(filename) - if not ext: - break - return filename - - -## -# Start script -def sidewaysFallFe(args): - ogo.message("Start of Script...") - - ## - # Collect the input arguments - image = args.calibrated_image - mask = args.bone_mask - compartment_mask = args.compartment_mask - - mask_threshold = args.mask_threshold - iso_resolution = args.iso_resolution - femur_side = args.femur_side - poissons_ratio = args.poissons_ratio - pmma_E = args.pmma_E - pmma_v = args.pmma_v - pmma_thick = args.pmma_thick - pmma_intrusion = args.pmma_intrusion - pmma_mat_id = args.pmma_mat_id - fe_displacement = args.fe_displacement - left_femur_reference = args.left_femur_reference - right_femur_reference = args.right_femur_reference - output_file = args.output_file - pmma_yield_compression = args.pmma_yield_compression - pmma_yield_tension = args.pmma_yield_tension - femur_shaft_length = args.femur_shaft_length - femur_cut_mode = args.femur_cut_mode - femur_bbox_ratio = args.femur_bbox_ratio - femur_bbox_crop_from = args.femur_bbox_crop_from - femur_lesser_trochanter_distal_offset = args.femur_lesser_trochanter_distal_offset - femur_lesser_trochanter_distal_offset_percent = args.femur_lesser_trochanter_distal_offset_percent - femur_input_margin = args.femur_input_margin - cortical_label = args.cortical_label - trabecular_label = args.trabecular_label - mask_smoothing_spacing_threshold = args.mask_smoothing_spacing_threshold - - - - ## - # Determine image locations and names of files - image_pathname = os.path.dirname(image) - image_basename = os.path.basename(image) - mask_pathname = os.path.dirname(mask) - mask_basename = os.path.basename(mask) - script_name = sys.argv[0] - - - try: - N88_fileName = sideways_fall_output_name(output_file, femur_side) - except ValueError: - ogo.message("Femur side not recognized. Terminating...") - sys.exit() - - - ## - # Message the input parameters to the terminal - ogo.message("Image Path: %s" % image_pathname) - ogo.message("Image File: %s" % image_basename) - ogo.message("Mask Path: %s" % mask_pathname) - ogo.message("Mask File: %s" % mask_basename) - ogo.message("Isotropic Voxel Size: %8.4f" % iso_resolution) - if femur_side == 1: - ogo.message("Femur Side for Model: Left") - elif femur_side == 2: - ogo.message("Femur Side for Model: Right") - else: - ogo.message("Femur side not recognized. Terminating...") - sys.exit() - ogo.message("Trabecular Elastic Function: %s" % (args.elastic_E_func or "default_E")) - ogo.message("Cortical Elastic Function: %s" % (args.cort_elastic_E_func or args.elastic_E_func or "default_E")) - ogo.message("Bone Poissons Ratio: %1.1f" % poissons_ratio) - ogo.message("PMMA Elastic Modulus [MPa]: %8.4f" % pmma_E) - ogo.message("PMMA Poissons Ration: %1.1f" % pmma_v) - ogo.message("PMMA Thickness [mm]: %8.4f" % pmma_thick) - ogo.message("PMMA Intrusion [mm]: %8.4f" % pmma_intrusion) - ogo.message("PMMA Material ID: %d" % pmma_mat_id) - ogo.message("Applied Displacement [mm]: %8.4f" % fe_displacement) - ogo.message("Femur Cut Mode: %s" % femur_cut_mode) - if femur_cut_mode == "bbox_ratio": - ogo.message("Femur BBox Ratio [reference, constrained, free]: %s" % str(femur_bbox_ratio)) - ogo.message("Femur BBox Crop From [reference, constrained, free]: %s" % str(femur_bbox_crop_from)) - if compartment_mask is not None: - ogo.message("Compartment Mask: %s" % compartment_mask) - ogo.message("Cortical Label: %d" % cortical_label) - ogo.message("Trabecular Label: %d" % trabecular_label) - if femur_cut_mode == "fixed_length": - ogo.message("Retained Proximal Femur Length [mm]: %8.4f" % femur_shaft_length) - elif femur_lesser_trochanter_distal_offset_percent is not None: - ogo.message( - "Lesser Trochanter Distal Cut Offset [%% greater-lesser distance]: %8.4f" - % femur_lesser_trochanter_distal_offset_percent - ) - else: - ogo.message( - "Lesser Trochanter Distal Cut Offset [mm]: %8.4f" - % femur_lesser_trochanter_distal_offset - ) - ogo.message("Input Foreground Safety Margin [mm]: %8.4f" % femur_input_margin) - - ## - # Read input image - ogo.message("Reading calibrated image...") - imageData = ogo.readNii(image) - input_spacing = imageData.GetSpacing() - - ## - # Read bone mask - ogo.message("Reading bone mask...") - maskData = ogo.readNii(mask) - maskThres = ogo.maskThreshold(maskData, mask_threshold) - compartmentData = None - if compartment_mask is not None: - ogo.message("Reading trabecular/cortical compartment mask...") - compartmentData = ogo.readNii(compartment_mask) - - distal_crop_face = None - bbox_crop_meta = None - if femur_cut_mode == "bbox_ratio": - ogo.message("Applying bbox-ratio femur crop in input image coordinates...") - images_to_crop = [imageData, maskThres] + ([compartmentData] if compartmentData is not None else []) - cropped_images, distal_crop_face, bbox_crop_meta = crop_vtk_images_to_bbox_ratio( - images_to_crop, - maskThres, - bbox_ratio=femur_bbox_ratio, - bbox_crop_from=femur_bbox_crop_from, - labels={1}, - ) - imageData = cropped_images[0] - maskThres = cropped_images[1] - if compartmentData is not None: - compartmentData = cropped_images[2] - ogo.message( - "BBox-ratio crop slices xyz=%s; output shape xyz=%s; crop face voxels=%d." - % ( - bbox_crop_meta["crop_slices_xyz"], - bbox_crop_meta["output_shape_xyz"], - bbox_crop_meta["crop_face_voxels"], - ) - ) - - ogo.message("Resampling cropped femur inputs to isotropic spacing before ICP...") - imageData = resample_vtk_image_like_workflow( - imageData, - iso_resolution, - interpolation="bspline", - ) - maskThres = resample_vtk_image_like_workflow( - maskThres, - iso_resolution, - interpolation="nearest", - ) - if distal_crop_face is not None: - distal_crop_face = resample_vtk_image_like_workflow( - distal_crop_face, - iso_resolution, - interpolation="nearest", - ) - if compartmentData is not None: - compartmentData = resample_vtk_image_like_workflow( - compartmentData, - iso_resolution, - interpolation="nearest", - ) - registration_mask = maskThres - - images_to_pad = [imageData, maskThres] - if distal_crop_face is not None: - images_to_pad.append(distal_crop_face) - if compartmentData is not None: - images_to_pad.append(compartmentData) - pad_constants = [0] * len(images_to_pad) - padded_images, padding = pad_vtk_images_to_foreground_margin( - images_to_pad, - maskThres, - margin_mm=femur_input_margin, - constants=pad_constants, - ) - imageData = padded_images[0] - maskThres = padded_images[1] - next_padded_index = 2 - if distal_crop_face is not None: - distal_crop_face = padded_images[next_padded_index] - next_padded_index += 1 - if compartmentData is not None: - compartmentData = padded_images[next_padded_index] - if any(padding["lower"]) or any(padding["upper"]): - ogo.message( - "Padded isotropic input image extent by lower=%s upper=%s voxels for FE transform safety." - % (padding["lower"], padding["upper"]) - ) - else: - ogo.message("Isotropic input image already has sufficient foreground safety margin.") - - ## - # Use the cropped, isotropic input geometry directly for ICP. - image_rot = imageData - mask_rot = maskThres - distal_crop_face_rot = distal_crop_face - compartment_rot = compartmentData - - ## - # Align the input femur with the reference model - ogo.message("Aligning input with reference model...") - sample_surface_points = surface_points_from_vtk_mask( - registration_mask, - max_points=8000, - sample_mode="stride", - sample_offset=0, - ) - mask_surface = polydata_from_points(sample_surface_points) - if femur_side == 1: - ref_poly = ogo.readPolyData(left_femur_reference) - elif femur_side == 2: - if os.path.exists(right_femur_reference): - ref_poly = ogo.readPolyData(right_femur_reference) - else: - ogo.message( - "Right femur reference not found; mirroring left reference in x:", - right_femur_reference, - ) - ref_poly = mirror_polydata_x(ogo.readPolyData(left_femur_reference)) - else: - print("Error: Femur Side not defined. Terminating...") - sys.exit() - - ref_poly, reference_scale = scale_reference_point_cloud_to_sample( - ref_poly, - sample_surface_points, - max_points=8000, - reference_sample_mode="linspace", - min_scale=DEFAULT_FEMUR_REFERENCE_MIN_SCALE, - max_scale=DEFAULT_FEMUR_REFERENCE_MAX_SCALE, - ) - ogo.message("Femur reference axis lengths: %s" % str(reference_scale["reference_axis_lengths"])) - ogo.message("Sample femur axis lengths: %s" % str(reference_scale["sample_axis_lengths"])) - ogo.message("Femur reference scale factors: %s" % str(reference_scale["scale_factors"])) - - icp = ogo.iterativeClosestPoint(ref_poly, mask_surface) - # ogo.message("Transform:\n %s" % str(icp)) - - ogo.message("Applying the transformation and isotropic resampling to the image and mask...") - output_origin, output_size = reference_grid_from_vtk_mask( - mask_rot, - icp, - margin_voxels=int(round(femur_input_margin / max(iso_resolution, 1.0e-6))), - ) - output_spacing = (float(iso_resolution), float(iso_resolution), float(iso_resolution)) - ogo.message( - "Reference-frame output grid: origin=%s size=%s spacing=%s" - % (output_origin, output_size, output_spacing) - ) - image_trans = transform_resample_vtk_image_to_reference_grid( - image_rot, - icp, - output_origin=output_origin, - output_size=output_size, - output_spacing=output_spacing, - interpolation="bspline", - ) - mask_trans = transform_resample_vtk_image_to_reference_grid( - mask_rot, - icp, - output_origin=output_origin, - output_size=output_size, - output_spacing=output_spacing, - interpolation="nearest", - ) - distal_crop_face_trans = ( - transform_resample_vtk_image_to_reference_grid( - distal_crop_face_rot, - icp, - output_origin=output_origin, - output_size=output_size, - output_spacing=output_spacing, - interpolation="nearest", - ) - if distal_crop_face_rot is not None - else None - ) - compartment_trans = ( - transform_resample_vtk_image_to_reference_grid( - compartment_rot, - icp, - output_origin=output_origin, - output_size=output_size, - output_spacing=output_spacing, - interpolation="nearest", - ) - if compartment_rot is not None - else None - ) - smooth_resampled_masks = should_smooth_resampled_mask( - input_spacing, - mask_smoothing_spacing_threshold, - ) - if smooth_resampled_masks: - ogo.message( - "smoothing resampled femur mask because input spacing " - f"{input_spacing} exceeds {mask_smoothing_spacing_threshold} mm..." - ) - mask_trans = smooth_binary_mask_vtk(mask_trans, close_iter=1, open_iter=1) - else: - ogo.message( - "skipping femur mask smoothing because input spacing " - f"{input_spacing} is <= {mask_smoothing_spacing_threshold} mm in all dimensions..." - ) - - if femur_cut_mode == "bbox_ratio": - ogo.message("Using transformed bbox-ratio crop face for distal shaft standardization.") - try: - mask_z_min, mask_z_max = femur_z_coverage(mask_trans) - except ValueError as exc: - ogo.message(str(exc)) - sys.exit(1) - retained_length_mm = mask_z_max - mask_z_min - shaft_crop = { - "cut_mode": "bbox_ratio", - "cut_plane": "transformed input crop face", - "bbox_ratio": bbox_crop_meta, - "mask_z_min": mask_z_min, - "mask_z_max": mask_z_max, - "retained_length_mm": retained_length_mm, - "warnings": [], - } - ogo.message( - "Transformed bbox-ratio mask z coverage [%8.4f, %8.4f]; retained z-span=%8.4f" - % (mask_z_min, mask_z_max, retained_length_mm) - ) - else: - ogo.message("Applying flat distal femur crop in reference coordinates...") - try: - image_trans, mask_trans, shaft_crop = standardize_femur_shaft_length( - image_trans, - mask_trans, - reference_bounds=ref_poly.GetBounds(), - retained_length_mm=femur_shaft_length, - cut_mode=femur_cut_mode, - lesser_trochanter_distal_offset_mm=femur_lesser_trochanter_distal_offset, - lesser_trochanter_distal_offset_percent=femur_lesser_trochanter_distal_offset_percent, - ) - except ValueError as exc: - ogo.message(str(exc)) - sys.exit(1) - retained_length_mm = shaft_crop["retained_length_mm"] - ogo.message( - "Distal cut z=%8.4f; input mask z coverage [%8.4f, %8.4f]; retained length=%8.4f" - % ( - shaft_crop["cut_z"], - shaft_crop["mask_z_min"], - shaft_crop["mask_z_max"], - retained_length_mm, - ) - ) - for warning in shaft_crop.get("warnings", []): - ogo.message("WARNING: Femur crop QC: %s" % warning) - if femur_cut_mode == "lesser_trochanter": - ogo.message( - "Detected greater trochanter z=%8.4f; lesser trochanter z=%8.4f" - % (shaft_crop["greater_trochanter_z"], shaft_crop["lesser_trochanter_z"]) - ) - if compartment_trans is not None: - compartment_trans = flat_crop_vtk_image_below_z(compartment_trans, shaft_crop["cut_z"]) - - # ogo.message("Writing out temp images...") - # ogo.writeNii(image_trans, "temp_image.nii", image_pathname) - # ogo.writeNii(mask_trans, "temp_mask.nii", image_pathname) - # - # left_femur_reference_image = "/Users/andrew.michalski/Development/Ogo/LT_FEMUR_SIDEWAYS_FALL_REF.nii" - # right_femur_reference_image = "/Users/andrew.michalski/Development/Ogo/RT_FEMUR_SIDEWAYS_FALL_REF.nii" - # ogo.message("Applying SimpleITK registration of the images...") - # if femur_side == 1: - # ogo.finalRegistration(left_femur_reference_image) - # elif femur_side == 2: - # ogo.finalRegistration(right_femur_reference_image) - # ogo.message("Reading temp images...") - # image_trans = ogo.readNii("temp_image2.nii") - # mask_trans = ogo.readNii("temp_mask2.nii") - # os.remove("temp_image2.nii") - # os.remove("temp_mask2.nii") - - # replace any negative values less than -31 to be equivalent to -31. - # -31 is used as that converts to a minumum elastic modulus value of 0.1 MPa - # K2HPO4 den = -31 mg/cc => Ash den = 6 mg/cc => E = 0.1 MPa - image_thres = ogo.bmd_preprocess(image_trans, -31) - image_ash = ogo.bmd_K2hpo4ToAsh(image_thres) - - cortical_mask = None - if compartment_trans is not None: - cortical_mask = cortical_compartment_mask( - compartment_trans, - cortical_label=cortical_label, - trabecular_label=trabecular_label, - ) - if smooth_resampled_masks: - ogo.message("smoothing derived cortical compartment mask...") - cortical_mask = smooth_binary_mask_vtk(cortical_mask, close_iter=1, open_iter=1) - binned_image, bin_centers = ogo.density2materialID( - image_ash, - n_bins=128, - cort_mask=cortical_mask, - ) - - ## - # Set up the FE model. - ogo.message("Setting up the Finite Element Model...") - # Cast the image to Short to "round" float values to nearest whole number - cast_image = ogo.cast2short(binned_image) - cast_mask = ogo.cast2unsignchar(mask_trans) - - - # Apply the mask to the bone - ogo.message("Applying the bone mask to the image...") - bone_image = ogo.applyMask(cast_image, cast_mask) - - # Ensure connectivity - ogo.message("Performing Image connectivity...") - conn = ogo.imageConnectivity(bone_image) - - - change = ogo.prepareFiniteElementImage(conn) - femur_mask_on_model_grid = ogo.prepareFiniteElementImage(cast_mask) - distal_crop_face_change = ( - ogo.prepareFiniteElementImage(distal_crop_face_trans) if distal_crop_face_trans is not None else None - ) - if distal_crop_face_change is not None: - distal_crop_face_change = ogo.applyMask(distal_crop_face_change, ogo.cast2unsignchar(change)) - if femur_cut_mode == "bbox_ratio": - ogo.message("Skipping model-grid distal shaft cut; bbox-ratio crop face is already transformed.") - else: - model_z_min, model_z_max = femur_z_coverage(change) - model_cut_z = model_z_max - retained_length_mm - if model_z_min > model_cut_z: - if model_z_min - model_cut_z <= 2.0: - model_cut_z = model_z_min - else: - ogo.message( - "Femur scan does not include enough distal shaft after model-grid alignment " - "(required retained length %.1f mm, available approximately %.1f mm)." - % (retained_length_mm, max(0.0, model_z_max - model_z_min)) - ) - sys.exit(1) - change = flat_crop_vtk_image_below_z(change, model_cut_z) - femur_mask_on_model_grid = flat_crop_vtk_image_below_z( - femur_mask_on_model_grid, - model_cut_z, - ) - ogo.message( - "Applied model-grid distal shaft cut z=%8.4f; model mask z coverage [%8.4f, %8.4f]" - % (model_cut_z, model_z_min, model_z_max) - ) - - # Convert image data to hexahedral elements - ogo.message("Meshing image data to elements...") - mesh = ogo.Image2Mesh(change) - - ## - # Set up the Material Table - ogo.message("Setting up the Finite Element Material Table...") - material_table = build_femur_material_table( - bin_centers, - n_bins=128, - elastic_E_func=args.elastic_E_func, - yield_comp_func=args.yield_comp_func, - yield_tens_func=args.yield_tens_func, - cort_elastic_E_func=args.cort_elastic_E_func, - cort_yield_comp_func=args.cort_yield_comp_func, - cort_yield_tens_func=args.cort_yield_tens_func, - cort_poissons_ratio=args.cort_poissons_ratio, - poissons_ratio=poissons_ratio, - pmma_mat_id=pmma_mat_id, - pmma_E=pmma_E, - pmma_v=pmma_v, - pmma_yield_tension=pmma_yield_tension, - pmma_yield_compression=pmma_yield_compression, - include_cortical=cortical_mask is not None, - ) - ## - # Create the preliminary FE model - ogo.message("Constructing the Finite Element Model...") - model = ogo.applyTestBase(mesh, material_table) - model.ComputeBounds() - mesh_model_bounds = model.GetBounds() - model_bounds = foreground_voxel_center_bounds(femur_mask_on_model_grid) - ogo.message("Model Mesh Bounds: %s" % str(mesh_model_bounds)) - ogo.message("Model Foreground Voxel-Center Bounds: %s" % str(model_bounds)) - - # Define support vectors for the two PMMA contact fixtures. - top_support_vector = (0, 1, 0) - bottom_support_vector = (0, -1, 0) - - ## - # Create PMMA disks from bbox-relative contact planes. The disk normal - # points from the contact plane toward the anatomy; the generated PMMA - # image clears any femur-mask voxels so PMMA never occupies the segmentation. - femoral_head_plane = bbox_relative_fixture_plane( - model_bounds, - center_fraction=FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, - size_fraction=SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, - projection_axis="y", - shape="rectangle", - ) - femoral_head_cap_direction = bbox_relative_fixture_direction( - FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, - projection_axis="y", - ) - ogo.message( - "Femoral Head PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" - % ( - femoral_head_plane["center"], - femoral_head_plane["normal"], - femoral_head_plane["u_axis"], - femoral_head_plane["v_axis"], - femoral_head_plane["size"], - ) - ) - ogo.message("Femoral Head PMMA cap direction: %s" % femoral_head_cap_direction) - femoralHeadPMMA = generate_projected_material_disk_vtk( - change, - surface_vtk_image=femur_mask_on_model_grid, - exclusion_vtk_image=femur_mask_on_model_grid, - center=femoral_head_plane["center"], - normal=femoral_head_plane["normal"], - u_axis=femoral_head_plane["u_axis"], - v_axis=femoral_head_plane["v_axis"], - size=femoral_head_plane["size"], - shape=femoral_head_plane["shape"], - thickness=pmma_thick, - intrusion=pmma_intrusion, - anatomy_constrained=True, - output_value=pmma_mat_id, - ) - - ## - # Creates the greater trochanter pmma cap - greater_trochanter_plane = bbox_relative_fixture_plane( - model_bounds, - center_fraction=GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, - size_fraction=SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, - projection_axis="y", - shape="rectangle", - ) - greater_trochanter_cap_direction = bbox_relative_fixture_direction( - GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, - projection_axis="y", - ) - ogo.message( - "Greater Trochanter PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" - % ( - greater_trochanter_plane["center"], - greater_trochanter_plane["normal"], - greater_trochanter_plane["u_axis"], - greater_trochanter_plane["v_axis"], - greater_trochanter_plane["size"], - ) - ) - ogo.message("Greater Trochanter PMMA cap direction: %s" % greater_trochanter_cap_direction) - greaterTrochanterPMMA = generate_projected_material_disk_vtk( - change, - surface_vtk_image=femur_mask_on_model_grid, - exclusion_vtk_image=femur_mask_on_model_grid, - center=greater_trochanter_plane["center"], - normal=greater_trochanter_plane["normal"], - u_axis=greater_trochanter_plane["u_axis"], - v_axis=greater_trochanter_plane["v_axis"], - size=greater_trochanter_plane["size"], - shape=greater_trochanter_plane["shape"], - thickness=pmma_thick, - intrusion=pmma_intrusion, - anatomy_constrained=True, - output_value=pmma_mat_id, - ) - - ## - # combines the pmma cap images with the model image. - ogo.message("Combine PMMA Cap Images with Model Image...") - combinedImage = ogo.combineImageData_SF(change, femoralHeadPMMA, greaterTrochanterPMMA, pmma_mat_id) - - ## - # Mesh the final image and create Finite Element Model - # Ensure connectivity - ogo.message("Performing Image connectivity...") - conn2 = ogo.imageConnectivity(combinedImage) - - # Convert image data to hexahedral elements - ogo.message("Meshing image data to elements...") - mesh2 = ogo.Image2Mesh(conn2) - - ## - # Create the final FE model - ogo.message("Constructing the Finite Element Model...") - model2 = ogo.applyTestBase(mesh2, material_table) - model2.ComputeBounds() - model2_bounds = model2.GetBounds() - ogo.message("Model 2 Bounds: %s" % str(model2_bounds)) - - ## - # Determine Femoral Head PMMA Cap support nodes. - ogo.message("Determining Femoral Head PMMA Cap nodes...") - if femoral_head_cap_direction == "up": - fh_pmma_bounds = ( - model2_bounds[0], - model2_bounds[1], - model2_bounds[3] - 1, - model2_bounds[3], - model2_bounds[4], - model2_bounds[5], - ) - femoral_head_support_vector = top_support_vector - else: - fh_pmma_bounds = ( - model2_bounds[0], - model2_bounds[1], - model2_bounds[2], - model2_bounds[2] + 1, - model2_bounds[4], - model2_bounds[5], - ) - femoral_head_support_vector = bottom_support_vector - fh_pmma_visible_node_IDS = directional_face_node_ids_from_voxel_mask( - model2, - femoralHeadPMMA, - direction=femoral_head_support_vector, - name=FEMORAL_HEAD_NODE_SET, - ) - - ogo.message("-- found %d outer-face nodes on Femoral Head PMMA Cap." - % fh_pmma_visible_node_IDS.GetNumberOfTuples()) - model2.AddNodeSet(fh_pmma_visible_node_IDS) - - ## - # Determine Greater Trochanter PMMA Cap support nodes - ogo.message("Determining Greater Trochanter PMMA Cap support nodes...") - if greater_trochanter_cap_direction == "up": - gt_pmma_bounds = ( - model2_bounds[0], - model2_bounds[1], - model2_bounds[3] - 1, - model2_bounds[3], - model2_bounds[4], - model2_bounds[5], - ) - greater_trochanter_support_vector = top_support_vector - else: - gt_pmma_bounds = ( - model2_bounds[0], - model2_bounds[1], - model2_bounds[2], - model2_bounds[2] + 1, - model2_bounds[4], - model2_bounds[5], - ) - greater_trochanter_support_vector = bottom_support_vector - gt_pmma_visible_node_IDS = directional_face_node_ids_from_voxel_mask( - model2, - greaterTrochanterPMMA, - direction=greater_trochanter_support_vector, - name=GREATER_TROCHANTER_NODE_SET, - ) - - ogo.message("-- found %d outer-face nodes on Greater Trochanter PMMA Cap." - % gt_pmma_visible_node_IDS.GetNumberOfTuples()) - - model2.AddNodeSet(gt_pmma_visible_node_IDS) - - ## - # Distal Femur (df) support nodes. In bbox-ratio mode the distal shaft cut - # was made before alignment, so its transformed crop-face label carries the - # correct oblique support angle in model coordinates. - ogo.message("Determining distal femur nodes...") - if femur_cut_mode == "bbox_ratio": - if distal_crop_face_change is None: - ogo.message("BBox-ratio cut mode requires a transformed distal crop-face mask.") - sys.exit(1) - try: - distal_crop_face_plane = crop_face_contact_plane(distal_crop_face_change, change) - distal_plane = bbox_relative_oriented_contact_plane( - model_bounds, - center_fraction=DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, - size_fraction=DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, - normal=distal_crop_face_plane["normal"], - shape="anatomy", - ) - except ValueError as exc: - ogo.message(str(exc)) - sys.exit(1) - ogo.message( - "Distal Femur bbox-relative oblique support plane: center=%s normal=%s outward=%s size=%s" - % ( - distal_plane["center"], - distal_plane["normal"], - distal_plane["outward_normal"], - distal_plane["size"], - ) - ) - distal_surface = projected_crop_face_surface_vtk( - change, - distal_plane, - intrusion=pmma_intrusion, - output_value=1, - ) - df_visible_node_IDS = interface_node_ids_from_voxel_mask( - model2, - distal_surface, - change, - name=DISTAL_FEMUR_NODE_SET, - direction=distal_plane["normal"], - ) - if df_visible_node_IDS.GetNumberOfTuples() == 0: - ogo.message("No distal femur nodes found on the bbox-relative oblique shaft support surface.") - sys.exit(1) - else: - distal_cut_z = model2_bounds[4] - ogo.message("Distal Femur Cut Plane z: %8.4f" % distal_cut_z) - df_visible_node_IDS = find_nodes_on_coordinate_plane(model2, "z", distal_cut_z) - - ogo.message("-- found %d distal shaft interface nodes." - % df_visible_node_IDS.GetNumberOfTuples()) - - df_visible_node_IDS.SetName(DISTAL_FEMUR_NODE_SET) - model2.AddNodeSet(df_visible_node_IDS) - - ## - # Apply Boundary conditions to PMMA caps at specific sites - ogo.message("Applying displacement boundary condition to Femoral Head PMMA cap...") - model2.ApplyBoundaryCondition( - FEMORAL_HEAD_NODE_SET, - vtkbone.vtkboneConstraint.SENSE_Y, - fe_displacement, - "top_displacement") - - ogo.message("Constraining Greater Trochanter PMMA cap in loading direction...") - model2.ApplyBoundaryCondition( - GREATER_TROCHANTER_NODE_SET, - vtkbone.vtkboneConstraint.SENSE_Y, - 0, - "bottom_fixed_y_PMMA") - - for SENSE, label in ( - (vtkbone.vtkboneConstraint.SENSE_X, "x"), - (vtkbone.vtkboneConstraint.SENSE_Z, "z"), - ): - ogo.message("Constraining distal femur rigid-body motion...") - model2.ApplyBoundaryCondition( - DISTAL_FEMUR_NODE_SET, - SENSE, - 0, - f"bottom_fixed_{label}") - - ## - # Post Processing parameters - ogo.message("Setting up Post Processing Parameters...") - append_postprocessing_sets( - model2, - SIDEWAYS_FALL_NODE_SETS, - ) - - model2.AppendHistory( - "Created by %s version %s." % (script_name, script_version)) - - ## - # Write out n88model file - ogo.message("Writing out n88model file: %s" % N88_fileName) - write_model(model2, N88_fileName) - - ## - ogo.message("Done writing n88model.") - - ## - # End of script - ogo.message("End of Script.") - sys.exit() - - -def main(): - # Setup description - description=''' -This script sets up the sideways fall FE model on the hip from the - density (K2HPO4) calibrated image. This script sets up the model for either a left or - right femur, as specified by the user. The analysis resamples the image to isotropic - voxels, transforms the image, applies the bone mask and bins the data. It then - creates the FE model for solving using FAIM (v8.1, Numerics Solutions Ltd, Calgary, - Canada - Steven Boyd). - - Input: Calibrated K2HPO4 Image (*.nii), Bone Mask (*_MASK.nii) - - Optional Parameters: - 1) Isotropic resample voxel size - 2) Left or Right Femur Bone Mask - - Output: N88 Model (*.n88model) - -''' - - - # Setup argument parsing - parser = argparse.ArgumentParser( - formatter_class=argparse.RawTextHelpFormatter, - prog="OgoSidewaysFallFe", - description=description - ) - - - parser.add_argument("calibrated_image", - help = "*_K2HPO4.nii image file") - parser.add_argument("bone_mask", - help = "*_MASK.nii mask image of bone") - - parser.add_argument("--mask_threshold", type = int, - default = 1, - help = "Set the threshold value to extract the bone of interest from the mask. (Default: %(default)s)") - parser.add_argument("--iso_resolution", type = float, default = DEFAULT_FEMUR_ISO_RESOLUTION_MM, - help = "Set the isotropic voxel size [in mm]. (default: %(default)s [mm])") - parser.add_argument("--mask_smoothing_spacing_threshold", type=float, default=DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - help="Smooth resampled femur masks only when an input spacing dimension exceeds this value. (default: %(default)s [mm])") - parser.add_argument("--femur_side", type = int, default = 1, - help = "Set whether the left or right femur is to be analyzed. 1 = Left; 2 = Right. (default: %(default)s)") - parser.add_argument("--output_path", type=str, default=None, - help="Set output path for the N88 model file. (default: same as input image)") - parser.add_argument("--poissons_ratio", type=float, default=0.3, - help="Sets the Poisson's ratio for the material(s) in the FE model. (default: %(default)s)") - parser.add_argument("--elastic_E_func", type=str, default=None, - help="Function name for trabecular bone Young's modulus. (default: default_E)") - parser.add_argument("--yield_comp_func", type=str, default=None, - help="Function name for trabecular compression yield. (default: none)") - parser.add_argument("--yield_tens_func", type=str, default=None, - help="Function name for trabecular tension yield. (default: none)") - parser.add_argument("--cort_elastic_E_func", type=str, default=None, - help="Function name for cortical bone Young's modulus. Defaults to --elastic_E_func.") - parser.add_argument("--cort_yield_comp_func", type=str, default=None, - help="Function name for cortical compression yield. Defaults to --yield_comp_func.") - parser.add_argument("--cort_yield_tens_func", type=str, default=None, - help="Function name for cortical tension yield. Defaults to --yield_tens_func.") - parser.add_argument("--cort_poissons_ratio", type=float, default=None, - help="Poisson's ratio for cortical bone. Defaults to --poissons_ratio.") - parser.add_argument("--pmma_E", type=float, default=2500, - help="Sets the Elastic Modulus for PMMA caps in the FE model. (default: %(default)s [MPa])") - parser.add_argument("--pmma_v", type=float, default=0.3, - help="Sets the Poisson's ratio for the PMMA material(s) in the FE model. (default: %(default)s)") - parser.add_argument("--pmma_thick", type=float, default=DEFAULT_PMMA_THICKNESS_MM, - help="Sets the fixed thickness for PMMA caps in the FE model. (default: %(default)s [mm])") - parser.add_argument("--pmma_intrusion", type=float, default=DEFAULT_PMMA_INTRUSION_MM, - help="Sets how far anatomy can occupy the fixed PMMA fixture thickness before bone overlap is preserved during material combination. (default: %(default)s [mm])") - parser.add_argument("--pmma_mat_id", type=int, default=5000, - help="Sets the material ID for the PMMA blocks. (default: %(default)s)") - parser.add_argument("--fe_displacement", type=float, default=DEFAULT_FEMUR_FE_DISPLACEMENT, - help="Sets the applied displacement endpoint for the sideways-fall model. The default reports the force at 4%% displacement. (default: %(default)s)") - parser.add_argument("--femur_shaft_length", type=float, default=DEFAULT_FEMUR_SHAFT_LENGTH_MM, - help="Retained proximal femur length [mm] for --femur_cut_mode fixed_length. (default: %(default)s [mm])") - parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "lesser_trochanter", "fixed_length"], - default=DEFAULT_FEMUR_CUT_MODE, - help="Set the distal femur crop. bbox_ratio crops in input image coordinates and uses the transformed crop face for distal support. (default: %(default)s)") - parser.add_argument("--femur_bbox_ratio", nargs=3, default=DEFAULT_FEMUR_BBOX_RATIO, - help="BBox-ratio crop in recipe order: reference constrained free. Use 'none' for a free axis. (default: %(default)s)") - parser.add_argument("--femur_bbox_crop_from", nargs=3, default=DEFAULT_FEMUR_BBOX_CROP_FROM, - help="BBox crop side in recipe order: reference constrained free. Values are min, max, center/null. (default: %(default)s)") - parser.add_argument("--femur_lesser_trochanter_distal_offset", type=float, default=DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, - help="Cut this many mm distal to the detected lesser trochanter in lesser_trochanter mode. (default: %(default)s [mm])") - parser.add_argument("--femur_lesser_trochanter_distal_offset_percent", type=float, default=None, - help="Optional percentage of the detected greater-to-lesser trochanter z distance to cut distal to the lesser trochanter. Overrides --femur_lesser_trochanter_distal_offset when set. (default: %(default)s)") - parser.add_argument("--femur_input_margin", type=float, default=DEFAULT_FEMUR_INPUT_MARGIN_MM, - help="Pad the input image/mask as needed so femur foreground has this margin before ICP. (default: %(default)s [mm])") - parser.add_argument("--compartment_mask", type=str, default=None, - help="Optional trabecular/cortical compartment mask aligned with the bone mask. Defaults: cortical=1, trabecular=2.") - parser.add_argument("--cortical_label", type=int, default=DEFAULT_CORTICAL_LABEL, - help="Label value for cortical bone in --compartment_mask. (default: %(default)s)") - parser.add_argument("--trabecular_label", type=int, default=DEFAULT_TRABECULAR_LABEL, - help="Label value for trabecular bone in --compartment_mask. (default: %(default)s)") - parser.add_argument("--reference_path", type=str, required=False, default=None, - help="Path to the reference vtk file for ICP registration. (default: None)") - parser.add_argument("--pmma_yield_compression", type=float, default=None, - help="Sets the yield strength in compression for PMMA material in the FE model. (default: %(default)s [MPa])") - parser.add_argument("--pmma_yield_tension", type=float, default=None, - help="Sets the yield strength in tension for PMMA material in the FE model. (default: %(default)s [MPa])") - # Parse and display - args = parser.parse_args() - - # Set default reference paths - if args.reference_path is None: - data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "dat") - args.left_femur_reference = os.path.join(data_dir, "LT_FEMUR_SIDEWAYS_FALL_REF.vtk") - args.right_femur_reference = os.path.join(data_dir, "RT_FEMUR_SIDEWAYS_FALL_REF.vtk") - else: - args.left_femur_reference = os.path.join(args.reference_path, "LT_FEMUR_SIDEWAYS_FALL_REF.vtk") - args.right_femur_reference = os.path.join(args.reference_path, "RT_FEMUR_SIDEWAYS_FALL_REF.vtk") - - - basename = remove_extension(os.path.basename(args.calibrated_image)) - - if args.output_path is None: - output_dir = os.path.dirname(args.calibrated_image) - else: - output_dir = args.output_path - - output_dir = os.path.abspath(output_dir) - - args.output_file = os.path.join(output_dir, f"{basename}.n88model") - - print(echo_arguments('ogoSidewaysFallFe', vars(args))) - - # Run program - sidewaysFallFe(args) - -if __name__ == '__main__': - main() diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 83cc4f6..3b014e4 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -127,8 +127,6 @@ def prepare_benchmark_images(input_image_path, input_mask_path, n_bins=128): """Reproduce the spineFE-benchmark notebook image and mask preparation.""" import numpy as np - from ogo.fea.spine_compression import convert_image_to_material, merge_vtk_images, read - input_image = read(str(input_image_path)).GetOutput() input_mask_with_disk = read(str(input_mask_path)).GetOutput() @@ -179,3 +177,1369 @@ def find_spinefe_benchmark_dir(start_dir=None, env_var="SPINEFE_BENCHMARK_DIR"): if candidate.exists(): return candidate return None + + +# ----------------------------------------------------------------------------- +# Spine compression workflow builder +# ----------------------------------------------------------------------------- + +import os +import argparse +import os +from glob import glob +import ogo.fea.material_laws + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import SimpleITK as sitk +from matplotlib import pyplot as plt +from scipy.ndimage import ( + binary_dilation, + binary_erosion, + find_objects, + label, +) +from skimage.exposure import rescale_intensity +import vtk +import vtkbone +from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + +from ogo.fea.alignment import ( + estimate_rigid_icp, + invert_point_transform, + output_grid_for_point_transform, + point_cloud_axis_lengths, + polydata_points, + resample_vtk_image_with_point_transform, + sample_points, + surface_points_from_vtk_mask, +) +from ogo.fea.boundary import ( + bbox_relative_contact_plane, + foreground_voxel_center_bounds, + generate_projected_material_disk_vtk, + pad_vtk_images_to_physical_bounds, + projected_material_disk_required_bounds, + should_smooth_resampled_mask, + smooth_binary_mask_vtk, + smooth_label_mask_vtk, +) +from ogo.fea.model import ( + apply_spine_boundary_conditions as apply_boundary_conditions, + create_microfe_model, + find_and_add_visible_nodes, + log_fe_arguments, +) +import ogo.util.Helper as ogo +from ogo.util.echo_arguments import echo_arguments +from scipy.ndimage import gaussian_filter + + +vtk.vtkObject.GlobalWarningDisplayOff() + + +def _as_bool(value): + if isinstance(value, bool): + return value + text = str(value).strip().lower() + if text in {"1", "true", "t", "yes", "y", "on"}: + return True + if text in {"0", "false", "f", "no", "n", "off"}: + return False + raise argparse.ArgumentTypeError(f"Invalid boolean value: {value}") + + +###################################################################### HELPERS (Python) +## General helper functions: +###################################################################### HELPERS (Python) + +def remove_extension(filename): + while True: + filename, ext = os.path.splitext(filename) + if not ext: + break + return filename + +def print_matrix(matrix): + for i in range(4): + # Create a string for the entire row + row_message = " ".join(f"{matrix.GetElement(i, j):0.4f}" for j in range(4)) + # Send the row as a single message + ogo.message(row_message) + + +def parse_scale_triplet(value, name="scale"): + if value is None: + return None + if isinstance(value, (int, float)): + return np.array([float(value)] * 3, dtype=float) + parts = [part.strip() for part in str(value).replace("x", ",").split(",") if part.strip()] + try: + values = [float(part) for part in parts] + except ValueError as exc: + raise ValueError(f"{name} must be a float or three comma-separated floats.") from exc + if len(values) == 1: + values = values * 3 + if len(values) != 3: + raise ValueError(f"{name} must be a float or three comma-separated floats.") + return np.array(values, dtype=float) + + +###################################################################### HELPERS (VTK) +## VTK Helper functions: +###################################################################### HELPERS (VTK) + + +class _NiftiImageHandle: + """Reader-like wrapper for NIfTI images loaded through Ogo's orientation path.""" + + def __init__(self, image): + self._image = image + + def GetOutput(self): + return self._image + + +def read(input_mask): + return _NiftiImageHandle(ogo.readNii(input_mask)) + +def check_vertebra_presence(mask_reader, vertebra): + if vertebra not in vtk_to_numpy(mask_reader.GetOutput().GetPointData().GetScalars()).ravel(): + raise ValueError(f'Mask does not contain label {vertebra} for "{vertebra}" vertebra.') + +def threshold(mask_output, label): + threshold = vtk.vtkImageThreshold() + threshold.SetInputData(mask_output) + threshold.ThresholdBetween(float(label), float(label)) + threshold.ReplaceInOn() + threshold.SetInValue(1) + threshold.ReplaceOutOn() + threshold.SetOutValue(0) + threshold.SetOutputScalarTypeToUnsignedChar() + threshold.Update() + return threshold + +def label_mask(mask, label_value): + # Use vtkImageMathematics to change 1 values to a specific label value + + caster = vtk.vtkImageCast() + caster.SetInputData(mask) + caster.SetOutputScalarTypeToUnsignedChar() + caster.ClampOverflowOn() # Optional, clamps values that are out of range of the target data type + caster.Update() + + math = vtk.vtkImageMathematics() + math.SetInputData(caster.GetOutput()) + math.SetOperationToMultiplyByK() + math.SetConstantK(label_value) + math.Update() + return math + +def combine_mask(mask1, mask2): + # Combine two masks using vtkImageLogic with a logical OR operation + logic = vtk.vtkImageLogic() + logic.SetInput1Data(mask1) + logic.SetInput2Data(mask2) + logic.SetOperationToOr() + logic.Update() + return logic + +def add_masks(mask1, mask2): + # Combine two masks by adding their values + math = vtk.vtkImageMathematics() + math.SetInput1Data(mask1) + math.SetInput2Data(mask2) + math.SetOperationToAdd() + math.Update() + + # Get the result of addition + added_mask = math.GetOutput() + + # Apply thresholding to ensure the max value is 2 + threshold = vtk.vtkImageThreshold() + threshold.SetInputData(added_mask) + threshold.ThresholdByUpper(2) # Set upper threshold limit to 2 + threshold.SetReplaceIn(True) + threshold.SetInValue(2) # Values above the upper threshold will be set to 2 + threshold.SetOutputScalarTypeToUnsignedChar() + threshold.Update() + + return threshold + +def crop_to_bounding_box(image_data, bb=None): + if bb is None: + + extents = image_data.GetExtent() + scalars = vtk_to_numpy(image_data.GetPointData().GetScalars()) + reshaped_scalars = scalars.reshape((extents[5]-extents[4]+1, extents[3]-extents[2]+1, extents[1]-extents[0]+1)) + + nz = np.nonzero(reshaped_scalars) + + if not nz[0].size: + raise ValueError("No non-zero voxels found after thresholding. Check earlier steps.") + + min_x, max_x = np.min(nz[2])+extents[0], np.max(nz[2])+extents[0] + min_y, max_y = np.min(nz[1])+extents[2], np.max(nz[1])+extents[2] + min_z, max_z = np.min(nz[0])+extents[4], np.max(nz[0])+extents[4] + bb = [min_x, max_x, min_y, max_y, min_z, max_z] + else: + min_x, max_x, min_y, max_y, min_z, max_z = bb + + + ogo.message(f"Cropping to extents: X({min_x} to {max_x}), Y({min_y} to {max_y}), Z({min_z} to {max_z})") + + crop = vtk.vtkImageClip() + crop.SetInputData(image_data) + crop.SetOutputWholeExtent(min_x, max_x, min_y, max_y, min_z, max_z) + crop.ClipDataOn() + crop.Update() + + if crop.GetOutput().GetNumberOfPoints() == 0: + raise ValueError("Cropping resulted in an empty image. Extents might be incorrect.") + + output = crop.GetOutput() + spacing = output.GetSpacing() + origin = image_data.GetOrigin() + output.SetOrigin( + float(origin[0]) + float(min_x) * float(spacing[0]), + float(origin[1]) + float(min_y) * float(spacing[1]), + float(origin[2]) + float(min_z) * float(spacing[2]), + ) + dims = output.GetDimensions() + output.SetExtent(0, dims[0] - 1, 0, dims[1] - 1, 0, dims[2] - 1) + + return crop, bb + +def pad_vtk_image(vtk_image, axis='x', pmma_thick=5, pad_value=0): + """ + Pad a VTK image along the specified axis with a given pmma_thick while preserving VTK metadata. + + Parameters: + - vtk_image: The input VTK image to pad. + - axis: The axis along which to pad ('x', 'y', or 'z'). + - pmma_thick: Number of voxels to pad on each side along the specified axis. + - pad_value: The value to pad with (default: 0). + + Returns: + - padded_vtk_image: The padded VTK image with updated metadata. + """ + # Map the axis to the corresponding dimension index + axis_map = {'x': 0, 'y': 1, 'z': 2} + if axis not in axis_map: + raise ValueError("Axis must be one of 'x', 'y', or 'z'.") + extrusion_axis = axis_map[axis] + + # Convert VTK image data to a NumPy array + input_array = vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape( + vtk_image.GetDimensions(), order='F' + ) + + # Define the padding configuration + pad_width = [(0, 0)] * 3 # Default no padding + pad_width[extrusion_axis] = (pmma_thick, pmma_thick) # Apply padding along the target axis + + # Apply padding + padded_array = np.pad(input_array, pad_width=pad_width, mode='constant', constant_values=pad_value) + + # Update VTK metadata + padded_origin = list(vtk_image.GetOrigin()) + padded_dimensions = list(padded_array.shape) + spacing = vtk_image.GetSpacing() + + # Adjust the origin for padding in the negative direction + padded_origin[extrusion_axis] -= pmma_thick * spacing[extrusion_axis] + + # Convert the padded array back to VTK format + padded_vtk_image = vtk_image.NewInstance() + padded_vtk_image.SetDimensions(padded_dimensions) + padded_vtk_image.SetOrigin(padded_origin) + padded_vtk_image.SetSpacing(spacing) + padded_vtk_image.GetPointData().SetScalars(numpy_to_vtk(padded_array.ravel(order='F'))) + + return padded_vtk_image + + +def merge_vtk_images(image_list, label_list=None, overwrite_existing=True): + """ + Merges an arbitrary list of vtkImageData objects into a single vtkImageData. + Optionally assigns specific label values for each image. + + Parameters: + - image_list: List of vtkImageData objects to merge. + - label_list: Optional list of label values corresponding to each image. + If None or an entry in the list is None, uses the original values in the image. + - overwrite_existing: If False, labeled images only write into zero-valued voxels already + present in the merged image. + + Returns: + - merged_image: vtkImageData containing the merged result. + """ + if not image_list: + raise ValueError("The image list is empty.") + + if label_list is not None and len(label_list) != len(image_list): + raise ValueError("The label list must be the same length as the image list.") + + # Get metadata from the first image + reference_image = image_list[0] + dims = reference_image.GetDimensions() + spacing = reference_image.GetSpacing() + origin = reference_image.GetOrigin() + + # Initialize the merged array as zeros with the same dimensions as the reference image + merged_array = np.zeros(dims, dtype=np.float32) + + # Merge each image into the merged array + for idx, image in enumerate(image_list): + image_array = vtk_to_numpy(image.GetPointData().GetScalars()).reshape(dims, order="F") + if label_list is not None and label_list[idx] is not None: + label_value = label_list[idx] + # Replace non-zero values in the image with the label value + write_mask = image_array > 0 + if not overwrite_existing: + write_mask &= merged_array == 0 + merged_array[write_mask] = label_value + else: + # Add the original values of the image where label is None + merged_array += image_array + + # Convert the merged array back to VTK format + merged_vtk_array = numpy_to_vtk(merged_array.ravel(order="F"), deep=True) + + # Create a new vtkImageData to hold the merged result + merged_image = vtk.vtkImageData() + merged_image.SetDimensions(dims) + merged_image.SetSpacing(spacing) + merged_image.SetOrigin(origin) + merged_image.GetPointData().SetScalars(merged_vtk_array) + + return merged_image + +###################################################################### REGISTRATION +## functions related to ICP registration +###################################################################### REGISTRATION + +def crop_and_transform(fullvertebra, body_output, process_output, image): + + _, bb = crop_to_bounding_box(fullvertebra.GetOutput()) + isolated_vertebra, _ = crop_to_bounding_box(body_output, bb) + isolated_process, _ = crop_to_bounding_box(process_output, bb) + isolated_image, _ = crop_to_bounding_box(image, bb) + + + return isolated_vertebra, isolated_process, isolated_image + +def perform_marching_cubes(body_output): + mcubes = vtk.vtkImageMarchingCubes() + mcubes.SetInputConnection(body_output.GetOutputPort()) + mcubes.SetValue(1, 1.0) + mcubes.Update() + if mcubes.GetOutput().GetNumberOfPoints() == 0: + raise ValueError("No data was generated by the Marching Cubes algorithm.") + return mcubes + + +def get_icp_with_scaling( + body, + reference_path, + scale_factors=None, + min_scale=(0.8, 0.8, 0.75), + max_scale=(1.2, 1.2, 1.3), +): + + sample_surface_points = surface_points_from_vtk_mask( + body.GetOutput(), + max_points=8000, + sample_mode="stride", + ) + reference_polydata = ogo.readPolyData(reference_path) + reference_points = sample_points( + polydata_points(reference_polydata), + max_points=8000, + mode="linspace", + ) + + sample_lengths = point_cloud_axis_lengths(sample_surface_points) + ref_lengths = point_cloud_axis_lengths(reference_points) + if scale_factors is None: + scale_factors = sample_lengths / ref_lengths + min_scale = parse_scale_triplet(min_scale, "registration_min_scale") + max_scale = parse_scale_triplet(max_scale, "registration_max_scale") + scale_factors = np.minimum(scale_factors, max_scale) + scale_factors = np.maximum(scale_factors, min_scale) + scale_source = "voxel-surface PCA axis lengths" + else: + scale_factors = parse_scale_triplet(scale_factors, "registration_scale") + scale_source = "manual override" + + ogo.message(f"PCA axis lengths (sample): {np.round(sample_lengths, 2)}") + ogo.message(f"PCA axis lengths (reference): {np.round(ref_lengths, 2)}") + ogo.message(f"Scale factors applied to reference ({scale_source}): {np.round(scale_factors, 3)}") + + transform = estimate_rigid_icp( + moving_points=reference_points * scale_factors, + fixed_points=sample_surface_points, + iterations=50, + tolerance=1.0e-4, + start_by_matching_centroids_only=False, + convergence="delta", + distance_mode="mean", + ) + + matrix = vtk.vtkMatrix4x4() + matrix.Identity() + rotation = transform["rotation"] + translation = transform["translation"] + for row in range(3): + for col in range(3): + matrix.SetElement(row, col, float(rotation[row, col])) + matrix.SetElement(row, 3, float(translation[row])) + + vtk_transform = vtk.vtkTransform() + vtk_transform.SetMatrix(matrix) + ogo.message( + "ICP (with scaled reference) iterations=%d mean_distance=%0.4f" + % (transform["iterations"], transform["mean_distance"]) + ) + ogo.message("ICP reference-to-sample Matrix:") + print_matrix(vtk_transform.GetMatrix()) + + return vtk_transform + +def get_icp(body, reference_path): + + mcubes = perform_marching_cubes(body) + mcubes_output = mcubes.GetOutput() + reference_bone = ogo.readPolyData(reference_path) + + icp = vtk.vtkIterativeClosestPointTransform() + icp.SetTarget(mcubes_output) + icp.SetSource(reference_bone) + + icp.StartByMatchingCentroidsOn() + icp.GetLandmarkTransform().SetModeToRigidBody() + icp.SetMeanDistanceModeToRMS() + icp.SetMaximumMeanDistance(0.05) + icp.CheckMeanDistanceOn() + icp.SetMaximumNumberOfLandmarks(250) + icp.SetMaximumNumberOfIterations(75) + icp.Update() + + ogo.message("ICP Matrix:") + print_matrix(icp.GetMatrix()) + return icp + +def transform_resample(image, matrix, iso_resolution, interpolation='cubic'): + output = ogo.transformResample(image, matrix, iso_resolution, interpolation=interpolation) + if output.GetNumberOfPoints() == 0: + ogo.message("Reslice output contains no points. Check the input and transformation.") + return output + + +def _matrix_rotation_translation(matrix): + rotation = np.asarray( + [[matrix.GetElement(row, col) for col in range(3)] for row in range(3)], + dtype=float, + ) + translation = np.asarray([matrix.GetElement(row, 3) for row in range(3)], dtype=float) + return rotation, translation + +###################################################################### DISK GENERATION +## Functions related to disk generation (image processing) +###################################################################### DISK GENERATION + +def identify_boundary_surface(input_vtk_image, top_value, bottom_value): + """Transforms a VTK image to highlight the topmost and bottommost components. + + Parameters: + input_vtk_image (vtk.vtkImageData): The input binary mask as a VTK image. + top_value (int): The value to assign to the top mask. + bottom_value (int): The value to assign to the bottom mask. + + Returns: + vtk.vtkImageData: The modified VTK image with the top and bottom masks highlighted. + """ + # Convert VTK image data to NumPy array + input_array = vtk_to_numpy(input_vtk_image.GetPointData().GetScalars()).reshape(input_vtk_image.GetDimensions(), order='F') + input_array = np.swapaxes(input_array, 0, 2) # Swap axes for processing along correct dimension + + binary_mask = np.copy(input_array) == 2 + + # Apply boundary conditions from the original method + eroded_mask = binary_erosion(binary_mask, iterations=1) + #dilated_mask = binary_dilation(binary_mask, iterations=1) + outline_mask = (eroded_mask == 0) & (binary_mask == 1) + + # Highlight the outline + struct_element_erosion = np.array([[[0, 1, 0], [1, 1, 1], [0, 1, 0]]]) + highlighted_mask = binary_erosion(outline_mask, structure=struct_element_erosion) + + struct_element_dilation = np.zeros((3, 3, 3)) + struct_element_dilation[1, 1, :] = 1 + highlighted_mask = binary_dilation(highlighted_mask, iterations=2, structure=struct_element_dilation) + highlighted_mask = binary_dilation(highlighted_mask, iterations=1) + highlighted_mask[binary_mask == 0] = 0 + + labeled_mask, num_features = label(highlighted_mask) + sizes = np.bincount(labeled_mask.ravel())[1:] + sorted_indices = np.argsort(sizes)[::-1] + object_slices = find_objects(labeled_mask) + + top_positions = [(i + 1, object_slices[i][0].start) for i in sorted_indices[:2]] + top_positions.sort(key=lambda x: x[1]) + top_component, bottom_component = top_positions + + top_mask = binary_dilation(labeled_mask == top_component[0], iterations=2) & outline_mask + bottom_mask = binary_dilation(labeled_mask == bottom_component[0], iterations=2) & outline_mask + + # Assign values and convert back to VTK + result_array = input_array.copy() + result_array[top_mask] = top_value + result_array[bottom_mask] = bottom_value + result_array[input_array==0]=0 + result_array = np.swapaxes(result_array, 0, 2) # Swap axes for processing along correct dimension + + vtk_data_array = numpy_to_vtk(result_array.ravel(order='F'), deep=True) + output_vtk_image = vtk.vtkImageData() + output_vtk_image.DeepCopy(input_vtk_image) + output_vtk_image.GetPointData().SetScalars(vtk_data_array) + + return output_vtk_image + +###################################################################### MICRO-FE (FAIM) +## Functions related to micro-FE model +###################################################################### MICRO-FE (FAIM) + +def generate_cortical_mask(image_vtk, mask_vtk, threshold=0.2, min_th=1, max_th=3): + """ + Generate a cortical mask from the input VTK image and mask using a threshold. + Ensures the cortical mask is at least as thick as the erosion shell. + + Parameters: + - image_vtk: The input image data (VTK Image Data). + - mask_vtk: The input mask data (VTK Image Data). + - threshold: The threshold value for binarization (default: 0.2). + - min_th: Minimum thickness constraint (default: 1 voxel). + - max_th: Maximum thickness constraint (default: 3 voxels). + + Returns: + - cortical_mask_vtk: The binary mask for the cortical region as a VTK Image Data. + """ + + # Convert VTK images to numpy arrays + image_np = vtk_to_numpy(image_vtk.GetPointData().GetScalars()).reshape(image_vtk.GetDimensions(), order='F').astype(np.float32) + #image_np = gaussian_filter(image_np, sigma=1) # Adjust sigma as needed + + mask_np = vtk_to_numpy(mask_vtk.GetPointData().GetScalars()).reshape(mask_vtk.GetDimensions(), order='F') > 0 + + # Apply thresholding to the image (convert to g/ccm by dividing by 1000) + binary_image = (image_np / 1000) > threshold + + # Compute cortical mask as intersection of thresholded image and mask + cortical_mask = binary_image & mask_np + + ogo.message(f'Average Image intensity for cort mask {np.mean(image_np[cortical_mask])}') + ogo.message(f'Average Image intensity for trab mask {np.mean(image_np[~cortical_mask & mask_np])}') + + + # Compute the shell (minimum thickness enforcement) + if min_th>0: + min_erosion = binary_erosion(mask_np, iterations=min_th, border_value=0) + min_shell = mask_np & ~min_erosion # Shell is the difference between the mask and its eroded version + else: + min_shell = np.zeros_like(mask_np) + + # Apply maximum thickness constraint + if max_th>0: + max_erosion = binary_erosion(mask_np, iterations=max_th, border_value=0) + max_shell = mask_np & ~max_erosion # Shell is the difference between the mask and its eroded version + else: + max_shell = np.ones_like(mask_np) + + # Ensure cortical mask is at least the shell + cortical_mask = (cortical_mask | min_shell) & max_shell # Ensure at least the shell is included + + # Convert the binary mask back to VTK format + cortical_mask_vtk = vtk.vtkImageData() + cortical_mask_vtk.DeepCopy(mask_vtk) # Copy spatial properties + + # Convert numpy array back to VTK format + mask_vtk_array = numpy_to_vtk(cortical_mask.flatten(order='F'), deep=True, array_type=vtk.VTK_UNSIGNED_CHAR) + cortical_mask_vtk.GetPointData().SetScalars(mask_vtk_array) + + ogo.message(f'Average Image intensity for cort mask {np.mean(image_np[cortical_mask])}') + ogo.message(f'Average Image intensity for trab mask {np.mean(image_np[~cortical_mask & mask_np])}') + + + return cortical_mask_vtk + + +def convert_image_to_material(image, mask, n_bins=128, cort_mask=None): + change = ogo.prepareFiniteElementImage(image) + mask_change = ogo.prepareFiniteElementImage(mask) + cort_mask_change = ogo.prepareFiniteElementImage(cort_mask) if cort_mask is not None else None + connected_image = ogo.imageConnectivity(change) + thr_image = ogo.bmd_preprocess(connected_image, -31) + #this was done previously - but if someone wants this they should just do it in the calib functions + #ash_image = ogo.bmd_K2hpo4ToAsh(thr_image) + cast_image = ogo.cast2short(thr_image) + bone_image = ogo.applyMaskByArray(cast_image, mask_change) + binned_image, bin_centers = ogo.density2materialID(bone_image, n_bins=n_bins, cort_mask=cort_mask_change) + + return binned_image, bin_centers + +def resolve_func(func_or_name, module): + """Resolve a material-law function by object or configured name.""" + from ogo.fea.materials import resolve_material_func + + return resolve_material_func(func_or_name, module) + +###################################################################### QUALITY CONTROL +## Functions to check image and boundary conditions +###################################################################### QUALITY CONTROL + +## Functions to check the results +def check_image_values(image): + array = vtk.util.numpy_support.vtk_to_numpy(image.GetPointData().GetScalars()) + unique_values = np.unique(array) + ogo.message("Unique values in the image:", unique_values) + +def calculate_features(image, label): + label_map = sitk.BinaryThreshold(image, lowerThreshold=label, upperThreshold=label, insideValue=1, outsideValue=0) + stats = sitk.LabelShapeStatisticsImageFilter() + stats.Execute(label_map) + volume = stats.GetPhysicalSize(1) + centroid = stats.GetCentroid(1) + principal_axes = stats.GetPrincipalAxes(1) + extent = stats.GetBoundingBox(1) + return {'volume': volume, 'centroid': centroid, 'principal_axes': principal_axes, 'extent': extent} + +def check_values(features, checks): + results = {} + pass_all = True + for check, condition in checks.items(): + result = eval(condition, {"features": features}) + results[check] = result + if not result: + pass_all = False + results['pass'] = pass_all + return results + +def parse_filename(filepath): + base_name = os.path.splitext(os.path.basename(filepath))[0] + parts = base_name.split('_') + return { + 'ID': parts[0] if len(parts) > 0 else '', + 'TREATMENT': parts[1] if len(parts) > 1 else '', + 'LOCATION': parts[2] if len(parts) > 2 else '', + 'NUMBER': parts[4] if len(parts) > 4 else '', + 'filename': '_'.join(parts[:-3]) if len(parts) > 3 else base_name, + } + +def visualize_slice(image, filepath): + # Get the dimensions of the vtkImageData object + dimensions = image.GetDimensions() # (x, y, z) + mid_x = dimensions[0] // 2 # Middle slice along the X-axis + + # Convert vtkImageData to a NumPy array + vtk_array = vtk_to_numpy(image.GetPointData().GetScalars()) + numpy_array = vtk_array.reshape(dimensions[::-1]) # Reverse dimensions to match NumPy order (z, y, x) + + # Extract the middle X slice + slice_img = numpy_array[:, :, mid_x] + + # Clip values at the 90th percentile + percentile_90 = np.percentile(slice_img, 90) + clipped_slice = np.clip(slice_img, 0, percentile_90) # Cap values at the 90th percentile + + # Normalize the clipped slice to [0, 1] range + normalized_slice = (clipped_slice - clipped_slice.min()) / (clipped_slice.max() - clipped_slice.min() + 1e-8) + + # Plot the normalized slice + plt.figure(figsize=(4.0, 4.0)) + plt.imshow(normalized_slice, cmap='viridis', origin="lower") + plt.title('Middle X', fontsize=9, pad=2) + plt.xlabel('Y', fontsize=8) + plt.ylabel('Z', fontsize=8) + plt.tick_params(axis='both', which='major', labelsize=8, length=2.5, pad=1.5) + + # Save the output image + plt.tight_layout(pad=0.2) + plt.savefig(filepath, dpi=300) + plt.close() + ogo.message(f"Slice image saved to {filepath}") + +def check_image(vtkimage, output_filename=None): + try: + # Assuming 'image' is a vtkImageData object + numpy_array = vtk_to_numpy(vtkimage.GetPointData().GetScalars()) + numpy_array = numpy_array.reshape(vtkimage.GetDimensions(), order='F') + numpy_array = np.transpose(numpy_array, (2, 1, 0)) # Adjust depending on your specific data + image = sitk.GetImageFromArray(numpy_array) + image.SetSpacing(vtkimage.GetSpacing()) + image.SetOrigin(vtkimage.GetOrigin()) + + features = {label: calculate_features(image, label) for label in range(1, 5)} + + checks = { + "VCHECK_PROCESS": "features[1]['volume'] > 4000", + "VCHECK_BODY": "features[2]['volume'] > 3000", + "VCHECK_BCBOT": "features[3]['volume'] > 200", + "VCHECK_BCTOP": "features[4]['volume'] > 200", + "DCHECK_BC": "features[4]['centroid'][2] > features[2]['centroid'][2] > features[3]['centroid'][2]", + "EXTENT_Z_BCBOT": "features[3]['extent'][5] < 20", + "EXTENT_Z_BCTOP": "features[4]['extent'][5] < 20" + } + + results = check_values(features, checks) + metadata = parse_filename(output_filename) + data = {**metadata, **{f'{label}_{key}': val for label, feats in features.items() for key, val in feats.items()}, **results} + + df = pd.DataFrame([data]) + + if output_filename is not None: + check_ending = f"{data['pass']}.csv" + output_filename = output_filename.replace('.csv', check_ending) + ogo.message(f"Saving QC results to {output_filename}") + df.to_csv(output_filename, index=False) + else: + ogo.message(df) + + except Exception as e: + ogo.message(e) + + data = { + "VCHECK_BODY": False, + "VCHECK_PROCESS": False, + "VCHECK_BCBOT": False, + "VCHECK_BCTOP": False, + "DCHECK_BC": False, + "EXTENT_Z_BCBOT": False, + "EXTENT_Z_BCTOP": False, + "pass": False + } + + df = pd.DataFrame([data]) + if output_filename is not None: + check_ending = f"{data['pass']}.csv" + output_filename = output_filename.replace('.csv', check_ending) + ogo.message(f"Saving QC results to {output_filename}") + df.to_csv(output_filename, index=False) + else: + ogo.message(df) + + return data['pass'] + +def resample_to_match(target_image, source_image, interpolation='nearest'): + resampler = vtk.vtkImageResample() + resampler.SetInputData(source_image) + + spacing = target_image.GetSpacing() + resampler.SetAxisOutputSpacing(0, spacing[0]) + resampler.SetAxisOutputSpacing(1, spacing[1]) + resampler.SetAxisOutputSpacing(2, spacing[2]) + + if interpolation == 'nearest': + resampler.SetInterpolationModeToNearestNeighbor() + elif interpolation == 'linear': + resampler.SetInterpolationModeToLinear() + elif interpolation == 'cubic': + resampler.SetInterpolationModeToCubic() + + resampler.Update() + return resampler + +def export_nifti_outputs( + image_vtk, + body_labeled_vtk, + process_labeled_vtk, + cortical_mask_vtk, + inferior_disk_vtk, + superior_disk_vtk, + output_path, +): + """ + Export resampled grayscale image and labeled segmentation for FE input. + + Labels: + 1 = trabecular body + 2 = cortical body + 3 = trabecular process + 4 = cortical process + 5 = inferior disk + 6 = superior disk + """ + import SimpleITK as sitk + import numpy as np + from vtk.util.numpy_support import vtk_to_numpy + import os + + def vtk_to_sitk(vtk_image): + dims = vtk_image.GetDimensions() + np_array = vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape(dims[::-1]) # (z, y, x) + sitk_image = sitk.GetImageFromArray(np_array) + sitk_image.SetSpacing(vtk_image.GetSpacing()) + sitk_image.SetOrigin(vtk_image.GetOrigin()) + return sitk_image + + def get_mask_np(vtk_image): + dims = vtk_image.GetDimensions() + return vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape(dims, order="F") + + ogo.message("Exporting NIfTI files for grayscale image and labeled segmentation...") + + dims = image_vtk.GetDimensions() + combined_array = np.zeros(dims, dtype=np.uint8) + + # Convert to NumPy + body_mask = get_mask_np(body_labeled_vtk) > 0 + process_mask = get_mask_np(process_labeled_vtk) > 0 + cortical_mask = get_mask_np(cortical_mask_vtk) > 0 + inferior_mask = get_mask_np(inferior_disk_vtk) > 0 + superior_mask = get_mask_np(superior_disk_vtk) > 0 + im = get_mask_np(image_vtk) + + + # Assign labels + combined_array[(body_mask) & (~cortical_mask)] = 1 + combined_array[(body_mask) & (cortical_mask)] = 2 + combined_array[(process_mask) & (~cortical_mask)] = 3 + combined_array[(process_mask) & (cortical_mask)] = 4 + combined_array[inferior_mask] = 5 + combined_array[superior_mask] = 6 + im[(combined_array>4) | (combined_array==0)] = 0 + + # Convert segmentation to SimpleITK + seg_sitk = sitk.GetImageFromArray(np.transpose(combined_array, (2, 1, 0))) + seg_sitk.SetSpacing(image_vtk.GetSpacing()) + seg_sitk.SetOrigin(image_vtk.GetOrigin()) + + im_sitk = sitk.GetImageFromArray(np.transpose(im, (2, 1, 0))) + im_sitk.SetSpacing(image_vtk.GetSpacing()) + im_sitk.SetOrigin(image_vtk.GetOrigin()) + + outbase = os.path.splitext(output_path)[0] + im_out = outbase + "_im.nii.gz" + seg_out = outbase + "_seg.nii.gz" + + sitk.WriteImage(im_sitk, im_out) + sitk.WriteImage(seg_sitk, seg_out) + + ogo.message(f"Export complete: {im_out}, {seg_out}") + +###################################################################### VERTEBRA PIPELINE +# Main pipeline to process a vertebra FE +###################################################################### VERTEBRA PIPELINE + + +def process_vertebra(input_mask, input_image, n88model_output_path, body_label, process_label, reference_path, **kwargs): + + pmma_mat_id = kwargs.get("pmma_mat_id", 5000) + iso_resolution = kwargs.get("iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM) + pmma_thick = kwargs.get("pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM) + pmma_intrusion = kwargs.get("pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM) + top_node_set_id = kwargs.get("top_node_set_id", DEFAULT_SPINE_TOP_NODE_SET_ID) + bottom_node_set_id = kwargs.get("bottom_node_set_id", DEFAULT_SPINE_BOTTOM_NODE_SET_ID) + quality_control = kwargs.get("quality_control", True) + registration_scale = kwargs.get("registration_scale", DEFAULT_SPINE_REGISTRATION_SCALE) + registration_min_scale = kwargs.get("registration_min_scale", DEFAULT_SPINE_REGISTRATION_MIN_SCALE) + registration_max_scale = kwargs.get("registration_max_scale", DEFAULT_SPINE_REGISTRATION_MAX_SCALE) + mask_smoothing_spacing_threshold = kwargs.get( + "mask_smoothing_spacing_threshold", + DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + ) + label_smoothing_sigma_mm = float( + kwargs.get("label_smoothing_sigma_mm", DEFAULT_SPINE_LABEL_SMOOTHING_SIGMA_MM) + ) + label_smoothing_threshold = float( + kwargs.get("label_smoothing_threshold", DEFAULT_SPINE_LABEL_SMOOTHING_THRESHOLD) + ) + + #Read Image and Mask + ogo.message(f"Reading image...: {input_image}") + image_reader = read(input_image) + ogo.message(f"Reading mask...: {input_mask}") + mask_reader = resample_to_match(image_reader.GetOutput(), read(input_mask).GetOutput()) + if label_smoothing_sigma_mm > 0: + ogo.message( + "smoothing input label mask " + f"(sigma={label_smoothing_sigma_mm} mm, threshold={label_smoothing_threshold})..." + ) + mask_reader = _NiftiImageHandle( + smooth_label_mask_vtk( + mask_reader.GetOutput(), + sigma_mm=label_smoothing_sigma_mm, + threshold=label_smoothing_threshold, + ) + ) + input_spacing = image_reader.GetOutput().GetSpacing() + + # Get and Check labels + ogo.message(f"Checking if labels {body_label} (body) and {process_label} (process) are present...") + check_vertebra_presence(mask_reader, body_label) + check_vertebra_presence(mask_reader, process_label) + + # Threshold images to extract body, process and full vertebra + ogo.message(f"Thresholding...") + body = threshold(mask_reader.GetOutput(), body_label) + process = threshold(mask_reader.GetOutput(), process_label) + fullvertebra = combine_mask(body.GetOutput(), process.GetOutput()) + + # Crop everything to same BB + ogo.message(f"Cropping to common bounding box...") + isolated_vertebra, isolated_process, isolated_image = crop_and_transform(fullvertebra, body.GetOutput(), process.GetOutput(), image_reader.GetOutput()) + + # Marching Cubes and Registration + ogo.message(f"Starting ICP registration to reference...: {reference_path} ") + icp = get_icp_with_scaling( + body, + reference_path, + scale_factors=registration_scale, + min_scale=registration_min_scale, + max_scale=registration_max_scale, + ) + + # Transform Images and resample at the same time (less interpolation) + reference_to_sample_rotation, reference_to_sample_translation = _matrix_rotation_translation( + icp.GetMatrix() + ) + sample_to_reference_rotation, sample_to_reference_translation = invert_point_transform( + reference_to_sample_rotation, + reference_to_sample_translation, + ) + output_spacing = (float(iso_resolution), float(iso_resolution), float(iso_resolution)) + isolated_model_mask = combine_mask( + isolated_vertebra.GetOutput(), + isolated_process.GetOutput(), + ) + model_surface_points = surface_points_from_vtk_mask( + isolated_model_mask.GetOutput(), + max_points=None, + ) + output_origin, output_size = output_grid_for_point_transform( + model_surface_points, + rotation=sample_to_reference_rotation, + translation=sample_to_reference_translation, + spacing=output_spacing, + margin_voxels=int(kwargs.get("registration_margin_voxels", 4)), + ) + transformed_vertebra = resample_vtk_image_with_point_transform( + isolated_vertebra.GetOutput(), + rotation=sample_to_reference_rotation, + translation=sample_to_reference_translation, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", + ) + transformed_process = resample_vtk_image_with_point_transform( + isolated_process.GetOutput(), + rotation=sample_to_reference_rotation, + translation=sample_to_reference_translation, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", + ) + transformed_image = resample_vtk_image_with_point_transform( + isolated_image.GetOutput(), + rotation=sample_to_reference_rotation, + translation=sample_to_reference_translation, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="bspline", + ) + + if should_smooth_resampled_mask(input_spacing, mask_smoothing_spacing_threshold): + ogo.message( + "smoothing resampled body/process masks because input spacing " + f"{input_spacing} exceeds {mask_smoothing_spacing_threshold} mm..." + ) + transformed_vertebra = smooth_binary_mask_vtk(transformed_vertebra, close_iter=1, open_iter=1) + transformed_process = smooth_binary_mask_vtk(transformed_process, close_iter=1, open_iter=1) + else: + ogo.message( + "skipping body/process mask smoothing because input spacing " + f"{input_spacing} is <= {mask_smoothing_spacing_threshold} mm in all dimensions..." + ) + + ogo.message(f"relabelling mask and identifying boundary surfaces...") + # Assuming mask1_data and mask2_data are your initial binary masks + mask1_labeled = label_mask(transformed_vertebra, 2) # Apply label 1 to the first mask + mask2_labeled = label_mask(transformed_process, 1) # Apply label 2 to the second mask + + # Combine masks here (otherwise it does weird interpolations) + combined_masks = add_masks(mask1_labeled.GetOutput(), mask2_labeled.GetOutput()) # Combine the labeled masks + + # This creates an image identifying the top and bottom surfaces of the vertebra + boundary_masks = identify_boundary_surface(combined_masks.GetOutput(), bottom_node_set_id, top_node_set_id) + + ogo.message("creating cortical mask....") + #1 - 3 alterantive (to create a continous cortical shell - changed to 0 5 ) + + cort_mask = generate_cortical_mask(transformed_image, combined_masks.GetOutput(), threshold=1, min_th=2, max_th=5) + + #writer = vtk.vtkXMLImageDataWriter() + #writer.SetFileName('/home/matthias.walle/work/fem/CT_FE_TEMPLATE/MODELS/10001_QCT_vertebra_20_cortmask.vti') + #writer.SetInputData(cort_mask) # For VTK 8+ + #writer.Write() + + ogo.message(f"converting image to material ID...") + # This converts the raw image to a material ID mapped image + n_bins = 128 + bone_image, bin_centers = convert_image_to_material(transformed_image, combined_masks.GetOutput(), n_bins=n_bins, cort_mask = cort_mask) + + ogo.message(f"padding images...") + # Pad images before we add the disks (otherwise they may not have right pmma_thick) + padded_image = pad_vtk_image(bone_image, axis='x', pmma_thick=pmma_thick, pad_value=0) + padded_transformed_image = pad_vtk_image(transformed_image, axis='x', pmma_thick=pmma_thick, pad_value=0) + padded_mask = pad_vtk_image(boundary_masks, axis='x', pmma_thick=pmma_thick, pad_value=0) + padded_cort_mask = pad_vtk_image(cort_mask, axis='x', pmma_thick=pmma_thick, pad_value=0) + padded_mask1 = pad_vtk_image(mask1_labeled.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) + padded_mask2 = pad_vtk_image(mask2_labeled.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) + + ogo.message( + "generating fixed-thickness anatomy body-cap disks " + f"(pmma_thick = {pmma_thick} mm total, pmma_intrusion = {pmma_intrusion} mm)..." + ) + transformed_body_bounds = foreground_voxel_center_bounds(padded_mask1) + body_bounds = transformed_body_bounds + inferior_plane = bbox_relative_contact_plane( + body_bounds, + center_fraction=SPINE_INFERIOR_CONTACT_CENTER_FRACTION, + size_fraction=SPINE_CONTACT_SIZE_FRACTION, + projection_axis="z", + shape="anatomy", + ) + superior_plane = bbox_relative_contact_plane( + body_bounds, + center_fraction=SPINE_SUPERIOR_CONTACT_CENTER_FRACTION, + size_fraction=SPINE_CONTACT_SIZE_FRACTION, + projection_axis="z", + shape="anatomy", + ) + ogo.message( + "Inferior PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" + % ( + inferior_plane["center"], + inferior_plane["normal"], + inferior_plane["u_axis"], + inferior_plane["v_axis"], + inferior_plane["size"], + ) + ) + ogo.message( + "Superior PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" + % ( + superior_plane["center"], + superior_plane["normal"], + superior_plane["u_axis"], + superior_plane["v_axis"], + superior_plane["size"], + ) + ) + required_bounds = [ + projected_material_disk_required_bounds( + padded_mask1, + center=plane["center"], + normal=plane["normal"], + u_axis=plane["u_axis"], + v_axis=plane["v_axis"], + size=plane["size"], + shape=plane["shape"], + thickness=pmma_thick, + intrusion=pmma_intrusion, + ) + for plane in (inferior_plane, superior_plane) + ] + required_bounds = [bounds for bounds in required_bounds if bounds is not None] + if required_bounds: + bounds_array = np.asarray(required_bounds, dtype=float) + desired_bounds = ( + float(bounds_array[:, 0].min()), + float(bounds_array[:, 1].max()), + float(bounds_array[:, 2].min()), + float(bounds_array[:, 3].max()), + float(bounds_array[:, 4].min()), + float(bounds_array[:, 5].max()), + ) + ( + padded_image, + padded_transformed_image, + padded_mask, + padded_cort_mask, + padded_mask1, + padded_mask2, + ), contact_padding = pad_vtk_images_to_physical_bounds( + [ + padded_image, + padded_transformed_image, + padded_mask, + padded_cort_mask, + padded_mask1, + padded_mask2, + ], + desired_bounds=desired_bounds, + constants=[0, 0, 0, 0, 0, 0], + ) + if any(contact_padding["lower"]) or any(contact_padding["upper"]): + ogo.message( + "expanded image canvas for projected disks: lower=%s upper=%s" + % (contact_padding["lower"], contact_padding["upper"]) + ) + inferior_disk = generate_projected_material_disk_vtk( + padded_image, + surface_vtk_image=padded_mask1, + exclusion_vtk_image=padded_mask1, + center=inferior_plane["center"], + normal=inferior_plane["normal"], + u_axis=inferior_plane["u_axis"], + v_axis=inferior_plane["v_axis"], + size=inferior_plane["size"], + shape=inferior_plane["shape"], + thickness=pmma_thick, + intrusion=pmma_intrusion, + anatomy_constrained=True, + output_value=1, + ) + superior_disk = generate_projected_material_disk_vtk( + padded_image, + surface_vtk_image=padded_mask1, + exclusion_vtk_image=padded_mask1, + center=superior_plane["center"], + normal=superior_plane["normal"], + u_axis=superior_plane["u_axis"], + v_axis=superior_plane["v_axis"], + size=superior_plane["size"], + shape=superior_plane["shape"], + thickness=pmma_thick, + intrusion=pmma_intrusion, + anatomy_constrained=True, + output_value=1, + ) + + ogo.message(f"merging disks with image...") + # Now we create the image with the disks and the boundary image as well + image_with_pads = merge_vtk_images( + [padded_image, superior_disk, inferior_disk], + [None, pmma_mat_id, pmma_mat_id], + overwrite_existing=False, + ) + + boundary_masks_with_pads = merge_vtk_images([ + padded_mask, inferior_disk, superior_disk], [None, bottom_node_set_id, top_node_set_id]) + + # This is the final model that we will use for the FEA. + # For the future --> pull out material table to have option without disks + ogo.message("generating n88model file...") + model = create_microfe_model( + image_with_pads, + boundary_masks_with_pads, + bin_centers, + top_boundary_mask_image=superior_disk, + bottom_boundary_mask_image=inferior_disk, + top_boundary_mask_label=1, + bottom_boundary_mask_label=1, + **kwargs, + ) + + if kwargs.get("export_nifti", False): + ogo.message("generating nifti files...") + + export_nifti_outputs( + padded_transformed_image, + padded_mask1, + padded_mask2, + padded_cort_mask, + inferior_disk, + superior_disk, + n88model_output_path + ) + + # Quality control and output. If quality control is activated output only provided if it passes + if quality_control: + image_path = n88model_output_path.replace(".n88model", ".png") + dataframe_path = n88model_output_path.replace(".n88model", "_BCcheck.csv") + + ogo.message(f"Starting QC...") + test = check_image(boundary_masks, dataframe_path) + visualize_slice(image_with_pads, image_path) + + ogo.message(f"Writing n88model file: {n88model_output_path}") + writer = vtkbone.vtkboneN88ModelWriter() + writer.SetInputData(model) + writer.SetFileName(n88model_output_path) + writer.Update() + + + +###################################################################### MAIN +## Main function containing file I/O via argparse +###################################################################### MAIN + + +def main(): + description = ''' + This script sets up the L4 vertebral compression FE model from the + density (K2HPO4) calibrated image. This script sets up the model for a L4 vertebra + (including arch and pedicles). The analysis resamples the image to isotropic voxels, + transforms the image, applies the bone mask and bins the data. It then creates the FE + model for solving using FAIM (v8.1, Numerics Solutions Ltd, Calgary, Canada - Steven + Boyd). + + Input: Calibrated K2HPO4 Image (*.nii), Bone Mask (*_MASK.nii) + + Optional Parameters: + 1) Mask Threshold + 2) Isotropic resample voxel size + 3) Power-law exponent + 4) Power-law coefficient + 5) Bone Poissons ratio + 6) PMMA Elastic Modulus + 7) PMMA Poissons ratio + 8) PMMA pmma_thick + 9) PMMA material ID + 10) FE displacement + + Output: N88 Model (*.n88model) + ''' + + parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, + prog="ogoFEA-spine-builder", + description=description + ) + + parser.add_argument("calibrated_image", help="*_K2HPO4.nii image file") + parser.add_argument("bone_mask", help="*_MASK.nii mask image of bone") + + parser.add_argument("--mask_threshold", type=int, required=True, + help="Label value for the vertebral body in the bone mask.") + parser.add_argument("--process_mask_threshold", type=int, required=True, + help="Label value for the vertebral process in the bone mask.") + parser.add_argument("--output_path", type=str, default=None, + help="Set output path for the N88 model file. (default: same as input image)") + parser.add_argument("--quality_control", type=_as_bool, default=True, + help="Set quality control flag (visualise output and add volume checks). (default: %(default)s)") + parser.add_argument("--iso_resolution", type=float, default=DEFAULT_SPINE_ISO_RESOLUTION_MM, + help="Set the isotropic voxel size [in mm]. (default: %(default)s [mm])") + parser.add_argument("--mask_smoothing_spacing_threshold", type=float, default=DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + help="Smooth resampled body/process masks only when an input spacing dimension exceeds this value. (default: %(default)s [mm])") + #parser.add_argument("--elastic_exponent", type=float, default=2.29, + # help="Sets the exponent (b) for power law: E=A(den)^b. (default: %(default)s)") + #parser.add_argument("--elastic_Emax", type=float, default=10500, + # help="Sets the coefficient (A) Elastic Modulus value for the power law: E=A(den)^b. (default: %(default)s [MPa])") + parser.add_argument("--poissons_ratio", type=float, default=DEFAULT_SPINE_POISSONS_RATIO, + help="Sets the Poisson's ratio for the material(s) in the FE model. (default: %(default)s)") + parser.add_argument("--pmma_E", type=float, default=DEFAULT_SPINE_PMMA_E_MPA, + help="Sets the Elastic Modulus for PMMA caps in the FE model. (default: %(default)s [MPa])") + parser.add_argument("--pmma_v", type=float, default=DEFAULT_SPINE_PMMA_POISSONS_RATIO, + help="Sets the Poisson's ratio for the PMMA material(s) in the FE model. (default: %(default)s)") + parser.add_argument("--pmma_thick", type=int, default=DEFAULT_SPINE_PMMA_THICKNESS_MM, + help="Sets the total PMMA cap thickness in the FE model. (default: %(default)s [mm])") + parser.add_argument("--pmma_intrusion", type=int, default=DEFAULT_SPINE_PMMA_INTRUSION_MM, + help="Sets how far anatomy can occupy the fixed PMMA cap thickness. (default: %(default)s [mm])") + parser.add_argument("--pmma_mat_id", type=int, default=DEFAULT_SPINE_PMMA_MATERIAL_ID, + help="Sets the material ID for the PMMA blocks. (default: %(default)s)") + parser.add_argument("--fe_displacement", type=float, default=DEFAULT_SPINE_FE_DISPLACEMENT_MM, + help="Sets the applied displacement in [mm] to the FE model. (default: %(default)s [mm])") + parser.add_argument("--reference_path", type=str, required=False, default=None, + help="Path to the reference vtk file for ICP registration. (default: None)") + parser.add_argument("--registration_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_SCALE, + help="Manual affine scale applied to the reference body before ICP. Use a scalar or sx,sy,sz. If omitted, PCA-based scaling is used.") + parser.add_argument("--registration_min_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_MIN_SCALE, + help="Minimum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") + parser.add_argument("--registration_max_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_MAX_SCALE, + help="Maximum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") + parser.add_argument("--registration_backend", choices=("vtk",), default=DEFAULT_SPINE_REGISTRATION_BACKEND, + help="Spine reference alignment backend. (default: %(default)s)") + parser.add_argument("--top_node_set_id", type=int, default=DEFAULT_SPINE_TOP_NODE_SET_ID, + help="ID for the top node set. (default: %(default)s)") + parser.add_argument("--bottom_node_set_id", type=int, default=DEFAULT_SPINE_BOTTOM_NODE_SET_ID, + help="ID for the bottom node set. (default: %(default)s)") + parser.add_argument("--pmma_yield_compression", type=float, default=None, + help="Sets the yield strength in compression for PMMA material in the FE model. (default: %(default)s [MPa])") + parser.add_argument("--pmma_yield_tension", type=float, default=None, + help="Sets the yield strength in tension for PMMA material in the FE model. (default: %(default)s [MPa])") +# parser.add_argument("--bone_yield_compression", type=float, default=None, +# help="Sets the yield strength in compression for bone material in the FE model. (default: %(default)s [MPa])") +# parser.add_argument("--bone_yield_tension", type=float, default=None, +# help="Sets the yield strength in tension for bone material in the FE model. (default: %(default)s [MPa])") +# parser.add_argument("--cort_elastic_Emax", type=float, +# help="Sets the maximum elastic modulus for cortical bone in the FE model. (default: %(default)s)") +# parser.add_argument("--cort_elastic_exponent", type=float, +# help="Sets the elastic exponent for cortical bone in the FE model.") + parser.add_argument("--cort_poissons_ratio", type=float, + help="Sets the Poisson's ratio for cortical bone in the FE model.") +# parser.add_argument("--cort_yield_compression", type=float, +# help="Sets the yield strength in compression for cortical bone in the FE model.") +# parser.add_argument("--cort_yield_tension", type=float, +# help="Sets the yield strength in tension for cortical bone in the FE model.") + parser.add_argument("--elastic_E_func", type=str, default="default_E", + help="Function name for trabecular bone Young’s modulus. (default: default_E)") + parser.add_argument("--yield_comp_func", type=str, + help="Function name for trabecular compression yield. (default: default_yc)") + parser.add_argument("--yield_tens_func", type=str, + help="Function name for trabecular tension yield. (default: default_yt)") + + parser.add_argument("--cort_elastic_E_func", type=str, + help="Function name for cortical bone Young’s modulus. (default: default_E)") + parser.add_argument("--cort_yield_comp_func", type=str, + help="Function name for cortical compression yield. (default: default_yc)") + parser.add_argument("--cort_yield_tens_func", type=str, + help="Function name for cortical tension yield. (default: default_yt)") + parser.add_argument("--appendix", default=None, type=str, + help="Extra appendix for output file") + parser.add_argument("--export_nifti", action='store_true', + help="If set, exports resampled grayscale image and labeled segmentation.") + + args = parser.parse_args() + + # Print arguments + ogo.message(echo_arguments("ogoFEA-spine-builder", vars(args))) + + # Prepare the output file + basename = remove_extension(os.path.basename(args.calibrated_image)) + + if args.output_path is None: + output_dir = os.path.dirname(args.calibrated_image) + else: + output_dir = args.output_path + + output_dir = os.path.abspath(output_dir) + + if args.appendix is None: + output_file = os.path.join(output_dir, f"{basename}_{args.mask_threshold}.n88model") + else: + output_file = os.path.join(output_dir, f"{basename}_{args.appendix}.n88model") + + ogo.message(f'N88Model File path: {output_file}') + # Set default reference path if not provided + reference_path = args.reference_path + if reference_path is None: + reference_path = str(default_spine_reference_path()) + + # Extract all kwargs + kwargs = vars(args).copy() # Convert parsed arguments to a dictionary + kwargs.pop("calibrated_image") # Remove positional argument + kwargs.pop("bone_mask") # Remove positional argument + kwargs.pop("mask_threshold") # Remove required argument + kwargs.pop("process_mask_threshold") # Remove required argument + kwargs.pop("reference_path") # Ensure updated reference path + + # Run the vertebra processing + process_vertebra( + args.bone_mask, + args.calibrated_image, + output_file, + args.mask_threshold, + args.process_mask_threshold, + reference_path, + **kwargs # Pass remaining arguments as kwargs + ) + +###################################################################### MAIN +if __name__ == '__main__': + main() diff --git a/ogo/fea/spine_compression.py b/ogo/fea/spine_compression.py deleted file mode 100644 index 1fe8028..0000000 --- a/ogo/fea/spine_compression.py +++ /dev/null @@ -1,1412 +0,0 @@ -##### -# SpineCompressionFE.py -# -# This script replaces the L4 vertebral compression FE model. It sets up a FAIM micro-FE model -# from the density (K2HPO4) calibrated image. -# This script sets up the model for a L4 vertebra (including arch and -# pedicles). The analysis resamples the image to isotropic voxels, transforms -# the image, applies the bone mask and bins the data. It then creates the FE model for -# solving using FAIM (>v8.0, Numerics Solutions Ltd, Calgary, Canada - Steven Boyd). -# -# Changes to the original script: -# - Updated to include pedicles and body int he FE model -# - Requires the input of the body and process labels (does not identify body - use nnUnet-Model -# - Generates Jelly Bean disks that fit the surface of the vertebra perfectly - -# To-Do: -# - Include option for no-disks (direct boundary conditions) -# - Include option for square and round disks - -##### -# -# Matthias Walle -# Postdoctoral Associate -# University of Calgary -# Jan 24, 2025x -# Modifed to Py3: March 25, 2020 -##### - - -import os -import argparse -import os -from glob import glob -import ogo.fea.material_laws - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -import SimpleITK as sitk -from matplotlib import pyplot as plt -from scipy.ndimage import ( - binary_dilation, - binary_erosion, - find_objects, - label, -) -from skimage.exposure import rescale_intensity -import vtk -import vtkbone -from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy - -from ogo.fea.alignment import ( - estimate_rigid_icp, - invert_point_transform, - output_grid_for_point_transform, - point_cloud_axis_lengths, - polydata_points, - resample_vtk_image_with_point_transform, - sample_points, - surface_points_from_vtk_mask, -) -from ogo.fea.boundary import ( - bbox_relative_contact_plane, - foreground_voxel_center_bounds, - generate_projected_material_disk_vtk, - pad_vtk_images_to_physical_bounds, - projected_material_disk_required_bounds, - should_smooth_resampled_mask, - smooth_binary_mask_vtk, - smooth_label_mask_vtk, -) -from ogo.fea.spine import ( - DEFAULT_SPINE_BOTTOM_NODE_SET_ID, - DEFAULT_SPINE_FE_DISPLACEMENT_MM, - DEFAULT_SPINE_ISO_RESOLUTION_MM, - DEFAULT_SPINE_LABEL_SMOOTHING_SIGMA_MM, - DEFAULT_SPINE_LABEL_SMOOTHING_THRESHOLD, - DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - DEFAULT_SPINE_PMMA_E_MPA, - DEFAULT_SPINE_PMMA_INTRUSION_MM, - DEFAULT_SPINE_PMMA_MATERIAL_ID, - DEFAULT_SPINE_PMMA_POISSONS_RATIO, - DEFAULT_SPINE_PMMA_THICKNESS_MM, - DEFAULT_SPINE_POISSONS_RATIO, - DEFAULT_SPINE_REGISTRATION_MAX_SCALE, - DEFAULT_SPINE_REGISTRATION_MIN_SCALE, - DEFAULT_SPINE_REGISTRATION_BACKEND, - DEFAULT_SPINE_REGISTRATION_SCALE, - DEFAULT_SPINE_TOP_NODE_SET_ID, - SPINE_CONTACT_SIZE_FRACTION, - SPINE_INFERIOR_CONTACT_CENTER_FRACTION, - SPINE_SUPERIOR_CONTACT_CENTER_FRACTION, - default_spine_reference_path, -) -from ogo.fea.model import ( - apply_spine_boundary_conditions as apply_boundary_conditions, - create_microfe_model, - find_and_add_visible_nodes, - log_fe_arguments, -) -import ogo.util.Helper as ogo -from ogo.util.echo_arguments import echo_arguments -from scipy.ndimage import gaussian_filter - - -vtk.vtkObject.GlobalWarningDisplayOff() - - -def _as_bool(value): - if isinstance(value, bool): - return value - text = str(value).strip().lower() - if text in {"1", "true", "t", "yes", "y", "on"}: - return True - if text in {"0", "false", "f", "no", "n", "off"}: - return False - raise argparse.ArgumentTypeError(f"Invalid boolean value: {value}") - - -###################################################################### HELPERS (Python) -## General helper functions: -###################################################################### HELPERS (Python) - -def remove_extension(filename): - while True: - filename, ext = os.path.splitext(filename) - if not ext: - break - return filename - -def print_matrix(matrix): - for i in range(4): - # Create a string for the entire row - row_message = " ".join(f"{matrix.GetElement(i, j):0.4f}" for j in range(4)) - # Send the row as a single message - ogo.message(row_message) - - -def parse_scale_triplet(value, name="scale"): - if value is None: - return None - if isinstance(value, (int, float)): - return np.array([float(value)] * 3, dtype=float) - parts = [part.strip() for part in str(value).replace("x", ",").split(",") if part.strip()] - try: - values = [float(part) for part in parts] - except ValueError as exc: - raise ValueError(f"{name} must be a float or three comma-separated floats.") from exc - if len(values) == 1: - values = values * 3 - if len(values) != 3: - raise ValueError(f"{name} must be a float or three comma-separated floats.") - return np.array(values, dtype=float) - - -###################################################################### HELPERS (VTK) -## VTK Helper functions: -###################################################################### HELPERS (VTK) - - -class _NiftiImageHandle: - """Reader-like wrapper for NIfTI images loaded through Ogo's orientation path.""" - - def __init__(self, image): - self._image = image - - def GetOutput(self): - return self._image - - -def read(input_mask): - return _NiftiImageHandle(ogo.readNii(input_mask)) - -def check_vertebra_presence(mask_reader, vertebra): - if vertebra not in vtk_to_numpy(mask_reader.GetOutput().GetPointData().GetScalars()).ravel(): - raise ValueError(f'Mask does not contain label {vertebra} for "{vertebra}" vertebra.') - -def threshold(mask_output, label): - threshold = vtk.vtkImageThreshold() - threshold.SetInputData(mask_output) - threshold.ThresholdBetween(float(label), float(label)) - threshold.ReplaceInOn() - threshold.SetInValue(1) - threshold.ReplaceOutOn() - threshold.SetOutValue(0) - threshold.SetOutputScalarTypeToUnsignedChar() - threshold.Update() - return threshold - -def label_mask(mask, label_value): - # Use vtkImageMathematics to change 1 values to a specific label value - - caster = vtk.vtkImageCast() - caster.SetInputData(mask) - caster.SetOutputScalarTypeToUnsignedChar() - caster.ClampOverflowOn() # Optional, clamps values that are out of range of the target data type - caster.Update() - - math = vtk.vtkImageMathematics() - math.SetInputData(caster.GetOutput()) - math.SetOperationToMultiplyByK() - math.SetConstantK(label_value) - math.Update() - return math - -def combine_mask(mask1, mask2): - # Combine two masks using vtkImageLogic with a logical OR operation - logic = vtk.vtkImageLogic() - logic.SetInput1Data(mask1) - logic.SetInput2Data(mask2) - logic.SetOperationToOr() - logic.Update() - return logic - -def add_masks(mask1, mask2): - # Combine two masks by adding their values - math = vtk.vtkImageMathematics() - math.SetInput1Data(mask1) - math.SetInput2Data(mask2) - math.SetOperationToAdd() - math.Update() - - # Get the result of addition - added_mask = math.GetOutput() - - # Apply thresholding to ensure the max value is 2 - threshold = vtk.vtkImageThreshold() - threshold.SetInputData(added_mask) - threshold.ThresholdByUpper(2) # Set upper threshold limit to 2 - threshold.SetReplaceIn(True) - threshold.SetInValue(2) # Values above the upper threshold will be set to 2 - threshold.SetOutputScalarTypeToUnsignedChar() - threshold.Update() - - return threshold - -def crop_to_bounding_box(image_data, bb=None): - if bb is None: - - extents = image_data.GetExtent() - scalars = vtk_to_numpy(image_data.GetPointData().GetScalars()) - reshaped_scalars = scalars.reshape((extents[5]-extents[4]+1, extents[3]-extents[2]+1, extents[1]-extents[0]+1)) - - nz = np.nonzero(reshaped_scalars) - - if not nz[0].size: - raise ValueError("No non-zero voxels found after thresholding. Check earlier steps.") - - min_x, max_x = np.min(nz[2])+extents[0], np.max(nz[2])+extents[0] - min_y, max_y = np.min(nz[1])+extents[2], np.max(nz[1])+extents[2] - min_z, max_z = np.min(nz[0])+extents[4], np.max(nz[0])+extents[4] - bb = [min_x, max_x, min_y, max_y, min_z, max_z] - else: - min_x, max_x, min_y, max_y, min_z, max_z = bb - - - ogo.message(f"Cropping to extents: X({min_x} to {max_x}), Y({min_y} to {max_y}), Z({min_z} to {max_z})") - - crop = vtk.vtkImageClip() - crop.SetInputData(image_data) - crop.SetOutputWholeExtent(min_x, max_x, min_y, max_y, min_z, max_z) - crop.ClipDataOn() - crop.Update() - - if crop.GetOutput().GetNumberOfPoints() == 0: - raise ValueError("Cropping resulted in an empty image. Extents might be incorrect.") - - output = crop.GetOutput() - spacing = output.GetSpacing() - origin = image_data.GetOrigin() - output.SetOrigin( - float(origin[0]) + float(min_x) * float(spacing[0]), - float(origin[1]) + float(min_y) * float(spacing[1]), - float(origin[2]) + float(min_z) * float(spacing[2]), - ) - dims = output.GetDimensions() - output.SetExtent(0, dims[0] - 1, 0, dims[1] - 1, 0, dims[2] - 1) - - return crop, bb - -def pad_vtk_image(vtk_image, axis='x', pmma_thick=5, pad_value=0): - """ - Pad a VTK image along the specified axis with a given pmma_thick while preserving VTK metadata. - - Parameters: - - vtk_image: The input VTK image to pad. - - axis: The axis along which to pad ('x', 'y', or 'z'). - - pmma_thick: Number of voxels to pad on each side along the specified axis. - - pad_value: The value to pad with (default: 0). - - Returns: - - padded_vtk_image: The padded VTK image with updated metadata. - """ - # Map the axis to the corresponding dimension index - axis_map = {'x': 0, 'y': 1, 'z': 2} - if axis not in axis_map: - raise ValueError("Axis must be one of 'x', 'y', or 'z'.") - extrusion_axis = axis_map[axis] - - # Convert VTK image data to a NumPy array - input_array = vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape( - vtk_image.GetDimensions(), order='F' - ) - - # Define the padding configuration - pad_width = [(0, 0)] * 3 # Default no padding - pad_width[extrusion_axis] = (pmma_thick, pmma_thick) # Apply padding along the target axis - - # Apply padding - padded_array = np.pad(input_array, pad_width=pad_width, mode='constant', constant_values=pad_value) - - # Update VTK metadata - padded_origin = list(vtk_image.GetOrigin()) - padded_dimensions = list(padded_array.shape) - spacing = vtk_image.GetSpacing() - - # Adjust the origin for padding in the negative direction - padded_origin[extrusion_axis] -= pmma_thick * spacing[extrusion_axis] - - # Convert the padded array back to VTK format - padded_vtk_image = vtk_image.NewInstance() - padded_vtk_image.SetDimensions(padded_dimensions) - padded_vtk_image.SetOrigin(padded_origin) - padded_vtk_image.SetSpacing(spacing) - padded_vtk_image.GetPointData().SetScalars(numpy_to_vtk(padded_array.ravel(order='F'))) - - return padded_vtk_image - - -def merge_vtk_images(image_list, label_list=None, overwrite_existing=True): - """ - Merges an arbitrary list of vtkImageData objects into a single vtkImageData. - Optionally assigns specific label values for each image. - - Parameters: - - image_list: List of vtkImageData objects to merge. - - label_list: Optional list of label values corresponding to each image. - If None or an entry in the list is None, uses the original values in the image. - - overwrite_existing: If False, labeled images only write into zero-valued voxels already - present in the merged image. - - Returns: - - merged_image: vtkImageData containing the merged result. - """ - if not image_list: - raise ValueError("The image list is empty.") - - if label_list is not None and len(label_list) != len(image_list): - raise ValueError("The label list must be the same length as the image list.") - - # Get metadata from the first image - reference_image = image_list[0] - dims = reference_image.GetDimensions() - spacing = reference_image.GetSpacing() - origin = reference_image.GetOrigin() - - # Initialize the merged array as zeros with the same dimensions as the reference image - merged_array = np.zeros(dims, dtype=np.float32) - - # Merge each image into the merged array - for idx, image in enumerate(image_list): - image_array = vtk_to_numpy(image.GetPointData().GetScalars()).reshape(dims, order="F") - if label_list is not None and label_list[idx] is not None: - label_value = label_list[idx] - # Replace non-zero values in the image with the label value - write_mask = image_array > 0 - if not overwrite_existing: - write_mask &= merged_array == 0 - merged_array[write_mask] = label_value - else: - # Add the original values of the image where label is None - merged_array += image_array - - # Convert the merged array back to VTK format - merged_vtk_array = numpy_to_vtk(merged_array.ravel(order="F"), deep=True) - - # Create a new vtkImageData to hold the merged result - merged_image = vtk.vtkImageData() - merged_image.SetDimensions(dims) - merged_image.SetSpacing(spacing) - merged_image.SetOrigin(origin) - merged_image.GetPointData().SetScalars(merged_vtk_array) - - return merged_image - -###################################################################### REGISTRATION -## functions related to ICP registration -###################################################################### REGISTRATION - -def crop_and_transform(fullvertebra, body_output, process_output, image): - - _, bb = crop_to_bounding_box(fullvertebra.GetOutput()) - isolated_vertebra, _ = crop_to_bounding_box(body_output, bb) - isolated_process, _ = crop_to_bounding_box(process_output, bb) - isolated_image, _ = crop_to_bounding_box(image, bb) - - - return isolated_vertebra, isolated_process, isolated_image - -def perform_marching_cubes(body_output): - mcubes = vtk.vtkImageMarchingCubes() - mcubes.SetInputConnection(body_output.GetOutputPort()) - mcubes.SetValue(1, 1.0) - mcubes.Update() - if mcubes.GetOutput().GetNumberOfPoints() == 0: - raise ValueError("No data was generated by the Marching Cubes algorithm.") - return mcubes - - -def get_icp_with_scaling( - body, - reference_path, - scale_factors=None, - min_scale=(0.8, 0.8, 0.75), - max_scale=(1.2, 1.2, 1.3), -): - - sample_surface_points = surface_points_from_vtk_mask( - body.GetOutput(), - max_points=8000, - sample_mode="stride", - ) - reference_polydata = ogo.readPolyData(reference_path) - reference_points = sample_points( - polydata_points(reference_polydata), - max_points=8000, - mode="linspace", - ) - - sample_lengths = point_cloud_axis_lengths(sample_surface_points) - ref_lengths = point_cloud_axis_lengths(reference_points) - if scale_factors is None: - scale_factors = sample_lengths / ref_lengths - min_scale = parse_scale_triplet(min_scale, "registration_min_scale") - max_scale = parse_scale_triplet(max_scale, "registration_max_scale") - scale_factors = np.minimum(scale_factors, max_scale) - scale_factors = np.maximum(scale_factors, min_scale) - scale_source = "voxel-surface PCA axis lengths" - else: - scale_factors = parse_scale_triplet(scale_factors, "registration_scale") - scale_source = "manual override" - - ogo.message(f"PCA axis lengths (sample): {np.round(sample_lengths, 2)}") - ogo.message(f"PCA axis lengths (reference): {np.round(ref_lengths, 2)}") - ogo.message(f"Scale factors applied to reference ({scale_source}): {np.round(scale_factors, 3)}") - - transform = estimate_rigid_icp( - moving_points=reference_points * scale_factors, - fixed_points=sample_surface_points, - iterations=50, - tolerance=1.0e-4, - start_by_matching_centroids_only=False, - convergence="delta", - distance_mode="mean", - ) - - matrix = vtk.vtkMatrix4x4() - matrix.Identity() - rotation = transform["rotation"] - translation = transform["translation"] - for row in range(3): - for col in range(3): - matrix.SetElement(row, col, float(rotation[row, col])) - matrix.SetElement(row, 3, float(translation[row])) - - vtk_transform = vtk.vtkTransform() - vtk_transform.SetMatrix(matrix) - ogo.message( - "ICP (with scaled reference) iterations=%d mean_distance=%0.4f" - % (transform["iterations"], transform["mean_distance"]) - ) - ogo.message("ICP reference-to-sample Matrix:") - print_matrix(vtk_transform.GetMatrix()) - - return vtk_transform - -def get_icp(body, reference_path): - - mcubes = perform_marching_cubes(body) - mcubes_output = mcubes.GetOutput() - reference_bone = ogo.readPolyData(reference_path) - - icp = vtk.vtkIterativeClosestPointTransform() - icp.SetTarget(mcubes_output) - icp.SetSource(reference_bone) - - icp.StartByMatchingCentroidsOn() - icp.GetLandmarkTransform().SetModeToRigidBody() - icp.SetMeanDistanceModeToRMS() - icp.SetMaximumMeanDistance(0.05) - icp.CheckMeanDistanceOn() - icp.SetMaximumNumberOfLandmarks(250) - icp.SetMaximumNumberOfIterations(75) - icp.Update() - - ogo.message("ICP Matrix:") - print_matrix(icp.GetMatrix()) - return icp - -def transform_resample(image, matrix, iso_resolution, interpolation='cubic'): - output = ogo.transformResample(image, matrix, iso_resolution, interpolation=interpolation) - if output.GetNumberOfPoints() == 0: - ogo.message("Reslice output contains no points. Check the input and transformation.") - return output - - -def _matrix_rotation_translation(matrix): - rotation = np.asarray( - [[matrix.GetElement(row, col) for col in range(3)] for row in range(3)], - dtype=float, - ) - translation = np.asarray([matrix.GetElement(row, 3) for row in range(3)], dtype=float) - return rotation, translation - -###################################################################### DISK GENERATION -## Functions related to disk generation (image processing) -###################################################################### DISK GENERATION - -def identify_boundary_surface(input_vtk_image, top_value, bottom_value): - """Transforms a VTK image to highlight the topmost and bottommost components. - - Parameters: - input_vtk_image (vtk.vtkImageData): The input binary mask as a VTK image. - top_value (int): The value to assign to the top mask. - bottom_value (int): The value to assign to the bottom mask. - - Returns: - vtk.vtkImageData: The modified VTK image with the top and bottom masks highlighted. - """ - # Convert VTK image data to NumPy array - input_array = vtk_to_numpy(input_vtk_image.GetPointData().GetScalars()).reshape(input_vtk_image.GetDimensions(), order='F') - input_array = np.swapaxes(input_array, 0, 2) # Swap axes for processing along correct dimension - - binary_mask = np.copy(input_array) == 2 - - # Apply boundary conditions from the original method - eroded_mask = binary_erosion(binary_mask, iterations=1) - #dilated_mask = binary_dilation(binary_mask, iterations=1) - outline_mask = (eroded_mask == 0) & (binary_mask == 1) - - # Highlight the outline - struct_element_erosion = np.array([[[0, 1, 0], [1, 1, 1], [0, 1, 0]]]) - highlighted_mask = binary_erosion(outline_mask, structure=struct_element_erosion) - - struct_element_dilation = np.zeros((3, 3, 3)) - struct_element_dilation[1, 1, :] = 1 - highlighted_mask = binary_dilation(highlighted_mask, iterations=2, structure=struct_element_dilation) - highlighted_mask = binary_dilation(highlighted_mask, iterations=1) - highlighted_mask[binary_mask == 0] = 0 - - labeled_mask, num_features = label(highlighted_mask) - sizes = np.bincount(labeled_mask.ravel())[1:] - sorted_indices = np.argsort(sizes)[::-1] - object_slices = find_objects(labeled_mask) - - top_positions = [(i + 1, object_slices[i][0].start) for i in sorted_indices[:2]] - top_positions.sort(key=lambda x: x[1]) - top_component, bottom_component = top_positions - - top_mask = binary_dilation(labeled_mask == top_component[0], iterations=2) & outline_mask - bottom_mask = binary_dilation(labeled_mask == bottom_component[0], iterations=2) & outline_mask - - # Assign values and convert back to VTK - result_array = input_array.copy() - result_array[top_mask] = top_value - result_array[bottom_mask] = bottom_value - result_array[input_array==0]=0 - result_array = np.swapaxes(result_array, 0, 2) # Swap axes for processing along correct dimension - - vtk_data_array = numpy_to_vtk(result_array.ravel(order='F'), deep=True) - output_vtk_image = vtk.vtkImageData() - output_vtk_image.DeepCopy(input_vtk_image) - output_vtk_image.GetPointData().SetScalars(vtk_data_array) - - return output_vtk_image - -###################################################################### MICRO-FE (FAIM) -## Functions related to micro-FE model -###################################################################### MICRO-FE (FAIM) - -def generate_cortical_mask(image_vtk, mask_vtk, threshold=0.2, min_th=1, max_th=3): - """ - Generate a cortical mask from the input VTK image and mask using a threshold. - Ensures the cortical mask is at least as thick as the erosion shell. - - Parameters: - - image_vtk: The input image data (VTK Image Data). - - mask_vtk: The input mask data (VTK Image Data). - - threshold: The threshold value for binarization (default: 0.2). - - min_th: Minimum thickness constraint (default: 1 voxel). - - max_th: Maximum thickness constraint (default: 3 voxels). - - Returns: - - cortical_mask_vtk: The binary mask for the cortical region as a VTK Image Data. - """ - - # Convert VTK images to numpy arrays - image_np = vtk_to_numpy(image_vtk.GetPointData().GetScalars()).reshape(image_vtk.GetDimensions(), order='F').astype(np.float32) - #image_np = gaussian_filter(image_np, sigma=1) # Adjust sigma as needed - - mask_np = vtk_to_numpy(mask_vtk.GetPointData().GetScalars()).reshape(mask_vtk.GetDimensions(), order='F') > 0 - - # Apply thresholding to the image (convert to g/ccm by dividing by 1000) - binary_image = (image_np / 1000) > threshold - - # Compute cortical mask as intersection of thresholded image and mask - cortical_mask = binary_image & mask_np - - ogo.message(f'Average Image intensity for cort mask {np.mean(image_np[cortical_mask])}') - ogo.message(f'Average Image intensity for trab mask {np.mean(image_np[~cortical_mask & mask_np])}') - - - # Compute the shell (minimum thickness enforcement) - if min_th>0: - min_erosion = binary_erosion(mask_np, iterations=min_th, border_value=0) - min_shell = mask_np & ~min_erosion # Shell is the difference between the mask and its eroded version - else: - min_shell = np.zeros_like(mask_np) - - # Apply maximum thickness constraint - if max_th>0: - max_erosion = binary_erosion(mask_np, iterations=max_th, border_value=0) - max_shell = mask_np & ~max_erosion # Shell is the difference between the mask and its eroded version - else: - max_shell = np.ones_like(mask_np) - - # Ensure cortical mask is at least the shell - cortical_mask = (cortical_mask | min_shell) & max_shell # Ensure at least the shell is included - - # Convert the binary mask back to VTK format - cortical_mask_vtk = vtk.vtkImageData() - cortical_mask_vtk.DeepCopy(mask_vtk) # Copy spatial properties - - # Convert numpy array back to VTK format - mask_vtk_array = numpy_to_vtk(cortical_mask.flatten(order='F'), deep=True, array_type=vtk.VTK_UNSIGNED_CHAR) - cortical_mask_vtk.GetPointData().SetScalars(mask_vtk_array) - - ogo.message(f'Average Image intensity for cort mask {np.mean(image_np[cortical_mask])}') - ogo.message(f'Average Image intensity for trab mask {np.mean(image_np[~cortical_mask & mask_np])}') - - - return cortical_mask_vtk - - -def convert_image_to_material(image, mask, n_bins=128, cort_mask=None): - change = ogo.prepareFiniteElementImage(image) - mask_change = ogo.prepareFiniteElementImage(mask) - cort_mask_change = ogo.prepareFiniteElementImage(cort_mask) if cort_mask is not None else None - connected_image = ogo.imageConnectivity(change) - thr_image = ogo.bmd_preprocess(connected_image, -31) - #this was done previously - but if someone wants this they should just do it in the calib functions - #ash_image = ogo.bmd_K2hpo4ToAsh(thr_image) - cast_image = ogo.cast2short(thr_image) - bone_image = ogo.applyMaskByArray(cast_image, mask_change) - binned_image, bin_centers = ogo.density2materialID(bone_image, n_bins=n_bins, cort_mask=cort_mask_change) - - return binned_image, bin_centers - -def resolve_func(func_or_name, module): - """Resolve a material-law function by object or configured name.""" - from ogo.fea.materials import resolve_material_func - - return resolve_material_func(func_or_name, module) - -###################################################################### QUALITY CONTROL -## Functions to check image and boundary conditions -###################################################################### QUALITY CONTROL - -## Functions to check the results -def check_image_values(image): - array = vtk.util.numpy_support.vtk_to_numpy(image.GetPointData().GetScalars()) - unique_values = np.unique(array) - ogo.message("Unique values in the image:", unique_values) - -def calculate_features(image, label): - label_map = sitk.BinaryThreshold(image, lowerThreshold=label, upperThreshold=label, insideValue=1, outsideValue=0) - stats = sitk.LabelShapeStatisticsImageFilter() - stats.Execute(label_map) - volume = stats.GetPhysicalSize(1) - centroid = stats.GetCentroid(1) - principal_axes = stats.GetPrincipalAxes(1) - extent = stats.GetBoundingBox(1) - return {'volume': volume, 'centroid': centroid, 'principal_axes': principal_axes, 'extent': extent} - -def check_values(features, checks): - results = {} - pass_all = True - for check, condition in checks.items(): - result = eval(condition, {"features": features}) - results[check] = result - if not result: - pass_all = False - results['pass'] = pass_all - return results - -def parse_filename(filepath): - base_name = os.path.splitext(os.path.basename(filepath))[0] - parts = base_name.split('_') - return { - 'ID': parts[0] if len(parts) > 0 else '', - 'TREATMENT': parts[1] if len(parts) > 1 else '', - 'LOCATION': parts[2] if len(parts) > 2 else '', - 'NUMBER': parts[4] if len(parts) > 4 else '', - 'filename': '_'.join(parts[:-3]) if len(parts) > 3 else base_name, - } - -def visualize_slice(image, filepath): - # Get the dimensions of the vtkImageData object - dimensions = image.GetDimensions() # (x, y, z) - mid_x = dimensions[0] // 2 # Middle slice along the X-axis - - # Convert vtkImageData to a NumPy array - vtk_array = vtk_to_numpy(image.GetPointData().GetScalars()) - numpy_array = vtk_array.reshape(dimensions[::-1]) # Reverse dimensions to match NumPy order (z, y, x) - - # Extract the middle X slice - slice_img = numpy_array[:, :, mid_x] - - # Clip values at the 90th percentile - percentile_90 = np.percentile(slice_img, 90) - clipped_slice = np.clip(slice_img, 0, percentile_90) # Cap values at the 90th percentile - - # Normalize the clipped slice to [0, 1] range - normalized_slice = (clipped_slice - clipped_slice.min()) / (clipped_slice.max() - clipped_slice.min() + 1e-8) - - # Plot the normalized slice - plt.figure(figsize=(4.0, 4.0)) - plt.imshow(normalized_slice, cmap='viridis', origin="lower") - plt.title('Middle X', fontsize=9, pad=2) - plt.xlabel('Y', fontsize=8) - plt.ylabel('Z', fontsize=8) - plt.tick_params(axis='both', which='major', labelsize=8, length=2.5, pad=1.5) - - # Save the output image - plt.tight_layout(pad=0.2) - plt.savefig(filepath, dpi=300) - plt.close() - ogo.message(f"Slice image saved to {filepath}") - -def check_image(vtkimage, output_filename=None): - try: - # Assuming 'image' is a vtkImageData object - numpy_array = vtk_to_numpy(vtkimage.GetPointData().GetScalars()) - numpy_array = numpy_array.reshape(vtkimage.GetDimensions(), order='F') - numpy_array = np.transpose(numpy_array, (2, 1, 0)) # Adjust depending on your specific data - image = sitk.GetImageFromArray(numpy_array) - image.SetSpacing(vtkimage.GetSpacing()) - image.SetOrigin(vtkimage.GetOrigin()) - - features = {label: calculate_features(image, label) for label in range(1, 5)} - - checks = { - "VCHECK_PROCESS": "features[1]['volume'] > 4000", - "VCHECK_BODY": "features[2]['volume'] > 3000", - "VCHECK_BCBOT": "features[3]['volume'] > 200", - "VCHECK_BCTOP": "features[4]['volume'] > 200", - "DCHECK_BC": "features[4]['centroid'][2] > features[2]['centroid'][2] > features[3]['centroid'][2]", - "EXTENT_Z_BCBOT": "features[3]['extent'][5] < 20", - "EXTENT_Z_BCTOP": "features[4]['extent'][5] < 20" - } - - results = check_values(features, checks) - metadata = parse_filename(output_filename) - data = {**metadata, **{f'{label}_{key}': val for label, feats in features.items() for key, val in feats.items()}, **results} - - df = pd.DataFrame([data]) - - if output_filename is not None: - check_ending = f"{data['pass']}.csv" - output_filename = output_filename.replace('.csv', check_ending) - ogo.message(f"Saving QC results to {output_filename}") - df.to_csv(output_filename, index=False) - else: - ogo.message(df) - - except Exception as e: - ogo.message(e) - - data = { - "VCHECK_BODY": False, - "VCHECK_PROCESS": False, - "VCHECK_BCBOT": False, - "VCHECK_BCTOP": False, - "DCHECK_BC": False, - "EXTENT_Z_BCBOT": False, - "EXTENT_Z_BCTOP": False, - "pass": False - } - - df = pd.DataFrame([data]) - if output_filename is not None: - check_ending = f"{data['pass']}.csv" - output_filename = output_filename.replace('.csv', check_ending) - ogo.message(f"Saving QC results to {output_filename}") - df.to_csv(output_filename, index=False) - else: - ogo.message(df) - - return data['pass'] - -def resample_to_match(target_image, source_image, interpolation='nearest'): - resampler = vtk.vtkImageResample() - resampler.SetInputData(source_image) - - spacing = target_image.GetSpacing() - resampler.SetAxisOutputSpacing(0, spacing[0]) - resampler.SetAxisOutputSpacing(1, spacing[1]) - resampler.SetAxisOutputSpacing(2, spacing[2]) - - if interpolation == 'nearest': - resampler.SetInterpolationModeToNearestNeighbor() - elif interpolation == 'linear': - resampler.SetInterpolationModeToLinear() - elif interpolation == 'cubic': - resampler.SetInterpolationModeToCubic() - - resampler.Update() - return resampler - -def export_nifti_outputs( - image_vtk, - body_labeled_vtk, - process_labeled_vtk, - cortical_mask_vtk, - inferior_disk_vtk, - superior_disk_vtk, - output_path, -): - """ - Export resampled grayscale image and labeled segmentation for FE input. - - Labels: - 1 = trabecular body - 2 = cortical body - 3 = trabecular process - 4 = cortical process - 5 = inferior disk - 6 = superior disk - """ - import SimpleITK as sitk - import numpy as np - from vtk.util.numpy_support import vtk_to_numpy - import os - - def vtk_to_sitk(vtk_image): - dims = vtk_image.GetDimensions() - np_array = vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape(dims[::-1]) # (z, y, x) - sitk_image = sitk.GetImageFromArray(np_array) - sitk_image.SetSpacing(vtk_image.GetSpacing()) - sitk_image.SetOrigin(vtk_image.GetOrigin()) - return sitk_image - - def get_mask_np(vtk_image): - dims = vtk_image.GetDimensions() - return vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape(dims, order="F") - - ogo.message("Exporting NIfTI files for grayscale image and labeled segmentation...") - - dims = image_vtk.GetDimensions() - combined_array = np.zeros(dims, dtype=np.uint8) - - # Convert to NumPy - body_mask = get_mask_np(body_labeled_vtk) > 0 - process_mask = get_mask_np(process_labeled_vtk) > 0 - cortical_mask = get_mask_np(cortical_mask_vtk) > 0 - inferior_mask = get_mask_np(inferior_disk_vtk) > 0 - superior_mask = get_mask_np(superior_disk_vtk) > 0 - im = get_mask_np(image_vtk) - - - # Assign labels - combined_array[(body_mask) & (~cortical_mask)] = 1 - combined_array[(body_mask) & (cortical_mask)] = 2 - combined_array[(process_mask) & (~cortical_mask)] = 3 - combined_array[(process_mask) & (cortical_mask)] = 4 - combined_array[inferior_mask] = 5 - combined_array[superior_mask] = 6 - im[(combined_array>4) | (combined_array==0)] = 0 - - # Convert segmentation to SimpleITK - seg_sitk = sitk.GetImageFromArray(np.transpose(combined_array, (2, 1, 0))) - seg_sitk.SetSpacing(image_vtk.GetSpacing()) - seg_sitk.SetOrigin(image_vtk.GetOrigin()) - - im_sitk = sitk.GetImageFromArray(np.transpose(im, (2, 1, 0))) - im_sitk.SetSpacing(image_vtk.GetSpacing()) - im_sitk.SetOrigin(image_vtk.GetOrigin()) - - outbase = os.path.splitext(output_path)[0] - im_out = outbase + "_im.nii.gz" - seg_out = outbase + "_seg.nii.gz" - - sitk.WriteImage(im_sitk, im_out) - sitk.WriteImage(seg_sitk, seg_out) - - ogo.message(f"Export complete: {im_out}, {seg_out}") - -###################################################################### VERTEBRA PIPELINE -# Main pipeline to process a vertebra FE -###################################################################### VERTEBRA PIPELINE - - -def process_vertebra(input_mask, input_image, n88model_output_path, body_label, process_label, reference_path, **kwargs): - - pmma_mat_id = kwargs.get("pmma_mat_id", 5000) - iso_resolution = kwargs.get("iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM) - pmma_thick = kwargs.get("pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM) - pmma_intrusion = kwargs.get("pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM) - top_node_set_id = kwargs.get("top_node_set_id", DEFAULT_SPINE_TOP_NODE_SET_ID) - bottom_node_set_id = kwargs.get("bottom_node_set_id", DEFAULT_SPINE_BOTTOM_NODE_SET_ID) - quality_control = kwargs.get("quality_control", True) - registration_scale = kwargs.get("registration_scale", DEFAULT_SPINE_REGISTRATION_SCALE) - registration_min_scale = kwargs.get("registration_min_scale", DEFAULT_SPINE_REGISTRATION_MIN_SCALE) - registration_max_scale = kwargs.get("registration_max_scale", DEFAULT_SPINE_REGISTRATION_MAX_SCALE) - mask_smoothing_spacing_threshold = kwargs.get( - "mask_smoothing_spacing_threshold", - DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - ) - label_smoothing_sigma_mm = float( - kwargs.get("label_smoothing_sigma_mm", DEFAULT_SPINE_LABEL_SMOOTHING_SIGMA_MM) - ) - label_smoothing_threshold = float( - kwargs.get("label_smoothing_threshold", DEFAULT_SPINE_LABEL_SMOOTHING_THRESHOLD) - ) - - #Read Image and Mask - ogo.message(f"Reading image...: {input_image}") - image_reader = read(input_image) - ogo.message(f"Reading mask...: {input_mask}") - mask_reader = resample_to_match(image_reader.GetOutput(), read(input_mask).GetOutput()) - if label_smoothing_sigma_mm > 0: - ogo.message( - "smoothing input label mask " - f"(sigma={label_smoothing_sigma_mm} mm, threshold={label_smoothing_threshold})..." - ) - mask_reader = _NiftiImageHandle( - smooth_label_mask_vtk( - mask_reader.GetOutput(), - sigma_mm=label_smoothing_sigma_mm, - threshold=label_smoothing_threshold, - ) - ) - input_spacing = image_reader.GetOutput().GetSpacing() - - # Get and Check labels - ogo.message(f"Checking if labels {body_label} (body) and {process_label} (process) are present...") - check_vertebra_presence(mask_reader, body_label) - check_vertebra_presence(mask_reader, process_label) - - # Threshold images to extract body, process and full vertebra - ogo.message(f"Thresholding...") - body = threshold(mask_reader.GetOutput(), body_label) - process = threshold(mask_reader.GetOutput(), process_label) - fullvertebra = combine_mask(body.GetOutput(), process.GetOutput()) - - # Crop everything to same BB - ogo.message(f"Cropping to common bounding box...") - isolated_vertebra, isolated_process, isolated_image = crop_and_transform(fullvertebra, body.GetOutput(), process.GetOutput(), image_reader.GetOutput()) - - # Marching Cubes and Registration - ogo.message(f"Starting ICP registration to reference...: {reference_path} ") - icp = get_icp_with_scaling( - body, - reference_path, - scale_factors=registration_scale, - min_scale=registration_min_scale, - max_scale=registration_max_scale, - ) - - # Transform Images and resample at the same time (less interpolation) - reference_to_sample_rotation, reference_to_sample_translation = _matrix_rotation_translation( - icp.GetMatrix() - ) - sample_to_reference_rotation, sample_to_reference_translation = invert_point_transform( - reference_to_sample_rotation, - reference_to_sample_translation, - ) - output_spacing = (float(iso_resolution), float(iso_resolution), float(iso_resolution)) - isolated_model_mask = combine_mask( - isolated_vertebra.GetOutput(), - isolated_process.GetOutput(), - ) - model_surface_points = surface_points_from_vtk_mask( - isolated_model_mask.GetOutput(), - max_points=None, - ) - output_origin, output_size = output_grid_for_point_transform( - model_surface_points, - rotation=sample_to_reference_rotation, - translation=sample_to_reference_translation, - spacing=output_spacing, - margin_voxels=int(kwargs.get("registration_margin_voxels", 4)), - ) - transformed_vertebra = resample_vtk_image_with_point_transform( - isolated_vertebra.GetOutput(), - rotation=sample_to_reference_rotation, - translation=sample_to_reference_translation, - output_origin=output_origin, - output_size=output_size, - output_spacing=output_spacing, - interpolation="nearest", - ) - transformed_process = resample_vtk_image_with_point_transform( - isolated_process.GetOutput(), - rotation=sample_to_reference_rotation, - translation=sample_to_reference_translation, - output_origin=output_origin, - output_size=output_size, - output_spacing=output_spacing, - interpolation="nearest", - ) - transformed_image = resample_vtk_image_with_point_transform( - isolated_image.GetOutput(), - rotation=sample_to_reference_rotation, - translation=sample_to_reference_translation, - output_origin=output_origin, - output_size=output_size, - output_spacing=output_spacing, - interpolation="bspline", - ) - - if should_smooth_resampled_mask(input_spacing, mask_smoothing_spacing_threshold): - ogo.message( - "smoothing resampled body/process masks because input spacing " - f"{input_spacing} exceeds {mask_smoothing_spacing_threshold} mm..." - ) - transformed_vertebra = smooth_binary_mask_vtk(transformed_vertebra, close_iter=1, open_iter=1) - transformed_process = smooth_binary_mask_vtk(transformed_process, close_iter=1, open_iter=1) - else: - ogo.message( - "skipping body/process mask smoothing because input spacing " - f"{input_spacing} is <= {mask_smoothing_spacing_threshold} mm in all dimensions..." - ) - - ogo.message(f"relabelling mask and identifying boundary surfaces...") - # Assuming mask1_data and mask2_data are your initial binary masks - mask1_labeled = label_mask(transformed_vertebra, 2) # Apply label 1 to the first mask - mask2_labeled = label_mask(transformed_process, 1) # Apply label 2 to the second mask - - # Combine masks here (otherwise it does weird interpolations) - combined_masks = add_masks(mask1_labeled.GetOutput(), mask2_labeled.GetOutput()) # Combine the labeled masks - - # This creates an image identifying the top and bottom surfaces of the vertebra - boundary_masks = identify_boundary_surface(combined_masks.GetOutput(), bottom_node_set_id, top_node_set_id) - - ogo.message("creating cortical mask....") - #1 - 3 alterantive (to create a continous cortical shell - changed to 0 5 ) - - cort_mask = generate_cortical_mask(transformed_image, combined_masks.GetOutput(), threshold=1, min_th=2, max_th=5) - - #writer = vtk.vtkXMLImageDataWriter() - #writer.SetFileName('/home/matthias.walle/work/fem/CT_FE_TEMPLATE/MODELS/10001_QCT_vertebra_20_cortmask.vti') - #writer.SetInputData(cort_mask) # For VTK 8+ - #writer.Write() - - ogo.message(f"converting image to material ID...") - # This converts the raw image to a material ID mapped image - n_bins = 128 - bone_image, bin_centers = convert_image_to_material(transformed_image, combined_masks.GetOutput(), n_bins=n_bins, cort_mask = cort_mask) - - ogo.message(f"padding images...") - # Pad images before we add the disks (otherwise they may not have right pmma_thick) - padded_image = pad_vtk_image(bone_image, axis='x', pmma_thick=pmma_thick, pad_value=0) - padded_transformed_image = pad_vtk_image(transformed_image, axis='x', pmma_thick=pmma_thick, pad_value=0) - padded_mask = pad_vtk_image(boundary_masks, axis='x', pmma_thick=pmma_thick, pad_value=0) - padded_cort_mask = pad_vtk_image(cort_mask, axis='x', pmma_thick=pmma_thick, pad_value=0) - padded_mask1 = pad_vtk_image(mask1_labeled.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) - padded_mask2 = pad_vtk_image(mask2_labeled.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) - - ogo.message( - "generating fixed-thickness anatomy body-cap disks " - f"(pmma_thick = {pmma_thick} mm total, pmma_intrusion = {pmma_intrusion} mm)..." - ) - transformed_body_bounds = foreground_voxel_center_bounds(padded_mask1) - body_bounds = transformed_body_bounds - inferior_plane = bbox_relative_contact_plane( - body_bounds, - center_fraction=SPINE_INFERIOR_CONTACT_CENTER_FRACTION, - size_fraction=SPINE_CONTACT_SIZE_FRACTION, - projection_axis="z", - shape="anatomy", - ) - superior_plane = bbox_relative_contact_plane( - body_bounds, - center_fraction=SPINE_SUPERIOR_CONTACT_CENTER_FRACTION, - size_fraction=SPINE_CONTACT_SIZE_FRACTION, - projection_axis="z", - shape="anatomy", - ) - ogo.message( - "Inferior PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" - % ( - inferior_plane["center"], - inferior_plane["normal"], - inferior_plane["u_axis"], - inferior_plane["v_axis"], - inferior_plane["size"], - ) - ) - ogo.message( - "Superior PMMA Plane: center=%s normal=%s u=%s v=%s size=%s" - % ( - superior_plane["center"], - superior_plane["normal"], - superior_plane["u_axis"], - superior_plane["v_axis"], - superior_plane["size"], - ) - ) - required_bounds = [ - projected_material_disk_required_bounds( - padded_mask1, - center=plane["center"], - normal=plane["normal"], - u_axis=plane["u_axis"], - v_axis=plane["v_axis"], - size=plane["size"], - shape=plane["shape"], - thickness=pmma_thick, - intrusion=pmma_intrusion, - ) - for plane in (inferior_plane, superior_plane) - ] - required_bounds = [bounds for bounds in required_bounds if bounds is not None] - if required_bounds: - bounds_array = np.asarray(required_bounds, dtype=float) - desired_bounds = ( - float(bounds_array[:, 0].min()), - float(bounds_array[:, 1].max()), - float(bounds_array[:, 2].min()), - float(bounds_array[:, 3].max()), - float(bounds_array[:, 4].min()), - float(bounds_array[:, 5].max()), - ) - ( - padded_image, - padded_transformed_image, - padded_mask, - padded_cort_mask, - padded_mask1, - padded_mask2, - ), contact_padding = pad_vtk_images_to_physical_bounds( - [ - padded_image, - padded_transformed_image, - padded_mask, - padded_cort_mask, - padded_mask1, - padded_mask2, - ], - desired_bounds=desired_bounds, - constants=[0, 0, 0, 0, 0, 0], - ) - if any(contact_padding["lower"]) or any(contact_padding["upper"]): - ogo.message( - "expanded image canvas for projected disks: lower=%s upper=%s" - % (contact_padding["lower"], contact_padding["upper"]) - ) - inferior_disk = generate_projected_material_disk_vtk( - padded_image, - surface_vtk_image=padded_mask1, - exclusion_vtk_image=padded_mask1, - center=inferior_plane["center"], - normal=inferior_plane["normal"], - u_axis=inferior_plane["u_axis"], - v_axis=inferior_plane["v_axis"], - size=inferior_plane["size"], - shape=inferior_plane["shape"], - thickness=pmma_thick, - intrusion=pmma_intrusion, - anatomy_constrained=True, - output_value=1, - ) - superior_disk = generate_projected_material_disk_vtk( - padded_image, - surface_vtk_image=padded_mask1, - exclusion_vtk_image=padded_mask1, - center=superior_plane["center"], - normal=superior_plane["normal"], - u_axis=superior_plane["u_axis"], - v_axis=superior_plane["v_axis"], - size=superior_plane["size"], - shape=superior_plane["shape"], - thickness=pmma_thick, - intrusion=pmma_intrusion, - anatomy_constrained=True, - output_value=1, - ) - - ogo.message(f"merging disks with image...") - # Now we create the image with the disks and the boundary image as well - image_with_pads = merge_vtk_images( - [padded_image, superior_disk, inferior_disk], - [None, pmma_mat_id, pmma_mat_id], - overwrite_existing=False, - ) - - boundary_masks_with_pads = merge_vtk_images([ - padded_mask, inferior_disk, superior_disk], [None, bottom_node_set_id, top_node_set_id]) - - # This is the final model that we will use for the FEA. - # For the future --> pull out material table to have option without disks - ogo.message("generating n88model file...") - model = create_microfe_model( - image_with_pads, - boundary_masks_with_pads, - bin_centers, - top_boundary_mask_image=superior_disk, - bottom_boundary_mask_image=inferior_disk, - top_boundary_mask_label=1, - bottom_boundary_mask_label=1, - **kwargs, - ) - - if kwargs.get("export_nifti", False): - ogo.message("generating nifti files...") - - export_nifti_outputs( - padded_transformed_image, - padded_mask1, - padded_mask2, - padded_cort_mask, - inferior_disk, - superior_disk, - n88model_output_path - ) - - # Quality control and output. If quality control is activated output only provided if it passes - if quality_control: - image_path = n88model_output_path.replace(".n88model", ".png") - dataframe_path = n88model_output_path.replace(".n88model", "_BCcheck.csv") - - ogo.message(f"Starting QC...") - test = check_image(boundary_masks, dataframe_path) - visualize_slice(image_with_pads, image_path) - - ogo.message(f"Writing n88model file: {n88model_output_path}") - writer = vtkbone.vtkboneN88ModelWriter() - writer.SetInputData(model) - writer.SetFileName(n88model_output_path) - writer.Update() - - - -###################################################################### MAIN -## Main function containing file I/O via argparse -###################################################################### MAIN - - -def main(): - description = ''' - This script sets up the L4 vertebral compression FE model from the - density (K2HPO4) calibrated image. This script sets up the model for a L4 vertebra - (including arch and pedicles). The analysis resamples the image to isotropic voxels, - transforms the image, applies the bone mask and bins the data. It then creates the FE - model for solving using FAIM (v8.1, Numerics Solutions Ltd, Calgary, Canada - Steven - Boyd). - - Input: Calibrated K2HPO4 Image (*.nii), Bone Mask (*_MASK.nii) - - Optional Parameters: - 1) Mask Threshold - 2) Isotropic resample voxel size - 3) Power-law exponent - 4) Power-law coefficient - 5) Bone Poissons ratio - 6) PMMA Elastic Modulus - 7) PMMA Poissons ratio - 8) PMMA pmma_thick - 9) PMMA material ID - 10) FE displacement - - Output: N88 Model (*.n88model) - ''' - - parser = argparse.ArgumentParser( - formatter_class=argparse.RawTextHelpFormatter, - prog="OgoSpineCompressionFe", - description=description - ) - - parser.add_argument("calibrated_image", help="*_K2HPO4.nii image file") - parser.add_argument("bone_mask", help="*_MASK.nii mask image of bone") - - parser.add_argument("--mask_threshold", type=int, required=True, - help="Label value for the vertebral body in the bone mask.") - parser.add_argument("--process_mask_threshold", type=int, required=True, - help="Label value for the vertebral process in the bone mask.") - parser.add_argument("--output_path", type=str, default=None, - help="Set output path for the N88 model file. (default: same as input image)") - parser.add_argument("--quality_control", type=_as_bool, default=True, - help="Set quality control flag (visualise output and add volume checks). (default: %(default)s)") - parser.add_argument("--iso_resolution", type=float, default=DEFAULT_SPINE_ISO_RESOLUTION_MM, - help="Set the isotropic voxel size [in mm]. (default: %(default)s [mm])") - parser.add_argument("--mask_smoothing_spacing_threshold", type=float, default=DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - help="Smooth resampled body/process masks only when an input spacing dimension exceeds this value. (default: %(default)s [mm])") - #parser.add_argument("--elastic_exponent", type=float, default=2.29, - # help="Sets the exponent (b) for power law: E=A(den)^b. (default: %(default)s)") - #parser.add_argument("--elastic_Emax", type=float, default=10500, - # help="Sets the coefficient (A) Elastic Modulus value for the power law: E=A(den)^b. (default: %(default)s [MPa])") - parser.add_argument("--poissons_ratio", type=float, default=DEFAULT_SPINE_POISSONS_RATIO, - help="Sets the Poisson's ratio for the material(s) in the FE model. (default: %(default)s)") - parser.add_argument("--pmma_E", type=float, default=DEFAULT_SPINE_PMMA_E_MPA, - help="Sets the Elastic Modulus for PMMA caps in the FE model. (default: %(default)s [MPa])") - parser.add_argument("--pmma_v", type=float, default=DEFAULT_SPINE_PMMA_POISSONS_RATIO, - help="Sets the Poisson's ratio for the PMMA material(s) in the FE model. (default: %(default)s)") - parser.add_argument("--pmma_thick", type=int, default=DEFAULT_SPINE_PMMA_THICKNESS_MM, - help="Sets the total PMMA cap thickness in the FE model. (default: %(default)s [mm])") - parser.add_argument("--pmma_intrusion", type=int, default=DEFAULT_SPINE_PMMA_INTRUSION_MM, - help="Sets how far anatomy can occupy the fixed PMMA cap thickness. (default: %(default)s [mm])") - parser.add_argument("--pmma_mat_id", type=int, default=DEFAULT_SPINE_PMMA_MATERIAL_ID, - help="Sets the material ID for the PMMA blocks. (default: %(default)s)") - parser.add_argument("--fe_displacement", type=float, default=DEFAULT_SPINE_FE_DISPLACEMENT_MM, - help="Sets the applied displacement in [mm] to the FE model. (default: %(default)s [mm])") - parser.add_argument("--reference_path", type=str, required=False, default=None, - help="Path to the reference vtk file for ICP registration. (default: None)") - parser.add_argument("--registration_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_SCALE, - help="Manual affine scale applied to the reference body before ICP. Use a scalar or sx,sy,sz. If omitted, PCA-based scaling is used.") - parser.add_argument("--registration_min_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_MIN_SCALE, - help="Minimum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") - parser.add_argument("--registration_max_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_MAX_SCALE, - help="Maximum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") - parser.add_argument("--registration_backend", choices=("vtk",), default=DEFAULT_SPINE_REGISTRATION_BACKEND, - help="Spine reference alignment backend. (default: %(default)s)") - parser.add_argument("--top_node_set_id", type=int, default=DEFAULT_SPINE_TOP_NODE_SET_ID, - help="ID for the top node set. (default: %(default)s)") - parser.add_argument("--bottom_node_set_id", type=int, default=DEFAULT_SPINE_BOTTOM_NODE_SET_ID, - help="ID for the bottom node set. (default: %(default)s)") - parser.add_argument("--pmma_yield_compression", type=float, default=None, - help="Sets the yield strength in compression for PMMA material in the FE model. (default: %(default)s [MPa])") - parser.add_argument("--pmma_yield_tension", type=float, default=None, - help="Sets the yield strength in tension for PMMA material in the FE model. (default: %(default)s [MPa])") -# parser.add_argument("--bone_yield_compression", type=float, default=None, -# help="Sets the yield strength in compression for bone material in the FE model. (default: %(default)s [MPa])") -# parser.add_argument("--bone_yield_tension", type=float, default=None, -# help="Sets the yield strength in tension for bone material in the FE model. (default: %(default)s [MPa])") -# parser.add_argument("--cort_elastic_Emax", type=float, -# help="Sets the maximum elastic modulus for cortical bone in the FE model. (default: %(default)s)") -# parser.add_argument("--cort_elastic_exponent", type=float, -# help="Sets the elastic exponent for cortical bone in the FE model.") - parser.add_argument("--cort_poissons_ratio", type=float, - help="Sets the Poisson's ratio for cortical bone in the FE model.") -# parser.add_argument("--cort_yield_compression", type=float, -# help="Sets the yield strength in compression for cortical bone in the FE model.") -# parser.add_argument("--cort_yield_tension", type=float, -# help="Sets the yield strength in tension for cortical bone in the FE model.") - parser.add_argument("--elastic_E_func", type=str, default="default_E", - help="Function name for trabecular bone Young’s modulus. (default: default_E)") - parser.add_argument("--yield_comp_func", type=str, - help="Function name for trabecular compression yield. (default: default_yc)") - parser.add_argument("--yield_tens_func", type=str, - help="Function name for trabecular tension yield. (default: default_yt)") - - parser.add_argument("--cort_elastic_E_func", type=str, - help="Function name for cortical bone Young’s modulus. (default: default_E)") - parser.add_argument("--cort_yield_comp_func", type=str, - help="Function name for cortical compression yield. (default: default_yc)") - parser.add_argument("--cort_yield_tens_func", type=str, - help="Function name for cortical tension yield. (default: default_yt)") - parser.add_argument("--appendix", default=None, type=str, - help="Extra appendix for output file") - parser.add_argument("--export_nifti", action='store_true', - help="If set, exports resampled grayscale image and labeled segmentation.") - - args = parser.parse_args() - - # Print arguments - ogo.message(echo_arguments('OgoSpineCompressionFe', vars(args))) - - # Prepare the output file - basename = remove_extension(os.path.basename(args.calibrated_image)) - - if args.output_path is None: - output_dir = os.path.dirname(args.calibrated_image) - else: - output_dir = args.output_path - - output_dir = os.path.abspath(output_dir) - - if args.appendix is None: - output_file = os.path.join(output_dir, f"{basename}_{args.mask_threshold}.n88model") - else: - output_file = os.path.join(output_dir, f"{basename}_{args.appendix}.n88model") - - ogo.message(f'N88Model File path: {output_file}') - # Set default reference path if not provided - reference_path = args.reference_path - if reference_path is None: - reference_path = str(default_spine_reference_path()) - - # Extract all kwargs - kwargs = vars(args).copy() # Convert parsed arguments to a dictionary - kwargs.pop("calibrated_image") # Remove positional argument - kwargs.pop("bone_mask") # Remove positional argument - kwargs.pop("mask_threshold") # Remove required argument - kwargs.pop("process_mask_threshold") # Remove required argument - kwargs.pop("reference_path") # Ensure updated reference path - - # Run the vertebra processing - process_vertebra( - args.bone_mask, - args.calibrated_image, - output_file, - args.mask_threshold, - args.process_mask_threshold, - reference_path, - **kwargs # Pass remaining arguments as kwargs - ) - -###################################################################### MAIN -if __name__ == '__main__': - main() diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index 6db4138..6e1fb7a 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -193,7 +193,7 @@ def test_dry_run_prints_commands_without_running(monkeypatch, capsys): assert calls == [] assert ( capsys.readouterr().out.strip() - == "OgoSpineCompressionFe density.nii.gz spine_mask.nii.gz " + == "ogoFEA-spine-builder density.nii.gz spine_mask.nii.gz " "--mask_threshold 6 --process_mask_threshold 5 --appendix L2 " "--quality_control False" ) diff --git a/tests/fea/test_module_layout.py b/tests/fea/test_module_layout.py new file mode 100644 index 0000000..8d987fb --- /dev/null +++ b/tests/fea/test_module_layout.py @@ -0,0 +1,20 @@ +from pathlib import Path + + +def test_fe_workflow_builders_live_in_domain_modules(): + repo_root = Path(__file__).resolve().parents[2] + fea_dir = repo_root / "ogo" / "fea" + + assert (fea_dir / "femur.py").exists() + assert (fea_dir / "spine.py").exists() + assert not (fea_dir / "sideways_fall.py").exists() + assert not (fea_dir / "spine_compression.py").exists() + + +def test_domain_modules_expose_workflow_entry_points(): + from ogo.fea import femur, spine + + assert callable(femur.main) + assert callable(femur.sidewaysFallFe) + assert callable(spine.main) + assert callable(spine.process_vertebra) diff --git a/tests/fea/test_sideways_fall.py b/tests/fea/test_sideways_fall.py index 95ae34b..106608d 100644 --- a/tests/fea/test_sideways_fall.py +++ b/tests/fea/test_sideways_fall.py @@ -9,9 +9,9 @@ def test_sideways_fall_icp_path_does_not_pre_rotate_input_images(): pytest.importorskip("vtk") pytest.importorskip("vtkbone") - from ogo.fea import sideways_fall + from ogo.fea import femur - source = textwrap.dedent(inspect.getsource(sideways_fall.sidewaysFallFe)) + source = textwrap.dedent(inspect.getsource(femur.sidewaysFallFe)) tree = ast.parse(source) pre_rotation_calls = [ node @@ -26,9 +26,9 @@ def test_sideways_fall_icp_path_uses_voxel_surface_points(): pytest.importorskip("vtk") pytest.importorskip("vtkbone") - from ogo.fea import sideways_fall + from ogo.fea import femur - source = textwrap.dedent(inspect.getsource(sideways_fall.sidewaysFallFe)) + source = textwrap.dedent(inspect.getsource(femur.sidewaysFallFe)) tree = ast.parse(source) called_names = { node.func.id diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py index 5d09722..f76a026 100644 --- a/tests/fea/test_spinefe_benchmark.py +++ b/tests/fea/test_spinefe_benchmark.py @@ -47,7 +47,7 @@ def GetPoint(self, point_id): def test_spine_qc_filename_parser_handles_compact_generated_names(): - from ogo.fea.spine_compression import parse_filename + from ogo.fea.spine import parse_filename metadata = parse_filename("/tmp/density_L4_BCcheck.csv") @@ -80,7 +80,7 @@ def test_merge_vtk_images_can_preserve_existing_material_voxels(): vtk = pytest.importorskip("vtk") from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy - from ogo.fea.spine_compression import merge_vtk_images + from ogo.fea.spine import merge_vtk_images material = np.zeros((3, 3, 3), dtype=np.float32) material[1, 1, 1] = 42 @@ -110,9 +110,9 @@ def vtk_image(array): def test_spine_workflow_uses_projected_contact_disks(): pytest.importorskip("vtk") - from ogo.fea import spine_compression + from ogo.fea import spine - source = textwrap.dedent(inspect.getsource(spine_compression.process_vertebra)) + source = textwrap.dedent(inspect.getsource(spine.process_vertebra)) tree = ast.parse(source) called_names = { node.func.id @@ -127,9 +127,9 @@ def test_spine_workflow_uses_projected_contact_disks(): def test_spine_registration_scaling_uses_voxel_surface_points(): pytest.importorskip("vtk") - from ogo.fea import spine_compression + from ogo.fea import spine - source = textwrap.dedent(inspect.getsource(spine_compression.get_icp_with_scaling)) + source = textwrap.dedent(inspect.getsource(spine.get_icp_with_scaling)) assert "surface_points_from_vtk_mask" in source assert "point_cloud_axis_lengths" in source @@ -139,9 +139,9 @@ def test_spine_registration_scaling_uses_voxel_surface_points(): def test_spine_workflow_resamples_to_explicit_reference_grid(): pytest.importorskip("vtk") - from ogo.fea import spine_compression + from ogo.fea import spine - source = textwrap.dedent(inspect.getsource(spine_compression.process_vertebra)) + source = textwrap.dedent(inspect.getsource(spine.process_vertebra)) assert "output_grid_for_point_transform" in source assert "resample_vtk_image_with_point_transform" in source @@ -151,9 +151,9 @@ def test_spine_workflow_resamples_to_explicit_reference_grid(): def test_spine_contact_planes_use_generated_model_bbox(): pytest.importorskip("vtk") - from ogo.fea import spine_compression + from ogo.fea import spine - source = textwrap.dedent(inspect.getsource(spine_compression.process_vertebra)) + source = textwrap.dedent(inspect.getsource(spine.process_vertebra)) assert "bounds_with_reference_extent" not in source assert "body_bounds = transformed_body_bounds" in source @@ -162,9 +162,9 @@ def test_spine_contact_planes_use_generated_model_bbox(): def test_spine_projected_disks_pad_to_contact_plane_bounds(): pytest.importorskip("vtk") - from ogo.fea import spine_compression + from ogo.fea import spine - source = textwrap.dedent(inspect.getsource(spine_compression.process_vertebra)) + source = textwrap.dedent(inspect.getsource(spine.process_vertebra)) assert "projected_material_disk_required_bounds" in source assert "pad_vtk_images_to_physical_bounds" in source @@ -198,7 +198,7 @@ def test_spine_crop_to_bounding_box_updates_origin_and_extent(): vtk = pytest.importorskip("vtk") from vtk.util.numpy_support import numpy_to_vtk - from ogo.fea.spine_compression import crop_to_bounding_box + from ogo.fea.spine import crop_to_bounding_box values = np.zeros((6, 7, 8), dtype=np.uint8) values[2:5, 3:6, 4:7] = 1 @@ -221,7 +221,7 @@ def test_spine_compression_read_preserves_reader_get_output_contract(tmp_path): sitk = pytest.importorskip("SimpleITK") pytest.importorskip("vtk") - from ogo.fea.spine_compression import read + from ogo.fea.spine import read image_path = tmp_path / "image.nii.gz" image = sitk.GetImageFromArray(np.ones((2, 3, 4), dtype=np.uint8)) From aa2a5fd0fcc2a4aa8c672c8e66542ccd0ba257d3 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:27:37 -0600 Subject: [PATCH 20/67] Stabilize FE workflow geometry --- ogo/fea/README.md | 4 +- ogo/fea/boundary.py | 130 ++++++++- ogo/fea/femur.py | 264 ++++++++++++++---- ogo/fea/spine.py | 86 ++++-- ogo/util/faim.py | 20 +- .../fixtures/fe_workflow_defaults_locked.json | 32 ++- tests/fea/test_boundary.py | 70 +++++ tests/fea/test_femur.py | 49 +++- tests/fea/test_sideways_fall.py | 76 +++++ tests/fea/test_spinefe_benchmark.py | 19 ++ tests/fea/test_workflow_locked_defaults.py | 2 + tests/util/test_faim.py | 26 ++ 12 files changed, 675 insertions(+), 103 deletions(-) diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 624fa84..2abf673 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -209,7 +209,7 @@ Maintained spine and femur models write the same columns: | `applied_displacement` | Prescribed displacement in the solved `.n88model`. | | `reaction_force_N` | Reaction force from the solved model at the prescribed displacement. | | `stiffness_N_per_mm` | Reaction force divided by applied displacement when computable. | -| `characteristic_length_mm` | Spine: distance between `body_top` and `body_bottom` centroids along the loading axis. Femur: distance between femoral-head and greater-trochanter PMMA node-set centroids along the loading axis. | +| `characteristic_length_mm` | Spine: distance between `body_top` and `body_bottom` centroids along the loading axis. Femur: full generated-model span along the loading axis. | ## Reaction Force and Displacement Endpoint @@ -234,7 +234,7 @@ size are compared at a similar normalized deformation: | Model | Default endpoint | Length used for conversion | Reason | | --- | --- | --- | --- | | Spine compression | `0.68%` strain | Distance between `body_top` and `body_bottom` centroids along the loading axis | Matches the vertebral strength endpoint reported by Crawford et al. (2003), and keeps the solved displacement scaled to each vertebral body height. | -| Hip sideways fall | `4%` displacement | Distance between femoral-head and greater-trochanter PMMA node-set centroids along the loading axis | Keeps the sideways-fall load endpoint scaled to each generated femur model instead of using one fixed millimeter displacement for all femora. | +| Hip sideways fall | `4%` displacement | Full generated-model span along the loading axis | Keeps the sideways-fall load endpoint scaled to each generated femur model instead of using one fixed millimeter displacement for all femora. | FAIM/N88 stores prescribed displacement in millimeters, not percent strain. Before solving, `ogoFEA` converts the percent endpoint to millimeters from the diff --git a/ogo/fea/boundary.py b/ogo/fea/boundary.py index 0d33520..f54df59 100644 --- a/ogo/fea/boundary.py +++ b/ogo/fea/boundary.py @@ -36,10 +36,33 @@ def _contact_plane_axes(projection_axis, normal_sign): """Return stable in-plane axes for a coordinate-axis contact plane.""" axis = axis_index(projection_axis) sign = 1.0 if float(normal_sign) >= 0.0 else -1.0 + # Workflow editor markups serialize nearly-cardinal RAS axes with tiny + # floating-point components. The projected-disk algorithm buckets columns in + # plane coordinates, so preserving that authored basis avoids off-by-one + # footprint differences at the contact edge. + ras_quarter_turn = 6.123233995736766e-17 if axis == 0: return (0.0, 0.0, sign), (0.0, -1.0, 0.0) if axis == 1: - return (0.0, 0.0, -sign), (-1.0, 0.0, 0.0) + if sign > 0.0: + return ( + 1.208164965010145e-16, + ras_quarter_turn, + -1.0, + ), ( + -1.0, + ras_quarter_turn, + -1.208164965010145e-16, + ) + return ( + 9.331099404642518e-17, + ras_quarter_turn, + 1.0, + ), ( + -1.0, + -ras_quarter_turn, + 9.331099404642518e-17, + ) return (1.0, 0.0, 0.0), (0.0, sign, 0.0) @@ -273,6 +296,50 @@ def smooth_label_mask_vtk(label_vtk, *, sigma_mm=1.0, threshold=0.5): ) +def resample_vtk_image_to_spacing(vtk_image, target_spacing_mm, *, interpolation="nearest"): + """Resample a VTK image to isotropic spacing while preserving physical origin.""" + import numpy as np + import SimpleITK as sitk + import vtk + from vtk.util.numpy_support import numpy_to_vtk + + target = float(target_spacing_mm) + target_spacing = (target, target, target) + array_xyz = vtk_image_to_numpy(vtk_image) + image = sitk.GetImageFromArray(np.transpose(array_xyz, (2, 1, 0))) + image.SetSpacing(tuple(float(value) for value in vtk_image.GetSpacing())) + image.SetOrigin(tuple(float(value) for value in vtk_image.GetOrigin())) + + original_size = np.asarray(image.GetSize(), dtype=np.int64) + original_spacing = np.asarray(image.GetSpacing(), dtype=np.float64) + new_spacing = np.asarray(target_spacing, dtype=np.float64) + new_size = np.maximum(1, np.round(original_size * original_spacing / new_spacing)).astype(int) + + resampler = sitk.ResampleImageFilter() + resampler.SetOutputSpacing(target_spacing) + resampler.SetSize([int(value) for value in new_size]) + resampler.SetOutputOrigin(image.GetOrigin()) + resampler.SetOutputDirection(image.GetDirection()) + resampler.SetDefaultPixelValue(0) + resampler.SetInterpolator( + sitk.sitkNearestNeighbor + if interpolation == "nearest" + else sitk.sitkBSpline + if interpolation == "bspline" + else sitk.sitkLinear + ) + + out_xyz = np.transpose(sitk.GetArrayFromImage(resampler.Execute(image)), (2, 1, 0)) + vtk_type = vtk.VTK_UNSIGNED_CHAR if interpolation == "nearest" else vtk_image.GetScalarType() + scalars = numpy_to_vtk(out_xyz.ravel(order="F"), deep=True, array_type=vtk_type) + out = vtk.vtkImageData() + out.SetDimensions(out_xyz.shape) + out.SetOrigin(*vtk_image.GetOrigin()) + out.SetSpacing(*target_spacing) + out.GetPointData().SetScalars(scalars) + return out + + def _bounding_box(mask): import numpy as np @@ -681,6 +748,67 @@ def pad_vtk_images_to_physical_bounds(vtk_images, *, desired_bounds, constants=N } +def fit_vtk_images_to_physical_bounds(vtk_images, *, desired_bounds, constants=None): + """Crop or pad aligned VTK images to the requested physical bounds. + + The output extent is chosen from voxel-center coordinates, then reset to a + zero-based extent with the origin shifted so physical coordinates are + preserved. Values outside the source extent are filled from ``constants``. + """ + import numpy as np + import vtk + + images = list(vtk_images) + if not images: + return [], {"output_extent": (0, -1, 0, -1, 0, -1)} + constants = [0] * len(images) if constants is None else list(constants) + if len(constants) != len(images): + raise ValueError("constants must match vtk_images length.") + if len(desired_bounds) != 6: + raise ValueError("desired_bounds must contain x/y/z min/max values.") + + reference = images[0] + spacing = np.asarray(reference.GetSpacing(), dtype=float) + if np.any(spacing <= 0.0): + raise ValueError("VTK image spacing must be positive.") + origin = np.asarray(reference.GetOrigin(), dtype=float) + desired = np.asarray(desired_bounds, dtype=float) + desired_min = desired[[0, 2, 4]] + desired_max = desired[[1, 3, 5]] + lower_index = np.floor((desired_min - origin) / spacing).astype(int) + upper_index = np.ceil((desired_max - origin) / spacing).astype(int) + output_extent = ( + int(lower_index[0]), + int(upper_index[0]), + int(lower_index[1]), + int(upper_index[1]), + int(lower_index[2]), + int(upper_index[2]), + ) + + fitted = [] + for image, constant in zip(images, constants): + pad = vtk.vtkImageConstantPad() + pad.SetInputData(image) + pad.SetOutputWholeExtent(output_extent) + pad.SetConstant(float(constant)) + pad.Update() + out = vtk.vtkImageData() + out.DeepCopy(pad.GetOutput()) + dims_out = out.GetDimensions() + image_origin = image.GetOrigin() + image_spacing = out.GetSpacing() + out.SetOrigin( + float(image_origin[0]) + float(output_extent[0]) * float(image_spacing[0]), + float(image_origin[1]) + float(output_extent[2]) * float(image_spacing[1]), + float(image_origin[2]) + float(output_extent[4]) * float(image_spacing[2]), + ) + out.SetExtent(0, dims_out[0] - 1, 0, dims_out[1] - 1, 0, dims_out[2] - 1) + fitted.append(out) + + return fitted, {"output_extent": output_extent} + + def projected_material_disk_required_bounds( surface_vtk_image, *, diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 03b0220..6d2f8f1 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -17,11 +17,12 @@ 3. Smooth the transformed femur mask with one binary close/open pass only when at least one input spacing dimension is coarser than 2 mm. If a compartment mask is supplied, the derived cortical binary mask follows the same rule. -4. Standardize the distal shaft by cutting on a flat model-grid z plane. The - default cut mode detects the lesser-trochanter cross-section peak and keeps - the femur to 50 mm distal to that landmark. If the scan does not include the - required distal field of view, model generation fails instead of silently - using a shorter shaft. ``fixed_length`` mode is available for debugging. +4. Standardize the distal shaft with a bbox-ratio crop in the input image + coordinate system before registration. The short femur dimension is kept as + the reference length, the shaft axis is cropped to 1.3 times that reference + length from the distal end, and the transformed crop face becomes the distal + support surface. ``lesser_trochanter`` and ``fixed_length`` modes are + available for debugging or historical comparisons. 5. Generate two geometric PMMA fixtures from bbox-relative contact planes: a femoral-head loading fixture on the high-y side and a greater-trochanter contact fixture on the low-y side. Defaults are 10 mm PMMA thickness and @@ -40,6 +41,7 @@ from pathlib import Path from ogo.fea.alignment import ( + estimate_rigid_icp, point_cloud_axis_lengths, polydata_from_points, polydata_points, @@ -66,13 +68,15 @@ DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM = 2.0 FEMORAL_HEAD_FIXTURE_WIDTH_EXTENSION_MM = 10.0 FEMORAL_HEAD_FIXTURE_LONG_AXIS_EXTENSION_MM = 80.0 -SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION = (1.1, 1.1) -FEMORAL_HEAD_FIXTURE_CENTER_FRACTION = (0.5, 1.0278767152, 0.9650933279) -GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION = (0.5, 0.0151983839, 0.6551527108) -DISTAL_SHAFT_FIXTURE_CENTER_FRACTION = (0.1958514895, 0.5708725889, 0.0601674635) -DISTAL_SHAFT_FIXTURE_SIZE_FRACTION = (0.3640088821, 0.6351022953) -DEFAULT_FEMUR_BBOX_RATIO = (1.0, 1.2, None) -DEFAULT_FEMUR_BBOX_CROP_FROM = (None, "min", None) +SIDEWAYS_FALL_FIXTURE_SHAPE = "anatomy" +SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION = (1.0, 1.0) +FEMORAL_HEAD_FIXTURE_CENTER_FRACTION = (0.5, 1.1, 0.5) +GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION = (0.5, -0.1, 0.5) +DISTAL_SHAFT_FIXTURE_CENTER_FRACTION = (0.5, 0.5, -0.1) +DISTAL_SHAFT_FIXTURE_SIZE_FRACTION = (1.0, 1.0) +DISTAL_SHAFT_FIXTURE_NORMAL = (0.0, -0.2588190451025207, 0.9659258262890683) +DEFAULT_FEMUR_BBOX_RATIO = (1.0, 1.3, None) +DEFAULT_FEMUR_BBOX_CROP_FROM = (None, "max", None) DEFAULT_FEMUR_REFERENCE_MIN_SCALE = (0.8, 0.8, 0.75) DEFAULT_FEMUR_REFERENCE_MAX_SCALE = (1.2, 1.2, 1.3) DEFAULT_PMMA_THICKNESS_MM = 10.0 @@ -124,6 +128,27 @@ def matrix4x4_to_numpy(matrix): return values +def point_transform_to_vtk_matrix(rotation, translation): + """Return a VTK matrix for ``p_out = p_in @ rotation.T + translation``.""" + import numpy as np + import vtk + + rotation_arr = np.asarray(rotation, dtype=np.float64) + translation_arr = np.asarray(translation, dtype=np.float64) + if rotation_arr.shape != (3, 3): + raise ValueError("rotation must have shape 3 x 3.") + if translation_arr.shape != (3,): + raise ValueError("translation must contain three values.") + + matrix = vtk.vtkMatrix4x4() + matrix.Identity() + for row in range(3): + for col in range(3): + matrix.SetElement(row, col, float(rotation_arr[row, col])) + matrix.SetElement(row, 3, float(translation_arr[row])) + return matrix + + def reference_grid_from_output_to_input_matrix( points_xyz, output_to_input_matrix, @@ -272,9 +297,11 @@ def scale_reference_point_cloud_to_sample( max_values = _scale_triplet(max_scale, "max_scale") scale = np.clip(scale, min_values, max_values) - scaled_points = reference_points * scale + reference_center = reference_points.mean(axis=0) + scaled_points = (reference_points - reference_center) * scale + reference_center return polydata_from_points(scaled_points), { "source": "voxel_surface_point_cloud", + "reference_center": reference_center.tolist(), "reference_axis_lengths": reference_lengths.tolist(), "sample_axis_lengths": sample_lengths.tolist(), "scale_factors": scale.tolist(), @@ -359,19 +386,30 @@ def _crop_from_value(value): raise ValueError("bbox_crop_from values must be min, max, center, or null.") +def _opposite_crop_end(value): + if value == "min": + return "max" + if value == "max": + return "min" + return value + + def _parse_bbox_crop_from_recipe(values): """Return bbox crop-end values in Ogo's x/y/z image order. The recipe uses the same reference/constrained/free convention as the Slicer-authored workflow. For the hip sideways-fall recipe the mapped order - is ``free, reference, constrained`` in Ogo's x/y/z image axes. + is ``free, reference, constrained`` in Ogo's x/y/z image axes. The workflow + crop ends are stored in patient-space axis directions, while VTK image + storage runs opposite on the free and constrained axes used by this recipe; + those two ends are therefore flipped during the axis-order conversion. """ if values is None: return None, None, None if len(values) != 3: raise ValueError("bbox_crop_from must contain reference/constrained/free values.") reference, constrained, free = (_crop_from_value(item) for item in values) - return free, reference, constrained + return _opposite_crop_end(free), reference, _opposite_crop_end(constrained) def _vtk_image_from_array(array, template_vtk_image, *, origin, vtk_array_type=None): @@ -448,8 +486,8 @@ def crop_vtk_images_to_bbox_ratio( Ratios are interpreted in Ogo's x/y/z VTK image order. The returned crop-face image has value 1 on the newly exposed one-sided crop surface. - For the hip recipe this is the z-min face created by - ``bbox_ratio=(1, 1.2, None)`` and ``bbox_crop_from=(None, "min", None)``. + For the hip recipe this is the exposed shaft face created by + ``bbox_ratio=(1, 1.3, None)`` and ``bbox_crop_from=(None, "max", None)``. """ import numpy as np import vtk @@ -635,6 +673,7 @@ def bbox_relative_oriented_contact_plane( center_fraction, size_fraction, normal, + size_bounds_axes=None, width_axis=(-1.0, 0.0, 0.0), shape="anatomy", ): @@ -674,10 +713,20 @@ def bbox_relative_oriented_contact_plane( lo, hi = _axis_bounds(model_bounds, axis) center.append(lo + float(center_fraction[axis]) * (hi - lo)) - size = ( - _axis_extent_from_vector(model_bounds, u_axis) * float(size_fraction[0]), - _axis_extent_from_vector(model_bounds, v_axis) * float(size_fraction[1]), - ) + if size_bounds_axes is None: + size = ( + _axis_extent_from_vector(model_bounds, u_axis) * float(size_fraction[0]), + _axis_extent_from_vector(model_bounds, v_axis) * float(size_fraction[1]), + ) + else: + size = _oriented_bbox_face_size( + model_bounds, + center=center, + u_axis=u_axis, + v_axis=v_axis, + varying_axes=size_bounds_axes, + size_fraction=size_fraction, + ) if size[0] <= 0.0 or size[1] <= 0.0: raise ValueError("size_fraction values must produce positive plane dimensions.") @@ -692,6 +741,53 @@ def bbox_relative_oriented_contact_plane( } +def _oriented_bbox_face_size( + model_bounds, + *, + center, + u_axis, + v_axis, + varying_axes, + size_fraction, +): + """Project a bbox face into an oblique plane's local coordinates.""" + import itertools + import numpy as np + + axis_tokens = { + "x": 0, + "y": 1, + "z": 2, + 0: 0, + 1: 1, + 2: 2, + } + axes = [] + for axis in varying_axes: + key = str(axis).strip().lower() if isinstance(axis, str) else axis + if key not in axis_tokens: + raise ValueError("size_bounds_axes values must be x, y, z, 0, 1, or 2.") + axes.append(axis_tokens[key]) + if not axes: + raise ValueError("size_bounds_axes must contain at least one axis.") + + center_arr = np.asarray(center, dtype=np.float64) + corners = [] + for choices in itertools.product((0, 1), repeat=len(axes)): + point = center_arr.copy() + for axis, choice in zip(axes, choices): + lo, hi = _axis_bounds(model_bounds, axis) + point[axis] = hi if choice else lo + corners.append(point) + relative = np.vstack(corners) - center_arr + u_values = relative @ np.asarray(u_axis, dtype=np.float64) + v_values = relative @ np.asarray(v_axis, dtype=np.float64) + return ( + float(np.ptp(u_values)) * float(size_fraction[0]), + float(np.ptp(v_values)) * float(size_fraction[1]), + ) + + def _inside_plane_shape(shape, u_values, v_values, half_u, half_v, *, tolerance): import numpy as np @@ -1406,11 +1502,14 @@ def expand_xz_footprint(bounds, *, x_extension_mm=0.0, z_extension_mm=0.0): import time from datetime import date from collections import OrderedDict +import numpy as np import vtk import vtkbone from ogo.fea.boundary import ( + fit_vtk_images_to_physical_bounds, generate_projected_material_disk_vtk, + projected_material_disk_required_bounds, should_smooth_resampled_mask, smooth_binary_mask_vtk, ) @@ -1671,8 +1770,23 @@ def sidewaysFallFe(args): ogo.message("Sample femur axis lengths: %s" % str(reference_scale["sample_axis_lengths"])) ogo.message("Femur reference scale factors: %s" % str(reference_scale["scale_factors"])) - icp = ogo.iterativeClosestPoint(ref_poly, mask_surface) - # ogo.message("Transform:\n %s" % str(icp)) + icp_transform = estimate_rigid_icp( + moving_points=polydata_points(ref_poly), + fixed_points=sample_surface_points, + iterations=50, + tolerance=1.0e-4, + start_by_matching_centroids_only=True, + convergence="delta", + distance_mode="mean", + ) + icp = point_transform_to_vtk_matrix( + icp_transform["rotation"], + icp_transform["translation"], + ) + ogo.message( + "ICP reference-to-sample iterations=%d mean_distance=%0.4f" + % (icp_transform["iterations"], icp_transform["mean_distance"]) + ) ogo.message("Applying the transformation and isotropic resampling to the image and mask...") output_origin, output_size = reference_grid_from_vtk_mask( @@ -1933,7 +2047,7 @@ def sidewaysFallFe(args): center_fraction=FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, size_fraction=SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, projection_axis="y", - shape="rectangle", + shape=SIDEWAYS_FALL_FIXTURE_SHAPE, ) femoral_head_cap_direction = bbox_relative_fixture_direction( FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, @@ -1950,21 +2064,6 @@ def sidewaysFallFe(args): ) ) ogo.message("Femoral Head PMMA cap direction: %s" % femoral_head_cap_direction) - femoralHeadPMMA = generate_projected_material_disk_vtk( - change, - surface_vtk_image=femur_mask_on_model_grid, - exclusion_vtk_image=femur_mask_on_model_grid, - center=femoral_head_plane["center"], - normal=femoral_head_plane["normal"], - u_axis=femoral_head_plane["u_axis"], - v_axis=femoral_head_plane["v_axis"], - size=femoral_head_plane["size"], - shape=femoral_head_plane["shape"], - thickness=pmma_thick, - intrusion=pmma_intrusion, - anatomy_constrained=True, - output_value=pmma_mat_id, - ) ## # Creates the greater trochanter pmma cap @@ -1973,7 +2072,7 @@ def sidewaysFallFe(args): center_fraction=GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, size_fraction=SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, projection_axis="y", - shape="rectangle", + shape=SIDEWAYS_FALL_FIXTURE_SHAPE, ) greater_trochanter_cap_direction = bbox_relative_fixture_direction( GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, @@ -1990,6 +2089,65 @@ def sidewaysFallFe(args): ) ) ogo.message("Greater Trochanter PMMA cap direction: %s" % greater_trochanter_cap_direction) + + required_bounds = [ + projected_material_disk_required_bounds( + femur_mask_on_model_grid, + center=plane["center"], + normal=plane["normal"], + u_axis=plane["u_axis"], + v_axis=plane["v_axis"], + size=plane["size"], + shape=plane["shape"], + thickness=pmma_thick, + intrusion=pmma_intrusion, + ) + for plane in (femoral_head_plane, greater_trochanter_plane) + ] + required_bounds = [bounds for bounds in required_bounds if bounds is not None] + if required_bounds: + bounds_array = np.asarray([model_bounds, *required_bounds], dtype=float) + desired_bounds = ( + float(bounds_array[:, 0].min()), + float(bounds_array[:, 1].max()), + float(bounds_array[:, 2].min()), + float(bounds_array[:, 3].max()), + float(bounds_array[:, 4].min()), + float(bounds_array[:, 5].max()), + ) + images_to_fit = [change, femur_mask_on_model_grid] + if distal_crop_face_change is not None: + images_to_fit.append(distal_crop_face_change) + fitted_images, contact_fit = fit_vtk_images_to_physical_bounds( + images_to_fit, + desired_bounds=desired_bounds, + constants=[0] * len(images_to_fit), + ) + change = fitted_images[0] + femur_mask_on_model_grid = fitted_images[1] + if distal_crop_face_change is not None: + distal_crop_face_change = fitted_images[2] + model_bounds = foreground_voxel_center_bounds(femur_mask_on_model_grid) + ogo.message( + "Fit model canvas to projected PMMA contact bounds: output_extent=%s" + % (contact_fit["output_extent"],) + ) + + femoralHeadPMMA = generate_projected_material_disk_vtk( + change, + surface_vtk_image=femur_mask_on_model_grid, + exclusion_vtk_image=femur_mask_on_model_grid, + center=femoral_head_plane["center"], + normal=femoral_head_plane["normal"], + u_axis=femoral_head_plane["u_axis"], + v_axis=femoral_head_plane["v_axis"], + size=femoral_head_plane["size"], + shape=femoral_head_plane["shape"], + thickness=pmma_thick, + intrusion=pmma_intrusion, + anatomy_constrained=True, + output_value=pmma_mat_id, + ) greaterTrochanterPMMA = generate_projected_material_disk_vtk( change, surface_vtk_image=femur_mask_on_model_grid, @@ -2099,26 +2257,22 @@ def sidewaysFallFe(args): model2.AddNodeSet(gt_pmma_visible_node_IDS) ## - # Distal Femur (df) support nodes. In bbox-ratio mode the distal shaft cut - # was made before alignment, so its transformed crop-face label carries the - # correct oblique support angle in model coordinates. + # Distal Femur (df) support nodes. The bbox-ratio crop standardizes shaft + # length before registration; the support itself remains a bbox-relative + # recipe plane in the generated model frame. ogo.message("Determining distal femur nodes...") if femur_cut_mode == "bbox_ratio": if distal_crop_face_change is None: ogo.message("BBox-ratio cut mode requires a transformed distal crop-face mask.") sys.exit(1) - try: - distal_crop_face_plane = crop_face_contact_plane(distal_crop_face_change, change) - distal_plane = bbox_relative_oriented_contact_plane( - model_bounds, - center_fraction=DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, - size_fraction=DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, - normal=distal_crop_face_plane["normal"], - shape="anatomy", - ) - except ValueError as exc: - ogo.message(str(exc)) - sys.exit(1) + distal_plane = bbox_relative_oriented_contact_plane( + model_bounds, + center_fraction=DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, + size_fraction=DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, + normal=DISTAL_SHAFT_FIXTURE_NORMAL, + size_bounds_axes=("x", "y"), + shape="anatomy", + ) ogo.message( "Distal Femur bbox-relative oblique support plane: center=%s normal=%s outward=%s size=%s" % ( diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 3b014e4..74116b7 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -40,6 +40,7 @@ DEFAULT_SPINE_TARGET_DISPLACEMENT_PERCENT = 0.68 DEFAULT_SPINE_LABEL_SMOOTHING_SIGMA_MM = 1.0 DEFAULT_SPINE_LABEL_SMOOTHING_THRESHOLD = 0.5 +SPINE_PREPROCESSING_CROP_MARGIN_MM = 8.0 DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM = 2.0 DEFAULT_SPINE_TOP_NODE_SET_ID = 4 DEFAULT_SPINE_BOTTOM_NODE_SET_ID = 3 @@ -221,6 +222,7 @@ def find_spinefe_benchmark_dir(start_dir=None, env_var="SPINEFE_BENCHMARK_DIR"): generate_projected_material_disk_vtk, pad_vtk_images_to_physical_bounds, projected_material_disk_required_bounds, + resample_vtk_image_to_spacing, should_smooth_resampled_mask, smooth_binary_mask_vtk, smooth_label_mask_vtk, @@ -520,15 +522,36 @@ def merge_vtk_images(image_list, label_list=None, overwrite_existing=True): ## functions related to ICP registration ###################################################################### REGISTRATION -def crop_and_transform(fullvertebra, body_output, process_output, image): +def _expand_bounding_box_by_margin(bb, image_data, margin_mm): + spacing = image_data.GetSpacing() + extent = image_data.GetExtent() + margins = [ + int(np.ceil(float(margin_mm) / max(float(spacing[0]), 1.0e-12))), + int(np.ceil(float(margin_mm) / max(float(spacing[1]), 1.0e-12))), + int(np.ceil(float(margin_mm) / max(float(spacing[2]), 1.0e-12))), + ] + return [ + max(int(extent[0]), int(bb[0]) - margins[0]), + min(int(extent[1]), int(bb[1]) + margins[0]), + max(int(extent[2]), int(bb[2]) - margins[1]), + min(int(extent[3]), int(bb[3]) + margins[1]), + max(int(extent[4]), int(bb[4]) - margins[2]), + min(int(extent[5]), int(bb[5]) + margins[2]), + ] + + +def crop_and_transform(fullvertebra, body_output, process_output, image, label_output=None, *, margin_mm=0.0): _, bb = crop_to_bounding_box(fullvertebra.GetOutput()) + if float(margin_mm) > 0.0: + bb = _expand_bounding_box_by_margin(bb, fullvertebra.GetOutput(), margin_mm) isolated_vertebra, _ = crop_to_bounding_box(body_output, bb) isolated_process, _ = crop_to_bounding_box(process_output, bb) isolated_image, _ = crop_to_bounding_box(image, bb) - - - return isolated_vertebra, isolated_process, isolated_image + if label_output is None: + return isolated_vertebra, isolated_process, isolated_image + isolated_label, _ = crop_to_bounding_box(label_output, bb) + return isolated_vertebra, isolated_process, isolated_image, isolated_label def perform_marching_cubes(body_output): mcubes = vtk.vtkImageMarchingCubes() @@ -1061,18 +1084,6 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, image_reader = read(input_image) ogo.message(f"Reading mask...: {input_mask}") mask_reader = resample_to_match(image_reader.GetOutput(), read(input_mask).GetOutput()) - if label_smoothing_sigma_mm > 0: - ogo.message( - "smoothing input label mask " - f"(sigma={label_smoothing_sigma_mm} mm, threshold={label_smoothing_threshold})..." - ) - mask_reader = _NiftiImageHandle( - smooth_label_mask_vtk( - mask_reader.GetOutput(), - sigma_mm=label_smoothing_sigma_mm, - threshold=label_smoothing_threshold, - ) - ) input_spacing = image_reader.GetOutput().GetSpacing() # Get and Check labels @@ -1088,12 +1099,49 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, # Crop everything to same BB ogo.message(f"Cropping to common bounding box...") - isolated_vertebra, isolated_process, isolated_image = crop_and_transform(fullvertebra, body.GetOutput(), process.GetOutput(), image_reader.GetOutput()) + ( + isolated_vertebra, + isolated_process, + isolated_image, + isolated_labels, + ) = crop_and_transform( + fullvertebra, + body.GetOutput(), + process.GetOutput(), + image_reader.GetOutput(), + mask_reader.GetOutput(), + margin_mm=SPINE_PREPROCESSING_CROP_MARGIN_MM, + ) + + ogo.message("Resampling cropped spine inputs to isotropic spacing before ICP...") + preprocessed_image = resample_vtk_image_to_spacing( + isolated_image.GetOutput(), + iso_resolution, + interpolation="bspline", + ) + preprocessed_labels = resample_vtk_image_to_spacing( + isolated_labels.GetOutput(), + iso_resolution, + interpolation="nearest", + ) + if label_smoothing_sigma_mm > 0: + ogo.message( + "smoothing isotropic label mask " + f"(sigma={label_smoothing_sigma_mm} mm, threshold={label_smoothing_threshold})..." + ) + preprocessed_labels = smooth_label_mask_vtk( + preprocessed_labels, + sigma_mm=label_smoothing_sigma_mm, + threshold=label_smoothing_threshold, + ) + registration_body = threshold(preprocessed_labels, body_label) + isolated_vertebra = registration_body + isolated_process = threshold(preprocessed_labels, process_label) # Marching Cubes and Registration ogo.message(f"Starting ICP registration to reference...: {reference_path} ") icp = get_icp_with_scaling( - body, + registration_body, reference_path, scale_factors=registration_scale, min_scale=registration_min_scale, @@ -1143,7 +1191,7 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, interpolation="nearest", ) transformed_image = resample_vtk_image_with_point_transform( - isolated_image.GetOutput(), + preprocessed_image, rotation=sample_to_reference_rotation, translation=sample_to_reference_translation, output_origin=output_origin, diff --git a/ogo/util/faim.py b/ogo/util/faim.py index 817aefd..50a6067 100644 --- a/ogo/util/faim.py +++ b/ogo/util/faim.py @@ -247,6 +247,17 @@ def _node_set_centroid(coords, node_sets, set_name): return [total / len(node_numbers) for total in totals] +def _coordinate_axis_span(coords, axis_index): + """Return the full model coordinate span along one physical axis.""" + values = [float(coord[axis_index]) for coord in coords] + if not values: + raise ValueError("Cannot infer characteristic length from a model with no nodes.") + length = max(values) - min(values) + if length <= 0: + raise ValueError("Could not infer a positive model coordinate span.") + return length + + def infer_profile_characteristic_length_mm(model_file, report_profile, failure_axis): """Infer the physical length used to convert percent strain endpoints to mm.""" profile = str(report_profile or "generic").strip().lower() @@ -261,14 +272,7 @@ def infer_profile_characteristic_length_mm(model_file, report_profile, failure_a coords, node_sets, ["body_bottom", "bottom_fixed_z"] ) elif profile == "femur": - from ogo.fea.femur import FEMORAL_HEAD_NODE_SET, GREATER_TROCHANTER_NODE_SET - - first = _first_available_node_set_centroid( - coords, node_sets, [FEMORAL_HEAD_NODE_SET, "top_displacement", "convergence_set"] - ) - second = _first_available_node_set_centroid( - coords, node_sets, [GREATER_TROCHANTER_NODE_SET, "bottom_fixed_y_PMMA"] - ) + return _coordinate_axis_span(coords, axis_index) else: return "" diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index 4f330cf..d784ee5 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -4,39 +4,45 @@ "hip-sideways-fall": { "bbox_crop_from": [ null, - "min", + "max", null ], "bbox_ratio": [ 1.0, - 1.2, + 1.3, null ], "cortical_label": 1, "cut_mode": "bbox_ratio", "distal_shaft_center_fraction": [ - 0.1958514895, - 0.5708725889, - 0.0601674635 + 0.5, + 0.5, + -0.1 ], "distal_shaft_size_fraction": [ - 0.3640088821, - 0.6351022953 + 1.0, + 1.0 + ], + "distal_shaft_normal": [ + 0.0, + -0.2588190451025207, + 0.9659258262890683 ], "fe_displacement_mm": -4.0, "femoral_head_center_fraction": [ 0.5, - 1.0278767152, - 0.9650933279 + 1.1, + 0.5 ], + "fixture_shape": "anatomy", "fixture_size_fraction": [ - 1.1, - 1.1 + 1.0, + 1.0 ], "greater_trochanter_center_fraction": [ 0.5, - 0.0151983839, - 0.6551527108 + -0.1, + 0.5 ], "input_margin_mm": 16.0, "iso_resolution_mm": 1.0, diff --git a/tests/fea/test_boundary.py b/tests/fea/test_boundary.py index b069d3a..0534c06 100644 --- a/tests/fea/test_boundary.py +++ b/tests/fea/test_boundary.py @@ -10,6 +10,7 @@ foreground_voxel_center_bounds_from_mask, generate_bone_cap_mask, generate_projected_material_disk_mask, + fit_vtk_images_to_physical_bounds, pad_vtk_images_to_physical_bounds, projected_material_disk_required_bounds, smooth_label_mask_vtk, @@ -47,6 +48,45 @@ def test_bbox_relative_contact_plane_uses_authored_axis_convention(): assert high_y_plane["v_axis"] == pytest.approx((-1, 0, 0)) +def test_bbox_relative_y_planes_keep_editor_axis_roundoff(): + bounds = ( + -76.45140061071066, + -31.451400610710664, + -106.34001525211478, + -32.34001525211478, + -0.39488812795613626, + 83.60511187204386, + ) + + low_y_plane = bbox_relative_contact_plane( + bounds, + center_fraction=(0.5, -0.1, 0.5), + size_fraction=(1.0, 1.0), + projection_axis="y", + shape="anatomy", + ) + high_y_plane = bbox_relative_contact_plane( + bounds, + center_fraction=(0.5, 1.1, 0.5), + size_fraction=(1.0, 1.0), + projection_axis="y", + shape="anatomy", + ) + + assert low_y_plane["u_axis"] == pytest.approx( + (1.208164965010145e-16, 6.123233995736765e-17, -1.0) + ) + assert low_y_plane["v_axis"] == pytest.approx( + (-1.0, 6.123233995736767e-17, -1.208164965010145e-16) + ) + assert high_y_plane["u_axis"] == pytest.approx( + (9.331099404642518e-17, 6.123233995736766e-17, 1.0) + ) + assert high_y_plane["v_axis"] == pytest.approx( + (-1.0, -6.123233995736766e-17, 9.331099404642518e-17) + ) + + def test_bbox_relative_contact_bounds_can_enforce_square_footprint(): bounds = (10, 50, -20, 80, 100, 200) @@ -114,6 +154,36 @@ def test_pad_vtk_images_to_physical_bounds_preserves_world_coordinates(): assert data[0, 4, 0] == 3 +def test_fit_vtk_images_to_physical_bounds_crops_and_pads_in_world_coordinates(): + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + values = np.zeros((6, 5, 4), dtype=np.uint8) + values[2, 1, 1] = 7 + image = vtk.vtkImageData() + image.SetDimensions(values.shape) + image.SetOrigin(10.0, 20.0, 30.0) + image.SetSpacing(2.0, 1.0, 5.0) + image.GetPointData().SetScalars(numpy_to_vtk(values.ravel(order="F"), deep=True)) + + fitted, meta = fit_vtk_images_to_physical_bounds( + [image], + desired_bounds=(8.0, 18.0, 21.0, 23.0, 35.0, 45.0), + constants=[3], + ) + + out = fitted[0] + data = vtk_image_to_numpy(out) + + assert out.GetDimensions() == (6, 3, 3) + assert out.GetOrigin() == pytest.approx((8.0, 21.0, 35.0)) + assert meta["output_extent"] == (-1, 4, 1, 3, 1, 3) + assert data[3, 0, 0] == 7 + assert data[0, 0, 0] == 3 + + def test_projected_material_disk_required_bounds_includes_lateral_plane_extent(): vtk = pytest.importorskip("vtk") from vtk.util.numpy_support import numpy_to_vtk diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 39cf52b..989a0a3 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -245,6 +245,45 @@ def test_scale_reference_to_sample_principal_lengths_clips_and_scales_origin(): assert scaled.GetBounds() == pytest.approx((-0.8, 0.8, -2.4, 2.4, -3.9, 3.9)) +def test_scale_reference_point_cloud_to_sample_preserves_reference_center(): + np = pytest.importorskip("numpy") + + reference = _polydata_from_points( + [ + (9.0, 20.0, 30.0), + (11.0, 20.0, 30.0), + (10.0, 18.0, 30.0), + (10.0, 22.0, 30.0), + (10.0, 20.0, 27.0), + (10.0, 20.0, 33.0), + ] + ) + sample_points = np.asarray( + [ + (-1.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + (0.0, -4.0, 0.0), + (0.0, 4.0, 0.0), + (0.0, 0.0, -6.0), + (0.0, 0.0, 6.0), + ] + ) + + scaled, metadata = femur.scale_reference_point_cloud_to_sample( + reference, + sample_points, + min_scale=(0.8, 0.8, 0.75), + max_scale=(1.2, 1.2, 1.3), + ) + + points = np.asarray( + [scaled.GetPoint(point_id) for point_id in range(scaled.GetNumberOfPoints())] + ) + assert points.mean(axis=0) == pytest.approx((10.0, 20.0, 30.0)) + assert metadata["reference_center"] == pytest.approx([10.0, 20.0, 30.0]) + assert metadata["scale_factors"] == pytest.approx([1.0, 1.2, 1.3]) + + def test_surface_points_from_vtk_mask_uses_voxel_surface_and_stride_sampling(): np = pytest.importorskip("numpy") @@ -311,7 +350,7 @@ def test_bbox_ratio_crop_matches_recipe_order_and_tracks_cut_face(): cropped, crop_face, meta = femur.crop_vtk_images_to_bbox_ratio( [_vtk_image_from_array(density)], _vtk_image_from_array(mask), - bbox_ratio=(1.0, 1.2, None), + bbox_ratio=femur.DEFAULT_FEMUR_BBOX_RATIO, bbox_crop_from=femur.DEFAULT_FEMUR_BBOX_CROP_FROM, labels={2}, ) @@ -319,11 +358,11 @@ def test_bbox_ratio_crop_matches_recipe_order_and_tracks_cut_face(): cropped_density = vtk_image_to_numpy(cropped[0]) crop_face_data = vtk_image_to_numpy(crop_face) - assert cropped_density.shape == (20, 10, 12) - assert cropped[0].GetOrigin() == pytest.approx((2, 4, 6)) - assert meta["ratio_xyz"] == (None, 1.0, 1.2) + assert cropped_density.shape == (20, 10, 13) + assert cropped[0].GetOrigin() == pytest.approx((2, 4, 5)) + assert meta["ratio_xyz"] == (None, 1.0, 1.3) assert meta["crop_from_xyz"] == (None, None, "min") - assert meta["crop_slices_xyz"] == ((2, 22), (4, 14), (6, 18)) + assert meta["crop_slices_xyz"] == ((2, 22), (4, 14), (5, 18)) assert crop_face_data.sum() == 20 * 10 assert np.all(crop_face_data[:, :, 0] == 1) assert not np.any(crop_face_data[:, :, 1:]) diff --git a/tests/fea/test_sideways_fall.py b/tests/fea/test_sideways_fall.py index 106608d..8d6c729 100644 --- a/tests/fea/test_sideways_fall.py +++ b/tests/fea/test_sideways_fall.py @@ -44,3 +44,79 @@ def test_sideways_fall_icp_path_uses_voxel_surface_points(): assert "surface_points_from_vtk_mask" in called_names assert "scale_reference_point_cloud_to_sample" in called_names assert marching_cube_calls == [] + + +def test_sideways_fall_icp_path_uses_deterministic_point_cloud_transform(): + pytest.importorskip("vtk") + pytest.importorskip("vtkbone") + + from ogo.fea import femur + + source = textwrap.dedent(inspect.getsource(femur.sidewaysFallFe)) + tree = ast.parse(source) + called_names = { + node.func.id + for node in ast.walk(tree) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) + } + helper_calls = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.Attribute) and node.attr == "iterativeClosestPoint" + ] + + assert "estimate_rigid_icp" in called_names + assert helper_calls == [] + + +def test_sideways_fall_distal_support_is_bbox_relative_recipe_plane(): + pytest.importorskip("vtk") + pytest.importorskip("vtkbone") + + from ogo.fea import femur + + bounds = ( + -76.45140061071066, + -31.451400610710657, + -102.34001525211478, + -36.34001525211478, + -0.39488812795613626, + 83.60511187204386, + ) + plane = femur.bbox_relative_oriented_contact_plane( + bounds, + center_fraction=femur.DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, + size_fraction=femur.DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, + normal=femur.DISTAL_SHAFT_FIXTURE_NORMAL, + size_bounds_axes=("x", "y"), + shape="anatomy", + ) + + assert femur.DISTAL_SHAFT_FIXTURE_CENTER_FRACTION == (0.5, 0.5, -0.1) + assert femur.DISTAL_SHAFT_FIXTURE_SIZE_FRACTION == (1.0, 1.0) + assert plane["center"] == pytest.approx( + (-53.95140061071066, -69.34001525211478, -8.794888127956137) + ) + assert plane["normal"] == pytest.approx( + femur.DISTAL_SHAFT_FIXTURE_NORMAL, + abs=1.0e-12, + ) + assert plane["size"] == pytest.approx((63.750, 45.0), abs=0.2) + + +def test_sideways_fall_fits_contact_canvas_before_generating_pmma(): + pytest.importorskip("vtk") + pytest.importorskip("vtkbone") + + from ogo.fea import femur + + source = textwrap.dedent(inspect.getsource(femur.sidewaysFallFe)) + tree = ast.parse(source) + called_names = { + node.func.id + for node in ast.walk(tree) + if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) + } + + assert "projected_material_disk_required_bounds" in called_names + assert "fit_vtk_images_to_physical_bounds" in called_names diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py index f76a026..8eefacb 100644 --- a/tests/fea/test_spinefe_benchmark.py +++ b/tests/fea/test_spinefe_benchmark.py @@ -136,6 +136,25 @@ def test_spine_registration_scaling_uses_voxel_surface_points(): assert "perform_marching_cubes(body)" not in source +def test_spine_registration_uses_preprocessed_isotropic_body(): + pytest.importorskip("vtk") + + from ogo.fea import spine + + source = textwrap.dedent(inspect.getsource(spine.process_vertebra)) + + assert "SPINE_PREPROCESSING_CROP_MARGIN_MM" in source + assert "resample_vtk_image_to_spacing" in source + assert "registration_body = threshold(" in source + assert "get_icp_with_scaling(\n registration_body," in source + assert source.index("resample_vtk_image_to_spacing") < source.index( + "registration_body = threshold(" + ) + assert source.index("registration_body = threshold(") < source.index( + "get_icp_with_scaling(" + ) + + def test_spine_workflow_resamples_to_explicit_reference_grid(): pytest.importorskip("vtk") diff --git a/tests/fea/test_workflow_locked_defaults.py b/tests/fea/test_workflow_locked_defaults.py index 9370281..a1cedea 100644 --- a/tests/fea/test_workflow_locked_defaults.py +++ b/tests/fea/test_workflow_locked_defaults.py @@ -50,6 +50,7 @@ def _locked_workflow_defaults(): "fe_displacement_mm": femur.DEFAULT_FEMUR_FE_DISPLACEMENT, "target_displacement_percent": femur.DEFAULT_FEMUR_TARGET_DISPLACEMENT_PERCENT, "mask_smoothing_spacing_threshold_mm": femur.DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + "fixture_shape": femur.SIDEWAYS_FALL_FIXTURE_SHAPE, "fixture_size_fraction": list(femur.SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION), "femoral_head_center_fraction": list( femur.FEMORAL_HEAD_FIXTURE_CENTER_FRACTION @@ -63,6 +64,7 @@ def _locked_workflow_defaults(): "distal_shaft_size_fraction": list( femur.DISTAL_SHAFT_FIXTURE_SIZE_FRACTION ), + "distal_shaft_normal": list(femur.DISTAL_SHAFT_FIXTURE_NORMAL), "bbox_ratio": list(femur.DEFAULT_FEMUR_BBOX_RATIO), "bbox_crop_from": list(femur.DEFAULT_FEMUR_BBOX_CROP_FROM), "reference_min_scale": list(femur.DEFAULT_FEMUR_REFERENCE_MIN_SCALE), diff --git a/tests/util/test_faim.py b/tests/util/test_faim.py index 9984d96..734d033 100644 --- a/tests/util/test_faim.py +++ b/tests/util/test_faim.py @@ -441,6 +441,32 @@ def test_set_prescribed_displacement_from_percent_updates_constraints(tmp_path): assert faim.read_prescribed_displacement(model_file, "spine") == -0.68 +def test_femur_characteristic_length_uses_loading_axis_model_span(tmp_path): + model_file = tmp_path / "model.n88model" + write_minimal_n88_model_with_constraints( + model_file, + coords=[ + [0.0, 0.0, 0.0], + [0.0, 84.0, 0.0], + [0.0, 4.0, 0.0], + [0.0, 80.0, 0.0], + ], + constraints={ + "top_displacement": [4], + "bottom_fixed_y_PMMA": [3], + "convergence_set": [4], + }, + ) + + length = faim.infer_profile_characteristic_length_mm( + model_file, + report_profile="femur", + failure_axis="y", + ) + + assert length == 84.0 + + def test_set_prescribed_displacement_from_percent_updates_femur_constraints(tmp_path): model_file = tmp_path / "model.n88model" write_minimal_n88_model_with_constraints( From 913297a5b529940a066114fbae99158f9d54e11c Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:08:19 -0600 Subject: [PATCH 21/67] Centralize FE solve profile settings --- ogo/cli/GenerateFEM.py | 56 +++++++++---------- ogo/fea/femur.py | 17 ++++++ ogo/fea/material_laws.py | 2 +- ogo/fea/spine.py | 34 +++++++++++ tests/cli/test_GenerateFEM.py | 40 +++++++++++++ .../fixtures/fe_workflow_defaults_locked.json | 32 ++++++++++- tests/fea/test_spine.py | 2 + tests/fea/test_spinefe_benchmark.py | 47 ++++++++++++++++ tests/fea/test_workflow_locked_defaults.py | 8 +++ 9 files changed, 205 insertions(+), 33 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index be3150b..8882cd3 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -16,7 +16,6 @@ from ogo.fea.spine import ( BENCHMARK_LINEAR_FE_DISPLACEMENT_MM, BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM, - DEFAULT_SPINE_FE_DISPLACEMENT_MM, DEFAULT_SPINE_ISO_RESOLUTION_MM, DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, DEFAULT_SPINE_PMMA_INTRUSION_MM, @@ -24,18 +23,16 @@ DEFAULT_SPINE_REGISTRATION_MAX_SCALE, DEFAULT_SPINE_REGISTRATION_MIN_SCALE, DEFAULT_SPINE_REGISTRATION_BACKEND, - DEFAULT_SPINE_TARGET_DISPLACEMENT_PERCENT, SPINE_ALIGNMENT_METHOD, default_spine_reference_path, + solve_report_profile as spine_solve_report_profile, ) from ogo.fea.femur import ( DEFAULT_FEMUR_BBOX_CROP_FROM, DEFAULT_FEMUR_BBOX_RATIO, DEFAULT_FEMUR_CUT_MODE, - DEFAULT_FEMUR_FE_DISPLACEMENT, DEFAULT_FEMUR_ISO_RESOLUTION_MM, DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - DEFAULT_FEMUR_TARGET_DISPLACEMENT_PERCENT, DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, DEFAULT_PMMA_INTRUSION_MM, DEFAULT_PMMA_THICKNESS_MM, @@ -44,6 +41,7 @@ FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, + solve_report_profile as femur_solve_report_profile, ) @@ -239,9 +237,19 @@ def target_displacement_percent(args: argparse.Namespace, model_type: str) -> fl """Return the maintained endpoint displacement percentage for this model.""" if args.target_displacement is not None: return args.target_displacement + return solve_report_profile(args, model_type)["target_displacement_percent"] + + +def solve_report_profile(args: argparse.Namespace, model_type: str) -> dict: + """Return site-specific solve/report settings with CLI overrides applied.""" if model_type == "spine": - return DEFAULT_SPINE_TARGET_DISPLACEMENT_PERCENT - return DEFAULT_FEMUR_TARGET_DISPLACEMENT_PERCENT + profile = spine_solve_report_profile(preset=getattr(args, "preset", None)) + else: + profile = femur_solve_report_profile() + profile = profile.copy() + if args.target_displacement is not None: + profile["target_displacement_percent"] = args.target_displacement + return profile def percent_displacement_metadata( @@ -296,35 +304,20 @@ def solve_model( spec.loader.exec_module(module) run_faim_pipeline = module.run_faim_pipeline - # Spine compression uses z reaction force; sideways-fall hip uses y. - if model_type == "spine": - analysis_var = "fz_ns1" - pistoia_vars = [] - failure_axis = "z" - else: - analysis_var = "fy_ns1" - pistoia_vars = ["pis_fy_fail", "pis_stiffy"] - failure_axis = "y" - - default_applied_displacement = ( - str(DEFAULT_SPINE_FE_DISPLACEMENT_MM) - if model_type == "spine" - else str(DEFAULT_FEMUR_FE_DISPLACEMENT) - ) + profile = solve_report_profile(args, model_type) + default_applied_displacement = str(profile["default_applied_displacement"]) applied_displacement = option_value( generator_argv, "--fe_displacement", default_applied_displacement, ) - target_displacement = target_displacement_percent(args, model_type) - report_profile = "spine" if model_type == "spine" else "femur" run_faim_pipeline( model_file=model_path, output_prefix=model_path.with_suffix(""), - analysis_var=analysis_var, - pistoia_vars=pistoia_vars, - failure_axis=failure_axis, + analysis_var=profile["analysis_var"], + pistoia_vars=profile["pistoia_vars"], + failure_axis=profile["failure_axis"], threads=args.threads, conda_env=args.faim_env, conda_executable=args.conda_executable, @@ -343,9 +336,9 @@ def solve_model( exclude=args.exclude, run_pistoia=False, applied_displacement=applied_displacement, - target_displacement=target_displacement, - report_profile=report_profile, - solve_displacement_percent=target_displacement, + target_displacement=profile["target_displacement_percent"], + report_profile=profile["report_profile"], + solve_displacement_percent=profile["target_displacement_percent"], compress=not args.no_compress, require_pistoia=args.require_pistoia, dry_run=args.dry_run, @@ -904,8 +897,9 @@ def _add_common_image_args(parser: argparse.ArgumentParser) -> None: type=float, default=None, help=( - "Profile reporting endpoint as percent strain. Defaults to 2.0 for spine " - "and 4.0 for femur; converted to mm from the generated model geometry." + "Profile reporting endpoint as percent strain. Spine presets default to " + "0.68 for benchmark-linear and 4.0 for benchmark-nonlinear; hip defaults " + "to 4.0. Values are converted to mm from generated model geometry." ), ) parser.add_argument( diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 6d2f8f1..b2582af 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -98,6 +98,23 @@ ] +def target_displacement_percent(): + """Return the maintained hip sideways-fall reporting endpoint.""" + return DEFAULT_FEMUR_TARGET_DISPLACEMENT_PERCENT + + +def solve_report_profile(): + """Return femur-specific FAIM reporting settings for the shared solver path.""" + return { + "report_profile": "femur", + "analysis_var": "fy_ns1", + "pistoia_vars": ["pis_fy_fail", "pis_stiffy"], + "failure_axis": "y", + "default_applied_displacement": DEFAULT_FEMUR_FE_DISPLACEMENT, + "target_displacement_percent": target_displacement_percent(), + } + + def side_suffix(femur_side): """Return the compact output stem for a femur side.""" if femur_side == LEFT_FEMUR: diff --git a/ogo/fea/material_laws.py b/ogo/fea/material_laws.py index 615c7c6..351db1f 100644 --- a/ogo/fea/material_laws.py +++ b/ogo/fea/material_laws.py @@ -75,4 +75,4 @@ def linear(A, B): def power_law(A, b): # use as: --elastic_E_func power_A_b - return lambda density_mgcc: A * (density_mgcc / 1000.0) ** b \ No newline at end of file + return lambda density_mgcc: A * (density_mgcc / 1000.0) ** b diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 74116b7..be3782f 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -57,9 +57,32 @@ BENCHMARK_LINEAR_FE_DISPLACEMENT_MM = -0.2 BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM = -2.0 +BENCHMARK_LINEAR_TARGET_DISPLACEMENT_PERCENT = 0.68 +BENCHMARK_NONLINEAR_TARGET_DISPLACEMENT_PERCENT = 4.0 BENCHMARK_PMMA_MATERIAL_ID = 300 +def target_displacement_percent(preset=None): + """Return the maintained spine reporting endpoint for a preset.""" + if preset == "benchmark-nonlinear": + return BENCHMARK_NONLINEAR_TARGET_DISPLACEMENT_PERCENT + if preset == "benchmark-linear": + return BENCHMARK_LINEAR_TARGET_DISPLACEMENT_PERCENT + return DEFAULT_SPINE_TARGET_DISPLACEMENT_PERCENT + + +def solve_report_profile(preset=None): + """Return spine-specific FAIM reporting settings for the shared solver path.""" + return { + "report_profile": "spine", + "analysis_var": "fz_ns1", + "pistoia_vars": [], + "failure_axis": "z", + "default_applied_displacement": DEFAULT_SPINE_FE_DISPLACEMENT_MM, + "target_displacement_percent": target_displacement_percent(preset), + } + + def default_spine_reference_path(): """Return the bundled reference body used for spine ICP alignment.""" return Path(__file__).resolve().parents[1] / "dat" / DEFAULT_SPINE_REFERENCE_FILENAME @@ -77,6 +100,7 @@ def benchmark_linear_params(): "top_direction": (0, 0, 1), "bottom_direction": (0, 0, -1), "fe_displacement": BENCHMARK_LINEAR_FE_DISPLACEMENT_MM, + "target_displacement_percent": BENCHMARK_LINEAR_TARGET_DISPLACEMENT_PERCENT, "pmma_yield_compression": None, "pmma_yield_tension": None, "cort_poissons_ratio": None, @@ -95,6 +119,7 @@ def benchmark_nonlinear_params(): params.update( { "fe_displacement": BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM, + "target_displacement_percent": BENCHMARK_NONLINEAR_TARGET_DISPLACEMENT_PERCENT, "pmma_yield_compression": 70.0, "pmma_yield_tension": 70.0, "yield_comp_func": "kopperdahl_trab_yc", @@ -152,13 +177,22 @@ def prepare_benchmark_images(input_image_path, input_mask_path, n_bins=128): def build_benchmark_sample_model(input_image_path, input_mask_path, output_model_path, nonlinear=False): """Build the public spineFE-benchmark sample model and write it to disk.""" from ogo.fea.model import create_microfe_model, write_model + from ogo.util.faim import set_prescribed_displacement_from_percent image_with_disk, mask_with_disk, bin_centers = prepare_benchmark_images( input_image_path, input_mask_path ) params = benchmark_nonlinear_params() if nonlinear else benchmark_linear_params() + target_displacement_percent = params.pop("target_displacement_percent") model = create_microfe_model(image_with_disk, mask_with_disk, bin_centers, **params) write_model(model, output_model_path) + set_prescribed_displacement_from_percent( + output_model_path, + report_profile="spine", + failure_axis="z", + target_displacement_percent=target_displacement_percent, + displacement_sign=-1.0 if params["fe_displacement"] < 0 else 1.0, + ) return model diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index 6e1fb7a..bff12e5 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -332,6 +332,46 @@ def _solve_args(**overrides): return SimpleNamespace(**data) +def test_spine_target_displacement_defaults_follow_benchmark_preset(): + assert ( + GenerateFEM.target_displacement_percent( + _solve_args(preset="benchmark-linear"), "spine" + ) + == 0.68 + ) + assert ( + GenerateFEM.target_displacement_percent( + _solve_args(preset="benchmark-nonlinear"), "spine" + ) + == 4.0 + ) + assert ( + GenerateFEM.target_displacement_percent( + _solve_args(preset="benchmark-nonlinear", target_displacement=2.5), + "spine", + ) + == 2.5 + ) + + +def test_solve_model_sets_spine_displacement_from_preset_percent(monkeypatch, tmp_path): + import ogo.util.faim as faim_module + + calls = [] + monkeypatch.setattr(faim_module, "run_faim_pipeline", lambda **kwargs: calls.append(kwargs)) + + GenerateFEM.solve_model( + tmp_path / "density_L1.n88model", + _solve_args(preset="benchmark-nonlinear"), + "spine", + ["density.nii.gz", "mask.nii.gz", "--fe_displacement", "-2.0"], + ) + + assert calls[0]["report_profile"] == "spine" + assert calls[0]["target_displacement"] == 4.0 + assert calls[0]["solve_displacement_percent"] == 4.0 + + def test_solve_model_sets_femur_displacement_from_percent(monkeypatch, tmp_path): import ogo.util.faim as faim_module diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index d784ee5..fc2b512 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -80,17 +80,47 @@ ], "bottom_node_set_id": 5, "cort_elastic_E_func": "kopperdahl_trab_E", + "cort_yield_comp_func": null, + "cort_yield_tens_func": null, "elastic_E_func": "kopperdahl_trab_E", "fe_displacement": -0.2, "pmma_E": 2500, "pmma_mat_id": 300, "pmma_v": 0.3, + "target_displacement_percent": 0.68, "top_direction": [ 0, 0, 1 ], - "top_node_set_id": 6 + "top_node_set_id": 6, + "yield_comp_func": null, + "yield_tens_func": null + }, + "benchmark_nonlinear": { + "bottom_direction": [ + 0, + 0, + -1 + ], + "bottom_node_set_id": 5, + "cort_elastic_E_func": "kopperdahl_trab_E", + "cort_yield_comp_func": "kopperdahl_trab_yc", + "cort_yield_tens_func": "kopperdahl_trab_yc", + "elastic_E_func": "kopperdahl_trab_E", + "fe_displacement": -2.0, + "pmma_E": 2500, + "pmma_mat_id": 300, + "pmma_v": 0.3, + "target_displacement_percent": 4.0, + "top_direction": [ + 0, + 0, + 1 + ], + "top_node_set_id": 6, + "yield_comp_func": "kopperdahl_trab_yc", + "yield_tens_func": "kopperdahl_trab_yc" }, "bottom_node_set_id": 3, "contact_size_fraction": [ diff --git a/tests/fea/test_spine.py b/tests/fea/test_spine.py index 4fc0dbd..9a2366d 100644 --- a/tests/fea/test_spine.py +++ b/tests/fea/test_spine.py @@ -12,9 +12,11 @@ def test_benchmark_presets_match_spinefe_notebook_settings(): nonlinear = spine.benchmark_nonlinear_params() assert linear["fe_displacement"] == -0.2 + assert linear["target_displacement_percent"] == 0.68 assert linear["elastic_E_func"] == "kopperdahl_trab_E" assert linear["yield_comp_func"] is None assert linear["pmma_yield_compression"] is None assert nonlinear["fe_displacement"] == -2.0 + assert nonlinear["target_displacement_percent"] == 4.0 assert nonlinear["yield_comp_func"] == "kopperdahl_trab_yc" assert nonlinear["pmma_yield_compression"] == 70.0 diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py index 8eefacb..587f266 100644 --- a/tests/fea/test_spinefe_benchmark.py +++ b/tests/fea/test_spinefe_benchmark.py @@ -12,6 +12,53 @@ from ogo.fea.spine import build_benchmark_sample_model, find_spinefe_benchmark_dir +def test_benchmark_sample_model_updates_prescribed_displacement(monkeypatch, tmp_path): + from ogo.fea import spine + import ogo.fea.model as model_module + import ogo.util.faim as faim_module + + updates = [] + monkeypatch.setattr( + spine, + "prepare_benchmark_images", + lambda input_image_path, input_mask_path: ("image", "mask", [1.0]), + ) + monkeypatch.setattr( + model_module, + "create_microfe_model", + lambda image, mask, bin_centers, **params: {"params": params}, + ) + monkeypatch.setattr( + model_module, + "write_model", + lambda model, output_model_path: None, + ) + monkeypatch.setattr( + faim_module, + "set_prescribed_displacement_from_percent", + lambda *args, **kwargs: updates.append((args, kwargs)), + ) + + build_benchmark_sample_model( + "image.nii.gz", + "mask.nii.gz", + tmp_path / "linear.n88model", + ) + build_benchmark_sample_model( + "image.nii.gz", + "mask.nii.gz", + tmp_path / "nonlinear.n88model", + nonlinear=True, + ) + + assert [call[1]["target_displacement_percent"] for call in updates] == [ + 0.68, + 4.0, + ] + assert [call[1]["displacement_sign"] for call in updates] == [-1.0, -1.0] + assert [call[1]["failure_axis"] for call in updates] == ["z", "z"] + + def test_filter_node_set_to_dominant_coordinate_plane_removes_small_rim(): vtk = pytest.importorskip("vtk") diff --git a/tests/fea/test_workflow_locked_defaults.py b/tests/fea/test_workflow_locked_defaults.py index a1cedea..37f6fa5 100644 --- a/tests/fea/test_workflow_locked_defaults.py +++ b/tests/fea/test_workflow_locked_defaults.py @@ -42,6 +42,9 @@ def _locked_workflow_defaults(): "benchmark_linear": _selected_spine_benchmark( spine.benchmark_linear_params() ), + "benchmark_nonlinear": _selected_spine_benchmark( + spine.benchmark_nonlinear_params() + ), }, "hip-sideways-fall": { "left_femur_code": femur.LEFT_FEMUR, @@ -93,8 +96,13 @@ def _selected_spine_benchmark(params): "top_direction", "bottom_direction", "fe_displacement", + "target_displacement_percent", "elastic_E_func", + "yield_comp_func", + "yield_tens_func", "cort_elastic_E_func", + "cort_yield_comp_func", + "cort_yield_tens_func", ) return { key: list(params[key]) if isinstance(params[key], tuple) else params[key] From 11a1c7e55c8a67eb1efe20cf28da6181f6d5b5ed Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:53:44 -0600 Subject: [PATCH 22/67] Report Pistoia metrics for FE profiles --- ogo/util/faim.py | 15 +++++++++++++++ tests/util/test_faim.py | 27 +++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/ogo/util/faim.py b/ogo/util/faim.py index 50a6067..bfb1127 100644 --- a/ogo/util/faim.py +++ b/ogo/util/faim.py @@ -424,6 +424,19 @@ def write_results_csv( "reaction_force_N": reaction_force, "stiffness_N_per_mm": computed_stiffness if computed_stiffness != "" else stiffness, } + pistoia_common = { + "pistoia_file": str(pistoia_file), + "pistoia_failure_var": failure_var, + "pistoia_stiffness_var": stiffness_var, + "pistoia_failure_load_N": pistoia_failure_load, + "pistoia_failure_load_kN": pistoia_failure_load_magnitude / 1000.0 + if pistoia_failure_load_magnitude != "" + else "", + "pistoia_stiffness_N_per_mm": stiffness, + "critical_volume_pct": pistoia_meta.get("critical_volume_pct", ""), + "critical_ees": pistoia_meta.get("critical_ees", ""), + "ees_at_crit_vol": pistoia_meta.get("ees_at_crit_vol", ""), + } if profile == "femur": row = { "model_file": common["model_file"], @@ -433,6 +446,7 @@ def write_results_csv( "reaction_force_N": common["reaction_force_N"], "stiffness_N_per_mm": common["stiffness_N_per_mm"], "characteristic_length_mm": characteristic_length, + **pistoia_common, } elif profile == "spine": row = { @@ -443,6 +457,7 @@ def write_results_csv( "reaction_force_N": common["reaction_force_N"], "stiffness_N_per_mm": common["stiffness_N_per_mm"], "characteristic_length_mm": characteristic_length, + **pistoia_common, } else: row = { diff --git a/tests/util/test_faim.py b/tests/util/test_faim.py index 734d033..f7ec84a 100644 --- a/tests/util/test_faim.py +++ b/tests/util/test_faim.py @@ -206,6 +206,15 @@ def test_write_results_csv_femur_profile_reports_4_percent_and_stiffness(tmp_pat "reaction_force_N", "stiffness_N_per_mm", "characteristic_length_mm", + "pistoia_file", + "pistoia_failure_var", + "pistoia_stiffness_var", + "pistoia_failure_load_N", + "pistoia_failure_load_kN", + "pistoia_stiffness_N_per_mm", + "critical_volume_pct", + "critical_ees", + "ees_at_crit_vol", ] assert row["analysis_var"] == "fy_ns1" assert row["applied_displacement"] == "1.0" @@ -264,6 +273,15 @@ def test_write_results_csv_spine_profile_reports_0p68_percent(tmp_path): "reaction_force_N", "stiffness_N_per_mm", "characteristic_length_mm", + "pistoia_file", + "pistoia_failure_var", + "pistoia_stiffness_var", + "pistoia_failure_load_N", + "pistoia_failure_load_kN", + "pistoia_stiffness_N_per_mm", + "critical_volume_pct", + "critical_ees", + "ees_at_crit_vol", ] assert row["analysis_var"] == "fz_ns1" assert row["applied_displacement"] == "-2.0" @@ -319,7 +337,7 @@ def test_write_results_csv_spine_profile_converts_percent_from_geometry(tmp_path assert row["stiffness_N_per_mm"] == "100.0" -def test_write_results_csv_spine_profile_excludes_pistoia_fields(tmp_path): +def test_write_results_csv_spine_profile_includes_pistoia_fields(tmp_path): analysis_file = tmp_path / "model_analysis.txt" model_file = tmp_path / "model.n88model" pistoia_file = tmp_path / "model_pistoia.txt" @@ -361,9 +379,10 @@ def test_write_results_csv_spine_profile_excludes_pistoia_fields(tmp_path): with results_csv.open(newline="") as handle: row = next(csv.DictReader(handle)) - assert "pistoia_failure_load_N" not in row - assert "pistoia_failure_load_kN" not in row - assert "pistoia_stiffness_N_per_mm" not in row + assert row["pistoia_failure_load_N"] == "-300.0" + assert row["pistoia_failure_load_kN"] == "0.3" + assert row["pistoia_stiffness_N_per_mm"] == "120.0" + assert row["critical_volume_pct"] == "2.0" def test_write_results_csv_spine_profile_can_use_constraint_groups(tmp_path): From 0ad679374b09f2fbea9f3d75d8c9fcb179ee1f13 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:11:28 -0600 Subject: [PATCH 23/67] Align hip FEA preprocessing defaults --- ogo/cli/GenerateFEM.py | 23 +- ogo/fea/femur.py | 429 ++++++++++++++---- ogo/util/faim.py | 148 ++++++ .../fixtures/fe_workflow_defaults_locked.json | 3 +- tests/fea/test_femur.py | 60 +++ tests/fea/test_workflow_locked_defaults.py | 1 + tests/util/test_faim.py | 89 ++++ 7 files changed, 669 insertions(+), 84 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 8882cd3..e5bc377 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -33,6 +33,7 @@ DEFAULT_FEMUR_CUT_MODE, DEFAULT_FEMUR_ISO_RESOLUTION_MM, DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, DEFAULT_PMMA_INTRUSION_MM, DEFAULT_PMMA_THICKNESS_MM, @@ -646,6 +647,11 @@ def write_modeling_metadata( }, "shaft_standardization": { "cut_mode": femur_cut_mode, + "crop_stage": ( + "after isotropic resampling and before ICP" + if femur_cut_mode in {"bbox_ratio", "proximal_box_ratio"} + else "after ICP on the generated model grid" + ), "fixed_length_mm": option_float(generator_argv, "--femur_shaft_length", 100.0), "lesser_trochanter_distal_offset_mm": option_float( generator_argv, @@ -654,9 +660,22 @@ def write_modeling_metadata( ), "bbox_ratio": femur_bbox_ratio, "bbox_crop_from": femur_bbox_crop_from, + "proximal_box_ratio": { + "ratio": option_float(generator_argv, "--femur_experimental_crop_ratio", 1.2), + "proximal_reference_distance_mm": option_float( + generator_argv, + "--femur_proximal_reference_distance", + DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, + ), + "reference_width": option_value( + generator_argv, + "--femur_proximal_reference_width", + "max_xy", + ), + }, "cut_plane": ( - "input bbox-ratio crop face transformed with the femur" - if femur_cut_mode == "bbox_ratio" + "post-resample pre-ICP crop face transformed with the femur" + if femur_cut_mode in {"bbox_ratio", "proximal_box_ratio"} else "flat model-grid z plane" ), "incomplete_fov_behavior": "fail model generation", diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index b2582af..79358c1 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -17,12 +17,12 @@ 3. Smooth the transformed femur mask with one binary close/open pass only when at least one input spacing dimension is coarser than 2 mm. If a compartment mask is supplied, the derived cortical binary mask follows the same rule. -4. Standardize the distal shaft with a bbox-ratio crop in the input image - coordinate system before registration. The short femur dimension is kept as - the reference length, the shaft axis is cropped to 1.3 times that reference - length from the distal end, and the transformed crop face becomes the distal - support surface. ``lesser_trochanter`` and ``fixed_length`` modes are - available for debugging or historical comparisons. +4. Standardize the distal shaft after isotropic resampling and before + registration. The default uses a proximal max(x, y) transverse width + reference and retains 1.2 times that width along the proximal-distal axis. + The transformed crop face becomes the distal support surface. ``bbox_ratio``, + ``lesser_trochanter``, and ``fixed_length`` modes are available for + debugging or historical comparisons. 5. Generate two geometric PMMA fixtures from bbox-relative contact planes: a femoral-head loading fixture on the high-y side and a greater-trochanter contact fixture on the low-y side. Defaults are 10 mm PMMA thickness and @@ -77,13 +77,17 @@ DISTAL_SHAFT_FIXTURE_NORMAL = (0.0, -0.2588190451025207, 0.9659258262890683) DEFAULT_FEMUR_BBOX_RATIO = (1.0, 1.3, None) DEFAULT_FEMUR_BBOX_CROP_FROM = (None, "max", None) +DEFAULT_FEMUR_EXPERIMENTAL_RATIO = 1.2 +DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM = 40.0 +DEFAULT_FEMUR_PROXIMAL_REFERENCE_WIDTH = "max_xy" DEFAULT_FEMUR_REFERENCE_MIN_SCALE = (0.8, 0.8, 0.75) DEFAULT_FEMUR_REFERENCE_MAX_SCALE = (1.2, 1.2, 1.3) DEFAULT_PMMA_THICKNESS_MM = 10.0 DEFAULT_PMMA_INTRUSION_MM = 6.0 DEFAULT_FEMUR_INPUT_MARGIN_MM = DEFAULT_PMMA_THICKNESS_MM + DEFAULT_PMMA_INTRUSION_MM DEFAULT_FEMUR_SHAFT_LENGTH_MM = 100.0 -DEFAULT_FEMUR_CUT_MODE = "bbox_ratio" +DEFAULT_FEMUR_CUT_MODE = "proximal_box_ratio" +PRE_ICP_CROP_MODES = {"bbox_ratio", "proximal_box_ratio"} DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM = 50.0 DEFAULT_CORTICAL_LABEL = 1 DEFAULT_TRABECULAR_LABEL = 2 @@ -519,6 +523,7 @@ def crop_vtk_images_to_bbox_ratio( active = mask_data != 0 if not np.any(active): raise ValueError("Cannot apply bbox-ratio crop to an empty femur mask.") + active_for_crop = _largest_connected_component_mask(active) ratio_xyz = _parse_bbox_ratio_recipe(bbox_ratio) crop_from_xyz = _parse_bbox_crop_from_recipe(bbox_crop_from) @@ -536,42 +541,14 @@ def crop_vtk_images_to_bbox_ratio( if not reference_axes: raise ValueError("bbox_ratio must contain one preserved axis with value 1.") - coords = np.argwhere(active) - lo = coords.min(axis=0).astype(np.int64) - hi = (coords.max(axis=0) + 1).astype(np.int64) - size = hi - lo spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) - physical_size = size.astype(np.float64) * spacing - reference_axis = min(reference_axes, key=lambda axis: float(physical_size[axis])) - reference_length_mm = float(size[reference_axis]) * float(spacing[reference_axis]) - - out_lo = lo.copy() - out_hi = hi.copy() - crop_surface = None - for axis, axis_ratio in enumerate(ratio_xyz): - if axis_ratio is None: - continue - target_mm = reference_length_mm * float(axis_ratio) - target_voxels = max(1, int(round(target_mm / float(spacing[axis])))) - target_voxels = min(int(size[axis]), target_voxels) - mode = crop_from_xyz[axis] - if mode == "min": - start = int(hi[axis]) - target_voxels - crop_surface = {"axis": axis, "side": "min", "local_index": 0, "normal_sign": -1.0} - elif mode == "max": - start = int(lo[axis]) - crop_surface = { - "axis": axis, - "side": "max", - "local_index": target_voxels - 1, - "normal_sign": 1.0, - } - else: - center = 0.5 * (float(lo[axis]) + float(hi[axis])) - start = int(round(center - 0.5 * float(target_voxels))) - start = max(int(lo[axis]), min(start, int(hi[axis]) - target_voxels)) - out_lo[axis] = start - out_hi[axis] = start + target_voxels + out_lo, out_hi, crop_surface, crop_meta = _stable_bbox_ratio_crop_bounds( + active=active_for_crop, + spacing=spacing, + ratio_xyz=ratio_xyz, + crop_from_xyz=crop_from_xyz, + reference_axes=reference_axes, + ) slices = tuple(slice(int(out_lo[axis]), int(out_hi[axis])) for axis in range(3)) origin = tuple( @@ -583,7 +560,7 @@ def crop_vtk_images_to_bbox_ratio( for image in images ] - cropped_active = active[slices] + cropped_active = active_for_crop[slices] crop_face = np.zeros(cropped_active.shape, dtype=np.uint8) if crop_surface is not None: axis = int(crop_surface["axis"]) @@ -604,12 +581,14 @@ def crop_vtk_images_to_bbox_ratio( "crop_from_recipe": tuple(bbox_crop_from) if bbox_crop_from is not None else None, "ratio_xyz": ratio_xyz, "crop_from_xyz": crop_from_xyz, - "reference_axis": "xyz"[reference_axis], - "reference_length_mm": reference_length_mm, - "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), + "reference_axis": crop_meta["reference_axis"], + "reference_length_mm": crop_meta["reference_length_mm"], + "input_bbox_xyz": crop_meta["input_bbox_xyz"], "crop_slices_xyz": tuple((int(out_lo[axis]), int(out_hi[axis])) for axis in range(3)), "output_shape_xyz": tuple(int(value) for value in cropped_active.shape), "output_origin": origin, + "crop_iterations": crop_meta["crop_iterations"], + "final_bbox_ratio_xyz": crop_meta["final_bbox_ratio_xyz"], "crop_surface": None if crop_surface is None else { @@ -622,6 +601,269 @@ def crop_vtk_images_to_bbox_ratio( return cropped_images, crop_face_image, meta +def _largest_connected_component_mask(active): + import numpy as np + + try: + from scipy import ndimage + except ImportError: + return active + + labeled, count = ndimage.label(active) + if count <= 1: + return active + sizes = np.bincount(labeled.ravel()) + sizes[0] = 0 + return labeled == int(np.argmax(sizes)) + + +def crop_vtk_images_to_proximal_box_ratio( + vtk_images, + vtk_mask, + *, + ratio=DEFAULT_FEMUR_EXPERIMENTAL_RATIO, + proximal_reference_distance_mm=DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, + reference_width=DEFAULT_FEMUR_PROXIMAL_REFERENCE_WIDTH, + labels=None, +): + """Crop from the distal side using proximal transverse width as reference.""" + import numpy as np + + from ogo.util.vtk_image import vtk_image_to_numpy + + mask_data = vtk_image_to_numpy(vtk_mask) + active = _active_crop_mask(mask_data, labels) + spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) + coords = np.argwhere(active) + lo = coords.min(axis=0).astype(np.int64) + hi = (coords.max(axis=0) + 1).astype(np.int64) + size = hi - lo + proximal_voxels = min( + int(size[2]), + max(1, int(round(float(proximal_reference_distance_mm) / float(spacing[2])))), + ) + proximal_start = int(hi[2]) - proximal_voxels + proximal = active[:, :, proximal_start : int(hi[2])] + proximal_coords = np.argwhere(proximal) + if not len(proximal_coords): + proximal_coords = coords - np.asarray([0, 0, proximal_start], dtype=np.int64) + x_width_mm = _percentile_index_width_mm( + proximal_coords[:, 0] + 0, + float(spacing[0]), + ) + y_width_mm = _percentile_index_width_mm( + proximal_coords[:, 1] + 0, + float(spacing[1]), + ) + reference_width = str(reference_width).strip().lower() + if reference_width == "max_xy": + reference_axis = 0 if x_width_mm >= y_width_mm else 1 + reference_width_mm = x_width_mm if reference_axis == 0 else y_width_mm + elif reference_width == "rms_xy": + reference_axis = None + reference_width_mm = float(np.sqrt((x_width_mm**2 + y_width_mm**2) / 2.0)) + else: + raise ValueError("reference_width must be max_xy or rms_xy.") + target_length_mm = float(ratio) * float(reference_width_mm) + target_voxels = min(int(size[2]), max(1, int(round(target_length_mm / float(spacing[2]))))) + status = "short" if int(size[2]) <= target_voxels else "cropped" + keep = np.zeros(active.shape, dtype=bool) + out_lo = lo.copy() + out_hi = hi.copy() + if status == "short": + keep[tuple(slice(int(lo[axis]), int(hi[axis])) for axis in range(3))] = True + else: + out_lo[2] = int(hi[2]) - target_voxels + keep[ + int(out_lo[0]) : int(out_hi[0]), + int(out_lo[1]) : int(out_hi[1]), + int(out_lo[2]) : int(out_hi[2]), + ] = True + return _crop_vtk_images_with_keep_mask( + vtk_images, + vtk_mask, + active=active, + keep=keep, + meta={ + "enabled": True, + "method": "proximal_box_ratio", + "ratio": float(ratio), + "proximal_reference_distance_mm": float(proximal_reference_distance_mm), + "status": status, + "reference_width": reference_width, + "reference_axis": "rms_xy" if reference_axis is None else "xyz"[reference_axis], + "reference_width_mm": float(reference_width_mm), + "proximal_x_width_mm": float(x_width_mm), + "proximal_y_width_mm": float(y_width_mm), + "target_length_mm": float(target_length_mm), + "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), + "crop_slices_xyz": tuple((int(out_lo[axis]), int(out_hi[axis])) for axis in range(3)), + }, + ) + + +def _active_crop_mask(mask_data, labels): + import numpy as np + + if labels: + active = np.isin(mask_data, sorted(int(label) for label in labels)) + else: + active = mask_data != 0 + if not np.any(active): + raise ValueError("Cannot crop an empty femur mask.") + return _largest_connected_component_mask(active) + + +def _percentile_index_width_mm(indices, spacing, *, low=1.0, high=99.0): + import numpy as np + + lo, hi = np.percentile(indices.astype(np.float64), [float(low), float(high)]) + return float(hi - lo + 1.0) * float(spacing) + + +def _crop_vtk_images_with_keep_mask(vtk_images, vtk_mask, *, active, keep, meta): + import numpy as np + import vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + kept_active = active & keep + if not np.any(kept_active): + raise ValueError("Femur crop removed all foreground voxels.") + coords = np.argwhere(kept_active) + out_lo = coords.min(axis=0).astype(np.int64) + out_hi = (coords.max(axis=0) + 1).astype(np.int64) + slices = tuple(slice(int(out_lo[axis]), int(out_hi[axis])) for axis in range(3)) + spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) + origin = tuple( + float(vtk_mask.GetOrigin()[axis]) + float(out_lo[axis]) * float(spacing[axis]) + for axis in range(3) + ) + cropped_images = [] + for image in vtk_images: + data = vtk_image_to_numpy(image).copy() + data[~keep] = 0 + cropped_images.append(_vtk_image_from_array(data[slices], image, origin=origin)) + crop_face = kept_active & _touches_removed_or_background(kept_active, active & ~keep) + crop_face_image = _vtk_image_from_array( + crop_face[slices].astype(np.uint8), + vtk_mask, + origin=origin, + vtk_array_type=vtk.VTK_UNSIGNED_CHAR, + ) + meta = dict(meta) + meta.update( + { + "crop_slices_xyz": tuple((int(out_lo[axis]), int(out_hi[axis])) for axis in range(3)), + "output_shape_xyz": tuple(int(value) for value in kept_active[slices].shape), + "output_origin": origin, + "crop_face_voxels": int(crop_face.sum()), + } + ) + return cropped_images, crop_face_image, meta + + +def _touches_removed_or_background(kept_active, removed_active): + import numpy as np + + face = np.zeros(kept_active.shape, dtype=bool) + for axis in range(3): + before = [slice(None), slice(None), slice(None)] + after = [slice(None), slice(None), slice(None)] + before[axis] = slice(1, None) + after[axis] = slice(None, -1) + face[tuple(before)] |= kept_active[tuple(before)] & removed_active[tuple(after)] + face[tuple(after)] |= kept_active[tuple(after)] & removed_active[tuple(before)] + return face + + +def _stable_bbox_ratio_crop_bounds( + *, + active, + spacing, + ratio_xyz, + crop_from_xyz, + reference_axes, + max_iterations=8, +): + import numpy as np + + coords = np.argwhere(active) + lo = coords.min(axis=0).astype(np.int64) + hi = (coords.max(axis=0) + 1).astype(np.int64) + input_lo = lo.copy() + input_hi = hi.copy() + out_lo = lo.copy() + out_hi = hi.copy() + crop_surface = None + input_size = input_hi - input_lo + input_physical_size = input_size.astype(np.float64) * spacing + reference_axis = min(reference_axes, key=lambda axis: float(input_physical_size[axis])) + reference_length_mm = 0.0 + + for iteration in range(1, int(max_iterations) + 1): + retained = active[tuple(slice(int(out_lo[axis]), int(out_hi[axis])) for axis in range(3))] + retained_coords = np.argwhere(retained) + if not len(retained_coords): + raise ValueError("bbox-ratio crop removed all foreground voxels.") + lo = out_lo + retained_coords.min(axis=0).astype(np.int64) + hi = out_lo + (retained_coords.max(axis=0) + 1).astype(np.int64) + size = hi - lo + physical_size = size.astype(np.float64) * spacing + reference_axis = min( + reference_axes, + key=lambda axis: (float(physical_size[axis]), axis != reference_axis), + ) + reference_length_mm = float(size[reference_axis]) * float(spacing[reference_axis]) + + next_lo = lo.copy() + next_hi = hi.copy() + crop_surface = None + for axis, axis_ratio in enumerate(ratio_xyz): + if axis_ratio is None: + continue + target_mm = reference_length_mm * float(axis_ratio) + target_voxels = max(1, int(round(target_mm / float(spacing[axis])))) + target_voxels = min(int(size[axis]), target_voxels) + mode = crop_from_xyz[axis] + if mode == "min": + start = int(hi[axis]) - target_voxels + crop_surface = {"axis": axis, "side": "min", "normal_sign": -1.0} + elif mode == "max": + start = int(lo[axis]) + crop_surface = {"axis": axis, "side": "max", "normal_sign": 1.0} + else: + center = 0.5 * (float(lo[axis]) + float(hi[axis])) + start = int(round(center - 0.5 * float(target_voxels))) + start = max(int(lo[axis]), min(start, int(hi[axis]) - target_voxels)) + next_lo[axis] = start + next_hi[axis] = start + target_voxels + + if np.array_equal(next_lo, out_lo) and np.array_equal(next_hi, out_hi): + break + out_lo, out_hi = next_lo, next_hi + + if crop_surface is not None: + axis = int(crop_surface["axis"]) + crop_surface["local_index"] = ( + 0 if crop_surface["side"] == "min" else int(out_hi[axis] - out_lo[axis] - 1) + ) + + final_size = out_hi - out_lo + final_physical = final_size.astype(np.float64) * spacing + final_ratio = tuple( + float(final_physical[axis] / reference_length_mm) if reference_length_mm > 0 else 0.0 + for axis in range(3) + ) + return out_lo, out_hi, crop_surface, { + "reference_axis": "xyz"[reference_axis], + "reference_length_mm": reference_length_mm, + "input_bbox_xyz": tuple((int(input_lo[axis]), int(input_hi[axis])) for axis in range(3)), + "crop_iterations": iteration, + "final_bbox_ratio_xyz": final_ratio, + } + + def crop_face_support_vector(crop_face_vtk, mask_vtk): """Estimate the outward normal of a transformed crop-face label image.""" import numpy as np @@ -1579,6 +1821,9 @@ def sidewaysFallFe(args): femur_cut_mode = args.femur_cut_mode femur_bbox_ratio = args.femur_bbox_ratio femur_bbox_crop_from = args.femur_bbox_crop_from + femur_experimental_crop_ratio = args.femur_experimental_crop_ratio + femur_proximal_reference_distance = args.femur_proximal_reference_distance + femur_proximal_reference_width = args.femur_proximal_reference_width femur_lesser_trochanter_distal_offset = args.femur_lesser_trochanter_distal_offset femur_lesser_trochanter_distal_offset_percent = args.femur_lesser_trochanter_distal_offset_percent femur_input_margin = args.femur_input_margin @@ -1631,6 +1876,10 @@ def sidewaysFallFe(args): if femur_cut_mode == "bbox_ratio": ogo.message("Femur BBox Ratio [reference, constrained, free]: %s" % str(femur_bbox_ratio)) ogo.message("Femur BBox Crop From [reference, constrained, free]: %s" % str(femur_bbox_crop_from)) + if femur_cut_mode == "proximal_box_ratio": + ogo.message("Femur Proximal Box Crop Ratio: %8.4f" % femur_experimental_crop_ratio) + ogo.message("Femur Proximal Reference Distance [mm]: %8.4f" % femur_proximal_reference_distance) + ogo.message("Femur Proximal Reference Width: %s" % femur_proximal_reference_width) if compartment_mask is not None: ogo.message("Compartment Mask: %s" % compartment_mask) ogo.message("Cortical Label: %d" % cortical_label) @@ -1667,30 +1916,8 @@ def sidewaysFallFe(args): distal_crop_face = None bbox_crop_meta = None - if femur_cut_mode == "bbox_ratio": - ogo.message("Applying bbox-ratio femur crop in input image coordinates...") - images_to_crop = [imageData, maskThres] + ([compartmentData] if compartmentData is not None else []) - cropped_images, distal_crop_face, bbox_crop_meta = crop_vtk_images_to_bbox_ratio( - images_to_crop, - maskThres, - bbox_ratio=femur_bbox_ratio, - bbox_crop_from=femur_bbox_crop_from, - labels={1}, - ) - imageData = cropped_images[0] - maskThres = cropped_images[1] - if compartmentData is not None: - compartmentData = cropped_images[2] - ogo.message( - "BBox-ratio crop slices xyz=%s; output shape xyz=%s; crop face voxels=%d." - % ( - bbox_crop_meta["crop_slices_xyz"], - bbox_crop_meta["output_shape_xyz"], - bbox_crop_meta["crop_face_voxels"], - ) - ) - ogo.message("Resampling cropped femur inputs to isotropic spacing before ICP...") + ogo.message("Resampling femur inputs to isotropic spacing before custom crop and ICP...") imageData = resample_vtk_image_like_workflow( imageData, iso_resolution, @@ -1713,6 +1940,40 @@ def sidewaysFallFe(args): iso_resolution, interpolation="nearest", ) + + if femur_cut_mode in PRE_ICP_CROP_MODES: + ogo.message("Applying %s femur crop after isotropic resampling and before ICP..." % femur_cut_mode) + images_to_crop = [imageData, maskThres] + ([compartmentData] if compartmentData is not None else []) + if femur_cut_mode == "bbox_ratio": + cropped_images, distal_crop_face, bbox_crop_meta = crop_vtk_images_to_bbox_ratio( + images_to_crop, + maskThres, + bbox_ratio=femur_bbox_ratio, + bbox_crop_from=femur_bbox_crop_from, + labels={1}, + ) + else: + cropped_images, distal_crop_face, bbox_crop_meta = crop_vtk_images_to_proximal_box_ratio( + images_to_crop, + maskThres, + ratio=femur_experimental_crop_ratio, + proximal_reference_distance_mm=femur_proximal_reference_distance, + reference_width=femur_proximal_reference_width, + labels={1}, + ) + imageData = cropped_images[0] + maskThres = cropped_images[1] + if compartmentData is not None: + compartmentData = cropped_images[2] + ogo.message( + "%s crop slices xyz=%s; output shape xyz=%s; crop face voxels=%d." + % ( + femur_cut_mode, + bbox_crop_meta["crop_slices_xyz"], + bbox_crop_meta["output_shape_xyz"], + bbox_crop_meta["crop_face_voxels"], + ) + ) registration_mask = maskThres images_to_pad = [imageData, maskThres] @@ -1872,8 +2133,8 @@ def sidewaysFallFe(args): f"{input_spacing} is <= {mask_smoothing_spacing_threshold} mm in all dimensions..." ) - if femur_cut_mode == "bbox_ratio": - ogo.message("Using transformed bbox-ratio crop face for distal shaft standardization.") + if femur_cut_mode in PRE_ICP_CROP_MODES: + ogo.message("Using transformed %s crop face for distal shaft standardization." % femur_cut_mode) try: mask_z_min, mask_z_max = femur_z_coverage(mask_trans) except ValueError as exc: @@ -1881,17 +2142,17 @@ def sidewaysFallFe(args): sys.exit(1) retained_length_mm = mask_z_max - mask_z_min shaft_crop = { - "cut_mode": "bbox_ratio", + "cut_mode": femur_cut_mode, "cut_plane": "transformed input crop face", - "bbox_ratio": bbox_crop_meta, + "pre_icp_crop": bbox_crop_meta, "mask_z_min": mask_z_min, "mask_z_max": mask_z_max, "retained_length_mm": retained_length_mm, "warnings": [], } ogo.message( - "Transformed bbox-ratio mask z coverage [%8.4f, %8.4f]; retained z-span=%8.4f" - % (mask_z_min, mask_z_max, retained_length_mm) + "Transformed %s mask z coverage [%8.4f, %8.4f]; retained z-span=%8.4f" + % (femur_cut_mode, mask_z_min, mask_z_max, retained_length_mm) ) else: ogo.message("Applying flat distal femur crop in reference coordinates...") @@ -1991,8 +2252,8 @@ def sidewaysFallFe(args): ) if distal_crop_face_change is not None: distal_crop_face_change = ogo.applyMask(distal_crop_face_change, ogo.cast2unsignchar(change)) - if femur_cut_mode == "bbox_ratio": - ogo.message("Skipping model-grid distal shaft cut; bbox-ratio crop face is already transformed.") + if femur_cut_mode in PRE_ICP_CROP_MODES: + ogo.message("Skipping model-grid distal shaft cut; pre-ICP crop face is already transformed.") else: model_z_min, model_z_max = femur_z_coverage(change) model_cut_z = model_z_max - retained_length_mm @@ -2278,9 +2539,9 @@ def sidewaysFallFe(args): # length before registration; the support itself remains a bbox-relative # recipe plane in the generated model frame. ogo.message("Determining distal femur nodes...") - if femur_cut_mode == "bbox_ratio": + if femur_cut_mode in PRE_ICP_CROP_MODES: if distal_crop_face_change is None: - ogo.message("BBox-ratio cut mode requires a transformed distal crop-face mask.") + ogo.message("%s cut mode requires a transformed distal crop-face mask." % femur_cut_mode) sys.exit(1) distal_plane = bbox_relative_oriented_contact_plane( model_bounds, @@ -2453,13 +2714,19 @@ def main(): help="Sets the applied displacement endpoint for the sideways-fall model. The default reports the force at 4%% displacement. (default: %(default)s)") parser.add_argument("--femur_shaft_length", type=float, default=DEFAULT_FEMUR_SHAFT_LENGTH_MM, help="Retained proximal femur length [mm] for --femur_cut_mode fixed_length. (default: %(default)s [mm])") - parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "lesser_trochanter", "fixed_length"], + parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "proximal_box_ratio", "lesser_trochanter", "fixed_length"], default=DEFAULT_FEMUR_CUT_MODE, - help="Set the distal femur crop. bbox_ratio crops in input image coordinates and uses the transformed crop face for distal support. (default: %(default)s)") + help="Set the distal femur crop. Pre-ICP crop modes run after isotropic resampling and before ICP; the transformed crop face is used for distal support. (default: %(default)s)") parser.add_argument("--femur_bbox_ratio", nargs=3, default=DEFAULT_FEMUR_BBOX_RATIO, help="BBox-ratio crop in recipe order: reference constrained free. Use 'none' for a free axis. (default: %(default)s)") parser.add_argument("--femur_bbox_crop_from", nargs=3, default=DEFAULT_FEMUR_BBOX_CROP_FROM, help="BBox crop side in recipe order: reference constrained free. Values are min, max, center/null. (default: %(default)s)") + parser.add_argument("--femur_experimental_crop_ratio", type=float, default=DEFAULT_FEMUR_EXPERIMENTAL_RATIO, + help="Retained proximal femur length as a multiple of the proximal transverse max(x, y) width in proximal_box_ratio mode. (default: %(default)s)") + parser.add_argument("--femur_proximal_reference_distance", type=float, default=DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, + help="Proximal-most distance [mm] used to estimate transverse reference width in proximal_box_ratio mode. (default: %(default)s)") + parser.add_argument("--femur_proximal_reference_width", choices=["max_xy", "rms_xy"], default=DEFAULT_FEMUR_PROXIMAL_REFERENCE_WIDTH, + help="Transverse width summary for proximal_box_ratio. max_xy uses max(x width, y width); rms_xy uses sqrt((x^2 + y^2) / 2). (default: %(default)s)") parser.add_argument("--femur_lesser_trochanter_distal_offset", type=float, default=DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, help="Cut this many mm distal to the detected lesser trochanter in lesser_trochanter mode. (default: %(default)s [mm])") parser.add_argument("--femur_lesser_trochanter_distal_offset_percent", type=float, default=None, diff --git a/ogo/util/faim.py b/ogo/util/faim.py index bfb1127..c6c9415 100644 --- a/ogo/util/faim.py +++ b/ogo/util/faim.py @@ -1,5 +1,6 @@ """Small FAIM/N88 command adapter used by Ogo FE CLIs.""" +from contextlib import contextmanager import csv import os from pathlib import Path @@ -185,6 +186,153 @@ def parse_pistoia_text(path): return out +def _read_masked_element_selection(model_file, mask_file, exclude_material_id): + """Return hexahedral elements whose centroids fall inside an image mask.""" + try: + import numpy as np + from netCDF4 import Dataset + import SimpleITK as sitk + except ImportError as exc: + raise RuntimeError( + "netCDF4, numpy, and SimpleITK are required to mask n88model elements." + ) from exc + + mask_image = sitk.ReadImage(str(mask_file)) + mask = sitk.GetArrayFromImage(mask_image) > 0 + spacing = np.asarray(mask_image.GetSpacing(), dtype=float) + origin = np.asarray(mask_image.GetOrigin(), dtype=float) + direction = np.asarray(mask_image.GetDirection(), dtype=float).reshape((3, 3)) + inverse_direction = np.linalg.inv(direction) + size = np.asarray(mask_image.GetSize(), dtype=np.int64) + + with Dataset(str(model_file), "r") as root: + part = root.groups["Parts"].groups["Part1"] + coordinates = np.asarray(part.variables["NodeCoordinates"][:], dtype=float) + hexahedrons = part.groups["Elements"].groups["Hexahedrons"] + node_numbers = np.asarray(hexahedrons.variables["NodeNumbers"][:], dtype=np.int64) - 1 + material_ids = np.asarray(hexahedrons.variables["MaterialID"][:]) + + centroids = coordinates[node_numbers].mean(axis=1) + continuous_index = ((centroids - origin) @ inverse_direction.T) / spacing + mask_index = np.floor(continuous_index + 0.5).astype(np.int64) + in_bounds = np.all((mask_index >= 0) & (mask_index < size), axis=1) + + in_mask = np.zeros(len(material_ids), dtype=bool) + valid_index = mask_index[in_bounds] + if len(valid_index): + in_mask[in_bounds] = mask[valid_index[:, 2], valid_index[:, 1], valid_index[:, 0]] + + bone_elements = material_ids != exclude_material_id + selected_elements = bone_elements & in_mask + return selected_elements, bone_elements, material_ids + + +@contextmanager +def temporary_pistoia_material_mask(model_file, mask_file, *, exclude_material_id=5000): + """Temporarily exclude bone elements outside an image mask from Pistoia. + + This is a narrow adapter around the current ``n88pistoia --exclude`` behavior. + If vtkbone gains native image-mask Pistoia support, callers should be able to + keep using this function while the implementation changes underneath. + """ + try: + from netCDF4 import Dataset + except ImportError as exc: + raise RuntimeError("netCDF4 is required to update n88model material IDs.") from exc + + selected_elements, bone_elements, original_material_ids = _read_masked_element_selection( + model_file, mask_file, exclude_material_id + ) + if not selected_elements.any(): + raise ValueError( + f"Mask '{mask_file}' does not select any non-excluded elements in '{model_file}'." + ) + + masked_material_ids = original_material_ids.copy() + masked_material_ids[bone_elements & ~selected_elements] = exclude_material_id + + stats = { + "mask_file": str(mask_file), + "exclude_material_id": int(exclude_material_id), + "total_elements": int(len(original_material_ids)), + "original_included_elements": int(bone_elements.sum()), + "selected_elements": int(selected_elements.sum()), + "temporarily_excluded_elements": int((bone_elements & ~selected_elements).sum()), + } + + with Dataset(str(model_file), "r+") as root: + material_ids = ( + root.groups["Parts"] + .groups["Part1"] + .groups["Elements"] + .groups["Hexahedrons"] + .variables["MaterialID"] + ) + material_ids[:] = masked_material_ids + + try: + yield stats + finally: + with Dataset(str(model_file), "r+") as root: + material_ids = ( + root.groups["Parts"] + .groups["Part1"] + .groups["Elements"] + .groups["Hexahedrons"] + .variables["MaterialID"] + ) + material_ids[:] = original_material_ids + + +def run_pistoia_with_image_mask( + *, + model_file, + mask_file, + output_file, + n88pistoia_command="n88pistoia", + critical_volume=2.0, + critical_strain=0.007, + exclude_material_id=5000, + conda_env=None, + conda_executable="conda", + env=None, + dry_run=False, +): + """Run Pistoia on model elements selected by an image mask.""" + command = [ + n88pistoia_command, + str(model_file), + "--critical_volume", + str(critical_volume), + "--critical_strain", + str(critical_strain), + "--exclude", + str(exclude_material_id), + "--output_file", + str(output_file), + ] + if dry_run: + run_command( + command, + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=True, + ) + return {"mask_file": str(mask_file), "dry_run": True} + + with temporary_pistoia_material_mask( + model_file, mask_file, exclude_material_id=exclude_material_id + ) as stats: + run_command( + command, + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + ) + return stats + + def _read_n88_node_coordinates_and_sets(model_file): """Read node coordinates and named node sets from an n88model file.""" try: diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index fc2b512..19b417d 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -13,7 +13,7 @@ null ], "cortical_label": 1, - "cut_mode": "bbox_ratio", + "cut_mode": "proximal_box_ratio", "distal_shaft_center_fraction": [ 0.5, 0.5, @@ -56,6 +56,7 @@ ], "pmma_intrusion_mm": 6.0, "pmma_thickness_mm": 10.0, + "proximal_reference_distance_mm": 40.0, "reference_max_scale": [ 1.2, 1.2, diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 989a0a3..44e9d66 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -390,6 +390,66 @@ def test_bbox_ratio_crop_uses_shortest_unit_axis_as_reference(): assert meta["reference_length_mm"] == pytest.approx(10.0) +def test_bbox_ratio_crop_recomputes_reference_after_distal_shaft_crop(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + density = np.zeros((12, 60, 90), dtype=np.float32) + mask = np.zeros_like(density, dtype=np.uint8) + density[2:10, 5:55, 0:10] = 700.0 + mask[2:10, 5:55, 0:10] = 2 + density[2:10, 20:40, 10:80] = 700.0 + mask[2:10, 20:40, 10:80] = 2 + + cropped, _crop_face, meta = femur.crop_vtk_images_to_bbox_ratio( + [_vtk_image_from_array(density)], + _vtk_image_from_array(mask), + bbox_ratio=femur.DEFAULT_FEMUR_BBOX_RATIO, + bbox_crop_from=femur.DEFAULT_FEMUR_BBOX_CROP_FROM, + labels={2}, + ) + + cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 + coords = np.argwhere(cropped_mask) + final_size = coords.max(axis=0) - coords.min(axis=0) + 1 + + assert final_size[1] == 20 + assert final_size[2] <= 26 + assert meta["crop_iterations"] > 1 + assert meta["reference_length_mm"] == pytest.approx(20.0) + assert meta["final_bbox_ratio_xyz"][2] <= 1.3 + + +def test_proximal_box_ratio_crop_uses_proximal_transverse_width(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + density = np.zeros((110, 120, 120), dtype=np.float32) + mask = np.zeros_like(density, dtype=np.uint8) + density[30:82, 45:73, 70:110] = 700.0 + mask[30:82, 45:73, 70:110] = 2 + density[46:66, 45:105, 10:70] = 700.0 + mask[46:66, 45:105, 10:70] = 2 + + cropped, crop_face, meta = femur.crop_vtk_images_to_proximal_box_ratio( + [_vtk_image_from_array(density)], + _vtk_image_from_array(mask), + ratio=1.2, + labels={2}, + ) + + cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 + coords = np.argwhere(cropped_mask) + final_size = coords.max(axis=0) - coords.min(axis=0) + 1 + + assert meta["reference_width_mm"] == pytest.approx(52.0) + assert meta["reference_axis"] == "x" + assert meta["target_length_mm"] == pytest.approx(62.4) + assert final_size[2] == 62 + assert final_size[1] == 60 + assert vtk_image_to_numpy(crop_face).sum() > 0 + + def test_crop_face_support_vector_points_away_from_retained_bone(): np = pytest.importorskip("numpy") diff --git a/tests/fea/test_workflow_locked_defaults.py b/tests/fea/test_workflow_locked_defaults.py index 37f6fa5..d462e96 100644 --- a/tests/fea/test_workflow_locked_defaults.py +++ b/tests/fea/test_workflow_locked_defaults.py @@ -70,6 +70,7 @@ def _locked_workflow_defaults(): "distal_shaft_normal": list(femur.DISTAL_SHAFT_FIXTURE_NORMAL), "bbox_ratio": list(femur.DEFAULT_FEMUR_BBOX_RATIO), "bbox_crop_from": list(femur.DEFAULT_FEMUR_BBOX_CROP_FROM), + "proximal_reference_distance_mm": femur.DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, "reference_min_scale": list(femur.DEFAULT_FEMUR_REFERENCE_MIN_SCALE), "reference_max_scale": list(femur.DEFAULT_FEMUR_REFERENCE_MAX_SCALE), "pmma_thickness_mm": femur.DEFAULT_PMMA_THICKNESS_MM, diff --git a/tests/util/test_faim.py b/tests/util/test_faim.py index f7ec84a..631c0e0 100644 --- a/tests/util/test_faim.py +++ b/tests/util/test_faim.py @@ -62,6 +62,95 @@ def write_minimal_n88_model_with_constraints(path, coords, constraints): value[:] = [0.0] * len(node_numbers) +def write_minimal_hexahedron_n88_model(path): + netCDF4 = pytest.importorskip("netCDF4") + + coords = [ + [-0.25, -0.25, -0.25], + [0.25, -0.25, -0.25], + [0.25, 0.25, -0.25], + [-0.25, 0.25, -0.25], + [-0.25, -0.25, 0.25], + [0.25, -0.25, 0.25], + [0.25, 0.25, 0.25], + [-0.25, 0.25, 0.25], + [2.75, -0.25, -0.25], + [3.25, -0.25, -0.25], + [3.25, 0.25, -0.25], + [2.75, 0.25, -0.25], + [2.75, -0.25, 0.25], + [3.25, -0.25, 0.25], + [3.25, 0.25, 0.25], + [2.75, 0.25, 0.25], + ] + + with netCDF4.Dataset(str(path), "w") as root: + parts = root.createGroup("Parts") + part = parts.createGroup("Part1") + part.createDimension("NumberOfNodes", len(coords)) + part.createDimension("NumberOfCoordinates", 3) + coordinates = part.createVariable( + "NodeCoordinates", + "f8", + ("NumberOfNodes", "NumberOfCoordinates"), + ) + coordinates[:] = coords + + elements = part.createGroup("Elements") + hexahedrons = elements.createGroup("Hexahedrons") + hexahedrons.createDimension("NumberOfElements", 2) + hexahedrons.createDimension("NumberOfNodesPerElement", 8) + node_numbers = hexahedrons.createVariable( + "NodeNumbers", + "i4", + ("NumberOfElements", "NumberOfNodesPerElement"), + ) + node_numbers[:] = [[1, 2, 3, 4, 5, 6, 7, 8], [9, 10, 11, 12, 13, 14, 15, 16]] + material_id = hexahedrons.createVariable("MaterialID", "i4", ("NumberOfElements",)) + material_id[:] = [1, 2] + + +def read_hexahedron_material_ids(path): + netCDF4 = pytest.importorskip("netCDF4") + + with netCDF4.Dataset(str(path), "r") as root: + return list( + root.groups["Parts"] + .groups["Part1"] + .groups["Elements"] + .groups["Hexahedrons"] + .variables["MaterialID"][:] + ) + + +def write_mask_image(path): + np = pytest.importorskip("numpy") + sitk = pytest.importorskip("SimpleITK") + + mask = np.zeros((1, 1, 4), dtype=np.uint8) + mask[0, 0, 0] = 1 + image = sitk.GetImageFromArray(mask) + image.SetOrigin((0.0, 0.0, 0.0)) + image.SetSpacing((1.0, 1.0, 1.0)) + sitk.WriteImage(image, str(path)) + + +def test_temporary_pistoia_material_mask_restores_model_materials(tmp_path): + model_file = tmp_path / "model.n88model" + mask_file = tmp_path / "femoral_neck.nii.gz" + write_minimal_hexahedron_n88_model(model_file) + write_mask_image(mask_file) + + with faim.temporary_pistoia_material_mask( + model_file, mask_file, exclude_material_id=5000 + ) as stats: + assert stats["selected_elements"] == 1 + assert stats["temporarily_excluded_elements"] == 1 + assert read_hexahedron_material_ids(model_file) == [1, 5000] + + assert read_hexahedron_material_ids(model_file) == [1, 2] + + def test_resolve_faim_command_uses_bin_64bit(tmp_path): install_root = tmp_path / "Faim 9.0" bin_dir = install_root / "bin" / "64bit" From e0a5964964e8e3154c0d14106b944cdfe50cf3cf Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:28:53 -0600 Subject: [PATCH 24/67] Use post-ICP flat femur crop by default --- ogo/cli/GenerateFEM.py | 14 +- ogo/fea/femur.py | 228 +++++++++++++++++- .../fixtures/fe_workflow_defaults_locked.json | 11 +- tests/fea/test_femur.py | 57 +++++ tests/fea/test_workflow_locked_defaults.py | 1 + 5 files changed, 295 insertions(+), 16 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index e5bc377..3b4ad1f 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -650,9 +650,15 @@ def write_modeling_metadata( "crop_stage": ( "after isotropic resampling and before ICP" if femur_cut_mode in {"bbox_ratio", "proximal_box_ratio"} + else "fixed rough crop before ICP, final flat ratio crop after ICP" + if femur_cut_mode == "post_icp_flat_ratio" else "after ICP on the generated model grid" ), "fixed_length_mm": option_float(generator_argv, "--femur_shaft_length", 100.0), + "rough_pre_icp_crop": { + "enabled": femur_cut_mode == "post_icp_flat_ratio", + "retained_length_mm": option_float(generator_argv, "--femur_shaft_length", 100.0), + }, "lesser_trochanter_distal_offset_mm": option_float( generator_argv, "--femur_lesser_trochanter_distal_offset", @@ -676,6 +682,8 @@ def write_modeling_metadata( "cut_plane": ( "post-resample pre-ICP crop face transformed with the femur" if femur_cut_mode in {"bbox_ratio", "proximal_box_ratio"} + else "flat post-ICP aligned-frame crop face" + if femur_cut_mode == "post_icp_flat_ratio" else "flat model-grid z plane" ), "incomplete_fov_behavior": "fail model generation", @@ -753,9 +761,11 @@ def write_modeling_metadata( "support_surface": ( "finite bbox-relative patch projected onto the transformed oblique shaft surface" if femur_cut_mode == "bbox_ratio" + else "post-ICP flat distal shaft crop face" + if femur_cut_mode == "post_icp_flat_ratio" else "flat distal shaft cut face" ), - "relative_to": "model_bbox" if femur_cut_mode == "bbox_ratio" else None, + "relative_to": "model_grid" if femur_cut_mode == "post_icp_flat_ratio" else "model_bbox" if femur_cut_mode == "bbox_ratio" else None, "center_fraction": ( list(DISTAL_SHAFT_FIXTURE_CENTER_FRACTION) if femur_cut_mode == "bbox_ratio" @@ -769,6 +779,8 @@ def write_modeling_metadata( "normal_source": ( "transformed input bbox-ratio crop face" if femur_cut_mode == "bbox_ratio" + else "post-ICP flat crop face" + if femur_cut_mode == "post_icp_flat_ratio" else "model-grid distal z face" ), }, diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 79358c1..c1a245e 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -17,12 +17,12 @@ 3. Smooth the transformed femur mask with one binary close/open pass only when at least one input spacing dimension is coarser than 2 mm. If a compartment mask is supplied, the derived cortical binary mask follows the same rule. -4. Standardize the distal shaft after isotropic resampling and before - registration. The default uses a proximal max(x, y) transverse width - reference and retains 1.2 times that width along the proximal-distal axis. - The transformed crop face becomes the distal support surface. ``bbox_ratio``, - ``lesser_trochanter``, and ``fixed_length`` modes are available for - debugging or historical comparisons. +4. Stabilize registration with a fixed 100 mm proximal rough crop after + isotropic resampling, then standardize the distal shaft after ICP with a flat + aligned-frame crop that keeps z length at 1.2 times the aligned y width. + The post-ICP crop face becomes the distal support surface. ``bbox_ratio``, + ``proximal_box_ratio``, ``lesser_trochanter``, and ``fixed_length`` modes are + available for debugging or historical comparisons. 5. Generate two geometric PMMA fixtures from bbox-relative contact planes: a femoral-head loading fixture on the high-y side and a greater-trochanter contact fixture on the low-y side. Defaults are 10 mm PMMA thickness and @@ -86,8 +86,10 @@ DEFAULT_PMMA_INTRUSION_MM = 6.0 DEFAULT_FEMUR_INPUT_MARGIN_MM = DEFAULT_PMMA_THICKNESS_MM + DEFAULT_PMMA_INTRUSION_MM DEFAULT_FEMUR_SHAFT_LENGTH_MM = 100.0 -DEFAULT_FEMUR_CUT_MODE = "proximal_box_ratio" +DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM = 100.0 +DEFAULT_FEMUR_CUT_MODE = "post_icp_flat_ratio" PRE_ICP_CROP_MODES = {"bbox_ratio", "proximal_box_ratio"} +ROUGH_PRE_ICP_CROP_MODES = {"post_icp_flat_ratio"} DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM = 50.0 DEFAULT_CORTICAL_LABEL = 1 DEFAULT_TRABECULAR_LABEL = 2 @@ -702,6 +704,126 @@ def crop_vtk_images_to_proximal_box_ratio( ) +def crop_vtk_images_to_fixed_proximal_length( + vtk_images, + vtk_mask, + *, + retained_length_mm=DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM, + labels=None, +): + """Crop from the distal side to a fixed proximal-distal retained length.""" + import numpy as np + + from ogo.util.vtk_image import vtk_image_to_numpy + + mask_data = vtk_image_to_numpy(vtk_mask) + active = _active_crop_mask(mask_data, labels) + spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) + coords = np.argwhere(active) + lo = coords.min(axis=0).astype(np.int64) + hi = (coords.max(axis=0) + 1).astype(np.int64) + size = hi - lo + retained_length_mm = float(retained_length_mm) + if retained_length_mm <= 0.0: + raise ValueError("retained_length_mm must be positive.") + target_voxels = min( + int(size[2]), + max(1, int(round(retained_length_mm / float(spacing[2])))), + ) + status = "short" if int(size[2]) <= target_voxels else "cropped" + keep = np.zeros(active.shape, dtype=bool) + out_lo = lo.copy() + out_hi = hi.copy() + if status == "short": + keep[tuple(slice(int(lo[axis]), int(hi[axis])) for axis in range(3))] = True + else: + out_lo[2] = int(hi[2]) - target_voxels + keep[ + int(out_lo[0]) : int(out_hi[0]), + int(out_lo[1]) : int(out_hi[1]), + int(out_lo[2]) : int(out_hi[2]), + ] = True + cropped_images, crop_face_image, meta = _crop_vtk_images_with_keep_mask( + vtk_images, + vtk_mask, + active=active, + keep=keep, + meta={ + "enabled": True, + "method": "fixed_proximal_length", + "retained_length_mm": float(target_voxels) * float(spacing[2]), + "requested_retained_length_mm": retained_length_mm, + "status": status, + "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), + "rough_pre_icp": True, + }, + ) + return cropped_images, crop_face_image, meta + + +def crop_vtk_images_to_flat_post_icp_ratio( + vtk_images, + vtk_mask, + *, + ratio=DEFAULT_FEMUR_EXPERIMENTAL_RATIO, + labels=None, +): + """Apply a flat aligned-frame distal crop using z length over y width.""" + import numpy as np + + from ogo.util.vtk_image import vtk_image_to_numpy + + mask_data = vtk_image_to_numpy(vtk_mask) + active = _active_crop_mask(mask_data, labels) + spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) + coords = np.argwhere(active) + lo = coords.min(axis=0).astype(np.int64) + hi = (coords.max(axis=0) + 1).astype(np.int64) + size = hi - lo + y_width_mm = float(size[1]) * float(spacing[1]) + z_length_mm = float(size[2]) * float(spacing[2]) + ratio = float(ratio) + if ratio <= 0.0: + raise ValueError("ratio must be positive.") + target_length_mm = min(z_length_mm, ratio * y_width_mm) + target_voxels = min( + int(size[2]), + max(1, int(round(target_length_mm / float(spacing[2])))), + ) + status = "short" if int(size[2]) <= target_voxels else "cropped" + keep = np.zeros(active.shape, dtype=bool) + out_lo = lo.copy() + out_hi = hi.copy() + if status == "short": + keep[tuple(slice(int(lo[axis]), int(hi[axis])) for axis in range(3))] = True + else: + out_lo[2] = int(hi[2]) - target_voxels + keep[ + int(out_lo[0]) : int(out_hi[0]), + int(out_lo[1]) : int(out_hi[1]), + int(out_lo[2]) : int(out_hi[2]), + ] = True + cropped_images, crop_face_image, meta = _crop_vtk_images_with_keep_mask( + vtk_images, + vtk_mask, + active=active, + keep=keep, + meta={ + "enabled": True, + "method": "post_icp_flat_ratio", + "ratio": ratio, + "reference_axis": "y", + "reference_width_mm": y_width_mm, + "input_z_length_mm": z_length_mm, + "target_length_mm": float(target_voxels) * float(spacing[2]), + "status": status, + "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), + "crop_stage": "after ICP on aligned reference grid", + }, + ) + return cropped_images, crop_face_image, meta + + def _active_crop_mask(mask_data, labels): import numpy as np @@ -1876,10 +1998,12 @@ def sidewaysFallFe(args): if femur_cut_mode == "bbox_ratio": ogo.message("Femur BBox Ratio [reference, constrained, free]: %s" % str(femur_bbox_ratio)) ogo.message("Femur BBox Crop From [reference, constrained, free]: %s" % str(femur_bbox_crop_from)) - if femur_cut_mode == "proximal_box_ratio": + if femur_cut_mode in {"proximal_box_ratio", "post_icp_flat_ratio"}: ogo.message("Femur Proximal Box Crop Ratio: %8.4f" % femur_experimental_crop_ratio) ogo.message("Femur Proximal Reference Distance [mm]: %8.4f" % femur_proximal_reference_distance) ogo.message("Femur Proximal Reference Width: %s" % femur_proximal_reference_width) + if femur_cut_mode == "post_icp_flat_ratio": + ogo.message("Femur Rough Pre-ICP Retained Length [mm]: %8.4f" % femur_shaft_length) if compartment_mask is not None: ogo.message("Compartment Mask: %s" % compartment_mask) ogo.message("Cortical Label: %d" % cortical_label) @@ -1974,6 +2098,30 @@ def sidewaysFallFe(args): bbox_crop_meta["crop_face_voxels"], ) ) + elif femur_cut_mode in ROUGH_PRE_ICP_CROP_MODES: + ogo.message( + "Applying fixed-length rough femur crop after isotropic resampling and before ICP..." + ) + images_to_crop = [imageData, maskThres] + ([compartmentData] if compartmentData is not None else []) + cropped_images, _rough_crop_face, bbox_crop_meta = crop_vtk_images_to_fixed_proximal_length( + images_to_crop, + maskThres, + retained_length_mm=femur_shaft_length, + labels={1}, + ) + imageData = cropped_images[0] + maskThres = cropped_images[1] + if compartmentData is not None: + compartmentData = cropped_images[2] + ogo.message( + "rough pre-ICP crop slices xyz=%s; output shape xyz=%s; retained length=%8.4f; status=%s." + % ( + bbox_crop_meta["crop_slices_xyz"], + bbox_crop_meta["output_shape_xyz"], + bbox_crop_meta["retained_length_mm"], + bbox_crop_meta["status"], + ) + ) registration_mask = maskThres images_to_pad = [imageData, maskThres] @@ -2154,6 +2302,34 @@ def sidewaysFallFe(args): "Transformed %s mask z coverage [%8.4f, %8.4f]; retained z-span=%8.4f" % (femur_cut_mode, mask_z_min, mask_z_max, retained_length_mm) ) + elif femur_cut_mode == "post_icp_flat_ratio": + ogo.message("Applying flat post-ICP femur crop in aligned reference coordinates...") + images_to_crop = [image_trans, mask_trans] + ([compartment_trans] if compartment_trans is not None else []) + try: + cropped_images, distal_crop_face_trans, shaft_crop = crop_vtk_images_to_flat_post_icp_ratio( + images_to_crop, + mask_trans, + ratio=femur_experimental_crop_ratio, + labels={1}, + ) + except ValueError as exc: + ogo.message(str(exc)) + sys.exit(1) + image_trans = cropped_images[0] + mask_trans = cropped_images[1] + if compartment_trans is not None: + compartment_trans = cropped_images[2] + retained_length_mm = shaft_crop["target_length_mm"] + shaft_crop["pre_icp_crop"] = bbox_crop_meta + ogo.message( + "Post-ICP flat crop retained z=%8.4f from y width=%8.4f; slices xyz=%s; status=%s." + % ( + retained_length_mm, + shaft_crop["reference_width_mm"], + shaft_crop["crop_slices_xyz"], + shaft_crop["status"], + ) + ) else: ogo.message("Applying flat distal femur crop in reference coordinates...") try: @@ -2254,6 +2430,8 @@ def sidewaysFallFe(args): distal_crop_face_change = ogo.applyMask(distal_crop_face_change, ogo.cast2unsignchar(change)) if femur_cut_mode in PRE_ICP_CROP_MODES: ogo.message("Skipping model-grid distal shaft cut; pre-ICP crop face is already transformed.") + elif femur_cut_mode == "post_icp_flat_ratio": + ogo.message("Skipping model-grid distal shaft cut; post-ICP crop face is already on the model grid.") else: model_z_min, model_z_max = femur_z_coverage(change) model_cut_z = model_z_max - retained_length_mm @@ -2576,6 +2754,36 @@ def sidewaysFallFe(args): if df_visible_node_IDS.GetNumberOfTuples() == 0: ogo.message("No distal femur nodes found on the bbox-relative oblique shaft support surface.") sys.exit(1) + elif femur_cut_mode == "post_icp_flat_ratio": + if distal_crop_face_change is None: + ogo.message("post_icp_flat_ratio cut mode requires a distal crop-face mask.") + sys.exit(1) + distal_plane = crop_face_contact_plane(distal_crop_face_change, change) + ogo.message( + "Distal Femur post-ICP flat support plane: center=%s normal=%s outward=%s size=%s" + % ( + distal_plane["center"], + distal_plane["normal"], + distal_plane["outward_normal"], + distal_plane["size"], + ) + ) + distal_surface = projected_crop_face_surface_vtk( + change, + distal_plane, + intrusion=pmma_intrusion, + output_value=1, + ) + df_visible_node_IDS = interface_node_ids_from_voxel_mask( + model2, + distal_surface, + change, + name=DISTAL_FEMUR_NODE_SET, + direction=distal_plane["normal"], + ) + if df_visible_node_IDS.GetNumberOfTuples() == 0: + ogo.message("No distal femur nodes found on the post-ICP flat shaft support surface.") + sys.exit(1) else: distal_cut_z = model2_bounds[4] ogo.message("Distal Femur Cut Plane z: %8.4f" % distal_cut_z) @@ -2714,9 +2922,9 @@ def main(): help="Sets the applied displacement endpoint for the sideways-fall model. The default reports the force at 4%% displacement. (default: %(default)s)") parser.add_argument("--femur_shaft_length", type=float, default=DEFAULT_FEMUR_SHAFT_LENGTH_MM, help="Retained proximal femur length [mm] for --femur_cut_mode fixed_length. (default: %(default)s [mm])") - parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "proximal_box_ratio", "lesser_trochanter", "fixed_length"], + parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "proximal_box_ratio", "post_icp_flat_ratio", "lesser_trochanter", "fixed_length"], default=DEFAULT_FEMUR_CUT_MODE, - help="Set the distal femur crop. Pre-ICP crop modes run after isotropic resampling and before ICP; the transformed crop face is used for distal support. (default: %(default)s)") + help="Set the distal femur crop. post_icp_flat_ratio uses a fixed rough pre-ICP crop, then a flat aligned-frame final ratio crop after ICP. (default: %(default)s)") parser.add_argument("--femur_bbox_ratio", nargs=3, default=DEFAULT_FEMUR_BBOX_RATIO, help="BBox-ratio crop in recipe order: reference constrained free. Use 'none' for a free axis. (default: %(default)s)") parser.add_argument("--femur_bbox_crop_from", nargs=3, default=DEFAULT_FEMUR_BBOX_CROP_FROM, diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index 19b417d..ca029f4 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -13,21 +13,21 @@ null ], "cortical_label": 1, - "cut_mode": "proximal_box_ratio", + "cut_mode": "post_icp_flat_ratio", "distal_shaft_center_fraction": [ 0.5, 0.5, -0.1 ], - "distal_shaft_size_fraction": [ - 1.0, - 1.0 - ], "distal_shaft_normal": [ 0.0, -0.2588190451025207, 0.9659258262890683 ], + "distal_shaft_size_fraction": [ + 1.0, + 1.0 + ], "fe_displacement_mm": -4.0, "femoral_head_center_fraction": [ 0.5, @@ -68,6 +68,7 @@ 0.75 ], "right_femur_code": 2, + "rough_pre_icp_length_mm": 100.0, "shaft_length_mm": 100.0, "target_displacement_percent": 4.0, "trabecular_label": 2 diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 44e9d66..655d420 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -450,6 +450,63 @@ def test_proximal_box_ratio_crop_uses_proximal_transverse_width(): assert vtk_image_to_numpy(crop_face).sum() > 0 +def test_fixed_proximal_length_crop_keeps_proximal_side(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + density = np.zeros((20, 20, 180), dtype=np.float32) + mask = np.zeros_like(density, dtype=np.uint8) + density[5:15, 5:15, 10:170] = 700.0 + mask[5:15, 5:15, 10:170] = 2 + + cropped, crop_face, meta = femur.crop_vtk_images_to_fixed_proximal_length( + [_vtk_image_from_array(density)], + _vtk_image_from_array(mask), + retained_length_mm=100.0, + labels={2}, + ) + + cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 + coords = np.argwhere(cropped_mask) + assert meta["method"] == "fixed_proximal_length" + assert meta["retained_length_mm"] == pytest.approx(100.0) + assert meta["status"] == "cropped" + assert coords[:, 2].min() == 0 + assert coords[:, 2].max() == 99 + assert cropped[0].GetOrigin()[2] == pytest.approx(70.0) + assert vtk_image_to_numpy(crop_face).sum() > 0 + + +def test_post_icp_flat_ratio_crop_uses_y_width_for_z_length(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + density = np.zeros((32, 80, 160), dtype=np.float32) + mask = np.zeros_like(density, dtype=np.uint8) + density[5:25, 10:70, 20:150] = 700.0 + mask[5:25, 10:70, 20:150] = 2 + + cropped, crop_face, meta = femur.crop_vtk_images_to_flat_post_icp_ratio( + [_vtk_image_from_array(density)], + _vtk_image_from_array(mask), + ratio=1.2, + labels={2}, + ) + + cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 + coords = np.argwhere(cropped_mask) + final_size = coords.max(axis=0) - coords.min(axis=0) + 1 + + assert meta["method"] == "post_icp_flat_ratio" + assert meta["reference_width_mm"] == pytest.approx(60.0) + assert meta["target_length_mm"] == pytest.approx(72.0) + assert meta["status"] == "cropped" + assert final_size[1] == 60 + assert final_size[2] == 72 + assert final_size[2] / final_size[1] == pytest.approx(1.2) + assert vtk_image_to_numpy(crop_face).sum() > 0 + + def test_crop_face_support_vector_points_away_from_retained_bone(): np = pytest.importorskip("numpy") diff --git a/tests/fea/test_workflow_locked_defaults.py b/tests/fea/test_workflow_locked_defaults.py index d462e96..ea0a531 100644 --- a/tests/fea/test_workflow_locked_defaults.py +++ b/tests/fea/test_workflow_locked_defaults.py @@ -78,6 +78,7 @@ def _locked_workflow_defaults(): "input_margin_mm": femur.DEFAULT_FEMUR_INPUT_MARGIN_MM, "cut_mode": femur.DEFAULT_FEMUR_CUT_MODE, "shaft_length_mm": femur.DEFAULT_FEMUR_SHAFT_LENGTH_MM, + "rough_pre_icp_length_mm": femur.DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM, "lesser_trochanter_distal_offset_mm": femur.DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, "cortical_label": femur.DEFAULT_CORTICAL_LABEL, "trabecular_label": femur.DEFAULT_TRABECULAR_LABEL, From d21beef4e769b7bc5096e5a204556ff3eb484759 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:09:48 -0600 Subject: [PATCH 25/67] Set hip rough pre-ICP crop to 120 mm --- ogo/cli/GenerateFEM.py | 4 ++-- ogo/fea/femur.py | 4 ++-- tests/fea/fixtures/fe_workflow_defaults_locked.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 3b4ad1f..d48a047 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -654,10 +654,10 @@ def write_modeling_metadata( if femur_cut_mode == "post_icp_flat_ratio" else "after ICP on the generated model grid" ), - "fixed_length_mm": option_float(generator_argv, "--femur_shaft_length", 100.0), + "fixed_length_mm": option_float(generator_argv, "--femur_shaft_length", 120.0), "rough_pre_icp_crop": { "enabled": femur_cut_mode == "post_icp_flat_ratio", - "retained_length_mm": option_float(generator_argv, "--femur_shaft_length", 100.0), + "retained_length_mm": option_float(generator_argv, "--femur_shaft_length", 120.0), }, "lesser_trochanter_distal_offset_mm": option_float( generator_argv, diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index c1a245e..b9105cf 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -85,8 +85,8 @@ DEFAULT_PMMA_THICKNESS_MM = 10.0 DEFAULT_PMMA_INTRUSION_MM = 6.0 DEFAULT_FEMUR_INPUT_MARGIN_MM = DEFAULT_PMMA_THICKNESS_MM + DEFAULT_PMMA_INTRUSION_MM -DEFAULT_FEMUR_SHAFT_LENGTH_MM = 100.0 -DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM = 100.0 +DEFAULT_FEMUR_SHAFT_LENGTH_MM = 120.0 +DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM = 120.0 DEFAULT_FEMUR_CUT_MODE = "post_icp_flat_ratio" PRE_ICP_CROP_MODES = {"bbox_ratio", "proximal_box_ratio"} ROUGH_PRE_ICP_CROP_MODES = {"post_icp_flat_ratio"} diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index ca029f4..e86258e 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -68,8 +68,8 @@ 0.75 ], "right_femur_code": 2, - "rough_pre_icp_length_mm": 100.0, - "shaft_length_mm": 100.0, + "rough_pre_icp_length_mm": 120.0, + "shaft_length_mm": 120.0, "target_displacement_percent": 4.0, "trabecular_label": 2 }, From 80f87769274988403c1f6923a4e06417903d44e2 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:20:12 -0600 Subject: [PATCH 26/67] Use straight distal support for post-ICP hip crop --- ogo/cli/GenerateFEM.py | 10 ++- ogo/fea/femur.py | 70 +++++++++++++++---- .../fixtures/fe_workflow_defaults_locked.json | 1 + tests/fea/test_femur.py | 22 ++++++ tests/fea/test_workflow_locked_defaults.py | 1 + 5 files changed, 89 insertions(+), 15 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index d48a047..0403f34 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -38,6 +38,7 @@ DEFAULT_PMMA_INTRUSION_MM, DEFAULT_PMMA_THICKNESS_MM, DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, + POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, @@ -761,7 +762,7 @@ def write_modeling_metadata( "support_surface": ( "finite bbox-relative patch projected onto the transformed oblique shaft surface" if femur_cut_mode == "bbox_ratio" - else "post-ICP flat distal shaft crop face" + else "central 90% straight patch on the post-ICP flat distal shaft crop face" if femur_cut_mode == "post_icp_flat_ratio" else "flat distal shaft cut face" ), @@ -776,10 +777,15 @@ def write_modeling_metadata( if femur_cut_mode == "bbox_ratio" else None ), + "support_fraction": ( + POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION + if femur_cut_mode == "post_icp_flat_ratio" + else None + ), "normal_source": ( "transformed input bbox-ratio crop face" if femur_cut_mode == "bbox_ratio" - else "post-ICP flat crop face" + else "model-grid distal z direction" if femur_cut_mode == "post_icp_flat_ratio" else "model-grid distal z face" ), diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index b9105cf..0322f8b 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -75,6 +75,7 @@ DISTAL_SHAFT_FIXTURE_CENTER_FRACTION = (0.5, 0.5, -0.1) DISTAL_SHAFT_FIXTURE_SIZE_FRACTION = (1.0, 1.0) DISTAL_SHAFT_FIXTURE_NORMAL = (0.0, -0.2588190451025207, 0.9659258262890683) +POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION = 0.9 DEFAULT_FEMUR_BBOX_RATIO = (1.0, 1.3, None) DEFAULT_FEMUR_BBOX_CROP_FROM = (None, "max", None) DEFAULT_FEMUR_EXPERIMENTAL_RATIO = 1.2 @@ -1329,6 +1330,54 @@ def projected_crop_face_surface_vtk(material_vtk, plane, *, intrusion=0.0, outpu return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) +def straight_crop_face_support_surface_vtk( + crop_face_vtk, + material_vtk, + *, + support_fraction=POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, + output_value=1, +): + """Return a central straight support patch on a flat distal crop face.""" + import numpy as np + import vtk + + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + + face = vtk_image_to_numpy(crop_face_vtk) != 0 + active = vtk_image_to_numpy(material_vtk) != 0 + out = np.zeros(face.shape, dtype=np.uint8) + if not np.any(face) or not np.any(active): + return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) + fraction = float(support_fraction) + if not (0.0 < fraction <= 1.0): + raise ValueError("support_fraction must be in (0, 1].") + + face = face & active + if not np.any(face): + return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) + + coords = np.argwhere(face) + lo = coords.min(axis=0) + hi = coords.max(axis=0) + 1 + keep_lo = lo.copy() + keep_hi = hi.copy() + for axis in (0, 1): + width = int(hi[axis] - lo[axis]) + target = max(1, min(width, int(np.floor(width * fraction)))) + margin = max(0, (width - target) // 2) + keep_lo[axis] = int(lo[axis]) + margin + keep_hi[axis] = int(keep_lo[axis]) + target + + keep = np.zeros(face.shape, dtype=bool) + keep[ + int(keep_lo[0]) : int(keep_hi[0]), + int(keep_lo[1]) : int(keep_hi[1]), + int(lo[2]) : int(hi[2]), + ] = True + out[face & keep] = int(output_value) + return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) + + def mirror_polydata_x(polydata): """Return a left/right mirrored copy of polydata around its x-bounds center.""" import vtk @@ -2758,20 +2807,15 @@ def sidewaysFallFe(args): if distal_crop_face_change is None: ogo.message("post_icp_flat_ratio cut mode requires a distal crop-face mask.") sys.exit(1) - distal_plane = crop_face_contact_plane(distal_crop_face_change, change) + distal_support_direction = (0.0, 0.0, 1.0) ogo.message( - "Distal Femur post-ICP flat support plane: center=%s normal=%s outward=%s size=%s" - % ( - distal_plane["center"], - distal_plane["normal"], - distal_plane["outward_normal"], - distal_plane["size"], - ) + "Distal Femur post-ICP straight support patch: fraction=%8.4f direction=%s" + % (POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, distal_support_direction) ) - distal_surface = projected_crop_face_surface_vtk( + distal_surface = straight_crop_face_support_surface_vtk( + distal_crop_face_change, change, - distal_plane, - intrusion=pmma_intrusion, + support_fraction=POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, output_value=1, ) df_visible_node_IDS = interface_node_ids_from_voxel_mask( @@ -2779,10 +2823,10 @@ def sidewaysFallFe(args): distal_surface, change, name=DISTAL_FEMUR_NODE_SET, - direction=distal_plane["normal"], + direction=distal_support_direction, ) if df_visible_node_IDS.GetNumberOfTuples() == 0: - ogo.message("No distal femur nodes found on the post-ICP flat shaft support surface.") + ogo.message("No distal femur nodes found on the post-ICP straight shaft support patch.") sys.exit(1) else: distal_cut_z = model2_bounds[4] diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index e86258e..11d8109 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -28,6 +28,7 @@ 1.0, 1.0 ], + "post_icp_distal_shaft_support_fraction": 0.9, "fe_displacement_mm": -4.0, "femoral_head_center_fraction": [ 0.5, diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 655d420..6809abe 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -578,6 +578,28 @@ def test_projected_crop_face_surface_mask_keeps_first_contact_layer(): assert not np.any(surface_data[:, :, 5:]) +def test_straight_crop_face_support_uses_central_fraction_on_flat_face(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + active = np.zeros((12, 12, 8), dtype=np.uint8) + active[1:11, 1:11, 3:7] = 1 + crop_face = np.zeros_like(active) + crop_face[1:11, 1:11, 3] = 1 + + surface = femur.straight_crop_face_support_surface_vtk( + _vtk_image_from_array(crop_face), + _vtk_image_from_array(active), + support_fraction=0.9, + ) + surface_data = vtk_image_to_numpy(surface) != 0 + + assert surface_data.sum() == 9 * 9 + assert np.all(surface_data[1:10, 1:10, 3]) + assert not np.any(surface_data[:, :, :3]) + assert not np.any(surface_data[:, :, 4:]) + + def test_flat_femur_shaft_crop_rejects_short_distal_coverage(): np = pytest.importorskip("numpy") vtk = pytest.importorskip("vtk") diff --git a/tests/fea/test_workflow_locked_defaults.py b/tests/fea/test_workflow_locked_defaults.py index ea0a531..7846609 100644 --- a/tests/fea/test_workflow_locked_defaults.py +++ b/tests/fea/test_workflow_locked_defaults.py @@ -68,6 +68,7 @@ def _locked_workflow_defaults(): femur.DISTAL_SHAFT_FIXTURE_SIZE_FRACTION ), "distal_shaft_normal": list(femur.DISTAL_SHAFT_FIXTURE_NORMAL), + "post_icp_distal_shaft_support_fraction": femur.POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, "bbox_ratio": list(femur.DEFAULT_FEMUR_BBOX_RATIO), "bbox_crop_from": list(femur.DEFAULT_FEMUR_BBOX_CROP_FROM), "proximal_reference_distance_mm": femur.DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, From da15dc62c4e0a3801de9296ea9887c9dc10c0eb0 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:40:10 -0600 Subject: [PATCH 27/67] Use oblique post-ICP femur ratio crop --- ogo/cli/GenerateFEM.py | 20 +- ogo/fea/femur.py | 219 ++++++++++++++++-- .../fixtures/fe_workflow_defaults_locked.json | 2 +- tests/fea/test_femur.py | 65 ++++++ 4 files changed, 280 insertions(+), 26 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 0403f34..d4fd629 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -651,13 +651,15 @@ def write_modeling_metadata( "crop_stage": ( "after isotropic resampling and before ICP" if femur_cut_mode in {"bbox_ratio", "proximal_box_ratio"} + else "fixed rough crop before ICP, final oblique ratio crop after ICP" + if femur_cut_mode == "post_icp_oblique_ratio" else "fixed rough crop before ICP, final flat ratio crop after ICP" if femur_cut_mode == "post_icp_flat_ratio" else "after ICP on the generated model grid" ), "fixed_length_mm": option_float(generator_argv, "--femur_shaft_length", 120.0), "rough_pre_icp_crop": { - "enabled": femur_cut_mode == "post_icp_flat_ratio", + "enabled": femur_cut_mode in {"post_icp_flat_ratio", "post_icp_oblique_ratio"}, "retained_length_mm": option_float(generator_argv, "--femur_shaft_length", 120.0), }, "lesser_trochanter_distal_offset_mm": option_float( @@ -683,6 +685,8 @@ def write_modeling_metadata( "cut_plane": ( "post-resample pre-ICP crop face transformed with the femur" if femur_cut_mode in {"bbox_ratio", "proximal_box_ratio"} + else "post-ICP oblique crop face following transformed rough pre-ICP crop face angle" + if femur_cut_mode == "post_icp_oblique_ratio" else "flat post-ICP aligned-frame crop face" if femur_cut_mode == "post_icp_flat_ratio" else "flat model-grid z plane" @@ -762,11 +766,21 @@ def write_modeling_metadata( "support_surface": ( "finite bbox-relative patch projected onto the transformed oblique shaft surface" if femur_cut_mode == "bbox_ratio" + else "projected patch on the post-ICP oblique distal shaft crop face" + if femur_cut_mode == "post_icp_oblique_ratio" else "central 90% straight patch on the post-ICP flat distal shaft crop face" if femur_cut_mode == "post_icp_flat_ratio" else "flat distal shaft cut face" ), - "relative_to": "model_grid" if femur_cut_mode == "post_icp_flat_ratio" else "model_bbox" if femur_cut_mode == "bbox_ratio" else None, + "relative_to": ( + "transformed_rough_crop_face" + if femur_cut_mode == "post_icp_oblique_ratio" + else "model_grid" + if femur_cut_mode == "post_icp_flat_ratio" + else "model_bbox" + if femur_cut_mode == "bbox_ratio" + else None + ), "center_fraction": ( list(DISTAL_SHAFT_FIXTURE_CENTER_FRACTION) if femur_cut_mode == "bbox_ratio" @@ -785,6 +799,8 @@ def write_modeling_metadata( "normal_source": ( "transformed input bbox-ratio crop face" if femur_cut_mode == "bbox_ratio" + else "transformed rough pre-ICP crop face shifted to final ratio target" + if femur_cut_mode == "post_icp_oblique_ratio" else "model-grid distal z direction" if femur_cut_mode == "post_icp_flat_ratio" else "model-grid distal z face" diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 0322f8b..3c590df 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -17,12 +17,13 @@ 3. Smooth the transformed femur mask with one binary close/open pass only when at least one input spacing dimension is coarser than 2 mm. If a compartment mask is supplied, the derived cortical binary mask follows the same rule. -4. Stabilize registration with a fixed 100 mm proximal rough crop after - isotropic resampling, then standardize the distal shaft after ICP with a flat - aligned-frame crop that keeps z length at 1.2 times the aligned y width. - The post-ICP crop face becomes the distal support surface. ``bbox_ratio``, - ``proximal_box_ratio``, ``lesser_trochanter``, and ``fixed_length`` modes are - available for debugging or historical comparisons. +4. Stabilize registration with a fixed 120 mm proximal rough crop after + isotropic resampling, then standardize the distal shaft after ICP with an + oblique crop that keeps z length at 1.2 times the aligned y width while + following the transformed rough-crop face angle. The post-ICP crop face + becomes the distal support surface. ``bbox_ratio``, ``proximal_box_ratio``, + ``post_icp_flat_ratio``, ``lesser_trochanter``, and ``fixed_length`` modes + are available for debugging or historical comparisons. 5. Generate two geometric PMMA fixtures from bbox-relative contact planes: a femoral-head loading fixture on the high-y side and a greater-trochanter contact fixture on the low-y side. Defaults are 10 mm PMMA thickness and @@ -88,9 +89,9 @@ DEFAULT_FEMUR_INPUT_MARGIN_MM = DEFAULT_PMMA_THICKNESS_MM + DEFAULT_PMMA_INTRUSION_MM DEFAULT_FEMUR_SHAFT_LENGTH_MM = 120.0 DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM = 120.0 -DEFAULT_FEMUR_CUT_MODE = "post_icp_flat_ratio" +DEFAULT_FEMUR_CUT_MODE = "post_icp_oblique_ratio" PRE_ICP_CROP_MODES = {"bbox_ratio", "proximal_box_ratio"} -ROUGH_PRE_ICP_CROP_MODES = {"post_icp_flat_ratio"} +ROUGH_PRE_ICP_CROP_MODES = {"post_icp_flat_ratio", "post_icp_oblique_ratio"} DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM = 50.0 DEFAULT_CORTICAL_LABEL = 1 DEFAULT_TRABECULAR_LABEL = 2 @@ -825,6 +826,129 @@ def crop_vtk_images_to_flat_post_icp_ratio( return cropped_images, crop_face_image, meta +def crop_vtk_images_to_oblique_post_icp_ratio( + vtk_images, + vtk_mask, + transformed_crop_face, + *, + ratio=DEFAULT_FEMUR_EXPERIMENTAL_RATIO, + labels=None, +): + """Apply a post-ICP distal crop using the transformed rough-crop face angle.""" + import numpy as np + import vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + mask_data = vtk_image_to_numpy(vtk_mask) + active = _active_crop_mask(mask_data, labels) + spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) + origin = np.asarray(vtk_mask.GetOrigin(), dtype=np.float64) + coords = np.argwhere(active) + lo = coords.min(axis=0).astype(np.int64) + hi = (coords.max(axis=0) + 1).astype(np.int64) + size = hi - lo + y_width_mm = float(size[1]) * float(spacing[1]) + z_length_mm = float(size[2]) * float(spacing[2]) + ratio = float(ratio) + if ratio <= 0.0: + raise ValueError("ratio must be positive.") + target_length_mm = min(z_length_mm, ratio * y_width_mm) + target_voxels = min( + int(size[2]), + max(1, int(round(target_length_mm / float(spacing[2])))), + ) + status = "short" if int(size[2]) <= target_voxels else "cropped" + cut_z_mm = float(origin[2]) + float(int(hi[2]) - target_voxels) * float(spacing[2]) + + transformed_face_data = vtk_image_to_numpy(transformed_crop_face) != 0 + if not np.any(transformed_face_data & active): + raise ValueError("Cannot apply oblique post-ICP crop from an empty transformed rough crop face.") + + if status == "short": + keep = np.zeros(active.shape, dtype=bool) + keep[ + int(lo[0]) : int(hi[0]), + int(lo[1]) : int(hi[1]), + int(lo[2]) : int(hi[2]), + ] = True + cropped_images, _unused_crop_face, meta = _crop_vtk_images_with_keep_mask( + vtk_images, + vtk_mask, + active=active, + keep=keep, + meta={ + "enabled": True, + "method": "post_icp_oblique_ratio", + "ratio": ratio, + "reference_axis": "y", + "reference_width_mm": y_width_mm, + "input_z_length_mm": z_length_mm, + "target_length_mm": float(target_voxels) * float(spacing[2]), + "target_cut_z_mm": cut_z_mm, + "status": status, + "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), + "crop_stage": "after ICP using transformed rough-crop face angle", + "source_plane_center": None, + "source_plane_normal": None, + }, + ) + slices = tuple( + slice(int(start), int(stop)) + for start, stop in meta["crop_slices_xyz"] + ) + crop_face = (transformed_face_data & active)[slices].astype(np.uint8) + crop_face_image = _vtk_image_from_array( + crop_face, + vtk_mask, + origin=meta["output_origin"], + vtk_array_type=vtk.VTK_UNSIGNED_CHAR, + ) + meta["crop_face_voxels"] = int(crop_face.sum()) + return cropped_images, crop_face_image, meta + + plane = crop_face_contact_plane(transformed_crop_face, vtk_mask) + normal = _unit_vector(plane["normal"], "post-ICP oblique crop normal") + if abs(float(normal[2])) < 1.0e-6: + raise ValueError("Transformed rough crop face is nearly vertical in model z.") + if float(normal[2]) < 0.0: + normal = -normal + center = np.asarray(plane["center"], dtype=np.float64) + shifted_center = center + normal * ((cut_z_mm - float(center[2])) / float(normal[2])) + + active_indices = np.argwhere(active) + active_points = _image_index_points(vtk_mask, active_indices) + distances = (active_points - shifted_center) @ normal + tolerance = max(float(np.min(spacing)) * 0.75, 1.0e-6) + keep_values = distances >= -tolerance + keep = np.zeros(active.shape, dtype=bool) + keep[tuple(active_indices[keep_values].T)] = True + cropped_images, crop_face_image, meta = _crop_vtk_images_with_keep_mask( + vtk_images, + vtk_mask, + active=active, + keep=keep, + meta={ + "enabled": True, + "method": "post_icp_oblique_ratio", + "ratio": ratio, + "reference_axis": "y", + "reference_width_mm": y_width_mm, + "input_z_length_mm": z_length_mm, + "target_length_mm": float(target_voxels) * float(spacing[2]), + "target_cut_z_mm": cut_z_mm, + "status": status, + "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), + "crop_stage": "after ICP using transformed rough-crop face angle", + "plane_center": tuple(float(value) for value in shifted_center), + "plane_normal": tuple(float(value) for value in normal), + "source_plane_center": tuple(float(value) for value in plane["center"]), + "source_plane_normal": tuple(float(value) for value in plane["normal"]), + }, + ) + return cropped_images, crop_face_image, meta + + def _active_crop_mask(mask_data, labels): import numpy as np @@ -2047,11 +2171,12 @@ def sidewaysFallFe(args): if femur_cut_mode == "bbox_ratio": ogo.message("Femur BBox Ratio [reference, constrained, free]: %s" % str(femur_bbox_ratio)) ogo.message("Femur BBox Crop From [reference, constrained, free]: %s" % str(femur_bbox_crop_from)) - if femur_cut_mode in {"proximal_box_ratio", "post_icp_flat_ratio"}: - ogo.message("Femur Proximal Box Crop Ratio: %8.4f" % femur_experimental_crop_ratio) + if femur_cut_mode in {"proximal_box_ratio", "post_icp_flat_ratio", "post_icp_oblique_ratio"}: + ogo.message("Femur Crop Ratio: %8.4f" % femur_experimental_crop_ratio) + if femur_cut_mode == "proximal_box_ratio": ogo.message("Femur Proximal Reference Distance [mm]: %8.4f" % femur_proximal_reference_distance) ogo.message("Femur Proximal Reference Width: %s" % femur_proximal_reference_width) - if femur_cut_mode == "post_icp_flat_ratio": + if femur_cut_mode in {"post_icp_flat_ratio", "post_icp_oblique_ratio"}: ogo.message("Femur Rough Pre-ICP Retained Length [mm]: %8.4f" % femur_shaft_length) if compartment_mask is not None: ogo.message("Compartment Mask: %s" % compartment_mask) @@ -2152,12 +2277,14 @@ def sidewaysFallFe(args): "Applying fixed-length rough femur crop after isotropic resampling and before ICP..." ) images_to_crop = [imageData, maskThres] + ([compartmentData] if compartmentData is not None else []) - cropped_images, _rough_crop_face, bbox_crop_meta = crop_vtk_images_to_fixed_proximal_length( + cropped_images, rough_crop_face, bbox_crop_meta = crop_vtk_images_to_fixed_proximal_length( images_to_crop, maskThres, retained_length_mm=femur_shaft_length, labels={1}, ) + if femur_cut_mode == "post_icp_oblique_ratio": + distal_crop_face = rough_crop_face imageData = cropped_images[0] maskThres = cropped_images[1] if compartmentData is not None: @@ -2351,16 +2478,31 @@ def sidewaysFallFe(args): "Transformed %s mask z coverage [%8.4f, %8.4f]; retained z-span=%8.4f" % (femur_cut_mode, mask_z_min, mask_z_max, retained_length_mm) ) - elif femur_cut_mode == "post_icp_flat_ratio": - ogo.message("Applying flat post-ICP femur crop in aligned reference coordinates...") + elif femur_cut_mode in {"post_icp_flat_ratio", "post_icp_oblique_ratio"}: + if femur_cut_mode == "post_icp_oblique_ratio": + ogo.message("Applying oblique post-ICP femur crop from transformed rough-crop face...") + if distal_crop_face_trans is None: + ogo.message("post_icp_oblique_ratio cut mode requires a transformed rough crop-face mask.") + sys.exit(1) + else: + ogo.message("Applying flat post-ICP femur crop in aligned reference coordinates...") images_to_crop = [image_trans, mask_trans] + ([compartment_trans] if compartment_trans is not None else []) try: - cropped_images, distal_crop_face_trans, shaft_crop = crop_vtk_images_to_flat_post_icp_ratio( - images_to_crop, - mask_trans, - ratio=femur_experimental_crop_ratio, - labels={1}, - ) + if femur_cut_mode == "post_icp_oblique_ratio": + cropped_images, distal_crop_face_trans, shaft_crop = crop_vtk_images_to_oblique_post_icp_ratio( + images_to_crop, + mask_trans, + distal_crop_face_trans, + ratio=femur_experimental_crop_ratio, + labels={1}, + ) + else: + cropped_images, distal_crop_face_trans, shaft_crop = crop_vtk_images_to_flat_post_icp_ratio( + images_to_crop, + mask_trans, + ratio=femur_experimental_crop_ratio, + labels={1}, + ) except ValueError as exc: ogo.message(str(exc)) sys.exit(1) @@ -2371,8 +2513,9 @@ def sidewaysFallFe(args): retained_length_mm = shaft_crop["target_length_mm"] shaft_crop["pre_icp_crop"] = bbox_crop_meta ogo.message( - "Post-ICP flat crop retained z=%8.4f from y width=%8.4f; slices xyz=%s; status=%s." + "Post-ICP %s crop retained z=%8.4f from y width=%8.4f; slices xyz=%s; status=%s." % ( + "oblique" if femur_cut_mode == "post_icp_oblique_ratio" else "flat", retained_length_mm, shaft_crop["reference_width_mm"], shaft_crop["crop_slices_xyz"], @@ -2803,6 +2946,36 @@ def sidewaysFallFe(args): if df_visible_node_IDS.GetNumberOfTuples() == 0: ogo.message("No distal femur nodes found on the bbox-relative oblique shaft support surface.") sys.exit(1) + elif femur_cut_mode == "post_icp_oblique_ratio": + if distal_crop_face_change is None: + ogo.message("post_icp_oblique_ratio cut mode requires a distal crop-face mask.") + sys.exit(1) + distal_plane = crop_face_contact_plane(distal_crop_face_change, change) + ogo.message( + "Distal Femur post-ICP oblique support plane: center=%s normal=%s outward=%s size=%s" + % ( + distal_plane["center"], + distal_plane["normal"], + distal_plane["outward_normal"], + distal_plane["size"], + ) + ) + distal_surface = projected_crop_face_surface_vtk( + change, + distal_plane, + intrusion=pmma_intrusion, + output_value=1, + ) + df_visible_node_IDS = interface_node_ids_from_voxel_mask( + model2, + distal_surface, + change, + name=DISTAL_FEMUR_NODE_SET, + direction=distal_plane["normal"], + ) + if df_visible_node_IDS.GetNumberOfTuples() == 0: + ogo.message("No distal femur nodes found on the post-ICP oblique shaft support surface.") + sys.exit(1) elif femur_cut_mode == "post_icp_flat_ratio": if distal_crop_face_change is None: ogo.message("post_icp_flat_ratio cut mode requires a distal crop-face mask.") @@ -2966,9 +3139,9 @@ def main(): help="Sets the applied displacement endpoint for the sideways-fall model. The default reports the force at 4%% displacement. (default: %(default)s)") parser.add_argument("--femur_shaft_length", type=float, default=DEFAULT_FEMUR_SHAFT_LENGTH_MM, help="Retained proximal femur length [mm] for --femur_cut_mode fixed_length. (default: %(default)s [mm])") - parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "proximal_box_ratio", "post_icp_flat_ratio", "lesser_trochanter", "fixed_length"], + parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "proximal_box_ratio", "post_icp_oblique_ratio", "post_icp_flat_ratio", "lesser_trochanter", "fixed_length"], default=DEFAULT_FEMUR_CUT_MODE, - help="Set the distal femur crop. post_icp_flat_ratio uses a fixed rough pre-ICP crop, then a flat aligned-frame final ratio crop after ICP. (default: %(default)s)") + help="Set the distal femur crop. post_icp_oblique_ratio uses a fixed rough pre-ICP crop, then an angled final ratio crop after ICP following the transformed rough-crop face. post_icp_flat_ratio keeps the flat aligned-frame comparison path. (default: %(default)s)") parser.add_argument("--femur_bbox_ratio", nargs=3, default=DEFAULT_FEMUR_BBOX_RATIO, help="BBox-ratio crop in recipe order: reference constrained free. Use 'none' for a free axis. (default: %(default)s)") parser.add_argument("--femur_bbox_crop_from", nargs=3, default=DEFAULT_FEMUR_BBOX_CROP_FROM, diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index 11d8109..fb95e2d 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -13,7 +13,7 @@ null ], "cortical_label": 1, - "cut_mode": "post_icp_flat_ratio", + "cut_mode": "post_icp_oblique_ratio", "distal_shaft_center_fraction": [ 0.5, 0.5, diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 6809abe..715559f 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -507,6 +507,71 @@ def test_post_icp_flat_ratio_crop_uses_y_width_for_z_length(): assert vtk_image_to_numpy(crop_face).sum() > 0 +def test_post_icp_oblique_ratio_crop_uses_transformed_crop_face_angle(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + density = np.zeros((32, 80, 160), dtype=np.float32) + mask = np.zeros_like(density, dtype=np.uint8) + rough_face = np.zeros_like(mask) + for x in range(5, 25): + for y in range(10, 70): + z = 20 + (x - 5) // 5 + density[x, y, z:150] = 700.0 + mask[x, y, z:150] = 2 + rough_face[x, y, z] = 1 + + cropped, crop_face, meta = femur.crop_vtk_images_to_oblique_post_icp_ratio( + [_vtk_image_from_array(density)], + _vtk_image_from_array(mask), + _vtk_image_from_array(rough_face), + ratio=1.2, + labels={2}, + ) + + cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 + crop_face_mask = vtk_image_to_numpy(crop_face) != 0 + crop_face_coords = np.argwhere(crop_face_mask) + + assert meta["method"] == "post_icp_oblique_ratio" + assert meta["reference_width_mm"] == pytest.approx(60.0) + assert meta["target_length_mm"] == pytest.approx(72.0) + assert meta["status"] == "cropped" + assert meta["plane_normal"][2] > 0.8 + assert crop_face_coords[:, 2].max() > crop_face_coords[:, 2].min() + assert cropped_mask.sum() > 0 + assert crop_face_mask.sum() > 0 + + +def test_post_icp_oblique_ratio_short_case_keeps_transformed_crop_face(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + density = np.zeros((20, 40, 50), dtype=np.float32) + mask = np.zeros_like(density, dtype=np.uint8) + rough_face = np.zeros_like(mask) + density[4:16, 5:35, 12:42] = 700.0 + mask[4:16, 5:35, 12:42] = 2 + rough_face[4:16, 5:35, 12] = 1 + + cropped, crop_face, meta = femur.crop_vtk_images_to_oblique_post_icp_ratio( + [_vtk_image_from_array(density)], + _vtk_image_from_array(mask), + _vtk_image_from_array(rough_face), + ratio=1.2, + labels={2}, + ) + + cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 + crop_face_mask = vtk_image_to_numpy(crop_face) != 0 + + assert meta["method"] == "post_icp_oblique_ratio" + assert meta["status"] == "short" + assert cropped_mask.sum() == np.count_nonzero(mask) + assert crop_face_mask.sum() == np.count_nonzero(rough_face) + assert np.unique(np.argwhere(crop_face_mask)[:, 2]).tolist() == [0] + + def test_crop_face_support_vector_points_away_from_retained_bone(): np = pytest.importorskip("numpy") From 5b75fbdec55fe392b5fe680b73cb5d87b6122b8f Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:53:30 -0600 Subject: [PATCH 28/67] Allow transformed femur crop face voxel offset --- ogo/fea/femur.py | 2 +- tests/fea/test_femur.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 3c590df..9e19aac 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -862,7 +862,7 @@ def crop_vtk_images_to_oblique_post_icp_ratio( cut_z_mm = float(origin[2]) + float(int(hi[2]) - target_voxels) * float(spacing[2]) transformed_face_data = vtk_image_to_numpy(transformed_crop_face) != 0 - if not np.any(transformed_face_data & active): + if not np.any(transformed_face_data): raise ValueError("Cannot apply oblique post-ICP crop from an empty transformed rough crop face.") if status == "short": diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 715559f..f03db64 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -572,6 +572,31 @@ def test_post_icp_oblique_ratio_short_case_keeps_transformed_crop_face(): assert np.unique(np.argwhere(crop_face_mask)[:, 2]).tolist() == [0] +def test_post_icp_oblique_ratio_allows_resampled_face_offset_from_mask(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + density = np.zeros((32, 80, 160), dtype=np.float32) + mask = np.zeros_like(density, dtype=np.uint8) + rough_face = np.zeros_like(mask) + density[5:25, 10:70, 20:150] = 700.0 + mask[5:25, 10:70, 20:150] = 2 + rough_face[5:25, 10:70, 19] = 1 + + cropped, crop_face, meta = femur.crop_vtk_images_to_oblique_post_icp_ratio( + [_vtk_image_from_array(density)], + _vtk_image_from_array(mask), + _vtk_image_from_array(rough_face), + ratio=1.2, + labels={2}, + ) + + assert meta["method"] == "post_icp_oblique_ratio" + assert meta["status"] == "cropped" + assert vtk_image_to_numpy(cropped[0]).sum() > 0 + assert vtk_image_to_numpy(crop_face).sum() > 0 + + def test_crop_face_support_vector_points_away_from_retained_bone(): np = pytest.importorskip("numpy") From 76a7137fb809e59188ae1db96bd0f92b6af99837 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:57:03 -0600 Subject: [PATCH 29/67] Preserve rough femur crop face through ICP --- ogo/fea/femur.py | 35 ++++++++++++++++++++++++++++++++++- tests/fea/test_femur.py | 22 ++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 9e19aac..bfb8420 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -949,6 +949,35 @@ def crop_vtk_images_to_oblique_post_icp_ratio( return cropped_images, crop_face_image, meta +def thicken_z_crop_face_into_mask(crop_face_vtk, mask_vtk, *, thickness_voxels=3): + """Return a thin mask-constrained z slab so a crop face survives resampling.""" + import numpy as np + import vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + face = vtk_image_to_numpy(crop_face_vtk) != 0 + active = vtk_image_to_numpy(mask_vtk) != 0 + out = np.zeros(face.shape, dtype=np.uint8) + coords = np.argwhere(face) + if coords.size == 0: + return _vtk_image_from_array( + out, + crop_face_vtk, + origin=crop_face_vtk.GetOrigin(), + vtk_array_type=vtk.VTK_UNSIGNED_CHAR, + ) + z0 = int(coords[:, 2].min()) + z1 = min(active.shape[2], z0 + max(1, int(thickness_voxels))) + out[:, :, z0:z1] = active[:, :, z0:z1].astype(np.uint8) + return _vtk_image_from_array( + out, + crop_face_vtk, + origin=crop_face_vtk.GetOrigin(), + vtk_array_type=vtk.VTK_UNSIGNED_CHAR, + ) + + def _active_crop_mask(mask_data, labels): import numpy as np @@ -2284,7 +2313,11 @@ def sidewaysFallFe(args): labels={1}, ) if femur_cut_mode == "post_icp_oblique_ratio": - distal_crop_face = rough_crop_face + distal_crop_face = thicken_z_crop_face_into_mask( + rough_crop_face, + cropped_images[1], + thickness_voxels=3, + ) imageData = cropped_images[0] maskThres = cropped_images[1] if compartmentData is not None: diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index f03db64..9395142 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -597,6 +597,28 @@ def test_post_icp_oblique_ratio_allows_resampled_face_offset_from_mask(): assert vtk_image_to_numpy(crop_face).sum() > 0 +def test_thicken_z_crop_face_into_mask_returns_mask_constrained_slab(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + mask = np.zeros((8, 8, 8), dtype=np.uint8) + mask[2:6, 2:6, 1:7] = 1 + face = np.zeros_like(mask) + face[2:6, 2:6, 1] = 1 + + slab = femur.thicken_z_crop_face_into_mask( + _vtk_image_from_array(face), + _vtk_image_from_array(mask), + thickness_voxels=3, + ) + slab_data = vtk_image_to_numpy(slab) != 0 + + assert slab_data.sum() == 4 * 4 * 3 + assert np.all(slab_data[2:6, 2:6, 1:4]) + assert not np.any(slab_data[:, :, 0]) + assert not np.any(slab_data[:, :, 4:]) + + def test_crop_face_support_vector_points_away_from_retained_bone(): np = pytest.importorskip("numpy") From 32caaeaf19d55c0792a510f5e9487b4b126cc461 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:00:10 -0600 Subject: [PATCH 30/67] Use distal slab for short femur oblique crop --- ogo/fea/femur.py | 14 ++++++++------ tests/fea/test_femur.py | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index bfb8420..44f16c0 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -961,12 +961,14 @@ def thicken_z_crop_face_into_mask(crop_face_vtk, mask_vtk, *, thickness_voxels=3 out = np.zeros(face.shape, dtype=np.uint8) coords = np.argwhere(face) if coords.size == 0: - return _vtk_image_from_array( - out, - crop_face_vtk, - origin=crop_face_vtk.GetOrigin(), - vtk_array_type=vtk.VTK_UNSIGNED_CHAR, - ) + coords = np.argwhere(active) + if coords.size == 0: + return _vtk_image_from_array( + out, + crop_face_vtk, + origin=crop_face_vtk.GetOrigin(), + vtk_array_type=vtk.VTK_UNSIGNED_CHAR, + ) z0 = int(coords[:, 2].min()) z1 = min(active.shape[2], z0 + max(1, int(thickness_voxels))) out[:, :, z0:z1] = active[:, :, z0:z1].astype(np.uint8) diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 9395142..32ed56c 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -619,6 +619,27 @@ def test_thicken_z_crop_face_into_mask_returns_mask_constrained_slab(): assert not np.any(slab_data[:, :, 4:]) +def test_thicken_z_crop_face_falls_back_to_distal_mask_slice(): + np = pytest.importorskip("numpy") + from ogo.util.vtk_image import vtk_image_to_numpy + + mask = np.zeros((8, 8, 8), dtype=np.uint8) + mask[2:6, 2:6, 2:7] = 1 + empty_face = np.zeros_like(mask) + + slab = femur.thicken_z_crop_face_into_mask( + _vtk_image_from_array(empty_face), + _vtk_image_from_array(mask), + thickness_voxels=2, + ) + slab_data = vtk_image_to_numpy(slab) != 0 + + assert slab_data.sum() == 4 * 4 * 2 + assert np.all(slab_data[2:6, 2:6, 2:4]) + assert not np.any(slab_data[:, :, :2]) + assert not np.any(slab_data[:, :, 4:]) + + def test_crop_face_support_vector_points_away_from_retained_bone(): np = pytest.importorskip("numpy") From e66fd1f7f9d588c48dce7a6af0cd10002e3ad16f Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 15:01:48 -0600 Subject: [PATCH 31/67] Use stored oblique femur crop plane for support --- ogo/fea/femur.py | 18 +++++++++++++++--- tests/fea/test_femur.py | 4 ++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 44f16c0..4c519ff 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -942,6 +942,10 @@ def crop_vtk_images_to_oblique_post_icp_ratio( "crop_stage": "after ICP using transformed rough-crop face angle", "plane_center": tuple(float(value) for value in shifted_center), "plane_normal": tuple(float(value) for value in normal), + "plane_u_axis": tuple(float(value) for value in plane["u_axis"]), + "plane_v_axis": tuple(float(value) for value in plane["v_axis"]), + "plane_size": tuple(float(value) for value in plane["size"]), + "plane_shape": plane.get("shape", "anatomy"), "source_plane_center": tuple(float(value) for value in plane["center"]), "source_plane_normal": tuple(float(value) for value in plane["normal"]), }, @@ -2982,10 +2986,18 @@ def sidewaysFallFe(args): ogo.message("No distal femur nodes found on the bbox-relative oblique shaft support surface.") sys.exit(1) elif femur_cut_mode == "post_icp_oblique_ratio": - if distal_crop_face_change is None: - ogo.message("post_icp_oblique_ratio cut mode requires a distal crop-face mask.") + if not shaft_crop or not shaft_crop.get("plane_center"): + ogo.message("post_icp_oblique_ratio cut mode requires stored oblique crop-plane metadata.") sys.exit(1) - distal_plane = crop_face_contact_plane(distal_crop_face_change, change) + distal_plane = { + "center": shaft_crop["plane_center"], + "normal": shaft_crop["plane_normal"], + "outward_normal": tuple(float(-value) for value in shaft_crop["plane_normal"]), + "u_axis": shaft_crop["plane_u_axis"], + "v_axis": shaft_crop["plane_v_axis"], + "size": shaft_crop["plane_size"], + "shape": shaft_crop.get("plane_shape", "anatomy"), + } ogo.message( "Distal Femur post-ICP oblique support plane: center=%s normal=%s outward=%s size=%s" % ( diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 32ed56c..abf6d1b 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -538,6 +538,10 @@ def test_post_icp_oblique_ratio_crop_uses_transformed_crop_face_angle(): assert meta["target_length_mm"] == pytest.approx(72.0) assert meta["status"] == "cropped" assert meta["plane_normal"][2] > 0.8 + assert meta["plane_u_axis"] is not None + assert meta["plane_v_axis"] is not None + assert meta["plane_size"][0] > 0 + assert meta["plane_size"][1] > 0 assert crop_face_coords[:, 2].max() > crop_face_coords[:, 2].min() assert cropped_mask.sum() > 0 assert crop_face_mask.sum() > 0 From d38fb012eb6f04f67062302ecaeb5b34c4c02780 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:02:56 -0600 Subject: [PATCH 32/67] Use flat post-ICP femur crop by default --- ogo/fea/femur.py | 17 ++++++++++------- .../fixtures/fe_workflow_defaults_locked.json | 2 +- tests/fea/test_femur.py | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 4c519ff..a291e93 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -89,7 +89,7 @@ DEFAULT_FEMUR_INPUT_MARGIN_MM = DEFAULT_PMMA_THICKNESS_MM + DEFAULT_PMMA_INTRUSION_MM DEFAULT_FEMUR_SHAFT_LENGTH_MM = 120.0 DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM = 120.0 -DEFAULT_FEMUR_CUT_MODE = "post_icp_oblique_ratio" +DEFAULT_FEMUR_CUT_MODE = "post_icp_flat_ratio" PRE_ICP_CROP_MODES = {"bbox_ratio", "proximal_box_ratio"} ROUGH_PRE_ICP_CROP_MODES = {"post_icp_flat_ratio", "post_icp_oblique_ratio"} DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM = 50.0 @@ -787,12 +787,15 @@ def crop_vtk_images_to_flat_post_icp_ratio( ratio = float(ratio) if ratio <= 0.0: raise ValueError("ratio must be positive.") - target_length_mm = min(z_length_mm, ratio * y_width_mm) - target_voxels = min( - int(size[2]), - max(1, int(round(target_length_mm / float(spacing[2])))), - ) - status = "short" if int(size[2]) <= target_voxels else "cropped" + target_length_mm = ratio * y_width_mm + target_voxels = max(1, int(round(target_length_mm / float(spacing[2])))) + if int(size[2]) <= target_voxels: + raise ValueError( + "Post-ICP flat femur crop is too short for the requested aspect ratio: " + "input z length %.4f mm, required %.4f mm for ratio %.4f." + % (z_length_mm, target_length_mm, ratio) + ) + status = "cropped" keep = np.zeros(active.shape, dtype=bool) out_lo = lo.copy() out_hi = hi.copy() diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index fb95e2d..11d8109 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -13,7 +13,7 @@ null ], "cortical_label": 1, - "cut_mode": "post_icp_oblique_ratio", + "cut_mode": "post_icp_flat_ratio", "distal_shaft_center_fraction": [ 0.5, 0.5, diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index abf6d1b..8b6cecf 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -507,6 +507,23 @@ def test_post_icp_flat_ratio_crop_uses_y_width_for_z_length(): assert vtk_image_to_numpy(crop_face).sum() > 0 +def test_post_icp_flat_ratio_crop_rejects_short_femur(): + np = pytest.importorskip("numpy") + + density = np.zeros((32, 80, 90), dtype=np.float32) + mask = np.zeros_like(density, dtype=np.uint8) + density[5:25, 10:70, 20:80] = 700.0 + mask[5:25, 10:70, 20:80] = 2 + + with pytest.raises(ValueError, match="too short for the requested aspect ratio"): + femur.crop_vtk_images_to_flat_post_icp_ratio( + [_vtk_image_from_array(density)], + _vtk_image_from_array(mask), + ratio=1.2, + labels={2}, + ) + + def test_post_icp_oblique_ratio_crop_uses_transformed_crop_face_angle(): np = pytest.importorskip("numpy") from ogo.util.vtk_image import vtk_image_to_numpy From 6abbacf2e044e4c5e9d083068b244911be500050 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:36:46 -0600 Subject: [PATCH 33/67] Add GT-relative femur length study crop --- ogo/cli/GenerateFEM.py | 35 +++++-- ogo/fea/femur.py | 181 +++++++++++++++++++++++++++------- tests/cli/test_GenerateFEM.py | 93 +++++++++++++++++ tests/fea/test_femur.py | 78 +++++++++++++++ 4 files changed, 343 insertions(+), 44 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index d4fd629..551753a 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -31,6 +31,7 @@ DEFAULT_FEMUR_BBOX_CROP_FROM, DEFAULT_FEMUR_BBOX_RATIO, DEFAULT_FEMUR_CUT_MODE, + DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, DEFAULT_FEMUR_ISO_RESOLUTION_MM, DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, @@ -190,6 +191,9 @@ def option_value(argv: Sequence[str], option: str, default: Optional[str] = None """Return the final value for an option from an argv-style list.""" value = default for index, token in enumerate(argv): + prefix = option + "=" + if token.startswith(prefix): + value = token[len(prefix) :] if token == option and index + 1 < len(argv): value = argv[index + 1] return value @@ -336,7 +340,7 @@ def solve_model( critical_volume=args.critical_volume, critical_strain=args.critical_strain, exclude=args.exclude, - run_pistoia=False, + run_pistoia=args.run_pistoia or args.require_pistoia, applied_displacement=applied_displacement, target_displacement=profile["target_displacement_percent"], report_profile=profile["report_profile"], @@ -395,7 +399,7 @@ def write_modeling_metadata( "solve_requested": not args.no_solve, "target_displacement_percent": target_displacement_percent(args, model_type), "target_displacement_definition": "percent strain converted from model geometry for spine; percent of femur length for femur", - "run_pistoia": False, + "run_pistoia": args.run_pistoia or args.require_pistoia, "compress_solved_model": not args.no_compress, }, } @@ -655,12 +659,22 @@ def write_modeling_metadata( if femur_cut_mode == "post_icp_oblique_ratio" else "fixed rough crop before ICP, final flat ratio crop after ICP" if femur_cut_mode == "post_icp_flat_ratio" + else "fixed rough crop before ICP for registration, final GT-relative flat crop after ICP on the full scan" + if femur_cut_mode == "greater_trochanter_length" else "after ICP on the generated model grid" ), "fixed_length_mm": option_float(generator_argv, "--femur_shaft_length", 120.0), "rough_pre_icp_crop": { - "enabled": femur_cut_mode in {"post_icp_flat_ratio", "post_icp_oblique_ratio"}, + "enabled": femur_cut_mode in {"post_icp_flat_ratio", "post_icp_oblique_ratio", "greater_trochanter_length"}, "retained_length_mm": option_float(generator_argv, "--femur_shaft_length", 120.0), + "registration_only": femur_cut_mode == "greater_trochanter_length", + }, + "greater_trochanter_length": { + "retained_length_mm": option_float( + generator_argv, + "--femur_greater_trochanter_distal_length", + DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, + ), }, "lesser_trochanter_distal_offset_mm": option_float( generator_argv, @@ -689,6 +703,8 @@ def write_modeling_metadata( if femur_cut_mode == "post_icp_oblique_ratio" else "flat post-ICP aligned-frame crop face" if femur_cut_mode == "post_icp_flat_ratio" + else "flat post-ICP aligned-frame crop face at fixed distance below detected greater trochanter" + if femur_cut_mode == "greater_trochanter_length" else "flat model-grid z plane" ), "incomplete_fov_behavior": "fail model generation", @@ -769,14 +785,14 @@ def write_modeling_metadata( else "projected patch on the post-ICP oblique distal shaft crop face" if femur_cut_mode == "post_icp_oblique_ratio" else "central 90% straight patch on the post-ICP flat distal shaft crop face" - if femur_cut_mode == "post_icp_flat_ratio" + if femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"} else "flat distal shaft cut face" ), "relative_to": ( "transformed_rough_crop_face" if femur_cut_mode == "post_icp_oblique_ratio" else "model_grid" - if femur_cut_mode == "post_icp_flat_ratio" + if femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"} else "model_bbox" if femur_cut_mode == "bbox_ratio" else None @@ -793,7 +809,7 @@ def write_modeling_metadata( ), "support_fraction": ( POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION - if femur_cut_mode == "post_icp_flat_ratio" + if femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"} else None ), "normal_source": ( @@ -802,7 +818,7 @@ def write_modeling_metadata( else "transformed rough pre-ICP crop face shifted to final ratio target" if femur_cut_mode == "post_icp_oblique_ratio" else "model-grid distal z direction" - if femur_cut_mode == "post_icp_flat_ratio" + if femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"} else "model-grid distal z face" ), }, @@ -960,6 +976,11 @@ def _add_common_image_args(parser: argparse.ArgumentParser) -> None: action="store_true", help="Fail if Pistoia postprocessing fails.", ) + parser.add_argument( + "--run_pistoia", + action="store_true", + help="Run Pistoia postprocessing and include Pistoia metrics when available.", + ) parser.add_argument( "--no_compress", action="store_true", diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index a291e93..45ae9d6 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -18,12 +18,13 @@ at least one input spacing dimension is coarser than 2 mm. If a compartment mask is supplied, the derived cortical binary mask follows the same rule. 4. Stabilize registration with a fixed 120 mm proximal rough crop after - isotropic resampling, then standardize the distal shaft after ICP with an - oblique crop that keeps z length at 1.2 times the aligned y width while - following the transformed rough-crop face angle. The post-ICP crop face - becomes the distal support surface. ``bbox_ratio``, ``proximal_box_ratio``, - ``post_icp_flat_ratio``, ``lesser_trochanter``, and ``fixed_length`` modes - are available for debugging or historical comparisons. + isotropic resampling, then standardize the distal shaft after ICP with a + flat crop that keeps z length at 1.2 times the aligned y width. The post-ICP + crop face becomes the distal support surface. ``bbox_ratio``, + ``proximal_box_ratio``, ``post_icp_oblique_ratio``, + ``greater_trochanter_length``, ``lesser_trochanter``, and ``fixed_length`` + modes are available for debugging, sensitivity studies, or historical + comparisons. 5. Generate two geometric PMMA fixtures from bbox-relative contact planes: a femoral-head loading fixture on the high-y side and a greater-trochanter contact fixture on the low-y side. Defaults are 10 mm PMMA thickness and @@ -90,8 +91,10 @@ DEFAULT_FEMUR_SHAFT_LENGTH_MM = 120.0 DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM = 120.0 DEFAULT_FEMUR_CUT_MODE = "post_icp_flat_ratio" +DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM = 175.0 PRE_ICP_CROP_MODES = {"bbox_ratio", "proximal_box_ratio"} -ROUGH_PRE_ICP_CROP_MODES = {"post_icp_flat_ratio", "post_icp_oblique_ratio"} +ROUGH_PRE_ICP_CROP_MODES = {"post_icp_flat_ratio", "post_icp_oblique_ratio", "greater_trochanter_length"} +FULL_SCAN_AFTER_ROUGH_CROP_MODES = {"greater_trochanter_length"} DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM = 50.0 DEFAULT_CORTICAL_LABEL = 1 DEFAULT_TRABECULAR_LABEL = 2 @@ -799,15 +802,12 @@ def crop_vtk_images_to_flat_post_icp_ratio( keep = np.zeros(active.shape, dtype=bool) out_lo = lo.copy() out_hi = hi.copy() - if status == "short": - keep[tuple(slice(int(lo[axis]), int(hi[axis])) for axis in range(3))] = True - else: - out_lo[2] = int(hi[2]) - target_voxels - keep[ - int(out_lo[0]) : int(out_hi[0]), - int(out_lo[1]) : int(out_hi[1]), - int(out_lo[2]) : int(out_hi[2]), - ] = True + out_lo[2] = int(hi[2]) - target_voxels + keep[ + int(out_lo[0]) : int(out_hi[0]), + int(out_lo[1]) : int(out_hi[1]), + int(out_lo[2]) : int(out_hi[2]), + ] = True cropped_images, crop_face_image, meta = _crop_vtk_images_with_keep_mask( vtk_images, vtk_mask, @@ -829,6 +829,79 @@ def crop_vtk_images_to_flat_post_icp_ratio( return cropped_images, crop_face_image, meta +def crop_vtk_images_to_greater_trochanter_length( + vtk_images, + vtk_mask, + *, + retained_length_mm=DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, + labels=None, +): + """Apply a flat distal crop at a fixed distance below the detected GT level.""" + import numpy as np + + from ogo.util.vtk_image import vtk_image_to_numpy + + retained_length_mm = float(retained_length_mm) + if retained_length_mm <= 0.0: + raise ValueError("retained_length_mm must be positive.") + + mask_data = vtk_image_to_numpy(vtk_mask) + active = _active_crop_mask(mask_data, labels) + if not np.any(active): + raise ValueError("Cannot apply greater-trochanter crop to an empty femur mask.") + + spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) + origin = np.asarray(vtk_mask.GetOrigin(), dtype=np.float64) + coords = np.argwhere(active) + lo = coords.min(axis=0).astype(np.int64) + hi = (coords.max(axis=0) + 1).astype(np.int64) + y_width_mm = float(hi[1] - lo[1]) * float(spacing[1]) + z_length_mm = float(hi[2] - lo[2]) * float(spacing[2]) + + landmark = detect_lesser_trochanter_cut_z(vtk_mask, distal_offset_mm=0.0) + gt_z = float(landmark["greater_trochanter_z"]) + cut_z = gt_z - retained_length_mm + available_mm = gt_z - float(landmark["mask_z_min"]) + if available_mm < retained_length_mm: + raise ValueError( + "Femur scan is too short for the requested greater-trochanter distal length: " + "requested %.4f mm, available %.4f mm below GT." + % (retained_length_mm, max(0.0, available_mm)) + ) + + z_centers = origin[2] + np.arange(active.shape[2], dtype=np.float64) * spacing[2] + keep = active & (z_centers[None, None, :] >= cut_z) + if not np.any(keep): + raise ValueError("Greater-trochanter distal crop removed the entire femur mask.") + + cropped_images, crop_face_image, meta = _crop_vtk_images_with_keep_mask( + vtk_images, + vtk_mask, + active=active, + keep=keep, + meta={ + "enabled": True, + "method": "greater_trochanter_length", + "retained_length_mm": retained_length_mm, + "requested_retained_length_mm": retained_length_mm, + "available_below_gt_mm": float(available_mm), + "greater_trochanter_z": gt_z, + "lesser_trochanter_z": float(landmark["lesser_trochanter_z"]), + "cut_z_mm": float(cut_z), + "reference_axis": "greater_trochanter_z", + "reference_width_mm": y_width_mm, + "input_z_length_mm": z_length_mm, + "status": "cropped", + "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), + "crop_stage": "after ICP on aligned full-scan reference grid", + }, + ) + meta["aspect_ratio_z_over_y"] = ( + float(retained_length_mm) / float(y_width_mm) if y_width_mm > 0.0 else None + ) + return cropped_images, crop_face_image, meta + + def crop_vtk_images_to_oblique_post_icp_ratio( vtk_images, vtk_mask, @@ -2155,6 +2228,7 @@ def sidewaysFallFe(args): femur_bbox_ratio = args.femur_bbox_ratio femur_bbox_crop_from = args.femur_bbox_crop_from femur_experimental_crop_ratio = args.femur_experimental_crop_ratio + femur_greater_trochanter_distal_length = args.femur_greater_trochanter_distal_length femur_proximal_reference_distance = args.femur_proximal_reference_distance femur_proximal_reference_width = args.femur_proximal_reference_width femur_lesser_trochanter_distal_offset = args.femur_lesser_trochanter_distal_offset @@ -2216,6 +2290,12 @@ def sidewaysFallFe(args): ogo.message("Femur Proximal Reference Width: %s" % femur_proximal_reference_width) if femur_cut_mode in {"post_icp_flat_ratio", "post_icp_oblique_ratio"}: ogo.message("Femur Rough Pre-ICP Retained Length [mm]: %8.4f" % femur_shaft_length) + if femur_cut_mode == "greater_trochanter_length": + ogo.message("Femur Rough Pre-ICP Retained Length [mm]: %8.4f" % femur_shaft_length) + ogo.message( + "Femur Greater Trochanter Distal Length [mm]: %8.4f" + % femur_greater_trochanter_distal_length + ) if compartment_mask is not None: ogo.message("Compartment Mask: %s" % compartment_mask) ogo.message("Cortical Label: %d" % cortical_label) @@ -2301,6 +2381,7 @@ def sidewaysFallFe(args): maskThres = cropped_images[1] if compartmentData is not None: compartmentData = cropped_images[2] + registration_mask = maskThres ogo.message( "%s crop slices xyz=%s; output shape xyz=%s; crop face voxels=%d." % ( @@ -2327,10 +2408,12 @@ def sidewaysFallFe(args): cropped_images[1], thickness_voxels=3, ) - imageData = cropped_images[0] - maskThres = cropped_images[1] - if compartmentData is not None: - compartmentData = cropped_images[2] + registration_mask = cropped_images[1] + if femur_cut_mode not in FULL_SCAN_AFTER_ROUGH_CROP_MODES: + imageData = cropped_images[0] + maskThres = cropped_images[1] + if compartmentData is not None: + compartmentData = cropped_images[2] ogo.message( "rough pre-ICP crop slices xyz=%s; output shape xyz=%s; retained length=%8.4f; status=%s." % ( @@ -2340,7 +2423,8 @@ def sidewaysFallFe(args): bbox_crop_meta["status"], ) ) - registration_mask = maskThres + else: + registration_mask = maskThres images_to_pad = [imageData, maskThres] if distal_crop_face is not None: @@ -2520,12 +2604,14 @@ def sidewaysFallFe(args): "Transformed %s mask z coverage [%8.4f, %8.4f]; retained z-span=%8.4f" % (femur_cut_mode, mask_z_min, mask_z_max, retained_length_mm) ) - elif femur_cut_mode in {"post_icp_flat_ratio", "post_icp_oblique_ratio"}: + elif femur_cut_mode in {"post_icp_flat_ratio", "post_icp_oblique_ratio", "greater_trochanter_length"}: if femur_cut_mode == "post_icp_oblique_ratio": ogo.message("Applying oblique post-ICP femur crop from transformed rough-crop face...") if distal_crop_face_trans is None: ogo.message("post_icp_oblique_ratio cut mode requires a transformed rough crop-face mask.") sys.exit(1) + elif femur_cut_mode == "greater_trochanter_length": + ogo.message("Applying flat post-ICP femur crop at fixed distance below detected GT...") else: ogo.message("Applying flat post-ICP femur crop in aligned reference coordinates...") images_to_crop = [image_trans, mask_trans] + ([compartment_trans] if compartment_trans is not None else []) @@ -2538,6 +2624,13 @@ def sidewaysFallFe(args): ratio=femur_experimental_crop_ratio, labels={1}, ) + elif femur_cut_mode == "greater_trochanter_length": + cropped_images, distal_crop_face_trans, shaft_crop = crop_vtk_images_to_greater_trochanter_length( + images_to_crop, + mask_trans, + retained_length_mm=femur_greater_trochanter_distal_length, + labels={1}, + ) else: cropped_images, distal_crop_face_trans, shaft_crop = crop_vtk_images_to_flat_post_icp_ratio( images_to_crop, @@ -2552,18 +2645,30 @@ def sidewaysFallFe(args): mask_trans = cropped_images[1] if compartment_trans is not None: compartment_trans = cropped_images[2] - retained_length_mm = shaft_crop["target_length_mm"] + retained_length_mm = shaft_crop.get("target_length_mm", shaft_crop["retained_length_mm"]) shaft_crop["pre_icp_crop"] = bbox_crop_meta - ogo.message( - "Post-ICP %s crop retained z=%8.4f from y width=%8.4f; slices xyz=%s; status=%s." - % ( - "oblique" if femur_cut_mode == "post_icp_oblique_ratio" else "flat", - retained_length_mm, - shaft_crop["reference_width_mm"], - shaft_crop["crop_slices_xyz"], - shaft_crop["status"], + if femur_cut_mode == "greater_trochanter_length": + ogo.message( + "Post-ICP GT-length crop retained z=%8.4f below GT z=%8.4f; cut z=%8.4f; available=%8.4f; slices xyz=%s." + % ( + retained_length_mm, + shaft_crop["greater_trochanter_z"], + shaft_crop["cut_z_mm"], + shaft_crop["available_below_gt_mm"], + shaft_crop["crop_slices_xyz"], + ) + ) + else: + ogo.message( + "Post-ICP %s crop retained z=%8.4f from y width=%8.4f; slices xyz=%s; status=%s." + % ( + "oblique" if femur_cut_mode == "post_icp_oblique_ratio" else "flat", + retained_length_mm, + shaft_crop["reference_width_mm"], + shaft_crop["crop_slices_xyz"], + shaft_crop["status"], + ) ) - ) else: ogo.message("Applying flat distal femur crop in reference coordinates...") try: @@ -2664,7 +2769,7 @@ def sidewaysFallFe(args): distal_crop_face_change = ogo.applyMask(distal_crop_face_change, ogo.cast2unsignchar(change)) if femur_cut_mode in PRE_ICP_CROP_MODES: ogo.message("Skipping model-grid distal shaft cut; pre-ICP crop face is already transformed.") - elif femur_cut_mode == "post_icp_flat_ratio": + elif femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"}: ogo.message("Skipping model-grid distal shaft cut; post-ICP crop face is already on the model grid.") else: model_z_min, model_z_max = femur_z_coverage(change) @@ -3026,9 +3131,9 @@ def sidewaysFallFe(args): if df_visible_node_IDS.GetNumberOfTuples() == 0: ogo.message("No distal femur nodes found on the post-ICP oblique shaft support surface.") sys.exit(1) - elif femur_cut_mode == "post_icp_flat_ratio": + elif femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"}: if distal_crop_face_change is None: - ogo.message("post_icp_flat_ratio cut mode requires a distal crop-face mask.") + ogo.message("%s cut mode requires a distal crop-face mask." % femur_cut_mode) sys.exit(1) distal_support_direction = (0.0, 0.0, 1.0) ogo.message( @@ -3189,15 +3294,17 @@ def main(): help="Sets the applied displacement endpoint for the sideways-fall model. The default reports the force at 4%% displacement. (default: %(default)s)") parser.add_argument("--femur_shaft_length", type=float, default=DEFAULT_FEMUR_SHAFT_LENGTH_MM, help="Retained proximal femur length [mm] for --femur_cut_mode fixed_length. (default: %(default)s [mm])") - parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "proximal_box_ratio", "post_icp_oblique_ratio", "post_icp_flat_ratio", "lesser_trochanter", "fixed_length"], + parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "proximal_box_ratio", "post_icp_oblique_ratio", "post_icp_flat_ratio", "greater_trochanter_length", "lesser_trochanter", "fixed_length"], default=DEFAULT_FEMUR_CUT_MODE, - help="Set the distal femur crop. post_icp_oblique_ratio uses a fixed rough pre-ICP crop, then an angled final ratio crop after ICP following the transformed rough-crop face. post_icp_flat_ratio keeps the flat aligned-frame comparison path. (default: %(default)s)") + help="Set the distal femur crop. post_icp_oblique_ratio uses a fixed rough pre-ICP crop, then an angled final ratio crop after ICP following the transformed rough-crop face. post_icp_flat_ratio keeps the flat aligned-frame comparison path. greater_trochanter_length uses the rough crop only for ICP, then crops the transformed full scan to a fixed distance distal to the detected greater trochanter. (default: %(default)s)") parser.add_argument("--femur_bbox_ratio", nargs=3, default=DEFAULT_FEMUR_BBOX_RATIO, help="BBox-ratio crop in recipe order: reference constrained free. Use 'none' for a free axis. (default: %(default)s)") parser.add_argument("--femur_bbox_crop_from", nargs=3, default=DEFAULT_FEMUR_BBOX_CROP_FROM, help="BBox crop side in recipe order: reference constrained free. Values are min, max, center/null. (default: %(default)s)") parser.add_argument("--femur_experimental_crop_ratio", type=float, default=DEFAULT_FEMUR_EXPERIMENTAL_RATIO, help="Retained proximal femur length as a multiple of the proximal transverse max(x, y) width in proximal_box_ratio mode. (default: %(default)s)") + parser.add_argument("--femur_greater_trochanter_distal_length", type=float, default=DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, + help="Retained femur length [mm] distal to the detected greater-trochanter level in greater_trochanter_length mode. (default: %(default)s [mm])") parser.add_argument("--femur_proximal_reference_distance", type=float, default=DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, help="Proximal-most distance [mm] used to estimate transverse reference width in proximal_box_ratio mode. (default: %(default)s)") parser.add_argument("--femur_proximal_reference_width", choices=["max_xy", "rms_xy"], default=DEFAULT_FEMUR_PROXIMAL_REFERENCE_WIDTH, diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index bff12e5..762745c 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -301,6 +301,8 @@ def _metadata_args(tmp_path, no_solve=True): "critical_volume": 2.0, "critical_strain": 0.007, "no_compress": False, + "run_pistoia": False, + "require_pistoia": False, }, )() @@ -324,6 +326,7 @@ def _solve_args(**overrides): "critical_strain": 0.007, "exclude": 5000, "no_compress": False, + "run_pistoia": False, "require_pistoia": False, "dry_run": False, "target_displacement": None, @@ -390,6 +393,58 @@ def test_solve_model_sets_femur_displacement_from_percent(monkeypatch, tmp_path) assert calls[0]["solve_displacement_percent"] == 4.0 +def test_solve_model_can_enable_pistoia(monkeypatch, tmp_path): + import ogo.util.faim as faim_module + + calls = [] + monkeypatch.setattr(faim_module, "run_faim_pipeline", lambda **kwargs: calls.append(kwargs)) + + GenerateFEM.solve_model( + tmp_path / "density_LT_FEMUR_SF.n88model", + _solve_args(run_pistoia=True), + "hip", + ["density.nii.gz", "mask.nii.gz", "--fe_displacement", "-4.0"], + ) + + assert calls[0]["run_pistoia"] is True + assert calls[0]["require_pistoia"] is False + + +def test_require_pistoia_implies_pistoia_run(monkeypatch, tmp_path): + import ogo.util.faim as faim_module + + calls = [] + monkeypatch.setattr(faim_module, "run_faim_pipeline", lambda **kwargs: calls.append(kwargs)) + + GenerateFEM.solve_model( + tmp_path / "density_LT_FEMUR_SF.n88model", + _solve_args(require_pistoia=True), + "hip", + ["density.nii.gz", "mask.nii.gz", "--fe_displacement", "-4.0"], + ) + + assert calls[0]["run_pistoia"] is True + assert calls[0]["require_pistoia"] is True + + +def test_option_value_accepts_equals_form(): + assert ( + GenerateFEM.option_value( + ["--femur_cut_mode=greater_trochanter_length"], + "--femur_cut_mode", + ) + == "greater_trochanter_length" + ) + assert ( + GenerateFEM.option_float( + ["--femur_greater_trochanter_distal_length=175"], + "--femur_greater_trochanter_distal_length", + 120.0, + ) + == 175.0 + ) + + def test_spine_modeling_metadata_records_materials_and_bcs(tmp_path): model = tmp_path / "density_vertebra_2_L1.n88model" model.write_text("dummy") @@ -471,3 +526,41 @@ def test_femur_modeling_metadata_records_materials_shaft_and_bcs(tmp_path): assert data["boundary_conditions"]["constraints"][0]["target_displacement_percent"] == 4.0 assert data["boundary_conditions"]["constraints"][0]["value_source"].startswith("target_displacement_percent") assert data["solve_and_reporting"]["target_displacement_percent"] == 4.0 + + +def test_femur_modeling_metadata_records_gt_length_study_mode(tmp_path): + model = tmp_path / "density_LT_FEMUR_SF.n88model" + model.write_text("dummy") + argv = [ + "density.nii.gz", + "mask.nii.gz", + "--femur_side", + "1", + "--femur_cut_mode=greater_trochanter_length", + "--femur_greater_trochanter_distal_length=175", + "--femur_shaft_length=120", + ] + + path = GenerateFEM.write_modeling_metadata( + model, + "hip", + argv, + _metadata_args(tmp_path, no_solve=False), + ) + data = json.loads(path.read_text()) + + shaft = data["shaft_standardization"] + assert shaft["cut_mode"] == "greater_trochanter_length" + assert shaft["crop_stage"] == ( + "fixed rough crop before ICP for registration, final GT-relative flat crop after ICP on the full scan" + ) + assert shaft["rough_pre_icp_crop"] == { + "enabled": True, + "retained_length_mm": 120.0, + "registration_only": True, + } + assert shaft["greater_trochanter_length"]["retained_length_mm"] == 175.0 + assert shaft["cut_plane"] == ( + "flat post-ICP aligned-frame crop face at fixed distance below detected greater trochanter" + ) + assert data["boundary_conditions"]["fixture_geometry"]["distal_shaft"]["support_fraction"] == 0.9 diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 8b6cecf..c166e6d 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -916,6 +916,84 @@ def test_lesser_trochanter_cut_uses_distal_area_peak(): assert meta["cut_z"] == pytest.approx(61) +def test_greater_trochanter_length_crop_uses_detected_gt_level(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from ogo.util.vtk_image import vtk_image_to_numpy + from vtk.util.numpy_support import numpy_to_vtk + + data = np.zeros((60, 80, 90), dtype=np.uint8) + x = np.arange(data.shape[0])[:, None] + y = np.arange(data.shape[1])[None, :] + for z in range(10, 86): + radius = 10 + y_center = 35 + if 58 <= z <= 64: + radius = 19 + if 73 <= z <= 79: + y_center = 50 + radius = 12 + section = ((x - 30) ** 2 + (y - y_center) ** 2) <= radius**2 + data[:, :, z] = section.astype(np.uint8) + + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(0, 0, 0) + image.SetSpacing(1, 1, 1) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + + cropped, crop_face, meta = femur.crop_vtk_images_to_greater_trochanter_length( + [image], + image, + retained_length_mm=30.0, + labels={1}, + ) + + cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 + assert meta["method"] == "greater_trochanter_length" + assert meta["greater_trochanter_z"] == pytest.approx(76) + assert meta["cut_z_mm"] == pytest.approx(46) + assert meta["available_below_gt_mm"] == pytest.approx(66) + assert meta["retained_length_mm"] == pytest.approx(30) + assert cropped[0].GetOrigin()[2] == pytest.approx(46) + assert np.argwhere(cropped_mask)[:, 2].min() == 0 + assert vtk_image_to_numpy(crop_face).sum() > 0 + + +def test_greater_trochanter_length_crop_rejects_short_femur(): + np = pytest.importorskip("numpy") + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + + data = np.zeros((60, 80, 90), dtype=np.uint8) + x = np.arange(data.shape[0])[:, None] + y = np.arange(data.shape[1])[None, :] + for z in range(10, 86): + radius = 10 + y_center = 35 + if 58 <= z <= 64: + radius = 19 + if 73 <= z <= 79: + y_center = 50 + radius = 12 + section = ((x - 30) ** 2 + (y - y_center) ** 2) <= radius**2 + data[:, :, z] = section.astype(np.uint8) + + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(0, 0, 0) + image.SetSpacing(1, 1, 1) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + + with pytest.raises(ValueError, match="too short for the requested greater-trochanter"): + femur.crop_vtk_images_to_greater_trochanter_length( + [image], + image, + retained_length_mm=80.0, + labels={1}, + ) + + def test_lesser_trochanter_cut_uses_peak_plateau_center_and_percent_offset(): np = pytest.importorskip("numpy") vtk = pytest.importorskip("vtk") From b06f4a865639a3ebf5f454e011397ffe6b9a3399 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:43:35 -0600 Subject: [PATCH 34/67] Support fixed femur ICP transforms --- ogo/fea/femur.py | 192 +++++++++++++++++++++++++++++----------- tests/fea/test_femur.py | 24 +++++ 2 files changed, 164 insertions(+), 52 deletions(-) diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 45ae9d6..f2038de 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -156,6 +156,53 @@ def matrix4x4_to_numpy(matrix): return values +def vtk_matrix_from_rows(rows): + """Return a VTK 4 x 4 matrix from JSON-serializable row values.""" + import vtk + + values = matrix4x4_to_numpy(rows) + matrix = vtk.vtkMatrix4x4() + matrix.Identity() + for row in range(4): + for col in range(4): + matrix.SetElement(row, col, float(values[row, col])) + return matrix + + +def load_icp_transform(path): + """Read an ICP transform sidecar JSON file.""" + import json + + with open(path, encoding="utf-8") as f: + data = json.load(f) + if data.get("type") != "ogo.femur.icp_transform": + raise ValueError("ICP transform file has an unexpected type: %s" % data.get("type")) + return data, vtk_matrix_from_rows(data["matrix"]) + + +def write_icp_transform(path, *, matrix, icp_transform, reference_scale, femur_side, rough_crop): + """Write an ICP transform sidecar JSON file for repeatable length sweeps.""" + import json + + output = Path(path) + output.parent.mkdir(parents=True, exist_ok=True) + matrix_rows = matrix4x4_to_numpy(matrix).tolist() + data = { + "type": "ogo.femur.icp_transform", + "version": 1, + "femur_side": int(femur_side), + "matrix": matrix_rows, + "icp": { + "iterations": int(icp_transform["iterations"]), + "mean_distance": float(icp_transform["mean_distance"]), + }, + "reference_scale": reference_scale, + "rough_pre_icp_crop": rough_crop, + } + output.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8") + return data + + def point_transform_to_vtk_matrix(rotation, translation): """Return a VTK matrix for ``p_out = p_in @ rotation.T + translation``.""" import numpy as np @@ -2234,6 +2281,8 @@ def sidewaysFallFe(args): femur_lesser_trochanter_distal_offset = args.femur_lesser_trochanter_distal_offset femur_lesser_trochanter_distal_offset_percent = args.femur_lesser_trochanter_distal_offset_percent femur_input_margin = args.femur_input_margin + femur_icp_transform_in = args.femur_icp_transform_in + femur_icp_transform_out = args.femur_icp_transform_out cortical_label = args.cortical_label trabecular_label = args.trabecular_label mask_smoothing_spacing_threshold = args.mask_smoothing_spacing_threshold @@ -2280,6 +2329,10 @@ def sidewaysFallFe(args): ogo.message("PMMA Material ID: %d" % pmma_mat_id) ogo.message("Applied Displacement [mm]: %8.4f" % fe_displacement) ogo.message("Femur Cut Mode: %s" % femur_cut_mode) + if femur_icp_transform_in: + ogo.message("Femur ICP Transform In: %s" % femur_icp_transform_in) + if femur_icp_transform_out: + ogo.message("Femur ICP Transform Out: %s" % femur_icp_transform_out) if femur_cut_mode == "bbox_ratio": ogo.message("Femur BBox Ratio [reference, constrained, free]: %s" % str(femur_bbox_ratio)) ogo.message("Femur BBox Crop From [reference, constrained, free]: %s" % str(femur_bbox_crop_from)) @@ -2462,59 +2515,90 @@ def sidewaysFallFe(args): compartment_rot = compartmentData ## - # Align the input femur with the reference model - ogo.message("Aligning input with reference model...") - sample_surface_points = surface_points_from_vtk_mask( - registration_mask, - max_points=8000, - sample_mode="stride", - sample_offset=0, - ) - mask_surface = polydata_from_points(sample_surface_points) - if femur_side == 1: - ref_poly = ogo.readPolyData(left_femur_reference) - elif femur_side == 2: - if os.path.exists(right_femur_reference): - ref_poly = ogo.readPolyData(right_femur_reference) - else: - ogo.message( - "Right femur reference not found; mirroring left reference in x:", - right_femur_reference, - ) - ref_poly = mirror_polydata_x(ogo.readPolyData(left_femur_reference)) + # Align the input femur with the reference model, or reuse a saved transform. + if femur_icp_transform_in: + ogo.message("Loading fixed femur ICP transform...") + try: + icp_sidecar, icp = load_icp_transform(femur_icp_transform_in) + except Exception as exc: + ogo.message("Unable to load femur ICP transform: %s" % exc) + sys.exit(1) + if int(icp_sidecar.get("femur_side", femur_side)) != int(femur_side): + ogo.message("Femur ICP transform side does not match requested femur side.") + sys.exit(1) + icp_transform = icp_sidecar.get("icp", {"iterations": 0, "mean_distance": 0.0}) + reference_scale = icp_sidecar.get("reference_scale", {}) + ogo.message( + "Using fixed ICP reference-to-sample transform iterations=%d mean_distance=%0.4f" + % (int(icp_transform.get("iterations", 0)), float(icp_transform.get("mean_distance", 0.0))) + ) else: - print("Error: Femur Side not defined. Terminating...") - sys.exit() - - ref_poly, reference_scale = scale_reference_point_cloud_to_sample( - ref_poly, - sample_surface_points, - max_points=8000, - reference_sample_mode="linspace", - min_scale=DEFAULT_FEMUR_REFERENCE_MIN_SCALE, - max_scale=DEFAULT_FEMUR_REFERENCE_MAX_SCALE, - ) - ogo.message("Femur reference axis lengths: %s" % str(reference_scale["reference_axis_lengths"])) - ogo.message("Sample femur axis lengths: %s" % str(reference_scale["sample_axis_lengths"])) - ogo.message("Femur reference scale factors: %s" % str(reference_scale["scale_factors"])) - - icp_transform = estimate_rigid_icp( - moving_points=polydata_points(ref_poly), - fixed_points=sample_surface_points, - iterations=50, - tolerance=1.0e-4, - start_by_matching_centroids_only=True, - convergence="delta", - distance_mode="mean", - ) - icp = point_transform_to_vtk_matrix( - icp_transform["rotation"], - icp_transform["translation"], - ) - ogo.message( - "ICP reference-to-sample iterations=%d mean_distance=%0.4f" - % (icp_transform["iterations"], icp_transform["mean_distance"]) - ) + ogo.message("Aligning input with reference model...") + sample_surface_points = surface_points_from_vtk_mask( + registration_mask, + max_points=8000, + sample_mode="stride", + sample_offset=0, + ) + mask_surface = polydata_from_points(sample_surface_points) + if femur_side == 1: + ref_poly = ogo.readPolyData(left_femur_reference) + elif femur_side == 2: + if os.path.exists(right_femur_reference): + ref_poly = ogo.readPolyData(right_femur_reference) + else: + ogo.message( + "Right femur reference not found; mirroring left reference in x:", + right_femur_reference, + ) + ref_poly = mirror_polydata_x(ogo.readPolyData(left_femur_reference)) + else: + print("Error: Femur Side not defined. Terminating...") + sys.exit() + + ref_poly, reference_scale = scale_reference_point_cloud_to_sample( + ref_poly, + sample_surface_points, + max_points=8000, + reference_sample_mode="linspace", + min_scale=DEFAULT_FEMUR_REFERENCE_MIN_SCALE, + max_scale=DEFAULT_FEMUR_REFERENCE_MAX_SCALE, + ) + ogo.message("Femur reference axis lengths: %s" % str(reference_scale["reference_axis_lengths"])) + ogo.message("Sample femur axis lengths: %s" % str(reference_scale["sample_axis_lengths"])) + ogo.message("Femur reference scale factors: %s" % str(reference_scale["scale_factors"])) + + icp_transform = estimate_rigid_icp( + moving_points=polydata_points(ref_poly), + fixed_points=sample_surface_points, + iterations=50, + tolerance=1.0e-4, + start_by_matching_centroids_only=True, + convergence="delta", + distance_mode="mean", + ) + icp = point_transform_to_vtk_matrix( + icp_transform["rotation"], + icp_transform["translation"], + ) + ogo.message( + "ICP reference-to-sample iterations=%d mean_distance=%0.4f" + % (icp_transform["iterations"], icp_transform["mean_distance"]) + ) + if femur_icp_transform_out: + try: + write_icp_transform( + femur_icp_transform_out, + matrix=icp, + icp_transform=icp_transform, + reference_scale=reference_scale, + femur_side=femur_side, + rough_crop=bbox_crop_meta, + ) + except Exception as exc: + ogo.message("Unable to write femur ICP transform: %s" % exc) + sys.exit(1) + ogo.message("Wrote femur ICP transform: %s" % femur_icp_transform_out) ogo.message("Applying the transformation and isotropic resampling to the image and mask...") output_origin, output_size = reference_grid_from_vtk_mask( @@ -3315,6 +3399,10 @@ def main(): help="Optional percentage of the detected greater-to-lesser trochanter z distance to cut distal to the lesser trochanter. Overrides --femur_lesser_trochanter_distal_offset when set. (default: %(default)s)") parser.add_argument("--femur_input_margin", type=float, default=DEFAULT_FEMUR_INPUT_MARGIN_MM, help="Pad the input image/mask as needed so femur foreground has this margin before ICP. (default: %(default)s [mm])") + parser.add_argument("--femur_icp_transform_in", type=str, default=None, + help="Optional femur ICP transform JSON to reuse instead of estimating ICP. Intended for fixed-transform length sweeps. (default: %(default)s)") + parser.add_argument("--femur_icp_transform_out", type=str, default=None, + help="Optional path to write the estimated femur ICP transform JSON. (default: %(default)s)") parser.add_argument("--compartment_mask", type=str, default=None, help="Optional trabecular/cortical compartment mask aligned with the bone mask. Defaults: cortical=1, trabecular=2.") parser.add_argument("--cortical_label", type=int, default=DEFAULT_CORTICAL_LABEL, diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index c166e6d..3468e35 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -41,6 +41,30 @@ def test_sideways_fall_output_name_uses_compact_side_suffix(): assert femur.sideways_fall_output_name("density.n88model", 2) == "density_RF.n88model" +def test_icp_transform_sidecar_round_trips_matrix(tmp_path): + np = pytest.importorskip("numpy") + + matrix = np.eye(4) + matrix[:3, 3] = [1.5, -2.0, 3.25] + path = tmp_path / "sub-RETRO2_00000_left_icp.json" + + femur.write_icp_transform( + path, + matrix=matrix, + icp_transform={"iterations": 12, "mean_distance": 0.25}, + reference_scale={"source": "test"}, + femur_side=1, + rough_crop={"retained_length_mm": 120.0}, + ) + + data, loaded = femur.load_icp_transform(path) + + assert data["type"] == "ogo.femur.icp_transform" + assert data["femur_side"] == 1 + assert data["icp"] == {"iterations": 12, "mean_distance": 0.25} + assert femur.matrix4x4_to_numpy(loaded) == pytest.approx(matrix) + + def test_invalid_femur_side_is_rejected(): with pytest.raises(ValueError): femur.side_suffix(3) From c9263657b6a17e12037ec6c800ec8aaf2e05d2e7 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 28 Jul 2026 11:15:10 -0600 Subject: [PATCH 35/67] docs: specify masked pistoia length study --- ...7-28-masked-pistoia-length-study-design.md | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-28-masked-pistoia-length-study-design.md diff --git a/docs/superpowers/specs/2026-07-28-masked-pistoia-length-study-design.md b/docs/superpowers/specs/2026-07-28-masked-pistoia-length-study-design.md new file mode 100644 index 0000000..85511b7 --- /dev/null +++ b/docs/superpowers/specs/2026-07-28-masked-pistoia-length-study-design.md @@ -0,0 +1,122 @@ +# Masked Pistoia and RETRO2 Femur Length Study Design + +## Goal + +Add a generic Pistoia region-of-interest mask workflow to Ogo so a solved FE +model can report both full-model Pistoia and masked-region Pistoia. The first +use case is the RETRO2 femur length study, where the mask is the femoral neck. +The same mechanism should also support spine models, for example vertebral-body +or trabecular-body-only Pistoia. + +## Inputs + +`ogo.cli.GenerateFEM` should accept a site-agnostic optional argument: + +```text +--pistoia_mask PATH +``` + +The mask is interpreted in the input image space. For hip this can be a +femoral-neck mask. For spine this can be a vertebral-body, trabecular-body, or +other ROI mask. The mask is optional; if it is not provided, the current full +model Pistoia behavior is unchanged. + +For the RETRO2 femur length study, inputs should come from the PI-preferred FEA +folder: + +```text +/home/matthias.walle/data/scratch/RETRO2/fea/QCT/hip_QCT/ +/home/matthias.walle/data/scratch/RETRO2/fea/segmentations/left_femur/ +/home/matthias.walle/data/scratch/RETRO2/fea/segmentations/femoral_neck_original_dim/ +``` + +Cases without a full hip QCT or without a reconstructed femoral-neck mask should +be reported in a manifest and skipped for masked femoral-neck Pistoia. + +## Pipeline Behavior + +The ROI mask must be carried through exactly the same spatial operations as the +FE model: + +- input resampling +- rough crop or crop-to-mask steps +- padding +- ICP/reference transformation +- reference-grid resampling +- final length crop + +ROI masks use nearest-neighbor interpolation. The transformed ROI mask should be +written beside the generated model for auditability. Its geometry must match the +final model grid so `ogo.util.faim.run_pistoia_with_image_mask()` can select +model elements directly. + +The existing post-hoc source-space masks must not be applied directly to +post-ICP models. Prior checks showed that source-space masks select zero +elements in existing length-crop `.n88model` files. + +## Pistoia Reporting + +When `--run_pistoia` is enabled, Ogo should continue to run full-model Pistoia +with the existing critical strain and critical volume settings. + +When `--pistoia_mask` is provided, Ogo should additionally run masked Pistoia +using the transformed model-space ROI mask. The lower-level mechanism should +reuse `ogo.util.faim.run_pistoia_with_image_mask()`, which temporarily excludes +elements outside the image mask and then restores the original model materials. + +The output CSV should include both full and masked values. Generic column names +avoid femur-specific assumptions: + +```text +pistoia_failure_load_N +pistoia_stiffness_N_per_mm +masked_pistoia_failure_load_N +masked_pistoia_stiffness_N_per_mm +masked_pistoia_critical_volume_pct +masked_pistoia_critical_ees +masked_pistoia_ees_at_crit_vol +masked_pistoia_mask_file +masked_pistoia_selected_elements +masked_pistoia_original_included_elements +``` + +If masked Pistoia fails and `--require_pistoia` is not set, the main solve +should still complete and report a warning, matching the current full-Pistoia +failure behavior. + +## RETRO2 Length Study Output + +Create a clean study folder: + +```text +/home/matthias.walle/data/scratch/RETRO2/fea/length_study_femoral_neck_pistoia/ +``` + +The folder should contain: + +- `models///...` for `.n88model` and sidecars +- `individual_results///...` for solver and Pistoia outputs +- `transformed_masks///...` for model-space Pistoia ROI masks +- `logs/` for generation and solve logs +- `tables/run_manifest.csv` +- `tables/results_summary.csv` + +`results_summary.csv` should retain the length-study fields from the existing +study and add the masked Pistoia fields above. + +## Validation + +Implementation should include unit or integration tests that verify: + +- `--pistoia_mask` appears in the hip and spine CLI. +- The femur pipeline resamples/transforms the ROI mask to the final model grid. +- The transformed ROI mask selects nonzero model elements in a generated model. +- Full Pistoia and masked Pistoia can both be reported in the same results CSV. +- Missing ROI masks produce manifest warnings rather than silent omission. + +Before running the full RETRO2 study, run a small smoke set with available +femoral-neck masks across several crop lengths and verify: + +- transformed ROI masks overlay the final model anatomy; +- selected-element counts are plausible and nonzero; +- full-model and masked Pistoia outputs are both present. From 17ab5094745cd4d0cc28bf8626f4a7f96e6c6927 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:16:18 -0600 Subject: [PATCH 36/67] Update hip and L1 FEA workflows --- ogo/cli/GenerateFEM.py | 145 ++++++++-- ogo/fea/alignment.py | 10 +- ogo/fea/boundary.py | 279 +++++++++++++++++- ogo/fea/femur.py | 235 ++++++++++++++-- ogo/fea/image_io.py | 20 ++ ogo/fea/model.py | 136 ++++++++- ogo/fea/spine.py | 422 +++++++++++++++++++++++++--- ogo/util/faim.py | 78 +++++ tests/cli/test_GenerateFEM.py | 192 +++++++++++-- tests/fea/test_boundary.py | 56 ++++ tests/fea/test_femur.py | 58 +++- tests/fea/test_spinefe_benchmark.py | 79 ++++++ tests/util/test_faim.py | 76 +++++ 13 files changed, 1649 insertions(+), 137 deletions(-) create mode 100644 ogo/fea/image_io.py diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 551753a..7b13858 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -32,6 +32,7 @@ DEFAULT_FEMUR_BBOX_RATIO, DEFAULT_FEMUR_CUT_MODE, DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, + DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, DEFAULT_FEMUR_ISO_RESOLUTION_MM, DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, @@ -181,6 +182,19 @@ def expected_femur_model_path(calibrated_image: Path, output_path: Optional[Path return output_dir / "{}_{}.n88model".format(remove_extension(calibrated_image), stem) +def pistoia_mask_output_path(model_path: Path) -> Path: + """Return the model-space Pistoia ROI mask sidecar path for a model.""" + model_path = Path(model_path) + return model_path.with_name(model_path.with_suffix("").name + "_pistoia_mask.nii.gz") + + +def model_space_pistoia_mask_path(model_path: Path, args: argparse.Namespace) -> Optional[Path]: + """Return the transformed Pistoia ROI mask sidecar expected by the solver.""" + if getattr(args, "pistoia_mask", None) is None: + return None + return pistoia_mask_output_path(model_path) + + def ensure_output_directory(output_path: Optional[Path]) -> None: """Create an explicit model output directory before generation starts.""" if output_path is not None: @@ -258,6 +272,20 @@ def solve_report_profile(args: argparse.Namespace, model_type: str) -> dict: return profile +def critical_volume_percent(args: argparse.Namespace, model_type: str) -> float: + """Return the Pistoia critical-volume setting for this model.""" + if args.critical_volume is not None: + return args.critical_volume + return float(solve_report_profile(args, model_type).get("critical_volume", 2.0)) + + +def critical_strain(args: argparse.Namespace, model_type: str) -> float: + """Return the Pistoia critical-strain setting for this model.""" + if args.critical_strain is not None: + return args.critical_strain + return float(solve_report_profile(args, model_type).get("critical_strain", 0.007)) + + def percent_displacement_metadata( model_path: Path, *, @@ -293,6 +321,21 @@ def percent_displacement_metadata( return metadata +def absolute_displacement_metadata(generator_argv: Sequence[str], default_value: str) -> dict: + """Describe a solve displacement kept as an explicit absolute mm value.""" + value = option_value(generator_argv, "--fe_displacement", default_value) + try: + value_mm = float(value) + except (TypeError, ValueError): + value_mm = value + return { + "value_mm": value_mm, + "target_displacement_percent": None, + "characteristic_length_mm": None, + "value_source": "explicit --fe_displacement used as absolute model displacement", + } + + def solve_model( model_path: Path, args: argparse.Namespace, @@ -318,6 +361,10 @@ def solve_model( default_applied_displacement, ) + solve_displacement_percent = None if args.use_absolute_fe_displacement else profile["target_displacement_percent"] + target_displacement = abs(float(applied_displacement)) if args.use_absolute_fe_displacement else profile["target_displacement_percent"] + run_pistoia = args.run_pistoia or args.require_pistoia or args.pistoia_mask is not None + run_faim_pipeline( model_file=model_path, output_prefix=model_path.with_suffix(""), @@ -337,14 +384,15 @@ def solve_model( n88pistoia_command=args.n88pistoia_command, n88tabulate_command=args.n88tabulate_command, n88copymodel_command=args.n88copymodel_command, - critical_volume=args.critical_volume, - critical_strain=args.critical_strain, + critical_volume=critical_volume_percent(args, model_type), + critical_strain=critical_strain(args, model_type), exclude=args.exclude, - run_pistoia=args.run_pistoia or args.require_pistoia, + run_pistoia=run_pistoia, + pistoia_mask_file=model_space_pistoia_mask_path(model_path, args), applied_displacement=applied_displacement, - target_displacement=profile["target_displacement_percent"], + target_displacement=target_displacement, report_profile=profile["report_profile"], - solve_displacement_percent=profile["target_displacement_percent"], + solve_displacement_percent=solve_displacement_percent, compress=not args.no_compress, require_pistoia=args.require_pistoia, dry_run=args.dry_run, @@ -399,7 +447,11 @@ def write_modeling_metadata( "solve_requested": not args.no_solve, "target_displacement_percent": target_displacement_percent(args, model_type), "target_displacement_definition": "percent strain converted from model geometry for spine; percent of femur length for femur", - "run_pistoia": args.run_pistoia or args.require_pistoia, + "run_pistoia": args.run_pistoia or args.require_pistoia or args.pistoia_mask is not None, + "source_pistoia_mask": None if args.pistoia_mask is None else str(args.pistoia_mask), + "model_space_pistoia_mask": None + if args.pistoia_mask is None + else str(pistoia_mask_output_path(model_path)), "compress_solved_model": not args.no_compress, }, } @@ -408,12 +460,18 @@ def write_modeling_metadata( body_label = option_int(generator_argv, "--mask_threshold", 0) process_label = option_int(generator_argv, "--process_mask_threshold", 0) appendix = option_value(generator_argv, "--appendix") - spine_displacement = percent_displacement_metadata( - model_path, - report_profile="spine", - failure_axis="z", - target_percent=common["solve_and_reporting"]["target_displacement_percent"], - ) + if args.use_absolute_fe_displacement: + spine_displacement = absolute_displacement_metadata( + generator_argv, + str(spine_solve_report_profile(preset=getattr(args, "preset", None))["default_applied_displacement"]), + ) + else: + spine_displacement = percent_displacement_metadata( + model_path, + report_profile="spine", + failure_axis="z", + target_percent=common["solve_and_reporting"]["target_displacement_percent"], + ) metadata = common.copy() metadata.update({ "model": "spine-compression", @@ -590,12 +648,18 @@ def write_modeling_metadata( 3, DEFAULT_FEMUR_BBOX_CROP_FROM, ) - femur_displacement = percent_displacement_metadata( - model_path, - report_profile="femur", - failure_axis="y", - target_percent=common["solve_and_reporting"]["target_displacement_percent"], - ) + if args.use_absolute_fe_displacement: + femur_displacement = absolute_displacement_metadata( + generator_argv, + str(femur_solve_report_profile()["default_applied_displacement"]), + ) + else: + femur_displacement = percent_displacement_metadata( + model_path, + report_profile="femur", + failure_axis="y", + target_percent=common["solve_and_reporting"]["target_displacement_percent"], + ) metadata = common.copy() metadata.update({ "model": "femur-sideways", @@ -659,7 +723,7 @@ def write_modeling_metadata( if femur_cut_mode == "post_icp_oblique_ratio" else "fixed rough crop before ICP, final flat ratio crop after ICP" if femur_cut_mode == "post_icp_flat_ratio" - else "fixed rough crop before ICP for registration, final GT-relative flat crop after ICP on the full scan" + else "fixed rough crop before ICP for registration, final GT-disk-relative flat crop after ICP on the full scan" if femur_cut_mode == "greater_trochanter_length" else "after ICP on the generated model grid" ), @@ -675,6 +739,12 @@ def write_modeling_metadata( "--femur_greater_trochanter_distal_length", DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, ), + "gt_inclusion_length_mm": option_float( + generator_argv, + "--femur_greater_trochanter_inclusion_length", + DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, + ), + "length_origin": "detected greater-trochanter disk distal edge", }, "lesser_trochanter_distal_offset_mm": option_float( generator_argv, @@ -703,7 +773,7 @@ def write_modeling_metadata( if femur_cut_mode == "post_icp_oblique_ratio" else "flat post-ICP aligned-frame crop face" if femur_cut_mode == "post_icp_flat_ratio" - else "flat post-ICP aligned-frame crop face at fixed distance below detected greater trochanter" + else "flat post-ICP aligned-frame crop face at fixed shaft length below detected GT-disk distal edge" if femur_cut_mode == "greater_trochanter_length" else "flat model-grid z plane" ), @@ -951,14 +1021,14 @@ def _add_common_image_args(parser: argparse.ArgumentParser) -> None: parser.add_argument( "--critical_volume", type=float, - default=2.0, - help="Pistoia critical volume percentage.", + default=None, + help="Pistoia critical volume percentage. Defaults are model-profile specific.", ) parser.add_argument( "--critical_strain", type=float, - default=0.007, - help="Pistoia critical EES strain.", + default=None, + help="Pistoia critical EES strain. Defaults are model-profile specific.", ) parser.add_argument("--exclude", type=int, default=5000, help="Material ID excluded by Pistoia.") parser.add_argument( @@ -971,6 +1041,14 @@ def _add_common_image_args(parser: argparse.ArgumentParser) -> None: "to 4.0. Values are converted to mm from generated model geometry." ), ) + parser.add_argument( + "--use_absolute_fe_displacement", + action="store_true", + help=( + "Solve at the explicit --fe_displacement value in mm instead of " + "converting the profile target displacement percent from model geometry." + ), + ) parser.add_argument( "--require_pistoia", action="store_true", @@ -981,6 +1059,16 @@ def _add_common_image_args(parser: argparse.ArgumentParser) -> None: action="store_true", help="Run Pistoia postprocessing and include Pistoia metrics when available.", ) + parser.add_argument( + "--pistoia_mask", + type=Path, + default=None, + help=( + "Optional ROI mask in input image space for an additional masked Pistoia " + "calculation. The model builder writes the transformed model-space mask " + "next to the .n88model." + ), + ) parser.add_argument( "--no_compress", action="store_true", @@ -1070,8 +1158,12 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if not args.dry_run: ensure_output_directory(args.output_path) + pistoia_mask_args = ( + ["--pistoia_mask", str(args.pistoia_mask)] if args.pistoia_mask is not None else [] + ) + if args.model_type == "spine": - spine_extra_args = spine_preset_args(args.preset) + list(extra_args) + [ + spine_extra_args = spine_preset_args(args.preset) + pistoia_mask_args + list(extra_args) + [ "--quality_control", str(bool(args.debug)), ] @@ -1104,13 +1196,14 @@ def main(argv: Optional[Sequence[str]] = None) -> None: return if args.model_type == "hip": + femur_extra_args = pistoia_mask_args + list(extra_args) for side in expand_sides(args.side or ["both"]): cmd = build_femur_command( calibrated_image=args.calibrated_image, bone_mask=args.bone_mask, side=side, output_path=args.output_path, - extra_args=extra_args, + extra_args=femur_extra_args, ) if args.dry_run: print_dry_run("ogoFEA-hip-builder", cmd) diff --git a/ogo/fea/alignment.py b/ogo/fea/alignment.py index 40092cc..9df9456 100644 --- a/ogo/fea/alignment.py +++ b/ogo/fea/alignment.py @@ -127,13 +127,21 @@ def estimate_rigid_icp( start_by_matching_centroids_only=False, convergence="delta", distance_mode="mean", + initial_transform=None, ): """Estimate a rigid transform from ``moving_points`` to ``fixed_points``.""" import numpy as np moving = _points_array(moving_points, "moving_points") fixed = _points_array(fixed_points, "fixed_points") - if start_by_matching_centroids_only: + if initial_transform is not None: + rotation = np.asarray(initial_transform["rotation"], dtype=float) + translation = np.asarray(initial_transform["translation"], dtype=float) + if rotation.shape != (3, 3): + raise ValueError("initial_transform rotation must have shape (3, 3).") + if translation.shape != (3,): + raise ValueError("initial_transform translation must contain three values.") + elif start_by_matching_centroids_only: rotation = np.eye(3) translation = fixed.mean(axis=0) - moving.mean(axis=0) else: diff --git a/ogo/fea/boundary.py b/ogo/fea/boundary.py index f54df59..6d3886a 100644 --- a/ogo/fea/boundary.py +++ b/ogo/fea/boundary.py @@ -597,6 +597,23 @@ def _fill_short_boolean_gaps_in_footprint(values, max_gap=2): return out +def _fill_short_boolean_gaps_in_volume(values, max_gap=2): + """Fill short bracketed gaps along each axis in a 3D mask.""" + import numpy as np + + out = np.asarray(values, dtype=bool).copy() + for y in range(out.shape[1]): + for z in range(out.shape[2]): + out[:, y, z] = _fill_short_boolean_gaps_in_line(out[:, y, z], max_gap) + for x in range(out.shape[0]): + for z in range(out.shape[2]): + out[x, :, z] = _fill_short_boolean_gaps_in_line(out[x, :, z], max_gap) + for x in range(out.shape[0]): + for y in range(out.shape[1]): + out[x, y, :] = _fill_short_boolean_gaps_in_line(out[x, y, :], max_gap) + return out + + def _clean_projected_footprint(mask): """Clean a candidate cap footprint before it is extruded into a disk.""" import numpy as np @@ -820,6 +837,10 @@ def projected_material_disk_required_bounds( shape="anatomy", thickness=3.0, intrusion=2.0, + stable_surface=False, + stable_surface_fraction=0.55, + stable_surface_min_area_fraction=0.12, + stable_surface_max_depth=None, ): """Return conservative physical bounds needed for a plane-projected disk.""" import numpy as np @@ -870,7 +891,22 @@ def projected_material_disk_required_bounds( if not np.any(inside & forward): return None - surface_distance = float(np.min(distance[inside & forward])) + first_distance, distance_by_key = _surface_distance_by_projected_bucket( + active_indices[inside & forward], + distance[inside & forward], + u[inside & forward], + v[inside & forward], + spacing=spacing, + ) + surface_distance = first_distance + if stable_surface and distance_by_key: + stable_contact = _stable_surface_contact_from_bucket_distances( + distance_by_key, + stable_surface_fraction=stable_surface_fraction, + stable_surface_min_area_fraction=stable_surface_min_area_fraction, + stable_surface_max_depth=stable_surface_max_depth, + ) + surface_distance = float(stable_contact["stable_distance"]) cap_inner_distance = surface_distance + max(float(intrusion), 0.0) cap_outer_distance = cap_inner_distance - max(float(thickness), 0.0) d_min = min(cap_inner_distance, cap_outer_distance) @@ -963,6 +999,179 @@ def _surface_distance_by_projected_bucket( return float(first_distance), {key: float(value[0]) for key, value in best.items()} +def _clean_footprint_keys(keys): + """Return the largest filled component from projected footprint keys.""" + import numpy as np + + if not keys: + return set() + coords = np.asarray(list(keys), dtype=int) + lower = coords.min(axis=0) + upper = coords.max(axis=0) + footprint = np.zeros(tuple((upper - lower + 1).tolist()), dtype=bool) + shifted = coords - lower + footprint[shifted[:, 0], shifted[:, 1]] = True + cleaned = _clean_projected_footprint(footprint) + cleaned_coords = np.argwhere(cleaned) + lower + return {tuple(int(value) for value in coord) for coord in cleaned_coords} + + +def _stable_surface_contact_from_bucket_distances( + distance_by_key, + *, + stable_surface_fraction=0.55, + stable_surface_min_area_fraction=0.12, + stable_surface_max_depth=None, +): + """Find the first broad projected surface behind isolated protrusions.""" + import numpy as np + + if not distance_by_key: + return { + "first_distance": 0.0, + "stable_distance": 0.0, + "stable_depth": 0.0, + "first_area": 0, + "stable_area": 0, + "peak_area": 0, + "stable_keys": set(), + } + + distances = np.asarray(list(distance_by_key.values()), dtype=float) + first_distance = float(np.min(distances)) + full_area = int(len(distance_by_key)) + first_keys = { + key + for key, distance in distance_by_key.items() + if float(distance) <= first_distance + 1.0e-9 + } + first_area = len(_clean_footprint_keys(first_keys)) + + if stable_surface_max_depth is None: + max_distance = float(np.max(distances)) + else: + max_distance = first_distance + max(float(stable_surface_max_depth), 0.0) + thresholds = np.unique(distances[distances <= max_distance + 1.0e-9]) + if thresholds.size == 0: + thresholds = np.asarray([first_distance], dtype=float) + + surfaces = [] + peak_area = 0 + for threshold in thresholds: + keys = { + key + for key, distance in distance_by_key.items() + if float(distance) <= float(threshold) + 1.0e-9 + } + clean_keys = _clean_footprint_keys(keys) + area = len(clean_keys) + peak_area = max(peak_area, area) + surfaces.append((float(threshold), area, clean_keys)) + + target_area = max( + 1, + int(np.ceil(max(float(stable_surface_fraction), 0.0) * peak_area)), + int(np.ceil(max(float(stable_surface_min_area_fraction), 0.0) * full_area)), + ) + stable_distance, stable_area, stable_keys = surfaces[-1] + for threshold, area, keys in surfaces: + if area >= target_area: + stable_distance = threshold + stable_area = area + stable_keys = keys + break + + return { + "first_distance": first_distance, + "stable_distance": float(stable_distance), + "stable_depth": float(stable_distance - first_distance), + "first_area": int(first_area), + "stable_area": int(stable_area), + "peak_area": int(peak_area), + "stable_keys": stable_keys, + } + + +def projected_stable_surface_contact( + active_mask, + *, + spacing, + origin, + center, + normal, + u_axis=None, + v_axis=None, + size=(24.0, 24.0), + shape="square", + extent_start=(0, 0, 0), + stable_surface_fraction=0.55, + stable_surface_min_area_fraction=0.12, + stable_surface_max_depth=None, +): + """Return stable projected contact-surface metrics for a physical plane.""" + import numpy as np + + active = np.asarray(active_mask, dtype=bool) + if active.ndim != 3: + raise ValueError("active_mask must be a 3D x/y/z array.") + if not np.any(active): + return _stable_surface_contact_from_bucket_distances({}) + + spacing = tuple(float(value) for value in spacing) + origin = tuple(float(value) for value in origin) + center = np.asarray(center, dtype=float) + if center.shape != (3,): + raise ValueError("center must contain three values.") + normal = _unit_vector(normal, "normal") + if u_axis is None or v_axis is None: + u_axis, v_axis = _plane_axes_from_normal(normal) + else: + u_axis = _unit_vector(u_axis, "u_axis") + v_axis = _unit_vector(v_axis, "v_axis") + if len(size) != 2: + raise ValueError("size must contain two in-plane lengths.") + half_u = max(float(size[0]) / 2.0, 0.0) + half_v = max(float(size[1]) / 2.0, 0.0) + + indices = np.argwhere(active) + points = _physical_points_from_indices( + indices, + spacing=spacing, + origin=origin, + extent_start=extent_start, + ) + rel = points - center + distance = rel @ normal + u = rel @ u_axis + v = rel @ v_axis + tolerance = max(min(spacing) * 0.75, 1.0e-6) + inside = _inside_projected_shape( + shape, + u, + v, + half_u, + half_v, + tolerance=tolerance, + ) + forward = distance >= -tolerance + candidate = inside & forward + if not np.any(candidate): + return _stable_surface_contact_from_bucket_distances({}) + _, distance_by_key = _surface_distance_by_projected_bucket( + indices[candidate], + distance[candidate], + u[candidate], + v[candidate], + spacing=spacing, + ) + return _stable_surface_contact_from_bucket_distances( + distance_by_key, + stable_surface_fraction=stable_surface_fraction, + stable_surface_min_area_fraction=stable_surface_min_area_fraction, + stable_surface_max_depth=stable_surface_max_depth, + ) + + def generate_projected_material_disk_mask( active_mask, *, @@ -977,6 +1186,12 @@ def generate_projected_material_disk_mask( thickness=3.0, intrusion=2.0, anatomy_constrained=True, + stable_surface=False, + stable_surface_fraction=0.55, + stable_surface_min_area_fraction=0.12, + stable_surface_max_depth=None, + close_gaps_3d=False, + close_gaps_3d_max_gap=2, material_mask=None, extent_start=(0, 0, 0), ): @@ -1054,17 +1269,39 @@ def generate_projected_material_disk_mask( if not distance_by_key: return np.zeros(active.shape, dtype=bool) + support_distance = first_distance + stable_keys = None + if stable_surface: + stable_contact = _stable_surface_contact_from_bucket_distances( + distance_by_key, + stable_surface_fraction=stable_surface_fraction, + stable_surface_min_area_fraction=stable_surface_min_area_fraction, + stable_surface_max_depth=stable_surface_max_depth, + ) + support_distance = float(stable_contact["stable_distance"]) + footprint_min = support_distance - max(2.0 * min(spacing), tolerance) + footprint_max = support_distance + intrusion + tolerance + stable_keys = _clean_footprint_keys( + { + key + for key, distance in distance_by_key.items() + if footprint_min <= float(distance) <= footprint_max + } + ) + if not stable_keys: + return np.zeros(active.shape, dtype=bool) + if anatomy_constrained: - depth_limit = first_distance + intrusion + tolerance + depth_limit = support_distance + intrusion + tolerance distance_by_key = { key: distance for key, distance in distance_by_key.items() - if distance <= depth_limit + if distance <= depth_limit and (stable_keys is None or key in stable_keys) } if not distance_by_key: return np.zeros(active.shape, dtype=bool) - cap_inner_distance = first_distance + intrusion + cap_inner_distance = support_distance + intrusion cap_outer_distance = cap_inner_distance - thickness full_indices = np.argwhere(np.ones(active.shape, dtype=bool)) full_points = _physical_points_from_indices( @@ -1097,11 +1334,17 @@ def generate_projected_material_disk_mask( ) bucket_mask = np.isfinite(local_surface) flat_outer = cap_outer_distance - if flat_outer >= first_distance: - flat_outer = first_distance - thickness - local_min = np.minimum(flat_outer, local_surface) - local_max = np.maximum(flat_outer, local_surface) - depth_ok = (distance >= local_min - tolerance) & (distance <= local_max + tolerance) + if flat_outer >= support_distance: + flat_outer = support_distance - thickness + if stable_surface: + depth_ok = ( + (distance >= flat_outer - tolerance) + & (distance <= cap_inner_distance + tolerance) + ) + else: + local_min = np.minimum(flat_outer, local_surface) + local_max = np.maximum(flat_outer, local_surface) + depth_ok = (distance >= local_min - tolerance) & (distance <= local_max + tolerance) else: bucket_mask = np.ones(full_indices.shape[0], dtype=bool) depth_ok = ( @@ -1113,6 +1356,12 @@ def generate_projected_material_disk_mask( disk = np.zeros(active.shape, dtype=bool) if np.any(keep): disk[tuple(full_indices[keep].T)] = True + if close_gaps_3d and np.any(disk): + disk = _fill_short_boolean_gaps_in_volume( + disk, + max_gap=int(close_gaps_3d_max_gap), + ) + disk &= ~material return disk @@ -1394,6 +1643,12 @@ def generate_projected_material_disk_vtk( thickness=3.0, intrusion=2.0, anatomy_constrained=True, + stable_surface=False, + stable_surface_fraction=0.55, + stable_surface_min_area_fraction=0.12, + stable_surface_max_depth=None, + close_gaps_3d=False, + close_gaps_3d_max_gap=2, output_value=1, ): """Generate a VTK material disk from a physical-space contact plane. @@ -1433,6 +1688,12 @@ def generate_projected_material_disk_vtk( thickness=thickness, intrusion=intrusion, anatomy_constrained=anatomy_constrained, + stable_surface=stable_surface, + stable_surface_fraction=stable_surface_fraction, + stable_surface_min_area_fraction=stable_surface_min_area_fraction, + stable_surface_max_depth=stable_surface_max_depth, + close_gaps_3d=close_gaps_3d, + close_gaps_3d_max_gap=close_gaps_3d_max_gap, material_mask=exclusion, extent_start=(extent[0], extent[2], extent[4]), ) diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index f2038de..257012a 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -25,12 +25,12 @@ ``greater_trochanter_length``, ``lesser_trochanter``, and ``fixed_length`` modes are available for debugging, sensitivity studies, or historical comparisons. -5. Generate two geometric PMMA fixtures from bbox-relative contact planes: +5. Generate two geometric PMMA fixtures from proximal contact planes: a femoral-head loading fixture on the high-y side and a greater-trochanter contact fixture on the low-y side. Defaults are 10 mm PMMA thickness and - 6 mm intrusion through that fixed thickness. The square fixture footprints - scale with the generated model bbox, and the fixture masks themselves do - not overwrite bone voxels. + 6 mm intrusion through that fixed thickness. The fixture footprints are + anchored at the proximal femur so they cannot contact the distal shaft crop + face, and the fixture masks themselves do not overwrite bone voxels. 6. Apply sideways-fall boundary conditions: prescribed displacement at the femoral-head PMMA cap toward the greater trochanter, loading-direction constraint at the greater-trochanter PMMA cap, and distal shaft constraints @@ -59,6 +59,7 @@ foreground_voxel_center_bounds, foreground_voxel_center_bounds_from_mask, ) +from ogo.fea.image_io import write_vtk_image_with_sitk_geometry LEFT_FEMUR = 1 @@ -92,6 +93,9 @@ DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM = 120.0 DEFAULT_FEMUR_CUT_MODE = "post_icp_flat_ratio" DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM = 175.0 +DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM = 0.0 +DEFAULT_FEMUR_PISTOIA_CRITICAL_VOLUME_PERCENT = 7.0 +DEFAULT_FEMUR_PISTOIA_CRITICAL_STRAIN = 0.009 PRE_ICP_CROP_MODES = {"bbox_ratio", "proximal_box_ratio"} ROUGH_PRE_ICP_CROP_MODES = {"post_icp_flat_ratio", "post_icp_oblique_ratio", "greater_trochanter_length"} FULL_SCAN_AFTER_ROUGH_CROP_MODES = {"greater_trochanter_length"} @@ -123,9 +127,47 @@ def solve_report_profile(): "failure_axis": "y", "default_applied_displacement": DEFAULT_FEMUR_FE_DISPLACEMENT, "target_displacement_percent": target_displacement_percent(), + "critical_volume": DEFAULT_FEMUR_PISTOIA_CRITICAL_VOLUME_PERCENT, + "critical_strain": DEFAULT_FEMUR_PISTOIA_CRITICAL_STRAIN, } +def proximal_sideways_fall_fixture_plane(model_bounds, *, center_fraction): + """Return a proximal-only sideways-fall PMMA contact plane. + + The shared bbox-relative helper scales y-projected fixtures over the full + model z extent. That is unsafe for shaft-length sensitivity models because + the proximal PMMA cap can also contact the distal shaft crop face. Keep the + contact footprint proximal by using a fixed long-axis footprint anchored at + the femoral-head end of the model. + """ + plane = bbox_relative_fixture_plane( + model_bounds, + center_fraction=center_fraction, + size_fraction=SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, + projection_axis="y", + shape=SIDEWAYS_FALL_FIXTURE_SHAPE, + ) + z_min = float(model_bounds[4]) + z_max = float(model_bounds[5]) + z_span = z_max - z_min + if z_span <= 0.0: + raise ValueError("model_bounds must have positive z span.") + long_axis_mm = min(float(FEMORAL_HEAD_FIXTURE_LONG_AXIS_EXTENSION_MM), z_span) + x_width_mm = ( + float(model_bounds[1]) + - float(model_bounds[0]) + + float(FEMORAL_HEAD_FIXTURE_WIDTH_EXTENSION_MM) + ) + center = list(plane["center"]) + center[2] = z_max - long_axis_mm / 2.0 + plane = dict(plane) + plane["center"] = tuple(float(value) for value in center) + plane["size"] = (float(long_axis_mm), float(x_width_mm)) + plane["footprint"] = "proximal_only" + return plane + + def side_suffix(femur_side): """Return the compact output stem for a femur side.""" if femur_side == LEFT_FEMUR: @@ -141,6 +183,12 @@ def sideways_fall_output_name(output_file, femur_side): return str(output_path.with_name(f"{output_path.stem}_{side_suffix(femur_side)}.n88model")) +def pistoia_mask_output_path(output_file): + """Return the model-space Pistoia ROI mask sidecar path for a model.""" + output_path = Path(output_file) + return output_path.with_name(f"{output_path.stem}_pistoia_mask.nii.gz") + + def matrix4x4_to_numpy(matrix): """Return a 4 x 4 NumPy matrix from a VTK matrix or array-like value.""" import numpy as np @@ -881,9 +929,10 @@ def crop_vtk_images_to_greater_trochanter_length( vtk_mask, *, retained_length_mm=DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, + gt_inclusion_length_mm=DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, labels=None, ): - """Apply a flat distal crop at a fixed distance below the detected GT level.""" + """Apply a flat distal crop below the detected GT-disk distal edge.""" import numpy as np from ogo.util.vtk_image import vtk_image_to_numpy @@ -891,6 +940,9 @@ def crop_vtk_images_to_greater_trochanter_length( retained_length_mm = float(retained_length_mm) if retained_length_mm <= 0.0: raise ValueError("retained_length_mm must be positive.") + gt_inclusion_length_mm = float(gt_inclusion_length_mm) + if gt_inclusion_length_mm < 0.0: + raise ValueError("gt_inclusion_length_mm must be non-negative.") mask_data = vtk_image_to_numpy(vtk_mask) active = _active_crop_mask(mask_data, labels) @@ -907,12 +959,14 @@ def crop_vtk_images_to_greater_trochanter_length( landmark = detect_lesser_trochanter_cut_z(vtk_mask, distal_offset_mm=0.0) gt_z = float(landmark["greater_trochanter_z"]) - cut_z = gt_z - retained_length_mm - available_mm = gt_z - float(landmark["mask_z_min"]) + gt_disk_distal_z = float(landmark["greater_trochanter_disk_distal_z"]) + distal_length_origin_z = gt_disk_distal_z - gt_inclusion_length_mm + cut_z = distal_length_origin_z - retained_length_mm + available_mm = distal_length_origin_z - float(landmark["mask_z_min"]) if available_mm < retained_length_mm: raise ValueError( - "Femur scan is too short for the requested greater-trochanter distal length: " - "requested %.4f mm, available %.4f mm below GT." + "Femur scan is too short for the requested greater-trochanter distal shaft length: " + "requested %.4f mm, available %.4f mm below the GT-disk distal length origin." % (retained_length_mm, max(0.0, available_mm)) ) @@ -931,11 +985,17 @@ def crop_vtk_images_to_greater_trochanter_length( "method": "greater_trochanter_length", "retained_length_mm": retained_length_mm, "requested_retained_length_mm": retained_length_mm, - "available_below_gt_mm": float(available_mm), + "gt_inclusion_length_mm": gt_inclusion_length_mm, + "available_below_gt_disk_mm": float(available_mm), + "available_below_gt_inclusion_mm": float(available_mm), + "available_below_gt_mm": float(gt_z - float(landmark["mask_z_min"])), "greater_trochanter_z": gt_z, + "greater_trochanter_disk_distal_z": gt_disk_distal_z, + "greater_trochanter_disk_proximal_z": float(landmark["greater_trochanter_disk_proximal_z"]), + "distal_length_origin_z": float(distal_length_origin_z), "lesser_trochanter_z": float(landmark["lesser_trochanter_z"]), "cut_z_mm": float(cut_z), - "reference_axis": "greater_trochanter_z", + "reference_axis": "greater_trochanter_disk_distal_z_minus_optional_offset", "reference_width_mm": y_width_mm, "input_z_length_mm": z_length_mm, "status": "cropped", @@ -1948,6 +2008,24 @@ def _peak_center_z(z, values, indices, *, relative_height=0.95): return peak_index, float(np.average(z[plateau], weights=weights)) +def _peak_disk_bounds_z(z, raw_values, peak_index, indices, *, relative_height=0.5): + """Return the distal and proximal z bounds of the dominant peak region.""" + import numpy as np + + values = np.asarray(raw_values, dtype=float) + valid = set(int(i) for i in indices) + peak_value = float(values[int(peak_index)]) + baseline = float(np.percentile(values[list(valid)], 25.0)) + threshold = baseline + max(0.0, peak_value - baseline) * float(relative_height) + left = int(peak_index) + right = int(peak_index) + while left - 1 in valid and float(values[left - 1]) >= threshold: + left -= 1 + while right + 1 in valid and float(values[right + 1]) >= threshold: + right += 1 + return float(z[left]), float(z[right]) + + def detect_lesser_trochanter_cut_z( vtk_mask, *, @@ -1985,6 +2063,12 @@ def detect_lesser_trochanter_cut_z( raise ValueError("Cannot identify greater trochanter from femur profile.") proximal_indices = np.where(proximal_mask)[0] greater_index, greater_z = _peak_center_z(z, y_max, proximal_indices) + gt_disk_distal_z, gt_disk_proximal_z = _peak_disk_bounds_z( + z, + profile["y_max"], + greater_index, + proximal_indices, + ) distal_mask = ( (z <= greater_z - float(min_distal_to_greater_mm)) @@ -2025,6 +2109,8 @@ def detect_lesser_trochanter_cut_z( "cut_z": float(cut_z), "lesser_trochanter_z": lesser_z, "greater_trochanter_z": greater_z, + "greater_trochanter_disk_distal_z": gt_disk_distal_z, + "greater_trochanter_disk_proximal_z": gt_disk_proximal_z, "mask_z_min": z_min, "mask_z_max": z_max, "retained_length_mm": float(z_max - cut_z), @@ -2254,6 +2340,7 @@ def sidewaysFallFe(args): image = args.calibrated_image mask = args.bone_mask compartment_mask = args.compartment_mask + pistoia_mask = args.pistoia_mask mask_threshold = args.mask_threshold iso_resolution = args.iso_resolution @@ -2276,6 +2363,7 @@ def sidewaysFallFe(args): femur_bbox_crop_from = args.femur_bbox_crop_from femur_experimental_crop_ratio = args.femur_experimental_crop_ratio femur_greater_trochanter_distal_length = args.femur_greater_trochanter_distal_length + femur_greater_trochanter_inclusion_length = args.femur_greater_trochanter_inclusion_length femur_proximal_reference_distance = args.femur_proximal_reference_distance femur_proximal_reference_width = args.femur_proximal_reference_width femur_lesser_trochanter_distal_offset = args.femur_lesser_trochanter_distal_offset @@ -2346,13 +2434,19 @@ def sidewaysFallFe(args): if femur_cut_mode == "greater_trochanter_length": ogo.message("Femur Rough Pre-ICP Retained Length [mm]: %8.4f" % femur_shaft_length) ogo.message( - "Femur Greater Trochanter Distal Length [mm]: %8.4f" + "Femur Additional Offset Distal To GT Disk [mm]: %8.4f" + % femur_greater_trochanter_inclusion_length + ) + ogo.message( + "Femur Shaft Length Distal To GT Disk [mm]: %8.4f" % femur_greater_trochanter_distal_length ) if compartment_mask is not None: ogo.message("Compartment Mask: %s" % compartment_mask) ogo.message("Cortical Label: %d" % cortical_label) ogo.message("Trabecular Label: %d" % trabecular_label) + if pistoia_mask is not None: + ogo.message("Pistoia ROI Mask: %s" % pistoia_mask) if femur_cut_mode == "fixed_length": ogo.message("Retained Proximal Femur Length [mm]: %8.4f" % femur_shaft_length) elif femur_lesser_trochanter_distal_offset_percent is not None: @@ -2382,6 +2476,10 @@ def sidewaysFallFe(args): if compartment_mask is not None: ogo.message("Reading trabecular/cortical compartment mask...") compartmentData = ogo.readNii(compartment_mask) + pistoiaMaskData = None + if pistoia_mask is not None: + ogo.message("Reading Pistoia ROI mask...") + pistoiaMaskData = ogo.readNii(pistoia_mask) distal_crop_face = None bbox_crop_meta = None @@ -2409,10 +2507,20 @@ def sidewaysFallFe(args): iso_resolution, interpolation="nearest", ) + if pistoiaMaskData is not None: + pistoiaMaskData = resample_vtk_image_like_workflow( + pistoiaMaskData, + iso_resolution, + interpolation="nearest", + ) if femur_cut_mode in PRE_ICP_CROP_MODES: ogo.message("Applying %s femur crop after isotropic resampling and before ICP..." % femur_cut_mode) - images_to_crop = [imageData, maskThres] + ([compartmentData] if compartmentData is not None else []) + images_to_crop = [imageData, maskThres] + if compartmentData is not None: + images_to_crop.append(compartmentData) + if pistoiaMaskData is not None: + images_to_crop.append(pistoiaMaskData) if femur_cut_mode == "bbox_ratio": cropped_images, distal_crop_face, bbox_crop_meta = crop_vtk_images_to_bbox_ratio( images_to_crop, @@ -2432,8 +2540,12 @@ def sidewaysFallFe(args): ) imageData = cropped_images[0] maskThres = cropped_images[1] + next_crop_index = 2 if compartmentData is not None: - compartmentData = cropped_images[2] + compartmentData = cropped_images[next_crop_index] + next_crop_index += 1 + if pistoiaMaskData is not None: + pistoiaMaskData = cropped_images[next_crop_index] registration_mask = maskThres ogo.message( "%s crop slices xyz=%s; output shape xyz=%s; crop face voxels=%d." @@ -2448,7 +2560,11 @@ def sidewaysFallFe(args): ogo.message( "Applying fixed-length rough femur crop after isotropic resampling and before ICP..." ) - images_to_crop = [imageData, maskThres] + ([compartmentData] if compartmentData is not None else []) + images_to_crop = [imageData, maskThres] + if compartmentData is not None: + images_to_crop.append(compartmentData) + if pistoiaMaskData is not None: + images_to_crop.append(pistoiaMaskData) cropped_images, rough_crop_face, bbox_crop_meta = crop_vtk_images_to_fixed_proximal_length( images_to_crop, maskThres, @@ -2465,8 +2581,12 @@ def sidewaysFallFe(args): if femur_cut_mode not in FULL_SCAN_AFTER_ROUGH_CROP_MODES: imageData = cropped_images[0] maskThres = cropped_images[1] + next_crop_index = 2 if compartmentData is not None: - compartmentData = cropped_images[2] + compartmentData = cropped_images[next_crop_index] + next_crop_index += 1 + if pistoiaMaskData is not None: + pistoiaMaskData = cropped_images[next_crop_index] ogo.message( "rough pre-ICP crop slices xyz=%s; output shape xyz=%s; retained length=%8.4f; status=%s." % ( @@ -2484,6 +2604,8 @@ def sidewaysFallFe(args): images_to_pad.append(distal_crop_face) if compartmentData is not None: images_to_pad.append(compartmentData) + if pistoiaMaskData is not None: + images_to_pad.append(pistoiaMaskData) pad_constants = [0] * len(images_to_pad) padded_images, padding = pad_vtk_images_to_foreground_margin( images_to_pad, @@ -2499,6 +2621,9 @@ def sidewaysFallFe(args): next_padded_index += 1 if compartmentData is not None: compartmentData = padded_images[next_padded_index] + next_padded_index += 1 + if pistoiaMaskData is not None: + pistoiaMaskData = padded_images[next_padded_index] if any(padding["lower"]) or any(padding["upper"]): ogo.message( "Padded isotropic input image extent by lower=%s upper=%s voxels for FE transform safety." @@ -2513,6 +2638,7 @@ def sidewaysFallFe(args): mask_rot = maskThres distal_crop_face_rot = distal_crop_face compartment_rot = compartmentData + pistoia_mask_rot = pistoiaMaskData ## # Align the input femur with the reference model, or reuse a saved transform. @@ -2651,6 +2777,18 @@ def sidewaysFallFe(args): if compartment_rot is not None else None ) + pistoia_mask_trans = ( + transform_resample_vtk_image_to_reference_grid( + pistoia_mask_rot, + icp, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", + ) + if pistoia_mask_rot is not None + else None + ) smooth_resampled_masks = should_smooth_resampled_mask( input_spacing, mask_smoothing_spacing_threshold, @@ -2698,7 +2836,11 @@ def sidewaysFallFe(args): ogo.message("Applying flat post-ICP femur crop at fixed distance below detected GT...") else: ogo.message("Applying flat post-ICP femur crop in aligned reference coordinates...") - images_to_crop = [image_trans, mask_trans] + ([compartment_trans] if compartment_trans is not None else []) + images_to_crop = [image_trans, mask_trans] + if compartment_trans is not None: + images_to_crop.append(compartment_trans) + if pistoia_mask_trans is not None: + images_to_crop.append(pistoia_mask_trans) try: if femur_cut_mode == "post_icp_oblique_ratio": cropped_images, distal_crop_face_trans, shaft_crop = crop_vtk_images_to_oblique_post_icp_ratio( @@ -2713,6 +2855,7 @@ def sidewaysFallFe(args): images_to_crop, mask_trans, retained_length_mm=femur_greater_trochanter_distal_length, + gt_inclusion_length_mm=femur_greater_trochanter_inclusion_length, labels={1}, ) else: @@ -2727,18 +2870,25 @@ def sidewaysFallFe(args): sys.exit(1) image_trans = cropped_images[0] mask_trans = cropped_images[1] + next_crop_index = 2 if compartment_trans is not None: - compartment_trans = cropped_images[2] + compartment_trans = cropped_images[next_crop_index] + next_crop_index += 1 + if pistoia_mask_trans is not None: + pistoia_mask_trans = cropped_images[next_crop_index] retained_length_mm = shaft_crop.get("target_length_mm", shaft_crop["retained_length_mm"]) shaft_crop["pre_icp_crop"] = bbox_crop_meta if femur_cut_mode == "greater_trochanter_length": ogo.message( - "Post-ICP GT-length crop retained z=%8.4f below GT z=%8.4f; cut z=%8.4f; available=%8.4f; slices xyz=%s." + "Post-ICP GT-length crop retained shaft z=%8.4f below GT-disk origin z=%8.4f; " + "GT center z=%8.4f; GT disk distal z=%8.4f; cut z=%8.4f; available=%8.4f; slices xyz=%s." % ( retained_length_mm, + shaft_crop["distal_length_origin_z"], shaft_crop["greater_trochanter_z"], + shaft_crop["greater_trochanter_disk_distal_z"], shaft_crop["cut_z_mm"], - shaft_crop["available_below_gt_mm"], + shaft_crop["available_below_gt_disk_mm"], shaft_crop["crop_slices_xyz"], ) ) @@ -2787,6 +2937,8 @@ def sidewaysFallFe(args): ) if compartment_trans is not None: compartment_trans = flat_crop_vtk_image_below_z(compartment_trans, shaft_crop["cut_z"]) + if pistoia_mask_trans is not None: + pistoia_mask_trans = flat_crop_vtk_image_below_z(pistoia_mask_trans, shaft_crop["cut_z"]) # ogo.message("Writing out temp images...") # ogo.writeNii(image_trans, "temp_image.nii", image_pathname) @@ -2833,6 +2985,11 @@ def sidewaysFallFe(args): # Cast the image to Short to "round" float values to nearest whole number cast_image = ogo.cast2short(binned_image) cast_mask = ogo.cast2unsignchar(mask_trans) + pistoia_mask_on_model_grid = ( + ogo.prepareFiniteElementImage(ogo.cast2unsignchar(pistoia_mask_trans)) + if pistoia_mask_trans is not None + else None + ) # Apply the mask to the bone @@ -2873,6 +3030,11 @@ def sidewaysFallFe(args): femur_mask_on_model_grid, model_cut_z, ) + if pistoia_mask_on_model_grid is not None: + pistoia_mask_on_model_grid = flat_crop_vtk_image_below_z( + pistoia_mask_on_model_grid, + model_cut_z, + ) ogo.message( "Applied model-grid distal shaft cut z=%8.4f; model mask z coverage [%8.4f, %8.4f]" % (model_cut_z, model_z_min, model_z_max) @@ -2921,12 +3083,9 @@ def sidewaysFallFe(args): # Create PMMA disks from bbox-relative contact planes. The disk normal # points from the contact plane toward the anatomy; the generated PMMA # image clears any femur-mask voxels so PMMA never occupies the segmentation. - femoral_head_plane = bbox_relative_fixture_plane( + femoral_head_plane = proximal_sideways_fall_fixture_plane( model_bounds, center_fraction=FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, - size_fraction=SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, - projection_axis="y", - shape=SIDEWAYS_FALL_FIXTURE_SHAPE, ) femoral_head_cap_direction = bbox_relative_fixture_direction( FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, @@ -2946,12 +3105,9 @@ def sidewaysFallFe(args): ## # Creates the greater trochanter pmma cap - greater_trochanter_plane = bbox_relative_fixture_plane( + greater_trochanter_plane = proximal_sideways_fall_fixture_plane( model_bounds, center_fraction=GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, - size_fraction=SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, - projection_axis="y", - shape=SIDEWAYS_FALL_FIXTURE_SHAPE, ) greater_trochanter_cap_direction = bbox_relative_fixture_direction( GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, @@ -2997,6 +3153,8 @@ def sidewaysFallFe(args): images_to_fit = [change, femur_mask_on_model_grid] if distal_crop_face_change is not None: images_to_fit.append(distal_crop_face_change) + if pistoia_mask_on_model_grid is not None: + images_to_fit.append(pistoia_mask_on_model_grid) fitted_images, contact_fit = fit_vtk_images_to_physical_bounds( images_to_fit, desired_bounds=desired_bounds, @@ -3006,6 +3164,11 @@ def sidewaysFallFe(args): femur_mask_on_model_grid = fitted_images[1] if distal_crop_face_change is not None: distal_crop_face_change = fitted_images[2] + next_fit_index = 3 + else: + next_fit_index = 2 + if pistoia_mask_on_model_grid is not None: + pistoia_mask_on_model_grid = fitted_images[next_fit_index] model_bounds = foreground_voxel_center_bounds(femur_mask_on_model_grid) ogo.message( "Fit model canvas to projected PMMA contact bounds: output_extent=%s" @@ -3289,6 +3452,14 @@ def sidewaysFallFe(args): model2.AppendHistory( "Created by %s version %s." % (script_name, script_version)) + if pistoia_mask_on_model_grid is not None: + mask_sidecar = pistoia_mask_output_path(N88_fileName) + ogo.message("Writing model-space Pistoia ROI mask: %s" % mask_sidecar) + write_vtk_image_with_sitk_geometry( + ogo.cast2unsignchar(pistoia_mask_on_model_grid), + mask_sidecar, + ) + ## # Write out n88model file ogo.message("Writing out n88model file: %s" % N88_fileName) @@ -3380,7 +3551,7 @@ def main(): help="Retained proximal femur length [mm] for --femur_cut_mode fixed_length. (default: %(default)s [mm])") parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "proximal_box_ratio", "post_icp_oblique_ratio", "post_icp_flat_ratio", "greater_trochanter_length", "lesser_trochanter", "fixed_length"], default=DEFAULT_FEMUR_CUT_MODE, - help="Set the distal femur crop. post_icp_oblique_ratio uses a fixed rough pre-ICP crop, then an angled final ratio crop after ICP following the transformed rough-crop face. post_icp_flat_ratio keeps the flat aligned-frame comparison path. greater_trochanter_length uses the rough crop only for ICP, then crops the transformed full scan to a fixed distance distal to the detected greater trochanter. (default: %(default)s)") + help="Set the distal femur crop. post_icp_oblique_ratio uses a fixed rough pre-ICP crop, then an angled final ratio crop after ICP following the transformed rough-crop face. post_icp_flat_ratio keeps the flat aligned-frame comparison path. greater_trochanter_length uses the rough crop only for ICP, then crops the transformed full scan to a fixed shaft length distal to the detected GT-disk distal edge. (default: %(default)s)") parser.add_argument("--femur_bbox_ratio", nargs=3, default=DEFAULT_FEMUR_BBOX_RATIO, help="BBox-ratio crop in recipe order: reference constrained free. Use 'none' for a free axis. (default: %(default)s)") parser.add_argument("--femur_bbox_crop_from", nargs=3, default=DEFAULT_FEMUR_BBOX_CROP_FROM, @@ -3388,7 +3559,9 @@ def main(): parser.add_argument("--femur_experimental_crop_ratio", type=float, default=DEFAULT_FEMUR_EXPERIMENTAL_RATIO, help="Retained proximal femur length as a multiple of the proximal transverse max(x, y) width in proximal_box_ratio mode. (default: %(default)s)") parser.add_argument("--femur_greater_trochanter_distal_length", type=float, default=DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, - help="Retained femur length [mm] distal to the detected greater-trochanter level in greater_trochanter_length mode. (default: %(default)s [mm])") + help="Retained shaft length [mm] distal to the detected GT-disk distal edge in greater_trochanter_length mode. (default: %(default)s [mm])") + parser.add_argument("--femur_greater_trochanter_inclusion_length", type=float, default=DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, + help="Optional extra distal offset [mm] below the detected GT-disk distal edge before measuring --femur_greater_trochanter_distal_length. The default makes the disk distal edge 0 mm. (default: %(default)s [mm])") parser.add_argument("--femur_proximal_reference_distance", type=float, default=DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, help="Proximal-most distance [mm] used to estimate transverse reference width in proximal_box_ratio mode. (default: %(default)s)") parser.add_argument("--femur_proximal_reference_width", choices=["max_xy", "rms_xy"], default=DEFAULT_FEMUR_PROXIMAL_REFERENCE_WIDTH, @@ -3405,6 +3578,8 @@ def main(): help="Optional path to write the estimated femur ICP transform JSON. (default: %(default)s)") parser.add_argument("--compartment_mask", type=str, default=None, help="Optional trabecular/cortical compartment mask aligned with the bone mask. Defaults: cortical=1, trabecular=2.") + parser.add_argument("--pistoia_mask", type=str, default=None, + help="Optional ROI mask aligned with the input image for model-space masked Pistoia reporting.") parser.add_argument("--cortical_label", type=int, default=DEFAULT_CORTICAL_LABEL, help="Label value for cortical bone in --compartment_mask. (default: %(default)s)") parser.add_argument("--trabecular_label", type=int, default=DEFAULT_TRABECULAR_LABEL, diff --git a/ogo/fea/image_io.py b/ogo/fea/image_io.py new file mode 100644 index 0000000..6fc7c89 --- /dev/null +++ b/ogo/fea/image_io.py @@ -0,0 +1,20 @@ +"""Image I/O helpers for FE sidecar files.""" + +from pathlib import Path + + +def write_vtk_image_with_sitk_geometry(vtk_image, output_file): + """Write a VTK image with spacing/origin preserved for SimpleITK readers.""" + import SimpleITK as sitk + from vtk.util.numpy_support import vtk_to_numpy + + output_file = Path(output_file) + dims = vtk_image.GetDimensions() + data = vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape(dims, order="F") + sitk_image = sitk.GetImageFromArray(data.transpose(2, 1, 0)) + sitk_image.SetSpacing(tuple(float(v) for v in vtk_image.GetSpacing())) + sitk_image.SetOrigin(tuple(float(v) for v in vtk_image.GetOrigin())) + sitk_image.SetDirection((1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) + output_file.parent.mkdir(parents=True, exist_ok=True) + sitk.WriteImage(sitk_image, str(output_file)) + return output_file diff --git a/ogo/fea/model.py b/ogo/fea/model.py index 713ab86..01f6eda 100644 --- a/ogo/fea/model.py +++ b/ogo/fea/model.py @@ -13,18 +13,29 @@ def log_fe_arguments(**kwargs): ogo.message("======================================") -def find_and_add_visible_nodes(model, bc_geometry, normal_vector, bone_material_id, node_set_name): - """Find exterior nodes facing a direction and attach them as a model node set.""" +def visible_node_ids_by_geometry(bc_geometry, normal_vector, bone_material_id, node_set_name): + """Return exterior nodes facing a direction on a material geometry.""" import vtk import vtkbone - import ogo.util.Helper as ogo - visible_nodes_ids = vtk.vtkIdTypeArray() vtkbone.vtkboneNodeSetsByGeometry.FindNodesOnVisibleSurface( visible_nodes_ids, bc_geometry, normal_vector, bone_material_id ) visible_nodes_ids.SetName(node_set_name) + return visible_nodes_ids + + +def find_and_add_visible_nodes(model, bc_geometry, normal_vector, bone_material_id, node_set_name): + """Find exterior nodes facing a direction and attach them as a model node set.""" + import ogo.util.Helper as ogo + + visible_nodes_ids = visible_node_ids_by_geometry( + bc_geometry, + normal_vector, + bone_material_id, + node_set_name, + ) model.AddNodeSet(visible_nodes_ids) ogo.message(f"Found {visible_nodes_ids.GetNumberOfTuples()} visible nodes for {node_set_name}.") return model @@ -316,6 +327,86 @@ def key(point): return node_ids +def nearest_coordinate_face_node_ids_from_voxel_mask( + model, + selected_vtk, + *, + direction, + name=None, + tolerance=1.0e-4, + max_distance_mm=None, +): + """Return model nodes on the nearest coordinate plane to a label-mask face.""" + import numpy as np + import vtk + + from ogo.util.vtk_image import vtk_image_to_numpy + + selected = vtk_image_to_numpy(selected_vtk) != 0 + if not np.any(selected): + node_ids = vtk.vtkIdTypeArray() + if name is not None: + node_ids.SetName(str(name)) + return node_ids + + spacing = np.asarray(selected_vtk.GetSpacing(), dtype=np.float64) + origin = np.asarray(selected_vtk.GetOrigin(), dtype=np.float64) + extent = selected_vtk.GetExtent() + offset = np.asarray([extent[0], extent[2], extent[4]], dtype=np.float64) + vector = np.asarray(direction, dtype=np.float64) + if vector.shape != (3,) or not np.all(np.isfinite(vector)) or not np.any(vector): + raise ValueError("direction must be a finite non-zero 3-vector.") + axis = int(np.argmax(np.abs(vector))) + side = 1.0 if vector[axis] >= 0.0 else -1.0 + coords = np.argwhere(selected).astype(np.float64) + centers = origin + (coords + offset) * spacing + target = ( + np.max(centers[:, axis] + spacing[axis] / 2.0) + if side > 0.0 + else np.min(centers[:, axis] - spacing[axis] / 2.0) + ) + lateral_axes = [idx for idx in range(3) if idx != axis] + lower = np.min(centers[:, lateral_axes] - spacing[lateral_axes] / 2.0, axis=0) + upper = np.max(centers[:, lateral_axes] + spacing[lateral_axes] / 2.0, axis=0) + band = ( + float(max_distance_mm) + if max_distance_mm is not None + else max(float(spacing[axis]) * 1.25, float(tolerance)) + ) + lateral_margin = np.maximum(spacing[lateral_axes], float(tolerance)) + + candidates = [] + for point_id in range(model.GetNumberOfPoints()): + point = np.asarray(model.GetPoint(point_id), dtype=np.float64) + if abs(float(point[axis] - target)) > band: + continue + lateral = point[lateral_axes] + if np.any(lateral < lower - lateral_margin) or np.any(lateral > upper + lateral_margin): + continue + candidates.append((int(point_id), float(point[axis]))) + + node_ids = vtk.vtkIdTypeArray() + if name is not None: + node_ids.SetName(str(name)) + if not candidates: + return node_ids + + planes = {} + plane_tolerance = max(float(tolerance), 1.0e-3) + for point_id, coordinate in candidates: + key = round(coordinate / plane_tolerance) + planes.setdefault(key, []).append((point_id, coordinate)) + chosen_plane = min( + planes.values(), + key=lambda values: abs( + sum(coordinate for _, coordinate in values) / len(values) - target + ), + ) + for point_id, _ in sorted(chosen_plane): + node_ids.InsertNextValue(point_id) + return node_ids + + def directional_face_node_ids_from_label_image( model, labels_vtk, @@ -336,7 +427,16 @@ def directional_face_node_ids_from_label_image( labels_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR, ) - return directional_face_node_ids_from_voxel_mask( + node_ids = directional_face_node_ids_from_voxel_mask( + model, + selected_vtk, + direction=direction, + name=name, + tolerance=tolerance, + ) + if node_ids.GetNumberOfTuples() > 0: + return node_ids + return nearest_coordinate_face_node_ids_from_voxel_mask( model, selected_vtk, direction=direction, @@ -452,6 +552,17 @@ def create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, name=top_node_set_name, tolerance=bc_filter_tolerance, ) + if top_nodes.GetNumberOfTuples() == 0: + ogo.message( + f"No exact outer-face nodes found for {top_node_set_name}; " + "falling back to visible PMMA cap nodes." + ) + top_nodes = visible_node_ids_by_geometry( + temp_bc_mesh, + top_direction, + top_node_set_id, + top_node_set_name, + ) bottom_nodes = directional_face_node_ids_from_label_image( model, bottom_boundary_mask_image, @@ -460,6 +571,17 @@ def create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, name=bottom_node_set_name, tolerance=bc_filter_tolerance, ) + if bottom_nodes.GetNumberOfTuples() == 0: + ogo.message( + f"No exact outer-face nodes found for {bottom_node_set_name}; " + "falling back to visible PMMA cap nodes." + ) + bottom_nodes = visible_node_ids_by_geometry( + temp_bc_mesh, + bottom_direction, + bottom_node_set_id, + bottom_node_set_name, + ) model.AddNodeSet(top_nodes) model.AddNodeSet(bottom_nodes) ogo.message( @@ -476,9 +598,7 @@ def create_microfe_model(image_with_pads, boundary_masks_with_pads, bin_centers, model, temp_bc_mesh, bottom_direction, bottom_node_set_id, bottom_node_set_name ) - if filter_bc_node_sets and ( - top_boundary_mask_image is None or bottom_boundary_mask_image is None - ): + if filter_bc_node_sets: model = filter_node_set_to_dominant_coordinate_plane( model, top_node_set_name, diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index be3782f..c743927 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -16,7 +16,8 @@ only when at least one input spacing dimension is coarser than 2 mm. Then generate fixed-thickness anatomy PMMA caps on the superior and inferior body surfaces. The maintained cap thickness is 10 mm and the intrusion depth is - 6 mm. + 6 mm; projected contact starts at the first stable body footprint so small + osteophytes do not define the support plane. 6. Build materials with the same shared bone/PMMA material-table helper used by the femur workflow. The spine convention is trabecular material IDs 1..128 and cortical IDs 129..256; PMMA is a separate material ID. @@ -32,6 +33,15 @@ DEFAULT_SPINE_ISO_RESOLUTION_MM = 1.0 DEFAULT_SPINE_PMMA_THICKNESS_MM = 10 DEFAULT_SPINE_PMMA_INTRUSION_MM = 6 +DEFAULT_SPINE_STABLE_CONTACT_ENABLED = True +DEFAULT_SPINE_STABLE_CONTACT_FRACTION = 0.55 +DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION = 0.12 +DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM = 24.0 +DEFAULT_SPINE_STABLE_CONTACT_INTRUSION_MM = 2 +DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D = True +DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_MAX_GAP = 2 +DEFAULT_SPINE_PROCESS_QC_MAX_AXIAL_TO_TRANSVERSE_RATIO = 0.75 +DEFAULT_SPINE_PROCESS_QC_AXIAL_TOLERANCE_MM = 2.0 DEFAULT_SPINE_PMMA_MATERIAL_ID = 5000 DEFAULT_SPINE_POISSONS_RATIO = 0.3 DEFAULT_SPINE_PMMA_E_MPA = 2500 @@ -48,6 +58,7 @@ DEFAULT_SPINE_REGISTRATION_MIN_SCALE = "0.8,0.8,0.75" DEFAULT_SPINE_REGISTRATION_MAX_SCALE = "1.2,1.2,1.3" DEFAULT_SPINE_REGISTRATION_BACKEND = "vtk" +DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC = True DEFAULT_SPINE_REFERENCE_FILENAME = "L4_BODY_SPINE_COMPRESSION_REF.vtk" SPINE_CONTACT_SIZE_FRACTION = (1.6, 1.6) SPINE_SUPERIOR_CONTACT_CENTER_FRACTION = (0.5, 0.5, 1.05) @@ -88,6 +99,97 @@ def default_spine_reference_path(): return Path(__file__).resolve().parents[1] / "dat" / DEFAULT_SPINE_REFERENCE_FILENAME +def pistoia_mask_output_path(output_file): + """Return the model-space Pistoia ROI mask sidecar path for a model.""" + output_path = Path(output_file) + return output_path.with_name(f"{output_path.stem}_pistoia_mask.nii.gz") + + +def _mask_centroid_physical(vtk_image): + """Return the physical centroid of nonzero voxels in a VTK image.""" + mask = vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape( + vtk_image.GetDimensions(), + order="F", + ) > 0 + coords = np.argwhere(mask) + if coords.size == 0: + raise ValueError("mask is empty") + origin = np.asarray(vtk_image.GetOrigin(), dtype=float) + spacing = np.asarray(vtk_image.GetSpacing(), dtype=float) + extent = vtk_image.GetExtent() + extent_start = np.asarray((extent[0], extent[2], extent[4]), dtype=float) + return origin + (coords.mean(axis=0) + extent_start) * spacing + + +def check_spine_process_orientation( + body_vtk_image, + process_vtk_image, + *, + axial_axis="z", + max_axial_to_transverse_ratio=DEFAULT_SPINE_PROCESS_QC_MAX_AXIAL_TO_TRANSVERSE_RATIO, + axial_tolerance_mm=DEFAULT_SPINE_PROCESS_QC_AXIAL_TOLERANCE_MM, +): + """Raise when the posterior process is oriented along the compression axis.""" + axis_index = {"x": 0, "y": 1, "z": 2}[str(axial_axis).lower()] + body_centroid = _mask_centroid_physical(body_vtk_image) + process_centroid = _mask_centroid_physical(process_vtk_image) + offset = process_centroid - body_centroid + axial_offset = abs(float(offset[axis_index])) + transverse_offset = float(np.linalg.norm(np.delete(offset, axis_index))) + limit = max(float(axial_tolerance_mm), float(max_axial_to_transverse_ratio) * transverse_offset) + metrics = { + "status": "ok", + "body_centroid": body_centroid.tolist(), + "process_centroid": process_centroid.tolist(), + "offset_mm": offset.tolist(), + "axial_axis": str(axial_axis).lower(), + "axial_offset_mm": axial_offset, + "transverse_offset_mm": transverse_offset, + "max_axial_to_transverse_ratio": float(max_axial_to_transverse_ratio), + "axial_tolerance_mm": float(axial_tolerance_mm), + } + if axial_offset > limit: + metrics["status"] = "failed" + raise ValueError( + "posterior process orientation QC failed: " + f"axial_offset_mm={axial_offset:.3f}, " + f"transverse_offset_mm={transverse_offset:.3f}, " + f"axial_axis={str(axial_axis).lower()}" + ) + return metrics + + +def spine_process_orientation_metrics_for_rotation( + body_centroid, + process_centroid, + reference_to_sample_rotation, + *, + axial_axis="z", + max_axial_to_transverse_ratio=DEFAULT_SPINE_PROCESS_QC_MAX_AXIAL_TO_TRANSVERSE_RATIO, + axial_tolerance_mm=DEFAULT_SPINE_PROCESS_QC_AXIAL_TOLERANCE_MM, +): + """Return process-orientation QC metrics for a reference-to-sample rotation.""" + axis_index = {"x": 0, "y": 1, "z": 2}[str(axial_axis).lower()] + offset = np.asarray(process_centroid, dtype=float) - np.asarray(body_centroid, dtype=float) + rotation = np.asarray(reference_to_sample_rotation, dtype=float) + if rotation.shape != (3, 3): + raise ValueError("reference_to_sample_rotation must have shape (3, 3).") + reference_offset = offset @ rotation + axial_offset = abs(float(reference_offset[axis_index])) + transverse_offset = float(np.linalg.norm(np.delete(reference_offset, axis_index))) + limit = max(float(axial_tolerance_mm), float(max_axial_to_transverse_ratio) * transverse_offset) + status = "failed" if axial_offset > limit else "ok" + return { + "status": status, + "offset_mm": reference_offset.tolist(), + "axial_axis": str(axial_axis).lower(), + "axial_offset_mm": axial_offset, + "transverse_offset_mm": transverse_offset, + "max_axial_to_transverse_ratio": float(max_axial_to_transverse_ratio), + "axial_tolerance_mm": float(axial_tolerance_mm), + } + + def benchmark_linear_params(): """Return the linear spineFE-benchmark model parameters.""" return { @@ -261,6 +363,7 @@ def find_spinefe_benchmark_dir(start_dir=None, env_var="SPINEFE_BENCHMARK_DIR"): smooth_binary_mask_vtk, smooth_label_mask_vtk, ) +from ogo.fea.image_io import write_vtk_image_with_sitk_geometry from ogo.fea.model import ( apply_spine_boundary_conditions as apply_boundary_conditions, create_microfe_model, @@ -574,7 +677,16 @@ def _expand_bounding_box_by_margin(bb, image_data, margin_mm): ] -def crop_and_transform(fullvertebra, body_output, process_output, image, label_output=None, *, margin_mm=0.0): +def crop_and_transform( + fullvertebra, + body_output, + process_output, + image, + label_output=None, + *, + margin_mm=0.0, + extra_label_outputs=None, +): _, bb = crop_to_bounding_box(fullvertebra.GetOutput()) if float(margin_mm) > 0.0: @@ -582,10 +694,15 @@ def crop_and_transform(fullvertebra, body_output, process_output, image, label_o isolated_vertebra, _ = crop_to_bounding_box(body_output, bb) isolated_process, _ = crop_to_bounding_box(process_output, bb) isolated_image, _ = crop_to_bounding_box(image, bb) + out = [isolated_vertebra, isolated_process, isolated_image] if label_output is None: - return isolated_vertebra, isolated_process, isolated_image + return tuple(out) isolated_label, _ = crop_to_bounding_box(label_output, bb) - return isolated_vertebra, isolated_process, isolated_image, isolated_label + out.append(isolated_label) + for extra_label_output in extra_label_outputs or []: + isolated_extra_label, _ = crop_to_bounding_box(extra_label_output, bb) + out.append(isolated_extra_label) + return tuple(out) def perform_marching_cubes(body_output): mcubes = vtk.vtkImageMarchingCubes() @@ -597,12 +714,100 @@ def perform_marching_cubes(body_output): return mcubes +def _principal_axes_from_points(points): + centered = np.asarray(points, dtype=float) - np.asarray(points, dtype=float).mean(axis=0) + _, _, vh = np.linalg.svd(centered, full_matrices=False) + axes = vh.T + if np.linalg.det(axes) < 0: + axes[:, -1] *= -1 + return axes + + +def _proper_signed_axis_permutation_matrices(): + matrices = [] + for permutation in [ + (0, 1, 2), + (0, 2, 1), + (1, 0, 2), + (1, 2, 0), + (2, 0, 1), + (2, 1, 0), + ]: + for signs in [ + (-1, -1, -1), + (-1, -1, 1), + (-1, 1, -1), + (-1, 1, 1), + (1, -1, -1), + (1, -1, 1), + (1, 1, -1), + (1, 1, 1), + ]: + matrix = np.zeros((3, 3), dtype=float) + for row, col in enumerate(permutation): + matrix[row, col] = signs[row] + if np.linalg.det(matrix) > 0: + matrices.append(matrix) + return matrices + + +def _initial_transform_from_rotation(moving_points, fixed_points, rotation): + moving_center = np.asarray(moving_points, dtype=float).mean(axis=0) + fixed_center = np.asarray(fixed_points, dtype=float).mean(axis=0) + return { + "rotation": np.asarray(rotation, dtype=float), + "translation": fixed_center - np.asarray(rotation, dtype=float) @ moving_center, + } + + +def _spine_pca_initial_transforms(reference_points, sample_points): + moving_axes = _principal_axes_from_points(reference_points) + fixed_axes = _principal_axes_from_points(sample_points) + candidates = [] + seen = set() + for axis_rotation in _proper_signed_axis_permutation_matrices(): + rotation = fixed_axes @ axis_rotation @ moving_axes.T + if np.linalg.det(rotation) < 0: + continue + key = tuple(np.round(rotation, 8).ravel()) + if key in seen: + continue + seen.add(key) + candidates.append(_initial_transform_from_rotation(reference_points, sample_points, rotation)) + return candidates + + +def _icp_transform_to_vtk(transform): + matrix = vtk.vtkMatrix4x4() + matrix.Identity() + rotation = transform["rotation"] + translation = transform["translation"] + for row in range(3): + for col in range(3): + matrix.SetElement(row, col, float(rotation[row, col])) + matrix.SetElement(row, 3, float(translation[row])) + + vtk_transform = vtk.vtkTransform() + vtk_transform.SetMatrix(matrix) + return vtk_transform + + +def _icp_process_orientation_metrics(transform, body_centroid, process_centroid): + return spine_process_orientation_metrics_for_rotation( + body_centroid, + process_centroid, + transform["rotation"], + ) + + def get_icp_with_scaling( body, reference_path, + process=None, scale_factors=None, min_scale=(0.8, 0.8, 0.75), max_scale=(1.2, 1.2, 1.3), + retry_on_process_qc=DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC, ): sample_surface_points = surface_points_from_vtk_mask( @@ -644,17 +849,69 @@ def get_icp_with_scaling( distance_mode="mean", ) - matrix = vtk.vtkMatrix4x4() - matrix.Identity() - rotation = transform["rotation"] - translation = transform["translation"] - for row in range(3): - for col in range(3): - matrix.SetElement(row, col, float(rotation[row, col])) - matrix.SetElement(row, 3, float(translation[row])) - - vtk_transform = vtk.vtkTransform() - vtk_transform.SetMatrix(matrix) + if process is not None and retry_on_process_qc: + body_centroid = _mask_centroid_physical(body.GetOutput()) + process_centroid = _mask_centroid_physical(process.GetOutput()) + default_process_qc = _icp_process_orientation_metrics( + transform, + body_centroid, + process_centroid, + ) + ogo.message( + "Default ICP process-vector QC: " + f"axial_offset={default_process_qc['axial_offset_mm']:.2f} mm, " + f"transverse_offset={default_process_qc['transverse_offset_mm']:.2f} mm" + ) + if default_process_qc["status"] != "ok": + ogo.message("Default ICP failed process-vector QC; trying alternate PCA starts...") + retry_candidates = [] + for initial in _spine_pca_initial_transforms(reference_points * scale_factors, sample_surface_points): + candidate = estimate_rigid_icp( + moving_points=reference_points * scale_factors, + fixed_points=sample_surface_points, + iterations=50, + tolerance=1.0e-4, + start_by_matching_centroids_only=False, + convergence="delta", + distance_mode="mean", + initial_transform=initial, + ) + process_qc = _icp_process_orientation_metrics( + candidate, + body_centroid, + process_centroid, + ) + retry_candidates.append((candidate, process_qc)) + valid = [ + (candidate, process_qc) + for candidate, process_qc in retry_candidates + if process_qc["status"] == "ok" + ] + if valid: + transform, chosen_qc = min(valid, key=lambda item: item[0]["mean_distance"]) + ogo.message( + "Selected alternate ICP start with process-vector QC: " + f"axial_offset={chosen_qc['axial_offset_mm']:.2f} mm, " + f"transverse_offset={chosen_qc['transverse_offset_mm']:.2f} mm, " + f"mean_distance={transform['mean_distance']:.4f}" + ) + else: + best_candidate, best_qc = min( + retry_candidates, + key=lambda item: ( + item[1]["axial_offset_mm"] / max(item[1]["transverse_offset_mm"], 1.0e-6), + item[0]["mean_distance"], + ), + ) + ogo.message( + "No alternate ICP start passed process-vector QC; " + "using least-bad candidate for downstream QC failure: " + f"axial_offset={best_qc['axial_offset_mm']:.2f} mm, " + f"transverse_offset={best_qc['transverse_offset_mm']:.2f} mm" + ) + transform = best_candidate + + vtk_transform = _icp_transform_to_vtk(transform) ogo.message( "ICP (with scaled reference) iterations=%d mean_distance=%0.4f" % (transform["iterations"], transform["mean_distance"]) @@ -1093,15 +1350,21 @@ def get_mask_np(vtk_image): def process_vertebra(input_mask, input_image, n88model_output_path, body_label, process_label, reference_path, **kwargs): pmma_mat_id = kwargs.get("pmma_mat_id", 5000) + pistoia_mask = kwargs.get("pistoia_mask") iso_resolution = kwargs.get("iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM) pmma_thick = kwargs.get("pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM) pmma_intrusion = kwargs.get("pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM) top_node_set_id = kwargs.get("top_node_set_id", DEFAULT_SPINE_TOP_NODE_SET_ID) bottom_node_set_id = kwargs.get("bottom_node_set_id", DEFAULT_SPINE_BOTTOM_NODE_SET_ID) quality_control = kwargs.get("quality_control", True) + process_orientation_qc = kwargs.get("process_orientation_qc", True) registration_scale = kwargs.get("registration_scale", DEFAULT_SPINE_REGISTRATION_SCALE) registration_min_scale = kwargs.get("registration_min_scale", DEFAULT_SPINE_REGISTRATION_MIN_SCALE) registration_max_scale = kwargs.get("registration_max_scale", DEFAULT_SPINE_REGISTRATION_MAX_SCALE) + registration_retry_on_process_qc = kwargs.get( + "registration_retry_on_process_qc", + DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC, + ) mask_smoothing_spacing_threshold = kwargs.get( "mask_smoothing_spacing_threshold", DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, @@ -1118,6 +1381,13 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, image_reader = read(input_image) ogo.message(f"Reading mask...: {input_mask}") mask_reader = resample_to_match(image_reader.GetOutput(), read(input_mask).GetOutput()) + pistoia_mask_reader = None + if pistoia_mask is not None: + ogo.message(f"Reading Pistoia ROI mask...: {pistoia_mask}") + pistoia_mask_reader = resample_to_match( + image_reader.GetOutput(), + read(pistoia_mask).GetOutput(), + ) input_spacing = image_reader.GetOutput().GetSpacing() # Get and Check labels @@ -1133,19 +1403,19 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, # Crop everything to same BB ogo.message(f"Cropping to common bounding box...") - ( - isolated_vertebra, - isolated_process, - isolated_image, - isolated_labels, - ) = crop_and_transform( + cropped_outputs = crop_and_transform( fullvertebra, body.GetOutput(), process.GetOutput(), image_reader.GetOutput(), mask_reader.GetOutput(), margin_mm=SPINE_PREPROCESSING_CROP_MARGIN_MM, + extra_label_outputs=[] + if pistoia_mask_reader is None + else [pistoia_mask_reader.GetOutput()], ) + isolated_vertebra, isolated_process, isolated_image, isolated_labels = cropped_outputs[:4] + isolated_pistoia_mask = cropped_outputs[4] if pistoia_mask_reader is not None else None ogo.message("Resampling cropped spine inputs to isotropic spacing before ICP...") preprocessed_image = resample_vtk_image_to_spacing( @@ -1158,6 +1428,15 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, iso_resolution, interpolation="nearest", ) + preprocessed_pistoia_mask = ( + resample_vtk_image_to_spacing( + isolated_pistoia_mask.GetOutput(), + iso_resolution, + interpolation="nearest", + ) + if isolated_pistoia_mask is not None + else None + ) if label_smoothing_sigma_mm > 0: ogo.message( "smoothing isotropic label mask " @@ -1177,9 +1456,11 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, icp = get_icp_with_scaling( registration_body, reference_path, + process=isolated_process, scale_factors=registration_scale, min_scale=registration_min_scale, max_scale=registration_max_scale, + retry_on_process_qc=registration_retry_on_process_qc, ) # Transform Images and resample at the same time (less interpolation) @@ -1233,6 +1514,19 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, output_spacing=output_spacing, interpolation="bspline", ) + transformed_pistoia_mask = ( + resample_vtk_image_with_point_transform( + preprocessed_pistoia_mask, + rotation=sample_to_reference_rotation, + translation=sample_to_reference_translation, + output_origin=output_origin, + output_size=output_size, + output_spacing=output_spacing, + interpolation="nearest", + ) + if preprocessed_pistoia_mask is not None + else None + ) if should_smooth_resampled_mask(input_spacing, mask_smoothing_spacing_threshold): ogo.message( @@ -1247,6 +1541,17 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, f"{input_spacing} is <= {mask_smoothing_spacing_threshold} mm in all dimensions..." ) + if process_orientation_qc: + process_qc = check_spine_process_orientation( + transformed_vertebra, + transformed_process, + ) + ogo.message( + "posterior process orientation QC: " + f"axial_offset={process_qc['axial_offset_mm']:.2f} mm, " + f"transverse_offset={process_qc['transverse_offset_mm']:.2f} mm" + ) + ogo.message(f"relabelling mask and identifying boundary surfaces...") # Assuming mask1_data and mask2_data are your initial binary masks mask1_labeled = label_mask(transformed_vertebra, 2) # Apply label 1 to the first mask @@ -1281,10 +1586,23 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, padded_cort_mask = pad_vtk_image(cort_mask, axis='x', pmma_thick=pmma_thick, pad_value=0) padded_mask1 = pad_vtk_image(mask1_labeled.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) padded_mask2 = pad_vtk_image(mask2_labeled.GetOutput(), axis='x', pmma_thick=pmma_thick, pad_value=0) + padded_pistoia_mask = ( + pad_vtk_image(transformed_pistoia_mask, axis='x', pmma_thick=pmma_thick, pad_value=0) + if transformed_pistoia_mask is not None + else None + ) ogo.message( "generating fixed-thickness anatomy body-cap disks " - f"(pmma_thick = {pmma_thick} mm total, pmma_intrusion = {pmma_intrusion} mm)..." + f"(pmma_thick = {pmma_thick} mm total, pmma_intrusion = {pmma_intrusion} mm, " + f"stable_intrusion = {DEFAULT_SPINE_STABLE_CONTACT_INTRUSION_MM} mm, " + f"stable_close_gaps_3d = {DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D}, " + f"stable_contact = {DEFAULT_SPINE_STABLE_CONTACT_ENABLED})..." + ) + disk_intrusion = ( + DEFAULT_SPINE_STABLE_CONTACT_INTRUSION_MM + if DEFAULT_SPINE_STABLE_CONTACT_ENABLED + else pmma_intrusion ) transformed_body_bounds = foreground_voxel_center_bounds(padded_mask1) body_bounds = transformed_body_bounds @@ -1332,7 +1650,11 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, size=plane["size"], shape=plane["shape"], thickness=pmma_thick, - intrusion=pmma_intrusion, + intrusion=disk_intrusion, + stable_surface=DEFAULT_SPINE_STABLE_CONTACT_ENABLED, + stable_surface_fraction=DEFAULT_SPINE_STABLE_CONTACT_FRACTION, + stable_surface_min_area_fraction=DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION, + stable_surface_max_depth=DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM, ) for plane in (inferior_plane, superior_plane) ] @@ -1347,25 +1669,31 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, float(bounds_array[:, 4].min()), float(bounds_array[:, 5].max()), ) - ( + images_to_pad = [ padded_image, padded_transformed_image, padded_mask, padded_cort_mask, padded_mask1, padded_mask2, - ), contact_padding = pad_vtk_images_to_physical_bounds( - [ - padded_image, - padded_transformed_image, - padded_mask, - padded_cort_mask, - padded_mask1, - padded_mask2, - ], + ] + if padded_pistoia_mask is not None: + images_to_pad.append(padded_pistoia_mask) + padded_outputs, contact_padding = pad_vtk_images_to_physical_bounds( + images_to_pad, desired_bounds=desired_bounds, - constants=[0, 0, 0, 0, 0, 0], + constants=[0] * len(images_to_pad), ) + ( + padded_image, + padded_transformed_image, + padded_mask, + padded_cort_mask, + padded_mask1, + padded_mask2, + ) = padded_outputs[:6] + if padded_pistoia_mask is not None: + padded_pistoia_mask = padded_outputs[6] if any(contact_padding["lower"]) or any(contact_padding["upper"]): ogo.message( "expanded image canvas for projected disks: lower=%s upper=%s" @@ -1382,8 +1710,14 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, size=inferior_plane["size"], shape=inferior_plane["shape"], thickness=pmma_thick, - intrusion=pmma_intrusion, + intrusion=disk_intrusion, anatomy_constrained=True, + stable_surface=DEFAULT_SPINE_STABLE_CONTACT_ENABLED, + stable_surface_fraction=DEFAULT_SPINE_STABLE_CONTACT_FRACTION, + stable_surface_min_area_fraction=DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION, + stable_surface_max_depth=DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM, + close_gaps_3d=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D, + close_gaps_3d_max_gap=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_MAX_GAP, output_value=1, ) superior_disk = generate_projected_material_disk_vtk( @@ -1397,8 +1731,14 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, size=superior_plane["size"], shape=superior_plane["shape"], thickness=pmma_thick, - intrusion=pmma_intrusion, + intrusion=disk_intrusion, anatomy_constrained=True, + stable_surface=DEFAULT_SPINE_STABLE_CONTACT_ENABLED, + stable_surface_fraction=DEFAULT_SPINE_STABLE_CONTACT_FRACTION, + stable_surface_min_area_fraction=DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION, + stable_surface_max_depth=DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM, + close_gaps_3d=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D, + close_gaps_3d_max_gap=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_MAX_GAP, output_value=1, ) @@ -1440,6 +1780,14 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, n88model_output_path ) + if padded_pistoia_mask is not None: + mask_sidecar = pistoia_mask_output_path(n88model_output_path) + ogo.message(f"Writing model-space Pistoia ROI mask: {mask_sidecar}") + write_vtk_image_with_sitk_geometry( + padded_pistoia_mask, + mask_sidecar, + ) + # Quality control and output. If quality control is activated output only provided if it passes if quality_control: image_path = n88model_output_path.replace(".n88model", ".png") @@ -1503,6 +1851,8 @@ def main(): help="Label value for the vertebral process in the bone mask.") parser.add_argument("--output_path", type=str, default=None, help="Set output path for the N88 model file. (default: same as input image)") + parser.add_argument("--pistoia_mask", type=str, default=None, + help="Optional ROI mask aligned with the input image for model-space masked Pistoia reporting.") parser.add_argument("--quality_control", type=_as_bool, default=True, help="Set quality control flag (visualise output and add volume checks). (default: %(default)s)") parser.add_argument("--iso_resolution", type=float, default=DEFAULT_SPINE_ISO_RESOLUTION_MM, diff --git a/ogo/util/faim.py b/ogo/util/faim.py index c6c9415..570acc1 100644 --- a/ogo/util/faim.py +++ b/ogo/util/faim.py @@ -509,6 +509,10 @@ def write_results_csv( analysis_var, pistoia_vars, failure_axis, + masked_pistoia_file=None, + masked_pistoia_csv=None, + pistoia_mask_file=None, + masked_pistoia_stats=None, applied_displacement=None, target_displacement=0.2, report_profile="generic", @@ -516,8 +520,11 @@ def write_results_csv( ): """Write a compact, user-facing CSV with loads and stiffness.""" pistoia_meta = parse_pistoia_text(pistoia_file) + masked_pistoia_meta = parse_pistoia_text(masked_pistoia_file) loads = _read_single_row_csv(loads_csv) pistoia = _read_single_row_csv(pistoia_csv) + masked_pistoia = _read_single_row_csv(masked_pistoia_csv) + masked_pistoia_stats = masked_pistoia_stats or {} pistoia_vars = list(pistoia_vars or []) failure_var = pistoia_vars[0] if pistoia_vars else "" @@ -535,6 +542,12 @@ def write_results_csv( pistoia_failure_load_magnitude = ( abs(float(pistoia_failure_load)) if pistoia_failure_load != "" else "" ) + masked_pistoia_failure_load = _safe_float( + masked_pistoia.get(failure_var) or masked_pistoia_meta.get(failure_key) + ) + masked_pistoia_stiffness = _safe_float( + masked_pistoia.get(stiffness_var) or masked_pistoia_meta.get(stiffness_key) + ) # If the linear model was solved at a different prescribed displacement, # scale the reaction force to the requested reporting endpoint. @@ -585,6 +598,21 @@ def write_results_csv( "critical_ees": pistoia_meta.get("critical_ees", ""), "ees_at_crit_vol": pistoia_meta.get("ees_at_crit_vol", ""), } + masked_pistoia_common = {} + if pistoia_mask_file is not None: + masked_pistoia_common = { + "masked_pistoia_file": str(masked_pistoia_file), + "masked_pistoia_mask_file": str(pistoia_mask_file), + "masked_pistoia_failure_load_N": masked_pistoia_failure_load, + "masked_pistoia_stiffness_N_per_mm": masked_pistoia_stiffness, + "masked_pistoia_critical_volume_pct": masked_pistoia_meta.get("critical_volume_pct", ""), + "masked_pistoia_critical_ees": masked_pistoia_meta.get("critical_ees", ""), + "masked_pistoia_ees_at_crit_vol": masked_pistoia_meta.get("ees_at_crit_vol", ""), + "masked_pistoia_selected_elements": masked_pistoia_stats.get("selected_elements", ""), + "masked_pistoia_original_included_elements": masked_pistoia_stats.get( + "original_included_elements", "" + ), + } if profile == "femur": row = { "model_file": common["model_file"], @@ -595,6 +623,7 @@ def write_results_csv( "stiffness_N_per_mm": common["stiffness_N_per_mm"], "characteristic_length_mm": characteristic_length, **pistoia_common, + **masked_pistoia_common, } elif profile == "spine": row = { @@ -606,6 +635,7 @@ def write_results_csv( "stiffness_N_per_mm": common["stiffness_N_per_mm"], "characteristic_length_mm": characteristic_length, **pistoia_common, + **masked_pistoia_common, } else: row = { @@ -623,6 +653,7 @@ def write_results_csv( "critical_volume_pct": pistoia_meta.get("critical_volume_pct", ""), "critical_ees": pistoia_meta.get("critical_ees", ""), "ees_at_crit_vol": pistoia_meta.get("ees_at_crit_vol", ""), + **masked_pistoia_common, "warnings": " | ".join(warnings or []), } @@ -659,6 +690,7 @@ def run_faim_pipeline( critical_strain=0.007, exclude=5000, run_pistoia=True, + pistoia_mask_file=None, applied_displacement=None, target_displacement=0.2, report_profile="generic", @@ -671,6 +703,8 @@ def run_faim_pipeline( model_file = Path(model_file) output_prefix = Path(output_prefix) if output_prefix is not None else model_file.with_suffix("") output_prefix.parent.mkdir(parents=True, exist_ok=True) + pistoia_mask_file = Path(pistoia_mask_file) if pistoia_mask_file is not None else None + run_pistoia = run_pistoia or pistoia_mask_file is not None resolved_bin_dir = resolve_faim_bin_dir(install_root=install_root, bin_dir=bin_dir) env = build_faim_env(bin_dir=resolved_bin_dir, license_dir=license_dir) @@ -719,12 +753,15 @@ def run_faim_pipeline( analysis_file = output_prefix.with_name(output_prefix.name + "_analysis.txt") pistoia_file = output_prefix.with_name(output_prefix.name + "_pistoia.txt") + masked_pistoia_file = output_prefix.with_name(output_prefix.name + "_masked_pistoia.txt") loads_csv = output_prefix.with_name(output_prefix.name + "_loads.csv") pistoia_csv = output_prefix.with_name(output_prefix.name + "_pistoia.csv") + masked_pistoia_csv = output_prefix.with_name(output_prefix.name + "_masked_pistoia.csv") results_csv = output_prefix.with_name(output_prefix.name + "_results.csv") log_file = output_prefix.with_name(output_prefix.name + ".log") warnings = [] + masked_pistoia_stats = None # Check whether the model already contains a solution so re-runs are cheap. modelinfo_text = run_command( [modelinfo, "--solutions", str(model_file)], @@ -817,6 +854,25 @@ def run_faim_pipeline( if require_pistoia: raise warnings.append("Pistoia postprocessing failed.") + if pistoia_mask_file is not None: + try: + masked_pistoia_stats = run_pistoia_with_image_mask( + model_file=model_file, + mask_file=pistoia_mask_file, + output_file=masked_pistoia_file, + n88pistoia_command=pistoia, + critical_volume=critical_volume, + critical_strain=critical_strain, + exclude_material_id=exclude, + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + ) + except subprocess.CalledProcessError: + if require_pistoia: + raise + warnings.append("Masked Pistoia postprocessing failed.") # Tabulate the reaction force and Pistoia values into small CSV files. run_command( @@ -845,6 +901,24 @@ def run_faim_pipeline( env=env, dry_run=dry_run, ) + if pistoia_mask_file is not None: + run_command( + [ + tabulate, + "-H", + "-d", + ",", + "-V", + ",".join(str(v) for v in pistoia_vars), + str(masked_pistoia_file), + "-o", + str(masked_pistoia_csv), + ], + conda_env=conda_env, + conda_executable=conda_executable, + env=env, + dry_run=dry_run, + ) if compress: run_command( @@ -876,6 +950,10 @@ def run_faim_pipeline( analysis_var=analysis_var, pistoia_vars=pistoia_vars, failure_axis=failure_axis, + masked_pistoia_file=masked_pistoia_file if pistoia_mask_file is not None else None, + masked_pistoia_csv=masked_pistoia_csv if pistoia_mask_file is not None else None, + pistoia_mask_file=pistoia_mask_file, + masked_pistoia_stats=masked_pistoia_stats, applied_displacement=applied_displacement, target_displacement=target_displacement, report_profile=report_profile, diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index 762745c..70fdcc1 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -283,27 +283,79 @@ def test_spine_explicit_options_follow_preset_for_overrides(monkeypatch, solve_c assert calls[0].count("--fe_displacement") == 2 -def _metadata_args(tmp_path, no_solve=True): +def test_spine_forwards_pistoia_mask_to_builder(monkeypatch, tmp_path, solve_calls): + monkeypatch.chdir(tmp_path) + calls = [] + monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "spine", + "density.nii.gz", + "spine_mask.nii.gz", + "--output_path", + "models", + "--vertebra", + "L1:2:1", + "--pistoia_mask", + "vertebral_body_roi.nii.gz", + "--no-solve", + ] + ) + + assert "--pistoia_mask" in calls[0] + assert calls[0][calls[0].index("--pistoia_mask") + 1] == "vertebral_body_roi.nii.gz" + + +def test_hip_forwards_pistoia_mask_to_builder(monkeypatch, tmp_path, solve_calls): + monkeypatch.chdir(tmp_path) + calls = [] + monkeypatch.setattr(GenerateFEM, "run_femur_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "hip", + "density.nii.gz", + "left_femur.nii.gz", + "--output_path", + "models", + "--side", + "left", + "--pistoia_mask", + "femoral_neck.nii.gz", + "--no-solve", + ] + ) + + assert "--pistoia_mask" in calls[0] + assert calls[0][calls[0].index("--pistoia_mask") + 1] == "femoral_neck.nii.gz" + + +def _metadata_args(tmp_path, no_solve=True, **overrides): + data = { + "dry_run": False, + "calibrated_image": tmp_path / "density.nii.gz", + "bone_mask": tmp_path / "mask.nii.gz", + "output_path": tmp_path, + "skip_bc_audit": False, + "bc_audit_flat_tolerance": 1.0e-4, + "debug": False, + "no_solve": no_solve, + "target_displacement": None, + "exclude": 5000, + "critical_volume": None, + "critical_strain": None, + "no_compress": False, + "run_pistoia": False, + "require_pistoia": False, + "use_absolute_fe_displacement": False, + "pistoia_mask": None, + } + data.update(overrides) return type( "Args", (), - { - "dry_run": False, - "calibrated_image": tmp_path / "density.nii.gz", - "bone_mask": tmp_path / "mask.nii.gz", - "output_path": tmp_path, - "skip_bc_audit": False, - "bc_audit_flat_tolerance": 1.0e-4, - "debug": False, - "no_solve": no_solve, - "target_displacement": None, - "exclude": 5000, - "critical_volume": 2.0, - "critical_strain": 0.007, - "no_compress": False, - "run_pistoia": False, - "require_pistoia": False, - }, + data, )() @@ -322,14 +374,16 @@ def _solve_args(**overrides): "n88pistoia_command": None, "n88tabulate_command": None, "n88copymodel_command": None, - "critical_volume": 2.0, - "critical_strain": 0.007, + "critical_volume": None, + "critical_strain": None, "exclude": 5000, "no_compress": False, "run_pistoia": False, "require_pistoia": False, "dry_run": False, "target_displacement": None, + "use_absolute_fe_displacement": False, + "pistoia_mask": None, } data.update(overrides) return SimpleNamespace(**data) @@ -391,6 +445,44 @@ def test_solve_model_sets_femur_displacement_from_percent(monkeypatch, tmp_path) assert calls[0]["report_profile"] == "femur" assert calls[0]["target_displacement"] == 4.0 assert calls[0]["solve_displacement_percent"] == 4.0 + assert calls[0]["critical_volume"] == 7.0 + assert calls[0]["critical_strain"] == 0.009 + + +def test_solve_model_can_keep_absolute_femur_displacement(monkeypatch, tmp_path): + import ogo.util.faim as faim_module + + calls = [] + monkeypatch.setattr(faim_module, "run_faim_pipeline", lambda **kwargs: calls.append(kwargs)) + + GenerateFEM.solve_model( + tmp_path / "density_LT_FEMUR_SF.n88model", + _solve_args(use_absolute_fe_displacement=True), + "hip", + ["density.nii.gz", "mask.nii.gz", "--fe_displacement", "-2.56"], + ) + + assert calls[0]["report_profile"] == "femur" + assert calls[0]["applied_displacement"] == "-2.56" + assert calls[0]["target_displacement"] == 2.56 + assert calls[0]["solve_displacement_percent"] is None + + +def test_solve_model_keeps_explicit_pistoia_criteria(monkeypatch, tmp_path): + import ogo.util.faim as faim_module + + calls = [] + monkeypatch.setattr(faim_module, "run_faim_pipeline", lambda **kwargs: calls.append(kwargs)) + + GenerateFEM.solve_model( + tmp_path / "density_LT_FEMUR_SF.n88model", + _solve_args(critical_volume=2.0, critical_strain=0.007), + "hip", + ["density.nii.gz", "mask.nii.gz", "--fe_displacement", "-4.0"], + ) + + assert calls[0]["critical_volume"] == 2.0 + assert calls[0]["critical_strain"] == 0.007 def test_solve_model_can_enable_pistoia(monkeypatch, tmp_path): @@ -427,6 +519,26 @@ def test_require_pistoia_implies_pistoia_run(monkeypatch, tmp_path): assert calls[0]["require_pistoia"] is True +def test_pistoia_mask_implies_pistoia_and_sidecar_handoff(monkeypatch, tmp_path): + import ogo.util.faim as faim_module + + model = tmp_path / "density_LF.n88model" + mask = tmp_path / "density_LF_pistoia_mask.nii.gz" + mask.write_text("mask", encoding="utf-8") + calls = [] + monkeypatch.setattr(faim_module, "run_faim_pipeline", lambda **kwargs: calls.append(kwargs)) + + GenerateFEM.solve_model( + model, + _solve_args(pistoia_mask=Path("source_femoral_neck.nii.gz")), + "hip", + ["density.nii.gz", "left_femur.nii.gz", "--fe_displacement", "-4.0"], + ) + + assert calls[0]["run_pistoia"] is True + assert calls[0]["pistoia_mask_file"] == mask + + def test_option_value_accepts_equals_form(): assert ( GenerateFEM.option_value( @@ -443,6 +555,14 @@ def test_option_value_accepts_equals_form(): ) == 175.0 ) + assert ( + GenerateFEM.option_float( + ["--femur_greater_trochanter_inclusion_length=50"], + "--femur_greater_trochanter_inclusion_length", + 0.0, + ) + == 50.0 + ) def test_spine_modeling_metadata_records_materials_and_bcs(tmp_path): @@ -552,7 +672,7 @@ def test_femur_modeling_metadata_records_gt_length_study_mode(tmp_path): shaft = data["shaft_standardization"] assert shaft["cut_mode"] == "greater_trochanter_length" assert shaft["crop_stage"] == ( - "fixed rough crop before ICP for registration, final GT-relative flat crop after ICP on the full scan" + "fixed rough crop before ICP for registration, final GT-disk-relative flat crop after ICP on the full scan" ) assert shaft["rough_pre_icp_crop"] == { "enabled": True, @@ -560,7 +680,35 @@ def test_femur_modeling_metadata_records_gt_length_study_mode(tmp_path): "registration_only": True, } assert shaft["greater_trochanter_length"]["retained_length_mm"] == 175.0 + assert shaft["greater_trochanter_length"]["gt_inclusion_length_mm"] == 0.0 + assert shaft["greater_trochanter_length"]["length_origin"] == "detected greater-trochanter disk distal edge" assert shaft["cut_plane"] == ( - "flat post-ICP aligned-frame crop face at fixed distance below detected greater trochanter" + "flat post-ICP aligned-frame crop face at fixed shaft length below detected GT-disk distal edge" ) assert data["boundary_conditions"]["fixture_geometry"]["distal_shaft"]["support_fraction"] == 0.9 + + +def test_femur_modeling_metadata_records_absolute_displacement(tmp_path): + model = tmp_path / "density_LT_FEMUR_SF.n88model" + model.write_text("dummy") + argv = [ + "density.nii.gz", + "mask.nii.gz", + "--femur_side", + "1", + "--fe_displacement", + "-2.56", + ] + + path = GenerateFEM.write_modeling_metadata( + model, + "hip", + argv, + _metadata_args(tmp_path, use_absolute_fe_displacement=True), + ) + data = json.loads(path.read_text()) + + constraint = data["boundary_conditions"]["constraints"][0] + assert constraint["value_mm"] == -2.56 + assert constraint["target_displacement_percent"] is None + assert constraint["value_source"] == "explicit --fe_displacement used as absolute model displacement" diff --git a/tests/fea/test_boundary.py b/tests/fea/test_boundary.py index 0534c06..4ae2452 100644 --- a/tests/fea/test_boundary.py +++ b/tests/fea/test_boundary.py @@ -13,6 +13,7 @@ fit_vtk_images_to_physical_bounds, pad_vtk_images_to_physical_bounds, projected_material_disk_required_bounds, + projected_stable_surface_contact, smooth_label_mask_vtk, ) @@ -483,6 +484,61 @@ def test_projected_material_disk_uses_fixed_thickness_and_anatomy_intrusion(): assert not np.any(disk & active) +def test_projected_material_disk_closes_short_3d_gaps_after_extrusion(): + active = np.zeros((14, 14, 14), dtype=bool) + active[4:10, 5:9, 4:10] = True + active[7, :, 7] = False + + disk = generate_projected_material_disk_mask( + active, + spacing=(1, 1, 1), + origin=(0, 0, 0), + center=(7, 0, 7), + normal=(0, 1, 0), + u_axis=(1, 0, 0), + v_axis=(0, 0, 1), + size=(10, 10), + shape="square", + thickness=6, + intrusion=2, + anatomy_constrained=True, + stable_surface=True, + stable_surface_max_depth=8, + close_gaps_3d=True, + close_gaps_3d_max_gap=2, + ) + + assert np.flatnonzero(disk[7, :, 7]).tolist() == [1, 2, 3, 4] + assert not np.any(disk & active) + + +def test_projected_stable_surface_contact_ignores_small_osteophyte(): + active = np.zeros((14, 14, 14), dtype=bool) + active[4:10, 5:9, 4:10] = True + active[7, 2:5, 7] = True + + contact = projected_stable_surface_contact( + active, + spacing=(1, 1, 1), + origin=(0, 0, 0), + center=(7, 0, 7), + normal=(0, 1, 0), + u_axis=(1, 0, 0), + v_axis=(0, 0, 1), + size=(10, 10), + shape="square", + stable_surface_fraction=0.55, + stable_surface_min_area_fraction=0.12, + stable_surface_max_depth=8, + ) + + assert contact["first_distance"] == pytest.approx(2.0) + assert contact["stable_distance"] == pytest.approx(5.0) + assert contact["stable_depth"] == pytest.approx(3.0) + assert contact["first_area"] == 1 + assert contact["stable_area"] >= 30 + + def test_projected_material_disk_vtk_preserves_geometry_and_output_value(): vtk = pytest.importorskip("vtk") from ogo.fea.boundary import generate_projected_material_disk_vtk diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 3468e35..fd9a898 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -1,6 +1,7 @@ import pytest from ogo.fea import femur +from ogo.fea.image_io import write_vtk_image_with_sitk_geometry def _vtk_image_from_array(data, *, origin=(0, 0, 0), spacing=(1, 1, 1)): @@ -41,6 +42,30 @@ def test_sideways_fall_output_name_uses_compact_side_suffix(): assert femur.sideways_fall_output_name("density.n88model", 2) == "density_RF.n88model" +def test_pistoia_mask_output_path_uses_model_stem(tmp_path): + assert ( + femur.pistoia_mask_output_path(tmp_path / "case_LF.n88model") + == tmp_path / "case_LF_pistoia_mask.nii.gz" + ) + + +def test_write_vtk_image_with_sitk_geometry_preserves_origin_and_spacing(tmp_path): + np = pytest.importorskip("numpy") + sitk = pytest.importorskip("SimpleITK") + + image = _vtk_image_from_array( + np.ones((3, 4, 5), dtype=np.uint8), + origin=(-10.5, 20.25, 3.75), + spacing=(0.8, 0.9, 1.1), + ) + out = write_vtk_image_with_sitk_geometry(image, tmp_path / "mask.nii.gz") + reread = sitk.ReadImage(str(out)) + + assert reread.GetSize() == (3, 4, 5) + assert reread.GetOrigin() == pytest.approx((-10.5, 20.25, 3.75)) + assert reread.GetSpacing() == pytest.approx((0.8, 0.9, 1.1)) + + def test_icp_transform_sidecar_round_trips_matrix(tmp_path): np = pytest.importorskip("numpy") @@ -215,6 +240,21 @@ def test_bbox_relative_fixture_plane_uses_authored_y_plane_axes(): assert high_y_plane["v_axis"] == pytest.approx((-1, 0, 0)) +def test_proximal_sideways_fall_fixture_plane_does_not_reach_distal_shaft(): + bounds = (-30, 30, -40, 80, -95, 120) + + plane = femur.proximal_sideways_fall_fixture_plane( + bounds, + center_fraction=femur.FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, + ) + + assert plane["footprint"] == "proximal_only" + assert plane["center"][2] == pytest.approx(80.0) + assert plane["size"] == pytest.approx((80.0, 70.0)) + assert plane["center"][2] - plane["size"][0] / 2.0 == pytest.approx(40.0) + assert plane["center"][2] + plane["size"][0] / 2.0 == pytest.approx(120.0) + + def test_bbox_relative_oriented_contact_plane_keeps_measured_normal(): np = pytest.importorskip("numpy") bounds = (10, 50, -20, 80, 100, 200) @@ -936,11 +976,13 @@ def test_lesser_trochanter_cut_uses_distal_area_peak(): meta = femur.detect_lesser_trochanter_cut_z(image) assert meta["greater_trochanter_z"] == pytest.approx(76) + assert meta["greater_trochanter_disk_distal_z"] == pytest.approx(73) + assert meta["greater_trochanter_disk_proximal_z"] == pytest.approx(79) assert meta["lesser_trochanter_z"] == pytest.approx(61) assert meta["cut_z"] == pytest.approx(61) -def test_greater_trochanter_length_crop_uses_detected_gt_level(): +def test_greater_trochanter_length_crop_uses_gt_disk_distal_origin(): np = pytest.importorskip("numpy") vtk = pytest.importorskip("vtk") from ogo.util.vtk_image import vtk_image_to_numpy @@ -969,17 +1011,21 @@ def test_greater_trochanter_length_crop_uses_detected_gt_level(): cropped, crop_face, meta = femur.crop_vtk_images_to_greater_trochanter_length( [image], image, - retained_length_mm=30.0, + retained_length_mm=10.0, labels={1}, ) cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 assert meta["method"] == "greater_trochanter_length" assert meta["greater_trochanter_z"] == pytest.approx(76) - assert meta["cut_z_mm"] == pytest.approx(46) + assert meta["greater_trochanter_disk_distal_z"] == pytest.approx(73) + assert meta["gt_inclusion_length_mm"] == pytest.approx(0) + assert meta["distal_length_origin_z"] == pytest.approx(73) + assert meta["cut_z_mm"] == pytest.approx(63) + assert meta["available_below_gt_disk_mm"] == pytest.approx(63) assert meta["available_below_gt_mm"] == pytest.approx(66) - assert meta["retained_length_mm"] == pytest.approx(30) - assert cropped[0].GetOrigin()[2] == pytest.approx(46) + assert meta["retained_length_mm"] == pytest.approx(10) + assert cropped[0].GetOrigin()[2] == pytest.approx(63) assert np.argwhere(cropped_mask)[:, 2].min() == 0 assert vtk_image_to_numpy(crop_face).sum() > 0 @@ -1050,6 +1096,8 @@ def test_lesser_trochanter_cut_uses_peak_plateau_center_and_percent_offset(): ) assert meta["greater_trochanter_z"] == pytest.approx(80) + assert meta["greater_trochanter_disk_distal_z"] == pytest.approx(78) + assert meta["greater_trochanter_disk_proximal_z"] == pytest.approx(82) assert meta["lesser_trochanter_z"] == pytest.approx(58) assert meta["distal_offset_mm"] == pytest.approx(11) assert meta["cut_z"] == pytest.approx(47) diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py index 587f266..786d05b 100644 --- a/tests/fea/test_spinefe_benchmark.py +++ b/tests/fea/test_spinefe_benchmark.py @@ -154,6 +154,85 @@ def vtk_image(array): assert out[2, 1, 1] == 5000 +def _binary_vtk_image(array): + vtk = pytest.importorskip("vtk") + from ogo.util.vtk_image import numpy_to_vtk_image + + image = vtk.vtkImageData() + image.SetDimensions(array.shape) + image.SetOrigin(0, 0, 0) + image.SetSpacing(1, 1, 1) + return numpy_to_vtk_image(array.astype("uint8"), image, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) + + +def test_spine_process_orientation_qc_allows_process_offset_in_xy(): + np = pytest.importorskip("numpy") + from ogo.fea.spine import check_spine_process_orientation + + body = np.zeros((24, 24, 24), dtype=bool) + process = np.zeros_like(body) + body[9:15, 9:15, 9:15] = True + process[17:22, 10:14, 10:14] = True + + qc = check_spine_process_orientation( + _binary_vtk_image(body), + _binary_vtk_image(process), + ) + + assert qc["status"] == "ok" + assert qc["transverse_offset_mm"] > qc["axial_offset_mm"] + + +def test_spine_process_orientation_qc_rejects_process_offset_along_z(): + np = pytest.importorskip("numpy") + from ogo.fea.spine import check_spine_process_orientation + + body = np.zeros((24, 24, 24), dtype=bool) + process = np.zeros_like(body) + body[9:15, 9:15, 9:15] = True + process[10:14, 10:14, 17:22] = True + + with pytest.raises(ValueError, match="posterior process orientation QC failed"): + check_spine_process_orientation( + _binary_vtk_image(body), + _binary_vtk_image(process), + ) + + +def test_spine_process_orientation_score_prefers_transverse_registration(): + np = pytest.importorskip("numpy") + from ogo.fea.spine import spine_process_orientation_metrics_for_rotation + + body_centroid = np.array([0.0, 0.0, 0.0]) + process_centroid = np.array([30.0, 0.0, 0.0]) + transverse = np.eye(3) + x_to_z_swap = np.array( + [ + [0.0, 0.0, 1.0], + [0.0, 1.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) + + good = spine_process_orientation_metrics_for_rotation( + body_centroid, + process_centroid, + transverse, + ) + bad = spine_process_orientation_metrics_for_rotation( + body_centroid, + process_centroid, + x_to_z_swap, + ) + + assert good["status"] == "ok" + assert good["axial_offset_mm"] == pytest.approx(0.0) + assert good["transverse_offset_mm"] == pytest.approx(30.0) + assert bad["status"] == "failed" + assert bad["axial_offset_mm"] == pytest.approx(30.0) + assert bad["transverse_offset_mm"] == pytest.approx(0.0) + + def test_spine_workflow_uses_projected_contact_disks(): pytest.importorskip("vtk") diff --git a/tests/util/test_faim.py b/tests/util/test_faim.py index 631c0e0..57d897d 100644 --- a/tests/util/test_faim.py +++ b/tests/util/test_faim.py @@ -211,6 +211,60 @@ def test_write_results_csv_collects_pistoia_and_loads(tmp_path): assert row["rescale_factor_to_target"] == "0.2" +def test_write_results_csv_includes_masked_pistoia_fields(tmp_path): + analysis_file = tmp_path / "model_analysis.txt" + pistoia_file = tmp_path / "model_pistoia.txt" + masked_pistoia_file = tmp_path / "model_masked_pistoia.txt" + loads_csv = tmp_path / "model_loads.csv" + pistoia_csv = tmp_path / "model_pistoia.csv" + masked_pistoia_csv = tmp_path / "model_masked_pistoia.csv" + results_csv = tmp_path / "model_results.csv" + + analysis_file.write_text("analysis", encoding="utf-8") + pistoia_file.write_text("Critical volume (%): 7.000\n", encoding="utf-8") + masked_pistoia_file.write_text( + "\n".join( + [ + "Critical volume (%): 7.000", + "Critical EES: 9.0000E-03", + "EES at vol_crit: 4.0000E-03", + ] + ), + encoding="utf-8", + ) + loads_csv.write_text("fy_ns1\n100.0\n", encoding="utf-8") + pistoia_csv.write_text("pis_fy_fail,pis_stiffy\n5000.0,2000.0\n", encoding="utf-8") + masked_pistoia_csv.write_text("pis_fy_fail,pis_stiffy\n2500.0,1800.0\n", encoding="utf-8") + + faim.write_results_csv( + output_file=results_csv, + model_file=tmp_path / "model.n88model", + analysis_file=analysis_file, + pistoia_file=pistoia_file, + loads_csv=loads_csv, + pistoia_csv=pistoia_csv, + analysis_var="fy_ns1", + pistoia_vars=["pis_fy_fail", "pis_stiffy"], + failure_axis="y", + masked_pistoia_file=masked_pistoia_file, + masked_pistoia_csv=masked_pistoia_csv, + pistoia_mask_file=tmp_path / "model_pistoia_mask.nii.gz", + masked_pistoia_stats={"selected_elements": 12, "original_included_elements": 40}, + ) + + with results_csv.open(newline="") as handle: + row = next(csv.DictReader(handle)) + + assert row["pistoia_failure_load_N"] == "5000.0" + assert row["masked_pistoia_failure_load_N"] == "2500.0" + assert row["masked_pistoia_stiffness_N_per_mm"] == "1800.0" + assert row["masked_pistoia_critical_volume_pct"] == "7.0" + assert row["masked_pistoia_critical_ees"] == "0.009" + assert row["masked_pistoia_ees_at_crit_vol"] == "0.004" + assert row["masked_pistoia_selected_elements"] == "12" + assert row["masked_pistoia_original_included_elements"] == "40" + + def test_write_results_csv_reports_generic_target_without_0p68_alias(tmp_path): analysis_file = tmp_path / "model_analysis.txt" pistoia_file = tmp_path / "model_pistoia.txt" @@ -646,3 +700,25 @@ def test_run_faim_pipeline_can_skip_pistoia(tmp_path, capsys): assert "n88pistoia" not in out assert "pis_fy_fail,pis_stiffy" not in out assert "n88tabulate -H -d , -V fy_ns1" in out + + +def test_run_faim_pipeline_runs_masked_pistoia_when_mask_supplied(tmp_path, capsys): + model = tmp_path / "model.n88model" + mask = tmp_path / "model_pistoia_mask.nii.gz" + model.write_text("", encoding="utf-8") + mask.write_text("", encoding="utf-8") + + faim.run_faim_pipeline( + model_file=model, + analysis_var="fy_ns1", + pistoia_vars=["pis_fy_fail", "pis_stiffy"], + failure_axis="y", + pistoia_mask_file=mask, + dry_run=True, + compress=False, + ) + + out = capsys.readouterr().out + assert "model_pistoia.txt" in out + assert "model_masked_pistoia.txt" in out + assert "model_masked_pistoia.csv" in out From d5bc738dba46419b2ad2931dfbc6811c1870641b Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Mon, 31 Aug 2026 09:24:35 -0600 Subject: [PATCH 37/67] docs: clarify spine FEA command --- ogo/fea/README.md | 53 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 2abf673..69c27fb 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -38,6 +38,53 @@ side-specific femur reference frame. ## Basic Commands +### Recommended Spine Command + +For a standard solved L1 compression model, run the high-level wrapper. This +generates the `.n88model`, audits the boundary conditions, solves it with +FAIM/N88, runs optional Pistoia postprocessing, and writes a compact +`_results.csv` next to the model: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --output_path derivatives/fea \ + --threads 4 \ + --faim_bin_dir /path/to/n88/bin \ + --run_pistoia \ + --critical_volume 12 \ + --critical_strain 0.007 +``` + +The `--vertebra` value is `LEVEL:BODY_LABEL:PROCESS_LABEL`. For example, +`L1:2:3` means that label `2` is the L1 vertebral body and label `3` is the L1 +posterior process. Repeat `--vertebra` to process more than one level from the +same image. + +To additionally report Pistoia for an ROI such as the vertebral body, pass a +mask in the same image space as the calibrated CT and segmentation: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --pistoia_mask sub-001_desc-L1Body_mask.nii.gz \ + --output_path derivatives/fea \ + --threads 4 \ + --faim_bin_dir /path/to/n88/bin \ + --run_pistoia \ + --critical_volume 12 \ + --critical_strain 0.007 +``` + +Ogo transforms this ROI mask through the same preprocessing, alignment, +resampling, and padding steps as the model, then writes the model-space mask as +`_pistoia_mask.nii.gz`. The `_results.csv` includes both full-model +Pistoia fields and `masked_pistoia_*` fields when a mask is supplied. + Generate and solve one L1 model: ```bash @@ -168,6 +215,9 @@ ogoFEA spine \ Other available solver-location options are `--faim_bin_dir`, `--faim_install_root`, `--faim_license_dir`, and explicit command overrides such as `--faim_command`, `--n88postfaim_command`, and `--n88tabulate_command`. +Ogo does not search your filesystem for an arbitrary N88 installation. If the +N88 commands are not already on `PATH`, use `--faim_bin_dir`, `--faim_env`, or +the explicit command overrides. FAIM/N88 does not interpret prescribed displacement as percent strain in these models. The `.n88model` stores the applied displacement in millimeters. For @@ -378,7 +428,8 @@ where `rho` is density in g/cc. In code this is The `benchmark-linear` preset uses this elastic law only. It does not assign a bone yield criterion, so the solved model is linear elastic. The maintained -spine workflow does not run or report Pistoia postprocessing. +spine workflow runs Pistoia when `--run_pistoia`, `--require_pistoia`, or +`--pistoia_mask` is supplied. The `benchmark-nonlinear` preset additionally assigns the Kopperdahl compressive yield law: From 30a6cdbbf0d0a08397b23fdec3c7ad9505bb6e7e Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Mon, 31 Aug 2026 15:42:22 -0600 Subject: [PATCH 38/67] docs: add vertebra FEA quickstart guide --- docs/fea/vertebra_fea_quickstart.md | 245 +++++++++++++++++++++++++ output/pdf/vertebra_fea_quickstart.pdf | Bin 0 -> 10153 bytes 2 files changed, 245 insertions(+) create mode 100644 docs/fea/vertebra_fea_quickstart.md create mode 100644 output/pdf/vertebra_fea_quickstart.pdf diff --git a/docs/fea/vertebra_fea_quickstart.md b/docs/fea/vertebra_fea_quickstart.md new file mode 100644 index 0000000..dab6cbd --- /dev/null +++ b/docs/fea/vertebra_fea_quickstart.md @@ -0,0 +1,245 @@ +# Vertebra FEA Quickstart + +This guide shows how to check out the Ogo FEA branch and run a solved vertebra +compression model with FAIM/N88. It is intended for users who already have a +density-calibrated CT image, a labelled vertebra segmentation, and access to +the Numerics88 command-line tools. + +## What This Workflow Does + +`ogoFEA spine` is the user-facing entry point for vertebral compression models. +For each requested vertebra it: + +1. Extracts the vertebral body and posterior process labels from a labelled + spine mask. +2. Crops the calibrated image and masks around the target vertebra. +3. Aligns the vertebral body to the bundled Ogo reference with ICP. +4. Resamples image and masks to isotropic model spacing. +5. Builds superior and inferior PMMA supports on stable vertebral-body contact + surfaces. +6. Converts density to finite-element material IDs. +7. Writes an `.n88model`, audits the boundary conditions, and records modeling + metadata. +8. Solves the model with FAIM/N88 and writes a compact `_results.csv`. +9. Optionally runs full-model and masked-ROI Pistoia postprocessing. + +## Check Out The FEA Branch + +```bash +git clone git@github.com:Bonelab/Ogo.git +cd Ogo +git checkout ogo-fea +git pull origin ogo-fea +``` + +Install or activate Ogo in the environment you want to use for model building: + +```bash +conda activate ogo +python -m pip install -e . +``` + +Check that the command is available: + +```bash +ogoFEA --help +``` + +## Required Inputs + +You need two images in the same image space: + +| Input | Requirement | +| --- | --- | +| Calibrated CT | Scalar density image in K2HPO4-equivalent units. | +| Labelled spine mask | Integer labels containing the vertebral body and posterior process. | + +The vertebra target is passed as: + +```text +LEVEL:BODY_LABEL:PROCESS_LABEL +``` + +For example, `L1:2:3` means: + +| Part | Meaning | +| --- | --- | +| `L1` | Output level name. | +| `2` | Label value for the L1 vertebral body. | +| `3` | Label value for the L1 posterior process. | + +If you want masked Pistoia for a region such as the vertebral body, provide a +binary ROI mask in the original CT image space. + +## Standard L1 Command + +This command generates, audits, solves, and postprocesses one L1 model: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --output_path derivatives/fea \ + --threads 4 \ + --faim_bin_dir /path/to/n88/bin \ + --run_pistoia \ + --critical_volume 12 \ + --critical_strain 0.007 +``` + +Use `--threads 4` for a typical single-model run. This controls FAIM solver +threads for that one model; it does not launch multiple subjects in parallel. + +## Full And Masked Pistoia + +To report both full-model Pistoia and an ROI-masked Pistoia result, add +`--pistoia_mask`: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --pistoia_mask sub-001_desc-L1Body_mask.nii.gz \ + --output_path derivatives/fea \ + --threads 4 \ + --faim_bin_dir /path/to/n88/bin \ + --run_pistoia \ + --critical_volume 12 \ + --critical_strain 0.007 +``` + +With this command, Ogo reports: + +| Result | Description | +| --- | --- | +| Full-model Pistoia | Uses the solved vertebra model and excludes PMMA. | +| Masked Pistoia | Uses only elements selected by the supplied ROI mask after transformation into model space. | + +The same `--critical_volume` and `--critical_strain` values are used for both +full-model and masked Pistoia in a single `ogoFEA` run. + +## N88 And FAIM Path Handling + +Ogo does not search the entire filesystem for N88. The commands are resolved in +this order: + +1. Explicit command overrides such as `--faim_command` or + `--n88pistoia_command`. +2. `--faim_bin_dir`, for example `/path/to/n88/bin`. +3. `--faim_install_root`, where Ogo checks common subdirectories. +4. The current shell `PATH`. + +If the N88 tools are in a separate conda environment, use: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --output_path derivatives/fea \ + --faim_env ogoloco-n88 \ + --threads 4 +``` + +## Expected Outputs + +By default, outputs are written to the calibrated image directory. In most +projects you should pass `--output_path derivatives/fea`. + +Important outputs: + +| File | Meaning | +| --- | --- | +| `*_L1.n88model` | Generated finite-element model. | +| `*_L1_modeling.json` | Traceable metadata for inputs, preprocessing, materials, boundary conditions, and reporting. | +| `*_L1_results.csv` | Compact one-row results table for downstream analysis. | +| `*_L1_pistoia.txt` | Full-model Pistoia report, when Pistoia is run. | +| `*_L1_masked_pistoia.txt` | ROI-masked Pistoia report, when `--pistoia_mask` is supplied. | +| `*_L1_pistoia_mask.nii.gz` | ROI mask transformed into model space. | + +The `_results.csv` includes reaction force, stiffness, full-model Pistoia +fields, and `masked_pistoia_*` fields when a mask is supplied. + +## Model Defaults + +The default high-level spine preset is `benchmark-linear`. + +| Setting | Default | +| --- | --- | +| Model type | Linear elastic vertebral compression. | +| Resampling | 1.0 mm isotropic. | +| Density-to-modulus law | `E = 2980 * rho^1.05 MPa` for benchmark-linear spine models. | +| Bone Poisson's ratio | 0.3. | +| PMMA modulus | 2500 MPa. | +| PMMA Poisson's ratio | 0.3. | +| PMMA thickness | 10 mm. | +| Default target endpoint | 0.68% compression strain. | +| Full-model Pistoia critical strain | User-supplied; commonly `0.007`. | + +FAIM stores prescribed displacement in millimeters. Before solving, Ogo +converts the 0.68% endpoint to millimeters from the generated vertebral-body +height and updates the `.n88model`. + +## Quick Quality Checks + +For a new dataset, first generate a debug model without solving: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --output_path derivatives/fea \ + --debug \ + --no-solve +``` + +Check: + +- The vertebral body is upright in the model frame. +- The posterior process is posterior to the body, not rotated into the axial + direction. +- Superior and inferior PMMA supports contact the body surfaces without being + dominated by osteophytes. +- The boundary-condition audit passes. +- The `_modeling.json` records the expected labels, material law, displacement + endpoint, and N88 settings. + +## Common Problems + +| Symptom | Likely cause | What to check | +| --- | --- | --- | +| Command cannot find `faim` or `n88pistoia` | N88 tools are not on `PATH`. | Pass `--faim_bin_dir`, `--faim_env`, or explicit command paths. | +| Missing masked Pistoia fields | No `--pistoia_mask` was supplied, or masked Pistoia failed. | Check `_results.csv`, `*_masked_pistoia.txt`, and the log. | +| Process appears rotated relative to the body | ICP alignment or label definition problem. | Run `--debug --no-solve` and inspect the model before solving. | +| PMMA support contacts osteophytes instead of a stable surface | Endplate surface is irregular. | Inspect debug output and `_modeling.json`; consider excluding the case if QC fails. | +| Existing solution is reused | The `.n88model` already contains a solution. | Remove old outputs or write to a clean `--output_path` before rerunning. | + +## Minimal Reproducible Example + +For a collaborator, the most useful handoff is: + +```bash +git clone git@github.com:Bonelab/Ogo.git +cd Ogo +git checkout ogo-fea +python -m pip install -e . + +ogoFEA spine \ + image_K2HPO4.nii.gz \ + spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --pistoia_mask L1_body_mask.nii.gz \ + --output_path derivatives/fea \ + --threads 4 \ + --faim_bin_dir /path/to/n88/bin \ + --run_pistoia \ + --critical_volume 12 \ + --critical_strain 0.007 +``` + +The main file to send onward for tabulated analysis is +`derivatives/fea/*_L1_results.csv`; the main file for visual/model inspection is +`derivatives/fea/*_L1.n88model`. diff --git a/output/pdf/vertebra_fea_quickstart.pdf b/output/pdf/vertebra_fea_quickstart.pdf new file mode 100644 index 0000000000000000000000000000000000000000..8d2c6bb0c7dabd479fe37ac138ce2901c63c2cef GIT binary patch literal 10153 zcmdUV+qSYu^5^~c6dMr%MG;UyMLZyQLO}403W$g(0;0}z&&9mJ%-!72vktqus{5<1 zTJ!I|Y}Z~JNW;jC(=Q{EfV6>BCMvZb(Es`0|LgyL>0T12Z`nD04ZX-qows-X$|%0h zvHN*=(fbRD6EAwjpValxAH-MnD?Z(RaNG}EBEHD%QUAgv@{9WQj{2&7-~HhEA6|6+ zCjF-Q?I(TbwMv2|v4p&z`?8**6)lpQid=#Q$fiaz75w|H^#izc}BwsDGSK$aEEW>Ax}&^)F6T|7jv2 zlR2}?>x=kR`!nwxIQ{r9UuH&C{i3sRra^ukzHrS+zb#07TP-t_?)}Y-udiRs_icw? zztPgzHN1Dm0spo=zVQB%*=YQ5e6RoORrvKf{yJnD&zQyU%vUnT_#$dN zHd^-MpKL>Cq`&FvA0qtfHvbtSFn=n-FaG|8|7VE6{;3Gx?v?q+e})K+pG5ffu~~f1 zkYM@plsL+6zK62d_|>uh1`~qwcEZ^I$s399Ncqhjd!P3;PQQ>n;zV)u*GK$$?NRg# z7gEo82XQp`c64OWB&#G{WhsKF*9ej_8-q{uN<25X1m5`<7E|x9|2mTt-w`PXr_@RR`c?kg|AfciQ+x+p297t+&7gXZ{PO}9 z!8^laDZtKY0% z@5~I3A4BIel6?h*xWW_f)#IJaI(P)@nle7>HFoWHp%a=+lO{Q5U+w03*y@gmSGm;P zks#GKyOoyq><>4m7jsxGxqEm2sf))lUVwM^0xhJ5Cf;wH56wBEM7AC(aHQDUCl#F1 zEY|2NnHyK1=gDz&6zzR(iSWP{K3EYjnl^+hbT4?p*5YPIU-NV3NE8l@oj2%JEA(Wn z4s(6EXW!;x;aFey_90bEq>&pwlt7o>UAs7yLefsXz~lL$pp% znCAV%3dqe%uNnD1v$K9}I|k_XZufzpN%)r0(#mT+i@o0SDlMK;_hxA29I)`rI&cO$ zcV2)hb+AU6m)8l@u7hQ|iOQ&U8sEC2(uCKWWpX`S9*+||Ts+4uF};Vh=~p|mBZ2$t zVo3F_=}x1K91)ENfm^s~*I0tEC1dc>S%x(YSx^98h3rqp6Cu14#c%Fnyks0|=>fs>!s?UI|_FAE61&CULc z`Vfcv9Iwv@3;*cSMO0lt9mCGA?+7HJwbhon)Ux%3caq`s`?N&uHjUQBT@M%O{2ZFP zK_B=)K6^$fP->oLucha3 z%Cv7xYu8{0O`s5NTWMp+($FH=TxPg4zeLs!$8QNDJ)zj*Y0)ViIsPO` zg@e>@464Jhp~EZ%$&b}$8I?E4G+fWt;GRlqct_$nvBfM!xU39D65V_>Kg0MDnB7}- zux;1&lCuej+hhnR)@Iw@g9M`)R8vjCpV28U&_ebaK8j)!n`# zmQi8FC7UUmH;9eb{0MD`?>4k?4t?Ni5Z9p;pBm?IIVsG6vr-M;$w_$CO63S)?RDe+ zL|0q+Y9P6{Rf1)6Vh}~%T}KpGwy?;5UifN$|CUXrQoIOCrtoOL0e8DJwT6wHgbYSY zG|XR-Db_+7ggLK5jam~(mw;{b$alB^+1(4D`BQA*sr`OQY7Q68jT2Yt3n{pTL#pzZ zbvQuw=?p9O<;m_DK9m;}w+L@Wn77E6UZ9@%i{(H`nK)Tht>UL3$lF}G0h74#u=e({ zCR^vO&lEA%#^~^4R9MOw=u&QcFXMv4S<}`8GPKm=#pNU3dOKTblLqgrljpTD15a{I z5o(XQJ3gWLk%I5gN1DFjvcDh4$^o3m!;u~l8gyKbFOryTUx83EZ|Ux}Mq&*;9&~+Y z44U?O+RdE?_C;^CdV)QoKB88ydKi5zpL(3o>d?peYvURSzt{?$DIXuL!g}r%+edeF z>HKyOR_OmSudCl={Q%XkWsCuK^Hno0j&`vAYZ9Y+_d{S2X z&8*65v$I6VJq#=t8WY&)w;9P8z!8A3$I@zZFzfkoe@X?PTaG&9_Lf0b+?5MO%keOo z4pt^VS(L(QJ4r`KJ3sW7#ZGiK#VsbVeeh%lQzxpW1h?!i(vfao7v1`DS|qx)8ON&R z$S7Tm7jWv{U*xeW+;#{#yR|^q9UO@{H9oyx2;$fWG4m{$oTZ{^TOIfXqwAqjudrO( z^nvhm;EY|n2~%pAUqLT4p43F)ctXpk^fC$g(>6aiwm49&KL%&QmPb_Yqh04?AW*_G zA+A>tfl%0XpL|vq|C-bMS|JlZ*RB617PdGU;_Zd zNiiRhMIuo<)Zj4m4jRQ7qB)$$haNCx_kl|!Gohe0MdT?|W;eOA6zo+z-xg#*H9zcm z)G7f_x`{GI$a^hz0SDC>8feh4`~l~@)|IgNQnmDHaXZv=f!SX^b5!x#RFomsv71s{ z90z{Bi7*wWTknIv z=KI6a5fon9L6W5M8ryO*&*YpCJH7{vokS0~^!|idbTE22TVH6FpxYiH)u5LBMK`&y zOq|ceY;z=WI`DS9x-?v6*s6KZdQdtrbBbe3`2rL-&f`37!@!-8e9Llof#>-pxGt_y z>|6qvNbQCD%CQalTq%->7F&QXla9hU{hNG&Z&6Juld2jEiiooSu|7h^3Z4LR?A&l& zljZ=&-=c9-wBhr<5TDlf~okrT`&;kJp%4ZMF02+c}MVkm(^ z9-?(GNay7-Ug%-a5g8CnOyGx;q-*Plc4-dn2QyY%&eKgo;>%#;Mi_D>BgcY#c*kDa3Zyk=$8f7;h zyNzIHmfUSjWj7A`dP;}29W?8m_Ye<;(CXSL)Vws`beS`2L7V1gFz={cuh&6HSPukPU0gQ?SOUG*? za8chC<+^L;lNnh%Ogk;6xVb5T*1$*EMoLb^h7ysvAvRbvxrGCML>t<6`{qa~(Qf-7 zKA$`q+Z{Oz!4}xhQlZWq>klWb7*D8b?!NB7_bnTDCV5LF_vKl1wZ~%+IodhGpB42= zE|i!`<>&*4GPAikLU(^BWF>spnet*UO|3^~y$1%16B+T-0Fdh1tK?%W&@l7e_cD*( z5((>c*TJ~S$i(W5rGA012gPBovKo)ZNMEh(p66DFnWXUs6dv4;Ikuu4N{`**WT2CC zd~npYqcWz-rA;%S?m~fx%9dJC=IO|Pb}ss?=9efIDBh+xw%LXXu#{LgY1XI1%p~4; z=T|@a1GLph5x4Iv8z)S3YrV9b-5M2@y@9Y#thxk_`3#N@jp44Cg01P}&<0*ReA|B= zylh_;K$f+K>r%|^iH%q?vc#xx{y?`N3i1=bJza~PNpUV1^)260PdA348TE+69bipo z&c$2_Gk0Jm2pA#P6#mP@fHwyY8T6I7OboLn5?9RfpUDx!9 ze1SoFQNu*=*YlmGEuUtb$m_pqld(~(6%WkK^6`PZA8+B_qGY2Uu8E6!X_u={5#}DL zN6t=b%tbHApayt-K+i&lKrg*2c($DRIC_mh!hp!Jl@T9i-iBg`?ARr@G%QY6>3+mJlm58yH7i*_-1Xl2iDc}M-ppLJNghUi(GZ4vM_ zAN$T@JG}G3YSbL_=zH9|ocp5{X3na<6-q^CrKp(Fxm9eskXQE#(FIckJ6hjE@Obw$l6+nkMc#(^YGQ7qoH#x z=N7>2x=Jfqf!#`JEcQs@_$s@PdU3G3R@Mu6rtc29bMD;&_Mqn!ALs35WeGV3s($!@ zJHVXOJb=c#+-OcO$ZBW|=9N@}eQ z21aRl>0Y6#J>f9=&r*+{kFfvU)T35oexG`bW(}GRhK1fFFFc@d)^*^x?Q8Gr?HU?$ z8_F_`BHC!#duKX&hJi(})iBAG1Z4DN8~&*bOKLY7E~J@%fGYlQ3TqSKZuF#5cf|(G zGRrHEAziJS)Ar={t_F$G1Uk@?i11nK!Bxs7h&J^aWz=iYV0r5R=V&o4-WeYT#m(Hj z3~TpkZ*;3`e7UXf;og`Qy%JfTY!^w7skUj{f0V42JDK~#R=aJgp-&onyLYe?I2AT2 z-t;lSk@XtC50L44zo_)8stli=XCvG8r~3uk z=@!#`1WInFJmw-P9?d2zjNh`rVDbSVmpH8kZXW9Co0%es{mU|%?x_7WX01ZG4`-DP z4~2VdPI~Ve2QKrMll&-c+VzmW?;d7@>UXy6y{50`Z0o!z`#WW#vW+u)zn_qCwCMtg z8pCP*b05uA@u0lty}_b0FBNS+nBg&LPx+-MwXW&s96b_$l*netuM=eCQU%lKbc#&P z!{wb%rNs)XpsTo>Zh+EQFXv|;YxMZY%%J>41Yq$Aoa3ai2=~>nHu|Ki%XRI7lb3lo z%x#k&)&LE;+}km+!(clAW?_iu)O|(Zn1wS!Hzc*9&9<3;!_G)O^>Wgd=y`-64Yjcl z=yg@;l)?Onmfp%ePw}N*AH3`-D`mBG?Q*xn&J}{F!B?ZxpipMad~U@MG=#LF!mI8| z`ly4gQGt8)r)^@`nh>QW+991ZUKY)gwS+3!cCc<1h5g>0StJPKvSGfi7h!fqlH;Nk z*e6Bbj-4^tTbW8@W9Qjuw%M2yBsg%LwK?<5nXeR(l{ASel7Z|x{k$9B;))x0R+9+n zdxpf5w-KqpW2c){_s7=wO;@iZhN-e`&51>`)md3tW->V)Te;OVHP?uZFUvNi z+nRU3MU@H>-6@n}sCK?#C01cm@t)fTd;Dno!M*=rs*G=Lf#c`e?r@!^oA8Th-fy`b zMHSvg)WR&o>%kq^AdBVtQgphFeJ*C=V!gJ7>fUG*o~~S?5T%#B7Ddc&+BnWlbH}83 zUD5meekDefSYzqlBX@tE2Q%xlg-g@M0M467jG3?PGOkWn_0b*^1Z?h;D=m-4^Bnlz z+}e>=Vn)W2y<~axl0JJ#1G$LlxDE7Qi!)&FXPqngun@M-#93y69N5pFa+n@b^Y6qL4+ouS=EHTgY&!(D997>JRW^4K_-+2KZC?R2Se&dMoS z+W7!#(9?S!EBa86MU~gL z)f!`PxjSPD9T85}uRTW&otn21S9P8ZH)!Ftsodo;W^1kKaNKJ*s@(ilI_3lo)H4u+ zxJ{=#!RNKP za;?h`e04~YRzp)7+jvRUCD-35i#L4_d$jqM$LIWDKFSw+ksFTMJ-97A#bADrT8~g# zex4XEyTwpbK#XoDA3n>9S-R!%^wrpwf?Jil5PD(b$YN0?-{sx*GM&tbd2yNT_Lr_U z3T>_2I*+xR|8C(H-??%4@@Wd}t8#qEwYi@iS@J-LGdIoX9|8TcmlpFHDOi&nkooBa zefmO&=Ob6mBff88!qwMbVWk8@Lexnn^;os>@kG60J4DuYgT~;QE%q^-AjQGNd+nUL zzVsmM!!F*K2`Dwa0myz@U7T;4#XY?_TbJ*>Vn3%yO>#^gHc^E~uNxAy%&Zm(*22pe z=Rv~7nab?henw-wSIu?w_L}gvwlND@9D+|(u`UwFTh$rz7Ynb!Zoc=; z;p(}?Hk&rBk4nQtxNFMrsdfbE@W=pW&rI_Hcm5Ec#u@M)$4X<~wU=3`*l=o9lb1C%y~oPqCVLN+M6Rsqd;ICR38FE_#Fb=syFpJgJGjLSmOh$I(Xw1>b5aTJAPO~ zxuhDipmqrIySK$+7y`3(-@(UeWVs*v7+b z_kgO(j(HiQ4|j%rcZKKN%iZVAandGM?~0oTtmkXAQr8N=SPgi&E==k~ZE}AqqxDKJ z7I%thl|bd2i*da8^Q%w$AEt7`<{ECM3?6)orQD&nHKX{57m3N#SXEY7M8EPQVvJJpwGlmYn?FnSer_W8H&Z>9 zB7c|Zy_`n}oReo*_c(G-IEA%<6|41eWmVR_4utH;Hte%ttVw#C=*SNX zhr5$}VjDBeZ6ua^fwJ9A2MbF&CyjCYAT{QyXVosb01!O?ZCF5`S(Ayd;69ztI}3vc zUEXRf6k6pTQ>p^{FRNZx-9>whZSQB^K5lndeJEZL|E%wIT_NH;Qr$dBSDX|y*?`WK zYpuAH^*Mcv)d!5M&QK5-PLW?_2XL#}Bzi&oYOXdX$tBPvf+(S{=rQlJ1H5TPt!KTk zc-Dq$gq%8vCq6WT>>PDsIFcHBfBi8f=N>5t)gN5R*5t)ihWp-X63=#>*Qjc<`N3vN z3Rx}U;G0&OuH`F!dTtGDGB4@`uQ2gu+qAnQ>b##*@BOvc^yqC=G$t+W&>{=$S;Sid zEYXvWlj}XsD^hZfGK6*v+%?r+(*8vJsd;BCi#X1y?A7cmz{U5t@+oz#oAff!mGNG; zhko;94xi@RuB_O`j=Jv`#{i!mr~5a0!_fX|;(2w^%`e{Fw~N3GJEC%lFhR$<9GlB+ zW_qd6V>XrM-O$uQ4KnT3pgG!KYe4Dcnz21U3KsE+*7I9_i;?F znySj&&l&_N%8cAdeluMb7uH5S?t7J1NhqoXbD;QF6(qo@svL2&_q2QcU1$!2QfUst zg?0hfrdM5-C({KjR1@P~Q=~_oBp)-HrOKf21UfpoPCok)x6D5l*TrKxOOEgkvrH5} z$$Y1M{*%!BOX^*IRKE!NNA>TYFS=f*>R*?yzmL&bPs8`;EBgBw@!bRTPh%vJbx8cv z7)AZ@w>2tzk^fJ0^qc<}YZs>C@ljE?NTG> Date: Tue, 1 Sep 2026 10:06:30 -0600 Subject: [PATCH 39/67] docs: expand vertebra FEA setup guide --- .gitattributes | 1 + docs/fea/vertebra_fea_quickstart.md | 74 ++++++++++++++++++++++++- output/pdf/vertebra_fea_quickstart.pdf | Bin 10153 -> 11658 bytes 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d72fd52 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.pdf binary diff --git a/docs/fea/vertebra_fea_quickstart.md b/docs/fea/vertebra_fea_quickstart.md index dab6cbd..b6923ce 100644 --- a/docs/fea/vertebra_fea_quickstart.md +++ b/docs/fea/vertebra_fea_quickstart.md @@ -23,7 +23,14 @@ For each requested vertebra it: 8. Solves the model with FAIM/N88 and writes a compact `_results.csv`. 9. Optionally runs full-model and masked-ROI Pistoia postprocessing. -## Check Out The FEA Branch +## Fresh Setup On A New Computer + +Use this section when setting up a new workstation from scratch. + +### 1. Get Ogo + +Install Git and a conda-style Python distribution such as Miniforge or +Miniconda, then check out the FEA branch: ```bash git clone git@github.com:Bonelab/Ogo.git @@ -32,19 +39,80 @@ git checkout ogo-fea git pull origin ogo-fea ``` -Install or activate Ogo in the environment you want to use for model building: +### 2. Create The Ogo Environment + +If the repository environment file works on your system, use it: + +```bash +conda env create -n ogo -f environment.yml +conda activate ogo +python -m pip install -e . +``` + +If dependency solving fails, create a plain environment and install Ogo +editable. Then add any missing Python packages reported by import errors: ```bash +conda create -n ogo python=3.10 conda activate ogo +python -m pip install -r requirements.txt python -m pip install -e . ``` -Check that the command is available: +Check that the command is available from this environment: ```bash +which ogoFEA ogoFEA --help ``` +### 3. Make N88/FAIM Discoverable + +Ogo builds the `.n88model` in Python, but solving and Pistoia require the +Numerics88/FAIM command-line tools. + +If the N88 commands are already on your `PATH`, this should print paths: + +```bash +which n88modelinfo +which n88pistoia +which n88tabulate +which faim +``` + +If they are not on `PATH`, decide how Ogo should find them: + +| Setup | How to run | +| --- | --- | +| N88 tools live in one directory | Pass `--faim_bin_dir /path/to/n88/bin`. | +| N88 tools live in a separate conda environment | Pass `--faim_env ENV_NAME`. | +| Individual tools have unusual names or locations | Pass explicit overrides such as `--n88pistoia_command /path/to/n88pistoia`. | + +On a machine with a separate N88 environment, a typical check is: + +```bash +conda run -n ogoloco-n88 n88modelinfo --help +conda run -n ogoloco-n88 n88pistoia --help +``` + +### 4. Verify With A Debug Model + +Before the first solved run on a new computer, generate an unsolved debug model: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:2:3 \ + --output_path derivatives/fea_test \ + --debug \ + --no-solve +``` + +This confirms that Ogo can read the images, find the labels, align the +vertebra, build the PMMA supports, and write the `.n88model`. After inspecting +the debug output, run the solved command below in a clean output directory. + ## Required Inputs You need two images in the same image space: diff --git a/output/pdf/vertebra_fea_quickstart.pdf b/output/pdf/vertebra_fea_quickstart.pdf index 8d2c6bb0c7dabd479fe37ac138ce2901c63c2cef..d783a9ed2f4f239057bc24d8d8a65004c20a517f 100644 GIT binary patch literal 11658 zcmdUV$-c5m^5Fh^isOKaf(W931E7E?j)0<~D99wJAgJ9{z33O{-u1qpwK;dF`l{~z z^}lLaE)Ha#$OvM{h{(tQR{RJ?>-Z1g|NQU&^?$z<_Zg?}nRWRR-N;R?x9fam5MNGg ze;#i1{;I@JH+sdN)ONui=vU(_zJxz|y&uR3{lc=L@`a4BujX%W%`g1>?FUW&a3kj% z^c&{)B}41wj^Yd{GIsC(&3y;9^@RwL9Xmnfe<9l-8bq()?-L8_;b!R3FGT%Z-hX{& zgLU>d8-G8t|75s+BK7?d{~uEDFuptg3O(#!r1#D0AL((KtO7UvR|qx#MMABg2yq$D znO8wYcQMmMNNwPs~TTlzo_q958wLC z6u3%bTXo+V2IO1!$lCo&rlZNj^4;NYPwuzpjiKV{)tH~%p# zpx^uS_s#IVPv)_cd9%&SUBprL@;#O9@87oVuda@>-qt<#f7(&#&(3)8d0*r7t8%PZ zQ5^mC5r1As5d1>8)V1D09P!_qwZeC>2G(v+4HRuO@Kzgcw9Ab~`FFTq5qzT8)3pOH zaGhUJOx?f!_c(Q4UzH3l`_lgkRpv~bkDWQd%HLzlw^x}3m()uC`UQU-e~KyJB)*+S z=HNru&ce;H^3NZz4&L|d)Wi9Z>RaIxPMW>&%*vWwv2-NJL50&mIIZWOvo1RW+Ci59^ErB z|2D`_&D_X;U%${+i}*(MdS}Xb{1C18qf39*i%wbb>Z1Q(dMB`Q==(e8{iv??zWjE) zQx|eHU|A8Fl2lrro3TX=qLn7t(n+&9PEbli^Eo_9=&3jdpQk7URRgnokODeY~h@a9Vw|y0rq(LWwI5Vel1C zyY=f>W@%eCsJ5-j&1E@wPaPNMYi75tQUX-Am*c_T^@H*or*d>hr#6C?uiIc|w*axVmJy}i z77k#|>X)aY{c5nAJ=a_8HuXiYPWNtY^C72pdjQQ_P&?x_)`nUlqO=Djr>-)IPc=10 z>(nr^mMk=1=Pznf&oxKrqHP?mdB|piN~b&Ks$N~Ym%5+%y*!Ln-$M^xnT3Q}>UW}T zq?|RnmLri@Wi?Wv0dyo9)mQ~47&EU{F{bP4@T2W@E{%usDr)qi=TZCU37onvc&gHg zNGqSMwyy99VZe}jCfhi(6zrLdT}zYUWJ&LkDm~-jI6-e_Z4B>s$kGQXd}H;fZp;id zw0CO-WO;WY2D6pY&vn`LNt(GBQ$(!E83MTkX>)F|-_3pAXho?FO105wUdM<->#@UK z6FN@;^S(KhK6#QzXYnZev&|o|gHIzhYEm`5Amp z{BzWBc|txn`){u!*<|dVhX+kdH$#7$*SL$@#q=1W1UjX`XS;|V?JFZB!aQHCR29pv z4#utB!Cz%&I^m+v%sXMj5QL|TjW6v=yaXfdtE=RYd(XJMakxCVbvPpL$>gP*x62Q8 z+Zj`FzD^j?gLY-~JPR+Qft#KURn{+TZ*Dfrknfb}@u!Iik9gbTZ{xXXoofs2G3w^n zqP1Vq`O>|qyzB5`mavDl*?$(snvqV1^pw_TX#yelBgX5pSl+rrn2_dj6^3ZUvpI zD$bltj@7ir`T^gxXW(E4aZo&xWAGf^+=EymgVr67qsvknKJWF#S z8IElhyoygNhC_rh^HSx+$?cMF^d-?C&ttG~ zPWrdLxqARif3;BxcS@XraR*qAPJ`B5Z;a)(Rv*{bRgWMZ_ZJfHz;2T`J+8fLt*l;x zJ92|3W&N{SI71c8FZ@B-O2`7)n5nqWd$3V9x})(7@D!XZ3_XUn72%r%Cu^HUhvORdN!Da*5R8y?j6oYW8Mu>R7D}5=5w&Ukc_dg$P zhFb3n9E|8ve`YD&kBCVI>vZ$}YS5R>%YmK4hR_yoE|YK4AM&%%tx?N6I=y#pAJ3Ga z4ueg-tOzdX@G2=2A>`T>K z_|Rh#_z+{IQkmGz{CXC0keLsI-E{pb-R-~v#+RE6CThun8{G2*jhe>)q_e})Z{gv$ zV-x!G;o{#6I9ZUV{vqIe#xou=CUR*^z4zWEb+4EeCeuo5eC4N4sd%f_^j1+}`R@3j z8{vMiN9BgIK_9Oa`JC9oD_2;R&@fG}GtA~}TTbM(^7&NZX0xf>4ghk%7WnMLAc1)S zna{h)TOY7f81;PKImx%?K8&Rje!VV%lTe6BIJDq59jp{q5@v9a?C#6gOqGiMvN{Sz zr`_dk_-ta#=DDS119#~->mbvDkT4ts_kzV-VvizwsFGg3-&YLPCQl4O2sshEmzKr1 z7j9}L)SjUE5<01Ghc~JAc-KGrVKZB=daVg$IV5id@#>O}nA)W`H`s^N6VB^ZNeiOM z0jSLi2^|8T!R1gVMz%N{*5~s}XPpG=gWINg{5tG4&ht>R3IaPhyWR_Gu&~Rl&Q7dPB!VeD~W9FwhgbjT$^ElQxjmZUbFuw zVdS)4qoyj^3l*ba=-~#w157hmMsPc1FKynS@;1JXIVE5^*QsGGQE;J|cXCb6<4PqN z>BEpQnMD%81)PL~QwqkbV?VI8eLiy2DSN=<~G;Wckqv@ZMb zP-WP@ROURA97ht%bjytDZ=&_hAk}BxCe)6Ml2P8L8;+ZcowPr0>T>p2niOXqZ!E-+*g|HF=Wx3)h?3QMEE-s__dM z;gLF!A1w1Ja=XRivpt+o=e6h4%D!eNqK>eKe0PCrhAyWwE4{(>^qh=u8qq*r^y2ZD zI~H9Gb5~62@B7FQGw#YQnUkw#A``hzD>oX#pA=-CgIi-S;R8WWu+vz?Tol5{5z2r(nm75}XZ^9jMw6!lWh#XCmzPJy1OtDnl3d)>P z?8iukx56TBG~cagIW+F`Dyf=d)xZ%@M;ORl?nlEm*qNpS4=b-;ds>*fWvJ=Vp1N6b zJ9nYlW_h)zFM)Li1Jk*G6mP*UrOiQVu(wOdw;%O;6{Och^9FI$-sc3qAvniFuPy8> zzz0q9F7|SpTPTcn96Lt9%tyX%Nix>i7ORgac-+5U)uYktlVQQB8~tt64yWUhfaE6U zu5*Z}!h+x`sk1enA))PRlxpt7lB!(mJfv)3OgHWQL%E;u6StUD+y+^w$&SO&(emSl z&Hm_#cAE!t6WK-fxID*FI>HtLRy&B<)@1#I$9He@s%X=fA-C)DT#xgM zq8ISx!&U>v&SPQBFnn2g5(*Z8Ysg)VaAkov_kO1`nQ!3vRpvgrCtqnuDt0QUW-Ik{Sk)29xYpD zr-s*uGD#oelU`o=y&+5J;;e5O-DEI2)71xcQ*Ip}RjVKL=nPGMH+lACHy`#|@6MH& zUq3ORlnPEdfa>Q?<*Zk$@phKg{$!-5wMr8lkDc9Z^yC?hFM#d%c}(Zml{2sM{bBQz zqiTyvTn}EyV(xy??837McIy@zAI8Jsa-KK#d?~>hS@CkN_O5kA@gY9QQM+WfSA9^c zS`SE$76!Mf5(AZbis#K>P}0uVTIX)v`x42E6o0yw3+l65a}`9oB|*z{5#7E+B8q88 zu3)OjmFXB0%X!bM`zGD+ckfNQL^$8?DM{%jujXZ^%oLAW-}JP?yuGB23WW{i*?Bd- zq|xpH!FL($j;Awn>U_Zv?rocm)QAWj+FI+SikjZ5O?pxy(~@MoUbku3dP;)}E{VjjW z_Lx`axcsz<7w?XEozs(oyWxn*-d;UfV!y~lOxZxq=)2TP4^K=}}>2Xd$ z9@Fy&G2ioiv7ENr?Id0b=nKa+e;Uzu^*wHnhXQvx1uwaEzj3Ex$<>J}baKva?-qgB z4otk$J)Ffg7FN7cZ%ucBfQ1yib||N#P(E$y$T--Ba*nt!#HH-;#r9y=te;-6hVSd} zVqz|KyGq>A z+IHCdv`YPjc?|mGM+ub0ds&`_Ei0w8n7ma1;kmqJRT|UP2PS1vmn$!m735fH7lz|$ zYtwy!jE)aZ$*ft4^14gxHm{&rZp(vcDl)tP2dZDRPXgNQT(5W}NW#HdFYmoW+=^FF zbyeQ;yVo(kAhT^FuUIXrzJ3_c2Z4v%B4XsYV1a8v-@O|!;ojh_mOx(>zyxFw))~|kHO{> zgmh5pm6r7JUD79o@p0y=*eY7Mb{Y$Q=*Sy~f1~M{(0QNxw5ev;@sSq-&8ukH-bx8D z5z1DWhYFhRyhfXqT~Ymv+&^S<0@GyeygDCg8xzs< zrAXnkoIWlWYBj_=(dT(mx}eSRqS90GbPwFsxE)ITa>kDszmy|HAqQCdSF0wCPp?zH z+-em%sv&`QumT?kjV_=~eIkr@wZbN;lLZDGo`GgTd$I2`w!JcVvoT4$V?Zoh?Mg9u zsK9R4PmffkUb&FkE)jDs?l)Fp?o!R33h@cBQ*7!y}$(KpWd@_z#yB-g# zz{+1I+YW4<-GM2fapV zULpgdu;DHD^f+85QCq2s`1L7Fqxj}~(RSUSk8{4awr?A4Y+6LKOQXX} zx5rhDMOIlD3NWiBK=n~>r5sTw2#6anvD}$OHPAazO@E` z3vo%JTko~%PL#Zoh(0q6P0Hyi?~>CR?>7u}9TdnBMvMWjjvfa0;pr%hWoM?JyX_+| zL)_!T+k%mc>?5ci`psPd^bmP0WM#>B>wLX41C7r%+mSyQb)B3=!~AO2hbO(TGlr;> zc1z4iziyx>tXz+#-)O!ow3XzPidRpdwv;iW$8%(E)6%|(H79w zdCm9!g1sXfKJ+&$(VF_@$38{bRoi|Y3vh1HjR-Ro_sQYDsR!|AI$ecO1qkff*^6@q zRM?W0`s9$;e4~Eza)f?IryHd;Sn|zf6^;m>uw)ya4Sj@6J^u)v{ifDAP9~gv)K`@T zdRRmu5^)nS6oy5r)UQuc+^aR7!Y$89(xBqNizf!he6DiwL3k%MH`p>lt}}}Ggtk!> z*nFFKF?bEwR414+E$*A8;Q*$3;^Ba9D^xLy(N zs5aO`v=f3Q{hNleD#+M}a7z0qJfIV@1`n|gIQjt;hMgOw_|WoI7cL(=0Y2wa+tMqC<$*?ShqzQV@*63w zgFCcyT8X|jnjV}2tQ^_%+^VAQ6W|@sO1I_(WdZN))A?+bB?X7zh?YNFPIy!zY*@02Fbh~z1v$xOqd^|0rTY0); zgK{D{rxo4mhRFm(oIawby1!hT)wr?BE)QB|ZNp3&HoDRlABTLc`wd&LiG)ozb!ppQ zDp?Yuomb~3OE{$fN+L1fb!;;#1uww7>@FAvnULN)J3(EUmqf?p5wJBC)J`?jI z96M*a&(P?NlBN$Pid=;g!6Z7jjv{a@&?JbTJMa7Q*4EE)J5m{~HljZK>2w~$FVmY3 zxY8{+6&M0fwCQ$O;*q|@^;)1cH0n%&|GXFUjBDkYcCEZ&AQD6ZrusD9Ctqql3r@?Y zz}*^@d9<+qn5hHJUG9|D4i-Bq21muv+OWxKc02qjH2oHrnITrSB0eWm2p zzFAo0_Mt8@X({KrN}#&>r9?h1cD=uJy!10>7$$6Rey&a|F5SI-xK7zl$}R~`9pu-| zXxVh$?ZFk@G#feLTCP6Vg=!S^KfR%TI`r37X1s4VTtw;*!J#Tf3dauzUcxJ;nX0aG z?e*G|_9f3N4#fvUKN(^zFhA@Sl=k_-)NU;0I$?I%z%NVl0IWx97K|e?D0ojs{Rqdc z(StSNVOl9L%#F<1R>Ur+v3TvrY;ixPZ)TBc^c1phET>)DjCRGh0Mfd_%;JaX!gMKc z!g)=HPWT5EtTj`;t2Tqnh>go16r%5$&ayGPL)RZ{;2 zLr?`=(BsB);?IOcJUx23pq)=A{?=fX$Z*8pd9b19ZS<380}_tWIq#+Od}Jo3xSgFTR{=MH@VAD_Eo zhIoDqHpR-ZlkEbg)URIG@9Gj-cBJD^jY1b1y54v9%8~>yBax59xHW zn<3lS5AG8?zIIBhq)Cp>D`9q3k&e``Zs`d#^R&9knAv|yU9DS~qX!2^Pgb*}w$BQw zrc`^>L2?&|SS9Zx$LR#RI?{6(@x+jwKP$P{GN>;Lv-s(>*5m4R9={=NzkOUDaC=9- zf<L7g%O8Ccw9!hKNE(Wh_}!d4-)Utn1EU@*o*Y zq$qoAbWGE6Uxuz%pAcdn;m(55>hJbok2}dS*vt_n;tnV<-pcA629*$6i%{OwbV|Ur zb`1?`!sPh8lJ=wu;d|zTEGB1`<+>A1gYOV&`eL4L%Aw!)Qe%~j_-0o)O#z>rMyGC= zKloOk+I~_ME0TM}N_XuSo#mqqSu;4qqO-&2aBQ}dtUw6Ul}>AR;eD~SnihIlvf!lE zR{RKCw4{y?1EBi4zniCujUPJPRPgSU!D5}LHm`G+OG0nUr3a+1VANa?o9e-FXT(W& zha(nNr4y}nL1o@kT7m<41yo3QF6w1gFy_d|*7PjI%O8h~b!y$q*K_VjBbY9o zyNX$Ucl1reYI>*!lcSkpdSk@{5lIFNN4_cKo#Knj#(T*;KD64p0{agpu-Dua8Sa(G zJ$L3Cy6xDD<&Z1(2W@fvYLo9~ql3pwLl^2+H=fS0Su6J@^0QA9&c6zqP98a3y1Y@U zci>{{RT7;rG3+W`)_{t=obz*ETkNk}&SIrfl^J9S)zB%0`O!(&5CvBApajo-uqQRW z?WpGfw*#`=Lj@yJmnw;x=y*TkeH$m*r8aK8s~07TuG;C{I%!8iA`T)DbUeqv@`Q@u z(7Gab7KPkE6&ooRt_O?DaG0o37Z8d-{NO|hp;8ns2TfKWPb ziu7VP8P57(GPrFWA$cqp_|RB2pE7*h)pkcC*h<3iVSfmXTma@_7x1u&2Wh{;1#Pwf zST-fCRf3<|Sz&AlrrT|EzWY#`3^b00kzTf&ay}*1B@%RUP2f--9&T^7AQx`4om3vO zsX|!ogn`TAzOpXcv*LBEmd|k0qOKR(Z&}>RQg!^ce@I=5Sr$65lq;swh5!S*#pOfU z%JUZ9ebM3Qg@{}>$Y6UHkbESkmw5x*nD6I;$O;TH!{(@5%;k&JGZ;kjVbA6t*?FrX z3s~xz+dql7e@R{MN8<}6e>DE~`)apZ&DNLq^|!fp)~5CS`-=WHhkkb`{nH$VW*tcX zG}mnYaV_4=j{g4y*ZvbY68i%j+Q9yR55>?w!r|YoW&cP6#mPU;5$!+lfs*(i@HKHN zt3m%GzE=B>bP2Ta2N_T(k@aW&zV@A3!QD-NX+NIuUq22#`(kC)8OpN>%fUUurf|23 zdQBT^x6qb{wdnsE!uSFF)CKMTc^5Pce>p+cmG@8m&_9q##6BCMvZb(Es`0|LgyL>0T12Z`nD04ZX-qows-X$|%0h zvHN*=(fbRD6EAwjpValxAH-MnD?Z(RaNG}EBEHD%QUAgv@{9WQj{2&7-~HhEA6|6+ zCjF-Q?I(TbwMv2|v4p&z`?8**6)lpQid=#Q$fiaz75w|H^#izc}BwsDGSK$aEEW>Ax}&^)F6T|7jv2 zlR2}?>x=kR`!nwxIQ{r9UuH&C{i3sRra^ukzHrS+zb#07TP-t_?)}Y-udiRs_icw? zztPgzHN1Dm0spo=zVQB%*=YQ5e6RoORrvKf{yJnD&zQyU%vUnT_#$dN zHd^-MpKL>Cq`&FvA0qtfHvbtSFn=n-FaG|8|7VE6{;3Gx?v?q+e})K+pG5ffu~~f1 zkYM@plsL+6zK62d_|>uh1`~qwcEZ^I$s399Ncqhjd!P3;PQQ>n;zV)u*GK$$?NRg# z7gEo82XQp`c64OWB&#G{WhsKF*9ej_8-q{uN<25X1m5`<7E|x9|2mTt-w`PXr_@RR`c?kg|AfciQ+x+p297t+&7gXZ{PO}9 z!8^laDZtKY0% z@5~I3A4BIel6?h*xWW_f)#IJaI(P)@nle7>HFoWHp%a=+lO{Q5U+w03*y@gmSGm;P zks#GKyOoyq><>4m7jsxGxqEm2sf))lUVwM^0xhJ5Cf;wH56wBEM7AC(aHQDUCl#F1 zEY|2NnHyK1=gDz&6zzR(iSWP{K3EYjnl^+hbT4?p*5YPIU-NV3NE8l@oj2%JEA(Wn z4s(6EXW!;x;aFey_90bEq>&pwlt7o>UAs7yLefsXz~lL$pp% znCAV%3dqe%uNnD1v$K9}I|k_XZufzpN%)r0(#mT+i@o0SDlMK;_hxA29I)`rI&cO$ zcV2)hb+AU6m)8l@u7hQ|iOQ&U8sEC2(uCKWWpX`S9*+||Ts+4uF};Vh=~p|mBZ2$t zVo3F_=}x1K91)ENfm^s~*I0tEC1dc>S%x(YSx^98h3rqp6Cu14#c%Fnyks0|=>fs>!s?UI|_FAE61&CULc z`Vfcv9Iwv@3;*cSMO0lt9mCGA?+7HJwbhon)Ux%3caq`s`?N&uHjUQBT@M%O{2ZFP zK_B=)K6^$fP->oLucha3 z%Cv7xYu8{0O`s5NTWMp+($FH=TxPg4zeLs!$8QNDJ)zj*Y0)ViIsPO` zg@e>@464Jhp~EZ%$&b}$8I?E4G+fWt;GRlqct_$nvBfM!xU39D65V_>Kg0MDnB7}- zux;1&lCuej+hhnR)@Iw@g9M`)R8vjCpV28U&_ebaK8j)!n`# zmQi8FC7UUmH;9eb{0MD`?>4k?4t?Ni5Z9p;pBm?IIVsG6vr-M;$w_$CO63S)?RDe+ zL|0q+Y9P6{Rf1)6Vh}~%T}KpGwy?;5UifN$|CUXrQoIOCrtoOL0e8DJwT6wHgbYSY zG|XR-Db_+7ggLK5jam~(mw;{b$alB^+1(4D`BQA*sr`OQY7Q68jT2Yt3n{pTL#pzZ zbvQuw=?p9O<;m_DK9m;}w+L@Wn77E6UZ9@%i{(H`nK)Tht>UL3$lF}G0h74#u=e({ zCR^vO&lEA%#^~^4R9MOw=u&QcFXMv4S<}`8GPKm=#pNU3dOKTblLqgrljpTD15a{I z5o(XQJ3gWLk%I5gN1DFjvcDh4$^o3m!;u~l8gyKbFOryTUx83EZ|Ux}Mq&*;9&~+Y z44U?O+RdE?_C;^CdV)QoKB88ydKi5zpL(3o>d?peYvURSzt{?$DIXuL!g}r%+edeF z>HKyOR_OmSudCl={Q%XkWsCuK^Hno0j&`vAYZ9Y+_d{S2X z&8*65v$I6VJq#=t8WY&)w;9P8z!8A3$I@zZFzfkoe@X?PTaG&9_Lf0b+?5MO%keOo z4pt^VS(L(QJ4r`KJ3sW7#ZGiK#VsbVeeh%lQzxpW1h?!i(vfao7v1`DS|qx)8ON&R z$S7Tm7jWv{U*xeW+;#{#yR|^q9UO@{H9oyx2;$fWG4m{$oTZ{^TOIfXqwAqjudrO( z^nvhm;EY|n2~%pAUqLT4p43F)ctXpk^fC$g(>6aiwm49&KL%&QmPb_Yqh04?AW*_G zA+A>tfl%0XpL|vq|C-bMS|JlZ*RB617PdGU;_Zd zNiiRhMIuo<)Zj4m4jRQ7qB)$$haNCx_kl|!Gohe0MdT?|W;eOA6zo+z-xg#*H9zcm z)G7f_x`{GI$a^hz0SDC>8feh4`~l~@)|IgNQnmDHaXZv=f!SX^b5!x#RFomsv71s{ z90z{Bi7*wWTknIv z=KI6a5fon9L6W5M8ryO*&*YpCJH7{vokS0~^!|idbTE22TVH6FpxYiH)u5LBMK`&y zOq|ceY;z=WI`DS9x-?v6*s6KZdQdtrbBbe3`2rL-&f`37!@!-8e9Llof#>-pxGt_y z>|6qvNbQCD%CQalTq%->7F&QXla9hU{hNG&Z&6Juld2jEiiooSu|7h^3Z4LR?A&l& zljZ=&-=c9-wBhr<5TDlf~okrT`&;kJp%4ZMF02+c}MVkm(^ z9-?(GNay7-Ug%-a5g8CnOyGx;q-*Plc4-dn2QyY%&eKgo;>%#;Mi_D>BgcY#c*kDa3Zyk=$8f7;h zyNzIHmfUSjWj7A`dP;}29W?8m_Ye<;(CXSL)Vws`beS`2L7V1gFz={cuh&6HSPukPU0gQ?SOUG*? za8chC<+^L;lNnh%Ogk;6xVb5T*1$*EMoLb^h7ysvAvRbvxrGCML>t<6`{qa~(Qf-7 zKA$`q+Z{Oz!4}xhQlZWq>klWb7*D8b?!NB7_bnTDCV5LF_vKl1wZ~%+IodhGpB42= zE|i!`<>&*4GPAikLU(^BWF>spnet*UO|3^~y$1%16B+T-0Fdh1tK?%W&@l7e_cD*( z5((>c*TJ~S$i(W5rGA012gPBovKo)ZNMEh(p66DFnWXUs6dv4;Ikuu4N{`**WT2CC zd~npYqcWz-rA;%S?m~fx%9dJC=IO|Pb}ss?=9efIDBh+xw%LXXu#{LgY1XI1%p~4; z=T|@a1GLph5x4Iv8z)S3YrV9b-5M2@y@9Y#thxk_`3#N@jp44Cg01P}&<0*ReA|B= zylh_;K$f+K>r%|^iH%q?vc#xx{y?`N3i1=bJza~PNpUV1^)260PdA348TE+69bipo z&c$2_Gk0Jm2pA#P6#mP@fHwyY8T6I7OboLn5?9RfpUDx!9 ze1SoFQNu*=*YlmGEuUtb$m_pqld(~(6%WkK^6`PZA8+B_qGY2Uu8E6!X_u={5#}DL zN6t=b%tbHApayt-K+i&lKrg*2c($DRIC_mh!hp!Jl@T9i-iBg`?ARr@G%QY6>3+mJlm58yH7i*_-1Xl2iDc}M-ppLJNghUi(GZ4vM_ zAN$T@JG}G3YSbL_=zH9|ocp5{X3na<6-q^CrKp(Fxm9eskXQE#(FIckJ6hjE@Obw$l6+nkMc#(^YGQ7qoH#x z=N7>2x=Jfqf!#`JEcQs@_$s@PdU3G3R@Mu6rtc29bMD;&_Mqn!ALs35WeGV3s($!@ zJHVXOJb=c#+-OcO$ZBW|=9N@}eQ z21aRl>0Y6#J>f9=&r*+{kFfvU)T35oexG`bW(}GRhK1fFFFc@d)^*^x?Q8Gr?HU?$ z8_F_`BHC!#duKX&hJi(})iBAG1Z4DN8~&*bOKLY7E~J@%fGYlQ3TqSKZuF#5cf|(G zGRrHEAziJS)Ar={t_F$G1Uk@?i11nK!Bxs7h&J^aWz=iYV0r5R=V&o4-WeYT#m(Hj z3~TpkZ*;3`e7UXf;og`Qy%JfTY!^w7skUj{f0V42JDK~#R=aJgp-&onyLYe?I2AT2 z-t;lSk@XtC50L44zo_)8stli=XCvG8r~3uk z=@!#`1WInFJmw-P9?d2zjNh`rVDbSVmpH8kZXW9Co0%es{mU|%?x_7WX01ZG4`-DP z4~2VdPI~Ve2QKrMll&-c+VzmW?;d7@>UXy6y{50`Z0o!z`#WW#vW+u)zn_qCwCMtg z8pCP*b05uA@u0lty}_b0FBNS+nBg&LPx+-MwXW&s96b_$l*netuM=eCQU%lKbc#&P z!{wb%rNs)XpsTo>Zh+EQFXv|;YxMZY%%J>41Yq$Aoa3ai2=~>nHu|Ki%XRI7lb3lo z%x#k&)&LE;+}km+!(clAW?_iu)O|(Zn1wS!Hzc*9&9<3;!_G)O^>Wgd=y`-64Yjcl z=yg@;l)?Onmfp%ePw}N*AH3`-D`mBG?Q*xn&J}{F!B?ZxpipMad~U@MG=#LF!mI8| z`ly4gQGt8)r)^@`nh>QW+991ZUKY)gwS+3!cCc<1h5g>0StJPKvSGfi7h!fqlH;Nk z*e6Bbj-4^tTbW8@W9Qjuw%M2yBsg%LwK?<5nXeR(l{ASel7Z|x{k$9B;))x0R+9+n zdxpf5w-KqpW2c){_s7=wO;@iZhN-e`&51>`)md3tW->V)Te;OVHP?uZFUvNi z+nRU3MU@H>-6@n}sCK?#C01cm@t)fTd;Dno!M*=rs*G=Lf#c`e?r@!^oA8Th-fy`b zMHSvg)WR&o>%kq^AdBVtQgphFeJ*C=V!gJ7>fUG*o~~S?5T%#B7Ddc&+BnWlbH}83 zUD5meekDefSYzqlBX@tE2Q%xlg-g@M0M467jG3?PGOkWn_0b*^1Z?h;D=m-4^Bnlz z+}e>=Vn)W2y<~axl0JJ#1G$LlxDE7Qi!)&FXPqngun@M-#93y69N5pFa+n@b^Y6qL4+ouS=EHTgY&!(D997>JRW^4K_-+2KZC?R2Se&dMoS z+W7!#(9?S!EBa86MU~gL z)f!`PxjSPD9T85}uRTW&otn21S9P8ZH)!Ftsodo;W^1kKaNKJ*s@(ilI_3lo)H4u+ zxJ{=#!RNKP za;?h`e04~YRzp)7+jvRUCD-35i#L4_d$jqM$LIWDKFSw+ksFTMJ-97A#bADrT8~g# zex4XEyTwpbK#XoDA3n>9S-R!%^wrpwf?Jil5PD(b$YN0?-{sx*GM&tbd2yNT_Lr_U z3T>_2I*+xR|8C(H-??%4@@Wd}t8#qEwYi@iS@J-LGdIoX9|8TcmlpFHDOi&nkooBa zefmO&=Ob6mBff88!qwMbVWk8@Lexnn^;os>@kG60J4DuYgT~;QE%q^-AjQGNd+nUL zzVsmM!!F*K2`Dwa0myz@U7T;4#XY?_TbJ*>Vn3%yO>#^gHc^E~uNxAy%&Zm(*22pe z=Rv~7nab?henw-wSIu?w_L}gvwlND@9D+|(u`UwFTh$rz7Ynb!Zoc=; z;p(}?Hk&rBk4nQtxNFMrsdfbE@W=pW&rI_Hcm5Ec#u@M)$4X<~wU=3`*l=o9lb1C%y~oPqCVLN+M6Rsqd;ICR38FE_#Fb=syFpJgJGjLSmOh$I(Xw1>b5aTJAPO~ zxuhDipmqrIySK$+7y`3(-@(UeWVs*v7+b z_kgO(j(HiQ4|j%rcZKKN%iZVAandGM?~0oTtmkXAQr8N=SPgi&E==k~ZE}AqqxDKJ z7I%thl|bd2i*da8^Q%w$AEt7`<{ECM3?6)orQD&nHKX{57m3N#SXEY7M8EPQVvJJpwGlmYn?FnSer_W8H&Z>9 zB7c|Zy_`n}oReo*_c(G-IEA%<6|41eWmVR_4utH;Hte%ttVw#C=*SNX zhr5$}VjDBeZ6ua^fwJ9A2MbF&CyjCYAT{QyXVosb01!O?ZCF5`S(Ayd;69ztI}3vc zUEXRf6k6pTQ>p^{FRNZx-9>whZSQB^K5lndeJEZL|E%wIT_NH;Qr$dBSDX|y*?`WK zYpuAH^*Mcv)d!5M&QK5-PLW?_2XL#}Bzi&oYOXdX$tBPvf+(S{=rQlJ1H5TPt!KTk zc-Dq$gq%8vCq6WT>>PDsIFcHBfBi8f=N>5t)gN5R*5t)ihWp-X63=#>*Qjc<`N3vN z3Rx}U;G0&OuH`F!dTtGDGB4@`uQ2gu+qAnQ>b##*@BOvc^yqC=G$t+W&>{=$S;Sid zEYXvWlj}XsD^hZfGK6*v+%?r+(*8vJsd;BCi#X1y?A7cmz{U5t@+oz#oAff!mGNG; zhko;94xi@RuB_O`j=Jv`#{i!mr~5a0!_fX|;(2w^%`e{Fw~N3GJEC%lFhR$<9GlB+ zW_qd6V>XrM-O$uQ4KnT3pgG!KYe4Dcnz21U3KsE+*7I9_i;?F znySj&&l&_N%8cAdeluMb7uH5S?t7J1NhqoXbD;QF6(qo@svL2&_q2QcU1$!2QfUst zg?0hfrdM5-C({KjR1@P~Q=~_oBp)-HrOKf21UfpoPCok)x6D5l*TrKxOOEgkvrH5} z$$Y1M{*%!BOX^*IRKE!NNA>TYFS=f*>R*?yzmL&bPs8`;EBgBw@!bRTPh%vJbx8cv z7)AZ@w>2tzk^fJ0^qc<}YZs>C@ljE?NTG> Date: Tue, 1 Sep 2026 10:45:48 -0600 Subject: [PATCH 40/67] docs: fix vertebra FEA install instructions --- docs/fea/vertebra_fea_quickstart.md | 44 ++++++++++++++++++------- output/pdf/vertebra_fea_quickstart.pdf | Bin 11658 -> 14316 bytes 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/docs/fea/vertebra_fea_quickstart.md b/docs/fea/vertebra_fea_quickstart.md index b6923ce..6885400 100644 --- a/docs/fea/vertebra_fea_quickstart.md +++ b/docs/fea/vertebra_fea_quickstart.md @@ -41,31 +41,51 @@ git pull origin ogo-fea ### 2. Create The Ogo Environment -If the repository environment file works on your system, use it: +The most reliable setup is to create an environment that gets `vtkbone` from +the Numerics88 conda channel. This avoids the slow full `environment.yml` solve +and avoids the common plain-`requirements.txt` failure where `vtkbone` is +missing. ```bash -conda env create -n ogo -f environment.yml +conda create -n ogo -c numerics88 -c conda-forge \ + git pandas nibabel pyyaml vtkbone pbr "setuptools<69" wheel python=3 conda activate ogo -python -m pip install -e . +PBR_VERSION=0.0.1 pip install -e . ``` -If dependency solving fails, create a plain environment and install Ogo -editable. Then add any missing Python packages reported by import errors: +Then verify the command-line entry point: ```bash -conda create -n ogo python=3.10 -conda activate ogo -python -m pip install -r requirements.txt -python -m pip install -e . +which ogoFEA +ogoFEA --help ``` -Check that the command is available from this environment: +The repository also contains `environment.yml`, but it can take a long time to +solve on a new machine. Use it only if you need to reproduce that exact pinned +environment: ```bash -which ogoFEA -ogoFEA --help +conda env create -n ogo-pinned -f environment.yml +``` + +If `pip install -e .` fails with a PBR versioning error, first check whether +Git is available inside the active environment: + +```bash +which git +``` + +If this prints nothing, install Git into the environment and rerun the editable +install: + +```bash +conda install -c conda-forge git +PBR_VERSION=0.0.1 pip install -e . ``` +`PBR_VERSION=0.0.1` is only used for package metadata during installation. It +does not change the checked-out Ogo code. + ### 3. Make N88/FAIM Discoverable Ogo builds the `.n88model` in Python, but solving and Pistoia require the diff --git a/output/pdf/vertebra_fea_quickstart.pdf b/output/pdf/vertebra_fea_quickstart.pdf index d783a9ed2f4f239057bc24d8d8a65004c20a517f..92029bdceaa352e15565503fbe8b4faf142405c8 100644 GIT binary patch literal 14316 zcmdVB%f7P8mM_>}PcZ>eKoL+75K$DoASj9mB7!0!;$76L%+ok8P}Qq`J?r6Hd+*GZ zd#&F&na#8@ftZ64j2I*SF)k7COc~%*mHrL>|Nh_q=l}mPA!6Ro%jNW5f-_n&`#<_}i>H-G+h$^Vn{`a@U0KH`7V72bPX5C56|YX8apeu?_${^WeD(m4Ci zbX5OOcEtRlBRQ8j*US5d`umtq;zJtt9{=*=EV|?a|O<_N3@7PIS1mG?g=aYmt05cEllFx->>rDuj-GVkN6K^K0djv z=L}33?gkQSJhw#6L2;5(a z@Jl!UZA9SzQiNZ+`EMfv^=qE~V>0}jCzHn^N3-QSo<8pR&9A0>e*b;i{tfEn@DpAi z$scAE^+z!7em|#2_JeGY@cy{}>+SJsDuYqBCrvqsh$Bu`QlNi#Hs zlZ_Hdmi|HaHwNGP`x!^+F^vzuv(Vyy{l6dC;r)Z;borP5pIGH!a`;9$03!dsw){dB zJ>6$v_OCzq&*l$n%C9bdfkqDS=Qzrjn+@{MA7G`QYyR>7ELx5`&u6|`5rgph{v*`< zK5+jS-W-i{oMV0nEzL#n^1EM#@;8hBo6-E56Xwq^voe2y=HG_ZKW4ASc- zmoG=l?m|{oj#1)#3H!AkUGri7`$Nu3kMGIn^iyLv`WLJBCs)Spw_*6XwG?7QBqj7d zCdo47ifI9=f{P383euGk3ytlhwGtk5+sLUvRM|`ClrY?* zv(@E82iKYRuFs9cRVs`;(vz_lY&~Anv}R4+W^s1v(xE-uxXFZlRgZoGygohJ96bb+ z=rD#3^+=QU-;I?oysHhBqy&SA4ECH>4)Uv20=Yr2Y*2E)P}IdUl6K%x)RnqWLoIIP zPK2>OrdNpIocRs6wW>K`yNgiA>zdxZh}CEr4BDFBp^PN_-se+kG++i@7gmaW%XJ3g z5>C6wd(uGPbSyTVM#p@DP^oH=^)`@5*AwOz337vMALZhtY)@`0Lei#;^%)PMmL``vDvt(xO!<+*)T zw*A87J*(Bmqq^u5<5n9rSUwGNRZu`P|v9f1JtB5*ej3Z;R`yz_U@q&V< zg4ry%U2}d{rh5uLP}pf|r^z=KLriY(R6p2yG_t=x+yM>x>k~g`zhR+X$$*WgPhqUMq$$atgHrEQfLH9LDX42%?Ia7D-RcFh+K2b-x66`);9orF2^ zeCH*NdUP*dI-h!)RnCGiSl5N;8>Ge_BShuvmzsVYxLfL!Al- z+x||Gmok2#-?t(14e{y-_ZB^SM17N?kXnNkTE4fEMrm<5ad!POGR^PI=k7*K)(~WL z&0dKqu36NW`Br^*cES{Npq>Lkt^~A#PHMECMQ*f{_jT~qEA1*LZ2W3o24_BrI(E(R zXm18rNqpz^lp5NS=H0e@D&vGiju!8!|7aHENpKYm+|bZ+3EWBwtnssheIl;w#;Z@Q z4d--Vk$vG*Z|=>uTZubo8eY>hH=g%L(i}f;$QEQmPDVCw#A}v_>jr<` z-bLUQNwMr6u90;;0wx%5-(^SkFY$9-;c;dqH9p=BLFj#79&W&8W<1~ldau1}=ro{| zsR~=&g>`ZSZn7r!z+JDsb_MwLtwFPKW{jBqbD@M3@>KWQAq&2@*V&p0?L)J4t`IMS zYqtu|iU-wx}3!D$JAn-{pk@xHFD9}i@0QrDhWi3Zx0-G$6>bBNmImGkz8 zo8ufcs23pD1y4CXNkk~um-9h7d7>Vc%?;l)OVuqd3s~oS8hp32Ml$JYv)u|_(S^p& zEnljgVZpovxjkO!?H^*S??uSWo zGH>1#Su*PfBcii5c@b|&`LRlf?6ZN^hq8tOQh5~&s5^}>X1lXaLLc86Zq{4C=~sKH zlOjiDJ9+Es3n(0`#q;Yail|FIfv4+>FXz;lOlw*r`~f zPf!s8e;cyZ@t7yrU3Gb7oWTw@N`-wJ!~r#3A6k@7-_SEdf6zO-+*iA5*=<=>qH-L6 z!eIDaXJYZax+tWkZX_ovj?RP{ZV}$Ndhk&{MAe5ojS-(8uL1|JoJO?Gxum3&;%cYR zu3fWt(J3DuTsUDD0x`!p(PPuaZsqTMo^ z$ZHcl3XImV#z<=ifU7bS-HF!ff&|^Zq;Gw1+pVv9ALYf*sx$d{xwvz`c<7e0rja8v z-u&p;=T>NX+W&0l`I5hTh=0$VP=CxX{Fi-By7ot(a}Nk{yPL6#l6mjd>+q%5F8f;v#Eb6*+s^&NX!&HX+_3qI-o3tzU5Z0Q zX2BY?xxQfMl?Ev(-Pe|e`oVXPj1HfB;u^PQHQO1DvjZQO%SyR+^O_gdyLj%iTlo+al#$<+3i>r!95fIa42SQ9dF~IgMJU&gvEv z&2q5aCOeh`sKi}v6~|$weGzunuewmIUi9y+78xVCN=`>z6rgu{cn`4co4zLwR9vxd zxp=f)+mlhF&wXoz^U$f3EQD{g)7HUZq`|wz_H3<;T5;0WTE(ftyda>f(5hCrM+oTDgx}4!bw^`pZ4MwuUiZCW=ZU^ zc>xw7&&?_=a%dP2j+CX#q*DSvHm~;R_kImePmH-bmAi}p0&@-QhbYn*P%#uGy*7>{V1qeTA;PF@3r=Ntv5}4 zK;_<%{FuUEu&!?CdKZxN0umuI^U18XS-@YBrb*HN~-0)fs20POY zH3Ku0bK}-Pg=+N(p6dN@BsdW!H%fuX%n=O^j4+)elXu+n?< zx!zIgXwgD`)jqq6so}a)BrjY5l+!o5d{Y@I}%4RKI4+H6$MfR@Z^m@y! zpe|DNAUfX!k%pVzVbN8#Pw`sp%TFYA;)q0h#O4&92885|fMaubet3I6>c&30hS~*t zBJtjU=)ck{ahM#4Hw5_Y^!B7Cz5Nq3$Afk|Rj8#Xl%Q&Vs*G1`0X)%qfLBpXIjfs^ zJawLXG`dgyI|>#lrSHXfpoc%Um`podL(2 zd@mzM>SL}N5faOVkv$sKJ^D;s@HNqBv;!o#P_6DuHDb~1^T-7E?!*k_zyjCOQ?jnt zZHb|kV~48&n?>rU4LZf}Eet{%M}cf7m&e9dRV?PXq> z$`OT=%g!d|m<1~DK3zR3%~f?D+bksj)%`3e*DsUJzO=*V(6*UK!a7tF8G_4vW_C6( z_#VIgE3)hl$^DdHwM&y}$McI|t810OR+(@0WYB3=k_qz|7Qajt9S-L^VI$+)`!cth z>w^6Rh$BxQPogtgz2qnv8Xf8~<1x#)6%4##w+_>6u^MbA*TYTSC))%Iy7U?S(9&M^ z``4y2-L+OnwKq&$TmquQh9npDZ0?Kh!xmFu+7&yj*CF6#iKHvR&_$-nLE99b{mp{s zqfh;?#FR7t;@VrNB}~%2rs4cwd6IuDApKW8Nt$K<*^^ABC|oHvjppL|5&&rmkT;x~ z;t;;Ugc-VkRv8kBv52pG6j^BSkA>tK!VJ5t;Jey|SocxtY+d8TnRI%6+PRBJIj5?r z>9x+b$$h@pp;Q-R@-P$?hXO|F3usfb2_(022#O}_Vw@b+-B=}=}Uz! zR=rL))Z-Q9nL+$DN2jOc!e5>kYwvOF%mi-S1d_oze4an&Ji!T9ei3H=OYeRu1`By(`|7tgGHQb&yn75*^K4r81gF#7?o?P_W~22q z`M!^cbQZ%#dK`+)6jbihTMv>d>$pK;&5FxZhT$b29rIxolhL)}h;F2eveVM(f^qq# zxtj+eVC<*Rd63t12XsnYo&`{d{#mlNwOb3Ny-OWsi#tt?U*%`{fTMw5_vUX1Uyl1b zdaNEXp!MEcIPj*1N-y5!+Q=7-(RqLbh+z{E<&r( zW&prYz%~GK}=Jgr!ll{EC`QzT~zhkr>akb znLtPP+;2l}?*Eo9SGm#|7GmzuI$$NztIzI*>rn`SD^qO~$Ay|+kSiFuLYC^p7p5Kk z5f4B1#CFG}`(3HtG34|#Mu-n=H$xUv`V29oIpi)eQnfTLl)%ZHrMpU zLZd)FtCRi`ZLFUKfLsperJ2^j%2jWi&1tfNd=}qL5gJwW3hWPOIQF8duz;_1>l({* z&#XZ~{jhj|yK=y$Zki)YmK4dyv3|#f%)Y7`_)C3C%i)M4tcNFUV!hRm=#jiv$@jPD zL&$KpcpX1r9=JoN>TBB@1WekqfB^zqVR~wubw9@-+P)C;6AC`p;|V;aL;-HLy>4bR z;LBP@&f2Yh_WWBWtfQM7MHeQE(`X8h^+QeTf|9cT(wm##RL1+2``c|CoLtsJwCVGr!Om|SRtb2 zcgWL=awAy2FqFN`m*;M9>LtzDDCw@>;3YARA*IsuQy;Jb9|!84p1~B9D7qQTM&F83 z-`pNg@^@lLR^YPav;h}|P9Gn0wfpzfvBTpF>h;!-1w1*^%B$5qjqESql{<-l+k94~ zTkb+>7Zs@Vv@|wuy2rj=T`I$lUGZ@lOl)e|!U1Sn0=C1~K*In+G@fkq1)EvB3S2DT zIlUU>5DrUd4=+pn3CY?|DgN&79$v8~r(@j8pNPK?XM8yIj!ZS|=q0grf1J28(gL&T0g3xT`@8g4Q)Z7Jbc#!!Vyj_Z z5w_fg3r=(>Zk=6m=Z^>BXa+NQbJ?)StCCbrsc&Sy3d8rrarYV7F#TI!;S?k_pZ?Jjdt=_H_2nz3#)~+Iou)}SZWY4-%u=P zK?yK>(4V&?sb3=P>;aSS`vz1{mf@8|2*S?=mRc^o<27IG%`3_^EtlJHRlRK%90fhhJwG{>7MmC`ob?**_H=6f+E!}cU`uXhg_pA0Pq@yN zO8Q(?CY%~)OV+Lh?brN1Kzi|qiZ*L#z5kA&lk-^x6yx8-K^a-*nK7FE2KvI6!BG_X zMRs5dpLsD4=%qEc=wlQRT$|ZpR`uZh8Q`0nb&^?iUUz-?89VpORNO8ydEA1bv^-zz zA9mY*cr5*_R%>AR!J%XNXfk~LdU5lrS+~s}Z|F_8yN079#2R0&uRxm2QQRArx#>x6 zK0kn0y1dKsTG>AjMJ<}e#rSSF*YD%9>B#^!B)mcSuzaRzZIVN}+^XbpuKpf4L*jcz z8_TR%zD#wec>_+*~t>>(_wk{89NI}Ev0V@W*3vub*4{OrnBVMX$9Hx29O0( z`}QhH1E~kt^Tzd4DShYnuet9<%dv#&C~(he^8{-bwg-KMw!U?FX~VZmd|^!`QY)X; z8>oZXLC>@edM7g5r$50h5yZ4+2COLht3bVaG$WOm&rM)JE#G&Y=z%PN;4WU4gA6@Ka{P~YCZ zad5cVdMobQXBy*K_wbIy7a{lji(njvO`f?R@_zW0Js0|F_ds#zJY(@R>)w?*ucp-M z=6x8l4y`=?uyy(ijI37a&1T#%3jNuW%NA4YTX2KCFbipmCwU)PEnuqV)zpgpo#Ihg z=r=I5V;uUllGK(DtC#HT!FAa>FV6wT>KeAN@ty5$ z5Cm%lWYN6ltrZjPpiR0xFkYuweNcKY<%v)lOks5C& z5p4}p4QTXTZUq}>phbSL5_`R}TnFm+L8%Ttuij|1I?PO}*J6x_>9ESS__-`f`KUH_ zjcIYie+qMi6s@xL1(QnwjTR&<_#B?j-$LUuP6jOzs1_@`dt}ImZ;yi-mQviGSLaC! zOpn5v8oOGwTvJs+BuB7HKJUSTD2e)w0hvP4tnZbYk+-XNA29#XNc5 z<)2o6aAmuLQ!`L?yRuPPd7=*1qgI7iP6sjE>9g4#{K{BXcOTWd!!O~1CRPyelV1F7 zbMZKT@4BBhHvTF+_J*#)Zz_8hVBr;$9=%RxuA6hyp6@oFZg*2=+2>hVc*BzL4Wc~i zvYS)|>2uulyT$Y;NpqbWm^MnMO~tzM(gLe-2-S$VZ3`W6Mrm2?lt|IeObM99+j-X$ z0vPXVV_~3vTQ586v|hq`X<0xVd#OTzvc%stmCWk*jwgkVm|3CZhiNc$ZeQ! zKu@XXZ8+AzCRix}gJPaCG&FNrfRaydJBFn%nLZv#X><*RAmUZRBecdL%(BJnE6Fh9{$SD&Xr;yPD-Ecgucpouq^YQ z&~Z%OORx{Z)kb6W7Cgr>D$J%k-DR?=$iTN_d$z$qQi#r`2)Y-gq(sk$ZMt0d>6mHb z9S}fQ;%lEZ2hCi}tM`CkzIsFReB0=XRJqR^5uEP4 zJAuc3aWiv^(-U2iYi0#lKSxQ$oY3}|=T%{*KGl#w^s;u}RAdh^%YX)E_IS?^uymaW zmFr@RO})i^PA~LlXSjySPRZ`zrc_!~-%QlrllW-_1zt4a=BTnB`g2#P&2hWk=gbYw z79mY(p8T~g>A-#fo?8_bSk<#{)kIG#j_lnQb)nJ2%XylArnH{0c9sf0n(Vu1EpCCy zW^5L##A6A~>P>>1<@j`p!}7a_%>{cc?2 z`pE6htcRsRULhHf<${cLJ}@fq$gTo3HkmcsHN+3u4%d*QjxFAp<*j+tsEgl!W3BT(bz4(iJQbnAq_>D$>B6`*cuB{!%QX(UmQcS@ z5ixY;zHcA%LSQ_SC$It3svWOa!jf8Wo+Wuo!67Jhc4&{=&Bz%Rsj~OJKpc=R{MqZ1 z?**xBuoe~BGa}x+*rQnd*4mZAvsy5P-7$MhX=KyRsCyR}%D;MUcU)2jI`Bb>YGudl9`nr7_tj zOnotd!9HHYkafGsTDhC1oEp@@fk1>G?n%}w^t;>6CI0O5Fj89Nsm84~sC9~^U!i{S zGKeNno#5}wW@Y}s+l_AI94lAUJ>RaIQrGQrit?P~!8WI6st}{z@*JaX?}JRm%-ja^ z6SL)$ap4MD&XV%q$*bbNZUbb~+VM$xx#Wvf=Zel`9JPP|1Wy-Joz6T`S*TxIu_oaVz2LGB14nD z*$o|6#HW-!6E=EV%#_eAxg=tlbEXT@6^ynAsHU28y3w29lK})Uz+jYbnW$Cp+qQi| zh+_poD19P1txHBY+pKvjA>8;Lgtt%6t7}%c672+-cj;XNKT-FTgmU!ug$huK&fW9%NZx!RUW@N zF>@bD$Me?^xqSZtj#k!glPVZtW&DowYMe7hF-)QJov1wzYfXqwz)80_v+DuZ?d>o9 zNppES6By}oo8h2{w~nx;^+@kRz|+S`p?8*uAsh=7q-++>e+K`Hz`y?a|+v89td*g+CDIMG)C(~1)~U~b;B(K-RSCo)3x zP-OW9ud@s-4@4V&SKvi|-URGx=X<;BT+?pQqfea>1F`BhjLCSxHBVzElu++|H2cux z`jUx5`?Ni`4GZGbZQNqAnY**`i}t~=!y4W6)Me`FA8~v81OPhB8RZ*q9Bik=aQ(n- z@iL5!u669(e1y<-QQmTtz}8BAr~)?HU4HfO?W}uZ!dbRn5rep{T?g~A=ZLq>sZi+% z=h61oDjjF0*XKI}g^j^kJo>hZWzjzG!qT#-9iH*pI5$ee^QOs$v*zl3-?RqBd92O& zv!J)qU02NF(gD?{i?%Pvo$@F<3#onbr4h80s$axe4}d}CqISCAR6fo7C&@+DwbJcD zfcPk8Sgq-a3P9?v<0`FxV%P znR3f3gHhOnM`z##Oz`S36k}Gd7I6o^$_rpuX=Y;m(wZYogPm59_$7Id^IuHzf63zG zZ{!cf{YL)V@29~qb>`>z^KWg9{2;)u-_QNu+NfW9;Qy(uM&*0l|EaBB|MRz+ z6J6sk=(yUS=qR%GCw^3o`ZFE<>+r&V?1Q4YKew@sKgmIH^q=_AOzlr}EXDkZj%RCm zCG0=+OVwC9-`oF>woewO*EswAfSaoP{N}Y7ileBQq>jf%SP%E{p4_tsjz1pgi09+H zO8S4M@PC8vMKE{&Pt4b07b7;_f#>y&oSxzaRE6f6I?uJ@Qjqzs`^34~z7F c#J`N=_p=3I_W9jhs5(oNzrk?d82t9X07&#E^Z)<= literal 11658 zcmdUV$-c5m^5Fh^isOKaf(W931E7E?j)0<~D99wJAgJ9{z33O{-u1qpwK;dF`l{~z z^}lLaE)Ha#$OvM{h{(tQR{RJ?>-Z1g|NQU&^?$z<_Zg?}nRWRR-N;R?x9fam5MNGg ze;#i1{;I@JH+sdN)ONui=vU(_zJxz|y&uR3{lc=L@`a4BujX%W%`g1>?FUW&a3kj% z^c&{)B}41wj^Yd{GIsC(&3y;9^@RwL9Xmnfe<9l-8bq()?-L8_;b!R3FGT%Z-hX{& zgLU>d8-G8t|75s+BK7?d{~uEDFuptg3O(#!r1#D0AL((KtO7UvR|qx#MMABg2yq$D znO8wYcQMmMNNwPs~TTlzo_q958wLC z6u3%bTXo+V2IO1!$lCo&rlZNj^4;NYPwuzpjiKV{)tH~%p# zpx^uS_s#IVPv)_cd9%&SUBprL@;#O9@87oVuda@>-qt<#f7(&#&(3)8d0*r7t8%PZ zQ5^mC5r1As5d1>8)V1D09P!_qwZeC>2G(v+4HRuO@Kzgcw9Ab~`FFTq5qzT8)3pOH zaGhUJOx?f!_c(Q4UzH3l`_lgkRpv~bkDWQd%HLzlw^x}3m()uC`UQU-e~KyJB)*+S z=HNru&ce;H^3NZz4&L|d)Wi9Z>RaIxPMW>&%*vWwv2-NJL50&mIIZWOvo1RW+Ci59^ErB z|2D`_&D_X;U%${+i}*(MdS}Xb{1C18qf39*i%wbb>Z1Q(dMB`Q==(e8{iv??zWjE) zQx|eHU|A8Fl2lrro3TX=qLn7t(n+&9PEbli^Eo_9=&3jdpQk7URRgnokODeY~h@a9Vw|y0rq(LWwI5Vel1C zyY=f>W@%eCsJ5-j&1E@wPaPNMYi75tQUX-Am*c_T^@H*or*d>hr#6C?uiIc|w*axVmJy}i z77k#|>X)aY{c5nAJ=a_8HuXiYPWNtY^C72pdjQQ_P&?x_)`nUlqO=Djr>-)IPc=10 z>(nr^mMk=1=Pznf&oxKrqHP?mdB|piN~b&Ks$N~Ym%5+%y*!Ln-$M^xnT3Q}>UW}T zq?|RnmLri@Wi?Wv0dyo9)mQ~47&EU{F{bP4@T2W@E{%usDr)qi=TZCU37onvc&gHg zNGqSMwyy99VZe}jCfhi(6zrLdT}zYUWJ&LkDm~-jI6-e_Z4B>s$kGQXd}H;fZp;id zw0CO-WO;WY2D6pY&vn`LNt(GBQ$(!E83MTkX>)F|-_3pAXho?FO105wUdM<->#@UK z6FN@;^S(KhK6#QzXYnZev&|o|gHIzhYEm`5Amp z{BzWBc|txn`){u!*<|dVhX+kdH$#7$*SL$@#q=1W1UjX`XS;|V?JFZB!aQHCR29pv z4#utB!Cz%&I^m+v%sXMj5QL|TjW6v=yaXfdtE=RYd(XJMakxCVbvPpL$>gP*x62Q8 z+Zj`FzD^j?gLY-~JPR+Qft#KURn{+TZ*Dfrknfb}@u!Iik9gbTZ{xXXoofs2G3w^n zqP1Vq`O>|qyzB5`mavDl*?$(snvqV1^pw_TX#yelBgX5pSl+rrn2_dj6^3ZUvpI zD$bltj@7ir`T^gxXW(E4aZo&xWAGf^+=EymgVr67qsvknKJWF#S z8IElhyoygNhC_rh^HSx+$?cMF^d-?C&ttG~ zPWrdLxqARif3;BxcS@XraR*qAPJ`B5Z;a)(Rv*{bRgWMZ_ZJfHz;2T`J+8fLt*l;x zJ92|3W&N{SI71c8FZ@B-O2`7)n5nqWd$3V9x})(7@D!XZ3_XUn72%r%Cu^HUhvORdN!Da*5R8y?j6oYW8Mu>R7D}5=5w&Ukc_dg$P zhFb3n9E|8ve`YD&kBCVI>vZ$}YS5R>%YmK4hR_yoE|YK4AM&%%tx?N6I=y#pAJ3Ga z4ueg-tOzdX@G2=2A>`T>K z_|Rh#_z+{IQkmGz{CXC0keLsI-E{pb-R-~v#+RE6CThun8{G2*jhe>)q_e})Z{gv$ zV-x!G;o{#6I9ZUV{vqIe#xou=CUR*^z4zWEb+4EeCeuo5eC4N4sd%f_^j1+}`R@3j z8{vMiN9BgIK_9Oa`JC9oD_2;R&@fG}GtA~}TTbM(^7&NZX0xf>4ghk%7WnMLAc1)S zna{h)TOY7f81;PKImx%?K8&Rje!VV%lTe6BIJDq59jp{q5@v9a?C#6gOqGiMvN{Sz zr`_dk_-ta#=DDS119#~->mbvDkT4ts_kzV-VvizwsFGg3-&YLPCQl4O2sshEmzKr1 z7j9}L)SjUE5<01Ghc~JAc-KGrVKZB=daVg$IV5id@#>O}nA)W`H`s^N6VB^ZNeiOM z0jSLi2^|8T!R1gVMz%N{*5~s}XPpG=gWINg{5tG4&ht>R3IaPhyWR_Gu&~Rl&Q7dPB!VeD~W9FwhgbjT$^ElQxjmZUbFuw zVdS)4qoyj^3l*ba=-~#w157hmMsPc1FKynS@;1JXIVE5^*QsGGQE;J|cXCb6<4PqN z>BEpQnMD%81)PL~QwqkbV?VI8eLiy2DSN=<~G;Wckqv@ZMb zP-WP@ROURA97ht%bjytDZ=&_hAk}BxCe)6Ml2P8L8;+ZcowPr0>T>p2niOXqZ!E-+*g|HF=Wx3)h?3QMEE-s__dM z;gLF!A1w1Ja=XRivpt+o=e6h4%D!eNqK>eKe0PCrhAyWwE4{(>^qh=u8qq*r^y2ZD zI~H9Gb5~62@B7FQGw#YQnUkw#A``hzD>oX#pA=-CgIi-S;R8WWu+vz?Tol5{5z2r(nm75}XZ^9jMw6!lWh#XCmzPJy1OtDnl3d)>P z?8iukx56TBG~cagIW+F`Dyf=d)xZ%@M;ORl?nlEm*qNpS4=b-;ds>*fWvJ=Vp1N6b zJ9nYlW_h)zFM)Li1Jk*G6mP*UrOiQVu(wOdw;%O;6{Och^9FI$-sc3qAvniFuPy8> zzz0q9F7|SpTPTcn96Lt9%tyX%Nix>i7ORgac-+5U)uYktlVQQB8~tt64yWUhfaE6U zu5*Z}!h+x`sk1enA))PRlxpt7lB!(mJfv)3OgHWQL%E;u6StUD+y+^w$&SO&(emSl z&Hm_#cAE!t6WK-fxID*FI>HtLRy&B<)@1#I$9He@s%X=fA-C)DT#xgM zq8ISx!&U>v&SPQBFnn2g5(*Z8Ysg)VaAkov_kO1`nQ!3vRpvgrCtqnuDt0QUW-Ik{Sk)29xYpD zr-s*uGD#oelU`o=y&+5J;;e5O-DEI2)71xcQ*Ip}RjVKL=nPGMH+lACHy`#|@6MH& zUq3ORlnPEdfa>Q?<*Zk$@phKg{$!-5wMr8lkDc9Z^yC?hFM#d%c}(Zml{2sM{bBQz zqiTyvTn}EyV(xy??837McIy@zAI8Jsa-KK#d?~>hS@CkN_O5kA@gY9QQM+WfSA9^c zS`SE$76!Mf5(AZbis#K>P}0uVTIX)v`x42E6o0yw3+l65a}`9oB|*z{5#7E+B8q88 zu3)OjmFXB0%X!bM`zGD+ckfNQL^$8?DM{%jujXZ^%oLAW-}JP?yuGB23WW{i*?Bd- zq|xpH!FL($j;Awn>U_Zv?rocm)QAWj+FI+SikjZ5O?pxy(~@MoUbku3dP;)}E{VjjW z_Lx`axcsz<7w?XEozs(oyWxn*-d;UfV!y~lOxZxq=)2TP4^K=}}>2Xd$ z9@Fy&G2ioiv7ENr?Id0b=nKa+e;Uzu^*wHnhXQvx1uwaEzj3Ex$<>J}baKva?-qgB z4otk$J)Ffg7FN7cZ%ucBfQ1yib||N#P(E$y$T--Ba*nt!#HH-;#r9y=te;-6hVSd} zVqz|KyGq>A z+IHCdv`YPjc?|mGM+ub0ds&`_Ei0w8n7ma1;kmqJRT|UP2PS1vmn$!m735fH7lz|$ zYtwy!jE)aZ$*ft4^14gxHm{&rZp(vcDl)tP2dZDRPXgNQT(5W}NW#HdFYmoW+=^FF zbyeQ;yVo(kAhT^FuUIXrzJ3_c2Z4v%B4XsYV1a8v-@O|!;ojh_mOx(>zyxFw))~|kHO{> zgmh5pm6r7JUD79o@p0y=*eY7Mb{Y$Q=*Sy~f1~M{(0QNxw5ev;@sSq-&8ukH-bx8D z5z1DWhYFhRyhfXqT~Ymv+&^S<0@GyeygDCg8xzs< zrAXnkoIWlWYBj_=(dT(mx}eSRqS90GbPwFsxE)ITa>kDszmy|HAqQCdSF0wCPp?zH z+-em%sv&`QumT?kjV_=~eIkr@wZbN;lLZDGo`GgTd$I2`w!JcVvoT4$V?Zoh?Mg9u zsK9R4PmffkUb&FkE)jDs?l)Fp?o!R33h@cBQ*7!y}$(KpWd@_z#yB-g# zz{+1I+YW4<-GM2fapV zULpgdu;DHD^f+85QCq2s`1L7Fqxj}~(RSUSk8{4awr?A4Y+6LKOQXX} zx5rhDMOIlD3NWiBK=n~>r5sTw2#6anvD}$OHPAazO@E` z3vo%JTko~%PL#Zoh(0q6P0Hyi?~>CR?>7u}9TdnBMvMWjjvfa0;pr%hWoM?JyX_+| zL)_!T+k%mc>?5ci`psPd^bmP0WM#>B>wLX41C7r%+mSyQb)B3=!~AO2hbO(TGlr;> zc1z4iziyx>tXz+#-)O!ow3XzPidRpdwv;iW$8%(E)6%|(H79w zdCm9!g1sXfKJ+&$(VF_@$38{bRoi|Y3vh1HjR-Ro_sQYDsR!|AI$ecO1qkff*^6@q zRM?W0`s9$;e4~Eza)f?IryHd;Sn|zf6^;m>uw)ya4Sj@6J^u)v{ifDAP9~gv)K`@T zdRRmu5^)nS6oy5r)UQuc+^aR7!Y$89(xBqNizf!he6DiwL3k%MH`p>lt}}}Ggtk!> z*nFFKF?bEwR414+E$*A8;Q*$3;^Ba9D^xLy(N zs5aO`v=f3Q{hNleD#+M}a7z0qJfIV@1`n|gIQjt;hMgOw_|WoI7cL(=0Y2wa+tMqC<$*?ShqzQV@*63w zgFCcyT8X|jnjV}2tQ^_%+^VAQ6W|@sO1I_(WdZN))A?+bB?X7zh?YNFPIy!zY*@02Fbh~z1v$xOqd^|0rTY0); zgK{D{rxo4mhRFm(oIawby1!hT)wr?BE)QB|ZNp3&HoDRlABTLc`wd&LiG)ozb!ppQ zDp?Yuomb~3OE{$fN+L1fb!;;#1uww7>@FAvnULN)J3(EUmqf?p5wJBC)J`?jI z96M*a&(P?NlBN$Pid=;g!6Z7jjv{a@&?JbTJMa7Q*4EE)J5m{~HljZK>2w~$FVmY3 zxY8{+6&M0fwCQ$O;*q|@^;)1cH0n%&|GXFUjBDkYcCEZ&AQD6ZrusD9Ctqql3r@?Y zz}*^@d9<+qn5hHJUG9|D4i-Bq21muv+OWxKc02qjH2oHrnITrSB0eWm2p zzFAo0_Mt8@X({KrN}#&>r9?h1cD=uJy!10>7$$6Rey&a|F5SI-xK7zl$}R~`9pu-| zXxVh$?ZFk@G#feLTCP6Vg=!S^KfR%TI`r37X1s4VTtw;*!J#Tf3dauzUcxJ;nX0aG z?e*G|_9f3N4#fvUKN(^zFhA@Sl=k_-)NU;0I$?I%z%NVl0IWx97K|e?D0ojs{Rqdc z(StSNVOl9L%#F<1R>Ur+v3TvrY;ixPZ)TBc^c1phET>)DjCRGh0Mfd_%;JaX!gMKc z!g)=HPWT5EtTj`;t2Tqnh>go16r%5$&ayGPL)RZ{;2 zLr?`=(BsB);?IOcJUx23pq)=A{?=fX$Z*8pd9b19ZS<380}_tWIq#+Od}Jo3xSgFTR{=MH@VAD_Eo zhIoDqHpR-ZlkEbg)URIG@9Gj-cBJD^jY1b1y54v9%8~>yBax59xHW zn<3lS5AG8?zIIBhq)Cp>D`9q3k&e``Zs`d#^R&9knAv|yU9DS~qX!2^Pgb*}w$BQw zrc`^>L2?&|SS9Zx$LR#RI?{6(@x+jwKP$P{GN>;Lv-s(>*5m4R9={=NzkOUDaC=9- zf<L7g%O8Ccw9!hKNE(Wh_}!d4-)Utn1EU@*o*Y zq$qoAbWGE6Uxuz%pAcdn;m(55>hJbok2}dS*vt_n;tnV<-pcA629*$6i%{OwbV|Ur zb`1?`!sPh8lJ=wu;d|zTEGB1`<+>A1gYOV&`eL4L%Aw!)Qe%~j_-0o)O#z>rMyGC= zKloOk+I~_ME0TM}N_XuSo#mqqSu;4qqO-&2aBQ}dtUw6Ul}>AR;eD~SnihIlvf!lE zR{RKCw4{y?1EBi4zniCujUPJPRPgSU!D5}LHm`G+OG0nUr3a+1VANa?o9e-FXT(W& zha(nNr4y}nL1o@kT7m<41yo3QF6w1gFy_d|*7PjI%O8h~b!y$q*K_VjBbY9o zyNX$Ucl1reYI>*!lcSkpdSk@{5lIFNN4_cKo#Knj#(T*;KD64p0{agpu-Dua8Sa(G zJ$L3Cy6xDD<&Z1(2W@fvYLo9~ql3pwLl^2+H=fS0Su6J@^0QA9&c6zqP98a3y1Y@U zci>{{RT7;rG3+W`)_{t=obz*ETkNk}&SIrfl^J9S)zB%0`O!(&5CvBApajo-uqQRW z?WpGfw*#`=Lj@yJmnw;x=y*TkeH$m*r8aK8s~07TuG;C{I%!8iA`T)DbUeqv@`Q@u z(7Gab7KPkE6&ooRt_O?DaG0o37Z8d-{NO|hp;8ns2TfKWPb ziu7VP8P57(GPrFWA$cqp_|RB2pE7*h)pkcC*h<3iVSfmXTma@_7x1u&2Wh{;1#Pwf zST-fCRf3<|Sz&AlrrT|EzWY#`3^b00kzTf&ay}*1B@%RUP2f--9&T^7AQx`4om3vO zsX|!ogn`TAzOpXcv*LBEmd|k0qOKR(Z&}>RQg!^ce@I=5Sr$65lq;swh5!S*#pOfU z%JUZ9ebM3Qg@{}>$Y6UHkbESkmw5x*nD6I;$O;TH!{(@5%;k&JGZ;kjVbA6t*?FrX z3s~xz+dql7e@R{MN8<}6e>DE~`)apZ&DNLq^|!fp)~5CS`-=WHhkkb`{nH$VW*tcX zG}mnYaV_4=j{g4y*ZvbY68i%j+Q9yR55>?w!r|YoW&cP6#mPU;5$!+lfs*(i@HKHN zt3m%GzE=B>bP2Ta2N_T(k@aW&zV@A3!QD-NX+NIuUq22#`(kC)8OpN>%fUUurf|23 zdQBT^x6qb{wdnsE!uSFF)CKMTc^5Pce>p+cmG@8m&_9q##6 Date: Tue, 1 Sep 2026 11:56:29 -0600 Subject: [PATCH 41/67] build: add netCDF4 runtime dependency --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index d7e49dd..6fa92ba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ click simpleitk numpy<2.0 matplotlib +netCDF4 # Statistics scipy>=1.7,<1.13 From 71e0f88855bdd6fb1443b27e8a6f61c3302be106 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:46:12 -0600 Subject: [PATCH 42/67] fix: define femur shaft length after GT support --- ogo/cli/CheckFEModelBC.py | 31 +++++++++++++++++++++++ ogo/fea/README.md | 52 ++++++++++++++++++++++++++++++--------- ogo/fea/femur.py | 45 +++++++++++++++++++++------------ tests/fea/test_femur.py | 39 +++++++++++++++-------------- 4 files changed, 122 insertions(+), 45 deletions(-) diff --git a/ogo/cli/CheckFEModelBC.py b/ogo/cli/CheckFEModelBC.py index 952008d..d5bb196 100644 --- a/ogo/cli/CheckFEModelBC.py +++ b/ogo/cli/CheckFEModelBC.py @@ -43,6 +43,15 @@ def _centroid(points): return {"x": float(center[0]), "y": float(center[1]), "z": float(center[2])} +def _z_percentiles(points): + z = np.asarray(points[:, 2], dtype=float) + return { + "p05": float(np.percentile(z, 5)), + "p50": float(np.percentile(z, 50)), + "p95": float(np.percentile(z, 95)), + } + + def _planarity(points): if len(points) < 3: return {"rms_mm": 0.0, "normal": [0.0, 0.0, 1.0]} @@ -65,6 +74,7 @@ def read_node_sets(root): "count": int(len(node_numbers)), "bounds": _bounds(points), "centroid": _centroid(points), + "z_percentiles": _z_percentiles(points), "planarity": _planarity(points), } return out @@ -134,6 +144,20 @@ def audit_femur_sideways(node_sets, constraints, tolerance): "detail": f"planarity_rms_mm={rms:.6g}", }) + if gt and distal: + gt_distal_z = gt["z_percentiles"]["p05"] + distal_z = distal["z_percentiles"]["p50"] + shaft_length = gt_distal_z - distal_z + checks.append({ + "name": "post-GT-support shaft length is positive", + "passed": shaft_length > max(float(tolerance), 1.0), + "detail": ( + "gt_z_p05_minus_distal_z_median=" + f"{shaft_length:.6g} mm; gt_z_p05={gt_distal_z:.6g}; " + f"distal_z_median={distal_z:.6g}" + ), + }) + expected_zero_constraints = { "bottom_fixed_x": "x", "bottom_fixed_z": "z", @@ -215,6 +239,7 @@ def write_csv(path, node_sets, constraints, checks): for name, data in source.items(): bounds = data["bounds"] center = data["centroid"] + z_percentiles = data.get("z_percentiles", {}) rows.append({ "kind": kind, "name": name, @@ -224,6 +249,9 @@ def write_csv(path, node_sets, constraints, checks): "centroid_x": center["x"], "centroid_y": center["y"], "centroid_z": center["z"], + "z_p05": z_percentiles.get("p05", ""), + "z_p50": z_percentiles.get("p50", ""), + "z_p95": z_percentiles.get("p95", ""), **bounds, }) @@ -237,6 +265,9 @@ def write_csv(path, node_sets, constraints, checks): "centroid_x": "", "centroid_y": "", "centroid_z": "", + "z_p05": "", + "z_p50": "", + "z_p95": "", "x_min": "", "x_max": "", "y_min": "", diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 69c27fb..bdf9642 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -365,13 +365,16 @@ The femur workflow builds one sideways-fall model per side: 5. Smooth the transformed femur mask with one binary close/open pass only when at least one input spacing dimension is coarser than 2 mm. If a compartment mask is supplied, the derived cortical binary mask follows the same rule. -6. Standardize the distal shaft with a flat model-grid cut. The default cut - mode detects the lesser trochanter and keeps `50 mm` distal to it. If the - required distal field of view is missing, model generation fails. -7. Generate bbox-relative rectangular PMMA fixtures for the femoral head and - greater trochanter. The femoral-head fixture is placed on the high-y side, - the greater-trochanter fixture is placed on the low-y side, and both - footprints scale with the generated model bbox. +6. Standardize the distal shaft with a flat post-ICP model-grid cut. In + `greater_trochanter_length` mode, the fixed rough crop is used only to make + ICP stable; the final model is then generated from the transformed full scan. + The requested shaft length is measured after the generated + greater-trochanter support, not from the femoral-head support and not from + the raw GT landmark alone. +7. Generate PMMA fixtures for the femoral head and greater trochanter. The + femoral-head fixture is placed on the high-y side and the greater-trochanter + fixture is placed on the low-y side. Both fixtures are anchored to the + proximal model footprint so they do not overwrite bone voxels. 8. Convert density to material IDs and construct the N88 model. 9. Apply sideways-fall boundary conditions. @@ -383,8 +386,34 @@ Default femur boundary conditions: | Greater trochanter PMMA | `Greater_Trochanter_PMMA_Nodes` | Constrained in the loading direction. | | Distal shaft cut face | `Distal_Femur_Nodes` | Constrained to remove rigid-body motion. | -Femur reporting defaults to stiffness and force at `4%` displacement. Pistoia -postprocessing is not run for femur models by the high-level wrapper. +Femur reporting defaults to stiffness and force at `4%` displacement. When +`--run_pistoia` is supplied, the high-level wrapper also reports full-model +Pistoia failure load and, if `--pistoia_mask` is supplied, masked-region +Pistoia failure load. + +For the maintained short-femur cohort model, use: + +```bash +ogoFEA hip image.nii.gz femur_mask.nii.gz \ + --side left \ + --femur_cut_mode greater_trochanter_length \ + --femur_greater_trochanter_distal_length 20 \ + --femur_shaft_length 120 \ + --run_pistoia \ + --critical_volume 10 \ + --critical_strain 0.009 +``` + +The generated model can be audited with `ogoFEA check-bc`. For hip sideways +fall, the post-GT-support shaft length is measured from final boundary-condition +node sets as: + +```text +p5(z of Greater_Trochanter_PMMA_Nodes) - median(z of Distal_Femur_Nodes) +``` + +The low GT percentile and distal-shaft median make this measurement robust to a +small number of edge voxels or support-surface bleed-through nodes. ## Materials @@ -490,12 +519,13 @@ ogoFEA spine image.nii.gz labels.nii.gz \ --iso_resolution 0.8 ``` -Change femur shaft retention: +Change femur shaft retention after the greater-trochanter support: ```bash ogoFEA hip image.nii.gz femur_mask.nii.gz \ --side left \ - --femur_lesser_trochanter_distal_offset 70 + --femur_cut_mode greater_trochanter_length \ + --femur_greater_trochanter_distal_length 45 ``` Change PMMA dimensions: diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 257012a..010a863 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -18,13 +18,12 @@ at least one input spacing dimension is coarser than 2 mm. If a compartment mask is supplied, the derived cortical binary mask follows the same rule. 4. Stabilize registration with a fixed 120 mm proximal rough crop after - isotropic resampling, then standardize the distal shaft after ICP with a - flat crop that keeps z length at 1.2 times the aligned y width. The post-ICP - crop face becomes the distal support surface. ``bbox_ratio``, - ``proximal_box_ratio``, ``post_icp_oblique_ratio``, - ``greater_trochanter_length``, ``lesser_trochanter``, and ``fixed_length`` - modes are available for debugging, sensitivity studies, or historical - comparisons. + isotropic resampling. For GT-length analyses, rerun the final model from the + transformed full scan and crop after ICP so the requested shaft length is + measured distal to the generated greater-trochanter PMMA support. The + post-ICP crop face becomes the distal support surface. Historical ratio, + lesser-trochanter, and fixed-length modes remain available for debugging and + sensitivity checks. 5. Generate two geometric PMMA fixtures from proximal contact planes: a femoral-head loading fixture on the high-y side and a greater-trochanter contact fixture on the low-y side. Defaults are 10 mm PMMA thickness and @@ -932,7 +931,14 @@ def crop_vtk_images_to_greater_trochanter_length( gt_inclusion_length_mm=DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, labels=None, ): - """Apply a flat distal crop below the detected GT-disk distal edge.""" + """Apply the post-ICP crop for a requested shaft length after GT support. + + The generated sideways-fall GT fixture uses a proximal z footprint with the + same long-axis length as the femoral-head fixture. Reserve that footprint + first, then keep ``retained_length_mm`` of shaft distal to it. Final QC + measures this on generated node sets as low-percentile GT-support z minus + median distal-shaft-support z. + """ import numpy as np from ogo.util.vtk_image import vtk_image_to_numpy @@ -960,13 +966,15 @@ def crop_vtk_images_to_greater_trochanter_length( landmark = detect_lesser_trochanter_cut_z(vtk_mask, distal_offset_mm=0.0) gt_z = float(landmark["greater_trochanter_z"]) gt_disk_distal_z = float(landmark["greater_trochanter_disk_distal_z"]) - distal_length_origin_z = gt_disk_distal_z - gt_inclusion_length_mm + support_length_mm = min(float(FEMORAL_HEAD_FIXTURE_LONG_AXIS_EXTENSION_MM), z_length_mm) + gt_support_distal_z = float(landmark["mask_z_max"]) - support_length_mm + distal_length_origin_z = gt_support_distal_z - gt_inclusion_length_mm cut_z = distal_length_origin_z - retained_length_mm available_mm = distal_length_origin_z - float(landmark["mask_z_min"]) if available_mm < retained_length_mm: raise ValueError( - "Femur scan is too short for the requested greater-trochanter distal shaft length: " - "requested %.4f mm, available %.4f mm below the GT-disk distal length origin." + "Femur scan is too short for the requested post-GT-support shaft length: " + "requested %.4f mm, available %.4f mm below the GT-support distal length origin." % (retained_length_mm, max(0.0, available_mm)) ) @@ -986,16 +994,19 @@ def crop_vtk_images_to_greater_trochanter_length( "retained_length_mm": retained_length_mm, "requested_retained_length_mm": retained_length_mm, "gt_inclusion_length_mm": gt_inclusion_length_mm, - "available_below_gt_disk_mm": float(available_mm), + "available_below_gt_disk_mm": float(gt_disk_distal_z - float(landmark["mask_z_min"])), + "available_below_gt_support_mm": float(available_mm), "available_below_gt_inclusion_mm": float(available_mm), "available_below_gt_mm": float(gt_z - float(landmark["mask_z_min"])), "greater_trochanter_z": gt_z, "greater_trochanter_disk_distal_z": gt_disk_distal_z, "greater_trochanter_disk_proximal_z": float(landmark["greater_trochanter_disk_proximal_z"]), + "greater_trochanter_support_distal_z": float(gt_support_distal_z), + "greater_trochanter_support_length_mm": float(support_length_mm), "distal_length_origin_z": float(distal_length_origin_z), "lesser_trochanter_z": float(landmark["lesser_trochanter_z"]), "cut_z_mm": float(cut_z), - "reference_axis": "greater_trochanter_disk_distal_z_minus_optional_offset", + "reference_axis": "greater_trochanter_support_distal_z_minus_optional_offset", "reference_width_mm": y_width_mm, "input_z_length_mm": z_length_mm, "status": "cropped", @@ -2880,15 +2891,17 @@ def sidewaysFallFe(args): shaft_crop["pre_icp_crop"] = bbox_crop_meta if femur_cut_mode == "greater_trochanter_length": ogo.message( - "Post-ICP GT-length crop retained shaft z=%8.4f below GT-disk origin z=%8.4f; " - "GT center z=%8.4f; GT disk distal z=%8.4f; cut z=%8.4f; available=%8.4f; slices xyz=%s." + "Post-ICP GT-length crop retained shaft z=%8.4f below GT-support origin z=%8.4f; " + "GT center z=%8.4f; GT disk distal z=%8.4f; GT support distal z=%8.4f; " + "cut z=%8.4f; available=%8.4f; slices xyz=%s." % ( retained_length_mm, shaft_crop["distal_length_origin_z"], shaft_crop["greater_trochanter_z"], shaft_crop["greater_trochanter_disk_distal_z"], + shaft_crop["greater_trochanter_support_distal_z"], shaft_crop["cut_z_mm"], - shaft_crop["available_below_gt_disk_mm"], + shaft_crop["available_below_gt_support_mm"], shaft_crop["crop_slices_xyz"], ) ) diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index fd9a898..11ff021 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -982,21 +982,21 @@ def test_lesser_trochanter_cut_uses_distal_area_peak(): assert meta["cut_z"] == pytest.approx(61) -def test_greater_trochanter_length_crop_uses_gt_disk_distal_origin(): +def test_greater_trochanter_length_crop_uses_gt_support_distal_origin(): np = pytest.importorskip("numpy") vtk = pytest.importorskip("vtk") from ogo.util.vtk_image import vtk_image_to_numpy from vtk.util.numpy_support import numpy_to_vtk - data = np.zeros((60, 80, 90), dtype=np.uint8) + data = np.zeros((60, 80, 130), dtype=np.uint8) x = np.arange(data.shape[0])[:, None] y = np.arange(data.shape[1])[None, :] - for z in range(10, 86): + for z in range(5, 126): radius = 10 y_center = 35 - if 58 <= z <= 64: + if 98 <= z <= 104: radius = 19 - if 73 <= z <= 79: + if 113 <= z <= 119: y_center = 50 radius = 12 section = ((x - 30) ** 2 + (y - y_center) ** 2) <= radius**2 @@ -1017,15 +1017,18 @@ def test_greater_trochanter_length_crop_uses_gt_disk_distal_origin(): cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 assert meta["method"] == "greater_trochanter_length" - assert meta["greater_trochanter_z"] == pytest.approx(76) - assert meta["greater_trochanter_disk_distal_z"] == pytest.approx(73) + assert meta["greater_trochanter_z"] == pytest.approx(116) + assert meta["greater_trochanter_disk_distal_z"] == pytest.approx(113) + assert meta["greater_trochanter_support_distal_z"] == pytest.approx(45) + assert meta["greater_trochanter_support_length_mm"] == pytest.approx(80) assert meta["gt_inclusion_length_mm"] == pytest.approx(0) - assert meta["distal_length_origin_z"] == pytest.approx(73) - assert meta["cut_z_mm"] == pytest.approx(63) - assert meta["available_below_gt_disk_mm"] == pytest.approx(63) - assert meta["available_below_gt_mm"] == pytest.approx(66) + assert meta["distal_length_origin_z"] == pytest.approx(45) + assert meta["cut_z_mm"] == pytest.approx(35) + assert meta["available_below_gt_disk_mm"] == pytest.approx(108) + assert meta["available_below_gt_support_mm"] == pytest.approx(40) + assert meta["available_below_gt_mm"] == pytest.approx(111) assert meta["retained_length_mm"] == pytest.approx(10) - assert cropped[0].GetOrigin()[2] == pytest.approx(63) + assert cropped[0].GetOrigin()[2] == pytest.approx(35) assert np.argwhere(cropped_mask)[:, 2].min() == 0 assert vtk_image_to_numpy(crop_face).sum() > 0 @@ -1035,15 +1038,15 @@ def test_greater_trochanter_length_crop_rejects_short_femur(): vtk = pytest.importorskip("vtk") from vtk.util.numpy_support import numpy_to_vtk - data = np.zeros((60, 80, 90), dtype=np.uint8) + data = np.zeros((60, 80, 130), dtype=np.uint8) x = np.arange(data.shape[0])[:, None] y = np.arange(data.shape[1])[None, :] - for z in range(10, 86): + for z in range(5, 126): radius = 10 y_center = 35 - if 58 <= z <= 64: + if 98 <= z <= 104: radius = 19 - if 73 <= z <= 79: + if 113 <= z <= 119: y_center = 50 radius = 12 section = ((x - 30) ** 2 + (y - y_center) ** 2) <= radius**2 @@ -1055,11 +1058,11 @@ def test_greater_trochanter_length_crop_rejects_short_femur(): image.SetSpacing(1, 1, 1) image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) - with pytest.raises(ValueError, match="too short for the requested greater-trochanter"): + with pytest.raises(ValueError, match="too short for the requested post-GT-support"): femur.crop_vtk_images_to_greater_trochanter_length( [image], image, - retained_length_mm=80.0, + retained_length_mm=50.0, labels={1}, ) From 8908f4ac28617b0bd7fec11eacb8f14599337147 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:51:11 -0600 Subject: [PATCH 43/67] docs: explain FEA workflow for new users --- ogo/fea/README.md | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/ogo/fea/README.md b/ogo/fea/README.md index bdf9642..d1f0393 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -36,6 +36,51 @@ The spine mask must contain both the body and posterior process labels. The femur workflow currently requires a side-specific run because alignment uses a side-specific femur reference frame. +## Conceptual Walkthrough + +Think of each FEA run as a reproducible conversion from calibrated CT data to a +mechanical test: + +1. Start with density-calibrated QCT and a segmentation in the same image + space. The CT values are treated as density information; the segmentation + defines which voxels become bone in the model. +2. Put the anatomy into a standard model frame. Spine models use the supplied + vertebra labels directly. Hip models first use a fixed proximal rough crop to + make femur ICP stable, then apply the final model crop after ICP from the + transformed full scan. +3. Resample to an isotropic finite-element grid. The maintained hip and spine + workflows use `1.0 mm` isotropic output spacing unless overridden. Density is + interpolated continuously; masks and labels use nearest-neighbor + interpolation. +4. Convert density to elastic material properties. The image is thresholded, + binned into material IDs, and each material receives an elastic modulus from + the selected density-modulus law. PMMA supports are added as separate stiff + material regions. +5. Build support geometry. Spine models receive superior and inferior PMMA caps + around the vertebral body. Hip sideways-fall models receive femoral-head and + greater-trochanter PMMA supports plus a distal shaft support. +6. Apply boundary conditions and solve. The spine model is compressed axially. + The hip model applies a prescribed femoral-head displacement toward the fixed + greater trochanter while the distal shaft removes rigid-body motion. +7. Report linear mechanics and Pistoia failure estimates. Reaction force and + stiffness come from the linear solve. Pistoia estimates the load at which the + weakest specified tissue volume reaches the critical strain. If a + `--pistoia_mask` is provided, both full-model and masked-ROI Pistoia values + are reported. + +The most important hip convention is the short-femur shaft definition. In +`greater_trochanter_length` mode, the requested length is not measured from the +femoral head and not from the raw GT landmark. It is measured after the +generated greater-trochanter support. The boundary-condition audit reports the +final model-space measurement as: + +```text +p5(z of Greater_Trochanter_PMMA_Nodes) - median(z of Distal_Femur_Nodes) +``` + +This definition makes the requested shaft length a property of the generated +mechanical model, not an intermediate preprocessing coordinate. + ## Basic Commands ### Recommended Spine Command From d02f415c7d37ac96b124d2cd4961bf33b6310440 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:54:15 -0600 Subject: [PATCH 44/67] docs: map FEA walkthrough to implementation --- ogo/fea/README.md | 120 +++++++++++++++++++++++++++++++--------------- 1 file changed, 81 insertions(+), 39 deletions(-) diff --git a/ogo/fea/README.md b/ogo/fea/README.md index d1f0393..767f4d5 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -36,50 +36,92 @@ The spine mask must contain both the body and posterior process labels. The femur workflow currently requires a side-specific run because alignment uses a side-specific femur reference frame. -## Conceptual Walkthrough - -Think of each FEA run as a reproducible conversion from calibrated CT data to a -mechanical test: - -1. Start with density-calibrated QCT and a segmentation in the same image - space. The CT values are treated as density information; the segmentation - defines which voxels become bone in the model. -2. Put the anatomy into a standard model frame. Spine models use the supplied - vertebra labels directly. Hip models first use a fixed proximal rough crop to - make femur ICP stable, then apply the final model crop after ICP from the - transformed full scan. -3. Resample to an isotropic finite-element grid. The maintained hip and spine - workflows use `1.0 mm` isotropic output spacing unless overridden. Density is - interpolated continuously; masks and labels use nearest-neighbor - interpolation. -4. Convert density to elastic material properties. The image is thresholded, - binned into material IDs, and each material receives an elastic modulus from - the selected density-modulus law. PMMA supports are added as separate stiff - material regions. -5. Build support geometry. Spine models receive superior and inferior PMMA caps - around the vertebral body. Hip sideways-fall models receive femoral-head and - greater-trochanter PMMA supports plus a distal shaft support. -6. Apply boundary conditions and solve. The spine model is compressed axially. - The hip model applies a prescribed femoral-head displacement toward the fixed - greater trochanter while the distal shaft removes rigid-body motion. -7. Report linear mechanics and Pistoia failure estimates. Reaction force and - stiffness come from the linear solve. Pistoia estimates the load at which the - weakest specified tissue volume reaches the critical strain. If a - `--pistoia_mask` is provided, both full-model and masked-ROI Pistoia values - are reported. - -The most important hip convention is the short-femur shaft definition. In -`greater_trochanter_length` mode, the requested length is not measured from the -femoral head and not from the raw GT landmark. It is measured after the -generated greater-trochanter support. The boundary-condition audit reports the -final model-space measurement as: +## Implementation Walkthrough + +The high-level command is `ogoFEA`, implemented in `ogo/cli/GenerateFEM.py`. +That file is intentionally the best starting point for a new developer because +it owns the user-facing arguments, the lower-level anatomy command construction, +the solve/reporting path, and the `_modeling.json` provenance record. + +Use this map when tracing a run: + +| Question | Main code location | +| --- | --- | +| Which command-line options are exposed? | `ogo/cli/GenerateFEM.py::build_parser` | +| How does `ogoFEA spine` become a lower-level model-builder call? | `ogo/cli/GenerateFEM.py::build_spine_command` | +| How does `ogoFEA hip` become a lower-level model-builder call? | `ogo/cli/GenerateFEM.py::build_femur_command` | +| Where are solve endpoints, Pistoia settings, and result columns selected? | `ogo/cli/GenerateFEM.py::solve_report_profile`, `critical_volume_percent`, `solve_model` | +| Where is model provenance written? | `ogo/cli/GenerateFEM.py::write_modeling_metadata` | +| Where is the generated `.n88model` audited? | `ogo/cli/GenerateFEM.py::audit_generated_model`, `ogo/cli/CheckFEModelBC.py` | + +The anatomy-specific implementation is split below the CLI: + +| Anatomy | Main file | What belongs there | +| --- | --- | --- | +| Spine compression | `ogo/fea/spine.py` | Vertebra labels, body/process QC, spine ICP, 1 mm resampling, stable PMMA cap generation, axial compression defaults. | +| Hip sideways fall | `ogo/fea/femur.py` | Side handling, femur reference alignment, rough pre-ICP crop, post-ICP GT-length crop, femoral-head/GT/distal supports, sideways-fall defaults. | +| Shared material tables | `ogo/fea/materials.py` | Bone material ID ranges, PMMA material, shared femur/spine material-table construction. | +| Material laws | `ogo/fea/material_laws.py` | Density-to-modulus and yield-strength functions such as `default_E` and `kopperdahl_trab_E`. | +| Shared geometry/BC helpers | `ogo/fea/boundary.py` | Resampling helpers, projected material disks, PMMA caps, contact footprint calculations. | +| N88 model writing | `ogo/fea/model.py` | `create_microfe_model` and `write_model`. | +| FAIM/N88 adapter | `ogo/util/faim.py` | Solver command resolution, running `faim`/N88 tools, Pistoia parsing, masked Pistoia, compact `_results.csv`. | + +For spine, the maintained path is: + +1. `GenerateFEM.py::build_spine_command` forwards the calibrated image, + segmentation, target `LEVEL:BODY_LABEL:PROCESS_LABEL`, preset, optional + Pistoia mask, and any lower-level overrides. +2. `spine.py::main` thresholds the requested body/process labels, crops around + the vertebra, checks posterior-process orientation, and runs scaled ICP to + the bundled L4 body reference from `default_spine_reference_path`. +3. `spine.py` applies the transform and resamples the density image with cubic + interpolation and labels/masks with nearest-neighbor interpolation. +4. `boundary.py::generate_bone_cap_mask` and related helpers generate superior + and inferior PMMA caps. The stable-contact settings are defined by + `DEFAULT_SPINE_STABLE_CONTACT_*` constants in `spine.py`. +5. `materials.py::build_spine_material_table` assigns bone and PMMA material + definitions. The default benchmark linear preset uses + `material_laws.kopperdahl_trab_E`. +6. `model.py::create_microfe_model` writes the N88 model structure, and the + high-level wrapper solves at the selected endpoint, defaulting to Crawford + `0.68%` strain for the maintained linear spine workflow. + +For hip, the maintained short-femur path is: + +1. `GenerateFEM.py::build_femur_command` forwards side, optional compartment + mask, optional Pistoia mask, and the femur crop settings. +2. `femur.py::main` selects the side-specific femur mask and aligns it to the + bundled left or mirrored-right reference. The fixed rough crop for ICP + stability is controlled by `DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM`. +3. In `greater_trochanter_length` mode, the rough crop is used only for the ICP + estimate. The final model is then regenerated from the transformed full scan. +4. `femur.py::crop_vtk_images_to_greater_trochanter_length` applies the final + post-ICP flat shaft crop. The requested distal length is measured after the + generated GT support, not from the femoral head and not from the raw GT + landmark. +5. `femur.py::proximal_sideways_fall_fixture_plane` and + `boundary.py::generate_projected_material_disk_vtk` create the femoral-head + and greater-trochanter PMMA supports. The distal crop face becomes the distal + shaft support. +6. `materials.py::build_femur_material_table` assigns bone and PMMA material + definitions. The simple whole-femur path uses one trabecular-style bone + region unless a compartment mask is supplied. +7. The high-level wrapper solves and reports the maintained hip endpoint, + defaulting to `4%` displacement. + +The most important short-femur convention is the final shaft-length definition. +In `greater_trochanter_length` mode the requested value is the distance between +the final GT support and the final distal shaft boundary condition. The audit in +`ogo/cli/CheckFEModelBC.py::audit_femur_sideways` reports the robust model-space +measurement as: ```text p5(z of Greater_Trochanter_PMMA_Nodes) - median(z of Distal_Femur_Nodes) ``` -This definition makes the requested shaft length a property of the generated -mechanical model, not an intermediate preprocessing coordinate. +This is deliberately a boundary-condition measurement. It verifies the +mechanical model that FAIM/N88 will solve rather than an intermediate crop +coordinate. ## Basic Commands From d5ba76cd675295ab4a9949ceef1b1b45925eb7d5 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 1 Sep 2026 13:54:06 -0600 Subject: [PATCH 45/67] feat: support label-selected Pistoia masks --- ogo/cli/GenerateFEM.py | 42 +++++++++++--- ogo/fea/README.md | 30 ++++++++++ ogo/fea/femur.py | 7 +++ ogo/fea/spine.py | 9 +++ tests/cli/test_GenerateFEM.py | 105 ++++++++++++++++++++++++++++++++++ 5 files changed, 185 insertions(+), 8 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 7b13858..1e414af 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -188,9 +188,18 @@ def pistoia_mask_output_path(model_path: Path) -> Path: return model_path.with_name(model_path.with_suffix("").name + "_pistoia_mask.nii.gz") +def pistoia_mask_source_path(args: argparse.Namespace) -> Optional[Path]: + """Return the input-space source image used for masked Pistoia, if any.""" + if getattr(args, "pistoia_mask", None) is not None: + return Path(args.pistoia_mask) + if getattr(args, "pistoia_mask_label", None): + return Path(args.bone_mask) + return None + + def model_space_pistoia_mask_path(model_path: Path, args: argparse.Namespace) -> Optional[Path]: """Return the transformed Pistoia ROI mask sidecar expected by the solver.""" - if getattr(args, "pistoia_mask", None) is None: + if pistoia_mask_source_path(args) is None: return None return pistoia_mask_output_path(model_path) @@ -363,7 +372,7 @@ def solve_model( solve_displacement_percent = None if args.use_absolute_fe_displacement else profile["target_displacement_percent"] target_displacement = abs(float(applied_displacement)) if args.use_absolute_fe_displacement else profile["target_displacement_percent"] - run_pistoia = args.run_pistoia or args.require_pistoia or args.pistoia_mask is not None + run_pistoia = args.run_pistoia or args.require_pistoia or pistoia_mask_source_path(args) is not None run_faim_pipeline( model_file=model_path, @@ -447,10 +456,13 @@ def write_modeling_metadata( "solve_requested": not args.no_solve, "target_displacement_percent": target_displacement_percent(args, model_type), "target_displacement_definition": "percent strain converted from model geometry for spine; percent of femur length for femur", - "run_pistoia": args.run_pistoia or args.require_pistoia or args.pistoia_mask is not None, - "source_pistoia_mask": None if args.pistoia_mask is None else str(args.pistoia_mask), + "run_pistoia": args.run_pistoia or args.require_pistoia or pistoia_mask_source_path(args) is not None, + "source_pistoia_mask": None + if pistoia_mask_source_path(args) is None + else str(pistoia_mask_source_path(args)), + "pistoia_mask_labels": list(args.pistoia_mask_label or []), "model_space_pistoia_mask": None - if args.pistoia_mask is None + if pistoia_mask_source_path(args) is None else str(pistoia_mask_output_path(model_path)), "compress_solved_model": not args.no_compress, }, @@ -1069,6 +1081,17 @@ def _add_common_image_args(parser: argparse.ArgumentParser) -> None: "next to the .n88model." ), ) + parser.add_argument( + "--pistoia_mask_label", + type=int, + action="append", + default=None, + help=( + "Label to keep from the Pistoia ROI source before masked Pistoia. Repeat " + "for a multi-label ROI. If labels are supplied without --pistoia_mask, " + "the main bone mask is used as the ROI source." + ), + ) parser.add_argument( "--no_compress", action="store_true", @@ -1158,9 +1181,12 @@ def main(argv: Optional[Sequence[str]] = None) -> None: if not args.dry_run: ensure_output_directory(args.output_path) - pistoia_mask_args = ( - ["--pistoia_mask", str(args.pistoia_mask)] if args.pistoia_mask is not None else [] - ) + pistoia_mask_source = pistoia_mask_source_path(args) + pistoia_mask_args = [] + if pistoia_mask_source is not None: + pistoia_mask_args.extend(["--pistoia_mask", str(pistoia_mask_source)]) + for label in args.pistoia_mask_label or []: + pistoia_mask_args.extend(["--pistoia_mask_label", str(label)]) if args.model_type == "spine": spine_extra_args = spine_preset_args(args.preset) + pistoia_mask_args + list(extra_args) + [ diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 767f4d5..c87429c 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -172,6 +172,30 @@ resampling, and padding steps as the model, then writes the model-space mask as `_pistoia_mask.nii.gz`. The `_results.csv` includes both full-model Pistoia fields and `masked_pistoia_*` fields when a mask is supplied. +If the ROI is already a label inside the main segmentation, keep the workflow +less error-prone by selecting that label directly. For example, if label `20` +is the L1 vertebral body and label `48` is the posterior process, this command +uses the main segmentation as the Pistoia ROI source but keeps only label `20` +for masked Pistoia: + +```bash +ogoFEA spine \ + sub-001_desc-vqct_ct.nii.gz \ + sub-001_desc-spine_labels.nii.gz \ + --vertebra L1:20:48 \ + --pistoia_mask_label 20 \ + --output_path derivatives/fea \ + --threads 4 \ + --run_pistoia \ + --critical_volume 12 \ + --critical_strain 0.007 +``` + +When `--pistoia_mask_label` is supplied without `--pistoia_mask`, the main +`bone_mask` is used as the ROI source. Repeat `--pistoia_mask_label` to keep a +multi-label ROI. Without any label selection, `--pistoia_mask` remains a binary +mask: every nonzero voxel in that mask is included. + Generate and solve one L1 model: ```bash @@ -478,6 +502,12 @@ Femur reporting defaults to stiffness and force at `4%` displacement. When Pistoia failure load and, if `--pistoia_mask` is supplied, masked-region Pistoia failure load. +Femur masked Pistoia follows the same ROI convention as spine. A separate +femoral-neck mask can be passed with `--pistoia_mask`. If the femoral-neck ROI +is a label inside the same segmentation used as the femur mask, pass +`--pistoia_mask_label LABEL` and omit `--pistoia_mask`; Ogo will use the main +segmentation as the ROI source and keep only that label. + For the maintained short-femur cohort model, use: ```bash diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 010a863..e570787 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -2352,6 +2352,7 @@ def sidewaysFallFe(args): mask = args.bone_mask compartment_mask = args.compartment_mask pistoia_mask = args.pistoia_mask + pistoia_mask_label = args.pistoia_mask_label or [] mask_threshold = args.mask_threshold iso_resolution = args.iso_resolution @@ -2458,6 +2459,8 @@ def sidewaysFallFe(args): ogo.message("Trabecular Label: %d" % trabecular_label) if pistoia_mask is not None: ogo.message("Pistoia ROI Mask: %s" % pistoia_mask) + if pistoia_mask_label: + ogo.message("Pistoia ROI Label(s): %s" % pistoia_mask_label) if femur_cut_mode == "fixed_length": ogo.message("Retained Proximal Femur Length [mm]: %8.4f" % femur_shaft_length) elif femur_lesser_trochanter_distal_offset_percent is not None: @@ -2491,6 +2494,8 @@ def sidewaysFallFe(args): if pistoia_mask is not None: ogo.message("Reading Pistoia ROI mask...") pistoiaMaskData = ogo.readNii(pistoia_mask) + if pistoia_mask_label: + pistoiaMaskData = ogo.maskThreshold(pistoiaMaskData, pistoia_mask_label) distal_crop_face = None bbox_crop_meta = None @@ -3593,6 +3598,8 @@ def main(): help="Optional trabecular/cortical compartment mask aligned with the bone mask. Defaults: cortical=1, trabecular=2.") parser.add_argument("--pistoia_mask", type=str, default=None, help="Optional ROI mask aligned with the input image for model-space masked Pistoia reporting.") + parser.add_argument("--pistoia_mask_label", type=int, action="append", default=None, + help="Label to keep from --pistoia_mask before masked Pistoia. Repeat for a multi-label ROI.") parser.add_argument("--cortical_label", type=int, default=DEFAULT_CORTICAL_LABEL, help="Label value for cortical bone in --compartment_mask. (default: %(default)s)") parser.add_argument("--trabecular_label", type=int, default=DEFAULT_TRABECULAR_LABEL, diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index c743927..78b96b8 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -1351,6 +1351,7 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, pmma_mat_id = kwargs.get("pmma_mat_id", 5000) pistoia_mask = kwargs.get("pistoia_mask") + pistoia_mask_label = kwargs.get("pistoia_mask_label") or [] iso_resolution = kwargs.get("iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM) pmma_thick = kwargs.get("pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM) pmma_intrusion = kwargs.get("pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM) @@ -1388,6 +1389,12 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, image_reader.GetOutput(), read(pistoia_mask).GetOutput(), ) + if pistoia_mask_label: + ogo.message(f"Keeping Pistoia ROI label(s): {pistoia_mask_label}") + pistoia_mask_reader = resample_to_match( + image_reader.GetOutput(), + ogo.maskThreshold(pistoia_mask_reader.GetOutput(), pistoia_mask_label), + ) input_spacing = image_reader.GetOutput().GetSpacing() # Get and Check labels @@ -1853,6 +1860,8 @@ def main(): help="Set output path for the N88 model file. (default: same as input image)") parser.add_argument("--pistoia_mask", type=str, default=None, help="Optional ROI mask aligned with the input image for model-space masked Pistoia reporting.") + parser.add_argument("--pistoia_mask_label", type=int, action="append", default=None, + help="Label to keep from --pistoia_mask before masked Pistoia. Repeat for a multi-label ROI.") parser.add_argument("--quality_control", type=_as_bool, default=True, help="Set quality control flag (visualise output and add volume checks). (default: %(default)s)") parser.add_argument("--iso_resolution", type=float, default=DEFAULT_SPINE_ISO_RESOLUTION_MM, diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index 70fdcc1..e5a7331 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -307,6 +307,62 @@ def test_spine_forwards_pistoia_mask_to_builder(monkeypatch, tmp_path, solve_cal assert calls[0][calls[0].index("--pistoia_mask") + 1] == "vertebral_body_roi.nii.gz" +def test_spine_pistoia_mask_label_uses_bone_mask_when_mask_omitted(monkeypatch, tmp_path, solve_calls): + monkeypatch.chdir(tmp_path) + calls = [] + monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "spine", + "density.nii.gz", + "spine_mask.nii.gz", + "--output_path", + "models", + "--vertebra", + "L1:20:48", + "--pistoia_mask_label", + "20", + "--no-solve", + ] + ) + + assert "--pistoia_mask" in calls[0] + assert calls[0][calls[0].index("--pistoia_mask") + 1] == "spine_mask.nii.gz" + assert "--pistoia_mask_label" in calls[0] + assert calls[0][calls[0].index("--pistoia_mask_label") + 1] == "20" + + +def test_spine_forwards_multiple_pistoia_mask_labels(monkeypatch, tmp_path, solve_calls): + monkeypatch.chdir(tmp_path) + calls = [] + monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "spine", + "density.nii.gz", + "spine_mask.nii.gz", + "--output_path", + "models", + "--vertebra", + "L1:20:48", + "--pistoia_mask", + "spine_labels.nii.gz", + "--pistoia_mask_label", + "20", + "--pistoia_mask_label", + "48", + "--no-solve", + ] + ) + + label_indexes = [ + index + 1 for index, value in enumerate(calls[0]) if value == "--pistoia_mask_label" + ] + assert [calls[0][index] for index in label_indexes] == ["20", "48"] + + def test_hip_forwards_pistoia_mask_to_builder(monkeypatch, tmp_path, solve_calls): monkeypatch.chdir(tmp_path) calls = [] @@ -331,6 +387,32 @@ def test_hip_forwards_pistoia_mask_to_builder(monkeypatch, tmp_path, solve_calls assert calls[0][calls[0].index("--pistoia_mask") + 1] == "femoral_neck.nii.gz" +def test_hip_pistoia_mask_label_uses_bone_mask_when_mask_omitted(monkeypatch, tmp_path, solve_calls): + monkeypatch.chdir(tmp_path) + calls = [] + monkeypatch.setattr(GenerateFEM, "run_femur_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "hip", + "density.nii.gz", + "left_femur_labels.nii.gz", + "--output_path", + "models", + "--side", + "left", + "--pistoia_mask_label", + "7", + "--no-solve", + ] + ) + + assert "--pistoia_mask" in calls[0] + assert calls[0][calls[0].index("--pistoia_mask") + 1] == "left_femur_labels.nii.gz" + assert "--pistoia_mask_label" in calls[0] + assert calls[0][calls[0].index("--pistoia_mask_label") + 1] == "7" + + def _metadata_args(tmp_path, no_solve=True, **overrides): data = { "dry_run": False, @@ -350,6 +432,7 @@ def _metadata_args(tmp_path, no_solve=True, **overrides): "require_pistoia": False, "use_absolute_fe_displacement": False, "pistoia_mask": None, + "pistoia_mask_label": None, } data.update(overrides) return type( @@ -362,6 +445,7 @@ def _metadata_args(tmp_path, no_solve=True, **overrides): def _solve_args(**overrides): data = { "threads": 4, + "bone_mask": Path("mask.nii.gz"), "faim_env": None, "conda_executable": "conda", "faim_install_root": None, @@ -384,6 +468,7 @@ def _solve_args(**overrides): "target_displacement": None, "use_absolute_fe_displacement": False, "pistoia_mask": None, + "pistoia_mask_label": None, } data.update(overrides) return SimpleNamespace(**data) @@ -539,6 +624,26 @@ def test_pistoia_mask_implies_pistoia_and_sidecar_handoff(monkeypatch, tmp_path) assert calls[0]["pistoia_mask_file"] == mask +def test_pistoia_mask_label_implies_pistoia_and_sidecar_handoff(monkeypatch, tmp_path): + import ogo.util.faim as faim_module + + model = tmp_path / "density_L1.n88model" + mask = tmp_path / "density_L1_pistoia_mask.nii.gz" + mask.write_text("mask", encoding="utf-8") + calls = [] + monkeypatch.setattr(faim_module, "run_faim_pipeline", lambda **kwargs: calls.append(kwargs)) + + GenerateFEM.solve_model( + model, + _solve_args(pistoia_mask=None, pistoia_mask_label=[20]), + "spine", + ["density.nii.gz", "spine_mask.nii.gz", "--fe_displacement", "-0.2"], + ) + + assert calls[0]["run_pistoia"] is True + assert calls[0]["pistoia_mask_file"] == mask + + def test_option_value_accepts_equals_form(): assert ( GenerateFEM.option_value( From 0e292801aefa7d23bbe25e7d00b2b8c87d4f776a Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:16:13 -0600 Subject: [PATCH 46/67] fix: use VTK ICP as spine registration default --- ogo/fea/README.md | 9 ++++-- ogo/fea/alignment.py | 52 +++++++++++++++++++++++++++++++++ ogo/fea/spine.py | 58 ++++++++++++++++++++++++++----------- tests/fea/test_alignment.py | 29 +++++++++++++++++++ 4 files changed, 129 insertions(+), 19 deletions(-) diff --git a/ogo/fea/README.md b/ogo/fea/README.md index c87429c..f7db5a9 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -58,7 +58,7 @@ The anatomy-specific implementation is split below the CLI: | Anatomy | Main file | What belongs there | | --- | --- | --- | -| Spine compression | `ogo/fea/spine.py` | Vertebra labels, body/process QC, spine ICP, 1 mm resampling, stable PMMA cap generation, axial compression defaults. | +| Spine compression | `ogo/fea/spine.py` | Vertebra labels, body/process QC, VTK spine ICP, 1 mm resampling, stable PMMA cap generation, axial compression defaults. | | Hip sideways fall | `ogo/fea/femur.py` | Side handling, femur reference alignment, rough pre-ICP crop, post-ICP GT-length crop, femoral-head/GT/distal supports, sideways-fall defaults. | | Shared material tables | `ogo/fea/materials.py` | Bone material ID ranges, PMMA material, shared femur/spine material-table construction. | | Material laws | `ogo/fea/material_laws.py` | Density-to-modulus and yield-strength functions such as `default_E` and `kopperdahl_trab_E`. | @@ -73,7 +73,12 @@ For spine, the maintained path is: Pistoia mask, and any lower-level overrides. 2. `spine.py::main` thresholds the requested body/process labels, crops around the vertebra, checks posterior-process orientation, and runs scaled ICP to - the bundled L4 body reference from `default_spine_reference_path`. + the bundled L4 body reference from `default_spine_reference_path`. The + default backend is VTK ICP through `alignment.py::estimate_rigid_icp_vtk`, + matching the original spine registration behavior. The deterministic NumPy + point-cloud ICP helper remains available as `--registration_backend numpy` + and is also used for alternate PCA-start rescue attempts when process-vector + QC flags an implausible orientation. 3. `spine.py` applies the transform and resamples the density image with cubic interpolation and labels/masks with nearest-neighbor interpolation. 4. `boundary.py::generate_bone_cap_mask` and related helpers generate superior diff --git a/ogo/fea/alignment.py b/ogo/fea/alignment.py index 9df9456..19ba508 100644 --- a/ogo/fea/alignment.py +++ b/ogo/fea/alignment.py @@ -184,6 +184,58 @@ def estimate_rigid_icp( } +def estimate_rigid_icp_vtk( + *, + moving_points, + fixed_points, + landmarks=250, + iterations=75, + maximum_mean_distance=0.05, + distance_mode="rms", + start_by_matching_centroids=True, +): + """Estimate a rigid ICP transform with VTK and return the shared dict format.""" + import numpy as np + import vtk + + moving_polydata = polydata_from_points(moving_points) + fixed_polydata = polydata_from_points(fixed_points) + + icp = vtk.vtkIterativeClosestPointTransform() + icp.SetSource(moving_polydata) + icp.SetTarget(fixed_polydata) + if start_by_matching_centroids: + icp.StartByMatchingCentroidsOn() + else: + icp.StartByMatchingCentroidsOff() + icp.GetLandmarkTransform().SetModeToRigidBody() + if str(distance_mode).strip().lower() == "rms": + icp.SetMeanDistanceModeToRMS() + else: + icp.SetMeanDistanceModeToAbsoluteValue() + icp.SetMaximumMeanDistance(float(maximum_mean_distance)) + icp.CheckMeanDistanceOn() + icp.SetMaximumNumberOfLandmarks(int(landmarks)) + icp.SetMaximumNumberOfIterations(int(iterations)) + icp.Update() + + matrix = icp.GetMatrix() + rotation = np.asarray( + [[float(matrix.GetElement(row, col)) for col in range(3)] for row in range(3)], + dtype=float, + ) + translation = np.asarray( + [float(matrix.GetElement(row, 3)) for row in range(3)], + dtype=float, + ) + return { + "rotation": rotation, + "translation": translation, + "iterations": int(icp.GetNumberOfIterations()), + "mean_distance": float(icp.GetMeanDistance()), + } + + def invert_point_transform(rotation, translation): """Invert ``points @ rotation.T + translation`` row-vector transforms.""" import numpy as np diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 78b96b8..6aa81fa 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -8,7 +8,9 @@ vertebra mask to the same bounding box. 3. ICP-align the vertebral body to the bundled L4 compression reference. By default the reference is PCA-scaled within 0.8,0.8,0.75 to 1.2,1.2,1.3 before - rigid ICP; users can override the scale/reference explicitly. + VTK rigid ICP; users can override the scale/reference explicitly. A + deterministic NumPy point-cloud ICP backend is kept for debugging and + process-orientation rescue attempts. 4. Apply the ICP transform and set the final output spacing to 1.0 x 1.0 x 1.0 mm in one shared VTK reslice helper. Image data use cubic interpolation; body/process labels use nearest-neighbor interpolation. @@ -344,6 +346,7 @@ def find_spinefe_benchmark_dir(start_dir=None, env_var="SPINEFE_BENCHMARK_DIR"): from ogo.fea.alignment import ( estimate_rigid_icp, + estimate_rigid_icp_vtk, invert_point_transform, output_grid_for_point_transform, point_cloud_axis_lengths, @@ -808,6 +811,7 @@ def get_icp_with_scaling( min_scale=(0.8, 0.8, 0.75), max_scale=(1.2, 1.2, 1.3), retry_on_process_qc=DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC, + backend=DEFAULT_SPINE_REGISTRATION_BACKEND, ): sample_surface_points = surface_points_from_vtk_mask( @@ -839,15 +843,30 @@ def get_icp_with_scaling( ogo.message(f"PCA axis lengths (reference): {np.round(ref_lengths, 2)}") ogo.message(f"Scale factors applied to reference ({scale_source}): {np.round(scale_factors, 3)}") - transform = estimate_rigid_icp( - moving_points=reference_points * scale_factors, - fixed_points=sample_surface_points, - iterations=50, - tolerance=1.0e-4, - start_by_matching_centroids_only=False, - convergence="delta", - distance_mode="mean", - ) + backend = str(backend).strip().lower() + scaled_reference_points = reference_points * scale_factors + if backend == "vtk": + transform = estimate_rigid_icp_vtk( + moving_points=scaled_reference_points, + fixed_points=sample_surface_points, + landmarks=250, + iterations=75, + maximum_mean_distance=0.05, + distance_mode="rms", + start_by_matching_centroids=True, + ) + elif backend == "numpy": + transform = estimate_rigid_icp( + moving_points=scaled_reference_points, + fixed_points=sample_surface_points, + iterations=50, + tolerance=1.0e-4, + start_by_matching_centroids_only=False, + convergence="delta", + distance_mode="mean", + ) + else: + raise ValueError("registration_backend must be 'vtk' or 'numpy'.") if process is not None and retry_on_process_qc: body_centroid = _mask_centroid_physical(body.GetOutput()) @@ -863,11 +882,14 @@ def get_icp_with_scaling( f"transverse_offset={default_process_qc['transverse_offset_mm']:.2f} mm" ) if default_process_qc["status"] != "ok": - ogo.message("Default ICP failed process-vector QC; trying alternate PCA starts...") + ogo.message( + "Default ICP failed process-vector QC; trying alternate PCA starts " + "with the deterministic point-cloud backend..." + ) retry_candidates = [] - for initial in _spine_pca_initial_transforms(reference_points * scale_factors, sample_surface_points): + for initial in _spine_pca_initial_transforms(scaled_reference_points, sample_surface_points): candidate = estimate_rigid_icp( - moving_points=reference_points * scale_factors, + moving_points=scaled_reference_points, fixed_points=sample_surface_points, iterations=50, tolerance=1.0e-4, @@ -913,8 +935,8 @@ def get_icp_with_scaling( vtk_transform = _icp_transform_to_vtk(transform) ogo.message( - "ICP (with scaled reference) iterations=%d mean_distance=%0.4f" - % (transform["iterations"], transform["mean_distance"]) + "ICP (%s backend, scaled reference) iterations=%d mean_distance=%0.4f" + % (backend, transform["iterations"], transform["mean_distance"]) ) ogo.message("ICP reference-to-sample Matrix:") print_matrix(vtk_transform.GetMatrix()) @@ -1362,6 +1384,7 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, registration_scale = kwargs.get("registration_scale", DEFAULT_SPINE_REGISTRATION_SCALE) registration_min_scale = kwargs.get("registration_min_scale", DEFAULT_SPINE_REGISTRATION_MIN_SCALE) registration_max_scale = kwargs.get("registration_max_scale", DEFAULT_SPINE_REGISTRATION_MAX_SCALE) + registration_backend = kwargs.get("registration_backend", DEFAULT_SPINE_REGISTRATION_BACKEND) registration_retry_on_process_qc = kwargs.get( "registration_retry_on_process_qc", DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC, @@ -1468,6 +1491,7 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, min_scale=registration_min_scale, max_scale=registration_max_scale, retry_on_process_qc=registration_retry_on_process_qc, + backend=registration_backend, ) # Transform Images and resample at the same time (less interpolation) @@ -1894,8 +1918,8 @@ def main(): help="Minimum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") parser.add_argument("--registration_max_scale", type=str, default=DEFAULT_SPINE_REGISTRATION_MAX_SCALE, help="Maximum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") - parser.add_argument("--registration_backend", choices=("vtk",), default=DEFAULT_SPINE_REGISTRATION_BACKEND, - help="Spine reference alignment backend. (default: %(default)s)") + parser.add_argument("--registration_backend", choices=("vtk", "numpy"), default=DEFAULT_SPINE_REGISTRATION_BACKEND, + help="Spine reference alignment backend. vtk keeps the original VTK ICP solver; numpy uses the deterministic point-cloud helper. (default: %(default)s)") parser.add_argument("--top_node_set_id", type=int, default=DEFAULT_SPINE_TOP_NODE_SET_ID, help="ID for the top node set. (default: %(default)s)") parser.add_argument("--bottom_node_set_id", type=int, default=DEFAULT_SPINE_BOTTOM_NODE_SET_ID, diff --git a/tests/fea/test_alignment.py b/tests/fea/test_alignment.py index 9ffbb56..3b0a139 100644 --- a/tests/fea/test_alignment.py +++ b/tests/fea/test_alignment.py @@ -59,6 +59,35 @@ def test_estimate_rigid_icp_recovers_simple_translation(): assert transform["translation"] == pytest.approx((3.0, -2.0, 5.0)) +def test_estimate_rigid_icp_vtk_recovers_simple_translation(): + pytest.importorskip("vtk") + np = pytest.importorskip("numpy") + + from ogo.fea.alignment import estimate_rigid_icp_vtk + + moving = np.asarray( + [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + [1.0, 1.0, 0.0], + [1.0, 0.0, 1.0], + ] + ) + fixed = moving + np.asarray([3.0, -2.0, 5.0]) + + transform = estimate_rigid_icp_vtk( + moving_points=moving, + fixed_points=fixed, + iterations=20, + maximum_mean_distance=1.0e-6, + ) + + assert transform["rotation"] == pytest.approx(np.eye(3), abs=1.0e-6) + assert transform["translation"] == pytest.approx((3.0, -2.0, 5.0), abs=1.0e-6) + + def test_invert_point_transform_reverses_row_vector_transform(): np = pytest.importorskip("numpy") From f72d0cc067e638ce39bac914040093182aa4fa47 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:21:06 -0600 Subject: [PATCH 47/67] feat: expose spine ICP tuning options --- ogo/cli/GenerateFEM.py | 59 ++++++++++++++++++- ogo/fea/README.md | 21 +++++++ ogo/fea/spine.py | 24 ++++++-- tests/cli/test_GenerateFEM.py | 41 +++++++++++++ .../fixtures/fe_workflow_defaults_locked.json | 2 + tests/fea/test_workflow_locked_defaults.py | 2 + 6 files changed, 141 insertions(+), 8 deletions(-) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 1e414af..14e9e69 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -23,6 +23,8 @@ DEFAULT_SPINE_REGISTRATION_MAX_SCALE, DEFAULT_SPINE_REGISTRATION_MIN_SCALE, DEFAULT_SPINE_REGISTRATION_BACKEND, + DEFAULT_SPINE_REGISTRATION_ITERATIONS, + DEFAULT_SPINE_REGISTRATION_LANDMARKS, SPINE_ALIGNMENT_METHOD, default_spine_reference_path, solve_report_profile as spine_solve_report_profile, @@ -122,6 +124,18 @@ def spine_preset_args(name: str) -> List[str]: return list(SPINE_PRESETS[name]) +def spine_registration_args(args: argparse.Namespace) -> List[str]: + """Return lower-level spine registration options owned by the wrapper.""" + return [ + "--registration_backend", + str(args.registration_backend), + "--registration_landmarks", + str(args.registration_landmarks), + "--registration_iterations", + str(args.registration_iterations), + ] + + def build_spine_command( *, calibrated_image: Path, @@ -515,6 +529,16 @@ def write_modeling_metadata( "--registration_backend", DEFAULT_SPINE_REGISTRATION_BACKEND, ), + "registration_landmarks": option_int( + generator_argv, + "--registration_landmarks", + DEFAULT_SPINE_REGISTRATION_LANDMARKS, + ), + "registration_iterations": option_int( + generator_argv, + "--registration_iterations", + DEFAULT_SPINE_REGISTRATION_ITERATIONS, + ), }, "image_processing": { "iso_resolution_mm": option_float(generator_argv, "--iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM), @@ -1152,6 +1176,32 @@ def build_parser() -> argparse.ArgumentParser: "explicit lower-level options." ), ) + spine.add_argument( + "--registration_backend", + choices=("vtk", "numpy"), + default=DEFAULT_SPINE_REGISTRATION_BACKEND, + help=( + "Spine reference alignment backend. vtk keeps the original VTK ICP " + "solver; numpy uses the deterministic point-cloud helper. " + "(default: %(default)s)" + ), + ) + spine.add_argument( + "--registration_landmarks", + type=int, + default=DEFAULT_SPINE_REGISTRATION_LANDMARKS, + help=( + "Maximum spine ICP landmarks/sampled points. For vtk this maps to " + "SetMaximumNumberOfLandmarks; for numpy this caps sampled surface " + "points. (default: %(default)s)" + ), + ) + spine.add_argument( + "--registration_iterations", + type=int, + default=DEFAULT_SPINE_REGISTRATION_ITERATIONS, + help="Maximum spine ICP iterations. (default: %(default)s)", + ) hip = subparsers.add_parser( "hip", @@ -1189,10 +1239,15 @@ def main(argv: Optional[Sequence[str]] = None) -> None: pistoia_mask_args.extend(["--pistoia_mask_label", str(label)]) if args.model_type == "spine": - spine_extra_args = spine_preset_args(args.preset) + pistoia_mask_args + list(extra_args) + [ + spine_extra_args = ( + spine_preset_args(args.preset) + + spine_registration_args(args) + + pistoia_mask_args + + list(extra_args) + + [ "--quality_control", str(bool(args.debug)), - ] + ]) for target in args.vertebra: cmd = build_spine_command( calibrated_image=args.calibrated_image, diff --git a/ogo/fea/README.md b/ogo/fea/README.md index f7db5a9..8a2a75b 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -469,6 +469,27 @@ Available spine presets: | `benchmark-nonlinear` | Nonlinear material/yield settings for benchmark-style runs. | | `none` | Pass only explicit lower-level options. | +Spine registration can be switched without editing code. The maintained default +keeps the original VTK ICP behavior: + +```bash +ogoFEA spine image.nii.gz spine_labels.nii.gz \ + --vertebra L1:20:48 \ + --registration_backend vtk \ + --registration_landmarks 250 \ + --registration_iterations 75 +``` + +For sensitivity checks, the deterministic point-cloud ICP path can be selected: + +```bash +ogoFEA spine image.nii.gz spine_labels.nii.gz \ + --vertebra L1:20:48 \ + --registration_backend numpy \ + --registration_landmarks 8000 \ + --registration_iterations 50 +``` + ## Femur Model The femur workflow builds one sideways-fall model per side: diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 6aa81fa..9e6caee 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -60,6 +60,8 @@ DEFAULT_SPINE_REGISTRATION_MIN_SCALE = "0.8,0.8,0.75" DEFAULT_SPINE_REGISTRATION_MAX_SCALE = "1.2,1.2,1.3" DEFAULT_SPINE_REGISTRATION_BACKEND = "vtk" +DEFAULT_SPINE_REGISTRATION_LANDMARKS = 250 +DEFAULT_SPINE_REGISTRATION_ITERATIONS = 75 DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC = True DEFAULT_SPINE_REFERENCE_FILENAME = "L4_BODY_SPINE_COMPRESSION_REF.vtk" SPINE_CONTACT_SIZE_FRACTION = (1.6, 1.6) @@ -812,17 +814,19 @@ def get_icp_with_scaling( max_scale=(1.2, 1.2, 1.3), retry_on_process_qc=DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC, backend=DEFAULT_SPINE_REGISTRATION_BACKEND, + landmarks=DEFAULT_SPINE_REGISTRATION_LANDMARKS, + iterations=DEFAULT_SPINE_REGISTRATION_ITERATIONS, ): sample_surface_points = surface_points_from_vtk_mask( body.GetOutput(), - max_points=8000, + max_points=landmarks, sample_mode="stride", ) reference_polydata = ogo.readPolyData(reference_path) reference_points = sample_points( polydata_points(reference_polydata), - max_points=8000, + max_points=landmarks, mode="linspace", ) @@ -849,8 +853,8 @@ def get_icp_with_scaling( transform = estimate_rigid_icp_vtk( moving_points=scaled_reference_points, fixed_points=sample_surface_points, - landmarks=250, - iterations=75, + landmarks=landmarks, + iterations=iterations, maximum_mean_distance=0.05, distance_mode="rms", start_by_matching_centroids=True, @@ -859,7 +863,7 @@ def get_icp_with_scaling( transform = estimate_rigid_icp( moving_points=scaled_reference_points, fixed_points=sample_surface_points, - iterations=50, + iterations=iterations, tolerance=1.0e-4, start_by_matching_centroids_only=False, convergence="delta", @@ -891,7 +895,7 @@ def get_icp_with_scaling( candidate = estimate_rigid_icp( moving_points=scaled_reference_points, fixed_points=sample_surface_points, - iterations=50, + iterations=iterations, tolerance=1.0e-4, start_by_matching_centroids_only=False, convergence="delta", @@ -1385,6 +1389,8 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, registration_min_scale = kwargs.get("registration_min_scale", DEFAULT_SPINE_REGISTRATION_MIN_SCALE) registration_max_scale = kwargs.get("registration_max_scale", DEFAULT_SPINE_REGISTRATION_MAX_SCALE) registration_backend = kwargs.get("registration_backend", DEFAULT_SPINE_REGISTRATION_BACKEND) + registration_landmarks = kwargs.get("registration_landmarks", DEFAULT_SPINE_REGISTRATION_LANDMARKS) + registration_iterations = kwargs.get("registration_iterations", DEFAULT_SPINE_REGISTRATION_ITERATIONS) registration_retry_on_process_qc = kwargs.get( "registration_retry_on_process_qc", DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC, @@ -1492,6 +1498,8 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, max_scale=registration_max_scale, retry_on_process_qc=registration_retry_on_process_qc, backend=registration_backend, + landmarks=registration_landmarks, + iterations=registration_iterations, ) # Transform Images and resample at the same time (less interpolation) @@ -1920,6 +1928,10 @@ def main(): help="Maximum sx,sy,sz clamp for automatic PCA registration scaling. (default: %(default)s)") parser.add_argument("--registration_backend", choices=("vtk", "numpy"), default=DEFAULT_SPINE_REGISTRATION_BACKEND, help="Spine reference alignment backend. vtk keeps the original VTK ICP solver; numpy uses the deterministic point-cloud helper. (default: %(default)s)") + parser.add_argument("--registration_landmarks", type=int, default=DEFAULT_SPINE_REGISTRATION_LANDMARKS, + help="Maximum spine ICP landmarks/sampled points. For vtk this maps to SetMaximumNumberOfLandmarks; for numpy this caps sampled surface points. (default: %(default)s)") + parser.add_argument("--registration_iterations", type=int, default=DEFAULT_SPINE_REGISTRATION_ITERATIONS, + help="Maximum spine ICP iterations. (default: %(default)s)") parser.add_argument("--top_node_set_id", type=int, default=DEFAULT_SPINE_TOP_NODE_SET_ID, help="ID for the top node set. (default: %(default)s)") parser.add_argument("--bottom_node_set_id", type=int, default=DEFAULT_SPINE_BOTTOM_NODE_SET_ID, diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index e5a7331..f699ae0 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -43,6 +43,14 @@ ] SPINE_DEFAULT_QC_ARGS = ["--quality_control", "False"] +SPINE_DEFAULT_REGISTRATION_ARGS = [ + "--registration_backend", + "vtk", + "--registration_landmarks", + "250", + "--registration_iterations", + "75", +] @pytest.fixture @@ -92,6 +100,7 @@ def test_spine_runs_each_requested_vertebra(monkeypatch, tmp_path, solve_calls): "--output_path", "models", *BENCHMARK_LINEAR_ARGS, + *SPINE_DEFAULT_REGISTRATION_ARGS, "--iso_resolution", "1.2", *SPINE_DEFAULT_QC_ARGS, @@ -108,6 +117,7 @@ def test_spine_runs_each_requested_vertebra(monkeypatch, tmp_path, solve_calls): "--output_path", "models", *BENCHMARK_LINEAR_ARGS, + *SPINE_DEFAULT_REGISTRATION_ARGS, "--iso_resolution", "1.2", *SPINE_DEFAULT_QC_ARGS, @@ -225,6 +235,7 @@ def test_spine_preset_none_keeps_command_minimal(monkeypatch, solve_calls): "7", "--appendix", "L3", + *SPINE_DEFAULT_REGISTRATION_ARGS, *SPINE_DEFAULT_QC_ARGS, ] ] @@ -257,11 +268,39 @@ def test_spine_nonlinear_benchmark_preset(monkeypatch, solve_calls): "--appendix", "T10", *BENCHMARK_NONLINEAR_ARGS, + *SPINE_DEFAULT_REGISTRATION_ARGS, *SPINE_DEFAULT_QC_ARGS, ] ] +def test_spine_forwards_registration_options(monkeypatch, solve_calls): + calls = [] + monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "spine", + "density.nii.gz", + "spine_mask.nii.gz", + "--vertebra", + "L1:20:48", + "--registration_backend", + "numpy", + "--registration_landmarks", + "8000", + "--registration_iterations", + "50", + "--no-solve", + ] + ) + + assert "--registration_backend" in calls[0] + assert calls[0][calls[0].index("--registration_backend") + 1] == "numpy" + assert calls[0][calls[0].index("--registration_landmarks") + 1] == "8000" + assert calls[0][calls[0].index("--registration_iterations") + 1] == "50" + + def test_spine_explicit_options_follow_preset_for_overrides(monkeypatch, solve_calls): calls = [] monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) @@ -697,6 +736,8 @@ def test_spine_modeling_metadata_records_materials_and_bcs(tmp_path): assert data["target"] == {"vertebra": "L1", "body_label": 2, "process_label": 3} assert data["alignment"]["registration_backend"] == "vtk" + assert data["alignment"]["registration_landmarks"] == 250 + assert data["alignment"]["registration_iterations"] == 75 assert data["geometry"]["model_coordinates"] == "preprocessed_image_physical_space" assert data["materials"]["trabecular"]["elastic_E_func"] == "kopperdahl_trab_E" assert data["materials"]["cortical"]["material_id_range"] == [129, 256] diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index 11d8109..856d771 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -145,6 +145,8 @@ "pmma_thickness_mm": 10, "reference_filename": "L4_BODY_SPINE_COMPRESSION_REF.vtk", "registration_backend": "vtk", + "registration_iterations": 75, + "registration_landmarks": 250, "registration_max_scale": "1.2,1.2,1.3", "registration_min_scale": "0.8,0.8,0.75", "superior_contact_center_fraction": [ diff --git a/tests/fea/test_workflow_locked_defaults.py b/tests/fea/test_workflow_locked_defaults.py index 7846609..92fa774 100644 --- a/tests/fea/test_workflow_locked_defaults.py +++ b/tests/fea/test_workflow_locked_defaults.py @@ -29,6 +29,8 @@ def _locked_workflow_defaults(): "top_node_set_id": spine.DEFAULT_SPINE_TOP_NODE_SET_ID, "bottom_node_set_id": spine.DEFAULT_SPINE_BOTTOM_NODE_SET_ID, "registration_backend": spine.DEFAULT_SPINE_REGISTRATION_BACKEND, + "registration_landmarks": spine.DEFAULT_SPINE_REGISTRATION_LANDMARKS, + "registration_iterations": spine.DEFAULT_SPINE_REGISTRATION_ITERATIONS, "registration_min_scale": spine.DEFAULT_SPINE_REGISTRATION_MIN_SCALE, "registration_max_scale": spine.DEFAULT_SPINE_REGISTRATION_MAX_SCALE, "reference_filename": spine.DEFAULT_SPINE_REFERENCE_FILENAME, From 5453542f6fbff39fdc096e617ce644ee3deed18c Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Tue, 8 Sep 2026 10:22:20 -0600 Subject: [PATCH 48/67] test: update spine ICP dry-run expectation --- tests/cli/test_GenerateFEM.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index f699ae0..12d5ffe 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -205,7 +205,8 @@ def test_dry_run_prints_commands_without_running(monkeypatch, capsys): capsys.readouterr().out.strip() == "ogoFEA-spine-builder density.nii.gz spine_mask.nii.gz " "--mask_threshold 6 --process_mask_threshold 5 --appendix L2 " - "--quality_control False" + + " ".join(SPINE_DEFAULT_REGISTRATION_ARGS) + + " --quality_control False" ) From 2b875c049403200cf8f2af6aa04c73bde4d99480 Mon Sep 17 00:00:00 2001 From: wallematthias Date: Thu, 1 Oct 2026 20:26:48 -0600 Subject: [PATCH 49/67] feat: lock hip and spine FEA recipe defaults --- ogo/cli/CheckFEModelBC.py | 13 ++ ogo/cli/GenerateFEM.py | 82 +++++++++- ogo/fea/README.md | 52 ++++-- ogo/fea/boundary.py | 151 +++++++++++++++--- ogo/fea/femur.py | 56 +++++-- ogo/fea/spine.py | 124 +++++++++----- tests/cli/test_GenerateFEM.py | 72 +++++++-- .../fixtures/fe_workflow_defaults_locked.json | 17 +- tests/fea/test_boundary.py | 150 +++++++++++++++++ tests/fea/test_workflow_locked_defaults.py | 11 ++ 10 files changed, 620 insertions(+), 108 deletions(-) diff --git a/ogo/cli/CheckFEModelBC.py b/ogo/cli/CheckFEModelBC.py index d5bb196..02ddcbd 100644 --- a/ogo/cli/CheckFEModelBC.py +++ b/ogo/cli/CheckFEModelBC.py @@ -26,6 +26,15 @@ def _node_coordinates(root, node_numbers): def _bounds(points): + if len(points) == 0: + return { + "x_min": None, + "x_max": None, + "y_min": None, + "y_max": None, + "z_min": None, + "z_max": None, + } mins = points.min(axis=0) maxs = points.max(axis=0) return { @@ -39,11 +48,15 @@ def _bounds(points): def _centroid(points): + if len(points) == 0: + return {"x": None, "y": None, "z": None} center = points.mean(axis=0) return {"x": float(center[0]), "y": float(center[1]), "z": float(center[2])} def _z_percentiles(points): + if len(points) == 0: + return {"p05": None, "p50": None, "p95": None} z = np.asarray(points[:, 2], dtype=float) return { "p05": float(np.percentile(z, 5)), diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 14e9e69..5241baf 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -38,6 +38,9 @@ DEFAULT_FEMUR_ISO_RESOLUTION_MM, DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, + DEFAULT_FEMUR_REGISTRATION_BACKEND, + DEFAULT_FEMUR_REGISTRATION_ITERATIONS, + DEFAULT_FEMUR_REGISTRATION_LANDMARKS, DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, DEFAULT_PMMA_INTRUSION_MM, DEFAULT_PMMA_THICKNESS_MM, @@ -136,6 +139,18 @@ def spine_registration_args(args: argparse.Namespace) -> List[str]: ] +def femur_registration_args(args: argparse.Namespace) -> List[str]: + """Return lower-level femur registration options owned by the wrapper.""" + return [ + "--registration_backend", + str(args.registration_backend), + "--registration_landmarks", + str(args.registration_landmarks), + "--registration_iterations", + str(args.registration_iterations), + ] + + def build_spine_command( *, calibrated_image: Path, @@ -486,6 +501,11 @@ def write_modeling_metadata( body_label = option_int(generator_argv, "--mask_threshold", 0) process_label = option_int(generator_argv, "--process_mask_threshold", 0) appendix = option_value(generator_argv, "--appendix") + spine_reference_path = option_value( + generator_argv, + "--reference_path", + str(default_spine_reference_path()), + ) if args.use_absolute_fe_displacement: spine_displacement = absolute_displacement_metadata( generator_argv, @@ -508,11 +528,7 @@ def write_modeling_metadata( }, "alignment": { "method": SPINE_ALIGNMENT_METHOD, - "reference_path": option_value( - generator_argv, - "--reference_path", - str(default_spine_reference_path()), - ), + "reference_path": spine_reference_path, "registration_scale": option_value(generator_argv, "--registration_scale", "auto"), "registration_min_scale": option_value( generator_argv, @@ -707,6 +723,21 @@ def write_modeling_metadata( "alignment": { "method": "ICP to side-specific femur reference from cropped/padded input geometry", "reference_path": option_value(generator_argv, "--reference_path", "bundled side-specific femur reference"), + "registration_backend": option_value( + generator_argv, + "--registration_backend", + DEFAULT_FEMUR_REGISTRATION_BACKEND, + ), + "registration_landmarks": option_int( + generator_argv, + "--registration_landmarks", + DEFAULT_FEMUR_REGISTRATION_LANDMARKS, + ), + "registration_iterations": option_int( + generator_argv, + "--registration_iterations", + DEFAULT_FEMUR_REGISTRATION_ITERATIONS, + ), }, "image_processing": { "iso_resolution_mm": option_float(generator_argv, "--iso_resolution", DEFAULT_FEMUR_ISO_RESOLUTION_MM), @@ -1176,6 +1207,15 @@ def build_parser() -> argparse.ArgumentParser: "explicit lower-level options." ), ) + spine.add_argument( + "--reference_path", + type=Path, + default=None, + help=( + "Optional spine ICP reference surface. If omitted, the bundled " + "vertebral-body reference is used." + ), + ) spine.add_argument( "--registration_backend", choices=("vtk", "numpy"), @@ -1202,7 +1242,6 @@ def build_parser() -> argparse.ArgumentParser: default=DEFAULT_SPINE_REGISTRATION_ITERATIONS, help="Maximum spine ICP iterations. (default: %(default)s)", ) - hip = subparsers.add_parser( "hip", help="Generate hip sideways-fall FE models for left, right, or both femurs.", @@ -1215,6 +1254,31 @@ def build_parser() -> argparse.ArgumentParser: default=None, help="Femur side to process. Repeatable; defaults to both.", ) + hip.add_argument( + "--registration_backend", + choices=("vtk", "numpy"), + default=DEFAULT_FEMUR_REGISTRATION_BACKEND, + help=( + "Femur reference alignment backend. vtk uses vtkIterativeClosestPointTransform; " + "numpy uses the deterministic point-cloud helper. (default: %(default)s)" + ), + ) + hip.add_argument( + "--registration_landmarks", + type=int, + default=DEFAULT_FEMUR_REGISTRATION_LANDMARKS, + help=( + "Maximum femur ICP landmarks/sampled points. For vtk this maps to " + "SetMaximumNumberOfLandmarks; for numpy this caps sampled surface " + "points. (default: %(default)s)" + ), + ) + hip.add_argument( + "--registration_iterations", + type=int, + default=DEFAULT_FEMUR_REGISTRATION_ITERATIONS, + help="Maximum femur ICP iterations. (default: %(default)s)", + ) return parser @@ -1239,9 +1303,13 @@ def main(argv: Optional[Sequence[str]] = None) -> None: pistoia_mask_args.extend(["--pistoia_mask_label", str(label)]) if args.model_type == "spine": + reference_args = [] + if args.reference_path is not None: + reference_args.extend(["--reference_path", str(args.reference_path)]) spine_extra_args = ( spine_preset_args(args.preset) + spine_registration_args(args) + + reference_args + pistoia_mask_args + list(extra_args) + [ @@ -1277,7 +1345,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: return if args.model_type == "hip": - femur_extra_args = pistoia_mask_args + list(extra_args) + femur_extra_args = femur_registration_args(args) + pistoia_mask_args + list(extra_args) for side in expand_sides(args.side or ["both"]): cmd = build_femur_command( calibrated_image=args.calibrated_image, diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 8a2a75b..18abd4c 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -58,7 +58,7 @@ The anatomy-specific implementation is split below the CLI: | Anatomy | Main file | What belongs there | | --- | --- | --- | -| Spine compression | `ogo/fea/spine.py` | Vertebra labels, body/process QC, VTK spine ICP, 1 mm resampling, stable PMMA cap generation, axial compression defaults. | +| Spine compression | `ogo/fea/spine.py` | Vertebra labels, body/process QC, dense NumPy spine ICP, 1 mm resampling, stable PMMA cap generation, axial compression defaults. | | Hip sideways fall | `ogo/fea/femur.py` | Side handling, femur reference alignment, rough pre-ICP crop, post-ICP GT-length crop, femoral-head/GT/distal supports, sideways-fall defaults. | | Shared material tables | `ogo/fea/materials.py` | Bone material ID ranges, PMMA material, shared femur/spine material-table construction. | | Material laws | `ogo/fea/material_laws.py` | Density-to-modulus and yield-strength functions such as `default_E` and `kopperdahl_trab_E`. | @@ -74,11 +74,9 @@ For spine, the maintained path is: 2. `spine.py::main` thresholds the requested body/process labels, crops around the vertebra, checks posterior-process orientation, and runs scaled ICP to the bundled L4 body reference from `default_spine_reference_path`. The - default backend is VTK ICP through `alignment.py::estimate_rigid_icp_vtk`, - matching the original spine registration behavior. The deterministic NumPy - point-cloud ICP helper remains available as `--registration_backend numpy` - and is also used for alternate PCA-start rescue attempts when process-vector - QC flags an implausible orientation. + default backend is the deterministic NumPy point-cloud ICP helper with dense + surface sampling. The same helper is used for alternate PCA-start rescue + attempts when process-vector QC flags an implausible orientation. 3. `spine.py` applies the transform and resamples the density image with cubic interpolation and labels/masks with nearest-neighbor interpolation. 4. `boundary.py::generate_bone_cap_mask` and related helpers generate superior @@ -469,23 +467,28 @@ Available spine presets: | `benchmark-nonlinear` | Nonlinear material/yield settings for benchmark-style runs. | | `none` | Pass only explicit lower-level options. | -Spine registration can be switched without editing code. The maintained default -keeps the original VTK ICP behavior: +The maintained spine registration default uses the vertebral body label only, +the bundled body reference, dense NumPy ICP, and process-vector QC. Use +vertebra-specific labels for a single vertebra, for example `L1:20:48`, where +`20` is the L1 body and `48` is the L1 posterior process. Do not use generic +body/process relabel masks that contain multiple vertebrae as a single-vertebra +input. ```bash ogoFEA spine image.nii.gz spine_labels.nii.gz \ --vertebra L1:20:48 \ - --registration_backend vtk \ - --registration_landmarks 250 \ - --registration_iterations 75 + --registration_backend numpy \ + --registration_landmarks 8000 \ + --registration_iterations 50 ``` -For sensitivity checks, the deterministic point-cloud ICP path can be selected: +VTK ICP remains available for sensitivity checks, but it is not the maintained +default for the spine workflow: ```bash ogoFEA spine image.nii.gz spine_labels.nii.gz \ --vertebra L1:20:48 \ - --registration_backend numpy \ + --registration_backend vtk \ --registration_landmarks 8000 \ --registration_iterations 50 ``` @@ -496,7 +499,10 @@ The femur workflow builds one sideways-fall model per side: 1. Read the calibrated image and whole-femur mask. 2. Pre-rotate the side to a stable starting orientation. -3. ICP-align to the left femur reference or the mirrored right reference. +3. ICP-align to the left femur reference or the mirrored right reference. The + maintained hip default is the deterministic NumPy point-cloud helper with + 8000 sampled points, matching the previous femur registration path. VTK ICP + remains available explicitly as `--registration_backend vtk`. 4. Apply transform and isotropic resampling through the shared VTK reslice helper. The default output spacing is `1.0 x 1.0 x 1.0 mm`. 5. Smooth the transformed femur mask with one binary close/open pass only when @@ -676,7 +682,7 @@ Change PMMA dimensions: ```bash ogoFEA hip image.nii.gz femur_mask.nii.gz \ --side left \ - --pmma_thick 6 \ + --pmma_thick 10 \ --pmma_intrusion 6 ``` @@ -695,6 +701,17 @@ Intrusion does not overwrite vertebral-body voxels; it limits how far from the superior/inferior body surface the cap is allowed to search for supporting anatomy before generating the flat PMMA disk. +Stable-contact selection is enabled for the spine default. The locked recipe +ignores the superficial 10% of the projected contact columns +(`--stable_surface_trim_fraction 0.10`) and uses that deeper footprint only +when it is at least 3 mm behind the first-contact surface +(`--stable_surface_min_shift 3`). This catches osteophyte/ridge contacts while +leaving normal shallow surface variation alone. The generated disk still uses +the requested `--pmma_thick` and `--pmma_intrusion`; stable contact does not +replace or silently reduce intrusion. The disk mask is repaired with in-plane +gap closing, constrained not to overwrite anatomy, and reduced to the largest +connected support component. + After vtkbone identifies visible cap nodes, spine compression filters each cap node set to the dominant coordinate plane along the load axis. This removes small rim/contact-side nodes from the jelly-bean disk surface while keeping the @@ -707,6 +724,11 @@ ogoFEA spine image.nii.gz labels.nii.gz \ --pmma_intrusion 6 ``` +The stable-contact values are code-level defaults in +`DEFAULT_SPINE_STABLE_CONTACT_*` and are locked by +`tests/fea/test_workflow_locked_defaults.py`. Routine spine FEA should use the +defaults rather than carrying per-run contact tuning flags. + Run model generation only, inspect the `.n88model`, then solve later: ```bash diff --git a/ogo/fea/boundary.py b/ogo/fea/boundary.py index 6d3886a..d2a4226 100644 --- a/ogo/fea/boundary.py +++ b/ogo/fea/boundary.py @@ -614,6 +614,26 @@ def _fill_short_boolean_gaps_in_volume(values, max_gap=2): return out +def _close_boolean_gaps_in_xy_slices(values, max_gap=2): + """Aggressively close in-plane disk holes without bridging across z slices.""" + import numpy as np + from scipy import ndimage + + out = np.asarray(values, dtype=bool).copy() + radius = max(int(max_gap), 0) + if radius <= 0: + return out + structure = np.ones((2 * radius + 1, 2 * radius + 1), dtype=bool) + for z in range(out.shape[2]): + plane = out[:, :, z] + if not np.any(plane): + continue + closed = ndimage.binary_closing(plane, structure=structure) + filled = ndimage.binary_fill_holes(closed) + out[:, :, z] = plane | filled + return out + + def _clean_projected_footprint(mask): """Clean a candidate cap footprint before it is extruded into a disk.""" import numpy as np @@ -841,6 +861,8 @@ def projected_material_disk_required_bounds( stable_surface_fraction=0.55, stable_surface_min_area_fraction=0.12, stable_surface_max_depth=None, + stable_surface_min_shift=0.0, + stable_surface_trim_fraction=0.0, ): """Return conservative physical bounds needed for a plane-projected disk.""" import numpy as np @@ -905,6 +927,8 @@ def projected_material_disk_required_bounds( stable_surface_fraction=stable_surface_fraction, stable_surface_min_area_fraction=stable_surface_min_area_fraction, stable_surface_max_depth=stable_surface_max_depth, + stable_surface_min_shift=stable_surface_min_shift, + stable_surface_trim_fraction=stable_surface_trim_fraction, ) surface_distance = float(stable_contact["stable_distance"]) cap_inner_distance = surface_distance + max(float(intrusion), 0.0) @@ -1022,8 +1046,18 @@ def _stable_surface_contact_from_bucket_distances( stable_surface_fraction=0.55, stable_surface_min_area_fraction=0.12, stable_surface_max_depth=None, + stable_surface_min_shift=0.0, + stable_surface_trim_fraction=0.0, ): - """Find the first broad projected surface behind isolated protrusions.""" + """Find the first broad projected surface behind isolated protrusions. + + The fraction criteria identify a broad footprint. ``stable_surface_min_shift`` + then makes the correction depth-aware: shallow shifts are treated as normal + anatomy/noise rather than osteophytes and the first-contact surface is kept. + When ``stable_surface_trim_fraction`` is positive, the selected surface is + the projected depth after ignoring that superficial fraction of the support + footprint instead of the older broad-footprint fraction rule. + """ import numpy as np if not distance_by_key: @@ -1034,6 +1068,7 @@ def _stable_surface_contact_from_bucket_distances( "first_area": 0, "stable_area": 0, "peak_area": 0, + "used_stable_surface": False, "stable_keys": set(), } @@ -1068,11 +1103,18 @@ def _stable_surface_contact_from_bucket_distances( peak_area = max(peak_area, area) surfaces.append((float(threshold), area, clean_keys)) - target_area = max( - 1, - int(np.ceil(max(float(stable_surface_fraction), 0.0) * peak_area)), - int(np.ceil(max(float(stable_surface_min_area_fraction), 0.0) * full_area)), - ) + trim_fraction = max(float(stable_surface_trim_fraction), 0.0) + if trim_fraction > 0.0: + target_area = max( + 1, + int(np.ceil(min(trim_fraction, 1.0) * peak_area)), + ) + else: + target_area = max( + 1, + int(np.ceil(max(float(stable_surface_fraction), 0.0) * peak_area)), + int(np.ceil(max(float(stable_surface_min_area_fraction), 0.0) * full_area)), + ) stable_distance, stable_area, stable_keys = surfaces[-1] for threshold, area, keys in surfaces: if area >= target_area: @@ -1080,14 +1122,22 @@ def _stable_surface_contact_from_bucket_distances( stable_area = area stable_keys = keys break + stable_depth = float(stable_distance - first_distance) + used_stable_surface = stable_depth >= max(float(stable_surface_min_shift), 0.0) + if not used_stable_surface: + stable_distance = first_distance + stable_depth = 0.0 + stable_area = first_area + stable_keys = _clean_footprint_keys(first_keys) return { "first_distance": first_distance, "stable_distance": float(stable_distance), - "stable_depth": float(stable_distance - first_distance), + "stable_depth": stable_depth, "first_area": int(first_area), "stable_area": int(stable_area), "peak_area": int(peak_area), + "used_stable_surface": bool(used_stable_surface), "stable_keys": stable_keys, } @@ -1107,6 +1157,8 @@ def projected_stable_surface_contact( stable_surface_fraction=0.55, stable_surface_min_area_fraction=0.12, stable_surface_max_depth=None, + stable_surface_min_shift=0.0, + stable_surface_trim_fraction=0.0, ): """Return stable projected contact-surface metrics for a physical plane.""" import numpy as np @@ -1169,6 +1221,8 @@ def projected_stable_surface_contact( stable_surface_fraction=stable_surface_fraction, stable_surface_min_area_fraction=stable_surface_min_area_fraction, stable_surface_max_depth=stable_surface_max_depth, + stable_surface_min_shift=stable_surface_min_shift, + stable_surface_trim_fraction=stable_surface_trim_fraction, ) @@ -1190,8 +1244,11 @@ def generate_projected_material_disk_mask( stable_surface_fraction=0.55, stable_surface_min_area_fraction=0.12, stable_surface_max_depth=None, + stable_surface_min_shift=0.0, + stable_surface_trim_fraction=0.0, close_gaps_3d=False, close_gaps_3d_max_gap=2, + keep_largest_component=False, material_mask=None, extent_start=(0, 0, 0), ): @@ -1277,19 +1334,35 @@ def generate_projected_material_disk_mask( stable_surface_fraction=stable_surface_fraction, stable_surface_min_area_fraction=stable_surface_min_area_fraction, stable_surface_max_depth=stable_surface_max_depth, + stable_surface_min_shift=stable_surface_min_shift, + stable_surface_trim_fraction=stable_surface_trim_fraction, ) - support_distance = float(stable_contact["stable_distance"]) - footprint_min = support_distance - max(2.0 * min(spacing), tolerance) - footprint_max = support_distance + intrusion + tolerance - stable_keys = _clean_footprint_keys( - { + if stable_contact["used_stable_surface"]: + support_distance = float(stable_contact["stable_distance"]) + footprint_min = support_distance - tolerance + footprint_max = support_distance + intrusion + tolerance + # Keep local surface bumps that are shallower than the selected + # support plane but smaller than the required osteophyte shift. + # Without this per-column guard, one real osteophyte can activate + # stable contact and then normal surface bumps punch holes in the + # PMMA footprint. + local_trim_limit = support_distance - max(float(stable_surface_min_shift), 0.0) - tolerance + support_keys = { key for key, distance in distance_by_key.items() - if footprint_min <= float(distance) <= footprint_max + if local_trim_limit <= float(distance) <= footprint_max } - ) - if not stable_keys: - return np.zeros(active.shape, dtype=bool) + stable_keys = _clean_footprint_keys(support_keys) + protrusion_keys = { + key + for key, distance in distance_by_key.items() + if float(distance) < local_trim_limit + } + stable_keys -= protrusion_keys + if not stable_keys: + return np.zeros(active.shape, dtype=bool) + else: + stable_surface = False if anatomy_constrained: depth_limit = support_distance + intrusion + tolerance @@ -1333,13 +1406,22 @@ def generate_projected_material_disk_mask( dtype=float, ) bucket_mask = np.isfinite(local_surface) + support_column_mask = bucket_mask.copy() flat_outer = cap_outer_distance if flat_outer >= support_distance: flat_outer = support_distance - thickness if stable_surface: + support_column_mask = np.asarray([key in stable_keys for key in keys], dtype=bool) + bucket_mask = support_column_mask + stable_surface_distance = np.where( + np.isfinite(local_surface), + local_surface, + support_distance - tolerance, + ) + stable_inner = np.minimum(cap_inner_distance, stable_surface_distance) depth_ok = ( (distance >= flat_outer - tolerance) - & (distance <= cap_inner_distance + tolerance) + & (distance <= stable_inner + 1.0e-9) ) else: local_min = np.minimum(flat_outer, local_surface) @@ -1347,24 +1429,45 @@ def generate_projected_material_disk_mask( depth_ok = (distance >= local_min - tolerance) & (distance <= local_max + tolerance) else: bucket_mask = np.ones(full_indices.shape[0], dtype=bool) + support_column_mask = bucket_mask.copy() depth_ok = ( (distance >= cap_outer_distance - tolerance) & (distance <= cap_inner_distance + tolerance) ) + supported_columns = np.zeros(active.shape, dtype=bool) + supported_columns[tuple(full_indices[support_column_mask].T)] = True keep = inside & depth_ok & empty & bucket_mask disk = np.zeros(active.shape, dtype=bool) if np.any(keep): disk[tuple(full_indices[keep].T)] = True if close_gaps_3d and np.any(disk): - disk = _fill_short_boolean_gaps_in_volume( - disk, - max_gap=int(close_gaps_3d_max_gap), - ) + gap = int(close_gaps_3d_max_gap) + disk = _fill_short_boolean_gaps_in_volume(disk, max_gap=gap) + disk = _close_boolean_gaps_in_xy_slices(disk, max_gap=gap) + # The closing step is intentionally allowed to repair holes in the + # projected PMMA footprint. The final safety rule is that PMMA must + # never overwrite anatomy. disk &= ~material + if keep_largest_component and np.any(disk): + disk = _largest_connected_component(disk) return disk +def _largest_connected_component(mask): + """Return only the largest 3D connected component from a boolean mask.""" + import numpy as np + from scipy import ndimage + + mask = np.asarray(mask, dtype=bool) + labels, count = ndimage.label(mask, structure=np.ones((3, 3, 3), dtype=bool)) + if count <= 1: + return mask + sizes = np.bincount(labels.ravel()) + sizes[0] = 0 + return labels == int(np.argmax(sizes)) + + def _generate_projected_bone_caps_from_mask(mask, *, extrusion_axis, thickness, intrusion_depth, shape): """Generate inferior and superior bone caps from projected surface footprints. @@ -1647,8 +1750,11 @@ def generate_projected_material_disk_vtk( stable_surface_fraction=0.55, stable_surface_min_area_fraction=0.12, stable_surface_max_depth=None, + stable_surface_min_shift=0.0, + stable_surface_trim_fraction=0.0, close_gaps_3d=False, close_gaps_3d_max_gap=2, + keep_largest_component=False, output_value=1, ): """Generate a VTK material disk from a physical-space contact plane. @@ -1692,8 +1798,11 @@ def generate_projected_material_disk_vtk( stable_surface_fraction=stable_surface_fraction, stable_surface_min_area_fraction=stable_surface_min_area_fraction, stable_surface_max_depth=stable_surface_max_depth, + stable_surface_min_shift=stable_surface_min_shift, + stable_surface_trim_fraction=stable_surface_trim_fraction, close_gaps_3d=close_gaps_3d, close_gaps_3d_max_gap=close_gaps_3d_max_gap, + keep_largest_component=keep_largest_component, material_mask=exclusion, extent_start=(extent[0], extent[2], extent[4]), ) diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index e570787..aa1022f 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -43,6 +43,7 @@ from ogo.fea.alignment import ( estimate_rigid_icp, + estimate_rigid_icp_vtk, point_cloud_axis_lengths, polydata_from_points, polydata_points, @@ -85,6 +86,9 @@ DEFAULT_FEMUR_PROXIMAL_REFERENCE_WIDTH = "max_xy" DEFAULT_FEMUR_REFERENCE_MIN_SCALE = (0.8, 0.8, 0.75) DEFAULT_FEMUR_REFERENCE_MAX_SCALE = (1.2, 1.2, 1.3) +DEFAULT_FEMUR_REGISTRATION_BACKEND = "numpy" +DEFAULT_FEMUR_REGISTRATION_LANDMARKS = 8000 +DEFAULT_FEMUR_REGISTRATION_ITERATIONS = 50 DEFAULT_PMMA_THICKNESS_MM = 10.0 DEFAULT_PMMA_INTRUSION_MM = 6.0 DEFAULT_FEMUR_INPUT_MARGIN_MM = DEFAULT_PMMA_THICKNESS_MM + DEFAULT_PMMA_INTRUSION_MM @@ -2383,6 +2387,9 @@ def sidewaysFallFe(args): femur_input_margin = args.femur_input_margin femur_icp_transform_in = args.femur_icp_transform_in femur_icp_transform_out = args.femur_icp_transform_out + registration_backend = args.registration_backend + registration_landmarks = args.registration_landmarks + registration_iterations = args.registration_iterations cortical_label = args.cortical_label trabecular_label = args.trabecular_label mask_smoothing_spacing_threshold = args.mask_smoothing_spacing_threshold @@ -2433,6 +2440,9 @@ def sidewaysFallFe(args): ogo.message("Femur ICP Transform In: %s" % femur_icp_transform_in) if femur_icp_transform_out: ogo.message("Femur ICP Transform Out: %s" % femur_icp_transform_out) + ogo.message("Femur Registration Backend: %s" % registration_backend) + ogo.message("Femur Registration Landmarks: %d" % registration_landmarks) + ogo.message("Femur Registration Iterations: %d" % registration_iterations) if femur_cut_mode == "bbox_ratio": ogo.message("Femur BBox Ratio [reference, constrained, free]: %s" % str(femur_bbox_ratio)) ogo.message("Femur BBox Crop From [reference, constrained, free]: %s" % str(femur_bbox_crop_from)) @@ -2678,7 +2688,7 @@ def sidewaysFallFe(args): ogo.message("Aligning input with reference model...") sample_surface_points = surface_points_from_vtk_mask( registration_mask, - max_points=8000, + max_points=registration_landmarks, sample_mode="stride", sample_offset=0, ) @@ -2701,7 +2711,7 @@ def sidewaysFallFe(args): ref_poly, reference_scale = scale_reference_point_cloud_to_sample( ref_poly, sample_surface_points, - max_points=8000, + max_points=registration_landmarks, reference_sample_mode="linspace", min_scale=DEFAULT_FEMUR_REFERENCE_MIN_SCALE, max_scale=DEFAULT_FEMUR_REFERENCE_MAX_SCALE, @@ -2710,22 +2720,36 @@ def sidewaysFallFe(args): ogo.message("Sample femur axis lengths: %s" % str(reference_scale["sample_axis_lengths"])) ogo.message("Femur reference scale factors: %s" % str(reference_scale["scale_factors"])) - icp_transform = estimate_rigid_icp( - moving_points=polydata_points(ref_poly), - fixed_points=sample_surface_points, - iterations=50, - tolerance=1.0e-4, - start_by_matching_centroids_only=True, - convergence="delta", - distance_mode="mean", - ) + reference_surface_points = polydata_points(ref_poly) + if registration_backend == "vtk": + icp_transform = estimate_rigid_icp_vtk( + moving_points=reference_surface_points, + fixed_points=sample_surface_points, + landmarks=registration_landmarks, + iterations=registration_iterations, + maximum_mean_distance=0.05, + distance_mode="rms", + start_by_matching_centroids=True, + ) + elif registration_backend == "numpy": + icp_transform = estimate_rigid_icp( + moving_points=reference_surface_points, + fixed_points=sample_surface_points, + iterations=registration_iterations, + tolerance=1.0e-4, + start_by_matching_centroids_only=True, + convergence="delta", + distance_mode="mean", + ) + else: + raise ValueError("registration_backend must be 'vtk' or 'numpy'.") icp = point_transform_to_vtk_matrix( icp_transform["rotation"], icp_transform["translation"], ) ogo.message( - "ICP reference-to-sample iterations=%d mean_distance=%0.4f" - % (icp_transform["iterations"], icp_transform["mean_distance"]) + "ICP (%s backend) reference-to-sample iterations=%d mean_distance=%0.4f" + % (registration_backend, icp_transform["iterations"], icp_transform["mean_distance"]) ) if femur_icp_transform_out: try: @@ -3594,6 +3618,12 @@ def main(): help="Optional femur ICP transform JSON to reuse instead of estimating ICP. Intended for fixed-transform length sweeps. (default: %(default)s)") parser.add_argument("--femur_icp_transform_out", type=str, default=None, help="Optional path to write the estimated femur ICP transform JSON. (default: %(default)s)") + parser.add_argument("--registration_backend", choices=("vtk", "numpy"), default=DEFAULT_FEMUR_REGISTRATION_BACKEND, + help="Femur reference alignment backend. vtk uses vtkIterativeClosestPointTransform; numpy uses the deterministic point-cloud helper. (default: %(default)s)") + parser.add_argument("--registration_landmarks", type=int, default=DEFAULT_FEMUR_REGISTRATION_LANDMARKS, + help="Maximum femur ICP landmarks/sampled points. For vtk this maps to SetMaximumNumberOfLandmarks; for numpy this caps sampled surface points. (default: %(default)s)") + parser.add_argument("--registration_iterations", type=int, default=DEFAULT_FEMUR_REGISTRATION_ITERATIONS, + help="Maximum femur ICP iterations. (default: %(default)s)") parser.add_argument("--compartment_mask", type=str, default=None, help="Optional trabecular/cortical compartment mask aligned with the bone mask. Defaults: cortical=1, trabecular=2.") parser.add_argument("--pistoia_mask", type=str, default=None, diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 9e6caee..650ec87 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -6,11 +6,11 @@ posterior-process labels are explicit and traceable. 2. Crop the vertebral body, posterior process, calibrated image, and combined vertebra mask to the same bounding box. -3. ICP-align the vertebral body to the bundled L4 compression reference. By - default the reference is PCA-scaled within 0.8,0.8,0.75 to 1.2,1.2,1.3 before - VTK rigid ICP; users can override the scale/reference explicitly. A - deterministic NumPy point-cloud ICP backend is kept for debugging and - process-orientation rescue attempts. +3. ICP-align the vertebral body to the bundled L4 compression reference. The + reference is PCA-scaled within 0.8,0.8,0.75 to 1.2,1.2,1.3, then registered + with the deterministic NumPy point-cloud ICP backend using dense surface + sampling. Process-orientation QC retries alternate PCA starts and records + failures instead of silently accepting flipped models. 4. Apply the ICP transform and set the final output spacing to 1.0 x 1.0 x 1.0 mm in one shared VTK reslice helper. Image data use cubic interpolation; body/process labels use nearest-neighbor interpolation. @@ -35,13 +35,14 @@ DEFAULT_SPINE_ISO_RESOLUTION_MM = 1.0 DEFAULT_SPINE_PMMA_THICKNESS_MM = 10 DEFAULT_SPINE_PMMA_INTRUSION_MM = 6 -DEFAULT_SPINE_STABLE_CONTACT_ENABLED = True -DEFAULT_SPINE_STABLE_CONTACT_FRACTION = 0.55 +DEFAULT_SPINE_STABLE_CONTACT_FRACTION = 0.0 +DEFAULT_SPINE_STABLE_CONTACT_TRIM_FRACTION = 0.10 DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION = 0.12 DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM = 24.0 -DEFAULT_SPINE_STABLE_CONTACT_INTRUSION_MM = 2 +DEFAULT_SPINE_STABLE_CONTACT_MIN_SHIFT_MM = 3.0 DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D = True DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_MAX_GAP = 2 +DEFAULT_SPINE_STABLE_CONTACT_KEEP_LARGEST_COMPONENT = True DEFAULT_SPINE_PROCESS_QC_MAX_AXIAL_TO_TRANSVERSE_RATIO = 0.75 DEFAULT_SPINE_PROCESS_QC_AXIAL_TOLERANCE_MM = 2.0 DEFAULT_SPINE_PMMA_MATERIAL_ID = 5000 @@ -59,16 +60,16 @@ DEFAULT_SPINE_REGISTRATION_SCALE = None DEFAULT_SPINE_REGISTRATION_MIN_SCALE = "0.8,0.8,0.75" DEFAULT_SPINE_REGISTRATION_MAX_SCALE = "1.2,1.2,1.3" -DEFAULT_SPINE_REGISTRATION_BACKEND = "vtk" -DEFAULT_SPINE_REGISTRATION_LANDMARKS = 250 -DEFAULT_SPINE_REGISTRATION_ITERATIONS = 75 +DEFAULT_SPINE_REGISTRATION_BACKEND = "numpy" +DEFAULT_SPINE_REGISTRATION_LANDMARKS = 8000 +DEFAULT_SPINE_REGISTRATION_ITERATIONS = 50 DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC = True DEFAULT_SPINE_REFERENCE_FILENAME = "L4_BODY_SPINE_COMPRESSION_REF.vtk" SPINE_CONTACT_SIZE_FRACTION = (1.6, 1.6) SPINE_SUPERIOR_CONTACT_CENTER_FRACTION = (0.5, 0.5, 1.05) SPINE_INFERIOR_CONTACT_CENTER_FRACTION = (0.5, 0.5, -0.05) -SPINE_ALIGNMENT_METHOD = "scaled ICP to reference vertebral body" +SPINE_ALIGNMENT_METHOD = "scaled NumPy ICP to reference vertebral body" BENCHMARK_LINEAR_FE_DISPLACEMENT_MM = -0.2 BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM = -2.0 @@ -99,7 +100,7 @@ def solve_report_profile(preset=None): def default_spine_reference_path(): - """Return the bundled reference body used for spine ICP alignment.""" + """Return the bundled vertebral-body reference used for spine ICP alignment.""" return Path(__file__).resolve().parents[1] / "dat" / DEFAULT_SPINE_REFERENCE_FILENAME @@ -962,8 +963,8 @@ def get_icp(body, reference_path): icp.SetMeanDistanceModeToRMS() icp.SetMaximumMeanDistance(0.05) icp.CheckMeanDistanceOn() - icp.SetMaximumNumberOfLandmarks(250) - icp.SetMaximumNumberOfIterations(75) + icp.SetMaximumNumberOfLandmarks(DEFAULT_SPINE_REGISTRATION_LANDMARKS) + icp.SetMaximumNumberOfIterations(DEFAULT_SPINE_REGISTRATION_ITERATIONS) icp.Update() ogo.message("ICP Matrix:") @@ -1381,6 +1382,39 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, iso_resolution = kwargs.get("iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM) pmma_thick = kwargs.get("pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM) pmma_intrusion = kwargs.get("pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM) + stable_surface_fraction = float( + kwargs.get("stable_surface_fraction", DEFAULT_SPINE_STABLE_CONTACT_FRACTION) + ) + stable_surface_trim_fraction = float( + kwargs.get("stable_surface_trim_fraction", DEFAULT_SPINE_STABLE_CONTACT_TRIM_FRACTION) + ) + stable_surface_min_area_fraction = float( + kwargs.get( + "stable_surface_min_area_fraction", + DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION, + ) + ) + stable_surface_max_depth = float( + kwargs.get("stable_surface_max_depth", DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM) + ) + stable_surface_min_shift = float( + kwargs.get("stable_surface_min_shift", DEFAULT_SPINE_STABLE_CONTACT_MIN_SHIFT_MM) + ) + stable_surface_close_gaps_3d = kwargs.get( + "stable_surface_close_gaps_3d", + DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D, + ) + stable_surface_close_gaps_max_gap = int( + kwargs.get( + "stable_surface_close_gaps_max_gap", + DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_MAX_GAP, + ) + ) + stable_surface_keep_largest_component = kwargs.get( + "stable_surface_keep_largest_component", + DEFAULT_SPINE_STABLE_CONTACT_KEEP_LARGEST_COMPONENT, + ) + stable_surface_enabled = stable_surface_fraction > 0.0 or stable_surface_trim_fraction > 0.0 top_node_set_id = kwargs.get("top_node_set_id", DEFAULT_SPINE_TOP_NODE_SET_ID) bottom_node_set_id = kwargs.get("bottom_node_set_id", DEFAULT_SPINE_BOTTOM_NODE_SET_ID) quality_control = kwargs.get("quality_control", True) @@ -1488,7 +1522,7 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, isolated_process = threshold(preprocessed_labels, process_label) # Marching Cubes and Registration - ogo.message(f"Starting ICP registration to reference...: {reference_path} ") + ogo.message(f"Starting ICP registration to vertebral-body reference...: {reference_path} ") icp = get_icp_with_scaling( registration_body, reference_path, @@ -1634,15 +1668,17 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, ogo.message( "generating fixed-thickness anatomy body-cap disks " f"(pmma_thick = {pmma_thick} mm total, pmma_intrusion = {pmma_intrusion} mm, " - f"stable_intrusion = {DEFAULT_SPINE_STABLE_CONTACT_INTRUSION_MM} mm, " - f"stable_close_gaps_3d = {DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D}, " - f"stable_contact = {DEFAULT_SPINE_STABLE_CONTACT_ENABLED})..." - ) - disk_intrusion = ( - DEFAULT_SPINE_STABLE_CONTACT_INTRUSION_MM - if DEFAULT_SPINE_STABLE_CONTACT_ENABLED - else pmma_intrusion + f"stable_surface_fraction = {stable_surface_fraction}, " + f"stable_surface_trim_fraction = {stable_surface_trim_fraction}, " + f"stable_surface_min_area_fraction = {stable_surface_min_area_fraction}, " + f"stable_surface_max_depth = {stable_surface_max_depth} mm, " + f"stable_surface_min_shift = {stable_surface_min_shift} mm, " + f"stable_close_gaps_3d = {stable_surface_close_gaps_3d}, " + f"stable_close_gaps_max_gap = {stable_surface_close_gaps_max_gap}, " + f"stable_keep_largest_component = {stable_surface_keep_largest_component}, " + f"stable_contact = {stable_surface_enabled})..." ) + disk_intrusion = pmma_intrusion transformed_body_bounds = foreground_voxel_center_bounds(padded_mask1) body_bounds = transformed_body_bounds inferior_plane = bbox_relative_contact_plane( @@ -1690,10 +1726,12 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, shape=plane["shape"], thickness=pmma_thick, intrusion=disk_intrusion, - stable_surface=DEFAULT_SPINE_STABLE_CONTACT_ENABLED, - stable_surface_fraction=DEFAULT_SPINE_STABLE_CONTACT_FRACTION, - stable_surface_min_area_fraction=DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION, - stable_surface_max_depth=DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM, + stable_surface=stable_surface_enabled, + stable_surface_fraction=stable_surface_fraction, + stable_surface_min_area_fraction=stable_surface_min_area_fraction, + stable_surface_max_depth=stable_surface_max_depth, + stable_surface_min_shift=stable_surface_min_shift, + stable_surface_trim_fraction=stable_surface_trim_fraction, ) for plane in (inferior_plane, superior_plane) ] @@ -1751,12 +1789,15 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, thickness=pmma_thick, intrusion=disk_intrusion, anatomy_constrained=True, - stable_surface=DEFAULT_SPINE_STABLE_CONTACT_ENABLED, - stable_surface_fraction=DEFAULT_SPINE_STABLE_CONTACT_FRACTION, - stable_surface_min_area_fraction=DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION, - stable_surface_max_depth=DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM, - close_gaps_3d=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D, - close_gaps_3d_max_gap=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_MAX_GAP, + stable_surface=stable_surface_enabled, + stable_surface_fraction=stable_surface_fraction, + stable_surface_min_area_fraction=stable_surface_min_area_fraction, + stable_surface_max_depth=stable_surface_max_depth, + stable_surface_min_shift=stable_surface_min_shift, + stable_surface_trim_fraction=stable_surface_trim_fraction, + close_gaps_3d=stable_surface_close_gaps_3d, + close_gaps_3d_max_gap=stable_surface_close_gaps_max_gap, + keep_largest_component=stable_surface_keep_largest_component, output_value=1, ) superior_disk = generate_projected_material_disk_vtk( @@ -1772,12 +1813,15 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, thickness=pmma_thick, intrusion=disk_intrusion, anatomy_constrained=True, - stable_surface=DEFAULT_SPINE_STABLE_CONTACT_ENABLED, - stable_surface_fraction=DEFAULT_SPINE_STABLE_CONTACT_FRACTION, - stable_surface_min_area_fraction=DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION, - stable_surface_max_depth=DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM, - close_gaps_3d=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D, - close_gaps_3d_max_gap=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_MAX_GAP, + stable_surface=stable_surface_enabled, + stable_surface_fraction=stable_surface_fraction, + stable_surface_min_area_fraction=stable_surface_min_area_fraction, + stable_surface_max_depth=stable_surface_max_depth, + stable_surface_min_shift=stable_surface_min_shift, + stable_surface_trim_fraction=stable_surface_trim_fraction, + close_gaps_3d=stable_surface_close_gaps_3d, + close_gaps_3d_max_gap=stable_surface_close_gaps_max_gap, + keep_largest_component=stable_surface_keep_largest_component, output_value=1, ) diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index 12d5ffe..04a957a 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -45,11 +45,19 @@ SPINE_DEFAULT_QC_ARGS = ["--quality_control", "False"] SPINE_DEFAULT_REGISTRATION_ARGS = [ "--registration_backend", - "vtk", + "numpy", "--registration_landmarks", - "250", + "8000", "--registration_iterations", - "75", + "50", +] +FEMUR_DEFAULT_REGISTRATION_ARGS = [ + "--registration_backend", + "numpy", + "--registration_landmarks", + "8000", + "--registration_iterations", + "50", ] @@ -137,8 +145,24 @@ def test_hip_defaults_to_both_sides(monkeypatch, tmp_path, solve_calls): GenerateFEM.main(["hip", "density.nii.gz", "hip_mask.nii.gz", "--output_path", "models"]) assert calls == [ - ["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "1", "--output_path", "models"], - ["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "2", "--output_path", "models"], + [ + "density.nii.gz", + "hip_mask.nii.gz", + "--femur_side", + "1", + "--output_path", + "models", + *FEMUR_DEFAULT_REGISTRATION_ARGS, + ], + [ + "density.nii.gz", + "hip_mask.nii.gz", + "--femur_side", + "2", + "--output_path", + "models", + *FEMUR_DEFAULT_REGISTRATION_ARGS, + ], ] assert solve_calls == [ (Path("models/density_LF.n88model"), "hip", calls[0]), @@ -178,7 +202,7 @@ def test_hip_can_run_one_side(monkeypatch, solve_calls): GenerateFEM.main(["hip", "density.nii.gz", "hip_mask.nii.gz", "--side", "right"]) - assert calls == [["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "2"]] + assert calls == [["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "2", *FEMUR_DEFAULT_REGISTRATION_ARGS]] assert solve_calls == [(Path("density_RF.n88model"), "hip", calls[0])] @@ -427,6 +451,33 @@ def test_hip_forwards_pistoia_mask_to_builder(monkeypatch, tmp_path, solve_calls assert calls[0][calls[0].index("--pistoia_mask") + 1] == "femoral_neck.nii.gz" +def test_hip_forwards_registration_options(monkeypatch, solve_calls): + calls = [] + monkeypatch.setattr(GenerateFEM, "run_femur_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "hip", + "density.nii.gz", + "left_femur.nii.gz", + "--side", + "left", + "--registration_backend", + "vtk", + "--registration_landmarks", + "250", + "--registration_iterations", + "25", + "--no-solve", + ] + ) + + assert "--registration_backend" in calls[0] + assert calls[0][calls[0].index("--registration_backend") + 1] == "vtk" + assert calls[0][calls[0].index("--registration_landmarks") + 1] == "250" + assert calls[0][calls[0].index("--registration_iterations") + 1] == "25" + + def test_hip_pistoia_mask_label_uses_bone_mask_when_mask_omitted(monkeypatch, tmp_path, solve_calls): monkeypatch.chdir(tmp_path) calls = [] @@ -736,9 +787,9 @@ def test_spine_modeling_metadata_records_materials_and_bcs(tmp_path): data = json.loads(path.read_text()) assert data["target"] == {"vertebra": "L1", "body_label": 2, "process_label": 3} - assert data["alignment"]["registration_backend"] == "vtk" - assert data["alignment"]["registration_landmarks"] == 250 - assert data["alignment"]["registration_iterations"] == 75 + assert data["alignment"]["registration_backend"] == "numpy" + assert data["alignment"]["registration_landmarks"] == 8000 + assert data["alignment"]["registration_iterations"] == 50 assert data["geometry"]["model_coordinates"] == "preprocessed_image_physical_space" assert data["materials"]["trabecular"]["elastic_E_func"] == "kopperdahl_trab_E" assert data["materials"]["cortical"]["material_id_range"] == [129, 256] @@ -778,6 +829,9 @@ def test_femur_modeling_metadata_records_materials_shaft_and_bcs(tmp_path): data = json.loads(path.read_text()) assert data["target"]["side"] == "left" + assert data["alignment"]["registration_backend"] == "numpy" + assert data["alignment"]["registration_landmarks"] == 8000 + assert data["alignment"]["registration_iterations"] == 50 assert data["geometry"]["model_coordinates"] == "preprocessed_image_physical_space" assert data["segmentation"]["compartment_labels"] == {"cortical": 1, "trabecular": 2} assert data["shaft_standardization"]["lesser_trochanter_distal_offset_mm"] == 50.0 diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index 856d771..b6f5549 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -68,6 +68,9 @@ 0.8, 0.75 ], + "registration_backend": "numpy", + "registration_iterations": 50, + "registration_landmarks": 8000, "right_femur_code": 2, "rough_pre_icp_length_mm": 120.0, "shaft_length_mm": 120.0, @@ -144,11 +147,19 @@ "pmma_material_id": 5000, "pmma_thickness_mm": 10, "reference_filename": "L4_BODY_SPINE_COMPRESSION_REF.vtk", - "registration_backend": "vtk", - "registration_iterations": 75, - "registration_landmarks": 250, + "registration_backend": "numpy", + "registration_iterations": 50, + "registration_landmarks": 8000, "registration_max_scale": "1.2,1.2,1.3", "registration_min_scale": "0.8,0.8,0.75", + "stable_surface_close_gaps_3d": true, + "stable_surface_close_gaps_max_gap": 2, + "stable_surface_fraction": 0.0, + "stable_surface_keep_largest_component": true, + "stable_surface_max_depth_mm": 24.0, + "stable_surface_min_area_fraction": 0.12, + "stable_surface_min_shift_mm": 3.0, + "stable_surface_trim_fraction": 0.1, "superior_contact_center_fraction": [ 0.5, 0.5, diff --git a/tests/fea/test_boundary.py b/tests/fea/test_boundary.py index 4ae2452..8195635 100644 --- a/tests/fea/test_boundary.py +++ b/tests/fea/test_boundary.py @@ -512,6 +512,36 @@ def test_projected_material_disk_closes_short_3d_gaps_after_extrusion(): assert not np.any(disk & active) +def test_projected_material_disk_can_keep_largest_connected_component(): + ndimage = pytest.importorskip("scipy.ndimage") + + active = np.zeros((18, 14, 18), dtype=bool) + active[4:10, 5:9, 4:10] = True + active[13:15, 5:9, 13:15] = True + + disk = generate_projected_material_disk_mask( + active, + spacing=(1, 1, 1), + origin=(0, 0, 0), + center=(8, 0, 8), + normal=(0, 1, 0), + u_axis=(1, 0, 0), + v_axis=(0, 0, 1), + size=(16, 16), + shape="square", + thickness=6, + intrusion=2, + anatomy_constrained=True, + keep_largest_component=True, + ) + + labels, count = ndimage.label(disk, structure=np.ones((3, 3, 3), dtype=bool)) + assert count == 1 + assert np.any(disk[4:10]) + assert not np.any(disk[13:15]) + assert not np.any(disk & active) + + def test_projected_stable_surface_contact_ignores_small_osteophyte(): active = np.zeros((14, 14, 14), dtype=bool) active[4:10, 5:9, 4:10] = True @@ -537,6 +567,126 @@ def test_projected_stable_surface_contact_ignores_small_osteophyte(): assert contact["stable_depth"] == pytest.approx(3.0) assert contact["first_area"] == 1 assert contact["stable_area"] >= 30 + assert contact["used_stable_surface"] is True + + +def test_projected_stable_surface_contact_ignores_shallow_shift(): + active = np.zeros((14, 14, 14), dtype=bool) + active[4:10, 5:9, 4:10] = True + active[7, 4, 7] = True + + contact = projected_stable_surface_contact( + active, + spacing=(1, 1, 1), + origin=(0, 0, 0), + center=(7, 0, 7), + normal=(0, 1, 0), + u_axis=(1, 0, 0), + v_axis=(0, 0, 1), + size=(10, 10), + shape="square", + stable_surface_fraction=0.55, + stable_surface_min_area_fraction=0.12, + stable_surface_max_depth=8, + stable_surface_min_shift=2, + ) + + assert contact["first_distance"] == pytest.approx(4.0) + assert contact["stable_distance"] == pytest.approx(4.0) + assert contact["stable_depth"] == pytest.approx(0.0) + assert contact["used_stable_surface"] is False + + +def test_projected_stable_surface_contact_can_trim_superficial_area(): + active = np.zeros((14, 14, 14), dtype=bool) + active[4:10, 5:9, 4:10] = True + active[7, 2:5, 7] = True + + contact = projected_stable_surface_contact( + active, + spacing=(1, 1, 1), + origin=(0, 0, 0), + center=(7, 0, 7), + normal=(0, 1, 0), + u_axis=(1, 0, 0), + v_axis=(0, 0, 1), + size=(10, 10), + shape="square", + stable_surface_trim_fraction=0.10, + stable_surface_max_depth=8, + stable_surface_min_shift=2, + ) + + assert contact["first_distance"] == pytest.approx(2.0) + assert contact["stable_distance"] == pytest.approx(5.0) + assert contact["stable_depth"] == pytest.approx(3.0) + assert contact["used_stable_surface"] is True + + +def test_projected_material_disk_trimmed_stable_surface_excludes_protrusion_column(): + active = np.zeros((14, 14, 14), dtype=bool) + active[4:10, 5:9, 4:10] = True + active[7, 2:5, 7] = True + + disk = generate_projected_material_disk_mask( + active, + spacing=(1, 1, 1), + origin=(0, 0, 0), + center=(7, 0, 7), + normal=(0, 1, 0), + u_axis=(1, 0, 0), + v_axis=(0, 0, 1), + size=(10, 10), + shape="square", + thickness=6, + intrusion=2, + anatomy_constrained=True, + stable_surface=True, + stable_surface_fraction=0, + stable_surface_trim_fraction=0.10, + stable_surface_max_depth=8, + stable_surface_min_shift=2, + close_gaps_3d=True, + ) + + assert not np.any(disk & active) + assert not np.any(disk[7, 2:5, 7]) + assert np.any(disk[7, :, 7]) + assert np.any(disk[5, :, 5]) + + + +def test_projected_material_disk_trim_keeps_bumps_below_min_shift(): + active = np.zeros((16, 16, 16), dtype=bool) + active[4:12, 7:11, 4:12] = True + active[5, 2:7, 5] = True # true protrusion, 5 mm before stable surface + active[8, 5:7, 8] = True # shallow bump, 2 mm before stable surface + + disk = generate_projected_material_disk_mask( + active, + spacing=(1, 1, 1), + origin=(0, 0, 0), + center=(8, 0, 8), + normal=(0, 1, 0), + u_axis=(1, 0, 0), + v_axis=(0, 0, 1), + size=(12, 12), + shape="square", + thickness=8, + intrusion=3, + anatomy_constrained=True, + stable_surface=True, + stable_surface_fraction=0, + stable_surface_trim_fraction=0.05, + stable_surface_max_depth=10, + stable_surface_min_shift=4, + close_gaps_3d=True, + close_gaps_3d_max_gap=4, + ) + + assert not np.any(disk & active) + assert not np.any(disk[5, 2:7, 5]) + assert np.any(disk[8, :, 8]) def test_projected_material_disk_vtk_preserves_geometry_and_output_value(): diff --git a/tests/fea/test_workflow_locked_defaults.py b/tests/fea/test_workflow_locked_defaults.py index 92fa774..8069f77 100644 --- a/tests/fea/test_workflow_locked_defaults.py +++ b/tests/fea/test_workflow_locked_defaults.py @@ -34,6 +34,14 @@ def _locked_workflow_defaults(): "registration_min_scale": spine.DEFAULT_SPINE_REGISTRATION_MIN_SCALE, "registration_max_scale": spine.DEFAULT_SPINE_REGISTRATION_MAX_SCALE, "reference_filename": spine.DEFAULT_SPINE_REFERENCE_FILENAME, + "stable_surface_fraction": spine.DEFAULT_SPINE_STABLE_CONTACT_FRACTION, + "stable_surface_trim_fraction": spine.DEFAULT_SPINE_STABLE_CONTACT_TRIM_FRACTION, + "stable_surface_min_area_fraction": spine.DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION, + "stable_surface_max_depth_mm": spine.DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM, + "stable_surface_min_shift_mm": spine.DEFAULT_SPINE_STABLE_CONTACT_MIN_SHIFT_MM, + "stable_surface_close_gaps_3d": spine.DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D, + "stable_surface_close_gaps_max_gap": spine.DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_MAX_GAP, + "stable_surface_keep_largest_component": spine.DEFAULT_SPINE_STABLE_CONTACT_KEEP_LARGEST_COMPONENT, "contact_size_fraction": list(spine.SPINE_CONTACT_SIZE_FRACTION), "superior_contact_center_fraction": list( spine.SPINE_SUPERIOR_CONTACT_CENTER_FRACTION @@ -76,6 +84,9 @@ def _locked_workflow_defaults(): "proximal_reference_distance_mm": femur.DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, "reference_min_scale": list(femur.DEFAULT_FEMUR_REFERENCE_MIN_SCALE), "reference_max_scale": list(femur.DEFAULT_FEMUR_REFERENCE_MAX_SCALE), + "registration_backend": femur.DEFAULT_FEMUR_REGISTRATION_BACKEND, + "registration_landmarks": femur.DEFAULT_FEMUR_REGISTRATION_LANDMARKS, + "registration_iterations": femur.DEFAULT_FEMUR_REGISTRATION_ITERATIONS, "pmma_thickness_mm": femur.DEFAULT_PMMA_THICKNESS_MM, "pmma_intrusion_mm": femur.DEFAULT_PMMA_INTRUSION_MM, "input_margin_mm": femur.DEFAULT_FEMUR_INPUT_MARGIN_MM, From e2b7d948700bd637968bb45fd61306b5f826e0a1 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Thu, 8 Oct 2026 11:18:55 -0600 Subject: [PATCH 50/67] feat: consolidate FEA validation, model QC, and short-shaft defaults --- ogo/cli/CheckFEModelBC.py | 6 +- ogo/cli/GenerateFEM.py | 180 +- ogo/cli/ValidateFEA.py | 121 + ogo/cli/Visualize.py | 24 +- ...L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk | 29474 ++++++++++++++++ ogo/fea/README.md | 336 +- ogo/fea/alignment.py | 16 +- ogo/fea/femur.py | 710 +- ogo/fea/gallery.py | 136 + ogo/fea/gallery_review.js | 173 + ogo/fea/hip_registration.py | 56 + ogo/fea/legacy_qc.py | 185 + ogo/fea/model_export.py | 163 + ogo/fea/qc_render.py | 370 + ogo/fea/review.py | 57 + ogo/fea/shaft_geometry.py | 164 + ogo/fea/spine.py | 68 +- ogo/fea/validation.py | 323 + ogo/util/faim.py | 13 + setup.cfg | 4 + tests/cli/test_GenerateFEM.py | 108 +- tests/cli/test_gt_shaft_length_audit.py | 21 + .../fixtures/fe_workflow_defaults_locked.json | 6 +- tests/fea/test_femur.py | 74 +- tests/fea/test_gt_disk_crop.py | 117 + tests/fea/test_hip_registration.py | 58 + tests/fea/test_legacy_evidence.py | 97 + tests/fea/test_model_export.py | 73 + tests/fea/test_qc_render.py | 94 + tests/fea/test_review.py | 173 + tests/fea/test_shaft_geometry.py | 118 + tests/fea/test_sideways_fall.py | 2 +- tests/fea/test_spine.py | 6 + tests/fea/test_spinefe_benchmark.py | 3 +- tests/fea/test_validation.py | 92 + tests/fea/test_workflow_locked_defaults.py | 4 + 36 files changed, 32924 insertions(+), 701 deletions(-) create mode 100644 ogo/cli/ValidateFEA.py create mode 100644 ogo/dat/L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk create mode 100644 ogo/fea/gallery.py create mode 100644 ogo/fea/gallery_review.js create mode 100644 ogo/fea/hip_registration.py create mode 100644 ogo/fea/legacy_qc.py create mode 100644 ogo/fea/model_export.py create mode 100644 ogo/fea/qc_render.py create mode 100644 ogo/fea/review.py create mode 100644 ogo/fea/shaft_geometry.py create mode 100644 ogo/fea/validation.py create mode 100644 tests/cli/test_gt_shaft_length_audit.py create mode 100644 tests/fea/test_gt_disk_crop.py create mode 100644 tests/fea/test_hip_registration.py create mode 100644 tests/fea/test_legacy_evidence.py create mode 100644 tests/fea/test_model_export.py create mode 100644 tests/fea/test_qc_render.py create mode 100644 tests/fea/test_review.py create mode 100644 tests/fea/test_shaft_geometry.py create mode 100644 tests/fea/test_validation.py diff --git a/ogo/cli/CheckFEModelBC.py b/ogo/cli/CheckFEModelBC.py index d5bb196..4ffcda2 100644 --- a/ogo/cli/CheckFEModelBC.py +++ b/ogo/cli/CheckFEModelBC.py @@ -145,15 +145,15 @@ def audit_femur_sideways(node_sets, constraints, tolerance): }) if gt and distal: - gt_distal_z = gt["z_percentiles"]["p05"] + gt_distal_z = gt["bounds"]["z_min"] distal_z = distal["z_percentiles"]["p50"] shaft_length = gt_distal_z - distal_z checks.append({ "name": "post-GT-support shaft length is positive", "passed": shaft_length > max(float(tolerance), 1.0), "detail": ( - "gt_z_p05_minus_distal_z_median=" - f"{shaft_length:.6g} mm; gt_z_p05={gt_distal_z:.6g}; " + "gt_z_min_minus_distal_z_median=" + f"{shaft_length:.6g} mm; gt_z_min={gt_distal_z:.6g}; " f"distal_z_median={distal_z:.6g}" ), }) diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 14e9e69..49b5579 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -25,25 +25,24 @@ DEFAULT_SPINE_REGISTRATION_BACKEND, DEFAULT_SPINE_REGISTRATION_ITERATIONS, DEFAULT_SPINE_REGISTRATION_LANDMARKS, + DEFAULT_SPINE_ICP_TARGET, SPINE_ALIGNMENT_METHOD, + SPINE_ICP_TARGETS, default_spine_reference_path, solve_report_profile as spine_solve_report_profile, ) from ogo.fea.femur import ( - DEFAULT_FEMUR_BBOX_CROP_FROM, - DEFAULT_FEMUR_BBOX_RATIO, DEFAULT_FEMUR_CUT_MODE, DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, DEFAULT_FEMUR_ISO_RESOLUTION_MM, DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, - DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, + DEFAULT_FEMUR_REGISTRATION_BACKEND, + DEFAULT_FEMUR_REGISTRATION_ITERATIONS, + DEFAULT_FEMUR_REGISTRATION_LANDMARKS, DEFAULT_PMMA_INTRUSION_MM, DEFAULT_PMMA_THICKNESS_MM, - DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, - DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, @@ -133,6 +132,8 @@ def spine_registration_args(args: argparse.Namespace) -> List[str]: str(args.registration_landmarks), "--registration_iterations", str(args.registration_iterations), + "--spine_icp_target", + str(args.spine_icp_target), ] @@ -486,6 +487,16 @@ def write_modeling_metadata( body_label = option_int(generator_argv, "--mask_threshold", 0) process_label = option_int(generator_argv, "--process_mask_threshold", 0) appendix = option_value(generator_argv, "--appendix") + spine_icp_target = option_value( + generator_argv, + "--spine_icp_target", + DEFAULT_SPINE_ICP_TARGET, + ) + spine_reference_path = option_value( + generator_argv, + "--reference_path", + str(default_spine_reference_path(spine_icp_target)), + ) if args.use_absolute_fe_displacement: spine_displacement = absolute_displacement_metadata( generator_argv, @@ -508,11 +519,8 @@ def write_modeling_metadata( }, "alignment": { "method": SPINE_ALIGNMENT_METHOD, - "reference_path": option_value( - generator_argv, - "--reference_path", - str(default_spine_reference_path()), - ), + "registration_target": spine_icp_target, + "reference_path": spine_reference_path, "registration_scale": option_value(generator_argv, "--registration_scale", "auto"), "registration_min_scale": option_value( generator_argv, @@ -676,14 +684,6 @@ def write_modeling_metadata( femur_side = option_value(generator_argv, "--femur_side", "1") side = "left" if femur_side == "1" else "right" if femur_side == "2" else "unknown" compartment_mask = option_path(generator_argv, "--compartment_mask") - femur_cut_mode = option_value(generator_argv, "--femur_cut_mode", DEFAULT_FEMUR_CUT_MODE) - femur_bbox_ratio = option_n_values(generator_argv, "--femur_bbox_ratio", 3, DEFAULT_FEMUR_BBOX_RATIO) - femur_bbox_crop_from = option_n_values( - generator_argv, - "--femur_bbox_crop_from", - 3, - DEFAULT_FEMUR_BBOX_CROP_FROM, - ) if args.use_absolute_fe_displacement: femur_displacement = absolute_displacement_metadata( generator_argv, @@ -707,6 +707,9 @@ def write_modeling_metadata( "alignment": { "method": "ICP to side-specific femur reference from cropped/padded input geometry", "reference_path": option_value(generator_argv, "--reference_path", "bundled side-specific femur reference"), + "registration_backend": DEFAULT_FEMUR_REGISTRATION_BACKEND, + "registration_landmarks": DEFAULT_FEMUR_REGISTRATION_LANDMARKS, + "registration_iterations": DEFAULT_FEMUR_REGISTRATION_ITERATIONS, }, "image_processing": { "iso_resolution_mm": option_float(generator_argv, "--iso_resolution", DEFAULT_FEMUR_ISO_RESOLUTION_MM), @@ -751,23 +754,14 @@ def write_modeling_metadata( }, }, "shaft_standardization": { - "cut_mode": femur_cut_mode, - "crop_stage": ( - "after isotropic resampling and before ICP" - if femur_cut_mode in {"bbox_ratio", "proximal_box_ratio"} - else "fixed rough crop before ICP, final oblique ratio crop after ICP" - if femur_cut_mode == "post_icp_oblique_ratio" - else "fixed rough crop before ICP, final flat ratio crop after ICP" - if femur_cut_mode == "post_icp_flat_ratio" - else "fixed rough crop before ICP for registration, final GT-disk-relative flat crop after ICP on the full scan" - if femur_cut_mode == "greater_trochanter_length" - else "after ICP on the generated model grid" - ), - "fixed_length_mm": option_float(generator_argv, "--femur_shaft_length", 120.0), + "cut_mode": DEFAULT_FEMUR_CUT_MODE, + "crop_stage": "fixed rough crop before ICP for registration, final GT-disk-relative flat crop after ICP on the full scan", + "coverage_definition": "GT disk distal voxel face minus safe flat face clearing the full transformed native scan end", + "geometry_sidecar": str(model_path.with_name(model_path.stem + "_shaft_geometry.json")), "rough_pre_icp_crop": { - "enabled": femur_cut_mode in {"post_icp_flat_ratio", "post_icp_oblique_ratio", "greater_trochanter_length"}, + "enabled": True, "retained_length_mm": option_float(generator_argv, "--femur_shaft_length", 120.0), - "registration_only": femur_cut_mode == "greater_trochanter_length", + "registration_only": True, }, "greater_trochanter_length": { "retained_length_mm": option_float( @@ -782,37 +776,7 @@ def write_modeling_metadata( ), "length_origin": "detected greater-trochanter disk distal edge", }, - "lesser_trochanter_distal_offset_mm": option_float( - generator_argv, - "--femur_lesser_trochanter_distal_offset", - DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, - ), - "bbox_ratio": femur_bbox_ratio, - "bbox_crop_from": femur_bbox_crop_from, - "proximal_box_ratio": { - "ratio": option_float(generator_argv, "--femur_experimental_crop_ratio", 1.2), - "proximal_reference_distance_mm": option_float( - generator_argv, - "--femur_proximal_reference_distance", - DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, - ), - "reference_width": option_value( - generator_argv, - "--femur_proximal_reference_width", - "max_xy", - ), - }, - "cut_plane": ( - "post-resample pre-ICP crop face transformed with the femur" - if femur_cut_mode in {"bbox_ratio", "proximal_box_ratio"} - else "post-ICP oblique crop face following transformed rough pre-ICP crop face angle" - if femur_cut_mode == "post_icp_oblique_ratio" - else "flat post-ICP aligned-frame crop face" - if femur_cut_mode == "post_icp_flat_ratio" - else "flat post-ICP aligned-frame crop face at fixed shaft length below detected GT-disk distal edge" - if femur_cut_mode == "greater_trochanter_length" - else "flat model-grid z plane" - ), + "cut_plane": "flat post-ICP aligned-frame crop face at fixed shaft length below detected GT-disk distal edge", "incomplete_fov_behavior": "fail model generation", }, "materials": { @@ -885,48 +849,10 @@ def write_modeling_metadata( }, "distal_shaft": { "node_set": "Distal_Femur_Nodes", - "support_surface": ( - "finite bbox-relative patch projected onto the transformed oblique shaft surface" - if femur_cut_mode == "bbox_ratio" - else "projected patch on the post-ICP oblique distal shaft crop face" - if femur_cut_mode == "post_icp_oblique_ratio" - else "central 90% straight patch on the post-ICP flat distal shaft crop face" - if femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"} - else "flat distal shaft cut face" - ), - "relative_to": ( - "transformed_rough_crop_face" - if femur_cut_mode == "post_icp_oblique_ratio" - else "model_grid" - if femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"} - else "model_bbox" - if femur_cut_mode == "bbox_ratio" - else None - ), - "center_fraction": ( - list(DISTAL_SHAFT_FIXTURE_CENTER_FRACTION) - if femur_cut_mode == "bbox_ratio" - else None - ), - "size_fraction": ( - list(DISTAL_SHAFT_FIXTURE_SIZE_FRACTION) - if femur_cut_mode == "bbox_ratio" - else None - ), - "support_fraction": ( - POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION - if femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"} - else None - ), - "normal_source": ( - "transformed input bbox-ratio crop face" - if femur_cut_mode == "bbox_ratio" - else "transformed rough pre-ICP crop face shifted to final ratio target" - if femur_cut_mode == "post_icp_oblique_ratio" - else "model-grid distal z direction" - if femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"} - else "model-grid distal z face" - ), + "support_surface": "central 90% straight patch on the post-ICP flat distal shaft crop face", + "relative_to": "model_grid", + "support_fraction": POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, + "normal_source": "model-grid distal z direction", }, }, "constraints": [ @@ -1041,7 +967,7 @@ def _add_common_image_args(parser: argparse.ArgumentParser) -> None: action="store_true", help="Only generate the .n88model; skip FAIM solve and postprocessing.", ) - parser.add_argument("--threads", type=int, default=4, help="FAIM solver thread count.") + parser.add_argument("--threads", type=int, default=4, help="Per-job VTK/ITK generation and FAIM solver thread limit.") parser.add_argument("--faim_env", default=None, help="Optional conda environment for FAIM/N88 tools.") parser.add_argument("--conda_executable", default="conda", help="Conda executable for --faim_env.") parser.add_argument("--faim_install_root", default=None, help="Optional FAIM install root.") @@ -1176,6 +1102,15 @@ def build_parser() -> argparse.ArgumentParser: "explicit lower-level options." ), ) + spine.add_argument( + "--reference_path", + type=Path, + default=None, + help=( + "Optional spine ICP reference surface. If omitted, the bundled " + "reference matching --spine_icp_target is used." + ), + ) spine.add_argument( "--registration_backend", choices=("vtk", "numpy"), @@ -1202,6 +1137,17 @@ def build_parser() -> argparse.ArgumentParser: default=DEFAULT_SPINE_REGISTRATION_ITERATIONS, help="Maximum spine ICP iterations. (default: %(default)s)", ) + spine.add_argument( + "--spine_icp_target", + choices=SPINE_ICP_TARGETS, + default=DEFAULT_SPINE_ICP_TARGET, + help=( + "Segmentation surface used for spine ICP. body uses the vertebral body " + "label only; vertebra uses body plus posterior-process labels and " + "therefore requires a matching full-vertebra reference surface. " + "(default: %(default)s)" + ), + ) hip = subparsers.add_parser( "hip", @@ -1219,6 +1165,19 @@ def build_parser() -> argparse.ArgumentParser: return parser +def limit_generation_threads(threads: int) -> None: + """Keep VTK/ITK model generation within the requested per-job CPU budget.""" + if threads < 1: + raise ValueError("threads must be positive.") + import SimpleITK as sitk + import vtk + + vtk.vtkMultiThreader.SetGlobalMaximumNumberOfThreads(threads) + vtk.vtkMultiThreader.SetGlobalDefaultNumberOfThreads(threads) + vtk.vtkSMPTools.Initialize(threads) + sitk.ProcessObject.SetGlobalDefaultNumberOfThreads(threads) + + def main(argv: Optional[Sequence[str]] = None) -> None: argv = list(sys.argv[1:] if argv is None else argv) @@ -1229,6 +1188,7 @@ def main(argv: Optional[Sequence[str]] = None) -> None: parser.error("model type is required: choose spine or hip") if not args.dry_run: + limit_generation_threads(args.threads) ensure_output_directory(args.output_path) pistoia_mask_source = pistoia_mask_source_path(args) @@ -1239,9 +1199,13 @@ def main(argv: Optional[Sequence[str]] = None) -> None: pistoia_mask_args.extend(["--pistoia_mask_label", str(label)]) if args.model_type == "spine": + reference_args = [] + if args.reference_path is not None: + reference_args.extend(["--reference_path", str(args.reference_path)]) spine_extra_args = ( spine_preset_args(args.preset) + spine_registration_args(args) + + reference_args + pistoia_mask_args + list(extra_args) + [ diff --git a/ogo/cli/ValidateFEA.py b/ogo/cli/ValidateFEA.py new file mode 100644 index 0000000..67c6c2c --- /dev/null +++ b/ogo/cli/ValidateFEA.py @@ -0,0 +1,121 @@ +"""Audit FEA measurements, backfill legacy models, and optionally create a gallery.""" + +import argparse +import csv +from concurrent.futures import ProcessPoolExecutor +from pathlib import Path + +from ogo.fea.validation import evaluate, generate_gallery +from ogo.fea.validation import write_measurements +from ogo.fea.gallery import export_gallery_zip +from ogo.fea.legacy_qc import backfill_measurements, evidence_signature, load_evidence, needs_backfill +from ogo.fea.review import identity, load_decisions, write_inclusion + + +def _backfill_task(task): + model, site, evidence = task + try: + return backfill_measurements(model, site=site, evidence=evidence), '' + except Exception as exc: + metrics = write_measurements(model, { + 'site': site or 'unknown', 'measurement_source': 'legacy_backfill_failed', + 'measurement_error': str(exc), 'generation_status': 'measurement_failed', + 'legacy_evidence_signature': evidence_signature(model, evidence)}) + return metrics, str(exc) + + +def main(argv=None): + parser = argparse.ArgumentParser(prog='ogoValidateFEA', description=__doc__) + parser.add_argument('input', type=Path, help='Model directory, .n88model, or *_qc_metrics.csv') + parser.add_argument('--output', type=Path, default=Path('qc'), help='Audit output directory') + parser.add_argument('--gallery', action='store_true', help='Also generate gallery.html') + parser.add_argument('--gallery-zip', action='store_true', help='Export portable gallery.zip with HTML, images, and QC/inclusion CSVs') + parser.add_argument('--site', choices=('hip', 'spine'), help='Anatomy for legacy models when it cannot be inferred') + parser.add_argument('--reviews', type=Path, help='Import a previously exported study_inclusion.csv') + parser.add_argument('--evidence', type=Path, help='CSV of registered body/process masks and shaft sidecars by site/model_id') + parser.add_argument('--workers', type=int, default=1, help='Parallel legacy backfill processes (default: 1)') + args = parser.parse_args(argv) + if not 1 <= args.workers <= 32: + parser.error('--workers must be between 1 and 32') + try: + evidence = load_evidence(args.evidence) if args.evidence else {} + except (OSError, ValueError) as exc: + parser.error(str(exc)) + if not args.input.exists(): + parser.error(f'Input does not exist: {args.input}') + if args.input.is_dir(): + files = list(args.input.rglob('*_qc_metrics.csv')) + models = sorted(args.input.rglob('*.n88model')) + elif args.input.suffix == '.n88model': + files, models = [], [args.input] + elif args.input.name.endswith('_qc_metrics.csv'): + files, models = [args.input], [] + else: + parser.error('Input must be a directory, .n88model, or *_qc_metrics.csv') + tasks = [] + for model in models: + metrics = model.with_name(model.stem + '_qc_metrics.csv') + row = {} + if metrics.exists(): + with metrics.open(newline='') as stream: + row = next(csv.DictReader(stream), {}) + site = args.site or row.get('site') + matches = [key for key in evidence if key[1] == model.stem and (not site or key[0] == site)] + if len(matches) > 1: + parser.error(f'Ambiguous evidence identity for {model}; specify --site') + sources = evidence[matches[0]] if matches else None + if matches: + site = matches[0][0] + anatomy_changed = row.get('measurement_source', '').startswith('legacy_backfill') and site and row.get('site') != site + if anatomy_changed or needs_backfill(model, row, sources): + print(f'Backfilling {model}') + tasks.append((model, site, sources)) + if metrics not in files: + files.append(metrics) + if args.workers == 1: + results = map(_backfill_task, tasks) + for metrics, error in results: + if error: + print(f'Review required: {metrics}: {error}') + elif tasks: + with ProcessPoolExecutor(max_workers=args.workers) as pool: + for metrics, error in pool.map(_backfill_task, tasks): + if error: + print(f'Review required: {metrics}: {error}') + files.sort() + if not files: + parser.error('No QC CSVs or n88models found') + rows = [] + for path in files: + with path.open(newline='') as stream: + records = list(csv.DictReader(stream)) + if len(records) != 1 or not {'model_id', 'site'} <= records[0].keys(): + parser.error(f'Invalid measurement CSV: {path}') + rows.append(evaluate(records[0])) + keys = [identity(row) for row in rows] + if len(set(keys)) != len(keys): + parser.error('Duplicate site/model_id identities. Audit each cohort or length run separately.') + try: + decisions = load_decisions(args.reviews) if args.reviews else {} + except ValueError as exc: + parser.error(str(exc)) + unknown = set(decisions) - set(keys) + if unknown: + parser.error(f'Review CSV contains {len(unknown)} identities not present in this audit') + args.output.mkdir(parents=True, exist_ok=True) + fields = sorted({key for row in rows for key in row}) + with (args.output / 'qc_summary.csv').open('w', newline='') as stream: + writer = csv.DictWriter(stream, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + write_inclusion(rows, args.output / 'study_inclusion.csv', decisions) + if args.gallery: + generate_gallery(rows, args.output / 'gallery.html', decisions=decisions) + if args.gallery_zip: + export_gallery_zip(rows, args.output / 'gallery.zip', decisions) + for status in ('pass', 'review', 'fail'): + print(f'{status}: {sum(row["qc_status"] == status for row in rows)}') + + +if __name__ == '__main__': + main() diff --git a/ogo/cli/Visualize.py b/ogo/cli/Visualize.py index ff8d7c6..d8859e1 100644 --- a/ogo/cli/Visualize.py +++ b/ogo/cli/Visualize.py @@ -107,7 +107,9 @@ def get_lookup_table(): lut.SetTableValue(idx, 0, 0, 0, 0) return lut -def vis3d(input_filename, select, collection, gaussian, radius, isosurface, elevation, azimuth, flip, outfile, offscreen, pad, overwrite, func): +def vis3d(input_filename, select, collection, gaussian, radius, isosurface, elevation, azimuth, flip, outfile, offscreen, pad, overwrite, func, *, renderer_setup=None, label_palette=None): + """Render labels; optional per-call palette and scene setup support FE QC.""" + label_definitions = lb.labels_dict if label_palette is None else label_palette # Check if output exists and should overwrite if outfile: @@ -153,14 +155,14 @@ def vis3d(input_filename, select, collection, gaussian, radius, isosurface, elev labels = getLabels(reader.GetOutput()) for idx,label in enumerate(labels): - rgb = lb.labels_dict[label]['RGB'] - desc = lb.labels_dict[label]['LABEL'] + rgb = label_definitions[label]['RGB'] + desc = label_definitions[label]['LABEL'] ogo.message('{:5d}: ({:3d},{:3d},{:3d}) – {}'.format(label,rgb[0],rgb[1],rgb[2],desc)) # Specify a selection of labels (helpful for TotalSegmentator results) if collection: if collection == 'all': - valid_list = [v['LABEL'] for k, v in lb.labels_dict.items()] + valid_list = [v['LABEL'] for k, v in label_definitions.items()] elif collection == 'ossai': valid_list = ['Femur Right', 'Femur Left', 'Pelvis Right', 'Pelvis Left', 'Sacrum', 'L6', 'L5', 'L4', 'L3', 'L2', 'L1'] elif collection == 'skeleton': @@ -186,7 +188,7 @@ def vis3d(input_filename, select, collection, gaussian, radius, isosurface, elev if label in labels: tmp.append(label) else: - ogo.message('[WARNING]: Selected label {} [{}] not in input image.'.format(lb.labels_dict[label]['LABEL'],label)) + ogo.message('[WARNING]: Selected label {} [{}] not in input image.'.format(label_definitions[label]['LABEL'],label)) labels = tmp # Create lists for VTK classes @@ -213,7 +215,7 @@ def vis3d(input_filename, select, collection, gaussian, radius, isosurface, elev for idx,label in enumerate(labels): - ogo.message('Processing label {} ({})'.format(label,lb.labels_dict[label]['LABEL'])) + ogo.message('Processing label {} ({})'.format(label,label_definitions[label]['LABEL'])) thres.append(vtk.vtkImageThreshold()) extract.append(vtk.vtkExtractVOI()) padding.append(vtk.vtkImageConstantPad()) @@ -221,7 +223,7 @@ def vis3d(input_filename, select, collection, gaussian, radius, isosurface, elev mcube.append(vtk.vtkImageMarchingCubes()) mapper.append(vtk.vtkDataSetMapper()) actor.append(vtk.vtkActor()) - rgb = lb.labels_dict[label]['RGB'] + rgb = label_definitions[label]['RGB'] thres[idx].SetInputConnection(reader.GetOutputPort()) thres[idx].SetInValue(127) @@ -279,7 +281,9 @@ def vis3d(input_filename, select, collection, gaussian, radius, isosurface, elev (ibounds[5]-ibounds[4])) # Focus of camera renderer.GetActiveCamera().Elevation(elevation) renderer.GetActiveCamera().Azimuth(azimuth) - renderer.ResetCamera() + renderer.ResetCamera() + if renderer_setup is not None: + renderer_setup(renderer) # Render the scene renderWindow.Render() @@ -297,7 +301,9 @@ def vis3d(input_filename, select, collection, gaussian, radius, isosurface, elev writer.SetInputConnection(windowToImage.GetOutputPort()) writer.Write() - ogo.message('Done.') + ogo.message('Done.') + if offscreen: + renderWindow.Finalize() def vis2d(input_filename, outfile, mask_image, window, level, nThreads, image_orientation, slice_percent, offscreen, overwrite, func): diff --git a/ogo/dat/L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk b/ogo/dat/L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk new file mode 100644 index 0000000..e6e52fa --- /dev/null +++ b/ogo/dat/L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk @@ -0,0 +1,29474 @@ +# vtk DataFile Version 5.1 +vtk output +ASCII +DATASET POLYDATA +POINTS 18937 float +-376.799 -2.09869 322.647 -376.779 -1.6008 322.487 -377.037 -1.75127 322.816 +-376.809 -1.93055 323.144 -376.789 -1.43265 322.984 -375.603 -3.19312 321.789 +-375.924 -2.72592 321.197 -376.206 -2.75116 321.822 -376.374 -2.64904 322.931 +-375.773 -3.23622 322.826 -376.195 -2.61908 324 -375.733 -3.19992 323.763 +-375.698 -2.92943 324.366 -375.829 -2.34369 320.755 -375.791 -1.65869 320.37 +-376.192 -1.84835 321.033 -376.434 -1.83153 321.989 -376.443 -1.64907 323.762 +-375.925 -2.38638 324.692 -376.213 -1.72577 324.777 -375.816 -1.83262 325.223 +-376.272 -0.720505 320.905 -375.878 -0.510672 320.302 -376.354 -0.685211 321.779 +-376.425 -0.933407 322.777 -376.228 -0.546388 323.714 -376.21 -0.868976 324.642 +-375.728 -0.814303 325.38 -376.214 0.0868336 320.813 -375.744 0.285489 320.307 +-376.297 0.230307 321.579 -376.205 0.159537 322.673 -375.842 -0.19611 324.147 +-375.889 0.166001 323.396 -375.738 -0.262038 324.889 -375.682 0.64252 320.955 +-375.746 0.707035 322.03 -375.593 0.632211 322.978 -374.705 -3.16047 321.035 +-374.83 -2.68666 320.314 -375.441 -2.70455 320.751 -374.874 -3.28093 321.871 +-374.752 -3.34222 322.731 -374.81 -3.29069 323.867 -375.394 -2.68398 325.128 +-374.82 -3.25473 324.821 -375.164 -2.62559 325.898 -374.799 -3.17345 325.717 +-374.617 -2.84719 326.242 -374.529 -2.22945 319.805 -374.843 -1.55042 319.508 +-375.221 -1.97174 319.954 -375.396 -1.64708 325.76 -374.743 -1.99923 326.281 +-375.36 -0.825508 319.692 -374.819 -0.770017 319.201 -375.313 -0.774226 325.844 +-374.605 -0.818189 326.356 -374.747 -0.266168 318.724 -374.571 0.263723 318.476 +-375.148 0.109928 318.988 -375.364 0.202745 319.67 -375.443 0.323044 323.751 +-375.267 0.23646 324.584 -374.807 0.272231 325.357 -374.831 -0.195083 325.839 +-374.849 0.640364 318.961 -374.881 0.792059 319.932 -374.818 0.868978 320.97 +-374.93 0.849572 321.828 -374.921 0.873085 322.834 -374.899 0.755636 323.952 +-374.674 0.757386 324.889 -373.546 -4.15302 322.722 -373.984 -3.70391 322.371 +-374.168 -3.6957 322.963 -373.714 -3.86797 323.416 -373.638 -3.17356 320.621 +-374.082 -2.79672 320.256 -373.862 -3.38698 321.636 -373.902 -3.39083 323.823 +-373.867 -3.28265 324.766 -373.894 -3.19055 325.731 -373.998 -2.60468 326.289 +-373.701 -2.42481 319.733 -373.934 -1.72217 319.329 -373.817 -1.86005 326.473 +-373.567 -1.23021 318.701 -373.991 -0.472155 318.32 -374.304 -0.885331 318.896 +-374.228 -0.705848 326.891 -374.005 -1.16527 326.816 -373.593 -0.883521 327.164 +-373.891 0.186189 318.242 -374.252 0.284541 325.821 -373.991 0.123584 326.294 +-373.838 -0.218781 326.91 -373.925 0.801516 318.734 -373.686 1.36426 319.256 +-374.189 1.22286 319.771 -374.323 1.24355 320.636 -374.31 1.21231 321.637 +-374.304 1.2201 322.612 -374.314 1.25279 323.627 -374.251 1.27993 324.605 +-373.836 1.28103 325.314 -373.796 0.655894 325.901 -373.719 1.60321 319.912 +-373.757 1.75239 320.954 -373.685 1.73487 322.077 -373.693 1.61175 323.046 +-373.695 1.64692 324.059 -373.72 1.6276 324.767 -372.816 -4.11631 322.049 +-372.887 -3.71621 321.439 -373.352 -3.8755 321.893 -372.803 -4.23348 322.99 +-373.231 -3.72074 324.018 -373.041 -4.12133 323.663 -372.63 -3.72888 324.201 +-372.645 -3.1672 319.934 -372.919 -2.57673 319.458 -373.339 -2.86924 319.909 +-372.747 -3.35197 320.982 -372.908 -3.27345 324.795 -372.544 -2.76053 325.369 +-373.235 -2.6928 325.908 -372.786 -1.86858 319.248 -372.605 -2.40077 326.046 +-372.885 -2.18227 326.679 -373.389 -1.65099 326.983 -372.75 -1.74261 327.23 +-372.722 -1.15579 317.983 -372.831 -0.622745 317.393 -373.348 -0.806858 317.878 +-372.709 -1.36411 318.892 -372.736 -1.19965 326.917 -373.206 -0.632686 326.899 +-372.681 -0.710323 326.538 -373.303 0.0372946 317.814 -372.521 0.11089 317.363 +-373.332 0.546202 326.771 -372.775 -0.352072 326.802 -372.896 0.0422095 327.207 +-372.898 0.770493 317.976 -372.485 1.26637 318.359 -373.229 1.22993 318.661 +-373.425 1.21034 326.064 -373.185 1.46242 326.684 -372.681 0.993889 327.31 +-372.967 1.60723 319.048 -372.938 1.90321 319.891 -372.752 2.33831 320.247 +-373.269 2.2162 320.759 -373.328 2.28708 321.542 -372.964 2.20457 322.249 +-372.908 1.89093 322.872 -372.726 1.79557 323.867 -373.038 1.80605 324.821 +-373.215 2.03644 325.784 -372.549 2.2914 325.378 -372.58 1.77577 326.921 +-372.91 2.24282 326.354 -372.708 2.64924 320.929 -372.62 2.62912 321.973 +-372.695 2.61334 325.89 -372.395 -3.70185 321.678 -372.275 -3.79085 322.593 +-372.335 -3.80606 323.613 -371.597 -3.22356 319.858 -371.983 -2.69301 319.404 +-371.843 -3.2754 320.748 -371.795 -3.32211 321.874 -371.645 -3.34516 323.06 +-371.793 -3.3637 324.105 -371.919 -3.13453 324.832 -371.574 -2.76637 325.339 +-371.85 -1.84433 319.213 -371.948 -2.38513 325.85 -371.905 -2.13562 326.742 +-371.982 -1.73747 327.202 -371.689 -1.18417 317.779 -372.049 -0.708808 317.323 +-371.784 -1.38092 318.578 -371.685 -0.758502 326.459 -371.735 -1.44378 326.832 +-371.558 0.286801 317.32 -372.055 -0.203394 326.816 -371.839 0.557829 327.264 +-372.05 0.817506 317.762 -371.826 1.25541 318.219 -371.969 1.30888 327.254 +-371.89 1.76484 318.776 -371.688 2.341 319.232 -372.266 2.19846 319.683 +-372.426 2.32665 322.766 -372.201 2.2214 323.572 -371.703 2.29084 324.328 +-372.042 1.91321 324.78 -371.697 2.39871 325.301 -371.959 2.27996 326.345 +-371.725 1.70998 326.811 -371.806 2.64145 319.961 -371.718 2.75085 320.93 +-371.818 2.76182 321.813 -371.772 2.66478 322.956 -371.667 2.57072 323.81 +-371.858 2.60905 325.84 -370.618 -3.21936 319.894 -371.026 -2.65506 319.312 +-370.857 -3.29418 320.773 -370.81 -3.23927 321.796 -370.747 -3.23067 322.818 +-370.858 -3.25162 323.7 -370.883 -3.14098 324.655 -370.827 -2.7885 325.283 +-370.518 -2.19899 318.83 -370.89 -1.66909 318.475 -371.231 -1.86025 318.929 +-370.88 -2.27945 325.837 -370.78 -1.51878 326.315 -371.306 -1.86248 326.815 +-370.701 -1.28186 317.897 -371.014 -0.697308 317.359 -370.78 -0.809345 326.299 +-370.659 0.115794 317.241 -371.347 0.19153 326.76 -370.739 0.128473 326.399 +-371.388 1.21767 317.837 -370.666 1.09827 317.397 -371.274 1.02357 326.87 +-370.806 1.34232 326.429 -370.918 1.79276 317.995 -370.521 2.28503 318.332 +-371.238 2.223 318.646 -371.515 2.26192 324.873 -370.824 2.19588 326.24 +-370.858 2.63358 319.072 -370.796 2.75384 319.801 -370.812 2.82948 320.742 +-370.908 2.83594 321.85 -370.864 2.84809 322.867 -371.045 2.85664 323.893 +-371.197 2.96672 324.878 -370.676 3.36232 324.296 -370.815 2.7802 325.745 +-370.811 3.28947 325.282 -370.581 3.5766 324.797 -369.632 -3.19544 319.908 +-370.031 -2.79667 319.311 -369.856 -3.29167 320.803 -369.799 -3.26613 321.826 +-369.741 -3.27187 322.842 -369.961 -3.19165 323.875 -369.487 -2.79701 324.372 +-370.242 -2.79302 324.842 -369.565 -2.24703 318.824 -370.029 -1.74771 318.319 +-369.912 -2.27413 325.026 -369.553 -1.60305 325.354 -370.24 -1.85216 325.695 +-369.56 -1.27861 317.798 -369.993 -0.675429 317.378 -369.941 -1.34744 325.901 +-369.8 -0.620759 326.209 -369.797 0.25155 317.255 -369.859 0.354694 326.333 +-369.602 1.16852 317.376 -369.763 1.24243 326.362 -370.039 1.79004 317.777 +-369.538 2.26036 318.317 -369.729 2.27757 326.283 -369.911 2.62993 318.819 +-369.967 2.87664 319.799 -369.715 3.33145 320.281 -370.213 3.18549 320.8 +-370.336 3.23954 321.646 -370.261 3.23023 322.639 -370.302 3.26082 323.578 +-369.896 3.41606 325.446 -369.776 2.90835 325.94 -369.671 3.6221 320.974 +-369.692 3.63509 322.055 -369.638 3.62228 323.051 -369.959 3.66189 324.002 +-369.664 3.83177 324.732 -368.613 -3.20588 319.736 -369.085 -2.77816 319.245 +-368.853 -3.27095 320.829 -368.794 -3.26094 321.858 -368.742 -3.2868 322.874 +-368.964 -3.19604 323.744 -368.559 -2.80629 324.364 -368.694 -2.38086 318.737 +-368.88 -1.83802 318.317 -369.051 -2.26862 324.813 -368.714 -1.7385 325.195 +-368.565 -1.26156 317.82 -369.003 -0.673621 317.389 -369.373 -0.867085 325.762 +-368.671 -0.891148 325.463 -368.805 0.239176 317.266 -368.826 -0.358465 325.9 +-368.819 0.229381 326.26 -368.584 1.32137 317.365 -368.782 1.24658 326.382 +-369.064 1.82409 317.799 -368.812 2.24601 318.253 -368.699 2.16615 326.381 +-368.879 2.75899 318.793 -368.688 3.34414 319.246 -369.248 3.20082 319.707 +-369.323 3.28191 325.866 -368.878 3.02011 326.23 -368.786 3.64245 319.975 +-368.837 3.75438 320.808 -368.818 3.83351 321.733 -368.873 3.83171 322.828 +-369.062 3.88121 323.89 -368.649 4.42552 324.379 -369.115 4.20801 324.734 +-368.998 4.12688 325.255 -368.683 3.70164 325.817 -368.63 4.51854 324.944 +-367.673 -3.1846 318.953 -367.938 -2.55895 318.445 -368.359 -2.86039 318.891 +-367.717 -3.28238 319.953 -367.828 -3.24444 320.863 -367.787 -3.2403 321.889 +-367.757 -3.26951 322.902 -367.982 -3.19182 323.79 -367.551 -2.79832 324.403 +-367.783 -1.86189 318.216 -368.038 -2.28565 324.873 -367.829 -1.63111 325.234 +-367.566 -1.23928 317.834 -368.019 -0.664761 317.409 -367.589 -0.827896 325.424 +-367.819 0.243672 317.28 -367.979 -0.21023 325.826 -367.748 0.357181 326.244 +-367.721 1.13341 317.286 -367.796 1.26372 326.376 -368.383 2.20988 317.853 +-367.694 2.07706 317.421 -367.834 2.30957 326.374 -367.911 2.7737 318.008 +-367.518 3.28101 318.345 -368.241 3.21563 318.663 -367.837 3.19771 326.263 +-367.847 3.62414 319.082 -367.779 3.74523 319.808 -367.972 3.86016 320.788 +-367.718 4.31208 321.276 -368.202 4.16695 321.784 -368.324 4.33081 322.756 +-368.492 4.22749 323.663 -368.046 4.27816 325.356 -367.608 3.77553 325.859 +-367.663 4.60296 321.969 -367.888 4.84442 322.895 -367.612 5.30574 323.358 +-368.195 5.0066 323.812 -367.773 4.77914 324.731 -367.777 5.33774 324.25 +-367.686 5.58876 323.883 -366.573 -3.20645 318.907 -367.006 -2.69718 318.404 +-366.817 -3.27395 319.823 -366.934 -3.22347 320.747 -366.723 -3.32186 321.651 +-366.894 -3.37721 322.673 -366.872 -3.13719 323.664 -366.703 -2.68253 324.217 +-366.842 -1.86435 318.17 -366.881 -2.2729 324.763 -366.8 -1.64741 325.211 +-366.725 -1.37126 317.757 -366.879 -0.767709 317.4 -366.628 -0.983348 325.482 +-366.803 0.229108 317.321 -367.049 -0.293097 325.824 -366.723 0.183926 326.274 +-366.849 1.24461 317.331 -366.81 1.26011 326.364 -366.597 2.30425 317.397 +-366.835 2.29118 326.354 -367.09 2.83369 317.783 -366.698 3.35402 318.155 +-366.753 3.28146 326.266 -366.756 3.65462 318.702 -366.943 3.8609 319.795 +-366.691 4.31809 320.27 -367.261 4.18173 320.715 -366.843 4.36087 325.428 +-366.781 3.89156 325.92 -366.788 4.62936 320.978 -366.662 4.70133 321.945 +-367.064 4.83892 322.691 -366.597 5.23019 323.322 -367.089 5.3184 324.379 +-366.683 4.8739 324.909 -366.786 5.6199 323.95 -365.504 -4.14029 321.821 +-365.899 -3.68032 321.427 -366.254 -3.79387 322.158 -366.275 -3.62619 322.979 +-365.832 -4.11847 322.688 -365.618 -3.7761 323.168 -365.643 -3.13309 318.815 +-366.018 -2.63956 318.316 -365.724 -3.34262 319.576 -365.953 -3.35523 320.708 +-365.764 -3.00288 323.464 -366.288 -2.71754 323.806 -365.499 -2.20272 317.79 +-365.939 -1.55046 317.468 -366.218 -1.88665 317.885 -365.881 -2.41185 324.063 +-365.911 -2.12543 324.802 -365.685 -1.67925 325.306 -365.761 -0.791034 317.329 +-366.343 -0.640113 325.914 -365.846 -1.15222 325.774 -365.715 -0.677814 326.205 +-365.807 0.330666 317.266 -365.784 0.233793 326.357 -365.968 1.47722 317.235 +-365.807 1.25099 326.32 -365.666 2.44556 317.206 -365.819 2.24678 326.32 +-366.349 3.14401 317.717 -365.723 3.16157 317.459 -365.687 3.12823 326.341 +-365.916 3.6129 318.018 -365.865 3.89807 318.86 -365.682 4.32988 319.231 +-366.233 4.1832 319.698 -366.335 4.25146 325.849 -365.875 3.99117 326.214 +-365.774 4.63565 319.958 -365.799 4.73476 320.784 -365.761 4.78483 321.735 +-366.042 4.83564 322.739 -365.786 5.34207 323.38 -366.324 5.33944 324.693 +-365.922 5.06888 325.239 -365.664 4.66329 325.814 -365.756 5.767 323.995 +-365.715 5.60728 324.733 -364.761 -4.17181 319.867 -364.812 -3.80116 319.408 +-365.199 -3.72936 320.08 -365.393 -3.80343 320.898 -364.857 -4.24877 320.728 +-364.75 -4.37013 321.758 -364.97 -4.26155 322.714 -364.899 -3.71486 323.354 +-364.965 -3.30251 318.948 -364.967 -2.83157 318.305 -365.255 -2.61665 323.857 +-364.71 -3.1697 323.876 -364.689 -2.75851 324.312 -364.708 -2.3794 317.724 +-364.859 -1.77787 317.412 -364.865 -2.31028 324.775 -364.698 -1.8368 325.444 +-364.883 -0.721049 317.212 -365.045 -1.34481 325.838 -364.719 -0.842993 326.245 +-364.708 -0.304133 316.748 -364.749 0.333769 316.441 -365.198 0.195881 316.901 +-364.771 0.222991 326.297 -365.357 1.05451 316.824 -364.763 1.29437 316.376 +-364.747 1.17078 326.387 -365.318 2.02094 316.805 -364.586 2.18493 316.444 +-364.816 2.30126 326.409 -364.902 2.66942 316.868 -364.673 3.18467 317.317 +-364.926 3.386 326.335 -365.008 3.77526 317.95 -364.536 4.28399 318.382 +-365.24 4.20979 318.659 -364.681 4.28221 326.251 -364.853 4.61967 319.078 +-364.767 4.72972 319.782 -364.921 4.81512 320.77 -364.725 5.32517 321.426 +-365.173 5.24741 321.927 -365.449 5.31032 322.832 -364.83 5.39859 325.46 +-364.743 4.92357 325.943 -364.904 5.82301 321.995 -364.739 6.38632 322.271 +-365.184 6.05637 322.708 -365.294 6.14535 323.597 -364.943 6.24484 324.331 +-364.822 5.89712 324.84 -364.675 6.69073 322.988 -364.608 6.67474 324.056 +-363.79 -8.19233 319.711 -363.85 -7.54853 319.318 -364.211 -7.74242 320.045 +-364.199 -7.59667 320.974 -363.847 -8.27553 320.532 -363.752 -7.93678 321.245 +-364.245 -6.8344 319.863 -363.819 -6.5986 319.302 -364.371 -6.81565 320.771 +-363.881 -7.33794 321.689 -364.2 -6.67345 321.874 -363.727 -6.88137 322.254 +-364.184 -5.80358 319.921 -363.782 -5.58942 319.361 -364.303 -5.76561 320.787 +-364.309 -5.75402 321.824 -363.867 -6.31602 322.709 -364.171 -5.65265 322.915 +-363.734 -5.88999 323.285 -364.305 -4.70313 319.785 -363.82 -4.72053 319.248 +-364.37 -4.84767 320.845 -364.362 -4.83307 321.879 -364.371 -4.67771 322.935 +-363.869 -5.31165 323.749 -364.125 -4.65764 323.778 -363.728 -4.78443 324.255 +-363.703 -4.17113 318.73 -363.85 -3.57695 318.252 -364.263 -3.73916 318.837 +-364.351 -3.88417 323.722 -363.878 -3.87445 324.358 -363.643 -3.29207 317.734 +-363.806 -2.6556 317.465 -364.227 -2.94173 317.968 -364.306 -2.72932 324.794 +-363.771 -3.20615 324.809 -363.756 -2.74267 325.334 -363.865 -1.72128 317.218 +-364.323 -1.65469 325.902 -363.74 -2.27901 325.845 -363.757 -1.70235 326.346 +-363.672 -1.32251 316.76 -363.884 -0.65596 316.42 -364.247 -0.885611 316.899 +-363.862 -0.808093 326.412 -363.842 0.245332 316.268 -363.858 0.158475 326.443 +-363.862 1.25078 316.292 -364.295 1.48521 326.837 -363.921 0.787696 326.748 +-363.602 1.06191 327.188 -363.605 2.2776 316.394 -364.236 2.25772 326.772 +-363.678 2.10174 327.221 -364.062 2.82742 316.802 -363.806 3.25483 317.247 +-363.947 3.26233 326.439 -363.717 2.78493 326.901 -363.849 3.76573 317.685 +-363.743 4.3428 318.228 -363.728 4.26569 326.323 -363.793 4.63161 318.727 +-363.925 4.81556 319.793 -363.824 5.49856 320.313 -364.294 5.3068 320.862 +-364.317 5.29447 325.862 -363.82 5.00578 326.196 -364.149 6.06427 320.844 +-363.737 6.29618 320.323 -364.383 6.11828 321.546 -364.329 6.38249 324.782 +-363.979 6.12821 325.217 -363.637 5.69611 325.663 -363.641 6.63263 320.93 +-364.003 6.6827 321.962 -363.568 6.86141 322.852 -363.948 6.81403 323.716 +-363.665 6.59828 324.579 -362.677 -9.13936 318.75 -362.872 -8.61352 318.455 +-363.181 -8.80584 318.972 -363.354 -8.69909 319.894 -362.717 -9.31493 319.52 +-363.324 -8.66211 321.048 -362.847 -9.29778 320.57 -363.24 -8.70072 322.02 +-362.764 -9.29995 321.742 -363.181 -8.60091 322.797 -362.764 -9.12122 322.791 +-362.722 -8.78158 323.323 -363.348 -8.03374 318.918 -362.732 -7.69942 318.283 +-363.439 -7.7331 321.892 -363.233 -7.73219 322.817 -362.834 -8.04694 323.331 +-363.312 -7.00142 318.809 -362.795 -6.52761 318.347 -363.424 -6.7049 322.912 +-362.858 -7.31494 323.642 -363.155 -6.66067 323.875 -362.701 -6.87285 324.237 +-363.289 -5.85913 318.764 -362.86 -5.66456 318.221 -363.438 -5.70199 323.966 +-362.854 -6.32352 324.71 -363.174 -5.64973 324.936 -362.728 -5.88265 325.313 +-362.802 -5.21914 317.815 -362.759 -4.5748 317.382 -363.124 -4.89259 318.054 +-363.441 -4.81945 318.761 -363.406 -4.71397 324.775 -362.882 -5.31148 325.781 +-363.107 -4.63705 325.596 -362.733 -4.88547 326.187 -363.332 -4.0027 317.96 +-362.908 -3.49516 317.3 -363.34 -3.90019 324.985 -362.689 -4.33285 325.968 +-362.902 -3.79804 325.426 -362.968 -2.58589 317.19 -363.201 -2.7681 325.862 +-362.922 -3.20453 325.651 -362.747 -2.85024 326.23 -362.637 -2.3096 316.771 +-362.852 -1.65485 316.442 -363.205 -1.89097 316.914 -363.202 -1.54475 326.85 +-362.959 -2.25844 326.699 -362.7 -1.87078 327.245 -362.793 -0.780612 316.299 +-363.251 -0.580203 326.806 -362.781 -1.01957 327.359 -362.81 0.222597 316.343 +-363.39 0.288731 326.874 -362.888 0.120742 327.348 -362.866 1.33352 316.252 +-362.83 1.33384 327.446 -362.701 2.3885 316.22 -362.838 2.27118 327.344 +-363.38 3.17234 316.873 -362.712 3.25709 316.434 -363.209 3.36001 326.748 +-362.766 3.08656 327.207 -362.949 3.79792 317.024 -362.63 4.32888 317.207 +-363.221 4.20555 317.715 -363.017 4.20656 326.467 -362.611 3.82182 326.898 +-362.806 4.5947 317.919 -362.825 4.8126 318.887 -362.773 5.48725 319.282 +-363.268 5.18181 319.732 -362.898 5.18732 326.337 -363.282 5.98394 319.833 +-362.828 6.20147 319.396 -362.885 6.25837 324.508 -362.724 5.87819 325.067 +-363.335 6.18588 324.948 -362.687 5.65757 325.988 -362.835 6.71631 319.871 +-362.782 6.86696 320.926 -363.068 6.87536 321.835 -362.717 7.38792 322.396 +-363.08 7.18208 322.673 -362.909 7.18231 323.276 -362.952 6.72659 323.932 +-362.726 7.57443 322.987 -362.762 21.8438 308.692 -362.899 22.2902 308.228 +-363.167 22.4407 308.918 -362.763 22.0138 309.278 -362.76 22.7805 307.622 +-362.826 23.4992 307.319 -363.212 23.112 308.047 -363.386 23.3189 308.917 +-362.901 22.6371 309.72 -363.182 23.2891 309.727 -362.736 23.1794 310.206 +-363.225 24.1729 307.88 -362.817 24.4257 307.308 -363.368 24.1877 308.771 +-363.261 24.2968 309.876 -362.791 23.9806 310.391 -363.225 25.2738 307.788 +-362.877 25.2574 307.183 -363.337 25.2021 308.799 -363.357 25.261 309.853 +-362.862 24.7033 310.718 -363.166 25.3373 310.766 -362.716 25.1894 311.221 +-362.814 25.718 306.803 -362.794 26.3938 306.372 -363.185 26.1493 307.051 +-363.321 26.3343 307.896 -363.318 26.212 308.812 -363.341 26.2617 309.869 +-363.232 26.2925 310.773 -362.806 26.0355 311.363 -362.767 25.8458 328.719 +-362.916 26.286 328.239 -363.134 26.4367 328.763 -362.748 26.0999 329.215 +-363.241 27.2106 306.86 -362.877 27.4493 306.26 -363.322 27.2153 307.754 +-363.31 27.2236 308.805 -363.328 27.2602 309.866 -363.221 27.3001 310.76 +-362.807 27.0445 311.345 -362.841 26.6854 327.758 -362.787 27.3538 327.345 +-363.227 27.1364 328.032 -363.253 27.3687 328.937 -362.811 26.9362 329.361 +-363.265 28.1953 306.879 -362.726 28.4338 306.293 -363.329 28.2236 307.775 +-363.301 28.2341 308.817 -363.323 28.2618 309.866 -363.218 28.3 310.758 +-362.8 28.0458 311.342 -363.246 28.202 327.847 -362.84 28.4267 327.252 +-363.372 28.2029 328.776 -362.868 27.6773 329.662 -363.216 28.3429 329.897 +-362.721 28.149 330.28 -363.205 29.1207 306.893 -362.608 29.3392 306.451 +-363.347 29.2255 307.808 -363.313 29.2469 308.857 -363.327 29.2682 309.888 +-363.223 29.3091 310.77 -362.802 29.0583 311.355 -363.247 29.2259 327.891 +-362.83 29.4174 327.27 -363.362 29.2444 328.828 -363.376 29.2674 329.896 +-362.908 28.7362 330.769 -363.218 29.3552 330.82 -362.607 29.1401 331.219 +-362.904 29.7521 306.908 -362.824 30.5213 307.238 -363.266 30.2031 307.785 +-363.345 30.2784 308.888 -363.342 30.2846 309.91 -363.242 30.3144 310.793 +-362.819 30.0856 311.376 -363.19 30.1843 327.896 -362.757 30.281 327.405 +-363.368 30.2975 328.847 -363.367 30.1878 329.779 -363.228 30.3547 330.632 +-362.708 29.9936 331.35 -363.201 31.2455 307.959 -362.835 31.1904 307.388 +-363.379 31.1808 308.76 -363.257 31.3546 309.65 -363.179 31.1929 310.617 +-362.836 31.0406 311.33 -362.895 30.8187 327.913 -362.75 31.4317 328.369 +-363.183 31.0851 328.643 -363.203 31.1515 329.71 -362.779 30.6828 331.007 +-362.892 31.1533 330.404 -362.749 31.77 307.71 -362.776 32.4274 308.379 +-363.172 32.0368 308.752 -362.809 31.8211 310.131 -362.88 32.1612 309.394 +-362.758 31.638 310.943 -362.807 31.7666 329.088 -362.73 31.6588 329.863 +-362.754 32.6005 308.946 -361.58 -12.1559 327.757 -361.921 -11.74 327.257 +-362.194 -11.5667 327.908 -361.731 -11.8636 328.398 -361.828 -11.3333 326.803 +-361.841 -10.7757 326.298 -362.222 -10.7909 326.929 -362.371 -10.7127 327.906 +-361.918 -11.3565 328.676 -362.223 -10.6933 328.874 -361.74 -10.8839 329.254 +-361.746 -10.0677 319.962 -361.964 -9.76879 319.442 -362.278 -9.83276 320.046 +-362.348 -9.83241 321.035 -361.664 -10.1606 320.873 -362.225 -9.73593 321.937 +-361.83 -10.1467 321.663 -361.532 -9.79669 322.342 -361.634 -10.1529 324.636 +-361.902 -9.69627 324.252 -362.22 -9.62468 324.929 -362.261 -9.82942 326.001 +-361.726 -10.3076 325.509 -362.32 -9.65474 326.875 -362.327 -9.81397 327.751 +-362.355 -9.79019 328.814 -361.878 -10.2965 329.728 -362.171 -9.68159 329.757 +-361.715 -9.79258 330.217 -361.707 -9.34125 318.96 -361.959 -8.63383 318.26 +-362.016 -9.3736 322.721 -362.309 -8.75425 323.996 -361.823 -9.31922 323.552 +-362.312 -8.68737 324.886 -362.335 -8.7998 325.768 -362.312 -8.76528 326.775 +-362.291 -8.76374 327.795 -362.324 -8.76933 328.841 -362.253 -8.68921 329.921 +-361.784 -9.01111 330.401 -361.687 -8.15902 317.842 -361.757 -7.56299 317.501 +-362.201 -7.91614 317.834 -362.427 -7.58227 323.992 -362.25 -7.7856 324.752 +-362.282 -7.7936 325.769 -362.299 -7.7544 326.812 -362.293 -7.73534 327.85 +-362.316 -7.74177 328.883 -362.371 -7.73792 329.908 -361.877 -8.30563 330.72 +-362.203 -7.68464 330.782 -361.745 -7.8284 331.213 -362.304 -7.01511 317.794 +-361.8 -6.6487 317.429 -362.386 -6.70306 324.901 -362.242 -6.77049 325.804 +-362.285 -6.75074 326.842 -362.318 -6.72391 327.885 -362.327 -6.85719 328.766 +-362.273 -6.69273 329.661 -362.239 -6.86044 330.629 -361.685 -6.8426 331.366 +-362.395 -5.78122 317.691 -361.719 -5.79734 317.26 -362.411 -5.70358 325.964 +-362.277 -5.75294 326.864 -362.335 -5.72858 327.906 -362.256 -5.81042 328.895 +-361.875 -6.20636 330.125 -361.768 -5.68891 329.435 -361.744 -6.26283 330.867 +-361.577 -5.17021 316.793 -361.858 -4.52061 316.45 -362.245 -4.88437 316.947 +-362.353 -4.64133 326.719 -362.327 -4.87271 327.758 -362.216 -4.67268 328.616 +-361.696 -4.78849 329.169 -362.315 -3.94443 316.758 -361.691 -3.73467 316.362 +-362.396 -3.69423 326.084 -362.255 -3.63674 326.937 -362.231 -3.83862 327.854 +-361.678 -4.16313 328.915 -361.908 -3.79893 328.498 -362.32 -3.01303 316.805 +-361.832 -2.69235 316.384 -362.379 -2.73821 326.931 -362.189 -2.74363 327.721 +-361.821 -2.95538 328.336 -361.78 -1.77038 316.309 -362.298 -1.66115 327.771 +-361.827 -1.95734 328.347 -361.775 -0.770016 316.364 -362.308 -0.544894 327.87 +-361.73 -0.847747 328.354 -361.914 0.294892 316.23 -362.316 0.457511 327.852 +-361.812 0.110085 328.222 -361.703 0.676695 315.739 -361.753 1.31407 315.43 +-362.229 1.19457 315.894 -362.183 1.29177 327.76 -361.617 1.09228 328.188 +-362.338 1.98597 315.82 -361.593 2.33342 315.421 -362.093 2.18881 327.462 +-361.679 1.79038 327.882 -362.008 2.69302 315.893 -361.754 3.33694 316.191 +-361.678 3.23009 327.436 -362.344 4.10987 316.675 -361.67 4.30334 316.42 +-362.301 4.57102 326.867 -361.925 4.10159 327.248 -361.874 4.69652 317.042 +-361.923 4.84878 317.974 -361.729 5.49105 318.263 -362.223 5.17623 318.695 +-362.307 5.31565 326.744 -361.667 5.03131 327.233 -362.293 6.07118 318.91 +-361.785 6.19024 318.419 -362.334 6.43488 324.69 -361.817 6.26085 325.304 +-361.809 5.84401 325.9 -361.78 5.62274 326.767 -361.825 6.79114 318.821 +-361.84 7.27651 319.332 -362.208 7.21282 319.757 -362.304 7.33672 320.747 +-362.441 7.34152 321.774 -362.451 7.32278 323.825 -362.182 7.36341 324.659 +-361.694 7.12488 325.198 -361.717 7.66828 319.898 -361.889 7.88652 320.945 +-361.822 8.27593 321.383 -362.134 8.0387 321.709 -362.313 8.07206 322.746 +-362.209 8.13762 323.814 -361.712 7.81106 324.936 -361.725 8.30211 324.432 +-361.71 8.70745 321.907 -361.876 8.78987 323.087 -361.747 8.76234 323.837 +-361.764 18.7866 329.714 -361.83 19.4319 329.322 -362.205 19.2534 330.057 +-362.215 19.4341 330.994 -361.845 18.7341 330.539 -361.771 19.1186 331.293 +-362.254 20.1479 329.877 -361.806 20.3998 329.29 -362.403 20.1998 330.797 +-361.692 19.6782 331.912 -362.22 20.3022 331.728 -361.716 20.247 332.368 +-361.754 20.8619 307.761 -361.915 21.3971 307.223 -362.16 21.33 307.88 +-362.324 21.3781 308.963 -361.818 20.6967 308.634 -362.175 21.4152 309.973 +-361.642 20.7235 309.785 -361.687 21.1423 310.376 -362.221 21.1721 329.913 +-361.796 21.4243 329.331 -362.372 21.2291 330.818 -362.262 21.2714 331.736 +-361.859 21.028 332.371 -361.725 21.7059 306.69 -361.735 22.3214 306.414 +-362.188 22.1104 306.984 -362.471 22.1136 307.839 -362.463 22.2438 309.925 +-361.899 21.6316 310.642 -362.182 22.3106 310.851 -361.745 22.0859 311.22 +-362.236 22.2729 329.805 -361.876 22.2667 329.199 -362.352 22.239 330.84 +-362.231 22.3031 331.761 -361.836 22.0599 332.374 -362.335 23.0385 306.888 +-361.709 23.2505 306.387 -362.381 23.3225 310.824 -361.869 22.6693 311.687 +-362.175 23.3311 311.902 -361.74 23.1075 312.279 -361.739 22.7858 328.714 +-361.81 23.4894 328.387 -362.184 23.1264 329.073 -362.341 23.3211 329.908 +-362.345 23.2393 330.829 -362.225 23.2933 331.762 -361.823 23.0708 332.373 +-362.318 24.0427 306.796 -361.704 24.2698 306.447 -362.434 24.3999 311.049 +-362.304 24.2243 311.845 -361.89 23.6886 312.748 -362.173 24.313 312.775 +-361.762 24.1996 313.256 -362.222 24.266 328.793 -361.871 24.2722 328.187 +-362.331 24.1961 329.791 -362.348 24.2467 330.842 -362.234 24.2783 331.764 +-361.83 24.0597 332.37 -362.402 25.2177 306.707 -361.79 25.2338 306.227 +-362.382 25.3285 311.843 -362.215 25.2609 312.75 -361.833 25.0579 313.381 +-361.713 24.8165 327.715 -361.771 25.5308 327.414 -362.128 25.0228 328.048 +-362.398 25.4094 328.883 -362.304 25.2191 329.834 -362.342 25.2512 330.88 +-362.256 25.2773 331.785 -361.861 25.0565 332.379 -361.716 25.7051 305.718 +-361.728 26.3227 305.416 -362.234 26.1298 305.958 -362.403 26.4621 311.947 +-362.194 26.2668 312.725 -361.823 26.0598 313.344 -362.382 26.1172 327.823 +-361.837 26.3199 327.229 -362.369 26.3316 329.846 -362.3 26.1245 330.747 +-362.203 26.3076 331.601 -361.709 26.2038 332.381 -362.367 27.0581 305.761 +-361.749 27.2815 305.369 -362.4 27.4557 311.945 -362.186 27.2519 312.725 +-361.809 27.05 313.333 -361.704 26.8471 326.725 -361.782 27.5611 326.392 +-362.182 27.0736 326.991 -362.421 27.4494 330.046 -362.18 27.1787 330.851 +-361.707 26.8024 331.922 -361.871 27.1621 331.476 -362.329 27.9865 305.782 +-361.591 28.37 305.431 -362.396 28.4552 311.952 -362.187 28.2378 312.744 +-361.81 28.0407 313.366 -362.305 28.0093 326.801 -361.778 28.4892 326.344 +-362.386 28.2524 330.802 -361.864 28.168 331.329 -361.995 28.6965 305.879 +-361.759 29.3378 306.178 -362.418 29.393 311.845 -362.146 29.2949 312.553 +-361.805 28.9823 313.318 -362.298 28.983 326.81 -361.783 29.4748 326.343 +-361.897 29.2119 331.515 -362.439 30.1278 306.701 -361.902 30.2244 306.401 +-362.293 30.3789 311.938 -361.733 29.693 312.969 -361.85 30.189 312.441 +-362.305 30.1245 326.894 -361.818 30.4854 326.316 -362.299 30.5206 331.741 +-361.947 29.82 331.806 -361.603 30.0932 332.226 -362.301 31.0474 306.869 +-361.514 31.313 306.426 -362.311 31.5133 311.723 -361.787 31.0368 312.323 +-362.17 31.204 326.917 -361.851 31.1892 326.388 -362.457 31.2062 327.707 +-362.448 31.3945 330.884 -362.245 31.3734 331.894 -361.766 31.0573 332.294 +-361.965 31.7743 306.988 -361.791 32.4674 307.21 -362.323 32.3364 307.789 +-362.436 32.5457 309.783 -362.412 32.1345 310.858 -362.147 32.4128 311.658 +-361.807 31.9713 312.313 -361.744 31.7452 326.726 -361.771 32.3623 327.411 +-362.191 32.1736 327.787 -362.408 32.257 328.776 -362.395 32.2754 329.91 +-362.343 32.3694 330.998 -362.27 32.2595 331.783 -361.786 32.0747 332.28 +-362.154 33.1604 307.9 -361.731 33.2738 307.426 -362.411 33.1524 308.858 +-362.309 33.295 309.85 -362.173 33.2328 310.69 -361.755 32.6569 312.025 +-361.867 33.1047 311.443 -361.899 32.8267 327.981 -361.757 33.4086 328.399 +-362.159 33.074 328.669 -362.2 33.182 329.592 -362.217 33.1492 330.616 +-362.208 33.1705 331.611 -361.685 33.1652 332.231 -361.892 33.813 307.944 +-361.756 34.4276 308.395 -362.183 34.076 308.653 -362.192 34.1439 309.704 +-361.726 33.6982 310.989 -361.869 34.1962 310.41 -361.808 33.7257 329.09 +-361.802 33.7499 330.028 -361.827 33.7564 331.067 -361.716 33.8067 331.874 +-361.828 34.7786 309.106 -361.758 34.7131 309.898 -360.819 -13.2422 329.848 +-360.808 -12.7705 329.282 -361.164 -12.6348 329.894 -361.258 -12.7274 331.012 +-360.844 -13.3479 330.57 -361.244 -12.5722 332.024 -360.877 -13.2686 331.57 +-360.597 -12.938 332.332 -360.791 -12.1895 326.937 -360.829 -11.6976 326.297 +-361.437 -11.69 326.788 -360.697 -12.419 327.652 -361.341 -11.7628 329.073 +-360.988 -12.3059 328.542 -361.271 -11.6704 329.871 -361.32 -11.8311 330.759 +-361.389 -11.7906 331.834 -360.907 -12.2539 332.752 -361.245 -11.6978 332.77 +-360.669 -11.7425 333.196 -360.529 -11.2323 325.782 -360.904 -10.6762 325.429 +-361.282 -10.9832 325.971 -361.401 -10.703 329.899 -361.267 -10.7932 330.781 +-361.334 -10.761 331.835 -361.273 -10.7244 332.763 -360.73 -10.9864 333.401 +-361.46 -9.75993 319.69 -361.24 -9.81171 320.526 -360.889 -9.8793 321.299 +-361.046 -10.1114 321.828 -360.682 -9.71896 322.246 -360.704 -10.0936 324.077 +-360.924 -9.69414 323.506 -361.382 -9.88371 323.947 -360.833 -10.3842 324.903 +-361.339 -9.66776 330.819 -361.288 -9.75249 331.848 -361.232 -9.69795 332.768 +-360.868 -9.9337 333.379 -360.811 -9.12801 318.951 -360.817 -8.70646 318.415 +-360.775 -9.30926 319.908 -360.538 -9.4446 320.997 -360.746 -9.38663 322.963 +-361.429 -8.60193 331.044 -361.277 -8.76719 331.838 -361.21 -8.72601 332.762 +-360.842 -8.92594 333.366 -361.22 -7.69158 317.74 -360.805 -7.8905 318.152 +-361.409 -7.67232 331.847 -361.221 -7.73987 332.76 -360.849 -7.94477 333.369 +-360.629 -7.18469 317.897 -361.087 -6.82195 317.457 -361.359 -6.67094 332.07 +-361.264 -6.70149 332.782 -360.782 -6.80802 333.38 -360.816 -5.89784 317.164 +-361.3 -5.65463 329.768 -361.267 -5.78231 330.531 -360.785 -5.76877 331.319 +-360.983 -6.13025 331.897 -360.668 -5.65784 332.263 -361.152 -5.90583 332.661 +-360.898 -5.86116 333.203 -360.738 -5.40689 316.715 -360.896 -4.77824 316.343 +-360.974 -4.94528 329.425 -360.76 -5.24002 330.048 -360.632 -5.28195 330.807 +-360.561 -5.40484 332.793 -360.768 -3.7941 316.255 -361.333 -3.46668 328.819 +-360.882 -4.01163 329.298 -360.763 -2.78809 316.292 -361.298 -2.52073 328.823 +-360.745 -3.00155 329.317 -360.755 -1.77951 316.343 -361.338 -1.5259 328.832 +-360.808 -1.97671 329.328 -360.849 -0.715835 316.281 -361.193 -0.692446 328.751 +-360.648 -0.810902 329.314 -360.55 -0.243505 315.823 -360.998 0.428098 315.431 +-361.255 0.0932166 315.9 -361.082 0.1894 328.464 -360.649 -0.194921 328.861 +-360.837 1.22028 315.239 -360.792 1.11217 328.451 -360.715 2.13074 315.269 +-361.357 2.49997 327.815 -360.853 2.12029 328.325 -361.401 3.14652 315.764 +-360.671 3.26457 315.437 -361.162 3.29329 327.765 -360.702 3.1849 328.168 +-360.907 3.71158 315.913 -360.857 4.33966 316.167 -360.952 4.30982 327.482 +-360.681 3.81865 327.906 -361.251 5.28038 316.901 -360.894 5.10666 316.378 +-361.412 5.23335 317.59 -360.919 5.29812 327.254 -360.694 5.80706 316.735 +-360.808 6.2832 317.357 -361.274 6.07025 317.788 -361.247 6.31851 325.735 +-360.748 6.25134 326.199 -360.702 5.74605 326.746 -360.805 6.76493 317.843 +-360.811 7.25305 318.339 -361.327 7.26718 318.838 -360.994 7.04894 325.502 +-360.628 6.79088 325.883 -360.841 7.76255 318.854 -360.72 8.34388 319.286 +-361.221 8.19704 319.787 -361.427 8.30918 320.71 -361.33 8.28186 324.826 +-360.923 7.99199 325.294 -360.706 8.58808 319.882 -360.854 8.84773 320.868 +-360.826 9.25078 321.329 -361.214 9.20924 321.781 -361.372 9.28596 322.658 +-361.302 9.27412 323.588 -360.806 9.2645 324.319 -360.854 8.67403 324.863 +-360.725 9.67384 321.89 -360.85 9.7758 323.112 -360.668 9.76882 323.877 +-360.766 16.8358 329.755 -360.886 17.3316 329.231 -361.207 17.3694 329.952 +-361.225 17.4296 330.973 -360.693 16.7179 330.738 -360.753 17.176 331.365 +-360.731 17.8235 328.649 -360.779 18.5479 328.348 -361.145 18.0356 329.006 +-361.438 18.2802 329.798 -361.445 18.2703 330.879 -360.72 17.6304 331.865 +-361.178 18.3063 331.726 -360.689 18.2362 332.356 -360.849 18.7427 307.866 +-360.849 19.222 307.256 -361.207 19.399 307.937 -361.165 19.4438 309.026 +-360.831 18.7088 308.539 -360.747 19.0597 309.267 -361.298 18.9627 328.936 +-360.775 19.4857 328.34 -361.384 19.0868 331.732 -360.892 19.0816 332.373 +-360.702 19.7093 306.667 -360.711 20.3174 306.437 -361.18 20.092 306.966 +-361.442 20.3379 307.813 -361.447 20.113 308.87 -360.859 19.699 309.741 +-361.241 20.1365 309.706 -360.817 20.2211 310.272 -361.288 20.1214 328.727 +-360.853 20.3191 328.241 -361.282 20.5265 332.887 -361.027 19.8123 332.716 +-360.594 20.0949 333.23 -361.383 20.9631 306.928 -360.747 21.3682 306.298 +-361.342 21.3564 311.086 -360.983 20.7425 310.74 -361.134 21.4291 311.978 +-360.765 20.7741 311.546 -360.731 21.0805 312.244 -360.732 20.8042 327.732 +-360.783 21.5065 327.379 -361.154 21.1875 328.131 -361.416 21.0949 328.839 +-361.349 21.4881 332.88 -360.735 21.0559 333.281 -360.931 22.3362 306.291 +-361.44 22.2719 311.909 -360.875 21.6547 312.684 -361.171 22.3267 312.883 +-360.726 22.1151 313.271 -360.787 21.9705 325.819 -360.742 22.3156 325.365 +-361.123 22.4332 325.863 -360.656 22.1377 326.262 -361.155 22.0997 327.831 +-360.74 22.4849 327.329 -361.438 22.0986 328.663 -361.332 22.5058 332.87 +-360.737 22.0655 333.266 -360.778 23.2104 306.345 -361.453 23.2931 312.951 +-360.862 22.6954 313.728 -361.139 23.3118 313.752 -360.711 23.1969 314.213 +-361.176 23.3266 325.937 -360.836 23.4583 325.328 -360.814 23.0182 326.358 +-361.276 23.1464 327.895 -360.797 23.4077 327.213 -361.329 23.5032 332.866 +-360.729 23.0665 333.263 -360.869 24.2906 306.276 -361.309 24.3851 313.913 +-360.776 23.9761 314.414 -361.222 24.2541 325.927 -360.802 24.4484 325.352 +-360.846 23.6599 326.683 -361.206 24.0818 326.879 -361.459 24.2154 327.743 +-361.343 24.503 332.868 -360.728 24.063 333.262 -360.549 24.7767 305.829 +-360.992 25.4322 305.43 -361.282 25.1068 305.88 -361.452 25.4034 313.849 +-360.853 24.6763 314.741 -361.105 25.3359 314.549 -360.713 25.0915 315.153 +-361.209 25.3386 325.829 -360.864 25.282 325.212 -361.385 25.0431 326.892 +-361.376 25.5251 332.882 -360.734 25.0829 333.278 -360.808 26.2144 305.253 +-361.305 26.3798 314.024 -360.708 25.6475 314.941 -360.845 26.0939 314.476 +-360.727 25.7975 324.691 -360.796 26.5154 324.341 -361.167 26.1892 325.083 +-361.298 26.2743 325.815 -361.418 26.1418 326.661 -361.3 26.2518 332.893 +-360.75 26.0652 333.237 -360.846 27.269 305.283 -361.421 27.4801 313.969 +-360.845 26.672 314.653 -361.136 27.2964 314.742 -360.716 27.1719 315.212 +-361.201 27.2363 324.894 -360.796 27.4577 324.296 -361.389 27.0406 325.864 +-361.338 27.5021 331.825 -360.983 27.1531 332.292 -360.698 26.7377 332.826 +-360.752 28.1924 305.282 -361.442 28.3959 313.877 -361.16 28.2888 314.582 +-360.802 27.9533 315.331 -361.202 28.2445 324.914 -360.802 28.4707 324.324 +-361.409 28.0554 325.707 -361.255 28.4646 331.828 -360.66 28.0245 332.225 +-361.387 29.1843 305.746 -360.688 29.1557 305.423 -361.298 29.3788 313.803 +-360.728 28.6688 314.999 -360.855 29.1674 314.479 -361.138 29.1915 324.932 +-360.731 29.3256 324.463 -361.406 29.0435 325.727 -361.411 29.318 331.925 +-360.79 29.1656 332.274 -360.842 29.6849 305.885 -360.876 30.2841 306.232 +-361.456 30.3168 312.918 -361.116 30.4076 313.581 -360.783 29.943 314.27 +-360.852 29.8377 324.97 -360.779 30.5706 325.284 -361.295 30.032 325.692 +-360.845 30.294 332.38 -360.684 31.1584 306.322 -361.291 31.335 312.907 +-360.712 30.6472 313.941 -360.846 31.1585 313.431 -361.315 31.0835 325.915 +-360.781 31.2018 325.416 -360.862 31.3082 332.343 -361.432 32.1652 306.688 +-360.832 32.1847 306.371 -361.323 32.4562 312.72 -360.797 32.0146 313.314 +-360.798 31.7291 325.823 -360.821 32.4873 326.264 -361.331 32.3285 326.837 +-360.802 32.2296 332.429 -361.306 33.1386 306.865 -360.73 33.2663 306.372 +-361.476 33.3022 311.821 -361.207 33.3598 312.646 -360.649 33.1736 313.335 +-361.182 33.1988 326.96 -360.846 33.1633 326.401 -361.478 33.1955 327.756 +-360.84 33.1332 332.476 -361.231 34.2655 306.889 -360.752 34.2587 306.371 +-361.446 34.2031 307.717 -361.465 34.3641 310.782 -361.236 34.2492 311.752 +-360.714 33.8079 312.909 -360.698 34.2717 312.432 -360.75 33.7808 326.723 +-360.854 34.3066 327.337 -361.185 34.0697 327.692 -361.316 34.2169 328.704 +-361.414 34.3529 329.814 -361.413 34.3486 330.832 -361.306 34.3707 331.741 +-360.963 34.0455 332.35 -361.28 35.2965 306.899 -360.777 35.2809 306.389 +-361.336 35.1514 307.683 -361.286 35.2769 308.559 -361.307 35.1851 309.707 +-361.194 35.1302 310.774 -360.718 34.8118 311.882 -360.696 35.3025 311.428 +-360.696 34.6601 327.863 -360.864 34.8523 328.901 -360.714 35.3362 329.398 +-361.17 35.1423 329.791 -361.344 35.2607 330.86 -361.222 35.266 331.765 +-360.822 35.0436 332.358 -361.25 36.2642 306.92 -360.756 36.0977 306.354 +-361.29 36.2582 307.967 -360.747 35.7906 309.045 -360.834 36.1165 308.488 +-360.806 35.7465 310.032 -360.711 35.7647 310.846 -360.863 35.8045 329.956 +-360.741 36.3959 330.401 -361.191 36.0569 330.684 -361.213 36.1875 331.603 +-360.688 36.1884 332.361 -360.785 36.8005 306.714 -360.89 37.3038 307.372 +-361.264 37.1022 307.715 -360.896 36.7091 308.7 -361.188 37.2433 308.619 +-360.575 37.331 309.245 -360.845 36.7319 331.136 -360.721 36.7817 331.863 +-360.769 37.687 307.915 -360.751 37.7776 308.881 -359.513 -14.1927 329.904 +-359.818 -13.6571 329.272 -360.313 -13.6611 329.878 -360.329 -13.8557 331.075 +-359.677 -14.2849 330.854 -360.35 -13.6982 331.899 -359.806 -14.1811 331.712 +-359.773 -13.7405 332.253 -359.578 -13.1405 327.833 -359.808 -12.637 327.358 +-360.31 -12.7493 328.065 -360.331 -12.9631 328.909 -359.726 -13.2657 328.654 +-359.599 -12.9181 332.471 -359.885 -12.2116 326.948 -359.852 -11.7 326.376 +-360.069 -12.2556 332.867 -359.695 -11.67 333.383 -359.721 -11.1756 325.868 +-360.073 -10.7765 325.348 -360.32 -10.5 333.906 -359.911 -11.1663 333.853 +-359.641 -10.8249 334.237 -360.431 -9.72264 321.74 -360.348 -9.66251 323.657 +-359.824 -9.82002 324.159 -359.64 -10.2985 324.863 -360.349 -9.50671 333.861 +-359.725 -10.0063 334.296 -360.263 -8.6764 318.756 -360.261 -8.82024 319.52 +-359.694 -8.71686 320.325 -359.883 -9.11535 320.861 -359.966 -9.25295 321.96 +-359.592 -9.12598 323.016 -359.692 -9.40162 323.716 -360.343 -8.4981 333.836 +-359.703 -8.96719 334.235 -359.763 -8.22372 319.1 -359.939 -7.92359 318.456 +-359.665 -8.31407 319.858 -360.372 -7.52718 333.836 -359.708 -7.98155 334.33 +-360.285 -6.5673 317.652 -359.892 -6.99174 318.279 -360.248 -6.69226 333.762 +-359.72 -6.84263 334.337 -359.716 -6.14945 317.058 -359.971 -5.61379 316.469 +-360.277 -5.79838 316.781 -359.566 -6.41322 317.976 -360.424 -5.76093 331.803 +-359.9 -5.72065 333.405 -359.718 -6.20413 333.878 -359.858 -4.8536 316.356 +-360.403 -4.51178 329.909 -360.246 -4.66821 330.704 -359.951 -4.88237 331.303 +-359.949 -5.25636 332.008 -359.701 -5.1686 332.809 -359.765 -3.74131 316.376 +-360.356 -3.5627 329.946 -360.154 -3.77561 330.734 -359.742 -3.99223 331.33 +-359.872 -2.68169 316.311 -360.417 -2.63201 329.841 -360.154 -2.70619 330.557 +-359.787 -3.04889 331.295 -359.758 -1.78794 316.345 -360.316 -1.62048 329.915 +-359.727 -2.33391 330.962 -359.857 -1.80892 330.446 -359.81 -0.88522 316.243 +-360.218 -0.762126 329.816 -359.777 -0.990195 330.259 -359.6 -0.313021 315.823 +-360.018 0.273342 315.376 -360.315 0.512209 328.794 -359.973 0.133502 329.303 +-359.821 -0.319048 329.878 -359.775 1.18612 315.264 -360.297 1.46157 328.803 +-359.772 1.01869 329.301 -359.803 2.25346 315.301 -360.184 2.31973 328.747 +-359.639 2.18563 329.305 -359.702 3.15798 315.33 -360.022 3.09744 328.499 +-359.653 2.8281 328.876 -360.402 4.24852 315.769 -359.833 4.30989 315.313 +-360.236 4.40388 327.754 -359.953 3.98531 328.287 -360.272 5.13691 315.91 +-359.807 5.24364 315.368 -359.881 5.21059 327.3 -359.511 4.69036 327.935 +-359.801 5.79349 315.808 -359.807 6.25094 316.336 -360.311 6.275 316.843 +-359.96 6.21436 326.518 -359.657 5.86211 326.864 -359.795 6.73081 316.833 +-359.79 7.23827 317.319 -360.302 7.24922 317.837 -360.299 7.54367 325.839 +-359.896 6.98648 326.319 -359.799 7.74512 317.817 -359.802 8.25953 318.314 +-360.273 8.21061 318.716 -360.273 8.5285 325.695 -359.773 8.03045 326.361 +-359.799 8.67741 318.874 -359.98 8.89292 319.966 -359.74 9.20517 320.293 +-360.28 9.22395 320.815 -360.34 9.15854 325.005 -360.087 9.28996 325.635 +-359.819 8.98533 326.329 -359.784 9.703 320.808 -359.755 10.3564 321.367 +-360.203 10.1804 321.74 -360.336 10.27 322.61 -360.228 10.2687 323.576 +-359.966 10.0965 324.27 -359.857 9.76583 325.006 -359.741 9.62388 325.935 +-359.819 10.7034 322.082 -359.776 10.7591 323.038 -359.666 10.6107 323.884 +-359.747 14.8663 329.804 -359.868 15.3792 329.282 -360.205 15.4115 329.993 +-360.168 15.4089 330.867 -359.68 14.7035 330.792 -359.717 15.2183 331.384 +-359.717 15.877 327.793 -359.796 16.507 327.412 -360.146 16.3354 328.099 +-360.214 16.065 328.993 -359.787 15.7007 328.569 -360.452 16.3299 329.832 +-360.37 16.0234 330.644 -359.934 16.0627 331.357 -360.205 17.2294 327.694 +-359.865 17.302 327.208 -360.442 17.0647 328.761 -360.337 17.2614 331.812 +-359.806 16.7701 331.832 -359.787 17.2457 332.357 -359.724 17.8146 307.777 +-359.842 18.3362 307.257 -360.314 18.3327 307.906 -360.315 18.3017 309.061 +-359.812 17.6597 308.619 -360.148 18.4237 309.979 -359.827 17.7157 309.527 +-359.758 18.061 310.221 -359.724 17.8237 326.719 -359.806 18.5245 326.375 +-360.164 18.1962 327.09 -360.407 18.1026 327.947 -360.242 18.5213 332.891 +-360.028 17.774 332.714 -359.586 18.0635 333.243 -359.772 18.7389 306.799 +-359.878 19.4347 306.37 -360.248 19.0193 306.947 -360.42 19.2809 309.938 +-359.871 18.6766 310.672 -360.138 19.3543 310.875 -359.704 19.1645 311.263 +-360.206 19.2387 326.932 -359.79 19.4626 326.326 -360.402 19.052 327.704 +-360.42 19.3239 332.961 -359.817 19.1731 333.297 -360.04 20.4085 306.139 +-360.414 20.1393 310.898 -359.828 19.6653 311.786 -360.276 20.2818 311.958 +-360.201 20.3144 313.039 -359.768 19.7644 312.642 -360.178 20.4118 313.992 +-359.792 19.7775 313.582 -359.735 20.0901 314.284 -359.774 19.8795 323.674 +-359.83 20.4545 323.363 -360.143 20.3824 324.026 -359.767 20.07 324.247 +-360.165 20.1097 326.863 -359.729 20.5099 326.365 -360.439 20.1048 327.673 +-359.833 20.2916 333.37 -359.718 20.6941 305.734 -359.587 21.3918 305.463 +-360.15 21.1009 305.882 -360.439 21.2692 312.902 -360.326 21.1884 313.792 +-359.886 20.6619 314.709 -360.207 21.3165 314.881 -359.73 21.1208 315.288 +-360.243 21.1747 323.919 -359.8 21.4269 323.321 -359.872 20.6783 324.694 +-360.234 21.3256 324.83 -359.669 21.1849 325.286 -360.27 21.1828 326.858 +-359.708 21.2355 326.294 -359.832 21.2976 333.357 -359.961 21.6812 305.873 +-359.797 22.3905 306.218 -360.424 22.2883 313.913 -360.323 22.22 314.814 +-359.885 21.7012 315.739 -360.176 22.3123 315.751 -359.728 22.2054 316.223 +-360.215 22.2191 323.929 -359.786 22.4558 323.335 -360.417 22.0864 324.62 +-360.06 21.6521 325.855 -360.478 22.2603 326.822 -359.824 22.2907 333.355 +-359.893 23.3544 306.406 -360.37 23.3317 314.831 -360.233 23.2927 315.897 +-359.799 22.9851 316.401 -360.189 23.2381 323.948 -359.781 23.4619 323.351 +-360.436 23.0208 324.722 -360.493 23.1254 326.897 -359.824 23.2858 333.36 +-359.808 24.1497 306.291 -360.439 24.3916 315.076 -360.307 24.2341 315.856 +-359.878 23.7007 316.722 -360.168 24.3202 316.769 -359.726 24.1939 317.218 +-360.177 24.2414 323.968 -359.773 24.478 323.385 -360.404 24.0192 324.748 +-359.823 24.2921 333.376 -359.586 24.7569 305.839 -360.008 25.3053 305.391 +-360.371 25.3436 315.713 -360.222 25.261 316.763 -359.807 25.0392 317.36 +-360.177 25.2659 323.75 -359.851 25.3367 323.248 -360.426 25.0947 324.672 +-359.835 25.322 333.403 -359.773 26.2063 305.292 -360.457 26.3134 315.083 +-360.29 26.3558 315.98 -360.208 26.2637 316.751 -359.805 26.0485 317.345 +-359.747 25.8639 321.76 -359.826 26.4874 321.329 -360.187 26.3337 322.053 +-360.215 26.2248 323.061 -359.798 25.6986 322.59 -360.394 26.0966 323.991 +-359.888 26.2233 333.222 -359.836 27.2662 305.309 -360.391 27.3317 315.87 +-360.207 27.2587 316.769 -359.802 27.0455 317.351 -360.185 27.1568 321.867 +-359.732 27.2795 321.415 -360.332 27.2611 322.782 -360.401 27.0438 323.693 +-359.871 27.2727 332.416 -359.733 26.6692 332.769 -359.859 28.3079 305.279 +-360.416 28.4096 315.712 -360.172 28.3098 316.579 -359.799 27.9714 317.292 +-359.875 27.7856 321.935 -359.793 28.5128 322.268 -360.233 28.2055 322.761 +-360.41 28.0413 323.711 -359.771 28.256 332.34 -359.64 29.0672 305.449 +-360.436 29.3793 315.035 -360.199 29.2536 315.957 -359.722 28.6748 316.946 +-359.869 29.1845 316.455 -360.158 29.1921 322.92 -359.73 29.2869 322.441 +-360.403 29.1704 323.834 -359.816 29.2768 332.349 -360.051 29.7495 305.82 +-359.738 30.2033 306.27 -360.401 30.3913 314.663 -360.158 30.2954 315.537 +-359.788 29.9575 316.239 -359.877 29.7982 322.924 -359.717 30.3798 323.374 +-360.164 30.1672 323.764 -360.452 30.1155 324.612 -359.824 30.2787 332.303 +-359.829 31.2698 306.339 -360.437 31.3821 314.006 -360.193 31.2516 314.935 +-359.713 30.6764 315.921 -359.865 31.1816 315.447 -359.83 30.8296 323.956 +-359.665 31.3813 324.405 -360.268 31.0643 324.787 -359.699 31.2521 332.373 +-359.829 32.2703 306.293 -360.442 32.4102 313.814 -360.159 32.3277 314.531 +-359.789 31.9654 315.262 -359.904 31.7834 325.058 -359.734 32.4941 325.269 +-360.271 32.2011 325.681 -360.278 32.501 332.863 -359.87 31.8573 332.766 +-359.607 32.2231 333.187 -359.821 33.2418 306.291 -360.234 33.1084 313.948 +-359.711 32.6862 314.954 -359.846 33.1544 314.378 -360.291 33.0291 325.888 +-359.678 33.2959 325.438 -360.334 33.4817 332.849 -359.732 33.0372 333.275 +-359.831 34.2461 306.315 -360.296 34.2669 312.818 -359.987 34.1109 313.258 +-359.818 33.6739 313.87 -359.931 33.7918 325.994 -359.672 34.3574 326.338 +-360.3 34.2567 326.783 -360.331 34.4936 332.836 -359.735 34.0537 333.229 +-359.948 35.3027 306.169 -360.291 35.2804 311.803 -359.98 35.0904 312.26 +-359.792 34.6482 312.878 -359.924 34.7689 327.001 -359.682 35.3375 327.378 +-360.172 35.2465 327.85 -360.451 35.3017 328.755 -360.358 35.4913 332.838 +-359.714 35.0319 333.332 -359.723 35.6655 305.704 -359.553 36.3421 305.406 +-360.248 36.2115 305.933 -360.479 36.466 309.011 -360.393 36.2427 309.659 +-360.23 36.4144 310.558 -359.997 36.0982 311.305 -359.8 35.6526 311.892 +-359.835 35.7679 327.959 -359.806 36.513 328.293 -360.204 36.1593 328.774 +-360.42 36.1721 329.731 -360.281 36.1922 332.836 -359.689 36.2134 333.349 +-359.976 36.8127 305.968 -359.763 37.2356 306.248 -360.317 37.2635 306.816 +-360.2 37.001 309.818 -359.674 36.7087 310.993 -359.808 37.1639 310.403 +-360.178 37.208 328.956 -359.833 37.1771 328.424 -360.33 37.274 329.872 +-360.406 37.2312 330.826 -360.32 37.3704 331.754 -360.044 37.1447 332.352 +-359.832 36.7688 332.842 -359.801 37.7321 306.783 -359.759 38.3511 307.364 +-360.212 38.1493 307.744 -360.247 38.4218 308.842 -360.035 38.1154 309.38 +-359.81 37.6892 309.867 -359.741 37.7673 328.747 -359.859 38.2746 329.351 +-360.236 38.2181 329.831 -360.362 38.1973 330.777 -360.192 38.3334 331.609 +-359.646 38.1505 332.366 -359.814 38.6541 308.097 -359.807 38.7947 308.766 +-359.866 39.2646 309.391 -360.126 39.1435 309.742 -359.908 38.7084 309.691 +-359.566 39.2193 310.2 -359.771 38.7734 329.771 -359.802 39.4422 330.42 +-360.187 39.0467 330.796 -359.687 38.8051 331.945 -359.918 39.1642 331.453 +-359.765 39.7285 309.97 -359.79 39.6163 330.99 -358.756 -14.1375 330.014 +-359.073 -13.8113 329.329 -358.869 -14.2722 330.863 -359.06 -14.2275 331.81 +-358.725 -13.5566 332.272 -358.631 -13.1835 327.96 -359.043 -12.8257 327.411 +-358.743 -13.412 328.811 -358.586 -12.6285 332.43 -359.297 -11.7976 326.778 +-358.762 -11.8648 327.266 -359.034 -12.2188 332.817 -358.792 -11.7844 333.248 +-359.347 -10.736 325.693 -358.797 -10.7918 326.345 -358.788 -11.2688 326.798 +-358.844 -11.2967 333.727 -358.74 -10.5116 334.291 -358.753 -10.1463 325.079 +-358.999 -9.79957 324.406 -358.788 -10.3222 325.758 -358.924 -9.62675 334.398 +-359.327 -8.65747 321.014 -359.407 -8.76775 321.676 -359.23 -8.59351 322.556 +-358.877 -8.84417 323.298 -358.834 -9.29338 323.863 -358.741 -8.85972 334.507 +-359.446 -7.51319 318.832 -359.228 -7.61489 319.563 -358.993 -7.97264 320.301 +-358.927 -8.17178 320.896 -359.091 -8.01832 321.741 -358.717 -7.58771 321.401 +-358.706 -8.30821 323.005 -358.869 -7.81936 322.423 -359.341 -7.4742 334.864 +-358.975 -8.20227 334.801 -358.646 -7.95499 335.232 -359.287 -6.81928 318.828 +-358.699 -7.346 319.958 -358.636 -6.70547 319.398 -358.751 -7.38006 321.977 +-359.337 -6.81194 334.874 -358.694 -6.88254 335.23 -359.28 -5.59744 316.746 +-358.857 -5.88524 317.271 -358.874 -6.1664 317.861 -358.795 -6.2682 318.706 +-359.284 -5.75354 333.804 -358.772 -5.75232 334.323 -358.853 -6.22455 334.859 +-359.282 -4.61438 316.783 -358.762 -5.05991 317.304 -359.387 -4.50979 331.921 +-359.252 -4.64423 332.881 -358.955 -4.92239 333.388 -358.72 -5.24636 333.811 +-359.194 -3.74508 316.737 -358.685 -3.83877 317.319 -359.38 -3.55391 331.959 +-359.276 -3.93067 332.722 -358.83 -4.33242 333.75 -359.135 -3.67098 333.599 +-358.479 -3.7989 334.175 -358.707 -3.22751 316.875 -358.932 -2.78433 316.43 +-359.398 -2.53049 331.816 -359.225 -2.85004 332.87 -358.842 -3.23805 333.866 +-358.915 -2.8006 333.499 -358.897 -1.65605 316.336 -359.424 -1.63086 331.031 +-359.307 -1.78524 331.874 -359.173 -1.70536 332.583 -358.805 -2.0139 333.29 +-358.706 -0.755291 316.252 -359.368 -0.572587 330.668 -359.178 -0.806831 331.683 +-358.726 -1.31036 332.975 -358.871 -0.883586 332.415 -358.757 -0.14507 315.903 +-358.906 0.357047 315.433 -359.424 0.320681 329.938 -359.142 0.303027 330.709 +-358.73 -0.333012 331.947 -358.858 0.0962711 331.359 -358.859 1.32424 315.288 +-359.294 1.37882 329.896 -358.712 0.662493 330.903 -358.845 1.18932 330.409 +-358.757 2.23268 315.33 -359.217 2.23504 329.81 -358.775 2.00967 330.249 +-358.889 3.30998 315.28 -359.337 3.55689 328.829 -358.993 3.15828 329.317 +-358.834 2.67792 329.879 -358.621 3.78062 314.714 -358.702 4.49614 314.35 +-359.251 4.1855 314.899 -359.305 4.27092 328.7 -358.621 4.19174 329.334 +-359.329 5.08159 314.88 -358.712 5.16345 314.456 -358.938 5.18817 327.44 +-358.767 4.88599 327.969 -358.8 4.72755 328.715 -358.793 5.79364 314.828 +-358.782 6.21958 315.301 -359.292 6.23488 315.827 -359.273 6.50519 326.788 +-358.846 5.95829 327.254 -358.765 6.68895 315.814 -358.687 7.3032 316.37 +-359.275 7.20919 316.809 -359.249 7.45968 326.82 -358.72 6.98824 327.297 +-358.911 7.75385 317.006 -358.654 8.32945 317.345 -359.265 8.23522 317.78 +-359.311 8.46725 326.874 -358.78 8.00289 327.336 -358.922 8.78648 317.993 +-358.673 9.37108 318.361 -359.134 9.17425 318.699 -359.343 9.26156 319.645 +-359.345 9.47458 326.75 -358.744 9.14792 327.344 -358.8 9.92529 316.699 +-358.817 10.3879 316.496 -359.061 10.2431 316.822 -358.779 10.1014 317.166 +-358.723 9.66571 319.075 -358.906 9.7917 319.972 -358.749 10.4575 320.253 +-359.295 10.2966 320.804 -359.399 10.5082 324.769 -359.414 10.1085 325.856 +-359.19 10.3605 326.646 -358.7 10.2302 327.194 -358.797 10.5639 316.963 +-359.154 11.1577 320.911 -358.722 11.2835 320.461 -359.425 11.2032 321.761 +-359.413 11.3859 322.824 -359.374 11.2356 323.911 -359.333 11.342 324.951 +-359.236 11.1069 325.835 -358.855 10.6929 326.837 -358.762 11.2651 326.429 +-358.898 11.8303 320.953 -358.763 12.4288 321.402 -359.184 12.0648 321.654 +-359.224 12.1869 322.607 -359.201 12.156 323.595 -359.235 12.1218 324.591 +-359.185 12.3468 325.83 -358.752 12.1407 326.275 -358.831 12.7339 322.11 +-358.813 12.7739 323.055 -358.799 12.7605 324.036 -358.808 12.7663 325.038 +-358.612 12.7742 325.88 -358.764 13.1708 326.224 -359.249 13.3078 326.882 +-358.762 12.7057 326.766 -359.246 13.3409 328.058 -358.657 12.7326 327.645 +-359.257 13.3533 329.046 -358.859 12.7379 328.625 -359.221 13.469 329.986 +-358.7 12.7653 329.741 -358.74 13.2038 330.371 -358.54 13.8336 317.843 +-358.91 14.2767 317.265 -359.246 14.3158 317.924 -359.337 14.2679 319.013 +-358.791 13.7717 318.871 -359.243 14.4167 319.79 -358.79 13.74 319.876 +-358.642 14.1298 320.299 -358.728 13.8158 325.728 -358.887 14.3497 325.25 +-359.226 14.2603 325.96 -359.325 14.374 326.94 -359.308 14.2559 327.832 +-359.313 14.1148 328.691 -359.436 14.2373 329.747 -358.693 13.6444 330.88 +-359.292 14.0943 330.652 -358.811 14.2188 331.394 -358.743 14.7468 316.708 +-358.861 15.3379 316.256 -359.216 15.1934 316.939 -359.341 15.2938 317.876 +-359.369 15.1693 318.755 -359.298 15.2736 319.828 -358.695 14.9696 320.301 +-358.738 14.7928 324.665 -358.818 15.4952 324.323 -359.2 15.1647 325.039 +-359.317 15.3838 325.899 -359.296 15.1205 326.662 -359.36 15.2676 327.752 +-359.417 14.9846 328.902 -359.26 15.5356 331.873 -359.044 14.7755 331.738 +-358.586 15.0923 332.21 -358.711 15.8246 315.704 -358.852 16.3939 315.254 +-359.199 16.2424 315.943 -359.329 16.3161 316.887 -359.313 16.201 317.77 +-359.305 16.2149 318.787 -359.333 16.2219 319.812 -358.909 15.6936 320.663 +-359.204 16.3386 320.882 -358.746 16.1205 321.242 -359.225 16.2239 324.886 +-358.795 16.4398 324.302 -359.306 16.2766 325.796 -359.367 16.0288 326.888 +-359.378 16.2098 331.855 -358.738 16.0589 332.357 -358.842 16.7668 306.846 +-358.782 17.3679 306.367 -359.122 17.198 306.991 -359.315 17.2869 307.926 +-358.848 16.6802 307.565 -359.323 17.1902 309.048 -358.734 16.7225 308.729 +-359.317 17.3265 310.032 -358.683 16.8406 309.828 -359.211 17.2852 310.99 +-358.688 16.8144 310.82 -359.234 17.288 311.995 -358.839 16.8178 311.68 +-359.246 17.3111 313.012 -358.697 16.739 312.578 -359.231 17.3307 314.012 +-358.831 16.7584 313.586 -359.233 17.1751 315.017 -358.799 16.7194 314.539 +-359.33 17.3368 315.886 -359.32 17.2067 316.766 -359.303 17.2268 317.796 +-359.286 17.2419 318.823 -359.297 17.2371 319.831 -359.336 17.2275 320.841 +-358.879 16.6705 321.698 -359.2 17.3382 321.903 -358.726 17.1263 322.269 +-359.201 17.2317 324.939 -358.757 17.4657 324.357 -359.307 17.152 325.697 +-359.42 17.1355 326.679 -359.338 17.351 332.974 -358.981 16.7926 332.722 +-358.693 17.1742 333.276 -359.366 18.165 306.812 -358.88 18.3596 306.156 +-359.432 18.2893 310.881 -359.277 18.2307 311.776 -359.302 18.2128 312.786 +-359.317 18.214 313.779 -359.321 18.2204 314.773 -359.321 18.2282 315.777 +-359.307 18.2302 316.794 -359.295 18.2365 317.822 -359.29 18.2473 318.838 +-359.289 18.2468 319.837 -359.305 18.2458 320.832 -359.334 18.2562 321.84 +-358.861 17.7268 322.73 -359.176 18.3841 322.91 -358.69 18.1993 323.317 +-359.182 18.3373 324.841 -358.824 18.3391 324.237 -359.386 18.0228 325.895 +-358.835 18.2595 333.38 -358.677 18.7409 305.717 -358.702 19.3393 305.45 +-359.186 19.1274 305.968 -359.409 19.191 311.901 -359.361 19.1523 312.845 +-359.361 19.2858 313.884 -359.267 19.2401 314.802 -359.289 19.2394 315.804 +-359.299 19.2445 316.827 -359.294 19.2465 317.838 -359.295 19.245 318.838 +-359.297 19.2417 319.832 -359.285 19.2672 320.833 -359.29 19.283 321.836 +-359.312 19.1433 322.708 -358.829 18.6093 323.752 -359.292 19.2627 323.908 +-359.244 19.4033 324.964 -359.383 19.0142 325.709 -358.821 19.2789 333.328 +-359.373 19.994 305.797 -358.907 20.3311 305.367 -359.397 20.2995 314.937 +-359.249 20.2446 315.823 -359.283 20.2406 316.829 -359.299 20.248 317.839 +-359.299 20.2432 318.833 -359.301 20.2373 319.828 -359.286 20.2679 320.836 +-359.261 20.2991 321.848 -359.386 20.016 322.879 -359.38 20.344 324.937 +-359.335 20.0589 325.729 -358.795 20.2533 333.304 -358.745 21.1776 305.246 +-359.398 21.3087 315.939 -359.257 21.2355 316.818 -359.292 21.2378 317.83 +-359.303 21.2418 318.83 -359.304 21.2356 319.824 -359.29 21.2646 320.838 +-359.255 21.3025 321.86 -359.372 21.0389 322.735 -359.41 21.0546 325.81 +-358.781 21.2278 333.301 -359.375 22.1664 305.717 -358.724 22.0298 305.455 +-359.344 22.3378 316.819 -359.27 22.2228 317.812 -359.303 22.2361 318.828 +-359.307 22.2352 319.822 -359.292 22.2622 320.833 -359.259 22.299 321.866 +-359.363 22.0476 322.751 -358.772 22.2267 333.31 -358.884 22.5928 305.892 +-358.795 23.1407 306.328 -359.424 23.4088 317.043 -359.25 23.2143 317.804 +-359.29 23.226 318.824 -359.306 23.2452 319.834 -359.285 23.2686 320.842 +-359.254 23.2992 321.865 -359.366 23.0481 322.753 -358.77 23.2333 333.323 +-358.88 24.1825 306.229 -359.352 24.315 317.814 -359.273 24.2254 318.819 +-359.293 24.2713 319.848 -359.274 24.2921 320.871 -359.242 24.3073 321.899 +-359.354 24.0554 322.779 -358.786 24.2565 333.361 -358.741 24.6368 305.756 +-358.856 25.2168 305.386 -359.375 25.4443 317.913 -359.259 25.2237 318.805 +-359.275 25.2767 319.844 -359.271 25.1692 320.743 -359.354 25.2911 321.802 +-359.417 25.0053 322.808 -358.876 25.3226 333.284 -358.759 26.2217 305.318 +-359.37 26.4455 317.906 -359.253 26.2048 318.79 -359.264 26.2591 319.827 +-359.373 26.0352 320.894 -358.947 26.445 332.452 -358.546 25.7827 332.825 +-359.248 26.128 332.902 -358.831 27.2671 305.317 -359.375 27.4345 317.919 +-359.26 27.1905 318.798 -359.263 27.2509 319.827 -359.36 27.1682 320.824 +-358.754 27.2182 332.288 -358.727 28.2674 305.386 -359.354 28.4298 317.742 +-359.267 28.1853 318.802 -359.276 28.2393 319.833 -359.252 28.2763 320.855 +-359.424 28.078 321.614 -358.778 28.2395 332.319 -359.33 29.3698 305.906 +-358.845 28.849 305.759 -358.7 29.3042 306.217 -359.406 29.3465 316.966 +-359.256 29.2946 317.918 -359.274 29.1811 318.802 -359.291 29.2273 319.826 +-359.27 29.2687 320.849 -359.346 29.1512 321.843 -358.816 29.2494 332.301 +-358.783 30.218 306.39 -359.359 30.4443 316.754 -359.276 30.1806 317.818 +-359.303 30.2066 318.844 -359.318 30.2434 319.853 -359.3 30.2594 320.845 +-359.261 30.2528 321.845 -359.404 30.1817 322.724 -358.753 30.1803 332.368 +-358.809 31.2462 306.343 -359.415 31.3686 316.004 -359.282 31.3487 316.987 +-359.298 31.0809 317.729 -359.243 31.3522 318.75 -359.353 31.2757 319.873 +-359.342 31.2754 320.86 -359.307 31.2589 321.854 -359.258 31.2568 322.856 +-359.376 31.199 323.744 -358.762 31.0328 332.474 -358.796 32.242 306.32 +-359.38 32.4546 315.802 -359.295 32.0939 316.726 -359.187 32.2025 317.713 +-358.863 31.8858 318.976 -358.851 32.0682 318.43 -358.724 32.3269 319.392 +-359.168 32.0273 319.677 -359.234 32.1654 320.614 -359.24 32.2472 321.734 +-359.32 32.2722 322.863 -359.259 32.2688 323.878 -359.383 32.0757 324.631 +-359.043 31.6864 332.824 -358.759 32.2245 333.288 -358.79 33.2369 306.348 +-359.404 33.4004 314.89 -359.322 33.2107 315.876 -359.212 33.2146 316.692 +-358.709 32.6605 317.974 -358.882 33.1509 317.408 -358.809 32.6927 320.104 +-358.799 32.7403 321.041 -358.862 32.8329 321.884 -358.724 33.3449 322.398 +-359.202 33.1577 322.796 -359.334 33.295 323.878 -359.352 33.1666 324.857 +-358.843 33.2994 333.348 -358.86 34.2885 306.273 -359.371 34.4093 313.88 +-359.315 34.213 314.858 -359.233 34.1937 315.689 -358.762 33.7263 316.927 +-358.689 34.3276 316.393 -358.901 33.8412 322.986 -358.837 34.2982 323.361 +-359.192 34.0817 323.702 -359.222 34.2676 324.781 -359.393 34.1898 325.723 +-358.778 34.1569 333.461 -358.55 34.7796 305.842 -359.017 35.4316 305.427 +-359.271 35.0842 305.891 -359.364 35.3325 312.802 -359.185 35.3668 313.695 +-359.2 35.0329 314.724 -358.709 34.8432 315.881 -358.691 35.3174 315.445 +-358.683 34.6681 323.891 -358.877 34.8628 324.946 -358.823 35.5025 325.297 +-359.206 35.2039 325.827 -359.384 35.1891 326.735 -359.317 35.5773 333.892 +-358.98 34.8094 333.795 -358.617 35.0767 334.253 -358.712 36.1088 305.219 +-359.394 36.3336 311.807 -359.164 36.2549 312.736 -358.834 35.7682 314.143 +-358.883 36.0655 313.408 -358.712 35.7456 314.869 -359.186 36.2328 325.977 +-358.844 36.1716 325.413 -359.306 36.2904 326.889 -359.427 36.0827 327.64 +-359.288 36.2704 333.895 -358.81 36.0439 334.194 -359.397 37.1954 305.777 +-358.666 37.0832 305.37 -359.402 37.3688 310.767 -359.202 37.2692 311.713 +-358.752 36.6582 312.926 -358.709 37.2814 312.377 -358.755 36.7875 325.734 +-358.864 37.3151 326.346 -359.193 37.102 326.706 -359.349 37.1602 327.782 +-359.374 37.5481 332.855 -358.85 37.1013 333.284 -358.831 36.602 333.746 +-358.91 37.7492 305.986 -358.724 38.356 306.286 -359.33 38.3592 306.793 +-359.426 38.3231 310.079 -359.175 38.0312 310.86 -358.752 37.7879 311.856 +-358.71 38.256 311.408 -358.707 37.7045 326.9 -358.945 37.865 328.004 +-358.76 38.206 328.332 -359.299 38.2262 328.842 -359.243 38.1904 332.869 +-358.633 38.1548 333.219 -359.168 39.0731 306.782 -358.582 39.152 306.438 +-359.329 39.2259 307.704 -359.483 39.3607 308.823 -358.857 38.7667 310.714 +-358.91 39.3053 310.499 -358.814 38.6976 328.853 -358.739 39.4861 329.312 +-359.379 39.3412 329.842 -359.376 39.5389 331.71 -359.029 39.1125 332.361 +-358.83 38.7648 332.867 -358.852 39.5935 306.977 -358.758 39.899 307.774 +-358.826 40.2837 308.397 -359.166 40.0739 308.738 -359.314 40.167 309.642 +-358.804 40.1733 310.384 -359.193 40.021 329.793 -358.65 40.4204 329.447 +-359.362 40.0612 330.699 -359.136 40.2557 331.664 -358.655 40.1673 332.375 +-358.732 40.7218 308.945 -358.727 40.8355 309.908 -358.87 40.7218 330.046 +-358.858 40.7869 331.064 -358.711 40.7663 331.884 -358.385 -13.7375 329.741 +-358.223 -13.8113 330.639 -358.287 -13.8445 331.649 -358.334 -12.5111 327.772 +-357.873 -12.8355 328.308 -357.858 -13.1896 328.864 -357.831 -13.3077 329.929 +-357.71 -13.2838 331.124 -357.856 -13.3014 331.93 -357.858 -12.7312 332.21 +-358.182 -11.7861 327.811 -357.753 -12.0307 328.288 -358.326 -11.83 332.86 +-357.793 -11.8041 332.424 -358.291 -10.7437 326.823 -357.786 -10.72 327.295 +-357.838 -11.3118 327.876 -357.906 -11.1802 333.059 -357.747 -10.7232 333.36 +-358.188 -10.8295 333.772 -358.412 -9.63011 324.722 -358.265 -9.69594 325.599 +-357.797 -9.77907 326.372 -357.785 -10.2459 326.826 -357.708 -10.356 333.748 +-358.038 -9.77708 334.346 -358.396 -8.70533 323.831 -358.175 -8.79317 324.778 +-357.687 -9.20363 325.922 -357.684 -8.748 325.438 -358.376 -8.66622 334.958 +-357.75 -9.20512 334.644 -357.747 -8.51648 335.219 -358.454 -7.48058 320.721 +-358.456 -7.60097 322.8 -358.179 -7.69387 323.742 -357.707 -8.24454 324.921 +-357.895 -7.93354 324.424 -357.995 -7.65087 335.399 -358.317 -6.90015 319.982 +-358.129 -6.76138 320.644 -358.321 -6.92195 321.686 -358.199 -6.79717 322.727 +-357.759 -7.30072 323.958 -357.917 -6.82026 323.344 -357.625 -6.80198 335.444 +-358.423 -5.56059 317.947 -358.274 -5.7007 318.731 -357.992 -5.88202 319.313 +-357.849 -6.26703 319.976 -357.803 -6.26391 321.029 -357.73 -6.2442 322.027 +-357.661 -6.37226 322.92 -358.264 -5.71439 334.845 -357.788 -5.76166 335.195 +-358.449 -4.63168 317.847 -358.167 -4.71105 318.58 -357.628 -4.83119 319.348 +-358.411 -4.61703 333.917 -357.988 -4.75161 334.308 -357.858 -5.30339 334.745 +-358.3 -3.91457 317.967 -357.684 -4.14286 318.943 -357.92 -3.83381 318.419 +-357.723 -3.96443 334.479 -358.224 -2.67338 316.748 -357.884 -2.79782 317.223 +-357.686 -3.29037 317.903 -358.439 -2.67964 334.048 -357.926 -3.27022 334.634 +-358.167 -2.55731 335.045 -357.723 -2.89206 335.283 -357.646 -2.2079 316.896 +-357.915 -1.73298 316.448 -358.389 -1.5362 333.801 -358.337 -1.91739 334.747 +-357.916 -2.30145 335.781 -358.189 -1.65916 335.625 -357.556 -1.80797 336.2 +-357.764 -0.723515 316.341 -358.41 -0.607333 332.88 -358.296 -0.769931 333.866 +-358.207 -0.811241 334.703 -357.718 -1.17665 335.949 -357.919 -0.90119 335.441 +-358.229 0.358523 315.753 -357.854 -0.000129289 316.206 -358.407 0.386331 331.848 +-358.295 0.230077 332.839 -358.18 0.21386 333.691 -357.739 -0.336698 334.969 +-357.862 0.127028 334.397 -357.507 0.711178 315.837 -357.967 1.1532 315.404 +-358.414 1.37102 330.996 -358.293 1.21983 331.838 -358.179 1.21202 332.686 +-357.721 0.694766 333.962 -357.855 1.14262 333.412 -357.741 2.23133 315.369 +-358.392 2.42753 330.675 -358.19 2.20258 331.679 -357.72 1.70681 332.974 +-357.865 2.1404 332.405 -357.875 3.16026 315.248 -358.462 3.32659 329.953 +-358.197 3.30159 330.736 -357.751 2.70739 331.95 -357.742 3.29208 331.349 +-357.818 3.89874 313.883 -357.582 4.43325 313.478 -358.303 4.12046 313.918 +-357.7 3.63419 314.635 -358.255 4.08487 329.963 -357.611 3.72308 330.718 +-357.687 4.25441 330.296 -357.922 4.63483 313.846 -358.062 5.29597 314.148 +-358.398 5.44883 327.877 -358.274 5.29836 328.713 -357.998 5.14357 329.35 +-357.771 4.79102 329.819 -357.671 5.71867 313.699 -357.58 6.39945 313.424 +-358.142 6.10203 313.967 -358.33 6.32878 314.718 -358.352 6.41213 327.933 +-358.167 6.19662 328.745 -357.792 6.01145 329.368 -357.799 6.688 314.033 +-357.914 6.7965 314.979 -357.844 7.31458 315.142 -358.269 7.3589 315.734 +-358.403 7.43647 327.973 -358.226 7.23024 328.794 -357.771 7.148 329.388 +-357.654 7.72725 314.657 -357.621 8.45258 314.525 -358.146 8.09905 315.099 +-358.3 8.31912 315.959 -358.38 8.13047 316.723 -358.478 8.3607 327.85 +-358.241 8.29548 328.582 -357.898 8.06005 329.203 -357.911 8.7765 314.907 +-357.804 9.50355 315.222 -358.209 9.03055 315.636 -358.371 9.33057 316.811 +-358.363 9.16782 317.731 -358.261 9.19989 327.858 -357.603 8.70725 328.806 +-357.731 9.29677 328.381 -358.329 10.1117 316.011 -357.819 10.4905 315.289 +-358.362 10.3828 317.754 -358.312 10.1865 318.801 -358.41 10.1982 319.626 +-357.832 10.1316 327.476 -357.503 9.84829 327.866 -358.184 11.2248 315.911 +-357.854 11.2041 315.364 -358.433 11.1642 316.861 -358.324 11.4254 317.999 +-358.281 11.2608 318.893 -358.345 11.1297 319.86 -358.279 11.2627 327.002 +-357.889 10.7752 327.683 -358.209 11.3653 328.033 -358.262 11.3677 329.064 +-357.654 10.7573 328.594 -358.185 11.4356 329.856 -357.712 10.7022 329.808 +-357.732 11.208 330.383 -357.757 11.7814 315.689 -357.739 12.421 316.376 +-358.184 12.088 316.639 -358.236 12.196 317.589 -358.222 12.1552 318.614 +-358.227 12.2334 319.786 -358.453 12.2095 320.763 -357.984 12.2528 326.381 +-357.703 11.9139 326.931 -357.775 12.2909 327.296 -358.269 12.0843 327.782 +-358.473 12.1468 328.832 -358.375 12.068 329.635 -357.931 12.0754 330.349 +-357.789 12.7549 317.089 -357.788 12.7876 318.014 -357.636 12.6988 319.052 +-357.826 12.7349 319.969 -357.651 13.3793 320.27 -358.305 13.1796 320.634 +-358.283 13.1126 321.595 -358.272 13.3341 322.739 -358.371 13.3655 323.83 +-358.379 13.2707 324.703 -358.313 13.1899 325.768 -357.981 12.7176 326.797 +-358.363 12.7239 326.406 -358.328 13.2673 330.812 -357.804 12.7732 330.832 +-357.765 13.2499 331.355 -357.642 13.8307 314.724 -357.92 14.2731 314.269 +-358.207 14.4042 314.977 -358.2 14.2384 316.013 -357.716 13.7418 315.646 +-358.422 14.2643 316.827 -357.89 13.7558 316.798 -357.738 13.6318 317.765 +-357.891 13.6305 318.672 -357.827 13.7603 319.612 -357.82 13.8362 320.994 +-357.935 14.381 320.438 -357.702 13.7197 322.076 -357.8 13.8511 322.964 +-357.822 14.3693 323.221 -358.168 14.2009 323.639 -358.378 14.0911 324.815 +-358.361 14.3664 331.975 -357.988 13.7885 331.725 -357.706 14.1913 332.231 +-357.816 14.6578 313.795 -357.793 15.2288 313.274 -358.177 15.2885 313.949 +-358.294 15.2712 314.795 -358.414 15.0777 315.808 -358.327 15.3889 321.059 +-357.844 14.7338 320.736 -358.157 15.3587 322.025 -357.697 14.7144 321.565 +-358.184 15.2185 323.091 -357.735 14.6694 322.594 -358.393 15.0934 324.001 +-357.706 15.2638 332.411 -357.595 15.848 306.963 -357.866 16.3168 306.296 +-358.369 16.3355 306.869 -358.369 16.1839 308.032 -357.764 15.7883 307.842 +-358.205 16.2939 308.886 -357.729 15.8766 308.777 -357.663 16.1749 309.397 +-357.79 15.7976 312.766 -357.847 16.2551 312.424 -358.256 16.1482 313.067 +-358.419 16.2999 313.941 -358.407 16.0115 314.868 -358.417 16.3119 321.893 +-358.251 16.2865 322.796 -358.386 16.0389 323.689 -358.33 16.3495 332.96 +-357.835 15.8822 332.788 -357.708 16.3335 333.221 -357.543 16.7769 305.766 +-357.822 17.449 305.451 -358.233 17.0522 305.918 -358.013 16.5898 309.658 +-357.873 16.6567 310.626 -357.982 16.6333 311.708 -358.375 17.359 322.939 +-358.337 17.0535 323.724 -357.811 17.2532 333.374 -358.383 17.997 305.762 +-357.855 18.3092 305.364 -358.441 18.0315 323.833 -357.816 18.2691 333.309 +-357.813 19.2496 305.29 -357.744 19.1731 333.379 -357.85 20.2737 305.275 +-357.9 20.1795 333.402 -357.733 21.2838 305.359 -357.772 21.2538 333.382 +-358.313 22.4119 305.91 -357.843 21.9047 305.741 -357.696 22.3526 306.221 +-357.867 22.3025 333.284 -357.859 23.3106 306.317 -357.723 23.19 333.302 +-357.551 23.7981 305.835 -357.953 24.4399 305.447 -358.26 24.1364 305.913 +-357.758 24.2358 333.355 -357.747 25.2001 305.285 -357.823 25.1543 333.269 +-357.781 26.2336 305.319 -358.002 26.3076 332.391 -357.602 25.7486 332.836 +-357.834 27.2804 305.295 -357.762 27.2069 332.295 -357.581 28.1748 305.389 +-357.804 28.256 332.304 -357.997 28.7265 305.837 -357.761 29.3226 306.274 +-357.721 29.254 332.387 -357.787 30.2282 306.373 -358.231 30.4922 332.828 +-357.879 29.8491 332.764 -357.601 30.2117 333.185 -357.798 31.2418 306.334 +-358.403 31.3163 332.901 -357.795 31.1479 333.291 -357.784 32.2415 306.335 +-358.455 32.4634 318.734 -357.828 32.2837 333.349 -357.794 33.2551 306.376 +-358.427 33.3451 317.743 -358.192 33.4309 318.78 -358.392 33.173 319.791 +-358.396 33.369 320.842 -358.428 33.3225 321.768 -357.71 33.268 333.392 +-357.821 34.1591 306.279 -358.37 34.153 316.956 -358.112 34.1296 317.726 +-357.86 33.8682 318.966 -357.848 34.0496 318.384 -357.726 34.3269 319.387 +-358.145 33.9932 319.672 -358.198 34.1623 320.614 -358.151 34.1422 321.604 +-358.358 34.1534 322.663 -358.364 34.3351 333.935 -357.839 33.8385 333.765 +-357.718 34.3098 334.241 -357.592 34.7575 305.846 -358.053 35.3381 305.388 +-358.284 35.2734 315.823 -357.984 35.0906 316.253 -357.863 34.7545 316.981 +-357.699 34.5748 317.864 -357.81 34.7017 320.104 -357.784 34.7695 321.046 +-357.747 34.7159 322.027 -357.917 34.8131 322.961 -357.687 35.3262 323.359 +-358.159 35.2615 323.855 -358.496 35.2121 324.638 -357.88 35.3009 334.331 +-357.828 36.272 305.251 -358.48 36.4496 313.649 -358.266 36.3854 314.551 +-358.005 36.0964 315.298 -357.793 35.6495 315.899 -357.825 35.7831 323.94 +-357.688 36.3804 324.385 -358.29 36.0242 324.769 -357.962 36.4893 333.473 +-357.548 35.802 333.843 -358.267 36.168 333.932 -357.588 37.1944 305.39 +-358.388 37.1141 312.982 -358.161 37.1329 313.75 -357.721 36.6928 314.949 +-357.683 37.3044 314.398 -357.931 36.8172 325.032 -357.768 37.2535 325.316 +-358.315 37.2798 325.833 -357.789 37.2648 333.312 -358.022 37.7565 305.789 +-357.817 38.3458 306.2 -358.367 38.218 311.798 -357.824 38.233 312.283 +-357.921 37.7501 312.979 -357.703 37.7538 313.809 -357.812 37.7663 325.839 +-357.71 38.3339 326.278 -358.213 38.2094 326.782 -358.386 38.286 327.69 +-357.679 38.1707 333.353 -357.596 39.3053 306.374 -358.356 39.4965 310.838 +-357.89 39.2149 311.323 -357.809 38.7202 311.801 -357.699 38.5858 326.882 +-357.83 38.819 327.884 -357.802 39.2232 328.374 -358.267 39.1339 328.753 +-358.386 39.5486 332.868 -357.889 39.0843 333.281 -358.085 39.8182 306.901 +-357.74 40.2475 307.253 -358.286 40.2648 307.824 -358.208 40.175 310.859 +-357.666 40.1851 311.181 -357.789 39.6297 328.757 -357.796 40.3497 329.232 +-358.24 40.2032 332.881 -357.649 40.1851 333.245 -357.801 40.7583 307.795 +-357.864 41.2633 308.308 -358.251 41.2302 308.79 -358.314 41.3196 309.604 +-358.051 41.1572 310.323 -357.795 40.7944 310.864 -358.34 41.1699 329.736 +-357.761 41.1764 329.459 -358.347 41.3128 330.558 -358.294 41.2959 331.57 +-358.051 41.1379 332.325 -357.823 40.8231 332.895 -357.637 41.6924 308.916 +-357.598 41.6064 310.038 -357.818 41.6089 329.971 -357.645 41.7888 330.946 +-357.609 41.6278 332.026 -357.438 -12.6312 328.851 -357.256 -12.6894 329.598 +-357.252 -12.7055 330.766 -357.431 -12.7692 331.696 -357.292 -11.6287 328.808 +-356.778 -12.2874 330.085 -356.724 -11.7321 329.419 -356.875 -12.194 330.92 +-357.29 -11.8916 331.807 -356.712 -11.5316 331.304 -357.387 -10.8464 328.024 +-357.142 -10.7179 328.66 -356.704 -10.775 329.19 -357.163 -10.9452 331.813 +-356.655 -10.5829 331.452 -357.363 -10.8006 332.658 -357.261 -9.75047 326.839 +-356.746 -9.75427 327.297 -356.901 -10.2205 328.007 -356.696 -10.1944 328.868 +-356.846 -10.2669 332.055 -356.94 -10.1276 333 -356.636 -9.70276 333.262 +-357.267 -9.79573 333.875 -357.3 -8.75003 325.825 -356.993 -8.91408 326.292 +-356.742 -9.26236 326.843 -356.801 -9.29382 333.951 -356.531 -8.78028 334.431 +-357.275 -8.8595 334.752 -357.372 -7.46461 324.712 -356.899 -7.76931 325.331 +-356.84 -8.37273 325.874 -356.891 -8.41322 334.964 -356.822 -7.78314 335.404 +-357.455 -6.75764 323.862 -357.135 -6.70586 324.678 -356.81 -6.9189 325.177 +-357.169 -6.64031 335.786 -356.93 -7.17159 335.827 -356.567 -6.73175 336.204 +-357.441 -5.47198 319.92 -357.412 -5.76145 320.68 -357.287 -5.61681 321.557 +-356.741 -5.69811 322.327 -356.813 -6.14846 322.862 -356.843 -6.32087 323.917 +-356.61 -6.40071 324.883 -357.03 -5.94665 335.527 -356.68 -6.17564 335.921 +-357.317 -4.80163 319.903 -357.191 -4.91314 320.681 -356.687 -5.17396 321.932 +-356.918 -4.83939 321.421 -357.359 -4.66377 334.993 -356.745 -5.28413 335.83 +-357.112 -4.58215 335.81 -356.721 -4.79506 336.371 -357.396 -3.61536 318.72 +-356.975 -3.8683 319.251 -356.744 -4.26839 319.941 -356.633 -4.36228 320.993 +-357.407 -3.58669 335.009 -357.179 -3.75526 335.693 -356.857 -4.00855 336.34 +-356.664 -3.08894 318.048 -356.896 -2.62262 317.476 -356.751 -3.38652 318.733 +-357.411 -2.78146 335.805 -356.909 -2.82673 336.338 -357.185 -1.61341 316.765 +-356.965 -1.96003 317.164 -356.781 -1.8488 336.461 -356.476 -1.27088 316.828 +-357 -0.760794 316.434 -357.363 -0.490543 335.867 -356.933 -1.01745 336.403 +-356.789 0.187221 316.248 -357.423 0.372638 334.874 -357.343 0.180157 335.882 +-356.884 -0.316492 336.751 -357.186 0.314772 336.614 -356.559 0.153966 337.184 +-356.55 0.771788 315.864 -356.964 1.34667 315.453 -357.427 1.32108 333.779 +-357.22 1.34747 334.663 -357.222 1.00608 335.731 -356.768 0.817704 336.937 +-356.73 1.30264 336.466 -356.845 2.304 315.231 -357.444 2.35887 332.769 +-357.183 2.25813 333.736 -356.858 1.77905 335.09 -356.681 2.26011 334.404 +-356.728 1.76376 335.845 -356.771 2.79574 314.86 -356.872 3.25441 314.437 +-357.233 3.17222 314.964 -357.471 3.21586 331.995 -357.227 3.31362 332.712 +-356.695 2.80972 333.9 -356.934 3.11738 333.399 -356.895 3.66256 313.812 +-356.8 4.11359 313.34 -356.931 4.19809 330.472 -356.995 3.86553 330.945 +-357.21 3.97736 331.759 -356.713 4.33843 331.388 -356.605 3.70389 332.924 +-356.775 4.2836 332.312 -357.46 5.11637 313.717 -356.929 5.21667 313.338 +-357.313 5.54502 329.834 -356.852 4.9948 330.299 -356.712 4.69234 331.919 +-356.753 6.17226 313.25 -357.327 6.47389 329.843 -356.768 6.01373 330.326 +-357.334 7.33013 313.919 -356.726 7.13574 313.404 -357.541 7.14779 314.616 +-357.242 7.32008 329.766 -356.679 7.17283 330.338 -356.743 7.72451 313.798 +-356.851 8.1284 314.229 -356.915 8.26963 329.387 -356.72 7.82527 329.878 +-357.452 9.16694 314.718 -356.807 9.16493 314.404 -357.011 9.18595 328.499 +-356.718 8.89306 328.8 -357.31 10.0486 314.779 -356.73 10.308 314.36 +-356.855 10.1998 327.523 -356.729 9.98935 327.759 -356.789 9.63348 328.902 +-357.379 10.1085 328.929 -356.918 10.2486 328.197 -357.279 10.2081 329.841 +-356.801 9.92956 329.69 -356.674 10.2543 330.338 -357.349 11.1083 314.841 +-356.565 11.2044 314.371 -356.952 10.5626 327.851 -357.253 11.5049 330.88 +-357.012 10.7936 330.749 -356.601 11.2044 331.197 -356.934 11.777 314.958 +-356.68 12.3609 315.296 -357.291 12.3665 315.792 -357.416 12.2276 326.83 +-357.375 12.1924 330.857 -356.727 12.0254 331.372 -357.296 13.1442 315.955 +-356.83 13.1482 315.357 -357.439 13.138 316.795 -357.45 13.294 317.839 +-357.326 13.3651 318.812 -356.805 13.2029 319.287 -356.997 12.8566 319.809 +-356.683 13.4123 320.207 -357.322 13.3549 331.955 -356.977 12.7843 331.716 +-356.701 13.1555 332.235 -356.685 13.8118 313.827 -356.834 14.289 313.3 +-357.407 14.2832 313.812 -356.782 13.607 314.777 -356.782 14.0559 319.517 +-357.207 14.2423 319.961 -357.425 14.293 320.957 -356.866 14.1495 320.208 +-357.386 14.2127 321.837 -357.473 14.1469 322.713 -356.787 14.1611 332.419 +-356.77 14.7006 312.844 -356.788 15.2758 312.335 -357.281 15.2463 312.822 +-356.716 14.602 319.857 -356.776 15.0325 332.489 -356.661 15.8156 306.69 +-357.11 16.1981 306.276 -356.817 15.6186 307.623 -356.836 15.6342 308.754 +-357.308 16.2668 310.132 -356.814 15.8506 309.802 -357.254 16.3056 311.084 +-356.743 15.9258 310.725 -357.28 16.1015 311.958 -356.633 15.7792 311.557 +-357.03 15.6942 332.819 -356.718 16.1897 333.265 -356.702 16.5682 305.716 +-356.862 17.2206 305.355 -356.794 17.2709 333.308 -356.771 18.2165 305.299 +-356.741 18.1492 333.426 -356.807 19.2426 305.318 -357.279 19.4837 333.841 +-356.944 18.7919 333.762 -356.621 19.0759 334.223 -356.827 20.2731 305.287 +-357.349 20.42 333.84 -356.769 20.0426 334.284 -356.634 21.0312 305.454 +-357.198 21.2663 333.764 -356.701 21.1426 334.239 -357.052 21.6795 305.833 +-356.735 22.1762 306.322 -356.915 22.2254 333.417 -356.721 21.824 333.916 +-356.841 23.1573 306.299 -356.832 23.3337 333.309 -356.601 23.7585 305.831 +-357.001 24.3171 305.391 -356.749 24.2571 333.361 -356.761 25.2172 305.291 +-356.874 25.1786 333.21 -356.809 26.2629 305.294 -356.879 26.2262 332.388 +-356.756 25.637 332.768 -356.816 27.2848 305.257 -356.784 27.2487 332.285 +-356.608 28.0647 305.431 -356.805 28.283 332.273 -357.043 28.7087 305.828 +-356.726 29.1992 306.274 -356.624 29.0549 332.45 -356.802 30.2446 306.35 +-357.076 29.6898 332.849 -356.75 30.1953 333.3 -356.796 31.2441 306.323 +-356.819 31.2559 333.367 -356.783 32.2481 306.344 -356.83 32.2788 333.293 +-356.812 33.2753 306.387 -356.581 33.1632 333.379 -356.889 34.1796 306.217 +-357.467 34.4636 318.733 -356.98 33.7254 333.831 -356.775 34.3467 334.308 +-356.735 34.632 305.764 -356.886 35.2422 305.378 -357.394 35.4576 316.67 +-357.269 35.302 317.798 -357.298 35.3459 318.985 -357.392 35.1904 319.793 +-357.41 35.3718 320.829 -357.378 35.3399 321.819 -357.414 35.3053 322.754 +-356.826 35.1488 334.306 -356.807 36.2913 305.259 -357.4 36.352 315.781 +-357.147 36.2668 316.723 -356.855 35.8796 317.965 -356.851 36.0398 317.393 +-356.741 36.3584 318.405 -357.149 36.0027 318.685 -357.223 36.1676 319.64 +-357.208 36.1691 320.627 -357.187 36.133 321.621 -357.188 36.2197 322.75 +-357.403 36.1763 323.731 -357.009 36.3313 333.392 -356.597 35.746 333.83 +-356.577 37.1996 305.394 -357.38 37.1362 314.987 -357.173 37.1551 315.736 +-356.744 36.6441 316.929 -356.702 37.2999 316.377 -356.836 36.7238 319.123 +-356.838 36.804 320.066 -356.816 36.7555 321.059 -356.807 36.7248 322.07 +-356.875 36.8705 322.92 -356.738 37.3963 323.418 -357.12 37.0289 323.684 +-357.343 37.1761 324.663 -356.78 37.2485 333.302 -357.011 37.7651 305.825 +-356.807 38.3561 306.238 -357.378 38.2502 312.88 -357.247 38.2614 313.545 +-356.797 38.2584 314.335 -356.927 37.7669 314.976 -356.726 37.7677 315.791 +-356.769 37.7136 324.112 -356.93 37.8659 324.992 -356.753 38.2367 325.309 +-357.234 38.1849 325.704 -356.815 38.2953 333.349 -356.719 39.1343 306.324 +-357.196 39.35 311.74 -356.754 39.2221 312.292 -356.866 38.755 313.008 +-356.639 38.7714 313.854 -356.778 38.6667 325.885 -356.975 38.8927 326.971 +-356.734 39.2186 327.305 -357.262 39.2263 327.841 -356.836 39.3143 333.367 +-357.388 40.2049 306.792 -356.704 40.0806 306.405 -356.994 40.0625 311.491 +-356.616 39.7859 311.858 -356.783 39.7384 327.866 -356.805 40.2741 328.396 +-357.261 40.1981 328.787 -356.703 40.1973 333.384 -356.912 40.7317 306.963 +-356.513 41.3292 307.31 -357.329 41.2328 307.766 -357.361 41.3608 310.734 +-356.76 41.153 311.352 -356.755 40.6637 328.732 -356.99 41.1423 329.278 +-357.387 41.4123 332.757 -356.876 41.064 333.258 -357.009 41.8074 307.999 +-356.83 42.2896 308.297 -357.135 42.2236 308.702 -356.959 42.1912 309.319 +-356.933 41.8837 309.956 -356.797 41.7272 310.769 -356.931 41.7899 329.785 +-356.727 42.3599 330.274 -357.187 42.2257 330.71 -357.025 42.187 331.272 +-356.797 41.8826 331.943 -356.774 41.6351 332.767 -356.615 42.5811 308.727 +-356.591 42.5745 330.961 -356.249 -11.678 329.774 -356.344 -11.8518 330.548 +-355.718 -11.2618 330.094 -355.966 -10.9158 329.472 -355.918 -11.3662 330.866 +-355.744 -10.7358 331.193 -356.335 -9.73641 327.867 -356.218 -9.70303 328.553 +-355.914 -9.98371 329.311 -356.33 -9.83172 331.737 -355.738 -9.84937 331.423 +-356.337 -9.71557 332.496 -356.314 -8.48577 326.689 -355.953 -8.88009 327.314 +-355.834 -9.2431 327.991 -355.69 -9.34421 328.917 -355.771 -9.39987 331.947 +-355.801 -9.2552 332.918 -355.979 -9.08462 333.848 -355.843 -8.78111 334.269 +-356.297 -7.76978 325.924 -356.134 -7.77357 326.635 -355.627 -7.83912 327.328 +-355.836 -8.27927 334.693 -355.823 -7.78646 335.3 -355.821 -7.19903 326.081 +-355.843 -6.73603 325.402 -355.707 -7.25231 326.838 -355.872 -7.30733 335.755 +-355.807 -6.57479 336.257 -356.331 -5.83058 322.787 -355.699 -5.71767 323.296 +-355.906 -6.12101 323.868 -355.824 -6.17134 324.74 -356.439 -5.71312 335.83 +-355.955 -5.82768 336.235 -356.396 -4.63234 321.722 -356.012 -4.91259 322.31 +-355.785 -5.29002 322.835 -356.305 -4.52365 336.902 -355.851 -5.18959 336.662 +-355.62 -4.81481 337.234 -355.649 -4.1735 319.797 -356 -3.75599 319.338 +-355.706 -4.19021 320.767 -355.703 -4.36605 321.91 -356.34 -3.54821 336.844 +-355.705 -4.04852 337.405 -356.253 -2.6156 317.751 -355.882 -2.83789 318.223 +-355.691 -3.28427 318.773 -356.279 -2.52139 336.802 -355.821 -3.01163 337.339 +-355.494 -2.28676 317.856 -356.019 -1.86126 317.323 -356.253 -1.51624 336.805 +-355.777 -1.99531 337.306 -355.521 -1.21308 316.825 -355.975 -0.60156 316.457 +-356.413 -0.6648 336.919 -355.885 -0.85045 337.33 -355.816 0.160858 316.199 +-355.838 0.113087 337.472 -355.842 0.674642 315.871 -355.974 1.28808 315.343 +-356.361 1.26064 336.841 -355.798 1.16061 337.348 -355.659 1.67526 314.759 +-355.698 2.23209 314.339 -356.29 2.35634 314.728 -356.422 2.23662 334.85 +-356.278 2.27171 335.509 -356.035 2.13368 336.273 -355.792 1.78964 336.836 +-355.515 2.76985 313.838 -355.95 3.43476 313.424 -356.262 3.03369 313.956 +-356.416 3.33816 333.675 -356.025 3.10672 334.244 -355.756 2.76028 334.998 +-355.576 2.62106 335.992 -355.758 4.18576 313.281 -356.424 4.45855 330.857 +-355.923 4.33167 332.433 -355.72 3.8497 332.968 -355.743 3.61388 333.843 +-355.795 5.22337 313.311 -356.358 5.33114 330.826 -356.244 5.21585 331.568 +-355.875 5.16979 332.212 -355.853 6.28452 313.288 -356.319 6.33249 330.939 +-355.676 5.81944 331.937 -355.769 6.30389 331.473 -355.62 7.20329 313.391 +-356.259 7.20914 330.859 -355.7 7.23708 331.208 -356.019 7.79637 313.815 +-355.763 8.36698 314.227 -356.318 8.27973 329.801 -356.022 8.13452 330.288 +-355.797 7.82364 330.891 -355.798 9.25783 314.295 -356.293 9.31417 328.864 +-355.766 9.1644 329.297 -355.585 8.70898 329.884 -355.799 10.236 314.344 +-356.436 10.2531 327.784 -355.961 9.74782 329.694 -355.547 10.2531 330.396 +-355.826 11.1079 314.427 -356.04 10.7111 330.918 -355.695 11.3055 331.373 +-355.837 11.6948 314.886 -355.907 12.3985 315.298 -356.336 12.3269 331.955 +-355.838 11.8244 331.817 -355.731 12.2931 332.214 -355.551 12.8075 314.853 +-355.936 13.3453 314.384 -356.265 13.1453 314.917 -356.398 13.4164 319.77 +-355.723 13.3451 332.42 -355.882 13.7542 313.001 -355.781 14.2243 312.276 +-356.366 14.3416 312.786 -355.876 13.5588 313.756 -356.422 14.1423 319.928 +-356.231 14.4471 332.841 -355.98 13.8711 332.789 -355.579 14.4115 333.152 +-355.583 14.8495 307.741 -356.003 15.291 307.386 -356.323 15.2188 308.109 +-356.199 15.3004 308.89 -355.764 14.869 308.75 -355.671 15.183 309.379 +-355.803 14.7917 311.791 -355.845 15.2671 311.432 -356.241 15.0723 311.979 +-356.378 15.2736 332.896 -355.874 15.1999 333.261 -355.706 15.8709 305.905 +-355.901 16.4079 305.429 -356.343 16.1301 305.948 -355.766 15.592 306.718 +-355.988 15.6114 309.675 -355.94 15.6704 310.701 -355.774 16.2504 333.327 +-355.737 17.1939 305.275 -355.726 17.1557 333.428 -355.771 18.2285 305.329 +-356.374 18.3705 333.912 -355.918 17.8087 333.753 -355.72 18.1999 334.241 +-355.827 19.2603 305.318 -355.854 19.2793 334.396 -355.724 20.2563 305.383 +-355.864 20.3139 334.38 -356.335 21.3451 305.913 -355.846 20.8515 305.749 +-355.697 21.3055 306.225 -355.736 21.1998 334.381 -355.799 22.2354 306.44 +-356.231 22.38 333.756 -355.918 22.0264 334.228 -355.9 23.1743 306.225 +-355.964 23.1701 333.409 -355.518 22.6978 333.84 -355.742 23.6347 305.752 +-355.869 24.2288 305.37 -355.822 24.3036 333.259 -355.768 25.232 305.274 +-355.966 25.4703 332.458 -355.519 24.8056 332.815 -356.251 25.1492 332.915 +-355.758 26.1987 305.377 -355.768 26.2305 332.259 -355.895 27.1947 305.438 +-355.778 27.2549 332.258 -356.326 28.4056 305.889 -355.893 27.8186 305.731 +-355.684 28.198 306.217 -355.731 28.164 332.414 -355.801 29.2507 306.363 +-356.356 29.3999 332.907 -355.915 28.8273 332.744 -355.698 29.1982 333.231 +-355.804 30.2478 306.332 -355.806 30.2478 333.379 -355.79 31.2397 306.323 +-355.826 31.2598 333.322 -355.796 32.2479 306.355 -355.835 32.2851 333.266 +-355.877 33.3077 306.274 -355.632 33.0518 333.44 -355.527 33.7501 305.809 +-355.931 34.4189 305.442 -356.249 34.119 305.902 -356.039 33.6963 333.836 +-355.733 34.2058 334.324 -355.749 35.1961 305.241 -355.894 35.1715 334.232 +-355.771 36.2626 305.24 -356.472 36.4925 317.708 -355.865 36.2161 333.371 +-355.743 35.6295 333.751 -355.604 37.066 305.44 -356.369 37.1045 316.968 +-356.135 37.2344 317.651 -356.285 37.1031 318.587 -356.309 37.2675 319.606 +-356.3 37.2435 320.59 -356.296 37.321 321.737 -356.477 37.3497 322.788 +-355.764 37.2319 333.303 -356.054 37.7208 305.836 -355.761 38.2138 306.271 +-356.362 38.2494 314.902 -356.251 38.2436 315.549 -355.81 38.2294 316.289 +-355.883 37.7484 316.955 -355.771 37.7492 318.041 -355.794 37.7488 319.073 +-355.786 37.7247 320.049 -355.775 37.7046 321.04 -355.858 37.8694 321.92 +-355.809 38.2643 322.373 -356.141 38.0345 322.701 -356.289 38.1229 323.612 +-356.348 38.3 324.674 -355.804 38.2628 333.337 -355.866 39.283 306.32 +-356.331 39.2543 312.894 -356.185 39.2675 313.576 -355.975 39.0898 314.334 +-355.873 38.7278 315.07 -355.673 38.7059 315.859 -355.695 38.6903 322.91 +-355.782 38.7606 324.057 -355.931 38.85 324.98 -355.773 39.2308 325.318 +-356.137 39.1678 325.752 -356.355 39.276 326.677 -355.819 39.2803 333.35 +-355.626 40.3276 306.382 -356.342 40.5185 311.811 -355.975 40.1184 312.29 +-355.828 39.746 313.004 -355.663 39.6214 313.933 -355.67 39.6536 325.886 +-355.954 39.8464 327.01 -355.661 40.2872 327.265 -356.293 40.2387 327.852 +-355.831 40.311 333.374 -356.103 40.776 306.778 -355.611 41.4021 307.17 +-356.238 41.1851 311.829 -355.635 41.1902 312.296 -355.8 40.6979 327.927 +-355.517 41.346 328.359 -356.289 41.2408 328.828 -355.736 41.3107 333.281 +-356.418 42.1707 307.825 -355.68 42.2388 307.444 -356.489 42.4484 309.716 +-356.279 42.3421 310.601 -355.791 42.2427 311.378 -355.814 41.7737 311.848 +-355.958 41.8097 329.012 -355.643 42.3494 329.203 -356.244 42.2171 329.678 +-356.416 42.2923 331.721 -355.699 42.3216 332.378 -355.875 41.8613 332.813 +-355.956 42.7906 307.989 -355.749 43.4096 308.338 -356.201 43.0925 308.871 +-356.188 43.1259 309.808 -355.693 42.7921 310.923 -355.716 43.2912 310.44 +-355.866 42.6404 329.92 -355.75 42.8745 330.869 -355.837 42.7483 331.773 +-355.854 43.7417 309.102 -355.748 43.7959 309.857 -355.412 -10.5525 329.84 +-355.269 -10.6867 330.579 -355.257 -9.61805 329.826 -354.833 -10.2764 330.914 +-354.779 -9.88732 330.499 -354.815 -9.80551 331.3 -355.437 -8.47003 327.913 +-355.394 -8.82789 328.84 -355.154 -8.55343 329.638 -354.749 -9.05945 330.307 +-355.013 -9.19621 331.906 -354.65 -8.63747 332.335 -355.104 -8.82902 332.701 +-355.34 -8.74205 333.668 -354.669 -8.17685 297.698 -354.951 -7.69912 297.23 +-355.286 -7.64043 297.932 -355.277 -7.58878 298.964 -354.667 -8.31692 298.761 +-354.787 -7.84533 299.388 -355.283 -7.80871 327.879 -355.162 -7.89603 328.666 +-354.719 -8.32009 329.983 -354.877 -7.83838 329.397 -354.729 -8.34147 333.062 +-354.881 -8.18179 333.967 -354.632 -7.66939 334.38 -355.215 -7.79977 334.847 +-354.851 -7.28866 296.758 -354.726 -6.74566 296.482 -355.148 -6.99952 296.957 +-355.267 -6.67254 297.754 -355.428 -6.79927 298.8 -354.729 -7.36648 299.835 +-355.254 -6.67213 299.864 -354.702 -6.84869 300.34 -355.343 -6.73857 325.855 +-355.218 -6.74167 326.524 -354.999 -6.90627 327.278 -354.866 -7.23042 328 +-354.705 -7.35174 328.863 -354.77 -6.63273 330.427 -354.763 -7.19111 330.722 +-355.127 -6.84705 330.996 -354.84 -7.26183 331.611 -355.116 -6.68608 331.665 +-354.7 -6.7707 332.189 -354.903 -7.24117 335.094 -354.772 -6.45033 335.317 +-355.283 -6.76993 335.794 -354.83 -6.29666 297.02 -354.885 -6.12368 297.874 +-354.799 -5.76259 298.361 -355.223 -5.84848 298.811 -355.413 -5.71004 299.902 +-354.904 -6.33359 300.687 -355.199 -5.64883 300.794 -354.753 -5.83201 301.241 +-355.35 -5.80247 323.903 -355.183 -5.76419 324.571 -354.941 -5.90284 325.294 +-354.817 -6.24611 326.003 -354.658 -6.3896 326.893 -354.859 -6.28807 331.055 +-354.839 -6.31586 331.767 -354.807 -6.09975 332.678 -355.087 -5.80772 332.787 +-354.824 -5.65071 332.501 -354.603 -5.72048 333.21 -355.232 -6.00662 335.903 +-354.628 -5.70228 335.328 -354.72 -5.24806 298.766 -354.866 -4.6968 299.369 +-355.23 -4.89543 299.747 -355.22 -4.76046 300.663 -354.835 -4.97967 301.383 +-355.324 -4.67235 322.742 -354.746 -4.74355 323.326 -354.847 -5.26476 323.997 +-354.666 -5.37965 324.912 -354.749 -5.33383 332.859 -354.798 -5.40224 335.937 +-354.805 -5.14987 336.762 -354.998 -4.70105 337.343 -354.736 -4.27068 299.889 +-354.786 -4.29454 300.985 -355.022 -4.11296 320.004 -354.85 -3.69535 319.367 +-354.664 -3.61659 320.285 -355.151 -3.85815 320.658 -354.976 -3.82573 321.262 +-354.906 -4.07654 321.928 -354.753 -4.2306 322.774 -355.307 -3.53285 337.915 +-354.783 -4.22112 337.673 -354.638 -3.58605 338.239 -354.6 -3.26514 318.651 +-355.045 -2.7716 318.337 -354.619 -3.4725 321.033 -355.329 -2.56389 337.873 +-354.86 -2.94209 338.315 -354.82 -2.31226 317.884 -354.928 -1.81572 317.342 +-355.302 -1.53571 337.848 -354.711 -1.99765 338.3 -354.813 -1.31491 316.825 +-354.948 -0.709318 316.344 -355.335 -0.521241 337.843 -354.703 -0.974069 338.264 +-354.756 -0.264848 315.843 -354.775 0.233126 315.322 -355.254 0.343819 315.752 +-355.362 0.499831 337.846 -354.736 0.0537452 338.248 -354.794 0.741701 314.808 +-354.884 1.41732 314.418 -355.222 1.02548 314.981 -355.262 1.22442 337.864 +-354.658 1.18479 338.246 -354.998 2.245 314.172 -355.336 2.37587 336.714 +-354.981 2.15797 337.323 -354.803 1.84205 337.905 -354.78 2.73689 313.716 +-354.799 3.40701 313.321 -355.352 3.3468 334.686 -354.915 3.20835 335.291 +-354.755 2.90665 335.966 -354.695 2.66738 336.927 -354.853 4.2928 313.351 +-355.28 4.30205 332.685 -354.762 4.28313 333.298 -354.804 3.86255 333.923 +-354.701 3.62868 334.918 -354.763 5.19708 313.356 -354.888 5.1976 332.461 +-354.677 4.77341 332.872 -354.827 6.26814 313.301 -355.234 6.4139 331.8 +-354.951 6.06698 332.2 -354.613 7.18793 313.382 -354.96 7.12254 331.485 +-354.501 6.74538 331.866 -355.003 7.78262 313.81 -354.758 8.34439 314.203 +-355.323 8.40659 330.766 -354.861 8.04133 331.221 -354.755 9.11746 314.44 +-354.899 9.20648 329.518 -354.633 8.90236 330.032 -354.877 8.62215 330.679 +-355.263 10.5578 314.784 -354.964 9.80286 314.736 -354.585 10.0631 315.158 +-354.993 9.71629 329.803 -354.562 10.1216 330.155 -355.381 11.2163 314.764 +-354.742 11.0655 315.315 -354.907 10.7511 330.76 -354.562 11.2121 331.363 +-355.019 11.8187 315.719 -355.275 12.3341 315.797 -354.621 12.0522 316.229 +-355.001 11.6843 331.877 -354.799 12.3125 332.26 -354.572 12.8159 312.874 +-354.848 13.2879 312.282 -355.293 13.3465 312.847 -355.427 13.1636 313.839 +-354.833 12.8034 313.891 -354.722 12.7884 315.019 -354.814 12.6316 315.834 +-354.695 13.1271 332.32 -354.553 13.752 311.821 -354.806 14.3792 311.326 +-355.31 14.2399 311.789 -355.414 14.1054 332.774 -354.697 14.1206 332.419 +-354.735 14.907 306.873 -354.843 15.3253 306.406 -355.392 15.167 306.915 +-354.621 14.6205 307.761 -354.881 14.7177 308.826 -355.31 15.2895 310.109 +-354.854 14.8394 309.792 -355.271 15.0751 310.936 -354.66 14.7707 310.727 +-354.871 14.6577 332.888 -354.859 15.2493 333.241 -354.654 15.745 305.859 +-354.956 16.3063 305.37 -354.709 16.1409 333.442 -354.75 17.2136 305.3 +-355.355 17.368 333.898 -354.912 16.801 333.751 -354.71 17.183 334.231 +-354.801 18.2473 305.333 -354.823 18.2596 334.376 -354.84 19.2699 305.303 +-354.817 19.2754 334.346 -354.597 20.1569 305.399 -354.813 20.2893 334.343 +-354.995 20.7127 305.833 -354.746 21.3102 306.299 -354.847 21.3098 334.364 +-354.855 22.2844 306.319 -354.755 22.305 334.277 -354.549 22.7842 305.835 +-354.96 23.4466 305.437 -355.257 23.126 305.906 -355.049 23.3329 333.503 +-354.747 22.8968 333.926 -354.766 24.2139 305.242 -354.791 24.0763 333.175 +-354.734 25.1199 305.433 -354.854 25.208 332.343 -354.716 24.5767 332.702 +-355.246 26.4998 305.832 -354.95 25.7789 305.77 -354.599 26.0398 306.227 +-354.703 26.1655 332.354 -355.412 27.2954 305.896 -354.81 27.137 306.287 +-354.873 27.1745 332.448 -354.809 28.2559 306.363 -355.341 28.3715 332.898 +-354.891 27.7906 332.747 -354.691 28.1826 333.229 -354.804 29.2519 306.338 +-354.805 29.2451 333.375 -354.794 30.2418 306.323 -354.804 30.2444 333.345 +-354.791 31.2376 306.335 -354.826 31.2669 333.294 -354.822 32.2521 306.359 +-354.724 32.2717 333.366 -354.748 33.2529 306.262 -355.325 33.3722 333.913 +-354.846 32.8763 333.745 -354.709 33.3369 334.225 -354.737 33.8195 305.884 +-355.021 34.2538 305.428 -354.868 34.3046 334.318 -354.838 35.1768 305.362 +-354.953 35.4394 333.443 -354.551 34.7984 333.834 -355.263 35.1321 333.913 +-354.861 36.1763 305.434 -354.751 36.1995 333.273 -355.33 37.4045 305.9 +-354.888 36.8196 305.738 -354.681 37.1995 306.225 -354.767 37.2241 333.315 +-354.822 38.2571 306.363 -355.338 38.2237 316.831 -355.266 38.3263 317.693 +-355.437 38.249 318.707 -355.319 38.3768 319.592 -355.276 38.1996 320.595 +-355.382 38.2762 321.696 -354.798 38.2381 333.334 -354.859 39.2713 306.317 +-355.443 39.4465 314.723 -355.284 39.3391 315.593 -354.785 39.1906 316.312 +-354.843 38.7021 316.972 -354.85 38.8543 317.894 -354.787 39.3934 318.445 +-355.147 38.9598 318.744 -354.795 38.7725 320.069 -354.696 39.2991 319.4 +-354.763 38.7052 321.033 -354.968 38.8568 322.008 -354.668 39.3138 322.257 +-355.207 39.2062 322.773 -355.319 39.2634 323.616 -355.384 39.2842 324.695 +-354.811 39.2577 333.328 -354.734 40.1537 306.296 -355.467 40.4586 312.796 +-355.273 40.2957 313.706 -355.165 40.1083 314.811 -354.707 39.7595 315.892 +-354.719 40.2762 315.454 -354.741 39.7145 318.929 -354.705 39.5849 322.886 +-354.721 39.7432 323.925 -354.978 39.862 325.02 -354.676 40.3412 325.268 +-355.184 40.1989 325.789 -355.362 40.2458 326.567 -354.821 40.2841 333.329 +-355.39 41.1038 306.763 -354.667 41.2596 306.436 -355.228 41.0829 312.908 +-354.822 40.8226 314.102 -354.879 41.1518 313.346 -354.718 40.7605 314.857 +-354.688 40.5981 325.892 -354.852 40.7183 326.98 -355.027 40.8723 327.867 +-354.633 41.4278 328.209 -354.688 41.1444 333.327 -354.911 41.6801 306.932 +-354.87 42.2982 307.195 -355.277 42.2948 311.867 -354.828 42.2455 312.222 +-354.644 41.6823 312.838 -355.345 42.1304 328.693 -354.719 42.1373 328.477 +-355.278 42.2422 332.88 -354.756 42.109 333.234 -355.419 43.3084 307.843 +-354.9 43.3873 307.316 -355.337 43.2524 310.818 -355.011 43.1132 311.282 +-354.747 42.8061 311.863 -354.935 42.6204 329 -354.919 42.9064 329.874 +-354.751 43.3265 330.249 -355.258 43.201 330.772 -355.321 43.2852 331.588 +-355.02 43.1271 332.339 -354.757 42.7907 332.89 -355.14 44.1118 307.8 +-354.716 44.3536 307.268 -355.348 44.1633 308.57 -355.308 44.3277 309.573 +-355.02 44.1271 310.227 -354.668 43.6902 310.886 -354.71 43.6431 330.95 +-354.612 43.6616 332.022 -354.656 44.6693 307.947 -354.726 44.695 309.083 +-354.618 44.602 309.861 -354.398 -9.54715 330.891 -353.731 -9.07343 298.758 +-353.863 -8.82468 298.375 -354.238 -8.83475 298.924 -353.892 -8.69756 299.162 +-354.293 -8.58032 330.796 -354.319 -8.85088 331.715 -353.724 -8.19414 296.822 +-353.863 -7.72169 296.293 -354.435 -7.67892 296.787 -353.788 -8.43767 297.754 +-354.374 -7.75871 299.823 -353.828 -8.18591 299.715 -353.818 -7.76022 300.321 +-354.386 -7.67854 329.761 -354.357 -7.72531 330.843 -354.323 -7.77907 331.948 +-354.307 -7.82981 332.795 -354.378 -7.66495 333.763 -353.791 -7.32559 295.84 +-353.835 -6.7651 295.284 -354.247 -6.71569 295.92 -354.366 -6.65879 301.082 +-353.893 -7.27088 300.786 -354.201 -6.59199 301.972 -353.716 -7.12681 301.729 +-353.74 -6.85279 302.371 -354.401 -6.47645 327.796 -354.348 -6.76133 328.892 +-354.359 -6.83974 330.003 -354.405 -6.63968 332.883 -354.239 -6.73191 333.871 +-354.395 -6.92034 334.63 -353.865 -6.2117 294.718 -353.746 -5.71855 294.432 +-354.143 -5.91784 294.946 -354.215 -5.70463 295.6 -354.306 -5.87461 296.52 +-354.367 -5.72201 297.665 -354.432 -5.68075 301.873 -353.73 -6.37558 302.873 +-354.204 -5.73283 302.737 -353.735 -5.78353 303.354 -354.399 -5.4615 325.791 +-354.361 -5.76199 326.954 -354.306 -5.77685 327.872 -354.215 -5.73034 328.597 +-354.202 -5.76282 329.723 -354.403 -5.77212 330.726 -354.508 -5.85256 331.977 +-354.271 -5.91653 333.792 -354.249 -5.8977 334.643 -353.838 -5.26412 295.014 +-353.788 -5.24624 295.999 -353.732 -5.29839 297.021 -353.889 -5.20412 297.967 +-353.662 -4.68823 298.357 -354.29 -4.76974 298.809 -354.442 -4.53913 301.741 +-354.188 -4.69859 302.594 -353.843 -5.07127 303.312 -354.329 -4.73733 323.86 +-354.263 -4.84006 324.709 -354.173 -4.75163 325.692 -354.222 -4.85447 326.635 +-354.216 -4.94328 327.721 -353.806 -5.23007 329.075 -353.682 -4.72874 328.391 +-353.881 -5.11787 329.912 -354.154 -4.93719 330.692 -353.813 -4.74252 330.358 +-354.229 -4.70021 331.767 -354.334 -4.70886 332.847 -353.995 -4.90525 333.362 +-353.819 -5.24965 333.989 -353.849 -5.26579 335.017 -353.91 -5.15538 335.95 +-353.628 -4.69803 336.354 -354.235 -4.83287 336.838 -353.953 -4.21045 299.012 +-353.735 -3.59942 299.371 -354.191 -3.79059 299.719 -354.34 -3.78393 300.745 +-354.158 -3.80719 301.796 -353.745 -4.34142 302.992 -353.875 -3.85845 302.418 +-354.545 -3.73259 319.792 -354.447 -3.66631 321.786 -354.21 -3.72605 322.588 +-353.963 -3.88645 323.29 -353.837 -4.23191 323.987 -353.783 -4.25598 325.033 +-353.833 -4.2473 326.065 -353.874 -4.22821 327.088 -353.741 -4.23137 327.847 +-353.678 -4.33304 330.879 -353.905 -4.08722 331.943 -354.192 -3.91152 332.734 +-353.852 -3.72435 332.375 -353.83 -4.27787 333.745 -354.121 -3.73298 333.653 +-353.717 -3.84518 334.238 -353.931 -4.23826 337.076 -353.795 -3.75494 337.39 +-354.26 -3.84514 337.793 -353.821 -3.25924 300.072 -353.823 -3.19607 301.026 +-353.747 -3.34541 301.881 -353.63 -3.22922 317.894 -353.835 -2.70777 317.285 +-354.347 -2.65137 317.823 -353.993 -3.23993 318.946 -354.207 -3.01798 319.795 +-353.594 -2.6968 319.367 -353.769 -3.16724 320.767 -353.76 -2.67078 320.204 +-353.797 -3.34219 321.933 -353.536 -3.39184 323.016 -353.742 -3.27859 332.924 +-353.794 -3.31103 334.013 -353.761 -3.36011 337.781 -353.865 -2.62327 338.333 +-353.771 -2.32325 316.851 -353.772 -1.78972 316.364 -354.246 -1.78153 316.849 +-353.66 -2.41105 319.854 -353.938 -1.63314 338.425 -353.717 -1.19222 315.732 +-353.845 -0.631379 315.289 -354.266 -0.780977 315.856 -353.805 -0.766524 338.387 +-353.721 -0.187625 314.705 -353.867 0.378978 314.262 -354.293 0.234984 314.837 +-353.817 0.263716 338.378 -353.68 0.69042 313.763 -353.729 1.26364 313.322 +-354.23 1.21368 313.869 -353.686 1.18491 338.402 -353.592 1.80582 312.784 +-353.902 2.47555 312.413 -354.201 2.11013 312.984 -354.477 2.2196 313.715 +-354.351 2.42878 337.796 -353.914 2.08832 338.238 -354.286 2.98737 312.907 +-353.582 3.33326 312.378 -354.357 3.33191 335.772 -353.942 3.19026 336.2 +-353.778 2.86234 336.647 -353.724 2.64846 337.627 -353.93 3.69117 312.849 +-353.776 4.36674 313.24 -354.333 4.21467 333.842 -353.879 4.17595 334.228 +-353.79 3.87611 334.761 -353.683 3.57462 335.602 -353.894 5.32985 313.361 +-354.209 5.40478 332.788 -353.984 5.08545 333.243 -353.66 4.66146 333.812 +-353.821 6.24963 313.306 -354.018 6.15729 332.374 -353.528 5.74559 332.886 +-353.676 7.05448 313.448 -354.065 7.41474 331.317 -353.546 6.7666 331.704 +-353.842 7.67317 313.93 -353.831 8.26367 314.427 -353.579 8.16882 330.472 +-353.648 7.64925 330.749 -354.226 8.3306 330.773 -354.379 9.33082 314.889 +-353.704 8.68738 314.925 -353.816 9.30983 315.343 -354.44 9.41808 329.856 +-353.932 9.11216 330.225 -353.913 10.1857 315.481 -353.847 9.83371 329.726 +-354.214 10.3062 329.778 -353.652 10.3547 329.522 -354.363 11.4065 315.948 +-353.71 10.7687 315.921 -353.843 11.38 316.39 -353.864 10.6304 330.07 +-353.921 10.8689 330.83 -353.531 11.2089 331.38 -353.864 12.1303 316.256 +-353.987 11.6951 331.892 -353.782 12.3289 332.265 -353.614 12.7711 312.877 +-354.079 13.1588 312.277 -353.822 12.6894 313.795 -353.953 12.7624 314.722 +-353.774 12.6573 315.61 -353.776 13.2 332.419 -353.637 13.8588 307.651 +-353.971 14.251 307.397 -354.208 14.2617 307.998 -353.634 14.2035 308.326 +-353.58 13.7186 311.81 -354.005 14.2467 311.353 -353.817 14.1045 332.454 +-353.977 14.6591 306.918 -353.94 15.1669 306.263 -353.938 14.6616 308.812 +-353.8 14.7756 309.876 -353.75 14.6558 310.758 -353.81 14.7114 332.906 +-353.784 15.2812 333.424 -353.687 15.6339 305.688 -353.842 16.2301 305.363 +-354.35 16.3508 333.901 -353.89 15.7653 333.768 -353.695 16.1499 334.244 +-353.765 17.2305 305.318 -353.821 17.2392 334.376 -353.82 18.2563 305.331 +-353.803 18.2519 334.334 -353.852 19.2817 305.293 -353.786 19.2413 334.318 +-353.599 20.1737 305.39 -353.8 20.2529 334.331 -353.98 20.7308 305.837 +-353.744 21.3262 306.302 -353.827 21.2853 334.35 -353.807 22.1229 306.31 +-353.707 22.1807 334.37 -353.612 22.7475 305.835 -354.024 23.3301 305.365 +-354.315 23.4055 333.681 -353.892 23.0263 334.241 -353.671 24.2241 305.354 +-353.967 24.3974 332.46 -353.743 23.8703 333.093 -354.283 24.1902 332.775 +-353.632 23.5873 333.872 -354.375 25.3305 305.935 -353.842 24.8411 305.772 +-353.731 25.3171 306.235 -353.835 25.1191 332.415 -353.809 26.2281 306.409 +-354.23 26.5099 332.835 -353.896 25.7829 332.75 -353.581 26.0807 333.212 +-353.818 27.2559 306.365 -354.41 27.2885 332.905 -353.813 27.1561 333.289 +-353.8 28.2537 306.331 -353.811 28.2538 333.369 -353.794 29.2428 306.326 +-353.799 29.2469 333.345 -353.791 30.2414 306.329 -353.807 30.2467 333.316 +-353.801 31.2477 306.336 -353.824 31.2681 333.28 -353.831 32.2713 306.345 +-353.635 32.0302 333.452 -353.706 33.155 306.354 -354.057 32.6755 333.833 +-353.738 33.1754 334.321 -354.318 34.4653 305.813 -353.867 34.0234 306.267 +-353.842 34.1566 334.299 -354.246 35.4341 305.82 -353.681 35.0355 306.256 +-354.002 35.3185 333.387 -353.601 34.7596 333.828 -354.393 36.2957 305.896 +-353.79 36.1539 306.277 -353.758 36.2114 333.294 -353.8 37.2512 306.369 +-353.785 37.2395 333.334 -353.81 38.2492 306.343 -353.802 38.2501 333.32 +-353.835 39.2565 306.318 -354.313 39.344 317.007 -354.453 39.3451 317.785 +-354.386 39.2265 319.844 -354.261 39.2167 320.565 -354.314 39.2274 321.542 +-353.801 39.2534 333.29 -353.852 40.2845 306.305 -354.327 40.2155 315.823 +-353.752 40.1981 316.291 -353.909 39.8359 316.847 -353.761 40.259 317.324 +-354.115 40.0333 317.676 -354.3 40.1483 318.601 -353.812 40.288 319.335 +-353.869 39.7442 319.964 -353.748 39.7343 321.032 -353.912 39.7346 322.063 +-354.022 39.9035 322.986 -353.662 40.3024 323.23 -354.175 40.176 323.741 +-354.363 40.2545 324.54 -353.766 40.1914 333.39 -353.707 41.1713 306.297 +-354.464 41.3532 313.708 -354.276 41.2901 314.546 -354.048 41.131 315.308 +-353.785 40.7542 315.842 -353.691 40.6913 317.893 -353.715 40.8225 318.884 +-353.593 40.5942 323.951 -353.95 40.6482 324.977 -353.894 40.898 325.875 +-353.69 41.3307 326.251 -354.192 41.1923 326.793 -354.376 41.2137 327.615 +-353.938 41.2348 333.428 -354.408 42.2407 306.779 -353.821 42.2251 306.402 +-353.978 42.2463 312.474 -353.632 41.8483 312.984 -353.818 41.6416 313.883 +-353.568 41.6206 315.028 -353.712 41.5857 326.906 -353.789 41.7292 327.761 +-353.965 42.1034 328.292 -353.703 42.244 333.449 -354.296 43.105 306.818 +-353.737 43.3093 306.397 -354.333 43.3832 311.704 -353.902 43.0172 312.298 +-353.887 42.7622 328.784 -353.684 43.3388 329.245 -354.264 43.2011 329.684 +-354.332 43.4111 332.756 -353.927 43.0926 333.207 -354.284 44.0681 306.757 +-353.581 44.174 306.415 -353.849 44.3696 310.402 -353.687 43.8786 310.989 +-353.719 43.6453 311.882 -353.809 43.6346 329.959 -353.726 43.7492 330.935 +-353.831 43.7828 331.838 -353.72 43.6566 332.761 -354.013 44.6524 306.962 +-353.722 44.8744 307.814 -353.945 44.8767 308.868 -353.856 44.8269 309.765 +-352.881 -9.06664 297.984 -352.823 -8.69765 297.444 -353.291 -8.78883 297.814 +-353.289 -8.88438 298.699 -352.582 -8.79535 298.353 -352.66 -8.16207 296.028 +-352.817 -7.70293 295.32 -353.396 -7.70134 295.787 -352.779 -8.37625 296.943 +-352.911 -8.39852 299.086 -352.687 -8.26271 299.93 -353.202 -7.69383 300.952 +-352.84 -8.14246 300.726 -352.631 -7.84478 301.397 -352.557 -7.25521 294.818 +-352.858 -6.67383 294.287 -353.341 -6.78392 294.767 -353.001 -7.42797 301.649 +-353.357 -6.75888 302.839 -352.779 -7.26853 302.742 -352.814 -6.76611 303.359 +-352.577 -6.2775 293.772 -352.858 -5.62051 293.44 -353.255 -5.83308 293.979 +-353.301 -5.50988 304.013 -353.045 -6.22731 303.68 -352.515 -5.92992 304.28 +-353.251 -4.75622 293.881 -352.758 -4.69917 293.246 -353.437 -4.76523 294.732 +-353.407 -4.6356 295.816 -353.363 -4.68117 296.833 -353.388 -4.70288 297.768 +-353.436 -4.56111 303.831 -352.838 -5.26071 304.752 -353.2 -4.71682 304.793 +-352.622 -4.73688 305.186 -353.355 -4.8091 328.822 -353.37 -4.74259 329.641 +-353.428 -4.53681 334.076 -353.306 -4.68831 334.82 -353.361 -4.71628 335.728 +-353.192 -3.77761 293.908 -352.773 -3.67818 293.402 -353.363 -3.82446 294.701 +-353.223 -3.66839 295.613 -353.167 -3.89486 296.627 -353.185 -3.78354 297.755 +-353.412 -3.81384 298.735 -353.436 -3.59562 302.872 -353.344 -3.63221 304.012 +-353.261 -3.7549 304.787 -352.689 -3.97874 305.387 -353.373 -3.65492 323.758 +-353.253 -3.7486 324.595 -353.291 -3.76218 325.603 -353.333 -3.74992 326.631 +-353.29 -3.74004 327.588 -352.785 -3.76081 328.319 -352.844 -4.30243 328.944 +-352.925 -4.18285 329.945 -352.707 -3.63558 330.379 -353.144 -3.84822 330.734 +-353.413 -3.70314 331.694 -353.389 -3.69517 334.666 -353.18 -3.74943 335.608 +-353.335 -3.86712 336.67 -352.908 -3.12614 293.941 -352.762 -2.53356 294.403 +-353.154 -2.96218 294.747 -352.784 -3.22201 296.119 -352.88 -2.84325 295.414 +-352.776 -3.31701 297.052 -352.859 -3.1432 297.898 -352.728 -2.61947 298.396 +-353.134 -2.95935 298.679 -353.298 -2.86168 299.576 -353.296 -2.72091 300.554 +-353.294 -2.83655 301.677 -353.166 -2.7711 302.651 -353.212 -2.86627 303.609 +-353.182 -2.82997 304.585 -352.857 -3.00334 305.314 -352.633 -3.21263 316.866 +-352.863 -2.67368 316.288 -353.392 -2.68351 316.807 -352.658 -3.35735 317.991 +-353.06 -3.19269 318.745 -352.633 -2.72509 319.333 -352.74 -3.10238 321.024 +-353.082 -2.70559 320.349 -352.886 -3.18686 321.845 -352.9 -3.14305 322.858 +-352.868 -3.23605 323.943 -352.789 -3.2504 325.066 -352.816 -3.24823 326.075 +-352.835 -3.23298 327.099 -352.68 -3.23297 327.874 -352.785 -3.31985 331.096 +-352.982 -3.13312 331.983 -352.67 -2.68293 332.217 -353.21 -2.78127 332.737 +-353.373 -2.7835 333.769 -353.201 -2.83607 334.816 -352.771 -3.22897 336.07 +-352.683 -2.72085 335.411 -352.93 -3.14864 337.015 -352.751 -2.75884 337.356 +-353.221 -2.82406 337.789 -352.753 -2.38059 294.979 -352.793 -2.28205 299.111 +-352.774 -2.24137 300.022 -352.766 -2.27171 301.002 -352.778 -2.28082 302.008 +-352.795 -2.26667 303.021 -352.809 -2.23226 304.04 -352.759 -2.35925 304.897 +-352.828 -2.20144 314.91 -352.836 -1.76648 314.3 -353.157 -1.64035 314.875 +-353.295 -1.86011 315.86 -352.799 -2.34867 315.574 -352.796 -2.36553 319.932 +-352.634 -2.37525 332.945 -352.878 -2.33972 333.973 -352.663 -2.22502 334.747 +-352.72 -2.37513 337.754 -352.818 -1.6324 338.301 -352.762 -1.16745 313.633 +-352.797 -0.4408 313.329 -353.154 -0.896306 313.996 -353.442 -0.835544 314.88 +-352.898 -0.671653 338.391 -353.342 -0.0200765 313.956 -352.925 0.503224 313.34 +-352.771 0.228842 338.398 -352.998 1.25345 313.168 -352.902 1.32907 338.253 +-352.724 1.59261 312.798 -352.946 2.23064 312.377 -352.758 2.25221 337.482 +-352.683 1.70532 337.784 -353.254 2.20743 337.96 -352.705 3.11061 312.288 +-352.849 3.30403 335.467 -352.663 2.88017 336.049 -353.378 3.17637 336.004 +-352.813 2.67975 336.998 -353.365 4.16583 312.737 -352.674 4.11303 312.453 +-352.975 4.27694 334.359 -352.646 3.75544 334.931 -352.836 4.64022 312.925 +-352.861 5.21992 313.274 -353.029 5.30374 333.377 -352.537 4.80941 333.848 +-352.722 6.22379 313.418 -353.092 6.28959 332.33 -352.586 5.79475 332.744 +-353.325 7.26436 313.805 -352.898 6.8793 313.739 -352.653 7.29131 314.233 +-352.934 7.41765 330.466 -352.696 6.86796 330.911 -353.35 7.13561 330.934 +-352.611 6.57315 331.615 -352.953 7.8344 314.673 -353.281 8.19904 314.829 +-352.656 8.26296 315.252 -352.882 8.28684 330.223 -352.977 8.80477 315.688 +-353.259 9.4996 315.839 -352.595 9.21777 316.152 -352.786 9.30174 328.346 +-352.663 8.91139 328.651 -353.223 9.34624 329.132 -352.681 8.71895 329.604 +-353.374 9.13412 329.885 -353.393 10.1748 315.802 -352.742 10.0427 316.327 +-352.681 9.65752 328.655 -352.781 10.1183 329.307 -352.804 10.7508 316.869 +-353.262 11.3995 316.691 -352.71 11.1717 317.319 -352.897 10.6252 329.773 +-352.883 10.8776 330.646 -352.815 11.2079 331.308 -352.738 11.828 316.003 +-352.835 12.3127 315.45 -353.316 12.265 315.849 -352.635 11.5449 316.999 +-352.839 11.7014 331.723 -352.981 12.332 332.317 -352.625 12.8521 313.017 +-353.103 13.1971 312.319 -352.777 12.6883 313.825 -352.858 12.6429 314.838 +-353.272 13.5073 332.832 -352.794 12.79 332.64 -352.628 13.433 333.173 +-352.747 13.7855 306.958 -352.784 14.308 306.3 -353.374 14.3276 306.763 +-352.915 13.8062 307.765 -352.824 14.3015 308.215 -352.551 13.7366 311.919 +-352.974 14.2637 311.339 -353.379 14.1897 332.779 -352.773 14.1826 333.308 +-352.512 14.7599 305.774 -352.939 15.4562 305.429 -353.215 15.0187 305.937 +-352.777 14.6941 308.821 -352.804 14.8062 309.848 -352.736 14.6796 310.749 +-353.304 15.3121 333.933 -352.888 14.8201 333.693 -352.688 15.2936 334.24 +-352.754 16.2119 305.287 -352.798 16.2119 334.394 -352.769 17.2305 305.329 +-352.816 17.2447 334.345 -352.826 18.258 305.331 -352.797 18.2424 334.316 +-352.859 19.2864 305.289 -352.786 19.2313 334.323 -352.601 20.1754 305.391 +-352.799 20.24 334.328 -352.976 20.749 305.841 -352.754 21.345 306.315 +-352.814 21.2744 334.341 -352.868 22.1431 306.249 -352.821 22.3177 334.381 +-352.758 22.6267 305.76 -352.9 23.2525 305.341 -352.832 23.2243 334.293 +-352.553 24.157 305.364 -353.289 24.447 332.771 -352.841 24.2016 333.243 +-352.714 23.7784 333.763 -352.984 24.738 305.831 -352.773 25.344 306.277 +-353.382 25.2664 332.887 -352.776 25.1318 333.249 -352.793 26.2433 306.373 +-352.816 26.2709 333.38 -352.801 27.2515 306.33 -352.817 27.267 333.36 +-352.794 28.2442 306.324 -352.796 28.2496 333.336 -352.793 29.2428 306.332 +-352.792 29.2395 333.329 -352.796 30.2494 306.322 -352.822 30.2557 333.305 +-352.808 31.2488 306.306 -352.724 31.255 333.381 -352.812 32.252 306.312 +-353.335 32.3483 333.912 -352.847 31.852 333.749 -352.696 32.305 334.226 +-352.799 33.2683 306.332 -352.797 33.2345 334.44 -352.788 34.2795 306.355 +-352.9 34.1738 334.224 -352.799 35.2775 306.366 -352.865 35.2229 333.379 +-352.744 34.6343 333.748 -352.808 36.2632 306.356 -352.763 36.2281 333.313 +-352.795 37.2464 306.339 -352.805 37.2571 333.326 -352.791 38.2405 306.331 +-352.801 38.2577 333.289 -352.808 39.2524 306.329 -352.742 39.1438 333.429 +-352.819 40.2662 306.318 -353.443 40.2955 316.925 -353.328 40.254 319.864 +-353.241 40.2431 320.581 -353.25 40.236 321.606 -353.329 40.2325 322.533 +-353.292 40.4954 333.838 -352.946 39.7951 333.761 -352.618 40.0701 334.22 +-352.814 41.2689 306.317 -353.325 41.3571 315.741 -352.76 41.2347 316.309 +-352.989 40.8045 316.943 -352.744 41.3668 317.361 -353.205 41.1811 317.739 +-353.297 41.3003 318.566 -353.017 41.135 319.282 -352.836 40.7624 320.002 +-352.754 40.7414 321.052 -352.774 40.7357 322.055 -352.917 40.7297 323.055 +-352.753 40.8505 323.951 -353.043 40.8691 324.859 -352.622 41.2802 325.243 +-353.22 41.1704 325.668 -353.369 41.4438 333.839 -352.769 41.0354 334.275 +-352.848 42.262 306.325 -353.318 42.5078 312.832 -352.782 42.28 313.324 +-352.931 41.8285 313.853 -352.733 41.8276 314.906 -352.798 41.6349 315.684 +-352.696 41.7192 318.092 -352.57 41.606 318.99 -352.751 41.5962 325.984 +-352.958 41.8202 326.984 -352.499 42.3468 327.354 -353.264 42.2236 327.827 +-353.221 42.3113 333.78 -352.696 42.1419 334.228 -352.755 43.3559 306.224 +-353.278 43.2954 312.755 -352.77 43.0504 313.186 -352.95 42.8032 328.01 +-352.664 43.3526 328.194 -353.226 43.2114 328.667 -352.87 43.2912 333.455 +-352.729 42.8621 333.939 -352.655 44.4063 306.379 -353.325 44.1899 310.769 +-352.731 44.3021 311.288 -352.787 43.8743 311.915 -352.734 43.6189 312.781 +-352.751 43.6458 328.962 -352.797 43.7323 329.799 -352.809 43.8171 330.738 +-352.913 43.8571 331.883 -352.83 43.8384 332.807 -353.101 44.8165 306.904 +-352.643 45.3485 307.211 -353.18 45.19 307.762 -353.346 45.2404 308.647 +-353.273 45.2538 309.607 -352.996 45.1125 310.32 -352.772 44.7504 310.834 +-352.707 45.6003 307.886 -352.664 45.7598 308.982 -352.586 45.6229 310.041 +-351.754 -9.14785 297.681 -352.031 -8.78113 297.259 -351.651 -8.77439 298.335 +-351.673 -8.13974 295.824 -352.041 -7.80821 295.328 -351.719 -8.39859 296.533 +-351.861 -8.31812 298.913 -351.811 -8.21716 299.797 -351.789 -8.28088 300.817 +-352.349 -7.79061 302.136 -351.835 -8.21844 301.794 -352.227 -7.79464 302.8 +-351.804 -8.11684 302.669 -351.652 -7.73718 303.262 -351.598 -7.29816 294.827 +-352.064 -6.80606 294.321 -352.356 -6.65392 303.918 -351.837 -7.17536 303.809 +-351.79 -6.68247 304.305 -351.609 -6.31831 293.818 -352.088 -5.70453 293.266 +-351.773 -5.99359 304.458 -351.557 -5.30996 292.765 -351.835 -4.64077 292.427 +-352.214 -4.9007 292.806 -352.052 -5.30932 304.811 -351.862 -4.74564 305.313 +-352.347 -3.87469 292.839 -351.742 -3.80119 292.34 -352.267 -3.47751 306.006 +-351.888 -4.20015 305.754 -351.687 -3.92215 306.252 -352.3 -3.64425 329.003 +-352.423 -3.66353 329.747 -352.209 -2.77962 292.901 -351.577 -2.68323 292.391 +-352.485 -2.74929 293.714 -352.445 -2.46228 295.791 -352.379 -2.70352 296.82 +-352.444 -2.68862 297.762 -352.429 -2.52167 305.816 -351.856 -3.34354 306.731 +-352.155 -2.70696 306.774 -351.73 -2.82663 307.231 -351.557 -3.17248 314.757 +-351.877 -2.68026 314.292 -352.324 -2.64551 314.915 -352.445 -2.88648 315.897 +-351.823 -3.22697 315.884 -351.645 -3.31871 316.936 -351.904 -3.33283 317.713 +-351.939 -3.16175 318.685 -351.779 -2.66873 319.206 -352.392 -2.68394 320.736 +-352.252 -2.78363 321.617 -352.298 -2.76256 322.628 -352.312 -2.73533 323.611 +-352.314 -2.72889 324.598 -352.327 -2.72607 325.589 -352.349 -2.71586 326.602 +-352.262 -2.71066 327.568 -351.983 -2.89833 328.277 -351.907 -3.13494 328.883 +-352.093 -2.96724 329.678 -351.751 -2.7268 329.35 -352.285 -2.88148 330.614 +-352.347 -2.74647 331.556 -352.345 -2.80078 335.824 -352.333 -2.73607 336.649 +-351.954 -2.11305 292.942 -351.703 -1.59432 293.242 -352.187 -1.89885 293.675 +-352.327 -1.89822 294.73 -352.197 -1.74575 295.724 -352.185 -1.90488 296.615 +-352.191 -1.79385 297.739 -352.394 -1.79024 298.791 -352.405 -1.62003 299.837 +-352.388 -1.65393 300.826 -352.391 -1.66414 301.822 -352.398 -1.65646 302.819 +-352.403 -1.64039 303.817 -352.403 -1.75447 304.926 -352.342 -1.61686 306.005 +-352.231 -1.74254 306.781 -351.802 -1.96937 307.378 -351.745 -2.14503 312.791 +-351.9 -1.63457 312.238 -352.162 -1.65888 312.888 -352.324 -1.87061 313.83 +-351.802 -2.34963 313.595 -351.756 -2.29074 319.798 -351.793 -2.31231 320.905 +-351.682 -2.35556 322.064 -351.7 -2.3621 323.05 -351.692 -2.36485 324.013 +-351.686 -2.36365 324.994 -351.7 -2.36008 325.996 -351.688 -2.35108 327.011 +-351.533 -2.41085 327.97 -351.667 -2.31041 329.893 -351.806 -2.22137 331.079 +-351.923 -2.25649 332.056 -351.772 -2.14337 332.949 -352.119 -2.10433 333.895 +-351.672 -1.7001 334.304 -352.16 -1.80389 334.743 -351.867 -1.73827 335.286 +-351.853 -2.27825 335.968 -351.91 -2.17067 337.011 -351.763 -1.78284 337.38 +-352.206 -1.86319 337.781 -351.695 -1.34178 293.885 -351.741 -1.22524 294.941 +-351.832 -1.27199 296.064 -351.801 -1.29103 297.063 -351.871 -1.13188 297.912 +-351.754 -0.597808 298.418 -352.153 -0.952747 298.688 -352.216 -0.818741 299.636 +-352.201 -0.84014 300.634 -352.198 -0.859029 301.641 -352.198 -0.856201 302.641 +-352.195 -0.849858 303.632 -352.199 -0.848239 304.62 -352.226 -0.833657 305.639 +-352.184 -0.808885 306.607 -351.817 -0.995058 307.32 -351.746 -1.177 311.639 +-351.722 -0.598214 311.474 -352.134 -0.851479 312.055 -352.435 -0.911094 312.946 +-351.616 -1.44693 334.726 -351.714 -1.40988 337.741 -351.776 -0.641998 338.297 +-351.832 -0.256865 299.133 -351.827 -0.209308 300.072 -351.821 -0.232407 301.077 +-351.823 -0.23222 302.081 -351.821 -0.230855 303.082 -351.819 -0.229351 304.077 +-351.827 -0.216225 305.068 -351.839 -0.197314 306.07 -351.761 -0.334759 306.924 +-351.838 -0.174361 311.924 -351.727 0.542958 312.318 -352.269 0.0181148 312.664 +-351.926 0.374803 338.311 -352.29 0.973182 312.892 -351.89 1.47582 312.397 +-351.941 1.50929 337.381 -351.49 0.751352 337.747 -352.235 1.07794 337.933 +-351.719 2.46047 312.27 -351.987 2.27833 337.164 -351.921 3.39515 312.344 +-352.029 3.20511 335.265 -351.591 2.83084 335.71 -351.716 2.63571 336.605 +-351.716 4.0843 312.342 -351.904 4.16397 334.332 -351.791 3.63064 334.82 +-351.884 4.76869 312.673 -351.758 5.51384 313.209 -351.99 5.21033 333.356 +-351.827 4.7111 333.827 -351.681 6.21905 313.447 -351.892 6.27749 331.41 +-351.676 5.87902 331.811 -352.382 6.18858 332.01 -351.64 5.63501 332.64 +-352.031 6.72596 313.806 -351.561 7.26353 314.291 -351.938 7.26178 330.332 +-351.928 6.62894 330.947 -351.884 7.75986 314.924 -351.731 8.20164 315.465 +-351.893 8.37993 329.454 -351.662 7.86477 329.73 -352.181 8.14422 329.954 +-351.828 8.65513 315.948 -351.82 9.28502 316.413 -351.631 9.1836 327.356 +-351.961 8.81374 327.976 -352.304 9.16822 327.877 -351.914 8.60912 328.785 +-352.251 10.2513 316.844 -351.873 9.80528 316.743 -351.789 10.1648 317.222 +-351.859 9.56779 327.845 -351.832 9.85858 328.703 -351.759 10.3466 329.361 +-351.995 10.7822 317.715 -352.25 11.3051 317.753 -351.734 11.1818 318.257 +-351.874 11.335 320.561 -351.748 10.9372 320.94 -352.046 11.3284 320.734 +-351.688 11.1173 321.177 -352.017 10.7884 329.975 -351.606 11.3076 330.271 +-352.227 11.2224 330.861 -351.764 11.8536 315.939 -351.777 12.3275 315.373 +-351.924 11.7975 316.78 -351.93 11.7529 317.846 -351.736 11.838 318.863 +-352.083 12.3839 318.852 -351.82 12.2798 318.452 -351.709 12.1781 319.253 +-351.865 11.5025 320.729 -351.753 11.7142 330.921 -351.503 12.2047 331.384 +-352.293 12.1961 331.857 -352.37 13.2789 312.634 -351.916 13.1433 313.253 +-351.868 12.8803 313.916 -351.793 12.7826 314.737 -351.908 12.5428 318.844 +-351.949 12.7169 332.085 -351.585 13.2624 332.425 -352.313 13.153 332.758 +-351.849 13.8338 306.965 -352.008 14.1552 306.303 -351.567 14.221 307.333 +-352.293 14.1416 307.684 -351.628 13.8899 312.002 -352.041 14.2616 311.36 +-351.703 13.5807 312.838 -351.94 13.6163 332.952 -351.705 14.1403 333.384 +-351.546 14.6887 305.777 -351.992 15.3102 305.384 -351.918 14.6467 308.059 +-351.704 14.7948 308.9 -351.821 14.7923 309.805 -351.728 14.6929 310.879 +-351.954 14.7026 333.897 -351.743 15.3113 334.291 -351.748 16.203 305.3 +-351.792 16.2176 334.361 -351.771 17.234 305.336 -351.811 17.2307 334.316 +-351.825 18.2574 305.331 -351.789 18.2242 334.308 -351.858 19.2843 305.288 +-351.77 19.2178 334.317 -351.599 20.167 305.397 -351.779 20.2267 334.323 +-351.984 20.754 305.85 -351.815 21.3666 306.224 -351.794 21.2581 334.346 +-351.516 21.7457 305.863 -351.986 22.4461 305.437 -352.244 22.098 305.935 +-351.809 22.3033 334.391 -351.802 23.25 305.215 -351.874 23.1965 334.219 +-351.618 24.0431 305.433 -351.868 24.2358 333.412 -351.729 23.6368 333.771 +-352.049 24.7038 305.829 -351.725 25.1775 306.28 -351.78 25.2524 333.338 +-351.801 26.2403 306.35 -351.805 26.2653 333.344 -351.8 27.2511 306.324 +-351.797 27.2473 333.329 -351.795 28.2453 306.331 -351.789 28.2394 333.33 +-351.8 29.2519 306.321 -351.803 29.2422 333.327 -351.8 30.2426 306.293 +-351.834 30.2653 333.299 -351.764 31.1686 306.392 -351.592 31.1629 333.399 +-351.797 32.3198 306.405 -351.993 31.7134 333.835 -351.743 32.3169 334.3 +-351.894 33.3726 306.308 -351.856 33.2926 334.317 -351.767 34.2578 306.323 +-351.954 34.442 333.444 -351.547 33.788 333.834 -352.258 34.1252 333.903 +-351.792 35.2593 306.334 -351.753 35.2049 333.28 -351.799 36.2454 306.331 +-351.78 36.2379 333.315 -351.792 37.2401 306.328 -351.811 37.2638 333.3 +-351.789 38.2395 306.332 -351.735 38.1392 333.433 -351.793 39.2424 306.333 +-352.371 39.3665 333.913 -351.919 38.789 333.76 -351.712 39.1825 334.248 +-351.791 40.2449 306.331 -351.854 40.2712 334.398 -351.801 41.238 306.347 +-352.463 41.4329 316.954 -352.375 41.3303 319.75 -352.248 41.2424 320.61 +-352.277 41.2367 321.619 -352.286 41.2356 322.612 -352.261 41.2202 323.609 +-352.299 41.2132 324.548 -351.864 41.3073 334.374 -351.916 42.2702 306.256 +-352.324 42.3117 313.812 -352.259 42.2099 314.538 -351.89 42.1818 315.292 +-352.014 41.8954 315.925 -352.164 42.0483 316.74 -351.645 42.3545 316.271 +-352.29 42.1441 317.527 -351.958 42.2204 318.251 -351.892 41.9187 318.883 +-351.854 41.7434 319.928 -351.789 41.743 321.071 -351.805 41.7505 322.067 +-351.796 41.7488 323.068 -351.782 41.7413 324.063 -351.908 41.7363 325.069 +-351.768 41.8369 325.984 -352.062 41.8418 326.732 -351.609 42.4211 327.216 +-351.719 42.2007 334.376 -351.622 42.7981 305.808 -351.66 43.4682 305.482 +-352.21 43.0771 305.876 -351.893 43.2002 313.357 -351.639 42.7139 313.963 +-351.58 42.5758 314.96 -351.666 42.674 316.939 -351.626 42.6547 317.975 +-352.365 43.1327 327.693 -351.736 43.1403 327.481 -352.334 43.3249 333.876 +-351.901 43.0535 334.241 -352.018 43.6602 305.867 -351.771 44.335 306.168 +-352.335 44.2072 311.804 -351.927 44.1769 312.288 -351.808 43.8916 312.833 +-351.952 43.6255 327.995 -351.673 43.8731 328.836 -352.071 43.868 329.886 +-351.678 44.291 330.283 -352.175 44.1535 330.792 -352.316 44.2508 331.677 +-352.23 44.2872 332.667 -352.007 44.1305 333.241 -351.666 43.7281 333.698 +-352.336 45.1316 306.68 -351.706 45.1343 306.445 -352.326 45.3749 310.737 +-351.941 45.1143 311.227 -351.628 44.6753 311.921 -351.638 44.6104 331.001 +-351.824 44.6614 332.032 -351.585 44.5715 332.656 -351.927 45.6102 306.993 +-351.929 45.908 307.857 -351.755 46.3393 308.245 -352.208 46.2038 308.698 +-352.036 46.208 309.279 -351.786 45.9172 309.951 -351.733 45.6298 310.735 +-351.611 46.6142 308.935 -350.716 -9.18917 296.922 -350.829 -8.72054 296.435 +-351.316 -8.8676 296.95 -350.887 -9.1826 297.73 -350.806 -8.67879 298.238 +-350.699 -8.2757 295.897 -351.04 -7.78881 295.321 -350.75 -8.26635 298.851 +-350.796 -8.17299 299.837 -350.766 -8.22179 300.849 -350.764 -8.2764 301.871 +-350.871 -8.20016 302.858 -350.717 -7.83307 303.458 -350.583 -7.301 294.819 +-350.901 -6.69034 294.327 -350.988 -7.45837 303.824 -351.241 -6.5179 304.899 +-350.756 -7.28296 304.695 -350.652 -6.86582 305.357 -350.726 -6.13488 293.905 +-350.866 -5.68726 293.408 -351.409 -5.61604 304.972 -350.86 -6.35723 305.643 +-351.142 -5.68679 305.813 -350.735 -5.87882 306.234 -350.751 -5.23435 292.866 +-350.953 -4.67712 292.393 -351.38 -4.74349 305.871 -350.894 -5.37032 306.753 +-351.235 -4.76328 306.902 -351.185 -4.56311 307.804 -350.707 -5.25042 307.758 +-350.743 -4.76243 308.302 -350.957 -3.68479 292.235 -351.423 -3.72926 306.912 +-351.231 -3.7081 307.844 -350.838 -4.0234 308.341 -350.671 -2.87331 292.232 +-351.379 -2.66724 307.821 -350.86 -3.36782 308.73 -351.205 -2.75792 308.899 +-351.211 -2.63586 310.04 -350.789 -3.23948 309.596 -351.22 -2.64284 311.041 +-350.823 -3.26211 310.602 -351.189 -2.76132 311.956 -350.847 -3.27451 311.586 +-351.307 -2.68568 312.945 -350.745 -3.26746 312.706 -351.448 -2.87634 313.918 +-350.831 -3.17855 313.904 -350.716 -3.27968 314.922 -350.83 -3.30526 315.79 +-350.774 -3.30837 316.815 -351 -3.20139 317.891 -350.534 -2.74246 318.363 +-351.237 -2.82375 318.713 -351.466 -2.62343 328.828 -351.415 -1.80784 292.693 +-350.647 -1.87457 292.363 -351.383 -1.55034 307.945 -351.272 -1.79071 308.796 +-351.3 -1.74844 309.796 -351.312 -1.86811 310.671 -351.436 -1.92332 311.779 +-350.891 -2.35137 319.072 -350.664 -2.22311 319.924 -350.773 -2.24014 320.823 +-350.906 -2.24619 321.71 -350.791 -2.17867 322.747 -350.903 -2.17248 323.813 +-350.873 -2.18392 324.799 -350.864 -2.18802 325.796 -350.867 -2.18815 326.8 +-350.848 -2.19016 327.802 -350.984 -2.18178 328.852 -350.696 -1.65326 329.379 +-351.163 -1.82465 329.734 -351.311 -1.73423 330.592 -351.286 -1.75447 331.587 +-351.246 -1.79061 332.58 -351.309 -1.79141 333.55 -351.427 -1.6647 335.881 +-351.329 -1.61113 336.655 -350.959 -1.3357 292.979 -350.89 -1.0699 293.836 +-350.805 -0.752655 294.312 -351.196 -0.815825 294.763 -351.337 -0.761747 295.617 +-351.302 -0.680744 296.748 -351.466 -0.653153 297.785 -351.391 -0.522594 307.793 +-351.281 -0.790314 308.837 -351.276 -0.745628 309.829 -351.361 -0.84342 310.928 +-350.797 -1.31385 330.081 -350.784 -1.26847 331.006 -350.748 -1.29798 332.003 +-350.724 -1.31312 333.014 -350.887 -1.28681 334.059 -351 -1.11904 334.921 +-351.125 -1.00595 335.753 -350.681 -0.575279 335.364 -350.908 -1.13165 337.053 +-350.815 -0.886069 336.369 -350.63 -0.645388 337.434 -351.162 -0.880939 337.746 +-350.721 -0.327208 294.861 -350.806 -0.263024 296.064 -350.855 -0.130933 296.934 +-350.801 0.257134 297.386 -351.132 0.0270886 297.704 -351.312 0.131873 298.625 +-351.31 0.277082 299.616 -351.298 0.25166 300.599 -351.3 0.245324 301.604 +-351.301 0.247732 302.606 -351.3 0.247171 303.608 -351.3 0.251374 304.603 +-351.305 0.259259 305.588 -351.3 0.145863 306.712 -351.2 0.317409 307.819 +-351.333 0.232448 308.854 -351.315 0.275489 309.867 -351.269 0.30185 310.878 +-351.416 0.118345 311.624 -350.68 -0.413879 335.937 -350.826 -0.338303 337.992 +-350.991 0.18313 338.238 -350.702 0.689032 297.925 -350.822 0.782459 299.084 +-350.796 0.754348 300.061 -350.793 0.740868 301.057 -350.796 0.743496 302.059 +-350.797 0.744594 303.06 -350.8 0.746418 304.06 -350.8 0.748927 305.041 +-350.794 0.729961 306.019 -350.791 0.700314 307.031 -350.873 0.842139 307.891 +-350.755 1.38924 308.403 -351.176 1.02109 308.671 -351.243 1.16535 309.626 +-351.206 1.28219 310.769 -351.357 1.05802 311.789 -350.738 1.5561 336.381 +-350.812 0.859823 336.908 -351.283 1.20743 336.95 -350.718 0.557146 337.733 +-350.835 1.71311 309.103 -350.816 1.75447 310.04 -350.837 1.87904 310.902 +-350.653 2.37486 311.445 -351.192 1.95841 311.746 -351.31 1.98195 336.84 +-350.855 2.32017 336.38 -350.837 2.67196 311.98 -351.03 3.35249 312.192 +-350.793 3.33938 334.318 -350.789 2.84655 334.962 -351.321 3.33762 334.8 +-350.845 2.6343 335.855 -350.619 3.76923 311.702 -350.637 4.33918 311.57 +-351.183 4.21679 311.992 -350.821 4.41653 333.3 -350.675 3.81283 333.75 +-351.251 4.24564 333.854 -350.944 4.75845 312.016 -350.555 5.24685 312.368 +-351.259 5.1808 312.705 -350.903 5.40151 332.425 -350.689 4.89932 332.727 +-351.232 5.08612 333.026 -350.901 5.61198 312.912 -350.73 6.16343 313.303 +-350.925 6.27769 328.278 -350.622 5.9187 328.642 -351.167 6.5148 328.931 +-350.662 6.09991 329.342 -350.787 6.38867 330.26 -350.772 5.85303 330.879 +-351.308 6.3503 330.785 -350.867 5.61925 331.787 -350.899 6.75316 313.866 +-350.718 7.2059 314.389 -350.763 7.51174 327.376 -350.74 6.73265 327.676 +-351.179 7.16257 328.083 -351.327 7.25777 328.804 -350.867 6.57896 329.699 +-351.259 7.10103 329.741 -350.989 7.57985 314.781 -350.756 7.75844 315.696 +-351.281 8.28181 315.805 -350.767 8.25641 316.296 -351.326 8.33508 327.764 +-350.839 8.31932 327.245 -351.417 8.00286 328.87 -350.988 8.7221 316.666 +-351.3 9.34572 317.054 -350.64 8.70742 317.696 -351.145 9.42805 317.928 +-350.683 9.17114 318.336 -350.904 9.40524 325.327 -350.736 8.89467 325.85 +-351.202 9.36752 325.94 -350.797 8.69414 326.638 -351.188 9.24894 326.784 +-351.406 10.245 317.913 -350.898 9.60868 318.706 -351.239 10.225 318.908 +-350.828 9.74566 319.605 -351.22 10.2289 320.003 -350.835 9.73255 320.648 +-351.161 10.3569 320.874 -350.835 9.7222 321.66 -351.303 10.3352 321.782 +-350.825 9.73137 322.668 -351.123 10.2248 322.892 -350.82 9.77524 323.66 +-351.146 10.2642 323.857 -350.828 9.73802 324.59 -351.205 10.1421 324.879 +-351.214 10.1695 325.747 -350.872 9.75298 327.065 -350.865 10.1294 326.361 +-350.935 9.82916 327.978 -350.755 10.4729 328.293 -351.26 10.3085 328.864 +-351.333 11.2807 318.845 -351.333 11.2134 319.687 -351.503 11.1685 320.925 +-350.76 10.7824 321.078 -350.845 11.0713 320.437 -351.256 10.6409 321.204 +-350.949 10.7816 321.843 -350.835 10.7266 323.056 -350.868 10.7631 324.012 +-350.866 10.7939 325.003 -350.754 10.6537 325.861 -351.124 11.0594 328.864 +-350.827 11.1873 328.441 -351.322 11.106 329.571 -351.311 12.1653 315.835 +-351.252 12.1956 316.56 -351.283 12.2601 317.735 -350.783 11.8706 319.026 +-351.343 12.1276 318.846 -350.742 12.3541 318.316 -351.234 11.7067 319.244 +-350.895 11.7494 319.733 -350.698 11.7013 328.883 -350.911 11.7478 330.065 +-350.946 11.9011 330.824 -350.825 12.1907 331.248 -351.417 13.3011 313.781 +-351.201 13.2571 314.587 -350.964 13.096 315.276 -350.829 12.7443 315.987 +-350.743 12.761 317.044 -350.746 12.6752 317.743 -350.798 13.3317 318.415 +-350.837 12.9346 318.715 -351.147 13.247 318.865 -350.683 13.0527 319.175 +-350.874 12.7244 331.696 -350.761 13.3312 332.217 -350.769 13.881 306.808 +-350.976 14.1638 306.295 -350.604 14.2131 307.302 -351.356 14.3151 311.672 +-350.933 14.1698 312.241 -350.739 13.8565 312.906 -350.789 13.6437 313.933 +-350.554 13.6057 315.013 -350.666 13.5308 318.791 -350.934 13.7683 332.752 +-350.555 14.2389 333.347 -350.518 14.6965 305.773 -350.98 15.3116 305.389 +-350.877 14.6784 307.876 -350.836 14.796 308.763 -350.816 14.7685 309.788 +-350.816 14.7814 310.824 -350.701 14.6384 311.882 -350.96 14.7187 333.883 +-350.748 15.3199 334.253 -350.746 16.2145 305.301 -350.788 16.2067 334.327 +-350.772 17.237 305.337 -350.69 17.2414 334.386 -350.826 18.2584 305.33 +-350.896 18.3295 334.297 -350.859 19.2731 305.288 -350.739 19.2003 334.303 +-350.589 20.1595 305.411 -350.748 20.22 334.315 -350.987 20.7429 305.864 +-350.75 21.2136 306.201 -350.785 21.247 334.354 -350.553 21.7198 305.873 +-351.042 22.3276 305.374 -350.875 22.3152 334.282 -350.705 23.2454 305.346 +-350.95 23.4404 333.457 -350.548 22.7902 333.827 -351.245 23.1214 333.907 +-351.344 24.3686 305.909 -350.849 23.8609 305.754 -350.709 24.3092 306.215 +-350.756 24.212 333.284 -350.784 25.2085 306.384 -350.773 25.2364 333.316 +-350.809 26.2434 306.342 -350.801 26.246 333.333 -350.8 27.2515 306.326 +-350.797 27.2421 333.332 -350.803 28.2514 306.324 -350.792 28.2393 333.337 +-350.804 29.2571 306.293 -350.817 29.2488 333.328 -350.746 30.1273 306.435 +-350.849 30.2871 333.288 -351.29 31.513 306.867 -350.958 30.7707 306.78 +-350.62 31.071 307.257 -350.595 31.1859 333.381 -351.199 32.3319 306.784 +-350.818 32.0154 307.188 -350.978 31.7498 333.833 -350.755 32.3497 334.294 +-350.703 32.5953 306.772 -350.897 33.1147 306.378 -350.82 33.1458 334.3 +-350.766 34.2396 306.308 -351.002 34.318 333.39 -350.602 33.7546 333.837 +-350.795 35.2466 306.329 -350.762 35.2132 333.296 -350.798 36.2428 306.33 +-350.808 36.2628 333.304 -350.795 37.2428 306.33 -350.708 37.2592 333.381 +-350.794 38.2422 306.331 -351.339 38.3363 333.915 -350.838 37.8539 333.759 +-350.72 38.3194 334.214 -350.792 39.2407 306.329 -350.807 39.2415 334.39 +-350.782 40.2393 306.337 -350.825 40.2745 334.35 -350.8 41.2485 306.374 +-350.812 41.2899 334.337 -350.846 42.1591 306.273 -351.452 42.3359 315.707 +-351.421 42.345 318.757 -351.281 42.2577 319.602 -351.31 42.2669 320.607 +-351.336 42.2802 321.595 -351.343 42.2835 322.586 -351.336 42.2875 323.583 +-351.313 42.2866 324.579 -351.273 42.2573 325.618 -351.318 42.2038 326.617 +-350.819 42.3222 334.372 -350.636 42.7726 305.819 -351.012 43.2589 305.379 +-350.845 43.3394 313.452 -350.668 42.8362 313.956 -350.832 42.7008 314.761 +-350.934 42.6796 315.919 -350.719 42.7708 316.9 -350.866 42.7985 317.772 +-350.82 42.6947 318.9 -350.714 42.6771 320.024 -350.741 42.6891 321.007 +-350.745 42.6903 322 -350.741 42.6894 323.005 -350.717 42.6811 324.001 +-350.679 42.6658 325.023 -350.765 42.6139 326.002 -350.754 42.7204 326.78 +-350.969 43.1053 327.301 -350.834 43.2238 334.279 -351.393 44.1288 305.718 +-350.683 44.0798 305.399 -351.308 44.3597 312.747 -350.898 44.0713 313.199 +-350.908 43.7718 327.792 -350.697 44.3494 328.27 -351.18 44.2041 328.792 +-351.363 44.2373 329.577 -350.728 44.444 332.497 -350.757 43.9459 333.093 +-351.389 44.212 332.969 -350.711 43.6754 333.87 -350.86 44.6273 305.879 +-350.65 45.3024 306.313 -351.07 45.2699 311.392 -350.627 44.9368 312.055 +-350.705 44.6264 312.742 -350.724 44.5933 328.895 -350.873 44.747 329.94 +-350.827 44.8973 330.964 -350.963 44.829 331.848 -351.043 45.8116 306.92 +-350.652 46.3515 307.188 -351.252 46.1999 307.651 -351.414 46.3567 309.73 +-350.855 46.2083 310.234 -350.659 45.7575 310.878 -350.826 46.6703 307.942 +-350.729 46.8314 308.945 -350.751 46.6452 309.748 -349.828 -9.17269 296.939 +-349.989 -8.8361 296.272 -349.522 -8.77574 297.36 -350.27 -8.85784 297.708 +-349.562 -8.31424 295.779 -349.909 -7.70298 295.307 -350.008 -8.35391 298.062 +-349.659 -8.28379 298.654 -349.91 -8.29304 299.624 -349.889 -8.31512 300.637 +-349.769 -8.31924 301.768 -349.748 -8.23896 302.87 -350.354 -7.76154 304.087 +-349.814 -8.16115 303.793 -350.221 -7.78331 304.736 -349.676 -8.11179 304.788 +-349.772 -7.80268 305.349 -349.745 -7.1806 294.877 -349.857 -6.68317 294.488 +-350.345 -6.6242 306.095 -349.992 -7.30403 305.709 -350.186 -6.52758 306.995 +-349.818 -7.24357 306.552 -349.603 -6.90063 307.285 -350.29 -5.75073 293.839 +-349.952 -5.97763 294.162 -350.501 -5.83664 306.894 -349.755 -6.23749 307.846 +-350.298 -5.87585 307.654 -349.837 -5.7632 308.369 -350.227 -4.72253 292.748 +-349.86 -4.84604 293.24 -349.652 -5.35152 293.782 -350.26 -4.46227 308.919 +-349.822 -5.27942 308.82 -349.674 -4.82979 309.327 -349.643 -4.23725 292.898 +-349.938 -3.76807 292.401 -350.477 -3.72107 309 -349.868 -4.38246 309.692 +-350.289 -3.83683 309.895 -350.33 -3.77208 311.036 -349.728 -4.24807 310.693 +-350.36 -3.79256 312.028 -349.725 -4.16413 311.813 -350.227 -3.72034 312.869 +-349.742 -4.14087 312.76 -349.716 -3.84956 313.38 -349.883 -2.58961 292.232 +-349.926 -3.38017 313.752 -349.81 -3.22309 314.806 -349.787 -3.26185 315.835 +-349.757 -3.30363 316.857 -349.991 -3.17875 317.735 -349.601 -2.75588 318.329 +-349.572 -1.92913 292.405 -349.818 -2.32631 318.888 -349.761 -2.23991 319.799 +-349.861 -2.27693 320.711 -349.921 -2.16626 321.827 -349.795 -1.78874 322.372 +-350.164 -1.85485 322.804 -350.309 -1.68311 323.776 -350.396 -1.67173 324.873 +-350.372 -1.67551 325.87 -350.363 -1.69064 326.861 -350.351 -1.70134 327.855 +-350.407 -1.69378 328.783 -350.061 -1.24411 292.88 -349.54 -0.849623 293.309 +-350.314 -0.730932 293.771 -349.711 -1.3374 322.887 -349.9 -1.10449 323.945 +-350.154 -0.96864 324.732 -349.832 -0.724804 324.373 -350.199 -0.853984 325.667 +-350.186 -0.86324 326.647 -350.169 -0.889618 327.639 -350.194 -0.778215 328.768 +-350.392 -0.801271 329.791 -350.41 -0.646419 330.809 -350.385 -0.68962 331.792 +-350.36 -0.704277 332.783 -350.329 -0.687526 333.784 -350.391 -0.64158 334.736 +-350.402 -0.452853 336.744 -349.988 -0.206512 293.984 -349.591 0.310322 294.286 +-350.242 0.240675 294.699 -350.314 0.271961 295.587 -350.385 0.297201 296.711 +-349.706 -0.321104 324.884 -349.823 -0.236269 326.054 -349.788 -0.270532 327.027 +-349.781 -0.293345 328.034 -349.871 -0.143563 328.889 -350.163 0.0469889 329.673 +-349.727 0.383114 329.38 -350.208 0.162847 330.614 -350.201 0.211665 331.749 +-350.294 0.193826 332.832 -350.255 0.2311 333.825 -350.217 0.253514 334.809 +-350.314 0.0431876 335.769 -350.32 0.390031 336.878 -350.401 0.0433865 337.702 +-349.701 0.632739 295.037 -349.773 0.632769 295.94 -349.855 0.849493 296.879 +-349.745 1.33269 297.225 -350.245 1.21451 297.768 -350.375 1.29617 298.61 +-350.339 1.2853 299.596 -350.323 1.27147 300.587 -350.326 1.27385 301.589 +-350.33 1.27643 302.589 -350.334 1.27956 303.589 -350.331 1.28013 304.586 +-350.315 1.26106 305.585 -350.316 1.33752 306.728 -350.474 1.35112 307.771 +-349.785 0.734431 330.113 -349.762 0.750104 331.093 -349.84 0.81516 331.97 +-350.231 1.24312 332.75 -349.845 1.29554 332.205 -350.284 1.26295 333.876 +-350.236 1.30685 334.857 -350.32 1.05637 335.882 -349.708 1.64839 297.914 +-349.76 1.6822 299 -349.733 1.67569 300.002 -349.733 1.67702 301.001 +-349.738 1.68137 302.002 -349.742 1.68643 303.002 -349.728 1.68794 304.007 +-349.698 1.66611 305.021 -349.692 1.63472 306.048 -349.825 1.82449 306.922 +-349.55 2.27448 307.421 -350.174 2.06399 307.683 -350.322 2.14049 308.596 +-350.296 2.33494 309.731 -350.424 2.3414 310.785 -349.874 2.33928 331.27 +-349.697 1.76017 331.768 -350.202 2.24764 331.996 -350.308 2.38581 332.978 +-350.286 2.14252 333.747 -350.336 2.30686 334.801 -350.383 2.0594 335.754 +-349.84 2.60585 308.056 -349.726 2.76643 308.935 -349.872 2.85904 309.935 +-349.815 3.22568 310.411 -350.164 3.19584 310.891 -350.449 3.02552 311.68 +-349.806 3.4987 330.399 -349.731 2.80265 330.701 -350.193 3.16951 331.066 +-350.322 3.40038 331.913 -350.3 3.14405 332.684 -350.378 3.09806 333.837 +-349.663 3.69829 310.695 -349.929 4.17757 311.265 -350.199 4.33331 330.807 +-349.862 4.29873 330.233 -350.301 4.14107 331.657 -350.388 4.1346 332.797 +-349.935 4.75243 311.694 -349.609 5.24244 312.296 -349.767 5.32438 329.35 +-349.701 4.85494 329.739 -350.16 5.27747 329.878 -350.355 5.38074 330.811 +-350.395 5.0265 331.876 -350.065 5.76565 312.807 -349.606 6.28999 313.27 +-349.816 6.24626 326.281 -349.801 5.80385 326.861 -350.13 6.33748 326.857 +-349.884 5.71744 327.719 -350.425 6.2208 327.78 -349.931 5.6394 328.832 +-350.483 6.05339 330.039 -349.798 6.68164 313.891 -349.829 6.84414 314.758 +-350.351 7.24113 315.066 -349.695 6.84757 315.8 -350.218 7.19993 315.749 +-349.809 7.18926 316.363 -349.829 7.21777 324.298 -349.839 6.79034 324.878 +-350.176 7.4345 324.924 -349.831 6.67929 325.517 -350.201 7.21676 326.038 +-350.384 7.06369 326.983 -349.876 7.73367 316.855 -350.347 8.22109 317.091 +-349.684 7.9007 317.809 -350.238 8.20782 317.761 -349.798 8.2264 318.351 +-349.806 8.24619 322.292 -349.821 7.80011 322.861 -350.154 8.434 322.92 +-349.821 7.66485 323.504 -350.204 8.20716 324.01 -350.286 8.27446 324.791 +-350.371 8.27445 325.756 -350.43 7.9843 326.759 -350.007 8.74293 318.7 +-350.346 9.26114 319.078 -349.757 8.75404 319.595 -350.283 9.25909 320.054 +-349.777 8.77324 320.603 -350.304 9.25829 321.07 -349.797 8.72651 321.538 +-350.299 9.11032 322.064 -350.422 9.26632 322.977 -350.392 9.1748 323.835 +-350.422 8.99605 324.899 -350.379 10.5142 326.76 -350.384 10.1676 327.613 +-350.384 11.4958 320.813 -350.385 11.2054 321.735 -350.273 11.3547 322.617 +-350.296 11.2146 323.588 -350.316 11.2566 324.566 -350.299 11.173 325.714 +-350.134 11.2365 326.694 -350.304 11.1048 327.749 -350.419 12.4382 318.919 +-350.284 12.0387 319.833 -350.13 12.2424 320.716 -350.131 12.0653 321.73 +-349.769 11.7608 323.141 -349.845 12.1111 322.447 -349.778 11.7033 324.053 +-349.804 11.7406 325.044 -349.804 11.7508 326.045 -349.78 11.7402 327.041 +-349.963 11.8731 327.992 -349.808 12.2558 328.329 -350.188 12.2138 328.767 +-350.289 12.2612 329.629 -350.339 12.2575 330.716 -350.386 13.3558 315.751 +-350.236 13.2787 316.578 -350.395 13.0663 317.813 -349.92 12.7637 319.897 +-349.812 13.4154 319.365 -349.806 12.792 321.061 -349.744 12.6597 321.932 +-349.703 12.676 328.896 -349.783 12.7461 330.089 -349.929 12.8387 331.032 +-349.657 13.3225 331.248 -350.231 13.1753 331.724 -349.948 13.9287 306.902 +-350.002 14.1653 306.358 -349.726 14.2722 307.197 -350.35 14.3177 312.699 +-349.885 14.167 313.273 -349.879 13.883 313.9 -349.91 13.8589 314.847 +-349.716 13.7704 315.939 -349.738 13.6252 316.924 -349.878 13.7595 317.95 +-349.821 13.8255 318.841 -349.77 13.6281 331.98 -349.945 13.8721 332.822 +-349.552 14.2142 333.343 -349.515 14.6936 305.91 -349.976 15.3174 305.372 +-349.743 14.662 307.798 -349.822 14.7758 308.787 -349.787 14.7426 309.807 +-349.798 14.7416 310.82 -349.803 14.7764 311.824 -349.679 14.632 312.885 +-349.974 14.7142 333.857 -349.759 15.318 334.225 -349.75 16.2162 305.294 +-349.749 16.0956 334.468 -349.776 17.2415 305.337 -350.165 17.3474 334.773 +-350 16.7712 334.774 -349.606 17.1536 335.207 -349.832 18.2557 305.33 +-349.945 18.2443 334.427 -349.742 17.8206 334.929 -349.854 19.2656 305.297 +-349.852 19.3313 334.29 -349.561 20.3266 305.406 -349.729 20.2171 334.309 +-350.02 20.8125 305.883 -349.924 21.3189 306.159 -349.783 21.2494 334.365 +-349.681 21.6431 305.804 -349.902 22.269 305.376 -349.839 22.1639 334.276 +-349.574 23.1545 305.382 -350.005 23.317 333.39 -349.605 22.759 333.838 +-349.991 23.7169 305.833 -349.753 24.2965 306.28 -349.763 24.2156 333.292 +-349.779 25.1941 306.377 -349.792 25.2474 333.331 -349.797 26.2268 306.338 +-349.807 26.2505 333.34 -349.8 27.2477 306.326 -349.8 27.2464 333.339 +-349.817 28.2662 306.3 -349.796 28.2417 333.345 -349.701 29.2546 306.372 +-349.831 29.259 333.324 -350.358 30.3369 306.933 -349.838 29.8579 306.759 +-349.731 30.3385 307.246 -349.85 30.2937 333.276 -349.894 31.3288 307.341 +-349.64 31.0534 333.444 -349.548 31.8361 306.84 -349.962 32.4897 306.459 +-350.291 32.1811 306.937 -350.041 31.7073 333.832 -349.732 32.2033 334.323 +-349.763 33.2315 306.282 -349.896 33.1726 334.234 -349.781 34.2444 306.32 +-349.864 34.2207 333.382 -349.745 33.6339 333.754 -349.801 35.246 306.335 +-349.779 35.2354 333.297 -349.798 36.2443 306.331 -349.825 36.2737 333.287 +-349.796 37.2444 306.33 -349.637 37.0339 333.458 -349.797 38.2439 306.329 +-350.055 37.6746 333.831 -349.732 38.1651 334.289 -349.793 39.2416 306.325 +-349.801 39.2348 334.355 -349.781 40.2467 306.345 -349.795 40.2427 334.313 +-349.802 41.2746 306.389 -349.789 41.2549 334.327 -349.879 42.1786 306.217 +-349.796 42.2939 334.378 -349.743 42.634 305.753 -349.916 43.2539 305.348 +-350.233 43.3282 313.707 -349.693 43.2793 314.302 -349.921 42.8342 314.855 +-349.91 42.819 315.825 -349.743 42.8126 316.879 -349.806 42.7161 317.796 +-349.814 42.7358 318.788 -349.811 42.7586 319.789 -349.824 42.7738 320.777 +-349.817 42.773 321.775 -349.801 42.768 322.789 -349.786 42.7554 323.807 +-349.776 42.7259 324.819 -349.998 42.8093 325.881 -349.504 43.3401 326.372 +-350.259 43.223 326.834 -349.823 43.2039 334.292 -349.588 44.3306 305.368 +-350.053 44.1254 313.38 -349.639 43.8117 313.906 -349.964 43.8024 327.007 +-349.676 44.3505 327.189 -350.25 44.215 327.665 -350.341 44.2189 332.776 +-349.909 44.1711 333.175 -349.738 43.7884 333.767 -350.085 44.8272 305.815 +-349.632 45.4975 306.164 -350.364 45.3183 311.698 -349.87 45.183 312.195 +-349.665 44.7761 312.88 -349.757 44.6442 327.965 -349.998 44.8579 328.97 +-349.585 45.2554 329.357 -350.23 45.1958 329.711 -350.356 45.2818 330.586 +-350.259 45.2986 331.575 -349.96 45.1263 332.243 -349.659 44.6552 332.776 +-350.335 46.1527 306.699 -349.722 46.1754 306.453 -349.992 46.2646 310.338 +-349.571 45.8476 310.924 -349.749 45.5726 311.712 -349.78 45.6326 330.059 +-349.73 45.7206 331.049 -349.583 45.6302 331.869 -349.849 46.6519 307.038 +-349.705 46.7649 307.896 -349.842 46.7829 308.783 -349.793 46.6837 309.707 +-348.953 -9.07902 296.845 -348.794 -8.72679 296.358 -348.693 -8.72717 297.152 +-348.635 -9.08923 298.786 -348.888 -8.66532 298.425 -349.274 -8.74222 299.085 +-349.327 -8.74421 300.049 -348.724 -9.29738 299.549 -349.311 -8.73974 301.066 +-348.766 -9.26902 300.721 -349.167 -8.66737 301.912 -348.699 -9.09066 301.776 +-348.642 -8.80055 302.411 -348.698 -8.17305 295.839 -348.842 -7.66426 295.45 +-348.794 -8.38287 297.799 -348.981 -8.37885 302.689 -348.863 -8.32959 303.633 +-348.778 -8.3423 304.751 -349.319 -7.8333 306.158 -348.794 -8.20903 305.839 +-349.328 -7.69027 306.882 -348.77 -8.12805 306.717 -348.796 -7.6649 307.212 +-349.242 -6.65899 294.752 -348.846 -6.86526 295.155 -348.727 -6.92535 307.472 +-348.663 -6.1773 294.878 -348.862 -5.7653 294.402 -348.919 -6.37696 307.916 +-349.29 -5.79342 308.868 -348.842 -6.11018 308.761 -348.772 -5.788 309.343 +-348.706 -5.12186 293.875 -348.828 -4.62848 293.473 -349.377 -4.76035 310.049 +-348.897 -5.28143 309.795 -349.207 -4.69256 310.865 -348.714 -5.10941 310.747 +-348.703 -4.83483 311.387 -349.164 -3.64822 292.747 -348.8 -3.84918 293.176 +-348.926 -4.39015 311.764 -348.813 -4.27469 312.836 -349.255 -3.68636 313.951 +-348.87 -4.15594 313.742 -348.681 -3.82861 314.387 -348.563 -3.19061 292.875 +-348.995 -2.85701 292.469 -348.886 -3.3487 314.762 -348.742 -3.21467 315.846 +-348.723 -3.27722 316.904 -348.994 -3.19087 317.789 -348.596 -2.76473 318.371 +-349.336 -1.58361 292.834 -348.86 -2.17924 292.722 -348.673 -1.77332 293.146 +-348.809 -2.35065 318.929 -348.884 -2.24303 319.681 -348.888 -2.15994 320.812 +-348.643 -1.70168 321.259 -349.284 -1.77796 321.855 -348.584 -0.799408 293.356 +-348.829 -1.23859 321.901 -348.541 -0.744209 322.369 -349.221 -0.785725 322.738 +-349.435 -0.679335 323.683 -348.947 -0.184086 293.845 -348.757 0.229977 294.384 +-348.826 -0.445847 323.007 -348.854 -0.187443 323.86 -348.739 0.341223 324.383 +-349.215 0.285899 324.873 -349.447 0.358262 325.815 -349.409 0.336331 326.794 +-349.363 0.305975 327.786 -349.42 0.316823 328.741 -348.865 0.620819 294.856 +-349.021 0.866376 295.83 -348.535 1.29971 296.338 -349.259 1.19881 296.648 +-348.897 0.819805 324.967 -349.172 1.03857 325.671 -348.834 1.28203 325.347 +-349.233 1.22816 326.73 -349.289 1.22367 327.824 -349.242 1.25305 328.833 +-349.324 1.23127 329.793 -349.352 1.27931 330.734 -349.444 1.26947 331.636 +-348.906 1.6549 297.058 -348.715 1.79724 297.908 -348.845 1.78657 298.785 +-348.833 1.77906 299.79 -348.83 1.77244 300.78 -348.836 1.77315 301.775 +-348.841 1.7793 302.773 -348.816 1.77602 303.777 -348.774 1.75581 304.798 +-348.893 1.73187 305.686 -348.911 1.85375 306.661 -348.859 2.21574 307.269 +-348.695 1.66387 325.876 -348.884 1.85619 326.915 -349.174 2.13218 327.811 +-348.723 2.33899 327.447 -349.29 2.28848 328.871 -349.237 2.18272 329.757 +-349.354 2.10355 330.843 -348.933 2.76163 307.811 -348.702 3.34065 308.251 +-349.198 3.20027 308.777 -349.42 3.28572 309.734 -348.855 2.77399 328.033 +-349.212 3.30661 328.752 -348.847 3.34434 328.205 -349.367 3.05421 329.933 +-348.723 3.58759 308.887 -348.88 3.84858 309.902 -348.7 4.34259 310.246 +-349.267 4.20984 310.746 -348.847 4.34896 327.267 -348.706 3.7705 327.76 +-349.143 4.29457 328.001 -349.267 4.29479 328.851 -349.42 4.13159 329.688 +-348.791 4.63951 310.965 -348.96 4.86052 311.787 -348.598 5.22791 312.292 +-348.78 5.27703 326.337 -348.782 4.74958 326.815 -349.287 5.28017 326.903 +-349.392 5.27185 327.932 -349.399 5.02667 328.888 -348.92 5.76401 312.887 +-348.74 6.21924 313.415 -348.795 6.24189 324.337 -348.797 5.82455 324.906 +-349.312 6.39682 324.91 -348.793 5.70932 325.557 -349.309 6.04152 325.918 +-348.847 6.64352 313.898 -348.759 6.79319 314.826 -348.773 6.72218 315.87 +-348.974 6.86229 316.669 -349.232 7.28651 317.023 -348.486 7.26836 317.307 +-348.793 7.26082 322.333 -348.793 6.82034 322.911 -349.307 7.39738 322.907 +-348.794 6.68351 323.555 -349.308 7.01597 323.934 -348.933 7.64993 317.702 +-348.769 7.74755 318.606 -349.322 8.23953 319.12 -348.796 7.73098 319.617 +-349.274 8.25209 320.056 -348.803 7.77328 320.619 -349.293 8.29168 321.062 +-348.794 7.74038 321.563 -349.299 8.06182 321.937 -349.47 12.4836 322.683 +-349.287 12.3614 323.602 -349.294 12.2141 324.611 -349.312 12.2445 325.597 +-349.283 12.24 326.58 -349.379 12.2856 327.673 -349.382 13.2213 319.855 +-349.301 13.2551 320.551 -349.333 13.2073 321.708 -349.173 13.1751 322.745 +-348.797 12.7937 324.105 -348.883 13.1348 323.373 -348.815 12.7449 325.055 +-348.638 12.742 326.054 -348.752 12.5866 326.944 -348.85 12.8288 327.883 +-348.734 13.3242 328.269 -349.262 13.2135 328.778 -349.367 13.2868 329.609 +-349.366 13.276 330.534 -349.442 14.2493 306.687 -349.384 14.3046 313.782 +-349.291 14.2461 314.621 -349.238 14.2551 315.551 -348.876 14.1765 316.236 +-349.019 13.9313 316.896 -348.738 14.2213 317.32 -349.151 14.1712 317.771 +-349.288 14.2596 318.596 -348.826 14.2394 319.353 -348.754 13.716 319.975 +-348.708 13.6408 320.989 -348.739 13.6789 322.007 -348.62 13.6275 322.942 +-348.698 13.6341 328.953 -348.738 13.6721 329.988 -348.833 13.6682 331.009 +-348.632 13.726 331.925 -349.008 13.8261 332.709 -348.557 14.2026 333.358 +-348.553 14.777 305.867 -348.984 15.3019 305.361 -348.92 14.6294 306.937 +-348.658 14.73 307.968 -348.779 14.728 308.833 -348.757 14.7082 309.852 +-348.751 14.6949 310.858 -348.767 14.7146 311.846 -348.771 14.7441 312.838 +-348.772 14.6525 313.936 -348.675 14.6315 315.048 -348.533 14.5654 315.97 +-348.673 14.6617 317.895 -348.702 14.7762 318.903 -348.998 14.7149 333.846 +-348.669 15.3014 334.332 -348.745 16.2088 305.289 -349.405 16.3344 334.934 +-348.845 15.8175 334.787 -348.755 16.3303 335.235 -348.773 17.235 305.33 +-348.726 17.1422 335.43 -348.824 18.2432 305.331 -349.26 18.384 334.76 +-348.941 18.0111 335.249 -348.844 19.2613 305.311 -348.972 19.1529 334.378 +-348.549 18.6927 334.822 -348.698 20.142 305.306 -348.71 20.2018 334.306 +-349.429 21.152 305.809 -348.917 21.2397 305.394 -348.77 21.2536 334.368 +-348.823 22.2751 305.267 -348.889 22.1794 334.214 -348.598 23.1554 305.394 +-348.867 23.2292 333.381 -348.75 22.6422 333.764 -348.989 23.7147 305.841 +-348.741 24.2849 306.272 -348.758 24.2371 333.3 -348.759 25.1739 306.373 +-348.792 25.2579 333.327 -348.781 26.2094 306.346 -348.794 26.255 333.327 +-348.808 27.2499 306.317 -348.786 27.2494 333.329 -348.826 28.2726 306.277 +-348.789 28.2457 333.33 -348.631 29.0279 306.449 -348.819 29.2728 333.294 +-349.047 29.6733 306.838 -348.742 30.1868 307.332 -348.715 30.2752 333.366 +-348.891 31.1589 307.233 -349.328 31.3673 333.915 -348.849 30.8708 333.749 +-348.712 31.335 334.228 -348.743 31.6094 306.741 -348.865 32.2218 306.365 +-348.869 32.3036 334.32 -348.763 33.2216 306.299 -348.951 33.4399 333.447 +-348.549 32.7942 333.836 -349.263 33.1306 333.913 -348.798 34.2475 306.336 +-348.753 34.203 333.278 -348.801 35.2463 306.335 -348.802 35.2485 333.294 +-348.797 36.2445 306.331 -348.729 36.2609 333.389 -348.797 37.2446 306.33 +-349.34 37.3531 333.91 -348.852 36.8572 333.756 -348.706 37.3044 334.212 +-348.797 38.2436 306.327 -348.78 38.203 334.385 -348.792 39.2398 306.325 +-348.803 39.2333 334.338 -348.788 40.2526 306.354 -348.794 40.236 334.312 +-348.864 41.3168 306.282 -348.783 41.2411 334.337 -348.535 41.7914 305.833 +-348.963 42.4412 305.446 -349.241 42.1224 305.909 -348.796 42.2778 334.385 +-348.803 43.2334 305.257 -349.36 43.1813 314.915 -349.287 43.2059 315.63 +-349.249 43.2296 316.58 -348.928 43.1646 317.294 -348.881 42.874 317.908 +-348.716 42.7865 318.894 -348.758 42.6928 319.839 -348.768 42.7081 320.831 +-348.754 42.7124 321.834 -348.736 42.7097 322.845 -348.869 42.712 323.722 +-348.751 42.7849 324.806 -349.054 42.8309 325.778 -348.649 43.4267 326.242 +-348.863 43.1851 334.221 -348.707 44.1598 305.277 -349.334 44.3549 313.726 +-348.916 44.0907 314.233 -348.757 43.6752 314.883 -348.623 43.7448 315.957 +-348.604 43.5843 317.001 -349.385 44.1301 326.689 -348.751 44.1498 326.471 +-348.902 44.1618 333.336 -348.738 43.6437 333.79 -349.4 45.1578 305.755 +-348.71 45.1339 305.441 -349.093 45.2545 312.39 -348.613 44.9224 313.055 +-348.696 44.5993 313.722 -348.824 44.6403 327.057 -348.751 44.7435 327.816 +-348.937 44.8669 328.653 -348.789 45.3071 329.21 -348.99 45.2992 332.355 +-348.552 44.7582 332.85 -348.885 45.6667 305.889 -348.68 46.1926 306.308 +-349.055 46.2433 310.369 -348.643 45.896 311.033 -348.771 45.6528 311.869 +-348.796 45.694 329.778 -348.855 45.8227 330.782 -348.785 45.7146 331.719 +-348.916 46.657 306.78 -348.81 46.8639 307.692 -348.775 46.8344 308.873 +-348.732 46.6533 309.888 -347.654 -10.0795 299.779 -347.94 -9.72873 299.416 +-348.342 -9.78173 300.072 -348.233 -9.687 300.894 -347.607 -10.2689 300.707 +-347.67 -9.79725 301.401 -348.402 -8.84045 296.783 -347.734 -9.28697 298.89 +-348.025 -8.75574 298.296 -348.026 -9.41177 301.668 -348.319 -8.7211 303.112 +-347.906 -9.24597 302.593 -348.298 -8.72196 304.055 -347.798 -9.2451 303.603 +-348.177 -8.65282 304.896 -347.65 -9.23469 304.765 -347.641 -8.78685 305.406 +-348.179 -7.63644 295.71 -347.966 -7.97452 296.24 -347.822 -8.29586 297.037 +-347.631 -8.37846 297.813 -347.98 -8.41725 305.694 -347.954 -8.28071 306.708 +-347.708 -7.76085 307.334 -347.667 -7.33451 295.929 -347.971 -6.90279 295.45 +-348.291 -6.62761 307.904 -347.807 -7.08656 307.825 -347.685 -6.78093 308.353 +-348.275 -5.71749 294.825 -347.72 -5.8195 295.301 -347.887 -6.36352 308.769 +-348.213 -5.70846 309.966 -347.817 -6.1146 309.749 -347.667 -5.81463 310.402 +-348.17 -4.62239 293.721 -347.972 -4.94395 294.265 -347.755 -5.24045 294.835 +-347.9 -5.40369 310.771 -348.254 -4.68464 311.96 -347.856 -5.12764 311.737 +-347.668 -4.81241 312.398 -347.651 -4.34474 293.942 -347.946 -3.91643 293.462 +-347.89 -4.37253 312.782 -347.773 -4.25805 313.862 -348.238 -3.68181 314.942 +-347.848 -4.14695 314.752 -347.677 -3.83427 315.379 -348.401 -2.62384 292.877 +-347.934 -2.87747 293.299 -347.871 -3.36275 315.769 -347.861 -3.24369 316.749 +-347.913 -3.12645 317.706 -347.753 -2.66819 318.207 -347.883 -1.71644 293.367 +-347.718 -2.33279 318.816 -347.999 -2.16729 319.878 -347.49 -1.76756 320.358 +-348.179 -1.83821 320.696 -347.771 -0.884133 293.435 -347.961 -1.42105 321.015 +-347.853 -1.09263 321.686 -347.856 -0.787153 322.235 -347.99 -0.375975 293.751 +-347.726 -0.22398 294.724 -348.209 0.328804 294.931 -347.619 0.177045 295.44 +-347.892 -0.302518 322.795 -347.81 0.514011 323.283 -348.299 0.322082 323.828 +-348.071 0.725559 295.663 -347.579 1.18971 296.307 -348.129 1.06262 323.83 +-347.842 1.19968 323.414 -348.401 1.16504 324.658 -347.809 1.73896 296.904 +-347.786 1.81867 297.811 -347.779 1.79129 298.838 -347.786 1.77714 299.846 +-347.782 1.77139 300.846 -347.792 1.76368 301.837 -347.808 1.75724 302.819 +-347.785 1.74224 303.817 -347.743 1.70843 304.835 -347.967 1.78579 305.887 +-347.495 2.35783 306.366 -348.255 2.19248 306.849 -347.7 1.68038 323.839 +-347.961 1.84507 324.965 -347.698 2.3373 325.38 -348.169 2.26485 325.882 +-348.452 2.32687 326.803 -347.964 2.80846 307.021 -347.679 3.37133 307.179 +-348.255 3.21034 307.664 -347.82 2.78432 325.992 -348.152 3.05769 326.719 +-347.743 3.54718 326.37 -348.457 3.1072 327.626 -347.775 3.65686 307.931 +-347.998 3.85177 308.943 -347.552 4.2572 309.289 -348.27 4.19522 309.634 +-348.335 4.18013 326.898 -347.81 4.3614 326.303 -347.869 4.65394 310.033 +-347.668 4.77281 310.903 -347.913 4.81649 311.785 -347.747 5.1664 312.422 +-347.796 5.24616 324.348 -347.785 4.832 324.937 -348.291 5.41501 324.925 +-347.763 4.70848 325.618 -348.271 5.06409 325.989 -347.85 5.59628 312.91 +-347.819 5.91377 313.709 -348.206 6.35689 313.936 -347.604 6.21645 314.392 +-347.796 6.24966 322.332 -347.802 5.80975 322.904 -348.297 6.40463 322.908 +-347.801 5.67879 323.555 -348.299 6.03089 323.932 -347.972 6.64196 314.654 +-347.866 6.66713 315.631 -347.779 6.70433 316.809 -347.652 7.22877 317.447 +-347.873 7.32408 318.398 -347.535 6.92022 318.643 -348.246 7.22777 319.121 +-347.683 6.74044 319.579 -348.336 7.24118 320.067 -347.831 6.77244 320.618 +-348.31 7.29159 321.068 -347.802 6.73445 321.563 -348.294 7.06515 321.943 +-348.004 7.56662 317.885 -348.452 13.3454 323.715 -348.332 13.2719 324.581 +-348.267 13.276 325.531 -347.888 13.1876 326.228 -348.006 12.9051 326.907 +-347.71 13.2097 327.31 -348.201 13.1522 327.692 -348.456 14.3341 316.804 +-348.275 14.2094 319.789 -347.648 14.2925 320.286 -347.891 13.869 320.833 +-347.934 13.8529 321.796 -347.777 13.8541 322.855 -347.819 13.6677 323.886 +-347.71 13.6528 325.019 -347.555 13.593 325.962 -347.82 13.6796 327.89 +-347.671 13.8528 328.978 -347.812 13.7276 329.794 -347.769 13.727 330.797 +-347.735 13.7066 331.825 -347.973 13.8031 332.741 -347.551 14.2019 333.391 +-347.596 14.7445 305.758 -347.95 15.2591 305.354 -347.797 14.7686 306.718 +-347.905 14.8349 307.861 -347.874 14.8243 308.875 -347.868 14.81 309.886 +-347.862 14.8061 310.891 -347.867 14.8079 311.887 -347.869 14.822 312.878 +-347.872 14.8313 313.862 -347.882 14.8272 314.842 -347.872 14.8232 315.826 +-347.999 14.8138 316.864 -347.72 15.3804 317.4 -348.178 15.1909 317.765 +-348.269 15.2879 318.6 -348.012 15.1071 319.342 -347.75 14.7321 319.842 +-348.018 14.7249 333.849 -347.554 15.2152 334.338 -347.709 16.1827 305.281 +-347.822 15.7277 318.12 -347.741 15.6601 318.943 -347.979 15.6948 334.863 +-347.774 16.3311 335.296 -347.746 17.2185 305.315 -347.838 17.264 335.413 +-347.786 18.2358 305.326 -347.783 18.2809 335.278 -347.816 19.2481 305.33 +-347.883 19.3584 334.423 -347.77 18.8681 334.887 -347.812 20.2505 305.335 +-347.843 20.3153 334.303 -347.805 21.24 305.323 -347.825 21.2711 334.238 +-347.831 22.2487 305.301 -347.942 22.4298 333.438 -347.513 21.7425 333.784 +-348.248 22.122 333.893 -347.615 23.3343 305.386 -347.729 23.1873 333.256 +-348.063 23.783 305.835 -347.689 24.476 306.23 -347.737 24.2142 333.284 +-347.878 25.3091 306.375 -347.762 25.2288 333.296 -347.769 26.2088 306.355 +-347.758 26.2249 333.293 -347.824 27.2574 306.31 -347.755 27.2198 333.296 +-347.725 28.2667 306.373 -347.767 28.2284 333.283 -348.327 29.3447 306.919 +-347.841 28.8526 306.752 -347.696 29.3154 307.234 -347.773 29.2567 333.254 +-347.858 30.2825 307.325 -347.597 30.044 333.435 -347.55 30.7907 306.839 +-347.958 31.4325 306.44 -348.263 31.1133 306.911 -348.053 30.6878 333.833 +-347.744 31.2017 334.317 -347.756 32.1997 306.273 -347.842 32.1636 334.297 +-347.773 33.2337 306.319 -347.998 33.3149 333.391 -347.597 32.7541 333.832 +-347.802 34.2489 306.338 -347.769 34.2137 333.291 -347.798 35.2452 306.332 +-347.827 35.2673 333.299 -347.796 36.2442 306.33 -347.597 36.169 333.408 +-347.797 37.2444 306.33 -348 36.7196 333.84 -347.748 37.3096 334.276 +-347.796 38.2428 306.325 -347.779 38.2169 334.374 -347.788 39.2416 306.332 +-347.803 39.2383 334.332 -347.803 40.2584 306.369 -347.794 40.2379 334.319 +-347.834 41.1629 306.271 -347.79 41.2456 334.35 -347.595 41.7487 305.835 +-348.008 42.3157 305.395 -347.862 42.3077 334.276 -347.798 43.2313 305.284 +-348.438 43.3111 317.772 -348.233 43.237 318.559 -347.908 43.1668 319.286 +-347.86 42.8669 319.924 -347.859 42.8018 320.878 -347.862 42.8111 321.891 +-347.841 42.8084 322.89 -348.02 42.849 323.945 -347.647 43.3156 324.283 +-348.146 43.1574 324.849 -348.359 43.193 325.662 -347.982 43.4583 333.482 +-347.549 42.7894 333.829 -348.24 43.1223 333.914 -347.852 44.3038 305.296 +-347.996 44.232 314.37 -347.995 43.9499 315.031 -347.683 44.2905 315.227 +-348.171 44.1843 315.678 -348.021 44.2088 316.25 -347.774 43.8905 316.917 +-347.811 43.6535 317.909 -347.579 43.5902 318.983 -347.699 43.582 324.923 +-347.823 43.7506 325.776 -347.862 44.2218 326.24 -347.708 44.2512 333.237 +-347.634 45.2115 305.398 -348.37 45.3307 312.707 -347.833 45.1856 313.2 +-347.611 44.7213 313.852 -347.59 44.5844 315.9 -347.767 44.6769 326.707 +-347.924 44.8651 327.805 -347.694 45.3116 328.281 -348.248 45.1611 328.745 +-348.038 45.2806 332.408 -347.711 44.9013 332.933 -348.033 45.8101 305.798 +-347.545 46.2532 306.312 -348.344 46.2995 310.704 -347.868 46.1704 311.21 +-347.722 45.8386 311.82 -347.741 45.5962 312.722 -347.821 45.6469 328.974 +-347.726 45.7643 329.907 -347.84 45.7795 330.784 -347.734 45.7072 331.883 +-348.031 46.7662 306.942 -347.585 47.2785 307.342 -348.221 47.2156 307.733 +-348.303 47.2676 308.55 -347.971 47.1867 309.302 -347.735 46.8888 309.982 +-347.71 46.5795 310.76 -347.784 47.6523 308.072 -347.628 47.645 309.014 +-346.566 -11.1061 300.745 -347.001 -10.7381 300.386 -347.231 -10.7944 300.864 +-346.846 -10.7693 301.365 -346.864 -10.3982 299.825 -346.904 -9.86562 299.284 +-347.302 -9.73049 302.11 -347 -10.231 301.717 -347.258 -9.7439 303.049 +-346.732 -10.2283 302.59 -347.313 -9.75639 304.088 -346.768 -10.2555 303.604 +-347.228 -9.75459 304.824 -346.788 -10.2133 304.59 -346.829 -9.81516 305.275 +-346.594 -9.25391 298.819 -346.888 -8.66029 298.321 -347.289 -8.74986 306.166 +-346.969 -9.23768 305.767 -347.263 -8.62216 306.9 -346.752 -9.19628 306.619 +-346.704 -8.79365 307.196 -347.318 -7.50959 296.695 -346.777 -7.69605 297.349 +-346.818 -8.15841 297.775 -346.625 -7.91988 307.431 -347.433 -6.67495 295.921 +-347.154 -6.60689 296.631 -346.694 -6.8722 297.202 -347.03 -7.3875 307.774 +-347.211 -6.65848 308.95 -346.785 -7.22832 308.686 -346.641 -6.79729 309.386 +-347.219 -5.9191 295.944 -346.696 -6.23021 296.95 -346.884 -5.89248 296.418 +-346.995 -6.40003 309.667 -347.249 -5.66531 310.958 -346.781 -6.25141 310.72 +-346.679 -5.78259 311.388 -347.298 -4.48984 294.697 -346.967 -4.83481 295.331 +-346.824 -5.33163 295.874 -346.893 -5.37565 311.787 -347.233 -4.66567 312.959 +-346.845 -5.11104 312.743 -346.67 -4.7916 313.401 -347.401 -3.72737 293.925 +-347.131 -3.63503 294.588 -346.612 -3.83545 295.31 -346.872 -4.35028 313.787 +-346.754 -4.252 314.869 -347.232 -3.70078 315.951 -346.835 -4.15012 315.763 +-346.662 -3.85884 316.423 -347.271 -2.69019 293.866 -346.676 -3.22346 294.852 +-346.876 -2.88462 294.402 -346.879 -3.31635 316.931 -346.68 -2.77489 317.454 +-347.238 -2.79295 317.678 -347.251 -1.49346 293.908 -346.765 -2.06398 294.321 +-346.912 -2.39386 318.044 -346.659 -2.22633 318.913 -346.995 -2.1453 319.695 +-346.545 -1.77553 320.301 -347.447 -0.700044 293.982 -346.805 -1.32557 294.694 +-347.166 -0.862383 294.647 -346.79 -0.811435 295.293 -347.016 -1.26044 320.922 +-346.508 -0.621865 321.334 -347.256 -0.79473 321.808 -346.973 -0.198622 295.751 +-347.352 0.399044 295.946 -346.704 0.241576 296.213 -346.914 -0.244713 322.035 +-346.768 0.518771 322.238 -347.263 0.206165 322.694 -346.659 1.11499 296.424 +-347.304 1.07441 322.9 -346.792 1.19385 322.355 -347.014 1.666 296.764 +-346.875 1.71748 297.567 -346.858 1.66876 298.613 -346.882 1.65316 299.635 +-346.775 1.68165 300.768 -346.753 1.79152 301.876 -346.768 1.7797 302.865 +-346.763 1.75122 303.865 -346.727 1.69984 304.883 -346.962 1.78613 305.765 +-346.668 2.20144 306.21 -346.807 1.74349 322.773 -346.732 2.332 323.375 +-347.19 2.28585 323.861 -347.441 2.33774 324.757 -347.395 3.16775 306.672 +-346.759 2.99187 306.442 -346.847 2.77265 323.989 -347.163 3.17869 324.831 +-346.748 3.51778 324.355 -347.411 3.05946 325.655 -346.921 3.64271 306.999 +-346.649 3.799 307.896 -346.898 3.84154 308.776 -346.745 4.18473 309.404 +-347.302 4.33724 324.861 -346.813 4.32861 324.3 -347.405 3.98654 325.813 +-346.867 4.64029 309.86 -346.79 4.81391 310.769 -346.777 4.75235 311.822 +-346.834 4.86843 312.733 -347.216 5.32024 312.932 -346.62 5.1738 313.39 +-346.833 5.39166 322.269 -346.694 4.88277 322.786 -347.281 5.39953 322.938 +-346.754 4.69353 323.626 -347.279 5.02977 323.994 -346.977 5.5968 313.638 +-346.902 5.75308 314.579 -347.31 6.27607 315.109 -346.714 5.74509 315.736 +-347.296 6.24738 316.069 -346.685 5.89421 316.79 -347.223 6.21824 316.77 +-346.76 6.24275 317.349 -346.863 6.19637 319.398 -346.809 5.81335 319.856 +-347.269 6.24173 320.084 -346.798 5.76121 320.576 -347.303 6.28274 321.057 +-346.78 5.72967 321.543 -347.284 6.05492 321.955 -347.016 6.77658 317.815 +-347.408 7.21779 317.931 -346.878 6.6124 318.748 -347.45 13.3472 326.801 +-347.33 14.1834 320.876 -347.282 14.2007 321.598 -347.252 14.2316 322.554 +-346.932 14.1674 323.276 -346.9 13.896 323.888 -346.725 13.8102 324.87 +-346.753 13.712 325.82 -346.972 13.7837 326.856 -346.556 14.2455 327.367 +-347.238 14.223 327.732 -347.316 14.2964 328.524 -346.94 14.1833 329.261 +-346.88 13.8672 329.898 -346.862 13.8048 330.854 -346.839 13.806 331.887 +-347.011 13.8534 332.797 -346.688 14.2798 333.266 -346.969 14.8416 305.999 +-346.964 15.2242 305.417 -346.647 15.3528 306.286 -347.195 15.1301 306.749 +-347.337 15.252 307.618 -347.308 15.2614 308.634 -347.299 15.2449 309.638 +-347.303 15.2449 310.638 -347.301 15.2469 311.638 -347.285 15.2454 312.645 +-347.273 15.2361 313.651 -347.269 15.2123 314.646 -347.267 15.3015 315.777 +-347.43 15.3321 316.795 -347.323 15.5626 319.726 -346.944 15.1278 320.346 +-346.84 14.7083 321.016 -346.658 14.7207 322.074 -346.585 14.5761 323.01 +-346.781 14.6477 328.054 -346.622 14.6446 328.964 -346.894 14.7211 333.757 +-346.571 15.1939 334.344 -346.706 16.2727 305.37 -346.758 15.5967 307.077 +-346.748 15.6922 308.021 -346.734 15.6883 309.018 -346.736 15.6834 310.02 +-346.721 15.68 311.03 -346.694 15.6699 312.039 -346.682 15.6593 313.059 +-346.788 15.6111 313.996 -346.705 15.7247 314.954 -346.872 15.871 315.947 +-347.125 16.0251 316.725 -346.822 16.2689 316.378 -347.311 16.1293 317.622 +-347.338 16.1939 318.753 -347.144 16.2895 319.634 -346.791 16.0015 320.304 +-346.975 15.6811 334.869 -346.748 16.297 335.277 -346.863 17.3531 305.302 +-346.718 16.7047 316.893 -346.856 16.7964 318.059 -346.712 16.7688 319.063 +-346.622 16.6118 320.02 -346.796 17.2198 335.388 -346.75 18.231 305.322 +-346.712 18.1301 335.376 -346.794 19.2381 305.353 -347.219 19.3191 334.733 +-346.804 19.118 335.229 -346.683 20.3452 305.268 -347.032 20.1331 334.445 +-346.651 19.802 334.902 -346.92 21.3367 305.356 -346.713 21.2757 334.368 +-346.823 22.2302 305.336 -347.028 22.2556 333.418 -346.718 21.8013 333.85 +-346.735 23.1581 305.297 -346.835 23.1584 333.381 -347.367 24.1369 305.753 +-346.698 24.1334 305.425 -346.859 24.1857 333.402 -346.826 24.6312 305.903 +-346.821 25.2093 306.278 -346.867 25.1895 333.399 -346.773 26.2156 306.361 +-346.863 26.1856 333.399 -346.839 27.2715 306.298 -346.864 27.185 333.399 +-346.594 28.1762 306.381 -346.866 28.2067 333.381 -346.991 28.7368 306.833 +-346.767 29.3424 307.306 -346.859 29.1762 333.43 -346.824 30.1501 307.306 +-347.327 30.3908 333.9 -346.881 29.8164 333.729 -346.675 30.2031 334.232 +-346.602 30.7583 306.836 -347.004 31.32 306.387 -346.819 31.2797 334.421 +-346.76 32.215 306.293 -346.898 32.1825 334.217 -346.789 33.2462 306.331 +-346.861 33.2178 333.384 -346.739 32.6287 333.752 -346.801 34.2478 306.335 +-346.783 34.2348 333.303 -346.797 35.2443 306.331 -346.848 35.2934 333.292 +-346.796 36.2442 306.33 -346.599 36.1921 333.387 -346.797 37.2445 306.329 +-346.985 36.7555 333.831 -346.748 37.3386 334.257 -346.793 38.2433 306.326 +-346.786 38.231 334.361 -346.788 39.2454 306.345 -346.804 39.2465 334.327 +-346.821 40.2678 306.381 -346.79 40.2423 334.328 -346.901 41.1644 306.205 +-346.801 41.2562 334.368 -346.741 41.6015 305.754 -346.856 42.1994 305.385 +-346.829 42.1583 334.269 -346.769 43.2226 305.298 -347.412 43.3135 319.77 +-347.267 43.2288 320.62 -347.284 43.2329 321.636 -347.274 43.2346 322.632 +-347.315 43.2339 323.562 -347.038 43.3437 333.408 -346.604 42.7567 333.833 +-346.836 44.2788 305.302 -347.44 44.2713 314.655 -347.405 44.3457 316.69 +-346.902 44.1758 317.273 -346.883 43.8906 317.887 -346.738 43.827 318.87 +-346.777 43.6447 319.915 -346.68 43.629 321.048 -346.692 43.6421 322.049 +-346.66 43.6307 323.035 -346.814 43.6465 324.041 -346.947 43.8636 324.982 +-346.461 44.2361 325.385 -347.22 44.2095 325.7 -346.666 44.1409 333.326 +-346.625 45.1859 305.398 -347.032 45.2432 313.384 -346.748 44.9268 314.012 +-346.876 44.7248 314.943 -346.724 44.7639 315.91 -346.731 44.6375 316.861 +-346.928 44.6088 326.059 -346.871 44.8947 326.9 -346.712 45.3246 327.248 +-347.246 45.1798 327.707 -347.34 45.3679 332.755 -346.845 45.0363 333.247 +-347.029 45.7764 305.795 -346.555 46.2273 306.331 -347.025 46.2317 311.331 +-346.596 45.8249 311.893 -346.754 45.6541 312.859 -346.817 45.6555 327.944 +-346.684 45.7681 328.908 -346.81 45.7412 329.802 -346.818 45.7326 330.831 +-346.81 45.7769 331.874 -346.741 45.6689 332.8 -347.02 46.7282 306.821 +-346.618 47.2584 307.3 -347.37 47.3453 309.755 -346.832 47.2111 310.257 +-346.667 46.7473 310.762 -346.89 47.6984 307.887 -346.853 47.8425 308.811 +-346.725 47.6511 309.756 -345.553 -12.1497 303.816 -345.883 -11.6649 303.253 +-346.207 -11.6503 303.942 -346.229 -11.6147 304.942 -345.749 -12.1651 304.799 +-345.737 -11.8683 305.412 -345.716 -11.1311 299.895 -345.927 -10.5606 299.439 +-346.367 -10.87 299.92 -345.662 -11.3925 300.64 -346.316 -10.736 302.114 +-345.95 -11.2605 301.617 -346.266 -10.914 303.039 -345.769 -11.3093 302.559 +-346.423 -10.8012 303.938 -346.405 -10.7608 304.861 -345.724 -11.3436 305.861 +-346.223 -10.6796 305.845 -345.751 -10.7896 306.357 -345.793 -9.86388 299.204 +-346.436 -9.87575 305.931 -345.719 -10.3525 306.865 -346.319 -9.7649 306.746 +-345.832 -9.66455 307.314 -345.607 -9.2328 298.84 -345.957 -8.74141 298.455 +-345.902 -8.80422 307.421 -346.207 -7.64637 297.769 -345.749 -7.90586 298.204 +-346.36 -7.70006 308.052 -345.905 -8.21417 307.741 -346.245 -7.74283 308.74 +-345.646 -8.24426 308.729 -345.822 -7.75668 309.33 -345.659 -7.17269 297.913 +-346.016 -6.95492 297.517 -346.346 -6.75521 310.109 -345.892 -7.25225 309.817 +-346.237 -6.77657 310.764 -345.69 -7.10145 310.786 -345.823 -6.77354 311.356 +-346.351 -5.4762 296.718 -345.845 -5.90075 297.361 -346.212 -5.6475 311.949 +-345.803 -6.24223 311.815 -345.639 -5.7856 312.37 -346.453 -4.68018 295.947 +-346.149 -4.59219 296.639 -345.68 -4.83164 297.202 -345.996 -5.39334 312.666 +-346.216 -4.63615 313.953 -345.752 -5.24056 313.721 -345.628 -4.77999 314.389 +-346.236 -3.93795 295.953 -345.673 -4.21207 296.933 -345.88 -3.88424 296.394 +-345.975 -4.37387 314.674 -345.768 -4.34739 315.79 -346.278 -3.78875 316.872 +-345.947 -4.11885 316.663 -345.633 -3.70337 317.257 -346.325 -2.50749 294.922 +-345.994 -2.9313 295.334 -345.836 -3.34838 295.845 -346.262 -2.63741 317.864 +-345.857 -3.13548 317.731 -345.693 -2.78436 318.335 -346.411 -1.61185 295.072 +-345.826 -2.35833 295.673 -346.087 -1.73001 295.739 -345.601 -1.72483 296.233 +-345.897 -2.35811 318.722 -345.843 -2.16917 319.823 -345.676 -1.85571 320.488 +-346.37 -0.821896 295.853 -345.888 -0.751065 296.245 -346.071 -1.32486 320.808 +-345.662 -0.93927 321.219 -345.742 0.200163 296.417 -346.427 0.179816 321.708 +-345.846 0.160838 321.363 -346.351 1.31615 297.03 -345.8 0.802865 296.873 +-345.669 0.878795 297.803 -346.266 1.25765 298.025 -345.676 0.847075 298.84 +-346.294 1.23043 299.052 -345.711 0.826968 299.837 -346.319 1.23523 300.055 +-345.721 0.888947 300.767 -346.182 1.32136 300.896 -345.672 1.18772 301.389 +-346.307 1.0727 321.848 -345.528 1.30407 321.414 -345.992 1.62431 301.675 +-345.875 1.68216 302.619 -345.78 1.67283 303.741 -345.732 1.72888 304.884 +-345.97 1.79991 305.783 -345.717 2.32401 306.34 -345.955 1.76369 322 +-345.765 2.45218 322.29 -346.272 2.29894 322.82 -346.288 3.37131 306.856 +-345.8 2.86829 306.839 -345.694 3.20109 307.345 -346.165 3.30014 322.871 +-345.846 3.27316 322.226 -346.43 3.02652 323.653 -345.881 3.66595 307.726 +-345.748 3.77475 308.825 -345.821 3.88237 309.738 -346.223 4.3415 309.929 +-345.643 4.1909 310.395 -345.792 4.28454 321.344 -345.799 3.75456 321.844 +-346.148 4.21837 321.931 -346.37 4.36457 322.868 -346.411 3.96314 323.808 +-346.002 4.63838 310.625 -345.771 4.69266 311.746 -345.745 4.75903 312.879 +-345.788 4.83645 313.812 -346.318 5.23418 314.109 -345.785 4.86513 314.709 +-346.317 5.23439 315.046 -345.583 4.72914 315.728 -346.208 5.31553 315.896 +-345.674 5.1967 316.4 -345.79 5.2564 319.311 -345.803 4.80442 319.891 +-346.297 5.41033 319.868 -345.798 4.69902 320.548 -346.293 5.14032 321.037 +-346.44 5.04973 321.989 -346.019 5.58575 316.676 -345.887 5.77365 317.619 +-346.288 6.29342 318.109 -345.752 5.7453 318.548 -346.265 6.06903 318.912 +-346.43 14.302 323.776 -346.233 14.2233 324.55 -345.889 14.1428 325.27 +-345.827 13.8495 325.907 -346.017 13.8486 326.733 -345.685 14.3776 327.288 +-346.41 14.3133 329.773 -346.271 14.2337 330.631 -346.265 14.237 331.653 +-346.301 14.2254 332.621 -346.491 15.335 305.696 -346.458 15.4529 320.851 +-346.254 15.3741 321.547 -345.959 15.1405 322.251 -345.931 14.9091 322.881 +-345.767 14.7443 323.953 -345.569 14.5565 324.994 -345.862 14.6494 327.896 +-345.863 14.8059 328.762 -345.818 14.7038 329.893 -345.698 14.6784 331.034 +-345.659 14.663 332.05 -345.814 14.6315 333.095 -345.938 14.8282 333.85 +-345.576 15.341 334.35 -346.197 16.1167 305.886 -345.811 15.8155 306.755 +-345.78 16.2541 306.238 -345.851 15.7663 307.776 -345.831 15.784 308.764 +-345.819 15.7777 309.769 -345.792 15.7687 310.785 -345.767 15.7577 311.808 +-345.905 15.7599 312.685 -345.969 15.8711 313.802 -345.711 16.3242 314.256 +-346.198 16.1784 314.758 -346.428 16.3 315.699 -346.315 16.169 320.864 +-345.538 15.6504 321.952 -345.7 16.2598 321.35 -346.045 15.7286 334.861 +-345.69 16.4746 335.221 -345.529 16.692 305.801 -345.982 17.1797 305.39 +-345.638 16.6053 314.958 -345.96 16.8122 315.996 -345.591 17.2655 316.365 +-346.25 17.224 316.744 -346.385 17.3157 317.583 -346.297 17.2894 318.524 +-345.95 17.1826 319.246 -345.926 16.9305 319.909 -345.815 16.7922 320.772 +-345.911 17.3493 335.376 -345.751 18.2298 305.325 -345.785 17.6364 317.057 +-345.759 17.7024 318.001 -345.602 17.6167 318.973 -345.814 18.2751 335.369 +-345.895 19.2857 305.253 -345.7 19.188 335.372 -345.647 19.8136 304.798 +-345.682 20.3002 304.518 -346.193 20.1012 304.861 -346.348 20.5575 334.866 +-345.873 20.0866 335.285 -345.883 20.5759 304.865 -345.847 21.1904 305.265 +-346.229 21.242 334.878 -345.768 21.0633 335.169 -345.789 22.2101 305.359 +-346.323 22.4568 333.802 -345.837 22.0522 334.274 -345.813 21.5869 334.747 +-345.851 23.2767 305.302 -346.278 23.4293 333.83 -345.687 23.0321 334.243 +-345.603 24.2013 305.388 -346.3 24.4392 333.835 -345.728 24.0556 334.27 +-345.985 24.7817 305.824 -345.728 25.325 306.263 -346.299 25.4368 333.831 +-345.735 25.0561 334.266 -345.795 26.2202 306.357 -346.298 26.4344 333.832 +-345.731 26.0536 334.264 -345.848 27.2763 306.281 -346.289 27.4341 333.827 +-345.729 27.0494 334.265 -345.642 28.0531 306.437 -346.269 28.4417 333.82 +-345.71 28.0398 334.257 -346.042 28.7027 306.836 -345.733 29.1997 307.324 +-346.391 29.2965 333.886 -345.792 29.1513 334.27 -345.896 30.1729 307.233 +-345.8 30.2631 334.389 -345.747 30.6371 306.752 -345.866 31.2242 306.377 +-345.878 31.3218 334.288 -345.764 32.2296 306.308 -345.948 32.4372 333.449 +-345.541 31.7907 333.825 -346.254 32.127 333.9 -345.798 33.2493 306.336 +-345.748 33.2002 333.284 -345.8 34.2461 306.333 -345.8 34.2464 333.305 +-345.796 35.244 306.33 -345.851 35.2926 333.282 -345.796 36.2444 306.33 +-345.646 36.0521 333.451 -345.794 37.2452 306.329 -346.049 36.7077 333.821 +-345.72 37.1717 334.278 -345.791 38.2409 306.329 -345.803 38.2369 334.353 +-345.81 39.2378 306.342 -345.798 39.2483 334.321 -345.808 40.3597 306.267 +-345.783 40.2496 334.343 -345.695 40.8772 305.882 -345.862 41.4423 305.474 +-346.261 41.0838 305.899 -345.814 41.2743 334.384 -345.847 42.2878 305.279 +-345.894 42.1783 334.212 -345.742 43.1957 305.294 -345.876 43.252 333.385 +-345.741 42.6366 333.757 -345.802 44.2747 305.28 -346.404 44.3158 317.785 +-346.225 44.2518 318.565 -345.908 44.1751 319.278 -345.871 43.8879 319.896 +-345.881 43.8341 320.839 -345.911 43.8531 321.836 -345.894 43.8532 322.829 +-345.842 43.8291 323.826 -345.997 43.8743 324.712 -345.791 44.1931 325.28 +-345.773 44.3051 333.341 -345.597 45.1891 305.381 -346.401 45.3177 313.755 +-346.238 45.2508 314.546 -345.95 45.1894 315.256 -345.763 44.874 315.893 +-345.825 44.7425 316.776 -345.817 44.68 317.908 -345.583 44.6179 318.998 +-345.879 44.7333 325.837 -345.724 45.3383 326.251 -346.264 45.1954 326.703 +-345.806 45.2357 333.302 -346.045 45.7762 305.803 -345.574 46.4147 306.327 +-346.078 46.2105 311.341 -345.635 45.855 312.001 -345.797 45.7015 312.788 +-345.808 45.655 313.917 -345.602 45.6154 315.013 -345.836 45.6666 326.933 +-345.696 45.7754 327.892 -345.784 45.7482 328.788 -345.902 45.723 329.686 +-345.802 45.8088 330.77 -345.771 45.8452 331.953 -345.811 45.6992 332.815 +-346.08 46.7921 306.837 -345.74 47.3368 307.181 -345.97 47.2776 310.355 +-345.545 46.7354 310.936 -345.735 47.6761 307.803 -345.837 47.7953 308.81 +-345.776 47.6939 309.723 -344.549 -12.1482 300.756 -344.97 -11.7242 300.339 +-345.29 -11.7742 301.112 -345.307 -11.7577 302.076 -344.833 -12.127 301.704 +-345.44 -11.8347 302.923 -344.776 -12.2322 302.715 -344.802 -12.3607 303.713 +-344.904 -12.3742 304.658 -345.316 -11.7386 305.881 -344.933 -12.2163 305.607 +-344.838 -11.7629 306.274 -344.832 -11.4178 299.809 -344.834 -10.7813 299.395 +-345.358 -10.7127 306.818 -344.796 -11.204 306.847 -344.789 -10.7392 307.349 +-344.793 -9.84443 299.26 -345.269 -9.493 307.936 -344.786 -10.2822 307.836 +-344.674 -9.83113 308.405 -344.593 -9.20661 298.857 -345.036 -8.61197 298.43 +-345.495 -8.76125 307.953 -344.66 -9.34999 308.901 -345.256 -8.90041 308.616 +-344.807 -8.74648 309.373 -344.661 -7.83927 298.322 -345.227 -7.67958 309.952 +-344.772 -8.23967 309.856 -344.63 -7.79704 310.394 -345.376 -6.44572 297.848 +-344.866 -6.9327 298.259 -345 -7.41343 310.658 -345.297 -6.76584 311.861 +-344.732 -7.2551 311.767 -344.762 -6.75454 312.343 -345.179 -5.65621 297.766 +-344.665 -5.83795 298.204 -345.342 -5.73585 313.1 -344.995 -6.24339 312.7 +-345.211 -5.74821 313.76 -344.617 -6.2483 313.766 -344.776 -5.76128 314.343 +-344.678 -5.14978 297.888 -345.005 -4.91408 297.5 -345.327 -4.73556 315.116 +-344.992 -5.25912 314.719 -345.168 -4.6737 315.933 -344.817 -5.20691 315.556 +-344.538 -4.87783 316.273 -345.349 -3.48436 296.691 -344.927 -3.99964 297.347 +-344.891 -4.2239 316.829 -344.674 -3.7876 317.491 -345.471 -2.67848 296.107 +-345.177 -2.64942 296.845 -344.708 -2.85014 297.332 -344.982 -3.43736 317.829 +-345.239 -2.65916 318.943 -344.763 -3.24047 318.733 -344.641 -2.79139 319.414 +-345.176 -1.82758 296.669 -344.641 -1.76251 297.164 -344.848 -2.38435 319.804 +-345.343 -1.6579 320.942 -344.898 -2.1206 320.673 -344.717 -1.6823 321.223 +-344.765 -1.25062 296.807 -345.051 -0.840006 296.493 -344.714 -0.736387 321.433 +-345.182 0.359368 296.904 -344.861 -0.157428 296.695 -344.644 0.146408 297.224 +-344.985 0.396215 321.278 -344.974 0.619006 297.652 -344.872 0.665326 298.644 +-344.893 0.646756 299.66 -344.922 0.623573 300.636 -344.846 0.746952 301.707 +-345.332 1.26718 302.102 -344.67 0.854092 302.834 -345.299 1.2556 303.043 +-344.711 0.889916 303.781 -345.198 1.30465 303.89 -344.706 1.16909 304.418 +-344.676 1.13871 321.308 -344.884 1.61331 304.806 -344.976 1.84823 305.759 +-344.547 2.27688 306.33 -345.407 2.1426 321.754 -344.778 2.26786 321.361 +-344.798 2.65406 306.937 -344.839 2.89391 307.734 -345.21 3.33602 307.966 +-344.617 3.20787 308.422 -345.353 3.20089 321.756 -344.812 3.35171 321.275 +-344.959 3.62396 308.683 -344.859 3.64896 309.636 -344.829 3.73373 310.718 +-345.342 4.26417 311.116 -344.683 3.91247 311.782 -345.158 4.33637 311.912 +-344.644 4.2004 312.41 -344.97 4.2856 315.435 -344.542 3.90401 315.762 +-345.208 4.19628 315.872 -344.838 4.2041 316.363 -344.807 4.26277 319.322 +-344.794 3.82545 319.906 -345.293 4.40686 319.917 -344.767 3.70438 320.592 +-345.265 4.05552 320.987 -344.977 4.6199 312.697 -344.822 4.67648 313.656 +-344.947 4.60843 314.711 -345.01 4.74183 316.713 -345.31 5.27028 317.119 +-344.756 4.78002 317.607 -345.25 5.29817 318.061 -344.786 4.73957 318.547 +-345.292 5.06499 318.937 -345.374 14.3845 325.904 -345.435 14.1906 326.673 +-345.46 15.2837 322.748 -345.27 15.2539 323.551 -344.953 15.1857 324.279 +-344.741 14.8529 324.933 -344.924 14.7769 325.758 -345.164 14.9695 326.788 +-344.575 15.2476 326.349 -344.821 14.8697 327.867 -344.873 15.3179 327.311 +-344.811 14.7283 328.783 -344.81 14.7578 329.776 -344.794 14.7689 330.788 +-344.763 14.7653 331.793 -344.877 14.7377 332.687 -344.927 14.8371 333.669 +-344.676 15.4494 334.195 -344.565 15.7851 306.923 -345.057 16.2297 306.296 +-344.789 15.6881 307.822 -344.774 15.7114 308.83 -344.75 15.7123 309.833 +-344.723 15.7044 310.846 -344.842 15.7046 311.723 -344.912 15.8391 312.824 +-344.684 16.3136 313.264 -345.253 16.1824 313.71 -344.831 15.8855 321.962 +-344.851 16.3392 321.464 -344.791 15.7233 322.945 -344.58 15.5851 324.023 +-344.663 15.5994 327.016 -345.344 16.0783 334.777 -344.742 16.1388 334.502 +-344.561 16.7132 305.757 -344.969 17.3411 305.393 -344.784 16.6372 313.965 +-344.794 16.7394 314.811 -344.951 16.8757 315.641 -344.779 17.322 316.182 +-345.429 17.3324 319.782 -345.224 17.3081 320.606 -344.943 17.0836 321.202 +-344.802 16.6045 334.903 -344.827 17.2207 335.263 -344.753 18.2216 305.335 +-344.777 17.6916 316.775 -344.864 17.7946 317.769 -344.837 17.7963 318.788 +-344.8 17.7185 319.936 -344.565 17.6393 320.868 -344.777 18.2435 335.368 +-344.842 19.1438 305.261 -344.813 19.2872 335.375 -344.665 19.7822 304.819 +-344.815 20.4051 304.442 -344.88 20.3546 335.285 -345.056 20.7662 304.791 +-344.664 21.4767 305.229 -344.964 21.4875 334.48 -344.545 20.8146 334.833 +-345.252 21.1811 334.926 -344.905 22.3335 305.369 -344.779 22.2527 334.316 +-344.826 23.2534 305.313 -344.809 23.2904 334.359 -344.599 24.1725 305.4 +-344.842 24.3072 334.376 -344.982 24.7596 305.825 -344.735 25.3085 306.267 +-344.838 25.3035 334.371 -344.808 26.2188 306.346 -344.837 26.303 334.37 +-344.738 27.2544 306.382 -344.833 27.296 334.369 -345.329 28.3654 306.91 +-344.851 27.8568 306.746 -344.699 28.312 307.231 -344.817 28.2768 334.359 +-344.859 29.2827 307.325 -344.798 29.2586 334.353 -344.551 29.787 306.838 +-344.955 30.4445 306.443 -345.264 30.1298 306.912 -344.807 30.2624 334.373 +-344.754 31.2072 306.275 -344.832 31.1589 334.265 -344.773 32.235 306.318 +-344.997 32.3133 333.394 -344.595 31.7495 333.83 -344.802 33.2485 306.337 +-344.757 33.2073 333.298 -344.798 34.2449 306.332 -344.818 34.2579 333.307 +-344.796 35.244 306.33 -344.73 35.2677 333.386 -344.795 36.2452 306.331 +-345.339 36.3593 333.905 -344.852 35.8571 333.758 -344.702 36.3088 334.212 +-344.792 37.2456 306.33 -344.782 37.2137 334.385 -344.806 38.2312 306.322 +-344.806 38.2429 334.342 -344.836 39.2191 306.321 -344.791 39.2449 334.323 +-344.623 40.188 306.447 -344.804 40.2455 334.345 -345.234 41.2865 305.737 +-344.906 41.1318 306.197 -344.889 41.298 334.255 -344.673 41.7723 305.893 +-344.895 42.1932 305.409 -344.937 42.4232 333.427 -344.538 41.7408 333.783 +-345.256 42.1203 333.892 -344.863 43.3372 305.271 -344.73 43.1926 333.256 +-344.75 44.2604 305.247 -345.407 44.3213 319.78 -345.275 44.2451 320.619 +-345.304 44.2524 321.619 -345.305 44.2583 322.615 -345.246 44.2333 323.626 +-345.31 44.2504 324.728 -344.747 44.256 333.335 -344.603 45.0632 305.435 +-345.383 45.3306 315.702 -344.919 45.1695 316.262 -344.715 44.8401 316.917 +-344.797 44.7119 317.803 -344.818 44.7602 318.798 -344.817 44.6837 319.898 +-344.721 44.6751 321.027 -344.714 44.6767 322.028 -344.674 44.6547 323.039 +-344.743 44.6019 323.987 -344.823 44.8313 324.896 -344.714 45.3395 325.223 +-345.265 45.1991 325.699 -344.788 45.1786 333.285 -345.104 45.74 305.809 +-344.675 46.1319 306.229 -345.387 46.2748 311.64 -344.935 46.1484 312.237 +-344.702 45.8328 312.922 -344.917 45.7007 313.687 -344.704 45.8308 314.809 +-344.722 45.6278 315.892 -344.833 45.6726 325.924 -344.712 45.7832 326.893 +-344.796 45.7555 327.793 -344.751 45.716 328.839 -345.011 45.7976 329.913 +-344.578 46.2559 330.412 -345.219 46.1834 330.798 -345.298 46.2798 331.604 +-344.887 46.2191 332.274 -344.695 45.774 332.816 -345.373 47.156 306.747 +-344.683 47.1336 306.43 -345.043 47.2542 310.361 -344.636 46.8944 311.007 +-344.719 46.5944 311.846 -344.772 46.6571 331.117 -344.64 46.6332 331.871 +-344.839 47.6519 307.074 -344.796 47.7547 307.815 -344.703 47.8242 308.805 +-344.737 47.6574 309.884 -343.75 -27.1991 300.758 -343.884 -26.7028 300.218 +-344.197 -26.6638 300.916 -344.231 -26.7212 302 -343.826 -27.3279 301.602 +-344.23 -26.696 303.011 -343.829 -27.3028 302.589 -344.238 -26.6862 304.025 +-343.826 -27.2922 303.58 -344.248 -26.6764 305.035 -343.824 -27.28 304.585 +-344.244 -26.6681 306.046 -343.82 -27.2777 305.588 -344.246 -26.6685 307.066 +-343.816 -27.2849 306.588 -344.265 -26.6717 308.085 -343.815 -27.2832 307.603 +-344.285 -26.6644 309.106 -343.833 -27.298 308.637 -344.186 -26.5656 309.901 +-343.851 -27.242 309.603 -343.772 -26.8506 310.238 -343.738 -26.226 299.621 +-343.809 -25.4962 299.292 -344.207 -25.8613 300.04 -344.361 -25.6736 300.907 +-344.34 -25.799 301.795 -344.34 -25.7745 302.826 -344.351 -25.7561 303.838 +-344.367 -25.7474 304.839 -344.376 -25.8876 305.703 -344.278 -25.6271 306.724 +-344.392 -25.8817 307.716 -344.289 -25.6882 308.608 -344.207 -25.8475 309.589 +-343.834 -26.0304 310.302 -344.193 -24.8216 299.879 -343.735 -24.7011 299.396 +-344.381 -24.7442 300.796 -344.357 -24.7208 301.842 -344.34 -24.7054 302.883 +-344.344 -24.829 303.759 -344.248 -24.647 304.636 -344.191 -24.9159 305.7 +-343.869 -25.0656 306.963 -343.861 -24.9017 306.392 -343.713 -24.6352 307.376 +-344.154 -25.013 307.734 -343.809 -25.1811 309.078 -343.883 -24.8472 308.367 +-343.732 -25.3882 309.888 -343.885 -24.1814 299.924 -343.725 -23.5987 300.373 +-344.192 -23.9369 300.634 -344.243 -23.8068 301.61 -344.226 -23.7889 302.644 +-344.188 -23.8661 303.737 -343.786 -24.189 305.091 -343.872 -23.8505 304.394 +-343.695 -24.4136 305.864 -343.719 -24.4414 307.93 -343.797 -23.2841 301.082 +-343.798 -23.2264 302.017 -343.793 -23.2264 303.018 -343.717 -23.3758 303.854 +-343.739 -14.1709 303.765 -343.863 -13.6482 303.211 -344.149 -13.6059 303.946 +-344.2 -13.7055 305.06 -343.806 -14.3121 304.63 -344.164 -13.5646 306.028 +-343.803 -14.2437 305.584 -343.736 -13.9132 306.294 -343.79 -13.2297 302.761 +-343.842 -12.7582 302.415 -344.247 -12.8672 303.061 -344.41 -12.7373 303.952 +-344.379 -12.8874 304.843 -344.38 -12.7084 305.922 -343.881 -13.329 306.732 +-344.129 -12.6826 306.764 -343.708 -12.8107 307.227 -343.617 -12.1632 299.98 +-343.895 -11.598 299.438 -344.353 -11.8879 299.877 -343.803 -12.276 300.829 +-343.844 -12.3424 301.664 -344.33 -11.9069 306.753 -343.831 -11.9228 307.362 +-343.747 -10.6672 299.249 -344.217 -10.7763 307.863 -343.927 -11.1956 307.691 +-343.748 -10.8435 308.233 -343.989 -9.7025 299.182 -344.279 -9.74897 308.863 +-343.966 -10.216 308.678 -343.787 -9.80483 309.246 -343.719 -9.34884 298.776 +-343.878 -8.73151 298.408 -344.25 -8.73914 309.895 -343.957 -9.18557 309.714 +-343.757 -8.79927 310.257 -343.795 -7.72078 298.332 -344.311 -7.73464 311.143 +-343.944 -8.23235 310.741 -344.163 -7.74116 311.823 -343.731 -8.18481 311.602 +-343.794 -7.80895 312.279 -343.848 -6.68697 298.353 -344.31 -6.75349 313.135 +-343.95 -7.25379 312.768 -344.184 -6.75374 313.796 -343.751 -7.19968 313.597 +-343.835 -6.80563 314.255 -343.731 -5.82014 298.34 -344.339 -5.77473 315.133 +-343.989 -6.25423 314.753 -344.328 -5.64067 315.841 -343.614 -6.25633 315.785 +-343.809 -5.6488 316.309 -344.342 -4.45419 297.824 -343.837 -4.9814 298.337 +-343.765 -4.91583 316.466 -344.293 -3.5171 297.835 -343.816 -3.97527 298.349 +-343.943 -4.38622 316.803 -344.308 -3.7262 318.148 -343.854 -4.20697 317.594 +-344.176 -3.75071 318.824 -343.738 -4.2101 318.591 -343.824 -3.82964 319.273 +-344.163 -2.6558 297.746 -343.804 -3.02213 298.264 -344.157 -2.66491 320.011 +-343.972 -3.18581 319.702 -343.65 -2.87009 320.316 -343.707 -2.29797 297.913 +-343.999 -1.95882 297.506 -344.006 -2.30893 320.75 -343.616 -1.71118 321.367 +-344.395 -0.692186 297.072 -343.909 -1.24745 297.593 -344.066 -0.500702 297.966 +-343.672 -0.879393 298.202 -344.109 -0.631171 321.838 -343.943 -1.08264 321.842 +-343.791 -0.760915 322.093 -344.391 0.177231 297.895 -343.824 -0.348952 298.751 +-344.252 0.206535 298.968 -343.778 -0.203217 299.617 -344.282 0.282126 300.1 +-343.82 -0.230323 300.618 -344.317 0.257489 301.073 -343.671 -0.254908 301.734 +-344.177 0.337235 301.901 -343.653 0.220821 302.379 -343.917 0.141968 321.424 +-343.622 -0.437965 321.83 -343.994 0.604393 302.667 -343.882 0.625887 303.632 +-343.966 0.772372 304.591 -344.252 1.3364 305 -343.544 1.13325 305.307 +-343.896 1.31807 321.207 -343.906 1.79629 305.784 -343.691 2.24129 306.42 +-343.737 2.25204 320.369 -343.711 1.68233 320.788 -344.213 2.31271 320.811 +-343.958 2.59938 306.802 -343.872 2.66194 307.586 -343.913 2.74236 308.581 +-344.31 3.2672 309.12 -343.726 2.72785 309.736 -344.297 3.23939 310.056 +-343.705 2.89987 310.784 -344.178 3.28992 310.9 -343.658 3.16529 311.424 +-343.882 3.39677 319.3 -343.54 2.81068 319.734 -344.312 3.28775 319.968 +-344.475 3.07356 320.883 -344.01 3.56563 311.675 -343.908 3.75431 312.597 +-344.284 4.28924 313.12 -343.75 3.7652 313.583 -344.216 4.27827 314.058 +-343.88 3.73809 314.708 -344.407 4.17599 314.915 -343.704 3.63153 315.83 +-343.959 3.85134 316.697 -344.333 4.24279 317.132 -343.619 3.77703 317.597 +-344.271 4.28236 318.064 -343.792 3.71496 318.57 -344.309 4.05803 318.954 +-344.349 15.2994 324.725 -343.693 15.2844 325.315 -344.056 14.9021 325.774 +-343.691 15.3699 326.268 -344.417 15.3768 327.678 -343.921 15.184 328.269 +-343.701 14.8369 328.939 -343.752 14.6793 329.834 -343.747 14.6976 330.844 +-343.729 14.7027 331.849 -343.976 14.7929 332.908 -343.507 15.2126 333.445 +-344.244 15.1354 333.757 -343.753 15.8732 307.016 -344.059 16.1843 306.327 +-343.87 15.7829 307.857 -343.876 15.8009 308.889 -343.852 15.8078 309.901 +-343.826 15.7991 310.9 -343.994 15.8318 311.944 -343.655 16.312 312.257 +-344.21 16.1621 312.722 -344.3 16.3414 321.749 -344.291 16.2094 322.492 +-343.975 16.1878 323.247 -343.723 15.8957 323.953 -343.819 15.6015 324.669 +-343.858 15.6638 326.911 -343.735 15.6487 327.856 -343.919 15.6018 333.933 +-343.729 16.1579 334.34 -343.546 16.6811 305.885 -343.958 17.3007 305.381 +-343.783 16.6475 312.945 -343.664 16.741 313.922 -344.021 16.8248 314.859 +-343.537 17.2411 315.377 -344.239 17.1708 315.695 -343.661 16.7884 322.066 +-344.051 17.1476 321.351 -343.614 16.5852 322.971 -343.963 16.7331 334.844 +-343.723 17.2968 335.255 -343.742 18.2167 305.351 -343.898 17.6382 316.068 +-343.706 17.784 316.897 -343.825 17.7636 317.787 -343.811 17.7564 318.8 +-343.815 17.7826 319.807 -343.739 17.7282 320.872 -343.754 18.1843 335.37 +-343.86 19.1559 305.215 -343.795 19.236 335.384 -343.775 19.6429 304.759 +-343.858 20.1743 304.364 -343.828 20.1611 335.267 -344.325 21.1099 304.729 +-343.655 21.1073 304.401 -344.004 21.3309 334.398 -343.597 20.7479 334.825 +-343.789 21.6189 304.908 -343.836 22.2221 305.276 -343.767 22.234 334.301 +-343.823 23.2607 305.324 -343.796 23.2667 334.336 -343.609 24.172 305.406 +-343.808 24.2679 334.341 -343.987 24.7406 305.831 -343.747 25.3079 306.261 +-343.803 25.2649 334.337 -343.813 26.2242 306.337 -343.804 26.2662 334.337 +-343.602 27.1479 306.405 -343.803 27.2633 334.336 -343.996 27.7169 306.835 +-343.757 28.317 307.312 -343.796 28.2507 334.327 -343.813 29.1254 307.314 +-343.785 29.2454 334.34 -343.602 29.747 306.838 -344.005 30.322 306.385 +-343.809 30.2712 334.382 -343.76 31.2164 306.291 -343.889 31.176 334.211 +-343.788 32.2457 306.33 -343.869 32.223 333.386 -343.747 31.6321 333.757 +-343.801 33.2476 306.336 -343.776 33.2296 333.308 -343.797 34.2443 306.331 +-343.833 34.2766 333.298 -343.796 35.2443 306.331 -343.592 35.1708 333.395 +-343.794 36.2459 306.331 -343.997 35.7339 333.833 -343.761 36.3256 334.27 +-343.796 37.2424 306.325 -343.789 37.2313 334.369 -343.819 38.2279 306.311 +-343.799 38.2477 334.33 -343.698 39.3463 306.421 -343.802 39.2336 334.321 +-344.136 40.3988 306.834 -343.966 39.9238 306.826 -343.814 40.2469 307.091 +-343.833 40.2272 334.328 -343.672 40.5816 306.841 -343.89 41.0602 306.465 +-343.655 41.2311 334.386 -344.197 42.3487 305.749 -343.778 42.1246 306.235 +-344.027 42.2424 333.405 -343.738 41.8058 333.846 -343.631 42.8181 305.889 +-344.004 43.1558 305.412 -343.84 43.1536 333.389 -343.83 44.1746 305.429 +-343.785 44.3217 333.44 -344.369 45.4164 305.894 -343.882 44.8165 305.741 +-343.706 45.2114 306.219 -344.34 45.3008 316.704 -343.883 45.151 317.287 +-343.862 44.8598 317.907 -343.73 44.7944 318.882 -343.8 44.7226 319.805 +-343.82 44.7501 320.781 -343.807 44.7618 321.777 -343.781 44.7517 322.788 +-344.019 44.8391 323.85 -343.551 45.255 324.339 -344.256 45.1959 324.648 +-343.918 45.2526 333.239 -343.866 46.3072 306.327 -344.33 46.2982 312.699 +-343.842 46.1498 313.285 -344.034 45.8952 313.944 -343.666 46.3148 314.274 +-344.136 46.1768 314.72 -344.004 46.2023 315.293 -343.777 45.9016 315.944 +-343.7 45.6126 316.885 -343.913 45.6552 325.028 -343.697 45.7924 325.879 +-343.795 45.7597 326.783 -343.752 45.7256 327.847 -343.857 45.6944 328.759 +-343.903 45.8355 329.718 -343.788 46.3145 330.218 -344.003 46.3057 332.381 +-343.539 45.7573 332.862 -343.61 47.2143 306.371 -344.358 47.2981 310.664 +-343.945 47.1702 311.252 -343.767 46.8831 311.925 -343.684 46.6004 312.872 +-343.572 46.6063 314.937 -343.783 46.7253 330.804 -343.821 46.7311 331.72 +-343.921 47.6853 306.76 -343.964 47.8902 307.768 -343.728 48.3419 308.257 +-344.163 48.1875 308.729 -344.003 48.1871 309.309 -343.766 47.8872 309.964 +-343.699 47.6103 310.904 -343.596 48.6126 308.93 -342.577 -29.1251 306.712 +-342.893 -28.6649 306.257 -343.143 -28.502 306.904 -342.73 -28.8726 307.348 +-342.832 -28.2479 299.901 -342.827 -27.787 299.302 -343.15 -27.6772 299.868 +-343.304 -27.7021 300.946 -342.817 -28.3167 300.616 -343.303 -27.7751 302.074 +-342.8 -28.2748 301.628 -343.3 -27.7602 303.062 -342.79 -28.2469 302.618 +-343.298 -27.7518 304.061 -342.786 -28.2429 303.613 -343.288 -27.7459 305.059 +-342.773 -28.2289 304.618 -343.302 -27.913 306.063 -342.784 -28.3103 305.563 +-343.434 -27.769 306.953 -342.89 -28.3948 307.719 -343.297 -27.8529 307.919 +-343.292 -27.7742 309.068 -342.788 -28.2439 308.578 -343.304 -27.6583 310.073 +-342.791 -28.275 309.585 -343.2 -27.7024 311.012 -342.811 -28.3002 310.584 +-343.222 -27.7009 311.995 -342.828 -28.2934 311.587 -343.233 -27.6952 312.987 +-342.833 -28.2848 312.587 -343.24 -27.7052 313.984 -342.839 -28.293 313.582 +-343.276 -27.7234 314.997 -342.748 -28.2894 314.728 -343.257 -27.5979 315.958 +-342.754 -28.1203 315.764 -342.749 -27.8345 316.394 -342.836 -27.228 298.773 +-342.851 -26.6952 298.25 -343.186 -26.7736 298.857 -343.444 -26.8763 299.851 +-343.389 -26.7027 310.817 -343.301 -26.813 311.775 -343.32 -26.7942 312.772 +-343.324 -26.7782 313.786 -343.353 -26.7983 314.799 -343.403 -26.8082 315.809 +-342.726 -27.3618 316.858 -343.232 -26.7272 316.694 -342.705 -26.7775 317.295 +-342.728 -26.2808 297.668 -342.748 -25.7027 297.428 -343.205 -25.7657 298.009 +-343.415 -25.906 298.907 -343.367 -25.5464 310.782 -343.281 -25.7918 311.829 +-343.302 -25.7587 312.836 -343.313 -25.7244 313.856 -343.323 -25.7168 314.872 +-343.355 -25.8772 315.737 -343.192 -25.5613 316.834 -342.834 -26.0433 317.381 +-343.23 -24.7707 297.878 -342.76 -24.6875 297.312 -343.392 -24.8506 298.748 +-343.438 -24.5203 306.694 -343.414 -24.4756 308.755 -343.361 -24.7754 309.911 +-343.286 -24.6465 310.995 -343.303 -24.7742 311.878 -343.326 -24.7368 312.889 +-343.338 -24.6773 313.904 -343.336 -24.7945 314.783 -343.207 -24.7433 315.725 +-342.798 -25.1651 316.846 -342.857 -24.9418 316.421 -342.815 -24.7668 317.361 +-343.084 -24.8242 317.766 -342.902 -25.2802 317.739 -342.751 -24.8171 318.187 +-343.257 -23.6875 297.802 -342.918 -23.7073 297.163 -343.291 -23.7758 298.802 +-343.403 -23.8084 299.706 -343.396 -23.5145 304.714 -343.336 -23.8033 305.875 +-343.251 -23.6642 306.958 -343.352 -23.9054 307.865 -343.297 -23.6343 308.986 +-343.303 -23.7504 309.85 -343.31 -23.8787 310.739 -343.232 -23.6909 311.639 +-343.222 -23.8567 312.641 -343.237 -23.7984 313.639 -343.203 -23.8415 314.722 +-342.707 -24.3425 315.957 -342.872 -23.831 315.391 -342.755 -24.4067 318.018 +-342.838 -23.2472 296.77 -342.717 -22.7393 296.478 -343.168 -22.8467 297.048 +-343.378 -22.6261 297.922 -343.336 -22.7547 298.847 -343.274 -22.748 299.87 +-343.353 -22.795 300.791 -343.374 -22.6565 301.803 -343.372 -22.6704 302.77 +-343.347 -22.7957 303.853 -343.251 -22.6768 304.924 -343.271 -22.7943 305.824 +-343.292 -22.751 306.873 -343.301 -22.8719 307.75 -343.226 -22.7014 308.634 +-343.218 -22.8423 309.636 -343.192 -22.8901 310.713 -342.794 -23.2236 312.094 +-342.876 -22.8635 311.397 -342.798 -23.2644 313.019 -342.806 -23.2118 314.01 +-342.731 -23.342 314.85 -342.902 -22.1594 296.87 -342.733 -21.6015 297.338 +-343.186 -21.9611 297.602 -343.263 -21.7429 298.733 -343.343 -21.7129 299.888 +-343.316 -21.7048 300.902 -343.3 -21.7369 301.872 -343.288 -21.7689 302.834 +-343.283 -21.7791 303.817 -343.289 -21.7674 304.836 -343.308 -21.7408 305.873 +-343.33 -21.8391 306.769 -343.208 -21.7415 307.725 -342.798 -22.2213 309.084 +-342.865 -21.9009 308.407 -342.802 -22.2386 310.02 -342.727 -22.363 310.86 +-342.792 -21.3138 298.087 -342.873 -21.1269 298.889 -342.72 -20.6331 299.373 +-343.178 -20.9474 299.675 -343.243 -20.8076 300.638 -343.229 -20.818 301.634 +-343.213 -20.852 302.632 -343.227 -20.7676 303.759 -343.341 -20.7265 304.868 +-343.343 -20.8208 305.753 -343.214 -20.7145 306.705 -342.718 -21.3428 307.946 +-342.869 -20.8627 307.39 -342.808 -20.3006 300.08 -342.812 -20.2394 301.01 +-342.793 -20.2507 302.023 -342.774 -20.2763 303.051 -342.854 -20.1747 303.915 +-342.777 -19.5498 304.316 -343.261 -19.8102 304.812 -343.259 -19.7659 305.853 +-342.716 -20.3083 306.904 -342.864 -19.7615 306.423 -343.25 -18.8024 304.96 +-342.787 -18.5948 304.391 -343.25 -18.7155 305.716 -342.79 -18.9257 306.272 +-343.264 -17.7402 304.815 -342.866 -17.7384 304.237 -343.276 -17.706 305.881 +-342.794 -17.9987 306.357 -342.72 -17.1905 303.751 -342.879 -16.6371 303.254 +-343.201 -16.7741 303.949 -343.338 -16.6946 304.893 -343.367 -16.7852 305.811 +-342.876 -17.2919 306.711 -343.179 -16.6758 306.742 -342.714 -16.8046 307.205 +-342.735 -16.1751 302.683 -342.794 -15.4798 302.373 -343.185 -15.8421 303.058 +-343.304 -15.6496 303.913 -343.284 -15.7856 304.778 -343.308 -15.7866 305.789 +-343.242 -15.7042 306.878 -342.793 -16.0126 307.384 -343.173 -14.9222 302.812 +-342.777 -14.5315 302.363 -343.402 -14.7915 303.732 -343.392 -14.8817 304.838 +-343.359 -14.7242 305.883 -343.301 -14.7697 306.834 -342.866 -15.303 307.689 +-343.193 -14.6563 307.904 -342.715 -14.8811 308.266 -343.359 -13.8316 302.827 +-342.817 -13.6571 302.243 -343.426 -13.7205 306.945 -343.328 -13.761 307.852 +-342.885 -14.3119 308.747 -343.189 -13.6762 308.778 -342.742 -13.8048 309.245 +-342.658 -13.1125 301.732 -342.864 -12.6035 301.413 -343.195 -12.9136 302.017 +-343.361 -12.6591 307.832 -343.215 -12.7295 308.721 -342.804 -12.9411 309.346 +-342.738 -12.1821 299.746 -343.027 -11.647 299.261 -342.941 -12.3603 300.693 +-343.364 -11.6072 308.046 -343.17 -11.6986 308.842 -342.776 -11.9879 309.36 +-342.624 -11.2704 298.671 -342.783 -10.5658 298.545 -343.122 -10.8383 298.904 +-343.348 -10.7326 308.916 -342.846 -11.3671 309.728 -343.191 -10.7766 309.894 +-343.178 -10.57 310.991 -342.788 -11.2136 310.543 -342.734 -10.9171 311.273 +-343.326 -10.0149 298.85 -342.819 -9.72162 298.425 -343.352 -9.72204 309.928 +-343.292 -9.78933 310.814 -342.896 -10.3412 311.71 -343.194 -9.67467 311.87 +-342.735 -9.87735 312.267 -342.754 -8.79853 298.322 -343.376 -8.87086 310.895 +-343.358 -8.71853 311.893 -342.866 -9.29543 312.696 -343.152 -8.64713 312.889 +-342.706 -8.84418 313.275 -342.782 -7.77109 298.34 -343.414 -7.86817 312.923 +-342.844 -8.36435 313.778 -343.282 -7.70753 313.988 -343.141 -7.52679 315.038 +-342.78 -8.19571 314.572 -342.716 -7.88979 315.288 -342.83 -6.71629 298.326 +-343.461 -6.85728 314.931 -342.856 -7.301 315.746 -343.225 -6.86583 315.683 +-342.8 -6.78013 316.277 -342.74 -5.72961 298.409 -343.25 -5.58223 316.973 +-342.966 -6.25582 316.742 -343.178 -5.71522 318.032 -342.745 -6.28173 317.622 +-343.174 -5.59429 318.989 -342.789 -6.24256 318.569 -342.74 -5.91215 319.283 +-343.286 -4.49658 298.871 -342.898 -5.13125 298.783 -342.613 -4.79739 299.176 +-343.405 -4.71075 316.964 -343.339 -4.89487 317.794 -343.39 -4.76678 318.887 +-342.901 -5.32654 319.699 -343.197 -4.66723 319.874 -342.755 -4.83043 320.279 +-343.336 -3.54872 298.848 -342.705 -4.03545 299.381 -343.417 -3.71871 319.954 +-342.675 -4.31114 320.902 -343.158 -3.7109 320.729 -342.708 -3.77506 321.358 +-343.278 -2.50971 298.635 -342.838 -2.99507 299.309 -343.369 -2.82336 320.805 +-342.916 -2.89173 321.343 -343.432 -1.64482 298.045 -343.137 -1.56211 298.935 +-342.782 -2.01977 299.316 -342.665 -1.75962 321.437 -343.421 -0.840405 298.853 +-342.835 -1.35106 299.705 -343.291 -0.816759 299.904 -342.716 -1.22646 300.719 +-343.35 -0.758681 301.059 -342.717 -1.10786 301.798 -343.276 -0.789194 301.757 +-342.817 -0.792689 302.328 -343.456 -0.889437 321.834 -342.969 -0.678808 321.439 +-343.006 -0.252325 302.682 -343.301 0.26476 303.105 -342.675 -0.292796 303.74 +-343.263 0.207782 304.099 -342.802 -0.119927 304.693 -343.321 0.33541 304.904 +-342.786 0.334586 305.224 -342.857 0.339131 321.187 -342.665 1.05566 305.484 +-342.88 1.34687 320.496 -342.648 0.694462 320.756 -343.239 1.1304 320.909 +-343.013 1.61158 305.78 -342.837 1.78451 306.654 -343.308 2.28851 307.055 +-342.684 1.84369 307.788 -343.273 2.22457 308.02 -342.719 1.78322 308.809 +-343.339 2.18161 309.039 -342.87 1.87378 309.633 -343.224 2.27223 309.917 +-342.552 2.24218 310.288 -342.884 2.1768 320.238 -342.85 2.66759 310.812 +-342.93 2.86859 311.703 -343.317 3.24608 312.145 -342.626 2.75331 312.602 +-343.27 3.25016 313.044 -342.828 2.72387 313.577 -343.314 3.2363 314.011 +-342.757 2.72786 314.728 -343.336 3.23601 315.033 -342.718 2.93822 315.789 +-343.188 3.34088 315.903 -342.65 3.23554 316.418 -342.811 3.20542 317.396 +-342.777 2.80883 317.838 -343.218 3.24807 318.076 -342.899 2.73809 318.731 +-343.444 3.18362 318.919 -342.812 2.5911 319.704 -343.066 3.59613 316.737 +-343.496 15.245 325.855 -343.33 15.3089 328.702 -342.871 15.1509 329.283 +-342.841 14.852 329.92 -342.852 14.7953 330.883 -342.838 14.7988 331.9 +-343.002 14.858 332.79 -342.694 15.2881 333.236 -343.418 16.2618 306.707 +-343.289 16.2185 307.602 -343.297 16.2411 308.638 -343.296 16.2479 309.649 +-343.276 16.2397 310.644 -343.32 16.2379 311.572 -343.354 16.3303 323.696 +-342.857 16.1825 324.278 -343.01 15.8813 324.907 -343.145 15.9426 325.845 +-342.645 16.345 325.254 -342.959 15.8907 326.835 -342.826 16.2789 326.335 +-342.77 15.8468 327.875 -342.688 15.6136 328.874 -342.91 15.7403 333.75 +-342.58 16.3833 334.336 -342.659 16.8502 306.007 -342.906 17.3742 305.443 +-342.828 16.6304 306.871 -342.721 16.6685 308.03 -342.727 16.685 309.031 +-342.728 16.681 310.026 -342.715 16.6752 311.012 -342.866 16.6902 312.006 +-342.657 16.7638 312.908 -342.769 16.7264 313.817 -342.999 16.8215 314.734 +-342.604 17.2396 315.349 -343.345 17.3552 321.668 -342.936 17.1961 322.253 +-342.767 16.8885 322.921 -342.691 16.6027 323.864 -342.555 16.6071 325.891 +-343.036 16.7731 334.853 -342.683 17.4687 335.224 -342.915 18.3971 305.249 +-342.855 17.6733 315.895 -342.831 17.7857 316.771 -342.802 17.7537 317.802 +-342.793 17.7309 318.833 -342.797 17.7381 319.826 -342.809 17.7839 320.831 +-342.696 17.6565 321.899 -342.869 18.3076 335.384 -342.475 18.7893 304.825 +-342.978 19.4782 304.452 -343.216 19.1264 304.921 -342.783 19.2371 335.402 +-342.831 20.2911 304.234 -342.887 20.1722 335.213 -342.58 21.211 304.369 +-342.864 21.2156 334.375 -342.745 20.6298 334.751 -342.968 21.7917 304.826 +-342.766 22.3622 305.246 -342.76 22.2221 334.302 -342.841 23.2749 305.323 +-342.793 23.2411 334.329 -342.612 24.1792 305.402 -342.794 24.2377 334.327 +-342.986 24.7399 305.832 -342.756 25.3138 306.257 -342.79 25.2362 334.325 +-342.826 26.2375 306.329 -342.792 26.2381 334.325 -342.604 27.1651 306.399 +-342.794 27.239 334.322 -342.98 27.743 306.844 -342.756 28.3478 307.314 +-342.79 28.2355 334.322 -342.872 29.1469 307.244 -342.785 29.2484 334.358 +-342.751 29.6265 306.759 -342.871 30.2275 306.374 -342.86 30.3253 334.289 +-342.764 31.2309 306.306 -342.955 31.4581 333.459 -342.53 30.8115 333.83 +-343.254 31.1414 333.913 -342.798 32.2491 306.336 -342.763 32.2157 333.291 +-342.8 33.2461 306.334 -342.807 33.2504 333.311 -342.796 34.2441 306.33 +-342.84 34.2903 333.282 -342.796 35.2446 306.331 -342.635 35.0555 333.449 +-342.794 36.2459 306.33 -343.049 35.6943 333.829 -342.722 36.1808 334.279 +-342.802 37.2421 306.321 -342.8 37.2425 334.354 -342.811 38.2324 306.326 +-342.8 38.2444 334.323 -342.68 39.1147 306.342 -342.815 39.2286 334.313 +-343.502 40.1065 306.832 -342.804 40.3569 306.349 -342.687 40.3652 334.417 +-342.925 41.4618 306.365 -343.15 41.2652 334.928 -342.942 40.9428 334.839 +-342.782 41.2729 335.103 -342.652 42.1849 306.381 -343.33 42.4581 333.797 +-342.852 42.0808 334.291 -342.793 41.5692 334.79 -343.288 43.509 305.816 +-342.838 43.0503 306.273 -343.3 43.4457 333.845 -342.705 43.0611 334.256 +-343.367 44.2929 305.891 -342.775 44.1546 306.271 -343.219 44.331 333.8 +-342.766 44.0238 334.228 -342.827 45.2636 306.362 -343.409 45.3068 317.788 +-343.223 45.2407 318.576 -342.91 45.1629 319.296 -342.883 44.8686 319.912 +-342.725 44.7911 320.884 -342.752 44.693 321.833 -342.732 44.6864 322.85 +-342.985 44.8172 323.726 -342.592 45.2269 324.35 -343.054 45.1021 333.343 +-342.61 44.724 333.865 -342.858 46.2919 306.302 -343.445 46.2966 313.704 +-343.405 46.3532 315.735 -342.86 46.1907 316.213 -342.704 45.8194 316.809 +-342.818 45.6722 317.907 -342.591 45.6228 319.002 -342.847 45.6715 324.893 +-342.795 45.7796 325.78 -342.74 45.732 326.832 -342.857 45.7016 327.757 +-342.9 45.8181 328.883 -342.689 46.3104 329.289 -343.24 46.155 329.759 +-342.972 46.2809 332.34 -342.53 45.7312 332.835 -342.609 47.1809 306.379 +-343.371 47.33 311.741 -342.857 47.1971 312.243 -342.696 46.817 312.829 +-342.957 46.6788 313.935 -342.72 46.8201 314.937 -342.758 46.6394 315.728 +-342.838 46.6607 329.959 -342.718 46.8136 330.917 -342.794 46.6985 331.701 +-343.029 47.7864 306.906 -342.545 48.2785 307.316 -343.252 48.2026 307.653 +-343.401 48.3426 309.73 -342.862 48.1879 310.219 -342.695 47.8427 310.853 +-342.726 47.6065 311.767 -342.923 48.6615 308.049 -342.727 48.8341 308.912 +-342.761 48.6323 309.721 -341.545 -29.1918 299.815 -341.873 -28.7144 299.286 +-342.33 -28.674 299.937 -342.32 -28.7994 301.09 -341.826 -29.1772 300.747 +-342.302 -28.7539 302.072 -341.682 -29.2676 301.617 -342.295 -28.744 303.057 +-341.821 -29.2592 302.594 -342.28 -28.7386 304.053 -341.805 -29.2565 303.572 +-342.283 -28.7436 305.056 -341.8 -29.2595 304.574 -342.454 -28.8543 305.903 +-341.84 -29.2766 305.815 -341.728 -29.2821 306.943 -342.389 -28.7757 308.055 +-341.903 -29.2073 307.748 -342.304 -28.7731 309.006 -341.716 -29.1748 308.783 +-342.315 -28.7883 310.035 -341.702 -29.1708 309.8 -342.319 -28.7751 311.062 +-341.81 -29.1391 310.692 -342.314 -28.7564 312.072 -341.671 -29.2618 311.58 +-342.313 -28.7572 313.071 -341.822 -29.2556 312.594 -342.316 -28.761 314.077 +-341.817 -29.2658 313.595 -342.165 -28.6694 314.942 -341.802 -29.203 314.55 +-341.711 -28.856 315.284 -341.795 -28.298 298.838 -341.803 -27.7434 298.303 +-342.315 -27.766 298.818 -342.015 -28.3864 315.703 -342.331 -27.7278 316.846 +-341.755 -28.2064 316.747 -341.761 -27.7255 317.345 -341.669 -27.2961 297.703 +-341.869 -26.6544 297.413 -342.272 -26.9282 297.934 -342.296 -26.6174 318.001 +-342 -27.2807 317.693 -342.158 -26.5928 319.017 -341.777 -27.2648 318.522 +-341.747 -26.949 319.247 -341.929 -25.7332 297.173 -342.465 -25.5947 318.068 +-342.321 -25.79 318.86 -341.899 -26.3354 319.741 -342.179 -25.7046 319.793 +-341.749 -25.8094 320.254 -341.697 -25.3453 296.749 -341.704 -24.7153 296.5 +-342.203 -24.8367 296.901 -342.44 -24.5495 316.707 -342.4 -24.6842 318.62 +-342.181 -24.6982 319.562 -341.8 -25.0226 320.311 -342.417 -23.8024 296.69 +-341.763 -23.817 296.236 -342.372 -23.6284 315.811 -342.231 -23.692 316.911 +-342.366 -23.8962 317.866 -342.222 -23.684 319.058 -341.708 -24.3221 319.93 +-341.858 -23.8769 319.484 -341.772 -23.185 295.682 -341.833 -22.4896 295.329 +-342.224 -22.8153 296.029 -342.398 -22.521 311.727 -342.358 -22.7158 312.771 +-342.37 -22.6771 313.764 -342.356 -22.7765 314.852 -342.26 -22.6787 315.901 +-342.255 -22.8153 316.781 -342.276 -22.7917 317.793 -342.326 -22.7474 318.82 +-341.862 -23.3058 319.648 -342.158 -22.673 319.71 -341.708 -22.8107 320.148 +-342.205 -21.8027 295.889 -341.825 -21.5632 295.29 -342.443 -21.8004 296.668 +-342.392 -21.5513 308.726 -342.366 -21.6989 309.767 -342.354 -21.7886 310.86 +-342.254 -21.6865 311.918 -342.253 -21.8277 312.798 -342.256 -21.8051 313.804 +-342.265 -21.7811 314.796 -342.275 -21.7844 315.794 -342.283 -21.7885 316.801 +-342.289 -21.7731 317.81 -342.324 -21.7549 318.834 -342.253 -21.6898 319.908 +-341.797 -22.0148 320.383 -342.213 -20.8118 295.918 -341.827 -20.5539 295.33 +-342.292 -20.7835 296.799 -342.346 -20.8048 297.766 -342.416 -20.67 298.757 +-342.376 -20.6573 307.795 -342.25 -20.7191 308.914 -342.253 -20.8281 309.795 +-342.264 -20.7936 310.797 -342.276 -20.7835 311.804 -342.285 -20.7875 312.82 +-342.289 -20.775 313.835 -342.288 -20.7597 314.835 -342.29 -20.7566 315.829 +-342.296 -20.7581 316.836 -342.302 -20.748 317.854 -342.322 -20.7391 318.878 +-342.356 -20.722 319.908 -341.884 -21.2995 320.734 -342.187 -20.6678 320.805 +-341.746 -20.7957 321.257 -342.268 -19.6877 295.804 -341.924 -19.6815 295.173 +-342.323 -19.7809 296.826 -342.268 -19.7517 297.836 -342.24 -19.7709 298.828 +-342.35 -19.83 299.752 -342.378 -19.6703 300.773 -342.358 -19.6568 301.781 +-342.327 -19.6641 302.823 -342.422 -19.812 303.645 -342.387 -19.6429 306.92 +-342.252 -19.704 307.883 -342.272 -19.8245 308.789 -342.284 -19.7981 309.823 +-342.29 -19.7661 310.833 -342.294 -19.7517 311.83 -342.301 -19.7538 312.828 +-342.302 -19.7522 313.832 -342.299 -19.7514 314.833 -342.299 -19.7449 315.839 +-342.307 -19.7382 316.863 -342.318 -19.7289 317.882 -342.331 -19.711 318.898 +-342.354 -19.8346 319.788 -342.217 -19.6536 320.636 -341.831 -20.0079 321.358 +-341.741 -19.2935 294.705 -341.595 -18.6258 294.423 -342.166 -18.9711 294.965 +-342.246 -18.6757 295.794 -342.355 -18.7794 296.842 -342.317 -18.7532 297.855 +-342.28 -18.7527 298.842 -342.271 -18.7676 299.832 -342.266 -18.7642 300.824 +-342.245 -18.7439 301.836 -342.211 -18.737 302.879 -342.319 -19.006 303.783 +-342.342 -18.5551 306.863 -342.247 -18.8192 307.759 -342.287 -18.7951 308.805 +-342.303 -18.7619 309.827 -342.303 -18.747 310.836 -342.301 -18.746 311.837 +-342.3 -18.7491 312.835 -342.298 -18.7518 313.831 -342.294 -18.757 314.827 +-342.302 -18.7508 315.833 -342.317 -18.7264 316.863 -342.327 -18.8479 317.75 +-342.232 -18.6626 318.622 -342.193 -18.8844 319.72 -341.762 -19.3024 321.016 +-341.892 -18.847 320.411 -341.841 -18.3169 295.041 -341.87 -18.1618 295.903 +-341.861 -17.6696 296.159 -342.21 -17.7933 296.738 -342.347 -17.7698 297.883 +-342.328 -17.7486 298.856 -342.305 -17.7398 299.838 -342.282 -17.7157 300.848 +-342.264 -17.6931 301.883 -342.24 -17.818 302.788 -342.382 -17.8755 303.745 +-342.411 -17.5924 307.025 -342.24 -17.8005 307.783 -342.286 -17.7848 308.818 +-342.307 -17.7471 309.839 -342.301 -17.7457 310.836 -342.298 -17.751 311.833 +-342.297 -17.7539 312.832 -342.295 -17.7552 313.83 -342.291 -17.7604 314.826 +-342.303 -17.761 315.836 -342.328 -17.7224 316.861 -342.23 -17.8032 317.855 +-341.778 -18.1939 319.045 -341.853 -17.8108 318.418 -341.724 -18.3813 319.845 +-341.653 -17.2833 295.648 -341.897 -16.5727 295.444 -342.121 -16.8088 296.122 +-341.746 -16.7651 296.465 -341.886 -17.0548 296.852 -341.841 -16.6146 297.153 +-342.184 -16.8707 297.628 -342.245 -16.7786 298.75 -342.33 -16.728 299.84 +-342.291 -16.6878 300.847 -342.265 -16.8033 301.742 -342.389 -16.8563 302.854 +-342.335 -16.6771 307.802 -342.267 -16.7833 308.81 -342.304 -16.751 309.84 +-342.301 -16.7484 310.836 -342.296 -16.7545 311.831 -342.296 -16.7561 312.83 +-342.294 -16.7562 313.829 -342.29 -16.7602 314.826 -342.304 -16.7649 315.839 +-342.337 -16.729 316.867 -342.222 -16.6965 317.751 -341.777 -16.967 318.294 +-342.269 -15.8045 295.835 -341.598 -15.7021 295.374 -341.833 -16.4444 296.667 +-342.192 -15.8456 296.991 -341.788 -15.7892 297.42 -341.832 -16.2143 297.932 +-341.852 -16.1871 298.871 -341.693 -15.69 299.394 -342.184 -15.8616 299.795 +-342.305 -15.7019 300.857 -342.367 -15.9283 301.903 -342.412 -15.5846 308.024 +-342.24 -15.7754 308.8 -342.283 -15.7685 309.822 -342.303 -15.7509 310.835 +-342.298 -15.7544 311.831 -342.296 -15.7563 312.83 -342.295 -15.7564 313.829 +-342.291 -15.7595 314.826 -342.302 -15.7654 315.833 -342.336 -15.7368 316.859 +-342.23 -15.6935 317.752 -341.788 -15.9518 318.317 -341.961 -15.1791 295.871 +-341.844 -14.76 296.309 -342.259 -14.8295 296.761 -341.88 -15.3215 297.653 +-342.21 -14.6564 297.728 -341.701 -14.8046 298.196 -341.856 -15.2156 300.009 +-341.76 -14.4801 300.357 -342.205 -14.7845 300.836 -342.393 -14.9423 301.763 +-342.396 -14.7149 308.927 -342.259 -14.7782 309.816 -342.295 -14.7608 310.83 +-342.301 -14.7526 311.832 -342.296 -14.7554 312.83 -342.295 -14.7567 313.83 +-342.291 -14.7588 314.826 -342.297 -14.7622 315.828 -342.328 -14.7416 316.854 +-342.222 -14.7065 317.74 -341.785 -14.9473 318.303 -341.714 -14.2824 296.769 +-341.904 -13.6583 297.162 -342.223 -13.646 297.775 -341.789 -13.9639 298.422 +-342.154 -13.7197 300.793 -341.811 -13.6491 300.281 -342.401 -13.8669 301.696 +-342.352 -13.6666 309.824 -342.276 -13.7783 310.822 -342.302 -13.7562 311.834 +-342.299 -13.7533 312.832 -342.295 -13.7563 313.83 -342.291 -13.7584 314.828 +-342.294 -13.7533 315.832 -342.327 -13.7437 316.862 -342.246 -13.6673 317.928 +-341.783 -14.0055 318.386 -341.84 -13.2862 296.827 -341.774 -12.8815 296.481 +-342.186 -12.8397 297.088 -342.371 -12.5766 297.946 -341.859 -13.2943 298.796 +-342.203 -12.7872 298.836 -342.249 -12.7375 299.987 -341.741 -13.2248 299.608 +-342.4 -12.906 301.032 -342.347 -12.5194 309.915 -342.242 -12.7846 310.803 +-342.287 -12.7731 311.824 -342.305 -12.7533 312.834 -342.298 -12.7545 313.832 +-342.292 -12.7581 314.83 -342.298 -12.752 315.83 -342.326 -12.7338 316.852 +-342.355 -12.8471 317.756 -341.871 -13.2944 318.757 -342.149 -12.618 318.607 +-341.726 -12.8901 319.198 -341.777 -12.2095 296.675 -341.855 -11.7244 297.34 +-342.221 -11.7812 297.832 -342.437 -12.0149 298.868 -342.443 -11.7277 310.003 +-342.329 -11.7257 310.862 -342.253 -11.7768 311.813 -342.291 -11.7613 312.822 +-342.304 -11.7548 313.833 -342.297 -11.7564 314.833 -342.3 -11.7584 315.828 +-342.321 -11.7333 316.834 -342.24 -11.8037 317.838 -341.734 -12.3183 318.989 +-341.876 -11.7848 318.444 -341.661 -11.3061 297.662 -342.01 -10.685 298.241 +-342.402 -10.7088 311.927 -342.251 -10.7485 312.813 -342.282 -10.7605 313.818 +-342.301 -10.759 314.831 -342.3 -10.7598 315.83 -342.317 -10.7564 316.825 +-342.228 -10.6834 317.864 -341.784 -11.0033 318.315 -341.71 -9.5492 298.255 +-342.391 -9.67156 312.929 -342.242 -9.74381 313.822 -342.274 -9.76251 314.819 +-342.293 -9.76236 315.823 -342.308 -9.7658 316.825 -342.337 -9.76636 317.833 +-341.845 -10.3079 318.652 -342.192 -9.65735 318.898 -341.714 -9.89972 319.263 +-341.868 -8.68737 298.352 -342.415 -8.83556 313.897 -342.347 -8.70703 314.879 +-342.258 -8.74945 315.818 -342.274 -8.75265 316.814 -342.309 -8.77316 317.818 +-342.356 -8.76866 318.839 -341.885 -9.32423 319.746 -342.195 -8.6803 319.761 +-341.759 -8.78796 320.246 -341.773 -7.78354 298.344 -342.382 -7.68349 315.952 +-342.225 -7.72872 316.837 -342.255 -7.74672 317.831 -342.311 -7.77024 318.825 +-342.269 -7.69233 319.885 -341.835 -7.96376 320.412 -341.826 -6.74203 298.301 +-342.376 -6.8405 316.935 -342.343 -6.8569 317.862 -342.367 -6.71038 318.905 +-342.317 -6.73524 319.845 -341.689 -7.3042 320.89 -342.189 -6.68269 320.734 +-341.721 -6.75234 321.362 -341.6 -5.84795 298.389 -342.435 -5.66192 319.956 +-342.193 -5.70274 320.748 -341.833 -5.9664 321.366 -342.02 -5.27063 298.833 +-341.681 -4.70311 299.386 -342.352 -4.8612 320.735 -341.881 -4.89645 321.369 +-342.318 -3.52151 299.868 -341.905 -4.18312 299.851 -341.654 -3.81148 300.228 +-342.29 -3.49323 321.885 -342.025 -4.1843 321.714 -341.59 -3.90052 322.229 +-342.342 -2.51101 299.948 -341.67 -2.96005 300.384 -342.344 -2.53578 321.875 +-341.743 -2.9499 322.306 -342.497 -1.71355 299.969 -341.738 -2.26472 300.846 +-342.217 -1.78613 300.756 -341.709 -1.82572 301.445 -342.167 -1.64872 321.801 +-341.784 -1.98309 322.188 -341.905 -1.40334 301.823 -341.832 -1.21182 302.828 +-342.347 -0.809418 303.107 -341.788 -1.12903 303.687 -342.169 -0.758414 303.94 +-341.523 -0.818166 304.375 -341.989 -0.781272 321.322 -341.801 -1.35122 321.89 +-342.061 -0.304587 304.76 -341.725 0.334391 305.37 -341.83 0.330159 320.441 +-341.638 -0.319056 320.701 -342.191 0.143574 320.861 -342.381 1.25863 306.061 +-341.758 0.772397 305.935 -341.839 0.893124 306.647 -342.206 1.28741 306.899 +-341.581 1.20745 307.337 -341.896 1.3073 320.234 -341.886 1.63689 307.792 +-341.822 1.70066 308.825 -341.923 1.7894 309.815 -341.709 2.20377 310.429 +-341.92 2.30753 312.374 -341.548 1.8375 312.736 -342.29 2.20029 313.077 +-341.746 1.7625 313.826 -342.366 2.18236 314.036 -341.855 1.86917 314.658 +-342.219 2.27636 314.934 -341.552 2.2304 315.356 -341.868 2.3445 317.249 +-341.564 1.81816 317.757 -342.3 2.38031 317.879 -341.83 1.83948 318.711 +-342.311 2.25841 319.101 -341.668 1.7171 319.605 -342.3 2.1706 319.924 +-341.949 2.60599 310.828 -341.926 2.6446 311.692 -341.997 2.65274 315.717 +-341.786 2.7326 316.56 -342.321 3.05282 316.994 -342.392 15.2946 329.774 +-342.274 15.2337 330.628 -342.275 15.2415 331.643 -342.31 15.2262 332.598 +-342.444 16.322 324.698 -342.471 16.3544 326.761 -342.261 16.2575 327.569 +-341.945 16.1863 328.291 -341.757 15.8832 328.93 -341.811 15.6559 329.904 +-341.722 15.6723 331.02 -341.682 15.6657 332.033 -341.812 15.6339 333.077 +-341.927 15.8464 333.844 -341.674 16.2166 334.219 -342.247 17.3331 305.761 +-341.912 17.1462 306.197 -341.753 16.8098 306.799 -341.84 16.7585 307.787 +-341.831 16.7887 308.784 -341.842 16.7893 309.772 -341.837 16.7886 310.768 +-341.804 16.7749 311.768 -341.758 16.746 312.795 -341.868 16.7143 313.714 +-341.91 16.843 314.701 -341.767 17.3163 315.221 -342.365 17.3373 322.733 +-341.854 17.1913 323.238 -341.7 16.8074 323.832 -341.947 16.669 324.929 +-341.734 16.7742 325.906 -341.853 16.7013 326.899 -341.595 16.6294 328.031 +-342.357 17.1085 334.777 -341.679 17.0765 334.473 -341.531 17.6986 305.698 +-342.056 18.0842 305.281 -341.766 17.6885 315.795 -341.829 17.7781 316.79 +-341.794 17.7521 317.818 -341.79 17.734 318.836 -341.794 17.7294 319.833 +-341.811 17.7572 320.835 -341.796 17.803 321.87 -341.714 17.6296 322.788 +-341.831 17.6185 334.905 -341.824 18.2202 335.285 -341.684 18.5886 304.72 +-341.872 19.2313 304.352 -341.843 19.2925 335.305 -341.817 20.2769 304.247 +-341.956 20.4421 334.445 -341.547 19.787 334.836 -342.258 20.129 334.906 +-341.642 21.0598 304.455 -341.754 21.2041 334.274 -342.044 21.721 304.827 +-341.74 22.1812 305.274 -341.771 22.2314 334.316 -341.864 23.2788 305.316 +-341.8 23.2447 334.335 -341.604 24.1919 305.393 -341.796 24.2412 334.33 +-341.98 24.7423 305.833 -341.757 25.3262 306.257 -341.794 25.2406 334.329 +-341.835 26.2545 306.325 -341.796 26.2418 334.328 -341.606 27.165 306.403 +-341.795 27.2405 334.324 -341.987 27.7598 306.844 -341.819 28.3705 307.219 +-341.785 28.237 334.338 -341.518 28.7497 306.86 -341.963 29.4356 306.451 +-342.25 29.1027 306.928 -341.801 29.2588 334.377 -341.76 30.209 306.27 +-341.879 30.1588 334.204 -341.773 31.2345 306.315 -341.864 31.2132 333.375 +-341.742 30.6069 333.748 -341.801 32.2475 306.337 -341.764 32.2159 333.308 +-341.798 33.2449 306.332 -341.83 33.2724 333.308 -341.796 34.2441 306.33 +-341.729 34.2745 333.376 -341.795 35.2448 306.33 -342.335 35.3719 333.904 +-341.851 34.8798 333.748 -341.714 35.3278 334.204 -341.793 36.2458 306.328 +-341.791 36.2285 334.378 -341.795 37.2447 306.336 -341.805 37.2501 334.341 +-341.815 38.2448 306.359 -341.805 38.2413 334.319 -341.942 39.3109 306.202 +-341.818 39.2403 334.31 -341.744 39.7108 305.707 -341.554 40.2371 305.453 +-342.149 40.1713 305.856 -341.673 40.1312 334.306 -341.876 40.6741 305.852 +-341.864 41.3051 306.25 -342.473 41.1436 334.827 -341.906 41.2556 334.422 +-341.785 42.2935 306.381 -341.79 42.3107 334.349 -341.802 43.2774 306.365 +-341.842 43.3165 334.387 -341.791 44.2527 306.356 -341.894 44.1917 334.275 +-341.818 45.2511 306.334 -342.431 45.328 319.785 -342.236 45.2455 320.565 +-341.912 45.1645 321.284 -341.852 44.8627 321.928 -341.815 44.7816 322.909 +-342.018 44.8442 323.808 -341.729 45.2814 324.261 -341.894 45.3317 333.329 +-341.649 44.7474 333.843 -341.844 46.2719 306.294 -342.055 46.2437 316.371 +-341.62 45.8587 317.015 -341.827 45.711 317.778 -341.844 45.779 318.791 +-341.826 45.7114 319.908 -341.584 45.6228 320.987 -341.727 45.6411 324.837 +-341.772 45.7391 325.83 -341.848 45.7161 326.747 -341.898 45.8216 327.873 +-341.691 46.3162 328.269 -342.229 46.1698 328.738 -342.017 46.2593 332.404 +-341.662 45.8772 332.912 -341.616 47.1737 306.391 -341.97 47.2277 312.349 +-341.606 46.8067 312.822 -341.706 46.7833 313.783 -341.851 46.7606 314.793 +-341.762 46.6999 315.858 -341.827 46.6598 328.937 -341.736 46.7818 329.876 +-341.851 46.7947 330.773 -341.733 46.71 331.878 -342.044 47.7825 306.772 +-341.612 48.281 307.272 -342.068 48.2525 310.398 -341.616 47.9038 311.053 +-341.759 47.6288 311.722 -341.853 48.6783 307.859 -341.848 48.8143 308.763 +-341.75 48.7079 309.875 -340.6 -30.156 301.71 -340.977 -29.7066 301.383 +-341.31 -29.7828 302.073 -341.353 -29.7865 302.998 -340.772 -30.1881 302.772 +-341.319 -29.7952 303.998 -340.716 -30.2002 303.794 -341.332 -29.8065 305.023 +-340.705 -30.2277 304.831 -341.229 -29.7181 305.888 -340.85 -30.1281 305.645 +-340.588 -29.7767 306.306 -340.559 -30.1716 311.854 -340.871 -29.7272 311.4 +-341.297 -29.7969 312.036 -341.359 -29.7912 312.994 -340.776 -30.2197 312.745 +-341.323 -29.8019 314.001 -340.729 -30.2071 313.769 -341.295 -29.6595 315.043 +-340.686 -30.2401 314.794 -341.205 -29.7055 316.077 -340.806 -30.2563 315.686 +-341.141 -29.6079 316.911 -340.787 -30.1498 316.612 -340.856 -29.7388 317.174 +-340.702 -29.173 298.991 -340.843 -28.6872 298.278 -341.418 -28.6743 298.776 +-340.821 -29.2933 299.845 -340.849 -29.3958 300.675 -340.899 -29.3379 306.778 +-340.822 -29.2648 307.825 -340.81 -29.2845 308.81 -340.784 -29.2725 309.808 +-340.875 -29.3643 310.802 -341.449 -28.8305 315.91 -341.304 -28.9299 316.679 +-340.808 -28.9486 317.334 -340.524 -28.2696 297.765 -340.952 -27.5598 297.442 +-341.252 -27.9967 297.94 -341.291 -27.7749 318.084 -340.954 -28.2205 317.72 +-341.299 -27.6617 319.017 -340.776 -28.2728 318.565 -341.203 -27.5766 319.918 +-340.705 -28.3023 319.723 -340.758 -27.8367 320.328 -340.841 -26.751 297.217 +-341.463 -26.7381 319.902 -340.808 -27.3948 320.819 -341.265 -26.7994 320.868 +-341.184 -26.6052 321.807 -340.738 -27.1496 321.776 -340.722 -26.8058 322.367 +-340.513 -26.2505 296.822 -340.972 -25.5707 296.482 -341.249 -25.9275 296.874 +-341.4 -25.6836 320.828 -341.275 -25.721 321.855 -340.872 -26.0232 322.374 +-340.836 -24.7226 296.277 -341.355 -24.5566 320.767 -341.339 -24.7897 321.858 +-340.714 -25.3221 322.873 -341.216 -24.7018 322.753 -340.741 -24.7447 323.373 +-340.588 -24.1018 294.892 -340.872 -23.5709 294.417 -341.188 -23.756 294.996 +-341.348 -23.7531 295.736 -340.72 -24.2399 295.767 -341.434 -23.6743 320.043 +-341.243 -23.672 320.935 -341.314 -23.8022 321.878 -341.273 -23.7579 322.805 +-340.793 -23.8269 323.394 -341.362 -22.9871 294.899 -340.719 -22.7484 294.346 +-341.347 -22.666 320.802 -341.29 -22.916 321.717 -341.211 -22.6957 322.622 +-340.74 -22.7677 323.228 -341.335 -21.9591 294.768 -340.727 -21.7167 294.419 +-341.444 -21.5588 321.063 -341.201 -21.8358 321.848 -340.699 -22.1492 322.94 +-340.901 -21.8082 322.497 -341.318 -20.9634 294.826 -340.637 -20.6021 294.332 +-341.291 -20.6403 321.768 -340.803 -20.9722 322.333 -341.384 -20.0155 294.835 +-341.012 -19.5591 294.409 -341.304 -19.5214 321.718 -340.796 -19.9848 322.343 +-340.732 -18.8545 294.274 -341.426 -18.6047 320.861 -341.189 -18.6487 321.842 +-340.811 -18.9955 322.355 -341.317 -17.6905 294.92 -340.71 -17.881 294.404 +-341.549 -17.8535 295.63 -341.404 -17.5138 318.849 -341.355 -17.7951 319.863 +-341.32 -17.6535 320.975 -341.24 -17.782 321.744 -340.857 -17.9894 322.361 +-340.73 -17.2913 294.792 -340.953 -16.7897 295.217 -341.554 -16.7552 296.828 +-341.35 -16.5699 318.88 -341.254 -16.7934 319.787 -341.305 -16.9013 320.71 +-341.216 -16.7079 321.583 -340.708 -16.8411 322.361 -340.7 -15.8745 295.289 +-341.441 -15.5752 297.998 -341.368 -15.7132 298.745 -341.365 -15.5635 318.894 +-341.277 -15.7809 319.815 -341.232 -15.8292 320.857 -340.707 -16.2116 321.907 +-340.906 -15.8189 321.484 -341.422 -14.8132 295.844 -340.635 -14.78 295.389 +-341.273 -14.6549 298.85 -341.37 -14.9638 299.641 -341.356 -14.5607 318.91 +-341.29 -14.7921 319.829 -341.23 -14.7312 320.751 -340.829 -14.9532 321.351 +-340.914 -14.2128 296.065 -340.803 -13.6119 296.125 -341.379 -13.6349 296.615 +-341.413 -13.6666 299.085 -341.363 -14.0036 299.794 -341.426 -13.6068 319.079 +-341.29 -13.8 319.857 -341.249 -13.7499 320.777 -340.864 -13.9313 321.38 +-340.623 -13.2813 295.689 -340.676 -12.8203 295.482 -341.211 -12.8542 295.995 +-341.381 -12.7387 319.614 -341.205 -12.7207 320.581 -340.704 -12.7829 321.386 +-341.014 -12.2334 295.943 -340.538 -11.8313 296.317 -341.349 -11.7077 296.806 +-341.412 -11.612 318.96 -341.186 -11.7764 319.908 -340.707 -12.1779 320.915 +-340.89 -11.8289 320.458 -340.932 -11.2179 297.06 -340.719 -10.5878 297.316 +-341.317 -10.7464 297.743 -341.416 -10.5916 318.982 -341.187 -10.7062 319.84 +-340.796 -11.0226 320.367 -341.257 -10.0042 297.847 -340.542 -9.78657 297.456 +-341.432 -9.70043 319.941 -340.896 -10.2743 320.714 -341.17 -9.65052 320.749 +-340.629 -9.86297 321.204 -340.856 -9.34326 297.893 -340.845 -8.7639 298.258 +-341.312 -8.59352 320.766 -340.697 -8.94351 321.36 -340.792 -7.75998 298.344 +-341.383 -7.74805 320.84 -340.887 -7.88835 321.352 -340.85 -6.73688 298.296 +-341.282 -6.47624 321.878 -341.033 -7.1847 321.703 -340.592 -6.92913 322.234 +-340.583 -5.68092 298.383 -341.309 -5.50931 321.872 -340.712 -5.97469 322.277 +-341.042 -5.18943 298.812 -340.805 -4.76906 299.281 -341.411 -4.6553 321.943 +-340.801 -4.83732 322.284 -340.853 -4.29923 299.749 -340.939 -3.50097 300.267 +-340.82 -3.73702 322.405 -340.762 -2.72738 300.319 -340.891 -2.68562 322.321 +-340.821 -2.36312 300.758 -340.914 -2.08261 301.617 -341.25 -1.69772 302.058 +-340.654 -1.7146 302.228 -340.861 -1.66368 321.308 -340.584 -2.2136 321.877 +-341.336 -1.62271 321.776 -340.819 -1.37934 302.681 -340.9 -1.17233 303.725 +-340.851 -0.806133 304.356 -340.946 -0.599736 320.424 -340.548 -1.2928 320.77 +-341.25 -0.997404 320.941 -340.89 -0.279869 304.735 -340.73 0.312603 305.226 +-340.84 0.232497 320.223 -340.775 0.603417 305.75 -340.951 0.834094 306.692 +-340.766 1.28771 307.263 -340.882 1.33169 319.502 -340.517 0.779477 319.838 +-341.235 1.1086 319.935 -340.746 1.64325 307.876 -340.814 1.72567 308.86 +-340.784 1.69271 309.828 -340.823 1.80884 310.731 -341.295 2.28546 311.052 +-340.787 1.81138 311.853 -341.399 2.18744 311.841 -340.667 1.68361 312.936 +-340.807 1.6591 313.858 -341.047 1.78213 314.789 -340.583 2.26262 315.415 +-340.848 2.31842 316.433 -340.693 1.83903 316.949 -341.381 2.13067 316.934 +-340.719 1.7174 317.927 -340.921 1.6424 318.841 -340.882 2.60448 315.958 +-341.368 16.3391 328.738 -340.85 16.1852 329.223 -340.71 15.8192 329.827 +-340.846 15.7674 330.794 -340.809 15.7879 331.789 -340.736 15.7309 332.825 +-340.971 15.7989 333.749 -340.795 16.3204 334.255 -340.598 16.8224 306.922 +-341.049 17.2428 306.343 -340.83 16.7173 307.799 -340.812 16.7483 308.808 +-340.809 16.7559 309.805 -340.811 16.7565 310.805 -340.778 16.7434 311.82 +-340.741 16.7146 312.843 -340.979 16.7974 313.917 -340.536 17.243 314.384 +-341.227 17.1669 314.726 -340.621 16.8067 323.838 -340.964 17.2413 323.375 +-340.836 16.7846 324.734 -340.795 16.8245 325.876 -340.844 16.7599 326.808 +-340.817 16.8142 327.86 -340.722 16.6391 328.774 -340.593 17.1513 334.432 +-340.656 17.8717 304.988 -340.93 18.4206 304.414 -341.352 18.1733 304.929 +-340.677 17.6364 305.847 -340.903 17.644 315.066 -340.709 17.7876 315.898 +-340.821 17.7655 316.801 -340.796 17.7473 317.833 -340.79 17.7378 318.836 +-340.797 17.7348 319.827 -340.806 17.7482 320.834 -340.787 17.7911 321.862 +-340.745 17.6883 322.774 -340.989 17.7695 334.83 -340.742 18.3466 335.287 +-340.775 19.2053 304.239 -340.805 19.1273 335.307 -340.707 20.2473 304.377 +-341.007 20.3212 334.387 -340.603 19.7431 334.841 -341.339 21.3616 304.913 +-340.855 20.8603 304.765 -340.7 21.2914 305.221 -340.761 21.2163 334.292 +-340.802 22.1959 305.382 -340.788 22.2458 334.33 -340.86 23.2772 305.299 +-340.801 23.2476 334.336 -340.596 24.1943 305.378 -340.797 24.2443 334.331 +-340.977 24.7631 305.827 -340.77 25.3458 306.248 -340.796 25.2444 334.331 +-340.845 26.2577 306.318 -340.797 26.2443 334.328 -340.597 27.1638 306.414 +-340.793 27.2402 334.327 -340.99 27.747 306.859 -340.754 28.2165 307.196 +-340.791 28.247 334.353 -340.546 28.7189 306.877 -341.01 29.3099 306.399 +-340.871 29.3076 334.279 -340.767 30.2208 306.288 -340.957 30.4324 333.45 +-340.543 29.7942 333.83 -341.258 30.1162 333.904 -340.788 31.2431 306.326 +-340.753 31.1978 333.284 -340.8 32.2457 306.334 -340.788 32.2408 333.31 +-340.797 33.2443 306.331 -340.827 33.2803 333.285 -340.796 34.2443 306.33 +-340.631 34.0377 333.456 -340.795 35.2447 306.329 -341.064 34.6745 333.833 +-340.734 35.1699 334.291 -340.789 36.2434 306.331 -340.806 36.241 334.362 +-340.796 37.2411 306.357 -340.798 37.248 334.328 -340.875 38.2791 306.269 +-340.806 38.2467 334.325 -340.534 38.7551 305.792 -340.996 39.4552 305.394 +-341.291 39.097 305.875 -340.813 39.2533 334.317 -340.598 40.023 305.393 +-340.794 40.2681 334.314 -341.053 40.7283 305.783 -340.723 41.2201 306.268 +-340.777 41.273 334.32 -340.802 42.2643 306.361 -340.789 42.2728 334.341 +-340.797 43.2513 306.336 -340.838 43.2824 334.366 -340.788 44.2374 306.333 +-340.785 44.2639 334.264 -340.813 45.2457 306.324 -341.401 45.323 321.777 +-341.22 45.2272 322.648 -341.29 45.2064 323.631 -340.846 45.3313 333.483 +-340.787 44.8533 333.893 -340.843 46.2744 306.3 -341.343 46.3072 316.679 +-340.918 46.1545 317.262 -340.751 45.8369 317.925 -340.827 45.7155 318.798 +-340.827 45.7739 319.799 -340.799 45.7909 320.81 -340.765 45.6781 321.916 +-340.753 45.6123 322.985 -340.861 45.7481 323.971 -340.645 45.8379 325.015 +-340.903 45.6988 325.724 -340.914 45.8235 326.84 -340.689 46.3226 327.245 +-341.225 46.1702 327.728 -341.287 46.3503 332.731 -340.895 46.0408 333.178 +-340.615 47.1766 306.401 -340.995 47.1858 312.364 -340.986 46.8986 313.046 +-340.679 47.3129 313.273 -341.15 47.1701 313.733 -340.939 47.1882 314.233 +-340.749 46.8158 314.823 -340.824 46.7654 315.835 -340.69 46.6474 316.916 +-340.816 46.6672 327.931 -340.719 46.7818 328.891 -340.84 46.7617 329.768 +-340.83 46.7584 330.782 -340.794 46.7941 331.833 -340.678 46.6412 332.754 +-341.058 47.7814 306.789 -340.617 48.2789 307.29 -341.347 48.3246 310.722 +-340.842 48.1841 311.217 -340.637 47.7278 311.847 -340.57 47.5835 313.811 +-340.846 48.6674 307.886 -340.823 48.7876 308.788 -340.778 48.8015 309.852 +-340.71 48.6204 310.769 -339.676 -30.164 300.923 -339.839 -29.6946 300.404 +-340.382 -29.8418 300.911 -339.749 -30.3177 301.88 -339.862 -30.2965 302.79 +-339.795 -30.3031 303.832 -339.766 -30.3304 304.878 -340.043 -30.2186 305.784 +-339.615 -29.7684 306.393 -339.795 -30.1426 311.984 -340.032 -29.8001 311.332 +-339.815 -30.2504 312.834 -339.833 -30.335 313.794 -339.776 -30.3355 314.824 +-340.047 -30.2575 315.888 -339.564 -29.7693 316.353 -340.307 -29.6969 316.899 +-339.62 -29.1924 298.865 -340.071 -28.8329 298.255 -339.891 -29.363 299.784 +-339.829 -29.3435 306.955 -339.782 -29.2399 307.843 -339.762 -29.2797 308.863 +-339.756 -29.277 309.85 -339.684 -29.3606 310.884 -339.658 -29.3661 316.964 +-339.793 -29.2122 317.766 -340.328 -28.7538 318.089 -340.327 -28.8098 319.053 +-339.661 -29.2051 318.861 -340.298 -28.8289 319.803 -339.834 -29.1384 319.708 +-339.715 -28.7635 320.32 -339.574 -28.3417 297.782 -339.993 -27.7132 297.412 +-340.278 -27.6848 320.924 -339.908 -28.1818 320.797 -339.671 -27.8257 321.409 +-339.788 -26.9187 297.22 -339.899 -27.4182 321.801 -340.265 -26.4881 322.878 +-339.973 -27.1391 322.686 -339.581 -26.7884 323.219 -339.558 -26.2916 296.84 +-339.99 -25.678 296.46 -340.422 -25.7885 322.826 -339.753 -25.9603 323.372 +-339.811 -24.8523 296.231 -340.334 -24.4652 323.898 -340.057 -25.1975 323.72 +-339.626 -24.9258 324.246 -339.765 -24.3671 294.76 -339.9 -23.8159 294.315 +-339.926 -24.3978 295.579 -340.26 -23.6461 323.796 -339.795 -23.9984 324.209 +-339.792 -22.7843 294.289 -339.994 -22.8768 323.439 -339.573 -23.2772 323.806 +-339.925 -21.7087 294.234 -340.362 -21.4623 322.823 -339.815 -21.9693 323.216 +-339.6 -21.2144 293.771 -339.63 -20.6877 293.499 -340.198 -20.8767 293.843 +-340.303 -20.541 322.824 -339.647 -20.9961 323.316 -339.877 -20.3667 293.844 +-339.883 -19.7292 294.222 -340.302 -19.5429 322.847 -339.737 -19.8538 323.348 +-339.836 -18.6938 294.296 -340.322 -18.5244 322.861 -339.801 -18.894 323.223 +-339.626 -17.7819 294.387 -340.366 -17.5083 322.879 -339.703 -17.955 323.268 +-340.033 -17.2014 294.802 -339.798 -16.6367 295.213 -340.295 -16.8034 322.891 +-339.641 -16.8398 323.267 -339.796 -15.7617 295.271 -340.354 -15.4988 321.826 +-339.892 -15.7626 322.347 -339.835 -16.2097 322.889 -339.612 -14.927 295.355 +-340.306 -14.5164 321.854 -339.77 -14.9103 322.212 -340.445 -13.8681 295.727 +-339.996 -13.7272 295.477 -340.358 -13.4524 321.897 -339.688 -13.943 322.278 +-339.783 -13.2275 295.779 -340.312 -12.6862 295.729 -339.68 -12.7747 296.138 +-340.287 -12.717 321.896 -339.788 -12.9362 322.17 -339.604 -11.9421 296.446 +-340.358 -11.4558 320.82 -339.854 -11.9338 321.278 -339.843 -12.3938 321.748 +-340.093 -11.2607 296.796 -339.745 -10.7665 297.19 -340.449 -10.6294 320.913 +-339.795 -10.8457 321.24 -339.626 -9.92117 297.448 -339.778 -9.86591 321.493 +-340.068 -9.24717 297.81 -339.722 -8.79106 298.264 -340.25 -8.47291 321.878 +-339.967 -9.20504 321.796 -339.569 -8.97905 322.236 -339.82 -7.7553 298.355 +-340.422 -7.67789 321.953 -339.79 -7.87335 322.324 -339.849 -6.74435 298.33 +-339.797 -6.77076 322.409 -339.689 -5.86778 298.336 -339.786 -5.7761 322.388 +-340.361 -4.80905 298.885 -339.721 -5.0196 298.437 -339.767 -4.78446 322.387 +-339.758 -4.23132 298.888 -339.782 -3.47395 299.286 -340.278 -3.78206 299.78 +-339.767 -3.77875 322.414 -340.245 -3.00301 299.885 -339.789 -2.79795 299.409 +-339.791 -2.88705 322.311 -339.781 -2.29859 299.887 -339.986 -2.10022 301.023 +-339.737 -1.7868 301.357 -340.2 -1.85992 301.771 -339.955 -1.70479 321.331 +-339.593 -2.25741 321.888 -339.762 -1.3458 301.942 -339.965 -1.11878 303.044 +-339.614 -0.721358 303.269 -340.253 -0.800457 303.885 -339.938 -0.670634 320.429 +-339.756 -1.21149 320.864 -339.797 -0.306964 303.929 -339.548 0.334675 304.353 +-340.255 0.16271 304.701 -339.921 0.165472 320.175 -339.932 0.682061 305.095 +-339.965 0.881569 306.016 -339.589 1.29702 306.255 -340.205 1.14879 306.745 +-340.004 1.26322 319.339 -339.54 0.718027 319.823 -339.834 1.58866 306.943 +-339.719 1.84565 307.848 -339.938 1.85532 308.868 -339.723 1.80419 309.91 +-339.745 1.67332 310.838 -339.75 1.67706 311.819 -339.746 1.68287 312.84 +-339.88 1.67426 313.746 -339.917 1.83877 314.735 -339.751 2.34043 315.249 +-340.011 2.25869 316.261 -339.603 1.8044 316.847 -339.847 1.72466 317.763 +-339.799 1.65599 318.708 -339.721 2.62125 315.724 -339.582 15.8312 329.978 +-339.99 16.2543 329.376 -339.824 15.7397 330.858 -339.796 15.7717 331.845 +-339.722 15.7318 332.869 -339.937 15.7934 333.784 -339.777 16.3238 334.27 +-339.603 16.8203 306.918 -340.027 17.2517 306.347 -339.84 16.7 307.803 +-339.816 16.7277 308.82 -339.797 16.7408 309.83 -339.786 16.7383 310.834 +-339.751 16.7229 311.859 -339.726 16.6948 312.891 -339.978 16.808 313.774 +-339.609 17.2398 314.34 -339.843 16.8546 323.949 -339.934 17.2224 323.355 +-339.619 17.2331 324.337 -340.266 17.1646 324.746 -340.312 17.2731 325.582 +-339.878 17.1931 326.245 -339.703 16.8166 326.865 -339.787 16.7732 327.874 +-339.758 16.6965 328.782 -339.58 17.1483 334.432 -339.571 17.8008 304.911 +-339.993 18.2748 304.354 -339.807 17.6435 305.731 -339.863 17.6741 314.882 +-339.837 17.7857 315.761 -339.807 17.755 316.795 -339.801 17.7387 317.828 +-339.795 17.7369 318.83 -339.801 17.7358 319.821 -339.809 17.7504 320.83 +-339.781 17.7859 321.86 -339.718 17.6731 322.758 -339.777 17.6685 325.111 +-339.621 17.6324 325.876 -339.98 17.757 334.839 -339.752 18.3541 335.309 +-339.773 19.1966 304.248 -339.87 19.1463 335.245 -339.58 20.1307 304.409 +-339.87 20.2258 334.376 -339.749 19.6217 334.762 -340.002 20.6977 304.854 +-339.745 21.2672 305.292 -339.765 21.2316 334.306 -339.791 22.1843 305.366 +-339.799 22.25 334.336 -339.833 23.2547 305.285 -339.8 23.2463 334.334 +-339.636 24.05 305.446 -339.796 24.2442 334.331 -340.045 24.7073 305.824 +-339.743 25.176 306.27 -339.796 25.2445 334.33 -339.856 26.2563 306.318 +-339.796 26.2432 334.326 -339.567 27.3314 306.402 -339.79 27.2414 334.332 +-340.024 27.8151 306.879 -339.928 28.3207 307.155 -339.804 28.2583 334.368 +-339.675 28.6403 306.807 -339.869 29.2468 306.406 -339.832 29.163 334.27 +-339.777 30.2446 306.31 -340 30.3173 333.397 -339.594 29.7536 333.835 +-339.797 31.245 306.332 -339.766 31.2159 333.291 -339.798 32.2444 306.332 +-339.814 32.2614 333.286 -339.796 33.2442 306.331 -339.703 33.2623 333.362 +-339.796 34.2444 306.33 -340.338 34.3573 333.915 -339.834 33.8806 333.745 +-339.705 34.3382 334.204 -339.794 35.2441 306.328 -339.789 35.2351 334.379 +-339.792 36.2411 306.337 -339.805 36.2481 334.34 -339.818 37.2447 306.36 +-339.795 37.2442 334.327 -339.725 38.2396 306.256 -339.799 38.2463 334.332 +-339.699 38.8309 305.872 -340.029 39.2455 305.461 -339.8 39.25 334.329 +-340.302 40.3867 305.875 -339.843 39.7783 305.712 -339.65 40.194 306.216 +-339.791 40.245 334.323 -339.793 41.2484 306.367 -339.786 41.2359 334.323 +-339.807 42.2433 306.334 -339.795 42.2407 334.333 -339.793 43.2387 306.326 +-339.825 43.2636 334.348 -339.788 44.237 306.333 -339.723 44.152 334.358 +-339.813 45.2488 306.326 -340.232 45.3685 333.764 -339.815 45.1135 334.239 +-339.843 46.2774 306.3 -340.346 46.3265 317.754 -339.847 46.1863 318.269 +-339.72 45.8031 318.845 -339.806 45.7188 319.831 -339.764 45.7371 320.835 +-339.753 45.7319 321.836 -340.031 45.8397 322.878 -339.551 46.2876 323.406 +-340.236 46.2209 323.758 -340.295 46.2999 324.574 -339.852 46.1944 325.249 +-339.897 45.8657 325.897 -339.552 46.2358 326.327 -340.242 46.1676 326.676 +-340.03 46.1306 333.404 -339.666 45.8555 333.94 -339.614 47.1788 306.398 +-340.452 47.2748 312.67 -340.051 47.2781 314.346 -339.592 46.8145 314.922 +-339.82 46.7319 315.843 -339.781 46.8056 316.901 -339.705 46.6447 317.817 +-339.742 46.6828 324.135 -339.601 46.6369 324.888 -339.901 46.658 327.026 +-339.695 46.7992 327.874 -339.809 46.7611 328.779 -339.801 46.7314 329.823 +-339.795 46.707 330.845 -339.778 46.724 331.853 -339.708 46.69 332.927 +-340.056 47.7834 306.793 -339.617 48.2797 307.287 -339.99 48.2619 311.343 +-339.578 47.8493 311.911 -339.923 47.7015 312.956 -339.656 47.6967 313.856 +-339.843 48.6677 307.896 -339.816 48.7797 308.812 -339.775 48.7925 309.862 +-339.753 48.6821 310.769 -338.607 -30.1451 300.992 -339.052 -29.7695 300.304 +-338.813 -30.3333 301.78 -338.801 -30.3106 302.823 -338.733 -30.2956 303.866 +-338.837 -30.3088 304.767 -338.902 -30.1604 305.731 -338.741 -29.6978 306.254 +-339.377 -29.7299 311.761 -339.265 -29.8147 312.578 -338.737 -29.6859 313.333 +-338.735 -30.1868 313.919 -338.724 -30.3489 314.847 -338.979 -30.2121 315.729 +-338.56 -29.7922 316.326 -338.671 -29.1268 298.982 -338.889 -28.6858 298.35 +-338.746 -29.3687 299.812 -338.713 -29.3473 306.863 -338.906 -29.251 307.721 +-338.762 -29.182 308.799 -338.876 -29.1768 309.897 -338.858 -29.1631 310.884 +-338.87 -29.271 311.981 -338.7 -29.2623 312.884 -338.827 -29.3566 316.894 +-338.775 -29.2619 317.811 -338.867 -29.3172 318.737 -338.921 -29.1867 319.738 +-338.747 -28.7229 320.275 -338.735 -28.1844 297.873 -338.925 -27.6183 297.471 +-338.722 -28.3801 320.887 -339.208 -27.7166 322.027 -338.971 -28.123 321.669 +-338.512 -27.7903 322.372 -338.925 -26.7854 297.208 -339.025 -27.2918 322.777 +-338.689 -26.6984 323.402 -338.547 -26.2672 296.85 -338.994 -25.6246 296.351 +-339.352 -25.6498 323.964 -338.838 -26.1618 323.822 -338.735 -25.6766 324.229 +-338.628 -25.1286 294.976 -338.892 -24.5812 294.422 -339.266 -24.7836 295.062 +-339.269 -24.8612 295.856 -338.691 -25.2863 295.688 -338.859 -24.7522 324.379 +-338.864 -23.7229 294.261 -338.696 -23.713 324.379 -338.761 -22.8063 294.329 +-339.025 -22.6391 323.433 -338.721 -23.1281 323.84 -338.712 -21.7621 294.238 +-338.83 -21.8256 323.452 -338.72 -21.1378 293.857 -338.966 -20.8358 293.509 +-339.316 -20.4526 323.875 -338.929 -21.1827 323.778 -338.615 -20.894 324.239 +-339.003 -20.3043 293.771 -338.693 -19.7704 294.219 -339.25 -19.6449 323.814 +-338.775 -19.9692 324.254 -338.821 -18.6934 294.279 -339.065 -18.7829 323.457 +-338.576 -19.2526 323.873 -338.658 -17.9123 294.437 -338.818 -17.6227 323.278 +-338.83 -17.3053 294.939 -338.831 -16.6917 295.36 -338.8 -16.5449 323.28 +-338.891 -15.8809 295.428 -339.201 -15.5598 322.815 -339.004 -15.8892 323.192 +-339.194 -14.5093 295.788 -338.889 -15.2525 295.694 -338.693 -14.8979 296.171 +-338.863 -14.8588 322.471 -338.596 -15.3883 322.826 -339.396 -13.6944 295.874 +-338.857 -13.8958 296.308 -338.823 -13.6864 322.32 -338.862 -12.8358 296.447 +-338.948 -12.5356 321.495 -338.48 -13.2168 321.879 -339.247 -12.8358 321.933 +-339.328 -11.6134 296.857 -338.855 -12.2172 296.719 -338.784 -11.7397 297.191 +-338.751 -11.8081 321.329 -338.865 -10.8209 297.477 -338.626 -10.684 321.445 +-339.338 -9.58305 297.884 -338.892 -10.1744 297.754 -338.655 -9.80956 298.214 +-339.363 -9.69341 321.939 -338.901 -10.1388 321.797 -338.809 -9.56232 322.178 +-338.771 -8.77134 298.376 -338.876 -8.58158 322.309 -338.81 -7.76356 298.375 +-338.881 -7.55349 322.291 -338.823 -6.64864 298.271 -338.871 -6.55926 322.273 +-338.904 -5.51058 298.237 -338.862 -5.58364 322.262 -338.986 -4.62507 298.205 +-338.842 -4.58203 322.268 -338.79 -4.21568 297.776 -338.76 -3.84594 297.452 +-339.146 -3.7907 298.058 -339.449 -3.67658 298.718 -338.845 -3.54821 322.297 +-338.722 -3.25376 297.657 -338.734 -2.63107 298.357 -339.294 -2.94519 298.741 +-338.959 -2.7385 322.208 -338.954 -2.18292 299.019 -338.718 -1.60455 299.369 +-339.127 -1.80921 299.704 -339.343 -1.71047 300.657 -338.954 -1.72606 321.493 +-338.677 -2.25805 321.696 -338.762 -1.30776 300.061 -338.939 -1.14294 300.979 +-338.785 -0.745078 301.315 -339.155 -0.811896 301.77 -339.327 -0.7584 302.567 +-339.241 -0.682499 320.751 -338.79 -0.908264 321.228 -338.672 -0.326236 301.876 +-338.929 -0.288382 303.082 -339.006 -0.145453 303.846 -338.625 0.387466 304.208 +-338.854 0.213322 320.402 -338.691 -0.170684 320.906 -339.315 1.10747 304.762 +-338.741 1.30501 304.451 -339.302 1.25054 305.505 -339.018 1.26951 319.384 +-338.665 0.871472 319.896 -338.803 1.72609 305.045 -338.892 1.75596 306.033 +-339.007 1.90109 306.986 -338.665 2.30903 307.234 -339.2 2.17191 307.758 +-339.359 2.25211 308.621 -339.269 2.25261 309.572 -338.91 2.16501 310.28 +-338.86 1.86209 310.919 -338.853 1.79533 311.876 -338.836 1.80675 312.886 +-339.001 1.84261 313.947 -338.632 2.31307 314.287 -339.211 2.16823 314.761 +-339.074 2.2563 316.313 -338.634 1.85513 316.998 -338.829 1.69209 317.768 +-338.743 1.66875 318.858 -338.645 2.60553 307.967 -338.738 2.65649 309.059 +-338.588 2.5941 309.999 -338.79 2.66049 314.979 -338.661 2.69345 315.946 +-338.574 15.8237 329.812 -339.047 16.2523 329.31 -338.947 15.7385 330.754 +-338.762 15.6702 331.641 -338.76 15.6496 332.782 -338.944 15.8163 333.804 +-338.793 16.3606 334.25 -338.606 16.8092 306.77 -339.09 17.243 306.278 +-338.845 16.7191 307.827 -338.81 16.7427 308.847 -338.785 16.757 309.855 +-338.769 16.7482 310.851 -338.738 16.7258 311.865 -338.848 16.6981 312.767 +-338.904 16.841 313.71 -338.776 17.3098 314.208 -338.798 16.8814 323.802 +-338.93 17.2105 323.345 -338.658 17.2213 324.31 -338.562 16.8381 326.98 +-339.002 17.2723 326.37 -338.911 16.74 327.754 -338.707 16.6288 328.589 +-338.586 17.1753 334.417 -338.638 17.8275 304.808 -338.998 18.2754 304.375 +-338.744 17.5958 305.547 -338.776 17.68 314.785 -338.837 17.7712 315.78 +-338.8 17.7458 316.808 -338.799 17.7321 317.826 -338.801 17.7338 318.824 +-338.801 17.7338 319.821 -338.805 17.7508 320.84 -338.786 17.7905 321.876 +-338.721 17.6728 322.772 -338.875 17.7307 324.927 -338.802 17.745 325.737 +-338.991 17.7685 334.846 -338.83 18.3946 335.211 -338.794 19.2237 304.263 +-338.959 19.4398 334.453 -338.53 18.7827 334.853 -339.246 19.1112 334.928 +-338.601 20.3308 304.386 -338.757 20.2109 334.274 -339.061 20.7702 304.852 +-338.694 21.4711 305.232 -338.774 21.2381 334.318 -338.917 22.3267 305.342 +-338.804 22.2507 334.338 -338.704 23.231 305.4 -338.799 23.2459 334.332 +-339.337 24.364 305.905 -338.849 23.8577 305.755 -338.704 24.3037 306.213 +-338.796 24.2441 334.33 -338.806 25.2052 306.378 -338.796 25.244 334.329 +-338.853 26.2568 306.32 -338.794 26.2415 334.324 -338.697 27.1506 306.292 +-338.786 27.2461 334.343 -339.433 28.1534 306.804 -338.901 28.2416 306.394 +-338.811 28.2748 334.384 -338.779 29.2577 306.307 -338.889 29.1783 334.212 +-338.79 30.2446 306.325 -338.869 30.2217 333.381 -338.739 29.6282 333.76 +-338.799 31.2437 306.333 -338.782 31.2331 333.276 -338.797 32.2441 306.331 +-338.763 32.1942 333.377 -338.796 33.2443 306.33 -338.775 33.0415 333.479 +-338.796 34.2443 306.329 -339.033 33.6896 333.811 -338.725 34.1958 334.274 +-338.793 35.2433 306.328 -338.799 35.2526 334.353 -338.802 36.2467 306.336 +-338.798 36.2459 334.327 -338.825 37.2652 306.35 -338.794 37.2429 334.33 +-338.693 38.1474 306.358 -338.795 38.2439 334.334 -339.427 39.3358 305.875 +-338.909 39.1273 306.273 -338.796 39.2438 334.334 -338.792 40.2592 306.363 +-338.796 40.2415 334.332 -338.806 41.251 306.34 -338.794 41.2385 334.329 +-338.799 42.2424 306.325 -338.799 42.2449 334.329 -338.792 43.2417 306.33 +-338.816 43.2684 334.339 -338.788 44.239 306.336 -338.84 44.293 334.353 +-338.813 45.2505 306.326 -338.72 45.1854 334.373 -338.842 46.2781 306.297 +-339.066 46.2592 318.325 -338.608 45.8037 318.761 -338.824 45.7188 319.839 +-338.768 45.7214 320.854 -338.861 45.7009 321.734 -338.897 45.8677 322.668 +-338.717 46.3574 323.207 -339.028 46.2603 325.324 -338.672 45.8502 325.783 +-338.778 46.1583 326.42 -339.343 46.3778 333.77 -338.797 46.1727 334.28 +-338.611 47.1693 306.397 -339.003 47.2683 314.334 -338.601 46.8173 314.914 +-338.812 46.7198 315.833 -338.908 46.7814 316.748 -338.698 46.66 317.603 +-338.872 46.6803 323.723 -338.892 46.6771 324.582 -338.905 46.6286 326.867 +-338.81 46.8048 327.751 -338.771 46.7477 328.799 -338.907 46.7098 329.718 +-338.791 46.8059 330.778 -338.719 46.782 331.929 -338.887 46.703 332.75 +-338.703 46.7875 333.715 -339.055 47.781 306.789 -338.613 48.2715 307.302 +-338.997 48.2442 311.314 -338.589 47.812 311.916 -338.813 47.7538 312.839 +-338.767 47.6969 313.756 -338.834 48.6731 307.915 -338.806 48.7903 308.831 +-338.779 48.7942 309.871 -338.762 48.6704 310.754 -338.331 -29.7094 300.645 +-337.879 -29.8546 301.257 -337.864 -30.1504 301.892 -337.86 -30.2084 302.852 +-337.815 -30.1925 303.869 -337.972 -30.1553 304.941 -337.718 -29.785 305.37 +-338.184 -29.8459 305.767 -338.283 -29.7857 313.809 -337.86 -29.8804 314.245 +-337.793 -30.1938 314.914 -337.974 -30.1917 315.764 -337.742 -29.6877 316.391 +-338.35 -28.7896 298.766 -337.965 -28.8411 299.218 -337.74 -29.1299 299.905 +-337.659 -29.4095 300.877 -337.782 -29.3417 305.911 -337.779 -29.1364 306.984 +-338.05 -29.1346 307.918 -337.636 -28.7116 308.273 -338.158 -28.8379 308.817 +-338.303 -28.7724 309.669 -338.29 -28.7742 310.673 -338.297 -28.7668 311.678 +-338.266 -28.7408 312.62 -337.791 -28.7459 313.332 -337.819 -29.3804 313.853 +-337.898 -29.2712 316.965 -337.861 -29.1599 317.847 -338.005 -29.1613 318.939 +-337.61 -28.7123 319.28 -338.201 -28.8551 319.755 -338.224 -27.6585 297.755 +-337.994 -27.9508 298.188 -337.661 -28.3487 298.871 -337.691 -28.4291 308.888 +-337.725 -28.2569 309.926 -337.821 -28.2555 311.094 -337.824 -28.2378 312.113 +-337.677 -28.218 312.876 -337.731 -28.3947 320.005 -337.769 -28.2847 320.859 +-337.923 -28.1507 321.714 -337.719 -27.7225 322.28 -337.519 -27.321 297.817 +-337.899 -26.8042 297.348 -337.896 -27.2825 322.807 -337.58 -26.7878 323.396 +-337.677 -26.1067 296.94 -338.047 -25.7643 296.398 -338.005 -26.3079 323.887 +-337.798 -25.6851 324.262 -338.268 -24.7524 294.7 -337.802 -24.7766 295.187 +-337.698 -25.2901 295.865 -337.784 -24.865 324.505 -337.674 -24.2661 294.881 +-337.91 -23.8058 294.414 -338.232 -23.6699 324.882 -338.014 -24.1462 324.807 +-337.616 -23.9623 325.117 -337.866 -22.6525 294.326 -338.285 -22.5044 323.786 +-337.833 -22.9554 324.265 -337.86 -23.42 324.747 -337.603 -21.8909 294.317 +-338.382 -21.6994 323.9 -337.756 -21.8669 324.28 -338.406 -20.6249 293.855 +-337.867 -20.8724 294.231 -337.821 -20.718 324.442 -337.733 -19.7872 294.388 +-337.888 -19.8083 324.242 -337.918 -18.7952 294.439 -337.916 -18.6746 323.344 +-337.727 -19.3394 323.788 -338.293 -17.6878 294.799 -337.921 -18.147 294.722 +-337.592 -17.8235 295.224 -337.695 -17.6652 322.376 -337.575 -18.2372 322.917 +-338.189 -17.8153 322.906 -337.93 -17.1719 295.664 -338.252 -16.5561 295.971 +-337.489 -16.7838 296.302 -338.306 -16.9369 322.866 -337.77 -16.4843 322.348 +-338.464 -15.6816 296.021 -337.841 -16.2985 296.76 -338.185 -15.7546 296.751 +-337.644 -15.7482 297.191 -338.31 -15.961 322.897 -337.852 -15.467 322.384 +-338.325 -14.6818 296.89 -337.622 -14.9297 297.354 -337.827 -14.5123 322.313 +-338.404 -13.5896 296.929 -337.846 -14.2326 297.752 -338.189 -13.7166 297.768 +-337.624 -13.7278 298.189 -337.93 -13.7652 322.23 -338.402 -12.6533 297.015 +-338.204 -12.8087 297.731 -337.712 -12.9923 298.394 -337.924 -12.588 321.334 +-337.73 -13.2671 321.755 -338.251 -11.7079 297.741 -337.747 -11.8794 298.357 +-337.839 -11.5621 321.246 -338.377 -10.7059 297.913 -337.868 -10.7875 298.241 +-337.749 -10.6082 321.261 -337.744 -9.78205 298.35 -338.377 -9.89963 321.812 +-337.856 -9.655 321.379 -337.76 -8.7705 298.366 -338.25 -9.00807 321.897 +-337.775 -8.51558 321.355 -337.909 -7.71175 298.215 -338.261 -7.98747 321.875 +-337.797 -7.52548 321.332 -337.716 -7.32924 297.726 -337.722 -6.71394 297.45 +-338.204 -6.81221 297.892 -338.27 -6.99712 321.853 -337.817 -6.52868 321.328 +-338.319 -5.95963 297.837 -337.697 -5.76225 297.445 -338.277 -6.01217 321.854 +-337.665 -5.51953 321.355 -338.398 -4.76412 297.687 -337.719 -4.78231 297.314 +-338.273 -5.02461 321.858 -337.802 -4.64271 321.495 -337.558 -4.17841 296.67 +-337.759 -3.63985 296.46 -338.25 -3.87879 297.014 -338.277 -3.86464 321.757 +-337.729 -3.80196 321.282 -337.968 -3.23403 296.984 -337.771 -2.52633 297.246 +-338.295 -2.68333 297.8 -337.674 -2.6644 320.49 -337.596 -3.16919 320.781 +-338.174 -2.75736 321.072 -338.464 -2.92111 321.87 -338.138 -1.84388 297.931 +-337.73 -1.70189 297.461 -338.451 -1.8043 298.724 -337.765 -2.3057 320.735 +-337.926 -1.75561 321.252 -337.89 -1.17082 297.961 -337.766 -0.552772 298.416 +-338.16 -0.945406 298.663 -338.289 -0.87124 299.58 -338.365 -0.7031 300.665 +-337.669 -0.847872 321.403 -337.803 -0.253256 299.122 -337.763 -0.242463 300.032 +-337.946 -0.146397 300.982 -337.8 0.237515 301.313 -338.184 0.193397 301.77 +-338.296 0.249467 302.642 -338.323 0.17844 303.612 -338.31 0.301548 320.857 +-337.782 0.158312 321.256 -337.694 0.671179 301.878 -337.766 0.746251 303.125 +-337.869 0.695335 303.937 -337.89 1.30874 304.225 -338.303 1.34414 319.708 +-337.966 1.12091 320.303 -337.786 0.830177 320.872 -338.339 2.19293 304.768 +-337.831 2.21856 304.308 -338.28 2.29647 305.552 -338.312 2.25519 306.513 +-338.412 2.30019 310.771 -338.277 2.22302 311.629 -338.258 2.23175 312.643 +-338.295 2.24132 313.585 -338.37 2.32119 316.655 -337.926 2.15863 317.246 +-337.739 1.83191 317.911 -337.802 1.71676 318.825 -337.677 1.64612 319.91 +-337.619 2.67111 304.932 -337.651 2.64453 306.028 -337.943 2.62945 306.976 +-337.703 2.84788 307.831 -337.949 2.86615 308.838 -337.942 2.87822 309.835 +-337.764 2.75421 310.953 -337.662 2.62757 312.043 -337.663 2.63725 313.042 +-337.961 2.65043 313.984 -337.603 2.8389 314.86 -337.833 2.76315 315.8 +-337.711 2.62573 316.857 -337.594 14.8351 331.734 -337.965 15.3136 331.401 +-338.292 15.2339 332.1 -338.18 15.2815 332.904 -337.738 14.8386 332.766 +-337.65 15.1135 333.437 -337.66 15.8745 329.015 -337.831 16.3176 328.473 +-338.342 16.1732 328.964 -337.814 15.7401 329.853 -337.844 15.6353 330.7 +-338.051 15.6774 333.739 -337.753 16.206 334.25 -337.689 16.8655 306.007 +-337.866 17.3067 305.442 -338.382 17.1645 305.949 -337.71 16.7323 306.986 +-337.826 16.7543 307.863 -337.786 16.7784 308.882 -337.754 16.7909 309.886 +-337.747 16.7821 310.878 -337.738 16.7533 311.874 -337.983 16.8214 312.927 +-337.531 17.2894 313.378 -338.239 17.1746 313.719 -337.81 16.8701 323.857 +-337.949 17.2031 323.375 -337.62 17.2277 324.367 -337.571 16.825 326.81 +-338.055 17.2687 326.286 -337.867 16.6558 327.867 -337.638 17.0587 334.466 +-337.66 17.7294 304.894 -337.99 18.3108 304.395 -337.898 17.656 314.079 +-337.703 17.7913 314.927 -337.818 17.7689 315.822 -337.79 17.7525 316.853 +-337.784 17.7453 317.859 -337.791 17.7475 318.852 -337.784 17.7488 319.851 +-337.782 17.7617 320.872 -337.773 17.7939 321.908 -337.737 17.6713 322.806 +-337.979 17.6856 324.85 -337.73 17.6904 325.55 -338.068 17.7225 334.845 +-337.755 18.1131 335.22 -337.817 19.2527 304.281 -338.008 19.3196 334.395 +-337.603 18.7456 334.854 -337.742 20.1793 304.289 -337.759 20.2136 334.291 +-338.36 21.1309 304.764 -337.692 21.1365 304.438 -337.789 21.2479 334.332 +-337.826 21.6289 304.906 -337.852 22.234 305.246 -337.807 22.2526 334.338 +-337.562 23.1384 305.436 -337.801 23.2475 334.332 -337.995 23.7121 305.844 +-337.751 24.3009 306.276 -337.797 24.2451 334.33 -337.793 25.2088 306.374 +-337.796 25.2433 334.327 -337.83 26.2509 306.325 -337.791 26.2389 334.325 +-337.809 27.2728 306.307 -337.79 27.2512 334.353 -337.785 28.262 306.313 +-337.87 28.3161 334.28 -337.788 29.2457 306.324 -337.953 29.4378 333.452 +-337.54 28.7897 333.833 -338.251 29.1259 333.907 -337.798 30.2435 306.333 +-337.771 30.2179 333.253 -337.797 31.2442 306.332 -337.701 31.2404 333.375 +-337.796 32.2442 306.331 -338.24 32.4992 333.833 -337.885 31.857 333.769 +-337.607 32.2131 334.186 -337.796 33.2443 306.331 -338.4 33.3056 333.897 +-337.793 33.1417 334.287 -337.795 34.2439 306.33 -337.802 34.2565 334.361 +-337.793 35.2436 306.327 -337.8 35.2509 334.331 -337.804 36.2529 306.328 +-337.795 36.2434 334.328 -337.815 37.2701 306.34 -337.794 37.245 334.333 +-337.805 38.2672 306.349 -337.799 38.2448 334.331 -337.79 39.2564 306.347 +-337.8 39.2399 334.329 -337.793 40.2518 306.334 -337.792 40.2372 334.331 +-337.8 41.2461 306.324 -337.791 41.2395 334.331 -337.797 42.2449 306.328 +-337.796 42.2428 334.33 -337.793 43.243 306.332 -337.804 43.2558 334.332 +-337.789 44.2397 306.336 -337.819 44.2822 334.339 -337.813 45.2508 306.325 +-337.833 45.3147 334.353 -337.841 46.2665 306.299 -337.821 46.3179 317.434 +-337.681 45.8467 317.965 -338.363 46.1728 317.941 -337.718 45.7203 318.948 +-337.825 45.7473 319.853 -337.786 45.7515 320.859 -337.869 45.8197 321.958 +-337.603 46.2127 322.461 -338.166 46.2031 322.684 -338.193 46.3397 323.93 +-337.785 46.2842 323.235 -337.538 45.8697 323.703 -337.816 45.783 324.896 +-338.398 46.1829 324.903 -337.688 45.6528 325.956 -337.998 45.8523 326.603 +-338.275 46.3243 326.936 -337.547 46.2494 327.261 -337.704 46.2123 334.374 +-337.613 47.1571 306.41 -338.063 47.2478 314.281 -337.613 46.8049 314.754 +-337.818 46.741 315.841 -337.843 46.6792 316.855 -338.006 46.5266 323.01 +-337.854 46.7101 327.743 -337.759 46.757 328.819 -337.901 46.7924 329.962 +-337.687 47.1854 330.486 -338.192 47.1946 330.76 -338.25 47.2631 331.583 +-337.739 47.3283 332.309 -338.06 46.8868 332.971 -337.674 47.4216 333.369 +-338.144 47.2253 333.767 -337.901 47.07 334.27 -338.048 47.7623 306.805 +-337.595 48.2657 307.322 -338.056 48.2413 311.386 -337.607 47.8915 312.074 +-337.934 47.7191 312.726 -337.719 47.6199 313.588 -337.79 47.5842 331.083 +-337.74 47.6425 331.758 -337.677 47.5706 333.976 -337.822 48.6781 307.942 +-337.784 48.8038 308.854 -337.898 48.8018 309.748 -337.589 48.6231 310.844 +-337.373 -29.7156 301.792 -337.239 -29.7924 302.65 -337.218 -29.8081 303.633 +-337.309 -29.751 304.695 -337.361 -29.5181 314.788 -337.378 -29.7278 315.85 +-337.361 -28.695 299.679 -336.889 -28.8345 300.269 -336.85 -29.1036 300.905 +-336.846 -29.2684 301.951 -336.768 -29.2807 303.066 -336.752 -29.2931 304.045 +-336.931 -29.1723 304.969 -336.725 -28.6166 305.376 -337.168 -28.8254 305.735 +-337.293 -28.7581 306.602 -337.313 -28.7797 307.555 -337.306 -28.9002 313.979 +-337.166 -28.661 314.81 -337.285 -28.9141 315.75 -337.297 -28.7699 316.628 +-337.262 -28.7729 317.637 -337.283 -28.7831 318.579 -336.623 -28.1188 299.01 +-336.838 -27.6285 298.375 -336.675 -28.4223 299.877 -336.806 -28.2918 306.095 +-336.776 -28.2638 307.036 -336.901 -28.2853 308.035 -337.003 -28.1117 308.941 +-336.696 -27.6574 309.334 -337.164 -27.8368 309.699 -337.318 -27.7567 310.57 +-337.315 -27.7441 311.599 -337.234 -27.731 312.568 -336.975 -27.9145 313.277 +-336.84 -28.2885 313.991 -336.873 -28.1463 314.892 -337.16 -28.0138 315.758 +-336.778 -27.6042 315.435 -336.794 -28.1976 317.088 -336.68 -27.6682 316.42 +-336.768 -28.2714 318.046 -336.906 -28.2657 319.055 -336.755 -28.144 319.944 +-337.033 -28.14 320.892 -336.598 -27.7162 321.28 -337.193 -27.8513 321.753 +-336.722 -27.1068 297.871 -336.981 -26.75 297.536 -336.779 -27.3348 310.052 +-336.778 -27.265 310.995 -336.766 -27.2555 312.024 -336.678 -27.3905 312.898 +-336.719 -27.2509 315.928 -336.819 -27.4192 321.964 -336.856 -27.1109 322.758 +-336.735 -26.7157 323.242 -337.328 -25.6689 296.752 -336.81 -26.0348 297.189 +-336.881 -26.2985 323.778 -336.67 -25.7183 324.376 -336.713 -25.1576 296.036 +-336.882 -24.6683 295.466 -336.925 -25.4461 296.667 -337.402 -24.6391 324.962 +-336.851 -25.1774 324.839 -336.734 -24.7371 325.221 -337.183 -23.6596 294.754 +-336.839 -23.8698 295.179 -336.632 -24.1835 324.957 -337.332 -23.7202 324.869 +-336.949 -23.529 324.508 -336.601 -23.1969 294.877 -337.005 -22.8301 294.442 +-336.736 -22.7929 324.348 -336.835 -21.7518 294.4 -336.752 -21.6879 324.306 +-336.867 -20.7928 294.455 -336.991 -20.5834 324.239 -337.237 -19.5158 294.789 +-336.679 -20.2693 294.888 -336.666 -19.8191 295.306 -336.808 -19.5647 323.434 +-336.727 -20.2713 323.864 -337.191 -19.8689 323.938 -337.41 -18.7041 294.859 +-336.87 -18.9102 295.285 -336.893 -18.6982 321.26 -336.744 -19.1128 321.723 +-337.182 -18.5236 321.892 -336.703 -18.9073 322.308 -336.922 -18.7102 323.154 +-336.728 -17.972 295.457 -336.791 -17.4785 320.362 -336.735 -18.2165 320.678 +-337.189 -17.8492 321.087 -337.434 -17.7902 321.721 -336.98 -18.4122 322.701 +-336.994 -17.3025 295.799 -336.7 -16.8841 296.309 -337.203 -16.7699 320.902 +-336.794 -16.5416 320.316 -337.431 -16.9769 321.686 -337.051 -16.306 296.872 +-336.866 -15.6663 297.276 -337.186 -15.7714 320.932 -336.79 -15.5522 320.357 +-337.38 -15.9514 321.71 -336.722 -14.9183 297.488 -337.178 -14.7651 320.97 +-336.794 -14.5421 320.38 -337.347 -14.9649 321.762 -337.041 -14.2681 297.832 +-336.87 -13.7192 298.35 -337.188 -13.7493 320.737 -336.873 -13.693 320.223 +-337.383 -13.8872 321.703 -337.336 -12.467 298.891 -336.955 -13.1631 298.816 +-336.606 -12.9141 299.207 -336.8 -12.4737 319.366 -336.728 -13.1783 319.716 +-337.161 -12.7964 320.089 -337.367 -12.9005 321.002 -337.248 -11.695 298.798 +-336.672 -11.8707 299.244 -337.197 -11.7524 319.905 -336.806 -11.5469 319.302 +-337.373 -11.9689 320.703 -336.711 -11.1587 298.928 -336.951 -10.7258 298.464 +-337.21 -10.7417 319.924 -336.802 -10.5403 319.305 -337.385 -10.9555 320.738 +-336.862 -9.64312 298.338 -337.149 -9.79435 319.921 -336.719 -9.68869 319.445 +-337.406 -9.93851 320.747 -336.81 -8.71483 298.262 -336.847 -9.17353 319.966 +-337.291 -8.96494 320.704 -336.753 -8.46182 320.321 -336.508 -8.21464 297.847 +-336.983 -7.56969 297.449 -337.253 -7.90586 297.889 -337.284 -7.97248 320.851 +-336.766 -7.50721 320.379 -336.781 -6.80651 297.314 -337.314 -6.9689 320.833 +-336.806 -6.47601 320.343 -336.768 -5.66018 297.299 -337.272 -5.99873 320.833 +-336.816 -5.77452 320.417 -337.006 -4.69696 297.209 -336.741 -5.33393 320.713 +-336.973 -4.62245 321.248 -336.821 -4.25599 295.88 -336.71 -3.75253 295.428 +-337.333 -3.67473 295.971 -336.806 -4.39211 296.646 -337.02 -3.75 321.17 +-337.215 -2.78628 295.872 -336.737 -2.71031 295.279 -337.461 -2.95785 296.54 +-336.72 -2.83205 320.484 -336.737 -3.3668 320.778 -337.173 -1.80693 295.888 +-336.782 -1.69891 295.401 -337.394 -1.85482 296.803 -337.031 -2.25346 320.79 +-336.772 -1.64871 321.287 -336.892 -1.17809 295.922 -336.735 -0.601114 296.389 +-337.177 -0.82024 296.787 -337.464 -0.759929 297.749 -336.804 -0.741642 321.404 +-336.879 -0.137872 296.996 -336.825 0.313526 297.379 -337.163 0.0904585 297.71 +-337.322 0.168791 298.627 -337.274 0.263133 299.611 -337.363 0.27467 300.668 +-336.693 0.156859 321.367 -336.698 0.715738 297.908 -336.804 0.773414 299.082 +-336.762 0.725811 300.05 -336.968 0.837184 300.983 -336.685 1.29262 301.232 +-337.212 1.17907 301.744 -337.316 1.37118 302.754 -337.455 1.26827 303.716 +-337.343 1.41313 320.756 -336.854 1.05646 321.232 -336.631 1.58497 301.956 +-336.809 1.83168 302.897 -336.501 2.2628 303.406 -337.245 2.02615 303.707 +-337.343 2.30275 317.699 -336.89 2.14695 318.278 -336.889 1.86698 318.9 +-336.749 1.83551 319.915 -336.734 1.64678 320.774 -336.978 2.65137 304.044 +-336.721 2.88473 304.841 -336.895 2.85637 305.837 -337.064 2.87155 306.884 +-336.666 3.31243 307.26 -337.188 3.17318 307.771 -337.341 3.24709 308.62 +-337.343 3.26001 309.617 -337.289 3.27413 310.552 -336.96 3.20696 311.259 +-336.702 2.87125 311.907 -336.874 2.72154 312.665 -336.967 2.86008 313.779 +-336.743 3.32541 314.273 -337.18 3.1944 314.704 -336.988 3.1788 315.28 +-336.925 2.89652 315.89 -336.749 2.82805 316.882 -336.678 2.59983 317.894 +-336.637 3.6143 307.963 -336.735 3.64305 309.069 -336.735 3.65554 310.061 +-336.609 3.60928 310.989 -336.599 3.58407 314.922 -336.692 14.8299 330.79 +-336.928 15.3112 330.369 -337.392 15.1701 330.936 -336.727 14.6848 331.93 +-336.824 14.6501 332.86 -337.328 15.321 333.971 -336.942 14.8095 333.684 +-336.708 15.294 334.243 -336.625 15.85 328.677 -337.061 16.2432 328.292 +-336.826 15.6241 329.659 -336.703 16.2302 334.429 -336.575 16.8221 305.773 +-337.063 17.2269 305.235 -336.955 16.7479 306.732 -336.755 16.6741 307.631 +-336.894 16.6694 308.637 -336.87 16.6807 309.629 -336.86 16.68 310.631 +-336.854 16.6496 311.642 -336.96 16.7415 312.679 -336.577 17.1436 313.332 +-336.876 16.893 323.802 -336.901 17.1496 323.303 -336.75 17.3257 324.208 +-336.67 16.871 326.04 -336.829 17.343 325.469 -337.352 17.1765 325.962 +-336.838 16.7393 326.867 -336.755 16.6115 327.587 -337.345 17.3934 334.927 +-336.853 16.8872 334.762 -336.738 17.3568 335.237 -336.668 17.5807 304.701 +-336.831 18.217 304.376 -336.809 17.7436 313.899 -336.788 17.8248 314.807 +-336.772 17.7897 315.855 -336.762 17.777 316.887 -336.755 17.778 317.892 +-336.76 17.7805 318.886 -336.756 17.7833 319.881 -336.74 17.7889 320.894 +-336.874 17.801 321.79 -336.681 17.641 322.637 -336.767 17.6608 324.84 +-336.892 18.1481 335.244 -336.813 19.2573 304.301 -336.865 19.2073 334.376 +-336.757 18.608 334.765 -336.869 20.3218 304.289 -336.746 20.2135 334.299 +-336.598 21.2156 304.385 -336.784 21.2418 334.334 -336.999 21.8042 304.812 +-336.789 22.388 305.212 -336.805 22.2502 334.338 -336.585 23.1657 305.43 +-336.805 23.2488 334.334 -336.991 23.7312 305.845 -336.739 24.3105 306.263 +-336.8 24.2463 334.331 -336.775 25.2052 306.37 -336.797 25.2426 334.326 +-336.805 26.2427 306.335 -336.789 26.2399 334.331 -336.796 27.2537 306.321 +-336.802 27.257 334.368 -336.793 28.2463 306.327 -336.83 28.1601 334.272 +-336.798 29.2451 306.331 -337.016 29.3228 333.381 -336.596 28.75 333.836 +-336.798 30.2458 306.331 -336.781 30.2403 333.244 -336.797 31.2449 306.33 +-336.62 31.0383 333.455 -336.796 32.2443 306.331 -337.082 31.6779 333.855 +-336.748 32.184 334.304 -336.796 33.2443 306.331 -336.811 33.2536 334.374 +-336.795 34.2443 306.329 -336.801 34.2549 334.333 -336.792 35.2435 306.326 +-336.794 35.2436 334.326 -336.801 36.2389 306.333 -336.794 36.2442 334.332 +-336.804 37.2361 306.345 -336.804 37.2431 334.326 -336.788 38.2269 306.348 +-336.806 38.2278 334.316 -336.778 39.2231 306.339 -336.786 39.2193 334.314 +-336.789 40.2381 306.327 -336.769 40.2287 334.32 -336.8 41.2467 306.326 +-336.779 41.2371 334.332 -336.797 42.2457 306.329 -336.794 42.2393 334.335 +-336.793 43.2431 306.332 -336.798 43.2461 334.321 -336.789 44.2405 306.335 +-336.809 44.2512 334.303 -336.812 45.2464 306.326 -336.804 45.2553 334.309 +-336.843 46.2618 306.308 -336.983 46.2418 317.321 -336.594 45.8319 317.946 +-336.822 45.7397 318.837 -336.932 45.775 319.738 -336.724 45.6814 320.641 +-336.807 45.6454 321.758 -336.925 45.8249 322.818 -337.476 46.2086 322.854 +-336.711 45.7052 323.924 -336.852 45.6979 324.789 -336.821 45.6837 325.817 +-336.907 45.781 326.831 -336.749 46.1784 327.442 -336.798 46.296 334.348 +-336.62 47.3319 306.379 -336.845 47.3046 313.476 -336.702 46.8493 313.986 +-337.367 47.1596 313.954 -336.725 46.727 314.966 -336.808 46.7665 315.872 +-336.747 46.6684 316.772 -336.872 46.6356 327.903 -336.774 46.7907 328.817 +-336.753 46.7135 329.848 -336.954 46.8676 330.582 -337.262 47.3357 330.905 +-336.535 47.2296 331.242 -337.539 47.2833 332.847 -336.826 47.2109 334.288 +-337.097 47.8236 306.811 -336.746 48.3491 307.167 -336.987 48.3012 310.4 +-336.581 47.8907 310.7 -337.202 48.2455 310.844 -337.319 48.3481 311.732 +-336.799 48.274 311.369 -336.847 48.2181 312.189 -336.705 47.7548 312.769 +-337.087 47.7693 331.758 -337.206 48.014 332.84 -336.543 48.3639 332.329 +-336.794 47.8167 333.77 -336.761 48.3563 333.297 -336.712 48.7067 307.817 +-336.907 48.81 308.719 -336.806 48.6584 309.687 -336.866 48.5455 311.757 +-336.616 48.6111 332.9 -336.37 -28.7133 300.783 -336.257 -28.7684 301.631 +-336.294 -28.7653 302.613 -336.314 -28.6809 303.729 -336.46 -28.6522 304.758 +-336.266 -27.779 298.783 -335.881 -27.8734 299.25 -335.876 -28.144 299.901 +-335.863 -28.2897 300.958 -335.799 -28.2617 302.071 -335.662 -28.2892 303.087 +-335.833 -28.2409 303.909 -336.181 -27.9749 304.664 -335.542 -27.6484 304.372 +-336.324 -27.8664 305.587 -336.293 -27.7487 306.585 -336.262 -27.7043 307.698 +-336.419 -27.6908 308.724 -336.38 -27.5116 313.81 -336.437 -27.6839 314.798 +-336.372 -27.7579 316.851 -336.276 -27.7736 317.568 -336.284 -27.7607 318.597 +-336.267 -27.7661 319.601 -336.292 -27.7784 320.55 -336.273 -26.5867 297.633 +-335.988 -26.899 298.239 -335.798 -27.3658 298.878 -335.854 -27.2693 305.083 +-335.811 -27.2362 306.025 -335.776 -27.2981 307.037 -335.832 -27.1863 307.898 +-336.127 -26.9046 308.788 -335.693 -26.6699 308.456 -336.363 -26.842 309.757 +-336.371 -26.6623 310.796 -336.373 -26.6499 311.799 -336.363 -26.7633 312.941 +-336.278 -26.778 313.888 -336.187 -26.7347 314.644 -336.247 -26.8782 315.588 +-336.004 -26.8782 316.286 -335.861 -27.2639 316.99 -335.779 -27.2773 318.046 +-335.798 -27.256 319.074 -335.774 -27.2575 320.069 -335.887 -27.2699 321.072 +-335.983 -27.1074 322.002 -335.607 -26.7156 322.243 -336.215 -26.8425 322.718 +-335.734 -26.1449 297.062 -335.825 -25.7303 296.521 -336.311 -25.6208 296.65 +-335.54 -26.4259 297.931 -335.844 -26.2215 309.039 -336.227 -25.7501 309.72 +-335.863 -25.654 309.182 -336.287 -25.7726 310.833 -336.284 -25.7393 311.831 +-336.297 -25.8561 312.706 -336.185 -25.7748 313.684 -335.769 -26.2196 315.114 +-335.858 -25.9097 314.391 -335.681 -26.3812 315.904 -335.809 -26.4346 322.922 +-335.803 -26.1436 323.72 -335.823 -25.8261 324.289 -336.234 -24.6399 295.709 +-335.733 -24.8747 296.317 -335.815 -24.4842 308.32 -335.725 -25.2071 308.707 +-336.198 -24.8394 309.074 -336.341 -24.6497 309.925 -336.314 -24.7738 310.824 +-336.316 -24.7372 311.846 -336.225 -24.7867 312.83 -335.713 -25.3269 313.897 +-335.859 -24.7744 313.407 -335.826 -25.3223 324.756 -335.943 -24.7452 325.185 +-335.626 -24.184 295.833 -335.99 -23.9223 295.454 -336.179 -23.8474 308.864 +-335.73 -23.6921 308.402 -336.352 -23.7837 309.767 -336.315 -23.7652 310.803 +-336.316 -23.761 311.826 -336.234 -23.6749 312.871 -335.783 -23.9915 313.32 +-335.858 -23.7304 324.4 -335.807 -24.4006 324.916 -336.273 -22.4902 294.791 +-335.882 -22.9993 295.304 -335.864 -23.2128 308.947 -336.242 -22.8331 309.752 +-335.784 -22.5 309.297 -336.313 -22.7571 310.811 -336.293 -22.7628 311.827 +-336.332 -22.7637 312.837 -335.855 -23.2978 313.666 -336.201 -22.6501 313.905 +-335.714 -22.884 314.27 -335.969 -22.6504 324.186 -336.224 -21.5275 294.777 +-335.732 -22.0075 295.285 -336.238 -21.7823 309.933 -335.793 -21.5573 309.331 +-336.337 -21.7571 310.832 -336.289 -21.7494 311.839 -336.303 -21.7707 312.828 +-336.353 -21.7645 313.847 -335.883 -22.3102 314.745 -336.185 -21.685 314.762 +-335.732 -21.8077 315.225 -335.728 -21.626 323.441 -335.773 -22.3075 323.848 +-336.127 -21.8729 323.937 -336.33 -20.8223 294.783 -335.831 -20.9686 295.285 +-336.195 -20.8253 309.902 -335.733 -20.7027 309.431 -336.374 -20.7436 310.815 +-336.328 -20.7317 311.844 -336.292 -20.7529 312.839 -336.319 -20.7698 313.827 +-336.251 -20.705 314.881 -335.8 -21.0143 315.387 -336.327 -20.8129 323.743 +-335.83 -20.658 323.217 -335.976 -20.25 295.645 -336.246 -19.5364 295.951 +-335.673 -19.9267 296.253 -335.894 -20.1819 309.914 -336.191 -19.9568 310.624 +-335.73 -19.6068 310.362 -336.253 -19.7412 311.724 -336.324 -19.7412 312.843 +-336.308 -19.7547 313.834 -336.327 -19.7642 314.824 -335.865 -20.2969 315.678 +-336.185 -19.6413 315.875 -335.698 -19.8353 316.257 -335.868 -19.6371 318.267 +-335.722 -20.1446 318.804 -336.187 -19.6052 318.947 -335.811 -20.2847 319.645 +-336.219 -19.6744 320.052 -335.823 -20.2586 320.641 -336.188 -19.7567 320.989 +-335.805 -20.2217 321.638 -336.276 -19.5371 321.991 -335.774 -20.2622 322.579 +-336.177 -19.8861 322.978 -336.375 -18.5918 295.855 -335.84 -19.3373 296.697 +-336.164 -18.69 296.856 -335.708 -18.8531 297.282 -335.797 -19.3098 311.084 +-335.863 -19.1407 311.881 -336.183 -18.8668 312.783 -335.699 -18.66 312.402 +-336.325 -18.7507 313.837 -336.311 -18.7575 314.829 -336.328 -18.764 315.82 +-335.852 -19.3294 316.774 -336.236 -18.7428 316.912 -335.785 -19.2694 317.558 +-336.231 -18.8087 318.024 -336.312 -18.614 318.901 -336.317 -18.8949 319.652 +-336.436 -18.9478 320.787 -336.441 -18.9965 322.817 -336.368 -17.6068 295.96 +-336.259 -17.8112 296.751 -335.866 -18.3639 297.787 -336.243 -17.7848 297.933 +-335.796 -18.1918 298.574 -336.197 -17.5579 299.048 -335.72 -17.9038 299.29 +-335.857 -18.2248 312.989 -336.237 -17.8253 313.786 -335.788 -17.5089 313.305 +-336.314 -17.749 314.832 -336.302 -17.7612 315.825 -336.321 -17.7641 316.809 +-336.315 -17.7412 317.8 -336.285 -17.7157 318.799 -336.375 -17.968 319.845 +-336.354 -16.7132 296.877 -336.29 -16.786 297.815 -336.391 -16.7722 298.89 +-335.898 -17.3407 299.753 -336.223 -16.7003 299.789 -335.766 -16.8038 300.259 +-336.224 -16.8016 313.923 -335.793 -16.5655 313.347 -336.328 -16.7658 314.816 +-336.299 -16.7544 315.83 -336.298 -16.7655 316.824 -336.294 -16.7358 317.827 +-336.263 -16.7023 318.842 -336.371 -16.9449 319.725 -336.314 -15.6399 297.796 +-336.322 -15.8854 298.725 -336.224 -15.7037 299.596 -335.661 -15.7981 300.365 +-336.23 -15.8152 313.901 -335.798 -15.5652 313.329 -336.336 -15.778 314.797 +-336.306 -15.7556 315.822 -336.297 -15.7606 316.83 -336.286 -15.7329 317.848 +-336.254 -15.7041 318.879 -336.365 -15.9591 319.756 -336.365 -14.552 298.013 +-336.171 -14.842 298.834 -335.685 -15.1938 299.908 -335.877 -14.8431 299.478 +-336.229 -14.8143 313.896 -335.795 -14.5644 313.332 -336.336 -14.7799 314.788 +-336.307 -14.7559 315.818 -336.295 -14.7477 316.832 -336.282 -14.7149 317.853 +-336.254 -14.6919 318.894 -336.364 -14.9537 319.79 -336.353 -13.7942 298.778 +-335.88 -13.8734 299.343 -336.229 -13.8128 313.9 -335.801 -13.5643 313.324 +-336.334 -13.7808 314.791 -336.305 -13.7553 315.82 -336.287 -13.7354 316.834 +-336.274 -13.7002 317.855 -336.256 -13.812 318.759 -336.406 -13.8637 319.713 +-335.818 -12.6552 299.471 -336.243 -12.7977 313.892 -335.805 -12.5584 313.299 +-336.341 -12.7745 314.795 -336.303 -12.7557 315.822 -336.28 -12.7322 316.841 +-336.263 -12.7017 317.855 -336.358 -12.9435 318.915 -335.827 -11.6987 299.385 +-336.188 -11.8384 313.879 -335.732 -11.7092 313.418 -336.362 -11.7584 314.794 +-336.317 -11.7454 315.828 -336.277 -11.7373 316.844 -336.255 -11.7135 317.856 +-336.376 -11.9556 318.734 -336.214 -10.5674 298.779 -335.91 -10.9679 299.173 +-335.881 -11.2067 313.911 -336.209 -10.8523 314.745 -335.721 -10.6345 314.371 +-336.349 -10.7362 315.824 -336.3 -10.7362 316.843 -336.257 -10.7271 317.853 +-336.374 -10.9714 318.731 -335.632 -10.392 298.763 -335.869 -9.87151 298.42 +-335.867 -10.2002 314.947 -336.204 -9.84928 315.765 -335.717 -9.64104 315.388 +-336.345 -9.73492 316.831 -336.282 -9.71812 317.854 -336.349 -9.83558 318.843 +-335.824 -8.87031 298.203 -335.867 -9.20336 315.946 -336.205 -8.85176 316.762 +-335.721 -8.6437 316.386 -336.329 -8.70877 317.845 -336.256 -8.70477 318.879 +-336.408 -8.90394 319.641 -335.867 -8.34917 297.869 -335.904 -7.72004 297.372 +-335.873 -8.20571 316.946 -336.196 -7.8354 317.768 -335.733 -7.6424 317.389 +-336.3 -7.69229 318.871 -336.353 -7.93202 319.789 -335.959 -6.64179 297.201 +-335.875 -7.19466 317.948 -336.17 -6.81978 318.773 -335.728 -6.62052 318.392 +-336.395 -6.91867 319.753 -335.747 -6.28189 296.871 -335.776 -5.74787 296.322 +-336.132 -5.69222 296.818 -335.843 -6.16593 318.948 -336.286 -5.916 319.788 +-335.681 -5.60453 319.404 -335.791 -5.2382 295.777 -335.88 -4.60371 295.367 +-336.23 -4.86243 296.088 -336.465 -4.89663 296.894 -335.895 -5.18997 320.084 +-335.707 -4.46909 320.335 -336.219 -4.77365 320.765 -336.009 -3.64613 295.148 +-336.289 -4.03691 320.905 -335.892 -3.48326 320.363 -335.71 -3.21633 294.709 +-335.808 -2.50705 294.364 -336.172 -2.88165 294.899 -335.849 -2.81103 320.343 +-336.336 -1.88088 294.883 -335.822 -1.56255 294.272 -335.862 -2.26491 320.696 +-335.741 -1.5115 321.237 -336.158 -0.814754 294.896 -335.759 -0.69653 294.42 +-336.458 -0.800027 295.694 -335.935 -0.644627 321.353 -335.892 -0.159267 294.944 +-335.739 0.431766 295.398 -336.133 0.0530365 295.649 -336.379 0.197176 296.67 +-335.775 0.20428 321.432 -335.771 0.722719 296.1 -335.951 0.875773 296.993 +-335.789 1.24321 297.315 -336.186 1.19699 297.774 -336.332 1.27131 298.64 +-336.284 1.24205 299.629 -336.328 1.22304 300.542 -335.718 1.32719 321.398 +-335.705 1.67218 297.882 -335.83 1.77125 299.079 -335.79 1.7521 300.058 +-335.928 1.73336 301.053 -335.76 1.84006 301.962 -336.042 1.87505 302.73 +-335.824 2.20476 303.285 -336.405 2.28457 318.779 -336.239 2.22559 319.577 +-335.922 2.18718 320.301 -335.823 1.88703 320.82 -335.912 2.74852 303.828 +-335.752 3.34279 304.263 -336.239 3.20138 304.787 -336.338 3.25681 305.622 +-336.344 3.24425 306.553 -336.342 3.31859 311.681 -335.826 3.14231 312.243 +-335.981 2.88134 312.909 -335.713 3.19346 313.322 -336.212 3.14424 313.703 +-336.468 3.32319 315.777 -336.23 3.23892 316.563 -335.909 3.16729 317.278 +-335.888 2.90313 317.901 -335.732 2.73218 318.977 -335.549 2.58155 320.026 +-335.701 3.64856 304.943 -335.709 3.66176 306.024 -335.836 3.66333 307.034 +-335.802 3.75461 307.788 -335.825 3.84415 308.727 -335.959 3.86666 309.841 +-335.76 3.84296 310.877 -335.643 3.58744 311.86 -335.722 3.67108 313.913 +-335.714 3.74736 314.926 -335.817 3.66883 315.917 -335.555 3.58935 317.015 +-335.679 14.8506 329.95 -335.835 15.3075 329.425 -336.362 15.1617 329.891 +-335.734 14.7107 330.908 -335.835 14.7183 331.807 -335.767 14.6838 332.894 +-335.961 14.7582 333.829 -335.757 15.3004 334.297 -335.544 15.8689 307.781 +-335.922 16.3218 307.398 -336.255 16.2481 308.111 -336.31 16.2528 309.043 +-335.731 15.843 308.827 -336.278 16.2579 310.038 -335.698 15.8507 309.813 +-336.261 16.2289 311.057 -335.672 15.8242 310.818 -336.262 16.1777 312.101 +-335.773 15.7936 311.717 -336.299 16.3123 312.898 -335.745 15.8677 312.61 +-335.962 16.3981 313.176 -335.627 15.8817 327.895 -335.928 16.3215 327.438 +-336.357 16.1844 327.999 -335.771 15.6521 328.95 -335.629 15.9896 334.517 +-335.726 16.8653 304.891 -335.898 17.4108 304.424 -336.335 17.1354 304.911 +-335.65 16.6229 305.629 -335.99 16.6174 306.694 -335.71 17.0613 313.431 +-335.744 16.9048 322.918 -335.855 17.3182 322.431 -336.239 17.2302 322.891 +-336.245 17.1609 323.656 -335.64 17.1928 323.453 -335.593 16.8506 325.694 +-336.024 17.2753 325.276 -335.842 16.6228 326.697 -336.077 16.6691 334.858 +-335.758 17.2945 335.236 -335.735 18.2013 304.283 -335.991 17.6496 313.781 +-335.872 17.7254 314.562 -335.861 17.6744 315.608 -335.868 17.6635 316.638 +-335.866 17.6694 317.638 -335.87 17.6717 318.633 -335.872 17.6732 319.627 +-335.844 17.6794 320.632 -335.9 17.645 321.731 -335.986 17.594 323.999 +-335.717 17.6277 324.593 -335.872 18.4492 334.47 -335.693 17.8804 334.897 +-336.267 18.0842 334.922 -335.81 19.2559 304.31 -335.859 19.2963 334.265 +-335.847 20.2957 304.283 -335.729 20.1892 334.296 -335.635 21.0461 304.454 +-335.761 21.2253 334.323 -336.065 21.7277 304.81 -335.77 22.2063 305.246 +-335.788 22.2371 334.333 -335.608 23.1765 305.421 -335.803 23.2459 334.335 +-335.993 23.7498 305.833 -335.734 24.3196 306.259 -335.803 24.2466 334.331 +-335.771 25.2132 306.368 -335.796 25.2419 334.324 -335.804 26.2358 306.339 +-335.785 26.2454 334.342 -335.801 27.2359 306.33 -335.81 27.2737 334.384 +-335.793 28.234 306.33 -335.887 28.1756 334.216 -335.794 29.2424 306.328 +-335.889 29.24 333.355 -335.741 28.6264 333.76 -335.798 30.2464 306.328 +-335.688 30.2572 333.349 -335.797 31.245 306.33 -336.348 31.3747 333.921 +-335.845 30.8852 333.749 -335.721 31.3398 334.21 -335.796 32.2443 306.331 +-335.796 32.2392 334.383 -335.796 33.2447 306.33 -335.806 33.254 334.339 +-335.794 34.2447 306.328 -335.795 34.2453 334.323 -335.791 35.2365 306.335 +-335.793 35.2428 334.332 -335.81 36.2268 306.353 -335.802 36.2436 334.326 +-335.691 37.3436 306.263 -335.81 37.2343 334.305 -335.901 38.3384 306.358 +-335.688 38.2543 334.396 -335.766 39.2163 306.352 -335.902 39.3447 334.301 +-335.792 40.2388 306.336 -335.757 40.2306 334.311 -335.801 41.2465 306.326 +-335.775 41.2442 334.33 -335.797 42.2452 306.329 -335.802 42.2538 334.322 +-335.793 43.2435 306.332 -335.795 43.2461 334.291 -335.788 44.2415 306.335 +-335.759 44.1593 334.385 -335.809 45.2437 306.331 -335.889 45.3075 320.419 +-335.591 44.8341 320.664 -336.262 45.2028 321.156 -335.821 44.8848 321.598 +-336.235 45.2711 321.954 -335.742 45.2908 322.178 -335.791 45.2801 334.398 +-335.858 46.2578 306.309 -336.038 46.2492 317.333 -335.64 45.8474 317.721 +-335.767 45.6584 318.618 -335.952 45.6369 319.716 -335.786 45.6847 322.732 +-335.813 45.7483 323.842 -335.818 45.7406 324.84 -335.804 45.7387 325.831 +-335.779 45.7036 326.845 -335.842 45.8395 327.738 -336.245 46.331 327.927 +-335.675 46.1632 328.392 -335.888 46.3662 334.345 -335.761 47.1409 306.267 +-336.077 47.2286 313.264 -335.606 46.8157 313.759 -335.824 46.7455 314.84 +-335.925 46.7828 315.76 -335.678 46.6514 316.628 -335.899 46.6522 328.763 +-335.75 46.7529 329.859 -335.854 46.7802 330.87 -335.731 47.1291 331.48 +-335.765 47.1492 334.271 -336.405 48.165 306.705 -335.731 48.1298 306.364 +-335.831 48.2968 309.418 -335.685 47.824 309.949 -336.392 48.1388 309.916 +-335.704 47.7026 310.94 -336.01 47.8484 311.813 -336.43 48.2289 312.035 +-335.582 47.5915 312.624 -335.865 47.6894 331.919 -335.714 48.2695 332.392 +-336.072 48.349 333.303 -335.524 47.7916 333.733 -335.885 48.6779 307.043 +-335.659 48.8186 307.936 -335.845 48.6973 308.822 -335.971 48.6014 332.72 +-335.402 -27.5481 299.698 -335.267 -27.6533 300.622 -335.295 -27.7728 301.613 +-335.232 -27.7518 302.561 -334.876 -27.8716 303.304 -335.044 -28.1285 303.813 +-334.736 -27.6505 304.148 -335.358 -26.7768 298.884 -335.099 -26.8498 299.788 +-334.772 -27.2508 301.082 -334.839 -26.9194 300.391 -334.769 -27.2727 302.024 +-334.666 -27.4042 302.912 -335.338 -26.8536 304.758 -334.829 -26.8254 304.404 +-335.307 -26.7257 305.549 -335.262 -26.6965 306.747 -335.423 -26.6962 307.807 +-335.409 -26.5477 316.646 -335.254 -26.6579 317.552 -335.287 -26.786 318.581 +-335.296 -26.7604 319.595 -335.262 -26.7634 320.583 -335.289 -26.7757 321.513 +-335.351 -25.5241 296.777 -334.875 -25.8478 297.267 -334.847 -26.1654 297.806 +-334.852 -26.2561 298.87 -334.706 -26.3915 299.858 -334.762 -26.2699 305.044 +-334.733 -26.2641 306.061 -334.812 -26.1748 306.978 -335.168 -25.84 307.651 +-334.835 -25.6693 307.206 -335.449 -25.8751 308.614 -335.397 -25.5312 314.707 +-335.355 -25.7967 315.893 -335.19 -25.7371 317.037 -334.739 -26.2653 318.027 +-334.819 -25.9432 317.464 -334.763 -26.309 318.989 -334.767 -26.2895 320.005 +-334.735 -26.2916 321 -334.858 -26.2953 322.023 -334.925 -26.1469 322.952 +-334.614 -25.6805 323.394 -335.209 -25.8299 323.843 -335.202 -24.8155 296.805 +-334.74 -25.0659 297.247 -334.797 -24.4949 306.375 -334.704 -25.2173 306.742 +-335.16 -24.8258 307.116 -335.394 -24.9227 308 -335.378 -24.643 313.914 +-335.234 -24.7101 314.874 -335.278 -24.8246 315.776 -335.327 -24.7749 316.818 +-334.851 -25.3329 317.663 -335.163 -24.682 317.724 -334.709 -24.8178 318.178 +-334.871 -25.2511 324.119 -334.802 -24.6494 324.208 -335.316 -24.6724 324.678 +-335.282 -23.4661 295.78 -334.958 -23.8604 296.267 -334.808 -24.3534 296.853 +-335.216 -23.7766 306.881 -334.809 -23.5644 306.302 -335.391 -23.8354 307.781 +-335.4 -23.5964 313.984 -335.226 -23.8131 314.761 -335.27 -23.814 315.796 +-335.331 -23.7752 316.82 -335.259 -23.6918 317.882 -334.797 -24.0156 318.387 +-334.772 -23.475 323.451 -334.692 -24.1833 323.773 -335.137 -23.9671 324.046 +-335.255 -22.5403 295.93 -334.722 -23.0433 296.315 -335.218 -22.7907 306.899 +-334.802 -22.5713 306.328 -335.283 -22.7352 307.813 -335.421 -22.9083 308.619 +-335.394 -22.7187 314.916 -335.259 -22.7826 315.807 -335.305 -22.7647 316.835 +-335.339 -22.758 317.84 -334.875 -23.3085 318.69 -335.192 -22.656 318.879 +-334.718 -22.8845 319.246 -335.355 -22.8451 323.722 -334.837 -22.6914 323.243 +-335.366 -21.5867 295.893 -334.833 -22.349 296.655 -335.17 -21.696 296.853 +-334.719 -21.8934 297.247 -335.225 -21.8042 306.902 -334.798 -21.5688 306.332 +-335.295 -21.7436 307.823 -335.36 -21.9588 308.755 -335.341 -21.6437 315.821 +-335.254 -21.7336 316.833 -335.289 -21.7343 317.859 -335.313 -21.7459 318.862 +-334.866 -22.3096 319.694 -335.168 -21.6254 319.906 -334.707 -21.8326 320.273 +-334.824 -21.5493 322.305 -334.692 -22.152 322.76 -335.182 -21.7922 322.906 +-335.391 -20.6309 296.012 -335.274 -20.7994 296.781 -334.878 -21.3093 297.707 +-335.18 -20.6622 297.869 -334.723 -20.8307 298.29 -335.232 -20.8119 306.897 +-334.804 -20.566 306.321 -335.313 -20.7583 307.801 -335.357 -20.8423 308.831 +-335.403 -20.5408 316.032 -335.213 -20.6962 316.835 -335.236 -20.8359 317.737 +-335.365 -20.7374 318.796 -335.416 -20.856 319.88 -334.833 -21.3005 320.785 +-335.296 -20.7937 320.966 -334.754 -21.2103 321.587 -335.281 -20.8398 322.096 +-335.481 -20.9151 322.85 -335.379 -19.7104 296.908 -335.273 -19.759 297.801 +-334.864 -20.34 298.788 -335.214 -19.7629 298.907 -334.793 -20.2403 299.633 +-335.234 -19.6945 300.041 -334.802 -20.2244 300.58 -335.199 -19.5783 301.021 +-334.736 -19.918 301.294 -335.244 -19.8022 306.888 -334.804 -19.5527 306.305 +-335.335 -19.7666 307.797 -335.268 -19.7386 308.831 -335.404 -19.7972 309.718 +-335.391 -19.792 316.898 -335.38 -19.9938 317.919 -335.421 -18.8238 297.897 +-335.355 -18.7081 298.865 -335.306 -18.7949 299.808 -335.37 -18.7919 300.841 +-334.894 -19.3315 301.736 -335.199 -18.6917 301.764 -334.734 -18.8035 302.232 +-335.179 -18.8463 306.892 -334.721 -18.7037 306.443 -335.355 -18.7698 307.793 +-335.299 -18.7413 308.834 -335.256 -18.7509 309.838 -335.338 -18.7975 310.78 +-335.403 -18.6607 311.777 -335.423 -17.7104 299.954 -335.321 -17.7414 300.858 +-335.243 -17.7253 301.773 -334.81 -17.9531 302.378 -334.877 -18.2126 306.937 +-335.256 -17.8197 307.751 -334.796 -17.4897 307.265 -335.331 -17.7491 308.825 +-335.291 -17.7529 309.839 -335.257 -17.7332 310.85 -335.219 -17.7341 311.872 +-335.397 -17.9406 312.651 -335.424 -16.6973 300.774 -335.173 -16.6394 301.59 +-334.802 -17.0157 302.321 -335.169 -16.8382 307.917 -334.713 -16.7017 307.468 +-335.346 -16.7648 308.814 -335.315 -16.7531 309.831 -335.287 -16.7312 310.84 +-335.246 -16.7006 311.882 -335.347 -16.9641 312.781 -335.238 -15.8626 300.984 +-334.715 -16.3107 301.987 -334.845 -15.8254 301.4 -334.869 -16.2171 307.959 +-335.246 -15.8333 308.766 -334.793 -15.5038 308.281 -335.326 -15.7586 309.822 +-335.289 -15.7313 310.839 -335.259 -15.6963 311.868 -335.365 -15.9493 312.768 +-335.337 -14.5087 299.819 -334.99 -14.8567 300.306 -334.824 -15.3148 300.865 +-335.236 -14.7949 308.912 -334.794 -14.5565 308.323 -335.339 -14.77 309.812 +-335.291 -14.728 310.841 -335.253 -14.6968 311.872 -335.364 -14.9461 312.762 +-335.306 -13.5615 299.846 -334.703 -13.8608 300.316 -335.178 -13.8513 308.897 +-334.718 -13.7068 308.443 -335.356 -13.7741 309.798 -335.301 -13.7269 310.836 +-335.25 -13.7028 311.87 -335.361 -13.9507 312.764 -335.188 -12.6983 299.796 +-334.826 -12.9694 300.149 -334.875 -13.214 308.934 -335.256 -12.8238 309.748 +-334.792 -12.4903 309.267 -335.321 -12.7372 310.825 -335.257 -12.7165 311.856 +-335.365 -12.9589 312.745 -334.536 -12.3041 299.849 -335.053 -11.7292 299.341 +-335.167 -11.8412 309.917 -334.707 -11.7082 309.466 -335.344 -11.7605 310.815 +-335.281 -11.7276 311.845 -335.347 -11.8431 312.831 -334.499 -11.2267 298.769 +-334.922 -10.5328 298.463 -335.211 -10.8419 298.878 -334.867 -11.2216 309.945 +-335.257 -10.8169 310.767 -334.793 -10.4963 310.26 -335.326 -10.7368 311.841 +-335.255 -10.7421 312.847 -335.392 -10.8129 313.721 -334.821 -9.73673 298.245 +-335.179 -9.82066 310.912 -334.734 -9.69574 310.453 -335.367 -9.73935 311.823 +-335.315 -9.72949 312.856 -335.25 -9.75037 313.856 -335.382 -9.82041 314.738 +-334.552 -9.225 297.857 -334.865 -8.66013 297.316 -335.31 -8.68153 297.759 +-334.895 -9.18212 310.929 -335.185 -8.96255 311.634 -334.753 -8.59342 311.379 +-335.252 -8.74467 312.728 -335.321 -8.7282 313.867 -335.267 -8.73698 314.883 +-335.39 -8.81946 315.751 -334.564 -8.24512 296.796 -334.814 -7.58657 296.504 +-335.203 -7.96653 296.976 -334.811 -8.31043 312.092 -334.864 -8.15276 312.878 +-335.197 -7.84744 313.787 -334.716 -7.66288 313.384 -335.347 -7.70317 314.876 +-335.29 -7.72273 315.885 -335.409 -7.81761 316.754 -335.363 -6.82781 296.716 +-334.759 -6.82648 296.209 -334.878 -7.19102 313.957 -335.186 -6.93467 314.659 +-334.746 -6.59211 314.383 -335.226 -6.81985 315.621 -335.199 -6.75571 316.752 +-335.425 -6.81954 317.746 -334.7 -6.32642 295.765 -334.73 -5.74827 295.359 +-335.316 -5.7547 295.817 -334.81 -6.28281 315.08 -334.786 -6.26857 316.026 +-334.84 -6.16141 316.887 -335.145 -5.86895 317.806 -334.693 -5.67679 317.405 +-335.409 -5.79997 318.748 -334.543 -5.22007 294.719 -334.906 -4.64133 294.247 +-335.232 -4.80147 294.881 -334.847 -5.20674 317.998 -335.193 -4.78622 318.822 +-334.809 -4.45858 318.306 -335.4 -4.90904 319.644 -334.832 -4.26844 293.775 +-334.766 -3.60937 293.368 -335.151 -3.91492 294.017 -335.45 -3.81734 294.707 +-335.15 -3.76918 318.976 -334.84 -3.79747 318.435 -335.372 -3.94103 319.763 +-335.294 -2.98017 293.942 -334.805 -2.52178 293.29 -334.752 -3.20704 318.743 +-335.294 -2.91677 319.813 -334.707 -2.64058 319.247 -335.308 -1.99157 293.786 +-334.799 -1.55115 293.302 -334.828 -2.23531 319.922 -334.56 -1.75036 320.356 +-335.25 -1.842 320.729 -335.314 -0.875701 293.879 -334.806 -0.563525 293.283 +-334.887 -1.41543 320.904 -334.797 -0.792194 321.32 -335.155 0.185958 293.899 +-334.753 0.29747 293.414 -335.467 0.219437 294.71 -335.3 0.520394 321.832 +-334.901 -0.155302 321.753 -334.636 0.216095 322.138 -334.891 0.835764 293.938 +-334.762 1.44341 294.413 -335.154 1.06714 294.655 -335.285 1.14373 295.591 +-335.364 1.29669 296.678 -335.259 1.18007 321.843 -334.698 0.953248 322.314 +-334.801 1.7469 295.138 -334.764 1.75392 296.056 -334.959 1.84681 296.993 +-334.678 2.29998 297.238 -335.217 2.19213 297.751 -335.362 2.27971 298.596 +-335.335 2.28455 299.59 -335.306 2.27338 300.574 -335.256 2.24316 301.589 +-335.328 2.25338 302.715 -335.31 2.33599 320.735 -334.96 2.10094 321.319 +-334.856 1.6521 321.933 -334.641 2.60356 297.95 -334.73 2.64967 299.023 +-334.706 2.64964 300.02 -334.675 2.64069 301.017 -334.747 2.59654 301.964 +-334.825 2.82702 302.889 -334.708 3.33605 303.222 -335.273 3.19839 303.696 +-335.426 3.31329 312.812 -335.403 3.31663 317.783 -335.202 3.239 318.585 +-334.89 3.15834 319.311 -334.876 2.90142 319.921 -334.793 2.63767 320.812 +-334.841 3.66589 303.927 -334.747 3.78547 304.883 -334.838 3.77599 305.769 +-334.788 3.74746 306.779 -335.031 3.83676 307.83 -334.556 4.26628 308.32 +-335.204 4.19525 308.742 -335.362 4.27259 309.612 -335.283 4.26788 310.56 +-334.907 4.178 311.272 -334.843 3.88596 311.891 -335.002 3.81709 312.89 +-334.81 4.23492 313.394 -335.14 4.18096 313.731 -334.725 4.33945 314.337 +-334.909 3.87477 314.851 -334.898 3.82848 315.814 -334.888 3.85954 316.848 +-334.864 3.76072 317.957 -334.693 3.60728 318.953 -334.758 4.64952 309.055 +-334.737 4.70676 310.026 -334.56 4.60751 310.995 -334.604 4.66809 313.753 +-334.572 14.8228 329.781 -335.049 15.2476 329.249 -334.831 14.7318 330.826 +-334.812 14.7482 331.84 -334.892 14.7338 332.747 -334.879 14.8521 333.716 +-334.833 15.3926 334.233 -334.556 15.8849 305.916 -334.907 16.2921 305.404 +-335.268 16.2296 306.065 -335.437 16.1702 306.868 -334.814 15.8178 306.923 +-334.816 15.7356 307.872 -334.782 15.6504 308.625 -334.937 15.6362 309.61 +-334.896 15.6157 310.604 -335.011 15.6896 311.809 -334.568 16.135 312.37 +-335.287 16.3319 312.901 -334.682 15.8859 326.75 -334.909 16.3189 326.389 +-335.365 16.1821 326.973 -334.635 15.6499 327.658 -334.899 15.6023 328.564 +-335.38 16.3346 334.916 -334.743 15.8232 334.651 -334.75 16.298 335.176 +-334.692 16.7157 304.863 -334.963 17.3006 304.376 -334.632 16.7003 313.005 +-334.797 16.8389 313.773 -335.333 17.3044 314.07 -335.267 17.2597 315.034 +-334.671 16.8655 314.828 -335.285 17.2419 316.049 -334.685 16.8421 315.831 +-335.289 17.2481 317.046 -334.693 16.8452 316.828 -335.294 17.2489 318.036 +-334.7 16.8455 317.817 -335.301 17.244 319.028 -334.707 16.8399 318.809 +-335.279 17.2476 320.027 -334.697 16.8403 319.819 -335.243 17.2484 321.028 +-334.671 16.8453 320.823 -335.368 17.1663 321.878 -334.886 16.8301 321.808 +-334.591 16.6342 322.783 -335.284 17.2995 324.053 -334.81 16.8623 323.745 +-335.351 17.2074 324.887 -334.839 16.8839 324.836 -334.692 16.6312 325.74 +-334.619 17.1935 335.458 -334.751 18.2116 304.3 -335.238 18.2898 334.738 +-334.899 18.1402 335.21 -334.824 19.2588 304.31 -334.907 19.1899 334.406 +-334.681 18.7739 334.895 -334.729 20.2562 304.39 -334.85 20.3073 334.293 +-335.347 21.357 304.905 -334.845 20.845 304.764 -334.725 21.3036 305.209 +-334.73 21.2047 334.307 -334.839 22.2482 305.343 -334.758 22.2297 334.322 +-334.611 23.1836 305.398 -334.792 23.244 334.333 -334.991 23.7699 305.818 +-334.744 24.3376 306.252 -334.804 24.2467 334.331 -334.787 25.2207 306.37 +-334.795 25.2399 334.326 -334.81 26.2277 306.351 -334.79 26.2511 334.353 +-334.794 27.2169 306.345 -334.868 27.3151 334.282 -334.781 28.2191 306.341 +-334.966 28.444 333.439 -334.542 27.7891 333.831 -335.249 28.1237 333.911 +-334.789 29.238 306.331 -334.785 29.2323 333.228 -334.799 30.2457 306.328 +-334.613 30.0237 333.442 -334.797 31.2446 306.33 -335.062 30.6841 333.831 +-334.735 31.1673 334.29 -334.796 32.2443 306.331 -334.806 32.24 334.357 +-334.796 33.2449 306.329 -334.8 33.2507 334.323 -334.79 34.2429 306.329 +-334.793 34.2445 334.329 -334.8 35.2369 306.352 -334.795 35.2436 334.333 +-334.915 36.2883 306.257 -334.814 36.2521 334.303 -334.63 36.8283 305.785 +-334.663 37.3258 305.506 -335.197 37.1105 305.856 -334.721 37.2463 334.395 +-334.884 37.6025 305.856 -334.842 38.2203 306.255 -335.163 38.3679 334.813 +-334.919 37.8717 334.801 -334.678 38.1991 335.154 -334.761 39.2277 306.367 +-335.039 39.1828 334.414 -334.577 38.7111 334.884 -334.801 40.2403 306.34 +-334.763 40.242 334.308 -334.8 41.2451 306.326 -334.794 41.2585 334.317 +-334.796 42.2447 306.329 -334.802 42.2606 334.291 -334.793 43.2444 306.332 +-334.742 43.1414 334.432 -334.787 44.2427 306.336 -335.304 44.4749 334.837 +-334.963 43.7867 334.768 -334.646 44.0779 335.224 -334.81 45.2365 306.329 +-334.969 45.3083 318.384 -334.554 44.8599 318.754 -335.238 45.2408 319.092 +-334.817 44.7835 319.898 -335.391 45.1558 319.881 -335.02 44.7902 320.996 +-334.548 45.2794 321.346 -335.304 45.1533 321.723 -335.215 45.2572 334.768 +-334.71 45.1308 335.243 -334.855 46.2346 306.299 -334.85 46.3383 316.447 +-334.678 45.8877 316.813 -335.321 46.1895 317.042 -334.768 45.6093 317.755 +-334.862 45.6666 322.107 -334.64 45.8288 322.985 -334.771 45.7984 323.881 +-334.767 45.7864 324.881 -334.776 45.7854 325.862 -334.766 45.7541 326.863 +-334.775 45.6954 327.868 -334.976 45.858 328.632 -335.251 46.3168 328.971 +-334.541 46.2443 329.271 -334.94 46.2582 334.462 -334.721 45.8185 334.92 +-334.743 47.3103 306.36 -334.83 47.3255 312.457 -334.677 46.8748 313.006 +-335.365 47.1657 312.94 -334.839 46.7312 313.83 -334.759 46.6548 314.596 +-334.932 46.6389 315.721 -334.814 46.685 329.798 -334.719 46.7169 330.916 +-334.928 46.8429 331.685 -335.307 47.1965 331.852 -334.661 47.2645 332.28 +-334.88 47.262 334.242 -334.723 48.3517 306.331 -334.988 48.2247 309.286 +-334.595 47.8117 309.911 -334.822 47.723 310.818 -334.86 47.6657 311.846 +-334.911 47.8304 332.917 -335.453 48.25 332.941 -334.626 47.6694 333.87 +-334.824 48.7524 306.846 -334.776 48.8362 307.813 -334.744 48.6742 308.72 +-334.425 -27.6141 303.646 -334.401 -26.5247 300.727 -334.364 -26.7007 301.773 +-334.321 -26.7879 302.91 -334.326 -26.798 303.938 -334.376 -25.5764 297.901 +-334.341 -25.7123 298.778 -334.349 -25.7893 299.872 -334.259 -25.6814 300.923 +-334.265 -25.8191 301.787 -334.265 -25.7886 302.796 -334.242 -25.7381 303.813 +-334.322 -25.7578 304.765 -334.338 -25.7218 305.705 -334.45 -25.7279 306.623 +-334.456 -25.5758 317.982 -334.335 -25.7174 318.751 -334.365 -25.7125 319.769 +-334.363 -25.6812 320.785 -334.322 -25.6624 321.805 -334.352 -25.6741 322.756 +-334.323 -24.574 297.755 -334.239 -24.8292 298.819 -334.26 -24.7947 299.811 +-334.278 -24.7805 300.809 -334.289 -24.7911 301.817 -334.299 -24.7787 302.824 +-334.284 -24.7254 303.834 -334.244 -24.6922 304.847 -334.325 -24.9685 305.905 +-334.324 -24.6965 318.788 -334.243 -24.8203 319.786 -334.27 -24.772 320.821 +-334.243 -24.7335 321.851 -334.208 -24.8735 322.736 -334.404 -24.8572 323.603 +-334.359 -23.6398 296.948 -334.248 -23.6951 297.903 -334.282 -23.8155 298.816 +-334.294 -23.7757 299.837 -334.299 -23.7498 300.831 -334.303 -23.7575 301.825 +-334.309 -23.7616 302.828 -334.297 -23.7295 303.844 -334.263 -23.6937 304.865 +-334.37 -23.9474 305.745 -334.414 -23.5884 319.027 -334.232 -23.766 319.802 +-334.261 -23.7328 320.841 -334.269 -23.6927 321.884 -334.347 -23.9442 322.956 +-334.392 -22.6036 296.99 -334.227 -22.7864 297.761 -334.272 -22.794 298.794 +-334.303 -22.7615 299.829 -334.306 -22.7464 300.835 -334.306 -22.7516 301.832 +-334.306 -22.7591 302.828 -334.292 -22.7323 303.84 -334.259 -22.6973 304.873 +-334.369 -22.9536 305.756 -334.384 -22.678 319.912 -334.233 -22.6917 320.846 +-334.253 -22.8009 321.749 -334.402 -22.8383 322.718 -334.387 -21.6774 297.912 +-334.232 -21.7366 298.82 -334.263 -21.7416 299.838 -334.28 -21.7498 300.829 +-334.294 -21.7594 301.823 -334.305 -21.761 302.827 -334.291 -21.7333 303.839 +-334.256 -21.6974 304.872 -334.363 -21.9498 305.767 -334.399 -21.7753 320.907 +-334.379 -21.9504 321.931 -334.41 -20.8036 298.921 -334.357 -20.8376 299.865 +-334.354 -20.7092 300.901 -334.263 -20.7617 301.824 -334.29 -20.7676 302.818 +-334.293 -20.7371 303.836 -334.258 -20.7044 304.866 -334.363 -20.9518 305.763 +-334.407 -19.7117 301.942 -334.264 -19.7706 302.817 -334.288 -19.7538 303.826 +-334.267 -19.7168 304.854 -334.37 -19.9549 305.746 -334.354 -18.6648 302.832 +-334.276 -18.7751 303.814 -334.274 -18.7295 304.843 -334.35 -18.8276 305.844 +-334.375 -17.5605 302.947 -334.264 -17.7942 303.811 -334.28 -17.7538 304.828 +-334.251 -17.7252 305.856 -334.422 -17.9163 306.618 -334.372 -16.5659 302.801 +-334.278 -16.8093 303.83 -334.291 -16.777 304.821 -334.266 -16.7225 305.853 +-334.336 -16.8271 306.86 -334.41 -15.6701 301.769 -334.21 -15.5967 302.792 +-334.322 -15.8044 303.847 -334.325 -15.7646 304.842 -334.292 -15.7228 305.848 +-334.238 -15.7153 306.873 -334.412 -15.9188 307.641 -334.438 -14.6963 300.903 +-334.162 -14.7432 301.698 -333.874 -15.1358 302.97 -333.871 -14.9705 302.389 +-334.163 -15.0277 303.675 -333.737 -14.6713 303.39 -334.253 -14.7596 304.735 +-334.334 -14.7244 305.839 -334.27 -14.7069 306.868 -334.353 -14.9622 307.757 +-334.212 -13.795 300.846 -333.74 -14.3577 301.912 -333.672 -13.6999 301.351 +-333.815 -14.3355 304.099 -333.878 -14.1374 304.878 -334.193 -13.858 305.765 +-333.724 -13.6535 305.381 -334.313 -13.7127 306.846 -334.356 -13.8234 307.853 +-333.702 -13.1499 300.841 -333.892 -12.7297 300.4 -333.868 -13.2103 305.956 +-334.178 -12.8475 306.781 -333.722 -12.6313 306.412 -334.282 -12.7192 307.855 +-334.425 -12.911 308.621 -333.727 -12.0967 299.912 -334.067 -11.7693 299.363 +-333.867 -12.1935 306.975 -334.217 -11.7821 307.798 -333.826 -11.4651 307.291 +-334.355 -11.8299 308.855 -333.553 -11.315 298.921 -333.994 -10.7068 298.387 +-334.187 -10.778 307.947 -333.854 -10.8102 307.426 -334.302 -10.722 308.857 +-334.428 -10.9192 309.621 -333.784 -9.90949 298.225 -333.766 -10.2231 307.754 +-334.211 -9.80153 308.818 -333.87 -9.72176 308.358 -334.388 -9.82367 309.855 +-333.591 -9.25758 297.876 -334.081 -8.78289 297.341 -333.758 -9.2322 308.775 +-334.211 -8.79602 309.848 -333.866 -8.72125 309.394 -334.461 -8.76563 310.755 +-333.591 -8.29261 296.85 -334.039 -7.64962 296.312 -333.782 -8.20377 309.809 +-334.196 -7.90448 310.729 -333.744 -7.61845 310.321 -334.315 -7.76879 311.732 +-334.445 -7.6728 312.774 -333.476 -7.23128 295.825 -333.973 -6.52898 295.506 +-334.233 -6.91382 295.878 -333.731 -7.3753 310.918 -333.771 -7.1369 311.805 +-334.176 -6.84078 312.829 -333.842 -6.75618 312.38 -334.433 -6.80511 313.747 +-333.866 -5.84321 295.219 -333.748 -6.2389 312.783 -334.201 -5.81684 313.836 +-333.868 -5.71899 313.391 -334.403 -5.77145 314.782 -334.376 -5.64154 315.82 +-334.406 -5.68881 316.767 -333.944 -5.21733 294.061 -333.812 -4.78352 293.318 +-334.418 -4.64865 293.756 -333.803 -5.44876 294.702 -333.77 -5.24241 313.788 +-334.177 -4.93991 314.706 -333.795 -4.59666 314.433 -334.22 -4.72205 315.772 +-334.265 -4.74783 316.863 -334.424 -4.94629 317.638 -333.755 -4.18605 292.651 +-333.817 -3.46961 292.344 -334.201 -3.93582 292.944 -333.842 -4.27391 315.14 +-333.916 -4.08006 315.933 -334.165 -3.93237 316.706 -333.706 -3.64471 316.264 +-334.362 -3.85109 317.78 -334.312 -2.97278 292.792 -333.799 -2.52921 292.318 +-333.625 -3.37131 316.978 -333.957 -3.17309 318.008 -333.565 -2.74909 318.37 +-334.289 -2.79182 318.676 -334.296 -1.98373 292.822 -333.787 -1.54012 292.338 +-333.891 -2.38611 319.074 -333.97 -2.11561 319.772 -333.591 -1.75365 320.27 +-334.294 -1.00493 292.814 -333.795 -0.548013 292.313 -333.9 -1.24873 320.888 +-333.706 -0.825561 321.446 -334.309 0.133918 292.89 -333.823 0.468953 292.29 +-333.817 -0.370448 321.961 -333.848 0.269159 322.478 -334.178 1.23038 292.943 +-333.85 1.19021 292.381 -334.479 1.23116 293.736 -334.302 1.53985 322.757 +-333.959 0.797853 322.832 -333.63 1.07452 323.224 -333.763 1.792 292.745 +-333.724 2.38046 293.315 -334.2 2.10155 293.716 -334.332 2.16694 294.63 +-334.281 2.26673 295.633 -334.325 2.22781 296.549 -334.425 2.34649 321.842 +-334.186 2.41269 322.652 -333.742 1.95612 323.317 -333.735 2.63395 293.943 +-333.731 2.76787 294.953 -333.796 2.74896 296.061 -333.937 2.73234 297.055 +-333.802 2.86036 297.951 -333.924 2.83975 298.825 -333.931 2.85044 299.836 +-333.746 2.81945 300.878 -334.026 2.81321 301.851 -333.562 3.22451 302.331 +-334.26 3.1832 302.644 -334.447 3.46538 319.701 -334.285 3.29322 320.721 +-334.138 3.14205 321.786 -333.801 2.66556 322.985 -333.904 3.11468 322.395 +-333.911 3.64441 303.04 -333.708 3.79346 303.896 -333.836 3.77743 304.782 +-333.822 3.76243 305.8 -333.787 3.72911 306.805 -333.882 3.80751 307.792 +-333.756 4.18394 308.392 -334.357 4.29891 311.778 -334.344 4.27293 312.735 +-334.334 4.19799 314.895 -334.235 4.1866 315.609 -334.251 4.2101 316.634 +-334.285 4.24582 317.631 -334.304 4.16609 318.758 -334.153 4.15407 319.754 +-333.797 3.83286 321.067 -333.882 4.14819 320.346 -333.741 3.63075 321.865 +-333.904 4.65113 308.841 -333.822 4.83472 309.753 -333.766 4.79237 310.796 +-333.852 4.63291 311.841 -333.863 4.8479 312.918 -333.761 5.36697 313.249 +-334.185 5.24143 313.738 -334.064 5.13557 314.297 -333.852 4.74784 315.026 +-333.754 4.73035 316.076 -333.799 4.74533 317.081 -333.833 4.76875 318.055 +-333.69 4.77044 319.044 -333.594 4.60535 319.945 -333.594 5.64005 313.888 +-333.588 14.8462 308.725 -333.953 15.3086 308.403 -334.293 15.2385 309.09 +-334.303 15.224 310.033 -333.738 14.8044 309.778 -334.277 15.1834 311.071 +-333.677 14.767 310.83 -334.318 15.3241 311.865 -333.795 14.8441 311.672 +-333.778 15.3147 312.182 -333.543 14.8628 327.773 -333.946 15.3 327.369 +-334.261 15.253 328.101 -334.43 15.1781 328.884 -333.815 14.8284 328.93 +-333.66 14.7581 330.007 -333.933 14.7639 330.742 -333.743 14.6747 331.623 +-333.828 14.7052 332.925 -333.679 15.214 333.459 -334.212 15.1885 333.684 +-333.576 15.8194 305.784 -334.089 16.2173 305.229 -333.929 15.7394 306.73 +-333.82 15.634 307.711 -333.678 16.0804 312.44 -333.677 15.9172 322.841 +-333.866 16.2023 322.34 -334.201 16.278 322.972 -333.726 16.1678 323.43 +-333.882 15.8113 325.956 -333.782 16.2245 325.393 -334.313 16.1985 325.916 +-333.894 15.5592 326.726 -333.887 15.5696 334.054 -333.761 15.8061 334.923 +-333.842 16.2321 335.481 -333.68 16.5824 304.7 -333.822 17.2113 304.369 +-334.042 16.655 312.758 -333.908 16.7135 313.561 -333.889 16.6487 314.603 +-333.902 16.6263 315.616 -333.907 16.6276 316.615 -333.907 16.6323 317.621 +-333.926 16.636 318.623 -333.801 16.6705 319.759 -333.903 16.7573 320.734 +-333.813 16.6159 321.688 -334.034 16.6056 323.671 -333.926 16.661 324.687 +-334.19 17.4398 335.801 -333.997 16.8045 335.824 -333.629 17.0131 336.12 +-333.777 18.2342 304.312 -333.896 18.0685 335.429 -333.739 17.6038 335.778 +-333.84 19.2707 304.307 -334.219 19.3514 334.762 -333.795 19.1381 335.232 +-333.591 20.1631 304.41 -333.901 20.233 334.422 -333.682 19.8419 334.915 +-333.99 20.7078 304.848 -333.776 21.3178 305.265 -333.847 21.3337 334.297 +-333.826 22.2632 305.319 -333.739 22.2275 334.31 -333.644 23.0529 305.452 +-333.781 23.2452 334.327 -334.051 23.7086 305.815 -333.717 24.1769 306.282 +-333.804 24.2464 334.33 -333.824 25.2254 306.381 -333.793 25.2414 334.333 +-333.701 26.357 306.263 -333.802 26.2569 334.368 -333.904 27.3481 306.36 +-333.828 27.1593 334.273 -333.77 28.2279 306.354 -334.03 28.3306 333.367 +-333.603 27.7525 333.829 -333.794 29.2432 306.337 -333.692 29.2324 333.351 +-333.801 30.2449 306.328 -334.345 30.3553 333.918 -333.844 29.8579 333.756 +-333.711 30.309 334.217 -333.797 31.244 306.33 -333.784 31.2063 334.386 +-333.796 32.2444 306.33 -333.807 32.2409 334.341 -333.794 33.2446 306.327 +-333.798 33.2496 334.323 -333.785 34.2435 306.336 -333.792 34.2441 334.334 +-333.81 35.2503 306.375 -333.809 35.2505 334.318 -333.874 36.1679 306.264 +-333.817 36.2624 334.276 -333.633 36.8005 305.792 -333.815 37.2799 305.459 +-333.631 37.0419 334.466 -333.978 37.7525 305.778 -333.725 38.3541 306.248 +-334.113 37.7115 334.877 -333.759 38.0949 335.163 -333.784 39.2421 306.371 +-333.873 39.241 334.396 -333.759 38.628 334.778 -333.806 40.2439 306.336 +-333.77 40.2421 334.299 -333.797 41.2442 306.327 -333.808 41.263 334.297 +-333.796 42.2446 306.33 -333.735 42.1409 334.434 -333.793 43.2452 306.332 +-334.375 43.3659 334.915 -333.916 42.7897 334.76 -333.718 43.182 335.248 +-333.791 44.2398 306.332 -333.874 44.2903 335.4 -333.809 45.2266 306.321 +-333.95 45.3107 317.383 -333.677 44.8541 317.855 -334.413 45.1654 317.935 +-333.694 44.7446 318.973 -333.96 44.7278 319.691 -333.736 44.6958 320.777 +-333.736 45.2317 321.39 -333.742 45.2024 335.389 -333.697 46.3485 306.402 +-333.861 46.3028 314.461 -333.524 45.8879 314.918 -334.214 46.2393 315.09 +-333.897 45.8387 315.807 -334.383 46.1837 315.893 -333.742 45.6096 316.729 +-333.985 45.6339 321.734 -333.874 45.7132 322.551 -333.868 45.687 323.605 +-333.872 45.672 324.633 -333.884 45.669 325.625 -333.794 45.6868 326.741 +-333.763 45.7585 327.868 -333.887 45.8079 328.856 -333.742 46.1731 329.432 +-334.227 46.429 334.788 -333.894 46.0378 335.243 -334.122 47.3936 306.807 +-333.946 46.9143 306.827 -333.786 47.2348 307.097 -334.054 47.2542 312.264 +-333.574 46.8271 312.773 -333.91 46.6449 313.839 -333.834 46.6131 329.914 +-333.856 46.7596 330.748 -333.886 46.8582 331.722 -333.734 47.3192 332.26 +-333.993 47.1647 334.364 -333.498 46.7762 334.881 -333.67 47.5816 306.799 +-333.934 48.0431 306.452 -333.975 48.2326 309.347 -333.566 47.8413 309.979 +-333.942 47.7386 310.718 -333.719 47.6223 311.564 -333.706 47.7126 332.873 +-333.775 47.7183 333.756 -333.854 48.7044 306.86 -333.733 48.8561 307.838 +-333.711 48.6909 308.775 -333.477 -14.5196 302.7 -333.348 -13.8981 301.958 +-333.117 -13.7907 302.635 -333.272 -13.8405 303.681 -333.447 -13.6736 304.765 +-333.311 -12.7223 300.785 -333.013 -12.9012 301.208 -332.858 -13.2583 301.963 +-332.749 -13.2987 303.028 -332.834 -13.1794 303.892 -333.139 -12.8685 304.793 +-332.717 -12.6594 304.408 -333.414 -12.8083 305.752 -333.363 -11.6955 299.688 +-332.96 -11.8973 300.22 -332.621 -12.3307 300.886 -332.873 -12.178 304.999 +-333.154 -11.8132 305.831 -332.815 -11.7158 305.374 -333.449 -11.905 306.645 +-332.645 -11.1454 299.019 -332.937 -10.62 298.433 -332.743 -11.4363 299.707 +-332.703 -11.2222 305.773 -333.317 -10.9207 306.824 -332.806 -10.7184 306.362 +-332.914 -9.76201 298.215 -332.809 -10.2363 306.846 -332.803 -9.75287 307.342 +-333.335 -9.73992 307.85 -332.57 -9.22576 297.873 -333.045 -8.74767 297.395 +-332.801 -9.26132 307.833 -332.804 -8.76647 308.345 -333.339 -8.75063 308.868 +-332.557 -8.27109 296.75 -333.033 -7.7997 296.301 -332.838 -8.24177 308.87 +-332.697 -7.65851 309.282 -333.297 -7.773 309.741 -332.806 -7.37537 295.847 +-332.964 -6.70131 295.363 -332.87 -7.37479 309.926 -332.881 -7.13853 310.854 +-332.765 -6.80452 311.296 -333.314 -6.77232 311.838 -332.578 -6.10687 293.682 +-332.898 -5.64664 293.309 -333.31 -5.59224 293.966 -333.29 -5.85251 294.901 +-332.639 -6.26858 294.575 -332.781 -6.27641 311.814 -332.794 -5.77215 312.329 +-333.338 -5.75408 312.86 -332.622 -5.12415 291.645 -332.849 -4.51799 291.349 +-333.169 -4.77253 292.031 -333.346 -4.86098 292.84 -332.691 -5.30621 292.558 +-332.828 -5.28023 312.867 -332.734 -4.47939 313.314 -333.379 -4.65001 313.856 +-333.344 -3.99871 291.942 -332.806 -3.54982 291.297 -333.209 -3.93008 313.805 +-332.709 -3.70727 313.485 -333.363 -3.85407 314.568 -333.385 -3.7081 315.534 +-333.3 -2.97725 291.807 -332.783 -2.53301 291.33 -332.777 -3.34308 314.032 +-332.702 -3.30069 314.987 -332.824 -3.32806 316.036 -332.765 -3.26263 316.811 +-332.945 -3.11942 317.645 -332.775 -2.67949 318.183 -333.286 -1.98014 291.835 +-332.797 -1.53086 291.343 -332.741 -2.33828 318.786 -332.912 -2.17812 319.781 +-332.734 -1.84094 320.435 -333.296 -0.984708 291.837 -332.829 -0.528591 291.326 +-332.855 -1.41471 320.919 -333.254 -0.760793 321.838 -332.938 -1.07368 321.601 +-332.599 -0.731023 322.268 -333.318 0.012935 291.804 -332.693 0.48305 291.294 +-333.35 0.354189 322.954 -332.918 -0.175602 322.7 -332.738 0.346166 323.203 +-333.339 1.05578 291.873 -332.608 1.40435 291.414 -332.807 1.17278 323.481 +-332.979 1.8099 291.989 -332.778 2.25112 292.269 -333.247 2.21289 292.696 +-333.31 2.51187 323.855 -332.745 1.74121 323.889 -332.681 2.13815 324.366 +-332.814 2.70367 292.872 -333.047 2.92437 294.003 -332.699 3.33309 294.235 +-333.222 3.18359 294.748 -333.357 3.26915 295.59 -333.333 3.26948 296.584 +-333.316 3.25244 297.578 -333.313 3.24171 298.599 -333.334 3.25829 299.632 +-333.269 3.27276 300.596 -332.843 3.16689 301.242 -332.867 2.86791 301.741 +-332.57 3.20432 302.332 -333.465 3.41723 322.876 -333.355 3.23792 323.898 +-332.893 2.63501 324.724 -333.151 3.33687 324.626 -332.758 3.07122 325.197 +-332.669 3.64936 294.947 -332.753 3.69037 296.023 -332.722 3.68366 297.026 +-332.709 3.67522 298.037 -332.721 3.69138 299.059 -332.73 3.71701 300.083 +-332.602 3.60145 300.885 -332.842 3.68106 302.927 -332.808 3.81089 303.821 +-332.784 3.78781 304.839 -332.795 3.77635 305.852 -332.775 3.75736 306.859 +-332.757 3.70464 307.865 -332.835 3.83288 308.742 -333.251 4.32885 308.903 +-332.673 4.15911 309.397 -333.441 4.3487 320.696 -333.301 4.17228 321.69 +-333.195 4.27322 322.682 -333.202 4.08895 323.728 -332.764 3.68142 325.055 +-332.9 4.16266 324.47 -332.891 4.64572 309.746 -332.744 4.76413 310.82 +-332.996 4.81868 311.88 -332.546 5.20858 312.357 -333.279 5.17882 312.668 +-333.437 5.37854 314.744 -333.287 5.28579 315.588 -333.323 5.27907 316.592 +-333.361 5.29151 317.584 -333.302 5.29275 318.52 -332.953 5.205 319.238 +-332.909 4.92127 319.867 -332.895 4.75225 320.896 -332.662 4.74257 322.05 +-332.823 4.64174 322.943 -332.657 4.67367 323.762 -332.925 5.63315 313.033 +-332.721 5.79544 313.869 -332.82 5.7046 314.87 -332.711 5.67122 316.006 +-332.737 5.67792 317.003 -332.744 5.68815 318.002 -332.598 5.61668 318.966 +-332.536 13.8152 331.79 -332.908 14.3116 331.392 -333.264 14.2049 332.135 +-333.272 14.177 332.802 -332.858 13.8525 332.687 -332.689 14.2452 333.308 +-332.664 14.8353 307.812 -332.877 15.3292 307.393 -333.357 15.1703 307.945 +-332.756 14.7104 308.933 -332.87 14.7032 309.804 -332.809 14.6663 310.833 +-333.017 14.7599 311.754 -332.702 15.3286 312.307 -332.568 14.8402 325.808 +-332.892 15.345 325.261 -333.292 15.3891 325.871 -333.445 15.1367 326.851 +-332.824 14.8039 326.922 -332.668 14.7432 327.992 -332.934 14.7449 328.716 +-332.728 14.6715 329.617 -332.958 14.634 330.708 -333.24 15.3426 333.875 +-332.869 14.8395 333.772 -332.669 15.1749 334.368 -332.728 15.8596 304.894 +-332.897 16.415 304.425 -333.346 16.1385 304.922 -332.652 15.6226 305.634 +-332.962 15.6251 306.706 -333.366 16.3141 313.036 -332.741 15.8429 312.878 +-333.281 16.2562 314.027 -332.674 15.8709 313.796 -333.295 16.2316 315.054 +-332.687 15.8454 314.83 -333.293 16.2363 316.056 -332.68 15.8552 315.835 +-333.283 16.2427 317.069 -332.658 15.8597 316.844 -333.302 16.2507 318.075 +-332.775 15.8799 317.712 -333.329 16.2676 319.071 -332.673 15.7493 318.598 +-333.158 16.3643 319.908 -332.672 15.744 319.729 -332.604 16.222 320.371 +-332.743 15.9287 321.896 -332.829 16.3262 321.401 -333.271 16.2245 321.837 +-333.126 16.1467 322.836 -332.659 16.2201 322.513 -332.822 16.3029 323.206 +-332.838 15.9398 323.579 -333.322 16.2875 324.15 -333.249 16.0606 324.936 +-332.633 15.7037 324.583 -332.852 15.5937 334.764 -333.364 16.3265 335.993 +-332.899 15.86 335.671 -332.72 16.2564 336.237 -332.727 17.1994 304.281 +-333.003 16.6017 320.76 -332.817 16.4818 322.844 -332.671 16.8073 335.931 +-333.361 17.2572 335.851 -332.986 17.4832 335.468 -332.797 18.2434 304.318 +-332.784 18.2851 335.294 -332.855 19.2948 304.295 -332.677 19.1996 335.356 +-332.594 20.1893 304.39 -333.227 20.3957 334.769 -332.901 20.0485 335.241 +-332.981 20.7488 304.835 -332.776 21.3596 305.229 -332.975 21.1724 334.39 +-332.539 20.7246 334.839 -332.712 22.2599 305.398 -332.739 22.2231 334.299 +-333.335 23.3741 305.897 -332.853 22.8825 305.747 -332.708 23.3318 306.201 +-332.777 23.24 334.322 -332.806 24.221 306.399 -332.802 24.2454 334.329 +-332.931 25.2798 306.258 -332.789 25.2471 334.344 -332.588 25.7834 305.762 +-332.629 26.3245 305.49 -333.187 26.1192 305.846 -332.81 26.2735 334.384 +-332.873 26.6309 305.848 -332.839 27.2432 306.248 -332.885 27.1753 334.215 +-332.763 28.2467 306.358 -332.899 28.2404 333.354 -332.746 27.629 333.756 +-332.804 29.2486 306.336 -332.565 29.1443 333.381 -332.8 30.2441 306.327 +-332.994 29.7204 333.836 -332.756 30.3058 334.279 -332.796 31.2439 306.33 +-332.78 31.2079 334.374 -332.796 32.2444 306.329 -332.803 32.2386 334.334 +-332.792 33.2434 306.325 -332.797 33.2488 334.327 -332.787 34.2442 306.347 +-332.795 34.2445 334.332 -332.816 35.2645 306.389 -332.823 35.2577 334.301 +-332.897 36.1741 306.193 -332.717 36.2533 334.379 -332.738 36.6436 305.71 +-332.823 37.1094 305.464 -333.365 37.3947 334.947 -332.848 36.8746 334.774 +-332.731 37.2754 335.192 -333.005 37.7066 305.785 -332.712 38.2101 306.269 +-332.651 37.7964 334.928 -333.326 38.2355 334.855 -332.948 38.4324 334.46 +-332.81 39.2514 306.358 -332.749 39.2045 334.27 -332.802 40.2453 306.329 +-332.792 40.2503 334.286 -332.796 41.2437 306.328 -332.708 41.2587 334.382 +-332.796 42.2448 306.331 -333.337 42.3357 334.914 -332.837 41.8541 334.76 +-332.719 42.3177 335.213 -332.793 43.2453 306.332 -332.816 43.2454 335.387 +-332.801 44.2399 306.329 -332.934 44.2803 320.437 -332.608 43.8843 320.673 +-333.184 44.1803 320.909 -332.706 44.2517 321.327 -332.828 44.2974 335.364 +-332.811 45.2318 306.317 -332.931 45.3051 316.357 -332.659 44.8535 316.851 +-333.375 45.1778 316.931 -332.839 44.7427 317.843 -332.763 44.6684 318.615 +-332.975 44.6131 319.708 -332.864 44.8107 321.85 -333.339 45.2895 322.039 +-332.685 44.8612 322.832 -333.272 45.2588 323.015 -332.687 44.8461 323.833 +-333.284 45.246 324.044 -332.69 44.8468 324.828 -333.296 45.2433 325.044 +-332.713 44.8302 325.826 -333.33 45.2371 326.035 -332.74 44.8776 326.77 +-333.214 45.3065 326.877 -332.707 45.1735 327.393 -332.82 45.3065 335.38 +-332.694 46.1178 306.29 -333.083 46.2295 314.282 -332.631 45.8414 314.657 +-332.836 45.6097 315.633 -332.889 45.6424 327.783 -332.751 45.763 328.861 +-332.806 45.8528 329.753 -333.209 46.3077 329.927 -332.617 46.1307 330.416 +-332.858 46.1936 335.227 -333.494 47.1022 306.819 -332.898 47.3246 306.437 +-332.817 47.3215 311.474 -332.653 46.8828 312.024 -333.34 47.1732 311.948 +-332.823 46.7444 312.841 -332.749 46.6098 313.563 -333.049 46.7134 330.841 +-332.522 47.1605 331.351 -333.223 47.211 331.727 -332.957 47.2296 334.381 +-332.856 46.736 334.89 -332.632 47.8422 306.872 -333.208 48.2678 306.799 +-332.659 48.2103 307.341 -333.009 48.2573 309.333 -332.541 47.8526 309.82 +-332.862 47.6697 310.874 -332.846 47.6976 332.111 -332.766 47.8581 332.852 +-332.712 47.6981 333.582 -332.964 48.6662 307.65 -332.802 48.6371 308.609 +-332.396 -12.5788 301.859 -332.379 -12.6343 302.834 -332.435 -12.6746 303.785 +-331.6 -12.1461 301.019 -331.796 -11.6251 300.407 -331.906 -12.2338 301.757 +-332.147 -11.9429 302.671 -331.517 -11.7803 302.376 -332.189 -11.8287 303.629 +-332.375 -11.8356 304.687 -332.235 -10.6733 298.722 -331.935 -10.8556 299.251 +-331.842 -11.1488 299.787 -331.826 -11.3946 303.036 -331.698 -11.2271 303.909 +-331.936 -11.1515 305 -331.756 -10.7901 305.314 -332.279 -10.7548 305.844 +-331.484 -10.3138 298.96 -332.019 -9.90719 298.309 -331.765 -10.2846 305.804 +-331.775 -9.77517 306.302 -332.29 -9.75703 306.827 -331.569 -9.23875 297.666 +-332.095 -8.72574 297.325 -331.778 -9.26877 306.803 -331.791 -8.7604 307.321 +-332.3 -8.76149 307.837 -331.708 -8.15665 295.806 -331.847 -7.66574 295.261 +-332.359 -7.64659 295.822 -331.65 -8.4165 296.633 -331.825 -8.24139 307.859 +-331.695 -7.65267 308.281 -332.264 -7.78702 308.724 -331.771 -7.25901 294.808 +-331.837 -6.78221 294.428 -332.206 -6.9663 295.008 -331.845 -7.39216 308.913 +-331.855 -7.17341 309.835 -331.682 -6.68602 310.349 -332.24 -6.81398 310.781 +-331.919 -6.18966 293.005 -331.835 -5.79203 292.423 -332.368 -5.84167 292.974 +-331.904 -6.40213 293.754 -331.913 -6.22257 311.013 -331.751 -5.74534 311.3 +-332.263 -5.76256 311.819 -331.942 -5.22639 290.988 -331.762 -4.62516 290.356 +-332.325 -4.82748 290.906 -331.905 -5.41842 291.758 -331.786 -5.25173 311.835 +-331.806 -4.75214 312.378 -332.258 -4.83854 312.762 -332.305 -4.01699 290.781 +-331.804 -3.51901 290.297 -331.79 -4.34123 312.718 -331.927 -3.77013 313.191 +-332.282 -2.98572 290.826 -331.803 -2.51772 290.339 -331.778 -3.28187 313.75 +-331.816 -3.20073 314.765 -331.757 -3.25511 315.799 -331.997 -3.18663 316.865 +-331.531 -2.75159 317.358 -332.244 -2.83102 317.698 -332.293 -1.98246 290.845 +-331.83 -1.52006 290.323 -331.902 -2.35872 318.063 -331.698 -2.22937 318.899 +-331.802 -2.2802 319.807 -332.225 -1.69812 320.968 -331.959 -2.12274 320.608 +-331.503 -1.77811 321.295 -332.333 -0.976221 290.827 -331.704 -0.487666 290.297 +-331.891 -1.26291 321.808 -331.664 -0.869036 322.519 -332.29 -0.0254651 290.807 +-331.623 0.260947 290.512 -332.02 -0.351652 322.878 -331.655 0.205498 323.343 +-331.896 0.600544 290.882 -331.828 1.12594 291.181 -332.328 1.27657 323.8 +-331.733 0.896159 323.862 -331.776 1.25317 324.301 -332.421 2.19917 291.785 +-331.685 2.06982 291.387 -332.368 2.3334 325.114 -332.01 1.72032 324.683 +-332.184 2.41918 326.001 -331.628 1.72194 325.75 -331.683 2.13703 326.367 +-331.834 2.77851 292.02 -331.705 3.22973 292.435 -332.193 3.22923 292.67 +-332.366 3.29004 293.505 -332.01 3.22468 301.427 -331.925 2.93392 301.925 +-331.669 3.29145 302.219 -332.427 3.34306 325.766 -331.914 2.64541 326.69 +-332.194 3.28874 326.772 -331.755 3.17707 327.221 -331.76 3.59326 293.028 +-331.849 3.72441 294.028 -331.669 3.82803 294.948 -331.823 3.81435 295.825 +-331.808 3.81145 296.832 -331.787 3.81206 297.828 -331.782 3.82358 298.821 +-331.939 3.8437 299.689 -331.592 3.63189 300.807 -331.674 3.71175 302.849 +-331.906 3.83014 303.739 -331.713 3.69493 304.636 -331.905 3.65859 305.635 +-331.794 3.68834 306.744 -331.734 3.75473 307.881 -331.735 3.69039 308.916 +-331.959 3.84916 309.686 -332.238 4.29551 310.016 -331.521 4.22374 310.363 +-332.482 4.36498 324.808 -332.239 4.42257 325.687 -332.173 4.14413 326.627 +-331.824 3.99757 327.345 -331.818 4.63342 310.839 -331.991 4.80324 311.762 +-331.618 5.36297 312.333 -332.417 5.32528 319.752 -332.284 5.25259 320.613 +-332.237 5.24312 321.572 -331.736 5.32099 322.3 -332.076 4.92183 322.883 +-331.743 5.37313 323.338 -332.212 5.09388 323.826 -332.188 5.16287 324.731 +-331.815 4.82544 326.116 -331.903 5.14184 325.352 -331.746 4.64862 326.954 +-331.792 5.74057 312.913 -331.806 5.73404 313.766 -331.786 5.73347 314.791 +-331.785 5.73657 315.815 -331.791 5.7416 316.805 -331.806 5.74948 317.797 +-331.799 5.75681 318.807 -331.768 5.65747 319.932 -331.667 5.64243 321.096 +-331.707 5.58137 321.792 -331.693 5.73168 324.057 -331.597 5.61206 324.93 +-331.491 13.904 329.783 -331.888 14.3351 329.406 -332.219 14.2546 330.099 +-332.419 14.1643 330.868 -331.809 13.8107 330.913 -331.831 13.6702 331.858 +-331.97 13.8306 332.702 -331.729 14.3196 333.253 -331.583 14.8586 305.843 +-331.916 15.2813 305.385 -332.269 15.2344 306.042 -332.417 15.1802 306.843 +-331.836 14.7961 306.838 -331.695 14.7214 307.934 -331.841 14.742 308.831 +-331.844 14.7562 309.843 -331.796 14.7342 310.855 -331.854 14.7964 311.848 +-331.683 15.1704 312.426 -331.648 14.8482 318.872 -331.927 15.3195 318.391 +-332.297 15.2225 319.105 -332.293 15.2005 319.747 -331.724 14.8778 319.775 +-331.815 15.223 320.313 -331.648 14.8741 324.889 -331.913 15.3075 324.425 +-332.405 15.1405 324.991 -331.829 14.7415 325.878 -331.763 14.652 326.62 +-331.891 14.6521 327.639 -331.947 14.6332 328.717 -331.71 14.6541 333.859 +-332.199 15.2756 335.009 -331.976 14.8774 334.646 -331.484 15.1967 335.356 +-331.696 15.6931 304.858 -331.955 16.286 304.369 -331.967 15.5689 312.768 +-331.883 15.6589 313.575 -331.897 15.6523 314.627 -331.894 15.6589 315.638 +-331.856 15.6731 316.627 -331.942 15.632 317.691 -332.389 16.2059 320.937 +-332.034 15.7421 320.779 -331.733 15.5899 321.627 -332.483 16.2462 322.984 +-331.95 15.7508 322.687 -331.852 15.5819 323.796 -331.978 15.7051 335.79 +-331.76 16.2171 336.24 -331.728 17.1965 304.295 -331.996 17.3257 335.395 +-331.532 16.7205 335.871 -331.809 18.2537 304.316 -331.768 18.2639 335.298 +-331.853 19.2945 304.283 -331.825 19.3214 335.372 -331.642 20.0511 304.455 +-331.864 20.2075 335.274 -332.056 20.7108 304.816 -331.742 21.1932 305.236 +-331.983 21.3323 334.403 -331.607 20.7606 334.834 -331.638 22.0251 305.479 +-331.741 22.2115 334.291 -332.065 22.6833 305.83 -331.727 23.1667 306.29 +-331.775 23.2336 334.32 -331.839 24.2261 306.396 -331.798 24.243 334.331 +-331.75 25.226 306.262 -331.793 25.2526 334.355 -331.707 25.841 305.858 +-331.958 26.1657 305.492 -331.867 26.3149 334.281 -331.998 26.7 305.764 +-331.658 27.2009 306.256 -331.97 27.4449 333.443 -331.536 26.7877 333.833 +-332.247 27.1234 333.909 -331.777 28.2542 306.354 -331.805 28.2411 333.238 +-331.806 29.2473 306.328 -331.578 29.1679 333.384 -331.798 30.2441 306.328 +-331.989 29.7404 333.84 -331.743 30.3147 334.267 -331.796 31.2442 306.331 +-331.775 31.2062 334.367 -331.796 32.244 306.328 -331.803 32.2381 334.336 +-331.793 33.2397 306.327 -331.796 33.2485 334.329 -331.802 34.2447 306.35 +-331.805 34.2473 334.325 -331.767 35.3621 306.276 -331.833 35.2547 334.292 +-331.629 35.8879 305.865 -331.982 36.4516 305.539 -332.223 36.1018 305.868 +-331.582 36.1447 334.416 -331.727 36.7012 305.781 -332.226 37.242 305.786 +-331.664 37.1766 306.204 -331.997 36.7236 334.868 -331.754 37.2126 335.207 +-331.803 38.2596 306.369 -331.986 38.3025 334.405 -331.531 37.7218 334.88 +-331.809 39.251 306.336 -331.778 39.2249 334.274 -331.797 40.2439 306.326 +-331.816 40.2685 334.277 -331.796 41.2438 306.33 -331.637 41.0344 334.459 +-331.795 42.2447 306.331 -332.055 41.6746 334.833 -331.731 42.164 335.286 +-331.794 43.2453 306.332 -331.801 43.2368 335.352 -331.801 44.246 306.334 +-331.932 44.3185 318.399 -331.544 43.8601 318.764 -332.221 44.2391 319.103 +-331.927 43.7864 319.84 -332.401 44.1421 319.913 -331.899 43.8529 320.779 +-331.742 44.3318 321.263 -331.794 44.2592 335.343 -331.815 45.2474 306.322 +-331.843 45.3061 315.437 -331.645 44.8771 316.006 -332.363 45.1633 315.911 +-331.811 44.7374 316.834 -331.85 44.6275 317.684 -331.783 44.6886 321.774 +-331.736 44.6852 322.62 -331.917 44.6345 323.626 -331.909 44.6372 324.613 +-331.914 44.6279 325.615 -331.816 44.639 326.752 -331.849 44.884 327.747 +-332.227 45.3453 327.962 -331.629 45.2065 328.399 -331.858 45.3017 335.283 +-331.842 46.284 306.278 -331.825 46.3444 313.464 -331.609 45.9017 314.004 +-332.35 46.1821 313.967 -331.765 45.6382 314.928 -331.977 45.62 328.671 +-331.752 45.6287 329.789 -331.932 45.8467 330.674 -332.341 46.3505 330.935 +-331.756 46.3442 331.188 -331.854 46.3516 334.346 -331.566 45.7854 334.869 +-332.314 46.355 334.782 -331.627 47.208 306.354 -332.037 47.2578 311.291 +-331.552 46.8517 311.796 -331.887 46.6828 312.844 -331.644 47.0838 331.424 +-331.894 47.359 333.408 -331.525 46.7945 333.698 -332.246 47.0518 334.034 +-331.815 47.6938 306.866 -331.841 47.8597 307.746 -332.301 48.2701 308.113 +-331.898 47.8669 308.838 -332.366 48.1946 308.94 -331.652 47.645 309.651 +-331.89 47.5956 310.566 -331.993 47.657 331.738 -331.942 47.706 332.652 +-331.231 -11.795 300.796 -330.846 -11.9227 301.25 -331.016 -12.1427 301.77 +-330.722 -11.7168 302.341 -331.322 -10.6843 299.721 -330.945 -10.9061 300.315 +-330.753 -11.4035 300.904 -331.069 -11.2093 302.948 -330.708 -10.6283 303.364 +-331.134 -10.8247 303.699 -331.35 -10.7367 304.644 -330.762 -10.0361 299 +-331.063 -9.7502 298.339 -330.794 -10.3479 299.796 -330.762 -10.3267 304.068 +-330.909 -10.19 304.951 -330.663 -9.65976 305.355 -331.246 -9.7981 305.783 +-330.687 -9.1139 296.945 -330.882 -8.67529 296.422 -331.394 -8.79715 296.948 +-330.658 -9.35634 297.919 -330.909 -9.22487 306.015 -330.759 -8.76052 306.321 +-331.277 -8.7602 306.822 -330.673 -8.15141 295.026 -330.801 -7.66327 294.33 +-331.389 -7.64437 294.762 -330.806 -8.37797 295.932 -330.806 -8.24158 306.858 +-330.682 -7.66298 307.266 -331.267 -7.78202 307.727 -330.536 -7.13287 292.835 +-330.862 -6.64797 292.275 -331.289 -6.57573 292.901 -331.291 -6.97662 293.924 +-330.689 -7.2673 293.743 -330.842 -7.3859 307.91 -330.882 -7.16734 308.862 +-330.724 -6.49092 309.302 -331.308 -6.70314 309.799 -330.791 -6.20083 290.883 +-330.831 -5.76387 290.263 -331.289 -5.59176 290.874 -331.266 -5.96424 291.926 +-330.779 -6.35285 291.547 -331.172 -5.95289 309.8 -330.684 -5.71421 309.508 +-331.377 -5.84352 310.624 -330.722 -5.30529 289.662 -330.735 -4.67747 289.419 +-331.225 -4.89398 289.901 -330.729 -5.38658 310.062 -330.907 -5.18667 310.971 +-330.525 -4.76536 311.379 -331.304 -4.74967 311.842 -331.341 -3.94647 289.768 +-330.738 -3.73766 289.421 -330.945 -4.24151 312.041 -330.584 -3.70891 312.342 +-331.281 -3.77442 312.672 -331.333 -2.94811 289.819 -330.667 -2.58682 289.313 +-330.902 -3.33241 313.04 -330.699 -3.20454 313.889 -330.812 -3.23391 314.788 +-330.769 -3.27942 315.825 -330.988 -3.17898 316.711 -330.594 -2.74596 317.289 +-331.344 -1.9515 289.82 -330.855 -1.56602 289.417 -330.837 -2.29718 317.876 +-330.827 -2.19549 318.769 -330.784 -2.27116 319.821 -330.864 -2.21638 320.86 +-330.706 -1.85597 321.496 -331.298 -0.989746 289.788 -330.556 -0.801955 289.432 +-330.979 -1.4705 321.848 -331.314 -0.676801 322.94 -330.82 -1.11747 322.549 +-330.904 -0.583734 323.13 -330.916 -0.322801 289.842 -330.705 0.346119 290.301 +-330.673 0.0371029 323.401 -331.089 0.809198 290.819 -330.757 1.56345 291.151 +-330.962 0.582497 323.793 -331.323 1.23464 325.125 -330.901 0.753482 324.57 +-331.193 1.2366 325.803 -330.777 0.776427 325.567 -330.828 1.18211 326.236 +-330.66 2.31002 291.394 -331.329 2.366 327.098 -330.977 1.74242 326.714 +-331.182 2.30838 328.049 -330.738 1.74305 327.597 -331.253 2.31206 329.088 +-330.788 1.71601 328.617 -331.165 2.42874 329.888 -330.828 1.76966 329.593 +-330.743 2.15885 330.235 -330.795 2.68765 291.875 -330.842 2.91674 292.683 +-331.236 3.35968 292.896 -330.626 3.20514 293.382 -330.948 3.32175 300.421 +-330.524 2.93756 300.786 -331.182 3.25314 300.853 -331.406 3.29925 301.717 +-330.808 3.27308 301.349 -330.914 3.31735 304.403 -330.558 2.85935 304.745 +-331.272 3.22698 305.097 -330.757 2.78594 305.796 -331.353 3.22891 306.025 +-330.743 2.84387 306.756 -331.198 3.30986 306.873 -330.691 3.14608 307.395 +-331.398 3.32996 327.859 -331.35 3.21788 328.86 -331.262 3.2493 329.767 +-330.817 3.01856 330.382 -330.989 3.64969 293.623 -330.862 3.71164 294.593 +-330.874 3.69307 295.618 -330.877 3.68338 296.636 -330.863 3.685 297.638 +-330.841 3.69279 298.63 -330.944 3.65892 299.691 -331.027 3.60906 301.925 +-330.716 3.7027 302.692 -330.923 3.66949 303.713 -330.867 3.62229 307.8 +-330.853 3.73809 308.776 -330.91 3.85606 309.733 -330.732 4.31426 310.25 +-331.414 4.51864 327.83 -331.334 4.13343 328.759 -331.204 4.34927 329.602 +-330.808 3.99753 330.337 -330.789 4.6259 310.766 -330.946 4.84067 311.695 +-330.682 5.44876 312.203 -331.486 5.42238 322.946 -331.431 5.3361 325.703 +-331.296 5.17194 326.728 -331.212 5.26848 327.704 -331.219 5.11228 328.713 +-330.746 4.75494 330.004 -330.917 5.20739 329.402 -331.303 6.15531 312.709 +-330.81 6.18763 312.476 -330.934 6.21297 313.221 -330.861 5.87869 313.883 +-330.843 5.78954 314.86 -330.855 5.80016 315.875 -330.858 5.80858 316.868 +-330.86 5.80807 317.862 -330.871 5.81499 318.858 -330.873 5.82965 319.859 +-330.865 5.85168 320.874 -331.059 5.85353 321.881 -331.193 6.04143 322.745 +-330.667 6.35519 322.288 -331.282 6.13483 323.522 -330.951 6.22224 324.238 +-330.891 5.9283 324.856 -330.865 5.74626 325.892 -330.829 5.74004 327.044 +-330.727 5.75339 328.039 -330.646 5.62753 328.945 -330.6 6.60131 313.017 +-330.645 6.65393 322.98 -330.597 6.62284 324.007 -330.572 13.8426 316.784 +-330.888 14.3492 316.235 -331.182 14.4114 316.934 -331.236 14.1873 317.934 +-330.758 13.7951 317.852 -331.222 14.3828 318.763 -330.735 13.8719 318.81 +-330.692 14.1635 319.426 -330.697 13.8878 326.727 -330.888 14.3447 326.386 +-331.206 14.2639 327.106 -331.251 14.2744 328.056 -330.773 13.7448 327.584 +-331.382 14.2066 328.905 -330.859 13.7613 328.723 -330.774 13.6404 329.699 +-330.909 13.6539 330.624 -330.848 13.7243 331.911 -330.655 14.2394 332.44 +-331.221 14.2165 332.691 -330.634 14.795 305.89 -331.06 15.1929 305.253 +-330.847 14.7057 306.792 -330.794 14.7476 307.839 -330.911 14.7688 308.726 +-330.743 14.6781 309.61 -330.81 14.6757 310.761 -330.746 14.7663 311.884 +-331.311 15.2558 313.084 -330.782 14.8359 312.808 -331.274 15.2694 314.074 +-330.763 14.8776 313.715 -331.279 15.2833 315.086 -330.649 14.7503 314.61 +-331.269 15.1393 316.066 -330.798 14.6868 315.548 -331.374 15.291 316.949 +-331.433 14.9664 317.84 -331.04 14.5625 319.669 -331.299 15.2314 321.077 +-330.829 14.7445 320.707 -331.234 15.2319 322.019 -330.64 14.8641 321.82 +-331.246 15.2415 323.041 -330.757 14.8853 322.68 -331.392 15.1876 323.903 +-330.739 14.7682 323.678 -330.792 14.6728 324.673 -330.978 14.6514 325.69 +-331.004 14.583 332.997 -330.706 14.6625 333.777 -331.024 14.8304 334.745 +-330.517 15.3508 335.363 -330.542 15.7001 304.777 -330.948 16.299 304.387 +-331.004 15.7729 335.943 -330.923 16.3155 336.163 -330.739 17.2087 304.304 +-330.838 17.2279 335.383 -330.652 16.62 335.788 -330.811 18.2539 304.304 +-330.764 18.2572 335.311 -330.732 19.2609 304.392 -330.824 19.2941 335.373 +-331.349 20.365 304.897 -330.854 19.852 304.761 -330.729 20.3129 305.189 +-330.859 20.1789 335.268 -330.71 21.2214 305.43 -330.996 21.3161 334.397 +-330.607 20.7507 334.836 -331.343 22.3597 305.905 -330.859 21.859 305.761 +-330.704 22.3119 306.2 -330.747 22.207 334.291 -330.783 23.2022 306.396 +-330.779 23.2361 334.319 -330.842 24.2337 306.393 -330.795 24.2437 334.336 +-330.605 25.2993 306.271 -330.804 25.2577 334.369 -331.392 26.3591 305.858 +-330.98 26.1842 306.275 -330.828 26.1595 334.271 -330.751 27.2453 306.38 +-331.025 27.3255 333.384 -330.589 26.7477 333.838 -330.796 28.248 306.344 +-330.82 28.256 333.253 -330.803 29.2463 306.324 -330.598 29.1808 333.396 +-330.797 30.2452 306.328 -330.992 29.7508 333.836 -330.74 30.3129 334.264 +-330.796 31.2445 306.331 -330.776 31.2053 334.368 -330.795 32.244 306.327 +-330.803 32.2379 334.337 -330.799 33.2408 306.329 -330.795 33.2486 334.329 +-330.818 34.2433 306.353 -330.816 34.2493 334.319 -330.687 35.1061 306.375 +-330.84 35.2596 334.293 -331.445 36.3109 305.861 -330.907 36.0883 306.276 +-330.563 36.3172 334.41 -330.783 37.241 306.374 -331.023 36.8102 334.884 +-330.923 37.3257 335.151 -330.797 38.2509 306.342 -330.86 38.2348 334.405 +-330.669 37.6347 334.805 -330.801 39.2478 306.323 -330.805 39.2554 334.283 +-330.797 40.2454 306.328 -330.727 40.2598 334.382 -330.796 41.2443 306.331 +-331.34 41.3538 334.911 -330.852 40.8591 334.757 -330.706 41.3059 335.212 +-330.795 42.2443 306.331 -330.78 42.2021 335.379 -330.793 43.2441 306.333 +-330.798 43.2344 335.337 -330.791 44.2427 306.341 -330.915 44.3103 317.42 +-330.722 43.8753 317.783 -331.373 44.1727 317.971 -330.7 43.6224 318.65 +-330.88 43.6771 319.887 -330.639 44.2324 320.435 -331.231 44.2039 320.685 +-330.917 44.3267 322.37 -330.567 43.8375 322.716 -331.275 44.231 323.098 +-330.759 43.802 323.771 -331.326 44.23 324.029 -330.732 43.8103 324.77 +-331.311 44.2325 325.024 -330.734 43.8043 325.782 -331.325 44.2291 326.042 +-330.734 43.8726 326.736 -331.181 44.309 326.894 -330.683 44.1647 327.374 +-330.804 44.2512 335.353 -330.815 45.2535 306.326 -331.025 45.2416 315.283 +-330.595 44.8484 315.701 -330.816 44.6265 316.686 -330.965 44.6103 321.014 +-330.845 44.651 321.741 -331.013 44.5812 327.673 -330.823 44.7415 328.712 +-331.317 45.2468 329.13 -330.802 44.9297 329.664 -331.151 45.281 329.959 +-330.545 45.2318 330.317 -330.818 45.1413 335.274 -330.859 46.2901 306.269 +-330.949 46.1886 313.31 -330.814 45.7531 313.874 -330.843 45.5715 314.544 +-331.038 45.7091 330.746 -330.711 46.3514 331.298 -331.061 46.2493 334.356 +-330.589 45.7725 334.883 -330.691 47.0995 306.4 -330.953 47.304 309.497 +-330.574 46.9398 310.019 -331.261 47.2266 310.075 -330.915 46.8257 310.815 +-331.414 47.1759 310.896 -330.667 46.6396 311.662 -330.915 46.6297 312.56 +-331.328 47.3153 332.028 -330.73 46.8464 331.877 -330.765 46.882 332.902 +-331.389 47.2154 332.867 -330.659 46.6521 333.855 -330.849 47.6736 306.874 +-330.891 47.8161 307.667 -330.669 47.6237 308.863 -330.39 -11.5717 301.778 +-330.382 -10.6752 300.829 -330.191 -10.5828 301.821 -330.399 -10.865 302.75 +-330.428 -9.66248 298.701 -330.291 -9.81069 299.717 -330.157 -9.84524 300.785 +-329.887 -10.1207 301.963 -329.739 -9.73262 301.373 -330.136 -10.0278 302.673 +-329.741 -9.65097 302.375 -330.29 -9.78702 303.72 -330.426 -9.65219 304.767 +-329.597 -9.18743 296.864 -330.022 -8.75786 296.273 -329.852 -9.27444 297.753 +-329.804 -9.32002 298.87 -329.708 -9.30871 300.03 -329.767 -9.37876 300.732 +-329.799 -9.29492 303.102 -329.876 -9.07657 303.93 -330.127 -8.94651 304.703 +-329.801 -8.72022 304.371 -330.348 -8.83941 305.681 -329.603 -8.16475 294.777 +-330.1 -7.77964 294.309 -329.793 -8.36425 295.704 -329.673 -8.32486 304.909 +-329.969 -8.13117 306.037 -329.648 -7.68378 306.264 -330.231 -7.80782 306.709 +-329.574 -7.14863 290.924 -329.848 -6.67022 290.299 -330.326 -6.65034 290.896 +-330.454 -6.88611 291.918 -329.833 -7.20407 291.958 -329.813 -7.25468 292.901 +-329.755 -7.38866 293.608 -329.806 -7.39903 306.892 -329.845 -7.14541 307.852 +-329.782 -6.78814 308.367 -330.238 -6.86878 308.73 -329.529 -6.22311 289.779 +-329.988 -5.54002 289.424 -330.287 -5.99777 289.928 -329.789 -6.35633 308.752 +-329.92 -5.77588 309.24 -329.819 -4.76886 289.296 -329.823 -5.33354 309.693 +-329.932 -5.10612 310.622 -329.726 -4.70646 311.205 -329.954 -3.70992 289.183 +-329.929 -4.25684 311.738 -329.631 -3.73469 312.314 -329.748 -3.32345 288.71 +-329.575 -2.76976 288.454 -330.198 -2.84252 288.855 -329.848 -3.32804 312.883 +-329.818 -3.2241 313.764 -329.803 -3.25139 314.798 -329.783 -3.2849 315.844 +-329.884 -3.20202 316.835 -329.744 -2.8172 317.432 -329.936 -2.32102 288.828 +-329.916 -1.69616 289.173 -329.867 -2.35427 317.888 -329.789 -2.16775 318.788 +-329.751 -2.2304 319.849 -329.745 -2.31293 320.903 -330.287 -1.61826 322.011 +-329.928 -2.15184 321.666 -329.596 -1.81101 322.177 -329.622 -0.942471 289.455 +-329.796 -1.04884 322.463 -330.314 -0.582703 322.944 -330.106 -0.234499 289.79 +-329.66 0.151479 290.194 -329.711 -0.40033 322.939 -329.804 -0.167829 323.721 +-330.341 0.254361 324.044 -330.297 0.195184 325.001 -329.687 -0.161178 324.803 +-330.306 0.331832 326.033 -329.712 -0.209623 325.821 -330.202 0.312305 326.996 +-329.719 -0.200934 326.818 -330.213 0.313536 328.008 -329.716 -0.188451 327.819 +-330.213 0.308657 329.024 -329.833 -0.16635 328.686 -330.218 0.308724 330.006 +-329.688 -0.286218 329.58 -330.237 0.289462 331 -329.856 -0.311643 330.578 +-330.264 0.265723 332.024 -329.868 -0.334346 331.604 -330.224 0.389466 332.997 +-329.774 -0.113721 332.557 -329.723 0.314502 333.15 -330.375 1.16408 290.75 +-329.724 1.04142 290.484 -330.422 1.13026 326.865 -330.378 1.12464 327.795 +-330.412 1.10522 328.821 -330.396 1.25423 329.866 -330.296 1.2025 330.773 +-330.318 1.19199 331.766 -330.357 1.17879 332.785 -329.654 0.703049 333.785 +-330.281 1.19087 333.907 -330.214 1.42854 334.963 -329.744 0.887931 334.763 +-329.719 1.15608 335.355 -329.678 1.62566 290.995 -329.806 2.15598 291.401 +-330.365 2.31596 330.842 -330.278 2.22568 331.819 -330.303 2.25393 332.828 +-330.332 2.24609 333.841 -330.365 2.23572 334.85 -329.943 1.58931 335.716 +-330.312 2.23261 335.933 -330.24 2.42042 336.862 -329.724 1.71531 336.742 +-329.792 2.20497 337.351 -329.944 2.62199 291.779 -329.85 2.67764 292.59 +-329.895 2.74436 293.606 -330.292 3.27734 294.111 -329.77 2.74036 294.615 +-330.248 3.27465 295.062 -329.789 2.75006 295.601 -330.264 3.27066 296.075 +-329.791 2.75736 296.599 -330.259 3.27657 297.083 -329.774 2.75982 297.597 +-330.233 3.28297 298.085 -329.747 2.76695 298.605 -330.218 3.28004 299.086 +-329.845 2.76161 299.763 -330.395 3.19073 299.936 -329.771 2.63116 300.748 +-329.921 2.76424 301.621 -330.3 3.30283 302.098 -329.77 2.76687 302.597 +-330.229 3.29357 303.028 -329.815 2.75251 303.796 -330.412 3.18651 303.876 +-329.705 2.75222 304.94 -329.824 2.71603 305.837 -329.814 2.66984 306.871 +-329.972 2.83828 307.649 -330.217 3.2839 307.996 -329.501 3.20765 308.328 +-330.387 3.45912 330.995 -330.282 3.22904 331.879 -330.317 3.2744 332.885 +-330.333 3.31918 333.896 -330.341 3.17304 334.761 -330.273 3.33011 335.644 +-330.235 3.13728 336.612 -329.733 3.16264 337.377 -330.021 3.74597 308.864 +-329.524 4.27776 309.358 -330.225 4.19413 309.718 -330.403 4.43972 330.694 +-330.207 4.29156 331.623 -330.223 4.14356 332.656 -330.262 4.19798 333.651 +-330.218 4.13344 334.716 -329.841 3.83462 336.14 -329.908 4.16916 335.399 +-329.721 3.76603 336.878 -329.842 4.61803 310.1 -329.949 4.82727 311.005 +-329.511 5.20181 311.389 -330.244 5.13139 311.738 -330.444 5.2521 329.861 +-330.157 5.15911 330.751 -329.815 4.81302 332.114 -329.882 5.13561 331.377 +-329.855 4.76031 333.063 -329.728 4.77149 334.046 -329.627 4.61159 334.954 +-329.886 5.59111 311.924 -329.705 6.18803 312.334 -330.384 6.32753 313.773 +-330.236 6.20357 314.664 -330.246 6.19001 315.677 -330.253 6.20103 316.667 +-330.252 6.20097 317.666 -330.264 6.20057 318.669 -330.278 6.20841 319.66 +-330.254 6.21952 320.634 -330.314 6.25199 321.566 -330.394 6.32131 324.767 +-330.243 6.22968 325.621 -330.294 6.2333 326.634 -330.3 6.23329 327.56 +-329.737 6.31386 328.311 -329.954 5.89603 328.826 -329.792 5.75452 329.897 +-329.586 5.5975 330.955 -329.946 6.69425 312.812 -329.797 6.78944 313.852 +-329.799 6.74734 315.096 -329.799 6.74583 316.085 -329.798 6.74815 317.077 +-329.808 6.74945 318.083 -329.829 6.75598 319.078 -329.654 6.73571 320.061 +-329.636 6.60357 321.025 -329.954 6.64653 321.979 -329.707 6.85555 322.807 +-329.938 6.88165 323.845 -329.865 6.74502 324.955 -329.775 6.73677 326.092 +-329.837 6.74176 327.098 -329.712 6.73209 327.851 -329.769 11.8327 328.716 +-329.855 12.3665 328.226 -330.172 12.4105 328.926 -330.173 12.4628 329.945 +-329.691 11.6917 329.707 -329.702 12.1573 330.345 -329.827 12.7943 325.895 +-329.813 13.2425 325.303 -330.137 13.3581 325.853 -330.284 13.3402 326.894 +-329.804 12.7319 326.606 -330.283 13.0977 328.044 -329.78 12.6981 327.546 +-330.414 13.2498 328.949 -330.411 13.1502 329.82 -329.892 12.6193 330.736 +-330.316 13.1807 330.941 -330.236 13.2865 331.799 -329.819 12.8112 331.59 +-329.672 13.179 332.271 -329.564 13.8197 309.932 -329.837 14.2981 309.48 +-330.299 14.2015 310.101 -330.24 14.2996 310.911 -329.912 13.8573 310.626 +-329.581 14.2341 311.285 -329.56 13.8632 314.785 -329.952 14.281 314.408 +-330.276 14.2148 315.113 -330.474 14.1312 315.902 -329.962 13.8323 315.845 +-329.698 13.6377 316.659 -329.842 13.645 317.759 -329.955 13.7387 318.753 +-330.369 14.2525 320.129 -329.797 13.7603 319.606 -330.168 14.3396 320.902 +-329.647 13.7638 320.771 -329.642 14.2315 321.411 -329.804 13.8404 323.86 +-329.835 14.2052 323.414 -330.244 14.285 324.055 -330.29 14.1575 325.02 +-329.806 13.706 324.508 -330.43 14.06 325.962 -330.368 14.2881 332.992 +-329.856 13.8201 332.771 -330.198 14.3071 333.831 -329.7 13.8845 333.738 +-329.7 14.1311 334.411 -329.614 14.8178 305.933 -330.044 15.1981 305.286 +-329.833 14.7317 306.853 -329.765 14.7854 307.888 -329.822 14.7006 308.884 +-329.812 14.712 311.826 -329.837 14.7921 312.77 -329.788 14.6258 313.696 +-329.984 14.6184 321.67 -329.944 14.6346 322.683 -329.893 14.6352 334.845 +-329.647 15.3143 335.386 -329.542 15.707 304.792 -329.954 16.3114 304.401 +-330.412 16.1151 335.817 -329.977 16.28 335.396 -329.754 17.2123 304.299 +-329.748 17.2365 335.272 -329.815 18.2593 304.297 -329.765 18.2505 335.318 +-329.594 19.1669 304.413 -329.818 19.2784 335.379 -330.008 19.7165 304.839 +-329.786 20.3306 305.243 -329.843 20.1834 335.278 -329.58 21.1321 305.445 +-329.997 21.328 334.404 -329.592 20.7706 334.839 -330.007 21.7127 305.843 +-329.746 22.304 306.265 -329.761 22.2229 334.297 -329.785 23.2039 306.402 +-329.788 23.243 334.322 -329.92 24.3051 306.277 -329.791 24.2487 334.347 +-329.623 24.8431 305.786 -329.678 25.3468 305.514 -330.21 25.1478 305.866 +-329.812 25.274 334.384 -329.908 25.6356 305.865 -329.852 26.245 306.272 +-329.887 26.1765 334.212 -329.762 27.2315 306.373 -329.884 27.2269 333.376 +-329.735 26.6252 333.761 -329.803 28.2378 306.336 -329.823 28.2693 333.268 +-329.801 29.248 306.324 -329.607 29.1827 333.402 -329.796 30.246 306.329 +-329.993 29.7483 333.835 -329.739 30.3112 334.264 -329.795 31.2446 306.33 +-329.776 31.2053 334.369 -329.793 32.2441 306.326 -329.802 32.238 334.338 +-329.801 33.2418 306.336 -329.794 33.2492 334.327 -329.825 34.2463 306.363 +-329.817 34.2489 334.321 -329.694 35.3686 306.275 -329.849 35.2514 334.315 +-329.898 36.3665 306.38 -329.71 36.1226 334.321 -329.764 37.2342 306.361 +-330.434 37.15 334.806 -329.901 37.2215 334.413 -329.794 38.2439 306.332 +-329.776 38.242 334.314 -329.802 39.2507 306.324 -329.821 39.2618 334.294 +-329.798 40.2477 306.328 -329.602 40.1577 334.406 -329.797 41.2448 306.33 +-330.003 40.718 334.84 -329.752 41.3015 335.275 -329.796 42.244 306.33 +-329.779 42.2061 335.365 -329.79 43.2412 306.335 -329.992 43.2984 318.387 +-329.548 42.8079 318.791 -330.318 43.1983 319.119 -329.85 42.8564 319.688 +-330.283 43.1768 319.791 -329.694 43.2464 320.295 -329.799 43.2417 335.34 +-329.787 44.236 306.343 -329.909 44.3127 316.415 -329.703 43.8837 316.79 +-330.35 44.1784 316.98 -329.787 43.6136 317.73 -329.851 43.8045 320.788 +-330.302 44.2983 320.996 -329.807 43.822 321.858 -330.402 44.1871 321.804 +-329.715 43.7043 322.942 -329.862 43.7033 323.816 -329.84 43.7116 324.827 +-329.824 43.7101 325.83 -329.835 43.7006 326.821 -329.864 43.7885 327.771 +-330.348 44.224 328.095 -329.67 43.8393 328.787 -330.152 44.2744 328.902 +-329.654 44.111 329.441 -329.809 44.261 335.371 -329.826 45.2608 306.311 +-329.809 45.3403 313.303 -329.529 44.8508 313.945 -330.26 45.3914 313.901 +-329.896 44.8218 314.861 -330.38 45.1588 314.925 -329.733 44.603 315.757 +-330.096 44.677 329.72 -329.599 45.1236 330.345 -329.858 45.1626 335.223 +-329.771 46.2898 306.339 -329.923 46.2962 311.434 -329.565 45.9064 311.908 +-330.296 46.2704 312.09 -329.725 45.7348 312.692 -330.303 46.0498 312.913 +-329.899 45.7709 330.917 -329.698 46.1982 331.407 -329.973 46.2061 334.371 +-329.86 45.715 334.885 -330.222 47.3876 306.883 -329.773 46.8993 306.787 +-329.62 47.204 307.333 -329.922 47.3117 308.391 -329.527 46.9011 308.722 +-330.158 47.2378 308.832 -330.336 47.3285 309.694 -329.792 47.2656 309.362 +-329.864 47.1977 310.15 -329.75 46.7308 310.723 -329.966 46.5662 331.758 +-329.876 46.6822 332.567 -329.872 46.6443 333.565 -330.026 47.616 307.71 +-329.871 47.5303 309.742 -329.513 -9.591 301.93 -328.551 -9.21409 296.908 +-328.992 -8.76745 296.292 -328.821 -9.32534 297.795 -328.777 -9.28693 298.815 +-328.744 -9.26268 299.835 -329.035 -9.20239 300.821 -329.194 -8.9851 301.686 +-328.507 -8.60855 301.39 -329.283 -8.89824 302.549 -329.376 -8.69323 303.685 +-328.68 -8.15957 293.804 -328.923 -7.67327 293.388 -329.393 -7.82149 293.983 +-328.724 -8.24761 294.931 -328.795 -8.34396 295.711 -328.799 -8.28458 302.074 +-328.763 -8.25162 303.017 -328.942 -8.15483 304 -328.796 -7.77285 304.339 +-329.176 -7.82737 304.787 -329.357 -7.74343 305.579 -328.582 -7.22865 290.743 +-329.101 -6.80061 290.229 -328.954 -7.28871 291.687 -328.829 -7.36763 292.669 +-328.673 -7.35311 304.867 -328.899 -7.26242 306.042 -328.931 -7.12179 306.948 +-328.713 -6.79784 307.304 -329.249 -6.78023 307.84 -328.678 -6.42928 289.694 +-328.836 -5.797 289.361 -328.793 -6.25542 307.862 -328.691 -5.64973 308.277 +-329.277 -5.77853 308.751 -328.729 -4.68246 289.224 -328.76 -5.36397 309.005 +-328.937 -5.15526 309.994 -328.48 -4.78854 310.404 -329.199 -4.83928 310.709 +-328.641 -4.11243 288.857 -329.039 -3.55665 288.464 -329.263 -3.93794 288.872 +-328.959 -4.44026 311.072 -328.883 -4.08737 311.734 -328.772 -3.67407 312.21 +-328.773 -2.92602 288.415 -328.735 -3.34113 312.836 -328.794 -3.26473 313.821 +-328.78 -3.28094 314.828 -328.788 -3.29036 315.834 -328.789 -3.29978 316.837 +-329.223 -2.65757 317.919 -328.84 -3.14319 317.717 -328.648 -2.82001 318.362 +-328.801 -2.29981 288.877 -328.794 -1.70523 289.419 -328.996 -2.35827 318.628 +-328.76 -2.30517 319.746 -328.734 -2.27346 320.911 -328.979 -2.20805 321.812 +-328.723 -1.69079 322.371 -329.377 -0.578977 289.885 -328.894 -1.20216 289.774 +-328.672 -0.805292 290.188 -329.296 -0.646607 322.873 -328.814 -1.145 322.851 +-328.714 -0.800888 323.347 -328.552 -1.15511 329.729 -328.946 -0.697533 329.384 +-329.286 -0.798764 330.09 -329.351 -0.833179 331.067 -328.715 -1.23247 330.836 +-329.288 -0.650621 331.974 -328.822 -1.14296 331.679 -328.612 -0.807657 332.173 +-328.74 0.19035 290.417 -328.905 -0.339313 323.734 -328.764 -0.20991 324.839 +-328.757 -0.246345 325.884 -328.918 -0.270827 326.757 -328.727 -0.348265 327.641 +-328.987 -0.408653 328.709 -328.779 -0.0234664 332.47 -329.334 0.277324 332.829 +-329.29 1.33064 290.784 -328.926 0.876954 290.697 -328.799 1.23344 291.174 +-328.853 1.3751 297.248 -328.747 0.837798 297.739 -329.162 1.39573 297.937 +-328.846 0.677508 298.579 -329.219 1.30768 298.99 -328.757 0.697535 299.727 +-329.229 1.32486 300.001 -328.717 0.882755 300.758 -329.166 1.44578 300.952 +-328.679 1.16501 301.374 -329.027 0.59951 333.019 -328.782 0.693309 333.64 +-328.924 0.647086 334.603 -329.399 1.23072 336.082 -328.871 0.729694 335.719 +-329.257 1.22372 336.748 -328.7 0.896301 336.802 -328.818 1.20406 337.352 +-328.936 1.79831 291.695 -329.261 2.3112 292.099 -328.716 1.78274 292.612 +-329.225 2.26953 293.091 -328.757 1.76572 293.634 -329.27 2.24467 294.095 +-328.776 1.76721 294.64 -329.287 2.25341 295.084 -328.769 1.79237 295.635 +-329.289 2.26964 296.071 -328.772 1.73076 296.56 -329.289 2.10802 297.061 +-329.412 2.23502 297.936 -329.389 2.12774 298.798 -329.401 2.13491 299.816 +-329.411 2.13795 300.829 -328.888 1.6114 301.714 -329.307 2.15882 301.902 +-328.823 1.75929 302.593 -329.321 2.25723 303.036 -328.651 1.75589 303.762 +-329.196 2.33095 303.877 -328.673 2.21074 304.378 -329.33 2.48621 337.853 +-328.845 1.68193 337.858 -328.625 2.31533 338.347 -329.04 2.61184 304.639 +-328.792 2.69086 305.739 -328.756 2.74036 306.889 -328.981 2.81138 307.801 +-328.573 3.23958 308.37 -329.275 3.17327 337.864 -328.706 3.19399 338.18 +-329.047 3.75905 308.83 -328.595 4.28749 309.285 -329.457 4.34196 335.733 +-329.264 4.27853 336.558 -329.043 4.14538 337.303 -328.804 3.80984 337.863 +-328.788 4.64504 309.91 -329.001 4.82468 310.701 -328.543 5.33866 311.328 +-329.423 5.33158 331.727 -329.3 5.24156 332.609 -329.279 5.23495 333.558 +-328.735 5.32528 334.315 -328.951 4.89739 334.837 -328.786 4.75879 335.927 +-328.59 4.63624 337.011 -329.044 5.78893 311.831 -328.81 6.26785 312.238 +-329.392 6.19539 328.889 -329.264 6.20693 329.536 -328.705 6.30996 330.313 +-328.897 5.89062 330.849 -328.87 5.73839 331.914 -328.81 5.74019 333.091 +-328.696 5.74808 333.859 -328.886 6.79372 312.782 -328.707 7.35745 313.248 +-329.208 7.22755 313.777 -329.35 7.28198 314.609 -329.336 7.27747 315.587 +-329.327 7.27802 316.571 -329.331 7.28015 317.575 -329.354 7.29027 318.58 +-329.294 7.27985 319.521 -328.925 7.16632 320.218 -328.845 6.85777 320.835 +-329.028 6.83469 321.823 -328.678 7.32452 322.369 -329.14 7.13455 322.723 +-329.304 7.22401 323.588 -329.282 7.22785 324.597 -329.263 7.22709 325.596 +-329.304 7.23496 326.617 -329.27 7.24011 327.576 -328.788 7.22943 328.326 +-328.89 6.71899 329.023 -328.676 6.72377 329.842 -328.661 7.63052 313.962 +-328.724 7.65094 315.033 -328.7 7.65032 316.021 -328.7 7.65255 317.019 +-328.713 7.65613 318.02 -328.712 7.65845 319.025 -328.565 7.5674 319.983 +-328.783 7.66594 323.098 -328.776 7.72299 324.038 -328.756 7.71203 325.029 +-328.778 7.71451 326.039 -328.808 7.72919 327.069 -328.663 7.73477 327.839 +-328.558 10.8568 327.8 -328.9 11.3508 327.266 -329.193 11.321 327.867 +-329.374 11.273 328.905 -328.762 10.7871 328.843 -329.288 11.1659 329.761 +-328.741 10.8231 329.814 -328.828 11.1752 330.363 -328.703 11.8582 325.772 +-328.843 12.3667 325.277 -329.295 12.3864 325.929 -329.3 12.0938 327.068 +-328.788 11.661 326.571 -329.478 12.0107 327.972 -329.351 12.2132 331.122 +-328.889 11.6935 330.842 -329.302 12.3409 331.897 -328.776 11.882 331.685 +-328.752 12.3139 332.218 -328.712 12.7935 316.991 -328.91 13.2063 316.406 +-329.304 13.2455 317.078 -329.182 13.3502 317.894 -328.692 12.9243 317.757 +-328.614 13.242 318.408 -328.799 12.7831 319.872 -328.818 13.2022 319.42 +-329.264 13.2282 320.102 -329.253 13.2177 320.777 -328.817 12.7433 320.552 +-328.851 13.1763 321.244 -328.717 12.8947 323.773 -328.818 13.4101 323.27 +-329.28 13.4165 323.908 -329.293 13.0154 324.945 -328.784 12.6883 324.563 +-328.688 13.0423 332.491 -328.645 13.779 309.638 -329.017 14.2359 309.296 +-328.913 13.7914 310.739 -328.743 14.1705 311.459 -328.634 13.852 313.864 +-328.865 14.3164 313.386 -329.366 14.149 313.962 -328.841 13.7556 314.844 +-328.883 13.6625 315.67 -329.05 13.6061 318.754 -329.282 14.3179 322.13 +-328.969 13.8027 321.736 -329.231 14.069 322.951 -328.718 13.7547 322.551 +-328.883 13.6179 332.92 -328.777 13.7205 333.848 -329.304 14.1846 334.894 +-328.945 13.8421 334.672 -328.623 14.2182 335.334 -328.594 14.8354 305.934 +-329.037 15.2089 305.301 -328.949 14.7397 306.72 -328.732 14.6874 307.628 +-328.832 14.6346 308.606 -328.953 14.6073 311.83 -328.887 14.6656 312.697 +-329.14 15.3436 335.817 -328.987 14.7998 335.746 -328.622 15.1647 336.184 +-328.546 15.7072 304.794 -328.962 16.3117 304.406 -329.035 16.1821 335.398 +-328.532 15.6991 335.89 -328.766 17.2208 304.295 -328.759 17.2353 335.283 +-328.827 18.2844 304.284 -328.775 18.2474 335.334 -328.598 19.1933 304.395 +-328.816 19.2852 335.387 -329.006 19.7628 304.825 -328.798 20.3732 305.219 +-328.888 20.1859 335.212 -328.589 21.162 305.425 -328.863 21.2281 334.383 +-328.738 20.6312 334.757 -328.992 21.731 305.836 -328.725 22.3103 306.27 +-328.767 22.2301 334.304 -328.78 23.2156 306.417 -328.796 23.2452 334.33 +-328.912 24.1544 306.206 -328.796 24.2538 334.356 -328.796 24.641 305.719 +-328.726 25.1692 305.427 -328.871 25.3165 334.279 -328.999 25.7531 305.789 +-328.724 26.3305 306.255 -328.946 26.4342 333.455 -328.539 25.7903 333.83 +-329.25 26.1256 333.906 -328.771 27.2104 306.372 -328.776 27.2122 333.271 +-328.806 28.2359 306.337 -328.831 28.274 333.279 -328.8 29.2497 306.324 +-328.61 29.1817 333.404 -328.796 30.2462 306.329 -328.991 29.7461 333.834 +-328.739 30.3109 334.263 -328.795 31.2442 306.329 -328.776 31.2054 334.369 +-328.789 32.2416 306.328 -328.801 32.2385 334.337 -328.804 33.2411 306.349 +-328.792 33.2496 334.326 -328.916 34.2997 306.254 -328.813 34.2388 334.336 +-328.636 34.8141 305.784 -328.666 35.3143 305.504 -329.193 35.1245 305.853 +-328.851 35.2359 334.344 -328.882 35.6133 305.854 -328.838 36.222 306.257 +-328.692 36.3741 334.241 -328.759 37.206 306.365 -328.891 37.3812 334.344 +-328.798 38.2288 306.338 -328.78 38.2481 334.336 -328.8 39.2482 306.324 +-328.845 39.2815 334.3 -328.799 40.2489 306.327 -328.602 40.1859 334.395 +-328.797 41.2453 306.329 -328.988 40.738 334.837 -328.741 41.3232 335.258 +-328.794 42.2436 306.332 -328.777 42.214 335.36 -328.788 43.2402 306.34 +-328.946 43.3155 317.37 -328.656 42.8141 317.838 -329.395 43.1651 317.933 +-328.82 42.6321 318.85 -328.963 42.8002 319.71 -328.742 43.3101 320.282 +-328.795 43.2344 335.368 -328.802 44.2461 306.33 -328.925 44.3001 315.39 +-328.683 43.881 315.759 -329.351 44.1817 315.972 -328.749 43.6159 316.721 +-328.711 43.6999 320.871 -328.794 43.7725 321.85 -328.786 43.7571 322.857 +-328.795 43.7623 323.871 -328.778 43.7717 324.885 -328.767 43.7748 325.885 +-328.782 43.7646 326.87 -328.776 43.7283 327.866 -328.758 43.668 328.88 +-328.936 43.8203 329.675 -329.372 44.3394 329.961 -328.758 44.3216 330.198 +-328.838 44.2904 335.295 -328.841 45.2796 306.273 -329.03 45.2037 313.349 +-328.645 44.8515 313.869 -328.836 44.596 314.667 -328.694 45.0583 330.452 +-328.801 45.3997 334.356 -328.517 44.8019 334.883 -329.289 45.3703 334.779 +-328.689 46.0739 306.399 -329.036 46.2399 311.305 -328.563 45.8598 311.932 +-328.789 45.6239 312.731 -328.879 45.6473 330.896 -328.804 45.8808 331.751 +-329.307 46.2726 332.07 -328.644 45.895 332.839 -329.262 46.2937 333.054 +-328.681 45.7825 333.739 -329.28 46.0718 333.961 -328.896 46.6165 306.847 +-328.909 46.8142 307.828 -329.456 47.1685 307.93 -328.657 46.7119 308.991 +-328.906 46.8398 309.915 -329.448 47.216 310.016 -328.639 46.6413 310.85 +-327.743 -9.12081 297.018 -328.018 -8.77335 296.366 -327.894 -9.22763 297.835 +-327.873 -9.20897 298.868 -327.805 -9.21259 299.898 -327.991 -9.14365 300.78 +-327.74 -8.59497 301.2 -327.68 -8.13058 293.023 -327.887 -7.68823 292.462 +-328.348 -7.82843 292.885 -327.68 -8.35577 293.872 -327.827 -8.30326 294.772 +-327.719 -8.3524 295.861 -328.34 -7.69821 301.924 -327.946 -7.93513 301.414 +-328.436 -7.71344 302.667 -328.353 -7.59817 303.652 -327.817 -7.14496 290.032 +-327.929 -6.6258 289.462 -328.336 -6.89342 289.887 -327.626 -7.22715 290.969 +-327.81 -7.35318 291.932 -327.736 -7.21883 301.724 -328.118 -6.98588 302.714 +-327.764 -6.5754 302.369 -327.927 -7.12607 304.027 -327.822 -6.89107 303.379 +-327.698 -6.66961 304.365 -328.162 -6.85034 304.735 -328.281 -6.76428 305.586 +-328.33 -6.73542 306.67 -327.826 -5.82407 289.379 -327.72 -6.42505 302.943 +-327.792 -6.33308 305.097 -327.768 -6.27759 306.051 -327.934 -6.16916 307.01 +-327.636 -5.70708 307.249 -328.241 -5.80958 307.701 -327.755 -4.9077 289.43 +-327.836 -5.39434 307.914 -327.68 -5.16033 308.846 -328.05 -5.11622 309.748 +-327.704 -4.72553 310.227 -328.3 -3.4971 288.75 -327.894 -4.0149 289.291 +-327.895 -4.27537 310.886 -327.494 -3.76789 311.396 -328.238 -3.81525 311.704 +-328.297 -2.80952 288.741 -327.801 -2.95397 289.282 -327.985 -3.40856 312.068 +-327.723 -3.13079 312.867 -327.91 -3.15577 313.878 -327.89 -3.17549 314.882 +-327.743 -3.19602 315.901 -327.791 -3.30442 316.826 -327.795 -3.30304 317.823 +-328.346 -2.75187 319.088 -327.841 -3.20655 318.782 -328.161 -2.69943 319.899 +-327.706 -3.14447 319.786 -327.683 -2.86782 320.426 -327.91 -2.23831 289.653 +-328.285 -1.67402 289.917 -327.756 -1.75681 290.212 -327.993 -2.42758 320.707 +-327.847 -2.13191 321.672 -327.717 -1.65304 322.212 -327.833 -0.818341 290.452 +-327.723 -1.32438 322.824 -328.23 -0.674826 323.949 -327.869 -1.12001 323.733 +-327.638 -0.8162 324.399 -327.553 -1.14341 327.722 -327.897 -0.6762 327.395 +-328.238 -0.758496 328.096 -328.446 -0.830908 328.862 -327.853 -1.19476 328.853 +-327.697 -1.30704 329.93 -327.8 -1.33816 330.85 -328.024 -1.23033 331.773 +-327.717 -0.682667 332.345 -328.295 0.388687 291.054 -327.86 -0.25081 290.761 +-327.731 -0.267248 291.571 -328.159 0.333127 292.029 -327.777 -0.25633 292.625 +-328.199 0.343447 293.045 -327.795 -0.244848 293.644 -328.2 0.344771 294.045 +-327.787 -0.247274 294.639 -328.193 0.360019 295.042 -327.786 -0.23562 295.64 +-328.193 0.373988 296.049 -327.782 -0.220789 296.654 -328.169 0.261623 296.985 +-327.77 -0.220648 297.675 -328.312 0.320607 297.956 -327.772 -0.238068 298.686 +-328.331 0.218919 299.097 -327.77 -0.173757 299.621 -328.17 0.322583 299.964 +-327.675 0.163537 300.339 -327.974 -0.375697 324.666 -327.827 -0.314881 325.637 +-327.922 -0.366898 326.73 -328.347 0.314731 332.998 -327.774 -0.173971 332.847 +-328.289 0.255119 333.99 -327.701 -0.156796 333.735 -328.329 0.198672 335.059 +-327.718 -0.217321 334.802 -328.206 0.281848 335.926 -327.851 -0.119304 335.637 +-327.554 0.245077 336.275 -328.397 1.13664 291.858 -328.381 1.13218 292.817 +-328.407 1.13837 293.825 -328.4 1.15017 294.825 -328.394 1.16935 295.819 +-328.439 0.970553 296.871 -327.998 0.600119 300.72 -327.886 0.775109 301.638 +-328.344 1.26046 302.115 -327.735 0.774742 302.673 -328.302 1.25536 303.098 +-327.769 0.802152 303.628 -328.224 1.23817 303.822 -327.843 1.19134 304.277 +-327.844 0.659345 336.813 -328.27 1.18241 337.883 -327.794 0.853501 337.799 +-327.754 1.17336 338.413 -328.011 1.7721 304.738 -328.348 2.29663 305.098 +-327.609 1.79052 305.771 -328.147 2.36931 305.888 -327.628 2.21528 306.407 +-328.149 2.35174 338.779 -328.05 1.74215 338.719 -327.567 2.12578 339.187 +-327.973 2.61546 306.683 -327.94 2.75214 307.698 -327.562 3.13213 308.338 +-328.026 3.0852 338.503 -327.702 2.83106 338.907 -328.054 3.79132 308.804 +-327.56 4.28981 309.281 -328.35 4.4162 337.73 -327.911 4.04393 338.317 +-327.916 4.62706 309.811 -327.936 4.85958 310.656 -327.647 5.44786 311.2 +-328.394 5.21491 334.887 -328.259 5.24049 335.553 -327.896 5.1909 336.216 +-327.738 4.86246 336.861 -327.71 4.69095 337.942 -328.363 6.18025 311.865 +-327.736 6.11529 311.475 -328.346 6.19929 330.881 -328.263 6.21499 331.591 +-328.308 6.24616 332.634 -328.286 6.27307 333.587 -328.05 6.11435 334.29 +-327.749 5.76466 335.06 -327.598 5.56509 335.866 -327.886 6.76386 311.996 +-327.513 7.26866 312.312 -328.233 7.22186 312.642 -328.35 7.35338 320.877 +-328.389 7.29016 321.755 -328.351 7.23741 328.9 -328.224 7.2383 329.555 +-327.773 7.23104 330.324 -327.857 6.72057 330.994 -327.808 6.74653 332.051 +-327.715 6.77904 333.088 -327.617 6.63276 334.014 -328.003 7.60908 313 +-327.725 7.87483 313.808 -327.925 7.85481 314.842 -327.901 7.84066 315.844 +-327.892 7.84337 316.832 -327.899 7.84191 317.823 -327.895 7.84062 318.824 +-327.87 7.8192 319.849 -327.918 7.80463 320.816 -327.799 8.48127 321.311 +-328.194 8.17045 321.815 -328.385 8.19535 322.802 -328.413 8.36822 323.842 +-328.398 8.34803 324.827 -328.403 8.33691 325.834 -328.435 8.34197 326.86 +-328.313 8.34999 327.743 -327.998 8.11892 328.326 -327.858 7.72859 329.015 +-327.634 7.73115 329.835 -328.158 9.06802 321.824 -327.765 9.31406 321.436 +-328.239 9.1762 322.612 -328.212 9.17723 323.627 -328.191 9.15371 324.624 +-328.186 9.14241 325.614 -328.215 9.14312 326.633 -328.176 9.15418 327.585 +-327.629 9.17859 328.325 -327.829 9.74416 322.055 -327.789 9.80159 323.023 +-327.767 9.76958 324.054 -327.756 9.75962 325.056 -327.768 9.76784 326.048 +-327.806 9.79169 327.065 -327.663 9.80207 327.836 -327.682 10.8452 324.811 +-327.849 11.3844 324.31 -328.124 11.3294 324.914 -328.275 11.2999 325.953 +-327.738 10.703 325.632 -328.446 11.1407 326.929 -327.854 10.7185 326.761 +-327.647 10.66 327.849 -327.792 10.732 328.877 -327.798 10.683 329.901 +-328.206 11.2518 331.024 -327.954 10.842 330.684 -327.483 11.1994 331.349 +-327.741 11.9692 316.811 -327.74 12.1819 316.259 -328.224 12.4677 316.762 +-327.62 12.0785 317.319 -327.703 11.8514 319.758 -327.798 12.3941 319.265 +-328.266 12.3745 319.893 -328.28 12.3445 321.071 -327.788 11.6953 320.61 +-328.173 12.3716 322.051 -327.784 11.7276 321.623 -328.171 12.2922 322.97 +-327.776 11.7906 322.642 -328.304 12.2203 323.963 -327.774 11.7388 323.602 +-328.455 12.0083 325.002 -328.012 11.6916 331.739 -327.718 12.3144 332.352 +-327.804 12.7801 315.825 -327.847 13.2659 315.456 -328.263 13.0671 315.988 +-328.001 12.5747 317.658 -328.276 13.0699 319.006 -327.878 12.7425 318.555 +-328.416 13.1577 321.887 -328.416 12.9918 322.868 -328.28 13.3607 332.929 +-327.922 12.8906 332.74 -327.533 13.2573 333.261 -327.544 13.8631 307.86 +-327.858 14.315 307.431 -328.222 14.2548 308.084 -328.394 14.1699 308.889 +-327.826 13.7791 308.859 -327.881 13.7928 310.014 -327.67 14.2219 310.478 +-328.117 14.1547 310.811 -327.814 14.3095 311.165 -327.759 13.8989 311.643 +-328.289 14.2657 312.079 -328.353 14.1853 312.859 -327.77 13.8268 312.926 +-327.796 13.7606 313.846 -327.843 13.6876 314.691 -327.819 13.6519 333.808 +-327.95 13.7808 334.812 -327.482 14.3159 335.437 -327.597 14.8381 305.91 +-328.044 15.2117 305.295 -327.885 14.6549 306.825 -327.812 14.5004 310.807 +-328.038 14.7567 335.96 -327.977 15.3265 336.176 -327.557 15.6881 304.785 +-327.964 16.3085 304.405 -327.865 16.2433 335.415 -327.697 15.636 335.817 +-327.776 17.2204 304.286 -327.755 17.2241 335.308 -327.824 18.2766 304.273 +-327.785 18.2469 335.353 -327.645 19.0506 304.46 -327.877 19.3209 335.277 +-328.074 19.7183 304.816 -327.772 20.2059 305.244 -327.949 20.4368 334.449 +-327.545 19.7847 334.821 -328.254 20.1279 334.905 -327.61 21.1684 305.416 +-327.748 21.2012 334.276 -327.989 21.7467 305.827 -327.726 22.3168 306.283 +-327.771 22.2323 334.314 -327.84 23.2689 306.319 -327.798 23.247 334.339 +-327.566 23.7934 305.831 -328.006 24.4714 305.411 -328.273 24.1201 305.902 +-327.807 24.2592 334.37 -327.56 25.1326 305.344 -327.832 25.1613 334.269 +-327.955 25.7277 305.803 -327.732 26.3143 306.268 -327.995 26.3113 333.397 +-327.592 25.7508 333.834 -327.78 27.2051 306.373 -327.781 27.2206 333.287 +-327.805 28.2376 306.336 -327.846 28.2846 333.291 -327.8 29.2502 306.325 +-327.61 29.1807 333.402 -327.796 30.2461 306.328 -327.99 29.7449 333.832 +-327.739 30.3109 334.263 -327.794 31.2433 306.327 -327.776 31.2057 334.369 +-327.785 32.2414 306.334 -327.801 32.239 334.336 -327.803 33.2515 306.371 +-327.794 33.2454 334.328 -327.866 34.1641 306.261 -327.82 34.2319 334.348 +-327.657 34.7762 305.806 -327.841 35.2413 305.471 -327.794 35.3578 334.253 +-327.985 35.7122 305.782 -327.712 36.291 306.253 -327.796 36.2415 333.566 +-327.672 35.9342 333.835 -328.128 36.0992 333.833 -327.756 37.1798 306.377 +-327.918 36.5529 333.834 -327.792 37.132 334.255 -327.788 38.2113 306.344 +-327.797 38.2664 334.344 -327.794 39.2441 306.326 -327.859 39.2954 334.287 +-327.8 40.2494 306.326 -327.604 40.1861 334.379 -327.796 41.2455 306.33 +-327.988 40.7708 334.819 -327.743 41.3283 335.258 -327.791 42.2441 306.336 +-327.76 42.1987 335.394 -327.797 43.2451 306.332 -327.943 43.3137 316.362 +-327.665 42.8483 316.85 -328.384 43.1801 316.924 -327.704 42.7119 317.941 +-328.054 42.7913 318.886 -327.499 43.3233 319.352 -328.227 43.1815 319.735 +-327.769 43.2089 335.408 -327.824 44.2573 306.293 -327.827 44.3186 314.438 +-327.684 43.91 314.895 -328.35 44.1646 314.928 -327.71 43.5969 315.694 +-327.959 43.6697 320.055 -327.625 43.7404 320.627 -327.894 43.7007 321.635 +-327.87 43.6822 322.656 -327.857 43.6832 323.655 -327.852 43.6882 324.66 +-327.847 43.6896 325.661 -327.872 43.6877 326.646 -327.783 43.693 327.752 +-327.74 43.7262 328.879 -327.976 43.7861 329.78 -327.721 44.3122 330.312 +-327.818 44.1125 335.239 -327.741 45.2764 306.337 -328.029 45.2511 313.34 +-327.635 44.7778 313.812 -328.268 45.3692 330.891 -327.78 44.8745 330.82 +-327.648 45.2014 331.348 -327.984 45.1836 334.372 -327.857 44.6657 334.892 +-328.263 46.3408 306.888 -327.783 45.8796 306.812 -327.686 46.1752 307.379 +-327.963 46.1855 311.336 -327.84 45.7624 311.922 -327.851 45.5871 312.592 +-327.986 45.6225 331.626 -327.855 45.6701 332.628 -327.887 45.6265 333.592 +-327.885 46.6248 307.771 -327.756 46.7791 308.869 -327.899 46.7879 309.76 +-327.711 46.6855 310.581 -327.354 -8.71934 296.748 -327.244 -8.81307 297.604 +-327.26 -8.81627 298.624 -327.225 -8.71624 299.782 -327.395 -8.82545 300.725 +-327.299 -7.71326 292.647 -326.851 -7.90008 293.224 -326.851 -8.20978 293.84 +-326.862 -8.25128 294.792 -326.841 -8.1956 295.811 -326.813 -8.29183 296.909 +-326.742 -8.30661 298.03 -326.748 -8.30635 299.037 -326.802 -8.15581 299.892 +-327.253 -7.98495 300.825 -326.659 -7.65344 300.397 -327.423 -6.66599 289.749 +-327.224 -6.70765 290.505 -326.903 -6.91567 291.243 -326.858 -7.15478 291.875 +-326.764 -7.41831 292.763 -326.93 -7.22258 301.047 -326.771 -6.51453 301.25 +-327.32 -6.657 301.785 -327.437 -6.49363 303.705 -327.251 -5.69563 289.87 +-326.69 -6.40093 290.931 -326.83 -5.86402 290.416 -327.147 -5.86123 301.897 +-326.708 -5.71237 301.451 -327.397 -5.88761 302.851 -327.285 -5.62958 303.989 +-327.384 -5.95036 304.666 -327.297 -5.63435 305.601 -327.317 -5.79109 306.552 +-327.265 -4.51758 289.774 -326.753 -4.97282 290.249 -326.864 -5.21802 301.976 +-327.252 -4.83148 302.807 -326.793 -4.50078 302.353 -327.352 -4.73958 303.889 +-327.229 -4.83284 304.885 -326.777 -5.232 306.07 -326.742 -4.69739 305.423 +-326.905 -5.28756 307.043 -327.006 -5.09882 307.99 -326.644 -4.69827 308.246 +-327.166 -4.83152 308.782 -327.347 -4.78366 309.588 -327.249 -3.52091 289.925 +-326.742 -3.99649 290.318 -327.251 -3.7942 302.765 -326.864 -3.72159 302.28 +-327.271 -3.70098 303.613 -327.193 -3.82638 304.617 -326.67 -3.73602 305.207 +-326.671 -4.43823 308.884 -326.804 -4.28629 309.951 -326.944 -4.0858 310.836 +-326.825 -3.7857 311.253 -327.372 -2.63863 290.017 -326.827 -3.37305 290.722 +-327.177 -2.78261 290.88 -326.779 -3.25124 291.604 -327.215 -2.67139 292.025 +-326.802 -3.25916 292.637 -327.23 -2.67669 293.024 -326.808 -3.25675 293.644 +-327.224 -2.6823 294.015 -326.804 -3.25737 294.639 -327.222 -2.68388 295.013 +-326.802 -3.26014 295.637 -327.223 -2.68263 296.012 -326.801 -3.2592 296.636 +-327.222 -2.6806 297.013 -326.799 -3.26157 297.639 -327.226 -2.69617 298.018 +-326.807 -3.28382 298.641 -327.242 -2.70437 299.02 -326.808 -3.20492 299.579 +-327.175 -2.54438 299.967 -326.718 -2.83535 300.291 -326.859 -2.57532 301.256 +-326.697 -3.16155 301.755 -327.226 -2.7355 301.983 -327.246 -2.76782 302.904 +-326.814 -3.19797 304.098 -326.861 -2.82435 303.391 -326.835 -3.31691 304.787 +-326.904 -3.2599 311.795 -326.705 -2.67776 312.227 -327.224 -2.79552 312.77 +-327.342 -2.744 313.629 -327.322 -2.75336 314.643 -327.265 -2.74979 315.578 +-326.952 -2.8152 316.271 -326.744 -3.14747 316.927 -326.785 -3.30044 317.834 +-326.783 -3.28557 318.866 -326.8 -3.30058 319.888 -327.298 -2.60774 321.032 +-326.987 -3.14112 320.658 -326.595 -2.75666 321.144 -327.309 -1.69028 290.795 +-327.274 -1.76288 291.804 -327.304 -1.75135 292.807 -327.3 -1.75759 293.793 +-327.297 -1.76002 294.791 -327.298 -1.75927 295.79 -327.298 -1.75732 296.79 +-327.299 -1.76991 297.787 -327.314 -1.79283 298.778 -327.327 -1.78269 299.765 +-326.868 -2.39941 300.686 -327.246 -1.87848 300.889 -327.351 -1.64452 301.886 +-327.253 -1.71677 302.816 -326.776 -2.03765 303.291 -326.638 -2.39627 312.925 +-326.713 -2.36286 314.03 -326.731 -2.34916 315.043 -326.626 -2.38822 315.988 +-326.717 -1.9978 321.45 -327.259 -1.69943 321.807 -327.399 -0.756767 290.989 +-327.34 -0.858649 291.836 -327.379 -0.837537 292.87 -327.38 -0.833912 293.867 +-327.375 -0.837213 294.862 -327.375 -0.833326 295.864 -327.371 -0.826761 296.87 +-327.364 -0.833394 297.873 -327.371 -0.848768 298.855 -327.347 -0.677637 299.886 +-327.249 -0.719641 300.808 -327.276 -0.742989 301.795 -327.314 -0.765436 302.801 +-326.852 -1.32135 303.649 -327.175 -0.656512 303.878 -326.709 -0.890138 304.25 +-326.993 -1.39442 321.997 -326.781 -1.31033 322.654 -326.791 -1.31623 323.769 +-327.291 -0.734082 325.102 -326.808 -1.14649 324.813 -327.238 -0.738772 326.035 +-326.642 -1.13308 325.83 -327.409 -0.825947 326.859 -326.811 -1.17707 326.884 +-326.714 -1.27375 327.94 -326.843 -1.27617 328.802 -326.808 -1.28445 329.826 +-326.765 -1.30913 330.88 -326.994 -1.20784 331.8 -326.592 -0.764536 332.369 +-327.375 0.199319 300.949 -327.315 0.189066 301.872 -327.354 0.14538 302.868 +-327.377 0.293398 303.932 -326.863 -0.317993 304.711 -327.16 0.367261 304.934 +-326.716 0.129674 305.286 -326.85 -0.348933 332.906 -326.838 -0.255994 333.774 +-326.82 -0.306384 334.81 -326.917 -0.212584 335.824 -326.73 0.190891 336.435 +-327.453 1.15787 304.944 -326.844 0.719414 305.755 -327.217 1.15719 305.7 +-326.812 1.22417 306.27 -326.989 0.589238 336.814 -326.758 0.652325 337.774 +-327.348 1.32063 338.999 -326.901 0.862687 338.706 -326.744 1.31383 339.243 +-326.956 1.75745 306.75 -327.293 2.25121 307.147 -326.749 1.7704 307.584 +-327.258 2.373 307.868 -326.86 2.29804 308.228 -326.687 2.05094 339.488 +-326.8 3.11278 308.502 -327.348 3.58784 338.873 -327 3.03742 339.388 +-326.811 3.72372 308.913 -326.678 4.21071 309.422 -327.275 4.34699 338.789 +-326.721 4.02823 339.232 -327.06 4.71292 309.967 -326.526 5.15968 310.371 +-327.247 5.18557 310.736 -327.038 5.24438 336.352 -326.575 4.84947 336.956 +-326.958 4.78473 337.752 -326.712 4.62587 338.643 -326.904 5.65891 310.918 +-326.72 6.18985 311.323 -327.37 6.37357 334.726 -326.843 6.23717 335.24 +-326.675 5.77396 335.768 -327.007 6.76651 311.78 -326.56 7.25962 312.292 +-327.332 7.2335 330.868 -327.292 7.24366 331.586 -327.298 7.27093 332.569 +-326.932 7.21014 333.213 -326.744 6.87107 333.859 -326.748 6.63836 334.759 +-327.059 7.762 312.92 -326.545 8.26776 313.329 -327.191 8.20732 313.729 +-327.313 8.25649 314.615 -327.283 8.22087 315.644 -327.267 8.20198 316.636 +-327.268 8.2029 317.609 -327.257 8.19876 318.603 -327.241 8.26991 319.759 +-327.447 8.16874 320.678 -327.462 8.50516 328.937 -327.282 8.34735 329.731 +-326.991 8.13 330.337 -326.867 7.73114 331.004 -326.686 7.74736 332.083 +-326.648 7.56707 332.86 -326.83 8.57536 313.999 -326.717 8.74731 314.926 +-326.776 8.70198 316.08 -326.762 8.68983 317.054 -326.745 8.7056 318.034 +-326.724 8.69881 319.051 -326.799 8.80891 319.929 -326.734 9.48518 320.335 +-327.261 9.06219 320.813 -327.31 9.15911 328.9 -327.146 9.14446 329.547 +-326.609 9.19414 330.323 -327.166 10.3306 320.865 -326.822 10.3238 320.277 +-327.356 10.2221 321.761 -327.334 10.4082 322.801 -327.343 10.2677 323.674 +-327.404 10.3254 324.747 -327.44 10.2354 325.819 -327.461 10.2731 326.821 +-327.297 10.3131 327.767 -326.945 10.1497 328.364 -326.849 9.78591 329.001 +-326.633 9.79718 329.817 -326.584 10.7838 315.965 -326.801 11.3552 315.412 +-327.181 11.4419 315.954 -326.665 11.2046 316.346 -326.807 10.8215 317.919 +-326.725 11.1266 317.38 -327.17 11.4766 317.899 -327.17 11.2764 318.986 +-326.801 10.7579 318.657 -327.288 11.204 319.986 -326.775 10.7192 319.641 +-327.414 11.2277 320.989 -327.368 11.1554 321.866 -327.335 11.1883 322.872 +-327.355 10.976 323.915 -326.918 10.6404 328.674 -326.687 10.6731 329.775 +-326.962 10.8285 330.794 -326.543 11.2429 331.365 -327.361 12.2945 315.635 +-326.858 12.3182 315.194 -327.109 11.6233 316.789 -327.443 12.2763 318.036 +-327.403 11.9891 318.881 -327.005 11.7224 331.869 -326.569 12.2287 332.338 +-326.759 12.8091 314.832 -326.818 13.2887 314.45 -327.21 13.0906 315.017 +-327.04 12.7249 332.825 -326.572 13.2294 333.332 -326.585 13.7803 307.884 +-327.015 14.2221 307.274 -326.85 13.656 308.769 -326.819 13.6808 309.791 +-327.493 14.2365 310.935 -326.895 13.8134 310.851 -326.613 13.7365 312.013 +-326.897 13.7463 312.749 -326.817 13.693 313.691 -326.894 13.5921 333.86 +-326.887 13.823 334.721 -326.764 14.3781 335.204 -326.631 14.8274 305.895 +-327.068 15.1925 305.279 -326.8 14.6271 306.684 -327.438 15.0992 335.858 +-326.826 15.3704 335.358 -326.572 15.6568 304.783 -326.967 16.2807 304.395 +-326.872 16.3469 335.322 -326.768 17.2047 304.267 -326.747 17.2021 335.341 +-326.688 18.2471 304.371 -326.802 18.2446 335.37 -327.354 19.3628 304.908 +-326.851 18.8528 304.769 -326.731 19.3163 305.21 -326.849 19.1561 335.252 +-326.844 20.2509 305.346 -326.994 20.2979 334.385 -326.612 19.7299 334.821 +-326.616 21.1716 305.404 -326.74 21.1961 334.285 -326.986 21.7468 305.827 +-326.739 22.3251 306.3 -326.773 22.2354 334.32 -326.802 23.1199 306.314 +-326.794 23.2517 334.35 -326.622 23.7546 305.833 -327.058 24.361 305.349 +-326.815 24.2757 334.385 -326.566 25.1487 305.359 -326.89 25.1781 334.211 +-326.959 25.7316 305.819 -326.744 26.3142 306.271 -326.859 26.2163 333.386 +-326.739 25.6286 333.757 -326.779 27.2059 306.371 -326.785 27.2355 333.302 +-326.804 28.2383 306.336 -326.856 28.2881 333.297 -326.8 29.2504 306.325 +-326.609 29.1792 333.4 -326.797 30.2459 306.327 -326.988 29.7442 333.831 +-326.739 30.3108 334.263 -326.793 31.2417 306.325 -326.776 31.2059 334.369 +-326.78 32.2459 306.346 -326.801 32.2387 334.335 -326.8 33.2721 306.389 +-326.802 33.2459 334.329 -326.898 34.1738 306.21 -326.82 34.2464 334.347 +-326.783 34.6391 305.743 -326.698 35.3015 305.424 -326.682 35.1218 334.359 +-327.025 35.7604 305.802 -326.676 36.4767 306.23 -327.463 36.388 333.836 +-326.903 36.1285 334.255 -326.886 37.3112 306.376 -326.762 37.2581 334.361 +-326.772 38.2123 306.352 -326.821 38.2591 334.326 -326.795 39.244 306.332 +-326.852 39.2723 334.275 -326.8 40.2494 306.327 -326.638 40.0204 334.454 +-326.794 41.2458 306.333 -327.038 40.685 334.826 -326.684 41.1435 335.308 +-326.801 42.2456 306.327 -326.766 42.2962 335.315 -326.818 43.2337 306.302 +-326.882 43.2828 315.367 -326.642 42.8441 315.869 -327.366 43.1611 315.931 +-326.828 42.7396 316.852 -326.785 42.6485 317.607 -326.808 42.7343 318.784 +-326.671 43.2808 319.401 -326.833 43.2411 320.365 -326.81 42.8041 320.846 +-327.235 43.2781 321.068 -326.813 42.7109 321.578 -327.281 43.2714 322.063 +-326.796 42.7365 322.602 -327.257 43.2803 323.078 -326.785 42.7519 323.603 +-327.255 43.281 324.076 -326.784 42.7509 324.602 -327.253 43.2806 325.076 +-326.789 42.7509 325.593 -327.271 43.2792 326.064 -326.829 42.7443 326.584 +-327.317 43.2804 327.048 -326.706 42.738 327.732 -327.214 43.3503 327.877 +-326.734 43.2107 328.363 -326.965 43.3794 335.242 -326.779 44.1799 306.365 +-326.931 44.1829 314.314 -326.916 43.6698 314.922 -327.041 43.6485 319.808 +-326.892 43.6358 328.774 -326.85 43.8232 329.849 -326.715 44.166 330.444 +-326.782 44.2931 334.367 -326.75 43.744 334.891 -327.255 44.3571 334.801 +-326.82 45.0718 306.449 -326.873 45.3243 311.259 -326.607 44.8379 311.692 +-327.277 45.4027 311.93 -326.763 44.7487 312.718 -327.375 45.2004 312.951 +-326.796 44.6045 313.633 -326.857 44.5806 330.892 -326.814 44.8533 331.758 +-327.299 45.2591 332.124 -326.754 44.9146 332.719 -327.261 45.295 333.099 +-326.642 44.7716 333.567 -327.28 45.091 333.988 -326.874 45.6163 306.871 +-326.98 45.9159 307.602 -327.221 46.3207 307.998 -326.491 46.3138 308.299 +-326.82 46.2547 310.448 -326.797 45.7386 310.816 -327.217 46.0563 310.998 +-326.918 46.7128 308.705 -326.78 46.7337 309.693 -326.354 -7.56756 293.773 +-326.329 -7.75276 294.824 -326.326 -7.72499 295.83 -326.332 -7.69995 296.818 +-326.349 -7.69097 297.798 -326.35 -7.6866 298.799 -326.392 -7.68319 299.753 +-326.383 -6.55955 291.74 -326.323 -6.83183 292.901 -326.256 -6.69339 293.941 +-326.258 -6.82631 294.82 -326.259 -6.81261 295.833 -326.261 -6.79592 296.831 +-326.269 -6.7941 297.816 -326.263 -6.78097 298.815 -326.234 -6.77567 299.837 +-326.391 -6.95317 300.614 -326.379 -5.65842 290.947 -326.238 -5.69359 291.897 +-326.252 -5.81297 292.79 -326.271 -5.78553 293.801 -326.287 -5.7708 294.806 +-326.29 -5.75743 295.82 -326.29 -5.74648 296.826 -326.293 -5.7579 297.822 +-326.296 -5.76124 298.823 -326.266 -5.73333 299.86 -326.335 -5.84479 300.859 +-326.326 -4.51797 290.876 -326.221 -4.77089 291.791 -326.245 -4.75731 292.83 +-326.26 -4.72188 293.844 -326.265 -4.71043 294.845 -326.266 -4.70787 295.849 +-326.264 -4.70515 296.853 -326.266 -4.72192 297.85 -326.275 -4.73042 298.842 +-326.263 -4.70115 299.853 -326.232 -4.69605 300.892 -326.413 -4.90829 301.664 +-326.257 -4.65522 305.731 -326.239 -4.79885 306.527 -326.303 -4.78372 307.506 +-326.431 -3.71402 291.009 -326.34 -3.8405 291.841 -326.362 -3.84493 292.876 +-326.373 -3.81848 293.894 -326.37 -3.81467 294.894 -326.367 -3.81809 295.891 +-326.365 -3.81861 296.89 -326.365 -3.83099 297.891 -326.374 -3.8461 298.879 +-326.347 -3.64989 299.922 -326.219 -3.79698 300.767 -326.367 -3.85569 301.734 +-325.71 -4.24933 306.006 -325.965 -3.99375 305.48 -325.722 -4.31185 306.996 +-325.891 -4.28461 308.04 -325.979 -4.11612 308.947 -325.689 -3.65906 309.341 +-326.108 -3.86278 309.704 -326.336 -3.74964 310.671 -326.428 -2.95559 300.979 +-326.401 -2.50909 303.848 -326.375 -2.8071 304.941 -325.921 -3.26457 305.658 +-326.106 -2.54429 305.863 -325.691 -2.82815 306.227 -325.746 -3.35403 310.071 +-325.903 -3.18074 310.965 -325.673 -2.65185 311.357 -326.203 -2.84165 311.646 +-326.384 -2.67311 316.691 -325.924 -2.82669 317.274 -325.721 -3.1398 317.96 +-325.769 -3.26691 318.867 -325.904 -3.25927 319.766 -325.706 -3.30381 320.811 +-325.812 -2.67823 321.405 -326.392 -1.60695 303.979 -326.256 -1.78648 304.758 +-326.222 -1.73679 305.71 -325.821 -1.93286 306.372 -325.925 -2.31675 312.065 +-325.763 -2.14716 312.909 -325.877 -2.17485 313.801 -325.925 -2.17165 314.822 +-325.813 -2.16479 315.87 -325.729 -2.37037 316.885 -326.315 -1.67772 322.013 +-325.875 -2.21229 321.733 -326.288 -1.71346 323.034 -325.769 -2.24923 322.546 +-326.154 -1.65655 323.953 -325.805 -2.21407 323.559 -325.714 -1.87118 324.299 +-326.402 -0.718483 304.899 -326.218 -0.681357 305.831 -325.845 -0.939133 306.378 +-325.974 -1.37601 324.727 -325.849 -1.32726 325.646 -325.895 -1.34468 326.621 +-325.822 -1.32522 327.742 -325.792 -1.22599 328.84 -325.763 -1.24065 329.856 +-325.878 -1.27155 330.757 -325.909 -1.13419 331.706 -325.761 -0.665677 332.2 +-326.413 0.311616 305.93 -325.68 -0.287512 306.828 -326.176 0.343268 306.848 +-325.695 0.20874 307.365 -325.75 -0.310553 332.805 -325.834 -0.234549 333.81 +-325.797 -0.266076 334.829 -325.77 -0.302415 335.844 -326.336 0.253382 337.089 +-325.824 -0.208298 336.784 -326.153 0.284819 337.929 -325.802 -0.107034 337.646 +-325.522 0.178045 338.326 -326.422 1.12039 306.901 -325.772 0.611701 307.896 +-326.323 1.24355 307.977 -325.845 0.893825 308.696 -326.157 1.40349 308.9 +-325.624 1.21535 309.211 -326.023 0.702703 338.749 -325.696 1.27538 339.37 +-326.323 2.35609 308.742 -325.677 1.99978 309.382 -326.303 2.52175 339.924 +-325.875 1.81626 339.821 -325.593 2.20347 340.235 -326.358 3.24511 308.79 +-325.866 3.08931 309.337 -326.32 3.40862 339.754 -325.795 3.00435 340.233 +-325.974 3.82828 309.651 -326.334 4.41438 309.873 -325.689 4.22993 310.175 +-325.926 4.35354 338.43 -325.776 3.9181 338.852 -326.309 4.3007 338.922 +-325.642 3.59801 339.671 -325.652 5.08433 310.468 -325.987 5.23895 336.327 +-325.614 4.87696 336.817 -325.812 4.63599 337.662 -326.103 5.77395 310.813 +-325.573 6.17315 311.312 -325.991 6.30809 335.384 -325.518 5.79181 335.844 +-326.007 6.79053 311.815 -325.571 7.27452 312.316 -326.058 7.25111 333.346 +-325.58 6.86496 333.962 -325.805 6.68448 334.761 -326.03 7.75426 312.798 +-325.585 8.26148 313.279 -326.472 8.45261 330.823 -326.258 8.37064 331.558 +-325.779 8.29556 332.251 -325.728 7.75682 332.749 -326.059 8.76454 313.915 +-325.54 9.25206 314.35 -326.216 9.29988 314.857 -326.44 9.21904 315.69 +-326.255 9.39391 316.709 -326.348 9.32886 317.828 -326.317 9.34886 318.846 +-326.407 9.21461 319.659 -326.213 9.06049 330.949 -325.683 8.80041 331.933 +-325.864 9.11247 331.401 -325.842 9.74485 314.971 -325.756 10.2905 315.195 +-326.298 10.1388 315.642 -325.727 9.88056 317.005 -325.887 10.1381 316.448 +-325.722 10.3677 317.164 -326.274 10.2573 317.715 -326.346 10.1426 318.902 +-326.381 9.97696 319.805 -326.423 10.3818 329.019 -326.241 10.3052 329.761 +-325.96 10.1359 330.362 -325.824 9.67773 330.866 -325.582 10.7591 314.848 +-325.843 11.4059 314.52 -326.252 11.0498 315.003 -326.297 11.0548 316.983 +-325.929 10.7007 316.651 -326.001 10.7239 330.694 -325.557 11.1706 331.346 +-326.383 12.2243 314.693 -325.745 12.1862 314.198 -326.019 11.749 331.821 +-325.566 12.237 332.32 -325.784 12.8031 313.824 -325.844 13.2709 313.447 +-326.215 13.108 313.995 -326.026 12.7268 332.826 -325.569 13.3882 333.325 +-325.602 13.8562 308.046 -326.06 14.2278 307.36 -325.961 13.6504 308.68 +-325.712 13.7899 309.802 -325.762 13.7247 310.844 -325.868 13.7532 311.732 +-325.802 13.6739 312.698 -326.067 13.7653 333.992 -325.677 14.5228 334.275 +-326.287 14.1866 334.75 -325.694 14.8843 306.016 -325.892 15.321 305.379 +-325.771 14.6538 306.856 -326.249 14.9429 334.842 -325.613 15.2501 334.507 +-325.731 15.7936 304.866 -325.902 16.3454 304.412 -325.848 15.6023 334.885 +-325.825 16.2068 335.243 -325.879 17.3447 304.242 -325.742 17.196 335.368 +-325.505 18.1379 304.368 -325.807 18.2471 335.382 -325.975 18.7289 304.839 +-325.784 19.3427 305.266 -325.858 19.1726 335.259 -325.852 20.2757 305.328 +-325.993 20.3053 334.396 -325.614 19.75 334.826 -325.61 21.1758 305.395 +-325.737 21.2033 334.29 -325.979 21.7495 305.836 -325.752 22.3437 306.315 +-325.769 22.2316 334.32 -325.866 23.1414 306.25 -325.797 23.2555 334.359 +-325.758 23.6269 305.76 -325.92 24.2613 305.342 -325.873 24.3176 334.281 +-325.572 25.1645 305.373 -325.949 25.4379 333.451 -325.543 24.7923 333.828 +-326.254 25.1275 333.904 -325.98 25.7462 305.83 -325.745 26.3152 306.269 +-325.747 26.2001 333.283 -325.777 27.2069 306.369 -325.793 27.2409 333.312 +-325.805 28.2402 306.336 -325.859 28.2873 333.298 -325.801 29.252 306.324 +-325.607 29.1785 333.399 -325.797 30.2459 306.326 -325.988 29.744 333.83 +-325.739 30.3115 334.263 -325.791 31.2392 306.326 -325.776 31.207 334.368 +-325.786 32.2508 306.355 -325.802 32.24 334.334 -325.86 33.3123 306.284 +-325.807 33.2555 334.325 -325.546 33.7911 305.833 -325.996 34.4562 305.434 +-326.263 34.1207 305.906 -325.808 34.2639 334.34 -325.692 35.1274 305.227 +-325.794 35.2656 334.349 -326.332 36.1234 305.743 -325.659 36.1151 305.411 +-325.778 36.2606 334.351 -325.82 36.6329 305.908 -325.83 37.2135 306.278 +-325.786 37.2452 334.342 -325.765 38.2164 306.362 -325.831 38.2444 334.309 +-325.802 39.2405 306.337 -325.7 39.3758 334.378 -325.798 40.2482 306.329 +-326.306 40.3198 334.926 -325.894 39.8966 334.774 -325.781 40.465 335.178 +-325.801 41.2458 306.326 -325.983 41.3815 335.234 -325.812 42.2415 306.303 +-325.944 42.324 317.43 -325.597 41.9143 317.674 -326.296 42.2493 318.134 +-325.593 41.7336 318.742 -326.266 42.2513 318.765 -325.774 42.2566 319.344 +-325.785 42.3631 320.276 -325.522 41.8768 320.69 -326.288 42.3791 320.895 +-325.674 41.7105 321.614 -326.327 42.208 322.058 -325.835 41.7285 322.624 +-326.307 42.2374 323.059 -325.803 41.7493 323.603 -326.293 42.2434 324.054 +-325.797 41.744 324.59 -326.297 42.2414 325.045 -325.819 41.7306 325.578 +-326.32 42.2355 326.035 -325.73 41.7308 326.728 -326.346 42.2241 327.054 +-325.702 41.8917 327.817 -326.27 42.2115 327.78 -325.834 42.2237 328.384 +-325.706 42.3068 334.504 -325.667 41.6929 334.783 -326.174 42.1703 334.934 +-325.709 43.2021 306.411 -325.883 43.345 314.269 -325.719 42.8502 314.774 +-326.354 43.3724 314.777 -325.66 42.6261 315.649 -326 42.6066 316.709 +-325.942 42.725 319.655 -326.294 43.0785 319.942 -325.853 42.7558 328.84 +-326.267 43.3438 328.935 -325.684 43.2093 329.38 -326.381 43.2009 334.746 +-325.762 43.2065 334.226 -326.269 44.4942 306.795 -325.824 43.9065 306.674 +-325.82 44.2629 307.077 -325.885 44.3016 312.46 -325.608 43.9443 312.825 +-326.278 44.2676 313.102 -325.684 43.6855 313.548 -326.296 44.0509 313.949 +-325.867 43.6244 329.777 -325.797 43.896 330.735 -326.2 44.3258 330.94 +-325.599 44.2201 331.405 -325.844 44.2659 333.454 -325.813 43.806 333.837 +-326.262 44.0857 334.009 -326.308 45.2759 306.921 -325.714 44.9583 307.341 +-325.875 45.3163 310.259 -325.656 44.8112 310.915 -326.417 45.3366 310.787 +-325.812 44.5962 311.968 -325.97 44.62 331.655 -325.94 44.665 332.7 +-325.848 45.7172 307.828 -325.661 46.2736 308.45 -325.787 46.2627 309.412 +-325.765 45.7385 309.789 -326.208 46.0563 309.984 -326.009 46.6443 308.885 +-325.431 -3.57474 306.012 -325.33 -3.69989 306.799 -325.358 -3.69564 307.827 +-325.427 -3.67946 308.752 -324.577 -4.11471 320.736 -324.981 -3.71933 320.418 +-325.195 -3.79235 320.878 -324.819 -3.77753 321.367 -325.364 -2.76553 306.736 +-325.168 -2.73817 307.588 -325.161 -2.90791 308.615 -325.273 -2.83188 309.711 +-325.425 -2.66904 310.771 -325.346 -2.68234 317.741 -324.824 -2.81653 318.261 +-324.744 -3.14834 318.764 -324.817 -3.38358 319.677 -325.296 -2.75082 322.058 +-324.979 -3.24391 321.721 -325.276 -2.78698 323.037 -324.744 -3.25232 322.591 +-325.298 -2.63564 324.077 -324.783 -3.28544 323.594 -325.114 -2.52219 325.004 +-324.793 -3.23429 324.538 -324.708 -2.88476 325.262 -325.285 -1.6054 306.926 +-324.755 -2.2508 308.072 -324.814 -1.84615 307.413 -324.757 -2.30723 309.055 +-324.844 -2.16123 309.933 -325.221 -1.80519 310.812 -324.797 -1.5047 310.291 +-325.383 -1.79362 311.791 -325.383 -1.64424 312.831 -325.369 -1.66923 313.852 +-325.411 -1.80545 314.742 -325.298 -1.66311 315.582 -324.76 -1.69932 316.348 +-324.792 -2.11103 316.947 -324.726 -2.39779 317.771 -325.423 -1.83262 324.92 +-324.835 -2.33919 325.775 -325.268 -1.80059 325.948 -325.308 -1.70674 327.083 +-324.786 -2.18882 326.64 -325.183 -1.64336 327.912 -324.633 -2.20675 327.789 +-324.649 -1.77831 328.408 -325.38 -0.733743 306.803 -324.838 -0.924787 307.271 +-325.153 -0.93319 310.822 -324.751 -0.677101 310.444 -325.228 -0.823869 311.612 +-325.192 -0.829223 312.628 -325.194 -0.852368 313.658 -325.191 -0.962536 314.756 +-324.728 -1.23239 315.925 -324.722 -0.716966 315.465 -324.996 -1.36939 328.662 +-324.854 -1.3214 329.628 -324.806 -1.27659 330.928 -324.682 -0.793439 331.484 +-325.237 -0.801908 331.69 -324.977 -0.181941 307.629 -325.207 0.321468 307.966 +-324.631 0.10684 308.257 -324.835 -0.260733 311.082 -324.809 -0.212981 312.053 +-324.807 -0.235715 313.084 -324.85 -0.245865 314.098 -324.748 -0.250822 314.874 +-324.927 -0.395975 332.06 -324.673 -0.210295 332.932 -324.797 -0.242137 333.836 +-324.78 -0.277544 334.851 -324.734 -0.307331 335.848 -324.718 -0.335338 336.86 +-324.985 -0.232022 337.761 -324.529 0.167884 338.389 -325.048 0.685706 308.724 +-324.884 1.24992 309.372 -325.002 0.67885 338.874 -324.483 1.32639 339.377 +-325.236 2.51806 309.862 -324.929 1.81457 309.789 -324.552 2.11098 310.218 +-325.019 1.71956 339.893 -324.994 2.4566 340.209 -325.407 3.32503 309.923 +-324.765 3.13357 310.224 -324.727 3.27624 339.482 -324.62 2.71257 339.825 +-325.268 3.17869 339.985 -324.733 4.15979 310.446 -324.809 4.17289 337.386 +-324.766 3.81313 338.059 -325.338 4.1824 337.907 -324.78 3.70919 338.94 +-325.394 5.4458 310.873 -324.956 4.84166 310.753 -324.722 5.23505 311.172 +-324.928 5.15909 336.318 -324.942 4.68003 336.933 -324.674 6.07502 311.466 +-324.954 6.21985 335.361 -324.814 5.71494 335.837 -325.09 6.74877 311.8 +-324.575 7.13246 312.286 -325.014 7.24604 333.325 -324.64 6.89777 333.83 +-324.728 6.65608 334.553 -325.025 7.7761 312.803 -324.566 8.26881 313.297 +-324.939 8.26019 332.47 -324.554 7.76419 332.801 -325.028 8.75815 313.774 +-324.578 9.26523 314.307 -325.354 9.56594 331.719 -324.857 9.01909 332.291 +-325.047 9.75469 314.854 -324.83 10.2216 315.154 -325.477 10.2035 316.861 +-325.427 10.3393 331.099 -325.173 10.214 331.778 -324.722 10.0089 332.236 +-324.6 10.6798 314.828 -325.066 11.3212 314.307 -324.752 10.7463 331.706 +-324.694 11.3298 331.464 -324.536 11.7255 313.8 -324.847 12.3331 313.293 +-325.319 12.3253 313.716 -325.027 11.7479 331.784 -324.557 12.2333 332.341 +-324.542 12.7997 312.78 -324.846 13.3634 312.444 -325.259 13.0581 312.976 +-325.028 12.7318 332.843 -324.673 13.2536 333.233 -325.332 14.3174 307.705 +-324.798 14.1573 308.218 -324.919 13.858 308.888 -324.558 14.2422 309.389 +-325.181 14.2056 309.722 -324.939 14.2255 310.22 -324.686 13.8403 310.85 +-324.84 13.6774 311.842 -325.392 14.1245 333.749 -324.676 14.0854 333.491 +-325.343 15.2182 305.801 -324.906 15.1618 306.165 -324.716 14.8316 306.806 +-324.697 14.5861 307.724 -324.691 14.5382 309.905 -324.915 14.6385 333.883 +-324.703 15.2977 334.32 -325.204 16.3073 304.734 -324.843 16.172 305.228 +-324.675 15.6477 305.758 -325.046 15.7887 334.82 -324.671 16.4826 335.212 +-324.62 16.7857 304.879 -325.022 17.1663 304.4 -324.874 17.3286 335.38 +-324.708 18.0648 304.46 -324.795 18.2416 335.39 -324.996 18.7035 304.819 +-324.733 19.2148 305.263 -324.85 19.1654 335.268 -324.865 20.2913 305.308 +-324.993 20.3159 334.396 -324.61 19.7554 334.831 -324.607 21.1703 305.396 +-324.744 21.2067 334.287 -324.985 21.7539 305.846 -324.815 22.3664 306.223 +-324.77 22.2329 334.325 -324.51 22.7443 305.865 -324.982 23.4416 305.445 +-325.241 23.0966 305.934 -324.807 23.2596 334.373 -324.817 24.2487 305.231 +-324.833 24.1619 334.271 -324.581 25.1706 305.381 -324.999 25.3153 333.393 +-324.596 24.7525 333.832 -324.99 25.7514 305.835 -324.743 26.3163 306.266 +-324.752 26.2086 333.299 -324.777 27.2088 306.369 -324.809 27.2516 333.324 +-324.806 28.2418 306.336 -324.859 28.2873 333.297 -324.802 29.2537 306.323 +-324.605 29.1799 333.397 -324.798 30.2463 306.323 -324.987 29.7458 333.83 +-324.739 30.3141 334.263 -324.79 31.2405 306.331 -324.776 31.2085 334.368 +-324.8 32.2563 306.369 -324.804 32.2407 334.334 -324.825 33.1582 306.273 +-324.804 33.2564 334.322 -324.591 33.7497 305.839 -325.041 34.3382 305.378 +-324.801 34.2588 334.329 -324.824 35.2776 305.246 -324.792 35.255 334.335 +-324.596 36.2055 305.389 -324.789 36.2457 334.333 -324.988 36.7831 305.83 +-324.736 37.329 306.258 -324.806 37.2392 334.325 -324.771 38.2089 306.369 +-324.834 38.2548 334.307 -324.803 39.2341 306.341 -324.705 39.1477 334.294 +-324.801 40.2471 306.33 -325.393 40.1573 334.783 -324.791 40.1744 334.415 +-324.812 41.2581 306.304 -324.996 41.2948 318.43 -324.544 40.9139 318.751 +-325.208 41.2126 318.877 -324.811 41.2285 319.352 -324.937 41.3009 321.418 +-324.568 40.9115 321.668 -325.287 41.2302 322.135 -324.696 40.7284 322.585 +-325.337 41.2411 323.051 -324.854 40.723 323.571 -325.313 41.2467 324.033 +-324.734 40.7377 324.707 -325.317 41.2327 325.032 -324.704 40.8476 325.823 +-325.34 41.2236 326.034 -324.712 40.8976 326.766 -325.201 41.2981 326.886 +-324.67 41.1627 327.409 -325.317 40.9692 334.863 -324.827 41.2973 334.438 +-324.726 42.2476 306.395 -324.872 42.3336 315.425 -324.578 41.9638 315.775 +-325.245 42.2531 316.082 -324.741 41.7457 316.658 -325.426 42.1883 316.906 +-324.934 41.5793 317.762 -325.033 41.7378 319.859 -325.414 42.1666 320 +-324.857 41.5701 320.783 -325.012 41.5671 327.69 -324.926 41.7715 328.626 +-325.318 42.2379 328.905 -324.643 42.1707 329.286 -324.885 42.2648 334.22 +-325.166 43.3525 306.743 -324.929 42.8563 306.775 -324.586 43.3212 307.135 +-324.876 43.298 313.449 -324.646 42.8787 313.993 -325.401 43.1315 313.953 +-324.803 42.6454 314.909 -324.85 42.8729 329.776 -325.204 43.3372 329.964 +-324.612 43.2028 330.407 -324.781 43.2719 333.359 -324.785 42.7202 333.88 +-325.291 43.3636 333.755 -325.292 44.3849 306.967 -325.051 44.0034 306.54 +-324.708 43.857 306.854 -325.049 44.2528 312.29 -324.647 43.7633 312.788 +-324.964 43.5952 330.678 -324.879 43.7858 331.613 -325.284 44.3127 332.125 +-324.761 43.758 332.567 -325.257 44.0955 332.963 -324.612 44.6062 306.952 +-324.742 44.8813 307.73 -325.272 45.277 307.868 -324.718 45.274 308.361 +-324.775 45.3571 309.269 -324.666 44.8621 309.82 -325.358 45.3603 309.776 +-324.639 44.6608 310.676 -324.895 44.5992 311.584 -324.921 45.7247 308.662 +-325.269 46.0849 308.965 -323.816 -5.2115 323.851 -323.814 -4.74411 323.28 +-324.163 -4.60682 323.922 -324.206 -4.70875 325.01 -323.807 -5.31442 324.571 +-324.241 -4.68626 326.057 -323.816 -5.31718 325.617 -324.158 -4.57571 326.861 +-323.832 -5.2366 326.586 -323.749 -4.83889 327.218 -323.681 -4.14619 319.983 +-323.855 -3.71141 319.402 -324.393 -3.85675 319.926 -323.843 -4.30319 320.83 +-324.329 -3.73206 322.111 -323.822 -4.26568 321.597 -324.277 -3.88713 323.038 +-323.809 -4.27147 322.543 -324.425 -3.79192 323.934 -324.383 -3.7379 324.844 +-324.314 -3.76605 325.809 -324.243 -3.69309 326.891 -323.8 -4.02451 327.39 +-323.665 -3.25477 318.916 -323.98 -2.74565 318.381 -324.441 -2.82877 325.888 +-324.415 -2.81456 326.859 -323.844 -3.2811 327.72 -324.236 -2.83798 327.686 +-323.799 -2.76986 328.258 -324.369 -1.53921 307.852 -324.325 -1.71596 308.775 +-324.443 -1.81711 309.63 -324.347 -1.78413 316.853 -323.894 -1.83681 317.22 +-323.698 -2.2591 317.765 -324.295 -1.71404 329.117 -323.96 -2.22565 328.739 +-324.265 -1.72886 330.092 -323.73 -2.22937 329.645 -324.218 -1.73698 330.811 +-323.786 -2.1699 330.621 -323.626 -1.81104 331.287 -324.342 -0.625984 307.991 +-324.178 -0.806315 308.77 -324.334 -0.884787 309.809 -324.344 -0.765022 315.846 +-323.862 -0.815852 316.241 -323.676 -1.38454 316.796 -324.252 -0.646685 331.909 +-323.975 -1.11 331.65 -323.519 -0.714172 332.256 -324.332 0.285656 308.917 +-324.223 0.260174 309.826 -324.384 0.245315 310.782 -324.417 0.407492 311.831 +-324.427 0.26435 312.703 -324.317 0.391632 313.615 -324.288 0.239359 314.591 +-323.829 0.277209 315.37 -323.814 -0.222067 315.847 -323.832 -0.312769 332.759 +-323.895 -0.248578 333.713 -323.772 -0.199765 334.781 -323.861 -0.215731 335.858 +-323.804 -0.232651 336.859 -323.971 -0.177778 337.771 -323.69 0.316146 338.396 +-324.349 1.26241 309.941 -324.21 1.09511 310.724 -324.205 1.18888 311.639 +-324.182 1.09551 312.69 -323.811 0.808757 314.138 -323.894 1.14402 313.39 +-323.685 0.757933 314.889 -324.006 0.748527 338.89 -323.693 1.43655 339.204 +-323.895 1.77802 310.921 -323.928 2.39173 310.432 -323.695 1.75493 312.068 +-323.634 1.62046 312.956 -324.408 2.06625 339.82 -323.895 2.40421 339.391 +-323.821 3.15195 310.452 -323.852 3.29836 337.302 -323.723 2.8995 337.76 +-324.168 3.44739 337.793 -323.666 3.13139 338.34 -323.907 3.34606 339.169 +-324.361 4.39159 310.883 -323.866 3.80374 310.735 -323.662 4.20384 311.157 +-323.822 4.35926 336.289 -323.709 3.76288 336.727 -324.253 4.20957 336.835 +-323.939 3.57612 338.635 -323.772 5.19125 311.435 -323.835 5.39918 335.259 +-323.704 4.80549 335.71 -324.253 5.26403 335.848 -324.408 6.40991 311.846 +-323.738 5.75659 311.906 -323.831 6.33971 312.312 -323.819 6.24652 334.424 +-323.775 5.7943 334.805 -324.215 6.08669 334.994 -323.822 7.05844 312.492 +-323.953 7.1695 333.309 -323.961 6.7315 333.953 -323.831 7.69672 312.925 +-323.697 8.18842 313.457 -323.86 8.25321 332.426 -323.694 7.6284 332.723 +-324.075 8.70234 313.805 -323.568 9.13561 314.311 -323.768 9.25225 332.418 +-323.894 9.75343 314.957 -323.859 10.1961 315.331 -323.803 10.1705 332.315 +-323.809 10.6812 314.955 -323.814 11.3139 314.376 -323.819 11.2885 331.491 +-323.576 10.8023 331.845 -323.688 11.8296 313.875 -324.092 12.1962 313.354 +-324.019 11.7524 331.787 -323.535 12.2377 332.375 -323.523 12.7111 312.951 +-323.976 13.2528 312.294 -323.998 12.7287 332.896 -323.78 13.3692 333.294 +-323.933 13.9514 308.9 -323.978 14.1996 308.399 -323.687 14.2712 309.189 +-323.598 13.886 311.032 -324.122 14.2726 310.383 -323.733 13.6068 311.702 +-323.742 14.0843 333.373 -323.605 14.8848 307.024 -324.095 15.2351 306.347 +-323.669 14.6225 307.872 -323.699 14.592 309.883 -323.936 14.7785 333.66 +-323.693 15.4717 334.187 -323.577 15.7301 305.912 -323.922 16.2482 305.434 +-324.328 16.0973 334.766 -323.743 16.1268 334.507 -324.282 17.5288 304.803 +-323.798 16.9975 305.227 -323.799 16.5851 334.911 -323.811 17.1938 335.279 +-324.352 18.3379 304.885 -323.729 18.1492 305.255 -323.761 18.2196 335.413 +-323.804 19.255 305.351 -323.822 19.1506 335.291 -323.868 20.2771 305.296 +-324.004 20.334 334.4 -323.599 19.7714 334.836 -323.597 21.1617 305.411 +-323.762 21.2209 334.291 -323.988 21.7433 305.862 -323.75 22.2142 306.198 +-323.776 22.2435 334.34 -323.54 22.7146 305.881 -324.031 23.3177 305.391 +-323.814 23.2754 334.388 -323.825 24.2623 305.248 -323.891 24.178 334.212 +-323.592 25.1698 305.393 -323.863 25.2197 333.383 -323.741 24.6293 333.756 +-323.99 25.7487 305.836 -323.739 26.3058 306.266 -323.756 26.2235 333.314 +-323.774 27.192 306.371 -323.818 27.2561 333.33 -323.8 28.2281 306.337 +-323.857 28.2867 333.294 -323.8 29.2494 306.322 -323.6 29.1705 333.397 +-323.797 30.246 306.321 -323.985 29.7451 333.83 -323.737 30.3052 334.264 +-323.787 31.2464 306.342 -323.774 31.1922 334.371 -323.81 32.2735 306.384 +-323.799 32.2276 334.336 -323.886 33.1755 306.213 -323.799 33.2495 334.321 +-323.733 33.6238 305.761 -323.887 34.233 305.376 -323.8 34.2502 334.324 +-323.822 35.2742 305.267 -323.799 35.2471 334.325 -323.606 36.1795 305.403 +-323.798 36.2459 334.325 -323.989 36.7598 305.831 -323.737 37.3091 306.265 +-323.81 37.2486 334.325 -323.774 38.1991 306.374 -323.821 38.2527 334.326 +-323.803 39.2329 306.346 -323.795 39.2508 334.336 -323.813 40.2637 306.31 +-323.958 40.2948 322.388 -323.517 39.8964 322.814 -324.31 40.2056 323.088 +-323.725 39.8312 323.87 -324.372 40.2025 324.027 -323.705 39.897 324.805 +-324.2 40.2976 324.887 -323.646 40.194 325.428 -323.755 40.2239 334.364 +-323.805 41.2761 306.273 -323.817 41.1677 316.375 -323.77 40.7989 316.832 +-324.223 41.2391 317.043 -323.882 40.7226 317.706 -324.443 41.1763 317.893 +-323.799 40.5981 318.708 -323.916 40.7456 319.598 -324.289 41.253 320.104 +-323.831 40.7583 320.725 -324.369 41.1903 320.893 -323.89 40.5787 321.764 +-324.012 40.5916 325.668 -323.901 40.6402 326.617 -323.844 40.7231 327.716 +-324.33 41.2046 328.165 -323.79 40.8929 328.682 -324.285 41.3351 328.906 +-323.742 41.3382 329.189 -323.845 41.2381 334.244 -323.627 42.0505 306.458 +-323.953 42.1775 315.306 -323.959 41.6787 315.906 -323.615 42.0644 329.461 +-323.793 42.2675 333.37 -323.761 41.7097 333.893 -324.28 42.3154 333.794 +-324.116 42.7115 306.856 -323.67 43.1366 307.248 -323.961 43.1832 313.317 +-323.832 42.7724 313.878 -323.859 42.6129 314.536 -323.985 42.5979 329.811 +-323.897 42.7743 330.576 -324.29 43.2638 331.128 -323.764 42.7752 331.617 +-324.267 43.2945 332.069 -323.801 42.7453 332.57 -324.295 43.0744 332.967 +-324.342 44.3971 306.875 -323.796 44.0404 307.342 -323.855 44.2133 310.391 +-323.79 43.8247 310.835 -324.219 44.2846 311.079 -323.864 43.76 311.715 +-324.379 44.2184 311.912 -323.777 43.5875 312.63 -323.946 44.6658 307.613 +-323.921 44.7795 308.724 -324.391 45.1592 309.001 -323.886 44.6191 309.77 +-322.593 -9.22199 328.894 -322.912 -8.70054 328.233 -323.185 -8.56967 328.875 +-322.7 -8.88568 329.373 -322.733 -8.20716 327.667 -322.851 -7.61271 327.218 +-323.212 -7.79188 327.924 -323.372 -7.72267 328.909 -322.915 -8.37987 329.663 +-323.235 -7.69308 329.881 -322.765 -7.88605 330.285 -322.831 -7.26046 324.879 +-322.812 -6.81546 324.293 -323.158 -6.61819 324.938 -323.189 -6.6677 326.017 +-322.824 -7.28824 325.581 -323.211 -6.82844 327.009 -322.801 -7.28599 326.516 +-323.314 -6.67801 327.876 -323.339 -6.82345 328.77 -323.375 -6.77895 329.84 +-322.922 -7.26527 330.757 -323.215 -6.64304 330.785 -322.607 -6.85641 331.203 +-322.806 -6.25688 323.789 -322.819 -5.70449 323.306 -323.31 -5.73077 323.905 +-323.421 -5.79658 324.954 -323.404 -5.90233 325.84 -323.391 -5.73247 326.87 +-323.287 -5.78226 327.77 -323.305 -5.78222 328.792 -323.341 -5.7493 329.85 +-323.268 -5.70469 330.775 -322.701 -5.95125 331.361 -322.576 -5.03394 321.815 +-322.846 -4.68553 321.471 -323.26 -4.73114 322.104 -323.331 -4.92657 322.919 +-322.672 -5.25003 322.524 -323.35 -4.66595 327.804 -323.267 -4.76746 328.809 +-323.314 -4.73426 329.865 -323.237 -4.70488 330.772 -322.861 -4.93819 331.381 +-322.578 -4.13668 319.903 -323.026 -3.77271 319.274 -322.886 -4.34623 320.827 +-323.383 -3.56314 328.049 -323.213 -3.74612 328.829 -323.277 -3.73309 329.876 +-323.206 -3.70946 330.774 -322.833 -3.94408 331.371 -322.583 -3.2825 318.803 +-323.002 -2.76742 318.339 -323.362 -2.84797 328.904 -323.351 -2.84587 329.884 +-323.35 -2.76842 330.797 -322.887 -2.81941 331.36 -322.573 -2.29387 317.829 +-322.899 -1.69166 317.337 -322.767 -1.92469 331.526 -322.734 -1.16017 316.892 +-322.849 -0.661097 316.497 -323.067 -1.27227 331.824 -322.556 -0.879339 332.319 +-323.271 0.304168 315.855 -322.937 0.057588 316.173 -322.902 -0.365199 332.822 +-322.928 -0.135592 333.797 -322.776 0.230463 334.328 -323.158 0.150103 334.799 +-323.294 0.27148 335.768 -323.343 0.281739 336.865 -323.343 0.29642 337.83 +-323.472 1.35779 313.736 -323.255 1.29708 314.574 -322.964 1.14028 315.262 +-322.618 0.70074 315.812 -322.665 0.61951 334.878 -322.864 0.814591 335.944 +-323.186 1.14752 336.835 -322.785 1.46584 336.356 -323.211 1.23238 337.9 +-323.353 1.05928 338.689 -323.398 2.38976 310.794 -323.198 2.33743 311.53 +-322.936 2.12212 312.26 -322.79 1.85432 312.889 -322.826 1.68471 313.877 +-322.596 1.61882 314.844 -323.192 2.21497 336.773 -322.855 2.29007 336.278 +-323.335 2.38141 337.866 -323.296 2.06723 338.815 -323.308 3.15037 310.954 +-322.677 2.61002 311.962 -322.881 3.26592 311.412 -322.875 3.40002 335.247 +-322.727 2.82137 335.752 -323.172 3.19473 335.943 -323.447 3.11347 336.914 +-323.404 3.04614 338.861 -322.86 4.20105 311.444 -322.773 4.52029 334.372 +-322.731 3.82565 334.654 -323.137 4.08225 335.019 -323.434 4.13321 335.884 +-323.315 5.28947 311.794 -322.91 4.85268 311.713 -322.592 5.1657 312.211 +-323.347 5.17684 334.822 -322.81 5.30583 334.243 -322.961 5.81982 312.702 +-323.252 6.50534 312.854 -322.56 6.21409 313.188 -322.829 6.41056 333.3 +-322.684 5.82759 333.763 -323.25 6.26659 333.855 -323.398 7.1939 312.808 +-322.665 7.12306 313.294 -322.906 7.52756 332.35 -322.715 6.83987 332.693 +-323.208 7.04355 332.983 -322.912 7.86198 313.672 -323.344 8.3817 313.902 +-322.716 8.37477 314.155 -322.846 8.4317 332.246 -322.663 9.08832 314.444 +-322.846 9.45059 332.311 -322.88 9.62418 314.919 -322.964 9.8867 315.677 +-323.28 10.4843 315.749 -322.613 10.2073 316.22 -322.952 10.273 332.209 +-323.375 11.0776 315.013 -323.137 11.2422 315.671 -322.643 11.1526 316.271 +-322.886 11.1287 331.467 -322.835 10.7104 331.851 -323.319 12.279 313.693 +-322.943 12.0638 314.28 -322.874 11.7508 315.036 -322.839 11.684 315.808 +-322.874 11.7586 331.628 -322.837 12.2359 332.296 -322.736 12.8946 313.037 +-323.009 13.201 312.358 -322.787 12.6043 313.797 -322.832 12.7123 332.749 +-322.996 13.4553 333.17 -323.388 14.252 308.692 -323.371 14.3048 310.683 +-322.672 14.3335 311.317 -322.844 13.6988 311.799 -322.562 14.1495 332.48 +-322.678 13.6297 332.767 -323.218 14.1956 332.982 -323.371 15.2875 306.648 +-322.897 15.1531 307.246 -322.837 14.8854 307.895 -322.954 14.7192 308.973 +-322.807 14.8903 310.015 -322.804 14.7573 310.742 -322.945 14.7555 333 +-322.52 15.2476 333.388 -323.283 15.1928 333.711 -322.678 15.8422 305.968 +-322.929 16.3812 305.408 -322.7 15.5718 306.837 -322.913 15.6019 333.946 +-322.701 16.2769 334.357 -322.852 17.3368 305.318 -323.025 16.7645 334.844 +-322.649 17.483 335.245 -322.72 18.2156 305.339 -322.873 18.3617 335.433 +-322.786 19.2412 305.322 -322.845 19.1643 335.233 -322.846 20.2643 305.295 +-322.892 20.2524 334.382 -322.744 19.6591 334.767 -322.57 21.331 305.41 +-322.778 21.2345 334.301 -323.025 21.814 305.882 -322.925 22.3195 306.157 +-322.793 22.2556 334.354 -322.671 22.6388 305.811 -322.888 23.2552 305.4 +-322.873 23.319 334.281 -322.833 24.2751 305.27 -322.952 24.4393 333.45 +-322.543 23.7933 333.828 -323.254 24.1272 333.905 -322.61 25.1555 305.405 +-322.751 25.204 333.279 -322.991 25.723 305.839 -322.738 26.2827 306.27 +-322.764 26.2298 333.324 -322.761 27.1737 306.373 -322.821 27.256 333.331 +-322.785 28.2126 306.342 -322.853 28.274 333.294 -322.794 29.2439 306.323 +-322.609 29.1557 333.404 -322.795 30.2442 306.322 -322.987 29.7223 333.836 +-322.737 30.2846 334.27 -322.792 31.2526 306.352 -322.762 31.1765 334.373 +-322.871 32.3164 306.279 -322.787 32.2158 334.34 -322.538 32.7895 305.829 +-322.945 33.4326 305.455 -323.25 33.1249 305.906 -322.797 33.249 334.322 +-322.775 34.2082 305.272 -322.806 34.2522 334.322 -322.831 35.2683 305.282 +-322.799 35.2396 334.329 -322.606 36.1824 305.405 -322.785 36.2241 334.342 +-322.99 36.7409 305.836 -322.736 37.3123 306.268 -322.777 37.2174 334.361 +-322.777 38.2122 306.375 -322.776 38.215 334.376 -322.808 39.2524 306.323 +-322.753 39.2908 334.291 -322.8 40.2748 306.274 -322.83 40.3337 316.268 +-322.72 39.8188 316.768 -323.274 40.3619 316.889 -322.78 39.7095 317.616 +-323.286 40.2269 318.066 -322.772 39.7532 318.624 -323.275 40.2591 319.084 +-322.76 39.7723 319.623 -323.248 40.2695 320.067 -322.747 39.7845 320.615 +-323.241 40.2676 321.061 -322.848 39.7893 321.747 -323.41 40.1885 321.901 +-322.755 39.6726 322.755 -322.876 39.6812 323.693 -322.874 39.6651 324.685 +-322.886 39.7763 325.634 -323.309 40.2797 326.145 -322.772 39.7769 326.627 +-323.293 40.2672 327.069 -322.662 39.7601 327.76 -323.197 40.3146 327.912 +-322.679 40.1848 328.427 -322.954 40.3498 334.237 -322.737 41.1618 306.424 +-322.805 41.4162 315.288 -322.687 40.8172 315.707 -323.243 41.2432 315.832 +-322.849 40.6603 328.849 -322.816 41.3162 329.41 -322.773 41.2759 333.377 +-322.743 40.7184 333.901 -323.256 41.3195 333.803 -323.378 42.4215 306.907 +-322.924 41.8319 306.747 -322.727 42.2309 307.195 -322.805 42.2754 313.32 +-322.791 41.8336 313.898 -323.264 42.446 313.908 -322.763 41.7116 314.563 +-323.235 42.0666 314.958 -323.316 42.2866 330.05 -322.887 41.7761 329.745 +-322.761 41.7463 330.552 -323.265 42.2567 331.035 -322.797 41.7624 331.608 +-323.3 42.2859 332.064 -322.806 41.7513 332.581 -323.301 42.0801 332.969 +-322.769 43.2075 307.428 -322.874 43.3512 310.285 -322.535 42.8542 310.826 +-323.308 43.3845 310.898 -322.825 42.8688 311.74 -323.304 43.2663 312.069 +-322.67 42.7503 312.558 -323.291 43.0826 312.943 -323.28 44.3154 308.078 +-322.905 43.8059 307.758 -322.743 43.7786 308.561 -323.229 44.299 309.043 +-322.776 43.697 309.553 -323.284 44.0453 309.923 -321.834 -10.2635 327.924 +-321.826 -9.78773 327.3 -322.168 -9.68997 327.899 -322.258 -9.67396 328.686 +-321.813 -10.2701 328.597 -321.836 -9.84828 329.268 -321.83 -9.21962 326.769 +-321.826 -8.68346 326.304 -322.174 -8.7623 326.9 -322.462 -8.86552 327.886 +-322.342 -8.62836 330.127 -321.993 -9.28534 329.735 -322.072 -8.56768 330.872 +-321.748 -9.22011 330.571 -321.689 -8.85154 331.223 -321.571 -8.14923 324.704 +-321.857 -7.71384 324.309 -322.324 -7.64408 324.931 -322.317 -7.85019 326.062 +-321.675 -8.25625 325.559 -322.449 -7.92473 326.981 -322.42 -7.77387 330.819 +-321.866 -7.84486 331.339 -321.777 -7.32552 323.851 -321.785 -6.75275 323.323 +-322.303 -6.77942 323.828 -321.885 -6.81001 331.472 -321.798 -6.19431 322.806 +-321.856 -5.75335 322.416 -322.252 -5.91468 322.971 -322.272 -5.49205 331.856 +-321.924 -6.22287 331.747 -321.629 -5.76368 332.227 -321.976 -5.28243 321.937 +-321.947 -4.81315 321.316 -322.33 -4.5302 331.872 -321.806 -4.929 332.232 +-321.738 -4.31992 319.775 -321.911 -3.88147 319.284 -321.885 -4.3737 320.521 +-322.307 -3.5191 331.857 -321.681 -3.98557 332.252 -321.591 -3.29098 318.823 +-321.904 -2.69897 318.322 -322.293 -2.51154 331.827 -321.672 -2.97112 332.246 +-321.732 -2.15792 317.881 -321.85 -1.6654 317.485 -322.425 -1.65555 331.876 +-321.809 -1.84491 332.247 -322.231 -0.642627 316.759 -321.842 -0.861711 317.182 +-321.567 -0.720794 332.426 -321.666 -0.162725 316.917 -321.851 0.269053 316.436 +-322.034 -0.224264 332.938 -321.649 0.330413 333.34 -322.244 0.220893 333.786 +-321.653 0.919977 315.946 -322.023 1.31443 315.421 -321.906 0.757183 334.038 +-321.761 1.48875 334.336 -322.208 1.31715 334.93 -322.473 1.19408 335.689 +-322.324 2.45598 312.695 -321.927 2.14884 313.336 -321.749 1.82981 313.898 +-321.717 1.68121 314.889 -322.167 2.22121 334.782 -321.849 2.29606 334.28 +-322.411 2.07094 335.722 -322.396 3.26506 311.921 -322.093 3.37007 312.597 +-321.783 2.88489 313.317 -321.862 3.38989 333.29 -321.722 2.81847 333.772 +-322.162 3.19151 333.951 -322.435 3.15315 334.894 -322.352 4.192 311.944 +-321.721 3.62744 312.958 -321.915 4.28212 312.378 -321.867 4.37523 332.254 +-321.721 3.82521 332.719 -322.184 4.28639 332.973 -322.401 4.08251 333.974 +-321.715 5.02152 312.452 -321.805 5.54346 331.358 -321.732 4.8268 331.689 +-322.181 5.20283 332.075 -322.312 5.27073 332.799 -322.418 5.11553 333.651 +-322.005 5.66016 312.828 -321.841 6.28568 313.26 -322.183 6.11797 331.796 +-321.79 6.49672 331.316 -322.423 6.10251 332.787 -321.719 7.04122 313.502 +-322.284 7.01588 331.954 -321.768 7.49598 331.341 -321.998 7.69153 313.823 +-321.878 8.28029 314.305 -322.251 7.99293 331.833 -321.748 8.47416 331.374 +-322.273 9.38231 314.909 -321.713 8.79307 314.884 -321.658 9.22241 315.38 +-322.22 8.97917 331.884 -321.739 9.47277 331.412 -321.84 9.67721 315.849 +-321.83 10.3396 316.402 -321.755 10.4023 318.47 -321.71 9.94926 318.67 +-322.182 10.2343 318.911 -321.75 10.1719 319.16 -322.32 10.2092 331.757 +-321.831 10.3428 331.26 -321.812 11.0486 316.516 -321.74 10.4848 318.965 +-321.707 10.8298 319.919 -322.209 11.1661 319.88 -321.612 11.2232 319.435 +-321.8 10.8018 320.835 -322.251 11.2446 320.789 -321.923 10.8986 321.753 +-322.188 11.313 321.732 -321.675 11.1551 322.247 -321.696 11.3537 330.33 +-321.752 10.8695 330.699 -322.194 11.1372 331.039 -322.417 12.4735 314.701 +-322.325 12.2644 315.804 -322.018 11.7577 316.694 -322.092 12.316 316.674 +-321.527 12.2443 317.202 -321.798 11.5968 319.95 -321.792 11.675 321.028 +-321.68 11.5708 321.94 -321.791 11.7276 330.862 -321.487 12.352 331.38 +-322.269 12.2131 331.835 -322.411 13.4335 312.643 -322.268 13.2739 313.711 +-322.114 13.142 314.769 -321.829 12.7849 316.11 -321.853 13.1089 315.356 +-321.855 12.6379 316.774 -321.938 12.7434 332.084 -321.82 13.4937 332.287 +-322.339 13.1149 332.755 -322.317 14.109 312.001 -322.104 14.1482 312.754 +-321.791 13.7866 314.081 -321.866 14.1201 313.38 -321.703 13.5915 314.826 +-321.642 14.375 332.382 -322.372 15.2957 307.775 -322.242 15.2296 308.642 +-322.301 15.243 309.656 -322.259 15.2627 310.589 -322.005 15.1166 311.273 +-321.86 14.7889 311.997 -321.717 14.6427 312.871 -322.068 14.7812 332.755 +-321.832 15.2433 333.219 -322.265 16.2775 305.692 -321.763 16.2847 306.288 +-321.756 15.8471 306.889 -321.865 15.6166 307.846 -321.716 15.7457 308.957 +-321.693 15.7615 310.08 -321.558 15.5968 310.985 -321.889 15.7464 333.684 +-321.677 16.4679 334.205 -321.697 16.7523 305.875 -321.904 17.2255 305.421 +-322.304 17.0775 334.779 -321.676 17.282 334.492 -321.841 18.3385 305.309 +-321.833 17.6589 334.928 -321.992 18.3465 335.202 -321.762 19.2327 305.301 +-321.827 19.3661 334.441 -321.594 18.7097 334.769 -322.198 19.1602 334.926 +-321.823 20.2591 305.299 -321.788 20.2439 334.282 -321.694 21.1428 305.305 +-321.793 21.2456 334.318 -322.434 22.154 305.809 -321.901 22.2405 305.405 +-321.812 22.2647 334.366 -321.805 23.2655 305.301 -321.837 23.166 334.266 +-321.85 24.2695 305.293 -322.003 24.3178 333.39 -321.601 23.756 333.828 +-321.619 25.3333 305.389 -321.758 25.2145 333.293 -322.06 25.7775 305.837 +-321.689 26.4744 306.23 -321.782 26.2439 333.333 -321.882 27.3113 306.373 +-321.822 27.2534 333.327 -321.77 28.2131 306.351 -321.855 28.2711 333.298 +-321.795 29.2422 306.328 -321.617 29.3361 333.385 -321.794 30.2451 306.326 +-322.058 29.7807 333.836 -321.688 30.472 334.23 -321.804 31.2583 306.365 +-321.884 31.3092 334.371 -321.831 32.16 306.268 -321.77 32.2033 334.347 +-321.592 32.7475 305.834 -321.994 33.3079 305.398 -321.791 33.2299 334.335 +-321.783 34.2163 305.29 -321.796 34.2331 334.341 -321.844 35.292 305.286 +-321.775 35.2216 334.36 -321.597 36.197 305.39 -321.758 36.2094 334.381 +-321.986 36.7595 305.83 -321.752 37.3397 306.256 -321.881 37.3421 321.241 +-321.764 36.8029 321.754 -322.201 37.3521 321.921 -321.844 36.7142 322.539 +-322.184 37.4118 322.962 -321.758 37.0812 323.251 -321.773 37.295 334.289 +-321.793 38.2336 306.343 -321.853 38.3445 317.251 -321.718 37.8343 317.756 +-322.151 38.3901 317.97 -321.799 37.698 318.608 -322.194 38.3346 319.053 +-321.798 37.7513 319.615 -322.183 38.3754 320.043 -321.793 37.7142 320.538 +-322.202 38.1903 321.024 -322.295 38.3598 321.897 -322.307 38.2281 322.789 +-321.877 37.6287 323.753 -322.197 38.2676 323.92 -321.783 37.7743 324.601 +-322.165 38.3676 325.059 -321.795 37.7272 325.61 -322.21 38.3368 326.063 +-321.837 37.778 326.567 -322.19 38.4799 327.02 -321.782 38.1429 327.294 +-321.992 38.3539 334.215 -321.794 39.2489 306.282 -321.828 39.2365 315.324 +-321.83 38.8201 315.898 -322.157 39.3477 315.861 -321.803 38.6745 316.538 +-322.298 39.1705 316.948 -322.422 39.2178 317.965 -322.399 39.1249 318.828 +-322.395 39.1517 319.824 -322.383 39.1671 320.811 -322.371 39.1772 321.818 +-322.367 39.1861 322.847 -322.36 39.1841 323.876 -322.354 39.1748 324.869 +-322.384 39.1442 325.837 -322.449 39.137 326.837 -321.673 38.6912 327.899 +-322.284 39.1375 327.656 -321.788 39.2575 328.353 -321.729 39.3892 333.445 +-321.783 38.7019 333.859 -322.126 39.1307 333.942 -321.718 40.1488 306.425 +-321.863 40.3641 314.263 -321.752 39.803 314.696 -322.168 40.1957 314.908 +-322.435 40.1286 315.874 -321.989 39.8005 328.69 -322.255 40.2454 328.865 +-321.768 40.1784 329.234 -322.336 40.192 333.764 -321.83 40.3659 333.225 +-322.37 41.3749 306.899 -321.917 40.8017 306.763 -321.71 41.196 307.197 +-321.844 41.387 313.24 -321.715 40.7957 313.693 -322.294 41.2742 313.93 +-322.443 41.0386 314.995 -321.933 40.7476 329.708 -322.29 41.2514 330.066 +-321.744 40.758 330.601 -322.275 41.2511 331.051 -321.782 40.7958 331.631 +-322.298 41.2963 332.083 -321.774 40.7614 332.586 -322.276 41.0866 332.987 +-321.77 42.1828 307.429 -321.835 42.269 312.444 -321.78 41.7995 312.804 +-322.242 42.0675 312.977 -322.328 43.2681 308.039 -321.774 42.7854 307.884 +-321.753 42.888 308.661 -322.273 43.2332 309.045 -321.735 42.7393 309.717 +-322.44 43.1486 309.946 -321.813 42.6194 310.722 -322 42.6473 311.725 +-320.692 -12.1309 330.908 -320.772 -11.8355 330.255 -321.194 -11.542 330.854 +-320.743 -11.9088 331.323 -320.792 -11.2505 327.9 -320.798 -10.8155 327.318 +-321.3 -10.6593 327.906 -321.269 -10.637 329.077 -320.785 -11.2915 328.613 +-321.212 -10.8335 330.021 -320.799 -11.3127 329.546 -321.343 -10.6831 330.898 +-320.924 -11.3868 331.635 -321.205 -10.6951 331.855 -320.743 -10.9148 332.224 +-320.796 -10.2723 326.808 -320.803 -9.72579 326.305 -321.311 -9.74827 326.8 +-321.413 -9.88311 329.89 -321.366 -9.74535 330.856 -321.325 -9.78871 331.835 +-320.862 -10.3193 332.705 -321.197 -9.66164 332.939 -320.74 -9.88414 333.298 +-320.802 -9.17167 325.804 -320.861 -8.73294 325.43 -321.256 -8.89612 325.978 +-321.375 -8.68007 331.842 -321.339 -8.89798 332.75 -320.913 -9.31438 333.773 +-321.177 -8.67285 333.613 -320.626 -8.68346 334.306 -320.924 -8.23511 324.036 +-320.807 -7.7978 323.289 -321.374 -7.67896 323.802 -320.912 -8.40759 324.778 +-321.374 -7.54933 331.919 -321.231 -7.86792 332.851 -320.729 -8.21732 333.938 +-320.929 -7.85441 333.494 -320.688 -7.35779 322.718 -320.711 -6.77572 322.318 +-321.299 -6.74538 322.803 -321.442 -6.64152 332.025 -321.2 -6.77471 332.721 +-320.753 -6.85455 333.346 -320.54 -6.21109 321.849 -320.818 -5.60724 321.33 +-321.29 -5.72827 321.827 -321.201 -5.83907 332.693 -320.731 -5.7845 333.202 +-320.624 -5.09165 320.006 -320.914 -4.5129 319.461 -321.258 -4.75791 320.051 +-321.286 -4.93452 320.912 -320.718 -5.27027 320.697 -320.953 -4.73109 332.483 +-320.688 -5.16142 332.94 -320.804 -3.67743 319.198 -320.887 -3.6131 332.36 +-320.744 -3.13849 318.909 -320.879 -2.66346 318.505 -320.777 -2.74398 332.364 +-321.216 -1.62909 317.742 -320.969 -1.99677 318.15 -320.833 -1.64689 332.261 +-320.494 -1.29944 317.817 -320.937 -0.90597 317.433 -320.828 -0.612293 332.213 +-321.304 0.306473 316.878 -320.824 0.0145857 317.217 -321.352 0.262047 332.866 +-320.883 0.340338 332.351 -321.281 1.35831 315.751 -320.963 1.10295 316.227 +-320.633 0.701051 316.709 -321.168 1.3413 332.858 -320.877 1.32693 332.235 +-321.391 1.03325 333.617 -321.335 2.43515 313.858 -320.771 2.28437 314.327 +-320.777 1.80764 314.922 -320.876 1.56283 315.683 -320.935 2.25741 330.249 +-320.638 1.83974 330.721 -321.226 2.39537 330.961 -320.701 1.70366 331.606 +-321.223 2.2191 332.089 -321.266 2.28218 332.887 -321.396 2.10275 333.717 +-321.302 3.44859 313.72 -320.775 3.07152 314.224 -320.792 3.35707 329.352 +-320.84 2.63953 329.772 -321.201 3.1598 330.039 -321.321 3.40995 330.913 +-321.313 3.14167 331.691 -321.401 3.11132 332.838 -321.423 4.35807 312.963 +-321.206 4.38888 313.821 -320.702 4.0441 314.237 -321.212 4.26246 329.869 +-320.843 4.47213 329.263 -321.304 4.16039 330.646 -321.404 4.12232 331.79 +-321.405 5.38823 312.978 -321.211 5.24276 313.665 -320.71 5.05998 314.318 +-321.201 5.25253 329.91 -320.828 5.45073 329.3 -321.387 5.04625 330.86 +-321.275 6.3832 313.827 -320.849 6.02219 314.368 -321.196 6.24059 329.924 +-320.823 6.43641 329.304 -321.405 6.04462 330.713 -321.408 7.42745 314.023 +-320.67 6.66887 314.874 -321.125 7.31252 314.71 -320.671 7.2378 315.336 +-321.199 7.23953 329.929 -320.813 7.41829 329.32 -321.397 7.04833 330.721 +-321.342 8.06728 314.674 -320.879 8.04828 315.319 -321.19 8.22839 329.952 +-320.808 8.41931 329.342 -321.393 8.04537 330.737 -320.963 8.812 315.657 +-321.225 9.25552 315.852 -320.73 9.1889 316.227 -321.219 9.35027 329.85 +-320.902 9.29873 329.192 -321.383 9.03597 330.773 -320.914 9.74215 316.706 +-321.25 10.4279 316.97 -320.861 9.72943 317.742 -321.4 10.2145 317.879 +-320.731 9.71526 318.944 -320.803 10.3417 319.292 -320.683 10.2368 328.354 +-320.858 9.76816 328.785 -321.177 10.1957 329.062 -321.348 10.3075 329.83 +-321.444 10.1442 330.689 -321.394 11.3722 317.002 -321.177 10.9678 317.781 +-321.011 10.8165 318.671 -320.707 11.2586 318.288 -320.909 10.7957 319.632 +-320.76 11.3251 319.349 -320.803 10.5898 320.743 -321.036 10.8021 321.851 +-320.641 11.379 322.26 -320.741 10.7635 328.743 -321.312 11.0011 329.78 +-320.517 11.363 329.328 -320.814 11.7006 317.733 -320.83 12.2329 317.544 +-320.887 11.729 319.808 -320.892 11.8785 320.746 -320.999 11.7822 321.654 +-320.941 11.684 330.05 -320.956 11.8945 330.807 -320.744 12.4241 331.154 +-321.409 13.4851 315.713 -321.405 13.1743 316.942 -320.947 12.7278 317.648 +-321.113 13.4429 317.828 -320.698 13.1646 318.164 -321.375 13.0991 331.78 +-320.915 13.0527 331.431 -321.441 14.5186 313.753 -321.37 14.2009 314.873 +-321.256 14.3168 315.913 -321.284 14.191 316.787 -321.233 14.2845 317.87 +-320.792 13.9748 318.366 -320.761 13.6715 331.727 -320.76 14.3448 332.222 +-321.392 15.353 311.729 -321.268 15.1626 312.695 -321.152 15.2072 313.665 +-321.212 15.1996 314.714 -321.308 15.2177 315.826 -321.3 15.2209 316.824 +-321.323 15.2178 317.817 -320.872 14.693 318.681 -321.191 15.3394 318.879 +-320.712 15.1263 319.248 -321.378 15.1778 332.842 -320.719 15.1362 332.483 +-321.296 16.2108 306.799 -320.874 16.1439 307.244 -321.041 15.9111 307.93 +-320.642 16.296 308.258 -321.202 16.1871 308.763 -321.317 16.2788 309.546 +-320.964 16.1974 310.24 -320.887 15.8986 310.862 -320.85 15.7383 311.893 +-320.772 15.7139 313.031 -320.78 15.6852 314.032 -320.857 15.8217 314.884 +-321.193 16.1453 315.787 -320.723 16.3409 315.399 -321.348 16.2746 316.843 +-321.334 16.2505 317.838 -321.348 16.224 318.844 -320.864 15.6801 319.703 +-321.202 16.3359 319.913 -320.718 16.1158 320.283 -320.893 15.7652 333.044 +-320.534 16.2692 333.427 -321.274 16.1955 333.734 -321.21 17.3519 305.75 +-320.997 17.0623 306.201 -320.618 16.6382 306.892 -320.649 16.6161 308.964 +-320.597 16.5894 309.979 -320.885 16.8204 315.965 -321.178 17.0576 316.66 +-320.74 17.403 316.389 -321.243 17.2628 317.734 -321.355 17.2502 318.856 +-321.374 17.2593 319.889 -320.872 16.6929 320.747 -321.182 17.3335 320.792 +-320.733 17.2103 321.265 -320.92 16.6106 333.965 -320.818 17.1167 334.279 +-320.49 17.6581 305.789 -320.958 18.1581 305.388 -320.798 17.6961 317.087 +-320.855 17.8499 317.875 -321.197 18.1502 318.793 -320.703 18.3304 318.381 +-321.385 18.3027 319.889 -321.268 18.3227 320.802 -320.828 18.0827 321.408 +-321.414 18.1214 334.776 -320.877 18.1879 334.418 -320.751 19.2042 305.291 +-320.872 18.8019 318.961 -321.195 19.0809 319.681 -320.732 19.4027 319.379 +-321.208 19.2345 320.592 -320.843 19.0471 321.312 -320.759 19.2095 334.304 +-320.788 20.2248 305.3 -320.82 19.7476 320.085 -320.762 19.6731 320.878 +-320.776 20.2148 334.314 -320.782 21.2472 305.31 -320.786 21.2292 334.35 +-320.768 22.248 305.322 -320.807 22.2598 334.391 -320.81 23.2449 305.327 +-320.891 23.1752 334.211 -320.863 24.2698 305.316 -320.873 24.2252 333.375 +-320.751 23.635 333.749 -320.752 25.1765 305.292 -320.767 25.2319 333.302 +-321.361 26.1307 305.756 -320.695 26.1287 305.432 -320.796 26.2504 333.332 +-320.823 26.6277 305.904 -320.825 27.2112 306.274 -320.821 27.252 333.324 +-320.764 28.2151 306.36 -320.858 28.2783 333.303 -320.801 29.2365 306.33 +-320.749 29.1759 333.282 -320.79 30.2494 306.336 -321.358 30.1283 333.755 +-320.706 30.1159 333.429 -320.809 31.2749 306.383 -320.829 30.6095 333.906 +-320.823 31.1881 334.274 -320.888 32.1764 306.212 -320.732 32.1747 334.376 +-320.744 32.6292 305.76 -320.869 33.219 305.391 -320.767 33.1951 334.378 +-320.797 34.2503 305.291 -320.78 34.3116 334.278 -320.834 35.3024 305.271 +-320.869 35.4402 334.269 -320.632 36.0724 305.446 -320.867 36.3152 317.257 +-320.739 35.8108 317.769 -321.164 36.3708 317.971 -320.799 35.693 318.617 +-321.194 36.341 319.057 -320.792 35.7463 319.628 -321.19 36.386 320.057 +-320.783 35.7832 320.651 -321.168 36.2639 320.988 -320.773 35.7734 321.674 +-321.306 36.3086 321.954 -320.762 35.7571 322.673 -321.293 36.3611 323.078 +-320.767 35.7506 323.64 -321.177 36.3489 324.036 -320.81 35.7447 324.62 +-321.21 36.334 325.024 -320.653 35.7556 325.761 -321.178 36.4467 325.952 +-320.702 36.1777 326.349 -320.985 36.3834 334.213 -321.049 36.7212 305.822 +-320.735 37.2004 306.25 -320.835 37.374 316.254 -320.723 36.7887 316.684 +-321.149 37.1549 316.918 -321.405 37.3071 317.826 -321.415 37.1076 318.836 +-321.405 37.1602 319.83 -321.444 36.9648 320.879 -321.449 37.1506 323.875 +-321.396 37.1303 324.814 -321.435 37.1026 325.819 -320.929 36.5979 326.708 +-321.357 37.2878 326.95 -320.704 36.7317 327.741 -321.155 37.4615 327.834 +-320.714 37.2264 328.312 -320.675 37.2744 333.541 -320.636 36.6547 333.792 +-321.16 37.1441 333.933 -320.755 38.1797 306.41 -320.826 38.3962 315.285 +-320.688 37.8334 315.703 -321.29 38.2706 315.948 -321.45 37.9927 316.99 +-321.404 38.0926 327.719 -320.894 38.0651 328.319 -321.383 38.1681 333.735 +-320.741 38.1744 333.279 -320.885 39.1641 306.454 -320.844 39.238 313.275 +-320.826 38.8273 313.891 -321.16 39.3579 313.87 -320.792 38.6992 314.561 +-321.306 39.1523 314.846 -320.963 38.7337 328.712 -321.278 39.3632 329.115 +-320.742 38.7625 329.521 -321.103 39.4624 329.992 -320.717 39.1175 330.245 +-320.846 39.4484 332.285 -320.729 38.8377 332.744 -321.2 39.2217 332.903 +-321.338 40.3738 306.897 -320.889 39.798 306.741 -320.681 40.1918 307.172 +-320.747 40.4169 312.386 -320.84 39.7437 312.751 -321.159 40.0857 313.001 +-321.448 40.143 313.88 -321.39 40.1297 329.882 -320.854 39.6718 330.765 +-321.273 40.1971 330.933 -320.774 39.7751 331.563 -321.276 40.173 332.09 +-321.473 40.1004 332.849 -320.738 41.1671 307.414 -321.343 41.2084 312.815 +-320.813 41.3792 312.202 -321.194 42.3602 307.907 -320.914 41.8547 307.695 +-320.666 42.1564 308.206 -320.879 42.3312 309.37 -320.702 41.889 309.727 +-321.231 42.2574 310.094 -320.79 41.7751 310.601 -321.293 42.2958 311.07 +-320.759 41.7583 311.568 -321.254 42.1093 311.97 -321.015 42.5909 308.694 +-319.65 -13.1364 329.622 -319.931 -12.6834 329.301 -320.232 -12.5131 329.911 +-319.608 -12.8247 330.281 -319.655 -12.1202 327.824 -319.866 -11.7197 327.332 +-320.325 -11.6595 327.922 -320.322 -11.9175 329.105 -319.693 -12.3473 328.548 +-320.5 -11.839 329.778 -319.921 -12.3467 330.811 -320.376 -11.6814 332.045 +-319.871 -12.242 331.744 -320.186 -11.5977 332.919 -319.704 -12.183 332.745 +-319.736 -11.8922 333.376 -319.588 -11.285 326.812 -319.855 -10.7024 326.306 +-320.337 -10.819 326.817 -320.446 -10.726 332.879 -319.715 -11.3943 333.866 +-320.217 -10.671 333.842 -319.724 -10.8466 334.404 -319.542 -10.2518 325.818 +-319.816 -9.637 325.312 -320.318 -9.74475 325.805 -320.488 -9.68416 333.963 +-319.936 -10.3099 334.734 -320.221 -9.63535 334.783 -319.63 -9.89267 335.281 +-319.586 -9.1583 323.872 -319.745 -8.606 323.267 -320.334 -8.62318 323.86 +-320.311 -8.94616 324.918 -319.727 -9.2819 324.678 -320.316 -8.8583 334.79 +-319.762 -8.92861 335.284 -319.686 -8.11533 322.867 -319.923 -7.51252 322.502 +-320.276 -8.03555 322.935 -320.411 -7.5003 333.839 -320.016 -7.84841 334.311 +-319.715 -8.25172 334.847 -319.961 -6.78873 322.205 -320.213 -6.67788 333.775 +-319.647 -6.87228 334.218 -319.592 -6.27886 321.849 -320.057 -5.76869 321.351 +-319.938 -5.74924 333.502 -319.712 -6.13293 333.932 -320.263 -4.69152 319.715 +-319.922 -4.88643 320.184 -319.749 -5.30396 320.736 -320.214 -4.5918 332.78 +-319.983 -4.91955 333.192 -319.533 -4.35926 319.821 -319.952 -3.95744 319.413 +-319.992 -3.78699 332.443 -319.48 -4.26149 332.851 -320.249 -2.6074 318.784 +-319.847 -3.03827 319.196 -319.887 -2.70924 332.213 -319.533 -2.28465 318.833 +-319.932 -1.78497 318.331 -319.713 -1.71827 331.489 -319.674 -2.34186 331.768 +-320.207 -1.81892 331.903 -319.711 -1.10961 317.895 -319.909 -0.528115 317.489 +-320.342 -0.965891 331.839 -319.715 -0.654283 331.318 -319.906 0.302494 317.236 +-320.301 0.0576531 331.846 -319.781 0.535613 331.35 -319.748 0.899384 316.092 +-319.703 1.38994 315.477 -320.348 1.32973 315.803 -319.677 0.622669 316.851 +-320.352 1.01383 331.878 -319.924 1.31884 331.4 -320.201 2.3335 314.744 +-319.781 2.11306 315.203 -319.891 2.34107 329.246 -319.71 1.85989 329.806 +-320.436 2.30683 329.784 -319.745 1.64073 330.806 -319.601 2.80101 314.868 +-320.043 3.18781 314.464 -319.709 3.30554 328.445 -319.686 2.65989 328.701 +-320.23 3.06924 328.943 -319.895 4.23918 314.467 -320.347 4.03586 328.77 +-319.71 4.26655 328.43 -320.293 5.56888 314.808 -319.718 4.78049 314.891 +-319.686 5.23483 315.282 -320.33 5.02318 328.805 -319.73 5.37089 328.33 +-320.415 6.21712 314.826 -319.892 6.04465 315.296 -320.317 6.01283 328.821 +-319.82 6.50408 328.318 -319.986 6.77696 315.644 -320.22 7.50572 315.949 +-319.676 7.102 316.236 -320.305 6.99485 328.824 -319.79 7.47145 328.349 +-320.382 8.39386 316.031 -319.85 7.61746 316.735 -320.187 8.24606 316.895 +-319.665 7.78036 317.737 -320.171 8.48232 317.986 -319.691 8.17383 318.381 +-320.287 7.97511 328.855 -319.776 8.45164 328.374 -320.359 9.11923 316.866 +-320.398 9.14163 317.845 -319.878 8.67792 318.694 -320.246 9.13805 318.663 +-319.827 9.20093 319.251 -320.396 9.19484 328.7 -319.863 9.32785 328.201 +-319.978 9.7567 319.769 -320.296 10.3117 319.983 -319.598 9.76053 320.78 +-320.129 10.2945 320.913 -319.631 10.1793 321.404 -319.664 10.2785 327.5 +-319.8 9.78327 327.783 -320.142 10.0678 327.933 -320.405 11.2467 318.889 +-320.032 10.6811 321.726 -319.718 11.1179 322.342 -319.785 11.3409 324.474 +-319.625 10.9146 324.65 -320.159 11.1531 324.982 -319.742 10.7455 325.604 +-320.214 11.2258 325.804 -319.827 10.8268 326.742 -320.127 11.3064 326.672 +-319.922 10.7106 328.051 -319.844 11.2395 327.241 -319.966 10.895 328.81 +-319.728 11.3704 329.165 -320.402 12.3089 318.059 -320.185 12.3235 318.902 +-320.303 12.1442 319.724 -320.337 12.3389 320.789 -320.347 12.2119 321.971 +-319.918 11.6897 322.759 -320.188 12.2414 323.012 -319.75 11.8054 323.731 +-320.124 12.4501 323.659 -319.864 11.6572 325.021 -319.808 12.1887 324.231 +-319.842 11.7391 326.002 -319.746 11.6095 326.922 -320.26 12.2376 329.892 +-319.821 12.165 329.44 -320.439 12.2155 330.61 -320.323 13.3141 318.776 +-320.232 13.2008 319.787 -320.265 13.2214 320.832 -320.297 13.2598 321.848 +-320.245 13.174 322.833 -319.734 12.6313 323.98 -319.888 13.1786 323.435 +-319.824 12.8336 329.933 -320.249 13.0583 330.795 -319.683 13.423 330.387 +-320.418 14.4177 319.016 -320.237 14.2244 319.784 -320.288 14.2212 320.831 +-320.338 14.2735 321.872 -320.228 14.3064 322.731 -319.797 14.0518 323.279 +-319.935 13.8476 331.088 -319.778 14.2891 331.386 -320.227 14.214 331.769 +-320.382 15.2932 319.907 -320.261 15.227 320.828 -320.335 15.2622 321.882 +-320.239 15.3114 322.773 -319.79 15.0615 323.311 -319.722 14.6685 331.731 +-319.969 15.1257 332.288 -320.423 16.2819 307.688 -320.393 16.2883 310.743 +-320.255 16.3021 311.728 -320.395 16.3446 312.827 -320.389 16.3324 313.826 +-320.431 16.3002 314.767 -320.403 16.3 320.96 -320.301 16.2587 321.886 +-320.229 16.2902 322.788 -319.802 16.049 323.353 -319.843 15.7638 332.665 +-319.865 16.263 333.284 -319.761 16.9196 306.979 -319.808 17.3308 306.356 +-319.979 16.7278 307.941 -319.811 16.8703 308.961 -319.915 16.8458 309.828 +-319.858 16.7079 310.937 -319.84 16.8382 311.92 -320.135 17.0143 312.674 +-319.744 17.3769 312.407 -320.195 17.1391 313.611 -320.187 17.2176 314.735 +-320.425 17.1808 315.738 -320.401 17.2811 321.78 -320.165 17.3246 322.577 +-319.797 16.9706 323.295 -319.892 16.7409 333.744 -319.725 17.4864 334.27 +-319.687 17.807 305.833 -320.017 18.2827 305.446 -319.819 17.7042 313.096 +-319.791 17.7304 314.036 -319.843 17.8328 314.883 -320.159 18.1279 315.779 +-319.707 18.3321 315.383 -320.379 18.2022 316.777 -320.419 18.3244 317.759 +-320.297 18.3798 321.931 -319.71 17.6639 322.928 -319.835 18.1781 322.409 +-319.869 18.3073 334.371 -319.76 19.258 305.384 -319.861 18.7991 315.962 +-320.17 19.1502 316.787 -319.711 19.3753 316.414 -320.273 19.2383 317.835 +-320.393 19.1761 318.725 -320.292 19.4915 321.788 -319.746 18.9524 322.301 +-319.741 19.1762 334.36 -319.889 20.3228 305.304 -319.858 19.8016 316.988 +-320.222 20.1786 317.791 -319.792 20.4949 317.301 -320.271 20.2392 318.832 +-320.352 20.204 319.752 -320.377 20.2378 320.876 -320.315 20.3219 321.948 +-319.826 19.6548 322.643 -320.201 20.3125 322.905 -319.725 20.1146 323.267 +-319.755 20.1874 334.362 -319.744 21.2048 305.31 -320.236 21.2063 317.911 +-319.805 21.4351 317.321 -320.325 21.2364 318.824 -320.272 21.2615 319.843 +-320.27 21.2505 320.84 -320.311 21.2196 321.851 -320.379 21.2196 322.899 +-319.925 20.6952 323.763 -320.236 21.3157 323.815 -319.617 21.1703 324.271 +-319.752 21.2027 334.392 -319.752 22.217 305.327 -320.183 22.1556 317.881 +-319.737 22.2872 317.423 -320.366 22.2508 318.812 -320.331 22.29 319.874 +-320.306 22.2957 320.905 -320.314 22.1508 321.772 -320.249 22.3094 322.661 +-320.222 22.1626 323.633 -319.628 22.1806 324.231 -319.827 22.2874 334.309 +-319.797 23.2316 305.343 -319.876 22.7929 317.913 -320.212 23.1644 318.758 +-319.72 23.3694 318.381 -320.372 23.3252 319.865 -320.348 23.21 320.782 +-320.205 23.27 321.727 -319.822 22.7963 323.131 -319.855 23.1036 322.404 +-319.854 22.6853 323.793 -319.948 23.4421 333.459 -319.495 22.7761 333.852 +-320.244 23.1219 333.912 -319.849 24.2668 305.329 -319.875 23.8236 318.956 +-320.191 24.0951 319.665 -319.733 24.4122 319.381 -320.214 24.1684 320.715 +-319.707 23.6678 321.934 -319.873 24.1724 321.384 -319.751 24.1988 333.287 +-319.876 25.3112 305.295 -319.811 24.758 320.073 -319.736 24.6719 320.845 +-319.765 25.214 333.322 -319.613 26.2042 305.397 -319.789 26.2231 333.34 +-319.986 26.7766 305.826 -319.734 27.3258 306.258 -319.798 27.2272 333.337 +-319.773 28.2066 306.366 -319.832 28.2483 333.322 -319.8 29.2294 306.333 +-319.863 29.275 333.297 -319.789 30.2549 306.355 -319.61 30.3123 333.389 +-319.86 31.3278 306.29 -319.875 31.3093 320.285 -319.734 30.8179 320.82 +-320.213 31.3255 320.944 -319.806 30.7491 321.586 -320.186 31.4087 321.988 +-319.723 31.1087 322.293 -320.06 30.8049 333.821 -319.66 31.4736 334.243 +-319.531 31.8131 305.836 -319.956 32.4565 305.464 -320.253 32.14 305.915 +-319.869 32.3438 319.266 -319.723 31.7934 319.728 -320.218 32.1945 319.939 +-320.354 32.2784 320.879 -320.366 32.1655 321.772 -319.881 31.613 322.79 +-320.272 32.2191 322.922 -319.81 31.8089 323.596 -320.151 32.4387 323.859 +-319.73 32.1731 324.229 -319.843 32.2978 334.423 -319.774 33.2292 305.276 +-319.865 33.3486 318.263 -319.728 32.8011 318.716 -320.2 33.2271 318.938 +-320.325 33.3211 319.872 -320.315 33.203 320.756 -320.309 33.2185 321.772 +-320.333 33.2254 322.794 -320.256 33.2973 323.872 -319.81 32.9621 324.376 +-319.863 33.2468 334.267 -319.797 34.2692 305.274 -319.863 34.3653 317.273 +-319.728 33.8068 317.713 -320.182 34.2482 317.977 -320.289 34.3571 318.918 +-320.285 34.2423 319.783 -320.269 34.2637 320.81 -320.251 34.2796 321.838 +-320.259 34.2826 322.844 -320.307 34.2796 323.854 -319.866 33.6898 324.671 +-320.173 34.3774 324.916 -319.69 34.1338 325.259 -319.823 34.5035 333.376 +-319.762 33.7903 333.725 -320.174 34.1506 333.931 -319.753 35.1714 305.428 +-319.862 35.3752 316.275 -319.731 34.8142 316.719 -320.164 35.172 316.924 +-320.408 35.3096 317.832 -320.398 35.1146 318.832 -320.379 35.156 319.848 +-320.365 35.1752 320.88 -320.351 35.1826 321.901 -320.347 35.1794 322.895 +-320.384 35.1689 323.88 -320.432 35.1674 324.895 -319.833 34.7147 325.754 +-320.242 35.1556 325.718 -319.816 35.2317 326.278 -320.279 34.9991 333.855 +-319.759 35.4532 333.326 -320.354 36.4036 305.89 -319.927 35.8282 305.749 +-319.711 36.1965 306.2 -319.865 36.3818 315.256 -319.735 35.8183 315.704 +-320.158 36.193 315.918 -320.441 36.1199 316.874 -320.001 35.7839 326.741 +-320.393 36.258 327.086 -319.614 35.7838 327.789 -320.258 36.2575 327.766 +-319.811 36.2388 328.369 -320.304 35.9461 333.859 -319.876 36.4761 333.358 +-319.701 37.233 306.398 -319.849 37.3651 314.251 -319.729 36.805 314.688 +-320.146 37.2041 314.917 -320.417 37.1598 315.869 -319.796 36.7222 328.836 +-320.247 37.503 328.932 -319.649 37.1763 329.363 -319.972 37.4085 333.243 +-320.237 38.4785 306.831 -319.859 37.8526 306.765 -319.57 38.2111 307.136 +-319.784 38.2275 313.322 -319.78 37.7283 313.817 -320.298 38.2761 313.922 +-320.429 38.0427 314.998 -320.445 38.2149 328.993 -319.859 37.6001 329.702 +-320.277 38.2765 329.914 -319.802 37.7538 330.561 -320.186 38.3908 331.043 +-319.797 37.7786 331.608 -320.157 38.275 332.016 -319.757 37.7151 332.621 +-320.281 38.2584 332.807 -320.411 39.2826 306.879 -319.735 39.0931 307.311 +-319.808 39.2343 312.317 -319.8 38.7296 312.825 -320.317 39.2444 312.781 +-320.449 39.1877 330.882 -320.43 39.014 331.908 -319.867 40.1818 307.42 +-319.805 40.2903 311.336 -319.814 39.7618 311.804 -320.207 40.2261 311.855 +-320.294 41.4142 308.055 -319.863 40.7556 307.748 -319.741 40.7406 308.57 +-320.131 41.2621 308.966 -319.79 40.7641 309.634 -320.291 41.3667 309.934 +-319.806 40.7498 310.597 -320.295 41.1521 311.08 -320.47 41.0995 311.836 +-320.43 41.9859 308.894 -318.64 -13.2342 328.979 -318.872 -12.7439 328.403 +-319.404 -12.9027 328.955 -318.978 -13.2479 329.824 -318.596 -12.8141 330.347 +-318.713 -12.2872 327.853 -318.935 -11.706 327.302 -318.826 -12.3779 330.946 +-318.767 -12.2934 331.84 -318.734 -12.3443 332.874 -319.346 -11.7984 333.874 +-318.952 -12.1634 333.669 -318.625 -11.7591 334.331 -318.758 -11.1437 326.873 +-318.85 -10.6688 326.402 -319.405 -10.6396 334.975 -318.971 -11.1902 334.734 +-318.729 -10.6838 335.293 -318.69 -10.1497 325.903 -319.074 -9.7827 325.384 +-318.807 -9.725 335.469 -318.655 -9.17203 323.99 -318.894 -8.70214 323.438 +-318.749 -9.39609 324.85 -318.879 -8.77358 335.243 -319.221 -7.64191 322.762 +-318.967 -8.00915 323.14 -318.904 -7.71624 334.453 -318.741 -8.31549 334.781 +-318.483 -7.29665 322.803 -319.039 -6.91354 322.319 -318.66 -6.84831 334.393 +-318.591 -6.28817 321.804 -319.042 -5.78994 321.337 -319.257 -5.52681 333.798 +-318.909 -5.96956 334.197 -318.6 -5.31718 320.816 -318.919 -4.70996 320.334 +-318.893 -4.87279 333.354 -318.65 -5.36433 333.794 -318.726 -4.16547 319.895 +-318.935 -3.58903 319.472 -318.882 -3.74468 332.323 -318.592 -4.24485 332.938 +-318.942 -2.75046 319.196 -318.923 -2.51741 331.381 -318.709 -3.21998 331.71 +-319.209 -2.91171 331.921 -318.576 -2.26649 318.814 -318.933 -1.74165 318.457 +-319.013 -1.63919 331.159 -319.215 -0.610777 317.779 -318.839 -1.02867 318.192 +-318.748 -0.631114 330.419 -318.785 -1.31211 330.833 -319.174 -0.858012 330.888 +-318.516 -0.287589 317.855 -319.031 0.122512 317.35 -319.297 0.0499602 330.814 +-318.761 0.524787 330.312 -319.269 1.31251 315.695 -318.759 1.2356 316.173 +-318.712 0.71076 316.719 -319.319 1.04323 330.863 -318.868 1.33872 330.349 +-318.647 1.77494 315.848 -319.008 2.04195 315.443 -318.831 2.31622 328.415 +-318.758 1.74178 328.89 -319.365 2.13823 328.917 -318.857 1.63853 329.768 +-319.279 3.54963 314.793 -318.873 3.01835 315.304 -318.796 3.32952 328.237 +-319.331 4.2034 314.779 -318.836 4.08814 315.305 -319.037 4.42108 328.178 +-318.77 4.75295 315.788 -319.238 5.49333 315.917 -318.634 5.16392 316.353 +-318.709 5.29428 327.485 -318.689 4.67855 327.768 -319.204 5.19287 327.89 +-319.415 6.38966 316.023 -318.873 5.57007 316.735 -319.225 6.24687 316.929 +-318.861 5.79248 317.609 -319.178 6.48794 317.896 -318.66 6.12057 318.243 +-319.341 6.04381 327.806 -318.702 6.22567 327.432 -319.451 7.15635 316.883 +-319.346 7.10535 317.669 -318.79 7.09976 318.351 -319.323 7.03797 327.816 +-318.703 7.31876 327.344 -319.016 7.74217 318.697 -319.329 8.36892 319.104 +-318.776 7.75706 319.52 -319.122 8.45376 320.009 -318.733 8.06565 320.246 +-319.288 8.01698 327.857 -318.769 8.46799 327.372 -319.438 9.1455 319.916 +-318.862 8.68672 320.733 -319.22 9.14184 320.681 -318.825 9.21444 321.269 +-319.356 9.20901 327.722 -318.817 9.32145 327.236 -318.976 9.747 321.736 +-319.286 10.3779 322.098 -318.753 9.78766 322.534 -319.086 10.4533 322.965 +-318.692 10.1287 323.231 -318.926 10.346 325.388 -318.701 9.9186 325.743 +-319.263 10.2464 326.132 -318.762 9.70717 326.573 -319.279 10.1504 327.038 +-319.409 11.1166 322.878 -318.938 10.6622 323.872 -319.38 11.1694 323.862 +-318.896 10.6319 324.76 -319.418 11.2736 327.654 -319.404 11.1849 328.662 +-319.433 12.2234 324.608 -319.375 12.3441 325.774 -319.358 12.2483 326.917 +-319.245 12.3762 328.008 -319.32 12.1166 328.89 -319.388 13.3521 323.963 +-319.234 13.3324 324.946 -319.258 13.2346 325.854 -319.284 13.2673 326.866 +-319.291 13.133 327.743 -319.209 13.3664 328.758 -319.414 13.2096 329.744 +-319.352 14.4461 323.872 -319.256 14.2101 324.802 -319.303 14.2598 325.878 +-319.326 14.1673 326.778 -319.2 14.2412 327.716 -318.858 13.8871 328.967 +-318.858 14.0689 328.417 -319.12 14.0122 329.667 -318.706 14.32 329.387 +-319.349 14.1901 330.67 -319.366 15.4429 323.904 -319.276 15.2197 324.807 +-319.325 15.1427 325.727 -319.217 15.2828 326.697 -318.709 14.6421 327.95 +-318.865 15.1373 327.384 -318.765 14.6726 330.103 -318.902 14.8523 330.989 +-318.652 15.3504 331.37 -319.231 15.2399 331.829 -319.381 16.429 323.94 +-319.292 16.224 324.846 -319.234 16.2093 325.851 -318.714 15.6958 326.897 +-318.864 16.236 326.417 -318.893 15.7857 332.036 -318.741 16.2396 332.334 +-319.262 16.2507 332.856 -319.323 17.2016 306.835 -319.241 17.2027 307.552 +-319.262 17.2145 308.587 -319.273 17.2076 309.597 -319.276 17.2959 310.74 +-319.451 17.3269 311.79 -319.367 17.4315 323.769 -319.31 17.2211 324.876 +-319.22 17.2878 325.768 -318.794 17.0628 326.308 -318.777 16.744 332.853 +-318.805 17.2416 333.382 -319.237 17.171 333.791 -319.323 18.4323 305.775 +-318.972 18.0716 306.268 -318.827 17.6978 306.965 -318.739 17.7078 308.025 +-318.754 17.697 309.034 -318.753 17.681 310.037 -318.835 17.8338 310.888 +-319.12 17.9991 311.663 -318.703 18.3521 311.379 -319.309 18.199 312.684 +-319.403 18.3484 313.801 -319.423 18.3137 314.751 -319.385 18.3538 322.931 +-319.28 18.3462 323.935 -319.319 18.0921 324.74 -319.195 18.3368 325.598 +-318.796 17.9873 326.28 -318.731 17.6156 333.754 -318.933 18.3195 334.262 +-319.157 19.2359 305.73 -318.618 19.1285 306.305 -318.775 18.6713 312.088 +-318.851 18.8507 312.865 -319.166 19.1258 313.755 -318.707 19.3364 313.378 +-319.273 19.2392 314.839 -319.39 19.185 315.756 -319.406 19.4064 322.985 +-319.269 19.2706 323.795 -319.221 19.1996 324.855 -318.723 18.6893 325.955 +-318.875 19.2328 325.472 -318.855 19.399 334.278 -318.665 19.7786 305.873 +-318.922 20.2223 305.417 -318.863 19.7806 313.963 -319.167 20.1522 314.814 +-318.709 20.36 314.444 -319.265 20.2695 315.894 -319.411 20.0787 316.666 +-319.384 20.2855 323.94 -319.15 20.1111 324.642 -318.778 20.0134 325.281 +-318.848 20.4092 334.274 -318.857 21.3229 305.297 -318.863 20.8085 315.032 +-319.238 21.302 315.744 -318.881 21.3783 315.177 -319.381 21.0295 316.779 +-318.86 20.7247 324.782 -318.965 21.4431 324.436 -318.852 21.4398 334.297 +-318.725 22.2086 305.31 -318.753 22.4142 314.471 -318.745 21.8354 314.698 +-319.12 22.0298 315.012 -319.222 22.3423 315.798 -319.395 22.142 316.835 +-318.747 22.0206 324.476 -318.956 22.235 334.199 -318.753 23.2271 305.331 +-318.809 22.6781 315.036 -318.859 22.8343 315.863 -319.214 23.1704 316.777 +-318.775 23.4795 316.276 -319.42 23.184 317.726 -319.398 23.4618 322.733 +-319.398 23.1979 323.964 -318.951 22.7335 324.657 -319.122 23.4531 324.865 +-318.732 23.1825 325.209 -318.967 23.3095 333.429 -318.647 22.6756 333.692 +-318.812 24.2587 305.324 -319.157 24.1572 316.932 -318.712 24.2901 316.473 +-319.3 24.2362 317.835 -319.405 24.2006 318.737 -319.375 24.3661 321.793 +-319.255 24.2806 322.911 -319.299 24.204 323.824 -319.226 24.266 324.751 +-318.83 24.0448 325.361 -318.727 24.1919 333.349 -318.855 25.294 305.288 +-318.874 24.7876 316.957 -319.235 25.1752 317.776 -318.788 25.501 317.291 +-319.284 25.2531 318.837 -319.357 25.2136 319.76 -319.357 25.2423 320.857 +-319.254 25.3208 321.895 -319.274 25.1696 322.797 -319.316 25.2204 323.853 +-319.225 25.2878 324.75 -318.834 25.0505 325.344 -318.749 25.1959 333.374 +-318.594 26.1852 305.392 -319.224 26.1945 317.921 -318.788 26.4378 317.352 +-319.324 26.2364 318.822 -319.279 26.2568 319.835 -319.266 26.2422 320.807 +-319.269 26.2212 321.794 -319.294 26.2081 322.818 -319.337 26.2529 323.866 +-319.23 26.3079 324.768 -318.81 26.0516 325.331 -318.761 26.1894 333.368 +-318.986 26.7585 305.83 -318.74 27.3181 306.267 -319.229 27.1812 317.901 +-318.801 27.4398 317.33 -319.341 27.22 318.796 -319.307 27.2509 319.827 +-319.289 27.2488 320.836 -319.289 27.2411 321.827 -319.306 27.2338 322.833 +-319.34 27.268 323.865 -319.23 27.3132 324.763 -318.794 27.057 325.329 +-318.758 27.1913 333.365 -318.78 28.2099 306.369 -319.231 28.1888 317.892 +-318.807 28.4353 317.318 -319.337 28.2269 318.791 -319.301 28.251 319.823 +-319.29 28.2436 320.831 -319.296 28.2424 321.828 -319.307 28.2391 322.83 +-319.334 28.2592 323.851 -319.224 28.2973 324.743 -318.785 28.0586 325.312 +-318.784 28.2033 333.356 -318.796 29.2347 306.351 -319.227 29.2039 317.906 +-318.8 29.4241 317.333 -319.324 29.2561 318.811 -319.281 29.273 319.855 +-319.264 29.254 320.859 -319.267 29.2462 321.828 -319.282 29.2382 322.815 +-319.323 29.2369 323.832 -319.247 29.3208 324.896 -318.786 28.9929 325.371 +-318.826 29.2128 333.336 -318.808 30.2634 306.377 -319.21 30.2213 317.936 +-318.788 30.4281 317.371 -319.309 30.2809 318.84 -319.27 30.1713 319.752 +-319.354 30.2528 320.808 -319.349 30.2983 321.912 -319.242 30.2549 322.838 +-319.281 30.2273 323.821 -319.341 30.2436 324.843 -318.871 29.706 325.705 +-319.174 30.328 325.748 -318.713 30.1906 326.197 -318.703 30.082 333.336 +-318.883 31.1614 306.209 -319.209 31.3316 317.847 -318.854 31.2795 317.25 +-319.296 31.1496 318.729 -319.391 31.1106 319.861 -319.419 31.1578 322.924 +-319.348 31.2924 323.898 -319.301 31.2213 324.83 -319.253 31.302 325.902 +-318.797 30.9802 326.401 -319.336 31.1089 333.758 -318.601 31.2212 333.471 +-318.744 31.6124 305.754 -318.869 32.2267 305.36 -318.872 32.3714 316.267 +-318.715 31.8052 316.753 -319.183 32.2823 316.991 -319.29 32.2708 317.846 +-319.398 32.1126 318.835 -319.361 32.3151 324.813 -319.323 32.2275 325.825 +-318.88 31.707 326.718 -319.175 32.3225 326.744 -318.716 32.1966 327.198 +-318.843 31.6653 333.954 -318.99 32.3127 334.24 -318.757 33.2406 305.253 +-318.798 33.5176 315.398 -318.734 32.8155 315.69 -319.171 33.1939 316.085 +-319.304 33.2678 316.8 -319.407 33.1065 317.801 -319.41 33.4071 325.009 +-319.265 33.2297 325.8 -319.238 33.3065 326.899 -318.803 32.9772 327.395 +-318.707 33.3279 332.364 -318.743 32.9309 332.909 -319.146 33.2446 333.04 +-318.614 32.7271 333.592 -319.303 33.2285 333.775 -318.732 34.1559 305.435 +-319.194 34.2411 315.729 -318.859 34.3084 315.247 -319.417 34.095 316.791 +-319.371 34.3102 325.934 -319.281 34.2791 326.858 -318.881 33.7021 327.726 +-319.15 34.3461 327.774 -318.709 34.1882 328.219 -319.346 33.9909 332.97 +-318.792 34.4762 332.366 -319.368 35.3696 305.903 -318.923 34.8026 305.766 +-318.72 35.1835 306.214 -318.87 35.4044 314.278 -318.722 34.8265 314.76 +-319.161 35.2039 314.948 -319.435 35.1617 315.895 -319.412 35.1885 326.932 +-319.28 35.104 327.695 -318.85 35.0744 328.371 -319.29 35.1328 332.726 +-318.837 35.3229 332.245 -318.784 36.1749 306.39 -318.772 36.5346 313.403 +-318.728 35.8423 313.689 -319.134 36.0862 314.044 -319.441 36.1473 314.88 +-318.947 35.8038 328.709 -319.25 36.2242 328.881 -318.75 36.1918 329.244 +-318.825 36.4344 331.278 -318.696 35.8529 331.759 -319.134 36.2994 332.01 +-319.363 36.1026 332.824 -318.789 37.0156 306.448 -319.354 37.1514 313.838 +-318.818 37.3008 313.267 -318.946 36.7763 329.716 -319.324 37.2716 330.079 +-318.73 36.7557 330.559 -319.281 37.1442 331.055 -319.393 37.2777 331.989 +-319.388 36.9801 332.76 -319.036 37.672 306.791 -318.861 38.2422 307.314 +-318.827 38.3706 312.329 -318.685 37.8033 312.785 -319.275 38.2203 312.877 +-319.284 39.3986 307.959 -318.766 38.7452 307.924 -318.674 38.901 308.752 +-319.16 39.4451 308.959 -318.675 39.1855 309.385 -318.82 39.4383 311.287 +-318.691 38.8347 311.734 -319.288 39.2522 311.858 -319.434 40.2217 307.947 +-319.392 40.1087 308.805 -318.885 39.6593 309.744 -319.308 40.1915 309.94 +-318.775 39.779 310.565 -319.285 40.0882 310.973 -317.714 -13.1721 328.983 +-318.028 -12.8342 328.29 -318.028 -13.2036 329.717 -317.758 -12.6558 330.368 +-317.502 -12.3131 327.861 -317.899 -11.7657 327.374 -317.896 -12.2396 330.974 +-317.869 -12.1417 331.856 -317.808 -12.2006 332.893 -317.976 -12.164 333.831 +-317.774 -11.8291 334.374 -318.288 -10.7451 326.815 -317.761 -11.045 327.253 +-317.807 -11.331 334.769 -317.853 -10.5367 335.284 -318.294 -9.69056 325.707 +-317.942 -9.92944 326.279 -317.81 -10.3641 326.884 -317.989 -9.58342 335.244 +-318.203 -8.67843 323.708 -317.884 -8.91234 324.209 -317.862 -9.16601 324.847 +-317.763 -9.39162 325.763 -317.809 -8.67028 334.482 -317.606 -9.34957 334.792 +-318.209 -8.85473 334.913 -317.654 -8.37697 323.927 -318.038 -7.90244 323.343 +-317.765 -7.70221 334.279 -317.746 -7.23448 322.898 -317.823 -6.63566 322.348 +-317.993 -6.58093 334.225 -317.742 -6.16932 321.842 -317.863 -5.68913 321.388 +-317.812 -5.54295 333.441 -317.741 -6.2586 333.875 -318.196 -5.83677 333.947 +-317.759 -5.2033 320.85 -317.861 -4.70205 320.469 -317.735 -4.80644 331.299 +-317.737 -5.01257 331.825 -318.177 -4.48581 331.888 -317.6 -4.90692 332.338 +-317.908 -4.66545 333.16 -318.217 -3.65944 319.762 -317.83 -3.87125 320.19 +-317.785 -3.45631 330.391 -317.751 -4.16656 330.718 -318.165 -3.95265 331.046 +-318.45 -3.723 331.626 -317.958 -4.3772 332.699 -317.642 -3.17473 319.913 +-317.87 -2.76055 319.406 -318.277 -3.03037 330.971 -317.751 -2.52183 330.359 +-317.718 -2.16148 318.878 -317.925 -1.57433 318.463 -318.351 -1.84672 330.727 +-317.83 -1.68913 330.253 -317.926 -0.736606 318.214 -317.806 -0.472042 329.393 +-317.73 -1.16632 329.743 -318.18 -0.944087 330.023 -317.577 -0.231894 317.836 +-317.887 0.340905 317.33 -318.276 0.163919 329.714 -317.842 0.311163 329.173 +-317.69 0.854689 316.886 -317.97 1.19644 316.512 -317.847 1.40979 328.361 +-317.753 0.748707 328.796 -318.191 1.12603 329.07 -318.431 1.1216 329.802 +-318.291 2.52907 315.81 -317.907 1.97875 316.311 -317.994 2.39803 328.141 +-318.216 3.50562 315.82 -317.738 3.03159 316.308 -317.691 3.31254 327.503 +-317.648 2.6811 327.751 -318.155 3.17589 327.914 -318.317 4.20617 315.821 +-317.817 4.0735 316.315 -318.362 3.96189 327.848 -317.856 4.37001 327.327 +-317.946 4.74357 316.709 -318.34 5.22446 317.135 -317.761 4.79302 317.617 +-318.343 5.3772 317.925 -317.737 5.22266 318.251 -317.903 5.43839 327.221 +-317.767 6.21699 318.444 -318.006 6.36244 327.182 -318.328 7.24224 319.065 +-317.902 6.79527 318.773 -317.789 6.73086 319.544 -318.305 7.33346 320.021 +-317.688 6.69049 320.71 -318.182 7.42186 320.943 -317.725 7.13292 321.33 +-317.734 7.40639 326.457 -317.775 6.7056 326.86 -318.154 7.14157 326.917 +-318.438 8.27269 320.928 -317.906 7.65387 321.627 -318.154 8.33919 321.858 +-317.729 8.13039 322.225 -318.28 8.16397 326.774 -317.84 8.37019 326.245 +-318.426 9.13062 321.904 -317.839 8.64834 322.767 -318.265 9.31265 322.955 +-317.775 8.78151 323.619 -318.161 9.41292 324.047 -317.781 8.78841 324.637 +-318.146 9.30181 324.988 -317.776 8.75054 325.607 -318.267 9.24963 325.98 +-318.459 9.06329 326.878 -318.425 10.1924 323.859 -318.403 10.022 324.85 +-318.447 14.4445 328.72 -318.376 15.3547 327.82 -318.264 15.3081 328.977 +-318.362 15.1536 329.798 -318.415 15.3351 330.766 -318.394 16.3638 326.931 +-318.274 16.333 327.912 -318.288 16.0757 328.704 -318.206 16.2835 329.614 +-318.16 16.1152 330.608 -318.325 16.1397 331.642 -318.373 17.433 326.905 +-318.287 17.2231 327.814 -318.223 17.17 328.835 -317.769 16.7714 330.066 +-317.866 17.1897 329.43 -317.741 16.6987 331.013 -317.901 16.8242 331.957 +-317.65 17.3384 332.366 -318.265 17.2252 332.828 -318.419 18.5027 306.905 +-318.368 18.3378 307.81 -318.388 18.329 308.817 -318.374 18.3097 309.811 +-318.423 18.3086 310.749 -318.365 18.4378 326.787 -318.308 18.2269 327.876 +-318.222 18.2885 328.772 -317.797 18.0447 329.319 -317.908 17.772 333.073 +-317.741 18.5224 333.327 -318.251 18.2018 333.759 -318.273 19.1662 306.898 +-318.168 19.1336 307.596 -318.181 19.1153 308.601 -318.206 19.2172 309.729 +-318.287 19.2218 310.827 -318.352 19.1837 311.778 -318.408 19.3292 312.766 +-318.407 19.3686 326.01 -318.288 19.3371 326.998 -318.319 19.0938 327.77 +-318.202 19.3374 328.632 -317.806 18.99 329.308 -318.246 18.9805 333.893 +-317.75 19.482 333.4 -318.186 20.3542 305.73 -318.002 20.0728 306.266 +-317.883 19.7434 307.021 -317.792 19.7323 308.058 -317.794 19.711 309.052 +-317.859 19.8322 309.884 -318.183 20.1315 310.781 -317.72 20.3315 310.387 +-318.312 20.2693 311.848 -318.243 20.2721 312.865 -318.374 20.1852 313.769 +-318.354 20.463 325.776 -318.29 20.0931 326.726 -318.207 20.2292 327.72 +-317.738 19.6791 328.985 -317.885 20.1401 328.425 -318.251 19.9843 333.887 +-317.764 20.4828 333.394 -317.665 20.6638 305.924 -317.869 21.2454 305.392 +-317.876 20.7887 310.956 -318.202 21.1637 311.767 -317.738 21.3677 311.392 +-318.308 21.3086 312.858 -318.237 21.1584 313.767 -318.445 21.1597 314.613 +-318.377 21.3543 324.972 -318.265 21.2182 325.813 -318.184 21.2218 326.671 +-317.731 20.6795 327.938 -317.873 21.135 327.379 -318.258 21.1262 333.784 +-317.855 21.3199 333.222 -317.824 22.3423 305.279 -317.879 21.8091 311.943 +-318.191 22.1699 312.758 -317.736 22.3764 312.387 -318.379 22.1751 313.959 +-318.415 22.394 325.008 -318.179 22.1593 325.791 -317.722 21.6904 326.903 +-317.853 22.2085 326.407 -317.714 22.4106 332.503 -317.732 21.8368 332.694 +-318.113 22.206 333.122 -318.458 22.0369 333.885 -317.706 23.2179 305.292 +-317.867 22.8174 312.948 -318.164 23.1627 313.766 -317.713 23.3789 313.417 +-318.377 23.2198 314.743 -318.455 23.2145 315.606 -318.283 23.3559 325.714 +-317.759 23.037 326.254 -317.762 22.7662 332.809 -317.942 23.3298 333.235 +-317.773 24.2372 305.291 -317.85 23.8015 313.99 -318.198 24.1994 314.791 +-317.806 24.5 314.298 -318.342 24.1549 315.848 -318.309 24.4897 325.969 +-317.755 23.9664 326.344 -317.834 24.4575 333.302 -317.822 25.2714 305.261 +-318.22 25.2408 314.931 -317.829 25.4413 314.311 -318.272 25.2825 315.87 +-318.417 25.091 316.646 -318.415 25.4516 325.916 -317.845 24.6655 326.689 +-318.136 25.2789 326.711 -317.714 25.1675 327.193 -317.859 25.4274 333.29 +-317.56 26.1747 305.372 -318.175 26.1766 314.903 -317.75 26.292 314.429 +-318.305 26.2873 315.846 -318.365 26.0593 316.773 -318.387 26.4267 325.892 +-318.21 26.2461 326.848 -317.792 25.9604 327.38 -317.861 26.4115 333.27 +-317.978 26.7622 305.828 -317.743 27.3376 306.259 -317.877 26.8025 314.943 +-318.207 27.2199 315.792 -317.783 27.5206 315.305 -318.387 27.0545 316.76 +-318.36 27.4298 325.881 -318.292 27.1688 326.794 -317.863 26.6821 327.696 +-318.203 27.3343 327.909 -317.735 27.1476 328.303 -317.854 27.411 333.263 +-317.779 28.2254 306.363 -318.172 28.2489 315.977 -317.789 28.4525 315.376 +-318.395 28.0358 316.749 -318.341 28.4394 325.882 -318.265 28.1697 326.787 +-318.362 28.2239 327.865 -317.919 27.738 328.776 -318.231 28.3604 328.801 +-317.613 28.1425 329.214 -317.872 28.4141 333.262 -317.81 29.244 306.359 +-318.2 29.3265 315.846 -317.882 29.2737 315.211 -318.394 29.0137 316.738 +-318.409 29.4012 326.018 -318.242 29.191 326.79 -318.328 29.2287 327.849 +-318.285 29.2969 328.777 -317.714 29.0529 329.362 -317.914 29.4077 333.259 +-317.883 30.2953 306.272 -317.812 30.4868 314.38 -317.747 29.7762 314.711 +-318.176 30.1736 315.103 -318.283 30.4009 315.98 -318.38 30.0214 316.752 +-318.339 30.3117 326.805 -318.291 30.2283 327.82 -318.24 30.298 328.738 +-317.857 30.0822 329.341 -317.903 30.4457 333.254 -317.535 30.7529 305.815 +-317.947 31.4248 305.434 -318.26 31.1105 305.906 -318.221 31.3317 314.804 +-317.874 31.2697 314.175 -318.292 31.139 315.708 -318.405 31.1152 316.704 +-318.421 31.4085 327.039 -318.265 31.2183 327.798 -318.222 31.3153 328.854 +-317.82 31.0284 329.359 -317.725 31.4313 333.235 -317.731 32.199 305.229 +-317.798 32.5106 313.39 -317.725 31.7918 313.683 -318.162 32.1822 314.091 +-318.304 32.277 314.807 -318.409 32.1113 315.802 -318.346 32.315 327.8 +-318.295 32.2127 328.789 -317.879 31.6511 329.724 -318.237 32.2445 329.905 +-317.714 31.7528 330.728 -318.232 32.3188 331.046 -317.808 31.9356 331.649 +-318.204 32.4441 331.935 -317.583 32.1097 332.369 -318.399 32.1454 333.835 +-317.929 32.2008 333.37 -317.693 33.1385 305.42 -318.201 33.326 313.837 +-317.865 33.2849 313.225 -318.385 33.0212 314.876 -318.425 33.396 328.035 +-318.26 33.2071 328.79 -318.303 33.2436 329.822 -318.32 33.2517 330.844 +-318.396 33.086 331.624 -318.047 32.6372 332.797 -318.367 34.3661 305.912 +-317.923 33.8077 305.77 -317.717 34.1953 306.213 -317.883 34.3519 312.231 +-317.731 33.7838 312.728 -318.18 34.2866 312.982 -318.282 34.2568 313.852 +-318.422 34.1089 314.687 -318.323 34.326 328.814 -318.252 34.2642 329.827 +-318.27 34.3075 330.889 -318.394 34.059 331.763 -317.725 35.2361 306.393 +-317.77 35.3662 311.394 -317.824 34.7165 311.773 -318.179 35.2009 312.042 +-318.307 35.2798 312.778 -318.414 35.1131 313.8 -318.351 35.421 329.06 +-318.203 35.2869 329.839 -318.237 35.19 330.759 -318.396 35.1628 331.733 +-318.287 36.4871 306.922 -317.902 35.8555 306.72 -317.517 36.2382 307.269 +-318.218 36.3442 311.801 -317.89 36.2821 311.166 -318.388 36.0407 312.85 +-318.34 36.181 329.9 -318.368 36.0297 330.927 -318.44 37.3973 306.983 +-317.727 36.7464 307.854 -318.162 37.2989 307.69 -317.686 37.2844 308.334 +-317.764 37.5275 310.368 -317.737 36.7843 310.675 -318.164 37.1928 311.099 +-318.301 37.2665 311.835 -318.412 37.1233 312.676 -318.258 38.1652 307.804 +-317.763 38.0399 308.38 -318.164 38.2785 310.72 -317.801 38.3548 310.204 +-318.397 38.0959 311.82 -317.956 38.6399 308.674 -317.869 38.7466 309.568 +-318.317 39.1651 310.122 -318.422 39.0895 311.006 -317.353 -12.6606 328.842 +-317.397 -12.7042 329.795 -316.695 -12.1632 327.929 -316.976 -11.75 327.455 +-316.914 -12.2167 328.726 -317.11 -11.988 329.682 -316.703 -11.6511 329.392 +-317.274 -11.8828 330.619 -317.272 -11.746 331.608 -317.225 -11.7899 332.62 +-317.289 -11.7768 333.759 -317.206 -10.4871 327.695 -316.842 -11.2295 327.734 +-316.651 -10.8781 328.193 -316.782 -11.2949 330.101 -316.78 -11.2508 331.013 +-316.75 -11.2806 332.014 -316.713 -11.3021 333.044 -316.867 -11.2078 334.038 +-316.695 -10.5299 334.326 -317.21 -10.8377 334.78 -317.369 -9.62465 326.857 +-317.205 -9.6242 327.984 -316.773 -10.0611 328.381 -317.275 -10.0909 334.898 +-316.858 -9.55027 334.385 -317.377 -8.5482 324.771 -317.322 -8.81761 325.901 +-317.261 -8.68426 326.917 -317.312 -8.81724 327.792 -316.873 -9.33292 328.675 +-317.198 -8.68483 328.867 -316.721 -8.87758 329.24 -316.965 -8.64051 334.215 +-317.375 -7.61049 323.839 -317.291 -7.66532 324.983 -317.285 -7.80492 325.839 +-317.287 -7.78633 326.814 -317.307 -7.79163 327.794 -317.346 -7.79662 328.817 +-316.875 -8.32112 329.705 -317.214 -7.66687 329.91 -316.734 -7.88491 330.292 +-316.767 -7.50962 333.46 -316.685 -8.21161 333.775 -317.125 -7.90917 333.957 +-317.331 -6.89318 322.945 -317.151 -6.76068 323.651 -317.207 -6.87021 324.62 +-317.221 -6.87759 325.639 -317.234 -6.75565 326.747 -317.327 -6.74184 327.835 +-317.322 -6.75659 328.84 -317.338 -6.758 329.862 -316.889 -7.30551 330.748 +-317.162 -6.66664 330.765 -316.711 -6.75999 331.235 -317.342 -6.84892 333.755 +-316.826 -6.70906 333.252 -317.301 -5.80556 321.796 -316.781 -5.7607 322.287 +-316.88 -6.25071 322.961 -316.822 -6.23103 324.043 -316.839 -6.22833 325.072 +-316.819 -6.24445 326.068 -316.885 -6.12895 326.921 -317.202 -5.84308 327.818 +-316.823 -5.78018 327.366 -317.321 -5.71074 328.875 -317.295 -5.71471 329.89 +-317.256 -5.66996 330.87 -316.754 -5.94576 331.407 -316.722 -5.67902 332.325 +-316.652 -6.1442 332.724 -317.155 -5.73622 332.849 -317.24 -4.68013 320.726 +-316.81 -4.77381 321.33 -316.786 -5.25646 321.83 -316.743 -5.2824 327.785 +-317.247 -4.77718 328.829 -316.718 -4.51835 328.316 -317.298 -4.82143 329.771 +-317.405 -4.76323 330.618 -317.063 -5.33556 331.825 -317.476 -4.90174 332.922 +-316.666 -4.20068 320.889 -316.87 -3.79689 320.467 -317.23 -3.75349 328.938 +-316.859 -3.67691 328.438 -317.398 -3.93973 329.912 -317.267 -2.68908 319.864 +-316.925 -2.91513 320.188 -317.223 -2.7328 328.901 -316.735 -2.78282 328.389 +-317.395 -2.93162 329.723 -317.203 -1.63929 318.762 -316.956 -1.91781 319.228 +-316.63 -2.28517 319.806 -317.208 -1.82921 328.785 -316.726 -1.74366 328.389 +-317.436 -1.84333 329.658 -316.508 -1.2948 318.855 -317.028 -0.890706 318.335 +-317.329 -0.940631 328.963 -316.726 -0.726191 328.422 -316.565 -0.244617 317.814 +-316.926 0.253704 317.445 -317.4 0.223264 328.683 -316.754 0.236412 328.27 +-317.321 1.51642 316.826 -316.822 1.01273 317.223 -316.814 1.44895 327.47 +-316.575 0.832624 327.797 -317.219 1.10062 327.947 -317.287 2.47687 316.819 +-316.692 2.05925 317.21 -317.343 1.96479 327.776 -316.836 2.291 327.397 +-317.265 3.48609 316.981 -316.684 2.97919 317.387 -316.912 3.30524 327.184 +-317.418 4.24174 317.049 -316.881 3.67546 317.744 -317.282 4.24144 317.776 +-316.719 4.20276 318.223 -316.732 4.31383 326.454 -316.717 3.69424 326.73 +-317.208 4.19043 326.866 -316.762 5.19581 318.415 -317.313 5.02 326.819 +-316.704 5.25836 326.448 -317.338 6.27095 319.02 -316.922 5.78705 318.73 +-316.716 5.74174 319.683 -317.331 6.21218 320.023 -316.722 5.87837 320.802 +-317.288 6.19097 320.768 -316.833 6.20826 321.357 -317.374 6.20085 326.732 +-316.765 6.23401 326.267 -316.911 6.72058 321.805 -317.358 7.37408 322.053 +-316.672 6.83377 322.785 -317.184 7.31706 322.987 -316.694 6.80978 323.801 +-317.208 7.31695 324.015 -316.839 6.86015 324.691 -317.211 7.37751 325.057 +-316.685 6.74703 325.58 -317.255 7.13099 325.974 -317.445 8.15113 322.852 +-317.398 8.12983 323.804 -317.404 8.15788 324.845 -317.454 8.02553 325.757 +-317.417 17.4734 329.863 -317.335 17.2906 330.769 -317.374 17.313 331.74 +-317.366 18.4164 329.902 -317.229 18.1883 330.793 -317.218 18.2255 331.841 +-317.381 18.0632 332.63 -317.362 19.4219 329.759 -317.255 19.209 330.828 +-317.244 19.2667 331.881 -317.348 19.0573 332.783 -317.436 20.4653 306.645 +-317.291 20.4232 307.708 -317.4 20.3228 308.825 -317.437 20.3121 309.766 +-317.388 20.3461 328.815 -317.263 20.2997 329.915 -317.265 20.2231 330.831 +-317.26 20.29 331.887 -317.366 20.081 332.801 -317.224 21.2275 305.878 +-317.109 21.1175 306.746 -316.864 20.8809 307.984 -316.864 21.0614 307.413 +-317.13 20.9749 308.665 -316.735 21.325 308.409 -317.194 21.2191 309.732 +-317.416 21.169 310.738 -317.379 21.3591 327.787 -317.259 21.2939 328.902 +-317.273 21.1757 329.787 -317.275 21.2356 330.822 -317.265 21.1644 331.738 +-317.414 21.1516 332.694 -316.743 21.7806 306.039 -316.971 22.1621 305.385 +-316.724 21.5806 306.883 -316.804 21.66 309.105 -316.861 21.8655 309.89 +-317.147 22.1337 310.784 -316.723 22.3578 310.389 -317.425 22.1888 311.741 +-317.39 22.3623 326.922 -317.253 22.3172 327.903 -317.282 22.1938 328.819 +-317.306 22.2084 329.84 -317.299 22.2617 330.845 -317.359 22.174 331.973 +-316.803 23.2001 305.374 -316.857 22.805 310.971 -317.147 23.1554 311.804 +-316.723 23.3783 311.419 -317.416 23.2044 312.757 -317.341 23.4415 326.867 +-317.262 23.21 327.791 -317.313 23.2462 328.864 -317.338 23.1275 329.754 +-317.243 23.379 330.746 -317.277 23.3072 331.884 -317.416 23.1311 332.629 +-316.874 24.2193 305.382 -316.86 23.8203 311.994 -317.194 24.2114 312.818 +-316.804 24.5287 312.29 -317.435 24.0771 313.658 -317.416 24.3971 327.025 +-317.268 24.2401 327.813 -317.32 24.1405 328.732 -317.209 24.2468 329.702 +-316.857 23.8742 330.964 -316.862 24.0615 330.394 -317.144 24.1538 331.833 +-316.698 24.307 331.398 -317.35 24.0669 332.797 -316.897 25.2495 305.36 +-317.135 25.1973 312.948 -316.734 25.3102 312.458 -317.406 25.0191 313.744 +-317.363 25.3594 327.83 -317.211 25.2006 328.835 -316.705 24.6549 329.92 +-316.864 25.2156 329.43 -316.838 24.7996 332.009 -317.259 25.0174 332.769 +-316.759 25.537 332.338 -316.754 26.0631 305.461 -316.858 25.8121 312.958 +-317.294 26.1151 313.787 -316.776 26.5247 313.29 -317.435 26.4496 328.063 +-317.155 26.2911 328.718 -316.769 26.0375 329.28 -317.259 26.0087 332.895 +-316.762 26.4988 332.389 -317.015 26.709 305.81 -316.691 27.1935 306.243 +-317.164 27.2412 313.969 -316.786 27.4685 313.352 -317.45 27.0698 314.615 +-317.398 27.2481 328.817 -316.851 27.1639 329.3 -317.259 26.9977 332.876 +-316.769 27.5042 332.376 -316.775 28.2281 306.333 -317.184 28.2409 313.975 +-316.793 28.4814 313.346 -317.389 28.0167 314.765 -316.88 28.1832 329.47 +-317.256 27.9975 332.87 -316.764 28.4956 332.374 -316.82 29.226 306.34 +-317.16 29.1213 313.864 -316.771 29.5083 313.382 -317.443 29.0882 314.678 +-317.257 29.5241 329.817 -316.914 28.7868 329.735 -316.681 29.1294 330.158 +-317.264 28.9871 332.876 -316.749 29.481 332.373 -316.724 30.2309 306.242 +-317.284 30.1419 313.907 -316.833 30.3749 313.251 -317.298 30.5152 329.947 +-316.759 29.9657 330.36 -317.271 29.9982 332.887 -316.716 30.5127 332.382 +-316.718 30.8229 305.87 -317.025 31.2796 305.419 -316.802 31.3592 311.354 +-316.84 30.8037 311.858 -317.165 31.37 312.049 -316.798 30.7132 312.556 +-317.177 31.1304 313.001 -317.456 31.1893 313.717 -317.486 31.2897 329.997 +-316.811 30.7194 330.677 -317.147 31.2381 330.773 -316.65 31.197 331.286 +-317.228 31.2439 332.768 -316.712 31.2456 332.318 -316.821 32.1374 305.427 +-317.219 32.2495 311.893 -316.826 32.439 311.303 -317.395 32.032 312.857 +-317.032 31.6338 331.836 -317.468 32.1085 332.939 -317.351 33.3802 305.907 +-316.878 32.7989 305.749 -316.687 33.1853 306.188 -317.165 33.0989 311.849 +-316.782 33.4387 311.38 -317.439 33.091 312.664 -316.751 34.1774 306.372 +-317.365 34.1436 311.858 -316.831 34.2828 311.264 -316.775 35.0372 306.435 +-316.856 35.3773 310.284 -316.709 34.7997 310.746 -317.195 35.1706 310.908 +-317.044 35.6883 306.758 -316.715 36.1604 307.3 -316.819 36.4297 309.261 +-316.711 35.8385 309.701 -317.137 36.2027 309.922 -317.47 36.1619 310.699 +-316.952 36.5978 307.863 -316.913 36.7645 308.566 -317.243 37.3523 309.036 +-317.373 37.0816 309.973 -317.396 38.3425 309.045 -317.375 38.0166 309.723 +-316.402 -11.6252 327.995 -316.381 -11.7372 328.723 -316.303 -10.721 328.8 +-316.339 -10.8401 329.747 -316.386 -10.6583 330.783 -316.367 -10.6562 331.786 +-316.329 -10.6662 332.814 -316.396 -10.7942 333.634 -316.412 -9.58876 329.024 +-316.224 -9.77713 329.795 -316.259 -9.77445 330.802 -316.265 -9.73617 331.83 +-316.225 -9.73117 332.871 -316.305 -9.98532 333.77 -316.39 -8.70268 329.913 +-316.242 -8.74863 330.819 -316.266 -8.71193 331.854 -316.254 -8.82114 332.763 +-316.369 -8.87767 333.722 -316.395 -7.67393 330.947 -316.233 -7.70037 331.852 +-316.347 -7.94298 332.944 -316.324 -6.66723 331.741 -316.373 -6.85069 332.684 +-316.363 -5.74132 322.859 -316.313 -5.72313 323.567 -316.331 -5.7074 324.584 +-316.295 -5.73028 325.59 -316.382 -5.7209 326.697 -316.435 -5.88581 332.058 +-316.267 -4.706 321.867 -315.935 -4.87373 322.228 -315.718 -5.27084 322.92 +-315.707 -5.33015 324.016 -315.676 -5.33823 325.017 -315.76 -5.38451 325.936 +-315.833 -5.17226 326.892 -315.805 -4.76729 327.368 -316.269 -4.85523 327.761 +-316.212 -3.58472 320.778 -315.972 -3.89642 321.243 -315.638 -4.30142 321.815 +-315.787 -4.38428 327.723 -315.858 -3.66838 328.22 -315.516 -3.26781 320.848 +-316.004 -2.83286 320.347 -315.925 -2.69254 328.307 -315.558 -2.20851 319.847 +-316.013 -1.71311 319.363 -315.773 -1.80073 328.328 -315.584 -1.22621 318.862 +-316.017 -0.735465 318.346 -315.812 -0.755875 328.367 -315.544 -0.246853 317.81 +-315.99 0.366853 317.416 -315.91 0.164808 328.187 -315.799 1.28731 317.303 +-315.882 1.22972 327.397 -315.762 0.610968 327.751 -315.744 2.29063 317.401 +-315.875 2.28702 327.198 -316.392 3.371 317.937 -315.898 2.88541 317.768 +-315.751 3.34176 318.158 -315.878 3.34117 326.43 -315.679 2.69124 326.757 +-316.253 3.12052 326.891 -315.747 4.15168 318.407 -315.803 4.22458 326.32 +-316.371 5.269 319.01 -315.949 4.76507 318.731 -315.686 4.74253 319.701 +-316.21 5.31585 319.858 -315.719 5.20006 320.386 -315.891 5.29761 326.226 +-315.913 5.60885 320.793 -315.82 5.88107 321.774 -316.207 6.30604 321.99 +-315.598 6.1955 322.425 -315.843 6.2436 325.482 -315.787 5.77557 325.872 +-316.246 6.15895 325.95 -315.977 6.58995 322.663 -315.867 6.65649 323.623 +-315.984 6.63518 324.716 -316.488 21.4788 307.741 -316.405 22.406 305.745 +-316.262 22.2447 306.651 -316.145 22.1808 307.667 -316.292 22.1587 308.697 +-316.457 22.333 309.766 -316.226 23.2942 305.857 -315.754 22.7337 307.035 +-315.813 23.1237 306.381 -315.762 22.6734 308.009 -315.839 22.8272 308.876 +-316.146 23.1287 309.78 -315.706 23.3403 309.386 -316.424 23.1894 310.743 +-316.259 24.4328 305.775 -315.743 23.9714 306.276 -315.85 23.8069 309.97 +-316.144 24.1522 310.804 -315.704 24.3862 310.418 -316.457 24.0742 311.63 +-316.424 24.4428 330.698 -316.256 25.4563 305.778 -315.761 24.9842 306.314 +-315.834 24.8171 310.998 -316.287 25.1141 311.81 -315.765 25.5195 311.319 +-316.374 25.3583 329.953 -316.201 25.3222 330.948 -316.397 25.0293 331.627 +-316.366 26.3169 305.865 -315.744 26.2406 306.306 -316.168 26.2362 311.964 +-315.783 26.4666 311.356 -316.449 26.0725 312.618 -316.337 26.4382 329.87 +-316.214 26.2249 330.815 -316.34 26.0222 331.76 -315.87 27.3686 306.296 +-316.185 27.2379 311.943 -315.81 27.4663 311.325 -316.394 27.0188 312.734 +-316.324 27.4297 329.88 -316.217 27.2316 330.808 -316.362 27.0511 331.756 +-315.72 28.1686 306.391 -316.186 28.2442 311.933 -315.802 28.4501 311.338 +-316.408 28.0242 312.718 -316.383 28.3658 330 -316.206 28.2288 330.812 +-316.359 28.0534 331.755 -315.802 29.3314 306.413 -316.167 29.2366 311.972 +-315.786 29.4268 311.366 -316.388 29.0432 312.743 -316.304 29.3348 330.816 +-316.342 29.0475 331.742 -315.785 30.2604 306.313 -316.342 30.3021 311.791 +-315.859 30.2782 311.215 -316.434 29.997 312.77 -316.376 30.4706 331.053 +-316.32 30.0762 331.741 -316.292 31.4771 305.79 -315.799 31.0042 306.203 +-315.753 31.3792 310.413 -315.797 30.7329 310.827 -316.203 31.057 310.96 +-316.418 31.0562 331.821 -316.365 32.2882 305.874 -315.7 32.1017 306.29 +-316.321 31.9959 310.792 -315.785 32.464 310.348 -315.889 33.1604 306.441 +-316.29 33.1217 310.77 -315.835 33.3165 310.263 -316.214 34.5178 306.89 +-315.902 33.7675 306.676 -315.679 34.0955 307.208 -315.854 34.4644 309.303 +-315.707 33.8561 309.765 -316.143 34.2219 309.975 -316.435 34.1738 310.775 +-316.389 35.4413 306.961 -315.827 34.6464 307.741 -316.153 35.305 307.888 +-315.778 34.7795 308.56 -316.171 35.1536 309.002 -316.423 35.1783 309.903 +-316.376 36.2108 307.867 -316.391 36.0226 308.875 -314.602 -5.1596 322.895 +-315.012 -4.72709 322.321 -314.802 -5.25991 323.803 -314.767 -5.25435 324.816 +-314.987 -5.16643 325.858 -314.495 -4.7842 326.399 -315.29 -4.76131 326.833 +-314.559 -4.23289 321.839 -315.019 -3.71997 321.357 -314.943 -4.27085 327.065 +-314.56 -3.62438 327.395 -315.268 -3.8565 327.726 -314.579 -3.24181 320.835 +-315.014 -2.73017 320.333 -314.925 -3.37475 327.922 -314.762 -2.67026 328.247 +-314.57 -2.25137 319.832 -315.028 -1.73841 319.349 -314.841 -1.69129 328.401 +-314.574 -1.25362 318.686 -315.072 -0.745022 318.273 -314.802 -0.740049 328.323 +-314.687 -0.41207 317.72 -314.85 0.237212 317.372 -314.939 0.439111 327.5 +-314.465 -0.224041 327.891 -315.234 0.0999133 327.93 -314.814 1.28294 317.272 +-314.861 1.27715 327.193 -314.687 2.06315 317.451 -314.859 2.3371 326.439 +-314.663 1.68003 326.75 -315.223 2.11837 326.897 -314.865 2.66215 317.923 +-314.843 3.28246 318.385 -314.774 3.19352 326.327 -315.373 4.23938 319.043 +-314.786 3.74018 318.882 -314.703 3.90132 319.766 -315.263 4.21988 319.741 +-314.838 4.2298 320.355 -314.854 4.24111 326.236 -314.813 4.76045 320.823 +-315.233 5.34931 320.968 -314.613 5.21801 321.387 -314.78 5.27562 325.359 +-314.763 4.72067 325.891 -315.268 5.35038 325.776 -314.972 5.60406 321.679 +-314.875 5.76053 322.607 -315.286 6.28598 323.126 -314.758 5.79093 323.615 +-315.248 6.31514 324.074 -314.782 5.75863 324.558 -315.269 6.09137 324.966 +-315.412 23.4598 306.873 -315.35 23.2788 307.776 -315.411 23.2971 308.746 +-315.382 24.4172 306.932 -315.267 24.181 307.816 -315.251 24.2066 308.846 +-315.394 24.1864 309.755 -315.416 25.3644 306.836 -315.206 25.2605 307.593 +-315.199 25.2182 308.775 -315.261 25.2692 309.898 -315.414 25.091 310.658 +-315.155 26.1923 306.834 -314.778 25.7422 308.11 -314.833 26.0942 307.343 +-314.851 25.8188 308.933 -315.199 26.2109 309.838 -314.778 26.4699 309.298 +-315.375 26.0445 310.782 -314.654 26.6716 306.863 -315.006 27.1363 306.439 +-315.207 27.2634 309.953 -314.794 27.4483 309.32 -315.413 27.0378 310.749 +-315.238 28.4879 306.819 -314.87 27.7704 306.706 -314.694 28.1326 307.176 +-315.153 28.2024 309.923 -314.724 28.3206 309.443 -315.43 28.0289 310.718 +-315.167 29.2935 306.738 -314.794 28.9301 307.267 -314.844 28.8266 309.981 +-315.292 28.9897 310.716 -314.749 29.5266 310.367 -314.694 29.6506 306.905 +-315.029 30.1991 306.408 -315.364 30.1718 310.755 -314.824 30.3002 310.311 +-314.815 31.1854 306.415 -314.863 31.3851 309.306 -314.71 30.8153 309.794 +-315.205 31.1705 309.912 -315.209 32.5312 306.913 -314.852 31.8121 306.694 +-314.667 32.138 307.222 -314.846 32.4346 308.25 -314.717 31.8575 308.722 +-315.167 32.306 308.989 -315.425 32.1076 309.831 -315.415 33.4017 306.984 +-314.831 32.5912 307.665 -315.182 33.1719 307.922 -315.265 33.3022 308.826 +-315.414 33.1325 309.673 -315.415 34.1793 307.875 -315.407 34.0396 308.903 +-313.607 -5.20044 322.886 -314.037 -4.7768 322.293 -313.79 -5.30691 323.811 +-313.877 -5.29357 324.71 -313.862 -5.15105 325.671 -313.836 -4.80367 326.291 +-313.565 -4.2659 321.828 -314.019 -3.74707 321.338 -313.918 -4.27663 326.683 +-313.69 -3.535 327.194 -313.578 -3.25218 320.83 -314.03 -2.75921 320.321 +-314.342 -2.92934 327.789 -313.719 -2.7048 327.5 -313.564 -2.24639 319.84 +-314.023 -1.74389 319.343 -313.849 -2.35685 327.942 -313.871 -1.65082 328.279 +-313.658 -1.15014 317.98 -313.925 -0.59323 317.416 -314.356 -0.846706 317.92 +-313.677 -1.37452 318.838 -313.901 -0.776363 328.237 -313.769 0.214025 317.234 +-314.036 0.389315 327.285 -313.725 -0.246851 327.751 -313.73 1.27939 317.357 +-313.837 1.33724 326.424 -313.635 0.691597 326.709 -314.207 1.11594 326.878 +-314.339 2.2605 317.792 -313.915 1.91616 317.72 -313.673 2.29926 318.202 +-313.777 2.20384 326.332 -313.836 2.83089 318.738 -314.206 3.30587 318.903 +-313.644 3.17409 319.364 -313.857 3.23598 326.251 -314.005 3.57659 319.636 +-313.764 3.7307 320.739 -314.309 4.24012 320.866 -313.772 4.24493 321.323 +-313.795 4.26771 325.374 -313.774 3.71921 325.899 -314.271 4.3238 325.801 +-313.97 4.75391 321.679 -314.28 5.26105 322.098 -313.758 4.73293 322.566 +-314.257 5.25433 323.053 -313.8 4.75932 323.595 -314.298 5.29153 324.06 +-313.814 4.74266 324.571 -314.301 5.08015 324.967 -314.349 26.4772 307.731 +-314.434 26.1511 308.641 -314.388 27.3085 307.038 -314.189 27.3303 307.931 +-314.332 26.9451 308.71 -314.33 28.3148 307.827 -314.329 28.1261 308.836 +-314.34 29.4691 307.776 -314.227 29.2582 308.89 -314.406 29.0979 309.665 +-314.34 30.3819 306.932 -314.215 30.3798 307.944 -314.23 30.131 308.771 +-314.373 30.127 309.761 -314.354 31.4296 306.979 -314.194 31.1344 307.655 +-314.37 31.1075 308.844 -314.426 32.0122 307.92 -312.602 -5.20287 322.906 +-313.045 -4.79265 322.301 -312.793 -5.32451 323.813 -312.972 -5.21466 324.903 +-312.484 -4.78178 325.385 -313.256 -4.79321 325.861 -312.569 -4.25533 321.85 +-313.013 -3.74322 321.347 -312.932 -4.25723 326.034 -312.543 -3.73207 326.369 +-313.287 -3.81857 326.706 -312.563 -3.23395 320.836 -313.025 -2.74515 320.339 +-312.901 -3.37748 326.925 -312.824 -2.85206 327.266 -312.569 -2.22122 319.874 +-313.014 -1.71875 319.372 -313.364 -1.67625 327.698 -312.833 -1.86738 327.303 +-312.572 -1.20626 317.892 -312.996 -0.718259 317.341 -312.814 -1.35986 318.739 +-312.824 -0.543142 326.417 -312.587 -1.1809 326.783 -313.139 -0.874616 327.01 +-313.385 -0.78458 327.77 -312.763 0.222907 317.223 -313.309 -0.0103334 326.927 +-312.826 0.281596 326.327 -312.644 1.06778 317.438 -312.766 1.21587 326.312 +-312.88 1.69954 317.929 -312.75 2.18821 318.445 -312.847 2.26693 326.309 +-312.855 2.58367 318.918 -312.843 2.82806 319.775 -313.356 3.22363 320.108 +-312.709 2.85661 320.79 -313.235 3.19748 320.766 -312.815 3.19751 321.343 +-312.906 3.36898 325.334 -312.568 2.78688 325.777 -313.312 3.32358 325.815 +-312.86 3.74116 321.825 -313.307 4.23126 322.073 -312.642 3.85821 322.818 +-313.286 4.23099 323.037 -312.775 3.88537 323.713 -313.312 4.26371 324.07 +-312.652 3.7043 324.587 -313.316 4.05069 324.986 -311.607 -5.18908 322.933 +-312.049 -4.77628 322.334 -311.795 -5.32474 323.833 -311.981 -5.1993 324.736 +-311.558 -4.78267 325.339 -311.559 -4.25883 321.704 -312.057 -3.74106 321.287 +-312.043 -4.24816 325.793 -311.567 -3.75352 326.307 -311.689 -3.38538 320.752 +-311.88 -2.83329 320.349 -312.001 -3.24335 326.855 -311.769 -2.63885 327.292 +-311.588 -2.23073 319.735 -312.087 -1.72366 319.333 -311.821 -1.85134 327.291 +-311.613 -1.19633 317.77 -311.968 -0.716902 317.341 -311.742 -1.42732 318.545 +-312.043 -0.663688 326.349 -311.624 -1.24436 326.833 -311.692 0.130385 317.403 +-311.759 0.209844 326.28 -312.341 1.30465 317.808 -311.944 0.855888 317.74 +-311.671 1.20012 318.248 -311.78 1.24261 326.379 -311.998 1.88752 318.647 +-312.241 2.34165 318.951 -311.527 2.30342 319.242 -311.906 2.15925 326.271 +-311.84 2.68604 319.779 -311.786 2.73322 320.867 -311.84 2.84723 321.767 +-312.224 3.29183 321.96 -311.62 3.17366 322.388 -311.896 3.30714 324.425 +-311.698 2.88716 324.82 -312.395 3.16524 325.028 -311.691 2.61435 325.684 +-311.963 3.59445 322.645 -311.918 3.61684 323.686 -310.61 -5.19582 322.753 +-311.123 -4.76378 322.276 -310.804 -5.31658 323.843 -310.994 -5.19867 324.761 +-310.554 -4.77939 325.358 -310.704 -4.11295 320.927 -310.93 -3.55122 320.483 +-311.331 -3.83877 320.953 -310.631 -4.42453 321.602 -311.039 -4.26439 325.819 +-310.541 -3.77763 326.335 -310.866 -2.86016 320.188 -310.945 -3.28597 326.88 +-310.725 -2.66256 327.332 -310.774 -2.09067 318.93 -310.725 -1.62363 318.412 +-311.389 -1.82203 318.995 -310.688 -2.39557 319.625 -310.846 -1.84569 327.256 +-310.774 -1.2001 317.955 -310.969 -0.77113 317.481 -310.886 -0.765505 326.376 +-310.749 -1.35692 326.778 -311.337 0.386629 317.893 -310.835 -0.224511 317.73 +-310.681 0.210843 318.179 -310.755 0.236527 326.327 -310.683 1.07198 318.475 +-310.867 1.3269 326.313 -310.869 1.69203 318.916 -310.729 2.22902 319.403 +-310.992 2.3534 325.454 -310.568 1.83099 325.754 -311.308 2.16786 325.992 +-310.986 2.61295 319.791 -310.776 2.69598 320.749 -310.751 2.76192 321.883 +-310.77 2.84836 322.8 -311.267 3.25622 323.098 -310.83 2.86654 323.829 +-311.354 3.18513 323.899 -310.734 2.62385 324.754 -309.712 -5.14899 321.962 +-309.867 -4.70032 321.429 -310.413 -4.82972 321.936 -309.735 -5.29254 322.909 +-309.821 -5.29974 323.82 -309.994 -5.1868 324.751 -309.543 -4.77329 325.337 +-309.644 -4.27302 320.908 -309.995 -3.62305 320.323 -310.024 -4.25391 325.822 +-309.543 -3.63079 326.327 -309.494 -3.16424 319.82 -309.902 -2.63721 319.461 +-310.253 -2.86911 319.914 -309.987 -3.24192 326.893 -309.945 -2.5436 327.195 +-309.717 -2.22971 318.88 -309.91 -1.77709 318.406 -309.837 -1.65136 326.425 +-309.622 -2.32704 326.78 -310.208 -1.85882 326.941 -310.395 -0.667614 317.86 +-309.879 -0.887679 318.201 -309.758 -0.77586 326.265 -309.741 0.297019 318.38 +-309.779 0.239092 326.343 -310.347 1.27064 318.783 -309.786 0.885616 318.835 +-309.808 1.23914 319.284 -309.895 1.16624 326.229 -309.891 1.74632 319.778 +-310.349 2.28321 320.057 -309.691 1.9077 320.739 -310.162 2.33384 320.874 +-309.668 2.18194 321.387 -309.879 2.31509 324.4 -309.697 1.89652 324.805 +-310.382 2.19728 324.991 -309.68 1.62076 325.666 -309.991 2.61389 321.677 +-309.83 2.6662 322.625 -309.894 2.61483 323.681 -308.612 -5.2198 321.732 +-309.047 -4.79247 321.239 -308.866 -5.28414 322.784 -308.809 -5.28388 323.828 +-308.846 -5.20386 324.858 -308.683 -4.85509 325.485 -308.79 -4.36698 320.825 +-308.909 -3.85317 320.325 -309.077 -4.31554 325.822 -308.646 -3.96484 326.23 +-308.834 -3.30034 319.814 -308.981 -2.8008 319.292 -309.44 -2.91801 326.792 +-308.801 -2.77787 326.444 -308.702 -2.36215 318.694 -308.847 -1.75799 318.356 +-308.92 -1.64271 326.237 -308.796 -0.723536 318.266 -308.726 -0.841829 326.388 +-308.694 0.0763682 318.431 -308.961 0.292371 326.371 -308.9 0.607121 318.895 +-308.857 0.864305 319.713 -309.219 1.27895 319.944 -308.662 1.15172 320.404 +-308.98 1.34993 325.424 -308.565 0.803937 325.855 -309.292 1.15143 325.945 +-308.885 1.59851 320.772 -308.82 1.84943 321.778 -309.31 2.26434 322.109 +-308.619 1.87055 322.828 -309.213 2.25409 323.04 -308.852 1.85656 323.827 +-309.362 2.17793 323.887 -308.742 1.62348 324.756 -307.708 -5.19097 320.973 +-307.811 -4.69217 320.313 -308.362 -4.6643 320.764 -307.746 -5.30074 321.914 +-307.845 -5.27094 322.802 -307.773 -5.26816 323.846 -307.723 -5.30953 324.899 +-308.354 -4.67674 325.937 -307.904 -5.16939 325.689 -307.728 -4.73451 326.245 +-307.547 -4.23949 319.836 -307.836 -3.63976 319.314 -308.287 -3.75948 319.818 +-307.683 -3.68177 326.45 -307.534 -3.22761 318.794 -307.932 -2.55176 318.429 +-308.243 -2.96925 318.944 -308.108 -2.61583 326.801 -307.947 -3.07433 326.824 +-307.797 -2.74097 327.08 -307.776 -1.76276 318.26 -307.823 -1.86954 326.439 +-307.659 -2.42185 326.802 -307.742 -0.728783 318.357 -308.232 -0.518605 326.911 +-307.831 -1.18043 326.785 -307.552 -0.805036 327.246 -308.29 0.346404 318.909 +-307.944 -0.0805247 318.697 -307.571 0.278689 319.229 -308.34 0.247517 326.793 +-307.751 -0.0364702 327.262 -307.857 0.666056 319.777 -307.836 0.875811 320.754 +-308.22 1.31168 320.969 -307.642 1.18494 321.403 -307.883 1.30586 324.383 +-307.715 0.869941 324.753 -308.377 1.18447 324.952 -307.699 0.844152 325.91 +-308.005 0.575079 326.74 -307.989 1.60351 321.663 -307.841 1.65963 322.604 +-307.907 1.61137 323.684 -306.631 -5.20411 320.88 -307.055 -4.82476 320.31 +-306.87 -5.28884 321.775 -306.823 -5.25862 322.804 -306.749 -5.25911 323.848 +-306.841 -5.29564 324.77 -306.882 -5.19981 325.741 -306.731 -4.54846 326.258 +-306.593 -4.27097 319.846 -307.055 -3.76802 319.347 -306.78 -3.80371 326.351 +-306.557 -3.27583 318.808 -306.995 -2.68172 318.378 -307.498 -2.88035 326.825 +-306.91 -2.79436 326.409 -306.791 -1.75708 318.249 -306.537 -1.70774 326.391 +-306.674 -0.949016 318.443 -307.024 -1.22447 326.86 -306.976 -0.537885 327.223 +-306.903 -0.32125 318.908 -306.768 0.210648 319.418 -306.595 0.247456 326.511 +-306.638 -0.29307 326.826 -307.291 0.349714 326.844 -306.846 0.629201 319.929 +-306.757 0.778626 320.864 -306.796 0.847887 321.807 -307.297 1.26096 322.108 +-306.624 0.857731 322.82 -307.218 1.24916 323.037 -306.871 0.833188 323.789 +-307.367 1.16437 323.865 -306.71 0.722086 324.917 -306.689 1.30976 325.518 +-307.188 1.29707 325.693 -306.894 1.08247 326.22 -306.848 1.5803 325.828 +-305.613 -5.19558 320.878 -306.043 -4.7922 320.292 -305.851 -5.28419 321.775 +-305.801 -5.25515 322.825 -305.746 -5.27005 323.854 -305.976 -5.2051 324.93 +-305.49 -4.80623 325.424 -306.202 -4.88245 325.764 -305.566 -4.26534 319.654 +-306.068 -3.74705 319.277 -305.844 -4.43948 325.94 -305.866 -3.80033 326.262 +-305.675 -3.41022 318.711 -305.844 -2.78933 318.341 -305.773 -2.78301 326.327 +-305.695 -1.73801 318.34 -305.651 -1.93106 326.324 -306.353 -0.736237 318.8 +-305.901 -1.07921 318.714 -305.688 -0.687408 319.2 -306.445 -0.886767 326.786 +-305.879 -0.791906 326.427 -305.857 -0.148846 319.745 -306.228 0.355168 319.918 +-305.633 0.201989 320.363 -305.963 0.286481 324.404 -305.547 -0.0759759 324.766 +-306.191 0.21313 324.853 -305.807 0.238505 325.378 -305.831 0.367781 326.227 +-305.973 0.628426 320.655 -305.848 0.663723 321.627 -305.845 0.642719 322.617 +-305.956 0.592895 323.677 -305.982 0.757399 325.686 -306.351 1.25608 325.903 +-304.65 -5.13307 321.001 -305.103 -4.77428 320.331 -304.819 -5.31204 321.78 +-304.778 -5.27821 322.835 -304.73 -5.28952 323.884 -304.967 -5.20351 324.769 +-304.535 -4.67269 325.38 -304.702 -4.13567 319.034 -304.969 -3.61002 318.457 +-305.337 -3.85149 318.903 -304.662 -4.37822 319.943 -305.036 -4.24674 325.868 +-304.859 -3.52282 326.197 -304.86 -2.79951 318.346 -304.694 -2.55802 326.283 +-304.776 -1.92466 318.454 -304.868 -1.66265 326.386 -304.859 -1.28375 318.9 +-304.745 -0.782681 319.425 -304.787 -0.701861 326.281 -304.848 -0.400232 319.916 +-304.805 -0.15003 320.773 -305.303 0.26029 321.105 -304.642 -0.139581 321.836 +-305.247 0.25161 322.064 -304.771 -0.118543 322.697 -305.256 0.248669 323.061 +-304.758 -0.246977 323.699 -305.418 0.175134 323.913 -304.83 -0.376548 324.724 +-304.909 -0.292118 325.634 -305.284 0.137091 325.971 -304.392 -4.68785 320.665 +-303.942 -4.83283 321.26 -303.73 -5.18121 321.903 -303.736 -5.33141 322.827 +-303.833 -5.31614 323.76 -303.871 -5.20156 324.729 -303.784 -4.63641 325.208 +-304.316 -3.55858 318.755 -303.849 -3.84758 319.19 -303.711 -4.22787 319.782 +-303.718 -4.3979 320.885 -304.4 -3.86666 325.796 -303.884 -3.68233 325.356 +-304.264 -2.52535 318.775 -303.641 -3.00214 319.178 -304.189 -3.04265 325.859 +-303.672 -2.64914 325.479 -304.382 -1.76874 318.78 -303.75 -1.89974 319.295 +-303.841 -2.3049 325.954 -303.966 -1.65922 326.242 -303.804 -1.21333 319.789 +-304.217 -0.642873 319.928 -303.622 -0.781705 320.369 -303.846 -0.794923 323.393 +-303.8 -1.17309 323.829 -304.262 -0.742679 324.068 -303.775 -1.23928 324.594 +-304.28 -0.727707 325.105 -303.706 -1.2774 325.634 -304.198 -0.865082 325.952 +-303.968 -0.384739 320.656 -303.844 -0.329784 321.629 -303.956 -0.371368 322.686 +-303.357 -4.7072 321.723 -302.689 -4.72195 322.302 -302.806 -5.18607 322.879 +-302.964 -5.20961 323.934 -302.702 -4.54154 324.346 -303.232 -4.8532 324.796 +-302.752 -4.14318 319.988 -303.028 -3.71018 319.439 -302.721 -4.22585 320.902 +-302.837 -4.41921 321.682 -303.276 -4.02847 324.894 -302.754 -3.54654 324.394 +-302.844 -2.79278 319.451 -303.261 -2.74796 324.86 -302.829 -2.69061 324.238 +-303.3 -1.70144 319.8 -302.656 -2.19873 319.879 -302.764 -1.72699 320.297 +-302.829 -1.57056 323.271 -302.697 -2.16792 323.74 -303.267 -1.71277 323.928 +-303.367 -1.73473 324.995 -303.423 -1.93539 325.668 -302.975 -1.23861 320.691 +-303.284 -0.712443 321.109 -302.746 -1.20271 321.598 -303.236 -0.690565 322.065 +-302.764 -1.2354 322.549 -303.261 -0.92671 322.932 -302.329 -4.628 323.039 +-302.372 -4.83627 323.653 -302.423 -3.6046 319.775 -302.174 -3.66799 320.522 +-301.841 -3.89418 321.297 -302.01 -4.18492 321.933 -302.183 -3.84239 322.877 +-301.712 -3.51915 322.332 -302.325 -4.00917 323.767 -302.291 -2.94246 319.904 +-301.608 -3.34666 320.967 -301.874 -2.78457 320.403 -302.157 -2.73339 322.776 +-301.802 -2.67929 322.24 -302.405 -2.8886 323.699 -301.927 -2.20806 320.679 +-302.287 -1.72627 321.089 -301.7 -2.28061 321.56 -302.261 -1.84297 322.086 +-302.444 -1.91819 323.039 -301.387 -3.58896 321.653 -301.383 -2.79953 321.103 +-301.374 -2.97776 321.866 +POLYGONS 37883 113646 +OFFSETS vtktypeint64 +0 3 6 9 12 15 18 21 24 +27 30 33 36 39 42 45 48 51 +54 57 60 63 66 69 72 75 78 +81 84 87 90 93 96 99 102 105 +108 111 114 117 120 123 126 129 132 +135 138 141 144 147 150 153 156 159 +162 165 168 171 174 177 180 183 186 +189 192 195 198 201 204 207 210 213 +216 219 222 225 228 231 234 237 240 +243 246 249 252 255 258 261 264 267 +270 273 276 279 282 285 288 291 294 +297 300 303 306 309 312 315 318 321 +324 327 330 333 336 339 342 345 348 +351 354 357 360 363 366 369 372 375 +378 381 384 387 390 393 396 399 402 +405 408 411 414 417 420 423 426 429 +432 435 438 441 444 447 450 453 456 +459 462 465 468 471 474 477 480 483 +486 489 492 495 498 501 504 507 510 +513 516 519 522 525 528 531 534 537 +540 543 546 549 552 555 558 561 564 +567 570 573 576 579 582 585 588 591 +594 597 600 603 606 609 612 615 618 +621 624 627 630 633 636 639 642 645 +648 651 654 657 660 663 666 669 672 +675 678 681 684 687 690 693 696 699 +702 705 708 711 714 717 720 723 726 +729 732 735 738 741 744 747 750 753 +756 759 762 765 768 771 774 777 780 +783 786 789 792 795 798 801 804 807 +810 813 816 819 822 825 828 831 834 +837 840 843 846 849 852 855 858 861 +864 867 870 873 876 879 882 885 888 +891 894 897 900 903 906 909 912 915 +918 921 924 927 930 933 936 939 942 +945 948 951 954 957 960 963 966 969 +972 975 978 981 984 987 990 993 996 +999 1002 1005 1008 1011 1014 1017 1020 1023 +1026 1029 1032 1035 1038 1041 1044 1047 1050 +1053 1056 1059 1062 1065 1068 1071 1074 1077 +1080 1083 1086 1089 1092 1095 1098 1101 1104 +1107 1110 1113 1116 1119 1122 1125 1128 1131 +1134 1137 1140 1143 1146 1149 1152 1155 1158 +1161 1164 1167 1170 1173 1176 1179 1182 1185 +1188 1191 1194 1197 1200 1203 1206 1209 1212 +1215 1218 1221 1224 1227 1230 1233 1236 1239 +1242 1245 1248 1251 1254 1257 1260 1263 1266 +1269 1272 1275 1278 1281 1284 1287 1290 1293 +1296 1299 1302 1305 1308 1311 1314 1317 1320 +1323 1326 1329 1332 1335 1338 1341 1344 1347 +1350 1353 1356 1359 1362 1365 1368 1371 1374 +1377 1380 1383 1386 1389 1392 1395 1398 1401 +1404 1407 1410 1413 1416 1419 1422 1425 1428 +1431 1434 1437 1440 1443 1446 1449 1452 1455 +1458 1461 1464 1467 1470 1473 1476 1479 1482 +1485 1488 1491 1494 1497 1500 1503 1506 1509 +1512 1515 1518 1521 1524 1527 1530 1533 1536 +1539 1542 1545 1548 1551 1554 1557 1560 1563 +1566 1569 1572 1575 1578 1581 1584 1587 1590 +1593 1596 1599 1602 1605 1608 1611 1614 1617 +1620 1623 1626 1629 1632 1635 1638 1641 1644 +1647 1650 1653 1656 1659 1662 1665 1668 1671 +1674 1677 1680 1683 1686 1689 1692 1695 1698 +1701 1704 1707 1710 1713 1716 1719 1722 1725 +1728 1731 1734 1737 1740 1743 1746 1749 1752 +1755 1758 1761 1764 1767 1770 1773 1776 1779 +1782 1785 1788 1791 1794 1797 1800 1803 1806 +1809 1812 1815 1818 1821 1824 1827 1830 1833 +1836 1839 1842 1845 1848 1851 1854 1857 1860 +1863 1866 1869 1872 1875 1878 1881 1884 1887 +1890 1893 1896 1899 1902 1905 1908 1911 1914 +1917 1920 1923 1926 1929 1932 1935 1938 1941 +1944 1947 1950 1953 1956 1959 1962 1965 1968 +1971 1974 1977 1980 1983 1986 1989 1992 1995 +1998 2001 2004 2007 2010 2013 2016 2019 2022 +2025 2028 2031 2034 2037 2040 2043 2046 2049 +2052 2055 2058 2061 2064 2067 2070 2073 2076 +2079 2082 2085 2088 2091 2094 2097 2100 2103 +2106 2109 2112 2115 2118 2121 2124 2127 2130 +2133 2136 2139 2142 2145 2148 2151 2154 2157 +2160 2163 2166 2169 2172 2175 2178 2181 2184 +2187 2190 2193 2196 2199 2202 2205 2208 2211 +2214 2217 2220 2223 2226 2229 2232 2235 2238 +2241 2244 2247 2250 2253 2256 2259 2262 2265 +2268 2271 2274 2277 2280 2283 2286 2289 2292 +2295 2298 2301 2304 2307 2310 2313 2316 2319 +2322 2325 2328 2331 2334 2337 2340 2343 2346 +2349 2352 2355 2358 2361 2364 2367 2370 2373 +2376 2379 2382 2385 2388 2391 2394 2397 2400 +2403 2406 2409 2412 2415 2418 2421 2424 2427 +2430 2433 2436 2439 2442 2445 2448 2451 2454 +2457 2460 2463 2466 2469 2472 2475 2478 2481 +2484 2487 2490 2493 2496 2499 2502 2505 2508 +2511 2514 2517 2520 2523 2526 2529 2532 2535 +2538 2541 2544 2547 2550 2553 2556 2559 2562 +2565 2568 2571 2574 2577 2580 2583 2586 2589 +2592 2595 2598 2601 2604 2607 2610 2613 2616 +2619 2622 2625 2628 2631 2634 2637 2640 2643 +2646 2649 2652 2655 2658 2661 2664 2667 2670 +2673 2676 2679 2682 2685 2688 2691 2694 2697 +2700 2703 2706 2709 2712 2715 2718 2721 2724 +2727 2730 2733 2736 2739 2742 2745 2748 2751 +2754 2757 2760 2763 2766 2769 2772 2775 2778 +2781 2784 2787 2790 2793 2796 2799 2802 2805 +2808 2811 2814 2817 2820 2823 2826 2829 2832 +2835 2838 2841 2844 2847 2850 2853 2856 2859 +2862 2865 2868 2871 2874 2877 2880 2883 2886 +2889 2892 2895 2898 2901 2904 2907 2910 2913 +2916 2919 2922 2925 2928 2931 2934 2937 2940 +2943 2946 2949 2952 2955 2958 2961 2964 2967 +2970 2973 2976 2979 2982 2985 2988 2991 2994 +2997 3000 3003 3006 3009 3012 3015 3018 3021 +3024 3027 3030 3033 3036 3039 3042 3045 3048 +3051 3054 3057 3060 3063 3066 3069 3072 3075 +3078 3081 3084 3087 3090 3093 3096 3099 3102 +3105 3108 3111 3114 3117 3120 3123 3126 3129 +3132 3135 3138 3141 3144 3147 3150 3153 3156 +3159 3162 3165 3168 3171 3174 3177 3180 3183 +3186 3189 3192 3195 3198 3201 3204 3207 3210 +3213 3216 3219 3222 3225 3228 3231 3234 3237 +3240 3243 3246 3249 3252 3255 3258 3261 3264 +3267 3270 3273 3276 3279 3282 3285 3288 3291 +3294 3297 3300 3303 3306 3309 3312 3315 3318 +3321 3324 3327 3330 3333 3336 3339 3342 3345 +3348 3351 3354 3357 3360 3363 3366 3369 3372 +3375 3378 3381 3384 3387 3390 3393 3396 3399 +3402 3405 3408 3411 3414 3417 3420 3423 3426 +3429 3432 3435 3438 3441 3444 3447 3450 3453 +3456 3459 3462 3465 3468 3471 3474 3477 3480 +3483 3486 3489 3492 3495 3498 3501 3504 3507 +3510 3513 3516 3519 3522 3525 3528 3531 3534 +3537 3540 3543 3546 3549 3552 3555 3558 3561 +3564 3567 3570 3573 3576 3579 3582 3585 3588 +3591 3594 3597 3600 3603 3606 3609 3612 3615 +3618 3621 3624 3627 3630 3633 3636 3639 3642 +3645 3648 3651 3654 3657 3660 3663 3666 3669 +3672 3675 3678 3681 3684 3687 3690 3693 3696 +3699 3702 3705 3708 3711 3714 3717 3720 3723 +3726 3729 3732 3735 3738 3741 3744 3747 3750 +3753 3756 3759 3762 3765 3768 3771 3774 3777 +3780 3783 3786 3789 3792 3795 3798 3801 3804 +3807 3810 3813 3816 3819 3822 3825 3828 3831 +3834 3837 3840 3843 3846 3849 3852 3855 3858 +3861 3864 3867 3870 3873 3876 3879 3882 3885 +3888 3891 3894 3897 3900 3903 3906 3909 3912 +3915 3918 3921 3924 3927 3930 3933 3936 3939 +3942 3945 3948 3951 3954 3957 3960 3963 3966 +3969 3972 3975 3978 3981 3984 3987 3990 3993 +3996 3999 4002 4005 4008 4011 4014 4017 4020 +4023 4026 4029 4032 4035 4038 4041 4044 4047 +4050 4053 4056 4059 4062 4065 4068 4071 4074 +4077 4080 4083 4086 4089 4092 4095 4098 4101 +4104 4107 4110 4113 4116 4119 4122 4125 4128 +4131 4134 4137 4140 4143 4146 4149 4152 4155 +4158 4161 4164 4167 4170 4173 4176 4179 4182 +4185 4188 4191 4194 4197 4200 4203 4206 4209 +4212 4215 4218 4221 4224 4227 4230 4233 4236 +4239 4242 4245 4248 4251 4254 4257 4260 4263 +4266 4269 4272 4275 4278 4281 4284 4287 4290 +4293 4296 4299 4302 4305 4308 4311 4314 4317 +4320 4323 4326 4329 4332 4335 4338 4341 4344 +4347 4350 4353 4356 4359 4362 4365 4368 4371 +4374 4377 4380 4383 4386 4389 4392 4395 4398 +4401 4404 4407 4410 4413 4416 4419 4422 4425 +4428 4431 4434 4437 4440 4443 4446 4449 4452 +4455 4458 4461 4464 4467 4470 4473 4476 4479 +4482 4485 4488 4491 4494 4497 4500 4503 4506 +4509 4512 4515 4518 4521 4524 4527 4530 4533 +4536 4539 4542 4545 4548 4551 4554 4557 4560 +4563 4566 4569 4572 4575 4578 4581 4584 4587 +4590 4593 4596 4599 4602 4605 4608 4611 4614 +4617 4620 4623 4626 4629 4632 4635 4638 4641 +4644 4647 4650 4653 4656 4659 4662 4665 4668 +4671 4674 4677 4680 4683 4686 4689 4692 4695 +4698 4701 4704 4707 4710 4713 4716 4719 4722 +4725 4728 4731 4734 4737 4740 4743 4746 4749 +4752 4755 4758 4761 4764 4767 4770 4773 4776 +4779 4782 4785 4788 4791 4794 4797 4800 4803 +4806 4809 4812 4815 4818 4821 4824 4827 4830 +4833 4836 4839 4842 4845 4848 4851 4854 4857 +4860 4863 4866 4869 4872 4875 4878 4881 4884 +4887 4890 4893 4896 4899 4902 4905 4908 4911 +4914 4917 4920 4923 4926 4929 4932 4935 4938 +4941 4944 4947 4950 4953 4956 4959 4962 4965 +4968 4971 4974 4977 4980 4983 4986 4989 4992 +4995 4998 5001 5004 5007 5010 5013 5016 5019 +5022 5025 5028 5031 5034 5037 5040 5043 5046 +5049 5052 5055 5058 5061 5064 5067 5070 5073 +5076 5079 5082 5085 5088 5091 5094 5097 5100 +5103 5106 5109 5112 5115 5118 5121 5124 5127 +5130 5133 5136 5139 5142 5145 5148 5151 5154 +5157 5160 5163 5166 5169 5172 5175 5178 5181 +5184 5187 5190 5193 5196 5199 5202 5205 5208 +5211 5214 5217 5220 5223 5226 5229 5232 5235 +5238 5241 5244 5247 5250 5253 5256 5259 5262 +5265 5268 5271 5274 5277 5280 5283 5286 5289 +5292 5295 5298 5301 5304 5307 5310 5313 5316 +5319 5322 5325 5328 5331 5334 5337 5340 5343 +5346 5349 5352 5355 5358 5361 5364 5367 5370 +5373 5376 5379 5382 5385 5388 5391 5394 5397 +5400 5403 5406 5409 5412 5415 5418 5421 5424 +5427 5430 5433 5436 5439 5442 5445 5448 5451 +5454 5457 5460 5463 5466 5469 5472 5475 5478 +5481 5484 5487 5490 5493 5496 5499 5502 5505 +5508 5511 5514 5517 5520 5523 5526 5529 5532 +5535 5538 5541 5544 5547 5550 5553 5556 5559 +5562 5565 5568 5571 5574 5577 5580 5583 5586 +5589 5592 5595 5598 5601 5604 5607 5610 5613 +5616 5619 5622 5625 5628 5631 5634 5637 5640 +5643 5646 5649 5652 5655 5658 5661 5664 5667 +5670 5673 5676 5679 5682 5685 5688 5691 5694 +5697 5700 5703 5706 5709 5712 5715 5718 5721 +5724 5727 5730 5733 5736 5739 5742 5745 5748 +5751 5754 5757 5760 5763 5766 5769 5772 5775 +5778 5781 5784 5787 5790 5793 5796 5799 5802 +5805 5808 5811 5814 5817 5820 5823 5826 5829 +5832 5835 5838 5841 5844 5847 5850 5853 5856 +5859 5862 5865 5868 5871 5874 5877 5880 5883 +5886 5889 5892 5895 5898 5901 5904 5907 5910 +5913 5916 5919 5922 5925 5928 5931 5934 5937 +5940 5943 5946 5949 5952 5955 5958 5961 5964 +5967 5970 5973 5976 5979 5982 5985 5988 5991 +5994 5997 6000 6003 6006 6009 6012 6015 6018 +6021 6024 6027 6030 6033 6036 6039 6042 6045 +6048 6051 6054 6057 6060 6063 6066 6069 6072 +6075 6078 6081 6084 6087 6090 6093 6096 6099 +6102 6105 6108 6111 6114 6117 6120 6123 6126 +6129 6132 6135 6138 6141 6144 6147 6150 6153 +6156 6159 6162 6165 6168 6171 6174 6177 6180 +6183 6186 6189 6192 6195 6198 6201 6204 6207 +6210 6213 6216 6219 6222 6225 6228 6231 6234 +6237 6240 6243 6246 6249 6252 6255 6258 6261 +6264 6267 6270 6273 6276 6279 6282 6285 6288 +6291 6294 6297 6300 6303 6306 6309 6312 6315 +6318 6321 6324 6327 6330 6333 6336 6339 6342 +6345 6348 6351 6354 6357 6360 6363 6366 6369 +6372 6375 6378 6381 6384 6387 6390 6393 6396 +6399 6402 6405 6408 6411 6414 6417 6420 6423 +6426 6429 6432 6435 6438 6441 6444 6447 6450 +6453 6456 6459 6462 6465 6468 6471 6474 6477 +6480 6483 6486 6489 6492 6495 6498 6501 6504 +6507 6510 6513 6516 6519 6522 6525 6528 6531 +6534 6537 6540 6543 6546 6549 6552 6555 6558 +6561 6564 6567 6570 6573 6576 6579 6582 6585 +6588 6591 6594 6597 6600 6603 6606 6609 6612 +6615 6618 6621 6624 6627 6630 6633 6636 6639 +6642 6645 6648 6651 6654 6657 6660 6663 6666 +6669 6672 6675 6678 6681 6684 6687 6690 6693 +6696 6699 6702 6705 6708 6711 6714 6717 6720 +6723 6726 6729 6732 6735 6738 6741 6744 6747 +6750 6753 6756 6759 6762 6765 6768 6771 6774 +6777 6780 6783 6786 6789 6792 6795 6798 6801 +6804 6807 6810 6813 6816 6819 6822 6825 6828 +6831 6834 6837 6840 6843 6846 6849 6852 6855 +6858 6861 6864 6867 6870 6873 6876 6879 6882 +6885 6888 6891 6894 6897 6900 6903 6906 6909 +6912 6915 6918 6921 6924 6927 6930 6933 6936 +6939 6942 6945 6948 6951 6954 6957 6960 6963 +6966 6969 6972 6975 6978 6981 6984 6987 6990 +6993 6996 6999 7002 7005 7008 7011 7014 7017 +7020 7023 7026 7029 7032 7035 7038 7041 7044 +7047 7050 7053 7056 7059 7062 7065 7068 7071 +7074 7077 7080 7083 7086 7089 7092 7095 7098 +7101 7104 7107 7110 7113 7116 7119 7122 7125 +7128 7131 7134 7137 7140 7143 7146 7149 7152 +7155 7158 7161 7164 7167 7170 7173 7176 7179 +7182 7185 7188 7191 7194 7197 7200 7203 7206 +7209 7212 7215 7218 7221 7224 7227 7230 7233 +7236 7239 7242 7245 7248 7251 7254 7257 7260 +7263 7266 7269 7272 7275 7278 7281 7284 7287 +7290 7293 7296 7299 7302 7305 7308 7311 7314 +7317 7320 7323 7326 7329 7332 7335 7338 7341 +7344 7347 7350 7353 7356 7359 7362 7365 7368 +7371 7374 7377 7380 7383 7386 7389 7392 7395 +7398 7401 7404 7407 7410 7413 7416 7419 7422 +7425 7428 7431 7434 7437 7440 7443 7446 7449 +7452 7455 7458 7461 7464 7467 7470 7473 7476 +7479 7482 7485 7488 7491 7494 7497 7500 7503 +7506 7509 7512 7515 7518 7521 7524 7527 7530 +7533 7536 7539 7542 7545 7548 7551 7554 7557 +7560 7563 7566 7569 7572 7575 7578 7581 7584 +7587 7590 7593 7596 7599 7602 7605 7608 7611 +7614 7617 7620 7623 7626 7629 7632 7635 7638 +7641 7644 7647 7650 7653 7656 7659 7662 7665 +7668 7671 7674 7677 7680 7683 7686 7689 7692 +7695 7698 7701 7704 7707 7710 7713 7716 7719 +7722 7725 7728 7731 7734 7737 7740 7743 7746 +7749 7752 7755 7758 7761 7764 7767 7770 7773 +7776 7779 7782 7785 7788 7791 7794 7797 7800 +7803 7806 7809 7812 7815 7818 7821 7824 7827 +7830 7833 7836 7839 7842 7845 7848 7851 7854 +7857 7860 7863 7866 7869 7872 7875 7878 7881 +7884 7887 7890 7893 7896 7899 7902 7905 7908 +7911 7914 7917 7920 7923 7926 7929 7932 7935 +7938 7941 7944 7947 7950 7953 7956 7959 7962 +7965 7968 7971 7974 7977 7980 7983 7986 7989 +7992 7995 7998 8001 8004 8007 8010 8013 8016 +8019 8022 8025 8028 8031 8034 8037 8040 8043 +8046 8049 8052 8055 8058 8061 8064 8067 8070 +8073 8076 8079 8082 8085 8088 8091 8094 8097 +8100 8103 8106 8109 8112 8115 8118 8121 8124 +8127 8130 8133 8136 8139 8142 8145 8148 8151 +8154 8157 8160 8163 8166 8169 8172 8175 8178 +8181 8184 8187 8190 8193 8196 8199 8202 8205 +8208 8211 8214 8217 8220 8223 8226 8229 8232 +8235 8238 8241 8244 8247 8250 8253 8256 8259 +8262 8265 8268 8271 8274 8277 8280 8283 8286 +8289 8292 8295 8298 8301 8304 8307 8310 8313 +8316 8319 8322 8325 8328 8331 8334 8337 8340 +8343 8346 8349 8352 8355 8358 8361 8364 8367 +8370 8373 8376 8379 8382 8385 8388 8391 8394 +8397 8400 8403 8406 8409 8412 8415 8418 8421 +8424 8427 8430 8433 8436 8439 8442 8445 8448 +8451 8454 8457 8460 8463 8466 8469 8472 8475 +8478 8481 8484 8487 8490 8493 8496 8499 8502 +8505 8508 8511 8514 8517 8520 8523 8526 8529 +8532 8535 8538 8541 8544 8547 8550 8553 8556 +8559 8562 8565 8568 8571 8574 8577 8580 8583 +8586 8589 8592 8595 8598 8601 8604 8607 8610 +8613 8616 8619 8622 8625 8628 8631 8634 8637 +8640 8643 8646 8649 8652 8655 8658 8661 8664 +8667 8670 8673 8676 8679 8682 8685 8688 8691 +8694 8697 8700 8703 8706 8709 8712 8715 8718 +8721 8724 8727 8730 8733 8736 8739 8742 8745 +8748 8751 8754 8757 8760 8763 8766 8769 8772 +8775 8778 8781 8784 8787 8790 8793 8796 8799 +8802 8805 8808 8811 8814 8817 8820 8823 8826 +8829 8832 8835 8838 8841 8844 8847 8850 8853 +8856 8859 8862 8865 8868 8871 8874 8877 8880 +8883 8886 8889 8892 8895 8898 8901 8904 8907 +8910 8913 8916 8919 8922 8925 8928 8931 8934 +8937 8940 8943 8946 8949 8952 8955 8958 8961 +8964 8967 8970 8973 8976 8979 8982 8985 8988 +8991 8994 8997 9000 9003 9006 9009 9012 9015 +9018 9021 9024 9027 9030 9033 9036 9039 9042 +9045 9048 9051 9054 9057 9060 9063 9066 9069 +9072 9075 9078 9081 9084 9087 9090 9093 9096 +9099 9102 9105 9108 9111 9114 9117 9120 9123 +9126 9129 9132 9135 9138 9141 9144 9147 9150 +9153 9156 9159 9162 9165 9168 9171 9174 9177 +9180 9183 9186 9189 9192 9195 9198 9201 9204 +9207 9210 9213 9216 9219 9222 9225 9228 9231 +9234 9237 9240 9243 9246 9249 9252 9255 9258 +9261 9264 9267 9270 9273 9276 9279 9282 9285 +9288 9291 9294 9297 9300 9303 9306 9309 9312 +9315 9318 9321 9324 9327 9330 9333 9336 9339 +9342 9345 9348 9351 9354 9357 9360 9363 9366 +9369 9372 9375 9378 9381 9384 9387 9390 9393 +9396 9399 9402 9405 9408 9411 9414 9417 9420 +9423 9426 9429 9432 9435 9438 9441 9444 9447 +9450 9453 9456 9459 9462 9465 9468 9471 9474 +9477 9480 9483 9486 9489 9492 9495 9498 9501 +9504 9507 9510 9513 9516 9519 9522 9525 9528 +9531 9534 9537 9540 9543 9546 9549 9552 9555 +9558 9561 9564 9567 9570 9573 9576 9579 9582 +9585 9588 9591 9594 9597 9600 9603 9606 9609 +9612 9615 9618 9621 9624 9627 9630 9633 9636 +9639 9642 9645 9648 9651 9654 9657 9660 9663 +9666 9669 9672 9675 9678 9681 9684 9687 9690 +9693 9696 9699 9702 9705 9708 9711 9714 9717 +9720 9723 9726 9729 9732 9735 9738 9741 9744 +9747 9750 9753 9756 9759 9762 9765 9768 9771 +9774 9777 9780 9783 9786 9789 9792 9795 9798 +9801 9804 9807 9810 9813 9816 9819 9822 9825 +9828 9831 9834 9837 9840 9843 9846 9849 9852 +9855 9858 9861 9864 9867 9870 9873 9876 9879 +9882 9885 9888 9891 9894 9897 9900 9903 9906 +9909 9912 9915 9918 9921 9924 9927 9930 9933 +9936 9939 9942 9945 9948 9951 9954 9957 9960 +9963 9966 9969 9972 9975 9978 9981 9984 9987 +9990 9993 9996 9999 10002 10005 10008 10011 10014 +10017 10020 10023 10026 10029 10032 10035 10038 10041 +10044 10047 10050 10053 10056 10059 10062 10065 10068 +10071 10074 10077 10080 10083 10086 10089 10092 10095 +10098 10101 10104 10107 10110 10113 10116 10119 10122 +10125 10128 10131 10134 10137 10140 10143 10146 10149 +10152 10155 10158 10161 10164 10167 10170 10173 10176 +10179 10182 10185 10188 10191 10194 10197 10200 10203 +10206 10209 10212 10215 10218 10221 10224 10227 10230 +10233 10236 10239 10242 10245 10248 10251 10254 10257 +10260 10263 10266 10269 10272 10275 10278 10281 10284 +10287 10290 10293 10296 10299 10302 10305 10308 10311 +10314 10317 10320 10323 10326 10329 10332 10335 10338 +10341 10344 10347 10350 10353 10356 10359 10362 10365 +10368 10371 10374 10377 10380 10383 10386 10389 10392 +10395 10398 10401 10404 10407 10410 10413 10416 10419 +10422 10425 10428 10431 10434 10437 10440 10443 10446 +10449 10452 10455 10458 10461 10464 10467 10470 10473 +10476 10479 10482 10485 10488 10491 10494 10497 10500 +10503 10506 10509 10512 10515 10518 10521 10524 10527 +10530 10533 10536 10539 10542 10545 10548 10551 10554 +10557 10560 10563 10566 10569 10572 10575 10578 10581 +10584 10587 10590 10593 10596 10599 10602 10605 10608 +10611 10614 10617 10620 10623 10626 10629 10632 10635 +10638 10641 10644 10647 10650 10653 10656 10659 10662 +10665 10668 10671 10674 10677 10680 10683 10686 10689 +10692 10695 10698 10701 10704 10707 10710 10713 10716 +10719 10722 10725 10728 10731 10734 10737 10740 10743 +10746 10749 10752 10755 10758 10761 10764 10767 10770 +10773 10776 10779 10782 10785 10788 10791 10794 10797 +10800 10803 10806 10809 10812 10815 10818 10821 10824 +10827 10830 10833 10836 10839 10842 10845 10848 10851 +10854 10857 10860 10863 10866 10869 10872 10875 10878 +10881 10884 10887 10890 10893 10896 10899 10902 10905 +10908 10911 10914 10917 10920 10923 10926 10929 10932 +10935 10938 10941 10944 10947 10950 10953 10956 10959 +10962 10965 10968 10971 10974 10977 10980 10983 10986 +10989 10992 10995 10998 11001 11004 11007 11010 11013 +11016 11019 11022 11025 11028 11031 11034 11037 11040 +11043 11046 11049 11052 11055 11058 11061 11064 11067 +11070 11073 11076 11079 11082 11085 11088 11091 11094 +11097 11100 11103 11106 11109 11112 11115 11118 11121 +11124 11127 11130 11133 11136 11139 11142 11145 11148 +11151 11154 11157 11160 11163 11166 11169 11172 11175 +11178 11181 11184 11187 11190 11193 11196 11199 11202 +11205 11208 11211 11214 11217 11220 11223 11226 11229 +11232 11235 11238 11241 11244 11247 11250 11253 11256 +11259 11262 11265 11268 11271 11274 11277 11280 11283 +11286 11289 11292 11295 11298 11301 11304 11307 11310 +11313 11316 11319 11322 11325 11328 11331 11334 11337 +11340 11343 11346 11349 11352 11355 11358 11361 11364 +11367 11370 11373 11376 11379 11382 11385 11388 11391 +11394 11397 11400 11403 11406 11409 11412 11415 11418 +11421 11424 11427 11430 11433 11436 11439 11442 11445 +11448 11451 11454 11457 11460 11463 11466 11469 11472 +11475 11478 11481 11484 11487 11490 11493 11496 11499 +11502 11505 11508 11511 11514 11517 11520 11523 11526 +11529 11532 11535 11538 11541 11544 11547 11550 11553 +11556 11559 11562 11565 11568 11571 11574 11577 11580 +11583 11586 11589 11592 11595 11598 11601 11604 11607 +11610 11613 11616 11619 11622 11625 11628 11631 11634 +11637 11640 11643 11646 11649 11652 11655 11658 11661 +11664 11667 11670 11673 11676 11679 11682 11685 11688 +11691 11694 11697 11700 11703 11706 11709 11712 11715 +11718 11721 11724 11727 11730 11733 11736 11739 11742 +11745 11748 11751 11754 11757 11760 11763 11766 11769 +11772 11775 11778 11781 11784 11787 11790 11793 11796 +11799 11802 11805 11808 11811 11814 11817 11820 11823 +11826 11829 11832 11835 11838 11841 11844 11847 11850 +11853 11856 11859 11862 11865 11868 11871 11874 11877 +11880 11883 11886 11889 11892 11895 11898 11901 11904 +11907 11910 11913 11916 11919 11922 11925 11928 11931 +11934 11937 11940 11943 11946 11949 11952 11955 11958 +11961 11964 11967 11970 11973 11976 11979 11982 11985 +11988 11991 11994 11997 12000 12003 12006 12009 12012 +12015 12018 12021 12024 12027 12030 12033 12036 12039 +12042 12045 12048 12051 12054 12057 12060 12063 12066 +12069 12072 12075 12078 12081 12084 12087 12090 12093 +12096 12099 12102 12105 12108 12111 12114 12117 12120 +12123 12126 12129 12132 12135 12138 12141 12144 12147 +12150 12153 12156 12159 12162 12165 12168 12171 12174 +12177 12180 12183 12186 12189 12192 12195 12198 12201 +12204 12207 12210 12213 12216 12219 12222 12225 12228 +12231 12234 12237 12240 12243 12246 12249 12252 12255 +12258 12261 12264 12267 12270 12273 12276 12279 12282 +12285 12288 12291 12294 12297 12300 12303 12306 12309 +12312 12315 12318 12321 12324 12327 12330 12333 12336 +12339 12342 12345 12348 12351 12354 12357 12360 12363 +12366 12369 12372 12375 12378 12381 12384 12387 12390 +12393 12396 12399 12402 12405 12408 12411 12414 12417 +12420 12423 12426 12429 12432 12435 12438 12441 12444 +12447 12450 12453 12456 12459 12462 12465 12468 12471 +12474 12477 12480 12483 12486 12489 12492 12495 12498 +12501 12504 12507 12510 12513 12516 12519 12522 12525 +12528 12531 12534 12537 12540 12543 12546 12549 12552 +12555 12558 12561 12564 12567 12570 12573 12576 12579 +12582 12585 12588 12591 12594 12597 12600 12603 12606 +12609 12612 12615 12618 12621 12624 12627 12630 12633 +12636 12639 12642 12645 12648 12651 12654 12657 12660 +12663 12666 12669 12672 12675 12678 12681 12684 12687 +12690 12693 12696 12699 12702 12705 12708 12711 12714 +12717 12720 12723 12726 12729 12732 12735 12738 12741 +12744 12747 12750 12753 12756 12759 12762 12765 12768 +12771 12774 12777 12780 12783 12786 12789 12792 12795 +12798 12801 12804 12807 12810 12813 12816 12819 12822 +12825 12828 12831 12834 12837 12840 12843 12846 12849 +12852 12855 12858 12861 12864 12867 12870 12873 12876 +12879 12882 12885 12888 12891 12894 12897 12900 12903 +12906 12909 12912 12915 12918 12921 12924 12927 12930 +12933 12936 12939 12942 12945 12948 12951 12954 12957 +12960 12963 12966 12969 12972 12975 12978 12981 12984 +12987 12990 12993 12996 12999 13002 13005 13008 13011 +13014 13017 13020 13023 13026 13029 13032 13035 13038 +13041 13044 13047 13050 13053 13056 13059 13062 13065 +13068 13071 13074 13077 13080 13083 13086 13089 13092 +13095 13098 13101 13104 13107 13110 13113 13116 13119 +13122 13125 13128 13131 13134 13137 13140 13143 13146 +13149 13152 13155 13158 13161 13164 13167 13170 13173 +13176 13179 13182 13185 13188 13191 13194 13197 13200 +13203 13206 13209 13212 13215 13218 13221 13224 13227 +13230 13233 13236 13239 13242 13245 13248 13251 13254 +13257 13260 13263 13266 13269 13272 13275 13278 13281 +13284 13287 13290 13293 13296 13299 13302 13305 13308 +13311 13314 13317 13320 13323 13326 13329 13332 13335 +13338 13341 13344 13347 13350 13353 13356 13359 13362 +13365 13368 13371 13374 13377 13380 13383 13386 13389 +13392 13395 13398 13401 13404 13407 13410 13413 13416 +13419 13422 13425 13428 13431 13434 13437 13440 13443 +13446 13449 13452 13455 13458 13461 13464 13467 13470 +13473 13476 13479 13482 13485 13488 13491 13494 13497 +13500 13503 13506 13509 13512 13515 13518 13521 13524 +13527 13530 13533 13536 13539 13542 13545 13548 13551 +13554 13557 13560 13563 13566 13569 13572 13575 13578 +13581 13584 13587 13590 13593 13596 13599 13602 13605 +13608 13611 13614 13617 13620 13623 13626 13629 13632 +13635 13638 13641 13644 13647 13650 13653 13656 13659 +13662 13665 13668 13671 13674 13677 13680 13683 13686 +13689 13692 13695 13698 13701 13704 13707 13710 13713 +13716 13719 13722 13725 13728 13731 13734 13737 13740 +13743 13746 13749 13752 13755 13758 13761 13764 13767 +13770 13773 13776 13779 13782 13785 13788 13791 13794 +13797 13800 13803 13806 13809 13812 13815 13818 13821 +13824 13827 13830 13833 13836 13839 13842 13845 13848 +13851 13854 13857 13860 13863 13866 13869 13872 13875 +13878 13881 13884 13887 13890 13893 13896 13899 13902 +13905 13908 13911 13914 13917 13920 13923 13926 13929 +13932 13935 13938 13941 13944 13947 13950 13953 13956 +13959 13962 13965 13968 13971 13974 13977 13980 13983 +13986 13989 13992 13995 13998 14001 14004 14007 14010 +14013 14016 14019 14022 14025 14028 14031 14034 14037 +14040 14043 14046 14049 14052 14055 14058 14061 14064 +14067 14070 14073 14076 14079 14082 14085 14088 14091 +14094 14097 14100 14103 14106 14109 14112 14115 14118 +14121 14124 14127 14130 14133 14136 14139 14142 14145 +14148 14151 14154 14157 14160 14163 14166 14169 14172 +14175 14178 14181 14184 14187 14190 14193 14196 14199 +14202 14205 14208 14211 14214 14217 14220 14223 14226 +14229 14232 14235 14238 14241 14244 14247 14250 14253 +14256 14259 14262 14265 14268 14271 14274 14277 14280 +14283 14286 14289 14292 14295 14298 14301 14304 14307 +14310 14313 14316 14319 14322 14325 14328 14331 14334 +14337 14340 14343 14346 14349 14352 14355 14358 14361 +14364 14367 14370 14373 14376 14379 14382 14385 14388 +14391 14394 14397 14400 14403 14406 14409 14412 14415 +14418 14421 14424 14427 14430 14433 14436 14439 14442 +14445 14448 14451 14454 14457 14460 14463 14466 14469 +14472 14475 14478 14481 14484 14487 14490 14493 14496 +14499 14502 14505 14508 14511 14514 14517 14520 14523 +14526 14529 14532 14535 14538 14541 14544 14547 14550 +14553 14556 14559 14562 14565 14568 14571 14574 14577 +14580 14583 14586 14589 14592 14595 14598 14601 14604 +14607 14610 14613 14616 14619 14622 14625 14628 14631 +14634 14637 14640 14643 14646 14649 14652 14655 14658 +14661 14664 14667 14670 14673 14676 14679 14682 14685 +14688 14691 14694 14697 14700 14703 14706 14709 14712 +14715 14718 14721 14724 14727 14730 14733 14736 14739 +14742 14745 14748 14751 14754 14757 14760 14763 14766 +14769 14772 14775 14778 14781 14784 14787 14790 14793 +14796 14799 14802 14805 14808 14811 14814 14817 14820 +14823 14826 14829 14832 14835 14838 14841 14844 14847 +14850 14853 14856 14859 14862 14865 14868 14871 14874 +14877 14880 14883 14886 14889 14892 14895 14898 14901 +14904 14907 14910 14913 14916 14919 14922 14925 14928 +14931 14934 14937 14940 14943 14946 14949 14952 14955 +14958 14961 14964 14967 14970 14973 14976 14979 14982 +14985 14988 14991 14994 14997 15000 15003 15006 15009 +15012 15015 15018 15021 15024 15027 15030 15033 15036 +15039 15042 15045 15048 15051 15054 15057 15060 15063 +15066 15069 15072 15075 15078 15081 15084 15087 15090 +15093 15096 15099 15102 15105 15108 15111 15114 15117 +15120 15123 15126 15129 15132 15135 15138 15141 15144 +15147 15150 15153 15156 15159 15162 15165 15168 15171 +15174 15177 15180 15183 15186 15189 15192 15195 15198 +15201 15204 15207 15210 15213 15216 15219 15222 15225 +15228 15231 15234 15237 15240 15243 15246 15249 15252 +15255 15258 15261 15264 15267 15270 15273 15276 15279 +15282 15285 15288 15291 15294 15297 15300 15303 15306 +15309 15312 15315 15318 15321 15324 15327 15330 15333 +15336 15339 15342 15345 15348 15351 15354 15357 15360 +15363 15366 15369 15372 15375 15378 15381 15384 15387 +15390 15393 15396 15399 15402 15405 15408 15411 15414 +15417 15420 15423 15426 15429 15432 15435 15438 15441 +15444 15447 15450 15453 15456 15459 15462 15465 15468 +15471 15474 15477 15480 15483 15486 15489 15492 15495 +15498 15501 15504 15507 15510 15513 15516 15519 15522 +15525 15528 15531 15534 15537 15540 15543 15546 15549 +15552 15555 15558 15561 15564 15567 15570 15573 15576 +15579 15582 15585 15588 15591 15594 15597 15600 15603 +15606 15609 15612 15615 15618 15621 15624 15627 15630 +15633 15636 15639 15642 15645 15648 15651 15654 15657 +15660 15663 15666 15669 15672 15675 15678 15681 15684 +15687 15690 15693 15696 15699 15702 15705 15708 15711 +15714 15717 15720 15723 15726 15729 15732 15735 15738 +15741 15744 15747 15750 15753 15756 15759 15762 15765 +15768 15771 15774 15777 15780 15783 15786 15789 15792 +15795 15798 15801 15804 15807 15810 15813 15816 15819 +15822 15825 15828 15831 15834 15837 15840 15843 15846 +15849 15852 15855 15858 15861 15864 15867 15870 15873 +15876 15879 15882 15885 15888 15891 15894 15897 15900 +15903 15906 15909 15912 15915 15918 15921 15924 15927 +15930 15933 15936 15939 15942 15945 15948 15951 15954 +15957 15960 15963 15966 15969 15972 15975 15978 15981 +15984 15987 15990 15993 15996 15999 16002 16005 16008 +16011 16014 16017 16020 16023 16026 16029 16032 16035 +16038 16041 16044 16047 16050 16053 16056 16059 16062 +16065 16068 16071 16074 16077 16080 16083 16086 16089 +16092 16095 16098 16101 16104 16107 16110 16113 16116 +16119 16122 16125 16128 16131 16134 16137 16140 16143 +16146 16149 16152 16155 16158 16161 16164 16167 16170 +16173 16176 16179 16182 16185 16188 16191 16194 16197 +16200 16203 16206 16209 16212 16215 16218 16221 16224 +16227 16230 16233 16236 16239 16242 16245 16248 16251 +16254 16257 16260 16263 16266 16269 16272 16275 16278 +16281 16284 16287 16290 16293 16296 16299 16302 16305 +16308 16311 16314 16317 16320 16323 16326 16329 16332 +16335 16338 16341 16344 16347 16350 16353 16356 16359 +16362 16365 16368 16371 16374 16377 16380 16383 16386 +16389 16392 16395 16398 16401 16404 16407 16410 16413 +16416 16419 16422 16425 16428 16431 16434 16437 16440 +16443 16446 16449 16452 16455 16458 16461 16464 16467 +16470 16473 16476 16479 16482 16485 16488 16491 16494 +16497 16500 16503 16506 16509 16512 16515 16518 16521 +16524 16527 16530 16533 16536 16539 16542 16545 16548 +16551 16554 16557 16560 16563 16566 16569 16572 16575 +16578 16581 16584 16587 16590 16593 16596 16599 16602 +16605 16608 16611 16614 16617 16620 16623 16626 16629 +16632 16635 16638 16641 16644 16647 16650 16653 16656 +16659 16662 16665 16668 16671 16674 16677 16680 16683 +16686 16689 16692 16695 16698 16701 16704 16707 16710 +16713 16716 16719 16722 16725 16728 16731 16734 16737 +16740 16743 16746 16749 16752 16755 16758 16761 16764 +16767 16770 16773 16776 16779 16782 16785 16788 16791 +16794 16797 16800 16803 16806 16809 16812 16815 16818 +16821 16824 16827 16830 16833 16836 16839 16842 16845 +16848 16851 16854 16857 16860 16863 16866 16869 16872 +16875 16878 16881 16884 16887 16890 16893 16896 16899 +16902 16905 16908 16911 16914 16917 16920 16923 16926 +16929 16932 16935 16938 16941 16944 16947 16950 16953 +16956 16959 16962 16965 16968 16971 16974 16977 16980 +16983 16986 16989 16992 16995 16998 17001 17004 17007 +17010 17013 17016 17019 17022 17025 17028 17031 17034 +17037 17040 17043 17046 17049 17052 17055 17058 17061 +17064 17067 17070 17073 17076 17079 17082 17085 17088 +17091 17094 17097 17100 17103 17106 17109 17112 17115 +17118 17121 17124 17127 17130 17133 17136 17139 17142 +17145 17148 17151 17154 17157 17160 17163 17166 17169 +17172 17175 17178 17181 17184 17187 17190 17193 17196 +17199 17202 17205 17208 17211 17214 17217 17220 17223 +17226 17229 17232 17235 17238 17241 17244 17247 17250 +17253 17256 17259 17262 17265 17268 17271 17274 17277 +17280 17283 17286 17289 17292 17295 17298 17301 17304 +17307 17310 17313 17316 17319 17322 17325 17328 17331 +17334 17337 17340 17343 17346 17349 17352 17355 17358 +17361 17364 17367 17370 17373 17376 17379 17382 17385 +17388 17391 17394 17397 17400 17403 17406 17409 17412 +17415 17418 17421 17424 17427 17430 17433 17436 17439 +17442 17445 17448 17451 17454 17457 17460 17463 17466 +17469 17472 17475 17478 17481 17484 17487 17490 17493 +17496 17499 17502 17505 17508 17511 17514 17517 17520 +17523 17526 17529 17532 17535 17538 17541 17544 17547 +17550 17553 17556 17559 17562 17565 17568 17571 17574 +17577 17580 17583 17586 17589 17592 17595 17598 17601 +17604 17607 17610 17613 17616 17619 17622 17625 17628 +17631 17634 17637 17640 17643 17646 17649 17652 17655 +17658 17661 17664 17667 17670 17673 17676 17679 17682 +17685 17688 17691 17694 17697 17700 17703 17706 17709 +17712 17715 17718 17721 17724 17727 17730 17733 17736 +17739 17742 17745 17748 17751 17754 17757 17760 17763 +17766 17769 17772 17775 17778 17781 17784 17787 17790 +17793 17796 17799 17802 17805 17808 17811 17814 17817 +17820 17823 17826 17829 17832 17835 17838 17841 17844 +17847 17850 17853 17856 17859 17862 17865 17868 17871 +17874 17877 17880 17883 17886 17889 17892 17895 17898 +17901 17904 17907 17910 17913 17916 17919 17922 17925 +17928 17931 17934 17937 17940 17943 17946 17949 17952 +17955 17958 17961 17964 17967 17970 17973 17976 17979 +17982 17985 17988 17991 17994 17997 18000 18003 18006 +18009 18012 18015 18018 18021 18024 18027 18030 18033 +18036 18039 18042 18045 18048 18051 18054 18057 18060 +18063 18066 18069 18072 18075 18078 18081 18084 18087 +18090 18093 18096 18099 18102 18105 18108 18111 18114 +18117 18120 18123 18126 18129 18132 18135 18138 18141 +18144 18147 18150 18153 18156 18159 18162 18165 18168 +18171 18174 18177 18180 18183 18186 18189 18192 18195 +18198 18201 18204 18207 18210 18213 18216 18219 18222 +18225 18228 18231 18234 18237 18240 18243 18246 18249 +18252 18255 18258 18261 18264 18267 18270 18273 18276 +18279 18282 18285 18288 18291 18294 18297 18300 18303 +18306 18309 18312 18315 18318 18321 18324 18327 18330 +18333 18336 18339 18342 18345 18348 18351 18354 18357 +18360 18363 18366 18369 18372 18375 18378 18381 18384 +18387 18390 18393 18396 18399 18402 18405 18408 18411 +18414 18417 18420 18423 18426 18429 18432 18435 18438 +18441 18444 18447 18450 18453 18456 18459 18462 18465 +18468 18471 18474 18477 18480 18483 18486 18489 18492 +18495 18498 18501 18504 18507 18510 18513 18516 18519 +18522 18525 18528 18531 18534 18537 18540 18543 18546 +18549 18552 18555 18558 18561 18564 18567 18570 18573 +18576 18579 18582 18585 18588 18591 18594 18597 18600 +18603 18606 18609 18612 18615 18618 18621 18624 18627 +18630 18633 18636 18639 18642 18645 18648 18651 18654 +18657 18660 18663 18666 18669 18672 18675 18678 18681 +18684 18687 18690 18693 18696 18699 18702 18705 18708 +18711 18714 18717 18720 18723 18726 18729 18732 18735 +18738 18741 18744 18747 18750 18753 18756 18759 18762 +18765 18768 18771 18774 18777 18780 18783 18786 18789 +18792 18795 18798 18801 18804 18807 18810 18813 18816 +18819 18822 18825 18828 18831 18834 18837 18840 18843 +18846 18849 18852 18855 18858 18861 18864 18867 18870 +18873 18876 18879 18882 18885 18888 18891 18894 18897 +18900 18903 18906 18909 18912 18915 18918 18921 18924 +18927 18930 18933 18936 18939 18942 18945 18948 18951 +18954 18957 18960 18963 18966 18969 18972 18975 18978 +18981 18984 18987 18990 18993 18996 18999 19002 19005 +19008 19011 19014 19017 19020 19023 19026 19029 19032 +19035 19038 19041 19044 19047 19050 19053 19056 19059 +19062 19065 19068 19071 19074 19077 19080 19083 19086 +19089 19092 19095 19098 19101 19104 19107 19110 19113 +19116 19119 19122 19125 19128 19131 19134 19137 19140 +19143 19146 19149 19152 19155 19158 19161 19164 19167 +19170 19173 19176 19179 19182 19185 19188 19191 19194 +19197 19200 19203 19206 19209 19212 19215 19218 19221 +19224 19227 19230 19233 19236 19239 19242 19245 19248 +19251 19254 19257 19260 19263 19266 19269 19272 19275 +19278 19281 19284 19287 19290 19293 19296 19299 19302 +19305 19308 19311 19314 19317 19320 19323 19326 19329 +19332 19335 19338 19341 19344 19347 19350 19353 19356 +19359 19362 19365 19368 19371 19374 19377 19380 19383 +19386 19389 19392 19395 19398 19401 19404 19407 19410 +19413 19416 19419 19422 19425 19428 19431 19434 19437 +19440 19443 19446 19449 19452 19455 19458 19461 19464 +19467 19470 19473 19476 19479 19482 19485 19488 19491 +19494 19497 19500 19503 19506 19509 19512 19515 19518 +19521 19524 19527 19530 19533 19536 19539 19542 19545 +19548 19551 19554 19557 19560 19563 19566 19569 19572 +19575 19578 19581 19584 19587 19590 19593 19596 19599 +19602 19605 19608 19611 19614 19617 19620 19623 19626 +19629 19632 19635 19638 19641 19644 19647 19650 19653 +19656 19659 19662 19665 19668 19671 19674 19677 19680 +19683 19686 19689 19692 19695 19698 19701 19704 19707 +19710 19713 19716 19719 19722 19725 19728 19731 19734 +19737 19740 19743 19746 19749 19752 19755 19758 19761 +19764 19767 19770 19773 19776 19779 19782 19785 19788 +19791 19794 19797 19800 19803 19806 19809 19812 19815 +19818 19821 19824 19827 19830 19833 19836 19839 19842 +19845 19848 19851 19854 19857 19860 19863 19866 19869 +19872 19875 19878 19881 19884 19887 19890 19893 19896 +19899 19902 19905 19908 19911 19914 19917 19920 19923 +19926 19929 19932 19935 19938 19941 19944 19947 19950 +19953 19956 19959 19962 19965 19968 19971 19974 19977 +19980 19983 19986 19989 19992 19995 19998 20001 20004 +20007 20010 20013 20016 20019 20022 20025 20028 20031 +20034 20037 20040 20043 20046 20049 20052 20055 20058 +20061 20064 20067 20070 20073 20076 20079 20082 20085 +20088 20091 20094 20097 20100 20103 20106 20109 20112 +20115 20118 20121 20124 20127 20130 20133 20136 20139 +20142 20145 20148 20151 20154 20157 20160 20163 20166 +20169 20172 20175 20178 20181 20184 20187 20190 20193 +20196 20199 20202 20205 20208 20211 20214 20217 20220 +20223 20226 20229 20232 20235 20238 20241 20244 20247 +20250 20253 20256 20259 20262 20265 20268 20271 20274 +20277 20280 20283 20286 20289 20292 20295 20298 20301 +20304 20307 20310 20313 20316 20319 20322 20325 20328 +20331 20334 20337 20340 20343 20346 20349 20352 20355 +20358 20361 20364 20367 20370 20373 20376 20379 20382 +20385 20388 20391 20394 20397 20400 20403 20406 20409 +20412 20415 20418 20421 20424 20427 20430 20433 20436 +20439 20442 20445 20448 20451 20454 20457 20460 20463 +20466 20469 20472 20475 20478 20481 20484 20487 20490 +20493 20496 20499 20502 20505 20508 20511 20514 20517 +20520 20523 20526 20529 20532 20535 20538 20541 20544 +20547 20550 20553 20556 20559 20562 20565 20568 20571 +20574 20577 20580 20583 20586 20589 20592 20595 20598 +20601 20604 20607 20610 20613 20616 20619 20622 20625 +20628 20631 20634 20637 20640 20643 20646 20649 20652 +20655 20658 20661 20664 20667 20670 20673 20676 20679 +20682 20685 20688 20691 20694 20697 20700 20703 20706 +20709 20712 20715 20718 20721 20724 20727 20730 20733 +20736 20739 20742 20745 20748 20751 20754 20757 20760 +20763 20766 20769 20772 20775 20778 20781 20784 20787 +20790 20793 20796 20799 20802 20805 20808 20811 20814 +20817 20820 20823 20826 20829 20832 20835 20838 20841 +20844 20847 20850 20853 20856 20859 20862 20865 20868 +20871 20874 20877 20880 20883 20886 20889 20892 20895 +20898 20901 20904 20907 20910 20913 20916 20919 20922 +20925 20928 20931 20934 20937 20940 20943 20946 20949 +20952 20955 20958 20961 20964 20967 20970 20973 20976 +20979 20982 20985 20988 20991 20994 20997 21000 21003 +21006 21009 21012 21015 21018 21021 21024 21027 21030 +21033 21036 21039 21042 21045 21048 21051 21054 21057 +21060 21063 21066 21069 21072 21075 21078 21081 21084 +21087 21090 21093 21096 21099 21102 21105 21108 21111 +21114 21117 21120 21123 21126 21129 21132 21135 21138 +21141 21144 21147 21150 21153 21156 21159 21162 21165 +21168 21171 21174 21177 21180 21183 21186 21189 21192 +21195 21198 21201 21204 21207 21210 21213 21216 21219 +21222 21225 21228 21231 21234 21237 21240 21243 21246 +21249 21252 21255 21258 21261 21264 21267 21270 21273 +21276 21279 21282 21285 21288 21291 21294 21297 21300 +21303 21306 21309 21312 21315 21318 21321 21324 21327 +21330 21333 21336 21339 21342 21345 21348 21351 21354 +21357 21360 21363 21366 21369 21372 21375 21378 21381 +21384 21387 21390 21393 21396 21399 21402 21405 21408 +21411 21414 21417 21420 21423 21426 21429 21432 21435 +21438 21441 21444 21447 21450 21453 21456 21459 21462 +21465 21468 21471 21474 21477 21480 21483 21486 21489 +21492 21495 21498 21501 21504 21507 21510 21513 21516 +21519 21522 21525 21528 21531 21534 21537 21540 21543 +21546 21549 21552 21555 21558 21561 21564 21567 21570 +21573 21576 21579 21582 21585 21588 21591 21594 21597 +21600 21603 21606 21609 21612 21615 21618 21621 21624 +21627 21630 21633 21636 21639 21642 21645 21648 21651 +21654 21657 21660 21663 21666 21669 21672 21675 21678 +21681 21684 21687 21690 21693 21696 21699 21702 21705 +21708 21711 21714 21717 21720 21723 21726 21729 21732 +21735 21738 21741 21744 21747 21750 21753 21756 21759 +21762 21765 21768 21771 21774 21777 21780 21783 21786 +21789 21792 21795 21798 21801 21804 21807 21810 21813 +21816 21819 21822 21825 21828 21831 21834 21837 21840 +21843 21846 21849 21852 21855 21858 21861 21864 21867 +21870 21873 21876 21879 21882 21885 21888 21891 21894 +21897 21900 21903 21906 21909 21912 21915 21918 21921 +21924 21927 21930 21933 21936 21939 21942 21945 21948 +21951 21954 21957 21960 21963 21966 21969 21972 21975 +21978 21981 21984 21987 21990 21993 21996 21999 22002 +22005 22008 22011 22014 22017 22020 22023 22026 22029 +22032 22035 22038 22041 22044 22047 22050 22053 22056 +22059 22062 22065 22068 22071 22074 22077 22080 22083 +22086 22089 22092 22095 22098 22101 22104 22107 22110 +22113 22116 22119 22122 22125 22128 22131 22134 22137 +22140 22143 22146 22149 22152 22155 22158 22161 22164 +22167 22170 22173 22176 22179 22182 22185 22188 22191 +22194 22197 22200 22203 22206 22209 22212 22215 22218 +22221 22224 22227 22230 22233 22236 22239 22242 22245 +22248 22251 22254 22257 22260 22263 22266 22269 22272 +22275 22278 22281 22284 22287 22290 22293 22296 22299 +22302 22305 22308 22311 22314 22317 22320 22323 22326 +22329 22332 22335 22338 22341 22344 22347 22350 22353 +22356 22359 22362 22365 22368 22371 22374 22377 22380 +22383 22386 22389 22392 22395 22398 22401 22404 22407 +22410 22413 22416 22419 22422 22425 22428 22431 22434 +22437 22440 22443 22446 22449 22452 22455 22458 22461 +22464 22467 22470 22473 22476 22479 22482 22485 22488 +22491 22494 22497 22500 22503 22506 22509 22512 22515 +22518 22521 22524 22527 22530 22533 22536 22539 22542 +22545 22548 22551 22554 22557 22560 22563 22566 22569 +22572 22575 22578 22581 22584 22587 22590 22593 22596 +22599 22602 22605 22608 22611 22614 22617 22620 22623 +22626 22629 22632 22635 22638 22641 22644 22647 22650 +22653 22656 22659 22662 22665 22668 22671 22674 22677 +22680 22683 22686 22689 22692 22695 22698 22701 22704 +22707 22710 22713 22716 22719 22722 22725 22728 22731 +22734 22737 22740 22743 22746 22749 22752 22755 22758 +22761 22764 22767 22770 22773 22776 22779 22782 22785 +22788 22791 22794 22797 22800 22803 22806 22809 22812 +22815 22818 22821 22824 22827 22830 22833 22836 22839 +22842 22845 22848 22851 22854 22857 22860 22863 22866 +22869 22872 22875 22878 22881 22884 22887 22890 22893 +22896 22899 22902 22905 22908 22911 22914 22917 22920 +22923 22926 22929 22932 22935 22938 22941 22944 22947 +22950 22953 22956 22959 22962 22965 22968 22971 22974 +22977 22980 22983 22986 22989 22992 22995 22998 23001 +23004 23007 23010 23013 23016 23019 23022 23025 23028 +23031 23034 23037 23040 23043 23046 23049 23052 23055 +23058 23061 23064 23067 23070 23073 23076 23079 23082 +23085 23088 23091 23094 23097 23100 23103 23106 23109 +23112 23115 23118 23121 23124 23127 23130 23133 23136 +23139 23142 23145 23148 23151 23154 23157 23160 23163 +23166 23169 23172 23175 23178 23181 23184 23187 23190 +23193 23196 23199 23202 23205 23208 23211 23214 23217 +23220 23223 23226 23229 23232 23235 23238 23241 23244 +23247 23250 23253 23256 23259 23262 23265 23268 23271 +23274 23277 23280 23283 23286 23289 23292 23295 23298 +23301 23304 23307 23310 23313 23316 23319 23322 23325 +23328 23331 23334 23337 23340 23343 23346 23349 23352 +23355 23358 23361 23364 23367 23370 23373 23376 23379 +23382 23385 23388 23391 23394 23397 23400 23403 23406 +23409 23412 23415 23418 23421 23424 23427 23430 23433 +23436 23439 23442 23445 23448 23451 23454 23457 23460 +23463 23466 23469 23472 23475 23478 23481 23484 23487 +23490 23493 23496 23499 23502 23505 23508 23511 23514 +23517 23520 23523 23526 23529 23532 23535 23538 23541 +23544 23547 23550 23553 23556 23559 23562 23565 23568 +23571 23574 23577 23580 23583 23586 23589 23592 23595 +23598 23601 23604 23607 23610 23613 23616 23619 23622 +23625 23628 23631 23634 23637 23640 23643 23646 23649 +23652 23655 23658 23661 23664 23667 23670 23673 23676 +23679 23682 23685 23688 23691 23694 23697 23700 23703 +23706 23709 23712 23715 23718 23721 23724 23727 23730 +23733 23736 23739 23742 23745 23748 23751 23754 23757 +23760 23763 23766 23769 23772 23775 23778 23781 23784 +23787 23790 23793 23796 23799 23802 23805 23808 23811 +23814 23817 23820 23823 23826 23829 23832 23835 23838 +23841 23844 23847 23850 23853 23856 23859 23862 23865 +23868 23871 23874 23877 23880 23883 23886 23889 23892 +23895 23898 23901 23904 23907 23910 23913 23916 23919 +23922 23925 23928 23931 23934 23937 23940 23943 23946 +23949 23952 23955 23958 23961 23964 23967 23970 23973 +23976 23979 23982 23985 23988 23991 23994 23997 24000 +24003 24006 24009 24012 24015 24018 24021 24024 24027 +24030 24033 24036 24039 24042 24045 24048 24051 24054 +24057 24060 24063 24066 24069 24072 24075 24078 24081 +24084 24087 24090 24093 24096 24099 24102 24105 24108 +24111 24114 24117 24120 24123 24126 24129 24132 24135 +24138 24141 24144 24147 24150 24153 24156 24159 24162 +24165 24168 24171 24174 24177 24180 24183 24186 24189 +24192 24195 24198 24201 24204 24207 24210 24213 24216 +24219 24222 24225 24228 24231 24234 24237 24240 24243 +24246 24249 24252 24255 24258 24261 24264 24267 24270 +24273 24276 24279 24282 24285 24288 24291 24294 24297 +24300 24303 24306 24309 24312 24315 24318 24321 24324 +24327 24330 24333 24336 24339 24342 24345 24348 24351 +24354 24357 24360 24363 24366 24369 24372 24375 24378 +24381 24384 24387 24390 24393 24396 24399 24402 24405 +24408 24411 24414 24417 24420 24423 24426 24429 24432 +24435 24438 24441 24444 24447 24450 24453 24456 24459 +24462 24465 24468 24471 24474 24477 24480 24483 24486 +24489 24492 24495 24498 24501 24504 24507 24510 24513 +24516 24519 24522 24525 24528 24531 24534 24537 24540 +24543 24546 24549 24552 24555 24558 24561 24564 24567 +24570 24573 24576 24579 24582 24585 24588 24591 24594 +24597 24600 24603 24606 24609 24612 24615 24618 24621 +24624 24627 24630 24633 24636 24639 24642 24645 24648 +24651 24654 24657 24660 24663 24666 24669 24672 24675 +24678 24681 24684 24687 24690 24693 24696 24699 24702 +24705 24708 24711 24714 24717 24720 24723 24726 24729 +24732 24735 24738 24741 24744 24747 24750 24753 24756 +24759 24762 24765 24768 24771 24774 24777 24780 24783 +24786 24789 24792 24795 24798 24801 24804 24807 24810 +24813 24816 24819 24822 24825 24828 24831 24834 24837 +24840 24843 24846 24849 24852 24855 24858 24861 24864 +24867 24870 24873 24876 24879 24882 24885 24888 24891 +24894 24897 24900 24903 24906 24909 24912 24915 24918 +24921 24924 24927 24930 24933 24936 24939 24942 24945 +24948 24951 24954 24957 24960 24963 24966 24969 24972 +24975 24978 24981 24984 24987 24990 24993 24996 24999 +25002 25005 25008 25011 25014 25017 25020 25023 25026 +25029 25032 25035 25038 25041 25044 25047 25050 25053 +25056 25059 25062 25065 25068 25071 25074 25077 25080 +25083 25086 25089 25092 25095 25098 25101 25104 25107 +25110 25113 25116 25119 25122 25125 25128 25131 25134 +25137 25140 25143 25146 25149 25152 25155 25158 25161 +25164 25167 25170 25173 25176 25179 25182 25185 25188 +25191 25194 25197 25200 25203 25206 25209 25212 25215 +25218 25221 25224 25227 25230 25233 25236 25239 25242 +25245 25248 25251 25254 25257 25260 25263 25266 25269 +25272 25275 25278 25281 25284 25287 25290 25293 25296 +25299 25302 25305 25308 25311 25314 25317 25320 25323 +25326 25329 25332 25335 25338 25341 25344 25347 25350 +25353 25356 25359 25362 25365 25368 25371 25374 25377 +25380 25383 25386 25389 25392 25395 25398 25401 25404 +25407 25410 25413 25416 25419 25422 25425 25428 25431 +25434 25437 25440 25443 25446 25449 25452 25455 25458 +25461 25464 25467 25470 25473 25476 25479 25482 25485 +25488 25491 25494 25497 25500 25503 25506 25509 25512 +25515 25518 25521 25524 25527 25530 25533 25536 25539 +25542 25545 25548 25551 25554 25557 25560 25563 25566 +25569 25572 25575 25578 25581 25584 25587 25590 25593 +25596 25599 25602 25605 25608 25611 25614 25617 25620 +25623 25626 25629 25632 25635 25638 25641 25644 25647 +25650 25653 25656 25659 25662 25665 25668 25671 25674 +25677 25680 25683 25686 25689 25692 25695 25698 25701 +25704 25707 25710 25713 25716 25719 25722 25725 25728 +25731 25734 25737 25740 25743 25746 25749 25752 25755 +25758 25761 25764 25767 25770 25773 25776 25779 25782 +25785 25788 25791 25794 25797 25800 25803 25806 25809 +25812 25815 25818 25821 25824 25827 25830 25833 25836 +25839 25842 25845 25848 25851 25854 25857 25860 25863 +25866 25869 25872 25875 25878 25881 25884 25887 25890 +25893 25896 25899 25902 25905 25908 25911 25914 25917 +25920 25923 25926 25929 25932 25935 25938 25941 25944 +25947 25950 25953 25956 25959 25962 25965 25968 25971 +25974 25977 25980 25983 25986 25989 25992 25995 25998 +26001 26004 26007 26010 26013 26016 26019 26022 26025 +26028 26031 26034 26037 26040 26043 26046 26049 26052 +26055 26058 26061 26064 26067 26070 26073 26076 26079 +26082 26085 26088 26091 26094 26097 26100 26103 26106 +26109 26112 26115 26118 26121 26124 26127 26130 26133 +26136 26139 26142 26145 26148 26151 26154 26157 26160 +26163 26166 26169 26172 26175 26178 26181 26184 26187 +26190 26193 26196 26199 26202 26205 26208 26211 26214 +26217 26220 26223 26226 26229 26232 26235 26238 26241 +26244 26247 26250 26253 26256 26259 26262 26265 26268 +26271 26274 26277 26280 26283 26286 26289 26292 26295 +26298 26301 26304 26307 26310 26313 26316 26319 26322 +26325 26328 26331 26334 26337 26340 26343 26346 26349 +26352 26355 26358 26361 26364 26367 26370 26373 26376 +26379 26382 26385 26388 26391 26394 26397 26400 26403 +26406 26409 26412 26415 26418 26421 26424 26427 26430 +26433 26436 26439 26442 26445 26448 26451 26454 26457 +26460 26463 26466 26469 26472 26475 26478 26481 26484 +26487 26490 26493 26496 26499 26502 26505 26508 26511 +26514 26517 26520 26523 26526 26529 26532 26535 26538 +26541 26544 26547 26550 26553 26556 26559 26562 26565 +26568 26571 26574 26577 26580 26583 26586 26589 26592 +26595 26598 26601 26604 26607 26610 26613 26616 26619 +26622 26625 26628 26631 26634 26637 26640 26643 26646 +26649 26652 26655 26658 26661 26664 26667 26670 26673 +26676 26679 26682 26685 26688 26691 26694 26697 26700 +26703 26706 26709 26712 26715 26718 26721 26724 26727 +26730 26733 26736 26739 26742 26745 26748 26751 26754 +26757 26760 26763 26766 26769 26772 26775 26778 26781 +26784 26787 26790 26793 26796 26799 26802 26805 26808 +26811 26814 26817 26820 26823 26826 26829 26832 26835 +26838 26841 26844 26847 26850 26853 26856 26859 26862 +26865 26868 26871 26874 26877 26880 26883 26886 26889 +26892 26895 26898 26901 26904 26907 26910 26913 26916 +26919 26922 26925 26928 26931 26934 26937 26940 26943 +26946 26949 26952 26955 26958 26961 26964 26967 26970 +26973 26976 26979 26982 26985 26988 26991 26994 26997 +27000 27003 27006 27009 27012 27015 27018 27021 27024 +27027 27030 27033 27036 27039 27042 27045 27048 27051 +27054 27057 27060 27063 27066 27069 27072 27075 27078 +27081 27084 27087 27090 27093 27096 27099 27102 27105 +27108 27111 27114 27117 27120 27123 27126 27129 27132 +27135 27138 27141 27144 27147 27150 27153 27156 27159 +27162 27165 27168 27171 27174 27177 27180 27183 27186 +27189 27192 27195 27198 27201 27204 27207 27210 27213 +27216 27219 27222 27225 27228 27231 27234 27237 27240 +27243 27246 27249 27252 27255 27258 27261 27264 27267 +27270 27273 27276 27279 27282 27285 27288 27291 27294 +27297 27300 27303 27306 27309 27312 27315 27318 27321 +27324 27327 27330 27333 27336 27339 27342 27345 27348 +27351 27354 27357 27360 27363 27366 27369 27372 27375 +27378 27381 27384 27387 27390 27393 27396 27399 27402 +27405 27408 27411 27414 27417 27420 27423 27426 27429 +27432 27435 27438 27441 27444 27447 27450 27453 27456 +27459 27462 27465 27468 27471 27474 27477 27480 27483 +27486 27489 27492 27495 27498 27501 27504 27507 27510 +27513 27516 27519 27522 27525 27528 27531 27534 27537 +27540 27543 27546 27549 27552 27555 27558 27561 27564 +27567 27570 27573 27576 27579 27582 27585 27588 27591 +27594 27597 27600 27603 27606 27609 27612 27615 27618 +27621 27624 27627 27630 27633 27636 27639 27642 27645 +27648 27651 27654 27657 27660 27663 27666 27669 27672 +27675 27678 27681 27684 27687 27690 27693 27696 27699 +27702 27705 27708 27711 27714 27717 27720 27723 27726 +27729 27732 27735 27738 27741 27744 27747 27750 27753 +27756 27759 27762 27765 27768 27771 27774 27777 27780 +27783 27786 27789 27792 27795 27798 27801 27804 27807 +27810 27813 27816 27819 27822 27825 27828 27831 27834 +27837 27840 27843 27846 27849 27852 27855 27858 27861 +27864 27867 27870 27873 27876 27879 27882 27885 27888 +27891 27894 27897 27900 27903 27906 27909 27912 27915 +27918 27921 27924 27927 27930 27933 27936 27939 27942 +27945 27948 27951 27954 27957 27960 27963 27966 27969 +27972 27975 27978 27981 27984 27987 27990 27993 27996 +27999 28002 28005 28008 28011 28014 28017 28020 28023 +28026 28029 28032 28035 28038 28041 28044 28047 28050 +28053 28056 28059 28062 28065 28068 28071 28074 28077 +28080 28083 28086 28089 28092 28095 28098 28101 28104 +28107 28110 28113 28116 28119 28122 28125 28128 28131 +28134 28137 28140 28143 28146 28149 28152 28155 28158 +28161 28164 28167 28170 28173 28176 28179 28182 28185 +28188 28191 28194 28197 28200 28203 28206 28209 28212 +28215 28218 28221 28224 28227 28230 28233 28236 28239 +28242 28245 28248 28251 28254 28257 28260 28263 28266 +28269 28272 28275 28278 28281 28284 28287 28290 28293 +28296 28299 28302 28305 28308 28311 28314 28317 28320 +28323 28326 28329 28332 28335 28338 28341 28344 28347 +28350 28353 28356 28359 28362 28365 28368 28371 28374 +28377 28380 28383 28386 28389 28392 28395 28398 28401 +28404 28407 28410 28413 28416 28419 28422 28425 28428 +28431 28434 28437 28440 28443 28446 28449 28452 28455 +28458 28461 28464 28467 28470 28473 28476 28479 28482 +28485 28488 28491 28494 28497 28500 28503 28506 28509 +28512 28515 28518 28521 28524 28527 28530 28533 28536 +28539 28542 28545 28548 28551 28554 28557 28560 28563 +28566 28569 28572 28575 28578 28581 28584 28587 28590 +28593 28596 28599 28602 28605 28608 28611 28614 28617 +28620 28623 28626 28629 28632 28635 28638 28641 28644 +28647 28650 28653 28656 28659 28662 28665 28668 28671 +28674 28677 28680 28683 28686 28689 28692 28695 28698 +28701 28704 28707 28710 28713 28716 28719 28722 28725 +28728 28731 28734 28737 28740 28743 28746 28749 28752 +28755 28758 28761 28764 28767 28770 28773 28776 28779 +28782 28785 28788 28791 28794 28797 28800 28803 28806 +28809 28812 28815 28818 28821 28824 28827 28830 28833 +28836 28839 28842 28845 28848 28851 28854 28857 28860 +28863 28866 28869 28872 28875 28878 28881 28884 28887 +28890 28893 28896 28899 28902 28905 28908 28911 28914 +28917 28920 28923 28926 28929 28932 28935 28938 28941 +28944 28947 28950 28953 28956 28959 28962 28965 28968 +28971 28974 28977 28980 28983 28986 28989 28992 28995 +28998 29001 29004 29007 29010 29013 29016 29019 29022 +29025 29028 29031 29034 29037 29040 29043 29046 29049 +29052 29055 29058 29061 29064 29067 29070 29073 29076 +29079 29082 29085 29088 29091 29094 29097 29100 29103 +29106 29109 29112 29115 29118 29121 29124 29127 29130 +29133 29136 29139 29142 29145 29148 29151 29154 29157 +29160 29163 29166 29169 29172 29175 29178 29181 29184 +29187 29190 29193 29196 29199 29202 29205 29208 29211 +29214 29217 29220 29223 29226 29229 29232 29235 29238 +29241 29244 29247 29250 29253 29256 29259 29262 29265 +29268 29271 29274 29277 29280 29283 29286 29289 29292 +29295 29298 29301 29304 29307 29310 29313 29316 29319 +29322 29325 29328 29331 29334 29337 29340 29343 29346 +29349 29352 29355 29358 29361 29364 29367 29370 29373 +29376 29379 29382 29385 29388 29391 29394 29397 29400 +29403 29406 29409 29412 29415 29418 29421 29424 29427 +29430 29433 29436 29439 29442 29445 29448 29451 29454 +29457 29460 29463 29466 29469 29472 29475 29478 29481 +29484 29487 29490 29493 29496 29499 29502 29505 29508 +29511 29514 29517 29520 29523 29526 29529 29532 29535 +29538 29541 29544 29547 29550 29553 29556 29559 29562 +29565 29568 29571 29574 29577 29580 29583 29586 29589 +29592 29595 29598 29601 29604 29607 29610 29613 29616 +29619 29622 29625 29628 29631 29634 29637 29640 29643 +29646 29649 29652 29655 29658 29661 29664 29667 29670 +29673 29676 29679 29682 29685 29688 29691 29694 29697 +29700 29703 29706 29709 29712 29715 29718 29721 29724 +29727 29730 29733 29736 29739 29742 29745 29748 29751 +29754 29757 29760 29763 29766 29769 29772 29775 29778 +29781 29784 29787 29790 29793 29796 29799 29802 29805 +29808 29811 29814 29817 29820 29823 29826 29829 29832 +29835 29838 29841 29844 29847 29850 29853 29856 29859 +29862 29865 29868 29871 29874 29877 29880 29883 29886 +29889 29892 29895 29898 29901 29904 29907 29910 29913 +29916 29919 29922 29925 29928 29931 29934 29937 29940 +29943 29946 29949 29952 29955 29958 29961 29964 29967 +29970 29973 29976 29979 29982 29985 29988 29991 29994 +29997 30000 30003 30006 30009 30012 30015 30018 30021 +30024 30027 30030 30033 30036 30039 30042 30045 30048 +30051 30054 30057 30060 30063 30066 30069 30072 30075 +30078 30081 30084 30087 30090 30093 30096 30099 30102 +30105 30108 30111 30114 30117 30120 30123 30126 30129 +30132 30135 30138 30141 30144 30147 30150 30153 30156 +30159 30162 30165 30168 30171 30174 30177 30180 30183 +30186 30189 30192 30195 30198 30201 30204 30207 30210 +30213 30216 30219 30222 30225 30228 30231 30234 30237 +30240 30243 30246 30249 30252 30255 30258 30261 30264 +30267 30270 30273 30276 30279 30282 30285 30288 30291 +30294 30297 30300 30303 30306 30309 30312 30315 30318 +30321 30324 30327 30330 30333 30336 30339 30342 30345 +30348 30351 30354 30357 30360 30363 30366 30369 30372 +30375 30378 30381 30384 30387 30390 30393 30396 30399 +30402 30405 30408 30411 30414 30417 30420 30423 30426 +30429 30432 30435 30438 30441 30444 30447 30450 30453 +30456 30459 30462 30465 30468 30471 30474 30477 30480 +30483 30486 30489 30492 30495 30498 30501 30504 30507 +30510 30513 30516 30519 30522 30525 30528 30531 30534 +30537 30540 30543 30546 30549 30552 30555 30558 30561 +30564 30567 30570 30573 30576 30579 30582 30585 30588 +30591 30594 30597 30600 30603 30606 30609 30612 30615 +30618 30621 30624 30627 30630 30633 30636 30639 30642 +30645 30648 30651 30654 30657 30660 30663 30666 30669 +30672 30675 30678 30681 30684 30687 30690 30693 30696 +30699 30702 30705 30708 30711 30714 30717 30720 30723 +30726 30729 30732 30735 30738 30741 30744 30747 30750 +30753 30756 30759 30762 30765 30768 30771 30774 30777 +30780 30783 30786 30789 30792 30795 30798 30801 30804 +30807 30810 30813 30816 30819 30822 30825 30828 30831 +30834 30837 30840 30843 30846 30849 30852 30855 30858 +30861 30864 30867 30870 30873 30876 30879 30882 30885 +30888 30891 30894 30897 30900 30903 30906 30909 30912 +30915 30918 30921 30924 30927 30930 30933 30936 30939 +30942 30945 30948 30951 30954 30957 30960 30963 30966 +30969 30972 30975 30978 30981 30984 30987 30990 30993 +30996 30999 31002 31005 31008 31011 31014 31017 31020 +31023 31026 31029 31032 31035 31038 31041 31044 31047 +31050 31053 31056 31059 31062 31065 31068 31071 31074 +31077 31080 31083 31086 31089 31092 31095 31098 31101 +31104 31107 31110 31113 31116 31119 31122 31125 31128 +31131 31134 31137 31140 31143 31146 31149 31152 31155 +31158 31161 31164 31167 31170 31173 31176 31179 31182 +31185 31188 31191 31194 31197 31200 31203 31206 31209 +31212 31215 31218 31221 31224 31227 31230 31233 31236 +31239 31242 31245 31248 31251 31254 31257 31260 31263 +31266 31269 31272 31275 31278 31281 31284 31287 31290 +31293 31296 31299 31302 31305 31308 31311 31314 31317 +31320 31323 31326 31329 31332 31335 31338 31341 31344 +31347 31350 31353 31356 31359 31362 31365 31368 31371 +31374 31377 31380 31383 31386 31389 31392 31395 31398 +31401 31404 31407 31410 31413 31416 31419 31422 31425 +31428 31431 31434 31437 31440 31443 31446 31449 31452 +31455 31458 31461 31464 31467 31470 31473 31476 31479 +31482 31485 31488 31491 31494 31497 31500 31503 31506 +31509 31512 31515 31518 31521 31524 31527 31530 31533 +31536 31539 31542 31545 31548 31551 31554 31557 31560 +31563 31566 31569 31572 31575 31578 31581 31584 31587 +31590 31593 31596 31599 31602 31605 31608 31611 31614 +31617 31620 31623 31626 31629 31632 31635 31638 31641 +31644 31647 31650 31653 31656 31659 31662 31665 31668 +31671 31674 31677 31680 31683 31686 31689 31692 31695 +31698 31701 31704 31707 31710 31713 31716 31719 31722 +31725 31728 31731 31734 31737 31740 31743 31746 31749 +31752 31755 31758 31761 31764 31767 31770 31773 31776 +31779 31782 31785 31788 31791 31794 31797 31800 31803 +31806 31809 31812 31815 31818 31821 31824 31827 31830 +31833 31836 31839 31842 31845 31848 31851 31854 31857 +31860 31863 31866 31869 31872 31875 31878 31881 31884 +31887 31890 31893 31896 31899 31902 31905 31908 31911 +31914 31917 31920 31923 31926 31929 31932 31935 31938 +31941 31944 31947 31950 31953 31956 31959 31962 31965 +31968 31971 31974 31977 31980 31983 31986 31989 31992 +31995 31998 32001 32004 32007 32010 32013 32016 32019 +32022 32025 32028 32031 32034 32037 32040 32043 32046 +32049 32052 32055 32058 32061 32064 32067 32070 32073 +32076 32079 32082 32085 32088 32091 32094 32097 32100 +32103 32106 32109 32112 32115 32118 32121 32124 32127 +32130 32133 32136 32139 32142 32145 32148 32151 32154 +32157 32160 32163 32166 32169 32172 32175 32178 32181 +32184 32187 32190 32193 32196 32199 32202 32205 32208 +32211 32214 32217 32220 32223 32226 32229 32232 32235 +32238 32241 32244 32247 32250 32253 32256 32259 32262 +32265 32268 32271 32274 32277 32280 32283 32286 32289 +32292 32295 32298 32301 32304 32307 32310 32313 32316 +32319 32322 32325 32328 32331 32334 32337 32340 32343 +32346 32349 32352 32355 32358 32361 32364 32367 32370 +32373 32376 32379 32382 32385 32388 32391 32394 32397 +32400 32403 32406 32409 32412 32415 32418 32421 32424 +32427 32430 32433 32436 32439 32442 32445 32448 32451 +32454 32457 32460 32463 32466 32469 32472 32475 32478 +32481 32484 32487 32490 32493 32496 32499 32502 32505 +32508 32511 32514 32517 32520 32523 32526 32529 32532 +32535 32538 32541 32544 32547 32550 32553 32556 32559 +32562 32565 32568 32571 32574 32577 32580 32583 32586 +32589 32592 32595 32598 32601 32604 32607 32610 32613 +32616 32619 32622 32625 32628 32631 32634 32637 32640 +32643 32646 32649 32652 32655 32658 32661 32664 32667 +32670 32673 32676 32679 32682 32685 32688 32691 32694 +32697 32700 32703 32706 32709 32712 32715 32718 32721 +32724 32727 32730 32733 32736 32739 32742 32745 32748 +32751 32754 32757 32760 32763 32766 32769 32772 32775 +32778 32781 32784 32787 32790 32793 32796 32799 32802 +32805 32808 32811 32814 32817 32820 32823 32826 32829 +32832 32835 32838 32841 32844 32847 32850 32853 32856 +32859 32862 32865 32868 32871 32874 32877 32880 32883 +32886 32889 32892 32895 32898 32901 32904 32907 32910 +32913 32916 32919 32922 32925 32928 32931 32934 32937 +32940 32943 32946 32949 32952 32955 32958 32961 32964 +32967 32970 32973 32976 32979 32982 32985 32988 32991 +32994 32997 33000 33003 33006 33009 33012 33015 33018 +33021 33024 33027 33030 33033 33036 33039 33042 33045 +33048 33051 33054 33057 33060 33063 33066 33069 33072 +33075 33078 33081 33084 33087 33090 33093 33096 33099 +33102 33105 33108 33111 33114 33117 33120 33123 33126 +33129 33132 33135 33138 33141 33144 33147 33150 33153 +33156 33159 33162 33165 33168 33171 33174 33177 33180 +33183 33186 33189 33192 33195 33198 33201 33204 33207 +33210 33213 33216 33219 33222 33225 33228 33231 33234 +33237 33240 33243 33246 33249 33252 33255 33258 33261 +33264 33267 33270 33273 33276 33279 33282 33285 33288 +33291 33294 33297 33300 33303 33306 33309 33312 33315 +33318 33321 33324 33327 33330 33333 33336 33339 33342 +33345 33348 33351 33354 33357 33360 33363 33366 33369 +33372 33375 33378 33381 33384 33387 33390 33393 33396 +33399 33402 33405 33408 33411 33414 33417 33420 33423 +33426 33429 33432 33435 33438 33441 33444 33447 33450 +33453 33456 33459 33462 33465 33468 33471 33474 33477 +33480 33483 33486 33489 33492 33495 33498 33501 33504 +33507 33510 33513 33516 33519 33522 33525 33528 33531 +33534 33537 33540 33543 33546 33549 33552 33555 33558 +33561 33564 33567 33570 33573 33576 33579 33582 33585 +33588 33591 33594 33597 33600 33603 33606 33609 33612 +33615 33618 33621 33624 33627 33630 33633 33636 33639 +33642 33645 33648 33651 33654 33657 33660 33663 33666 +33669 33672 33675 33678 33681 33684 33687 33690 33693 +33696 33699 33702 33705 33708 33711 33714 33717 33720 +33723 33726 33729 33732 33735 33738 33741 33744 33747 +33750 33753 33756 33759 33762 33765 33768 33771 33774 +33777 33780 33783 33786 33789 33792 33795 33798 33801 +33804 33807 33810 33813 33816 33819 33822 33825 33828 +33831 33834 33837 33840 33843 33846 33849 33852 33855 +33858 33861 33864 33867 33870 33873 33876 33879 33882 +33885 33888 33891 33894 33897 33900 33903 33906 33909 +33912 33915 33918 33921 33924 33927 33930 33933 33936 +33939 33942 33945 33948 33951 33954 33957 33960 33963 +33966 33969 33972 33975 33978 33981 33984 33987 33990 +33993 33996 33999 34002 34005 34008 34011 34014 34017 +34020 34023 34026 34029 34032 34035 34038 34041 34044 +34047 34050 34053 34056 34059 34062 34065 34068 34071 +34074 34077 34080 34083 34086 34089 34092 34095 34098 +34101 34104 34107 34110 34113 34116 34119 34122 34125 +34128 34131 34134 34137 34140 34143 34146 34149 34152 +34155 34158 34161 34164 34167 34170 34173 34176 34179 +34182 34185 34188 34191 34194 34197 34200 34203 34206 +34209 34212 34215 34218 34221 34224 34227 34230 34233 +34236 34239 34242 34245 34248 34251 34254 34257 34260 +34263 34266 34269 34272 34275 34278 34281 34284 34287 +34290 34293 34296 34299 34302 34305 34308 34311 34314 +34317 34320 34323 34326 34329 34332 34335 34338 34341 +34344 34347 34350 34353 34356 34359 34362 34365 34368 +34371 34374 34377 34380 34383 34386 34389 34392 34395 +34398 34401 34404 34407 34410 34413 34416 34419 34422 +34425 34428 34431 34434 34437 34440 34443 34446 34449 +34452 34455 34458 34461 34464 34467 34470 34473 34476 +34479 34482 34485 34488 34491 34494 34497 34500 34503 +34506 34509 34512 34515 34518 34521 34524 34527 34530 +34533 34536 34539 34542 34545 34548 34551 34554 34557 +34560 34563 34566 34569 34572 34575 34578 34581 34584 +34587 34590 34593 34596 34599 34602 34605 34608 34611 +34614 34617 34620 34623 34626 34629 34632 34635 34638 +34641 34644 34647 34650 34653 34656 34659 34662 34665 +34668 34671 34674 34677 34680 34683 34686 34689 34692 +34695 34698 34701 34704 34707 34710 34713 34716 34719 +34722 34725 34728 34731 34734 34737 34740 34743 34746 +34749 34752 34755 34758 34761 34764 34767 34770 34773 +34776 34779 34782 34785 34788 34791 34794 34797 34800 +34803 34806 34809 34812 34815 34818 34821 34824 34827 +34830 34833 34836 34839 34842 34845 34848 34851 34854 +34857 34860 34863 34866 34869 34872 34875 34878 34881 +34884 34887 34890 34893 34896 34899 34902 34905 34908 +34911 34914 34917 34920 34923 34926 34929 34932 34935 +34938 34941 34944 34947 34950 34953 34956 34959 34962 +34965 34968 34971 34974 34977 34980 34983 34986 34989 +34992 34995 34998 35001 35004 35007 35010 35013 35016 +35019 35022 35025 35028 35031 35034 35037 35040 35043 +35046 35049 35052 35055 35058 35061 35064 35067 35070 +35073 35076 35079 35082 35085 35088 35091 35094 35097 +35100 35103 35106 35109 35112 35115 35118 35121 35124 +35127 35130 35133 35136 35139 35142 35145 35148 35151 +35154 35157 35160 35163 35166 35169 35172 35175 35178 +35181 35184 35187 35190 35193 35196 35199 35202 35205 +35208 35211 35214 35217 35220 35223 35226 35229 35232 +35235 35238 35241 35244 35247 35250 35253 35256 35259 +35262 35265 35268 35271 35274 35277 35280 35283 35286 +35289 35292 35295 35298 35301 35304 35307 35310 35313 +35316 35319 35322 35325 35328 35331 35334 35337 35340 +35343 35346 35349 35352 35355 35358 35361 35364 35367 +35370 35373 35376 35379 35382 35385 35388 35391 35394 +35397 35400 35403 35406 35409 35412 35415 35418 35421 +35424 35427 35430 35433 35436 35439 35442 35445 35448 +35451 35454 35457 35460 35463 35466 35469 35472 35475 +35478 35481 35484 35487 35490 35493 35496 35499 35502 +35505 35508 35511 35514 35517 35520 35523 35526 35529 +35532 35535 35538 35541 35544 35547 35550 35553 35556 +35559 35562 35565 35568 35571 35574 35577 35580 35583 +35586 35589 35592 35595 35598 35601 35604 35607 35610 +35613 35616 35619 35622 35625 35628 35631 35634 35637 +35640 35643 35646 35649 35652 35655 35658 35661 35664 +35667 35670 35673 35676 35679 35682 35685 35688 35691 +35694 35697 35700 35703 35706 35709 35712 35715 35718 +35721 35724 35727 35730 35733 35736 35739 35742 35745 +35748 35751 35754 35757 35760 35763 35766 35769 35772 +35775 35778 35781 35784 35787 35790 35793 35796 35799 +35802 35805 35808 35811 35814 35817 35820 35823 35826 +35829 35832 35835 35838 35841 35844 35847 35850 35853 +35856 35859 35862 35865 35868 35871 35874 35877 35880 +35883 35886 35889 35892 35895 35898 35901 35904 35907 +35910 35913 35916 35919 35922 35925 35928 35931 35934 +35937 35940 35943 35946 35949 35952 35955 35958 35961 +35964 35967 35970 35973 35976 35979 35982 35985 35988 +35991 35994 35997 36000 36003 36006 36009 36012 36015 +36018 36021 36024 36027 36030 36033 36036 36039 36042 +36045 36048 36051 36054 36057 36060 36063 36066 36069 +36072 36075 36078 36081 36084 36087 36090 36093 36096 +36099 36102 36105 36108 36111 36114 36117 36120 36123 +36126 36129 36132 36135 36138 36141 36144 36147 36150 +36153 36156 36159 36162 36165 36168 36171 36174 36177 +36180 36183 36186 36189 36192 36195 36198 36201 36204 +36207 36210 36213 36216 36219 36222 36225 36228 36231 +36234 36237 36240 36243 36246 36249 36252 36255 36258 +36261 36264 36267 36270 36273 36276 36279 36282 36285 +36288 36291 36294 36297 36300 36303 36306 36309 36312 +36315 36318 36321 36324 36327 36330 36333 36336 36339 +36342 36345 36348 36351 36354 36357 36360 36363 36366 +36369 36372 36375 36378 36381 36384 36387 36390 36393 +36396 36399 36402 36405 36408 36411 36414 36417 36420 +36423 36426 36429 36432 36435 36438 36441 36444 36447 +36450 36453 36456 36459 36462 36465 36468 36471 36474 +36477 36480 36483 36486 36489 36492 36495 36498 36501 +36504 36507 36510 36513 36516 36519 36522 36525 36528 +36531 36534 36537 36540 36543 36546 36549 36552 36555 +36558 36561 36564 36567 36570 36573 36576 36579 36582 +36585 36588 36591 36594 36597 36600 36603 36606 36609 +36612 36615 36618 36621 36624 36627 36630 36633 36636 +36639 36642 36645 36648 36651 36654 36657 36660 36663 +36666 36669 36672 36675 36678 36681 36684 36687 36690 +36693 36696 36699 36702 36705 36708 36711 36714 36717 +36720 36723 36726 36729 36732 36735 36738 36741 36744 +36747 36750 36753 36756 36759 36762 36765 36768 36771 +36774 36777 36780 36783 36786 36789 36792 36795 36798 +36801 36804 36807 36810 36813 36816 36819 36822 36825 +36828 36831 36834 36837 36840 36843 36846 36849 36852 +36855 36858 36861 36864 36867 36870 36873 36876 36879 +36882 36885 36888 36891 36894 36897 36900 36903 36906 +36909 36912 36915 36918 36921 36924 36927 36930 36933 +36936 36939 36942 36945 36948 36951 36954 36957 36960 +36963 36966 36969 36972 36975 36978 36981 36984 36987 +36990 36993 36996 36999 37002 37005 37008 37011 37014 +37017 37020 37023 37026 37029 37032 37035 37038 37041 +37044 37047 37050 37053 37056 37059 37062 37065 37068 +37071 37074 37077 37080 37083 37086 37089 37092 37095 +37098 37101 37104 37107 37110 37113 37116 37119 37122 +37125 37128 37131 37134 37137 37140 37143 37146 37149 +37152 37155 37158 37161 37164 37167 37170 37173 37176 +37179 37182 37185 37188 37191 37194 37197 37200 37203 +37206 37209 37212 37215 37218 37221 37224 37227 37230 +37233 37236 37239 37242 37245 37248 37251 37254 37257 +37260 37263 37266 37269 37272 37275 37278 37281 37284 +37287 37290 37293 37296 37299 37302 37305 37308 37311 +37314 37317 37320 37323 37326 37329 37332 37335 37338 +37341 37344 37347 37350 37353 37356 37359 37362 37365 +37368 37371 37374 37377 37380 37383 37386 37389 37392 +37395 37398 37401 37404 37407 37410 37413 37416 37419 +37422 37425 37428 37431 37434 37437 37440 37443 37446 +37449 37452 37455 37458 37461 37464 37467 37470 37473 +37476 37479 37482 37485 37488 37491 37494 37497 37500 +37503 37506 37509 37512 37515 37518 37521 37524 37527 +37530 37533 37536 37539 37542 37545 37548 37551 37554 +37557 37560 37563 37566 37569 37572 37575 37578 37581 +37584 37587 37590 37593 37596 37599 37602 37605 37608 +37611 37614 37617 37620 37623 37626 37629 37632 37635 +37638 37641 37644 37647 37650 37653 37656 37659 37662 +37665 37668 37671 37674 37677 37680 37683 37686 37689 +37692 37695 37698 37701 37704 37707 37710 37713 37716 +37719 37722 37725 37728 37731 37734 37737 37740 37743 +37746 37749 37752 37755 37758 37761 37764 37767 37770 +37773 37776 37779 37782 37785 37788 37791 37794 37797 +37800 37803 37806 37809 37812 37815 37818 37821 37824 +37827 37830 37833 37836 37839 37842 37845 37848 37851 +37854 37857 37860 37863 37866 37869 37872 37875 37878 +37881 37884 37887 37890 37893 37896 37899 37902 37905 +37908 37911 37914 37917 37920 37923 37926 37929 37932 +37935 37938 37941 37944 37947 37950 37953 37956 37959 +37962 37965 37968 37971 37974 37977 37980 37983 37986 +37989 37992 37995 37998 38001 38004 38007 38010 38013 +38016 38019 38022 38025 38028 38031 38034 38037 38040 +38043 38046 38049 38052 38055 38058 38061 38064 38067 +38070 38073 38076 38079 38082 38085 38088 38091 38094 +38097 38100 38103 38106 38109 38112 38115 38118 38121 +38124 38127 38130 38133 38136 38139 38142 38145 38148 +38151 38154 38157 38160 38163 38166 38169 38172 38175 +38178 38181 38184 38187 38190 38193 38196 38199 38202 +38205 38208 38211 38214 38217 38220 38223 38226 38229 +38232 38235 38238 38241 38244 38247 38250 38253 38256 +38259 38262 38265 38268 38271 38274 38277 38280 38283 +38286 38289 38292 38295 38298 38301 38304 38307 38310 +38313 38316 38319 38322 38325 38328 38331 38334 38337 +38340 38343 38346 38349 38352 38355 38358 38361 38364 +38367 38370 38373 38376 38379 38382 38385 38388 38391 +38394 38397 38400 38403 38406 38409 38412 38415 38418 +38421 38424 38427 38430 38433 38436 38439 38442 38445 +38448 38451 38454 38457 38460 38463 38466 38469 38472 +38475 38478 38481 38484 38487 38490 38493 38496 38499 +38502 38505 38508 38511 38514 38517 38520 38523 38526 +38529 38532 38535 38538 38541 38544 38547 38550 38553 +38556 38559 38562 38565 38568 38571 38574 38577 38580 +38583 38586 38589 38592 38595 38598 38601 38604 38607 +38610 38613 38616 38619 38622 38625 38628 38631 38634 +38637 38640 38643 38646 38649 38652 38655 38658 38661 +38664 38667 38670 38673 38676 38679 38682 38685 38688 +38691 38694 38697 38700 38703 38706 38709 38712 38715 +38718 38721 38724 38727 38730 38733 38736 38739 38742 +38745 38748 38751 38754 38757 38760 38763 38766 38769 +38772 38775 38778 38781 38784 38787 38790 38793 38796 +38799 38802 38805 38808 38811 38814 38817 38820 38823 +38826 38829 38832 38835 38838 38841 38844 38847 38850 +38853 38856 38859 38862 38865 38868 38871 38874 38877 +38880 38883 38886 38889 38892 38895 38898 38901 38904 +38907 38910 38913 38916 38919 38922 38925 38928 38931 +38934 38937 38940 38943 38946 38949 38952 38955 38958 +38961 38964 38967 38970 38973 38976 38979 38982 38985 +38988 38991 38994 38997 39000 39003 39006 39009 39012 +39015 39018 39021 39024 39027 39030 39033 39036 39039 +39042 39045 39048 39051 39054 39057 39060 39063 39066 +39069 39072 39075 39078 39081 39084 39087 39090 39093 +39096 39099 39102 39105 39108 39111 39114 39117 39120 +39123 39126 39129 39132 39135 39138 39141 39144 39147 +39150 39153 39156 39159 39162 39165 39168 39171 39174 +39177 39180 39183 39186 39189 39192 39195 39198 39201 +39204 39207 39210 39213 39216 39219 39222 39225 39228 +39231 39234 39237 39240 39243 39246 39249 39252 39255 +39258 39261 39264 39267 39270 39273 39276 39279 39282 +39285 39288 39291 39294 39297 39300 39303 39306 39309 +39312 39315 39318 39321 39324 39327 39330 39333 39336 +39339 39342 39345 39348 39351 39354 39357 39360 39363 +39366 39369 39372 39375 39378 39381 39384 39387 39390 +39393 39396 39399 39402 39405 39408 39411 39414 39417 +39420 39423 39426 39429 39432 39435 39438 39441 39444 +39447 39450 39453 39456 39459 39462 39465 39468 39471 +39474 39477 39480 39483 39486 39489 39492 39495 39498 +39501 39504 39507 39510 39513 39516 39519 39522 39525 +39528 39531 39534 39537 39540 39543 39546 39549 39552 +39555 39558 39561 39564 39567 39570 39573 39576 39579 +39582 39585 39588 39591 39594 39597 39600 39603 39606 +39609 39612 39615 39618 39621 39624 39627 39630 39633 +39636 39639 39642 39645 39648 39651 39654 39657 39660 +39663 39666 39669 39672 39675 39678 39681 39684 39687 +39690 39693 39696 39699 39702 39705 39708 39711 39714 +39717 39720 39723 39726 39729 39732 39735 39738 39741 +39744 39747 39750 39753 39756 39759 39762 39765 39768 +39771 39774 39777 39780 39783 39786 39789 39792 39795 +39798 39801 39804 39807 39810 39813 39816 39819 39822 +39825 39828 39831 39834 39837 39840 39843 39846 39849 +39852 39855 39858 39861 39864 39867 39870 39873 39876 +39879 39882 39885 39888 39891 39894 39897 39900 39903 +39906 39909 39912 39915 39918 39921 39924 39927 39930 +39933 39936 39939 39942 39945 39948 39951 39954 39957 +39960 39963 39966 39969 39972 39975 39978 39981 39984 +39987 39990 39993 39996 39999 40002 40005 40008 40011 +40014 40017 40020 40023 40026 40029 40032 40035 40038 +40041 40044 40047 40050 40053 40056 40059 40062 40065 +40068 40071 40074 40077 40080 40083 40086 40089 40092 +40095 40098 40101 40104 40107 40110 40113 40116 40119 +40122 40125 40128 40131 40134 40137 40140 40143 40146 +40149 40152 40155 40158 40161 40164 40167 40170 40173 +40176 40179 40182 40185 40188 40191 40194 40197 40200 +40203 40206 40209 40212 40215 40218 40221 40224 40227 +40230 40233 40236 40239 40242 40245 40248 40251 40254 +40257 40260 40263 40266 40269 40272 40275 40278 40281 +40284 40287 40290 40293 40296 40299 40302 40305 40308 +40311 40314 40317 40320 40323 40326 40329 40332 40335 +40338 40341 40344 40347 40350 40353 40356 40359 40362 +40365 40368 40371 40374 40377 40380 40383 40386 40389 +40392 40395 40398 40401 40404 40407 40410 40413 40416 +40419 40422 40425 40428 40431 40434 40437 40440 40443 +40446 40449 40452 40455 40458 40461 40464 40467 40470 +40473 40476 40479 40482 40485 40488 40491 40494 40497 +40500 40503 40506 40509 40512 40515 40518 40521 40524 +40527 40530 40533 40536 40539 40542 40545 40548 40551 +40554 40557 40560 40563 40566 40569 40572 40575 40578 +40581 40584 40587 40590 40593 40596 40599 40602 40605 +40608 40611 40614 40617 40620 40623 40626 40629 40632 +40635 40638 40641 40644 40647 40650 40653 40656 40659 +40662 40665 40668 40671 40674 40677 40680 40683 40686 +40689 40692 40695 40698 40701 40704 40707 40710 40713 +40716 40719 40722 40725 40728 40731 40734 40737 40740 +40743 40746 40749 40752 40755 40758 40761 40764 40767 +40770 40773 40776 40779 40782 40785 40788 40791 40794 +40797 40800 40803 40806 40809 40812 40815 40818 40821 +40824 40827 40830 40833 40836 40839 40842 40845 40848 +40851 40854 40857 40860 40863 40866 40869 40872 40875 +40878 40881 40884 40887 40890 40893 40896 40899 40902 +40905 40908 40911 40914 40917 40920 40923 40926 40929 +40932 40935 40938 40941 40944 40947 40950 40953 40956 +40959 40962 40965 40968 40971 40974 40977 40980 40983 +40986 40989 40992 40995 40998 41001 41004 41007 41010 +41013 41016 41019 41022 41025 41028 41031 41034 41037 +41040 41043 41046 41049 41052 41055 41058 41061 41064 +41067 41070 41073 41076 41079 41082 41085 41088 41091 +41094 41097 41100 41103 41106 41109 41112 41115 41118 +41121 41124 41127 41130 41133 41136 41139 41142 41145 +41148 41151 41154 41157 41160 41163 41166 41169 41172 +41175 41178 41181 41184 41187 41190 41193 41196 41199 +41202 41205 41208 41211 41214 41217 41220 41223 41226 +41229 41232 41235 41238 41241 41244 41247 41250 41253 +41256 41259 41262 41265 41268 41271 41274 41277 41280 +41283 41286 41289 41292 41295 41298 41301 41304 41307 +41310 41313 41316 41319 41322 41325 41328 41331 41334 +41337 41340 41343 41346 41349 41352 41355 41358 41361 +41364 41367 41370 41373 41376 41379 41382 41385 41388 +41391 41394 41397 41400 41403 41406 41409 41412 41415 +41418 41421 41424 41427 41430 41433 41436 41439 41442 +41445 41448 41451 41454 41457 41460 41463 41466 41469 +41472 41475 41478 41481 41484 41487 41490 41493 41496 +41499 41502 41505 41508 41511 41514 41517 41520 41523 +41526 41529 41532 41535 41538 41541 41544 41547 41550 +41553 41556 41559 41562 41565 41568 41571 41574 41577 +41580 41583 41586 41589 41592 41595 41598 41601 41604 +41607 41610 41613 41616 41619 41622 41625 41628 41631 +41634 41637 41640 41643 41646 41649 41652 41655 41658 +41661 41664 41667 41670 41673 41676 41679 41682 41685 +41688 41691 41694 41697 41700 41703 41706 41709 41712 +41715 41718 41721 41724 41727 41730 41733 41736 41739 +41742 41745 41748 41751 41754 41757 41760 41763 41766 +41769 41772 41775 41778 41781 41784 41787 41790 41793 +41796 41799 41802 41805 41808 41811 41814 41817 41820 +41823 41826 41829 41832 41835 41838 41841 41844 41847 +41850 41853 41856 41859 41862 41865 41868 41871 41874 +41877 41880 41883 41886 41889 41892 41895 41898 41901 +41904 41907 41910 41913 41916 41919 41922 41925 41928 +41931 41934 41937 41940 41943 41946 41949 41952 41955 +41958 41961 41964 41967 41970 41973 41976 41979 41982 +41985 41988 41991 41994 41997 42000 42003 42006 42009 +42012 42015 42018 42021 42024 42027 42030 42033 42036 +42039 42042 42045 42048 42051 42054 42057 42060 42063 +42066 42069 42072 42075 42078 42081 42084 42087 42090 +42093 42096 42099 42102 42105 42108 42111 42114 42117 +42120 42123 42126 42129 42132 42135 42138 42141 42144 +42147 42150 42153 42156 42159 42162 42165 42168 42171 +42174 42177 42180 42183 42186 42189 42192 42195 42198 +42201 42204 42207 42210 42213 42216 42219 42222 42225 +42228 42231 42234 42237 42240 42243 42246 42249 42252 +42255 42258 42261 42264 42267 42270 42273 42276 42279 +42282 42285 42288 42291 42294 42297 42300 42303 42306 +42309 42312 42315 42318 42321 42324 42327 42330 42333 +42336 42339 42342 42345 42348 42351 42354 42357 42360 +42363 42366 42369 42372 42375 42378 42381 42384 42387 +42390 42393 42396 42399 42402 42405 42408 42411 42414 +42417 42420 42423 42426 42429 42432 42435 42438 42441 +42444 42447 42450 42453 42456 42459 42462 42465 42468 +42471 42474 42477 42480 42483 42486 42489 42492 42495 +42498 42501 42504 42507 42510 42513 42516 42519 42522 +42525 42528 42531 42534 42537 42540 42543 42546 42549 +42552 42555 42558 42561 42564 42567 42570 42573 42576 +42579 42582 42585 42588 42591 42594 42597 42600 42603 +42606 42609 42612 42615 42618 42621 42624 42627 42630 +42633 42636 42639 42642 42645 42648 42651 42654 42657 +42660 42663 42666 42669 42672 42675 42678 42681 42684 +42687 42690 42693 42696 42699 42702 42705 42708 42711 +42714 42717 42720 42723 42726 42729 42732 42735 42738 +42741 42744 42747 42750 42753 42756 42759 42762 42765 +42768 42771 42774 42777 42780 42783 42786 42789 42792 +42795 42798 42801 42804 42807 42810 42813 42816 42819 +42822 42825 42828 42831 42834 42837 42840 42843 42846 +42849 42852 42855 42858 42861 42864 42867 42870 42873 +42876 42879 42882 42885 42888 42891 42894 42897 42900 +42903 42906 42909 42912 42915 42918 42921 42924 42927 +42930 42933 42936 42939 42942 42945 42948 42951 42954 +42957 42960 42963 42966 42969 42972 42975 42978 42981 +42984 42987 42990 42993 42996 42999 43002 43005 43008 +43011 43014 43017 43020 43023 43026 43029 43032 43035 +43038 43041 43044 43047 43050 43053 43056 43059 43062 +43065 43068 43071 43074 43077 43080 43083 43086 43089 +43092 43095 43098 43101 43104 43107 43110 43113 43116 +43119 43122 43125 43128 43131 43134 43137 43140 43143 +43146 43149 43152 43155 43158 43161 43164 43167 43170 +43173 43176 43179 43182 43185 43188 43191 43194 43197 +43200 43203 43206 43209 43212 43215 43218 43221 43224 +43227 43230 43233 43236 43239 43242 43245 43248 43251 +43254 43257 43260 43263 43266 43269 43272 43275 43278 +43281 43284 43287 43290 43293 43296 43299 43302 43305 +43308 43311 43314 43317 43320 43323 43326 43329 43332 +43335 43338 43341 43344 43347 43350 43353 43356 43359 +43362 43365 43368 43371 43374 43377 43380 43383 43386 +43389 43392 43395 43398 43401 43404 43407 43410 43413 +43416 43419 43422 43425 43428 43431 43434 43437 43440 +43443 43446 43449 43452 43455 43458 43461 43464 43467 +43470 43473 43476 43479 43482 43485 43488 43491 43494 +43497 43500 43503 43506 43509 43512 43515 43518 43521 +43524 43527 43530 43533 43536 43539 43542 43545 43548 +43551 43554 43557 43560 43563 43566 43569 43572 43575 +43578 43581 43584 43587 43590 43593 43596 43599 43602 +43605 43608 43611 43614 43617 43620 43623 43626 43629 +43632 43635 43638 43641 43644 43647 43650 43653 43656 +43659 43662 43665 43668 43671 43674 43677 43680 43683 +43686 43689 43692 43695 43698 43701 43704 43707 43710 +43713 43716 43719 43722 43725 43728 43731 43734 43737 +43740 43743 43746 43749 43752 43755 43758 43761 43764 +43767 43770 43773 43776 43779 43782 43785 43788 43791 +43794 43797 43800 43803 43806 43809 43812 43815 43818 +43821 43824 43827 43830 43833 43836 43839 43842 43845 +43848 43851 43854 43857 43860 43863 43866 43869 43872 +43875 43878 43881 43884 43887 43890 43893 43896 43899 +43902 43905 43908 43911 43914 43917 43920 43923 43926 +43929 43932 43935 43938 43941 43944 43947 43950 43953 +43956 43959 43962 43965 43968 43971 43974 43977 43980 +43983 43986 43989 43992 43995 43998 44001 44004 44007 +44010 44013 44016 44019 44022 44025 44028 44031 44034 +44037 44040 44043 44046 44049 44052 44055 44058 44061 +44064 44067 44070 44073 44076 44079 44082 44085 44088 +44091 44094 44097 44100 44103 44106 44109 44112 44115 +44118 44121 44124 44127 44130 44133 44136 44139 44142 +44145 44148 44151 44154 44157 44160 44163 44166 44169 +44172 44175 44178 44181 44184 44187 44190 44193 44196 +44199 44202 44205 44208 44211 44214 44217 44220 44223 +44226 44229 44232 44235 44238 44241 44244 44247 44250 +44253 44256 44259 44262 44265 44268 44271 44274 44277 +44280 44283 44286 44289 44292 44295 44298 44301 44304 +44307 44310 44313 44316 44319 44322 44325 44328 44331 +44334 44337 44340 44343 44346 44349 44352 44355 44358 +44361 44364 44367 44370 44373 44376 44379 44382 44385 +44388 44391 44394 44397 44400 44403 44406 44409 44412 +44415 44418 44421 44424 44427 44430 44433 44436 44439 +44442 44445 44448 44451 44454 44457 44460 44463 44466 +44469 44472 44475 44478 44481 44484 44487 44490 44493 +44496 44499 44502 44505 44508 44511 44514 44517 44520 +44523 44526 44529 44532 44535 44538 44541 44544 44547 +44550 44553 44556 44559 44562 44565 44568 44571 44574 +44577 44580 44583 44586 44589 44592 44595 44598 44601 +44604 44607 44610 44613 44616 44619 44622 44625 44628 +44631 44634 44637 44640 44643 44646 44649 44652 44655 +44658 44661 44664 44667 44670 44673 44676 44679 44682 +44685 44688 44691 44694 44697 44700 44703 44706 44709 +44712 44715 44718 44721 44724 44727 44730 44733 44736 +44739 44742 44745 44748 44751 44754 44757 44760 44763 +44766 44769 44772 44775 44778 44781 44784 44787 44790 +44793 44796 44799 44802 44805 44808 44811 44814 44817 +44820 44823 44826 44829 44832 44835 44838 44841 44844 +44847 44850 44853 44856 44859 44862 44865 44868 44871 +44874 44877 44880 44883 44886 44889 44892 44895 44898 +44901 44904 44907 44910 44913 44916 44919 44922 44925 +44928 44931 44934 44937 44940 44943 44946 44949 44952 +44955 44958 44961 44964 44967 44970 44973 44976 44979 +44982 44985 44988 44991 44994 44997 45000 45003 45006 +45009 45012 45015 45018 45021 45024 45027 45030 45033 +45036 45039 45042 45045 45048 45051 45054 45057 45060 +45063 45066 45069 45072 45075 45078 45081 45084 45087 +45090 45093 45096 45099 45102 45105 45108 45111 45114 +45117 45120 45123 45126 45129 45132 45135 45138 45141 +45144 45147 45150 45153 45156 45159 45162 45165 45168 +45171 45174 45177 45180 45183 45186 45189 45192 45195 +45198 45201 45204 45207 45210 45213 45216 45219 45222 +45225 45228 45231 45234 45237 45240 45243 45246 45249 +45252 45255 45258 45261 45264 45267 45270 45273 45276 +45279 45282 45285 45288 45291 45294 45297 45300 45303 +45306 45309 45312 45315 45318 45321 45324 45327 45330 +45333 45336 45339 45342 45345 45348 45351 45354 45357 +45360 45363 45366 45369 45372 45375 45378 45381 45384 +45387 45390 45393 45396 45399 45402 45405 45408 45411 +45414 45417 45420 45423 45426 45429 45432 45435 45438 +45441 45444 45447 45450 45453 45456 45459 45462 45465 +45468 45471 45474 45477 45480 45483 45486 45489 45492 +45495 45498 45501 45504 45507 45510 45513 45516 45519 +45522 45525 45528 45531 45534 45537 45540 45543 45546 +45549 45552 45555 45558 45561 45564 45567 45570 45573 +45576 45579 45582 45585 45588 45591 45594 45597 45600 +45603 45606 45609 45612 45615 45618 45621 45624 45627 +45630 45633 45636 45639 45642 45645 45648 45651 45654 +45657 45660 45663 45666 45669 45672 45675 45678 45681 +45684 45687 45690 45693 45696 45699 45702 45705 45708 +45711 45714 45717 45720 45723 45726 45729 45732 45735 +45738 45741 45744 45747 45750 45753 45756 45759 45762 +45765 45768 45771 45774 45777 45780 45783 45786 45789 +45792 45795 45798 45801 45804 45807 45810 45813 45816 +45819 45822 45825 45828 45831 45834 45837 45840 45843 +45846 45849 45852 45855 45858 45861 45864 45867 45870 +45873 45876 45879 45882 45885 45888 45891 45894 45897 +45900 45903 45906 45909 45912 45915 45918 45921 45924 +45927 45930 45933 45936 45939 45942 45945 45948 45951 +45954 45957 45960 45963 45966 45969 45972 45975 45978 +45981 45984 45987 45990 45993 45996 45999 46002 46005 +46008 46011 46014 46017 46020 46023 46026 46029 46032 +46035 46038 46041 46044 46047 46050 46053 46056 46059 +46062 46065 46068 46071 46074 46077 46080 46083 46086 +46089 46092 46095 46098 46101 46104 46107 46110 46113 +46116 46119 46122 46125 46128 46131 46134 46137 46140 +46143 46146 46149 46152 46155 46158 46161 46164 46167 +46170 46173 46176 46179 46182 46185 46188 46191 46194 +46197 46200 46203 46206 46209 46212 46215 46218 46221 +46224 46227 46230 46233 46236 46239 46242 46245 46248 +46251 46254 46257 46260 46263 46266 46269 46272 46275 +46278 46281 46284 46287 46290 46293 46296 46299 46302 +46305 46308 46311 46314 46317 46320 46323 46326 46329 +46332 46335 46338 46341 46344 46347 46350 46353 46356 +46359 46362 46365 46368 46371 46374 46377 46380 46383 +46386 46389 46392 46395 46398 46401 46404 46407 46410 +46413 46416 46419 46422 46425 46428 46431 46434 46437 +46440 46443 46446 46449 46452 46455 46458 46461 46464 +46467 46470 46473 46476 46479 46482 46485 46488 46491 +46494 46497 46500 46503 46506 46509 46512 46515 46518 +46521 46524 46527 46530 46533 46536 46539 46542 46545 +46548 46551 46554 46557 46560 46563 46566 46569 46572 +46575 46578 46581 46584 46587 46590 46593 46596 46599 +46602 46605 46608 46611 46614 46617 46620 46623 46626 +46629 46632 46635 46638 46641 46644 46647 46650 46653 +46656 46659 46662 46665 46668 46671 46674 46677 46680 +46683 46686 46689 46692 46695 46698 46701 46704 46707 +46710 46713 46716 46719 46722 46725 46728 46731 46734 +46737 46740 46743 46746 46749 46752 46755 46758 46761 +46764 46767 46770 46773 46776 46779 46782 46785 46788 +46791 46794 46797 46800 46803 46806 46809 46812 46815 +46818 46821 46824 46827 46830 46833 46836 46839 46842 +46845 46848 46851 46854 46857 46860 46863 46866 46869 +46872 46875 46878 46881 46884 46887 46890 46893 46896 +46899 46902 46905 46908 46911 46914 46917 46920 46923 +46926 46929 46932 46935 46938 46941 46944 46947 46950 +46953 46956 46959 46962 46965 46968 46971 46974 46977 +46980 46983 46986 46989 46992 46995 46998 47001 47004 +47007 47010 47013 47016 47019 47022 47025 47028 47031 +47034 47037 47040 47043 47046 47049 47052 47055 47058 +47061 47064 47067 47070 47073 47076 47079 47082 47085 +47088 47091 47094 47097 47100 47103 47106 47109 47112 +47115 47118 47121 47124 47127 47130 47133 47136 47139 +47142 47145 47148 47151 47154 47157 47160 47163 47166 +47169 47172 47175 47178 47181 47184 47187 47190 47193 +47196 47199 47202 47205 47208 47211 47214 47217 47220 +47223 47226 47229 47232 47235 47238 47241 47244 47247 +47250 47253 47256 47259 47262 47265 47268 47271 47274 +47277 47280 47283 47286 47289 47292 47295 47298 47301 +47304 47307 47310 47313 47316 47319 47322 47325 47328 +47331 47334 47337 47340 47343 47346 47349 47352 47355 +47358 47361 47364 47367 47370 47373 47376 47379 47382 +47385 47388 47391 47394 47397 47400 47403 47406 47409 +47412 47415 47418 47421 47424 47427 47430 47433 47436 +47439 47442 47445 47448 47451 47454 47457 47460 47463 +47466 47469 47472 47475 47478 47481 47484 47487 47490 +47493 47496 47499 47502 47505 47508 47511 47514 47517 +47520 47523 47526 47529 47532 47535 47538 47541 47544 +47547 47550 47553 47556 47559 47562 47565 47568 47571 +47574 47577 47580 47583 47586 47589 47592 47595 47598 +47601 47604 47607 47610 47613 47616 47619 47622 47625 +47628 47631 47634 47637 47640 47643 47646 47649 47652 +47655 47658 47661 47664 47667 47670 47673 47676 47679 +47682 47685 47688 47691 47694 47697 47700 47703 47706 +47709 47712 47715 47718 47721 47724 47727 47730 47733 +47736 47739 47742 47745 47748 47751 47754 47757 47760 +47763 47766 47769 47772 47775 47778 47781 47784 47787 +47790 47793 47796 47799 47802 47805 47808 47811 47814 +47817 47820 47823 47826 47829 47832 47835 47838 47841 +47844 47847 47850 47853 47856 47859 47862 47865 47868 +47871 47874 47877 47880 47883 47886 47889 47892 47895 +47898 47901 47904 47907 47910 47913 47916 47919 47922 +47925 47928 47931 47934 47937 47940 47943 47946 47949 +47952 47955 47958 47961 47964 47967 47970 47973 47976 +47979 47982 47985 47988 47991 47994 47997 48000 48003 +48006 48009 48012 48015 48018 48021 48024 48027 48030 +48033 48036 48039 48042 48045 48048 48051 48054 48057 +48060 48063 48066 48069 48072 48075 48078 48081 48084 +48087 48090 48093 48096 48099 48102 48105 48108 48111 +48114 48117 48120 48123 48126 48129 48132 48135 48138 +48141 48144 48147 48150 48153 48156 48159 48162 48165 +48168 48171 48174 48177 48180 48183 48186 48189 48192 +48195 48198 48201 48204 48207 48210 48213 48216 48219 +48222 48225 48228 48231 48234 48237 48240 48243 48246 +48249 48252 48255 48258 48261 48264 48267 48270 48273 +48276 48279 48282 48285 48288 48291 48294 48297 48300 +48303 48306 48309 48312 48315 48318 48321 48324 48327 +48330 48333 48336 48339 48342 48345 48348 48351 48354 +48357 48360 48363 48366 48369 48372 48375 48378 48381 +48384 48387 48390 48393 48396 48399 48402 48405 48408 +48411 48414 48417 48420 48423 48426 48429 48432 48435 +48438 48441 48444 48447 48450 48453 48456 48459 48462 +48465 48468 48471 48474 48477 48480 48483 48486 48489 +48492 48495 48498 48501 48504 48507 48510 48513 48516 +48519 48522 48525 48528 48531 48534 48537 48540 48543 +48546 48549 48552 48555 48558 48561 48564 48567 48570 +48573 48576 48579 48582 48585 48588 48591 48594 48597 +48600 48603 48606 48609 48612 48615 48618 48621 48624 +48627 48630 48633 48636 48639 48642 48645 48648 48651 +48654 48657 48660 48663 48666 48669 48672 48675 48678 +48681 48684 48687 48690 48693 48696 48699 48702 48705 +48708 48711 48714 48717 48720 48723 48726 48729 48732 +48735 48738 48741 48744 48747 48750 48753 48756 48759 +48762 48765 48768 48771 48774 48777 48780 48783 48786 +48789 48792 48795 48798 48801 48804 48807 48810 48813 +48816 48819 48822 48825 48828 48831 48834 48837 48840 +48843 48846 48849 48852 48855 48858 48861 48864 48867 +48870 48873 48876 48879 48882 48885 48888 48891 48894 +48897 48900 48903 48906 48909 48912 48915 48918 48921 +48924 48927 48930 48933 48936 48939 48942 48945 48948 +48951 48954 48957 48960 48963 48966 48969 48972 48975 +48978 48981 48984 48987 48990 48993 48996 48999 49002 +49005 49008 49011 49014 49017 49020 49023 49026 49029 +49032 49035 49038 49041 49044 49047 49050 49053 49056 +49059 49062 49065 49068 49071 49074 49077 49080 49083 +49086 49089 49092 49095 49098 49101 49104 49107 49110 +49113 49116 49119 49122 49125 49128 49131 49134 49137 +49140 49143 49146 49149 49152 49155 49158 49161 49164 +49167 49170 49173 49176 49179 49182 49185 49188 49191 +49194 49197 49200 49203 49206 49209 49212 49215 49218 +49221 49224 49227 49230 49233 49236 49239 49242 49245 +49248 49251 49254 49257 49260 49263 49266 49269 49272 +49275 49278 49281 49284 49287 49290 49293 49296 49299 +49302 49305 49308 49311 49314 49317 49320 49323 49326 +49329 49332 49335 49338 49341 49344 49347 49350 49353 +49356 49359 49362 49365 49368 49371 49374 49377 49380 +49383 49386 49389 49392 49395 49398 49401 49404 49407 +49410 49413 49416 49419 49422 49425 49428 49431 49434 +49437 49440 49443 49446 49449 49452 49455 49458 49461 +49464 49467 49470 49473 49476 49479 49482 49485 49488 +49491 49494 49497 49500 49503 49506 49509 49512 49515 +49518 49521 49524 49527 49530 49533 49536 49539 49542 +49545 49548 49551 49554 49557 49560 49563 49566 49569 +49572 49575 49578 49581 49584 49587 49590 49593 49596 +49599 49602 49605 49608 49611 49614 49617 49620 49623 +49626 49629 49632 49635 49638 49641 49644 49647 49650 +49653 49656 49659 49662 49665 49668 49671 49674 49677 +49680 49683 49686 49689 49692 49695 49698 49701 49704 +49707 49710 49713 49716 49719 49722 49725 49728 49731 +49734 49737 49740 49743 49746 49749 49752 49755 49758 +49761 49764 49767 49770 49773 49776 49779 49782 49785 +49788 49791 49794 49797 49800 49803 49806 49809 49812 +49815 49818 49821 49824 49827 49830 49833 49836 49839 +49842 49845 49848 49851 49854 49857 49860 49863 49866 +49869 49872 49875 49878 49881 49884 49887 49890 49893 +49896 49899 49902 49905 49908 49911 49914 49917 49920 +49923 49926 49929 49932 49935 49938 49941 49944 49947 +49950 49953 49956 49959 49962 49965 49968 49971 49974 +49977 49980 49983 49986 49989 49992 49995 49998 50001 +50004 50007 50010 50013 50016 50019 50022 50025 50028 +50031 50034 50037 50040 50043 50046 50049 50052 50055 +50058 50061 50064 50067 50070 50073 50076 50079 50082 +50085 50088 50091 50094 50097 50100 50103 50106 50109 +50112 50115 50118 50121 50124 50127 50130 50133 50136 +50139 50142 50145 50148 50151 50154 50157 50160 50163 +50166 50169 50172 50175 50178 50181 50184 50187 50190 +50193 50196 50199 50202 50205 50208 50211 50214 50217 +50220 50223 50226 50229 50232 50235 50238 50241 50244 +50247 50250 50253 50256 50259 50262 50265 50268 50271 +50274 50277 50280 50283 50286 50289 50292 50295 50298 +50301 50304 50307 50310 50313 50316 50319 50322 50325 +50328 50331 50334 50337 50340 50343 50346 50349 50352 +50355 50358 50361 50364 50367 50370 50373 50376 50379 +50382 50385 50388 50391 50394 50397 50400 50403 50406 +50409 50412 50415 50418 50421 50424 50427 50430 50433 +50436 50439 50442 50445 50448 50451 50454 50457 50460 +50463 50466 50469 50472 50475 50478 50481 50484 50487 +50490 50493 50496 50499 50502 50505 50508 50511 50514 +50517 50520 50523 50526 50529 50532 50535 50538 50541 +50544 50547 50550 50553 50556 50559 50562 50565 50568 +50571 50574 50577 50580 50583 50586 50589 50592 50595 +50598 50601 50604 50607 50610 50613 50616 50619 50622 +50625 50628 50631 50634 50637 50640 50643 50646 50649 +50652 50655 50658 50661 50664 50667 50670 50673 50676 +50679 50682 50685 50688 50691 50694 50697 50700 50703 +50706 50709 50712 50715 50718 50721 50724 50727 50730 +50733 50736 50739 50742 50745 50748 50751 50754 50757 +50760 50763 50766 50769 50772 50775 50778 50781 50784 +50787 50790 50793 50796 50799 50802 50805 50808 50811 +50814 50817 50820 50823 50826 50829 50832 50835 50838 +50841 50844 50847 50850 50853 50856 50859 50862 50865 +50868 50871 50874 50877 50880 50883 50886 50889 50892 +50895 50898 50901 50904 50907 50910 50913 50916 50919 +50922 50925 50928 50931 50934 50937 50940 50943 50946 +50949 50952 50955 50958 50961 50964 50967 50970 50973 +50976 50979 50982 50985 50988 50991 50994 50997 51000 +51003 51006 51009 51012 51015 51018 51021 51024 51027 +51030 51033 51036 51039 51042 51045 51048 51051 51054 +51057 51060 51063 51066 51069 51072 51075 51078 51081 +51084 51087 51090 51093 51096 51099 51102 51105 51108 +51111 51114 51117 51120 51123 51126 51129 51132 51135 +51138 51141 51144 51147 51150 51153 51156 51159 51162 +51165 51168 51171 51174 51177 51180 51183 51186 51189 +51192 51195 51198 51201 51204 51207 51210 51213 51216 +51219 51222 51225 51228 51231 51234 51237 51240 51243 +51246 51249 51252 51255 51258 51261 51264 51267 51270 +51273 51276 51279 51282 51285 51288 51291 51294 51297 +51300 51303 51306 51309 51312 51315 51318 51321 51324 +51327 51330 51333 51336 51339 51342 51345 51348 51351 +51354 51357 51360 51363 51366 51369 51372 51375 51378 +51381 51384 51387 51390 51393 51396 51399 51402 51405 +51408 51411 51414 51417 51420 51423 51426 51429 51432 +51435 51438 51441 51444 51447 51450 51453 51456 51459 +51462 51465 51468 51471 51474 51477 51480 51483 51486 +51489 51492 51495 51498 51501 51504 51507 51510 51513 +51516 51519 51522 51525 51528 51531 51534 51537 51540 +51543 51546 51549 51552 51555 51558 51561 51564 51567 +51570 51573 51576 51579 51582 51585 51588 51591 51594 +51597 51600 51603 51606 51609 51612 51615 51618 51621 +51624 51627 51630 51633 51636 51639 51642 51645 51648 +51651 51654 51657 51660 51663 51666 51669 51672 51675 +51678 51681 51684 51687 51690 51693 51696 51699 51702 +51705 51708 51711 51714 51717 51720 51723 51726 51729 +51732 51735 51738 51741 51744 51747 51750 51753 51756 +51759 51762 51765 51768 51771 51774 51777 51780 51783 +51786 51789 51792 51795 51798 51801 51804 51807 51810 +51813 51816 51819 51822 51825 51828 51831 51834 51837 +51840 51843 51846 51849 51852 51855 51858 51861 51864 +51867 51870 51873 51876 51879 51882 51885 51888 51891 +51894 51897 51900 51903 51906 51909 51912 51915 51918 +51921 51924 51927 51930 51933 51936 51939 51942 51945 +51948 51951 51954 51957 51960 51963 51966 51969 51972 +51975 51978 51981 51984 51987 51990 51993 51996 51999 +52002 52005 52008 52011 52014 52017 52020 52023 52026 +52029 52032 52035 52038 52041 52044 52047 52050 52053 +52056 52059 52062 52065 52068 52071 52074 52077 52080 +52083 52086 52089 52092 52095 52098 52101 52104 52107 +52110 52113 52116 52119 52122 52125 52128 52131 52134 +52137 52140 52143 52146 52149 52152 52155 52158 52161 +52164 52167 52170 52173 52176 52179 52182 52185 52188 +52191 52194 52197 52200 52203 52206 52209 52212 52215 +52218 52221 52224 52227 52230 52233 52236 52239 52242 +52245 52248 52251 52254 52257 52260 52263 52266 52269 +52272 52275 52278 52281 52284 52287 52290 52293 52296 +52299 52302 52305 52308 52311 52314 52317 52320 52323 +52326 52329 52332 52335 52338 52341 52344 52347 52350 +52353 52356 52359 52362 52365 52368 52371 52374 52377 +52380 52383 52386 52389 52392 52395 52398 52401 52404 +52407 52410 52413 52416 52419 52422 52425 52428 52431 +52434 52437 52440 52443 52446 52449 52452 52455 52458 +52461 52464 52467 52470 52473 52476 52479 52482 52485 +52488 52491 52494 52497 52500 52503 52506 52509 52512 +52515 52518 52521 52524 52527 52530 52533 52536 52539 +52542 52545 52548 52551 52554 52557 52560 52563 52566 +52569 52572 52575 52578 52581 52584 52587 52590 52593 +52596 52599 52602 52605 52608 52611 52614 52617 52620 +52623 52626 52629 52632 52635 52638 52641 52644 52647 +52650 52653 52656 52659 52662 52665 52668 52671 52674 +52677 52680 52683 52686 52689 52692 52695 52698 52701 +52704 52707 52710 52713 52716 52719 52722 52725 52728 +52731 52734 52737 52740 52743 52746 52749 52752 52755 +52758 52761 52764 52767 52770 52773 52776 52779 52782 +52785 52788 52791 52794 52797 52800 52803 52806 52809 +52812 52815 52818 52821 52824 52827 52830 52833 52836 +52839 52842 52845 52848 52851 52854 52857 52860 52863 +52866 52869 52872 52875 52878 52881 52884 52887 52890 +52893 52896 52899 52902 52905 52908 52911 52914 52917 +52920 52923 52926 52929 52932 52935 52938 52941 52944 +52947 52950 52953 52956 52959 52962 52965 52968 52971 +52974 52977 52980 52983 52986 52989 52992 52995 52998 +53001 53004 53007 53010 53013 53016 53019 53022 53025 +53028 53031 53034 53037 53040 53043 53046 53049 53052 +53055 53058 53061 53064 53067 53070 53073 53076 53079 +53082 53085 53088 53091 53094 53097 53100 53103 53106 +53109 53112 53115 53118 53121 53124 53127 53130 53133 +53136 53139 53142 53145 53148 53151 53154 53157 53160 +53163 53166 53169 53172 53175 53178 53181 53184 53187 +53190 53193 53196 53199 53202 53205 53208 53211 53214 +53217 53220 53223 53226 53229 53232 53235 53238 53241 +53244 53247 53250 53253 53256 53259 53262 53265 53268 +53271 53274 53277 53280 53283 53286 53289 53292 53295 +53298 53301 53304 53307 53310 53313 53316 53319 53322 +53325 53328 53331 53334 53337 53340 53343 53346 53349 +53352 53355 53358 53361 53364 53367 53370 53373 53376 +53379 53382 53385 53388 53391 53394 53397 53400 53403 +53406 53409 53412 53415 53418 53421 53424 53427 53430 +53433 53436 53439 53442 53445 53448 53451 53454 53457 +53460 53463 53466 53469 53472 53475 53478 53481 53484 +53487 53490 53493 53496 53499 53502 53505 53508 53511 +53514 53517 53520 53523 53526 53529 53532 53535 53538 +53541 53544 53547 53550 53553 53556 53559 53562 53565 +53568 53571 53574 53577 53580 53583 53586 53589 53592 +53595 53598 53601 53604 53607 53610 53613 53616 53619 +53622 53625 53628 53631 53634 53637 53640 53643 53646 +53649 53652 53655 53658 53661 53664 53667 53670 53673 +53676 53679 53682 53685 53688 53691 53694 53697 53700 +53703 53706 53709 53712 53715 53718 53721 53724 53727 +53730 53733 53736 53739 53742 53745 53748 53751 53754 +53757 53760 53763 53766 53769 53772 53775 53778 53781 +53784 53787 53790 53793 53796 53799 53802 53805 53808 +53811 53814 53817 53820 53823 53826 53829 53832 53835 +53838 53841 53844 53847 53850 53853 53856 53859 53862 +53865 53868 53871 53874 53877 53880 53883 53886 53889 +53892 53895 53898 53901 53904 53907 53910 53913 53916 +53919 53922 53925 53928 53931 53934 53937 53940 53943 +53946 53949 53952 53955 53958 53961 53964 53967 53970 +53973 53976 53979 53982 53985 53988 53991 53994 53997 +54000 54003 54006 54009 54012 54015 54018 54021 54024 +54027 54030 54033 54036 54039 54042 54045 54048 54051 +54054 54057 54060 54063 54066 54069 54072 54075 54078 +54081 54084 54087 54090 54093 54096 54099 54102 54105 +54108 54111 54114 54117 54120 54123 54126 54129 54132 +54135 54138 54141 54144 54147 54150 54153 54156 54159 +54162 54165 54168 54171 54174 54177 54180 54183 54186 +54189 54192 54195 54198 54201 54204 54207 54210 54213 +54216 54219 54222 54225 54228 54231 54234 54237 54240 +54243 54246 54249 54252 54255 54258 54261 54264 54267 +54270 54273 54276 54279 54282 54285 54288 54291 54294 +54297 54300 54303 54306 54309 54312 54315 54318 54321 +54324 54327 54330 54333 54336 54339 54342 54345 54348 +54351 54354 54357 54360 54363 54366 54369 54372 54375 +54378 54381 54384 54387 54390 54393 54396 54399 54402 +54405 54408 54411 54414 54417 54420 54423 54426 54429 +54432 54435 54438 54441 54444 54447 54450 54453 54456 +54459 54462 54465 54468 54471 54474 54477 54480 54483 +54486 54489 54492 54495 54498 54501 54504 54507 54510 +54513 54516 54519 54522 54525 54528 54531 54534 54537 +54540 54543 54546 54549 54552 54555 54558 54561 54564 +54567 54570 54573 54576 54579 54582 54585 54588 54591 +54594 54597 54600 54603 54606 54609 54612 54615 54618 +54621 54624 54627 54630 54633 54636 54639 54642 54645 +54648 54651 54654 54657 54660 54663 54666 54669 54672 +54675 54678 54681 54684 54687 54690 54693 54696 54699 +54702 54705 54708 54711 54714 54717 54720 54723 54726 +54729 54732 54735 54738 54741 54744 54747 54750 54753 +54756 54759 54762 54765 54768 54771 54774 54777 54780 +54783 54786 54789 54792 54795 54798 54801 54804 54807 +54810 54813 54816 54819 54822 54825 54828 54831 54834 +54837 54840 54843 54846 54849 54852 54855 54858 54861 +54864 54867 54870 54873 54876 54879 54882 54885 54888 +54891 54894 54897 54900 54903 54906 54909 54912 54915 +54918 54921 54924 54927 54930 54933 54936 54939 54942 +54945 54948 54951 54954 54957 54960 54963 54966 54969 +54972 54975 54978 54981 54984 54987 54990 54993 54996 +54999 55002 55005 55008 55011 55014 55017 55020 55023 +55026 55029 55032 55035 55038 55041 55044 55047 55050 +55053 55056 55059 55062 55065 55068 55071 55074 55077 +55080 55083 55086 55089 55092 55095 55098 55101 55104 +55107 55110 55113 55116 55119 55122 55125 55128 55131 +55134 55137 55140 55143 55146 55149 55152 55155 55158 +55161 55164 55167 55170 55173 55176 55179 55182 55185 +55188 55191 55194 55197 55200 55203 55206 55209 55212 +55215 55218 55221 55224 55227 55230 55233 55236 55239 +55242 55245 55248 55251 55254 55257 55260 55263 55266 +55269 55272 55275 55278 55281 55284 55287 55290 55293 +55296 55299 55302 55305 55308 55311 55314 55317 55320 +55323 55326 55329 55332 55335 55338 55341 55344 55347 +55350 55353 55356 55359 55362 55365 55368 55371 55374 +55377 55380 55383 55386 55389 55392 55395 55398 55401 +55404 55407 55410 55413 55416 55419 55422 55425 55428 +55431 55434 55437 55440 55443 55446 55449 55452 55455 +55458 55461 55464 55467 55470 55473 55476 55479 55482 +55485 55488 55491 55494 55497 55500 55503 55506 55509 +55512 55515 55518 55521 55524 55527 55530 55533 55536 +55539 55542 55545 55548 55551 55554 55557 55560 55563 +55566 55569 55572 55575 55578 55581 55584 55587 55590 +55593 55596 55599 55602 55605 55608 55611 55614 55617 +55620 55623 55626 55629 55632 55635 55638 55641 55644 +55647 55650 55653 55656 55659 55662 55665 55668 55671 +55674 55677 55680 55683 55686 55689 55692 55695 55698 +55701 55704 55707 55710 55713 55716 55719 55722 55725 +55728 55731 55734 55737 55740 55743 55746 55749 55752 +55755 55758 55761 55764 55767 55770 55773 55776 55779 +55782 55785 55788 55791 55794 55797 55800 55803 55806 +55809 55812 55815 55818 55821 55824 55827 55830 55833 +55836 55839 55842 55845 55848 55851 55854 55857 55860 +55863 55866 55869 55872 55875 55878 55881 55884 55887 +55890 55893 55896 55899 55902 55905 55908 55911 55914 +55917 55920 55923 55926 55929 55932 55935 55938 55941 +55944 55947 55950 55953 55956 55959 55962 55965 55968 +55971 55974 55977 55980 55983 55986 55989 55992 55995 +55998 56001 56004 56007 56010 56013 56016 56019 56022 +56025 56028 56031 56034 56037 56040 56043 56046 56049 +56052 56055 56058 56061 56064 56067 56070 56073 56076 +56079 56082 56085 56088 56091 56094 56097 56100 56103 +56106 56109 56112 56115 56118 56121 56124 56127 56130 +56133 56136 56139 56142 56145 56148 56151 56154 56157 +56160 56163 56166 56169 56172 56175 56178 56181 56184 +56187 56190 56193 56196 56199 56202 56205 56208 56211 +56214 56217 56220 56223 56226 56229 56232 56235 56238 +56241 56244 56247 56250 56253 56256 56259 56262 56265 +56268 56271 56274 56277 56280 56283 56286 56289 56292 +56295 56298 56301 56304 56307 56310 56313 56316 56319 +56322 56325 56328 56331 56334 56337 56340 56343 56346 +56349 56352 56355 56358 56361 56364 56367 56370 56373 +56376 56379 56382 56385 56388 56391 56394 56397 56400 +56403 56406 56409 56412 56415 56418 56421 56424 56427 +56430 56433 56436 56439 56442 56445 56448 56451 56454 +56457 56460 56463 56466 56469 56472 56475 56478 56481 +56484 56487 56490 56493 56496 56499 56502 56505 56508 +56511 56514 56517 56520 56523 56526 56529 56532 56535 +56538 56541 56544 56547 56550 56553 56556 56559 56562 +56565 56568 56571 56574 56577 56580 56583 56586 56589 +56592 56595 56598 56601 56604 56607 56610 56613 56616 +56619 56622 56625 56628 56631 56634 56637 56640 56643 +56646 56649 56652 56655 56658 56661 56664 56667 56670 +56673 56676 56679 56682 56685 56688 56691 56694 56697 +56700 56703 56706 56709 56712 56715 56718 56721 56724 +56727 56730 56733 56736 56739 56742 56745 56748 56751 +56754 56757 56760 56763 56766 56769 56772 56775 56778 +56781 56784 56787 56790 56793 56796 56799 56802 56805 +56808 56811 56814 56817 56820 56823 56826 56829 56832 +56835 56838 56841 56844 56847 56850 56853 56856 56859 +56862 56865 56868 56871 56874 56877 56880 56883 56886 +56889 56892 56895 56898 56901 56904 56907 56910 56913 +56916 56919 56922 56925 56928 56931 56934 56937 56940 +56943 56946 56949 56952 56955 56958 56961 56964 56967 +56970 56973 56976 56979 56982 56985 56988 56991 56994 +56997 57000 57003 57006 57009 57012 57015 57018 57021 +57024 57027 57030 57033 57036 57039 57042 57045 57048 +57051 57054 57057 57060 57063 57066 57069 57072 57075 +57078 57081 57084 57087 57090 57093 57096 57099 57102 +57105 57108 57111 57114 57117 57120 57123 57126 57129 +57132 57135 57138 57141 57144 57147 57150 57153 57156 +57159 57162 57165 57168 57171 57174 57177 57180 57183 +57186 57189 57192 57195 57198 57201 57204 57207 57210 +57213 57216 57219 57222 57225 57228 57231 57234 57237 +57240 57243 57246 57249 57252 57255 57258 57261 57264 +57267 57270 57273 57276 57279 57282 57285 57288 57291 +57294 57297 57300 57303 57306 57309 57312 57315 57318 +57321 57324 57327 57330 57333 57336 57339 57342 57345 +57348 57351 57354 57357 57360 57363 57366 57369 57372 +57375 57378 57381 57384 57387 57390 57393 57396 57399 +57402 57405 57408 57411 57414 57417 57420 57423 57426 +57429 57432 57435 57438 57441 57444 57447 57450 57453 +57456 57459 57462 57465 57468 57471 57474 57477 57480 +57483 57486 57489 57492 57495 57498 57501 57504 57507 +57510 57513 57516 57519 57522 57525 57528 57531 57534 +57537 57540 57543 57546 57549 57552 57555 57558 57561 +57564 57567 57570 57573 57576 57579 57582 57585 57588 +57591 57594 57597 57600 57603 57606 57609 57612 57615 +57618 57621 57624 57627 57630 57633 57636 57639 57642 +57645 57648 57651 57654 57657 57660 57663 57666 57669 +57672 57675 57678 57681 57684 57687 57690 57693 57696 +57699 57702 57705 57708 57711 57714 57717 57720 57723 +57726 57729 57732 57735 57738 57741 57744 57747 57750 +57753 57756 57759 57762 57765 57768 57771 57774 57777 +57780 57783 57786 57789 57792 57795 57798 57801 57804 +57807 57810 57813 57816 57819 57822 57825 57828 57831 +57834 57837 57840 57843 57846 57849 57852 57855 57858 +57861 57864 57867 57870 57873 57876 57879 57882 57885 +57888 57891 57894 57897 57900 57903 57906 57909 57912 +57915 57918 57921 57924 57927 57930 57933 57936 57939 +57942 57945 57948 57951 57954 57957 57960 57963 57966 +57969 57972 57975 57978 57981 57984 57987 57990 57993 +57996 57999 58002 58005 58008 58011 58014 58017 58020 +58023 58026 58029 58032 58035 58038 58041 58044 58047 +58050 58053 58056 58059 58062 58065 58068 58071 58074 +58077 58080 58083 58086 58089 58092 58095 58098 58101 +58104 58107 58110 58113 58116 58119 58122 58125 58128 +58131 58134 58137 58140 58143 58146 58149 58152 58155 +58158 58161 58164 58167 58170 58173 58176 58179 58182 +58185 58188 58191 58194 58197 58200 58203 58206 58209 +58212 58215 58218 58221 58224 58227 58230 58233 58236 +58239 58242 58245 58248 58251 58254 58257 58260 58263 +58266 58269 58272 58275 58278 58281 58284 58287 58290 +58293 58296 58299 58302 58305 58308 58311 58314 58317 +58320 58323 58326 58329 58332 58335 58338 58341 58344 +58347 58350 58353 58356 58359 58362 58365 58368 58371 +58374 58377 58380 58383 58386 58389 58392 58395 58398 +58401 58404 58407 58410 58413 58416 58419 58422 58425 +58428 58431 58434 58437 58440 58443 58446 58449 58452 +58455 58458 58461 58464 58467 58470 58473 58476 58479 +58482 58485 58488 58491 58494 58497 58500 58503 58506 +58509 58512 58515 58518 58521 58524 58527 58530 58533 +58536 58539 58542 58545 58548 58551 58554 58557 58560 +58563 58566 58569 58572 58575 58578 58581 58584 58587 +58590 58593 58596 58599 58602 58605 58608 58611 58614 +58617 58620 58623 58626 58629 58632 58635 58638 58641 +58644 58647 58650 58653 58656 58659 58662 58665 58668 +58671 58674 58677 58680 58683 58686 58689 58692 58695 +58698 58701 58704 58707 58710 58713 58716 58719 58722 +58725 58728 58731 58734 58737 58740 58743 58746 58749 +58752 58755 58758 58761 58764 58767 58770 58773 58776 +58779 58782 58785 58788 58791 58794 58797 58800 58803 +58806 58809 58812 58815 58818 58821 58824 58827 58830 +58833 58836 58839 58842 58845 58848 58851 58854 58857 +58860 58863 58866 58869 58872 58875 58878 58881 58884 +58887 58890 58893 58896 58899 58902 58905 58908 58911 +58914 58917 58920 58923 58926 58929 58932 58935 58938 +58941 58944 58947 58950 58953 58956 58959 58962 58965 +58968 58971 58974 58977 58980 58983 58986 58989 58992 +58995 58998 59001 59004 59007 59010 59013 59016 59019 +59022 59025 59028 59031 59034 59037 59040 59043 59046 +59049 59052 59055 59058 59061 59064 59067 59070 59073 +59076 59079 59082 59085 59088 59091 59094 59097 59100 +59103 59106 59109 59112 59115 59118 59121 59124 59127 +59130 59133 59136 59139 59142 59145 59148 59151 59154 +59157 59160 59163 59166 59169 59172 59175 59178 59181 +59184 59187 59190 59193 59196 59199 59202 59205 59208 +59211 59214 59217 59220 59223 59226 59229 59232 59235 +59238 59241 59244 59247 59250 59253 59256 59259 59262 +59265 59268 59271 59274 59277 59280 59283 59286 59289 +59292 59295 59298 59301 59304 59307 59310 59313 59316 +59319 59322 59325 59328 59331 59334 59337 59340 59343 +59346 59349 59352 59355 59358 59361 59364 59367 59370 +59373 59376 59379 59382 59385 59388 59391 59394 59397 +59400 59403 59406 59409 59412 59415 59418 59421 59424 +59427 59430 59433 59436 59439 59442 59445 59448 59451 +59454 59457 59460 59463 59466 59469 59472 59475 59478 +59481 59484 59487 59490 59493 59496 59499 59502 59505 +59508 59511 59514 59517 59520 59523 59526 59529 59532 +59535 59538 59541 59544 59547 59550 59553 59556 59559 +59562 59565 59568 59571 59574 59577 59580 59583 59586 +59589 59592 59595 59598 59601 59604 59607 59610 59613 +59616 59619 59622 59625 59628 59631 59634 59637 59640 +59643 59646 59649 59652 59655 59658 59661 59664 59667 +59670 59673 59676 59679 59682 59685 59688 59691 59694 +59697 59700 59703 59706 59709 59712 59715 59718 59721 +59724 59727 59730 59733 59736 59739 59742 59745 59748 +59751 59754 59757 59760 59763 59766 59769 59772 59775 +59778 59781 59784 59787 59790 59793 59796 59799 59802 +59805 59808 59811 59814 59817 59820 59823 59826 59829 +59832 59835 59838 59841 59844 59847 59850 59853 59856 +59859 59862 59865 59868 59871 59874 59877 59880 59883 +59886 59889 59892 59895 59898 59901 59904 59907 59910 +59913 59916 59919 59922 59925 59928 59931 59934 59937 +59940 59943 59946 59949 59952 59955 59958 59961 59964 +59967 59970 59973 59976 59979 59982 59985 59988 59991 +59994 59997 60000 60003 60006 60009 60012 60015 60018 +60021 60024 60027 60030 60033 60036 60039 60042 60045 +60048 60051 60054 60057 60060 60063 60066 60069 60072 +60075 60078 60081 60084 60087 60090 60093 60096 60099 +60102 60105 60108 60111 60114 60117 60120 60123 60126 +60129 60132 60135 60138 60141 60144 60147 60150 60153 +60156 60159 60162 60165 60168 60171 60174 60177 60180 +60183 60186 60189 60192 60195 60198 60201 60204 60207 +60210 60213 60216 60219 60222 60225 60228 60231 60234 +60237 60240 60243 60246 60249 60252 60255 60258 60261 +60264 60267 60270 60273 60276 60279 60282 60285 60288 +60291 60294 60297 60300 60303 60306 60309 60312 60315 +60318 60321 60324 60327 60330 60333 60336 60339 60342 +60345 60348 60351 60354 60357 60360 60363 60366 60369 +60372 60375 60378 60381 60384 60387 60390 60393 60396 +60399 60402 60405 60408 60411 60414 60417 60420 60423 +60426 60429 60432 60435 60438 60441 60444 60447 60450 +60453 60456 60459 60462 60465 60468 60471 60474 60477 +60480 60483 60486 60489 60492 60495 60498 60501 60504 +60507 60510 60513 60516 60519 60522 60525 60528 60531 +60534 60537 60540 60543 60546 60549 60552 60555 60558 +60561 60564 60567 60570 60573 60576 60579 60582 60585 +60588 60591 60594 60597 60600 60603 60606 60609 60612 +60615 60618 60621 60624 60627 60630 60633 60636 60639 +60642 60645 60648 60651 60654 60657 60660 60663 60666 +60669 60672 60675 60678 60681 60684 60687 60690 60693 +60696 60699 60702 60705 60708 60711 60714 60717 60720 +60723 60726 60729 60732 60735 60738 60741 60744 60747 +60750 60753 60756 60759 60762 60765 60768 60771 60774 +60777 60780 60783 60786 60789 60792 60795 60798 60801 +60804 60807 60810 60813 60816 60819 60822 60825 60828 +60831 60834 60837 60840 60843 60846 60849 60852 60855 +60858 60861 60864 60867 60870 60873 60876 60879 60882 +60885 60888 60891 60894 60897 60900 60903 60906 60909 +60912 60915 60918 60921 60924 60927 60930 60933 60936 +60939 60942 60945 60948 60951 60954 60957 60960 60963 +60966 60969 60972 60975 60978 60981 60984 60987 60990 +60993 60996 60999 61002 61005 61008 61011 61014 61017 +61020 61023 61026 61029 61032 61035 61038 61041 61044 +61047 61050 61053 61056 61059 61062 61065 61068 61071 +61074 61077 61080 61083 61086 61089 61092 61095 61098 +61101 61104 61107 61110 61113 61116 61119 61122 61125 +61128 61131 61134 61137 61140 61143 61146 61149 61152 +61155 61158 61161 61164 61167 61170 61173 61176 61179 +61182 61185 61188 61191 61194 61197 61200 61203 61206 +61209 61212 61215 61218 61221 61224 61227 61230 61233 +61236 61239 61242 61245 61248 61251 61254 61257 61260 +61263 61266 61269 61272 61275 61278 61281 61284 61287 +61290 61293 61296 61299 61302 61305 61308 61311 61314 +61317 61320 61323 61326 61329 61332 61335 61338 61341 +61344 61347 61350 61353 61356 61359 61362 61365 61368 +61371 61374 61377 61380 61383 61386 61389 61392 61395 +61398 61401 61404 61407 61410 61413 61416 61419 61422 +61425 61428 61431 61434 61437 61440 61443 61446 61449 +61452 61455 61458 61461 61464 61467 61470 61473 61476 +61479 61482 61485 61488 61491 61494 61497 61500 61503 +61506 61509 61512 61515 61518 61521 61524 61527 61530 +61533 61536 61539 61542 61545 61548 61551 61554 61557 +61560 61563 61566 61569 61572 61575 61578 61581 61584 +61587 61590 61593 61596 61599 61602 61605 61608 61611 +61614 61617 61620 61623 61626 61629 61632 61635 61638 +61641 61644 61647 61650 61653 61656 61659 61662 61665 +61668 61671 61674 61677 61680 61683 61686 61689 61692 +61695 61698 61701 61704 61707 61710 61713 61716 61719 +61722 61725 61728 61731 61734 61737 61740 61743 61746 +61749 61752 61755 61758 61761 61764 61767 61770 61773 +61776 61779 61782 61785 61788 61791 61794 61797 61800 +61803 61806 61809 61812 61815 61818 61821 61824 61827 +61830 61833 61836 61839 61842 61845 61848 61851 61854 +61857 61860 61863 61866 61869 61872 61875 61878 61881 +61884 61887 61890 61893 61896 61899 61902 61905 61908 +61911 61914 61917 61920 61923 61926 61929 61932 61935 +61938 61941 61944 61947 61950 61953 61956 61959 61962 +61965 61968 61971 61974 61977 61980 61983 61986 61989 +61992 61995 61998 62001 62004 62007 62010 62013 62016 +62019 62022 62025 62028 62031 62034 62037 62040 62043 +62046 62049 62052 62055 62058 62061 62064 62067 62070 +62073 62076 62079 62082 62085 62088 62091 62094 62097 +62100 62103 62106 62109 62112 62115 62118 62121 62124 +62127 62130 62133 62136 62139 62142 62145 62148 62151 +62154 62157 62160 62163 62166 62169 62172 62175 62178 +62181 62184 62187 62190 62193 62196 62199 62202 62205 +62208 62211 62214 62217 62220 62223 62226 62229 62232 +62235 62238 62241 62244 62247 62250 62253 62256 62259 +62262 62265 62268 62271 62274 62277 62280 62283 62286 +62289 62292 62295 62298 62301 62304 62307 62310 62313 +62316 62319 62322 62325 62328 62331 62334 62337 62340 +62343 62346 62349 62352 62355 62358 62361 62364 62367 +62370 62373 62376 62379 62382 62385 62388 62391 62394 +62397 62400 62403 62406 62409 62412 62415 62418 62421 +62424 62427 62430 62433 62436 62439 62442 62445 62448 +62451 62454 62457 62460 62463 62466 62469 62472 62475 +62478 62481 62484 62487 62490 62493 62496 62499 62502 +62505 62508 62511 62514 62517 62520 62523 62526 62529 +62532 62535 62538 62541 62544 62547 62550 62553 62556 +62559 62562 62565 62568 62571 62574 62577 62580 62583 +62586 62589 62592 62595 62598 62601 62604 62607 62610 +62613 62616 62619 62622 62625 62628 62631 62634 62637 +62640 62643 62646 62649 62652 62655 62658 62661 62664 +62667 62670 62673 62676 62679 62682 62685 62688 62691 +62694 62697 62700 62703 62706 62709 62712 62715 62718 +62721 62724 62727 62730 62733 62736 62739 62742 62745 +62748 62751 62754 62757 62760 62763 62766 62769 62772 +62775 62778 62781 62784 62787 62790 62793 62796 62799 +62802 62805 62808 62811 62814 62817 62820 62823 62826 +62829 62832 62835 62838 62841 62844 62847 62850 62853 +62856 62859 62862 62865 62868 62871 62874 62877 62880 +62883 62886 62889 62892 62895 62898 62901 62904 62907 +62910 62913 62916 62919 62922 62925 62928 62931 62934 +62937 62940 62943 62946 62949 62952 62955 62958 62961 +62964 62967 62970 62973 62976 62979 62982 62985 62988 +62991 62994 62997 63000 63003 63006 63009 63012 63015 +63018 63021 63024 63027 63030 63033 63036 63039 63042 +63045 63048 63051 63054 63057 63060 63063 63066 63069 +63072 63075 63078 63081 63084 63087 63090 63093 63096 +63099 63102 63105 63108 63111 63114 63117 63120 63123 +63126 63129 63132 63135 63138 63141 63144 63147 63150 +63153 63156 63159 63162 63165 63168 63171 63174 63177 +63180 63183 63186 63189 63192 63195 63198 63201 63204 +63207 63210 63213 63216 63219 63222 63225 63228 63231 +63234 63237 63240 63243 63246 63249 63252 63255 63258 +63261 63264 63267 63270 63273 63276 63279 63282 63285 +63288 63291 63294 63297 63300 63303 63306 63309 63312 +63315 63318 63321 63324 63327 63330 63333 63336 63339 +63342 63345 63348 63351 63354 63357 63360 63363 63366 +63369 63372 63375 63378 63381 63384 63387 63390 63393 +63396 63399 63402 63405 63408 63411 63414 63417 63420 +63423 63426 63429 63432 63435 63438 63441 63444 63447 +63450 63453 63456 63459 63462 63465 63468 63471 63474 +63477 63480 63483 63486 63489 63492 63495 63498 63501 +63504 63507 63510 63513 63516 63519 63522 63525 63528 +63531 63534 63537 63540 63543 63546 63549 63552 63555 +63558 63561 63564 63567 63570 63573 63576 63579 63582 +63585 63588 63591 63594 63597 63600 63603 63606 63609 +63612 63615 63618 63621 63624 63627 63630 63633 63636 +63639 63642 63645 63648 63651 63654 63657 63660 63663 +63666 63669 63672 63675 63678 63681 63684 63687 63690 +63693 63696 63699 63702 63705 63708 63711 63714 63717 +63720 63723 63726 63729 63732 63735 63738 63741 63744 +63747 63750 63753 63756 63759 63762 63765 63768 63771 +63774 63777 63780 63783 63786 63789 63792 63795 63798 +63801 63804 63807 63810 63813 63816 63819 63822 63825 +63828 63831 63834 63837 63840 63843 63846 63849 63852 +63855 63858 63861 63864 63867 63870 63873 63876 63879 +63882 63885 63888 63891 63894 63897 63900 63903 63906 +63909 63912 63915 63918 63921 63924 63927 63930 63933 +63936 63939 63942 63945 63948 63951 63954 63957 63960 +63963 63966 63969 63972 63975 63978 63981 63984 63987 +63990 63993 63996 63999 64002 64005 64008 64011 64014 +64017 64020 64023 64026 64029 64032 64035 64038 64041 +64044 64047 64050 64053 64056 64059 64062 64065 64068 +64071 64074 64077 64080 64083 64086 64089 64092 64095 +64098 64101 64104 64107 64110 64113 64116 64119 64122 +64125 64128 64131 64134 64137 64140 64143 64146 64149 +64152 64155 64158 64161 64164 64167 64170 64173 64176 +64179 64182 64185 64188 64191 64194 64197 64200 64203 +64206 64209 64212 64215 64218 64221 64224 64227 64230 +64233 64236 64239 64242 64245 64248 64251 64254 64257 +64260 64263 64266 64269 64272 64275 64278 64281 64284 +64287 64290 64293 64296 64299 64302 64305 64308 64311 +64314 64317 64320 64323 64326 64329 64332 64335 64338 +64341 64344 64347 64350 64353 64356 64359 64362 64365 +64368 64371 64374 64377 64380 64383 64386 64389 64392 +64395 64398 64401 64404 64407 64410 64413 64416 64419 +64422 64425 64428 64431 64434 64437 64440 64443 64446 +64449 64452 64455 64458 64461 64464 64467 64470 64473 +64476 64479 64482 64485 64488 64491 64494 64497 64500 +64503 64506 64509 64512 64515 64518 64521 64524 64527 +64530 64533 64536 64539 64542 64545 64548 64551 64554 +64557 64560 64563 64566 64569 64572 64575 64578 64581 +64584 64587 64590 64593 64596 64599 64602 64605 64608 +64611 64614 64617 64620 64623 64626 64629 64632 64635 +64638 64641 64644 64647 64650 64653 64656 64659 64662 +64665 64668 64671 64674 64677 64680 64683 64686 64689 +64692 64695 64698 64701 64704 64707 64710 64713 64716 +64719 64722 64725 64728 64731 64734 64737 64740 64743 +64746 64749 64752 64755 64758 64761 64764 64767 64770 +64773 64776 64779 64782 64785 64788 64791 64794 64797 +64800 64803 64806 64809 64812 64815 64818 64821 64824 +64827 64830 64833 64836 64839 64842 64845 64848 64851 +64854 64857 64860 64863 64866 64869 64872 64875 64878 +64881 64884 64887 64890 64893 64896 64899 64902 64905 +64908 64911 64914 64917 64920 64923 64926 64929 64932 +64935 64938 64941 64944 64947 64950 64953 64956 64959 +64962 64965 64968 64971 64974 64977 64980 64983 64986 +64989 64992 64995 64998 65001 65004 65007 65010 65013 +65016 65019 65022 65025 65028 65031 65034 65037 65040 +65043 65046 65049 65052 65055 65058 65061 65064 65067 +65070 65073 65076 65079 65082 65085 65088 65091 65094 +65097 65100 65103 65106 65109 65112 65115 65118 65121 +65124 65127 65130 65133 65136 65139 65142 65145 65148 +65151 65154 65157 65160 65163 65166 65169 65172 65175 +65178 65181 65184 65187 65190 65193 65196 65199 65202 +65205 65208 65211 65214 65217 65220 65223 65226 65229 +65232 65235 65238 65241 65244 65247 65250 65253 65256 +65259 65262 65265 65268 65271 65274 65277 65280 65283 +65286 65289 65292 65295 65298 65301 65304 65307 65310 +65313 65316 65319 65322 65325 65328 65331 65334 65337 +65340 65343 65346 65349 65352 65355 65358 65361 65364 +65367 65370 65373 65376 65379 65382 65385 65388 65391 +65394 65397 65400 65403 65406 65409 65412 65415 65418 +65421 65424 65427 65430 65433 65436 65439 65442 65445 +65448 65451 65454 65457 65460 65463 65466 65469 65472 +65475 65478 65481 65484 65487 65490 65493 65496 65499 +65502 65505 65508 65511 65514 65517 65520 65523 65526 +65529 65532 65535 65538 65541 65544 65547 65550 65553 +65556 65559 65562 65565 65568 65571 65574 65577 65580 +65583 65586 65589 65592 65595 65598 65601 65604 65607 +65610 65613 65616 65619 65622 65625 65628 65631 65634 +65637 65640 65643 65646 65649 65652 65655 65658 65661 +65664 65667 65670 65673 65676 65679 65682 65685 65688 +65691 65694 65697 65700 65703 65706 65709 65712 65715 +65718 65721 65724 65727 65730 65733 65736 65739 65742 +65745 65748 65751 65754 65757 65760 65763 65766 65769 +65772 65775 65778 65781 65784 65787 65790 65793 65796 +65799 65802 65805 65808 65811 65814 65817 65820 65823 +65826 65829 65832 65835 65838 65841 65844 65847 65850 +65853 65856 65859 65862 65865 65868 65871 65874 65877 +65880 65883 65886 65889 65892 65895 65898 65901 65904 +65907 65910 65913 65916 65919 65922 65925 65928 65931 +65934 65937 65940 65943 65946 65949 65952 65955 65958 +65961 65964 65967 65970 65973 65976 65979 65982 65985 +65988 65991 65994 65997 66000 66003 66006 66009 66012 +66015 66018 66021 66024 66027 66030 66033 66036 66039 +66042 66045 66048 66051 66054 66057 66060 66063 66066 +66069 66072 66075 66078 66081 66084 66087 66090 66093 +66096 66099 66102 66105 66108 66111 66114 66117 66120 +66123 66126 66129 66132 66135 66138 66141 66144 66147 +66150 66153 66156 66159 66162 66165 66168 66171 66174 +66177 66180 66183 66186 66189 66192 66195 66198 66201 +66204 66207 66210 66213 66216 66219 66222 66225 66228 +66231 66234 66237 66240 66243 66246 66249 66252 66255 +66258 66261 66264 66267 66270 66273 66276 66279 66282 +66285 66288 66291 66294 66297 66300 66303 66306 66309 +66312 66315 66318 66321 66324 66327 66330 66333 66336 +66339 66342 66345 66348 66351 66354 66357 66360 66363 +66366 66369 66372 66375 66378 66381 66384 66387 66390 +66393 66396 66399 66402 66405 66408 66411 66414 66417 +66420 66423 66426 66429 66432 66435 66438 66441 66444 +66447 66450 66453 66456 66459 66462 66465 66468 66471 +66474 66477 66480 66483 66486 66489 66492 66495 66498 +66501 66504 66507 66510 66513 66516 66519 66522 66525 +66528 66531 66534 66537 66540 66543 66546 66549 66552 +66555 66558 66561 66564 66567 66570 66573 66576 66579 +66582 66585 66588 66591 66594 66597 66600 66603 66606 +66609 66612 66615 66618 66621 66624 66627 66630 66633 +66636 66639 66642 66645 66648 66651 66654 66657 66660 +66663 66666 66669 66672 66675 66678 66681 66684 66687 +66690 66693 66696 66699 66702 66705 66708 66711 66714 +66717 66720 66723 66726 66729 66732 66735 66738 66741 +66744 66747 66750 66753 66756 66759 66762 66765 66768 +66771 66774 66777 66780 66783 66786 66789 66792 66795 +66798 66801 66804 66807 66810 66813 66816 66819 66822 +66825 66828 66831 66834 66837 66840 66843 66846 66849 +66852 66855 66858 66861 66864 66867 66870 66873 66876 +66879 66882 66885 66888 66891 66894 66897 66900 66903 +66906 66909 66912 66915 66918 66921 66924 66927 66930 +66933 66936 66939 66942 66945 66948 66951 66954 66957 +66960 66963 66966 66969 66972 66975 66978 66981 66984 +66987 66990 66993 66996 66999 67002 67005 67008 67011 +67014 67017 67020 67023 67026 67029 67032 67035 67038 +67041 67044 67047 67050 67053 67056 67059 67062 67065 +67068 67071 67074 67077 67080 67083 67086 67089 67092 +67095 67098 67101 67104 67107 67110 67113 67116 67119 +67122 67125 67128 67131 67134 67137 67140 67143 67146 +67149 67152 67155 67158 67161 67164 67167 67170 67173 +67176 67179 67182 67185 67188 67191 67194 67197 67200 +67203 67206 67209 67212 67215 67218 67221 67224 67227 +67230 67233 67236 67239 67242 67245 67248 67251 67254 +67257 67260 67263 67266 67269 67272 67275 67278 67281 +67284 67287 67290 67293 67296 67299 67302 67305 67308 +67311 67314 67317 67320 67323 67326 67329 67332 67335 +67338 67341 67344 67347 67350 67353 67356 67359 67362 +67365 67368 67371 67374 67377 67380 67383 67386 67389 +67392 67395 67398 67401 67404 67407 67410 67413 67416 +67419 67422 67425 67428 67431 67434 67437 67440 67443 +67446 67449 67452 67455 67458 67461 67464 67467 67470 +67473 67476 67479 67482 67485 67488 67491 67494 67497 +67500 67503 67506 67509 67512 67515 67518 67521 67524 +67527 67530 67533 67536 67539 67542 67545 67548 67551 +67554 67557 67560 67563 67566 67569 67572 67575 67578 +67581 67584 67587 67590 67593 67596 67599 67602 67605 +67608 67611 67614 67617 67620 67623 67626 67629 67632 +67635 67638 67641 67644 67647 67650 67653 67656 67659 +67662 67665 67668 67671 67674 67677 67680 67683 67686 +67689 67692 67695 67698 67701 67704 67707 67710 67713 +67716 67719 67722 67725 67728 67731 67734 67737 67740 +67743 67746 67749 67752 67755 67758 67761 67764 67767 +67770 67773 67776 67779 67782 67785 67788 67791 67794 +67797 67800 67803 67806 67809 67812 67815 67818 67821 +67824 67827 67830 67833 67836 67839 67842 67845 67848 +67851 67854 67857 67860 67863 67866 67869 67872 67875 +67878 67881 67884 67887 67890 67893 67896 67899 67902 +67905 67908 67911 67914 67917 67920 67923 67926 67929 +67932 67935 67938 67941 67944 67947 67950 67953 67956 +67959 67962 67965 67968 67971 67974 67977 67980 67983 +67986 67989 67992 67995 67998 68001 68004 68007 68010 +68013 68016 68019 68022 68025 68028 68031 68034 68037 +68040 68043 68046 68049 68052 68055 68058 68061 68064 +68067 68070 68073 68076 68079 68082 68085 68088 68091 +68094 68097 68100 68103 68106 68109 68112 68115 68118 +68121 68124 68127 68130 68133 68136 68139 68142 68145 +68148 68151 68154 68157 68160 68163 68166 68169 68172 +68175 68178 68181 68184 68187 68190 68193 68196 68199 +68202 68205 68208 68211 68214 68217 68220 68223 68226 +68229 68232 68235 68238 68241 68244 68247 68250 68253 +68256 68259 68262 68265 68268 68271 68274 68277 68280 +68283 68286 68289 68292 68295 68298 68301 68304 68307 +68310 68313 68316 68319 68322 68325 68328 68331 68334 +68337 68340 68343 68346 68349 68352 68355 68358 68361 +68364 68367 68370 68373 68376 68379 68382 68385 68388 +68391 68394 68397 68400 68403 68406 68409 68412 68415 +68418 68421 68424 68427 68430 68433 68436 68439 68442 +68445 68448 68451 68454 68457 68460 68463 68466 68469 +68472 68475 68478 68481 68484 68487 68490 68493 68496 +68499 68502 68505 68508 68511 68514 68517 68520 68523 +68526 68529 68532 68535 68538 68541 68544 68547 68550 +68553 68556 68559 68562 68565 68568 68571 68574 68577 +68580 68583 68586 68589 68592 68595 68598 68601 68604 +68607 68610 68613 68616 68619 68622 68625 68628 68631 +68634 68637 68640 68643 68646 68649 68652 68655 68658 +68661 68664 68667 68670 68673 68676 68679 68682 68685 +68688 68691 68694 68697 68700 68703 68706 68709 68712 +68715 68718 68721 68724 68727 68730 68733 68736 68739 +68742 68745 68748 68751 68754 68757 68760 68763 68766 +68769 68772 68775 68778 68781 68784 68787 68790 68793 +68796 68799 68802 68805 68808 68811 68814 68817 68820 +68823 68826 68829 68832 68835 68838 68841 68844 68847 +68850 68853 68856 68859 68862 68865 68868 68871 68874 +68877 68880 68883 68886 68889 68892 68895 68898 68901 +68904 68907 68910 68913 68916 68919 68922 68925 68928 +68931 68934 68937 68940 68943 68946 68949 68952 68955 +68958 68961 68964 68967 68970 68973 68976 68979 68982 +68985 68988 68991 68994 68997 69000 69003 69006 69009 +69012 69015 69018 69021 69024 69027 69030 69033 69036 +69039 69042 69045 69048 69051 69054 69057 69060 69063 +69066 69069 69072 69075 69078 69081 69084 69087 69090 +69093 69096 69099 69102 69105 69108 69111 69114 69117 +69120 69123 69126 69129 69132 69135 69138 69141 69144 +69147 69150 69153 69156 69159 69162 69165 69168 69171 +69174 69177 69180 69183 69186 69189 69192 69195 69198 +69201 69204 69207 69210 69213 69216 69219 69222 69225 +69228 69231 69234 69237 69240 69243 69246 69249 69252 +69255 69258 69261 69264 69267 69270 69273 69276 69279 +69282 69285 69288 69291 69294 69297 69300 69303 69306 +69309 69312 69315 69318 69321 69324 69327 69330 69333 +69336 69339 69342 69345 69348 69351 69354 69357 69360 +69363 69366 69369 69372 69375 69378 69381 69384 69387 +69390 69393 69396 69399 69402 69405 69408 69411 69414 +69417 69420 69423 69426 69429 69432 69435 69438 69441 +69444 69447 69450 69453 69456 69459 69462 69465 69468 +69471 69474 69477 69480 69483 69486 69489 69492 69495 +69498 69501 69504 69507 69510 69513 69516 69519 69522 +69525 69528 69531 69534 69537 69540 69543 69546 69549 +69552 69555 69558 69561 69564 69567 69570 69573 69576 +69579 69582 69585 69588 69591 69594 69597 69600 69603 +69606 69609 69612 69615 69618 69621 69624 69627 69630 +69633 69636 69639 69642 69645 69648 69651 69654 69657 +69660 69663 69666 69669 69672 69675 69678 69681 69684 +69687 69690 69693 69696 69699 69702 69705 69708 69711 +69714 69717 69720 69723 69726 69729 69732 69735 69738 +69741 69744 69747 69750 69753 69756 69759 69762 69765 +69768 69771 69774 69777 69780 69783 69786 69789 69792 +69795 69798 69801 69804 69807 69810 69813 69816 69819 +69822 69825 69828 69831 69834 69837 69840 69843 69846 +69849 69852 69855 69858 69861 69864 69867 69870 69873 +69876 69879 69882 69885 69888 69891 69894 69897 69900 +69903 69906 69909 69912 69915 69918 69921 69924 69927 +69930 69933 69936 69939 69942 69945 69948 69951 69954 +69957 69960 69963 69966 69969 69972 69975 69978 69981 +69984 69987 69990 69993 69996 69999 70002 70005 70008 +70011 70014 70017 70020 70023 70026 70029 70032 70035 +70038 70041 70044 70047 70050 70053 70056 70059 70062 +70065 70068 70071 70074 70077 70080 70083 70086 70089 +70092 70095 70098 70101 70104 70107 70110 70113 70116 +70119 70122 70125 70128 70131 70134 70137 70140 70143 +70146 70149 70152 70155 70158 70161 70164 70167 70170 +70173 70176 70179 70182 70185 70188 70191 70194 70197 +70200 70203 70206 70209 70212 70215 70218 70221 70224 +70227 70230 70233 70236 70239 70242 70245 70248 70251 +70254 70257 70260 70263 70266 70269 70272 70275 70278 +70281 70284 70287 70290 70293 70296 70299 70302 70305 +70308 70311 70314 70317 70320 70323 70326 70329 70332 +70335 70338 70341 70344 70347 70350 70353 70356 70359 +70362 70365 70368 70371 70374 70377 70380 70383 70386 +70389 70392 70395 70398 70401 70404 70407 70410 70413 +70416 70419 70422 70425 70428 70431 70434 70437 70440 +70443 70446 70449 70452 70455 70458 70461 70464 70467 +70470 70473 70476 70479 70482 70485 70488 70491 70494 +70497 70500 70503 70506 70509 70512 70515 70518 70521 +70524 70527 70530 70533 70536 70539 70542 70545 70548 +70551 70554 70557 70560 70563 70566 70569 70572 70575 +70578 70581 70584 70587 70590 70593 70596 70599 70602 +70605 70608 70611 70614 70617 70620 70623 70626 70629 +70632 70635 70638 70641 70644 70647 70650 70653 70656 +70659 70662 70665 70668 70671 70674 70677 70680 70683 +70686 70689 70692 70695 70698 70701 70704 70707 70710 +70713 70716 70719 70722 70725 70728 70731 70734 70737 +70740 70743 70746 70749 70752 70755 70758 70761 70764 +70767 70770 70773 70776 70779 70782 70785 70788 70791 +70794 70797 70800 70803 70806 70809 70812 70815 70818 +70821 70824 70827 70830 70833 70836 70839 70842 70845 +70848 70851 70854 70857 70860 70863 70866 70869 70872 +70875 70878 70881 70884 70887 70890 70893 70896 70899 +70902 70905 70908 70911 70914 70917 70920 70923 70926 +70929 70932 70935 70938 70941 70944 70947 70950 70953 +70956 70959 70962 70965 70968 70971 70974 70977 70980 +70983 70986 70989 70992 70995 70998 71001 71004 71007 +71010 71013 71016 71019 71022 71025 71028 71031 71034 +71037 71040 71043 71046 71049 71052 71055 71058 71061 +71064 71067 71070 71073 71076 71079 71082 71085 71088 +71091 71094 71097 71100 71103 71106 71109 71112 71115 +71118 71121 71124 71127 71130 71133 71136 71139 71142 +71145 71148 71151 71154 71157 71160 71163 71166 71169 +71172 71175 71178 71181 71184 71187 71190 71193 71196 +71199 71202 71205 71208 71211 71214 71217 71220 71223 +71226 71229 71232 71235 71238 71241 71244 71247 71250 +71253 71256 71259 71262 71265 71268 71271 71274 71277 +71280 71283 71286 71289 71292 71295 71298 71301 71304 +71307 71310 71313 71316 71319 71322 71325 71328 71331 +71334 71337 71340 71343 71346 71349 71352 71355 71358 +71361 71364 71367 71370 71373 71376 71379 71382 71385 +71388 71391 71394 71397 71400 71403 71406 71409 71412 +71415 71418 71421 71424 71427 71430 71433 71436 71439 +71442 71445 71448 71451 71454 71457 71460 71463 71466 +71469 71472 71475 71478 71481 71484 71487 71490 71493 +71496 71499 71502 71505 71508 71511 71514 71517 71520 +71523 71526 71529 71532 71535 71538 71541 71544 71547 +71550 71553 71556 71559 71562 71565 71568 71571 71574 +71577 71580 71583 71586 71589 71592 71595 71598 71601 +71604 71607 71610 71613 71616 71619 71622 71625 71628 +71631 71634 71637 71640 71643 71646 71649 71652 71655 +71658 71661 71664 71667 71670 71673 71676 71679 71682 +71685 71688 71691 71694 71697 71700 71703 71706 71709 +71712 71715 71718 71721 71724 71727 71730 71733 71736 +71739 71742 71745 71748 71751 71754 71757 71760 71763 +71766 71769 71772 71775 71778 71781 71784 71787 71790 +71793 71796 71799 71802 71805 71808 71811 71814 71817 +71820 71823 71826 71829 71832 71835 71838 71841 71844 +71847 71850 71853 71856 71859 71862 71865 71868 71871 +71874 71877 71880 71883 71886 71889 71892 71895 71898 +71901 71904 71907 71910 71913 71916 71919 71922 71925 +71928 71931 71934 71937 71940 71943 71946 71949 71952 +71955 71958 71961 71964 71967 71970 71973 71976 71979 +71982 71985 71988 71991 71994 71997 72000 72003 72006 +72009 72012 72015 72018 72021 72024 72027 72030 72033 +72036 72039 72042 72045 72048 72051 72054 72057 72060 +72063 72066 72069 72072 72075 72078 72081 72084 72087 +72090 72093 72096 72099 72102 72105 72108 72111 72114 +72117 72120 72123 72126 72129 72132 72135 72138 72141 +72144 72147 72150 72153 72156 72159 72162 72165 72168 +72171 72174 72177 72180 72183 72186 72189 72192 72195 +72198 72201 72204 72207 72210 72213 72216 72219 72222 +72225 72228 72231 72234 72237 72240 72243 72246 72249 +72252 72255 72258 72261 72264 72267 72270 72273 72276 +72279 72282 72285 72288 72291 72294 72297 72300 72303 +72306 72309 72312 72315 72318 72321 72324 72327 72330 +72333 72336 72339 72342 72345 72348 72351 72354 72357 +72360 72363 72366 72369 72372 72375 72378 72381 72384 +72387 72390 72393 72396 72399 72402 72405 72408 72411 +72414 72417 72420 72423 72426 72429 72432 72435 72438 +72441 72444 72447 72450 72453 72456 72459 72462 72465 +72468 72471 72474 72477 72480 72483 72486 72489 72492 +72495 72498 72501 72504 72507 72510 72513 72516 72519 +72522 72525 72528 72531 72534 72537 72540 72543 72546 +72549 72552 72555 72558 72561 72564 72567 72570 72573 +72576 72579 72582 72585 72588 72591 72594 72597 72600 +72603 72606 72609 72612 72615 72618 72621 72624 72627 +72630 72633 72636 72639 72642 72645 72648 72651 72654 +72657 72660 72663 72666 72669 72672 72675 72678 72681 +72684 72687 72690 72693 72696 72699 72702 72705 72708 +72711 72714 72717 72720 72723 72726 72729 72732 72735 +72738 72741 72744 72747 72750 72753 72756 72759 72762 +72765 72768 72771 72774 72777 72780 72783 72786 72789 +72792 72795 72798 72801 72804 72807 72810 72813 72816 +72819 72822 72825 72828 72831 72834 72837 72840 72843 +72846 72849 72852 72855 72858 72861 72864 72867 72870 +72873 72876 72879 72882 72885 72888 72891 72894 72897 +72900 72903 72906 72909 72912 72915 72918 72921 72924 +72927 72930 72933 72936 72939 72942 72945 72948 72951 +72954 72957 72960 72963 72966 72969 72972 72975 72978 +72981 72984 72987 72990 72993 72996 72999 73002 73005 +73008 73011 73014 73017 73020 73023 73026 73029 73032 +73035 73038 73041 73044 73047 73050 73053 73056 73059 +73062 73065 73068 73071 73074 73077 73080 73083 73086 +73089 73092 73095 73098 73101 73104 73107 73110 73113 +73116 73119 73122 73125 73128 73131 73134 73137 73140 +73143 73146 73149 73152 73155 73158 73161 73164 73167 +73170 73173 73176 73179 73182 73185 73188 73191 73194 +73197 73200 73203 73206 73209 73212 73215 73218 73221 +73224 73227 73230 73233 73236 73239 73242 73245 73248 +73251 73254 73257 73260 73263 73266 73269 73272 73275 +73278 73281 73284 73287 73290 73293 73296 73299 73302 +73305 73308 73311 73314 73317 73320 73323 73326 73329 +73332 73335 73338 73341 73344 73347 73350 73353 73356 +73359 73362 73365 73368 73371 73374 73377 73380 73383 +73386 73389 73392 73395 73398 73401 73404 73407 73410 +73413 73416 73419 73422 73425 73428 73431 73434 73437 +73440 73443 73446 73449 73452 73455 73458 73461 73464 +73467 73470 73473 73476 73479 73482 73485 73488 73491 +73494 73497 73500 73503 73506 73509 73512 73515 73518 +73521 73524 73527 73530 73533 73536 73539 73542 73545 +73548 73551 73554 73557 73560 73563 73566 73569 73572 +73575 73578 73581 73584 73587 73590 73593 73596 73599 +73602 73605 73608 73611 73614 73617 73620 73623 73626 +73629 73632 73635 73638 73641 73644 73647 73650 73653 +73656 73659 73662 73665 73668 73671 73674 73677 73680 +73683 73686 73689 73692 73695 73698 73701 73704 73707 +73710 73713 73716 73719 73722 73725 73728 73731 73734 +73737 73740 73743 73746 73749 73752 73755 73758 73761 +73764 73767 73770 73773 73776 73779 73782 73785 73788 +73791 73794 73797 73800 73803 73806 73809 73812 73815 +73818 73821 73824 73827 73830 73833 73836 73839 73842 +73845 73848 73851 73854 73857 73860 73863 73866 73869 +73872 73875 73878 73881 73884 73887 73890 73893 73896 +73899 73902 73905 73908 73911 73914 73917 73920 73923 +73926 73929 73932 73935 73938 73941 73944 73947 73950 +73953 73956 73959 73962 73965 73968 73971 73974 73977 +73980 73983 73986 73989 73992 73995 73998 74001 74004 +74007 74010 74013 74016 74019 74022 74025 74028 74031 +74034 74037 74040 74043 74046 74049 74052 74055 74058 +74061 74064 74067 74070 74073 74076 74079 74082 74085 +74088 74091 74094 74097 74100 74103 74106 74109 74112 +74115 74118 74121 74124 74127 74130 74133 74136 74139 +74142 74145 74148 74151 74154 74157 74160 74163 74166 +74169 74172 74175 74178 74181 74184 74187 74190 74193 +74196 74199 74202 74205 74208 74211 74214 74217 74220 +74223 74226 74229 74232 74235 74238 74241 74244 74247 +74250 74253 74256 74259 74262 74265 74268 74271 74274 +74277 74280 74283 74286 74289 74292 74295 74298 74301 +74304 74307 74310 74313 74316 74319 74322 74325 74328 +74331 74334 74337 74340 74343 74346 74349 74352 74355 +74358 74361 74364 74367 74370 74373 74376 74379 74382 +74385 74388 74391 74394 74397 74400 74403 74406 74409 +74412 74415 74418 74421 74424 74427 74430 74433 74436 +74439 74442 74445 74448 74451 74454 74457 74460 74463 +74466 74469 74472 74475 74478 74481 74484 74487 74490 +74493 74496 74499 74502 74505 74508 74511 74514 74517 +74520 74523 74526 74529 74532 74535 74538 74541 74544 +74547 74550 74553 74556 74559 74562 74565 74568 74571 +74574 74577 74580 74583 74586 74589 74592 74595 74598 +74601 74604 74607 74610 74613 74616 74619 74622 74625 +74628 74631 74634 74637 74640 74643 74646 74649 74652 +74655 74658 74661 74664 74667 74670 74673 74676 74679 +74682 74685 74688 74691 74694 74697 74700 74703 74706 +74709 74712 74715 74718 74721 74724 74727 74730 74733 +74736 74739 74742 74745 74748 74751 74754 74757 74760 +74763 74766 74769 74772 74775 74778 74781 74784 74787 +74790 74793 74796 74799 74802 74805 74808 74811 74814 +74817 74820 74823 74826 74829 74832 74835 74838 74841 +74844 74847 74850 74853 74856 74859 74862 74865 74868 +74871 74874 74877 74880 74883 74886 74889 74892 74895 +74898 74901 74904 74907 74910 74913 74916 74919 74922 +74925 74928 74931 74934 74937 74940 74943 74946 74949 +74952 74955 74958 74961 74964 74967 74970 74973 74976 +74979 74982 74985 74988 74991 74994 74997 75000 75003 +75006 75009 75012 75015 75018 75021 75024 75027 75030 +75033 75036 75039 75042 75045 75048 75051 75054 75057 +75060 75063 75066 75069 75072 75075 75078 75081 75084 +75087 75090 75093 75096 75099 75102 75105 75108 75111 +75114 75117 75120 75123 75126 75129 75132 75135 75138 +75141 75144 75147 75150 75153 75156 75159 75162 75165 +75168 75171 75174 75177 75180 75183 75186 75189 75192 +75195 75198 75201 75204 75207 75210 75213 75216 75219 +75222 75225 75228 75231 75234 75237 75240 75243 75246 +75249 75252 75255 75258 75261 75264 75267 75270 75273 +75276 75279 75282 75285 75288 75291 75294 75297 75300 +75303 75306 75309 75312 75315 75318 75321 75324 75327 +75330 75333 75336 75339 75342 75345 75348 75351 75354 +75357 75360 75363 75366 75369 75372 75375 75378 75381 +75384 75387 75390 75393 75396 75399 75402 75405 75408 +75411 75414 75417 75420 75423 75426 75429 75432 75435 +75438 75441 75444 75447 75450 75453 75456 75459 75462 +75465 75468 75471 75474 75477 75480 75483 75486 75489 +75492 75495 75498 75501 75504 75507 75510 75513 75516 +75519 75522 75525 75528 75531 75534 75537 75540 75543 +75546 75549 75552 75555 75558 75561 75564 75567 75570 +75573 75576 75579 75582 75585 75588 75591 75594 75597 +75600 75603 75606 75609 75612 75615 75618 75621 75624 +75627 75630 75633 75636 75639 75642 75645 75648 75651 +75654 75657 75660 75663 75666 75669 75672 75675 75678 +75681 75684 75687 75690 75693 75696 75699 75702 75705 +75708 75711 75714 75717 75720 75723 75726 75729 75732 +75735 75738 75741 75744 75747 75750 75753 75756 75759 +75762 75765 75768 75771 75774 75777 75780 75783 75786 +75789 75792 75795 75798 75801 75804 75807 75810 75813 +75816 75819 75822 75825 75828 75831 75834 75837 75840 +75843 75846 75849 75852 75855 75858 75861 75864 75867 +75870 75873 75876 75879 75882 75885 75888 75891 75894 +75897 75900 75903 75906 75909 75912 75915 75918 75921 +75924 75927 75930 75933 75936 75939 75942 75945 75948 +75951 75954 75957 75960 75963 75966 75969 75972 75975 +75978 75981 75984 75987 75990 75993 75996 75999 76002 +76005 76008 76011 76014 76017 76020 76023 76026 76029 +76032 76035 76038 76041 76044 76047 76050 76053 76056 +76059 76062 76065 76068 76071 76074 76077 76080 76083 +76086 76089 76092 76095 76098 76101 76104 76107 76110 +76113 76116 76119 76122 76125 76128 76131 76134 76137 +76140 76143 76146 76149 76152 76155 76158 76161 76164 +76167 76170 76173 76176 76179 76182 76185 76188 76191 +76194 76197 76200 76203 76206 76209 76212 76215 76218 +76221 76224 76227 76230 76233 76236 76239 76242 76245 +76248 76251 76254 76257 76260 76263 76266 76269 76272 +76275 76278 76281 76284 76287 76290 76293 76296 76299 +76302 76305 76308 76311 76314 76317 76320 76323 76326 +76329 76332 76335 76338 76341 76344 76347 76350 76353 +76356 76359 76362 76365 76368 76371 76374 76377 76380 +76383 76386 76389 76392 76395 76398 76401 76404 76407 +76410 76413 76416 76419 76422 76425 76428 76431 76434 +76437 76440 76443 76446 76449 76452 76455 76458 76461 +76464 76467 76470 76473 76476 76479 76482 76485 76488 +76491 76494 76497 76500 76503 76506 76509 76512 76515 +76518 76521 76524 76527 76530 76533 76536 76539 76542 +76545 76548 76551 76554 76557 76560 76563 76566 76569 +76572 76575 76578 76581 76584 76587 76590 76593 76596 +76599 76602 76605 76608 76611 76614 76617 76620 76623 +76626 76629 76632 76635 76638 76641 76644 76647 76650 +76653 76656 76659 76662 76665 76668 76671 76674 76677 +76680 76683 76686 76689 76692 76695 76698 76701 76704 +76707 76710 76713 76716 76719 76722 76725 76728 76731 +76734 76737 76740 76743 76746 76749 76752 76755 76758 +76761 76764 76767 76770 76773 76776 76779 76782 76785 +76788 76791 76794 76797 76800 76803 76806 76809 76812 +76815 76818 76821 76824 76827 76830 76833 76836 76839 +76842 76845 76848 76851 76854 76857 76860 76863 76866 +76869 76872 76875 76878 76881 76884 76887 76890 76893 +76896 76899 76902 76905 76908 76911 76914 76917 76920 +76923 76926 76929 76932 76935 76938 76941 76944 76947 +76950 76953 76956 76959 76962 76965 76968 76971 76974 +76977 76980 76983 76986 76989 76992 76995 76998 77001 +77004 77007 77010 77013 77016 77019 77022 77025 77028 +77031 77034 77037 77040 77043 77046 77049 77052 77055 +77058 77061 77064 77067 77070 77073 77076 77079 77082 +77085 77088 77091 77094 77097 77100 77103 77106 77109 +77112 77115 77118 77121 77124 77127 77130 77133 77136 +77139 77142 77145 77148 77151 77154 77157 77160 77163 +77166 77169 77172 77175 77178 77181 77184 77187 77190 +77193 77196 77199 77202 77205 77208 77211 77214 77217 +77220 77223 77226 77229 77232 77235 77238 77241 77244 +77247 77250 77253 77256 77259 77262 77265 77268 77271 +77274 77277 77280 77283 77286 77289 77292 77295 77298 +77301 77304 77307 77310 77313 77316 77319 77322 77325 +77328 77331 77334 77337 77340 77343 77346 77349 77352 +77355 77358 77361 77364 77367 77370 77373 77376 77379 +77382 77385 77388 77391 77394 77397 77400 77403 77406 +77409 77412 77415 77418 77421 77424 77427 77430 77433 +77436 77439 77442 77445 77448 77451 77454 77457 77460 +77463 77466 77469 77472 77475 77478 77481 77484 77487 +77490 77493 77496 77499 77502 77505 77508 77511 77514 +77517 77520 77523 77526 77529 77532 77535 77538 77541 +77544 77547 77550 77553 77556 77559 77562 77565 77568 +77571 77574 77577 77580 77583 77586 77589 77592 77595 +77598 77601 77604 77607 77610 77613 77616 77619 77622 +77625 77628 77631 77634 77637 77640 77643 77646 77649 +77652 77655 77658 77661 77664 77667 77670 77673 77676 +77679 77682 77685 77688 77691 77694 77697 77700 77703 +77706 77709 77712 77715 77718 77721 77724 77727 77730 +77733 77736 77739 77742 77745 77748 77751 77754 77757 +77760 77763 77766 77769 77772 77775 77778 77781 77784 +77787 77790 77793 77796 77799 77802 77805 77808 77811 +77814 77817 77820 77823 77826 77829 77832 77835 77838 +77841 77844 77847 77850 77853 77856 77859 77862 77865 +77868 77871 77874 77877 77880 77883 77886 77889 77892 +77895 77898 77901 77904 77907 77910 77913 77916 77919 +77922 77925 77928 77931 77934 77937 77940 77943 77946 +77949 77952 77955 77958 77961 77964 77967 77970 77973 +77976 77979 77982 77985 77988 77991 77994 77997 78000 +78003 78006 78009 78012 78015 78018 78021 78024 78027 +78030 78033 78036 78039 78042 78045 78048 78051 78054 +78057 78060 78063 78066 78069 78072 78075 78078 78081 +78084 78087 78090 78093 78096 78099 78102 78105 78108 +78111 78114 78117 78120 78123 78126 78129 78132 78135 +78138 78141 78144 78147 78150 78153 78156 78159 78162 +78165 78168 78171 78174 78177 78180 78183 78186 78189 +78192 78195 78198 78201 78204 78207 78210 78213 78216 +78219 78222 78225 78228 78231 78234 78237 78240 78243 +78246 78249 78252 78255 78258 78261 78264 78267 78270 +78273 78276 78279 78282 78285 78288 78291 78294 78297 +78300 78303 78306 78309 78312 78315 78318 78321 78324 +78327 78330 78333 78336 78339 78342 78345 78348 78351 +78354 78357 78360 78363 78366 78369 78372 78375 78378 +78381 78384 78387 78390 78393 78396 78399 78402 78405 +78408 78411 78414 78417 78420 78423 78426 78429 78432 +78435 78438 78441 78444 78447 78450 78453 78456 78459 +78462 78465 78468 78471 78474 78477 78480 78483 78486 +78489 78492 78495 78498 78501 78504 78507 78510 78513 +78516 78519 78522 78525 78528 78531 78534 78537 78540 +78543 78546 78549 78552 78555 78558 78561 78564 78567 +78570 78573 78576 78579 78582 78585 78588 78591 78594 +78597 78600 78603 78606 78609 78612 78615 78618 78621 +78624 78627 78630 78633 78636 78639 78642 78645 78648 +78651 78654 78657 78660 78663 78666 78669 78672 78675 +78678 78681 78684 78687 78690 78693 78696 78699 78702 +78705 78708 78711 78714 78717 78720 78723 78726 78729 +78732 78735 78738 78741 78744 78747 78750 78753 78756 +78759 78762 78765 78768 78771 78774 78777 78780 78783 +78786 78789 78792 78795 78798 78801 78804 78807 78810 +78813 78816 78819 78822 78825 78828 78831 78834 78837 +78840 78843 78846 78849 78852 78855 78858 78861 78864 +78867 78870 78873 78876 78879 78882 78885 78888 78891 +78894 78897 78900 78903 78906 78909 78912 78915 78918 +78921 78924 78927 78930 78933 78936 78939 78942 78945 +78948 78951 78954 78957 78960 78963 78966 78969 78972 +78975 78978 78981 78984 78987 78990 78993 78996 78999 +79002 79005 79008 79011 79014 79017 79020 79023 79026 +79029 79032 79035 79038 79041 79044 79047 79050 79053 +79056 79059 79062 79065 79068 79071 79074 79077 79080 +79083 79086 79089 79092 79095 79098 79101 79104 79107 +79110 79113 79116 79119 79122 79125 79128 79131 79134 +79137 79140 79143 79146 79149 79152 79155 79158 79161 +79164 79167 79170 79173 79176 79179 79182 79185 79188 +79191 79194 79197 79200 79203 79206 79209 79212 79215 +79218 79221 79224 79227 79230 79233 79236 79239 79242 +79245 79248 79251 79254 79257 79260 79263 79266 79269 +79272 79275 79278 79281 79284 79287 79290 79293 79296 +79299 79302 79305 79308 79311 79314 79317 79320 79323 +79326 79329 79332 79335 79338 79341 79344 79347 79350 +79353 79356 79359 79362 79365 79368 79371 79374 79377 +79380 79383 79386 79389 79392 79395 79398 79401 79404 +79407 79410 79413 79416 79419 79422 79425 79428 79431 +79434 79437 79440 79443 79446 79449 79452 79455 79458 +79461 79464 79467 79470 79473 79476 79479 79482 79485 +79488 79491 79494 79497 79500 79503 79506 79509 79512 +79515 79518 79521 79524 79527 79530 79533 79536 79539 +79542 79545 79548 79551 79554 79557 79560 79563 79566 +79569 79572 79575 79578 79581 79584 79587 79590 79593 +79596 79599 79602 79605 79608 79611 79614 79617 79620 +79623 79626 79629 79632 79635 79638 79641 79644 79647 +79650 79653 79656 79659 79662 79665 79668 79671 79674 +79677 79680 79683 79686 79689 79692 79695 79698 79701 +79704 79707 79710 79713 79716 79719 79722 79725 79728 +79731 79734 79737 79740 79743 79746 79749 79752 79755 +79758 79761 79764 79767 79770 79773 79776 79779 79782 +79785 79788 79791 79794 79797 79800 79803 79806 79809 +79812 79815 79818 79821 79824 79827 79830 79833 79836 +79839 79842 79845 79848 79851 79854 79857 79860 79863 +79866 79869 79872 79875 79878 79881 79884 79887 79890 +79893 79896 79899 79902 79905 79908 79911 79914 79917 +79920 79923 79926 79929 79932 79935 79938 79941 79944 +79947 79950 79953 79956 79959 79962 79965 79968 79971 +79974 79977 79980 79983 79986 79989 79992 79995 79998 +80001 80004 80007 80010 80013 80016 80019 80022 80025 +80028 80031 80034 80037 80040 80043 80046 80049 80052 +80055 80058 80061 80064 80067 80070 80073 80076 80079 +80082 80085 80088 80091 80094 80097 80100 80103 80106 +80109 80112 80115 80118 80121 80124 80127 80130 80133 +80136 80139 80142 80145 80148 80151 80154 80157 80160 +80163 80166 80169 80172 80175 80178 80181 80184 80187 +80190 80193 80196 80199 80202 80205 80208 80211 80214 +80217 80220 80223 80226 80229 80232 80235 80238 80241 +80244 80247 80250 80253 80256 80259 80262 80265 80268 +80271 80274 80277 80280 80283 80286 80289 80292 80295 +80298 80301 80304 80307 80310 80313 80316 80319 80322 +80325 80328 80331 80334 80337 80340 80343 80346 80349 +80352 80355 80358 80361 80364 80367 80370 80373 80376 +80379 80382 80385 80388 80391 80394 80397 80400 80403 +80406 80409 80412 80415 80418 80421 80424 80427 80430 +80433 80436 80439 80442 80445 80448 80451 80454 80457 +80460 80463 80466 80469 80472 80475 80478 80481 80484 +80487 80490 80493 80496 80499 80502 80505 80508 80511 +80514 80517 80520 80523 80526 80529 80532 80535 80538 +80541 80544 80547 80550 80553 80556 80559 80562 80565 +80568 80571 80574 80577 80580 80583 80586 80589 80592 +80595 80598 80601 80604 80607 80610 80613 80616 80619 +80622 80625 80628 80631 80634 80637 80640 80643 80646 +80649 80652 80655 80658 80661 80664 80667 80670 80673 +80676 80679 80682 80685 80688 80691 80694 80697 80700 +80703 80706 80709 80712 80715 80718 80721 80724 80727 +80730 80733 80736 80739 80742 80745 80748 80751 80754 +80757 80760 80763 80766 80769 80772 80775 80778 80781 +80784 80787 80790 80793 80796 80799 80802 80805 80808 +80811 80814 80817 80820 80823 80826 80829 80832 80835 +80838 80841 80844 80847 80850 80853 80856 80859 80862 +80865 80868 80871 80874 80877 80880 80883 80886 80889 +80892 80895 80898 80901 80904 80907 80910 80913 80916 +80919 80922 80925 80928 80931 80934 80937 80940 80943 +80946 80949 80952 80955 80958 80961 80964 80967 80970 +80973 80976 80979 80982 80985 80988 80991 80994 80997 +81000 81003 81006 81009 81012 81015 81018 81021 81024 +81027 81030 81033 81036 81039 81042 81045 81048 81051 +81054 81057 81060 81063 81066 81069 81072 81075 81078 +81081 81084 81087 81090 81093 81096 81099 81102 81105 +81108 81111 81114 81117 81120 81123 81126 81129 81132 +81135 81138 81141 81144 81147 81150 81153 81156 81159 +81162 81165 81168 81171 81174 81177 81180 81183 81186 +81189 81192 81195 81198 81201 81204 81207 81210 81213 +81216 81219 81222 81225 81228 81231 81234 81237 81240 +81243 81246 81249 81252 81255 81258 81261 81264 81267 +81270 81273 81276 81279 81282 81285 81288 81291 81294 +81297 81300 81303 81306 81309 81312 81315 81318 81321 +81324 81327 81330 81333 81336 81339 81342 81345 81348 +81351 81354 81357 81360 81363 81366 81369 81372 81375 +81378 81381 81384 81387 81390 81393 81396 81399 81402 +81405 81408 81411 81414 81417 81420 81423 81426 81429 +81432 81435 81438 81441 81444 81447 81450 81453 81456 +81459 81462 81465 81468 81471 81474 81477 81480 81483 +81486 81489 81492 81495 81498 81501 81504 81507 81510 +81513 81516 81519 81522 81525 81528 81531 81534 81537 +81540 81543 81546 81549 81552 81555 81558 81561 81564 +81567 81570 81573 81576 81579 81582 81585 81588 81591 +81594 81597 81600 81603 81606 81609 81612 81615 81618 +81621 81624 81627 81630 81633 81636 81639 81642 81645 +81648 81651 81654 81657 81660 81663 81666 81669 81672 +81675 81678 81681 81684 81687 81690 81693 81696 81699 +81702 81705 81708 81711 81714 81717 81720 81723 81726 +81729 81732 81735 81738 81741 81744 81747 81750 81753 +81756 81759 81762 81765 81768 81771 81774 81777 81780 +81783 81786 81789 81792 81795 81798 81801 81804 81807 +81810 81813 81816 81819 81822 81825 81828 81831 81834 +81837 81840 81843 81846 81849 81852 81855 81858 81861 +81864 81867 81870 81873 81876 81879 81882 81885 81888 +81891 81894 81897 81900 81903 81906 81909 81912 81915 +81918 81921 81924 81927 81930 81933 81936 81939 81942 +81945 81948 81951 81954 81957 81960 81963 81966 81969 +81972 81975 81978 81981 81984 81987 81990 81993 81996 +81999 82002 82005 82008 82011 82014 82017 82020 82023 +82026 82029 82032 82035 82038 82041 82044 82047 82050 +82053 82056 82059 82062 82065 82068 82071 82074 82077 +82080 82083 82086 82089 82092 82095 82098 82101 82104 +82107 82110 82113 82116 82119 82122 82125 82128 82131 +82134 82137 82140 82143 82146 82149 82152 82155 82158 +82161 82164 82167 82170 82173 82176 82179 82182 82185 +82188 82191 82194 82197 82200 82203 82206 82209 82212 +82215 82218 82221 82224 82227 82230 82233 82236 82239 +82242 82245 82248 82251 82254 82257 82260 82263 82266 +82269 82272 82275 82278 82281 82284 82287 82290 82293 +82296 82299 82302 82305 82308 82311 82314 82317 82320 +82323 82326 82329 82332 82335 82338 82341 82344 82347 +82350 82353 82356 82359 82362 82365 82368 82371 82374 +82377 82380 82383 82386 82389 82392 82395 82398 82401 +82404 82407 82410 82413 82416 82419 82422 82425 82428 +82431 82434 82437 82440 82443 82446 82449 82452 82455 +82458 82461 82464 82467 82470 82473 82476 82479 82482 +82485 82488 82491 82494 82497 82500 82503 82506 82509 +82512 82515 82518 82521 82524 82527 82530 82533 82536 +82539 82542 82545 82548 82551 82554 82557 82560 82563 +82566 82569 82572 82575 82578 82581 82584 82587 82590 +82593 82596 82599 82602 82605 82608 82611 82614 82617 +82620 82623 82626 82629 82632 82635 82638 82641 82644 +82647 82650 82653 82656 82659 82662 82665 82668 82671 +82674 82677 82680 82683 82686 82689 82692 82695 82698 +82701 82704 82707 82710 82713 82716 82719 82722 82725 +82728 82731 82734 82737 82740 82743 82746 82749 82752 +82755 82758 82761 82764 82767 82770 82773 82776 82779 +82782 82785 82788 82791 82794 82797 82800 82803 82806 +82809 82812 82815 82818 82821 82824 82827 82830 82833 +82836 82839 82842 82845 82848 82851 82854 82857 82860 +82863 82866 82869 82872 82875 82878 82881 82884 82887 +82890 82893 82896 82899 82902 82905 82908 82911 82914 +82917 82920 82923 82926 82929 82932 82935 82938 82941 +82944 82947 82950 82953 82956 82959 82962 82965 82968 +82971 82974 82977 82980 82983 82986 82989 82992 82995 +82998 83001 83004 83007 83010 83013 83016 83019 83022 +83025 83028 83031 83034 83037 83040 83043 83046 83049 +83052 83055 83058 83061 83064 83067 83070 83073 83076 +83079 83082 83085 83088 83091 83094 83097 83100 83103 +83106 83109 83112 83115 83118 83121 83124 83127 83130 +83133 83136 83139 83142 83145 83148 83151 83154 83157 +83160 83163 83166 83169 83172 83175 83178 83181 83184 +83187 83190 83193 83196 83199 83202 83205 83208 83211 +83214 83217 83220 83223 83226 83229 83232 83235 83238 +83241 83244 83247 83250 83253 83256 83259 83262 83265 +83268 83271 83274 83277 83280 83283 83286 83289 83292 +83295 83298 83301 83304 83307 83310 83313 83316 83319 +83322 83325 83328 83331 83334 83337 83340 83343 83346 +83349 83352 83355 83358 83361 83364 83367 83370 83373 +83376 83379 83382 83385 83388 83391 83394 83397 83400 +83403 83406 83409 83412 83415 83418 83421 83424 83427 +83430 83433 83436 83439 83442 83445 83448 83451 83454 +83457 83460 83463 83466 83469 83472 83475 83478 83481 +83484 83487 83490 83493 83496 83499 83502 83505 83508 +83511 83514 83517 83520 83523 83526 83529 83532 83535 +83538 83541 83544 83547 83550 83553 83556 83559 83562 +83565 83568 83571 83574 83577 83580 83583 83586 83589 +83592 83595 83598 83601 83604 83607 83610 83613 83616 +83619 83622 83625 83628 83631 83634 83637 83640 83643 +83646 83649 83652 83655 83658 83661 83664 83667 83670 +83673 83676 83679 83682 83685 83688 83691 83694 83697 +83700 83703 83706 83709 83712 83715 83718 83721 83724 +83727 83730 83733 83736 83739 83742 83745 83748 83751 +83754 83757 83760 83763 83766 83769 83772 83775 83778 +83781 83784 83787 83790 83793 83796 83799 83802 83805 +83808 83811 83814 83817 83820 83823 83826 83829 83832 +83835 83838 83841 83844 83847 83850 83853 83856 83859 +83862 83865 83868 83871 83874 83877 83880 83883 83886 +83889 83892 83895 83898 83901 83904 83907 83910 83913 +83916 83919 83922 83925 83928 83931 83934 83937 83940 +83943 83946 83949 83952 83955 83958 83961 83964 83967 +83970 83973 83976 83979 83982 83985 83988 83991 83994 +83997 84000 84003 84006 84009 84012 84015 84018 84021 +84024 84027 84030 84033 84036 84039 84042 84045 84048 +84051 84054 84057 84060 84063 84066 84069 84072 84075 +84078 84081 84084 84087 84090 84093 84096 84099 84102 +84105 84108 84111 84114 84117 84120 84123 84126 84129 +84132 84135 84138 84141 84144 84147 84150 84153 84156 +84159 84162 84165 84168 84171 84174 84177 84180 84183 +84186 84189 84192 84195 84198 84201 84204 84207 84210 +84213 84216 84219 84222 84225 84228 84231 84234 84237 +84240 84243 84246 84249 84252 84255 84258 84261 84264 +84267 84270 84273 84276 84279 84282 84285 84288 84291 +84294 84297 84300 84303 84306 84309 84312 84315 84318 +84321 84324 84327 84330 84333 84336 84339 84342 84345 +84348 84351 84354 84357 84360 84363 84366 84369 84372 +84375 84378 84381 84384 84387 84390 84393 84396 84399 +84402 84405 84408 84411 84414 84417 84420 84423 84426 +84429 84432 84435 84438 84441 84444 84447 84450 84453 +84456 84459 84462 84465 84468 84471 84474 84477 84480 +84483 84486 84489 84492 84495 84498 84501 84504 84507 +84510 84513 84516 84519 84522 84525 84528 84531 84534 +84537 84540 84543 84546 84549 84552 84555 84558 84561 +84564 84567 84570 84573 84576 84579 84582 84585 84588 +84591 84594 84597 84600 84603 84606 84609 84612 84615 +84618 84621 84624 84627 84630 84633 84636 84639 84642 +84645 84648 84651 84654 84657 84660 84663 84666 84669 +84672 84675 84678 84681 84684 84687 84690 84693 84696 +84699 84702 84705 84708 84711 84714 84717 84720 84723 +84726 84729 84732 84735 84738 84741 84744 84747 84750 +84753 84756 84759 84762 84765 84768 84771 84774 84777 +84780 84783 84786 84789 84792 84795 84798 84801 84804 +84807 84810 84813 84816 84819 84822 84825 84828 84831 +84834 84837 84840 84843 84846 84849 84852 84855 84858 +84861 84864 84867 84870 84873 84876 84879 84882 84885 +84888 84891 84894 84897 84900 84903 84906 84909 84912 +84915 84918 84921 84924 84927 84930 84933 84936 84939 +84942 84945 84948 84951 84954 84957 84960 84963 84966 +84969 84972 84975 84978 84981 84984 84987 84990 84993 +84996 84999 85002 85005 85008 85011 85014 85017 85020 +85023 85026 85029 85032 85035 85038 85041 85044 85047 +85050 85053 85056 85059 85062 85065 85068 85071 85074 +85077 85080 85083 85086 85089 85092 85095 85098 85101 +85104 85107 85110 85113 85116 85119 85122 85125 85128 +85131 85134 85137 85140 85143 85146 85149 85152 85155 +85158 85161 85164 85167 85170 85173 85176 85179 85182 +85185 85188 85191 85194 85197 85200 85203 85206 85209 +85212 85215 85218 85221 85224 85227 85230 85233 85236 +85239 85242 85245 85248 85251 85254 85257 85260 85263 +85266 85269 85272 85275 85278 85281 85284 85287 85290 +85293 85296 85299 85302 85305 85308 85311 85314 85317 +85320 85323 85326 85329 85332 85335 85338 85341 85344 +85347 85350 85353 85356 85359 85362 85365 85368 85371 +85374 85377 85380 85383 85386 85389 85392 85395 85398 +85401 85404 85407 85410 85413 85416 85419 85422 85425 +85428 85431 85434 85437 85440 85443 85446 85449 85452 +85455 85458 85461 85464 85467 85470 85473 85476 85479 +85482 85485 85488 85491 85494 85497 85500 85503 85506 +85509 85512 85515 85518 85521 85524 85527 85530 85533 +85536 85539 85542 85545 85548 85551 85554 85557 85560 +85563 85566 85569 85572 85575 85578 85581 85584 85587 +85590 85593 85596 85599 85602 85605 85608 85611 85614 +85617 85620 85623 85626 85629 85632 85635 85638 85641 +85644 85647 85650 85653 85656 85659 85662 85665 85668 +85671 85674 85677 85680 85683 85686 85689 85692 85695 +85698 85701 85704 85707 85710 85713 85716 85719 85722 +85725 85728 85731 85734 85737 85740 85743 85746 85749 +85752 85755 85758 85761 85764 85767 85770 85773 85776 +85779 85782 85785 85788 85791 85794 85797 85800 85803 +85806 85809 85812 85815 85818 85821 85824 85827 85830 +85833 85836 85839 85842 85845 85848 85851 85854 85857 +85860 85863 85866 85869 85872 85875 85878 85881 85884 +85887 85890 85893 85896 85899 85902 85905 85908 85911 +85914 85917 85920 85923 85926 85929 85932 85935 85938 +85941 85944 85947 85950 85953 85956 85959 85962 85965 +85968 85971 85974 85977 85980 85983 85986 85989 85992 +85995 85998 86001 86004 86007 86010 86013 86016 86019 +86022 86025 86028 86031 86034 86037 86040 86043 86046 +86049 86052 86055 86058 86061 86064 86067 86070 86073 +86076 86079 86082 86085 86088 86091 86094 86097 86100 +86103 86106 86109 86112 86115 86118 86121 86124 86127 +86130 86133 86136 86139 86142 86145 86148 86151 86154 +86157 86160 86163 86166 86169 86172 86175 86178 86181 +86184 86187 86190 86193 86196 86199 86202 86205 86208 +86211 86214 86217 86220 86223 86226 86229 86232 86235 +86238 86241 86244 86247 86250 86253 86256 86259 86262 +86265 86268 86271 86274 86277 86280 86283 86286 86289 +86292 86295 86298 86301 86304 86307 86310 86313 86316 +86319 86322 86325 86328 86331 86334 86337 86340 86343 +86346 86349 86352 86355 86358 86361 86364 86367 86370 +86373 86376 86379 86382 86385 86388 86391 86394 86397 +86400 86403 86406 86409 86412 86415 86418 86421 86424 +86427 86430 86433 86436 86439 86442 86445 86448 86451 +86454 86457 86460 86463 86466 86469 86472 86475 86478 +86481 86484 86487 86490 86493 86496 86499 86502 86505 +86508 86511 86514 86517 86520 86523 86526 86529 86532 +86535 86538 86541 86544 86547 86550 86553 86556 86559 +86562 86565 86568 86571 86574 86577 86580 86583 86586 +86589 86592 86595 86598 86601 86604 86607 86610 86613 +86616 86619 86622 86625 86628 86631 86634 86637 86640 +86643 86646 86649 86652 86655 86658 86661 86664 86667 +86670 86673 86676 86679 86682 86685 86688 86691 86694 +86697 86700 86703 86706 86709 86712 86715 86718 86721 +86724 86727 86730 86733 86736 86739 86742 86745 86748 +86751 86754 86757 86760 86763 86766 86769 86772 86775 +86778 86781 86784 86787 86790 86793 86796 86799 86802 +86805 86808 86811 86814 86817 86820 86823 86826 86829 +86832 86835 86838 86841 86844 86847 86850 86853 86856 +86859 86862 86865 86868 86871 86874 86877 86880 86883 +86886 86889 86892 86895 86898 86901 86904 86907 86910 +86913 86916 86919 86922 86925 86928 86931 86934 86937 +86940 86943 86946 86949 86952 86955 86958 86961 86964 +86967 86970 86973 86976 86979 86982 86985 86988 86991 +86994 86997 87000 87003 87006 87009 87012 87015 87018 +87021 87024 87027 87030 87033 87036 87039 87042 87045 +87048 87051 87054 87057 87060 87063 87066 87069 87072 +87075 87078 87081 87084 87087 87090 87093 87096 87099 +87102 87105 87108 87111 87114 87117 87120 87123 87126 +87129 87132 87135 87138 87141 87144 87147 87150 87153 +87156 87159 87162 87165 87168 87171 87174 87177 87180 +87183 87186 87189 87192 87195 87198 87201 87204 87207 +87210 87213 87216 87219 87222 87225 87228 87231 87234 +87237 87240 87243 87246 87249 87252 87255 87258 87261 +87264 87267 87270 87273 87276 87279 87282 87285 87288 +87291 87294 87297 87300 87303 87306 87309 87312 87315 +87318 87321 87324 87327 87330 87333 87336 87339 87342 +87345 87348 87351 87354 87357 87360 87363 87366 87369 +87372 87375 87378 87381 87384 87387 87390 87393 87396 +87399 87402 87405 87408 87411 87414 87417 87420 87423 +87426 87429 87432 87435 87438 87441 87444 87447 87450 +87453 87456 87459 87462 87465 87468 87471 87474 87477 +87480 87483 87486 87489 87492 87495 87498 87501 87504 +87507 87510 87513 87516 87519 87522 87525 87528 87531 +87534 87537 87540 87543 87546 87549 87552 87555 87558 +87561 87564 87567 87570 87573 87576 87579 87582 87585 +87588 87591 87594 87597 87600 87603 87606 87609 87612 +87615 87618 87621 87624 87627 87630 87633 87636 87639 +87642 87645 87648 87651 87654 87657 87660 87663 87666 +87669 87672 87675 87678 87681 87684 87687 87690 87693 +87696 87699 87702 87705 87708 87711 87714 87717 87720 +87723 87726 87729 87732 87735 87738 87741 87744 87747 +87750 87753 87756 87759 87762 87765 87768 87771 87774 +87777 87780 87783 87786 87789 87792 87795 87798 87801 +87804 87807 87810 87813 87816 87819 87822 87825 87828 +87831 87834 87837 87840 87843 87846 87849 87852 87855 +87858 87861 87864 87867 87870 87873 87876 87879 87882 +87885 87888 87891 87894 87897 87900 87903 87906 87909 +87912 87915 87918 87921 87924 87927 87930 87933 87936 +87939 87942 87945 87948 87951 87954 87957 87960 87963 +87966 87969 87972 87975 87978 87981 87984 87987 87990 +87993 87996 87999 88002 88005 88008 88011 88014 88017 +88020 88023 88026 88029 88032 88035 88038 88041 88044 +88047 88050 88053 88056 88059 88062 88065 88068 88071 +88074 88077 88080 88083 88086 88089 88092 88095 88098 +88101 88104 88107 88110 88113 88116 88119 88122 88125 +88128 88131 88134 88137 88140 88143 88146 88149 88152 +88155 88158 88161 88164 88167 88170 88173 88176 88179 +88182 88185 88188 88191 88194 88197 88200 88203 88206 +88209 88212 88215 88218 88221 88224 88227 88230 88233 +88236 88239 88242 88245 88248 88251 88254 88257 88260 +88263 88266 88269 88272 88275 88278 88281 88284 88287 +88290 88293 88296 88299 88302 88305 88308 88311 88314 +88317 88320 88323 88326 88329 88332 88335 88338 88341 +88344 88347 88350 88353 88356 88359 88362 88365 88368 +88371 88374 88377 88380 88383 88386 88389 88392 88395 +88398 88401 88404 88407 88410 88413 88416 88419 88422 +88425 88428 88431 88434 88437 88440 88443 88446 88449 +88452 88455 88458 88461 88464 88467 88470 88473 88476 +88479 88482 88485 88488 88491 88494 88497 88500 88503 +88506 88509 88512 88515 88518 88521 88524 88527 88530 +88533 88536 88539 88542 88545 88548 88551 88554 88557 +88560 88563 88566 88569 88572 88575 88578 88581 88584 +88587 88590 88593 88596 88599 88602 88605 88608 88611 +88614 88617 88620 88623 88626 88629 88632 88635 88638 +88641 88644 88647 88650 88653 88656 88659 88662 88665 +88668 88671 88674 88677 88680 88683 88686 88689 88692 +88695 88698 88701 88704 88707 88710 88713 88716 88719 +88722 88725 88728 88731 88734 88737 88740 88743 88746 +88749 88752 88755 88758 88761 88764 88767 88770 88773 +88776 88779 88782 88785 88788 88791 88794 88797 88800 +88803 88806 88809 88812 88815 88818 88821 88824 88827 +88830 88833 88836 88839 88842 88845 88848 88851 88854 +88857 88860 88863 88866 88869 88872 88875 88878 88881 +88884 88887 88890 88893 88896 88899 88902 88905 88908 +88911 88914 88917 88920 88923 88926 88929 88932 88935 +88938 88941 88944 88947 88950 88953 88956 88959 88962 +88965 88968 88971 88974 88977 88980 88983 88986 88989 +88992 88995 88998 89001 89004 89007 89010 89013 89016 +89019 89022 89025 89028 89031 89034 89037 89040 89043 +89046 89049 89052 89055 89058 89061 89064 89067 89070 +89073 89076 89079 89082 89085 89088 89091 89094 89097 +89100 89103 89106 89109 89112 89115 89118 89121 89124 +89127 89130 89133 89136 89139 89142 89145 89148 89151 +89154 89157 89160 89163 89166 89169 89172 89175 89178 +89181 89184 89187 89190 89193 89196 89199 89202 89205 +89208 89211 89214 89217 89220 89223 89226 89229 89232 +89235 89238 89241 89244 89247 89250 89253 89256 89259 +89262 89265 89268 89271 89274 89277 89280 89283 89286 +89289 89292 89295 89298 89301 89304 89307 89310 89313 +89316 89319 89322 89325 89328 89331 89334 89337 89340 +89343 89346 89349 89352 89355 89358 89361 89364 89367 +89370 89373 89376 89379 89382 89385 89388 89391 89394 +89397 89400 89403 89406 89409 89412 89415 89418 89421 +89424 89427 89430 89433 89436 89439 89442 89445 89448 +89451 89454 89457 89460 89463 89466 89469 89472 89475 +89478 89481 89484 89487 89490 89493 89496 89499 89502 +89505 89508 89511 89514 89517 89520 89523 89526 89529 +89532 89535 89538 89541 89544 89547 89550 89553 89556 +89559 89562 89565 89568 89571 89574 89577 89580 89583 +89586 89589 89592 89595 89598 89601 89604 89607 89610 +89613 89616 89619 89622 89625 89628 89631 89634 89637 +89640 89643 89646 89649 89652 89655 89658 89661 89664 +89667 89670 89673 89676 89679 89682 89685 89688 89691 +89694 89697 89700 89703 89706 89709 89712 89715 89718 +89721 89724 89727 89730 89733 89736 89739 89742 89745 +89748 89751 89754 89757 89760 89763 89766 89769 89772 +89775 89778 89781 89784 89787 89790 89793 89796 89799 +89802 89805 89808 89811 89814 89817 89820 89823 89826 +89829 89832 89835 89838 89841 89844 89847 89850 89853 +89856 89859 89862 89865 89868 89871 89874 89877 89880 +89883 89886 89889 89892 89895 89898 89901 89904 89907 +89910 89913 89916 89919 89922 89925 89928 89931 89934 +89937 89940 89943 89946 89949 89952 89955 89958 89961 +89964 89967 89970 89973 89976 89979 89982 89985 89988 +89991 89994 89997 90000 90003 90006 90009 90012 90015 +90018 90021 90024 90027 90030 90033 90036 90039 90042 +90045 90048 90051 90054 90057 90060 90063 90066 90069 +90072 90075 90078 90081 90084 90087 90090 90093 90096 +90099 90102 90105 90108 90111 90114 90117 90120 90123 +90126 90129 90132 90135 90138 90141 90144 90147 90150 +90153 90156 90159 90162 90165 90168 90171 90174 90177 +90180 90183 90186 90189 90192 90195 90198 90201 90204 +90207 90210 90213 90216 90219 90222 90225 90228 90231 +90234 90237 90240 90243 90246 90249 90252 90255 90258 +90261 90264 90267 90270 90273 90276 90279 90282 90285 +90288 90291 90294 90297 90300 90303 90306 90309 90312 +90315 90318 90321 90324 90327 90330 90333 90336 90339 +90342 90345 90348 90351 90354 90357 90360 90363 90366 +90369 90372 90375 90378 90381 90384 90387 90390 90393 +90396 90399 90402 90405 90408 90411 90414 90417 90420 +90423 90426 90429 90432 90435 90438 90441 90444 90447 +90450 90453 90456 90459 90462 90465 90468 90471 90474 +90477 90480 90483 90486 90489 90492 90495 90498 90501 +90504 90507 90510 90513 90516 90519 90522 90525 90528 +90531 90534 90537 90540 90543 90546 90549 90552 90555 +90558 90561 90564 90567 90570 90573 90576 90579 90582 +90585 90588 90591 90594 90597 90600 90603 90606 90609 +90612 90615 90618 90621 90624 90627 90630 90633 90636 +90639 90642 90645 90648 90651 90654 90657 90660 90663 +90666 90669 90672 90675 90678 90681 90684 90687 90690 +90693 90696 90699 90702 90705 90708 90711 90714 90717 +90720 90723 90726 90729 90732 90735 90738 90741 90744 +90747 90750 90753 90756 90759 90762 90765 90768 90771 +90774 90777 90780 90783 90786 90789 90792 90795 90798 +90801 90804 90807 90810 90813 90816 90819 90822 90825 +90828 90831 90834 90837 90840 90843 90846 90849 90852 +90855 90858 90861 90864 90867 90870 90873 90876 90879 +90882 90885 90888 90891 90894 90897 90900 90903 90906 +90909 90912 90915 90918 90921 90924 90927 90930 90933 +90936 90939 90942 90945 90948 90951 90954 90957 90960 +90963 90966 90969 90972 90975 90978 90981 90984 90987 +90990 90993 90996 90999 91002 91005 91008 91011 91014 +91017 91020 91023 91026 91029 91032 91035 91038 91041 +91044 91047 91050 91053 91056 91059 91062 91065 91068 +91071 91074 91077 91080 91083 91086 91089 91092 91095 +91098 91101 91104 91107 91110 91113 91116 91119 91122 +91125 91128 91131 91134 91137 91140 91143 91146 91149 +91152 91155 91158 91161 91164 91167 91170 91173 91176 +91179 91182 91185 91188 91191 91194 91197 91200 91203 +91206 91209 91212 91215 91218 91221 91224 91227 91230 +91233 91236 91239 91242 91245 91248 91251 91254 91257 +91260 91263 91266 91269 91272 91275 91278 91281 91284 +91287 91290 91293 91296 91299 91302 91305 91308 91311 +91314 91317 91320 91323 91326 91329 91332 91335 91338 +91341 91344 91347 91350 91353 91356 91359 91362 91365 +91368 91371 91374 91377 91380 91383 91386 91389 91392 +91395 91398 91401 91404 91407 91410 91413 91416 91419 +91422 91425 91428 91431 91434 91437 91440 91443 91446 +91449 91452 91455 91458 91461 91464 91467 91470 91473 +91476 91479 91482 91485 91488 91491 91494 91497 91500 +91503 91506 91509 91512 91515 91518 91521 91524 91527 +91530 91533 91536 91539 91542 91545 91548 91551 91554 +91557 91560 91563 91566 91569 91572 91575 91578 91581 +91584 91587 91590 91593 91596 91599 91602 91605 91608 +91611 91614 91617 91620 91623 91626 91629 91632 91635 +91638 91641 91644 91647 91650 91653 91656 91659 91662 +91665 91668 91671 91674 91677 91680 91683 91686 91689 +91692 91695 91698 91701 91704 91707 91710 91713 91716 +91719 91722 91725 91728 91731 91734 91737 91740 91743 +91746 91749 91752 91755 91758 91761 91764 91767 91770 +91773 91776 91779 91782 91785 91788 91791 91794 91797 +91800 91803 91806 91809 91812 91815 91818 91821 91824 +91827 91830 91833 91836 91839 91842 91845 91848 91851 +91854 91857 91860 91863 91866 91869 91872 91875 91878 +91881 91884 91887 91890 91893 91896 91899 91902 91905 +91908 91911 91914 91917 91920 91923 91926 91929 91932 +91935 91938 91941 91944 91947 91950 91953 91956 91959 +91962 91965 91968 91971 91974 91977 91980 91983 91986 +91989 91992 91995 91998 92001 92004 92007 92010 92013 +92016 92019 92022 92025 92028 92031 92034 92037 92040 +92043 92046 92049 92052 92055 92058 92061 92064 92067 +92070 92073 92076 92079 92082 92085 92088 92091 92094 +92097 92100 92103 92106 92109 92112 92115 92118 92121 +92124 92127 92130 92133 92136 92139 92142 92145 92148 +92151 92154 92157 92160 92163 92166 92169 92172 92175 +92178 92181 92184 92187 92190 92193 92196 92199 92202 +92205 92208 92211 92214 92217 92220 92223 92226 92229 +92232 92235 92238 92241 92244 92247 92250 92253 92256 +92259 92262 92265 92268 92271 92274 92277 92280 92283 +92286 92289 92292 92295 92298 92301 92304 92307 92310 +92313 92316 92319 92322 92325 92328 92331 92334 92337 +92340 92343 92346 92349 92352 92355 92358 92361 92364 +92367 92370 92373 92376 92379 92382 92385 92388 92391 +92394 92397 92400 92403 92406 92409 92412 92415 92418 +92421 92424 92427 92430 92433 92436 92439 92442 92445 +92448 92451 92454 92457 92460 92463 92466 92469 92472 +92475 92478 92481 92484 92487 92490 92493 92496 92499 +92502 92505 92508 92511 92514 92517 92520 92523 92526 +92529 92532 92535 92538 92541 92544 92547 92550 92553 +92556 92559 92562 92565 92568 92571 92574 92577 92580 +92583 92586 92589 92592 92595 92598 92601 92604 92607 +92610 92613 92616 92619 92622 92625 92628 92631 92634 +92637 92640 92643 92646 92649 92652 92655 92658 92661 +92664 92667 92670 92673 92676 92679 92682 92685 92688 +92691 92694 92697 92700 92703 92706 92709 92712 92715 +92718 92721 92724 92727 92730 92733 92736 92739 92742 +92745 92748 92751 92754 92757 92760 92763 92766 92769 +92772 92775 92778 92781 92784 92787 92790 92793 92796 +92799 92802 92805 92808 92811 92814 92817 92820 92823 +92826 92829 92832 92835 92838 92841 92844 92847 92850 +92853 92856 92859 92862 92865 92868 92871 92874 92877 +92880 92883 92886 92889 92892 92895 92898 92901 92904 +92907 92910 92913 92916 92919 92922 92925 92928 92931 +92934 92937 92940 92943 92946 92949 92952 92955 92958 +92961 92964 92967 92970 92973 92976 92979 92982 92985 +92988 92991 92994 92997 93000 93003 93006 93009 93012 +93015 93018 93021 93024 93027 93030 93033 93036 93039 +93042 93045 93048 93051 93054 93057 93060 93063 93066 +93069 93072 93075 93078 93081 93084 93087 93090 93093 +93096 93099 93102 93105 93108 93111 93114 93117 93120 +93123 93126 93129 93132 93135 93138 93141 93144 93147 +93150 93153 93156 93159 93162 93165 93168 93171 93174 +93177 93180 93183 93186 93189 93192 93195 93198 93201 +93204 93207 93210 93213 93216 93219 93222 93225 93228 +93231 93234 93237 93240 93243 93246 93249 93252 93255 +93258 93261 93264 93267 93270 93273 93276 93279 93282 +93285 93288 93291 93294 93297 93300 93303 93306 93309 +93312 93315 93318 93321 93324 93327 93330 93333 93336 +93339 93342 93345 93348 93351 93354 93357 93360 93363 +93366 93369 93372 93375 93378 93381 93384 93387 93390 +93393 93396 93399 93402 93405 93408 93411 93414 93417 +93420 93423 93426 93429 93432 93435 93438 93441 93444 +93447 93450 93453 93456 93459 93462 93465 93468 93471 +93474 93477 93480 93483 93486 93489 93492 93495 93498 +93501 93504 93507 93510 93513 93516 93519 93522 93525 +93528 93531 93534 93537 93540 93543 93546 93549 93552 +93555 93558 93561 93564 93567 93570 93573 93576 93579 +93582 93585 93588 93591 93594 93597 93600 93603 93606 +93609 93612 93615 93618 93621 93624 93627 93630 93633 +93636 93639 93642 93645 93648 93651 93654 93657 93660 +93663 93666 93669 93672 93675 93678 93681 93684 93687 +93690 93693 93696 93699 93702 93705 93708 93711 93714 +93717 93720 93723 93726 93729 93732 93735 93738 93741 +93744 93747 93750 93753 93756 93759 93762 93765 93768 +93771 93774 93777 93780 93783 93786 93789 93792 93795 +93798 93801 93804 93807 93810 93813 93816 93819 93822 +93825 93828 93831 93834 93837 93840 93843 93846 93849 +93852 93855 93858 93861 93864 93867 93870 93873 93876 +93879 93882 93885 93888 93891 93894 93897 93900 93903 +93906 93909 93912 93915 93918 93921 93924 93927 93930 +93933 93936 93939 93942 93945 93948 93951 93954 93957 +93960 93963 93966 93969 93972 93975 93978 93981 93984 +93987 93990 93993 93996 93999 94002 94005 94008 94011 +94014 94017 94020 94023 94026 94029 94032 94035 94038 +94041 94044 94047 94050 94053 94056 94059 94062 94065 +94068 94071 94074 94077 94080 94083 94086 94089 94092 +94095 94098 94101 94104 94107 94110 94113 94116 94119 +94122 94125 94128 94131 94134 94137 94140 94143 94146 +94149 94152 94155 94158 94161 94164 94167 94170 94173 +94176 94179 94182 94185 94188 94191 94194 94197 94200 +94203 94206 94209 94212 94215 94218 94221 94224 94227 +94230 94233 94236 94239 94242 94245 94248 94251 94254 +94257 94260 94263 94266 94269 94272 94275 94278 94281 +94284 94287 94290 94293 94296 94299 94302 94305 94308 +94311 94314 94317 94320 94323 94326 94329 94332 94335 +94338 94341 94344 94347 94350 94353 94356 94359 94362 +94365 94368 94371 94374 94377 94380 94383 94386 94389 +94392 94395 94398 94401 94404 94407 94410 94413 94416 +94419 94422 94425 94428 94431 94434 94437 94440 94443 +94446 94449 94452 94455 94458 94461 94464 94467 94470 +94473 94476 94479 94482 94485 94488 94491 94494 94497 +94500 94503 94506 94509 94512 94515 94518 94521 94524 +94527 94530 94533 94536 94539 94542 94545 94548 94551 +94554 94557 94560 94563 94566 94569 94572 94575 94578 +94581 94584 94587 94590 94593 94596 94599 94602 94605 +94608 94611 94614 94617 94620 94623 94626 94629 94632 +94635 94638 94641 94644 94647 94650 94653 94656 94659 +94662 94665 94668 94671 94674 94677 94680 94683 94686 +94689 94692 94695 94698 94701 94704 94707 94710 94713 +94716 94719 94722 94725 94728 94731 94734 94737 94740 +94743 94746 94749 94752 94755 94758 94761 94764 94767 +94770 94773 94776 94779 94782 94785 94788 94791 94794 +94797 94800 94803 94806 94809 94812 94815 94818 94821 +94824 94827 94830 94833 94836 94839 94842 94845 94848 +94851 94854 94857 94860 94863 94866 94869 94872 94875 +94878 94881 94884 94887 94890 94893 94896 94899 94902 +94905 94908 94911 94914 94917 94920 94923 94926 94929 +94932 94935 94938 94941 94944 94947 94950 94953 94956 +94959 94962 94965 94968 94971 94974 94977 94980 94983 +94986 94989 94992 94995 94998 95001 95004 95007 95010 +95013 95016 95019 95022 95025 95028 95031 95034 95037 +95040 95043 95046 95049 95052 95055 95058 95061 95064 +95067 95070 95073 95076 95079 95082 95085 95088 95091 +95094 95097 95100 95103 95106 95109 95112 95115 95118 +95121 95124 95127 95130 95133 95136 95139 95142 95145 +95148 95151 95154 95157 95160 95163 95166 95169 95172 +95175 95178 95181 95184 95187 95190 95193 95196 95199 +95202 95205 95208 95211 95214 95217 95220 95223 95226 +95229 95232 95235 95238 95241 95244 95247 95250 95253 +95256 95259 95262 95265 95268 95271 95274 95277 95280 +95283 95286 95289 95292 95295 95298 95301 95304 95307 +95310 95313 95316 95319 95322 95325 95328 95331 95334 +95337 95340 95343 95346 95349 95352 95355 95358 95361 +95364 95367 95370 95373 95376 95379 95382 95385 95388 +95391 95394 95397 95400 95403 95406 95409 95412 95415 +95418 95421 95424 95427 95430 95433 95436 95439 95442 +95445 95448 95451 95454 95457 95460 95463 95466 95469 +95472 95475 95478 95481 95484 95487 95490 95493 95496 +95499 95502 95505 95508 95511 95514 95517 95520 95523 +95526 95529 95532 95535 95538 95541 95544 95547 95550 +95553 95556 95559 95562 95565 95568 95571 95574 95577 +95580 95583 95586 95589 95592 95595 95598 95601 95604 +95607 95610 95613 95616 95619 95622 95625 95628 95631 +95634 95637 95640 95643 95646 95649 95652 95655 95658 +95661 95664 95667 95670 95673 95676 95679 95682 95685 +95688 95691 95694 95697 95700 95703 95706 95709 95712 +95715 95718 95721 95724 95727 95730 95733 95736 95739 +95742 95745 95748 95751 95754 95757 95760 95763 95766 +95769 95772 95775 95778 95781 95784 95787 95790 95793 +95796 95799 95802 95805 95808 95811 95814 95817 95820 +95823 95826 95829 95832 95835 95838 95841 95844 95847 +95850 95853 95856 95859 95862 95865 95868 95871 95874 +95877 95880 95883 95886 95889 95892 95895 95898 95901 +95904 95907 95910 95913 95916 95919 95922 95925 95928 +95931 95934 95937 95940 95943 95946 95949 95952 95955 +95958 95961 95964 95967 95970 95973 95976 95979 95982 +95985 95988 95991 95994 95997 96000 96003 96006 96009 +96012 96015 96018 96021 96024 96027 96030 96033 96036 +96039 96042 96045 96048 96051 96054 96057 96060 96063 +96066 96069 96072 96075 96078 96081 96084 96087 96090 +96093 96096 96099 96102 96105 96108 96111 96114 96117 +96120 96123 96126 96129 96132 96135 96138 96141 96144 +96147 96150 96153 96156 96159 96162 96165 96168 96171 +96174 96177 96180 96183 96186 96189 96192 96195 96198 +96201 96204 96207 96210 96213 96216 96219 96222 96225 +96228 96231 96234 96237 96240 96243 96246 96249 96252 +96255 96258 96261 96264 96267 96270 96273 96276 96279 +96282 96285 96288 96291 96294 96297 96300 96303 96306 +96309 96312 96315 96318 96321 96324 96327 96330 96333 +96336 96339 96342 96345 96348 96351 96354 96357 96360 +96363 96366 96369 96372 96375 96378 96381 96384 96387 +96390 96393 96396 96399 96402 96405 96408 96411 96414 +96417 96420 96423 96426 96429 96432 96435 96438 96441 +96444 96447 96450 96453 96456 96459 96462 96465 96468 +96471 96474 96477 96480 96483 96486 96489 96492 96495 +96498 96501 96504 96507 96510 96513 96516 96519 96522 +96525 96528 96531 96534 96537 96540 96543 96546 96549 +96552 96555 96558 96561 96564 96567 96570 96573 96576 +96579 96582 96585 96588 96591 96594 96597 96600 96603 +96606 96609 96612 96615 96618 96621 96624 96627 96630 +96633 96636 96639 96642 96645 96648 96651 96654 96657 +96660 96663 96666 96669 96672 96675 96678 96681 96684 +96687 96690 96693 96696 96699 96702 96705 96708 96711 +96714 96717 96720 96723 96726 96729 96732 96735 96738 +96741 96744 96747 96750 96753 96756 96759 96762 96765 +96768 96771 96774 96777 96780 96783 96786 96789 96792 +96795 96798 96801 96804 96807 96810 96813 96816 96819 +96822 96825 96828 96831 96834 96837 96840 96843 96846 +96849 96852 96855 96858 96861 96864 96867 96870 96873 +96876 96879 96882 96885 96888 96891 96894 96897 96900 +96903 96906 96909 96912 96915 96918 96921 96924 96927 +96930 96933 96936 96939 96942 96945 96948 96951 96954 +96957 96960 96963 96966 96969 96972 96975 96978 96981 +96984 96987 96990 96993 96996 96999 97002 97005 97008 +97011 97014 97017 97020 97023 97026 97029 97032 97035 +97038 97041 97044 97047 97050 97053 97056 97059 97062 +97065 97068 97071 97074 97077 97080 97083 97086 97089 +97092 97095 97098 97101 97104 97107 97110 97113 97116 +97119 97122 97125 97128 97131 97134 97137 97140 97143 +97146 97149 97152 97155 97158 97161 97164 97167 97170 +97173 97176 97179 97182 97185 97188 97191 97194 97197 +97200 97203 97206 97209 97212 97215 97218 97221 97224 +97227 97230 97233 97236 97239 97242 97245 97248 97251 +97254 97257 97260 97263 97266 97269 97272 97275 97278 +97281 97284 97287 97290 97293 97296 97299 97302 97305 +97308 97311 97314 97317 97320 97323 97326 97329 97332 +97335 97338 97341 97344 97347 97350 97353 97356 97359 +97362 97365 97368 97371 97374 97377 97380 97383 97386 +97389 97392 97395 97398 97401 97404 97407 97410 97413 +97416 97419 97422 97425 97428 97431 97434 97437 97440 +97443 97446 97449 97452 97455 97458 97461 97464 97467 +97470 97473 97476 97479 97482 97485 97488 97491 97494 +97497 97500 97503 97506 97509 97512 97515 97518 97521 +97524 97527 97530 97533 97536 97539 97542 97545 97548 +97551 97554 97557 97560 97563 97566 97569 97572 97575 +97578 97581 97584 97587 97590 97593 97596 97599 97602 +97605 97608 97611 97614 97617 97620 97623 97626 97629 +97632 97635 97638 97641 97644 97647 97650 97653 97656 +97659 97662 97665 97668 97671 97674 97677 97680 97683 +97686 97689 97692 97695 97698 97701 97704 97707 97710 +97713 97716 97719 97722 97725 97728 97731 97734 97737 +97740 97743 97746 97749 97752 97755 97758 97761 97764 +97767 97770 97773 97776 97779 97782 97785 97788 97791 +97794 97797 97800 97803 97806 97809 97812 97815 97818 +97821 97824 97827 97830 97833 97836 97839 97842 97845 +97848 97851 97854 97857 97860 97863 97866 97869 97872 +97875 97878 97881 97884 97887 97890 97893 97896 97899 +97902 97905 97908 97911 97914 97917 97920 97923 97926 +97929 97932 97935 97938 97941 97944 97947 97950 97953 +97956 97959 97962 97965 97968 97971 97974 97977 97980 +97983 97986 97989 97992 97995 97998 98001 98004 98007 +98010 98013 98016 98019 98022 98025 98028 98031 98034 +98037 98040 98043 98046 98049 98052 98055 98058 98061 +98064 98067 98070 98073 98076 98079 98082 98085 98088 +98091 98094 98097 98100 98103 98106 98109 98112 98115 +98118 98121 98124 98127 98130 98133 98136 98139 98142 +98145 98148 98151 98154 98157 98160 98163 98166 98169 +98172 98175 98178 98181 98184 98187 98190 98193 98196 +98199 98202 98205 98208 98211 98214 98217 98220 98223 +98226 98229 98232 98235 98238 98241 98244 98247 98250 +98253 98256 98259 98262 98265 98268 98271 98274 98277 +98280 98283 98286 98289 98292 98295 98298 98301 98304 +98307 98310 98313 98316 98319 98322 98325 98328 98331 +98334 98337 98340 98343 98346 98349 98352 98355 98358 +98361 98364 98367 98370 98373 98376 98379 98382 98385 +98388 98391 98394 98397 98400 98403 98406 98409 98412 +98415 98418 98421 98424 98427 98430 98433 98436 98439 +98442 98445 98448 98451 98454 98457 98460 98463 98466 +98469 98472 98475 98478 98481 98484 98487 98490 98493 +98496 98499 98502 98505 98508 98511 98514 98517 98520 +98523 98526 98529 98532 98535 98538 98541 98544 98547 +98550 98553 98556 98559 98562 98565 98568 98571 98574 +98577 98580 98583 98586 98589 98592 98595 98598 98601 +98604 98607 98610 98613 98616 98619 98622 98625 98628 +98631 98634 98637 98640 98643 98646 98649 98652 98655 +98658 98661 98664 98667 98670 98673 98676 98679 98682 +98685 98688 98691 98694 98697 98700 98703 98706 98709 +98712 98715 98718 98721 98724 98727 98730 98733 98736 +98739 98742 98745 98748 98751 98754 98757 98760 98763 +98766 98769 98772 98775 98778 98781 98784 98787 98790 +98793 98796 98799 98802 98805 98808 98811 98814 98817 +98820 98823 98826 98829 98832 98835 98838 98841 98844 +98847 98850 98853 98856 98859 98862 98865 98868 98871 +98874 98877 98880 98883 98886 98889 98892 98895 98898 +98901 98904 98907 98910 98913 98916 98919 98922 98925 +98928 98931 98934 98937 98940 98943 98946 98949 98952 +98955 98958 98961 98964 98967 98970 98973 98976 98979 +98982 98985 98988 98991 98994 98997 99000 99003 99006 +99009 99012 99015 99018 99021 99024 99027 99030 99033 +99036 99039 99042 99045 99048 99051 99054 99057 99060 +99063 99066 99069 99072 99075 99078 99081 99084 99087 +99090 99093 99096 99099 99102 99105 99108 99111 99114 +99117 99120 99123 99126 99129 99132 99135 99138 99141 +99144 99147 99150 99153 99156 99159 99162 99165 99168 +99171 99174 99177 99180 99183 99186 99189 99192 99195 +99198 99201 99204 99207 99210 99213 99216 99219 99222 +99225 99228 99231 99234 99237 99240 99243 99246 99249 +99252 99255 99258 99261 99264 99267 99270 99273 99276 +99279 99282 99285 99288 99291 99294 99297 99300 99303 +99306 99309 99312 99315 99318 99321 99324 99327 99330 +99333 99336 99339 99342 99345 99348 99351 99354 99357 +99360 99363 99366 99369 99372 99375 99378 99381 99384 +99387 99390 99393 99396 99399 99402 99405 99408 99411 +99414 99417 99420 99423 99426 99429 99432 99435 99438 +99441 99444 99447 99450 99453 99456 99459 99462 99465 +99468 99471 99474 99477 99480 99483 99486 99489 99492 +99495 99498 99501 99504 99507 99510 99513 99516 99519 +99522 99525 99528 99531 99534 99537 99540 99543 99546 +99549 99552 99555 99558 99561 99564 99567 99570 99573 +99576 99579 99582 99585 99588 99591 99594 99597 99600 +99603 99606 99609 99612 99615 99618 99621 99624 99627 +99630 99633 99636 99639 99642 99645 99648 99651 99654 +99657 99660 99663 99666 99669 99672 99675 99678 99681 +99684 99687 99690 99693 99696 99699 99702 99705 99708 +99711 99714 99717 99720 99723 99726 99729 99732 99735 +99738 99741 99744 99747 99750 99753 99756 99759 99762 +99765 99768 99771 99774 99777 99780 99783 99786 99789 +99792 99795 99798 99801 99804 99807 99810 99813 99816 +99819 99822 99825 99828 99831 99834 99837 99840 99843 +99846 99849 99852 99855 99858 99861 99864 99867 99870 +99873 99876 99879 99882 99885 99888 99891 99894 99897 +99900 99903 99906 99909 99912 99915 99918 99921 99924 +99927 99930 99933 99936 99939 99942 99945 99948 99951 +99954 99957 99960 99963 99966 99969 99972 99975 99978 +99981 99984 99987 99990 99993 99996 99999 100002 100005 +100008 100011 100014 100017 100020 100023 100026 100029 100032 +100035 100038 100041 100044 100047 100050 100053 100056 100059 +100062 100065 100068 100071 100074 100077 100080 100083 100086 +100089 100092 100095 100098 100101 100104 100107 100110 100113 +100116 100119 100122 100125 100128 100131 100134 100137 100140 +100143 100146 100149 100152 100155 100158 100161 100164 100167 +100170 100173 100176 100179 100182 100185 100188 100191 100194 +100197 100200 100203 100206 100209 100212 100215 100218 100221 +100224 100227 100230 100233 100236 100239 100242 100245 100248 +100251 100254 100257 100260 100263 100266 100269 100272 100275 +100278 100281 100284 100287 100290 100293 100296 100299 100302 +100305 100308 100311 100314 100317 100320 100323 100326 100329 +100332 100335 100338 100341 100344 100347 100350 100353 100356 +100359 100362 100365 100368 100371 100374 100377 100380 100383 +100386 100389 100392 100395 100398 100401 100404 100407 100410 +100413 100416 100419 100422 100425 100428 100431 100434 100437 +100440 100443 100446 100449 100452 100455 100458 100461 100464 +100467 100470 100473 100476 100479 100482 100485 100488 100491 +100494 100497 100500 100503 100506 100509 100512 100515 100518 +100521 100524 100527 100530 100533 100536 100539 100542 100545 +100548 100551 100554 100557 100560 100563 100566 100569 100572 +100575 100578 100581 100584 100587 100590 100593 100596 100599 +100602 100605 100608 100611 100614 100617 100620 100623 100626 +100629 100632 100635 100638 100641 100644 100647 100650 100653 +100656 100659 100662 100665 100668 100671 100674 100677 100680 +100683 100686 100689 100692 100695 100698 100701 100704 100707 +100710 100713 100716 100719 100722 100725 100728 100731 100734 +100737 100740 100743 100746 100749 100752 100755 100758 100761 +100764 100767 100770 100773 100776 100779 100782 100785 100788 +100791 100794 100797 100800 100803 100806 100809 100812 100815 +100818 100821 100824 100827 100830 100833 100836 100839 100842 +100845 100848 100851 100854 100857 100860 100863 100866 100869 +100872 100875 100878 100881 100884 100887 100890 100893 100896 +100899 100902 100905 100908 100911 100914 100917 100920 100923 +100926 100929 100932 100935 100938 100941 100944 100947 100950 +100953 100956 100959 100962 100965 100968 100971 100974 100977 +100980 100983 100986 100989 100992 100995 100998 101001 101004 +101007 101010 101013 101016 101019 101022 101025 101028 101031 +101034 101037 101040 101043 101046 101049 101052 101055 101058 +101061 101064 101067 101070 101073 101076 101079 101082 101085 +101088 101091 101094 101097 101100 101103 101106 101109 101112 +101115 101118 101121 101124 101127 101130 101133 101136 101139 +101142 101145 101148 101151 101154 101157 101160 101163 101166 +101169 101172 101175 101178 101181 101184 101187 101190 101193 +101196 101199 101202 101205 101208 101211 101214 101217 101220 +101223 101226 101229 101232 101235 101238 101241 101244 101247 +101250 101253 101256 101259 101262 101265 101268 101271 101274 +101277 101280 101283 101286 101289 101292 101295 101298 101301 +101304 101307 101310 101313 101316 101319 101322 101325 101328 +101331 101334 101337 101340 101343 101346 101349 101352 101355 +101358 101361 101364 101367 101370 101373 101376 101379 101382 +101385 101388 101391 101394 101397 101400 101403 101406 101409 +101412 101415 101418 101421 101424 101427 101430 101433 101436 +101439 101442 101445 101448 101451 101454 101457 101460 101463 +101466 101469 101472 101475 101478 101481 101484 101487 101490 +101493 101496 101499 101502 101505 101508 101511 101514 101517 +101520 101523 101526 101529 101532 101535 101538 101541 101544 +101547 101550 101553 101556 101559 101562 101565 101568 101571 +101574 101577 101580 101583 101586 101589 101592 101595 101598 +101601 101604 101607 101610 101613 101616 101619 101622 101625 +101628 101631 101634 101637 101640 101643 101646 101649 101652 +101655 101658 101661 101664 101667 101670 101673 101676 101679 +101682 101685 101688 101691 101694 101697 101700 101703 101706 +101709 101712 101715 101718 101721 101724 101727 101730 101733 +101736 101739 101742 101745 101748 101751 101754 101757 101760 +101763 101766 101769 101772 101775 101778 101781 101784 101787 +101790 101793 101796 101799 101802 101805 101808 101811 101814 +101817 101820 101823 101826 101829 101832 101835 101838 101841 +101844 101847 101850 101853 101856 101859 101862 101865 101868 +101871 101874 101877 101880 101883 101886 101889 101892 101895 +101898 101901 101904 101907 101910 101913 101916 101919 101922 +101925 101928 101931 101934 101937 101940 101943 101946 101949 +101952 101955 101958 101961 101964 101967 101970 101973 101976 +101979 101982 101985 101988 101991 101994 101997 102000 102003 +102006 102009 102012 102015 102018 102021 102024 102027 102030 +102033 102036 102039 102042 102045 102048 102051 102054 102057 +102060 102063 102066 102069 102072 102075 102078 102081 102084 +102087 102090 102093 102096 102099 102102 102105 102108 102111 +102114 102117 102120 102123 102126 102129 102132 102135 102138 +102141 102144 102147 102150 102153 102156 102159 102162 102165 +102168 102171 102174 102177 102180 102183 102186 102189 102192 +102195 102198 102201 102204 102207 102210 102213 102216 102219 +102222 102225 102228 102231 102234 102237 102240 102243 102246 +102249 102252 102255 102258 102261 102264 102267 102270 102273 +102276 102279 102282 102285 102288 102291 102294 102297 102300 +102303 102306 102309 102312 102315 102318 102321 102324 102327 +102330 102333 102336 102339 102342 102345 102348 102351 102354 +102357 102360 102363 102366 102369 102372 102375 102378 102381 +102384 102387 102390 102393 102396 102399 102402 102405 102408 +102411 102414 102417 102420 102423 102426 102429 102432 102435 +102438 102441 102444 102447 102450 102453 102456 102459 102462 +102465 102468 102471 102474 102477 102480 102483 102486 102489 +102492 102495 102498 102501 102504 102507 102510 102513 102516 +102519 102522 102525 102528 102531 102534 102537 102540 102543 +102546 102549 102552 102555 102558 102561 102564 102567 102570 +102573 102576 102579 102582 102585 102588 102591 102594 102597 +102600 102603 102606 102609 102612 102615 102618 102621 102624 +102627 102630 102633 102636 102639 102642 102645 102648 102651 +102654 102657 102660 102663 102666 102669 102672 102675 102678 +102681 102684 102687 102690 102693 102696 102699 102702 102705 +102708 102711 102714 102717 102720 102723 102726 102729 102732 +102735 102738 102741 102744 102747 102750 102753 102756 102759 +102762 102765 102768 102771 102774 102777 102780 102783 102786 +102789 102792 102795 102798 102801 102804 102807 102810 102813 +102816 102819 102822 102825 102828 102831 102834 102837 102840 +102843 102846 102849 102852 102855 102858 102861 102864 102867 +102870 102873 102876 102879 102882 102885 102888 102891 102894 +102897 102900 102903 102906 102909 102912 102915 102918 102921 +102924 102927 102930 102933 102936 102939 102942 102945 102948 +102951 102954 102957 102960 102963 102966 102969 102972 102975 +102978 102981 102984 102987 102990 102993 102996 102999 103002 +103005 103008 103011 103014 103017 103020 103023 103026 103029 +103032 103035 103038 103041 103044 103047 103050 103053 103056 +103059 103062 103065 103068 103071 103074 103077 103080 103083 +103086 103089 103092 103095 103098 103101 103104 103107 103110 +103113 103116 103119 103122 103125 103128 103131 103134 103137 +103140 103143 103146 103149 103152 103155 103158 103161 103164 +103167 103170 103173 103176 103179 103182 103185 103188 103191 +103194 103197 103200 103203 103206 103209 103212 103215 103218 +103221 103224 103227 103230 103233 103236 103239 103242 103245 +103248 103251 103254 103257 103260 103263 103266 103269 103272 +103275 103278 103281 103284 103287 103290 103293 103296 103299 +103302 103305 103308 103311 103314 103317 103320 103323 103326 +103329 103332 103335 103338 103341 103344 103347 103350 103353 +103356 103359 103362 103365 103368 103371 103374 103377 103380 +103383 103386 103389 103392 103395 103398 103401 103404 103407 +103410 103413 103416 103419 103422 103425 103428 103431 103434 +103437 103440 103443 103446 103449 103452 103455 103458 103461 +103464 103467 103470 103473 103476 103479 103482 103485 103488 +103491 103494 103497 103500 103503 103506 103509 103512 103515 +103518 103521 103524 103527 103530 103533 103536 103539 103542 +103545 103548 103551 103554 103557 103560 103563 103566 103569 +103572 103575 103578 103581 103584 103587 103590 103593 103596 +103599 103602 103605 103608 103611 103614 103617 103620 103623 +103626 103629 103632 103635 103638 103641 103644 103647 103650 +103653 103656 103659 103662 103665 103668 103671 103674 103677 +103680 103683 103686 103689 103692 103695 103698 103701 103704 +103707 103710 103713 103716 103719 103722 103725 103728 103731 +103734 103737 103740 103743 103746 103749 103752 103755 103758 +103761 103764 103767 103770 103773 103776 103779 103782 103785 +103788 103791 103794 103797 103800 103803 103806 103809 103812 +103815 103818 103821 103824 103827 103830 103833 103836 103839 +103842 103845 103848 103851 103854 103857 103860 103863 103866 +103869 103872 103875 103878 103881 103884 103887 103890 103893 +103896 103899 103902 103905 103908 103911 103914 103917 103920 +103923 103926 103929 103932 103935 103938 103941 103944 103947 +103950 103953 103956 103959 103962 103965 103968 103971 103974 +103977 103980 103983 103986 103989 103992 103995 103998 104001 +104004 104007 104010 104013 104016 104019 104022 104025 104028 +104031 104034 104037 104040 104043 104046 104049 104052 104055 +104058 104061 104064 104067 104070 104073 104076 104079 104082 +104085 104088 104091 104094 104097 104100 104103 104106 104109 +104112 104115 104118 104121 104124 104127 104130 104133 104136 +104139 104142 104145 104148 104151 104154 104157 104160 104163 +104166 104169 104172 104175 104178 104181 104184 104187 104190 +104193 104196 104199 104202 104205 104208 104211 104214 104217 +104220 104223 104226 104229 104232 104235 104238 104241 104244 +104247 104250 104253 104256 104259 104262 104265 104268 104271 +104274 104277 104280 104283 104286 104289 104292 104295 104298 +104301 104304 104307 104310 104313 104316 104319 104322 104325 +104328 104331 104334 104337 104340 104343 104346 104349 104352 +104355 104358 104361 104364 104367 104370 104373 104376 104379 +104382 104385 104388 104391 104394 104397 104400 104403 104406 +104409 104412 104415 104418 104421 104424 104427 104430 104433 +104436 104439 104442 104445 104448 104451 104454 104457 104460 +104463 104466 104469 104472 104475 104478 104481 104484 104487 +104490 104493 104496 104499 104502 104505 104508 104511 104514 +104517 104520 104523 104526 104529 104532 104535 104538 104541 +104544 104547 104550 104553 104556 104559 104562 104565 104568 +104571 104574 104577 104580 104583 104586 104589 104592 104595 +104598 104601 104604 104607 104610 104613 104616 104619 104622 +104625 104628 104631 104634 104637 104640 104643 104646 104649 +104652 104655 104658 104661 104664 104667 104670 104673 104676 +104679 104682 104685 104688 104691 104694 104697 104700 104703 +104706 104709 104712 104715 104718 104721 104724 104727 104730 +104733 104736 104739 104742 104745 104748 104751 104754 104757 +104760 104763 104766 104769 104772 104775 104778 104781 104784 +104787 104790 104793 104796 104799 104802 104805 104808 104811 +104814 104817 104820 104823 104826 104829 104832 104835 104838 +104841 104844 104847 104850 104853 104856 104859 104862 104865 +104868 104871 104874 104877 104880 104883 104886 104889 104892 +104895 104898 104901 104904 104907 104910 104913 104916 104919 +104922 104925 104928 104931 104934 104937 104940 104943 104946 +104949 104952 104955 104958 104961 104964 104967 104970 104973 +104976 104979 104982 104985 104988 104991 104994 104997 105000 +105003 105006 105009 105012 105015 105018 105021 105024 105027 +105030 105033 105036 105039 105042 105045 105048 105051 105054 +105057 105060 105063 105066 105069 105072 105075 105078 105081 +105084 105087 105090 105093 105096 105099 105102 105105 105108 +105111 105114 105117 105120 105123 105126 105129 105132 105135 +105138 105141 105144 105147 105150 105153 105156 105159 105162 +105165 105168 105171 105174 105177 105180 105183 105186 105189 +105192 105195 105198 105201 105204 105207 105210 105213 105216 +105219 105222 105225 105228 105231 105234 105237 105240 105243 +105246 105249 105252 105255 105258 105261 105264 105267 105270 +105273 105276 105279 105282 105285 105288 105291 105294 105297 +105300 105303 105306 105309 105312 105315 105318 105321 105324 +105327 105330 105333 105336 105339 105342 105345 105348 105351 +105354 105357 105360 105363 105366 105369 105372 105375 105378 +105381 105384 105387 105390 105393 105396 105399 105402 105405 +105408 105411 105414 105417 105420 105423 105426 105429 105432 +105435 105438 105441 105444 105447 105450 105453 105456 105459 +105462 105465 105468 105471 105474 105477 105480 105483 105486 +105489 105492 105495 105498 105501 105504 105507 105510 105513 +105516 105519 105522 105525 105528 105531 105534 105537 105540 +105543 105546 105549 105552 105555 105558 105561 105564 105567 +105570 105573 105576 105579 105582 105585 105588 105591 105594 +105597 105600 105603 105606 105609 105612 105615 105618 105621 +105624 105627 105630 105633 105636 105639 105642 105645 105648 +105651 105654 105657 105660 105663 105666 105669 105672 105675 +105678 105681 105684 105687 105690 105693 105696 105699 105702 +105705 105708 105711 105714 105717 105720 105723 105726 105729 +105732 105735 105738 105741 105744 105747 105750 105753 105756 +105759 105762 105765 105768 105771 105774 105777 105780 105783 +105786 105789 105792 105795 105798 105801 105804 105807 105810 +105813 105816 105819 105822 105825 105828 105831 105834 105837 +105840 105843 105846 105849 105852 105855 105858 105861 105864 +105867 105870 105873 105876 105879 105882 105885 105888 105891 +105894 105897 105900 105903 105906 105909 105912 105915 105918 +105921 105924 105927 105930 105933 105936 105939 105942 105945 +105948 105951 105954 105957 105960 105963 105966 105969 105972 +105975 105978 105981 105984 105987 105990 105993 105996 105999 +106002 106005 106008 106011 106014 106017 106020 106023 106026 +106029 106032 106035 106038 106041 106044 106047 106050 106053 +106056 106059 106062 106065 106068 106071 106074 106077 106080 +106083 106086 106089 106092 106095 106098 106101 106104 106107 +106110 106113 106116 106119 106122 106125 106128 106131 106134 +106137 106140 106143 106146 106149 106152 106155 106158 106161 +106164 106167 106170 106173 106176 106179 106182 106185 106188 +106191 106194 106197 106200 106203 106206 106209 106212 106215 +106218 106221 106224 106227 106230 106233 106236 106239 106242 +106245 106248 106251 106254 106257 106260 106263 106266 106269 +106272 106275 106278 106281 106284 106287 106290 106293 106296 +106299 106302 106305 106308 106311 106314 106317 106320 106323 +106326 106329 106332 106335 106338 106341 106344 106347 106350 +106353 106356 106359 106362 106365 106368 106371 106374 106377 +106380 106383 106386 106389 106392 106395 106398 106401 106404 +106407 106410 106413 106416 106419 106422 106425 106428 106431 +106434 106437 106440 106443 106446 106449 106452 106455 106458 +106461 106464 106467 106470 106473 106476 106479 106482 106485 +106488 106491 106494 106497 106500 106503 106506 106509 106512 +106515 106518 106521 106524 106527 106530 106533 106536 106539 +106542 106545 106548 106551 106554 106557 106560 106563 106566 +106569 106572 106575 106578 106581 106584 106587 106590 106593 +106596 106599 106602 106605 106608 106611 106614 106617 106620 +106623 106626 106629 106632 106635 106638 106641 106644 106647 +106650 106653 106656 106659 106662 106665 106668 106671 106674 +106677 106680 106683 106686 106689 106692 106695 106698 106701 +106704 106707 106710 106713 106716 106719 106722 106725 106728 +106731 106734 106737 106740 106743 106746 106749 106752 106755 +106758 106761 106764 106767 106770 106773 106776 106779 106782 +106785 106788 106791 106794 106797 106800 106803 106806 106809 +106812 106815 106818 106821 106824 106827 106830 106833 106836 +106839 106842 106845 106848 106851 106854 106857 106860 106863 +106866 106869 106872 106875 106878 106881 106884 106887 106890 +106893 106896 106899 106902 106905 106908 106911 106914 106917 +106920 106923 106926 106929 106932 106935 106938 106941 106944 +106947 106950 106953 106956 106959 106962 106965 106968 106971 +106974 106977 106980 106983 106986 106989 106992 106995 106998 +107001 107004 107007 107010 107013 107016 107019 107022 107025 +107028 107031 107034 107037 107040 107043 107046 107049 107052 +107055 107058 107061 107064 107067 107070 107073 107076 107079 +107082 107085 107088 107091 107094 107097 107100 107103 107106 +107109 107112 107115 107118 107121 107124 107127 107130 107133 +107136 107139 107142 107145 107148 107151 107154 107157 107160 +107163 107166 107169 107172 107175 107178 107181 107184 107187 +107190 107193 107196 107199 107202 107205 107208 107211 107214 +107217 107220 107223 107226 107229 107232 107235 107238 107241 +107244 107247 107250 107253 107256 107259 107262 107265 107268 +107271 107274 107277 107280 107283 107286 107289 107292 107295 +107298 107301 107304 107307 107310 107313 107316 107319 107322 +107325 107328 107331 107334 107337 107340 107343 107346 107349 +107352 107355 107358 107361 107364 107367 107370 107373 107376 +107379 107382 107385 107388 107391 107394 107397 107400 107403 +107406 107409 107412 107415 107418 107421 107424 107427 107430 +107433 107436 107439 107442 107445 107448 107451 107454 107457 +107460 107463 107466 107469 107472 107475 107478 107481 107484 +107487 107490 107493 107496 107499 107502 107505 107508 107511 +107514 107517 107520 107523 107526 107529 107532 107535 107538 +107541 107544 107547 107550 107553 107556 107559 107562 107565 +107568 107571 107574 107577 107580 107583 107586 107589 107592 +107595 107598 107601 107604 107607 107610 107613 107616 107619 +107622 107625 107628 107631 107634 107637 107640 107643 107646 +107649 107652 107655 107658 107661 107664 107667 107670 107673 +107676 107679 107682 107685 107688 107691 107694 107697 107700 +107703 107706 107709 107712 107715 107718 107721 107724 107727 +107730 107733 107736 107739 107742 107745 107748 107751 107754 +107757 107760 107763 107766 107769 107772 107775 107778 107781 +107784 107787 107790 107793 107796 107799 107802 107805 107808 +107811 107814 107817 107820 107823 107826 107829 107832 107835 +107838 107841 107844 107847 107850 107853 107856 107859 107862 +107865 107868 107871 107874 107877 107880 107883 107886 107889 +107892 107895 107898 107901 107904 107907 107910 107913 107916 +107919 107922 107925 107928 107931 107934 107937 107940 107943 +107946 107949 107952 107955 107958 107961 107964 107967 107970 +107973 107976 107979 107982 107985 107988 107991 107994 107997 +108000 108003 108006 108009 108012 108015 108018 108021 108024 +108027 108030 108033 108036 108039 108042 108045 108048 108051 +108054 108057 108060 108063 108066 108069 108072 108075 108078 +108081 108084 108087 108090 108093 108096 108099 108102 108105 +108108 108111 108114 108117 108120 108123 108126 108129 108132 +108135 108138 108141 108144 108147 108150 108153 108156 108159 +108162 108165 108168 108171 108174 108177 108180 108183 108186 +108189 108192 108195 108198 108201 108204 108207 108210 108213 +108216 108219 108222 108225 108228 108231 108234 108237 108240 +108243 108246 108249 108252 108255 108258 108261 108264 108267 +108270 108273 108276 108279 108282 108285 108288 108291 108294 +108297 108300 108303 108306 108309 108312 108315 108318 108321 +108324 108327 108330 108333 108336 108339 108342 108345 108348 +108351 108354 108357 108360 108363 108366 108369 108372 108375 +108378 108381 108384 108387 108390 108393 108396 108399 108402 +108405 108408 108411 108414 108417 108420 108423 108426 108429 +108432 108435 108438 108441 108444 108447 108450 108453 108456 +108459 108462 108465 108468 108471 108474 108477 108480 108483 +108486 108489 108492 108495 108498 108501 108504 108507 108510 +108513 108516 108519 108522 108525 108528 108531 108534 108537 +108540 108543 108546 108549 108552 108555 108558 108561 108564 +108567 108570 108573 108576 108579 108582 108585 108588 108591 +108594 108597 108600 108603 108606 108609 108612 108615 108618 +108621 108624 108627 108630 108633 108636 108639 108642 108645 +108648 108651 108654 108657 108660 108663 108666 108669 108672 +108675 108678 108681 108684 108687 108690 108693 108696 108699 +108702 108705 108708 108711 108714 108717 108720 108723 108726 +108729 108732 108735 108738 108741 108744 108747 108750 108753 +108756 108759 108762 108765 108768 108771 108774 108777 108780 +108783 108786 108789 108792 108795 108798 108801 108804 108807 +108810 108813 108816 108819 108822 108825 108828 108831 108834 +108837 108840 108843 108846 108849 108852 108855 108858 108861 +108864 108867 108870 108873 108876 108879 108882 108885 108888 +108891 108894 108897 108900 108903 108906 108909 108912 108915 +108918 108921 108924 108927 108930 108933 108936 108939 108942 +108945 108948 108951 108954 108957 108960 108963 108966 108969 +108972 108975 108978 108981 108984 108987 108990 108993 108996 +108999 109002 109005 109008 109011 109014 109017 109020 109023 +109026 109029 109032 109035 109038 109041 109044 109047 109050 +109053 109056 109059 109062 109065 109068 109071 109074 109077 +109080 109083 109086 109089 109092 109095 109098 109101 109104 +109107 109110 109113 109116 109119 109122 109125 109128 109131 +109134 109137 109140 109143 109146 109149 109152 109155 109158 +109161 109164 109167 109170 109173 109176 109179 109182 109185 +109188 109191 109194 109197 109200 109203 109206 109209 109212 +109215 109218 109221 109224 109227 109230 109233 109236 109239 +109242 109245 109248 109251 109254 109257 109260 109263 109266 +109269 109272 109275 109278 109281 109284 109287 109290 109293 +109296 109299 109302 109305 109308 109311 109314 109317 109320 +109323 109326 109329 109332 109335 109338 109341 109344 109347 +109350 109353 109356 109359 109362 109365 109368 109371 109374 +109377 109380 109383 109386 109389 109392 109395 109398 109401 +109404 109407 109410 109413 109416 109419 109422 109425 109428 +109431 109434 109437 109440 109443 109446 109449 109452 109455 +109458 109461 109464 109467 109470 109473 109476 109479 109482 +109485 109488 109491 109494 109497 109500 109503 109506 109509 +109512 109515 109518 109521 109524 109527 109530 109533 109536 +109539 109542 109545 109548 109551 109554 109557 109560 109563 +109566 109569 109572 109575 109578 109581 109584 109587 109590 +109593 109596 109599 109602 109605 109608 109611 109614 109617 +109620 109623 109626 109629 109632 109635 109638 109641 109644 +109647 109650 109653 109656 109659 109662 109665 109668 109671 +109674 109677 109680 109683 109686 109689 109692 109695 109698 +109701 109704 109707 109710 109713 109716 109719 109722 109725 +109728 109731 109734 109737 109740 109743 109746 109749 109752 +109755 109758 109761 109764 109767 109770 109773 109776 109779 +109782 109785 109788 109791 109794 109797 109800 109803 109806 +109809 109812 109815 109818 109821 109824 109827 109830 109833 +109836 109839 109842 109845 109848 109851 109854 109857 109860 +109863 109866 109869 109872 109875 109878 109881 109884 109887 +109890 109893 109896 109899 109902 109905 109908 109911 109914 +109917 109920 109923 109926 109929 109932 109935 109938 109941 +109944 109947 109950 109953 109956 109959 109962 109965 109968 +109971 109974 109977 109980 109983 109986 109989 109992 109995 +109998 110001 110004 110007 110010 110013 110016 110019 110022 +110025 110028 110031 110034 110037 110040 110043 110046 110049 +110052 110055 110058 110061 110064 110067 110070 110073 110076 +110079 110082 110085 110088 110091 110094 110097 110100 110103 +110106 110109 110112 110115 110118 110121 110124 110127 110130 +110133 110136 110139 110142 110145 110148 110151 110154 110157 +110160 110163 110166 110169 110172 110175 110178 110181 110184 +110187 110190 110193 110196 110199 110202 110205 110208 110211 +110214 110217 110220 110223 110226 110229 110232 110235 110238 +110241 110244 110247 110250 110253 110256 110259 110262 110265 +110268 110271 110274 110277 110280 110283 110286 110289 110292 +110295 110298 110301 110304 110307 110310 110313 110316 110319 +110322 110325 110328 110331 110334 110337 110340 110343 110346 +110349 110352 110355 110358 110361 110364 110367 110370 110373 +110376 110379 110382 110385 110388 110391 110394 110397 110400 +110403 110406 110409 110412 110415 110418 110421 110424 110427 +110430 110433 110436 110439 110442 110445 110448 110451 110454 +110457 110460 110463 110466 110469 110472 110475 110478 110481 +110484 110487 110490 110493 110496 110499 110502 110505 110508 +110511 110514 110517 110520 110523 110526 110529 110532 110535 +110538 110541 110544 110547 110550 110553 110556 110559 110562 +110565 110568 110571 110574 110577 110580 110583 110586 110589 +110592 110595 110598 110601 110604 110607 110610 110613 110616 +110619 110622 110625 110628 110631 110634 110637 110640 110643 +110646 110649 110652 110655 110658 110661 110664 110667 110670 +110673 110676 110679 110682 110685 110688 110691 110694 110697 +110700 110703 110706 110709 110712 110715 110718 110721 110724 +110727 110730 110733 110736 110739 110742 110745 110748 110751 +110754 110757 110760 110763 110766 110769 110772 110775 110778 +110781 110784 110787 110790 110793 110796 110799 110802 110805 +110808 110811 110814 110817 110820 110823 110826 110829 110832 +110835 110838 110841 110844 110847 110850 110853 110856 110859 +110862 110865 110868 110871 110874 110877 110880 110883 110886 +110889 110892 110895 110898 110901 110904 110907 110910 110913 +110916 110919 110922 110925 110928 110931 110934 110937 110940 +110943 110946 110949 110952 110955 110958 110961 110964 110967 +110970 110973 110976 110979 110982 110985 110988 110991 110994 +110997 111000 111003 111006 111009 111012 111015 111018 111021 +111024 111027 111030 111033 111036 111039 111042 111045 111048 +111051 111054 111057 111060 111063 111066 111069 111072 111075 +111078 111081 111084 111087 111090 111093 111096 111099 111102 +111105 111108 111111 111114 111117 111120 111123 111126 111129 +111132 111135 111138 111141 111144 111147 111150 111153 111156 +111159 111162 111165 111168 111171 111174 111177 111180 111183 +111186 111189 111192 111195 111198 111201 111204 111207 111210 +111213 111216 111219 111222 111225 111228 111231 111234 111237 +111240 111243 111246 111249 111252 111255 111258 111261 111264 +111267 111270 111273 111276 111279 111282 111285 111288 111291 +111294 111297 111300 111303 111306 111309 111312 111315 111318 +111321 111324 111327 111330 111333 111336 111339 111342 111345 +111348 111351 111354 111357 111360 111363 111366 111369 111372 +111375 111378 111381 111384 111387 111390 111393 111396 111399 +111402 111405 111408 111411 111414 111417 111420 111423 111426 +111429 111432 111435 111438 111441 111444 111447 111450 111453 +111456 111459 111462 111465 111468 111471 111474 111477 111480 +111483 111486 111489 111492 111495 111498 111501 111504 111507 +111510 111513 111516 111519 111522 111525 111528 111531 111534 +111537 111540 111543 111546 111549 111552 111555 111558 111561 +111564 111567 111570 111573 111576 111579 111582 111585 111588 +111591 111594 111597 111600 111603 111606 111609 111612 111615 +111618 111621 111624 111627 111630 111633 111636 111639 111642 +111645 111648 111651 111654 111657 111660 111663 111666 111669 +111672 111675 111678 111681 111684 111687 111690 111693 111696 +111699 111702 111705 111708 111711 111714 111717 111720 111723 +111726 111729 111732 111735 111738 111741 111744 111747 111750 +111753 111756 111759 111762 111765 111768 111771 111774 111777 +111780 111783 111786 111789 111792 111795 111798 111801 111804 +111807 111810 111813 111816 111819 111822 111825 111828 111831 +111834 111837 111840 111843 111846 111849 111852 111855 111858 +111861 111864 111867 111870 111873 111876 111879 111882 111885 +111888 111891 111894 111897 111900 111903 111906 111909 111912 +111915 111918 111921 111924 111927 111930 111933 111936 111939 +111942 111945 111948 111951 111954 111957 111960 111963 111966 +111969 111972 111975 111978 111981 111984 111987 111990 111993 +111996 111999 112002 112005 112008 112011 112014 112017 112020 +112023 112026 112029 112032 112035 112038 112041 112044 112047 +112050 112053 112056 112059 112062 112065 112068 112071 112074 +112077 112080 112083 112086 112089 112092 112095 112098 112101 +112104 112107 112110 112113 112116 112119 112122 112125 112128 +112131 112134 112137 112140 112143 112146 112149 112152 112155 +112158 112161 112164 112167 112170 112173 112176 112179 112182 +112185 112188 112191 112194 112197 112200 112203 112206 112209 +112212 112215 112218 112221 112224 112227 112230 112233 112236 +112239 112242 112245 112248 112251 112254 112257 112260 112263 +112266 112269 112272 112275 112278 112281 112284 112287 112290 +112293 112296 112299 112302 112305 112308 112311 112314 112317 +112320 112323 112326 112329 112332 112335 112338 112341 112344 +112347 112350 112353 112356 112359 112362 112365 112368 112371 +112374 112377 112380 112383 112386 112389 112392 112395 112398 +112401 112404 112407 112410 112413 112416 112419 112422 112425 +112428 112431 112434 112437 112440 112443 112446 112449 112452 +112455 112458 112461 112464 112467 112470 112473 112476 112479 +112482 112485 112488 112491 112494 112497 112500 112503 112506 +112509 112512 112515 112518 112521 112524 112527 112530 112533 +112536 112539 112542 112545 112548 112551 112554 112557 112560 +112563 112566 112569 112572 112575 112578 112581 112584 112587 +112590 112593 112596 112599 112602 112605 112608 112611 112614 +112617 112620 112623 112626 112629 112632 112635 112638 112641 +112644 112647 112650 112653 112656 112659 112662 112665 112668 +112671 112674 112677 112680 112683 112686 112689 112692 112695 +112698 112701 112704 112707 112710 112713 112716 112719 112722 +112725 112728 112731 112734 112737 112740 112743 112746 112749 +112752 112755 112758 112761 112764 112767 112770 112773 112776 +112779 112782 112785 112788 112791 112794 112797 112800 112803 +112806 112809 112812 112815 112818 112821 112824 112827 112830 +112833 112836 112839 112842 112845 112848 112851 112854 112857 +112860 112863 112866 112869 112872 112875 112878 112881 112884 +112887 112890 112893 112896 112899 112902 112905 112908 112911 +112914 112917 112920 112923 112926 112929 112932 112935 112938 +112941 112944 112947 112950 112953 112956 112959 112962 112965 +112968 112971 112974 112977 112980 112983 112986 112989 112992 +112995 112998 113001 113004 113007 113010 113013 113016 113019 +113022 113025 113028 113031 113034 113037 113040 113043 113046 +113049 113052 113055 113058 113061 113064 113067 113070 113073 +113076 113079 113082 113085 113088 113091 113094 113097 113100 +113103 113106 113109 113112 113115 113118 113121 113124 113127 +113130 113133 113136 113139 113142 113145 113148 113151 113154 +113157 113160 113163 113166 113169 113172 113175 113178 113181 +113184 113187 113190 113193 113196 113199 113202 113205 113208 +113211 113214 113217 113220 113223 113226 113229 113232 113235 +113238 113241 113244 113247 113250 113253 113256 113259 113262 +113265 113268 113271 113274 113277 113280 113283 113286 113289 +113292 113295 113298 113301 113304 113307 113310 113313 113316 +113319 113322 113325 113328 113331 113334 113337 113340 113343 +113346 113349 113352 113355 113358 113361 113364 113367 113370 +113373 113376 113379 113382 113385 113388 113391 113394 113397 +113400 113403 113406 113409 113412 113415 113418 113421 113424 +113427 113430 113433 113436 113439 113442 113445 113448 113451 +113454 113457 113460 113463 113466 113469 113472 113475 113478 +113481 113484 113487 113490 113493 113496 113499 113502 113505 +113508 113511 113514 113517 113520 113523 113526 113529 113532 +113535 113538 113541 113544 113547 113550 113553 113556 113559 +113562 113565 113568 113571 113574 113577 113580 113583 113586 +113589 113592 113595 113598 113601 113604 113607 113610 113613 +113616 113619 113622 113625 113628 113631 113634 113637 113640 +113643 113646 +CONNECTIVITY vtktypeint64 +0 1 2 2 3 0 1 4 2 +2 4 3 5 6 7 8 9 7 +7 9 5 10 11 8 8 11 9 +10 12 11 13 14 15 13 15 6 +15 7 6 15 16 7 16 1 0 +8 16 0 7 16 8 8 0 3 +17 8 3 10 8 17 12 10 18 +10 19 18 10 17 19 19 20 18 +15 14 21 21 14 22 15 23 16 +15 21 23 24 4 1 16 24 1 +23 24 16 17 3 4 24 17 4 +25 17 24 17 26 19 17 25 26 +20 19 27 27 19 26 21 22 28 +28 22 29 21 30 23 21 28 30 +23 31 24 23 30 31 32 25 33 +25 31 33 25 24 31 25 32 26 +26 32 34 26 34 27 29 35 28 +28 35 30 30 35 36 30 36 31 +31 36 37 31 37 33 38 39 40 +6 5 40 5 38 40 5 41 38 +42 5 9 42 41 5 43 9 11 +43 42 9 44 45 12 45 11 12 +45 43 11 46 47 44 44 47 45 +46 48 47 49 50 51 49 51 39 +39 51 40 51 14 40 14 13 40 +40 13 6 18 44 12 44 18 20 +52 44 20 46 44 52 48 46 53 +53 46 52 51 50 54 54 50 55 +54 22 14 54 14 51 27 56 52 +27 52 20 53 52 57 57 52 56 +58 59 60 58 60 55 60 54 55 +60 61 54 61 29 22 61 22 54 +33 62 32 63 34 32 63 32 62 +63 64 34 34 64 27 64 65 27 +65 56 27 56 65 57 59 66 60 +60 66 61 61 66 67 61 67 29 +67 35 29 67 68 35 35 69 36 +68 69 35 36 70 37 69 70 36 +33 37 62 37 71 62 37 70 71 +62 71 63 63 71 72 63 72 64 +73 74 75 75 76 73 77 78 38 +38 78 39 79 38 41 79 77 38 +42 75 74 79 42 74 41 42 79 +80 76 75 42 80 75 43 80 42 +81 43 45 81 80 43 82 45 47 +82 81 45 47 48 82 82 48 83 +84 85 49 49 85 50 49 39 78 +49 78 84 53 83 48 53 86 83 +87 88 89 85 87 89 55 85 89 +50 85 55 57 90 91 86 57 91 +53 57 86 90 92 91 89 88 58 +88 59 58 88 93 59 89 58 55 +64 94 65 94 95 65 65 95 57 +95 96 57 96 90 57 90 96 92 +93 97 59 59 97 66 97 98 99 +67 97 99 66 97 67 100 68 67 +100 67 99 101 69 68 101 68 100 +102 70 69 102 69 101 103 71 70 +103 70 102 104 72 71 104 71 103 +104 105 72 72 105 64 105 106 64 +106 94 64 94 106 95 98 107 99 +99 107 100 100 107 108 100 108 101 +101 108 109 101 109 102 102 109 110 +102 110 103 103 110 111 103 111 104 +104 111 112 104 112 105 113 114 115 +74 73 115 73 113 115 73 116 113 +117 118 76 118 73 76 118 116 73 +117 119 118 120 121 122 78 77 122 +77 120 122 77 123 120 79 115 114 +123 79 114 77 79 123 115 79 74 +80 117 76 124 119 117 80 124 117 +81 124 80 125 124 126 124 82 126 +124 81 82 83 126 82 122 121 84 +121 85 84 121 127 85 122 84 78 +128 125 126 83 86 126 86 129 126 +129 128 126 129 86 130 130 131 129 +132 133 134 88 87 134 87 132 134 +87 135 132 87 85 127 87 127 135 +130 86 91 136 137 138 137 136 92 +136 131 92 130 91 92 130 92 131 +134 133 139 139 133 140 139 93 88 +139 88 134 96 95 141 137 142 138 +142 137 143 137 92 143 96 141 143 +96 143 92 140 144 139 144 145 146 +144 146 97 139 144 97 93 139 97 +146 98 97 105 147 106 141 95 106 +147 141 106 148 141 147 143 141 149 +149 141 148 145 150 146 146 150 98 +150 107 98 150 151 107 151 152 153 +108 151 153 107 151 108 154 109 108 +154 108 153 109 154 155 156 109 155 +110 109 156 110 157 111 156 157 110 +111 158 112 157 158 111 112 158 105 +158 159 105 159 147 105 159 158 160 +161 148 162 148 159 162 148 147 159 +148 161 149 152 163 153 153 163 154 +154 163 164 154 164 155 160 165 159 +159 165 162 166 114 113 116 167 113 +113 167 166 118 168 116 116 168 167 +119 168 118 169 170 120 120 170 121 +171 120 123 171 169 120 123 114 166 +172 123 166 171 123 172 172 166 167 +172 167 173 173 167 168 173 168 174 +174 168 119 124 174 119 175 174 124 +124 125 175 175 125 176 170 127 121 +177 127 170 178 176 125 178 125 128 +179 128 129 179 178 128 129 131 179 +179 131 180 181 182 132 132 182 133 +183 132 135 183 181 132 135 127 177 +135 177 183 184 185 136 184 136 138 +131 136 180 180 136 185 182 140 133 +186 140 182 187 184 138 187 138 142 +142 143 187 187 143 188 186 189 140 +140 189 144 145 144 189 145 189 190 +149 188 143 149 191 188 190 192 145 +145 192 150 192 193 194 151 192 194 +150 192 151 194 152 151 155 195 156 +196 157 156 196 156 195 157 196 197 +198 157 197 158 157 198 160 158 198 +160 198 199 200 201 161 200 161 162 +149 161 191 191 161 201 193 202 194 +194 202 152 202 163 152 202 203 163 +163 204 164 203 204 163 155 164 195 +164 205 195 164 204 205 195 205 196 +196 205 206 196 206 197 199 207 160 +160 207 165 162 165 200 200 165 207 +208 209 169 169 209 170 210 169 171 +210 208 169 211 171 172 211 210 171 +212 172 173 212 211 172 213 173 174 +213 212 173 214 174 175 214 213 174 +175 176 214 214 176 215 216 217 218 +209 216 218 177 209 218 170 209 177 +219 215 176 219 176 178 220 219 221 +219 179 221 219 178 179 180 221 179 +222 223 181 181 223 182 183 218 217 +222 183 217 181 183 222 218 183 177 +220 221 185 184 220 185 224 220 184 +185 221 180 223 186 182 225 186 223 +184 187 226 227 184 226 224 184 227 +188 226 187 189 186 228 186 229 228 +186 225 229 228 190 189 227 226 230 +227 230 231 191 230 188 188 230 226 +229 232 228 232 233 234 232 234 192 +228 232 192 190 228 192 234 193 192 +197 235 198 235 199 198 231 230 201 +200 231 201 236 231 200 201 230 191 +233 237 234 234 237 193 237 202 193 +237 238 202 202 239 203 238 239 202 +203 240 204 239 240 203 204 241 205 +240 241 204 205 242 206 241 242 205 +206 242 197 242 243 197 243 235 197 +243 242 244 235 243 199 243 245 199 +245 207 199 243 246 245 200 207 236 +236 207 245 244 247 243 243 247 246 +248 249 208 208 249 209 250 208 210 +250 248 208 251 210 211 251 250 210 +252 211 212 252 251 211 253 212 213 +253 252 212 254 253 255 253 214 255 +253 213 214 215 255 214 256 257 216 +216 257 217 216 209 249 216 249 256 +258 254 255 259 258 260 260 258 219 +258 255 219 255 215 219 220 260 219 +261 262 222 222 262 223 222 217 257 +222 257 261 263 259 260 260 220 263 +220 264 263 220 224 264 262 225 223 +265 225 262 227 264 224 227 266 264 +265 229 225 267 229 265 231 266 227 +231 268 266 267 269 229 229 269 232 +233 232 269 233 269 270 236 268 231 +236 271 268 270 272 233 233 272 237 +237 273 238 272 273 237 273 274 275 +239 273 275 238 273 239 276 240 239 +276 239 275 277 241 240 277 240 276 +278 242 241 278 241 277 278 244 242 +279 280 245 279 245 246 236 245 271 +271 245 280 274 281 275 275 281 276 +276 281 282 276 282 277 277 282 283 +277 283 278 278 283 284 278 284 244 +284 247 244 284 285 247 246 247 279 +279 247 285 286 287 248 248 287 249 +288 248 250 288 286 248 289 250 251 +289 288 250 290 251 252 290 289 251 +291 252 253 291 290 252 253 254 291 +291 254 292 293 294 256 256 294 257 +256 249 287 256 287 293 295 292 254 +295 254 258 258 259 295 295 259 296 +297 298 261 261 298 262 261 257 294 +261 294 297 259 263 299 300 259 299 +296 259 300 264 299 263 298 265 262 +301 265 298 302 300 299 299 264 302 +264 303 302 264 266 303 301 267 265 +304 267 301 268 303 266 268 305 303 +304 306 267 267 306 269 270 269 306 +270 306 307 271 305 268 271 308 305 +307 309 270 270 309 272 309 310 311 +273 309 311 272 309 273 311 274 273 +279 312 280 312 313 280 313 271 280 +313 308 271 310 314 311 311 314 274 +314 281 274 314 315 281 281 316 282 +315 316 281 282 317 283 316 317 282 +283 318 284 317 318 283 318 319 320 +285 318 320 284 318 285 320 321 285 +285 321 279 321 322 279 322 312 279 +312 322 313 319 323 320 320 323 321 +324 325 326 287 286 326 286 324 326 +286 327 324 328 286 288 328 327 286 +329 288 289 329 328 288 330 289 290 +330 329 289 331 290 291 331 330 290 +291 292 331 331 292 332 326 325 293 +325 294 293 325 333 294 326 293 287 +334 332 292 334 292 295 295 296 334 +334 296 335 336 337 297 297 337 298 +297 294 333 297 333 336 300 335 296 +300 338 335 337 301 298 339 301 337 +340 338 300 340 300 302 302 303 340 +340 303 341 339 304 301 342 304 339 +305 341 303 305 343 341 306 304 344 +304 345 344 304 342 345 344 307 306 +308 343 305 308 346 343 345 347 344 +347 348 349 347 349 309 344 347 309 +307 344 309 349 310 309 313 346 308 +313 350 346 348 351 349 349 351 310 +351 314 310 351 352 314 314 353 315 +352 353 314 353 354 355 316 353 355 +315 353 316 356 317 316 356 316 355 +357 318 317 357 317 356 357 319 318 +358 359 322 358 322 321 313 322 350 +350 322 359 354 360 355 355 360 356 +356 360 361 362 363 361 363 356 361 +363 357 356 357 363 319 363 364 319 +364 323 319 363 365 364 321 323 358 +358 323 364 362 366 363 363 366 365 +367 368 324 324 368 325 369 324 327 +369 367 324 370 327 328 370 369 327 +371 328 329 371 370 328 372 329 330 +372 371 329 373 330 331 373 372 330 +331 332 373 373 332 374 368 333 325 +375 333 368 376 374 332 376 332 334 +334 335 376 376 335 377 378 379 336 +336 379 337 336 333 375 336 375 378 +338 377 335 338 380 377 379 339 337 +381 339 379 382 380 338 382 338 340 +340 341 382 382 341 383 381 342 339 +384 342 381 343 383 341 343 385 383 +384 345 342 386 345 384 346 385 343 +346 387 385 386 388 345 345 388 347 +348 347 388 348 388 389 350 387 346 +350 390 387 389 391 348 348 391 351 +351 392 352 391 392 351 392 393 394 +353 392 394 352 392 353 394 354 353 +395 396 359 395 359 358 350 359 390 +390 359 396 393 397 394 394 397 354 +397 360 354 397 398 360 360 399 361 +398 399 360 362 361 399 362 399 400 +401 402 364 401 364 365 358 364 395 +395 364 402 400 403 362 362 403 366 +365 366 401 401 366 403 404 405 406 +407 408 406 406 408 404 407 409 408 +410 411 367 367 411 368 412 367 369 +412 410 367 413 369 370 413 412 369 +371 406 405 413 371 405 370 371 413 +406 371 372 406 372 407 372 373 407 +373 414 407 414 409 407 373 415 414 +374 415 373 416 417 418 411 416 418 +375 411 418 368 411 375 419 414 415 +419 415 374 376 419 374 420 419 376 +376 377 420 420 377 421 418 417 378 +417 379 378 417 422 379 418 378 375 +380 423 424 421 380 424 377 380 421 +423 425 424 422 381 379 426 381 422 +382 423 380 382 383 423 383 425 423 +383 427 425 426 384 381 428 384 426 +385 427 383 385 429 427 428 386 384 +430 386 428 387 429 385 387 431 429 +388 386 432 386 433 432 386 430 433 +432 389 388 390 431 387 390 434 431 +433 435 432 432 435 389 435 391 389 +435 436 391 436 437 438 392 436 438 +391 436 392 438 393 392 395 439 396 +439 440 396 440 390 396 440 434 390 +437 441 438 438 441 393 441 397 393 +441 442 397 397 443 398 442 443 397 +398 444 399 443 444 398 400 399 444 +400 444 445 401 446 402 446 447 402 +402 447 395 447 448 395 448 439 395 +439 448 440 445 449 400 400 449 403 +401 403 446 403 450 446 403 449 450 +446 450 447 451 452 453 454 455 453 +453 455 451 405 404 454 404 455 454 +404 456 455 457 404 408 457 456 404 +408 409 457 457 409 458 459 460 410 +410 460 411 412 453 452 459 412 452 +410 412 459 453 412 413 453 413 454 +454 413 405 414 461 462 458 414 462 +409 414 458 461 463 462 464 465 416 +416 465 417 416 411 460 416 460 464 +419 461 414 466 463 461 419 466 461 +420 466 419 420 421 466 466 421 467 +465 422 417 468 422 465 469 467 421 +469 421 424 424 425 469 469 425 470 +471 472 473 468 471 473 426 468 473 +422 468 426 427 470 425 427 474 470 +473 472 475 475 472 476 475 428 426 +475 426 473 429 474 427 429 477 474 +475 476 478 478 476 479 478 430 428 +478 428 475 431 477 429 431 480 477 +479 481 478 430 478 481 482 430 481 +433 430 482 434 480 431 434 483 480 +482 484 433 433 484 435 484 485 486 +436 484 486 435 484 436 486 437 436 +440 483 434 440 487 483 485 488 486 +486 488 437 488 441 437 488 489 441 +441 490 442 489 490 441 490 491 492 +443 490 492 442 490 443 493 444 443 +493 443 492 493 445 444 494 495 448 +494 448 447 440 448 487 487 448 495 +491 496 492 497 498 496 498 492 496 +498 493 492 499 449 445 493 499 445 +498 499 493 449 499 500 501 449 500 +450 449 501 447 450 494 494 450 501 +497 502 498 498 502 499 499 502 503 +499 503 500 504 505 506 507 508 506 +506 508 504 507 509 508 506 505 510 +510 505 511 506 512 507 506 510 512 +509 507 513 507 514 513 507 512 514 +514 515 513 510 511 516 516 511 517 +510 518 512 510 516 518 512 519 514 +512 518 519 515 514 520 514 521 520 +514 519 521 521 522 520 516 517 523 +523 517 524 516 525 518 516 523 525 +518 526 519 518 525 526 519 527 521 +519 526 527 522 521 528 521 529 528 +521 527 529 529 530 528 531 532 533 +531 533 524 524 533 523 533 452 523 +452 451 523 523 451 455 523 455 525 +525 455 456 525 456 526 526 456 457 +526 457 527 527 457 458 534 527 458 +529 527 534 530 529 535 535 529 534 +536 537 538 536 538 532 532 538 533 +538 460 533 460 459 533 533 459 452 +462 534 458 539 540 463 463 540 462 +540 535 462 535 534 462 539 541 540 +538 537 464 537 465 464 537 542 465 +538 464 460 466 539 463 543 544 467 +467 544 466 544 541 466 541 539 466 +543 545 544 546 547 548 542 546 548 +468 542 548 465 542 468 469 543 467 +469 470 543 470 545 543 470 549 545 +548 547 471 547 472 471 547 550 472 +548 471 468 474 549 470 474 551 549 +550 476 472 552 476 550 477 553 554 +551 477 554 474 477 551 553 555 554 +552 479 476 556 479 552 480 557 553 +480 553 477 555 553 558 558 553 557 +556 559 479 479 559 481 482 481 559 +482 559 560 561 562 557 480 561 557 +483 561 480 557 562 558 560 563 482 +482 563 484 485 484 563 485 563 564 +487 561 483 487 565 561 564 566 485 +485 566 488 488 567 489 566 567 488 +567 568 569 490 567 569 489 567 490 +569 491 490 494 570 495 570 571 495 +571 487 495 571 565 487 569 568 572 +572 568 573 574 496 491 569 574 491 +572 574 569 574 497 496 500 575 501 +575 576 501 501 576 494 576 577 494 +577 570 494 570 577 571 573 578 572 +572 578 574 574 578 579 574 579 497 +579 502 497 579 580 502 502 581 503 +580 581 502 500 503 575 503 582 575 +503 581 582 575 582 576 583 584 585 +586 587 585 585 587 583 588 589 586 +586 589 587 590 591 588 588 591 589 +592 593 590 590 593 591 592 594 593 +585 584 595 595 584 596 595 505 504 +586 595 504 585 595 586 586 504 508 +586 508 588 588 508 509 597 588 509 +590 588 597 590 598 592 590 597 598 +594 592 599 599 592 598 595 596 600 +600 596 601 600 511 505 600 505 595 +513 597 509 597 513 515 602 597 515 +598 597 602 599 598 603 598 604 603 +598 602 604 604 605 603 600 601 606 +606 601 607 606 517 511 606 511 600 +520 602 515 602 520 522 608 602 522 +604 602 608 605 604 609 604 610 609 +604 608 610 610 611 609 612 613 614 +612 614 607 614 606 607 614 615 606 +615 524 517 615 517 606 528 608 522 +608 528 530 616 608 530 610 608 616 +611 610 617 610 618 617 610 616 618 +618 619 617 614 613 620 620 613 621 +620 532 531 615 620 531 614 620 615 +615 531 524 535 622 616 535 616 530 +623 618 624 618 622 624 618 616 622 +618 623 619 620 621 536 621 537 536 +621 625 537 620 536 532 540 622 535 +626 627 541 541 627 540 627 624 540 +624 622 540 626 628 627 629 630 631 +625 629 631 542 625 631 537 625 542 +544 626 541 632 633 545 545 633 544 +633 628 544 628 626 544 632 634 633 +631 630 546 630 547 546 630 635 547 +631 546 542 549 636 632 549 632 545 +634 632 637 637 632 636 635 550 547 +638 550 635 551 639 636 551 636 549 +637 636 640 640 636 639 638 552 550 +641 552 638 554 639 551 554 555 639 +555 640 639 555 642 640 641 556 552 +643 556 641 558 642 555 558 644 642 +559 556 645 556 646 645 556 643 646 +645 560 559 561 647 562 647 648 562 +648 558 562 648 644 558 646 649 645 +649 650 651 649 651 563 645 649 563 +560 645 563 651 564 563 652 653 647 +561 652 647 565 652 561 647 653 648 +650 654 651 651 654 564 654 566 564 +654 655 566 655 656 657 567 655 657 +566 655 567 657 568 567 571 652 565 +571 658 652 657 656 659 659 656 660 +659 573 568 659 568 657 661 662 663 +577 576 663 662 577 663 664 577 662 +571 577 658 658 577 664 660 665 659 +659 665 573 665 578 573 665 666 578 +578 667 579 666 667 578 667 668 669 +580 667 669 579 667 580 580 669 670 +671 580 670 581 580 671 663 582 661 +582 671 661 582 581 671 582 663 576 +668 672 669 669 672 670 673 674 675 +675 676 673 677 678 679 677 679 674 +679 675 674 679 680 675 676 675 681 +675 682 681 675 680 682 682 683 681 +679 678 684 684 678 685 679 686 680 +679 684 686 680 687 682 680 686 687 +683 682 688 688 682 687 684 685 689 +689 685 690 684 691 686 684 689 691 +686 692 687 686 691 692 688 687 693 +687 694 693 687 692 694 694 695 693 +696 697 698 696 698 690 698 689 690 +698 699 689 689 700 691 689 699 700 +691 701 692 691 700 701 692 702 694 +692 701 702 695 694 703 703 694 702 +704 705 706 706 707 704 698 697 708 +708 697 709 698 710 699 698 708 710 +699 711 700 699 710 711 700 712 701 +700 711 712 701 713 702 701 712 713 +703 702 714 714 702 713 715 716 717 +715 717 705 717 706 705 717 718 706 +707 706 719 719 706 718 708 709 720 +720 709 721 708 722 710 708 720 722 +710 723 711 710 722 723 711 724 712 +711 723 724 712 725 713 712 724 725 +714 713 726 726 713 725 717 716 727 +727 716 728 717 729 718 717 727 729 +719 718 730 718 731 730 718 729 731 +731 732 730 720 721 733 733 721 734 +720 735 722 720 733 735 722 736 723 +722 735 736 723 737 724 723 736 737 +724 738 725 724 737 738 726 725 739 +739 725 738 727 728 740 740 728 741 +727 742 729 727 740 742 729 743 731 +729 742 743 732 731 744 731 745 744 +731 743 745 745 746 744 734 747 733 +748 749 747 749 733 747 749 735 733 +735 750 736 735 749 750 736 751 737 +736 750 751 737 752 738 737 751 752 +739 738 753 753 738 752 740 741 754 +754 741 755 740 756 742 740 754 756 +742 757 743 742 756 757 743 758 745 +743 757 758 746 745 759 759 745 758 +749 748 760 760 748 761 749 762 750 +749 760 762 750 763 751 750 762 763 +751 764 752 751 763 764 753 752 765 +765 752 764 755 766 754 767 768 766 +768 754 766 768 756 754 756 769 757 +756 768 769 770 758 771 758 769 771 +758 757 769 758 770 759 761 772 760 +773 774 772 774 760 772 774 762 760 +775 763 776 763 774 776 763 762 774 +763 775 764 764 775 777 764 777 765 +767 778 768 768 778 769 769 778 779 +769 779 771 773 780 774 774 780 776 +781 782 783 783 784 781 785 786 787 +785 787 782 787 783 782 787 788 783 +784 783 789 783 790 789 783 788 790 +790 791 789 792 793 794 795 796 794 +794 796 792 797 798 795 795 798 796 +797 799 798 800 801 802 803 804 802 +802 804 800 804 803 786 803 787 786 +803 805 787 787 806 788 787 805 806 +788 807 790 788 806 807 791 790 808 +790 809 808 790 807 809 809 810 808 +811 812 583 583 812 584 587 794 793 +811 587 793 583 587 811 794 587 589 +794 589 795 795 589 591 795 591 797 +813 799 797 591 813 797 593 813 591 +814 815 594 815 593 594 815 813 593 +815 814 801 814 802 801 814 816 802 +802 817 803 802 816 817 803 818 805 +803 817 818 805 819 806 805 818 819 +806 820 807 806 819 820 807 821 809 +807 820 821 810 809 822 822 809 821 +823 824 825 812 823 825 596 812 825 +584 812 596 599 826 814 599 814 594 +814 827 816 814 826 827 816 828 817 +816 827 828 817 829 818 817 828 829 +818 830 819 818 829 830 819 831 820 +819 830 831 820 832 821 820 831 832 +822 821 833 821 834 833 821 832 834 +834 835 833 825 824 836 836 824 837 +836 601 596 836 596 825 603 826 599 +826 603 605 838 826 605 827 826 838 +827 839 828 827 838 839 828 840 829 +828 839 840 829 841 830 829 840 841 +830 842 831 830 841 842 831 843 832 +831 842 843 832 844 834 832 843 844 +835 834 845 845 834 844 836 837 846 +846 837 847 846 607 601 846 601 836 +609 838 605 838 609 611 848 838 611 +839 838 848 839 849 840 839 848 849 +840 850 841 840 849 850 841 851 842 +841 850 851 852 843 853 843 851 853 +843 842 851 843 852 844 844 852 854 +844 854 845 855 856 857 855 857 847 +847 857 846 857 613 846 613 612 846 +846 612 607 617 848 611 848 617 619 +858 848 619 849 848 858 849 859 850 +849 858 859 850 860 851 850 859 860 +853 851 861 861 851 860 857 856 862 +862 856 863 862 621 613 862 613 857 +624 864 623 858 619 623 864 858 623 +865 858 864 858 866 859 858 865 866 +867 860 868 860 866 868 860 859 866 +860 867 861 862 863 869 869 863 870 +869 625 621 869 621 862 627 864 624 +864 627 628 871 864 628 865 864 871 +865 872 866 865 871 872 868 866 873 +873 866 872 869 870 629 870 630 629 +870 874 630 869 629 625 633 871 628 +871 633 634 875 871 634 872 871 875 +873 872 876 876 872 875 874 635 630 +877 635 874 637 878 875 637 875 634 +876 875 879 879 875 878 877 638 635 +880 638 877 640 881 878 640 878 637 +879 878 882 882 878 881 883 884 885 +880 883 885 641 880 885 638 880 641 +642 886 881 642 881 640 882 881 887 +887 881 886 885 884 888 888 884 889 +888 643 641 888 641 885 890 891 886 +642 890 886 644 890 642 886 891 887 +889 892 888 643 888 892 893 643 892 +646 643 893 648 890 644 648 894 890 +649 646 895 646 896 895 646 893 896 +895 650 649 652 897 653 897 898 653 +898 648 653 898 894 648 896 899 895 +895 899 650 899 654 650 899 900 654 +900 901 902 655 900 902 654 900 655 +902 656 655 658 903 897 658 897 652 +898 897 904 904 897 903 902 901 905 +905 901 906 905 660 656 905 656 902 +661 907 662 662 907 908 909 662 908 +664 662 909 658 664 903 664 910 903 +664 909 910 903 910 904 906 911 905 +911 912 913 911 913 665 905 911 665 +660 905 665 914 666 665 914 665 913 +915 667 666 915 666 914 915 668 667 +670 916 671 907 661 671 916 907 671 +917 907 916 908 907 918 918 907 917 +912 919 913 913 919 914 914 919 920 +921 922 920 922 914 920 922 915 914 +923 672 668 915 923 668 922 923 915 +916 670 672 923 916 672 924 916 923 +925 917 926 917 924 926 917 916 924 +917 925 918 921 927 922 922 927 923 +923 927 928 923 928 924 924 928 929 +924 929 926 930 931 932 933 934 932 +932 934 930 933 935 934 932 931 936 +936 931 937 932 938 933 932 936 938 +935 933 939 933 940 939 933 938 940 +940 941 939 942 943 944 945 946 944 +944 946 942 947 948 945 945 948 946 +947 949 948 936 937 950 950 937 951 +936 952 938 936 950 952 938 953 940 +938 952 953 941 940 954 954 940 953 +955 956 957 955 957 943 957 944 943 +957 958 944 958 674 673 945 958 673 +944 958 945 945 673 676 959 945 676 +947 945 959 949 947 960 947 961 960 +947 959 961 961 962 960 950 951 963 +963 951 964 950 965 952 950 963 965 +952 966 953 952 965 966 954 953 967 +967 953 966 957 956 968 968 956 969 +968 678 677 958 968 677 957 968 958 +958 677 674 681 959 676 959 681 683 +970 959 683 961 959 970 962 961 971 +961 972 971 961 970 972 972 973 971 +974 975 976 974 976 964 976 963 964 +976 977 963 963 978 965 963 977 978 +965 979 966 965 978 979 967 966 980 +980 966 979 968 969 981 981 969 982 +981 685 678 981 678 968 688 983 970 +688 970 683 970 984 972 970 983 984 +973 972 985 972 986 985 972 984 986 +986 987 985 976 975 988 988 975 989 +976 990 977 976 988 990 977 991 978 +977 990 991 978 992 979 978 991 992 +980 979 993 993 979 992 981 982 994 +994 982 995 994 690 685 994 685 981 +693 983 688 983 693 695 996 983 695 +984 983 996 984 997 986 984 996 997 +987 986 998 998 986 997 999 1000 1001 +999 1001 989 1001 988 989 1001 1002 988 +988 1003 990 988 1002 1003 990 1004 991 +990 1003 1004 991 1005 992 991 1004 1005 +993 992 1006 1006 992 1005 1007 1008 1009 +1007 1009 995 995 1009 994 1009 697 994 +697 696 994 994 696 690 703 1010 996 +703 996 695 996 1011 997 996 1010 1011 +998 997 1012 1012 997 1011 1001 1000 1013 +1013 1000 1014 1013 705 704 1002 1013 704 +1001 1013 1002 1002 704 707 1015 1002 707 +1003 1002 1015 1003 1016 1004 1003 1015 1016 +1004 1017 1005 1004 1016 1017 1006 1005 1018 +1018 1005 1017 1009 1008 1019 1019 1008 1020 +1019 709 697 1019 697 1009 714 1021 1010 +714 1010 703 1010 1022 1011 1010 1021 1022 +1012 1011 1023 1023 1011 1022 1024 1025 1026 +1024 1026 1014 1014 1026 1013 1026 716 1013 +716 715 1013 1013 715 705 719 1027 1015 +719 1015 707 1015 1028 1016 1015 1027 1028 +1029 1017 1030 1017 1028 1030 1017 1016 1028 +1017 1029 1018 1019 1020 1031 1031 1020 1032 +1031 721 709 1031 709 1019 726 1033 1021 +726 1021 714 1021 1034 1022 1021 1033 1034 +1023 1022 1035 1035 1022 1034 1026 1025 1036 +1036 1025 1037 1036 728 716 1036 716 1026 +730 1027 719 1027 730 732 1038 1027 732 +1028 1027 1038 1030 1028 1039 1039 1028 1038 +1032 1040 1031 721 1031 1040 1041 721 1040 +734 721 1041 739 1042 1033 739 1033 726 +1033 1043 1034 1033 1042 1043 1035 1034 1044 +1044 1034 1043 1036 1037 1045 1045 1037 1046 +1045 741 728 1045 728 1036 744 1038 732 +744 746 1038 746 1039 1038 746 1047 1039 +747 734 1048 734 1049 1048 734 1041 1049 +1048 748 747 753 1050 1042 753 1042 739 +1051 1043 1052 1043 1050 1052 1043 1042 1050 +1043 1051 1044 1045 1046 1053 1053 1046 1054 +1053 755 741 1053 741 1045 759 1055 1056 +1047 759 1056 746 759 1047 1055 1057 1056 +1048 1049 1058 1058 1049 1059 1058 761 748 +1058 748 1048 765 1060 1050 765 1050 753 +1052 1050 1061 1061 1050 1060 1053 1054 1062 +1062 1054 1063 1064 766 755 1053 1064 755 +1062 1064 1053 1064 767 766 771 1065 770 +1055 759 770 1065 1055 770 1066 1055 1065 +1057 1055 1067 1067 1055 1066 1059 1068 1058 +1068 1069 1070 1068 1070 772 1058 1068 772 +761 1058 772 1070 773 772 776 1071 775 +1072 777 775 1072 775 1071 1060 765 777 +1072 1060 777 1073 1060 1072 1061 1060 1074 +1074 1060 1073 1063 1075 1062 1076 1077 1075 +1077 1062 1075 1077 1064 1062 1078 778 767 +1064 1078 767 1077 1078 1064 1079 779 778 +1079 778 1078 1065 771 779 1079 1065 779 +1080 1065 1079 1065 1081 1066 1065 1080 1081 +1067 1066 1082 1082 1066 1081 1070 1069 1083 +1083 1069 1084 1085 780 773 1070 1085 773 +1083 1085 1070 1071 776 780 1085 1071 780 +1086 1071 1085 1071 1087 1072 1071 1086 1087 +1088 1073 1089 1073 1087 1089 1073 1072 1087 +1073 1088 1074 1076 1090 1077 1091 1092 1090 +1092 1077 1090 1092 1078 1077 1078 1093 1079 +1078 1092 1093 1079 1094 1080 1079 1093 1094 +1080 1095 1081 1080 1094 1095 1082 1081 1096 +1096 1081 1095 1084 1097 1083 1098 1099 1097 +1099 1083 1097 1099 1085 1083 1085 1100 1086 +1085 1099 1100 1101 1087 1102 1087 1100 1102 +1087 1086 1100 1087 1101 1089 1091 1103 1092 +1092 1103 1093 1093 1103 1104 1093 1104 1094 +1094 1104 1105 1094 1105 1095 1095 1105 1106 +1095 1106 1096 1098 1107 1099 1099 1107 1100 +1100 1107 1108 1100 1108 1102 1109 1110 1111 +1112 1113 1111 1111 1113 1109 1114 1115 1112 +1112 1115 1113 1114 1116 1115 1117 1118 1119 +782 781 1119 781 1117 1119 781 1120 1117 +1121 1122 784 1122 781 784 1122 1120 781 +1122 1121 1110 1121 1111 1110 1121 1123 1111 +1111 1124 1112 1111 1123 1124 1112 1125 1114 +1112 1124 1125 1116 1114 1126 1114 1127 1126 +1114 1125 1127 1127 1128 1126 1129 1130 1131 +1129 1131 1118 1118 1131 1119 1131 786 1119 +786 785 1119 1119 785 782 789 1121 784 +1121 789 791 1132 1121 791 1123 1121 1132 +1123 1133 1124 1123 1132 1133 1124 1134 1125 +1124 1133 1134 1125 1135 1127 1125 1134 1135 +1128 1127 1136 1136 1127 1135 1137 793 792 +796 1138 792 792 1138 1137 1138 796 1139 +796 1140 1139 796 798 1140 798 799 1140 +1140 799 1141 1142 1143 1144 801 800 1144 +800 1142 1144 800 1145 1142 804 1131 1130 +1145 804 1130 800 804 1145 1131 804 786 +808 1132 791 1132 808 810 1146 1132 810 +1133 1132 1146 1133 1147 1134 1133 1146 1147 +1134 1148 1135 1134 1147 1148 1136 1135 1149 +1149 1135 1148 1150 1151 811 811 1151 812 +811 793 1137 1152 811 1137 1150 811 1152 +1152 1137 1138 1152 1138 1153 1153 1138 1139 +1154 1141 799 1154 799 813 815 1144 1143 +1154 815 1143 813 815 1154 1144 815 801 +822 1155 1146 822 1146 810 1146 1156 1147 +1146 1155 1156 1147 1157 1148 1147 1156 1157 +1149 1148 1158 1158 1148 1157 1159 824 823 +1160 1159 823 812 1160 823 1151 1160 812 +833 1155 822 1155 833 835 1161 1155 835 +1156 1155 1161 1156 1162 1157 1156 1161 1162 +1158 1157 1163 1163 1157 1162 1164 1165 1159 +1165 824 1159 1165 837 824 1164 1159 1160 +845 1166 1161 845 1161 835 1161 1167 1162 +1161 1166 1167 1163 1162 1168 1168 1162 1167 +1165 847 837 1169 847 1165 853 1170 852 +1171 854 852 1171 852 1170 1171 1172 854 +854 1172 845 1172 1173 845 1173 1166 845 +1174 1175 1173 1175 1166 1173 1175 1167 1166 +1168 1167 1176 1176 1167 1175 1177 1178 855 +855 1178 856 855 847 1169 855 1169 1177 +1179 1180 1170 853 1179 1170 861 1179 853 +1170 1180 1171 1171 1180 1181 1171 1181 1172 +1174 1182 1175 1175 1182 1176 1178 863 856 +1183 863 1178 868 1184 867 1184 1185 867 +1185 861 867 1185 1179 861 1183 870 863 +1186 870 1183 873 1187 1184 873 1184 868 +1185 1184 1188 1188 1184 1187 1186 874 870 +1189 874 1186 876 1190 1187 876 1187 873 +1188 1187 1191 1191 1187 1190 1189 877 874 +1192 877 1189 879 1193 1190 879 1190 876 +1191 1190 1194 1194 1190 1193 1195 1196 1197 +1192 1195 1197 880 1192 1197 877 1192 880 +1198 1199 1193 879 1198 1193 882 1198 879 +1193 1199 1194 1197 1196 883 1196 884 883 +1196 1200 884 1197 883 880 887 1198 882 +887 1201 1198 1200 889 884 1202 889 1200 +890 1203 891 1203 1204 891 1204 887 891 +1204 1201 887 892 889 1205 889 1206 1205 +889 1202 1206 1205 893 892 894 1207 1203 +894 1203 890 1204 1203 1208 1208 1203 1207 +1206 1209 1205 893 1205 1209 1210 893 1209 +896 893 1210 1211 1212 1207 894 1211 1207 +898 1211 894 1207 1212 1208 899 896 1213 +896 1214 1213 896 1210 1214 1215 900 899 +1215 899 1213 1215 901 900 904 1211 898 +904 1216 1211 1214 1217 1213 1218 1219 1217 +1219 1213 1217 1219 1215 1213 1219 906 901 +1219 901 1215 908 1220 909 909 1220 1221 +1222 909 1221 910 909 1222 904 910 1216 +1216 910 1222 1218 1223 1219 1223 1224 1225 +1223 1225 911 1219 1223 911 906 1219 911 +1225 912 911 1226 1227 1220 908 1226 1220 +918 1226 908 1220 1227 1221 1224 1228 1225 +1228 1229 1230 1228 1230 919 1225 1228 919 +912 1225 919 1231 920 919 1231 919 1230 +1231 921 920 926 1232 925 1232 1233 925 +1233 918 925 1233 1226 918 1229 1234 1230 +1230 1234 1231 1231 1234 1235 1235 1236 1237 +1235 1237 927 1231 1235 927 921 1231 927 +1238 928 927 1238 927 1237 1239 929 928 +1239 928 1238 1239 1240 929 929 1240 926 +1240 1241 926 1241 1232 926 1232 1241 1233 +1236 1242 1237 1237 1242 1238 1238 1242 1243 +1238 1243 1239 1239 1243 1244 1239 1244 1240 +1245 1246 1247 1248 1249 1247 1247 1249 1245 +1248 1250 1249 1251 1252 1253 1251 1253 1246 +1253 1247 1246 1253 1254 1247 1247 1255 1248 +1247 1254 1255 1250 1248 1256 1248 1257 1256 +1248 1255 1257 1257 1258 1256 1259 1260 1261 +1262 1263 1261 1261 1263 1259 1262 1264 1263 +1253 1252 1265 1265 1252 1266 1265 931 930 +1254 1265 930 1253 1265 1254 1254 930 934 +1254 934 1255 1255 934 935 1267 1255 935 +1257 1255 1267 1258 1257 1268 1268 1257 1267 +1269 1270 1271 1269 1271 1260 1271 1261 1260 +1271 1272 1261 1261 1273 1262 1261 1272 1273 +1264 1262 1274 1262 1275 1274 1262 1273 1275 +1275 1276 1274 1265 1266 1277 1277 1266 1278 +1277 937 931 1277 931 1265 939 1267 935 +1279 1280 941 941 1280 939 1280 1268 939 +1268 1267 939 1279 1281 1280 1271 1270 1282 +1282 1270 1283 1282 943 942 1272 1282 942 +1271 1282 1272 1272 942 946 1272 946 1273 +1273 946 948 1273 948 1275 1284 1285 949 +949 1285 948 1285 1276 948 1276 1275 948 +1286 1287 1284 1284 1287 1285 1286 1288 1287 +1289 1290 1291 1289 1291 1278 1291 1277 1278 +1291 1292 1277 1292 951 937 1292 937 1277 +954 1293 1279 954 1279 941 1281 1279 1294 +1294 1279 1293 1282 1283 955 1283 956 955 +1283 1295 956 1282 955 943 960 1284 949 +1284 960 962 1296 1284 962 1286 1284 1296 +1288 1286 1297 1286 1298 1297 1286 1296 1298 +1298 1299 1297 1300 1301 1302 1302 1303 1300 +1291 1290 1304 1304 1290 1305 1291 1306 1292 +1291 1304 1306 1306 964 951 1306 951 1292 +967 1307 1293 967 1293 954 1294 1293 1308 +1308 1293 1307 1295 969 956 1309 969 1295 +971 1296 962 1296 971 973 1310 1296 973 +1298 1296 1310 1299 1298 1311 1298 1312 1311 +1298 1310 1312 1312 1313 1311 1302 1301 1314 +1314 1301 1315 1303 1302 1316 1316 1302 1314 +1304 1305 1317 1317 1305 1318 1317 975 974 +1306 1317 974 1304 1317 1306 1306 974 964 +980 1319 1307 980 1307 967 1308 1307 1320 +1320 1307 1319 1309 982 969 1321 982 1309 +985 1310 973 1310 985 987 1322 1310 987 +1312 1310 1322 1313 1312 1323 1323 1312 1322 +1314 1315 1324 1324 1315 1325 1316 1314 1326 +1314 1327 1326 1314 1324 1327 1326 1327 1318 +1327 1317 1318 1327 1328 1317 1328 989 975 +1328 975 1317 993 1329 1319 993 1319 980 +1320 1319 1330 1330 1319 1329 1331 1332 1333 +1321 1331 1333 995 1321 1333 982 1321 995 +998 1334 1322 998 1322 987 1323 1322 1335 +1322 1336 1335 1322 1334 1336 1336 1337 1335 +1324 1325 1338 1338 1325 1339 1324 1340 1327 +1324 1338 1340 1340 1000 999 1328 1340 999 +1327 1340 1328 1328 999 989 1006 1341 1329 +1006 1329 993 1330 1329 1342 1342 1329 1341 +1333 1332 1007 1332 1008 1007 1332 1343 1008 +1333 1007 995 1012 1344 1334 1012 1334 998 +1345 1336 1346 1336 1344 1346 1336 1334 1344 +1336 1345 1337 1347 1348 1349 1347 1349 1339 +1349 1338 1339 1349 1350 1338 1338 1351 1340 +1338 1350 1351 1351 1014 1000 1351 1000 1340 +1018 1352 1341 1018 1341 1006 1342 1341 1353 +1353 1341 1352 1343 1020 1008 1354 1020 1343 +1023 1355 1344 1023 1344 1012 1346 1344 1356 +1344 1357 1356 1344 1355 1357 1357 1358 1356 +1349 1348 1359 1359 1348 1360 1349 1361 1350 +1349 1359 1361 1361 1025 1024 1351 1361 1024 +1350 1361 1351 1351 1024 1014 1030 1362 1029 +1362 1363 1029 1029 1363 1018 1363 1364 1018 +1364 1352 1018 1352 1364 1353 1354 1032 1020 +1365 1032 1354 1035 1366 1355 1035 1355 1023 +1355 1367 1357 1355 1366 1367 1358 1357 1368 +1368 1357 1367 1359 1360 1369 1369 1360 1370 +1359 1371 1361 1359 1369 1371 1371 1037 1025 +1371 1025 1361 1039 1372 1362 1039 1362 1030 +1363 1362 1373 1373 1362 1372 1040 1032 1374 +1032 1375 1374 1032 1365 1375 1374 1041 1040 +1044 1376 1366 1044 1366 1035 1377 1367 1378 +1367 1376 1378 1367 1366 1376 1367 1377 1368 +1369 1370 1379 1379 1370 1380 1369 1381 1371 +1369 1379 1381 1381 1046 1037 1381 1037 1371 +1047 1382 1372 1047 1372 1039 1373 1372 1383 +1383 1372 1382 1375 1384 1374 1041 1374 1384 +1385 1041 1384 1049 1041 1385 1052 1386 1051 +1376 1044 1051 1386 1376 1051 1387 1376 1386 +1378 1376 1388 1388 1376 1387 1380 1389 1379 +1390 1391 1389 1391 1379 1389 1391 1381 1379 +1391 1054 1046 1391 1046 1381 1056 1382 1047 +1056 1057 1382 1057 1383 1382 1057 1392 1383 +1385 1059 1049 1393 1059 1385 1061 1394 1386 +1061 1386 1052 1395 1387 1396 1387 1394 1396 +1387 1386 1394 1387 1395 1388 1391 1390 1397 +1397 1390 1398 1397 1063 1054 1397 1054 1391 +1067 1392 1057 1067 1399 1392 1068 1059 1400 +1059 1401 1400 1059 1393 1401 1400 1069 1068 +1074 1402 1394 1074 1394 1061 1396 1394 1403 +1403 1394 1402 1398 1404 1397 1404 1405 1406 +1404 1406 1075 1397 1404 1075 1063 1397 1075 +1406 1076 1075 1082 1399 1067 1082 1407 1399 +1400 1401 1408 1408 1401 1409 1408 1084 1069 +1408 1069 1400 1089 1410 1088 1402 1074 1088 +1410 1402 1088 1411 1402 1410 1403 1402 1412 +1412 1402 1411 1406 1405 1413 1413 1405 1414 +1415 1090 1076 1406 1415 1076 1413 1415 1406 +1415 1091 1090 1096 1407 1082 1096 1416 1407 +1408 1409 1417 1417 1409 1418 1419 1097 1084 +1408 1419 1084 1417 1419 1408 1419 1098 1097 +1102 1420 1101 1410 1089 1101 1420 1410 1101 +1421 1410 1420 1422 1411 1423 1411 1421 1423 +1411 1410 1421 1411 1422 1412 1414 1424 1413 +1425 1426 1424 1426 1413 1424 1426 1415 1413 +1427 1103 1091 1415 1427 1091 1426 1427 1415 +1428 1104 1103 1428 1103 1427 1429 1105 1104 +1429 1104 1428 1430 1106 1105 1430 1105 1429 +1430 1431 1106 1431 1096 1106 1431 1416 1096 +1417 1418 1432 1432 1418 1433 1417 1434 1419 +1417 1432 1434 1435 1107 1098 1419 1435 1098 +1434 1435 1419 1436 1108 1107 1436 1107 1435 +1420 1102 1108 1436 1420 1108 1437 1420 1436 +1438 1421 1439 1421 1437 1439 1421 1420 1437 +1421 1438 1423 1425 1440 1426 1426 1440 1427 +1427 1440 1441 1442 1443 1441 1443 1427 1441 +1443 1428 1427 1428 1444 1429 1428 1443 1444 +1429 1445 1430 1429 1444 1445 1431 1430 1446 +1446 1430 1445 1432 1433 1447 1447 1433 1448 +1432 1449 1434 1432 1447 1449 1450 1435 1451 +1435 1449 1451 1435 1434 1449 1435 1450 1436 +1436 1450 1452 1436 1452 1437 1437 1452 1453 +1437 1453 1439 1442 1454 1443 1455 1456 1454 +1456 1443 1454 1456 1444 1443 1444 1457 1445 +1444 1456 1457 1446 1445 1458 1458 1445 1457 +1448 1459 1447 1460 1461 1459 1461 1447 1459 +1461 1449 1447 1451 1449 1462 1449 1463 1462 +1449 1461 1463 1463 1464 1462 1455 1465 1456 +1456 1465 1457 1457 1465 1466 1457 1466 1458 +1460 1467 1461 1461 1467 1463 1463 1467 1468 +1463 1468 1464 1469 1470 1471 1472 1473 1471 +1471 1473 1469 1474 1475 1472 1472 1475 1473 +1474 1476 1475 1477 1478 1479 1480 1481 1479 +1479 1481 1477 1481 1480 1470 1470 1480 1471 +1480 1110 1471 1110 1109 1471 1471 1109 1113 +1471 1113 1472 1472 1113 1115 1472 1115 1474 +1115 1116 1474 1116 1476 1474 1116 1482 1476 +1483 1484 1117 1117 1484 1118 1120 1479 1478 +1483 1120 1478 1117 1120 1483 1479 1120 1122 +1479 1122 1480 1480 1122 1110 1485 1482 1116 +1485 1116 1126 1126 1128 1485 1485 1128 1486 +1487 1488 1129 1129 1488 1130 1129 1118 1484 +1129 1484 1487 1136 1489 1490 1486 1136 1490 +1128 1136 1486 1489 1491 1490 1492 1139 1140 +1141 1492 1140 1493 1143 1142 1493 1142 1494 +1142 1495 1494 1142 1145 1495 1145 1130 1488 +1145 1488 1495 1149 1496 1489 1149 1489 1136 +1491 1489 1497 1497 1489 1496 1498 1151 1150 +1152 1499 1150 1150 1499 1498 1499 1152 1500 +1152 1501 1500 1152 1153 1501 1153 1139 1492 +1502 1153 1492 1501 1153 1502 1502 1492 1141 +1154 1502 1141 1503 1502 1154 1154 1143 1493 +1504 1154 1493 1503 1154 1504 1504 1493 1494 +1158 1505 1496 1158 1496 1149 1497 1496 1506 +1506 1496 1505 1507 1508 1498 1508 1151 1498 +1508 1160 1151 1507 1498 1499 1507 1499 1509 +1509 1499 1500 1163 1510 1505 1163 1505 1158 +1506 1505 1511 1511 1505 1510 1512 1165 1164 +1513 1512 1164 1160 1513 1164 1508 1513 1160 +1168 1514 1510 1168 1510 1163 1511 1510 1515 +1515 1510 1514 1516 1517 1518 1519 1516 1512 +1516 1169 1512 1169 1165 1512 1516 1518 1169 +1519 1512 1513 1172 1520 1173 1520 1174 1173 +1521 1522 1514 1168 1521 1514 1176 1521 1168 +1514 1522 1515 1518 1517 1177 1517 1178 1177 +1517 1523 1178 1518 1177 1169 1179 1524 1180 +1525 1181 1180 1525 1180 1524 1525 1526 1181 +1181 1526 1172 1526 1527 1172 1527 1520 1172 +1520 1527 1174 1527 1182 1174 1527 1528 1182 +1176 1182 1521 1521 1182 1528 1523 1183 1178 +1529 1183 1523 1185 1530 1524 1185 1524 1179 +1524 1531 1525 1524 1530 1531 1526 1525 1532 +1532 1525 1531 1529 1186 1183 1533 1186 1529 +1188 1534 1530 1188 1530 1185 1530 1535 1531 +1530 1534 1535 1532 1531 1536 1536 1531 1535 +1533 1189 1186 1537 1189 1533 1191 1538 1534 +1191 1534 1188 1539 1535 1540 1535 1538 1540 +1535 1534 1538 1535 1539 1536 1537 1192 1189 +1541 1192 1537 1194 1542 1538 1194 1538 1191 +1540 1538 1543 1543 1538 1542 1544 1545 1195 +1195 1545 1196 1195 1192 1541 1195 1541 1544 +1198 1546 1199 1546 1547 1199 1199 1547 1194 +1547 1548 1194 1548 1542 1194 1542 1548 1543 +1545 1200 1196 1549 1200 1545 1201 1550 1546 +1201 1546 1198 1547 1546 1551 1551 1546 1550 +1549 1202 1200 1552 1202 1549 1204 1553 1550 +1204 1550 1201 1551 1550 1554 1554 1550 1553 +1552 1206 1202 1555 1206 1552 1556 1557 1553 +1204 1556 1553 1208 1556 1204 1553 1557 1554 +1209 1206 1558 1206 1559 1558 1206 1555 1559 +1558 1210 1209 1211 1560 1212 1560 1561 1212 +1561 1208 1212 1561 1556 1208 1558 1559 1562 +1562 1559 1563 1562 1214 1210 1562 1210 1558 +1564 1565 1560 1211 1564 1560 1216 1564 1211 +1560 1565 1561 1563 1566 1562 1566 1567 1568 +1566 1568 1217 1562 1566 1217 1214 1562 1217 +1568 1218 1217 1569 1570 1222 1569 1222 1221 +1216 1222 1564 1564 1222 1570 1567 1571 1568 +1571 1572 1573 1571 1573 1223 1568 1571 1223 +1218 1568 1223 1573 1224 1223 1226 1574 1227 +1574 1575 1227 1575 1221 1227 1575 1569 1221 +1572 1576 1573 1576 1577 1578 1576 1578 1228 +1573 1576 1228 1224 1573 1228 1578 1229 1228 +1233 1579 1574 1233 1574 1226 1575 1574 1580 +1580 1574 1579 1577 1581 1578 1578 1581 1229 +1581 1234 1229 1581 1582 1234 1582 1583 1584 +1235 1582 1584 1234 1582 1235 1584 1236 1235 +1240 1585 1241 1579 1233 1241 1585 1579 1241 +1586 1579 1585 1580 1579 1587 1587 1579 1586 +1583 1588 1584 1588 1589 1590 1588 1590 1242 +1584 1588 1242 1236 1584 1242 1591 1243 1242 +1591 1242 1590 1592 1244 1243 1592 1243 1591 +1592 1593 1244 1244 1593 1240 1593 1594 1240 +1594 1585 1240 1585 1594 1586 1586 1594 1595 +1586 1595 1587 1589 1596 1590 1590 1596 1591 +1591 1596 1597 1591 1597 1592 1592 1597 1598 +1592 1598 1593 1599 1600 1601 1602 1603 1601 +1601 1603 1599 1602 1604 1603 1605 1606 1607 +1608 1609 1607 1607 1609 1605 1609 1608 1600 +1608 1601 1600 1608 1610 1601 1601 1611 1602 +1601 1610 1611 1604 1602 1612 1612 1602 1611 +1607 1606 1613 1613 1606 1614 1607 1615 1608 +1607 1613 1615 1615 1246 1245 1610 1615 1245 +1608 1615 1610 1610 1245 1249 1610 1249 1611 +1616 1617 1250 1250 1617 1249 1617 1612 1249 +1612 1611 1249 1616 1618 1617 1619 1620 1621 +1622 1623 1621 1621 1623 1619 1624 1625 1622 +1622 1625 1623 1624 1626 1625 1627 1628 1629 +1627 1629 1614 1629 1613 1614 1629 1630 1613 +1630 1252 1251 1615 1630 1251 1613 1630 1615 +1615 1251 1246 1256 1616 1250 1631 1632 1258 +1258 1632 1256 1632 1618 1256 1618 1616 1256 +1631 1633 1632 1634 1635 1636 1634 1636 1620 +1620 1636 1621 1636 1260 1621 1260 1259 1621 +1621 1259 1263 1621 1263 1622 1622 1263 1264 +1637 1622 1264 1624 1622 1637 1626 1624 1638 +1624 1639 1638 1624 1637 1639 1639 1640 1638 +1629 1628 1641 1641 1628 1642 1629 1643 1630 +1629 1641 1643 1643 1266 1252 1643 1252 1630 +1268 1644 1631 1268 1631 1258 1633 1631 1645 +1645 1631 1644 1636 1635 1269 1635 1270 1269 +1635 1646 1270 1636 1269 1260 1274 1637 1264 +1637 1274 1276 1647 1637 1276 1639 1637 1647 +1640 1639 1648 1639 1649 1648 1639 1647 1649 +1650 1651 1649 1649 1651 1648 1652 1653 1650 +1650 1653 1651 1652 1654 1653 1655 1656 1657 +1657 1658 1655 1641 1642 1659 1659 1642 1660 +1641 1661 1643 1641 1659 1661 1661 1278 1266 +1661 1266 1643 1280 1644 1268 1280 1281 1644 +1281 1645 1644 1281 1662 1645 1663 1664 1665 +1646 1663 1665 1283 1646 1665 1270 1646 1283 +1285 1647 1276 1647 1285 1287 1647 1287 1649 +1649 1287 1288 1666 1649 1288 1650 1649 1666 +1650 1667 1652 1650 1666 1667 1654 1652 1668 +1652 1669 1668 1652 1667 1669 1669 1670 1668 +1657 1656 1671 1671 1656 1672 1658 1657 1673 +1657 1674 1673 1657 1671 1674 1674 1675 1673 +1659 1660 1676 1676 1660 1677 1676 1290 1289 +1661 1676 1289 1659 1676 1661 1661 1289 1278 +1294 1662 1281 1294 1678 1662 1664 1679 1665 +1283 1665 1679 1680 1283 1679 1295 1283 1680 +1297 1666 1288 1666 1297 1299 1681 1666 1299 +1667 1666 1681 1667 1682 1669 1667 1681 1682 +1670 1669 1683 1669 1684 1683 1669 1682 1684 +1684 1685 1683 1671 1672 1686 1686 1672 1687 +1671 1688 1674 1671 1686 1688 1300 1689 1301 +1689 1674 1301 1674 1688 1301 1674 1689 1675 +1690 1676 1303 1676 1689 1303 1689 1300 1303 +1676 1677 1689 1690 1305 1290 1690 1290 1676 +1308 1678 1294 1308 1691 1678 1680 1309 1295 +1692 1309 1680 1311 1681 1299 1681 1311 1313 +1693 1681 1313 1682 1681 1693 1682 1694 1684 +1682 1693 1694 1685 1684 1695 1695 1684 1694 +1686 1687 1696 1696 1687 1697 1686 1698 1688 +1686 1696 1698 1698 1315 1301 1698 1301 1688 +1316 1699 1690 1316 1690 1303 1699 1318 1305 +1699 1305 1690 1320 1691 1308 1320 1700 1691 +1692 1321 1309 1701 1321 1692 1323 1702 1693 +1323 1693 1313 1693 1703 1694 1693 1702 1703 +1695 1694 1704 1694 1705 1704 1694 1703 1705 +1705 1706 1704 1696 1697 1707 1707 1697 1708 +1696 1709 1698 1696 1707 1709 1709 1325 1315 +1709 1315 1698 1326 1699 1316 1699 1326 1318 +1330 1700 1320 1330 1710 1700 1711 1712 1331 +1331 1712 1332 1331 1321 1701 1331 1701 1711 +1335 1702 1323 1702 1335 1337 1713 1702 1337 +1703 1702 1713 1703 1714 1705 1703 1713 1714 +1706 1705 1715 1715 1705 1714 1707 1708 1716 +1716 1708 1717 1707 1718 1709 1707 1716 1718 +1718 1339 1325 1718 1325 1709 1342 1710 1330 +1342 1719 1710 1712 1343 1332 1720 1343 1712 +1346 1721 1345 1713 1337 1345 1721 1713 1345 +1722 1713 1721 1713 1723 1714 1713 1722 1723 +1715 1714 1724 1724 1714 1723 1725 1726 1727 +1728 1729 1727 1727 1729 1725 1729 1728 1717 +1728 1716 1717 1728 1730 1716 1730 1348 1347 +1718 1730 1347 1716 1730 1718 1718 1347 1339 +1353 1719 1342 1353 1731 1719 1720 1354 1343 +1732 1354 1720 1356 1721 1346 1721 1356 1358 +1733 1721 1358 1722 1721 1733 1722 1734 1723 +1722 1733 1734 1724 1723 1735 1735 1723 1734 +1727 1726 1736 1736 1726 1737 1727 1738 1728 +1727 1736 1738 1728 1739 1730 1728 1738 1739 +1739 1360 1348 1739 1348 1730 1740 1741 1364 +1740 1364 1363 1353 1364 1731 1731 1364 1741 +1732 1365 1354 1742 1365 1732 1368 1743 1733 +1368 1733 1358 1733 1744 1734 1733 1743 1744 +1735 1734 1745 1745 1734 1744 1737 1746 1736 +1747 1748 1746 1748 1736 1746 1748 1738 1736 +1738 1749 1739 1738 1748 1749 1749 1370 1360 +1749 1360 1739 1373 1740 1363 1373 1750 1740 +1742 1375 1365 1751 1375 1742 1378 1752 1377 +1743 1368 1377 1752 1743 1377 1753 1743 1752 +1754 1744 1755 1744 1753 1755 1744 1743 1753 +1744 1754 1745 1748 1747 1756 1756 1747 1757 +1748 1758 1749 1748 1756 1758 1758 1380 1370 +1758 1370 1749 1383 1750 1373 1383 1759 1750 +1751 1760 1375 1375 1760 1384 1385 1384 1760 +1385 1760 1761 1388 1762 1752 1388 1752 1378 +1752 1763 1753 1752 1762 1763 1755 1753 1764 +1764 1753 1763 1757 1765 1756 1766 1767 1765 +1767 1756 1765 1767 1758 1756 1768 1389 1380 +1758 1768 1380 1767 1768 1758 1768 1390 1389 +1392 1759 1383 1392 1769 1759 1761 1393 1385 +1770 1393 1761 1396 1771 1395 1762 1388 1395 +1771 1762 1395 1772 1762 1771 1773 1763 1774 +1763 1772 1774 1763 1762 1772 1763 1773 1764 +1766 1775 1767 1776 1777 1775 1777 1767 1775 +1777 1768 1767 1777 1398 1390 1777 1390 1768 +1399 1769 1392 1399 1778 1769 1770 1401 1393 +1779 1401 1770 1403 1780 1771 1403 1771 1396 +1771 1781 1772 1771 1780 1781 1774 1772 1782 +1782 1772 1781 1776 1783 1777 1783 1784 1785 +1783 1785 1404 1777 1783 1404 1398 1777 1404 +1785 1405 1404 1407 1786 1787 1778 1407 1787 +1399 1407 1778 1786 1788 1787 1779 1409 1401 +1789 1409 1779 1412 1790 1780 1412 1780 1403 +1791 1781 1792 1781 1790 1792 1781 1780 1790 +1781 1791 1782 1785 1784 1793 1793 1784 1794 +1793 1414 1405 1793 1405 1785 1416 1795 1786 +1416 1786 1407 1788 1786 1796 1796 1786 1795 +1789 1418 1409 1797 1418 1789 1423 1798 1422 +1798 1799 1422 1422 1799 1412 1799 1800 1412 +1800 1790 1412 1790 1800 1792 1794 1801 1793 +1801 1802 1803 1801 1803 1424 1793 1801 1424 +1414 1793 1424 1803 1425 1424 1431 1804 1795 +1431 1795 1416 1796 1795 1805 1805 1795 1804 +1797 1433 1418 1806 1433 1797 1439 1807 1438 +1807 1808 1438 1438 1808 1423 1808 1809 1423 +1809 1798 1423 1798 1809 1799 1802 1810 1803 +1810 1811 1812 1810 1812 1440 1803 1810 1440 +1425 1803 1440 1813 1441 1440 1813 1440 1812 +1813 1442 1441 1446 1814 1804 1446 1804 1431 +1805 1804 1815 1815 1804 1814 1816 1817 1818 +1806 1816 1818 1448 1806 1818 1433 1806 1448 +1451 1819 1450 1820 1452 1450 1820 1450 1819 +1821 1453 1452 1821 1452 1820 1821 1822 1453 +1453 1822 1439 1822 1823 1439 1823 1807 1439 +1807 1823 1808 1811 1824 1812 1825 1826 1824 +1826 1812 1824 1826 1813 1812 1827 1454 1442 +1813 1827 1442 1826 1827 1813 1827 1455 1454 +1458 1828 1814 1458 1814 1446 1815 1814 1829 +1829 1814 1828 1817 1830 1818 1830 1831 1832 +1830 1832 1459 1818 1830 1459 1448 1818 1459 +1832 1460 1459 1462 1819 1451 1819 1462 1464 +1833 1819 1464 1820 1819 1833 1834 1821 1835 +1821 1833 1835 1821 1820 1833 1821 1834 1822 +1826 1825 1836 1836 1825 1837 1826 1838 1827 +1826 1836 1838 1839 1465 1455 1827 1839 1455 +1838 1839 1827 1840 1466 1465 1840 1465 1839 +1840 1841 1466 1466 1841 1458 1841 1842 1458 +1842 1828 1458 1828 1842 1829 1831 1843 1832 +1843 1844 1845 1843 1845 1467 1832 1843 1467 +1460 1832 1467 1846 1468 1467 1846 1467 1845 +1846 1847 1468 1468 1847 1464 1847 1848 1464 +1848 1833 1464 1833 1848 1835 1837 1849 1836 +1850 1851 1849 1851 1836 1849 1851 1838 1836 +1838 1852 1839 1838 1851 1852 1839 1853 1840 +1839 1852 1853 1841 1840 1854 1854 1840 1853 +1844 1855 1845 1845 1855 1846 1846 1855 1856 +1856 1857 1846 1857 1858 1846 1859 1847 1846 +1859 1846 1858 1858 1860 1859 1850 1861 1851 +1862 1863 1861 1863 1851 1861 1863 1852 1851 +1864 1853 1865 1853 1863 1865 1853 1852 1863 +1853 1864 1854 1857 1866 1858 1858 1866 1860 +1862 1867 1863 1863 1867 1865 1868 1869 1469 +1469 1869 1470 1870 1469 1473 1870 1868 1469 +1871 1473 1475 1871 1870 1473 1475 1476 1871 +1871 1476 1872 1873 1874 1477 1477 1874 1478 +1875 1477 1481 1875 1873 1477 1481 1470 1869 +1481 1869 1875 1482 1872 1476 1482 1876 1872 +1877 1484 1483 1878 1877 1483 1478 1878 1483 +1874 1878 1478 1879 1876 1482 1879 1482 1485 +1485 1486 1879 1879 1486 1880 1881 1488 1487 +1882 1881 1487 1882 1487 1484 1883 1882 1484 +1877 1883 1484 1883 1877 1878 1884 1880 1486 +1884 1486 1490 1490 1491 1884 1884 1491 1885 +1886 1887 1495 1495 1887 1494 1495 1488 1881 +1888 1495 1881 1886 1495 1888 1888 1881 1882 +1497 1885 1491 1497 1889 1885 1890 1500 1501 +1502 1891 1501 1501 1891 1890 1503 1892 1502 +1502 1892 1891 1892 1503 1893 1503 1894 1893 +1503 1504 1894 1504 1494 1887 1504 1887 1894 +1506 1889 1497 1506 1895 1889 1896 1508 1507 +1509 1897 1507 1507 1897 1896 1898 1897 1509 +1898 1509 1500 1899 1898 1500 1890 1899 1500 +1900 1901 1899 1890 1900 1899 1891 1900 1890 +1892 1902 1903 1900 1892 1903 1891 1892 1900 +1902 1892 1893 1511 1904 1905 1895 1511 1905 +1506 1511 1895 1904 1906 1905 1907 1513 1896 +1896 1513 1508 1897 1908 1909 1907 1897 1909 +1896 1897 1907 1908 1897 1898 1910 1901 1900 +1910 1900 1903 1515 1911 1904 1515 1904 1511 +1906 1904 1912 1912 1904 1911 1913 1517 1516 +1913 1516 1914 1516 1915 1914 1516 1519 1915 +1519 1513 1907 1916 1519 1907 1915 1519 1916 +1916 1907 1909 1521 1917 1522 1917 1918 1522 +1522 1918 1515 1918 1919 1515 1919 1911 1515 +1911 1919 1912 1920 1523 1913 1913 1523 1517 +1913 1914 1921 1913 1921 1920 1526 1922 1527 +1923 1528 1527 1923 1527 1922 1923 1924 1528 +1528 1924 1521 1924 1925 1521 1925 1917 1521 +1917 1925 1918 1926 1529 1920 1920 1529 1523 +1920 1921 1927 1920 1927 1926 1532 1928 1922 +1532 1922 1526 1922 1929 1923 1922 1928 1929 +1924 1923 1930 1923 1931 1930 1923 1929 1931 +1931 1932 1930 1933 1934 1926 1934 1529 1926 +1934 1533 1529 1933 1926 1927 1536 1935 1928 +1536 1928 1532 1928 1936 1929 1928 1935 1936 +1937 1931 1938 1931 1936 1938 1931 1929 1936 +1931 1937 1932 1934 1537 1533 1939 1537 1934 +1540 1940 1539 1935 1536 1539 1940 1935 1539 +1941 1935 1940 1935 1942 1936 1935 1941 1942 +1938 1936 1943 1943 1936 1942 1939 1541 1537 +1944 1541 1939 1543 1945 1940 1543 1940 1540 +1940 1946 1941 1940 1945 1946 1947 1942 1948 +1942 1946 1948 1942 1941 1946 1942 1947 1943 +1949 1950 1544 1544 1950 1545 1544 1541 1944 +1544 1944 1949 1547 1951 1548 1945 1543 1548 +1951 1945 1548 1952 1945 1951 1953 1946 1954 +1946 1952 1954 1946 1945 1952 1946 1953 1948 +1950 1549 1545 1955 1549 1950 1551 1956 1951 +1551 1951 1547 1957 1952 1958 1952 1956 1958 +1952 1951 1956 1952 1957 1954 1955 1552 1549 +1959 1552 1955 1554 1960 1956 1554 1956 1551 +1958 1956 1961 1961 1956 1960 1959 1555 1552 +1962 1555 1959 1556 1963 1557 1963 1964 1557 +1557 1964 1554 1964 1965 1554 1965 1960 1554 +1960 1965 1961 1966 1967 1968 1962 1966 1968 +1559 1962 1968 1555 1962 1559 1561 1969 1963 +1561 1963 1556 1964 1963 1970 1970 1963 1969 +1968 1967 1971 1971 1967 1972 1971 1563 1559 +1971 1559 1968 1973 1974 1565 1973 1565 1564 +1561 1565 1969 1565 1975 1969 1565 1974 1975 +1969 1975 1970 1972 1976 1971 1976 1977 1978 +1976 1978 1566 1971 1976 1566 1563 1971 1566 +1978 1567 1566 1569 1979 1570 1979 1980 1570 +1980 1564 1570 1980 1973 1564 1977 1981 1978 +1981 1982 1983 1981 1983 1571 1978 1981 1571 +1567 1978 1571 1983 1572 1571 1575 1984 1979 +1575 1979 1569 1980 1979 1985 1985 1979 1984 +1982 1986 1983 1986 1987 1988 1986 1988 1576 +1983 1986 1576 1572 1983 1576 1988 1577 1576 +1580 1989 1984 1580 1984 1575 1985 1984 1990 +1990 1984 1989 1987 1991 1988 1991 1992 1993 +1991 1993 1581 1988 1991 1581 1577 1988 1581 +1994 1582 1581 1994 1581 1993 1994 1583 1582 +1587 1995 1989 1587 1989 1580 1990 1989 1996 +1996 1989 1995 1997 1998 1999 1999 2000 1997 +1992 2001 1993 1993 2001 1994 1994 2001 2002 +2002 2003 2004 2002 2004 1588 1994 2002 1588 +1583 1994 1588 2004 1589 1588 1593 2005 1594 +2006 1595 1594 2006 1594 2005 1995 1587 1595 +2006 1995 1595 2007 1995 2006 1996 1995 2008 +2008 1995 2007 1998 2009 1999 1999 2009 2000 +2004 2003 2010 2010 2003 2011 2012 1596 1589 +2004 2012 1589 2010 2012 2004 2013 1597 1596 +2013 1596 2012 2014 1598 1597 2014 1597 2013 +2005 1593 1598 2014 2005 1598 2015 2005 2014 +2005 2016 2006 2005 2015 2016 2017 2007 2018 +2007 2016 2018 2007 2006 2016 2007 2017 2008 +2011 2019 2010 2020 2021 2019 2021 2010 2019 +2021 2012 2010 2012 2022 2013 2012 2021 2022 +2013 2023 2014 2013 2022 2023 2014 2024 2015 +2014 2023 2024 2015 2025 2016 2015 2024 2025 +2018 2016 2026 2026 2016 2025 2020 2027 2021 +2021 2027 2022 2022 2027 2028 2022 2028 2023 +2023 2028 2029 2023 2029 2024 2024 2029 2030 +2024 2030 2025 2025 2030 2031 2031 2032 2025 +2032 2033 2025 2034 2026 2025 2034 2025 2033 +2035 2036 2033 2033 2036 2034 2037 2038 2035 +2035 2038 2036 2039 2040 2037 2037 2040 2038 +2039 2041 2040 2042 2043 2044 2045 2046 2044 +2044 2046 2042 2047 2048 2045 2045 2048 2046 +2047 2049 2048 2050 2051 2052 2050 2052 2032 +2052 2033 2032 2052 2053 2033 2033 2054 2035 +2033 2053 2054 2035 2055 2037 2035 2054 2055 +2037 2056 2039 2037 2055 2056 2041 2039 2057 +2039 2058 2057 2039 2056 2058 2058 2059 2057 +2060 2061 2062 2060 2062 2043 2062 2044 2043 +2062 2063 2044 2044 2064 2045 2044 2063 2064 +2045 2065 2047 2045 2064 2065 2049 2047 2066 +2066 2047 2065 2067 2068 2069 2067 2069 2051 +2069 2052 2051 2069 2070 2052 2052 2071 2053 +2052 2070 2071 2053 2072 2054 2053 2071 2072 +2054 2073 2055 2054 2072 2073 2073 1600 1599 +2056 2073 1599 2055 2073 2056 2056 1599 1603 +2056 1603 2058 2074 2075 1604 1604 2075 1603 +2075 2059 1603 2059 2058 1603 2074 2076 2075 +2077 2078 2079 2077 2079 2061 2079 2062 2061 +2079 2080 2062 2062 2081 2063 2062 2080 2081 +2063 2082 2064 2063 2081 2082 2064 2083 2065 +2064 2082 2083 2066 2065 2084 2065 2085 2084 +2065 2083 2085 2085 2086 2084 2069 2068 2087 +2087 2068 2088 2069 2089 2070 2069 2087 2089 +2070 2090 2071 2070 2089 2090 2090 1606 1605 +2072 2090 1605 2071 2090 2072 2072 1605 1609 +2072 1609 2073 2073 1609 1600 1612 2091 2074 +1612 2074 1604 2076 2074 2092 2092 2074 2091 +2093 2094 2095 2096 2097 2095 2095 2097 2093 +2098 2099 2096 2096 2099 2097 2100 2101 2098 +2098 2101 2099 2102 2103 2100 2100 2103 2101 +2104 2105 2102 2102 2105 2103 2106 2107 2104 +2104 2107 2105 2108 2109 2106 2106 2109 2107 +2110 2111 2108 2108 2111 2109 2111 2110 2078 +2110 2079 2078 2110 2112 2079 2079 2113 2080 +2079 2112 2113 2080 2114 2081 2080 2113 2114 +2081 2115 2082 2081 2114 2115 2082 2116 2083 +2082 2115 2116 2083 2117 2085 2083 2116 2117 +2086 2085 2118 2085 2119 2118 2085 2117 2119 +2119 2120 2118 2087 2088 2121 2121 2088 2122 +2087 2123 2089 2087 2121 2123 2089 2124 2090 +2089 2123 2124 2124 1614 1606 2124 1606 2090 +1617 2091 1612 2125 2126 1618 1618 2126 1617 +2126 2092 1617 2092 2091 1617 2125 2127 2126 +2095 2094 2128 2128 2094 2129 2128 1620 1619 +2096 2128 1619 2095 2128 2096 2096 1619 1623 +2096 1623 2098 2098 1623 1625 2098 1625 2100 +2100 1625 1626 2130 2100 1626 2102 2100 2130 +2102 2131 2104 2102 2130 2131 2104 2132 2106 +2104 2131 2132 2106 2133 2108 2106 2132 2133 +2108 2134 2110 2108 2133 2134 2110 2135 2112 +2110 2134 2135 2112 2136 2113 2112 2135 2136 +2113 2137 2114 2113 2136 2137 2114 2138 2115 +2114 2137 2138 2115 2139 2116 2115 2138 2139 +2116 2140 2117 2116 2139 2140 2117 2141 2119 +2117 2140 2141 2120 2119 2142 2119 2143 2142 +2119 2141 2143 2143 2144 2142 2121 2122 2145 +2145 2122 2146 2121 2147 2123 2121 2145 2147 +2147 1628 1627 2124 2147 1627 2123 2147 2124 +2124 1627 1614 1632 2125 1618 1632 1633 2125 +1633 2127 2125 1633 2148 2127 2149 2150 2151 +2149 2151 2129 2129 2151 2128 2151 1635 2128 +1635 1634 2128 2128 1634 1620 1638 2130 1626 +2130 1638 1640 2152 2130 1640 2131 2130 2152 +2131 2153 2132 2131 2152 2153 2132 2154 2133 +2132 2153 2154 2133 2155 2134 2133 2154 2155 +2134 2156 2135 2134 2155 2156 2135 2157 2136 +2135 2156 2157 2136 2158 2137 2136 2157 2158 +2137 2159 2138 2137 2158 2159 2138 2160 2139 +2138 2159 2160 2139 2161 2140 2139 2160 2161 +2140 2162 2141 2140 2161 2162 2141 2163 2143 +2141 2162 2163 2144 2143 2164 2143 2165 2164 +2143 2163 2165 2164 2165 2146 2165 2145 2146 +2165 2166 2145 2145 2167 2147 2145 2166 2167 +2167 1642 1628 2167 1628 2147 1645 2148 1633 +1645 2168 2148 2151 2150 2169 2169 2150 2170 +2169 1646 1635 2169 1635 2151 1648 2152 1640 +2152 1648 1651 2152 1651 2153 2153 1651 1653 +2153 1653 2154 2154 1653 1654 2171 2154 1654 +2155 2154 2171 2155 2172 2156 2155 2171 2172 +2156 2173 2157 2156 2172 2173 2157 2174 2158 +2157 2173 2174 2158 2175 2159 2158 2174 2175 +2159 2176 2160 2159 2175 2176 2160 2177 2161 +2160 2176 2177 2161 2178 2162 2161 2177 2178 +2162 2179 2163 2162 2178 2179 2179 1656 1655 +2165 2179 1655 2163 2179 2165 2165 1655 1658 +2180 2165 1658 2166 2165 2180 2166 2181 2167 +2166 2180 2181 2181 1660 1642 2181 1642 2167 +1662 2168 1645 1662 2182 2168 2169 2170 1663 +2170 1664 1663 2170 2183 1664 2169 1663 1646 +1668 2171 1654 2171 1668 1670 2184 2171 1670 +2172 2171 2184 2172 2185 2173 2172 2184 2185 +2173 2186 2174 2173 2185 2186 2174 2187 2175 +2174 2186 2187 2175 2188 2176 2175 2187 2188 +2176 2189 2177 2176 2188 2189 2177 2190 2178 +2177 2189 2190 2178 2191 2179 2178 2190 2191 +2191 1672 1656 2191 1656 2179 1673 2180 1658 +2180 1673 1675 2192 2180 1675 2181 2180 2192 +2192 1677 1660 2192 1660 2181 1678 2182 1662 +1678 2193 2182 1679 1664 2194 1664 2195 2194 +1664 2183 2195 2194 1680 1679 1683 2184 1670 +2184 1683 1685 2196 2184 1685 2185 2184 2196 +2185 2197 2186 2185 2196 2197 2186 2198 2187 +2186 2197 2198 2187 2199 2188 2187 2198 2199 +2188 2200 2189 2188 2199 2200 2189 2201 2190 +2189 2200 2201 2190 2202 2191 2190 2201 2202 +2202 1687 1672 2202 1672 2191 1689 2192 1675 +2192 1689 1677 1691 2193 1678 1691 2203 2193 +2195 2204 2194 1680 2194 2204 2205 1680 2204 +1692 1680 2205 1695 2206 2196 1695 2196 1685 +2196 2207 2197 2196 2206 2207 2197 2208 2198 +2197 2207 2208 2198 2209 2199 2198 2208 2209 +2199 2210 2200 2199 2209 2210 2200 2211 2201 +2200 2210 2211 2201 2212 2202 2201 2211 2212 +2212 1697 1687 2212 1687 2202 1700 2203 1691 +1700 2213 2203 2205 1701 1692 2214 1701 2205 +1704 2206 1695 2206 1704 1706 2215 2206 1706 +2207 2206 2215 2207 2216 2208 2207 2215 2216 +2208 2217 2209 2208 2216 2217 2209 2218 2210 +2209 2217 2218 2210 2219 2211 2210 2218 2219 +2211 2220 2212 2211 2219 2220 2220 1708 1697 +2220 1697 2212 1710 2213 1700 1710 2221 2213 +2222 2223 1711 1711 2223 1712 1711 1701 2214 +1711 2214 2222 1715 2224 2215 1715 2215 1706 +2215 2225 2216 2215 2224 2225 2216 2226 2217 +2216 2225 2226 2217 2227 2218 2217 2226 2227 +2218 2228 2219 2218 2227 2228 2219 2229 2220 +2219 2228 2229 2229 1717 1708 2229 1708 2220 +1719 2221 1710 1719 2230 2221 2223 1720 1712 +2231 1720 2223 1724 2232 2224 1724 2224 1715 +2224 2233 2225 2224 2232 2233 2225 2234 2226 +2225 2233 2234 2226 2235 2227 2226 2234 2235 +2235 1726 1725 2228 2235 1725 2227 2235 2228 +2228 1725 1729 2228 1729 2229 2229 1729 1717 +2236 2237 2238 2237 2230 2238 2230 1731 2238 +2230 1719 1731 2231 1732 1720 2239 1732 2231 +1735 2240 2232 1735 2232 1724 2232 2241 2233 +2232 2240 2241 2233 2242 2234 2233 2241 2242 +2234 2243 2235 2234 2242 2243 2243 1737 1726 +2243 1726 2235 2236 2238 1741 1740 2236 1741 +2244 2236 1740 1741 2238 1731 2239 1742 1732 +2245 1742 2239 1745 2246 2240 1745 2240 1735 +2240 2247 2241 2240 2246 2247 2241 2248 2242 +2241 2247 2248 2242 2249 2243 2242 2248 2249 +2250 1746 1737 2243 2250 1737 2249 2250 2243 +2250 1747 1746 1750 2244 1740 1750 2251 2244 +2252 1751 2253 1751 2245 2253 1751 1742 2245 +2254 2252 2253 1755 2255 1754 2246 1745 1754 +2255 2246 1754 2256 2246 2255 2246 2257 2247 +2246 2256 2257 2247 2258 2248 2247 2257 2258 +2248 2259 2249 2248 2258 2259 2249 2260 2250 +2249 2259 2260 2260 1757 1747 2260 1747 2250 +1759 2251 1750 1759 2261 2251 2252 1760 1751 +1761 1760 2252 2254 1761 2252 2262 1761 2254 +1764 2263 2255 1764 2255 1755 2255 2264 2256 +2255 2263 2264 2256 2265 2257 2256 2264 2265 +2257 2266 2258 2257 2265 2266 2258 2267 2259 +2258 2266 2267 2259 2268 2260 2259 2267 2268 +2269 1765 1757 2260 2269 1757 2268 2269 2260 +2269 1766 1765 1769 2261 1759 1769 2270 2261 +2262 1770 1761 2271 1770 2262 1774 2272 1773 +2263 1764 1773 2272 2263 1773 2273 2263 2272 +2263 2274 2264 2263 2273 2274 2264 2275 2265 +2264 2274 2275 2265 2276 2266 2265 2275 2276 +2266 2277 2267 2266 2276 2277 2267 2278 2268 +2267 2277 2278 2268 2279 2269 2268 2278 2279 +2280 1775 1766 2269 2280 1766 2279 2280 2269 +2280 1776 1775 1778 2270 1769 1778 2281 2270 +2271 1779 1770 2282 1779 2271 1782 2283 2272 +1782 2272 1774 2272 2284 2273 2272 2283 2284 +2273 2285 2274 2273 2284 2285 2286 2275 2287 +2275 2285 2287 2275 2274 2285 2288 2289 2286 +2289 2275 2286 2289 2276 2275 2276 2290 2277 +2276 2289 2290 2277 2291 2278 2277 2290 2291 +2278 2292 2279 2278 2291 2292 2279 2293 2280 +2279 2292 2293 2294 1783 1776 2280 2294 1776 +2293 2294 2280 2294 1784 1783 2295 2281 1778 +2295 1778 1787 1787 1788 2295 2295 1788 2296 +2282 1789 1779 2297 1789 2282 1792 2298 1791 +2283 1782 1791 2298 2283 1791 2299 2283 2298 +2283 2300 2284 2283 2299 2300 2301 2285 2302 +2285 2300 2302 2285 2284 2300 2285 2301 2287 +2288 2303 2289 2289 2303 2290 2290 2303 2304 +2290 2304 2291 2291 2304 2305 2306 2307 2305 +2307 2291 2305 2307 2292 2291 2292 2308 2293 +2292 2307 2308 2293 2309 2294 2293 2308 2309 +2309 1794 1784 2309 1784 2294 1796 2296 1788 +1796 2310 2296 2297 1797 1789 2311 1797 2297 +1799 2312 1800 2298 1792 1800 2312 2298 1800 +2313 2298 2312 2298 2314 2299 2298 2313 2314 +2315 2300 2316 2300 2314 2316 2300 2299 2314 +2300 2315 2302 2306 2317 2307 2318 2319 2317 +2319 2307 2317 2319 2308 2307 2308 2320 2309 +2308 2319 2320 2321 1801 1794 2309 2321 1794 +2320 2321 2309 2321 1802 1801 1805 2310 1796 +1805 2322 2310 2323 2324 2325 2311 2323 2325 +1806 2311 2325 1797 2311 1806 1808 2326 1809 +2312 1799 1809 2326 2312 1809 2327 2312 2326 +2312 2328 2313 2312 2327 2328 2329 2314 2330 +2314 2328 2330 2314 2313 2328 2314 2329 2316 +2318 2331 2319 2319 2331 2320 2320 2331 2332 +2333 2334 2332 2334 2320 2332 2334 2321 2320 +2335 1810 1802 2321 2335 1802 2334 2335 2321 +2335 1811 1810 1815 2336 2337 2322 1815 2337 +1805 1815 2322 2336 2338 2337 2325 2324 1816 +2324 1817 1816 2324 2339 1817 2325 1816 1806 +1822 2340 1823 2326 1808 1823 2340 2326 1823 +2341 2326 2340 2342 2327 2343 2327 2341 2343 +2327 2326 2341 2327 2342 2328 2328 2342 2344 +2328 2344 2330 2334 2333 2345 2345 2333 2346 +2334 2347 2335 2334 2345 2347 2348 1824 1811 +2335 2348 1811 2347 2348 2335 2348 1825 1824 +1829 2349 2336 1829 2336 1815 2338 2336 2350 +2350 2336 2349 1830 1817 2351 1817 2352 2351 +1817 2339 2352 2351 1831 1830 1835 2353 1834 +2340 1822 1834 2353 2340 1834 2354 2340 2353 +2355 2341 2356 2341 2354 2356 2341 2340 2354 +2341 2355 2343 2346 2357 2345 2358 2359 2357 +2359 2345 2357 2359 2347 2345 2347 2360 2348 +2347 2359 2360 2360 1837 1825 2360 1825 2348 +1841 2361 1842 2361 2362 1842 1842 2362 1829 +2362 2363 1829 2363 2349 1829 2349 2363 2350 +2352 2364 2351 2364 2365 2366 2364 2366 1843 +2351 2364 1843 1831 2351 1843 2366 1844 1843 +1847 2367 1848 2353 1835 1848 2367 2353 1848 +2368 2353 2367 2369 2354 2370 2354 2368 2370 +2354 2353 2368 2354 2369 2356 2358 2371 2359 +2359 2371 2360 2360 2371 2372 2372 2373 2374 +2372 2374 1849 2360 2372 1849 1837 2360 1849 +2374 1850 1849 1854 2375 2361 1854 2361 1841 +2362 2361 2376 2376 2361 2375 2366 2365 2377 +2377 2365 2378 2379 1855 1844 2366 2379 1844 +2377 2379 2366 2380 1856 1855 2380 1855 2379 +1859 2367 1847 1857 1856 2380 2368 2367 2381 +2367 1860 2381 1860 2382 2381 1860 2367 1859 +2368 2381 2370 2373 2383 2374 2383 2384 2385 +2383 2385 1861 2374 2383 1861 1850 2374 1861 +2385 1862 1861 1865 2386 1864 2386 2387 1864 +1864 2387 1854 2387 2388 1854 2388 2375 1854 +2375 2388 2376 2378 2389 2377 2377 2389 2379 +2379 2389 2390 2391 2392 2390 2392 2379 2390 +2392 2380 2379 2393 1866 1857 2380 2393 1857 +2392 2393 2380 2393 2394 1866 2394 1860 1866 +2394 2382 1860 2385 2384 2395 2395 2384 2396 +2397 1867 1862 2385 2397 1862 2395 2397 2385 +2386 1865 1867 2397 2386 1867 2398 2386 2397 +2387 2386 2399 2399 2386 2398 2391 2400 2392 +2392 2400 2393 2393 2400 2401 2393 2401 2394 +2396 2402 2395 2395 2402 2397 2397 2402 2403 +2397 2403 2398 2398 2403 2404 2398 2404 2399 +2405 1869 1868 1870 2406 1868 1868 2406 2405 +1871 2407 1870 1870 2407 2406 1872 2407 1871 +2408 1874 1873 2408 1873 2409 1873 2410 2409 +1873 1875 2410 1875 1869 2405 2411 1875 2405 +2410 1875 2411 2411 2405 2406 2411 2406 2412 +2412 2406 2407 2412 2407 2413 2407 1872 2413 +1872 2414 2413 1872 1876 2414 2415 1878 2408 +2408 1878 1874 2408 2409 2416 2408 2416 2415 +1876 1879 2417 2418 1876 2417 2414 1876 2418 +1880 2417 1879 2419 1882 1883 2420 2419 1883 +2420 1883 1878 2421 2420 1878 2415 2421 1878 +2421 2415 2416 2422 2418 2417 2423 2422 2424 +2424 2422 1884 2422 2417 1884 2417 1880 1884 +1885 2424 1884 2425 1887 1886 1888 2426 1886 +1886 2426 2425 2427 2426 1888 2427 1888 1882 +2428 2427 1882 2419 2428 1882 2428 2419 2420 +2429 2423 2424 2424 1885 2429 1885 2430 2429 +1885 1889 2430 2431 1893 1894 1887 2425 1894 +2425 2431 1894 2425 2432 2431 2426 2433 2434 +2432 2426 2434 2425 2426 2432 2433 2426 2427 +1895 2435 2436 2430 1895 2436 1889 1895 2430 +2435 2437 2436 2438 1898 1899 1901 2438 1899 +2439 1903 1902 1893 2431 1902 2431 2439 1902 +2431 2440 2439 2432 2441 2442 2440 2432 2442 +2431 2432 2440 2441 2432 2434 1905 2435 1895 +1905 1906 2435 1906 2437 2435 1906 2443 2437 +2444 1909 1908 1898 2438 1908 2438 2444 1908 +2438 2445 2444 1910 2446 1901 2446 2438 1901 +2446 2445 2438 1903 2439 1910 2439 2446 1910 +2439 2447 2446 2440 2448 2449 2447 2440 2449 +2439 2440 2447 2448 2440 2442 1912 2443 1906 +1912 2450 2443 2451 1914 1915 1916 2452 1915 +1915 2452 2451 2453 2452 1916 2453 1916 1909 +2454 2453 1909 2444 2454 1909 2454 2444 2445 +2454 2445 2455 2455 2445 2446 2455 2446 2456 +2456 2446 2447 2456 2447 2457 2457 2447 2449 +1918 2458 1919 2458 2459 1919 2459 1912 1919 +2459 2450 1912 2460 1921 2451 2451 1921 1914 +2461 2451 2452 2460 2451 2461 2452 2453 2462 +2452 2462 2461 1924 2463 1925 2463 2464 1925 +1925 2464 1918 2464 2465 1918 2465 2458 1918 +2458 2465 2459 2466 1927 2460 2460 1927 1921 +2461 2467 2468 2466 2461 2468 2460 2461 2466 +2467 2461 2462 1930 2463 1924 1930 1932 2463 +1932 2464 2463 1932 2469 2464 2470 1934 1933 +2471 2470 1933 2471 1933 1927 2472 2471 1927 +2466 2472 1927 2472 2466 2468 1938 2473 1937 +2469 1932 2474 1932 2473 2474 2473 2475 2474 +1932 1937 2473 2475 2476 2474 2477 2478 2470 +2478 1934 2470 2478 1939 1934 2477 2470 2471 +1943 2479 2473 1943 2473 1938 2473 2480 2475 +2473 2479 2480 2476 2475 2481 2475 2482 2481 +2475 2480 2482 2482 2483 2481 2478 1944 1939 +2484 1944 2478 1948 2485 1947 2479 1943 1947 +2485 2479 1947 2486 2479 2485 2479 2487 2480 +2479 2486 2487 2488 2482 2489 2482 2487 2489 +2482 2480 2487 2482 2488 2483 2490 1950 1949 +2491 2490 1949 1944 2491 1949 2484 2491 1944 +1954 2492 1953 2485 1948 1953 2492 2485 1953 +2493 2485 2492 2485 2494 2486 2485 2493 2494 +2495 2487 2496 2487 2494 2496 2487 2486 2494 +2487 2495 2489 2497 2498 2490 2498 1950 2490 +2498 1955 1950 2497 2490 2491 1958 2499 1957 +2492 1954 1957 2499 2492 1957 2500 2492 2499 +2492 2501 2493 2492 2500 2501 2502 2494 2503 +2494 2501 2503 2494 2493 2501 2494 2502 2496 +2498 1959 1955 2504 1959 2498 1961 2505 2499 +1961 2499 1958 2499 2506 2500 2499 2505 2506 +2507 2501 2508 2501 2506 2508 2501 2500 2506 +2501 2507 2503 2504 1962 1959 2509 1962 2504 +1964 2510 1965 2505 1961 1965 2510 2505 1965 +2511 2505 2510 2512 2506 2513 2506 2511 2513 +2506 2505 2511 2506 2512 2508 2514 2515 2516 +1967 1966 2516 1966 2514 2516 1966 2517 2514 +1966 1962 2509 1966 2509 2517 1970 2518 2510 +1970 2510 1964 2519 2511 2520 2511 2518 2520 +2511 2510 2518 2511 2519 2513 2515 2521 2516 +1967 2516 2521 2522 1967 2521 1972 1967 2522 +1973 2523 1974 2524 1975 1974 2524 1974 2523 +2524 2525 1975 1975 2525 1970 2525 2526 1970 +2526 2518 1970 2518 2526 2520 2527 2528 2529 +2529 2530 1976 2522 2529 1976 1972 2522 1976 +2529 2522 2527 2530 1977 1976 1980 2531 2523 +1980 2523 1973 2523 2532 2524 2523 2531 2532 +2525 2524 2533 2533 2524 2532 2528 2534 2529 +2529 2534 2530 2530 2534 2535 2535 2536 2537 +2535 2537 1981 2530 2535 1981 1977 2530 1981 +2537 1982 1981 1985 2538 2531 1985 2531 1980 +2531 2539 2532 2531 2538 2539 2533 2532 2540 +2540 2532 2539 2541 2542 2543 2541 2543 2536 +2543 2537 2536 2543 2544 2537 2545 1986 1982 +2537 2545 1982 2544 2545 2537 2545 1987 1986 +1990 2546 2538 1990 2538 1985 2538 2547 2539 +2538 2546 2547 2540 2539 2548 2548 2539 2547 +2542 2549 2543 2550 2551 2549 2551 2543 2549 +2551 2544 2543 2544 2552 2545 2544 2551 2552 +2553 1991 1987 2545 2553 1987 2552 2553 2545 +2553 1992 1991 1996 2554 2546 1996 2546 1990 +2555 2547 2556 2547 2554 2556 2547 2546 2554 +2547 2555 2548 2551 2550 2557 2557 2550 2558 +2557 1998 1997 2552 2557 1997 2551 2557 2552 +2552 1997 2000 2559 2552 2000 2553 2552 2559 +2560 2001 1992 2553 2560 1992 2559 2560 2553 +2561 2002 2001 2561 2001 2560 2561 2003 2002 +2562 2563 2554 1996 2562 2554 2008 2562 1996 +2554 2563 2556 2557 2558 2564 2564 2558 2565 +2566 2009 1998 2557 2566 1998 2564 2566 2557 +2559 2000 2009 2566 2559 2009 2567 2559 2566 +2559 2568 2560 2559 2567 2568 2560 2569 2561 +2560 2568 2569 2569 2011 2003 2569 2003 2561 +2018 2570 2017 2562 2008 2571 2008 2570 2571 +2570 2572 2571 2008 2017 2570 2573 2574 2572 +2572 2574 2571 2575 2576 2573 2573 2576 2574 +2575 2577 2576 2565 2578 2564 2579 2580 2578 +2580 2564 2578 2580 2566 2564 2566 2581 2567 +2566 2580 2581 2567 2582 2568 2567 2581 2582 +2568 2583 2569 2568 2582 2583 2584 2019 2011 +2569 2584 2011 2583 2584 2569 2584 2020 2019 +2585 2586 2570 2018 2585 2570 2026 2585 2018 +2587 2588 2586 2588 2570 2586 2588 2572 2570 +2572 2589 2573 2572 2588 2589 2573 2590 2575 +2573 2589 2590 2577 2575 2591 2591 2575 2590 +2579 2592 2580 2580 2592 2581 2581 2592 2593 +2581 2593 2582 2582 2593 2594 2582 2594 2583 +2583 2594 2595 2596 2597 2595 2597 2583 2595 +2597 2584 2583 2598 2027 2020 2584 2598 2020 +2597 2598 2584 2599 2028 2027 2599 2027 2598 +2600 2029 2028 2600 2028 2599 2601 2030 2029 +2601 2029 2600 2602 2031 2030 2602 2030 2601 +2603 2604 2034 2603 2032 2604 2032 2603 2602 +2585 2602 2603 2602 2585 2031 2604 2031 2585 +2585 2026 2604 2604 2026 2034 2032 2031 2604 +2036 2588 2587 2603 2036 2587 2034 2036 2603 +2588 2036 2038 2588 2038 2589 2589 2038 2040 +2589 2040 2590 2605 2606 2041 2041 2606 2040 +2606 2591 2040 2591 2590 2040 2605 2607 2606 +2608 2609 2610 2611 2612 2610 2610 2612 2608 +2613 2614 2611 2611 2614 2612 2043 2042 2613 +2042 2614 2613 2042 2615 2614 2616 2042 2046 +2616 2615 2042 2617 2046 2048 2617 2616 2046 +2048 2596 2617 2596 2048 2597 2049 2597 2048 +2597 2049 2618 2619 2618 2049 2597 2618 2598 +2598 2618 2620 2598 2620 2599 2599 2620 2621 +2622 2623 2621 2623 2599 2621 2623 2600 2599 +2600 2624 2601 2600 2623 2624 2624 2051 2050 +2602 2624 2050 2601 2624 2602 2602 2050 2032 +2057 2605 2041 2625 2626 2059 2059 2626 2057 +2626 2607 2057 2607 2605 2057 2625 2627 2626 +2628 2629 2630 2628 2630 2609 2630 2610 2609 +2630 2631 2610 2610 2632 2611 2610 2631 2632 +2632 2061 2060 2613 2632 2060 2611 2632 2613 +2613 2060 2043 2066 2633 2634 2619 2066 2634 +2049 2066 2619 2635 2636 2633 2633 2636 2634 +2637 2638 2635 2635 2638 2636 2638 2637 2622 +2637 2623 2622 2637 2639 2623 2639 2068 2067 +2624 2639 2067 2623 2639 2624 2624 2067 2051 +2075 2625 2059 2075 2076 2625 2076 2627 2625 +2076 2640 2627 2641 2642 2643 2644 2645 2643 +2643 2645 2641 2646 2647 2644 2644 2647 2645 +2646 2648 2647 2649 2650 2651 2649 2651 2629 +2651 2630 2629 2651 2652 2630 2630 2653 2631 +2630 2652 2653 2653 2078 2077 2632 2653 2077 +2631 2653 2632 2632 2077 2061 2084 2633 2066 +2633 2084 2086 2654 2633 2086 2635 2633 2654 +2635 2655 2637 2635 2654 2655 2637 2656 2639 +2637 2655 2656 2656 2088 2068 2656 2068 2639 +2092 2657 2658 2640 2092 2658 2076 2092 2640 +2657 2659 2658 2660 2661 2662 2660 2662 2642 +2642 2662 2643 2662 2094 2643 2094 2093 2643 +2643 2093 2097 2643 2097 2644 2644 2097 2099 +2644 2099 2646 2663 2648 2646 2099 2663 2646 +2101 2663 2099 2664 2101 2103 2664 2663 2101 +2665 2103 2105 2665 2664 2103 2107 2651 2650 +2665 2107 2650 2105 2107 2665 2651 2107 2109 +2651 2109 2652 2652 2109 2111 2652 2111 2653 +2653 2111 2078 2118 2654 2086 2654 2118 2120 +2666 2654 2120 2655 2654 2666 2655 2667 2656 +2655 2666 2667 2667 2122 2088 2667 2088 2656 +2126 2657 2092 2126 2127 2657 2127 2659 2657 +2127 2668 2659 2662 2661 2669 2669 2661 2670 +2669 2129 2094 2669 2094 2662 2142 2666 2120 +2666 2142 2144 2671 2666 2144 2667 2666 2671 +2671 2146 2122 2671 2122 2667 2148 2668 2127 +2148 2672 2668 2669 2670 2149 2670 2150 2149 +2670 2673 2150 2669 2149 2129 2164 2671 2144 +2671 2164 2146 2168 2672 2148 2168 2674 2672 +2673 2170 2150 2675 2170 2673 2182 2674 2168 +2182 2676 2674 2675 2183 2170 2677 2183 2675 +2193 2676 2182 2193 2678 2676 2679 2195 2680 +2195 2677 2680 2195 2183 2677 2681 2679 2680 +2203 2678 2193 2203 2682 2678 2679 2204 2195 +2205 2204 2679 2681 2205 2679 2683 2205 2681 +2213 2682 2203 2213 2684 2682 2685 2686 2687 +2683 2685 2687 2214 2683 2687 2205 2683 2214 +2221 2684 2213 2221 2688 2684 2687 2686 2222 +2686 2223 2222 2686 2689 2223 2687 2222 2214 +2230 2688 2221 2230 2690 2688 2689 2231 2223 +2691 2231 2689 2692 2693 2237 2692 2237 2236 +2230 2237 2690 2690 2237 2693 2691 2239 2231 +2694 2239 2691 2244 2692 2236 2244 2695 2692 +2694 2245 2239 2696 2245 2694 2251 2695 2244 +2251 2697 2695 2696 2698 2245 2245 2698 2253 +2254 2253 2698 2254 2698 2699 2261 2697 2251 +2261 2700 2697 2699 2262 2254 2701 2262 2699 +2270 2702 2703 2700 2270 2703 2261 2270 2700 +2702 2704 2703 2701 2271 2262 2705 2271 2701 +2281 2706 2702 2281 2702 2270 2704 2702 2707 +2707 2702 2706 2705 2282 2271 2708 2282 2705 +2287 2709 2286 2709 2288 2286 2295 2706 2281 +2295 2296 2706 2296 2707 2706 2296 2710 2707 +2708 2297 2282 2711 2297 2708 2302 2712 2301 +2709 2287 2301 2712 2709 2301 2713 2709 2712 +2714 2303 2288 2709 2714 2288 2713 2714 2709 +2715 2304 2303 2715 2303 2714 2716 2305 2304 +2716 2304 2715 2716 2306 2305 2310 2710 2296 +2310 2717 2710 2711 2311 2297 2718 2311 2711 +2316 2719 2315 2712 2302 2315 2719 2712 2315 +2720 2712 2719 2721 2713 2722 2713 2720 2722 +2713 2712 2720 2723 2724 2721 2724 2713 2721 +2724 2714 2713 2714 2725 2715 2714 2724 2725 +2715 2726 2716 2715 2725 2726 2727 2317 2306 +2716 2727 2306 2726 2727 2716 2727 2318 2317 +2322 2728 2729 2717 2322 2729 2310 2322 2717 +2728 2730 2729 2731 2732 2323 2323 2732 2324 +2323 2311 2718 2323 2718 2731 2330 2733 2329 +2733 2734 2329 2329 2734 2316 2734 2735 2316 +2735 2719 2316 2719 2735 2720 2720 2735 2736 +2720 2736 2722 2723 2737 2724 2724 2737 2725 +2725 2737 2738 2725 2738 2726 2726 2738 2739 +2726 2739 2727 2727 2739 2740 2740 2741 2742 +2740 2742 2331 2727 2740 2331 2318 2727 2331 +2743 2332 2331 2743 2331 2742 2743 2333 2332 +2337 2728 2322 2337 2338 2728 2338 2730 2728 +2338 2744 2730 2732 2339 2324 2745 2339 2732 +2343 2746 2342 2747 2344 2342 2747 2342 2746 +2747 2748 2344 2344 2748 2330 2748 2749 2330 +2749 2733 2330 2733 2749 2734 2741 2750 2742 +2751 2752 2750 2752 2742 2750 2752 2743 2742 +2752 2346 2333 2752 2333 2743 2753 2754 2755 +2754 2744 2755 2744 2350 2755 2744 2338 2350 +2745 2352 2339 2756 2352 2745 2356 2757 2355 +2746 2343 2355 2757 2746 2355 2758 2746 2757 +2759 2747 2760 2747 2758 2760 2747 2746 2758 +2747 2759 2748 2751 2761 2752 2761 2762 2763 +2761 2763 2357 2752 2761 2357 2346 2752 2357 +2763 2358 2357 2753 2755 2363 2362 2753 2363 +2764 2753 2362 2363 2755 2350 2756 2765 2352 +2352 2765 2364 2365 2364 2765 2365 2765 2766 +2370 2767 2369 2767 2768 2369 2369 2768 2356 +2768 2769 2356 2769 2757 2356 2757 2769 2758 +2758 2769 2770 2758 2770 2760 2762 2771 2763 +2771 2772 2773 2771 2773 2371 2763 2771 2371 +2358 2763 2371 2774 2372 2371 2774 2371 2773 +2774 2373 2372 2376 2764 2362 2376 2775 2764 +2766 2378 2365 2776 2378 2766 2382 2777 2381 +2777 2778 2381 2381 2778 2370 2778 2779 2370 +2779 2767 2370 2767 2779 2768 2772 2780 2773 +2773 2780 2774 2774 2780 2781 2781 2782 2783 +2781 2783 2383 2774 2781 2383 2373 2774 2383 +2783 2384 2383 2387 2784 2388 2784 2785 2388 +2785 2376 2388 2785 2775 2376 2776 2786 2378 +2378 2786 2389 2786 2787 2788 2390 2786 2788 +2389 2786 2390 2788 2391 2390 2394 2789 2777 +2394 2777 2382 2778 2777 2790 2790 2777 2789 +2782 2791 2783 2384 2783 2791 2792 2384 2791 +2396 2384 2792 2399 2793 2784 2399 2784 2387 +2785 2784 2794 2794 2784 2793 2787 2795 2788 +2795 2796 2797 2795 2797 2400 2788 2795 2400 +2391 2788 2400 2798 2401 2400 2798 2400 2797 +2798 2799 2401 2401 2799 2394 2799 2800 2394 +2800 2789 2394 2789 2800 2790 2402 2396 2801 +2396 2802 2801 2396 2792 2802 2803 2403 2402 +2803 2402 2801 2804 2404 2403 2804 2403 2803 +2804 2805 2404 2404 2805 2399 2805 2806 2399 +2806 2793 2399 2793 2806 2794 2796 2807 2797 +2797 2807 2798 2798 2807 2808 2798 2808 2799 +2802 2809 2801 2801 2809 2803 2803 2809 2810 +2803 2810 2804 2804 2810 2811 2804 2811 2805 +2812 2409 2410 2411 2813 2410 2410 2813 2812 +2412 2814 2411 2411 2814 2813 2413 2815 2412 +2412 2815 2814 2414 2815 2413 2816 2416 2812 +2812 2416 2409 2813 2817 2818 2816 2813 2818 +2812 2813 2816 2817 2813 2814 2817 2814 2819 +2820 2821 2819 2814 2820 2819 2815 2820 2814 +2418 2820 2414 2414 2820 2815 2822 2420 2421 +2416 2816 2421 2816 2822 2421 2816 2823 2822 +2816 2818 2824 2816 2824 2823 2821 2820 2825 +2821 2825 2826 2422 2827 2418 2827 2820 2418 +2827 2825 2820 2423 2827 2422 2828 2427 2428 +2829 2828 2428 2829 2428 2420 2830 2829 2420 +2822 2830 2420 2830 2822 2823 2830 2823 2831 +2831 2823 2824 2832 2826 2825 2832 2825 2827 +2832 2827 2833 2834 2833 2835 2835 2833 2429 +2833 2827 2429 2827 2423 2429 2430 2835 2429 +2836 2434 2433 2837 2836 2433 2837 2433 2427 +2838 2837 2427 2828 2838 2427 2838 2828 2829 +2839 2834 2835 2840 2839 2841 2841 2839 2436 +2839 2835 2436 2835 2430 2436 2437 2841 2436 +2842 2442 2441 2843 2842 2441 2843 2441 2434 +2844 2843 2434 2836 2844 2434 2844 2836 2837 +2845 2840 2841 2841 2437 2845 2437 2846 2845 +2437 2443 2846 2847 2449 2448 2442 2842 2448 +2842 2847 2448 2842 2848 2847 2842 2843 2849 +2842 2849 2848 2450 2850 2851 2846 2450 2851 +2443 2450 2846 2850 2852 2851 2853 2453 2454 +2455 2854 2454 2454 2854 2853 2456 2855 2455 +2455 2855 2854 2855 2456 2856 2456 2857 2856 +2456 2457 2857 2457 2449 2847 2858 2457 2847 +2857 2457 2858 2858 2847 2848 2858 2848 2859 +2859 2848 2849 2860 2861 2850 2450 2860 2850 +2459 2860 2450 2850 2861 2852 2862 2462 2853 +2853 2462 2453 2863 2853 2854 2862 2853 2863 +2855 2864 2865 2863 2855 2865 2854 2855 2863 +2864 2855 2856 2464 2866 2465 2860 2459 2867 +2459 2866 2867 2866 2868 2867 2459 2465 2866 +2868 2869 2867 2870 2468 2467 2871 2870 2467 +2871 2467 2462 2872 2871 2462 2862 2872 2462 +2872 2862 2863 2872 2863 2873 2873 2863 2865 +2469 2874 2866 2469 2866 2464 2866 2875 2868 +2866 2874 2875 2869 2868 2876 2876 2868 2875 +2877 2878 2472 2472 2878 2471 2472 2468 2870 +2879 2472 2870 2877 2472 2879 2879 2870 2871 +2474 2874 2469 2874 2474 2476 2880 2874 2476 +2875 2874 2880 2876 2875 2881 2881 2875 2880 +2882 2478 2477 2883 2882 2477 2471 2883 2477 +2878 2883 2471 2481 2880 2476 2481 2483 2880 +2483 2881 2880 2483 2884 2881 2885 2886 2882 +2886 2478 2882 2886 2484 2478 2885 2882 2883 +2489 2887 2488 2887 2888 2488 2888 2483 2488 +2888 2884 2483 2886 2491 2484 2889 2491 2886 +2496 2890 2495 2887 2489 2495 2890 2887 2495 +2891 2887 2890 2888 2887 2892 2887 2893 2892 +2887 2891 2893 2893 2894 2892 2895 2896 2497 +2497 2896 2498 2497 2491 2889 2497 2889 2895 +2503 2897 2502 2890 2496 2502 2897 2890 2502 +2898 2890 2897 2890 2899 2891 2890 2898 2899 +2900 2893 2901 2893 2899 2901 2893 2891 2899 +2893 2900 2894 2896 2504 2498 2902 2504 2896 +2508 2903 2507 2897 2503 2507 2903 2897 2507 +2904 2897 2903 2905 2898 2906 2898 2904 2906 +2898 2897 2904 2898 2905 2899 2899 2905 2907 +2899 2907 2901 2908 2909 2910 2902 2908 2910 +2509 2902 2910 2504 2902 2509 2513 2911 2512 +2903 2508 2512 2911 2903 2512 2912 2903 2911 +2913 2904 2914 2904 2912 2914 2904 2903 2912 +2904 2913 2906 2915 2916 2514 2514 2916 2515 +2517 2910 2909 2915 2517 2909 2514 2517 2915 +2910 2517 2509 2917 2918 2519 2917 2519 2520 +2519 2918 2513 2918 2919 2513 2919 2911 2513 +2919 2918 2920 2921 2912 2922 2912 2919 2922 +2912 2911 2919 2912 2921 2914 2521 2515 2923 +2515 2924 2923 2515 2916 2924 2923 2522 2521 +2525 2925 2526 2925 2926 2526 2926 2520 2526 +2926 2917 2520 2920 2927 2919 2919 2927 2922 +2923 2924 2527 2924 2528 2527 2924 2928 2528 +2923 2527 2522 2533 2929 2925 2533 2925 2525 +2926 2925 2930 2930 2925 2929 2534 2528 2931 +2528 2932 2931 2528 2928 2932 2933 2535 2534 +2933 2534 2931 2933 2536 2535 2540 2934 2929 +2540 2929 2533 2930 2929 2935 2935 2929 2934 +2932 2936 2931 2937 2542 2541 2931 2937 2541 +2933 2931 2541 2936 2937 2931 2933 2541 2536 +2938 2939 2934 2540 2938 2934 2548 2938 2540 +2934 2939 2935 2549 2542 2940 2542 2941 2940 +2542 2937 2941 2940 2550 2549 2942 2943 2555 +2942 2555 2556 2548 2555 2938 2938 2555 2943 +2940 2941 2944 2944 2941 2945 2944 2558 2550 +2944 2550 2940 2946 2947 2563 2946 2563 2562 +2556 2948 2942 2948 2556 2949 2563 2949 2556 +2949 2563 2950 2947 2950 2563 2951 2952 2949 +2949 2952 2948 2951 2953 2952 2944 2945 2954 +2954 2945 2955 2954 2565 2558 2954 2558 2944 +2956 2946 2562 2956 2562 2571 2574 2949 2950 +2956 2574 2950 2571 2574 2956 2949 2574 2576 +2949 2576 2951 2957 2958 2577 2577 2958 2576 +2958 2953 2576 2953 2951 2576 2957 2959 2958 +2955 2960 2954 2960 2961 2962 2960 2962 2578 +2954 2960 2578 2565 2954 2578 2962 2579 2578 +2585 2963 2586 2963 2587 2586 2591 2964 2957 +2591 2957 2577 2959 2957 2965 2965 2957 2964 +2962 2961 2966 2966 2961 2967 2968 2592 2579 +2962 2968 2579 2966 2968 2962 2969 2593 2592 +2969 2592 2968 2970 2594 2593 2970 2593 2969 +2594 2970 2971 2972 2594 2971 2595 2594 2972 +2596 2595 2972 2596 2972 2973 2603 2963 2585 +2963 2603 2587 2606 2964 2591 2974 2975 2607 +2607 2975 2606 2975 2965 2606 2965 2964 2606 +2974 2976 2975 2977 2978 2979 2609 2608 2979 +2608 2977 2979 2608 2980 2977 2612 2966 2967 +2980 2612 2967 2608 2612 2980 2966 2612 2614 +2966 2614 2968 2968 2614 2615 2968 2615 2969 +2969 2615 2616 2969 2616 2970 2616 2617 2970 +2617 2981 2970 2981 2971 2970 2617 2982 2981 +2983 2618 2619 2596 2982 2617 2982 2596 2984 +2973 2984 2596 2985 2620 2618 2985 2618 2983 +2986 2621 2620 2986 2620 2985 2986 2622 2621 +2626 2974 2607 2626 2627 2974 2627 2976 2974 +2627 2987 2976 2988 2989 2990 2988 2990 2978 +2978 2990 2979 2990 2629 2979 2629 2628 2979 +2979 2628 2609 2991 2981 2982 2619 2634 2983 +2982 2984 2991 2983 2634 2636 2983 2636 2985 +2985 2636 2638 2985 2638 2986 2986 2638 2622 +2640 2987 2627 2640 2992 2987 2993 2994 2641 +2641 2994 2642 2995 2641 2645 2995 2993 2641 +2996 2645 2647 2996 2995 2645 2997 2998 2648 +2998 2647 2648 2998 2996 2647 2999 3000 2997 +2997 3000 2998 3001 3002 2999 2999 3002 3000 +3002 3001 2989 2989 3001 2990 3001 2650 2990 +2650 2649 2990 2990 2649 2629 3003 2992 2640 +3003 2640 2658 2658 2659 3003 3003 2659 3004 +3005 3006 2660 2660 3006 2661 2660 2642 2994 +2660 2994 3005 2663 2997 2648 2997 2663 2664 +2997 2664 2999 2999 2664 2665 2999 2665 3001 +3001 2665 2650 2668 3004 2659 2668 3007 3004 +3006 2670 2661 3008 2670 3006 2672 3007 2668 +2672 3009 3007 3008 2673 2670 3010 2673 3008 +2674 3011 3012 3009 2674 3012 2672 2674 3009 +3011 3013 3012 3010 2675 2673 3014 2675 3010 +2676 3015 3011 2676 3011 2674 3013 3011 3016 +3016 3011 3015 3014 2677 2675 3017 2677 3014 +2678 3018 3015 2678 3015 2676 3016 3015 3019 +3019 3015 3018 3017 3020 2677 2677 3020 2680 +2681 2680 3020 2681 3020 3021 3022 3023 3018 +2678 3022 3018 2682 3022 2678 3018 3023 3019 +3021 2683 2681 3024 2683 3021 2684 3022 2682 +2684 3025 3022 3026 3027 2685 2685 3027 2686 +2685 2683 3024 2685 3024 3026 2688 3025 2684 +2688 3028 3025 3027 2689 2686 3029 2689 3027 +2690 3028 2688 2690 3030 3028 3029 2691 2689 +3031 2691 3029 3032 3033 2693 3032 2693 2692 +2690 2693 3030 3030 2693 3033 3031 2694 2691 +3034 2694 3031 2695 3032 2692 2695 3035 3032 +3034 2696 2694 3036 2696 3034 2697 3035 2695 +2697 3037 3035 3036 3038 2696 2696 3038 2698 +2699 2698 3038 2699 3038 3039 2700 3037 2697 +2700 3040 3037 3039 2701 2699 3041 2701 3039 +3042 3040 2700 3042 2700 2703 2703 2704 3042 +3042 2704 3043 3041 2705 2701 3044 2705 3041 +2707 3043 2704 2707 3045 3043 3044 2708 2705 +3046 2708 3044 2710 3045 2707 2710 3047 3045 +3046 2711 2708 3048 2711 3046 2717 3047 2710 +2717 3049 3047 3048 2718 2711 3050 2718 3048 +2722 3051 2721 3051 2723 2721 3052 3049 2717 +3052 2717 2729 2729 2730 3052 3052 2730 3053 +3054 3055 2731 2731 3055 2732 2731 2718 3050 +2731 3050 3054 2734 3056 2735 3057 2736 2735 +3057 2735 3056 3051 2722 2736 3057 3051 2736 +3058 3051 3057 3059 2737 2723 3051 3059 2723 +3058 3059 3051 3060 2738 2737 3060 2737 3059 +3061 2739 2738 3061 2738 3060 3062 2740 2739 +3062 2739 3061 3062 2741 2740 2744 3053 2730 +2744 3063 3053 3055 2745 2732 3064 2745 3055 +2748 3065 2749 3056 2734 2749 3065 3056 2749 +3066 3056 3065 3067 3057 3068 3057 3066 3068 +3057 3056 3066 3069 3070 3067 3070 3057 3067 +3070 3058 3057 3058 3071 3059 3058 3070 3071 +3059 3072 3060 3059 3071 3072 3060 3073 3061 +3060 3072 3073 3061 3074 3062 3061 3073 3074 +3075 2750 2741 3062 3075 2741 3074 3075 3062 +3075 2751 2750 3076 3077 2754 3076 2754 2753 +2744 2754 3063 3063 2754 3077 3064 2756 2745 +3078 2756 3064 2760 3079 2759 3065 2748 2759 +3079 3065 2759 3080 3065 3079 3081 3066 3082 +3066 3080 3082 3066 3065 3080 3066 3081 3068 +3069 3083 3070 3070 3083 3071 3071 3083 3084 +3071 3084 3072 3072 3084 3085 3072 3085 3073 +3073 3085 3086 3073 3086 3074 3074 3086 3087 +3088 3089 3087 3089 3074 3087 3089 3075 3074 +3090 2761 2751 3075 3090 2751 3089 3090 3075 +3090 2762 2761 2764 3076 2753 2764 3091 3076 +3078 3092 2756 2756 3092 2765 2766 2765 3092 +2766 3092 3093 2768 3094 2769 3095 2770 2769 +3095 2769 3094 3095 3096 2770 2770 3096 2760 +3096 3097 2760 3097 3079 2760 3079 3097 3080 +3080 3097 3098 3080 3098 3082 3088 3099 3089 +3089 3099 3090 3090 3099 3100 3100 3101 3102 +3100 3102 2771 3090 3100 2771 2762 3090 2771 +3102 2772 2771 2775 3091 2764 2775 3103 3091 +3093 2776 2766 3104 2776 3093 2778 3105 2779 +3105 3106 2779 2779 3106 2768 3106 3107 2768 +3107 3094 2768 3094 3107 3095 3095 3107 3108 +3095 3108 3096 3101 3109 3102 3102 3109 2772 +3109 2780 2772 3109 3110 2780 3110 3111 3112 +2781 3110 3112 2780 3110 2781 3112 2782 2781 +2785 3103 2775 2785 3113 3103 2786 2776 3114 +2776 3115 3114 2776 3104 3115 3114 2787 2786 +3116 3117 3105 2778 3116 3105 2790 3116 2778 +3105 3117 3106 3111 3118 3112 3118 3119 3120 +3118 3120 2791 3112 3118 2791 2782 3112 2791 +3120 2792 2791 2794 3113 2785 2794 3121 3113 +3115 3122 3114 3122 3123 3124 3122 3124 2795 +3114 3122 2795 2787 3114 2795 3124 2796 2795 +2799 3125 2800 3125 3126 2800 3126 2790 2800 +3126 3116 2790 3119 3127 3120 2792 3120 3127 +3128 2792 3127 2802 2792 3128 2805 3129 2806 +3129 3130 2806 3130 2794 2806 3130 3121 2794 +3123 3131 3124 3131 3132 3133 3131 3133 2807 +3124 3131 2807 2796 3124 2807 2807 3133 3134 +3135 2807 3134 2808 2807 3135 2799 2808 3125 +2808 3136 3125 2808 3135 3136 3125 3136 3126 +3128 3137 2802 2802 3137 2809 3137 3138 3139 +2810 3137 3139 2809 3137 2810 2810 3139 3140 +3141 2810 3140 2811 2810 3141 2805 2811 3129 +2811 3142 3129 2811 3141 3142 3129 3142 3130 +3132 3143 3133 3133 3143 3134 3138 3144 3139 +3139 3144 3140 3145 2818 2817 2819 3146 2817 +2817 3146 3145 2821 3146 2819 3147 3148 3145 +3148 2818 3145 3148 2824 2818 3147 3145 3146 +3147 3146 3149 3146 2821 3149 2821 3150 3149 +2821 2826 3150 3151 2829 2830 2831 3152 2830 +2830 3152 3151 3153 3152 2831 2824 3153 2831 +3148 3153 2824 2826 2832 3154 3155 2826 3154 +3150 2826 3155 2833 3156 2832 2832 3156 3154 +2834 3156 2833 3157 2837 2838 3158 3157 2838 +3158 2838 2829 3159 3158 2829 3151 3159 2829 +3159 3151 3152 3159 3152 3160 3160 3152 3153 +3161 3155 3154 3161 3154 3156 3161 3156 3162 +3162 3156 2834 2839 3162 2834 3163 3162 2839 +2839 2840 3163 3163 2840 3164 3165 2843 2844 +2837 3157 2844 3157 3165 2844 3157 3166 3165 +3157 3158 3167 3157 3167 3166 3168 3164 2840 +3168 2840 2845 2845 2846 3168 3168 2846 3169 +3170 3171 3165 3171 2843 3165 3171 2849 2843 +3170 3165 3166 3170 3166 3172 3172 3166 3167 +3173 3169 2846 3173 2846 2851 2851 2852 3173 +3173 2852 3174 3175 2856 2857 3175 2857 3176 +2857 3177 3176 2857 2858 3177 3178 2858 2859 +3178 3177 2858 2859 2849 3171 2859 3171 3178 +2860 3179 2861 3179 3180 2861 3180 2852 2861 +3180 3174 2852 3181 2865 2864 3182 3181 2864 +3182 2864 2856 3183 3182 2856 3175 3183 2856 +3183 3175 3176 2867 3179 2860 3184 3185 2869 +2869 3185 2867 3185 3180 2867 3180 3179 2867 +3184 3186 3185 3187 3188 2872 2872 3188 2871 +3189 2872 2873 3189 3187 2872 2873 2865 3181 +3190 2873 3181 3189 2873 3190 3190 3181 3182 +2876 3191 3184 2876 3184 2869 3186 3184 3192 +3192 3184 3191 3193 2878 2877 3193 2877 3194 +2877 3195 3194 2877 2879 3195 2879 2871 3188 +2879 3188 3195 2881 3196 3191 2881 3191 2876 +3192 3191 3197 3197 3191 3196 3198 3199 3193 +3199 2878 3193 3199 2883 2878 3198 3193 3194 +2884 3200 3196 2884 3196 2881 3197 3196 3201 +3201 3196 3200 3202 3203 2885 2885 3203 2886 +2885 2883 3199 2885 3199 3202 2888 3204 3200 +2888 3200 2884 3201 3200 3205 3205 3200 3204 +3203 2889 2886 3206 2889 3203 2892 3204 2888 +2892 2894 3204 2894 3205 3204 2894 3207 3205 +3208 3209 2895 2895 3209 2896 2895 2889 3206 +2895 3206 3208 2901 3210 2900 3210 3211 2900 +3211 2894 2900 3211 3207 2894 3212 3213 3214 +3209 3212 3214 2902 3209 3214 2896 3209 2902 +2906 3215 2905 3216 2907 2905 3216 2905 3215 +3216 3217 2907 2907 3217 2901 3217 3218 2901 +3218 3210 2901 3210 3218 3211 3219 3220 3221 +3219 3221 3213 3213 3221 3214 3221 2909 3214 +2909 2908 3214 3214 2908 2902 2914 3222 2913 +3222 3223 2913 2913 3223 2906 3223 3224 2906 +3224 3215 2906 3215 3224 3216 3216 3224 3225 +3216 3225 3217 3221 3220 2915 3220 2916 2915 +3220 3226 2916 3221 2915 2909 2917 3227 2918 +3227 2920 2918 3228 3229 2921 3228 2921 2922 +2914 2921 3222 2921 3230 3222 2921 3229 3230 +3222 3230 3223 3226 2924 2916 3231 2924 3226 +2926 3232 3227 2926 3227 2917 3233 2927 2920 +3227 3233 2920 3232 3233 3227 3233 3234 2927 +3234 2922 2927 3234 3228 2922 3231 2928 2924 +3235 2928 3231 2930 3236 3232 2930 3232 2926 +3237 3233 3238 3233 3236 3238 3233 3232 3236 +3233 3237 3234 3235 2932 2928 3239 2932 3235 +2935 3240 3236 2935 3236 2930 3238 3236 3241 +3241 3236 3240 3239 3242 2932 2932 3242 2936 +2937 2936 3242 2937 3242 3243 2938 3244 2939 +3244 3245 2939 2939 3245 2935 3245 3246 2935 +3246 3240 2935 3240 3246 3241 3243 2941 2937 +3247 2941 3243 2942 3248 2943 3248 3249 2943 +2943 3249 2938 3249 3250 2938 3250 3244 2938 +3244 3250 3245 3247 2945 2941 3251 2945 3247 +2946 3252 2947 2948 3248 2942 2950 2947 3252 +3253 3249 3248 2948 3253 3248 2952 3253 2948 +2952 2953 3253 3253 2953 3254 3251 2955 2945 +3255 2955 3251 2956 3252 2946 3252 2956 2950 +3256 3254 2953 3256 2953 2958 2958 2959 3256 +3256 2959 3257 3255 3258 2955 2955 3258 2960 +2961 2960 3258 2961 3258 3259 2965 3260 3261 +3257 2965 3261 2959 2965 3257 3260 3262 3261 +3263 3264 3265 3259 3263 3265 2967 3259 3265 +2961 3259 2967 2971 3266 2972 3266 2973 2972 +2975 3260 2965 2975 2976 3260 2976 3262 3260 +2976 3267 3262 3268 3269 3270 2978 2977 3270 +2977 3268 3270 2977 3271 3268 2980 3265 3264 +3271 2980 3264 2977 2980 3271 3265 2980 2967 +2981 3272 3266 2981 3266 2971 3272 2984 2973 +3272 2973 3266 2987 3273 3274 3267 2987 3274 +2976 2987 3267 3273 3275 3274 3276 3277 3278 +3279 3280 3278 3278 3280 3276 3279 3281 3280 +3282 3283 3284 3282 3284 3269 3269 3284 3270 +3284 2989 3270 2989 2988 3270 3270 2988 2978 +2991 3272 2981 3272 2991 2984 2992 3285 3273 +2992 3273 2987 3275 3273 3286 3286 3273 3285 +3287 3288 3289 2994 2993 3289 2993 3287 3289 +2993 3290 3287 2995 3278 3277 3290 2995 3277 +2993 2995 3290 3278 2995 2996 3278 2996 3279 +3291 3281 3279 2996 3291 3279 2998 3291 2996 +3292 2998 3000 3292 3291 2998 3002 3284 3283 +3292 3002 3283 3000 3002 3292 3284 3002 2989 +3003 3285 2992 3003 3004 3285 3004 3286 3285 +3004 3293 3286 3289 3288 3005 3288 3006 3005 +3288 3294 3006 3289 3005 2994 3007 3293 3004 +3007 3295 3293 3294 3008 3006 3296 3008 3294 +3009 3297 3298 3295 3009 3298 3007 3009 3295 +3297 3299 3298 3296 3010 3008 3300 3010 3296 +3012 3297 3009 3012 3013 3297 3013 3299 3297 +3013 3301 3299 3300 3014 3010 3302 3014 3300 +3016 3301 3013 3016 3303 3301 3304 3017 3305 +3017 3302 3305 3017 3014 3302 3306 3304 3305 +3019 3303 3016 3019 3307 3303 3304 3020 3017 +3021 3020 3304 3306 3021 3304 3308 3021 3306 +3022 3309 3023 3309 3310 3023 3310 3019 3023 +3310 3307 3019 3308 3024 3021 3311 3024 3308 +3312 3313 3309 3022 3312 3309 3025 3312 3022 +3309 3313 3310 3314 3315 3026 3026 3315 3027 +3026 3024 3311 3026 3311 3314 3028 3312 3025 +3028 3316 3312 3315 3029 3027 3317 3029 3315 +3318 3319 3320 3319 3316 3320 3316 3030 3320 +3316 3028 3030 3317 3031 3029 3321 3031 3317 +3318 3320 3033 3032 3318 3033 3322 3318 3032 +3033 3320 3030 3321 3034 3031 3323 3034 3321 +3035 3322 3032 3035 3324 3322 3325 3036 3326 +3036 3323 3326 3036 3034 3323 3327 3325 3326 +3037 3324 3035 3037 3328 3324 3325 3038 3036 +3039 3038 3325 3327 3039 3325 3329 3039 3327 +3040 3330 3331 3328 3040 3331 3037 3040 3328 +3330 3332 3331 3329 3041 3039 3333 3041 3329 +3042 3330 3040 3042 3043 3330 3043 3332 3330 +3043 3334 3332 3333 3044 3041 3335 3044 3333 +3045 3334 3043 3045 3336 3334 3335 3046 3044 +3337 3046 3335 3047 3336 3045 3047 3338 3336 +3337 3048 3046 3339 3048 3337 3049 3338 3047 +3049 3340 3338 3341 3342 3343 3339 3341 3343 +3050 3339 3343 3048 3339 3050 3344 3340 3049 +3344 3049 3052 3052 3053 3344 3344 3053 3345 +3343 3342 3054 3342 3055 3054 3342 3346 3055 +3343 3054 3050 3063 3345 3053 3063 3347 3345 +3346 3064 3055 3348 3064 3346 3068 3349 3067 +3349 3069 3067 3350 3351 3077 3350 3077 3076 +3063 3077 3347 3347 3077 3351 3348 3078 3064 +3352 3078 3348 3082 3353 3081 3349 3068 3081 +3353 3349 3081 3354 3349 3353 3355 3083 3069 +3349 3355 3069 3354 3355 3349 3356 3084 3083 +3356 3083 3355 3357 3085 3084 3357 3084 3356 +3358 3086 3085 3358 3085 3357 3359 3087 3086 +3359 3086 3358 3359 3088 3087 3091 3350 3076 +3091 3360 3350 3352 3361 3078 3078 3361 3092 +3093 3092 3361 3093 3361 3362 3096 3363 3097 +3364 3098 3097 3364 3097 3363 3364 3365 3098 +3098 3365 3082 3365 3366 3082 3366 3353 3082 +3353 3366 3354 3354 3366 3367 3354 3367 3355 +3355 3367 3368 3355 3368 3356 3356 3368 3369 +3356 3369 3357 3357 3369 3370 3357 3370 3358 +3358 3370 3371 3372 3373 3371 3373 3358 3371 +3373 3359 3358 3374 3099 3088 3359 3374 3088 +3373 3374 3359 3375 3100 3099 3375 3099 3374 +3375 3101 3100 3103 3360 3091 3103 3376 3360 +3362 3104 3093 3377 3104 3362 3106 3378 3107 +3379 3108 3107 3379 3107 3378 3379 3380 3108 +3108 3380 3096 3380 3381 3096 3381 3363 3096 +3363 3381 3364 3364 3381 3382 3364 3382 3365 +3372 3383 3373 3373 3383 3374 3374 3383 3384 +3374 3384 3375 3375 3384 3385 3385 3386 3387 +3385 3387 3109 3375 3385 3109 3101 3375 3109 +3388 3110 3109 3388 3109 3387 3388 3111 3110 +3113 3376 3103 3113 3389 3376 3377 3115 3104 +3390 3115 3377 3116 3391 3117 3391 3392 3117 +3117 3392 3106 3392 3393 3106 3393 3378 3106 +3378 3393 3379 3379 3393 3394 3379 3394 3380 +3386 3395 3387 3387 3395 3388 3388 3395 3396 +3396 3397 3398 3396 3398 3118 3388 3396 3118 +3111 3388 3118 3398 3119 3118 3121 3389 3113 +3121 3399 3389 3390 3400 3115 3115 3400 3122 +3123 3122 3400 3123 3400 3401 3126 3402 3391 +3126 3391 3116 3392 3391 3403 3403 3391 3402 +3397 3404 3398 3404 3405 3406 3404 3406 3127 +3398 3404 3127 3119 3398 3127 3406 3128 3127 +3130 3399 3121 3130 3407 3399 3131 3123 3408 +3123 3409 3408 3123 3401 3409 3408 3132 3131 +3134 3410 3135 3411 3136 3135 3411 3135 3410 +3411 3412 3136 3136 3412 3126 3412 3413 3126 +3413 3402 3126 3402 3413 3403 3405 3414 3406 +3414 3415 3416 3414 3416 3137 3406 3414 3137 +3128 3406 3137 3416 3138 3137 3140 3417 3141 +3141 3417 3418 3419 3141 3418 3142 3141 3419 +3130 3142 3407 3407 3142 3419 3409 3420 3408 +3420 3421 3422 3420 3422 3143 3408 3420 3143 +3132 3408 3143 3410 3134 3143 3422 3410 3143 +3423 3410 3422 3424 3411 3425 3411 3423 3425 +3411 3410 3423 3411 3424 3412 3415 3426 3416 +3416 3426 3138 3426 3144 3138 3426 3427 3144 +3140 3144 3417 3144 3428 3417 3144 3427 3428 +3417 3428 3418 3421 3429 3422 3422 3429 3423 +3423 3429 3430 3423 3430 3425 3431 3148 3147 +3149 3432 3147 3147 3432 3431 3150 3432 3149 +3433 3153 3431 3431 3153 3148 3432 3434 3435 +3433 3432 3435 3431 3432 3433 3432 3150 3434 +3150 3436 3434 3150 3155 3436 3437 3158 3159 +3160 3438 3159 3159 3438 3437 3153 3433 3160 +3433 3438 3160 3433 3439 3438 3433 3435 3440 +3433 3440 3439 3441 3436 3155 3441 3155 3161 +3442 3441 3443 3441 3162 3443 3441 3161 3162 +3163 3444 3162 3162 3444 3443 3164 3444 3163 +3445 3446 3447 3448 3449 3447 3447 3449 3445 +3448 3450 3449 3451 3167 3437 3437 3167 3158 +3452 3437 3438 3451 3437 3452 3439 3453 3454 +3452 3439 3454 3438 3439 3452 3453 3439 3440 +3455 3442 3443 3455 3443 3444 3455 3444 3456 +3457 3456 3458 3458 3456 3168 3456 3444 3168 +3444 3164 3168 3169 3458 3168 3459 3460 3461 +3459 3461 3446 3461 3447 3446 3461 3462 3447 +3447 3463 3448 3447 3462 3463 3450 3448 3464 +3448 3465 3464 3448 3463 3465 3465 3466 3464 +3467 3171 3170 3172 3468 3170 3170 3468 3467 +3469 3468 3172 3469 3172 3167 3470 3469 3167 +3451 3470 3167 3470 3451 3452 3470 3452 3471 +3471 3452 3454 3472 3473 3474 3475 3476 3474 +3475 3474 3473 3477 3476 3475 3478 3457 3458 +3479 3478 3480 3480 3478 3173 3478 3458 3173 +3458 3169 3173 3174 3480 3173 3460 3481 3461 +3461 3481 3462 3462 3481 3482 3483 3484 3482 +3484 3462 3482 3484 3463 3462 3463 3485 3465 +3463 3484 3485 3466 3465 3486 3465 3487 3486 +3465 3485 3487 3487 3488 3486 3489 3176 3177 +3178 3490 3177 3177 3490 3489 3491 3490 3178 +3491 3178 3171 3492 3491 3171 3467 3492 3171 +3492 3467 3468 3492 3468 3493 3493 3468 3469 +3494 3472 3474 3494 3474 3476 3494 3476 3495 +3496 3497 3498 3477 3495 3476 3499 3497 3496 +3479 3480 3500 3479 3500 3501 3180 3500 3174 +3174 3500 3480 3483 3502 3484 3503 3504 3502 +3504 3484 3502 3504 3485 3484 3485 3505 3487 +3485 3504 3505 3488 3487 3506 3506 3487 3505 +3507 3182 3183 3508 3507 3183 3508 3183 3176 +3509 3508 3176 3489 3509 3176 3509 3489 3490 +3509 3490 3510 3510 3490 3491 3511 3498 3497 +3511 3497 3499 3512 3501 3500 3512 3500 3180 +3185 3512 3180 3513 3512 3185 3185 3186 3513 +3513 3186 3514 3503 3515 3504 3504 3515 3505 +3505 3515 3516 3505 3516 3506 3517 3518 3187 +3187 3518 3188 3519 3517 3520 3517 3189 3520 +3517 3187 3189 3520 3189 3521 3189 3522 3521 +3189 3190 3522 3190 3182 3507 3523 3190 3507 +3522 3190 3523 3523 3507 3508 3192 3524 3525 +3514 3192 3525 3186 3192 3514 3524 3526 3525 +3527 3528 3195 3195 3528 3194 3195 3188 3518 +3195 3518 3527 3529 3519 3520 3529 3520 3521 +3197 3530 3524 3197 3524 3192 3526 3524 3531 +3531 3524 3530 3532 3533 3198 3198 3533 3199 +3198 3194 3528 3198 3528 3532 3201 3534 3530 +3201 3530 3197 3531 3530 3535 3535 3530 3534 +3536 3537 3202 3202 3537 3203 3202 3199 3533 +3202 3533 3536 3205 3538 3534 3205 3534 3201 +3535 3534 3539 3539 3534 3538 3540 3541 3542 +3537 3540 3542 3206 3537 3542 3203 3537 3206 +3207 3543 3538 3207 3538 3205 3539 3538 3544 +3544 3538 3543 3545 3546 3547 3545 3547 3541 +3541 3547 3542 3547 3209 3542 3209 3208 3542 +3542 3208 3206 3211 3548 3543 3211 3543 3207 +3544 3543 3549 3549 3543 3548 3547 3546 3212 +3546 3213 3212 3546 3550 3213 3547 3212 3209 +3217 3551 3218 3551 3552 3218 3218 3552 3211 +3552 3553 3211 3553 3548 3211 3548 3553 3549 +3554 3555 3219 3219 3555 3220 3219 3213 3550 +3219 3550 3554 3223 3556 3224 3224 3556 3557 +3558 3224 3557 3225 3224 3558 3217 3225 3551 +3225 3559 3551 3225 3558 3559 3551 3559 3552 +3555 3226 3220 3560 3226 3555 3228 3561 3229 +3229 3561 3562 3563 3229 3562 3230 3229 3563 +3223 3230 3556 3230 3564 3556 3230 3563 3564 +3556 3564 3557 3560 3231 3226 3565 3231 3560 +3566 3567 3561 3228 3566 3561 3234 3566 3228 +3561 3567 3562 3565 3235 3231 3568 3235 3565 +3238 3569 3237 3569 3570 3237 3570 3234 3237 +3570 3566 3234 3568 3239 3235 3571 3239 3568 +3572 3573 3569 3238 3572 3569 3241 3572 3238 +3569 3573 3570 3571 3574 3239 3239 3574 3242 +3243 3242 3574 3243 3574 3575 3245 3576 3246 +3576 3577 3246 3577 3241 3246 3577 3572 3241 +3575 3247 3243 3578 3247 3575 3579 3580 3250 +3579 3250 3249 3245 3250 3576 3250 3581 3576 +3250 3580 3581 3576 3581 3577 3582 3251 3583 +3251 3578 3583 3251 3247 3578 3584 3582 3583 +3585 3579 3249 3585 3249 3253 3253 3254 3585 +3585 3254 3586 3587 3255 3582 3582 3255 3251 +3582 3584 3588 3582 3588 3587 3589 3586 3254 +3589 3254 3256 3256 3257 3589 3589 3257 3590 +3587 3258 3255 3591 3592 3259 3591 3259 3258 +3588 3591 3258 3587 3588 3258 3593 3592 3591 +3594 3590 3257 3594 3257 3261 3261 3262 3594 +3594 3262 3595 3596 3597 3598 3599 3600 3598 +3598 3600 3596 3264 3263 3599 3263 3600 3599 +3263 3601 3600 3263 3259 3592 3602 3263 3592 +3601 3263 3602 3602 3592 3593 3267 3595 3262 +3267 3603 3595 3604 3605 3606 3604 3606 3597 +3597 3606 3598 3606 3269 3598 3269 3268 3598 +3598 3268 3271 3598 3271 3599 3599 3271 3264 +3267 3274 3607 3608 3267 3607 3603 3267 3608 +3275 3607 3274 3609 3610 3611 3277 3276 3611 +3276 3609 3611 3276 3612 3609 3613 3276 3280 +3613 3612 3276 3614 3615 3281 3615 3280 3281 +3615 3613 3280 3616 3617 3614 3614 3617 3615 +3617 3616 3605 3605 3616 3606 3616 3283 3606 +3283 3282 3606 3606 3282 3269 3618 3608 3607 +3607 3275 3618 3275 3619 3618 3275 3286 3619 +3620 3621 3287 3287 3621 3288 3290 3611 3610 +3620 3290 3610 3287 3290 3620 3611 3290 3277 +3291 3614 3281 3614 3291 3292 3614 3292 3616 +3616 3292 3283 3293 3619 3286 3293 3622 3619 +3621 3294 3288 3623 3294 3621 3295 3624 3625 +3622 3295 3625 3293 3295 3622 3624 3626 3625 +3623 3296 3294 3627 3296 3623 3298 3624 3295 +3298 3299 3624 3299 3626 3624 3299 3628 3626 +3627 3300 3296 3629 3300 3627 3301 3628 3299 +3301 3630 3628 3629 3302 3300 3631 3302 3629 +3303 3630 3301 3303 3632 3630 3631 3633 3302 +3302 3633 3305 3306 3305 3633 3306 3633 3634 +3307 3632 3303 3307 3635 3632 3634 3308 3306 +3636 3308 3634 3310 3635 3307 3310 3637 3635 +3638 3639 3640 3636 3638 3640 3311 3636 3640 +3308 3636 3311 3641 3642 3313 3641 3313 3312 +3310 3313 3637 3637 3313 3642 3640 3639 3314 +3639 3315 3314 3639 3643 3315 3640 3314 3311 +3316 3641 3312 3316 3644 3641 3643 3317 3315 +3645 3317 3643 3646 3647 3319 3646 3319 3318 +3316 3319 3644 3644 3319 3647 3648 3321 3649 +3321 3645 3649 3321 3317 3645 3650 3648 3649 +3322 3646 3318 3322 3651 3646 3652 3323 3648 +3648 3323 3321 3648 3650 3653 3648 3653 3652 +3324 3651 3322 3324 3654 3651 3652 3326 3323 +3327 3326 3652 3653 3327 3652 3655 3327 3653 +3328 3656 3657 3654 3328 3657 3324 3328 3654 +3656 3658 3657 3655 3329 3327 3659 3329 3655 +3331 3656 3328 3331 3332 3656 3332 3658 3656 +3332 3660 3658 3659 3333 3329 3661 3333 3659 +3334 3660 3332 3334 3662 3660 3661 3335 3333 +3663 3335 3661 3336 3662 3334 3336 3664 3662 +3663 3337 3335 3665 3337 3663 3338 3664 3336 +3338 3666 3664 3665 3339 3337 3667 3339 3665 +3340 3668 3669 3666 3340 3669 3338 3340 3666 +3668 3670 3669 3671 3672 3341 3341 3672 3342 +3341 3339 3667 3341 3667 3671 3344 3668 3340 +3344 3345 3668 3345 3670 3668 3345 3673 3670 +3672 3346 3342 3674 3346 3672 3675 3676 3677 +3676 3673 3677 3673 3347 3677 3673 3345 3347 +3674 3348 3346 3678 3348 3674 3675 3677 3351 +3350 3675 3351 3679 3675 3350 3351 3677 3347 +3680 3352 3681 3352 3678 3681 3352 3348 3678 +3682 3680 3681 3360 3679 3350 3360 3683 3679 +3680 3361 3352 3362 3361 3680 3682 3362 3680 +3684 3362 3682 3365 3685 3366 3686 3367 3366 +3686 3366 3685 3687 3368 3367 3687 3367 3686 +3688 3369 3368 3688 3368 3687 3689 3370 3369 +3689 3369 3688 3690 3371 3370 3690 3370 3689 +3690 3372 3371 3376 3683 3360 3376 3691 3683 +3684 3377 3362 3692 3377 3684 3380 3693 3381 +3694 3382 3381 3694 3381 3693 3694 3695 3382 +3382 3695 3365 3695 3696 3365 3696 3685 3365 +3685 3696 3686 3686 3696 3697 3698 3699 3697 +3699 3686 3697 3699 3687 3686 3700 3688 3701 +3688 3699 3701 3688 3687 3699 3688 3700 3689 +3689 3700 3702 3689 3702 3690 3690 3702 3703 +3703 3704 3705 3703 3705 3383 3690 3703 3383 +3372 3690 3383 3706 3384 3383 3706 3383 3705 +3707 3385 3384 3707 3384 3706 3707 3386 3385 +3389 3691 3376 3389 3708 3691 3692 3390 3377 +3709 3390 3692 3392 3710 3393 3711 3394 3393 +3711 3393 3710 3693 3380 3394 3711 3693 3394 +3712 3693 3711 3713 3694 3714 3694 3712 3714 +3694 3693 3712 3694 3713 3695 3698 3715 3699 +3699 3715 3701 3704 3716 3705 3705 3716 3706 +3706 3716 3717 3706 3717 3707 3707 3717 3718 +3718 3719 3720 3718 3720 3395 3707 3718 3395 +3386 3707 3395 3721 3396 3395 3721 3395 3720 +3721 3397 3396 3399 3708 3389 3399 3722 3708 +3400 3390 3723 3390 3724 3723 3390 3709 3724 +3723 3401 3400 3403 3725 3710 3403 3710 3392 +3726 3711 3727 3711 3725 3727 3711 3710 3725 +3711 3726 3712 3712 3726 3728 3712 3728 3714 +3719 3729 3720 3720 3729 3721 3721 3729 3730 +3721 3730 3397 3730 3404 3397 3730 3731 3404 +3405 3404 3731 3405 3731 3732 3407 3722 3399 +3407 3733 3722 3724 3734 3723 3401 3723 3734 +3735 3401 3734 3409 3401 3735 3412 3736 3413 +3736 3737 3413 3413 3737 3403 3737 3738 3403 +3738 3725 3403 3725 3738 3727 3414 3405 3739 +3405 3740 3739 3405 3732 3740 3739 3415 3414 +3418 3741 3419 3741 3742 3419 3742 3407 3419 +3742 3733 3407 3420 3409 3743 3409 3744 3743 +3409 3735 3744 3743 3421 3420 3425 3745 3424 +3745 3746 3424 3424 3746 3412 3746 3747 3412 +3747 3736 3412 3736 3747 3737 3740 3748 3739 +3739 3748 3415 3748 3426 3415 3748 3749 3426 +3749 3750 3751 3427 3749 3751 3426 3749 3427 +3752 3428 3427 3752 3427 3751 3752 3753 3428 +3428 3753 3418 3753 3754 3418 3754 3741 3418 +3741 3754 3742 3743 3744 3755 3755 3744 3756 +3757 3429 3421 3743 3757 3421 3755 3757 3743 +3758 3430 3429 3758 3429 3757 3758 3759 3430 +3430 3759 3425 3759 3760 3425 3760 3745 3425 +3745 3760 3746 3750 3761 3751 3751 3761 3752 +3752 3761 3762 3752 3762 3753 3756 3763 3755 +3755 3763 3757 3757 3763 3764 3757 3764 3758 +3758 3764 3765 3758 3765 3759 3766 3435 3434 +3436 3766 3434 3767 3768 3769 3769 3770 3767 +3771 3440 3766 3766 3440 3435 3441 3772 3436 +3772 3766 3436 3772 3771 3766 3442 3772 3441 +3773 3774 3775 3446 3445 3775 3445 3773 3775 +3445 3776 3773 3449 3769 3768 3776 3449 3768 +3445 3449 3776 3777 3778 3450 3450 3778 3449 +3778 3770 3449 3770 3769 3449 3777 3779 3778 +3780 3454 3453 3440 3771 3453 3771 3780 3453 +3771 3781 3780 3782 3771 3772 3781 3771 3782 +3455 3783 3442 3783 3772 3442 3783 3782 3772 +3456 3784 3455 3455 3784 3783 3457 3784 3456 +3785 3786 3787 3785 3787 3774 3774 3787 3775 +3787 3460 3775 3460 3459 3775 3775 3459 3446 +3464 3777 3450 3788 3789 3466 3466 3789 3464 +3789 3779 3464 3779 3777 3464 3790 3791 3788 +3788 3791 3789 3790 3792 3791 3793 3469 3470 +3471 3794 3470 3470 3794 3793 3454 3780 3471 +3780 3794 3471 3780 3795 3794 3472 3795 3473 +3795 3781 3473 3795 3780 3781 3473 3781 3475 +3475 3781 3782 3475 3782 3477 3782 3796 3477 +3782 3783 3796 3797 3783 3784 3796 3783 3797 +3478 3798 3457 3798 3784 3457 3798 3797 3784 +3479 3798 3478 3799 3800 3801 3799 3801 3786 +3801 3787 3786 3801 3802 3787 3803 3481 3460 +3787 3803 3460 3802 3803 3787 3804 3482 3481 +3804 3481 3803 3804 3483 3482 3486 3788 3466 +3788 3486 3488 3805 3788 3488 3790 3788 3805 +3792 3790 3806 3790 3807 3806 3790 3805 3807 +3807 3808 3806 3809 3491 3492 3493 3810 3492 +3492 3810 3809 3469 3793 3493 3793 3810 3493 +3793 3811 3810 3812 3793 3794 3811 3793 3812 +3813 3794 3795 3812 3794 3813 3494 3814 3472 +3814 3795 3472 3814 3813 3795 3495 3815 3494 +3494 3815 3814 3495 3477 3815 3477 3796 3815 +3496 3498 3815 3496 3815 3796 3496 3796 3499 +3796 3816 3499 3796 3797 3816 3817 3797 3798 +3816 3797 3817 3501 3817 3479 3479 3817 3798 +3800 3818 3801 3801 3818 3802 3802 3818 3819 +3802 3819 3803 3803 3819 3820 3803 3820 3804 +3804 3820 3821 3821 3822 3823 3821 3823 3502 +3804 3821 3502 3483 3804 3502 3823 3503 3502 +3506 3824 3805 3506 3805 3488 3805 3825 3807 +3805 3824 3825 3808 3807 3826 3826 3807 3825 +3827 3508 3509 3510 3828 3509 3509 3828 3827 +3491 3809 3510 3809 3828 3510 3809 3829 3828 +3830 3809 3810 3829 3809 3830 3831 3810 3811 +3830 3810 3831 3812 3832 3833 3831 3812 3833 +3811 3812 3831 3832 3812 3813 3832 3813 3834 +3835 3836 3834 3813 3835 3834 3814 3835 3813 +3837 3814 3815 3835 3814 3837 3511 3838 3498 +3838 3815 3498 3838 3837 3815 3839 3838 3511 +3839 3511 3499 3840 3839 3499 3816 3840 3499 +3840 3816 3817 3840 3817 3841 3841 3817 3501 +3512 3841 3501 3842 3841 3512 3843 3842 3844 +3842 3513 3844 3842 3512 3513 3514 3844 3513 +3822 3845 3823 3845 3846 3847 3845 3847 3515 +3823 3845 3515 3503 3823 3515 3848 3516 3515 +3848 3515 3847 3824 3506 3516 3848 3824 3516 +3849 3824 3848 3850 3825 3851 3825 3849 3851 +3825 3824 3849 3825 3850 3826 3852 3518 3517 +3519 3852 3517 3853 3521 3522 3523 3854 3522 +3522 3854 3853 3855 3854 3523 3855 3523 3508 +3856 3855 3508 3827 3856 3508 3856 3827 3828 +3856 3828 3857 3857 3828 3829 3857 3829 3858 +3858 3829 3830 3858 3830 3859 3859 3830 3831 +3859 3831 3860 3860 3831 3833 3861 3836 3835 +3861 3835 3837 3861 3837 3862 3863 3864 3862 +3837 3863 3862 3838 3863 3837 3838 3839 3865 +3866 3838 3865 3863 3838 3866 3867 3866 3865 +3868 3843 3844 3869 3868 3870 3870 3868 3525 +3868 3844 3525 3844 3514 3525 3526 3870 3525 +3846 3871 3847 3847 3871 3848 3848 3871 3872 +3848 3872 3849 3849 3872 3873 3849 3873 3851 +3874 3875 3876 3528 3527 3876 3527 3874 3876 +3527 3877 3874 3877 3527 3518 3878 3877 3518 +3852 3878 3518 3877 3878 3879 3878 3852 3519 +3880 3878 3519 3529 3880 3519 3881 3878 3880 +3529 3521 3853 3882 3529 3853 3880 3529 3882 +3882 3853 3854 3882 3854 3883 3883 3854 3855 +3884 3864 3863 3884 3863 3866 3884 3866 3885 +3885 3866 3867 3886 3869 3870 3870 3526 3886 +3526 3887 3886 3526 3531 3887 3888 3889 3890 +3888 3890 3875 3875 3890 3876 3890 3533 3876 +3533 3532 3876 3876 3532 3528 3891 3879 3878 +3891 3878 3881 3535 3887 3531 3535 3892 3887 +3893 3894 3895 3893 3895 3889 3889 3895 3890 +3895 3537 3890 3537 3536 3890 3890 3536 3533 +3539 3892 3535 3539 3896 3892 3897 3898 3899 +3897 3899 3894 3894 3899 3895 3899 3541 3895 +3541 3540 3895 3895 3540 3537 3544 3896 3539 +3544 3900 3896 3901 3902 3903 3901 3903 3898 +3898 3903 3899 3903 3546 3899 3546 3545 3899 +3899 3545 3541 3549 3900 3544 3549 3904 3900 +3905 3906 3907 3905 3907 3902 3907 3903 3902 +3907 3908 3903 3908 3550 3546 3908 3546 3903 +3552 3909 3553 3909 3910 3553 3910 3549 3553 +3910 3904 3549 3907 3906 3911 3911 3906 3912 +3911 3555 3554 3908 3911 3554 3907 3911 3908 +3908 3554 3550 3557 3913 3558 3558 3913 3914 +3915 3558 3914 3559 3558 3915 3552 3559 3909 +3559 3916 3909 3559 3915 3916 3909 3916 3910 +3912 3917 3911 3555 3911 3917 3918 3555 3917 +3560 3555 3918 3562 3919 3563 3563 3919 3920 +3921 3563 3920 3564 3563 3921 3557 3564 3913 +3564 3922 3913 3564 3921 3922 3913 3922 3914 +3918 3565 3560 3923 3565 3918 3566 3924 3567 +3924 3925 3567 3567 3925 3562 3925 3926 3562 +3926 3919 3562 3919 3926 3920 3923 3568 3565 +3927 3568 3923 3928 3929 3924 3566 3928 3924 +3570 3928 3566 3924 3929 3925 3927 3571 3568 +3930 3571 3927 3931 3932 3573 3931 3573 3572 +3570 3573 3928 3928 3573 3932 3930 3933 3571 +3571 3933 3574 3575 3574 3933 3575 3933 3934 +3935 3936 3937 3936 3931 3937 3931 3577 3937 +3931 3572 3577 3938 3578 3939 3578 3934 3939 +3578 3575 3934 3940 3938 3939 3579 3941 3580 +3942 3935 3937 3580 3942 3937 3581 3580 3937 +3942 3580 3941 3581 3937 3577 3938 3583 3578 +3584 3583 3938 3940 3584 3938 3943 3584 3940 +3941 3579 3585 3944 3945 3946 3945 3944 3586 +3944 3942 3586 3941 3585 3586 3941 3586 3942 +3947 3588 3948 3588 3943 3948 3588 3584 3943 +3949 3947 3948 3950 3946 3945 3950 3945 3586 +3589 3950 3586 3951 3950 3589 3589 3590 3951 +3951 3590 3952 3947 3591 3588 3593 3591 3947 +3949 3593 3947 3953 3593 3949 3954 3952 3590 +3954 3590 3594 3594 3595 3954 3954 3595 3955 +3956 3957 3596 3596 3957 3597 3958 3596 3600 +3958 3956 3596 3959 3600 3601 3959 3958 3600 +3960 3601 3602 3960 3959 3601 3602 3593 3953 +3602 3953 3960 3603 3955 3595 3603 3961 3955 +3962 3963 3964 3964 3965 3962 3966 3967 3604 +3604 3967 3605 3604 3597 3957 3604 3957 3966 +3608 3961 3603 3608 3968 3961 3969 3970 3609 +3609 3970 3610 3612 3964 3963 3969 3612 3963 +3609 3612 3969 3971 3965 3964 3612 3971 3964 +3613 3971 3612 3972 3613 3615 3972 3971 3613 +3973 3615 3617 3973 3972 3615 3617 3605 3967 +3617 3967 3973 3974 3968 3608 3974 3608 3618 +3618 3619 3974 3974 3619 3975 3976 3977 3620 +3620 3977 3621 3620 3610 3970 3620 3970 3976 +3622 3978 3979 3975 3622 3979 3619 3622 3975 +3978 3980 3979 3977 3623 3621 3981 3623 3977 +3625 3978 3622 3625 3626 3978 3626 3980 3978 +3626 3982 3980 3981 3627 3623 3983 3627 3981 +3628 3982 3626 3628 3984 3982 3983 3629 3627 +3985 3629 3983 3630 3984 3628 3630 3986 3984 +3985 3631 3629 3987 3631 3985 3632 3986 3630 +3632 3988 3986 3987 3989 3631 3631 3989 3633 +3634 3633 3989 3634 3989 3990 3635 3988 3632 +3635 3991 3988 3990 3636 3634 3992 3636 3990 +3637 3991 3635 3637 3993 3991 3994 3995 3638 +3638 3995 3639 3638 3636 3992 3638 3992 3994 +3641 3996 3642 3996 3997 3642 3997 3637 3642 +3997 3993 3637 3995 3643 3639 3998 3643 3995 +3999 4000 4001 4000 4002 3996 3644 4000 3996 +3641 3644 3996 4001 4000 3644 3996 4002 3997 +4003 3645 4004 3645 3998 4004 3645 3643 3998 +4005 4003 4004 3999 4001 3647 3646 3999 3647 +4006 3999 3646 3647 4001 3644 4003 3649 3645 +3650 3649 4003 4005 3650 4003 4007 3650 4005 +3651 4008 4009 4006 3651 4009 3646 3651 4006 +4008 4010 4009 4007 3653 3650 4011 3653 4007 +3654 4012 4008 3654 4008 3651 4010 4008 4013 +4013 4008 4012 4011 3655 3653 4014 3655 4011 +3657 4012 3654 3657 3658 4012 3658 4013 4012 +3658 4015 4013 4014 3659 3655 4016 3659 4014 +3660 4015 3658 3660 4017 4015 4016 3661 3659 +4018 3661 4016 3662 4017 3660 3662 4019 4017 +4018 3663 3661 4020 3663 4018 3664 4019 3662 +3664 4021 4019 4020 3665 3663 4022 3665 4020 +3666 4021 3664 3666 4023 4021 4022 3667 3665 +4024 3667 4022 4025 4023 3666 4025 3666 3669 +3669 3670 4025 4025 3670 4026 4027 3672 3671 +4028 4027 3671 3667 4028 3671 4024 4028 3667 +3673 4026 3670 3673 4029 4026 4030 3674 4027 +4027 3674 3672 4027 4028 4031 4027 4031 4030 +4032 4033 3676 4032 3676 3675 3673 3676 4029 +4029 3676 4033 4034 3678 4030 4030 3678 3674 +4030 4031 4035 4030 4035 4034 3679 4032 3675 +3679 4036 4032 4034 3681 3678 3682 3681 4034 +4035 3682 4034 4037 3682 4035 3683 4036 3679 +3683 4038 4036 4037 3684 3682 4039 3684 4037 +3691 4038 3683 3691 4040 4038 4039 3692 3684 +4041 3692 4039 3695 4042 3696 4043 3697 3696 +4043 3696 4042 4043 3698 3697 3701 4044 3700 +4045 3702 3700 4045 3700 4044 4046 3703 3702 +4046 3702 4045 4046 3704 3703 3708 4040 3691 +3708 4047 4040 4041 3709 3692 4048 3709 4041 +3714 4049 3713 4049 4050 3713 3713 4050 3695 +4050 4051 3695 4051 4042 3695 4052 4053 4051 +4053 4042 4051 4053 4043 4042 4054 3715 3698 +4043 4054 3698 4053 4054 4043 4054 4055 3715 +3715 4055 3701 4055 4056 3701 4056 4044 3701 +4044 4056 4045 4045 4056 4057 4045 4057 4046 +4046 4057 4058 4046 4058 3704 4058 3716 3704 +4058 4059 3716 4059 4060 4061 3717 4059 4061 +3716 4059 3717 4062 3718 3717 4062 3717 4061 +4062 3719 3718 3722 4047 3708 3722 4063 4047 +4048 3724 3709 4064 3724 4048 3727 4065 3726 +4066 3728 3726 4066 3726 4065 4066 4067 3728 +3728 4067 3714 4067 4068 3714 4068 4049 3714 +4049 4068 4050 4052 4069 4053 4053 4069 4054 +4054 4069 4070 4054 4070 4055 4060 4071 4061 +4061 4071 4062 4062 4071 4072 4062 4072 3719 +4072 3729 3719 4072 4073 3729 4073 4074 4075 +3730 4073 4075 3729 4073 3730 4076 3731 3730 +4076 3730 4075 4076 3732 3731 3733 4063 3722 +3733 4077 4063 3734 3724 4078 3724 4079 4078 +3724 4064 4079 4078 3735 3734 4080 4081 3738 +4080 3738 3737 3727 3738 4065 3738 4082 4065 +3738 4081 4082 4065 4082 4066 4066 4082 4083 +4066 4083 4067 4074 4084 4075 4075 4084 4076 +4076 4084 4085 3732 4076 4085 4086 3732 4085 +3740 3732 4086 3742 4077 3733 3742 4087 4077 +4078 4079 4088 4088 4079 4089 4088 3744 3735 +4088 3735 4078 3746 4090 3747 4090 4091 3747 +4091 3737 3747 4091 4080 3737 4086 4092 3740 +3740 4092 3748 4092 4093 4094 3749 4092 4094 +3748 4092 3749 4094 3750 3749 3753 4095 3754 +4095 4096 3754 4096 3742 3754 4096 4087 3742 +4088 4089 4097 4097 4089 4098 4097 3756 3744 +4097 3744 4088 4099 4100 3760 4099 3760 3759 +3746 3760 4090 3760 4101 4090 3760 4100 4101 +4090 4101 4091 4093 4102 4094 4094 4102 3750 +4102 3761 3750 4102 4103 3761 3761 4104 3762 +4103 4104 3761 3753 3762 4095 3762 4105 4095 +3762 4104 4105 4095 4105 4096 4098 4106 4097 +4097 4106 3756 4106 3763 3756 4106 4107 3763 +3763 4108 3764 4107 4108 3763 3764 4109 3765 +4108 4109 3764 3759 3765 4099 4099 3765 4109 +4110 4111 4112 4112 3768 4110 3768 3767 4110 +4113 4114 4110 4113 4110 3767 3770 4113 3767 +4115 4116 4117 3774 3773 4117 3773 4115 4117 +3773 4118 4115 3776 4112 4111 4118 3776 4111 +3773 3776 4118 4113 4119 4114 3768 4112 3776 +4119 4113 3770 3778 4119 3770 4120 4119 3778 +4121 4122 3779 4122 3778 3779 4122 4120 3778 +4121 4123 4122 4124 4125 4126 4124 4126 4116 +4116 4126 4117 4126 3786 4117 3786 3785 4117 +4117 3785 3774 3789 4121 3779 4127 4123 4121 +3789 4127 4121 3791 4127 3789 4128 4129 3792 +4129 3791 3792 4129 4127 3791 4128 4130 4129 +4131 4132 4133 4131 4133 4125 4125 4133 4126 +4133 3800 4126 3800 3799 4126 4126 3799 3786 +3806 4128 3792 4134 4135 3808 3808 4135 3806 +4135 4130 3806 4130 4128 3806 4134 4136 4135 +4133 4132 4137 4137 4132 4138 4139 3818 3800 +4133 4139 3800 4137 4139 4133 4140 3819 3818 +4140 3818 4139 4141 3820 3819 4141 3819 4140 +4142 3821 3820 4142 3820 4141 4142 3822 3821 +3826 4143 4134 3826 4134 3808 4136 4134 4144 +4134 4145 4144 4134 4143 4145 4145 4146 4144 +4147 3833 3832 3834 4148 3832 3832 4148 4147 +3836 4148 3834 4149 3839 3840 3841 4150 3840 +3840 4150 4149 3842 4151 3841 3841 4151 4150 +3843 4151 3842 4137 4138 4152 4152 4138 4153 +4137 4154 4139 4137 4152 4154 4139 4155 4140 +4139 4154 4155 4140 4156 4141 4140 4155 4156 +4141 4157 4142 4141 4156 4157 4158 3845 3822 +4142 4158 3822 4157 4158 4142 4158 3846 3845 +3851 4159 3850 4143 3826 3850 4159 4143 3850 +4160 4143 4159 4143 4161 4145 4143 4160 4161 +4146 4145 4162 4162 4145 4161 4163 3855 3856 +3857 4164 3856 3856 4164 4163 3858 4165 3857 +3857 4165 4164 3859 4166 3858 3858 4166 4165 +3860 4167 3859 3859 4167 4166 4168 4167 3860 +4168 3860 3833 4169 4168 3833 4147 4169 3833 +4169 4147 4148 4169 4148 4170 4171 4170 4172 +4172 4170 3861 4170 4148 3861 4148 3836 3861 +3862 4173 3861 3861 4173 4172 3864 4173 3862 +4149 3865 3839 3865 4149 3867 4149 4174 3867 +4149 4150 4174 4175 4150 4151 4174 4150 4175 +3868 4176 3843 4176 4151 3843 4176 4175 4151 +3869 4176 3868 4153 4177 4152 4178 4179 4177 +4179 4152 4177 4179 4154 4152 4180 4155 4181 +4155 4179 4181 4155 4154 4179 4155 4180 4156 +4156 4180 4182 4156 4182 4157 4157 4182 4183 +4184 4185 4183 4185 4157 4183 4185 4158 4157 +4186 3871 3846 4158 4186 3846 4185 4186 4158 +4187 3872 3871 4187 3871 4186 4188 3873 3872 +4188 3872 4187 4159 3851 3873 4188 4159 3873 +4189 4159 4188 4159 4190 4160 4159 4189 4190 +4160 4191 4161 4160 4190 4191 4162 4161 4192 +4192 4161 4191 4193 4194 4195 3875 3874 4195 +3874 4193 4195 3874 4196 4193 4197 3874 3877 +4197 4196 3874 3877 3879 4197 4197 3879 4198 +4199 4200 3880 3880 4200 3881 4201 3880 3882 +4201 4199 3880 4202 3882 3883 4202 4201 3882 +3883 3855 4163 4203 3883 4163 4202 3883 4203 +4203 4163 4164 4203 4164 4204 4204 4164 4165 +4204 4165 4205 4205 4165 4166 4205 4166 4206 +4206 4166 4167 4206 4167 4207 4207 4167 4168 +4208 4171 4172 4208 4172 4173 4208 4173 4209 +4210 4209 4211 4211 4209 3884 4209 4173 3884 +4173 3864 3884 3885 4212 3884 3884 4212 4211 +3867 4174 3885 4174 4212 3885 4174 4213 4212 +4175 4214 4215 4213 4175 4215 4174 4175 4213 +4214 4175 4176 4214 4176 4216 4217 4216 4218 +4218 4216 3886 4216 4176 3886 4176 3869 3886 +3887 4218 3886 4178 4219 4179 4179 4219 4181 +4184 4220 4185 4185 4220 4186 4186 4220 4221 +4186 4221 4187 4187 4221 4222 4187 4222 4188 +4188 4222 4223 4188 4223 4189 4189 4223 4224 +4189 4224 4190 4190 4224 4225 4190 4225 4191 +4191 4225 4226 4191 4226 4192 4227 4228 4229 +4230 4231 4229 4229 4231 4227 4231 4230 4194 +4194 4230 4195 4230 3889 4195 3889 3888 4195 +4195 3888 3875 4232 4198 3879 4232 3879 3891 +3891 3881 4200 3891 4200 4232 4233 4210 4211 +4233 4211 4212 4233 4212 4234 4234 4212 4213 +4234 4213 4235 4235 4213 4215 4236 4217 4218 +4218 3887 4236 3887 4237 4236 3887 3892 4237 +4238 4239 4240 4238 4240 4228 4240 4229 4228 +4240 4241 4229 4241 3894 3893 4230 4241 3893 +4229 4241 4230 4230 3893 3889 3896 4237 3892 +3896 4242 4237 4240 4239 4243 4243 4239 4244 +4243 3898 3897 4241 4243 3897 4240 4243 4241 +4241 3897 3894 3900 4242 3896 3900 4245 4242 +4243 4244 3901 4244 3902 3901 4244 4246 3902 +4243 3901 3898 3904 4245 3900 3904 4247 4245 +4248 4249 3905 3905 4249 3906 3905 3902 4246 +3905 4246 4248 4250 4251 4252 4251 4247 4252 +4247 3910 4252 4247 3904 3910 4249 3912 3906 +4253 3912 4249 4254 4255 4256 3915 3914 4256 +4255 3915 4256 4257 3915 4255 4252 3916 4250 +3916 4257 4250 3916 3915 4257 3916 4252 3910 +3917 3912 4258 3912 4259 4258 3912 4253 4259 +4258 3918 3917 4260 4261 3921 4260 3921 3920 +4256 3922 4254 3922 4261 4254 3922 3921 4261 +3922 4256 3914 4259 4262 4258 3918 4258 4262 +4263 3918 4262 3923 3918 4263 4264 4265 3926 +4264 3926 3925 3920 3926 4260 4260 3926 4265 +4263 3927 3923 4266 3927 4263 4267 4268 3929 +4267 3929 3928 3925 3929 4264 4264 3929 4268 +4269 3930 4270 3930 4266 4270 3930 3927 4266 +4271 4269 4270 4272 4273 4274 3932 3931 4274 +4273 3932 4274 4275 3932 4273 3928 3932 4267 +4267 3932 4275 4269 3933 3930 4276 4277 3934 +4276 3934 3933 4271 4276 3933 4269 4271 3933 +4278 4277 4276 4272 4274 3936 3935 4272 3936 +4279 4272 3935 3936 4274 3931 4277 3939 3934 +4280 4281 3940 4280 3940 3939 4278 4280 3939 +4277 4278 3939 4282 4281 4280 4283 4284 4285 +4286 4287 4285 4286 4285 4284 4286 4279 4287 +4279 3942 4287 4279 3935 3942 4288 3943 4281 +4281 3943 3940 4281 4282 4289 4281 4289 4288 +4290 4283 4285 3946 4291 3944 4291 4285 3944 +4285 4287 3944 4291 4290 4285 3944 4287 3942 +4288 3948 3943 4292 4293 3949 4292 3949 3948 +4289 4292 3948 4288 4289 3948 4294 4293 4292 +4295 4291 3946 4295 3946 3950 4296 3950 3951 +4296 4295 3950 3951 3952 4296 4296 3952 4297 +4298 4299 4300 4301 4298 4293 4298 3953 4293 +3953 3949 4293 4298 4300 3953 4301 4293 4294 +4302 4297 3952 4302 3952 3954 3954 3955 4302 +4302 3955 4303 4304 4305 3956 3956 4305 3957 +4306 3956 3958 4306 4304 3956 4307 3958 3959 +4307 4306 3958 3960 4300 4299 4307 3960 4299 +3959 3960 4307 4300 3960 3953 3961 4308 4309 +4303 3961 4309 3955 3961 4303 4308 4310 4309 +4311 4312 4313 3963 3962 4313 3962 4311 4313 +3962 4314 4311 3962 3965 4314 4314 3965 4315 +4316 4317 3966 3966 4317 3967 3966 3957 4305 +3966 4305 4316 3968 4318 4308 3968 4308 3961 +4310 4308 4319 4319 4308 4318 4320 4321 4322 +4320 4322 4312 4312 4322 4313 4322 3970 4313 +3970 3969 4313 4313 3969 3963 4323 4315 3965 +4323 3965 3971 4324 3971 3972 4324 4323 3971 +4325 3972 3973 4325 4324 3972 3973 3967 4317 +3973 4317 4325 3974 4318 3968 4326 4327 3975 +3975 4327 3974 4327 4319 3974 4319 4318 3974 +4326 4328 4327 4322 4321 3976 4321 3977 3976 +4321 4329 3977 4322 3976 3970 3979 4326 3975 +3979 3980 4326 3980 4328 4326 3980 4330 4328 +4329 3981 3977 4331 3981 4329 3982 4330 3980 +3982 4332 4330 4331 3983 3981 4333 3983 4331 +3984 4332 3982 3984 4334 4332 4333 3985 3983 +4335 3985 4333 3986 4334 3984 3986 4336 4334 +4335 3987 3985 4337 3987 4335 3988 4336 3986 +3988 4338 4336 4337 4339 3987 3987 4339 3989 +3990 3989 4339 3990 4339 4340 3991 4338 3988 +3991 4341 4338 4340 3992 3990 4342 3992 4340 +3993 4341 3991 3993 4343 4341 4344 4345 3994 +3994 4345 3995 3994 3992 4342 3994 4342 4344 +3997 4343 3993 3997 4346 4343 4345 3998 3995 +4347 3998 4345 3999 4348 4000 4000 4348 4349 +4350 4000 4349 4002 4000 4350 3997 4002 4346 +4346 4002 4350 4347 4351 3998 3998 4351 4004 +4005 4004 4351 4005 4351 4352 4006 4353 4348 +4006 4348 3999 4349 4348 4354 4354 4348 4353 +4352 4007 4005 4355 4007 4352 4009 4353 4006 +4009 4010 4353 4010 4354 4353 4010 4356 4354 +4355 4011 4007 4357 4011 4355 4013 4356 4010 +4013 4358 4356 4357 4014 4011 4359 4014 4357 +4015 4358 4013 4015 4360 4358 4359 4016 4014 +4361 4016 4359 4017 4360 4015 4017 4362 4360 +4361 4018 4016 4363 4018 4361 4019 4362 4017 +4019 4364 4362 4363 4020 4018 4365 4020 4363 +4021 4364 4019 4021 4366 4364 4365 4022 4020 +4367 4022 4365 4023 4368 4369 4366 4023 4369 +4021 4023 4366 4368 4370 4369 4367 4024 4022 +4371 4024 4367 4025 4368 4023 4025 4026 4368 +4026 4370 4368 4026 4372 4370 4371 4028 4024 +4373 4028 4371 4029 4372 4026 4029 4374 4372 +4373 4031 4028 4375 4031 4373 4376 4377 4033 +4376 4033 4032 4029 4033 4374 4374 4033 4377 +4375 4035 4031 4378 4035 4375 4036 4376 4032 +4036 4379 4376 4378 4037 4035 4380 4037 4378 +4038 4379 4036 4038 4381 4379 4380 4039 4037 +4382 4039 4380 4040 4381 4038 4040 4383 4381 +4382 4041 4039 4384 4041 4382 4047 4383 4040 +4047 4385 4383 4384 4048 4041 4386 4048 4384 +4050 4387 4051 4387 4052 4051 4055 4388 4056 +4389 4057 4056 4389 4056 4388 4390 4058 4057 +4390 4057 4389 4391 4059 4058 4391 4058 4390 +4391 4060 4059 4063 4392 4393 4385 4063 4393 +4047 4063 4385 4392 4394 4393 4386 4064 4048 +4395 4064 4386 4067 4396 4068 4396 4397 4068 +4068 4397 4050 4397 4398 4050 4398 4387 4050 +4398 4399 4400 4398 4400 4069 4387 4398 4069 +4052 4387 4069 4401 4070 4069 4401 4069 4400 +4401 4402 4070 4070 4402 4055 4402 4403 4055 +4403 4388 4055 4388 4403 4389 4389 4403 4404 +4389 4404 4390 4390 4404 4405 4390 4405 4391 +4391 4405 4406 4391 4406 4060 4406 4071 4060 +4406 4407 4071 4071 4408 4072 4407 4408 4071 +4408 4409 4410 4073 4408 4410 4072 4408 4073 +4410 4074 4073 4077 4411 4392 4077 4392 4063 +4394 4392 4412 4412 4392 4411 4395 4079 4064 +4413 4079 4395 4080 4414 4081 4081 4414 4415 +4416 4081 4415 4082 4081 4416 4082 4417 4083 +4416 4417 4082 4067 4083 4396 4083 4418 4396 +4083 4417 4418 4396 4418 4397 4399 4419 4400 +4400 4419 4401 4401 4419 4420 4401 4420 4402 +4409 4421 4410 4410 4421 4074 4421 4084 4074 +4421 4422 4084 4422 4423 4424 4085 4422 4424 +4084 4422 4085 4424 4086 4085 4087 4425 4411 +4087 4411 4077 4412 4411 4426 4426 4411 4425 +4413 4089 4079 4427 4089 4413 4091 4428 4414 +4091 4414 4080 4415 4414 4429 4429 4414 4428 +4423 4430 4424 4430 4431 4432 4430 4432 4092 +4424 4430 4092 4086 4424 4092 4432 4093 4092 +4433 4434 4425 4087 4433 4425 4096 4433 4087 +4425 4434 4426 4427 4098 4089 4435 4098 4427 +4099 4436 4100 4100 4436 4437 4438 4100 4437 +4101 4100 4438 4091 4101 4428 4101 4439 4428 +4101 4438 4439 4428 4439 4429 4431 4440 4432 +4432 4440 4093 4440 4102 4093 4440 4441 4102 +4102 4442 4103 4441 4442 4102 4103 4443 4104 +4442 4443 4103 4104 4444 4105 4443 4444 4104 +4096 4105 4433 4433 4105 4444 4435 4445 4098 +4098 4445 4106 4445 4446 4447 4107 4445 4447 +4106 4445 4107 4448 4108 4107 4448 4107 4447 +4449 4109 4108 4449 4108 4448 4449 4450 4109 +4109 4450 4099 4450 4451 4099 4451 4436 4099 +4436 4451 4437 4446 4452 4447 4447 4452 4448 +4448 4452 4453 4448 4453 4449 4449 4453 4454 +4449 4454 4450 4455 4456 4110 4110 4456 4111 +4110 4114 4455 4455 4114 4457 4458 4459 4115 +4115 4459 4116 4460 4115 4118 4460 4458 4115 +4118 4111 4456 4118 4456 4460 4461 4457 4114 +4461 4114 4119 4462 4119 4120 4462 4461 4119 +4463 4120 4122 4463 4462 4120 4464 4465 4123 +4465 4122 4123 4465 4463 4122 4466 4467 4464 +4464 4467 4465 4466 4468 4467 4469 4470 4124 +4124 4470 4125 4124 4116 4459 4124 4459 4469 +4127 4464 4123 4464 4127 4129 4464 4129 4466 +4471 4472 4130 4130 4472 4129 4472 4468 4129 +4468 4466 4129 4471 4473 4472 4474 4475 4131 +4131 4475 4132 4131 4125 4470 4131 4470 4474 +4135 4471 4130 4135 4136 4471 4136 4473 4471 +4136 4476 4473 4477 4478 4479 4475 4477 4479 +4138 4475 4479 4132 4475 4138 4480 4476 4136 +4480 4136 4144 4144 4146 4480 4480 4146 4481 +4479 4478 4482 4482 4478 4483 4482 4153 4138 +4482 4138 4479 4162 4484 4485 4481 4162 4485 +4146 4162 4481 4484 4486 4485 4487 4168 4169 +4170 4488 4169 4169 4488 4487 4171 4488 4170 +4482 4483 4489 4489 4483 4490 4491 4177 4153 +4482 4491 4153 4489 4491 4482 4491 4178 4177 +4181 4492 4180 4493 4182 4180 4493 4180 4492 +4494 4183 4182 4494 4182 4493 4494 4184 4183 +4192 4495 4484 4192 4484 4162 4486 4484 4496 +4484 4497 4496 4484 4495 4497 4497 4498 4496 +4499 4500 4501 4502 4503 4501 4501 4503 4499 +4194 4193 4502 4193 4503 4502 4193 4504 4503 +4505 4193 4196 4505 4504 4193 4506 4196 4197 +4506 4505 4196 4197 4198 4506 4506 4198 4507 +4508 4200 4199 4201 4509 4199 4199 4509 4508 +4202 4510 4201 4201 4510 4509 4203 4511 4202 +4202 4511 4510 4204 4512 4203 4203 4512 4511 +4205 4513 4204 4204 4513 4512 4206 4514 4205 +4205 4514 4513 4207 4515 4206 4206 4515 4514 +4516 4515 4207 4516 4207 4168 4517 4516 4168 +4487 4517 4168 4518 4519 4517 4487 4518 4517 +4488 4518 4487 4208 4520 4171 4520 4488 4171 +4520 4518 4488 4209 4521 4208 4208 4521 4520 +4210 4521 4209 4522 4215 4214 4216 4523 4214 +4214 4523 4522 4217 4523 4216 4490 4524 4489 +4525 4526 4524 4526 4489 4524 4526 4491 4489 +4527 4219 4178 4491 4527 4178 4526 4527 4491 +4492 4181 4219 4527 4492 4219 4528 4492 4527 +4492 4529 4493 4492 4528 4529 4493 4530 4494 +4493 4529 4530 4531 4220 4184 4494 4531 4184 +4530 4531 4494 4532 4221 4220 4532 4220 4531 +4533 4222 4221 4533 4221 4532 4534 4223 4222 +4534 4222 4533 4535 4224 4223 4535 4223 4534 +4536 4225 4224 4536 4224 4535 4537 4226 4225 +4537 4225 4536 4495 4192 4226 4537 4495 4226 +4538 4495 4537 4495 4539 4497 4495 4538 4539 +4498 4497 4540 4540 4497 4539 4541 4542 4543 +4544 4545 4543 4543 4545 4541 4545 4544 4500 +4500 4544 4501 4544 4228 4501 4228 4227 4501 +4501 4227 4231 4501 4231 4502 4502 4231 4194 +4546 4507 4198 4546 4198 4232 4232 4200 4508 +4547 4232 4508 4546 4232 4547 4547 4508 4509 +4547 4509 4548 4548 4509 4510 4548 4510 4549 +4549 4510 4511 4549 4511 4550 4550 4511 4512 +4550 4512 4551 4551 4512 4513 4551 4513 4552 +4552 4513 4514 4552 4514 4553 4553 4514 4515 +4553 4515 4554 4554 4515 4516 4555 4519 4518 +4555 4518 4520 4555 4520 4556 4556 4520 4521 +4556 4521 4557 4557 4521 4210 4233 4557 4210 +4558 4557 4233 4559 4233 4234 4559 4558 4233 +4560 4559 4561 4559 4235 4561 4559 4234 4235 +4562 4561 4235 4562 4235 4215 4563 4562 4215 +4522 4563 4215 4563 4522 4523 4563 4523 4564 +4565 4564 4566 4566 4564 4236 4564 4523 4236 +4523 4217 4236 4237 4566 4236 4525 4567 4526 +4526 4567 4527 4527 4567 4568 4527 4568 4528 +4528 4568 4569 4528 4569 4529 4529 4569 4570 +4529 4570 4530 4530 4570 4571 4572 4573 4571 +4573 4530 4571 4573 4531 4530 4531 4574 4532 +4531 4573 4574 4532 4575 4533 4532 4574 4575 +4533 4576 4534 4533 4575 4576 4534 4577 4535 +4534 4576 4577 4535 4578 4536 4535 4577 4578 +4536 4579 4537 4536 4578 4579 4537 4580 4538 +4537 4579 4580 4538 4581 4539 4538 4580 4581 +4540 4539 4582 4582 4539 4581 4583 4584 4585 +4583 4585 4542 4585 4543 4542 4585 4586 4543 +4586 4239 4238 4544 4586 4238 4543 4586 4544 +4544 4238 4228 4587 4560 4561 4587 4561 4562 +4588 4565 4566 4566 4237 4588 4237 4589 4588 +4237 4242 4589 4572 4590 4573 4573 4590 4574 +4574 4590 4591 4574 4591 4575 4575 4591 4592 +4575 4592 4576 4576 4592 4593 4576 4593 4577 +4577 4593 4594 4577 4594 4578 4578 4594 4595 +4578 4595 4579 4579 4595 4596 4579 4596 4580 +4580 4596 4597 4580 4597 4581 4581 4597 4598 +4581 4598 4582 4584 4599 4585 4600 4601 4599 +4601 4585 4599 4601 4586 4585 4601 4244 4239 +4601 4239 4586 4245 4589 4242 4245 4602 4589 +4601 4600 4603 4603 4600 4604 4603 4246 4244 +4603 4244 4601 4605 4606 4607 4606 4602 4607 +4602 4247 4607 4602 4245 4247 4603 4604 4248 +4604 4249 4248 4604 4608 4249 4603 4248 4246 +4605 4607 4251 4250 4605 4251 4609 4605 4250 +4251 4607 4247 4608 4253 4249 4610 4253 4608 +4611 4612 4255 4611 4255 4254 4255 4613 4257 +4612 4613 4255 4250 4257 4609 4609 4257 4613 +4610 4259 4253 4614 4259 4610 4615 4616 4261 +4615 4261 4260 4254 4261 4611 4611 4261 4616 +4614 4617 4259 4259 4617 4262 4263 4262 4617 +4263 4617 4618 4619 4620 4265 4619 4265 4264 +4260 4265 4615 4615 4265 4620 4618 4266 4263 +4621 4266 4618 4622 4623 4624 4268 4267 4624 +4623 4268 4624 4625 4268 4623 4264 4268 4619 +4619 4268 4625 4621 4626 4266 4266 4626 4270 +4271 4270 4626 4271 4626 4627 4628 4629 4273 +4628 4273 4272 4624 4275 4622 4275 4629 4622 +4275 4273 4629 4275 4624 4267 4627 4630 4271 +4271 4630 4276 4278 4276 4630 4278 4630 4631 +4632 4633 4634 4633 4628 4634 4628 4279 4634 +4628 4272 4279 4631 4635 4278 4278 4635 4280 +4282 4280 4635 4282 4635 4636 4637 4638 4639 +4284 4283 4639 4638 4284 4639 4640 4284 4638 +4634 4286 4632 4286 4640 4632 4286 4284 4640 +4286 4634 4279 4641 4289 4642 4289 4636 4642 +4289 4282 4636 4643 4641 4642 4644 4637 4639 +4644 4639 4283 4290 4644 4283 4645 4644 4290 +4290 4291 4645 4645 4291 4646 4641 4292 4289 +4647 4648 4294 4647 4294 4292 4643 4647 4292 +4641 4643 4292 4649 4648 4647 4650 4651 4652 +4653 4652 4651 4654 4646 4291 4654 4291 4295 +4655 4654 4656 4654 4296 4656 4654 4295 4296 +4297 4656 4296 4657 4658 4298 4298 4658 4299 +4659 4298 4301 4659 4657 4298 4301 4294 4648 +4660 4301 4648 4659 4301 4660 4661 4662 4663 +4649 4660 4648 4664 4662 4661 4665 4650 4652 +4665 4652 4653 4666 4655 4656 4667 4666 4668 +4668 4666 4302 4666 4656 4302 4656 4297 4302 +4303 4668 4302 4669 4305 4304 4669 4304 4670 +4304 4671 4670 4304 4306 4671 4672 4306 4307 +4672 4671 4306 4307 4299 4658 4307 4658 4672 +4673 4663 4662 4673 4662 4664 4674 4667 4668 +4675 4674 4676 4676 4674 4309 4674 4668 4309 +4668 4303 4309 4310 4676 4309 4677 4678 4311 +4311 4678 4312 4679 4677 4680 4677 4314 4680 +4677 4311 4314 4315 4680 4314 4681 4682 4316 +4316 4682 4317 4316 4305 4669 4683 4316 4669 +4681 4316 4683 4683 4669 4670 4684 4675 4676 +4676 4310 4684 4310 4685 4684 4310 4319 4685 +4686 4687 4320 4320 4687 4321 4320 4312 4678 +4320 4678 4686 4688 4679 4680 4688 4680 4315 +4323 4688 4315 4689 4688 4323 4690 4323 4324 +4690 4689 4323 4691 4324 4325 4691 4690 4324 +4325 4317 4682 4325 4682 4691 4692 4685 4319 +4692 4319 4327 4327 4328 4692 4692 4328 4693 +4687 4329 4321 4694 4329 4687 4330 4693 4328 +4330 4695 4693 4694 4331 4329 4696 4331 4694 +4332 4695 4330 4332 4697 4695 4696 4333 4331 +4698 4333 4696 4334 4697 4332 4334 4699 4697 +4698 4335 4333 4700 4335 4698 4336 4699 4334 +4336 4701 4699 4700 4337 4335 4702 4337 4700 +4338 4701 4336 4338 4703 4701 4702 4704 4337 +4337 4704 4339 4340 4339 4704 4340 4704 4705 +4341 4703 4338 4341 4706 4703 4707 4708 4709 +4705 4707 4709 4342 4705 4709 4340 4705 4342 +4343 4706 4341 4343 4710 4706 4709 4708 4344 +4708 4345 4344 4708 4711 4345 4709 4344 4342 +4346 4710 4343 4346 4712 4710 4711 4347 4345 +4713 4347 4711 4714 4715 4350 4714 4350 4349 +4346 4350 4712 4712 4350 4715 4713 4716 4347 +4347 4716 4351 4352 4351 4716 4352 4716 4717 +4354 4714 4349 4354 4718 4714 4717 4355 4352 +4719 4355 4717 4356 4718 4354 4356 4720 4718 +4719 4357 4355 4721 4357 4719 4358 4720 4356 +4358 4722 4720 4721 4359 4357 4723 4359 4721 +4360 4722 4358 4360 4724 4722 4723 4361 4359 +4725 4361 4723 4362 4724 4360 4362 4726 4724 +4725 4363 4361 4727 4363 4725 4364 4726 4362 +4364 4728 4726 4727 4365 4363 4729 4365 4727 +4366 4728 4364 4366 4730 4728 4729 4367 4365 +4731 4367 4729 4732 4730 4366 4732 4366 4369 +4369 4370 4732 4732 4370 4733 4731 4371 4367 +4734 4371 4731 4372 4733 4370 4372 4735 4733 +4734 4373 4371 4736 4373 4734 4737 4738 4739 +4738 4735 4739 4735 4374 4739 4735 4372 4374 +4736 4375 4373 4740 4375 4736 4737 4739 4377 +4376 4737 4377 4741 4737 4376 4377 4739 4374 +4740 4378 4375 4742 4378 4740 4379 4741 4376 +4379 4743 4741 4742 4380 4378 4744 4380 4742 +4381 4743 4379 4381 4745 4743 4744 4382 4380 +4746 4382 4744 4383 4745 4381 4383 4747 4745 +4746 4384 4382 4748 4384 4746 4385 4749 4750 +4747 4385 4750 4383 4385 4747 4749 4751 4750 +4748 4386 4384 4752 4386 4748 4393 4749 4385 +4393 4394 4749 4394 4751 4749 4394 4753 4751 +4752 4395 4386 4754 4395 4752 4397 4755 4398 +4755 4399 4398 4402 4756 4403 4757 4404 4403 +4757 4403 4756 4758 4405 4404 4758 4404 4757 +4759 4406 4405 4759 4405 4758 4760 4407 4406 +4760 4406 4759 4761 4408 4407 4761 4407 4760 +4761 4409 4408 4412 4753 4394 4412 4762 4753 +4754 4413 4395 4763 4413 4754 4415 4764 4416 +4765 4417 4416 4765 4416 4764 4417 4765 4766 +4767 4417 4766 4418 4417 4767 4418 4767 4397 +4767 4768 4397 4768 4755 4397 4768 4767 4769 +4770 4419 4399 4755 4770 4399 4768 4770 4755 +4419 4770 4771 4772 4419 4771 4420 4419 4772 +4402 4420 4756 4420 4773 4756 4420 4772 4773 +4756 4773 4757 4757 4773 4774 4757 4774 4758 +4758 4774 4775 4758 4775 4759 4759 4775 4776 +4759 4776 4760 4760 4776 4777 4760 4777 4761 +4761 4777 4778 4761 4778 4409 4778 4421 4409 +4778 4779 4421 4421 4780 4422 4779 4780 4421 +4423 4422 4780 4423 4780 4781 4426 4762 4412 +4426 4782 4762 4783 4784 4785 4763 4783 4785 +4427 4763 4785 4413 4763 4427 4786 4787 4764 +4415 4786 4764 4429 4786 4415 4764 4787 4765 +4765 4787 4788 4765 4788 4766 4769 4789 4768 +4768 4789 4770 4770 4789 4790 4770 4790 4771 +4430 4423 4791 4423 4792 4791 4423 4781 4792 +4791 4431 4430 4433 4793 4434 4793 4794 4434 +4794 4426 4434 4794 4782 4426 4784 4795 4785 +4427 4785 4795 4796 4427 4795 4435 4427 4796 +4437 4797 4438 4438 4797 4798 4799 4438 4798 +4439 4438 4799 4429 4439 4786 4786 4439 4799 +4792 4800 4791 4791 4800 4431 4800 4440 4431 +4800 4801 4440 4440 4802 4441 4801 4802 4440 +4802 4803 4804 4442 4802 4804 4441 4802 4442 +4805 4443 4442 4805 4442 4804 4806 4444 4443 +4806 4443 4805 4806 4807 4444 4444 4807 4433 +4807 4808 4433 4808 4793 4433 4793 4808 4794 +4445 4435 4809 4435 4810 4809 4435 4796 4810 +4809 4446 4445 4450 4811 4451 4811 4812 4451 +4451 4812 4437 4812 4813 4437 4813 4797 4437 +4797 4813 4798 4803 4814 4804 4804 4814 4805 +4805 4814 4815 4805 4815 4806 4806 4815 4816 +4806 4816 4807 4810 4817 4809 4809 4817 4446 +4817 4452 4446 4817 4818 4452 4818 4819 4820 +4453 4818 4820 4452 4818 4453 4453 4820 4821 +4822 4453 4821 4454 4453 4822 4450 4454 4811 +4454 4823 4811 4454 4822 4823 4811 4823 4812 +4819 4824 4820 4820 4824 4821 4825 4826 4827 +4456 4455 4827 4455 4825 4827 4455 4828 4825 +4455 4457 4828 4828 4457 4829 4830 4831 4458 +4458 4831 4459 4460 4827 4826 4830 4460 4826 +4458 4460 4830 4827 4460 4456 4832 4829 4457 +4832 4457 4461 4833 4461 4462 4833 4832 4461 +4834 4462 4463 4834 4833 4462 4835 4463 4465 +4835 4834 4463 4836 4465 4467 4836 4835 4465 +4467 4468 4836 4836 4468 4837 4838 4839 4469 +4469 4839 4470 4469 4459 4831 4469 4831 4838 +4840 4837 4468 4840 4468 4472 4841 4842 4473 +4842 4472 4473 4842 4840 4472 4841 4843 4842 +4844 4845 4474 4474 4845 4475 4474 4470 4839 +4474 4839 4844 4476 4846 4841 4476 4841 4473 +4843 4841 4847 4841 4848 4847 4841 4846 4848 +4848 4849 4847 4850 4851 4477 4477 4851 4478 +4477 4475 4845 4477 4845 4850 4480 4846 4476 +4846 4480 4481 4852 4846 4481 4848 4846 4852 +4849 4848 4853 4848 4854 4853 4848 4852 4854 +4855 4856 4854 4854 4856 4853 4855 4857 4856 +4851 4483 4478 4858 4483 4851 4485 4852 4481 +4852 4485 4486 4859 4852 4486 4854 4852 4859 +4854 4860 4855 4854 4859 4860 4857 4855 4861 +4861 4855 4860 4858 4490 4483 4862 4490 4858 +4496 4859 4486 4859 4496 4498 4863 4859 4498 +4860 4859 4863 4861 4860 4864 4860 4865 4864 +4860 4863 4865 4866 4867 4865 4865 4867 4864 +4868 4869 4866 4866 4869 4867 4870 4871 4868 +4868 4871 4869 4872 4873 4870 4870 4873 4871 +4874 4875 4872 4872 4875 4873 4500 4499 4874 +4499 4875 4874 4499 4876 4875 4877 4499 4503 +4877 4876 4499 4878 4503 4504 4878 4877 4503 +4879 4504 4505 4879 4878 4504 4880 4879 4881 +4879 4506 4881 4879 4505 4506 4507 4881 4506 +4882 4516 4517 4519 4882 4517 4524 4490 4883 +4490 4884 4883 4490 4862 4884 4883 4525 4524 +4540 4885 4863 4540 4863 4498 4863 4886 4865 +4863 4885 4886 4865 4887 4866 4865 4886 4887 +4866 4888 4868 4866 4887 4888 4868 4889 4870 +4868 4888 4889 4889 4542 4541 4872 4889 4541 +4870 4889 4872 4872 4541 4545 4872 4545 4874 +4874 4545 4500 4890 4880 4881 4890 4881 4507 +4546 4890 4507 4891 4890 4546 4892 4546 4547 +4892 4891 4546 4893 4547 4548 4893 4892 4547 +4894 4548 4549 4894 4893 4548 4895 4549 4550 +4895 4894 4549 4896 4550 4551 4896 4895 4550 +4897 4551 4552 4897 4896 4551 4898 4552 4553 +4898 4897 4552 4899 4553 4554 4899 4898 4553 +4554 4516 4882 4900 4554 4882 4899 4554 4900 +4901 4900 4902 4902 4900 4555 4900 4882 4555 +4882 4519 4555 4556 4903 4555 4555 4903 4902 +4557 4904 4556 4556 4904 4903 4558 4905 4557 +4557 4905 4904 4559 4906 4558 4558 4906 4905 +4560 4906 4559 4907 4562 4563 4564 4908 4563 +4563 4908 4907 4565 4908 4564 4884 4909 4883 +4883 4909 4525 4909 4567 4525 4909 4910 4567 +4910 4911 4912 4568 4910 4912 4567 4910 4568 +4913 4569 4568 4913 4568 4912 4914 4570 4569 +4914 4569 4913 4915 4571 4570 4915 4570 4914 +4915 4572 4571 4582 4916 4885 4582 4885 4540 +4885 4917 4886 4885 4916 4917 4886 4918 4887 +4886 4917 4918 4887 4919 4888 4887 4918 4919 +4919 4584 4583 4889 4919 4583 4888 4919 4889 +4889 4583 4542 4920 4901 4902 4920 4902 4903 +4920 4903 4921 4921 4903 4904 4921 4904 4922 +4922 4904 4905 4922 4905 4923 4923 4905 4906 +4923 4906 4924 4924 4906 4560 4587 4924 4560 +4925 4924 4587 4925 4587 4562 4926 4925 4562 +4907 4926 4562 4925 4926 4927 4908 4928 4929 +4926 4908 4929 4907 4908 4926 4930 4928 4931 +4931 4928 4588 4928 4908 4588 4908 4565 4588 +4589 4931 4588 4911 4932 4912 4912 4932 4913 +4913 4932 4933 4913 4933 4914 4914 4933 4934 +4935 4936 4934 4936 4914 4934 4936 4915 4914 +4937 4590 4572 4915 4937 4572 4936 4937 4915 +4938 4591 4590 4938 4590 4937 4939 4592 4591 +4939 4591 4938 4940 4593 4592 4940 4592 4939 +4941 4594 4593 4941 4593 4940 4942 4595 4594 +4942 4594 4941 4943 4596 4595 4943 4595 4942 +4944 4597 4596 4944 4596 4943 4945 4598 4597 +4945 4597 4944 4916 4582 4598 4945 4916 4598 +4946 4916 4945 4916 4947 4917 4916 4946 4947 +4917 4948 4918 4917 4947 4948 4918 4949 4919 +4918 4948 4949 4950 4599 4584 4919 4950 4584 +4949 4950 4919 4950 4600 4599 4951 4927 4926 +4951 4926 4929 4952 4930 4931 4931 4589 4952 +4589 4953 4952 4589 4602 4953 4935 4954 4936 +4936 4954 4937 4937 4954 4955 4937 4955 4938 +4938 4955 4956 4938 4956 4939 4939 4956 4957 +4939 4957 4940 4940 4957 4958 4940 4958 4941 +4941 4958 4959 4941 4959 4942 4942 4959 4960 +4942 4960 4943 4943 4960 4961 4943 4961 4944 +4944 4961 4962 4944 4962 4945 4945 4962 4963 +4945 4963 4946 4946 4963 4964 4965 4966 4964 +4966 4946 4964 4966 4947 4946 4947 4967 4948 +4947 4966 4967 4948 4968 4949 4948 4967 4968 +4949 4969 4950 4949 4968 4969 4969 4604 4600 +4969 4600 4950 4970 4971 4972 4606 4605 4972 +4971 4606 4972 4973 4606 4971 4602 4606 4953 +4953 4606 4973 4965 4974 4966 4966 4974 4967 +4967 4974 4975 4967 4975 4968 4968 4975 4976 +4977 4978 4976 4978 4968 4976 4978 4969 4968 +4978 4608 4604 4978 4604 4969 4970 4972 4979 +4970 4979 4980 4609 4979 4605 4605 4979 4972 +4977 4981 4978 4608 4978 4981 4982 4608 4981 +4610 4608 4982 4983 4984 4985 4612 4611 4985 +4984 4612 4985 4986 4612 4984 4979 4613 4980 +4613 4986 4980 4613 4612 4986 4613 4979 4609 +4987 4988 4989 4982 4987 4989 4614 4982 4989 +4610 4982 4614 4990 4991 4992 4992 4991 4983 +4992 4983 4985 4615 4992 4985 4616 4615 4985 +4616 4985 4611 4988 4993 4989 4993 4994 4995 +4993 4995 4617 4989 4993 4617 4614 4989 4617 +4995 4618 4617 4996 4997 4998 4998 4997 4990 +4998 4990 4992 4619 4998 4992 4620 4619 4992 +4620 4992 4615 4994 4999 4995 4618 4995 4999 +5000 4618 4999 4621 4618 5000 5001 5002 5003 +5004 5003 5002 5005 5006 5007 4623 4622 5007 +5006 4623 5007 5008 4623 5006 4998 4625 4996 +4625 5008 4996 4625 4623 5008 4625 4998 4619 +5000 5009 4621 4621 5009 4626 4627 4626 5009 +4627 5009 5010 5011 5012 5013 5013 5012 5001 +5003 5013 5001 5014 5013 5003 5003 5004 5015 +5016 5003 5015 5014 5003 5016 5016 5015 5005 +5016 5005 5007 4628 5016 5007 4629 4628 5007 +4629 5007 4622 5010 5017 4627 4627 5017 4630 +5018 5019 4631 4630 5018 4631 5017 5018 4630 +5020 5019 5018 5011 5013 5021 5011 5021 5022 +5023 5013 5014 5021 5013 5023 4632 5023 4633 +5023 5016 4633 5023 5014 5016 4633 5016 4628 +5019 4635 4631 5024 5025 4636 5024 4636 4635 +5020 5024 4635 5019 5020 4635 5026 5027 5025 +5026 5025 5024 5028 5027 5026 5029 5030 5031 +5032 5033 5031 5032 5031 5030 5033 5032 5022 +5033 5022 5021 4637 5033 5021 4638 4637 5021 +4638 5021 4640 4640 5021 5023 4640 5023 4632 +5025 4642 4636 4642 5025 4643 5025 5034 4643 +5025 5027 5034 5027 5028 5035 5036 5027 5035 +5034 5027 5036 5037 5038 5036 5037 5036 5035 +5039 5040 5038 5039 5038 5037 5041 5042 5040 +5041 5040 5039 5043 5044 5042 5043 5042 5041 +5045 5046 5044 5045 5044 5043 5047 5048 5046 +5047 5046 5045 5048 5047 5029 5031 5048 5029 +5049 5048 5031 5033 5050 5051 5049 5033 5051 +5031 5033 5049 5050 5033 4637 4644 5050 4637 +5052 5050 4644 5053 5052 5054 5052 4645 5054 +5052 4644 4645 4646 5054 4645 5034 4647 4643 +4647 5034 4649 5034 5055 4649 5034 5036 5055 +5056 5036 5038 5055 5036 5056 5057 4651 4650 +5058 5059 5040 5056 5040 5059 5040 5056 5038 +5058 5060 5061 5060 5057 5061 4653 5061 5057 +5061 4653 5042 4651 5042 4653 5042 4651 5060 +5060 5040 5042 5058 5040 5060 5060 4651 5057 +5061 5042 5044 5061 5044 5062 5062 5044 5046 +5062 5046 5063 5063 5046 5048 5063 5048 5064 +5064 5048 5049 5064 5049 5065 5065 5049 5051 +5053 5054 5066 5053 5066 5067 4654 5068 4646 +5068 5054 4646 5068 5066 5054 4655 5068 4654 +5069 4658 4657 4659 5070 4657 4657 5070 5069 +4660 5071 4659 4659 5071 5070 5055 5072 5073 +5074 5073 5072 5073 5074 5071 5073 5071 4663 +4660 4663 5071 4663 4660 4661 4649 4661 4660 +4661 4649 5055 4661 5055 5073 5072 5075 5076 +5075 5073 5076 4664 5076 5073 5076 4664 5056 +4661 5056 4664 5056 4661 5075 5075 5055 5056 +5072 5055 5075 5075 4661 5073 4650 4665 5057 +5056 5059 5076 5057 4665 4653 5077 5067 5066 +5077 5066 5068 5077 5068 5078 5078 5068 4655 +4666 5078 4655 5079 5078 4666 4666 4667 5079 +5079 4667 5080 5081 4670 4671 4672 5082 4671 +4671 5082 5081 5083 5082 4672 5083 4672 4658 +5084 5083 4658 5069 5084 4658 5084 5069 5070 +5084 5070 5085 5085 5070 5071 5085 5071 5086 +4663 4673 5073 5087 5088 5089 5074 5086 5071 +4673 4664 5073 5089 5088 5090 5091 5080 4667 +5091 4667 4674 4674 4675 5091 5091 4675 5092 +5093 5094 4677 4677 5094 4678 4677 4679 5093 +5093 4679 5095 5096 4682 4681 5096 4681 5097 +4681 5098 5097 4681 4683 5098 4683 4670 5081 +5099 4683 5081 5098 4683 5099 5099 5081 5082 +5099 5082 5100 5100 5082 5083 5101 5087 5089 +5101 5089 5090 5102 5092 4675 5102 4675 4684 +4684 4685 5102 5102 4685 5103 5104 5105 4686 +4686 5105 4687 4686 4678 5094 4686 5094 5104 +5106 5095 4679 5106 4679 4688 5107 4688 4689 +5107 5106 4688 5108 4689 4690 5108 5107 4689 +5109 4690 4691 5109 5108 4690 4691 4682 5096 +5110 4691 5096 5109 4691 5110 5110 5096 5097 +5111 5103 4685 5111 4685 4692 4692 4693 5111 +5111 4693 5112 5105 4694 4687 5113 4694 5105 +4695 5112 4693 4695 5114 5112 5113 4696 4694 +5115 4696 5113 4697 5114 4695 4697 5116 5114 +5115 4698 4696 5117 4698 5115 4699 5116 4697 +4699 5118 5116 5117 4700 4698 5119 4700 5117 +4701 5118 4699 4701 5120 5118 5119 4702 4700 +5121 4702 5119 4703 5120 4701 4703 5122 5120 +5121 5123 4702 4702 5123 4704 4705 4704 5123 +4705 5123 5124 4706 5122 4703 4706 5125 5122 +5126 5127 4707 4707 5127 4708 4707 4705 5124 +4707 5124 5126 4710 5125 4706 4710 5128 5125 +5127 4711 4708 5129 4711 5127 5130 5131 5132 +5131 5128 5132 5128 4712 5132 5128 4710 4712 +5133 4713 5134 4713 5129 5134 4713 4711 5129 +5135 5133 5134 5130 5132 4715 4714 5130 4715 +5136 5130 4714 4715 5132 4712 5133 4716 4713 +4717 4716 5133 5135 4717 5133 5137 4717 5135 +4718 5136 4714 4718 5138 5136 5137 4719 4717 +5139 4719 5137 4720 5138 4718 4720 5140 5138 +5139 4721 4719 5141 4721 5139 4722 5140 4720 +4722 5142 5140 5141 4723 4721 5143 4723 5141 +4724 5142 4722 4724 5144 5142 5143 4725 4723 +5145 4725 5143 4726 5144 4724 4726 5146 5144 +5145 4727 4725 5147 4727 5145 4728 5146 4726 +4728 5148 5146 5149 4729 5150 4729 5147 5150 +4729 4727 5147 5151 5149 5150 4730 5148 4728 +4730 5152 5148 5153 4731 5149 5149 4731 4729 +5149 5151 5154 5149 5154 5153 5155 5152 4730 +5155 4730 4732 4732 4733 5155 5155 4733 5156 +5157 5158 5153 5158 4731 5153 5158 4734 4731 +5157 5153 5154 4735 5156 4733 4735 5159 5156 +5158 4736 4734 5160 4736 5158 5161 5162 4738 +5161 4738 4737 4735 4738 5159 5159 4738 5162 +5160 4740 4736 5163 4740 5160 4741 5161 4737 +4741 5164 5161 5163 4742 4740 5165 4742 5163 +4743 5164 4741 4743 5166 5164 5165 4744 4742 +5167 4744 5165 4745 5166 4743 4745 5168 5166 +5167 4746 4744 5169 4746 5167 4747 5170 5171 +5168 4747 5171 4745 4747 5168 5170 5172 5171 +5169 4748 4746 5173 4748 5169 4750 5170 4747 +4750 4751 5170 4751 5172 5170 4751 5174 5172 +5173 4752 4748 5175 4752 5173 4753 5174 4751 +4753 5176 5174 5175 4754 4752 5177 4754 5175 +4762 5176 4753 4762 5178 5176 5177 4763 4754 +5179 4763 5177 4766 5180 4767 5180 4769 4767 +4771 5181 4772 5182 4773 4772 5182 4772 5181 +5183 4774 4773 5183 4773 5182 5184 4775 4774 +5184 4774 5183 5185 4776 4775 5185 4775 5184 +5186 4777 4776 5186 4776 5185 5187 4778 4777 +5187 4777 5186 5188 4779 4778 5188 4778 5187 +5189 4780 4779 5189 4779 5188 5189 4781 4780 +4782 5178 4762 4782 5190 5178 5191 5192 4783 +4783 5192 4784 4783 4763 5179 4783 5179 5191 +5193 5194 4787 5193 4787 4786 4787 5195 4788 +5194 5195 4787 4766 4788 5180 4788 5196 5180 +4788 5195 5196 5180 5196 4769 5196 4789 4769 +5196 5197 4789 4789 5198 4790 5197 5198 4789 +4771 4790 5181 4790 5199 5181 4790 5198 5199 +5181 5199 5182 5182 5199 5200 5182 5200 5183 +5183 5200 5201 5183 5201 5184 5184 5201 5202 +5184 5202 5185 5185 5202 5203 5185 5203 5186 +5186 5203 5204 5186 5204 5187 5187 5204 5205 +5187 5205 5188 5188 5205 5206 5188 5206 5189 +5189 5206 5207 4781 5189 5207 5208 4781 5207 +4792 4781 5208 4794 5190 4782 4794 5209 5190 +4795 4784 5210 4784 5211 5210 4784 5192 5211 +5210 4796 4795 4798 5212 4799 5212 5213 4799 +5213 4786 4799 5213 5193 4786 5208 5214 4792 +4792 5214 4800 5214 5215 5216 4801 5214 5216 +4800 5214 4801 5217 4802 4801 5217 4801 5216 +5217 4803 4802 5218 5219 5220 4808 4807 5220 +5219 4808 5220 5221 4808 5219 4794 4808 5209 +5209 4808 5221 5211 5222 5210 4796 5210 5222 +5223 4796 5222 4810 4796 5223 5224 5225 4813 +5224 4813 4812 4798 4813 5212 4813 5226 5212 +4813 5225 5226 5212 5226 5213 5215 5227 5216 +5216 5227 5217 5217 5227 5228 5217 5228 4803 +5228 4814 4803 5228 5229 4814 4814 5230 4815 +5229 5230 4814 5220 4816 5218 4816 5230 5218 +4816 4815 5230 4816 5220 4807 5223 5231 4810 +4810 5231 4817 5231 5232 5233 4818 5231 5233 +4817 5231 4818 5233 4819 4818 4821 5234 4822 +4822 5234 5235 5236 4822 5235 4823 4822 5236 +4812 4823 5224 5224 4823 5236 5232 5237 5233 +5233 5237 4819 5237 4824 4819 5237 5238 4824 +4821 4824 5234 4824 5239 5234 4824 5238 5239 +5234 5239 5235 5240 5241 4825 4825 5241 4826 +5242 5240 5243 5240 4828 5243 5240 4825 4828 +4829 5243 4828 5244 5245 4830 4830 5245 4831 +4830 4826 5241 4830 5241 5244 5246 5242 5243 +5246 5243 4829 4832 5246 4829 5247 5246 4832 +5248 4832 4833 5248 5247 4832 5249 4833 4834 +5249 5248 4833 5250 4834 4835 5250 5249 4834 +5251 4835 4836 5251 5250 4835 5252 5253 4837 +5253 4836 4837 5253 5251 4836 5254 5255 5252 +5252 5255 5253 5254 5256 5255 5257 5258 4838 +4838 5258 4839 4838 4831 5245 4838 5245 5257 +4840 5252 4837 5252 4840 4842 5252 4842 5254 +5259 5260 4843 4843 5260 4842 5260 5256 4842 +5256 5254 4842 5261 5262 5259 5259 5262 5260 +5261 5263 5262 5264 4845 4844 5265 5264 4844 +4839 5265 4844 5258 5265 4839 4847 5259 4843 +5259 4847 4849 5266 5259 4849 5261 5259 5266 +5263 5261 5267 5261 5268 5267 5261 5266 5268 +5268 5269 5267 5270 4851 4850 5271 5270 4850 +5271 4850 4845 5272 5271 4845 5264 5272 4845 +5272 5264 5265 4853 5266 4849 5266 4853 4856 +5266 4856 5268 5273 5274 4857 4857 5274 4856 +5274 5269 4856 5269 5268 4856 5273 5275 5274 +5276 5277 5270 5277 4851 5270 5277 4858 4851 +5276 5270 5271 4861 5278 5273 4861 5273 4857 +5275 5273 5279 5273 5280 5279 5273 5278 5280 +5281 5282 5280 5280 5282 5279 5283 5284 5281 +5281 5284 5282 5285 5286 5283 5283 5286 5284 +5285 5287 5286 5277 4862 4858 5288 4862 5277 +4864 5278 4861 5278 4864 4867 5278 4867 5280 +5280 4867 4869 5280 4869 5281 5281 4869 4871 +5281 4871 5283 5283 4871 4873 5283 4873 5285 +5289 5287 5285 4873 5289 5285 4875 5289 4873 +5290 4875 4876 5290 5289 4875 5291 4876 4877 +5291 5290 4876 5292 4877 4878 5292 5291 4877 +5293 4878 4879 5293 5292 4878 4879 4880 5293 +5293 4880 5294 5288 4884 4862 5295 4884 5288 +5296 5294 4880 5296 4880 4890 5297 4890 4891 +5297 5296 4890 5298 4891 4892 5298 5297 4891 +5299 4892 4893 5299 5298 4892 5300 5299 5301 +5299 4894 5301 5299 4893 4894 4895 5302 4894 +4894 5302 5301 4896 5303 4895 4895 5303 5302 +4897 5304 4896 4896 5304 5303 4898 5305 4897 +4897 5305 5304 4899 5306 4898 4898 5306 5305 +4900 5307 4899 4899 5307 5306 4901 5307 4900 +5295 5308 4884 4884 5308 4909 5308 5309 5310 +4910 5308 5310 4909 5308 4910 5310 4911 4910 +5311 5300 5301 5311 5301 5302 5311 5302 5312 +5313 5314 5312 5302 5313 5312 5303 5313 5302 +5315 5303 5304 5313 5303 5315 5316 5304 5305 +5315 5304 5316 5317 5305 5306 5316 5305 5317 +5318 5306 5307 5317 5306 5318 4920 5319 4901 +5319 5307 4901 5319 5318 5307 4921 5320 4920 +4920 5320 5319 4922 5321 4921 4921 5321 5320 +4923 5322 4922 4922 5322 5321 4924 5323 4923 +4923 5323 5322 4925 5324 4924 4924 5324 5323 +4927 5324 4925 5325 4929 4928 4930 5325 4928 +5309 5326 5310 5326 5327 5328 5326 5328 4932 +5310 5326 4932 4911 5310 4932 5329 4933 4932 +5329 4932 5328 5330 4934 4933 5330 4933 5329 +5330 4935 4934 5331 5314 5313 5331 5313 5315 +5331 5315 5332 5332 5315 5316 5332 5316 5333 +5333 5316 5317 5333 5317 5334 5334 5317 5318 +5334 5318 5335 5336 5337 5335 5318 5336 5335 +5319 5336 5318 5338 5319 5320 5336 5319 5338 +5339 5320 5321 5338 5320 5339 5340 5321 5322 +5339 5321 5340 5341 5322 5323 5340 5322 5341 +5342 5323 5324 5341 5323 5342 4951 5343 4927 +5343 5324 4927 5343 5342 5324 4929 5325 4951 +5325 5343 4951 5325 5344 5343 4952 5345 4930 +5345 5325 4930 5345 5344 5325 4953 5345 4952 +5327 5346 5328 5328 5346 5329 5329 5346 5347 +5329 5347 5330 5330 5347 5348 5348 5349 5350 +5348 5350 4954 5330 5348 4954 4935 5330 4954 +5351 4955 4954 5351 4954 5350 5352 4956 4955 +5352 4955 5351 5353 4957 4956 5353 4956 5352 +5354 4958 4957 5354 4957 5353 5355 4959 4958 +5355 4958 5354 5356 4960 4959 5356 4959 5355 +5357 4961 4960 5357 4960 5356 5358 4962 4961 +5358 4961 5357 5359 4963 4962 5359 4962 5358 +5360 4964 4963 5360 4963 5359 5360 4965 4964 +5361 5337 5336 5361 5336 5338 5361 5338 5362 +5362 5338 5339 5362 5339 5363 5364 5365 5363 +5339 5364 5363 5340 5364 5339 5366 5340 5341 +5364 5340 5366 5367 5341 5342 5366 5341 5367 +5368 5342 5343 5367 5342 5368 4970 5368 4971 +5368 5344 4971 5368 5343 5344 4971 5344 4973 +4973 5344 5345 4973 5345 4953 5349 5369 5350 +5350 5369 5351 5351 5369 5370 5351 5370 5352 +5352 5370 5371 5352 5371 5353 5353 5371 5372 +5353 5372 5354 5354 5372 5373 5354 5373 5355 +5355 5373 5374 5355 5374 5356 5356 5374 5375 +5356 5375 5357 5357 5375 5376 5357 5376 5358 +5358 5376 5377 5358 5377 5359 5359 5377 5378 +5379 5380 5378 5380 5359 5378 5380 5360 5359 +5381 4974 4965 5360 5381 4965 5380 5381 5360 +5382 4975 4974 5382 4974 5381 5383 4976 4975 +5383 4975 5382 5383 4977 4976 5384 5385 5386 +5386 5385 5365 5364 5386 5365 5387 5386 5364 +5388 5364 5366 5387 5364 5388 5389 5366 5367 +5388 5366 5389 5390 5367 5368 5389 5367 5390 +4980 5390 4970 4970 5390 5368 5379 5391 5380 +5380 5391 5381 5381 5391 5392 5381 5392 5382 +5382 5392 5393 5394 5395 5393 5395 5382 5393 +5395 5383 5382 5396 4981 4977 5383 5396 4977 +5395 5396 5383 5396 4982 4981 5397 5398 5399 +5399 5398 5384 5386 5399 5384 5400 5399 5386 +5401 5386 5387 5400 5386 5401 5402 5387 5388 +5401 5387 5402 4983 5402 4984 5402 5389 4984 +5402 5388 5389 4984 5389 4986 4986 5389 5390 +4986 5390 4980 5394 5403 5395 5404 4988 4987 +5395 5404 4987 5396 5395 4987 5403 5404 5395 +5396 4987 4982 5397 5399 5405 5397 5405 5406 +5407 5399 5400 5405 5399 5407 5408 5400 5401 +5407 5400 5408 4990 5408 4991 5408 5402 4991 +5408 5401 5402 4991 5402 4983 5404 5409 4988 +4988 5409 4993 4994 4993 5409 4994 5409 5410 +5411 5412 5413 5413 5412 5406 5405 5413 5406 +5414 5413 5405 5415 5405 5407 5414 5405 5415 +4996 5415 4997 5415 5408 4997 5415 5407 5408 +4997 5408 4990 5410 5416 4994 4994 5416 4999 +5000 4999 5416 5000 5416 5417 5418 5419 5420 +5421 5422 5420 5421 5420 5419 5002 5001 5422 +5421 5002 5422 5423 5002 5421 5413 5424 5004 +5423 5413 5004 5002 5423 5004 5411 5413 5423 +5005 5424 5006 5424 5414 5006 5424 5413 5414 +5006 5414 5008 5008 5414 5415 5008 5415 4996 +5417 5425 5000 5000 5425 5009 5426 5427 5010 +5009 5426 5010 5425 5426 5009 5428 5429 5427 +5428 5427 5426 5430 5429 5428 5431 5432 5433 +5434 5435 5433 5434 5433 5432 5435 5434 5418 +5420 5435 5418 5436 5435 5420 5011 5436 5012 +5436 5422 5012 5436 5420 5422 5012 5422 5001 +5424 5015 5004 5015 5424 5005 5427 5017 5010 +5017 5427 5018 5018 5427 5429 5437 5438 5020 +5437 5020 5018 5430 5437 5018 5429 5430 5018 +5439 5440 5438 5439 5438 5437 5441 5440 5439 +5442 5443 5444 5445 5446 5444 5445 5444 5443 +5446 5445 5431 5433 5446 5431 5447 5446 5433 +5448 5433 5435 5447 5433 5448 5449 5435 5436 +5448 5435 5449 5022 5449 5011 5011 5449 5436 +5438 5024 5020 5024 5438 5026 5026 5438 5440 +5450 5451 5028 5450 5028 5026 5441 5450 5026 +5440 5441 5026 5452 5453 5451 5452 5451 5450 +5454 5455 5453 5454 5453 5452 5456 5457 5455 +5456 5455 5454 5457 5456 5442 5444 5457 5442 +5458 5457 5444 5459 5444 5446 5458 5444 5459 +5460 5446 5447 5459 5446 5460 5029 5460 5030 +5460 5448 5030 5460 5447 5448 5030 5448 5032 +5032 5448 5449 5032 5449 5022 5451 5035 5028 +5035 5451 5037 5037 5451 5453 5037 5453 5039 +5039 5453 5455 5039 5455 5041 5041 5455 5457 +5041 5457 5043 5043 5457 5458 5043 5458 5045 +5045 5458 5459 5045 5459 5047 5047 5459 5460 +5047 5460 5029 5461 5051 5050 5052 5462 5050 +5050 5462 5461 5053 5462 5052 5463 5059 5058 +5061 5464 5058 5058 5464 5463 5062 5465 5061 +5061 5465 5464 5063 5466 5062 5062 5466 5465 +5064 5467 5063 5063 5467 5466 5065 5468 5064 +5064 5468 5467 5051 5461 5065 5461 5468 5065 +5461 5469 5468 5470 5461 5462 5469 5461 5470 +5067 5470 5053 5053 5470 5462 5471 5074 5072 +5076 5472 5072 5072 5472 5471 5059 5463 5076 +5463 5472 5076 5463 5473 5472 5474 5463 5464 +5473 5463 5474 5465 5475 5476 5474 5465 5476 +5464 5465 5474 5475 5465 5466 5475 5466 5477 +5477 5466 5467 5477 5467 5478 5478 5467 5468 +5478 5468 5479 5479 5468 5469 5479 5469 5480 +5480 5469 5470 5480 5470 5481 5482 5481 5483 +5483 5481 5077 5481 5470 5077 5470 5067 5077 +5078 5484 5077 5077 5484 5483 5079 5485 5078 +5078 5485 5484 5080 5485 5079 5486 5083 5084 +5085 5487 5084 5084 5487 5486 5086 5488 5085 +5085 5488 5487 5086 5074 5488 5074 5471 5488 +5088 5087 5488 5088 5488 5471 5471 5472 5489 +5090 5471 5489 5490 5090 5489 5471 5090 5088 +5489 5472 5473 5489 5473 5491 5491 5473 5474 +5491 5474 5492 5492 5474 5476 5493 5482 5483 +5493 5483 5484 5493 5484 5494 5494 5484 5485 +5494 5485 5495 5496 5495 5497 5497 5495 5091 +5495 5485 5091 5485 5080 5091 5092 5497 5091 +5498 5499 5093 5093 5499 5094 5093 5095 5498 +5498 5095 5500 5501 5097 5098 5501 5098 5502 +5098 5503 5502 5098 5099 5503 5504 5099 5100 +5504 5503 5099 5100 5083 5486 5505 5100 5486 +5504 5100 5505 5505 5486 5487 5505 5487 5506 +5506 5487 5488 5506 5488 5507 5507 5488 5087 +5101 5507 5087 5508 5507 5101 5101 5090 5490 +5101 5490 5508 5509 5496 5497 5509 5497 5092 +5102 5509 5092 5510 5509 5102 5102 5103 5510 +5510 5103 5511 5512 5513 5104 5104 5513 5105 +5104 5094 5499 5104 5499 5512 5514 5500 5095 +5514 5095 5106 5515 5106 5107 5515 5514 5106 +5516 5107 5108 5516 5515 5107 5517 5108 5109 +5517 5516 5108 5518 5109 5110 5518 5517 5109 +5110 5097 5501 5519 5110 5501 5518 5110 5519 +5519 5501 5502 5520 5511 5103 5520 5103 5111 +5111 5112 5520 5520 5112 5521 5513 5113 5105 +5522 5113 5513 5114 5521 5112 5114 5523 5521 +5522 5115 5113 5524 5115 5522 5116 5525 5526 +5523 5116 5526 5114 5116 5523 5525 5527 5526 +5524 5117 5115 5528 5117 5524 5529 5530 5525 +5116 5529 5525 5118 5529 5116 5525 5530 5527 +5528 5119 5117 5531 5119 5528 5120 5529 5118 +5120 5532 5529 5531 5121 5119 5533 5121 5531 +5122 5532 5120 5122 5534 5532 5533 5535 5121 +5121 5535 5123 5124 5123 5535 5124 5535 5536 +5125 5534 5122 5125 5537 5534 5538 5539 5126 +5126 5539 5127 5126 5124 5536 5126 5536 5538 +5128 5537 5125 5128 5540 5537 5539 5129 5127 +5541 5129 5539 5542 5543 5131 5542 5131 5130 +5128 5131 5540 5540 5131 5543 5541 5544 5129 +5129 5544 5134 5135 5134 5544 5135 5544 5545 +5136 5542 5130 5136 5546 5542 5545 5137 5135 +5547 5137 5545 5138 5546 5136 5138 5548 5546 +5547 5139 5137 5549 5139 5547 5140 5548 5138 +5140 5550 5548 5549 5141 5139 5551 5141 5549 +5142 5550 5140 5142 5552 5550 5551 5143 5141 +5553 5143 5551 5144 5552 5142 5144 5554 5552 +5553 5145 5143 5555 5145 5553 5146 5554 5144 +5146 5556 5554 5557 5147 5558 5147 5555 5558 +5147 5145 5555 5559 5557 5558 5148 5556 5146 +5148 5560 5556 5557 5150 5147 5151 5150 5557 +5559 5151 5557 5561 5151 5559 5152 5560 5148 +5152 5562 5560 5563 5564 5565 5561 5563 5565 +5154 5561 5565 5151 5561 5154 5566 5562 5152 +5566 5152 5155 5155 5156 5566 5566 5156 5567 +5565 5564 5157 5564 5158 5157 5564 5568 5158 +5565 5157 5154 5159 5567 5156 5159 5569 5567 +5568 5160 5158 5570 5160 5568 5571 5572 5162 +5571 5162 5161 5159 5162 5569 5569 5162 5572 +5570 5163 5160 5573 5163 5570 5164 5571 5161 +5164 5574 5571 5573 5165 5163 5575 5165 5573 +5166 5574 5164 5166 5576 5574 5575 5167 5165 +5577 5167 5575 5168 5576 5166 5168 5578 5576 +5577 5169 5167 5579 5169 5577 5580 5578 5168 +5580 5168 5171 5171 5172 5580 5580 5172 5581 +5579 5173 5169 5582 5173 5579 5174 5581 5172 +5174 5583 5581 5582 5175 5173 5584 5175 5582 +5176 5583 5174 5176 5585 5583 5584 5177 5175 +5586 5177 5584 5178 5585 5176 5178 5587 5585 +5586 5179 5177 5588 5179 5586 5190 5587 5178 +5190 5589 5587 5590 5591 5191 5191 5591 5192 +5191 5179 5588 5191 5588 5590 5193 5592 5194 +5194 5592 5593 5594 5194 5593 5195 5194 5594 +5195 5595 5196 5594 5595 5195 5196 5596 5197 +5595 5596 5196 5197 5597 5198 5596 5597 5197 +5198 5598 5199 5597 5598 5198 5199 5599 5200 +5598 5599 5199 5200 5600 5201 5599 5600 5200 +5201 5601 5202 5600 5601 5201 5202 5602 5203 +5601 5602 5202 5203 5603 5204 5602 5603 5203 +5204 5604 5205 5603 5604 5204 5205 5605 5206 +5604 5605 5205 5605 5606 5607 5207 5605 5607 +5206 5605 5207 5607 5208 5207 5209 5589 5190 +5209 5608 5589 5591 5211 5192 5609 5211 5591 +5610 5611 5592 5193 5610 5592 5213 5610 5193 +5592 5611 5593 5606 5612 5607 5612 5613 5614 +5612 5614 5214 5607 5612 5214 5208 5607 5214 +5614 5215 5214 5218 5615 5219 5219 5615 5616 +5617 5219 5616 5221 5219 5617 5209 5221 5608 +5608 5221 5617 5609 5618 5211 5211 5618 5222 +5223 5222 5618 5223 5618 5619 5224 5620 5225 +5225 5620 5621 5622 5225 5621 5226 5225 5622 +5213 5226 5610 5610 5226 5622 5613 5623 5614 +5614 5623 5215 5623 5227 5215 5623 5624 5227 +5624 5625 5626 5228 5624 5626 5227 5624 5228 +5627 5229 5228 5627 5228 5626 5628 5230 5229 +5628 5229 5627 5628 5629 5230 5230 5629 5218 +5629 5630 5218 5630 5615 5218 5615 5630 5616 +5231 5223 5631 5223 5632 5631 5223 5619 5632 +5631 5232 5231 5633 5634 5236 5633 5236 5235 +5224 5236 5620 5236 5635 5620 5236 5634 5635 +5620 5635 5621 5625 5636 5626 5626 5636 5627 +5627 5636 5637 5627 5637 5628 5628 5637 5638 +5628 5638 5629 5632 5639 5631 5631 5639 5232 +5639 5237 5232 5639 5640 5237 5237 5641 5238 +5640 5641 5237 5238 5642 5239 5641 5642 5238 +5235 5239 5633 5633 5239 5642 5643 5644 5240 +5240 5644 5241 5240 5242 5643 5643 5242 5645 +5646 5647 5648 5649 5650 5648 5648 5650 5646 +5651 5652 5649 5649 5652 5650 5653 5654 5651 +5651 5654 5652 5653 5655 5654 5656 5657 5244 +5244 5657 5245 5244 5241 5644 5244 5644 5656 +5658 5645 5242 5658 5242 5246 5247 5648 5647 +5658 5247 5647 5246 5247 5658 5648 5247 5248 +5648 5248 5649 5649 5248 5249 5649 5249 5651 +5651 5249 5250 5651 5250 5653 5659 5655 5653 +5250 5659 5653 5251 5659 5250 5660 5251 5253 +5660 5659 5251 5661 5253 5255 5661 5660 5253 +5662 5663 5256 5663 5255 5256 5663 5661 5255 +5664 5665 5662 5662 5665 5663 5664 5666 5665 +5667 5258 5257 5668 5667 5257 5245 5668 5257 +5657 5668 5245 5260 5662 5256 5662 5260 5262 +5662 5262 5664 5262 5263 5664 5263 5666 5664 +5263 5669 5666 5670 5671 5667 5671 5258 5667 +5671 5265 5258 5670 5667 5668 5672 5669 5263 +5672 5263 5267 5673 5674 5269 5674 5267 5269 +5674 5672 5267 5673 5675 5674 5676 5677 5272 +5272 5677 5271 5272 5265 5671 5272 5671 5676 +5274 5673 5269 5678 5679 5275 5275 5679 5274 +5679 5675 5274 5675 5673 5274 5680 5681 5678 +5678 5681 5679 5680 5682 5681 5683 5277 5276 +5684 5683 5276 5271 5684 5276 5677 5684 5271 +5279 5678 5275 5678 5279 5282 5678 5282 5680 +5685 5682 5680 5282 5685 5680 5284 5685 5282 +5686 5284 5286 5686 5685 5284 5687 5688 5287 +5688 5286 5287 5688 5686 5286 5687 5689 5688 +5690 5691 5683 5691 5277 5683 5691 5288 5277 +5690 5683 5684 5289 5687 5287 5692 5689 5687 +5289 5692 5687 5290 5692 5289 5693 5290 5291 +5693 5692 5290 5694 5291 5292 5694 5693 5291 +5695 5292 5293 5695 5694 5292 5293 5294 5695 +5695 5294 5696 5697 5295 5698 5295 5691 5698 +5295 5288 5691 5699 5697 5698 5700 5696 5294 +5700 5294 5296 5701 5296 5297 5701 5700 5296 +5702 5297 5298 5702 5701 5297 5703 5702 5704 +5702 5299 5704 5702 5298 5299 5300 5704 5299 +5697 5308 5295 5309 5308 5697 5699 5309 5697 +5705 5309 5699 5706 5703 5704 5707 5706 5708 +5708 5706 5311 5706 5704 5311 5704 5300 5311 +5312 5709 5311 5311 5709 5708 5314 5709 5312 +5705 5710 5309 5309 5710 5326 5327 5326 5710 +5327 5710 5711 5712 5707 5708 5712 5708 5709 +5712 5709 5713 5714 5713 5715 5715 5713 5331 +5713 5709 5331 5709 5314 5331 5332 5716 5331 +5331 5716 5715 5333 5717 5332 5332 5717 5716 +5334 5718 5333 5333 5718 5717 5335 5719 5334 +5334 5719 5718 5337 5719 5335 5711 5720 5327 +5327 5720 5346 5346 5721 5347 5720 5721 5346 +5721 5722 5723 5348 5721 5723 5347 5721 5348 +5723 5349 5348 5724 5714 5715 5725 5726 5724 +5715 5725 5724 5716 5725 5715 5727 5716 5717 +5725 5716 5727 5728 5717 5718 5727 5717 5728 +5729 5718 5719 5728 5718 5729 5361 5730 5337 +5730 5719 5337 5730 5729 5719 5362 5731 5361 +5361 5731 5730 5363 5732 5362 5362 5732 5731 +5365 5732 5363 5722 5733 5723 5723 5733 5349 +5733 5369 5349 5733 5734 5369 5369 5735 5370 +5734 5735 5369 5370 5736 5371 5735 5736 5370 +5371 5737 5372 5736 5737 5371 5372 5738 5373 +5737 5738 5372 5373 5739 5374 5738 5739 5373 +5374 5740 5375 5739 5740 5374 5375 5741 5376 +5740 5741 5375 5376 5742 5377 5741 5742 5376 +5377 5743 5378 5742 5743 5377 5379 5378 5743 +5379 5743 5744 5745 5726 5725 5745 5725 5727 +5745 5727 5746 5747 5748 5746 5727 5747 5746 +5728 5747 5727 5749 5728 5729 5747 5728 5749 +5750 5729 5730 5749 5729 5750 5751 5730 5731 +5750 5730 5751 5384 5751 5385 5751 5732 5385 +5751 5731 5732 5385 5732 5365 5744 5752 5379 +5379 5752 5391 5752 5753 5754 5392 5752 5754 +5391 5752 5392 5755 5393 5392 5755 5392 5754 +5755 5394 5393 5756 5748 5747 5757 5758 5756 +5747 5757 5756 5749 5757 5747 5759 5749 5750 +5757 5749 5759 5397 5759 5398 5759 5751 5398 +5759 5750 5751 5398 5751 5384 5753 5760 5754 +5754 5760 5755 5755 5760 5761 5761 5762 5763 +5761 5763 5403 5755 5761 5403 5394 5755 5403 +5763 5404 5403 5764 5765 5766 5766 5765 5758 +5757 5766 5758 5767 5766 5757 5768 5757 5759 +5767 5757 5768 5406 5768 5397 5397 5768 5759 +5762 5769 5763 5763 5769 5404 5769 5409 5404 +5769 5770 5409 5410 5409 5770 5410 5770 5771 +5772 5773 5774 5774 5773 5764 5766 5774 5764 +5775 5774 5766 5776 5766 5767 5775 5766 5776 +5411 5776 5412 5776 5768 5412 5776 5767 5768 +5412 5768 5406 5771 5777 5410 5410 5777 5416 +5417 5416 5777 5417 5777 5778 5779 5780 5781 +5782 5783 5781 5782 5781 5780 5783 5782 5772 +5783 5772 5774 5418 5783 5774 5419 5418 5774 +5419 5774 5421 5421 5774 5775 5421 5775 5423 +5423 5775 5776 5423 5776 5411 5778 5784 5417 +5417 5784 5425 5425 5785 5426 5784 5785 5425 +5426 5786 5428 5785 5786 5426 5787 5788 5430 +5428 5787 5430 5786 5787 5428 5789 5788 5787 +5790 5791 5792 5793 5794 5792 5793 5792 5791 +5794 5793 5779 5794 5779 5781 5431 5794 5781 +5432 5431 5781 5432 5781 5434 5434 5781 5783 +5434 5783 5418 5788 5437 5430 5789 5795 5788 +5795 5437 5788 5795 5439 5437 5796 5797 5441 +5439 5796 5441 5795 5796 5439 5798 5799 5797 +5798 5797 5796 5800 5801 5799 5800 5799 5798 +5802 5803 5801 5802 5801 5800 5803 5802 5790 +5803 5790 5792 5442 5803 5792 5443 5442 5792 +5443 5792 5445 5445 5792 5794 5445 5794 5431 +5797 5450 5441 5450 5797 5452 5452 5797 5799 +5452 5799 5454 5454 5799 5801 5454 5801 5456 +5456 5801 5803 5456 5803 5442 5804 5476 5475 +5477 5805 5475 5475 5805 5804 5478 5806 5477 +5477 5806 5805 5479 5807 5478 5478 5807 5806 +5480 5808 5479 5479 5808 5807 5481 5809 5480 +5480 5809 5808 5482 5809 5481 5810 5490 5489 +5491 5811 5489 5489 5811 5810 5492 5812 5491 +5491 5812 5811 5476 5804 5492 5804 5812 5492 +5804 5813 5812 5805 5814 5815 5813 5805 5815 +5804 5805 5813 5814 5805 5806 5814 5806 5816 +5816 5806 5807 5816 5807 5817 5817 5807 5808 +5817 5808 5818 5818 5808 5809 5818 5809 5819 +5820 5819 5821 5821 5819 5493 5819 5809 5493 +5809 5482 5493 5494 5822 5493 5493 5822 5821 +5495 5823 5494 5494 5823 5822 5496 5823 5495 +5824 5499 5498 5500 5824 5498 5825 5502 5503 +5504 5826 5503 5503 5826 5825 5505 5827 5504 +5504 5827 5826 5827 5505 5828 5505 5829 5828 +5505 5506 5829 5830 5829 5831 5829 5507 5831 +5829 5506 5507 5508 5832 5507 5507 5832 5831 +5833 5832 5508 5833 5508 5490 5834 5833 5490 +5810 5834 5490 5834 5810 5811 5834 5811 5835 +5835 5811 5812 5835 5812 5836 5836 5812 5813 +5836 5813 5837 5837 5813 5815 5838 5820 5821 +5838 5821 5822 5838 5822 5839 5839 5822 5823 +5839 5823 5840 5840 5823 5496 5509 5840 5496 +5841 5840 5509 5842 5509 5510 5842 5841 5509 +5510 5511 5842 5842 5511 5843 5844 5845 5512 +5512 5845 5513 5512 5499 5824 5846 5512 5824 +5844 5512 5846 5846 5824 5500 5514 5846 5500 +5847 5846 5514 5848 5514 5515 5848 5847 5514 +5849 5515 5516 5849 5848 5515 5850 5516 5517 +5850 5849 5516 5851 5517 5518 5851 5850 5517 +5852 5518 5519 5852 5851 5518 5519 5502 5825 +5853 5519 5825 5852 5519 5853 5853 5825 5826 +5853 5826 5854 5854 5826 5827 5854 5827 5855 +5855 5827 5828 5856 5830 5831 5856 5831 5832 +5856 5832 5857 5857 5832 5833 5858 5843 5511 +5858 5511 5520 5520 5521 5858 5858 5521 5859 +5845 5522 5513 5860 5522 5845 5523 5861 5862 +5859 5523 5862 5521 5523 5859 5861 5863 5862 +5860 5524 5522 5864 5524 5860 5526 5861 5523 +5526 5527 5861 5527 5863 5861 5527 5865 5863 +5864 5528 5524 5866 5528 5864 5529 5867 5530 +5867 5868 5530 5868 5527 5530 5868 5865 5527 +5866 5531 5528 5869 5531 5866 5870 5871 5867 +5529 5870 5867 5532 5870 5529 5867 5871 5868 +5869 5533 5531 5872 5533 5869 5534 5870 5532 +5534 5873 5870 5535 5533 5874 5533 5875 5874 +5533 5872 5875 5874 5536 5535 5537 5873 5534 +5537 5876 5873 5874 5875 5538 5875 5539 5538 +5875 5877 5539 5874 5538 5536 5540 5876 5537 +5540 5878 5876 5877 5541 5539 5879 5541 5877 +5880 5881 5543 5880 5543 5542 5540 5543 5878 +5878 5543 5881 5879 5882 5541 5541 5882 5544 +5545 5544 5882 5545 5882 5883 5546 5880 5542 +5546 5884 5880 5883 5547 5545 5885 5547 5883 +5548 5884 5546 5548 5886 5884 5885 5549 5547 +5887 5549 5885 5550 5886 5548 5550 5888 5886 +5887 5551 5549 5889 5551 5887 5552 5888 5550 +5552 5890 5888 5889 5553 5551 5891 5553 5889 +5554 5890 5552 5554 5892 5890 5891 5555 5553 +5893 5555 5891 5556 5892 5554 5556 5894 5892 +5893 5895 5555 5555 5895 5558 5559 5558 5895 +5559 5895 5896 5560 5894 5556 5560 5897 5894 +5896 5561 5559 5898 5561 5896 5562 5899 5900 +5897 5562 5900 5560 5562 5897 5899 5901 5900 +5902 5903 5563 5563 5903 5564 5563 5561 5898 +5563 5898 5902 5566 5899 5562 5566 5567 5899 +5567 5901 5899 5567 5904 5901 5903 5568 5564 +5905 5568 5903 5906 5907 5908 5907 5904 5908 +5904 5569 5908 5904 5567 5569 5905 5570 5568 +5909 5570 5905 5906 5908 5572 5571 5906 5572 +5910 5906 5571 5572 5908 5569 5909 5573 5570 +5911 5573 5909 5574 5910 5571 5574 5912 5910 +5911 5575 5573 5913 5575 5911 5576 5912 5574 +5576 5914 5912 5913 5577 5575 5915 5577 5913 +5578 5916 5917 5914 5578 5917 5576 5578 5914 +5916 5918 5917 5915 5579 5577 5919 5579 5915 +5580 5916 5578 5580 5581 5916 5581 5918 5916 +5581 5920 5918 5919 5582 5579 5921 5582 5919 +5583 5920 5581 5583 5922 5920 5921 5584 5582 +5923 5584 5921 5585 5922 5583 5585 5924 5922 +5923 5586 5584 5925 5586 5923 5587 5924 5585 +5587 5926 5924 5927 5928 5929 5925 5927 5929 +5588 5925 5929 5586 5925 5588 5589 5926 5587 +5589 5930 5926 5929 5928 5590 5928 5591 5590 +5928 5931 5591 5929 5590 5588 5593 5932 5594 +5933 5595 5594 5933 5594 5932 5934 5596 5595 +5934 5595 5933 5596 5934 5935 5936 5596 5935 +5597 5596 5936 5597 5937 5598 5936 5937 5597 +5598 5938 5599 5937 5938 5598 5599 5939 5600 +5938 5939 5599 5600 5940 5601 5939 5940 5600 +5601 5941 5602 5940 5941 5601 5602 5942 5603 +5941 5942 5602 5603 5943 5604 5942 5943 5603 +5604 5944 5605 5943 5944 5604 5606 5605 5944 +5606 5944 5945 5608 5930 5589 5608 5946 5930 +5931 5609 5591 5947 5609 5931 5610 5948 5611 +5948 5949 5611 5611 5949 5593 5949 5950 5593 +5950 5932 5593 5932 5950 5933 5933 5950 5951 +5933 5951 5934 5934 5951 5952 5934 5952 5935 +5612 5606 5953 5606 5954 5953 5606 5945 5954 +5953 5613 5612 5955 5956 5617 5955 5617 5616 +5608 5617 5946 5946 5617 5956 5618 5609 5957 +5609 5958 5957 5609 5947 5958 5957 5619 5618 +5959 5960 5622 5959 5622 5621 5610 5622 5948 +5622 5961 5948 5622 5960 5961 5948 5961 5949 +5954 5962 5953 5953 5962 5613 5962 5623 5613 +5962 5963 5623 5623 5964 5624 5963 5964 5623 +5625 5624 5964 5625 5964 5965 5966 5967 5630 +5966 5630 5629 5616 5630 5955 5955 5630 5967 +5958 5968 5957 5619 5957 5968 5969 5619 5968 +5632 5619 5969 5970 5971 5634 5970 5634 5633 +5634 5972 5635 5971 5972 5634 5621 5635 5959 +5959 5635 5972 5965 5973 5625 5625 5973 5636 +5636 5974 5637 5973 5974 5636 5637 5975 5638 +5974 5975 5637 5629 5638 5966 5966 5638 5975 +5969 5976 5632 5632 5976 5639 5639 5977 5640 +5976 5977 5639 5640 5978 5641 5977 5978 5640 +5641 5979 5642 5978 5979 5641 5633 5642 5970 +5970 5642 5979 5980 5981 5982 5983 5984 5982 +5982 5984 5980 5983 5985 5984 5986 5644 5643 +5645 5986 5643 5987 5988 5646 5646 5988 5647 +5650 5982 5981 5987 5650 5981 5646 5650 5987 +5982 5650 5652 5982 5652 5983 5989 5985 5983 +5652 5989 5983 5654 5989 5652 5990 5991 5655 +5991 5654 5655 5991 5989 5654 5992 5993 5990 +5990 5993 5991 5994 5995 5992 5992 5995 5993 +5994 5996 5995 5997 5657 5656 5998 5997 5656 +5998 5656 5644 5999 5998 5644 5986 5999 5644 +5999 5986 5645 5658 5999 5645 6000 5999 5658 +5658 5647 5988 5658 5988 6000 5659 5990 5655 +5990 5659 5660 5990 5660 5992 5992 5660 5661 +5992 5661 5994 6001 5996 5994 5661 6001 5994 +5663 6001 5661 6002 5663 5665 6002 6001 5663 +5665 5666 6002 6002 5666 6003 6004 6005 5997 +6005 5657 5997 6005 5668 5657 6004 5997 5998 +5669 6006 6007 6003 5669 6007 5666 5669 6003 +6006 6008 6007 6009 5671 5670 6010 6009 5670 +5668 6010 5670 6005 6010 5668 5672 6006 5669 +6011 6008 6006 5672 6011 6006 5674 6011 5672 +6012 6013 5675 6013 5674 5675 6013 6011 5674 +6012 6014 6013 6015 5677 5676 6016 6015 5676 +6016 5676 5671 6017 6016 5671 6009 6017 5671 +6017 6009 6010 5679 6012 5675 6018 6014 6012 +5679 6018 6012 5681 6018 5679 6019 6020 5682 +6020 5681 5682 6020 6018 5681 6019 6021 6020 +6022 6023 6015 6023 5677 6015 6023 5684 5677 +6022 6015 6016 5685 6019 5682 6024 6021 6019 +5685 6024 6019 5686 6024 5685 6025 5686 5688 +6025 6024 5686 6026 6027 5689 6027 5688 5689 +6027 6025 5688 6026 6028 6027 6029 5691 5690 +6030 6029 5690 5684 6030 5690 6023 6030 5684 +5692 6026 5689 6031 6028 6026 5692 6031 6026 +5693 6031 5692 6032 5693 5694 6032 6031 5693 +6033 5694 5695 6033 6032 5694 5695 5696 6033 +6033 5696 6034 6029 5698 5691 5699 5698 6029 +6030 5699 6029 6035 5699 6030 6036 6034 5696 +6036 5696 5700 6037 5700 5701 6037 6036 5700 +6038 6037 6039 6037 5702 6039 6037 5701 5702 +5703 6039 5702 6035 5705 5699 6040 5705 6035 +6041 6038 6039 6041 6039 5703 5706 6041 5703 +6042 6041 5706 5706 5707 6042 6042 5707 6043 +6040 6044 5705 5705 6044 5710 6045 6046 5711 +5710 6045 5711 6044 6045 5710 6047 6046 6045 +6048 6043 5707 6048 5707 5712 6049 6048 6050 +6048 5713 6050 6048 5712 5713 5714 6050 5713 +6046 5720 5711 6047 6051 6046 6051 5720 6046 +6051 5721 5720 5722 5721 6051 5722 6051 6052 +6049 6050 6053 6049 6053 6054 5724 6055 5714 +6055 6050 5714 6055 6053 6050 5726 6055 5724 +6052 6056 5722 5722 6056 5733 5733 6057 5734 +6056 6057 5733 5734 6058 5735 6057 6058 5734 +5735 6059 5736 6058 6059 5735 5736 6060 5737 +6059 6060 5736 5737 6061 5738 6060 6061 5737 +5738 6062 5739 6061 6062 5738 5739 6063 5740 +6062 6063 5739 5740 6064 5741 6063 6064 5740 +5741 6065 5742 6064 6065 5741 6065 6066 6067 +5743 6065 6067 5742 6065 5743 6067 5744 5743 +6068 6054 6053 6068 6053 6055 6068 6055 6069 +6070 6069 6071 6071 6069 5745 6069 6055 5745 +6055 5726 5745 5746 6072 5745 5745 6072 6071 +5748 6072 5746 6066 6073 6067 6073 6074 6075 +6073 6075 5752 6067 6073 5752 5744 6067 5752 +6075 5753 5752 6076 6070 6071 6077 6078 6076 +6071 6077 6076 6072 6077 6071 5756 6079 5748 +6079 6072 5748 6079 6077 6072 5758 6079 5756 +6074 6080 6075 6075 6080 5753 6080 5760 5753 +6080 6081 5760 6081 6082 6083 5761 6081 6083 +5760 6081 5761 6083 5762 5761 6078 6077 6084 +6078 6084 6085 5764 6084 5765 6084 6079 5765 +6084 6077 6079 5765 6079 5758 6082 6086 6083 +6083 6086 5762 6086 5769 5762 6086 6087 5769 +5769 6088 5770 6087 6088 5769 5771 5770 6088 +5771 6088 6089 6090 6091 6092 6093 6094 6092 +6093 6092 6091 6094 6093 6085 6094 6085 6084 +5772 6094 6084 5773 5772 6084 5773 6084 5764 +6089 6095 5771 5771 6095 5777 6096 6097 5778 +5777 6096 5778 6095 6096 5777 6098 6097 6096 +6099 6100 6101 6102 6103 6101 6102 6101 6100 +6103 6102 6090 6103 6090 6092 5779 6103 6092 +5780 5779 6092 5780 6092 5782 5782 6092 6094 +5782 6094 5772 6097 5784 5778 6098 6104 6097 +6104 5784 6097 6104 5785 5784 5785 6105 5786 +6104 6105 5785 5786 6106 5787 6105 6106 5786 +5789 5787 6106 5789 6106 6107 6108 6109 6110 +6111 6112 6110 6111 6110 6109 6113 6114 6112 +6113 6112 6111 6115 6116 6114 6115 6114 6113 +6116 6115 6099 6116 6099 6101 5790 6116 6101 +5791 5790 6101 5791 6101 5793 5793 6101 6103 +5793 6103 5779 6107 6117 5789 5789 6117 5795 +6110 5796 6108 5796 6117 6108 5796 5795 6117 +5796 6110 5798 5798 6110 6112 5798 6112 5800 +5800 6112 6114 5800 6114 5802 5802 6114 6116 +5802 6116 5790 6118 5815 5814 5816 6119 5814 +5814 6119 6118 5817 6120 5816 5816 6120 6119 +6120 5817 6121 5817 6122 6121 5817 5818 6122 +6123 6122 6124 6122 5819 6124 6122 5818 5819 +5820 6124 5819 6125 5828 5829 5830 6125 5829 +6126 5833 5834 6126 5834 6127 5834 6128 6127 +5834 5835 6128 6129 5835 5836 6129 6128 5835 +6130 5836 5837 6130 6129 5836 5837 5815 6118 +6131 5837 6118 6130 5837 6131 6131 6118 6119 +6131 6119 6132 6132 6119 6120 6132 6120 6133 +6133 6120 6121 6134 6123 6124 6134 6124 5820 +5838 6134 5820 6135 6134 5838 6136 5838 5839 +6136 6135 5838 6137 5839 5840 6137 6136 5839 +6138 5840 5841 6138 6137 5840 6139 5841 5842 +6139 6138 5841 5842 5843 6139 6139 5843 6140 +6141 6142 5844 5844 6142 5845 6143 5844 5846 +6143 6141 5844 6144 5846 5847 6144 6143 5846 +6145 5847 5848 6145 6144 5847 6146 5848 5849 +6146 6145 5848 6147 5849 5850 6147 6146 5849 +6148 5850 5851 6148 6147 5850 6149 5851 5852 +6149 6148 5851 6150 5852 5853 6150 6149 5852 +6151 5853 5854 6151 6150 5853 6152 5854 5855 +6152 6151 5854 5855 5828 6125 6153 5855 6125 +6152 5855 6153 6154 6153 6155 6155 6153 5856 +6153 6125 5856 6125 5830 5856 5857 6156 5856 +5856 6156 6155 6157 6156 5857 6157 5857 5833 +6158 6157 5833 6126 6158 5833 6158 6126 6127 +6159 6140 5843 6159 5843 5858 5858 5859 6159 +6159 5859 6160 6142 5860 5845 6161 5860 6142 +6162 6154 6155 6162 6155 6156 6162 6156 6163 +6163 6156 6157 6164 6160 5859 6164 5859 5862 +5862 5863 6164 6164 5863 6165 6161 5864 5860 +6166 5864 6161 5865 6165 5863 5865 6167 6165 +6166 5866 5864 6168 5866 6166 5868 6167 5865 +5868 6169 6167 6168 5869 5866 6170 5869 6168 +6171 6172 5871 6171 5871 5870 5868 5871 6169 +6169 5871 6172 6170 5872 5869 6173 5872 6170 +5873 6171 5870 5873 6174 6171 6173 5875 5872 +6175 5875 6173 5876 6174 5873 5876 6176 6174 +6175 5877 5875 6177 5877 6175 6178 6179 6180 +6179 6176 6180 6176 5878 6180 6176 5876 5878 +6177 5879 5877 6181 5879 6177 6178 6180 5881 +5880 6178 5881 6182 6178 5880 5881 6180 5878 +6181 6183 5879 5879 6183 5882 5883 5882 6183 +5883 6183 6184 5884 6182 5880 5884 6185 6182 +6184 5885 5883 6186 5885 6184 5886 6185 5884 +5886 6187 6185 6186 5887 5885 6188 5887 6186 +5888 6187 5886 5888 6189 6187 6188 5889 5887 +6190 5889 6188 5890 6189 5888 5890 6191 6189 +6190 5891 5889 6192 5891 6190 5892 6191 5890 +5892 6193 6191 6194 5893 6195 5893 6192 6195 +5893 5891 6192 6196 6194 6195 5894 6193 5892 +5894 6197 6193 6194 5895 5893 5896 5895 6194 +6196 5896 6194 6198 5896 6196 5897 6197 5894 +5897 6199 6197 6200 6201 6202 6198 6200 6202 +5898 6198 6202 5896 6198 5898 6203 6199 5897 +6203 5897 5900 5900 5901 6203 6203 5901 6204 +6202 6201 5902 6201 5903 5902 6201 6205 5903 +6202 5902 5898 5904 6204 5901 5904 6206 6204 +6205 5905 5903 6207 5905 6205 6208 6209 5907 +6208 5907 5906 5904 5907 6206 6206 5907 6209 +6207 5909 5905 6210 5909 6207 5910 6208 5906 +5910 6211 6208 6210 5911 5909 6212 5911 6210 +5912 6211 5910 5912 6213 6211 6212 5913 5911 +6214 5913 6212 5914 6213 5912 5914 6215 6213 +6214 5915 5913 6216 5915 6214 6217 6215 5914 +6217 5914 5917 5917 5918 6217 6217 5918 6218 +6216 5919 5915 6219 5919 6216 5920 6218 5918 +5920 6220 6218 6219 5921 5919 6221 5921 6219 +5922 6220 5920 5922 6222 6220 6221 5923 5921 +6223 5923 6221 5924 6222 5922 5924 6224 6222 +6223 5925 5923 6225 5925 6223 5926 6224 5924 +5926 6226 6224 6227 6228 5927 5927 6228 5928 +5927 5925 6225 5927 6225 6227 5930 6226 5926 +5930 6229 6226 6228 5931 5928 6230 5931 6228 +5935 6231 5936 6232 5937 5936 6232 5936 6231 +5937 6232 6233 6234 5937 6233 5938 5937 6234 +5938 6235 5939 6234 6235 5938 5939 6236 5940 +6235 6236 5939 5940 6237 5941 6236 6237 5940 +5941 6238 5942 6237 6238 5941 6238 6239 6240 +5943 6238 6240 5942 6238 5943 6241 5944 5943 +6241 5943 6240 6241 5945 5944 6242 6243 6244 +6243 6229 6244 6229 5946 6244 6229 5930 5946 +6230 5947 5931 6245 5947 6230 6246 6247 5950 +6246 5950 5949 6247 6248 6249 5951 6247 6249 +5950 6247 5951 5951 6249 6250 6251 5951 6250 +5952 5951 6251 5935 5952 6231 5952 6252 6231 +5952 6251 6252 6231 6252 6232 6232 6252 6253 +6232 6253 6233 6239 6254 6240 6240 6254 6241 +6241 6254 6255 5945 6241 6255 6256 5945 6255 +5954 5945 6256 6242 6244 5956 5955 6242 5956 +6257 6242 5955 5956 6244 5946 6245 5958 5947 +6258 5958 6245 5959 6259 5960 5960 6259 6260 +6261 5960 6260 5961 5960 6261 5949 5961 6246 +6246 5961 6261 6248 6262 6249 6249 6262 6250 +6256 6263 5954 5954 6263 5962 5962 6264 5963 +6263 6264 5962 6264 6265 6266 5964 6264 6266 +5963 6264 5964 6266 5965 5964 6267 6268 5967 +6267 5967 5966 5955 5967 6257 6257 5967 6268 +6258 6269 5958 5958 6269 5968 5969 5968 6269 +5969 6269 6270 5970 6271 5971 5971 6271 6272 +6273 5971 6272 5972 5971 6273 5959 5972 6259 +5972 6274 6259 5972 6273 6274 6259 6274 6260 +6265 6275 6266 6266 6275 5965 6275 5973 5965 +6275 6276 5973 5973 6277 5974 6276 6277 5973 +5974 6278 5975 6277 6278 5974 5966 5975 6267 +6267 5975 6278 6270 6279 5969 5969 6279 5976 +6279 6280 6281 5977 6279 6281 5976 6279 5977 +6282 5978 5977 6282 5977 6281 5978 6282 6283 +6284 5978 6283 5979 5978 6284 5970 5979 6271 +5979 6285 6271 5979 6284 6285 6271 6285 6272 +6280 6286 6281 6281 6286 6282 6282 6286 6287 +6282 6287 6283 6288 6289 6290 6290 6291 6288 +6292 6293 5980 5980 6293 5981 5984 6290 6289 +6292 5984 6289 5980 5984 6292 6294 6295 5985 +5985 6295 5984 6295 6291 5984 6291 6290 5984 +6296 6297 6294 6294 6297 6295 6298 6299 6296 +6296 6299 6297 6300 6301 6298 6298 6301 6299 +6300 6302 6301 6303 6304 5987 5987 6304 5988 +5987 5981 6293 5987 6293 6303 5989 6294 5985 +6294 5989 5991 6294 5991 6296 6296 5991 5993 +6296 5993 6298 6298 5993 5995 6298 5995 6300 +6305 6306 5996 5996 6306 5995 6306 6302 5995 +6302 6300 5995 6307 6308 6305 6305 6308 6306 +6307 6309 6308 6310 5998 5999 6310 5999 6311 +5999 6312 6311 5999 6000 6312 6000 5988 6304 +6000 6304 6312 6001 6305 5996 6305 6001 6002 +6305 6002 6307 6002 6003 6307 6003 6309 6307 +6003 6313 6309 6314 6005 6004 5998 6310 6004 +6310 6314 6004 6310 6315 6314 6310 6311 6316 +6310 6316 6315 6317 6313 6003 6317 6003 6007 +6318 6319 6008 6319 6007 6008 6319 6317 6007 +6318 6320 6319 6321 6010 6314 6314 6010 6005 +6315 6322 6323 6321 6315 6323 6314 6315 6321 +6322 6315 6316 6011 6318 6008 6324 6320 6318 +6011 6324 6318 6013 6324 6011 6325 6326 6014 +6326 6013 6014 6326 6324 6013 6325 6327 6326 +6328 6016 6017 6329 6328 6017 6329 6017 6010 +6330 6329 6010 6321 6330 6010 6330 6321 6323 +6018 6325 6014 6331 6327 6325 6018 6331 6325 +6020 6331 6018 6332 6333 6021 6333 6020 6021 +6333 6331 6020 6332 6334 6333 6335 6023 6022 +6016 6328 6022 6328 6335 6022 6328 6336 6335 +6328 6329 6337 6328 6337 6336 6024 6332 6021 +6338 6334 6332 6024 6338 6332 6025 6338 6024 +6339 6025 6027 6339 6338 6025 6340 6341 6028 +6341 6027 6028 6341 6339 6027 6340 6342 6341 +6343 6030 6335 6335 6030 6023 6336 6344 6345 +6343 6336 6345 6335 6336 6343 6344 6336 6337 +6031 6340 6028 6346 6342 6340 6031 6346 6340 +6032 6346 6031 6347 6346 6348 6346 6033 6348 +6346 6032 6033 6034 6348 6033 6349 6035 6343 +6343 6035 6030 6343 6345 6350 6343 6350 6349 +6351 6347 6348 6351 6348 6034 6036 6351 6034 +6352 6351 6036 6353 6036 6037 6353 6352 6036 +6037 6038 6353 6353 6038 6354 6355 6040 6349 +6349 6040 6035 6349 6350 6356 6357 6349 6356 +6355 6349 6357 6358 6357 6356 6359 6354 6038 +6359 6038 6041 6360 6359 6361 6359 6042 6361 +6359 6041 6042 6043 6361 6042 6355 6044 6040 +6044 6355 6045 6045 6355 6357 6362 6363 6047 +6362 6047 6045 6358 6362 6045 6357 6358 6045 +6364 6363 6362 6365 6360 6361 6366 6365 6367 +6367 6365 6048 6365 6361 6048 6361 6043 6048 +6049 6367 6048 6363 6051 6047 6052 6051 6363 +6364 6052 6363 6368 6052 6364 6366 6367 6369 +6366 6369 6370 6054 6369 6049 6049 6369 6367 +6368 6371 6052 6052 6371 6056 6056 6372 6057 +6371 6372 6056 6057 6373 6058 6372 6373 6057 +6058 6374 6059 6373 6374 6058 6059 6375 6060 +6374 6375 6059 6060 6376 6061 6375 6376 6060 +6061 6377 6062 6376 6377 6061 6062 6378 6063 +6377 6378 6062 6063 6379 6064 6378 6379 6063 +6064 6380 6065 6379 6380 6064 6066 6065 6380 +6066 6380 6381 6382 6370 6369 6383 6382 6384 +6384 6382 6068 6382 6369 6068 6369 6054 6068 +6069 6385 6068 6068 6385 6384 6070 6385 6069 +6073 6066 6386 6066 6387 6386 6066 6381 6387 +6386 6074 6073 6388 6383 6384 6389 6390 6388 +6384 6389 6388 6385 6389 6384 6076 6391 6070 +6391 6385 6070 6391 6389 6385 6078 6391 6076 +6387 6392 6386 6386 6392 6074 6392 6080 6074 +6392 6393 6080 6080 6394 6081 6393 6394 6080 +6082 6081 6394 6082 6394 6395 6390 6389 6396 +6390 6396 6397 6398 6389 6391 6396 6389 6398 +6085 6398 6078 6078 6398 6391 6395 6399 6082 +6082 6399 6086 6086 6400 6087 6399 6400 6086 +6087 6401 6088 6400 6401 6087 6402 6403 6089 +6088 6402 6089 6401 6402 6088 6404 6403 6402 +6405 6406 6407 6408 6409 6407 6408 6407 6406 +6409 6408 6397 6409 6397 6396 6090 6409 6396 +6091 6090 6396 6091 6396 6093 6093 6396 6398 +6093 6398 6085 6403 6095 6089 6404 6410 6403 +6410 6095 6403 6410 6096 6095 6411 6412 6098 +6096 6411 6098 6410 6411 6096 6413 6414 6412 +6413 6412 6411 6415 6416 6414 6415 6414 6413 +6417 6416 6415 6418 6419 6420 6421 6422 6420 +6421 6420 6419 6423 6424 6422 6423 6422 6421 +6424 6423 6405 6424 6405 6407 6099 6424 6407 +6100 6099 6407 6100 6407 6102 6102 6407 6409 +6102 6409 6090 6412 6104 6098 6104 6412 6105 +6105 6412 6414 6105 6414 6106 6106 6414 6416 +6425 6426 6107 6425 6107 6106 6417 6425 6106 +6416 6417 6106 6109 6108 6426 6425 6109 6426 +6427 6109 6425 6420 6111 6418 6111 6427 6418 +6111 6109 6427 6111 6420 6113 6113 6420 6422 +6113 6422 6115 6115 6422 6424 6115 6424 6099 +6426 6117 6107 6117 6426 6108 6428 6121 6122 +6123 6428 6122 6429 6127 6128 6129 6430 6128 +6128 6430 6429 6130 6431 6129 6129 6431 6430 +6431 6130 6432 6130 6433 6432 6130 6131 6433 +6434 6131 6132 6434 6433 6131 6435 6132 6133 +6435 6434 6132 6133 6121 6428 6436 6133 6428 +6435 6133 6436 6437 6436 6438 6438 6436 6134 +6436 6428 6134 6428 6123 6134 6135 6439 6134 +6134 6439 6438 6439 6135 6440 6135 6441 6440 +6135 6136 6441 6442 6136 6137 6442 6441 6136 +6443 6137 6138 6443 6442 6137 6444 6138 6139 +6444 6443 6138 6139 6140 6444 6444 6140 6445 +6446 6447 6141 6141 6447 6142 6448 6446 6449 +6446 6143 6449 6446 6141 6143 6144 6450 6143 +6143 6450 6449 6145 6451 6144 6144 6451 6450 +6146 6452 6145 6145 6452 6451 6147 6453 6146 +6146 6453 6452 6148 6454 6147 6147 6454 6453 +6149 6455 6148 6148 6455 6454 6150 6456 6149 +6149 6456 6455 6151 6457 6150 6150 6457 6456 +6152 6458 6151 6151 6458 6457 6153 6459 6152 +6152 6459 6458 6154 6459 6153 6460 6157 6158 +6461 6460 6158 6461 6158 6127 6462 6461 6127 +6429 6462 6127 6462 6429 6430 6462 6430 6463 +6463 6430 6431 6463 6431 6464 6464 6431 6432 +6465 6437 6438 6465 6438 6439 6465 6439 6466 +6466 6439 6440 6467 6445 6140 6467 6140 6159 +6159 6160 6467 6467 6160 6468 6447 6161 6142 +6469 6161 6447 6470 6448 6449 6470 6449 6450 +6470 6450 6471 6471 6450 6451 6471 6451 6472 +6472 6451 6452 6472 6452 6473 6473 6452 6453 +6473 6453 6474 6474 6453 6454 6474 6454 6475 +6475 6454 6455 6475 6455 6476 6476 6455 6456 +6476 6456 6477 6477 6456 6457 6477 6457 6478 +6478 6457 6458 6478 6458 6479 6480 6481 6479 +6458 6480 6479 6459 6480 6458 6162 6482 6154 +6482 6459 6154 6482 6480 6459 6163 6483 6162 +6162 6483 6482 6157 6460 6163 6460 6483 6163 +6460 6484 6483 6460 6461 6485 6460 6485 6484 +6486 6468 6160 6486 6160 6164 6164 6165 6486 +6486 6165 6487 6469 6166 6161 6488 6166 6469 +6489 6481 6480 6489 6480 6482 6489 6482 6490 +6490 6482 6483 6490 6483 6491 6491 6483 6484 +6491 6484 6492 6492 6484 6485 6167 6487 6165 +6167 6493 6487 6488 6168 6166 6494 6168 6488 +6169 6493 6167 6169 6495 6493 6494 6170 6168 +6496 6170 6494 6171 6497 6172 6497 6498 6172 +6498 6169 6172 6498 6495 6169 6496 6173 6170 +6499 6173 6496 6500 6501 6497 6171 6500 6497 +6174 6500 6171 6497 6501 6498 6499 6175 6173 +6502 6175 6499 6176 6500 6174 6176 6503 6500 +6502 6177 6175 6504 6177 6502 6505 6506 6179 +6505 6179 6178 6176 6179 6503 6503 6179 6506 +6504 6181 6177 6507 6181 6504 6182 6505 6178 +6182 6508 6505 6183 6181 6509 6181 6510 6509 +6181 6507 6510 6509 6184 6183 6185 6508 6182 +6185 6511 6508 6510 6512 6509 6184 6509 6512 +6513 6184 6512 6186 6184 6513 6187 6511 6185 +6187 6514 6511 6513 6188 6186 6515 6188 6513 +6189 6514 6187 6189 6516 6514 6515 6190 6188 +6517 6190 6515 6191 6516 6189 6191 6518 6516 +6517 6192 6190 6519 6192 6517 6193 6518 6191 +6193 6520 6518 6519 6521 6192 6192 6521 6195 +6196 6195 6521 6196 6521 6522 6197 6520 6193 +6197 6523 6520 6522 6198 6196 6524 6198 6522 +6199 6525 6526 6523 6199 6526 6197 6199 6523 +6525 6527 6526 6528 6529 6200 6200 6529 6201 +6200 6198 6524 6200 6524 6528 6203 6525 6199 +6203 6204 6525 6204 6527 6525 6204 6530 6527 +6529 6205 6201 6531 6205 6529 6206 6530 6204 +6206 6532 6530 6531 6207 6205 6533 6207 6531 +6534 6535 6209 6534 6209 6208 6206 6209 6532 +6532 6209 6535 6533 6210 6207 6536 6210 6533 +6211 6534 6208 6211 6537 6534 6536 6212 6210 +6538 6212 6536 6213 6537 6211 6213 6539 6537 +6538 6214 6212 6540 6214 6538 6215 6539 6213 +6215 6541 6539 6540 6216 6214 6542 6216 6540 +6543 6541 6215 6543 6215 6217 6217 6218 6543 +6543 6218 6544 6542 6219 6216 6545 6219 6542 +6220 6544 6218 6220 6546 6544 6545 6221 6219 +6547 6221 6545 6222 6546 6220 6222 6548 6546 +6547 6223 6221 6549 6223 6547 6224 6548 6222 +6224 6550 6548 6549 6225 6223 6551 6225 6549 +6226 6550 6224 6226 6552 6550 6553 6554 6227 +6227 6554 6228 6227 6225 6551 6227 6551 6553 +6229 6552 6226 6229 6555 6552 6554 6230 6228 +6556 6230 6554 6233 6557 6234 6558 6235 6234 +6558 6234 6557 6559 6236 6235 6559 6235 6558 +6560 6237 6236 6560 6236 6559 6561 6238 6237 +6561 6237 6560 6561 6239 6238 6562 6563 6243 +6562 6243 6242 6229 6243 6555 6555 6243 6563 +6556 6245 6230 6564 6245 6556 6246 6565 6247 +6565 6248 6247 6250 6566 6251 6251 6566 6567 +6568 6251 6567 6252 6251 6568 6252 6569 6253 +6568 6569 6252 6233 6253 6557 6253 6570 6557 +6253 6569 6570 6557 6570 6558 6558 6570 6571 +6558 6571 6559 6559 6571 6572 6559 6572 6560 +6560 6572 6573 6560 6573 6561 6561 6573 6574 +6561 6574 6239 6574 6254 6239 6574 6575 6254 +6575 6576 6577 6255 6575 6577 6254 6575 6255 +6577 6256 6255 6257 6562 6242 6257 6578 6562 +6564 6258 6245 6579 6258 6564 6580 6581 6261 +6580 6261 6260 6246 6261 6565 6261 6582 6565 +6261 6581 6582 6565 6582 6248 6582 6262 6248 +6582 6583 6262 6250 6262 6566 6262 6584 6566 +6262 6583 6584 6566 6584 6567 6576 6585 6577 +6577 6585 6256 6585 6263 6256 6585 6586 6263 +6586 6587 6588 6264 6586 6588 6263 6586 6264 +6588 6265 6264 6267 6589 6268 6589 6590 6268 +6590 6257 6268 6590 6578 6257 6579 6591 6258 +6258 6591 6269 6270 6269 6591 6270 6591 6592 +6593 6594 6273 6593 6273 6272 6273 6595 6274 +6594 6595 6273 6260 6274 6580 6580 6274 6595 +6587 6596 6588 6588 6596 6265 6596 6275 6265 +6596 6597 6275 6275 6598 6276 6597 6598 6275 +6276 6599 6277 6598 6599 6276 6277 6600 6278 +6599 6600 6277 6267 6278 6589 6278 6601 6589 +6278 6600 6601 6589 6601 6590 6592 6602 6270 +6270 6602 6279 6280 6279 6602 6280 6602 6603 +6283 6604 6284 6284 6604 6605 6606 6284 6605 +6285 6284 6606 6272 6285 6593 6593 6285 6606 +6603 6607 6280 6280 6607 6286 6286 6608 6287 +6607 6608 6286 6283 6287 6604 6287 6609 6604 +6287 6608 6609 6604 6609 6605 6610 6611 6612 +6613 6614 6612 6612 6614 6610 6613 6615 6614 +6616 6617 6618 6289 6288 6618 6288 6616 6618 +6288 6619 6616 6620 6621 6291 6621 6288 6291 +6621 6619 6288 6622 6623 6620 6620 6623 6621 +6623 6622 6611 6622 6612 6611 6622 6624 6612 +6612 6625 6613 6612 6624 6625 6615 6613 6626 +6613 6627 6626 6613 6625 6627 6627 6628 6626 +6618 6617 6292 6617 6293 6292 6617 6629 6293 +6618 6292 6289 6295 6620 6291 6620 6295 6297 +6620 6297 6622 6622 6297 6299 6622 6299 6624 +6624 6299 6301 6624 6301 6625 6625 6301 6302 +6630 6625 6302 6627 6625 6630 6628 6627 6631 +6627 6632 6631 6627 6630 6632 6632 6633 6631 +6634 6635 6303 6303 6635 6304 6303 6293 6629 +6303 6629 6634 6306 6630 6302 6630 6306 6308 +6630 6308 6632 6308 6309 6632 6309 6633 6632 +6309 6636 6633 6637 6311 6312 6638 6637 6312 +6304 6638 6312 6635 6638 6304 6313 6639 6640 +6636 6313 6640 6309 6313 6636 6641 6642 6639 +6639 6642 6640 6641 6643 6642 6644 6645 6637 +6645 6311 6637 6645 6316 6311 6644 6637 6638 +6317 6639 6313 6639 6317 6319 6639 6319 6641 +6646 6647 6320 6320 6647 6319 6647 6643 6319 +6643 6641 6319 6648 6649 6646 6646 6649 6647 +6648 6650 6649 6651 6323 6322 6652 6651 6322 +6316 6652 6322 6645 6652 6316 6324 6646 6320 +6646 6324 6326 6646 6326 6648 6653 6654 6327 +6327 6654 6326 6654 6650 6326 6650 6648 6326 +6653 6655 6654 6656 6329 6330 6323 6651 6330 +6651 6656 6330 6651 6657 6656 6651 6652 6658 +6651 6658 6657 6331 6653 6327 6659 6655 6653 +6331 6659 6653 6333 6659 6331 6660 6661 6334 +6661 6333 6334 6661 6659 6333 6660 6662 6661 +6663 6337 6656 6656 6337 6329 6657 6664 6665 +6663 6657 6665 6656 6657 6663 6664 6657 6658 +6338 6660 6334 6666 6662 6660 6338 6666 6660 +6339 6666 6338 6667 6339 6341 6667 6666 6339 +6668 6669 6342 6669 6341 6342 6669 6667 6341 +6668 6670 6669 6671 6345 6344 6672 6671 6344 +6672 6344 6337 6673 6672 6337 6663 6673 6337 +6673 6663 6665 6346 6668 6342 6674 6675 6347 +6347 6675 6346 6675 6670 6346 6670 6668 6346 +6674 6676 6675 6677 6350 6671 6671 6350 6345 +6671 6672 6678 6679 6671 6678 6677 6671 6679 +6680 6679 6678 6351 6674 6347 6681 6676 6674 +6351 6681 6674 6352 6681 6351 6682 6352 6353 +6682 6681 6352 6353 6354 6682 6682 6354 6683 +6677 6356 6350 6356 6677 6358 6677 6684 6358 +6677 6679 6684 6679 6680 6685 6679 6685 6684 +6686 6683 6354 6686 6354 6359 6359 6360 6686 +6686 6360 6687 6684 6362 6358 6364 6362 6684 +6685 6364 6684 6688 6364 6685 6360 6365 6689 +6690 6360 6689 6687 6360 6690 6366 6689 6365 +6691 6368 6692 6368 6688 6692 6368 6364 6688 +6693 6694 6691 6693 6691 6692 6695 6696 6694 +6695 6694 6693 6697 6698 6696 6697 6696 6695 +6699 6700 6698 6699 6698 6697 6701 6700 6699 +6690 6689 6702 6690 6702 6703 6370 6702 6366 +6366 6702 6689 6691 6371 6368 6371 6691 6372 +6372 6691 6694 6372 6694 6373 6373 6694 6696 +6373 6696 6374 6374 6696 6698 6374 6698 6375 +6375 6698 6700 6701 6704 6700 6704 6375 6700 +6704 6376 6375 6376 6705 6377 6704 6705 6376 +6377 6706 6378 6705 6706 6377 6378 6707 6379 +6706 6707 6378 6379 6708 6380 6707 6708 6379 +6381 6380 6708 6381 6708 6709 6710 6703 6702 +6711 6710 6712 6712 6710 6382 6710 6702 6382 +6702 6370 6382 6383 6712 6382 6713 6387 6714 +6387 6709 6714 6387 6381 6709 6715 6713 6714 +6711 6712 6716 6711 6716 6717 6388 6718 6383 +6718 6712 6383 6718 6716 6712 6390 6718 6388 +6713 6392 6387 6715 6719 6713 6719 6392 6713 +6719 6393 6392 6393 6720 6394 6719 6720 6393 +6721 6722 6395 6394 6721 6395 6720 6721 6394 +6723 6722 6721 6724 6725 6726 6726 6725 6717 +6716 6726 6717 6727 6726 6716 6728 6716 6718 +6727 6716 6728 6397 6728 6390 6390 6728 6718 +6722 6399 6395 6723 6729 6722 6729 6399 6722 +6729 6400 6399 6400 6730 6401 6729 6730 6400 +6401 6731 6402 6730 6731 6401 6732 6733 6404 +6402 6732 6404 6731 6732 6402 6734 6735 6733 +6734 6733 6732 6736 6737 6735 6736 6735 6734 +6738 6737 6736 6739 6740 6741 6742 6743 6741 +6742 6741 6740 6743 6742 6724 6726 6743 6724 +6744 6743 6726 6405 6744 6406 6744 6727 6406 +6744 6726 6727 6406 6727 6408 6408 6727 6728 +6408 6728 6397 6733 6410 6404 6410 6733 6411 +6411 6733 6735 6411 6735 6413 6413 6735 6737 +6738 6745 6737 6745 6413 6737 6745 6415 6413 +6746 6747 6417 6415 6746 6417 6745 6746 6415 +6748 6749 6747 6748 6747 6746 6749 6748 6739 +6749 6739 6741 6418 6749 6741 6419 6418 6741 +6419 6741 6421 6421 6741 6743 6421 6743 6423 +6423 6743 6744 6423 6744 6405 6747 6425 6417 +6425 6747 6427 6427 6747 6749 6427 6749 6418 +6750 6432 6433 6434 6751 6433 6433 6751 6750 +6751 6434 6752 6434 6753 6752 6434 6435 6753 +6754 6435 6436 6754 6753 6435 6436 6437 6754 +6754 6437 6755 6756 6440 6441 6442 6757 6441 +6441 6757 6756 6443 6758 6442 6442 6758 6757 +6444 6759 6443 6443 6759 6758 6445 6759 6444 +6760 6447 6446 6448 6760 6446 6761 6461 6462 +6463 6762 6462 6462 6762 6761 6762 6463 6763 +6463 6764 6763 6463 6464 6764 6464 6432 6750 +6765 6464 6750 6764 6464 6765 6765 6750 6751 +6765 6751 6766 6766 6751 6752 6767 6755 6437 +6767 6437 6465 6768 6465 6466 6768 6767 6465 +6466 6440 6756 6769 6466 6756 6768 6466 6769 +6769 6756 6757 6769 6757 6770 6770 6757 6758 +6770 6758 6771 6771 6758 6759 6771 6759 6772 +6772 6759 6445 6467 6772 6445 6773 6772 6467 +6467 6468 6773 6773 6468 6774 6775 6469 6760 +6760 6469 6447 6775 6760 6448 6776 6775 6448 +6470 6776 6448 6777 6775 6776 6778 6470 6471 +6778 6776 6470 6779 6471 6472 6779 6778 6471 +6780 6472 6473 6780 6779 6472 6781 6473 6474 +6781 6780 6473 6782 6474 6475 6782 6781 6474 +6783 6475 6476 6783 6782 6475 6784 6476 6477 +6784 6783 6476 6785 6784 6786 6784 6478 6786 +6784 6477 6478 6479 6787 6478 6478 6787 6786 +6481 6787 6479 6788 6485 6761 6761 6485 6461 +6762 6789 6790 6788 6762 6790 6761 6762 6788 +6789 6762 6763 6791 6774 6468 6791 6468 6486 +6486 6487 6791 6791 6487 6792 6793 6794 6775 +6794 6469 6775 6794 6488 6469 6793 6775 6777 +6795 6785 6786 6795 6786 6787 6795 6787 6796 +6797 6796 6798 6798 6796 6489 6796 6787 6489 +6787 6481 6489 6490 6799 6489 6489 6799 6798 +6491 6800 6490 6490 6800 6799 6800 6491 6801 +6491 6802 6801 6491 6492 6802 6492 6485 6788 +6803 6492 6788 6802 6492 6803 6803 6788 6790 +6493 6792 6487 6493 6804 6792 6794 6494 6488 +6805 6494 6794 6806 6797 6798 6806 6798 6799 +6806 6799 6807 6807 6799 6800 6807 6800 6808 +6808 6800 6801 6495 6804 6493 6495 6809 6804 +6805 6496 6494 6810 6496 6805 6498 6809 6495 +6498 6811 6809 6812 6813 6814 6810 6812 6814 +6499 6810 6814 6496 6810 6499 6500 6815 6501 +6815 6816 6501 6816 6498 6501 6816 6811 6498 +6813 6817 6814 6499 6814 6817 6818 6499 6817 +6502 6499 6818 6503 6819 6815 6503 6815 6500 +6816 6815 6820 6820 6815 6819 6818 6504 6502 +6821 6504 6818 6505 6822 6506 6822 6823 6506 +6506 6823 6503 6823 6824 6503 6824 6819 6503 +6819 6824 6820 6821 6507 6504 6825 6507 6821 +6508 6826 6822 6508 6822 6505 6823 6822 6827 +6827 6822 6826 6825 6510 6507 6828 6510 6825 +6511 6829 6826 6511 6826 6508 6827 6826 6830 +6830 6826 6829 6828 6831 6510 6510 6831 6512 +6513 6512 6831 6513 6831 6832 6514 6833 6829 +6514 6829 6511 6830 6829 6834 6834 6829 6833 +6832 6515 6513 6835 6515 6832 6516 6836 6833 +6516 6833 6514 6834 6833 6837 6837 6833 6836 +6835 6517 6515 6838 6517 6835 6518 6839 6836 +6518 6836 6516 6837 6836 6840 6840 6836 6839 +6838 6519 6517 6841 6519 6838 6520 6842 6839 +6520 6839 6518 6840 6839 6843 6843 6839 6842 +6841 6844 6519 6519 6844 6521 6522 6521 6844 +6522 6844 6845 6523 6846 6842 6523 6842 6520 +6843 6842 6847 6847 6842 6846 6845 6524 6522 +6848 6524 6845 6526 6846 6523 6526 6527 6846 +6527 6847 6846 6527 6849 6847 6850 6851 6528 +6528 6851 6529 6528 6524 6848 6528 6848 6850 +6530 6849 6527 6530 6852 6849 6851 6531 6529 +6853 6531 6851 6854 6855 6856 6855 6852 6856 +6852 6532 6856 6852 6530 6532 6853 6533 6531 +6857 6533 6853 6854 6856 6535 6534 6854 6535 +6858 6854 6534 6535 6856 6532 6857 6536 6533 +6859 6536 6857 6537 6858 6534 6537 6860 6858 +6859 6538 6536 6861 6538 6859 6539 6860 6537 +6539 6862 6860 6861 6540 6538 6863 6540 6861 +6541 6862 6539 6541 6864 6862 6863 6542 6540 +6865 6542 6863 6866 6864 6541 6866 6541 6543 +6543 6544 6866 6866 6544 6867 6865 6545 6542 +6868 6545 6865 6546 6867 6544 6546 6869 6867 +6868 6547 6545 6870 6547 6868 6548 6869 6546 +6548 6871 6869 6870 6549 6547 6872 6549 6870 +6550 6871 6548 6550 6873 6871 6874 6875 6876 +6872 6874 6876 6551 6872 6876 6549 6872 6551 +6552 6873 6550 6552 6877 6873 6876 6875 6553 +6875 6554 6553 6875 6878 6554 6876 6553 6551 +6555 6877 6552 6555 6879 6877 6878 6556 6554 +6880 6556 6878 6881 6882 6563 6881 6563 6562 +6555 6563 6879 6879 6563 6882 6880 6564 6556 +6883 6564 6880 6567 6884 6568 6885 6569 6568 +6885 6568 6884 6569 6885 6886 6887 6569 6886 +6570 6569 6887 6570 6888 6571 6887 6888 6570 +6571 6889 6572 6888 6889 6571 6572 6890 6573 +6889 6890 6572 6573 6891 6574 6890 6891 6573 +6574 6892 6575 6891 6892 6574 6576 6575 6892 +6576 6892 6893 6578 6881 6562 6578 6894 6881 +6883 6579 6564 6895 6579 6883 6580 6896 6581 +6897 6582 6581 6897 6581 6896 6582 6897 6898 +6899 6582 6898 6583 6582 6899 6583 6900 6584 +6899 6900 6583 6567 6584 6884 6584 6901 6884 +6584 6900 6901 6884 6901 6885 6885 6901 6902 +6885 6902 6886 6893 6903 6576 6576 6903 6585 +6903 6904 6905 6586 6903 6905 6585 6903 6586 +6905 6587 6586 6590 6894 6578 6590 6906 6894 +6895 6907 6579 6579 6907 6591 6592 6591 6907 +6592 6907 6908 6909 6910 6594 6909 6594 6593 +6594 6911 6595 6910 6911 6594 6580 6595 6896 +6595 6912 6896 6595 6911 6912 6896 6912 6897 +6897 6912 6913 6897 6913 6898 6904 6914 6905 +6905 6914 6587 6914 6596 6587 6914 6915 6596 +6596 6916 6597 6915 6916 6596 6597 6917 6598 +6916 6917 6597 6598 6918 6599 6917 6918 6598 +6599 6919 6600 6918 6919 6599 6600 6920 6601 +6919 6920 6600 6590 6601 6906 6906 6601 6920 +6908 6921 6592 6592 6921 6602 6603 6602 6921 +6603 6921 6922 6923 6924 6606 6923 6606 6605 +6593 6606 6909 6909 6606 6924 6922 6925 6603 +6603 6925 6607 6607 6926 6608 6925 6926 6607 +6608 6927 6609 6926 6927 6608 6605 6609 6923 +6923 6609 6927 6928 6929 6930 6931 6932 6930 +6930 6932 6928 6933 6934 6931 6931 6934 6932 +6611 6610 6933 6610 6934 6933 6610 6935 6934 +6936 6610 6614 6936 6935 6610 6937 6938 6615 +6938 6614 6615 6938 6936 6614 6937 6939 6938 +6940 6941 6616 6616 6941 6617 6619 6930 6929 +6940 6619 6929 6616 6619 6940 6930 6619 6621 +6930 6621 6931 6931 6621 6623 6931 6623 6933 +6933 6623 6611 6626 6937 6615 6942 6943 6628 +6628 6943 6626 6943 6939 6626 6939 6937 6626 +6942 6944 6943 6941 6629 6617 6945 6629 6941 +6631 6942 6628 6946 6947 6633 6633 6947 6631 +6947 6944 6631 6944 6942 6631 6946 6948 6947 +6949 6950 6634 6634 6950 6635 6634 6629 6945 +6634 6945 6949 6636 6951 6946 6636 6946 6633 +6948 6946 6952 6946 6953 6952 6946 6951 6953 +6953 6954 6952 6950 6638 6635 6955 6638 6950 +6640 6951 6636 6951 6640 6642 6951 6642 6953 +6956 6957 6643 6643 6957 6642 6957 6954 6642 +6954 6953 6642 6956 6958 6957 6959 6645 6644 +6960 6959 6644 6638 6960 6644 6955 6960 6638 +6647 6956 6643 6961 6958 6956 6647 6961 6956 +6649 6961 6647 6962 6963 6650 6963 6649 6650 +6963 6961 6649 6962 6964 6963 6965 6652 6959 +6959 6652 6645 6959 6960 6966 6959 6966 6965 +6654 6962 6650 6967 6968 6655 6655 6968 6654 +6968 6964 6654 6964 6962 6654 6969 6970 6967 +6967 6970 6968 6969 6971 6970 6972 6973 6965 +6973 6652 6965 6973 6658 6652 6972 6965 6966 +6659 6967 6655 6967 6659 6661 6967 6661 6969 +6974 6975 6662 6662 6975 6661 6975 6971 6661 +6971 6969 6661 6976 6977 6974 6974 6977 6975 +6976 6978 6977 6979 6665 6664 6980 6979 6664 +6658 6980 6664 6973 6980 6658 6666 6974 6662 +6974 6666 6667 6974 6667 6976 6981 6978 6976 +6667 6981 6976 6669 6981 6667 6669 6670 6981 +6981 6670 6982 6983 6672 6673 6665 6979 6673 +6979 6983 6673 6979 6984 6983 6979 6980 6985 +6979 6985 6984 6986 6982 6670 6986 6670 6675 +6987 6988 6676 6988 6675 6676 6988 6986 6675 +6987 6989 6988 6983 6678 6672 6678 6983 6680 +6983 6990 6680 6983 6984 6990 6984 6985 6991 +6984 6991 6990 6681 6987 6676 6992 6989 6987 +6681 6992 6987 6682 6992 6681 6993 6994 6683 +6994 6682 6683 6994 6992 6682 6993 6995 6994 +6996 6997 6990 6997 6680 6990 6997 6685 6680 +6996 6990 6991 6686 6993 6683 6686 6687 6993 +6687 6995 6993 6687 6998 6995 6999 6688 7000 +6688 6997 7000 6688 6685 6997 7001 6999 7000 +6690 6998 6687 6690 7002 6998 6999 6692 6688 +7001 7003 6999 7003 6692 6999 7003 6693 6692 +6693 7004 6695 7003 7004 6693 6695 7005 6697 +7004 7005 6695 6697 7006 6699 7005 7006 6697 +7007 7008 6701 6699 7007 6701 7006 7007 6699 +7009 7010 7008 7009 7008 7007 7011 7012 7010 +7011 7010 7009 7013 7012 7011 6703 7002 6690 +6703 7014 7002 7008 6704 6701 6704 7008 6705 +6705 7008 7010 6705 7010 6706 6706 7010 7012 +7013 7015 7012 7015 6706 7012 7015 6707 6706 +6707 7016 6708 7015 7016 6707 6709 6708 7016 +6709 7016 7017 6703 6710 7018 7019 6703 7018 +7014 6703 7019 6711 7018 6710 7017 7020 6709 +6709 7020 6714 7021 7022 6715 6714 7021 6715 +7020 7021 6714 7023 7022 7021 7019 7018 7024 +7019 7024 7025 6717 7024 6711 6711 7024 7018 +7022 6719 6715 7023 7026 7022 7026 6719 7022 +7026 6720 6719 6720 7027 6721 7026 7027 6720 +7028 7029 6723 6721 7028 6723 7027 7028 6721 +7030 7031 7029 7030 7029 7028 7032 7031 7030 +7033 7034 7035 7036 7035 7034 7037 7038 7039 +7040 7041 7039 7040 7039 7038 7041 7040 7025 +7041 7025 7024 6724 7041 7024 6725 6724 7024 +6725 7024 6717 7029 6729 6723 6729 7029 6730 +6730 7029 7031 7032 7042 7031 7042 6730 7031 +7042 6731 6730 6731 7043 6732 7042 7043 6731 +6732 7044 6734 7043 7044 6732 7035 6736 7033 +6736 7044 7033 6736 6734 7044 7045 7046 6738 +7045 6738 6736 7036 7045 6736 7035 7036 6736 +7047 7048 7046 7047 7046 7045 7049 7050 7048 +7049 7048 7047 7050 7049 7037 7050 7037 7039 +6739 7050 7039 6740 6739 7039 6740 7039 6742 +6742 7039 7041 6742 7041 6724 7046 6745 6738 +6745 7046 6746 6746 7046 7048 6746 7048 6748 +6748 7048 7050 6748 7050 6739 7051 6752 6753 +6754 7052 6753 6753 7052 7051 6755 7052 6754 +7053 6763 6764 6765 7054 6764 6764 7054 7053 +7054 6765 7055 6765 7056 7055 6765 6766 7056 +6766 6752 7051 7057 6766 7051 7056 6766 7057 +7058 7059 7057 7051 7058 7057 7052 7058 7051 +7058 7052 6755 7060 7058 6755 6767 7060 6755 +7061 7058 7060 7062 6767 6768 7062 7060 6767 +7063 6768 6769 7063 7062 6768 7064 6769 6770 +7064 7063 6769 7065 6770 6771 7065 7064 6770 +7066 6771 6772 7066 7065 6771 7067 6772 6773 +7067 7066 6772 6773 6774 7067 7067 6774 7068 +7069 7070 6776 6776 7070 6777 7071 6776 6778 +7071 7069 6776 7072 6778 6779 7072 7071 6778 +7073 6779 6780 7073 7072 6779 7074 6780 6781 +7074 7073 6780 7075 6781 6782 7075 7074 6781 +7076 6782 6783 7076 7075 6782 7077 7076 7078 +7076 6784 7078 7076 6783 6784 6785 7078 6784 +7079 7080 6789 6789 7080 6790 6789 6763 7053 +7081 6789 7053 7079 6789 7081 7081 7053 7054 +7081 7054 7082 7082 7054 7055 7083 7059 7058 +7083 7058 7061 6774 6791 7084 7085 6774 7084 +7068 6774 7085 6792 7084 6791 7086 7087 6793 +6793 7087 6794 6793 6777 7070 6793 7070 7086 +7088 7077 7078 7088 7078 6785 6795 7088 6785 +7089 7088 6795 7090 6795 6796 7090 7089 6795 +6796 6797 7090 7090 6797 7091 7092 6801 6802 +6803 7093 6802 6802 7093 7092 7094 7093 6803 +6790 7094 6803 7080 7094 6790 7095 7085 7084 +7084 6792 7095 6792 7096 7095 6792 6804 7096 +7087 6805 6794 7097 6805 7087 7098 7091 6797 +7098 6797 6806 7099 6806 6807 7099 7098 6806 +7100 6807 6808 7100 7099 6807 6808 6801 7092 +7101 6808 7092 7100 6808 7101 7101 7092 7093 +7101 7093 7102 7102 7093 7094 6809 7096 6804 +6809 7103 7096 7097 6810 6805 7104 6810 7097 +6811 7103 6809 6811 7105 7103 7106 7107 6812 +6812 7107 6813 6812 6810 7104 6812 7104 7106 +6816 7105 6811 6816 7108 7105 7107 7109 6813 +6813 7109 6817 6818 6817 7109 6818 7109 7110 +7111 7112 7113 7112 7108 7113 7108 6820 7113 +7108 6816 6820 7110 6821 6818 7114 6821 7110 +7111 7113 6824 6823 7111 6824 7115 7111 6823 +6824 7113 6820 7114 6825 6821 7116 6825 7114 +6827 7115 6823 6827 7117 7115 7116 6828 6825 +7118 6828 7116 6830 7117 6827 6830 7119 7117 +7118 7120 6828 6828 7120 6831 6832 6831 7120 +6832 7120 7121 6834 7119 6830 6834 7122 7119 +7121 6835 6832 7123 6835 7121 6837 7122 6834 +6837 7124 7122 7123 6838 6835 7125 6838 7123 +6840 7124 6837 6840 7126 7124 7127 6841 7128 +6841 7125 7128 6841 6838 7125 7129 7127 7128 +6843 7126 6840 6843 7130 7126 7127 6844 6841 +6845 6844 7127 7129 6845 7127 7131 6845 7129 +6847 7130 6843 6847 7132 7130 7133 7134 7135 +7131 7133 7135 6848 7131 7135 6845 7131 6848 +6849 7132 6847 6849 7136 7132 7135 7134 6850 +7134 6851 6850 7134 7137 6851 7135 6850 6848 +6852 7136 6849 6852 7138 7136 7137 6853 6851 +7139 6853 7137 7140 7141 6855 7140 6855 6854 +6852 6855 7138 7138 6855 7141 7139 6857 6853 +7142 6857 7139 6858 7140 6854 6858 7143 7140 +7142 6859 6857 7144 6859 7142 6860 7143 6858 +6860 7145 7143 7144 6861 6859 7146 6861 7144 +6862 7145 6860 6862 7147 7145 7146 6863 6861 +7148 6863 7146 6864 7149 7150 7147 6864 7150 +6862 6864 7147 7149 7151 7150 7148 6865 6863 +7152 6865 7148 6866 7149 6864 6866 6867 7149 +6867 7151 7149 6867 7153 7151 7152 6868 6865 +7154 6868 7152 6869 7153 6867 6869 7155 7153 +7154 6870 6868 7156 6870 7154 6871 7155 6869 +6871 7157 7155 7156 6872 6870 7158 6872 7156 +6873 7157 6871 6873 7159 7157 7160 6875 6874 +7161 7160 6874 6872 7161 6874 7158 7161 6872 +6877 7159 6873 6877 7162 7159 7163 7164 7160 +7164 6875 7160 7164 6878 6875 7163 7160 7161 +7165 7166 7167 7166 7162 7167 7162 6879 7167 +7162 6877 6879 7164 6880 6878 7168 6880 7164 +7165 7167 6882 6881 7165 6882 7169 7165 6881 +6882 7167 6879 7168 6883 6880 7170 6883 7168 +6886 7171 6887 7172 6888 6887 7172 6887 7171 +7173 6889 6888 7173 6888 7172 7174 6890 6889 +7174 6889 7173 7175 6891 6890 7175 6890 7174 +7176 6892 6891 7176 6891 7175 7176 6893 6892 +6894 7169 6881 6894 7177 7169 7170 6895 6883 +7178 6895 7170 6898 7179 6899 6899 7179 7180 +7181 6899 7180 6900 6899 7181 6900 7182 6901 +7181 7182 6900 6901 7183 6902 7182 7183 6901 +6886 6902 7171 6902 7184 7171 6902 7183 7184 +7171 7184 7172 7172 7184 7185 7172 7185 7173 +7173 7185 7186 7173 7186 7174 7174 7186 7187 +7174 7187 7175 7175 7187 7188 7175 7188 7176 +7176 7188 7189 7189 7190 7191 7189 7191 6903 +7176 7189 6903 6893 7176 6903 7191 6904 6903 +6906 7177 6894 6906 7192 7177 7178 7193 6895 +6895 7193 6907 6908 6907 7193 6908 7193 7194 +6909 7195 6910 6910 7195 7196 7197 6910 7196 +6911 6910 7197 6911 7198 6912 7197 7198 6911 +6912 7199 6913 7198 7199 6912 6898 6913 7179 +6913 7200 7179 6913 7199 7200 7179 7200 7180 +7190 7201 7191 7191 7201 6904 7201 6914 6904 +7201 7202 6914 6914 7203 6915 7202 7203 6914 +6915 7204 6916 7203 7204 6915 6916 7205 6917 +7204 7205 6916 7205 7206 7207 6918 7205 7207 +6917 7205 6918 7208 6919 6918 7208 6918 7207 +6919 7208 7209 7210 6919 7209 6920 6919 7210 +6906 6920 7192 7192 6920 7210 6921 6908 7211 +6908 7212 7211 6908 7194 7212 7211 6922 6921 +7213 7214 6924 7213 6924 6923 6909 6924 7195 +6924 7215 7195 6924 7214 7215 7195 7215 7196 +7206 7216 7207 7207 7216 7208 7208 7216 7217 +7208 7217 7209 7212 7218 7211 7211 7218 6922 +7218 6925 6922 7218 7219 6925 6925 7220 6926 +7219 7220 6925 6926 7221 6927 7220 7221 6926 +6923 6927 7213 7213 6927 7221 7222 7223 7224 +7225 7226 7224 7224 7226 7222 7227 7228 7225 +7225 7228 7226 7229 7230 7227 7227 7230 7228 +7231 7232 7229 7229 7232 7230 7233 7234 7231 +7231 7234 7232 7235 7236 7233 7233 7236 7234 +7237 7238 7235 7235 7238 7236 7239 7240 7237 +7237 7240 7238 7241 7242 7239 7239 7242 7240 +7241 7243 7242 7244 7245 7246 7244 7246 7223 +7246 7224 7223 7246 7247 7224 7224 7248 7225 +7224 7247 7248 7225 7249 7227 7225 7248 7249 +7227 7250 7229 7227 7249 7250 7229 7251 7231 +7229 7250 7251 7231 7252 7233 7231 7251 7252 +7233 7253 7235 7233 7252 7253 7235 7254 7237 +7235 7253 7254 7237 7255 7239 7237 7254 7255 +7239 7256 7241 7239 7255 7256 7243 7241 7257 +7257 7241 7256 7246 7245 7258 7258 7245 7259 +7246 7260 7247 7246 7258 7260 7247 7261 7248 +7247 7260 7261 7248 7262 7249 7248 7261 7262 +7249 7263 7250 7249 7262 7263 7250 7264 7251 +7250 7263 7264 7251 7265 7252 7251 7264 7265 +7266 7253 7267 7253 7265 7267 7253 7252 7265 +7268 7269 7266 7269 7253 7266 7269 7254 7253 +7270 7255 7271 7255 7269 7271 7255 7254 7269 +7255 7270 7256 7256 7270 7272 7256 7272 7257 +7259 7273 7258 7274 7275 7273 7275 7258 7273 +7275 7260 7258 7260 7276 7261 7260 7275 7276 +7261 7277 7262 7261 7276 7277 7262 7278 7263 +7262 7277 7278 7279 7264 7280 7264 7278 7280 +7264 7263 7278 7264 7279 7265 7265 7279 7281 +7265 7281 7267 7268 7282 7269 7269 7282 7271 +7274 7283 7275 7275 7283 7276 7276 7283 7284 +7276 7284 7277 7277 7284 7285 7277 7285 7278 +7278 7285 7286 7278 7286 7280 7287 7288 7289 +7290 7291 7289 7289 7291 7287 7292 7293 7290 +7290 7293 7291 7292 7294 7293 7295 7296 7297 +7295 7297 7288 7297 7289 7288 7297 7298 7289 +7289 7299 7290 7289 7298 7299 7290 7300 7292 +7290 7299 7300 7294 7292 7301 7292 7302 7301 +7292 7300 7302 7302 7303 7301 7304 7305 7306 +6929 6928 7306 6928 7304 7306 6928 7307 7304 +7308 6928 6932 7308 7307 6928 6934 7297 7296 +7308 6934 7296 6932 6934 7308 7297 6934 6935 +7297 6935 7298 7298 6935 6936 7298 6936 7299 +7299 6936 6938 7299 6938 7300 7300 6938 6939 +7309 7300 6939 7302 7300 7309 7303 7302 7310 +7310 7302 7309 7306 7305 6940 7305 6941 6940 +7305 7311 6941 7306 6940 6929 6943 7309 6939 +7312 7313 6944 6944 7313 6943 7313 7310 6943 +7310 7309 6943 7312 7314 7313 7311 6945 6941 +7315 6945 7311 6947 7312 6944 7316 7317 6948 +6948 7317 6947 7317 7314 6947 7314 7312 6947 +7316 7318 7317 7319 7320 6949 6949 7320 6950 +6949 6945 7315 6949 7315 7319 6952 7316 6948 +7321 7322 6954 6954 7322 6952 7322 7318 6952 +7318 7316 6952 7321 7323 7322 7320 6955 6950 +7324 6955 7320 6957 7321 6954 7325 7326 6958 +6958 7326 6957 7326 7323 6957 7323 7321 6957 +7327 7328 7325 7325 7328 7326 7327 7329 7328 +7324 6960 6955 7330 6960 7324 6961 7325 6958 +7325 6961 6963 7325 6963 7327 7331 7332 6964 +6964 7332 6963 7332 7329 6963 7329 7327 6963 +7333 7334 7331 7331 7334 7332 7333 7335 7334 +7330 6966 6960 7336 6966 7330 6968 7331 6964 +7331 6968 6970 7331 6970 7333 7337 7338 6971 +6971 7338 6970 7338 7335 6970 7335 7333 6970 +7339 7340 7337 7337 7340 7338 7339 7341 7340 +7342 6973 6972 7343 7342 6972 6966 7343 6972 +7336 7343 6966 6975 7337 6971 7337 6975 6977 +7337 6977 7339 6977 6978 7339 6978 7341 7339 +6978 7344 7341 7345 6980 7342 7342 6980 6973 +7342 7343 7346 7342 7346 7345 7347 7344 6978 +7347 6978 6981 7348 7349 6982 7349 6981 6982 +7349 7347 6981 7350 7351 7348 7348 7351 7349 +7350 7352 7351 7353 6985 7345 7345 6985 6980 +7345 7346 7354 7345 7354 7353 6986 7348 6982 +7348 6986 6988 7348 6988 7350 7355 7356 6989 +6989 7356 6988 7356 7352 6988 7352 7350 6988 +7355 7357 7356 7358 7359 7353 7359 6985 7353 +7359 6991 6985 7358 7353 7354 6992 7355 6989 +7360 7357 7355 6992 7360 7355 6994 7360 6992 +6994 6995 7360 7360 6995 7361 7362 6997 6996 +6991 7359 7363 7362 6991 7363 7364 7362 7363 +6996 6991 7362 7365 7364 7363 6998 7366 7367 +7361 6998 7367 6995 6998 7361 7366 7368 7367 +7362 7000 6997 7000 7362 7001 7362 7369 7001 +7362 7364 7369 7364 7365 7370 7371 7364 7370 +7369 7364 7371 7372 7373 7371 7372 7371 7370 +7374 7375 7373 7374 7373 7372 7376 7377 7375 +7376 7375 7374 7378 7377 7376 7379 7380 7366 +6998 7379 7366 7002 7379 6998 7366 7380 7368 +7369 7003 7001 7003 7369 7004 7004 7369 7371 +7004 7371 7005 7005 7371 7373 7005 7373 7006 +7006 7373 7375 7006 7375 7007 7007 7375 7377 +7378 7381 7377 7381 7007 7377 7381 7009 7007 +7009 7382 7011 7381 7382 7009 7383 7384 7013 +7011 7383 7013 7382 7383 7011 7385 7384 7383 +7014 7379 7002 7014 7386 7379 7384 7015 7013 +7385 7387 7384 7387 7015 7384 7387 7016 7015 +7017 7016 7387 7017 7387 7388 7389 7390 7391 +7390 7386 7391 7386 7019 7391 7386 7014 7019 +7388 7392 7017 7017 7392 7020 7020 7393 7021 +7392 7393 7020 7394 7395 7023 7021 7394 7023 +7393 7394 7021 7396 7397 7395 7396 7395 7394 +7398 7399 7397 7398 7397 7396 7400 7399 7398 +7401 7402 7403 7403 7402 7389 7391 7403 7389 +7404 7403 7391 7025 7404 7019 7019 7404 7391 +7395 7026 7023 7026 7395 7027 7027 7395 7397 +7027 7397 7028 7028 7397 7399 7400 7405 7399 +7405 7028 7399 7405 7030 7028 7406 7407 7032 +7030 7406 7032 7405 7406 7030 7408 7409 7407 +7408 7407 7406 7410 7411 7409 7410 7409 7408 +7034 7033 7411 7410 7034 7411 7412 7034 7410 +7413 7414 7036 7034 7413 7036 7412 7413 7034 +7415 7416 7414 7415 7414 7413 7417 7418 7416 +7417 7416 7415 7418 7417 7401 7418 7401 7403 +7037 7418 7403 7038 7037 7403 7038 7403 7040 +7040 7403 7404 7040 7404 7025 7407 7042 7032 +7042 7407 7043 7043 7407 7409 7043 7409 7044 +7044 7409 7411 7044 7411 7033 7414 7045 7036 +7045 7414 7047 7047 7414 7416 7047 7416 7049 +7049 7416 7418 7049 7418 7037 7419 7055 7056 +7419 7056 7420 7056 7421 7420 7056 7057 7421 +7057 7059 7421 7421 7059 7422 7423 7061 7060 +7423 7060 7424 7060 7425 7424 7060 7062 7425 +7426 7062 7063 7426 7425 7062 7427 7063 7064 +7427 7426 7063 7428 7064 7065 7428 7427 7064 +7429 7065 7066 7429 7428 7065 7430 7429 7431 +7429 7067 7431 7429 7066 7067 7068 7431 7067 +7432 7433 7069 7069 7433 7070 7434 7069 7071 +7434 7432 7069 7435 7071 7072 7435 7434 7071 +7436 7072 7073 7436 7435 7072 7437 7073 7074 +7437 7436 7073 7438 7074 7075 7438 7437 7074 +7439 7438 7440 7438 7076 7440 7438 7075 7076 +7077 7440 7076 7441 7080 7079 7081 7442 7079 +7079 7442 7441 7442 7081 7443 7081 7444 7443 +7081 7082 7444 7082 7055 7419 7445 7082 7419 +7444 7082 7445 7445 7419 7420 7446 7422 7059 +7446 7059 7083 7083 7061 7423 7447 7083 7423 +7446 7083 7447 7447 7423 7424 7448 7430 7431 +7431 7068 7448 7068 7449 7448 7068 7085 7449 +7450 7451 7086 7086 7451 7087 7086 7070 7433 +7086 7433 7450 7452 7439 7440 7452 7440 7077 +7088 7452 7077 7453 7452 7088 7454 7088 7089 +7454 7453 7088 7455 7454 7456 7454 7090 7456 +7454 7089 7090 7091 7456 7090 7457 7458 7441 +7458 7080 7441 7458 7094 7080 7457 7441 7442 +7457 7442 7459 7459 7442 7443 7460 7449 7085 +7460 7085 7095 7095 7096 7460 7460 7096 7461 +7451 7097 7087 7462 7097 7451 7463 7455 7456 +7463 7456 7091 7098 7463 7091 7464 7463 7098 +7465 7098 7099 7465 7464 7098 7466 7099 7100 +7466 7465 7099 7467 7100 7101 7467 7466 7100 +7468 7101 7102 7468 7467 7101 7102 7094 7458 +7102 7458 7468 7103 7461 7096 7103 7469 7461 +7462 7104 7097 7470 7104 7462 7105 7469 7103 +7105 7471 7469 7472 7473 7106 7106 7473 7107 +7106 7104 7470 7106 7470 7472 7108 7471 7105 +7108 7474 7471 7109 7107 7475 7107 7476 7475 +7107 7473 7476 7475 7110 7109 7477 7478 7112 +7477 7112 7111 7108 7112 7474 7474 7112 7478 +7476 7479 7475 7110 7475 7479 7480 7110 7479 +7114 7110 7480 7115 7477 7111 7115 7481 7477 +7480 7116 7114 7482 7116 7480 7117 7481 7115 +7117 7483 7481 7482 7118 7116 7484 7118 7482 +7119 7483 7117 7119 7485 7483 7484 7486 7118 +7118 7486 7120 7121 7120 7486 7121 7486 7487 +7122 7485 7119 7122 7488 7485 7487 7123 7121 +7489 7123 7487 7124 7488 7122 7124 7490 7488 +7489 7125 7123 7491 7125 7489 7126 7490 7124 +7126 7492 7490 7491 7493 7125 7125 7493 7128 +7129 7128 7493 7129 7493 7494 7130 7492 7126 +7130 7495 7492 7494 7131 7129 7496 7131 7494 +7132 7495 7130 7132 7497 7495 7498 7499 7133 +7133 7499 7134 7133 7131 7496 7133 7496 7498 +7136 7497 7132 7136 7500 7497 7499 7137 7134 +7501 7137 7499 7138 7500 7136 7138 7502 7500 +7501 7139 7137 7503 7139 7501 7504 7505 7141 +7504 7141 7140 7138 7141 7502 7502 7141 7505 +7503 7142 7139 7506 7142 7503 7143 7504 7140 +7143 7507 7504 7506 7144 7142 7508 7144 7506 +7145 7507 7143 7145 7509 7507 7508 7146 7144 +7510 7146 7508 7147 7509 7145 7147 7511 7509 +7510 7148 7146 7512 7148 7510 7513 7511 7147 +7513 7147 7150 7150 7151 7513 7513 7151 7514 +7512 7152 7148 7515 7152 7512 7153 7514 7151 +7153 7516 7514 7515 7154 7152 7517 7154 7515 +7155 7516 7153 7155 7518 7516 7517 7156 7154 +7519 7156 7517 7157 7518 7155 7157 7520 7518 +7521 7158 7522 7158 7519 7522 7158 7156 7519 +7523 7521 7522 7159 7520 7157 7159 7524 7520 +7525 7526 7521 7526 7158 7521 7526 7161 7158 +7525 7521 7523 7162 7524 7159 7162 7527 7524 +7528 7164 7163 7529 7528 7163 7161 7529 7163 +7526 7529 7161 7530 7531 7166 7530 7166 7165 +7162 7166 7527 7527 7166 7531 7532 7533 7528 +7533 7164 7528 7533 7168 7164 7532 7528 7529 +7169 7530 7165 7169 7534 7530 7533 7170 7168 +7535 7170 7533 7177 7534 7169 7177 7536 7534 +7537 7178 7538 7178 7535 7538 7178 7170 7535 +7539 7537 7538 7180 7540 7181 7181 7540 7541 +7542 7181 7541 7182 7181 7542 7182 7543 7183 +7542 7543 7182 7183 7544 7184 7543 7544 7183 +7184 7545 7185 7544 7545 7184 7185 7546 7186 +7545 7546 7185 7186 7547 7187 7546 7547 7186 +7187 7548 7188 7547 7548 7187 7548 7549 7550 +7189 7548 7550 7188 7548 7189 7550 7190 7189 +7192 7536 7177 7192 7551 7536 7537 7193 7178 +7194 7193 7537 7539 7194 7537 7552 7194 7539 +7196 7553 7197 7197 7553 7554 7555 7197 7554 +7198 7197 7555 7555 7556 7557 7199 7555 7557 +7198 7555 7199 7199 7557 7558 7559 7199 7558 +7200 7199 7559 7180 7200 7540 7200 7560 7540 +7200 7559 7560 7540 7560 7541 7549 7561 7550 +7550 7561 7190 7561 7201 7190 7561 7562 7201 +7201 7563 7202 7562 7563 7201 7202 7564 7203 +7563 7564 7202 7203 7565 7204 7564 7565 7203 +7204 7566 7205 7565 7566 7204 7206 7205 7566 +7206 7566 7567 7568 7569 7210 7568 7210 7209 +7192 7210 7551 7551 7210 7569 7552 7212 7194 +7570 7212 7552 7213 7571 7214 7214 7571 7572 +7573 7214 7572 7215 7214 7573 7196 7215 7553 +7215 7574 7553 7215 7573 7574 7553 7574 7554 +7556 7575 7557 7557 7575 7558 7567 7576 7206 +7206 7576 7216 7216 7577 7217 7576 7577 7216 +7209 7217 7568 7568 7217 7577 7570 7578 7212 +7212 7578 7218 7218 7579 7219 7578 7579 7218 +7579 7580 7581 7220 7579 7581 7219 7579 7220 +7220 7581 7582 7583 7220 7582 7221 7220 7583 +7213 7221 7571 7221 7584 7571 7221 7583 7584 +7571 7584 7572 7580 7585 7581 7581 7585 7582 +7586 7587 7588 7588 7589 7586 7590 7591 7592 +7593 7594 7592 7592 7594 7590 7595 7596 7593 +7593 7596 7594 7597 7598 7595 7595 7598 7596 +7599 7600 7597 7597 7600 7598 7601 7602 7599 +7599 7602 7600 7603 7604 7601 7601 7604 7602 +7604 7603 7587 7603 7588 7587 7603 7605 7588 +7589 7588 7606 7588 7607 7606 7588 7605 7607 +7608 7609 7607 7607 7609 7606 7610 7611 7608 +7608 7611 7609 7612 7613 7610 7610 7613 7611 +7614 7615 7612 7612 7615 7613 7616 7617 7614 +7614 7617 7615 7618 7619 7616 7616 7619 7617 +7620 7621 7618 7618 7621 7619 7622 7623 7620 +7620 7623 7621 7622 7624 7623 7625 7626 7627 +7625 7627 7591 7627 7592 7591 7627 7628 7592 +7628 7223 7222 7593 7628 7222 7592 7628 7593 +7593 7222 7226 7593 7226 7595 7595 7226 7228 +7595 7228 7597 7597 7228 7230 7597 7230 7599 +7599 7230 7232 7599 7232 7601 7601 7232 7234 +7601 7234 7603 7603 7234 7236 7603 7236 7605 +7605 7236 7238 7605 7238 7607 7607 7238 7240 +7607 7240 7608 7608 7240 7242 7608 7242 7610 +7610 7242 7243 7629 7610 7243 7612 7610 7629 +7612 7630 7614 7612 7629 7630 7614 7631 7616 +7614 7630 7631 7616 7632 7618 7616 7631 7632 +7618 7633 7620 7618 7632 7633 7620 7634 7622 +7620 7633 7634 7624 7622 7635 7622 7636 7635 +7622 7634 7636 7636 7637 7635 7638 7639 7640 +7638 7640 7626 7640 7627 7626 7640 7641 7627 +7641 7245 7244 7628 7641 7244 7627 7641 7628 +7628 7244 7223 7257 7642 7629 7257 7629 7243 +7629 7643 7630 7629 7642 7643 7630 7644 7631 +7630 7643 7644 7631 7645 7632 7631 7644 7645 +7632 7646 7633 7632 7645 7646 7633 7647 7634 +7633 7646 7647 7634 7648 7636 7634 7647 7648 +7637 7636 7649 7649 7636 7648 7640 7639 7650 +7650 7639 7651 7640 7652 7641 7640 7650 7652 +7652 7259 7245 7652 7245 7641 7267 7653 7266 +7653 7268 7266 7271 7654 7270 7655 7272 7270 +7655 7270 7654 7642 7257 7272 7655 7642 7272 +7656 7642 7655 7642 7657 7643 7642 7656 7657 +7643 7658 7644 7643 7657 7658 7644 7659 7645 +7644 7658 7659 7645 7660 7646 7645 7659 7660 +7646 7661 7647 7646 7660 7661 7662 7648 7663 +7648 7661 7663 7648 7647 7661 7662 7664 7648 +7664 7665 7648 7666 7649 7648 7666 7648 7665 +7665 7667 7666 7650 7651 7668 7668 7651 7669 +7650 7670 7652 7650 7668 7670 7671 7273 7259 +7652 7671 7259 7670 7671 7652 7671 7274 7273 +7280 7672 7279 7673 7281 7279 7673 7279 7672 +7653 7267 7281 7673 7653 7281 7674 7653 7673 +7675 7282 7268 7653 7675 7268 7674 7675 7653 +7654 7271 7282 7675 7654 7282 7676 7654 7675 +7654 7677 7655 7654 7676 7677 7655 7678 7656 +7655 7677 7678 7656 7679 7657 7656 7678 7679 +7657 7680 7658 7657 7679 7680 7658 7681 7659 +7658 7680 7681 7659 7682 7660 7659 7681 7682 +7683 7661 7684 7661 7682 7684 7661 7660 7682 +7661 7683 7663 7664 7685 7665 7665 7685 7667 +7686 7687 7688 7686 7688 7669 7688 7668 7669 +7688 7689 7668 7668 7690 7670 7668 7689 7690 +7670 7691 7671 7670 7690 7691 7692 7283 7274 +7671 7692 7274 7691 7692 7671 7693 7284 7283 +7693 7283 7692 7694 7285 7284 7694 7284 7693 +7695 7286 7285 7695 7285 7694 7672 7280 7286 +7695 7672 7286 7696 7672 7695 7672 7697 7673 +7672 7696 7697 7673 7698 7674 7673 7697 7698 +7674 7699 7675 7674 7698 7699 7675 7700 7676 +7675 7699 7700 7676 7701 7677 7676 7700 7701 +7677 7702 7678 7677 7701 7702 7703 7679 7704 +7679 7702 7704 7679 7678 7702 7679 7703 7680 +7680 7703 7705 7680 7705 7681 7681 7705 7706 +7681 7706 7682 7682 7706 7707 7682 7707 7684 +7687 7708 7688 7709 7710 7708 7710 7688 7708 +7710 7689 7688 7689 7711 7690 7689 7710 7711 +7690 7712 7691 7690 7711 7712 7691 7713 7692 +7691 7712 7713 7692 7714 7693 7692 7713 7714 +7693 7715 7694 7693 7714 7715 7694 7716 7695 +7694 7715 7716 7695 7717 7696 7695 7716 7717 +7696 7718 7697 7696 7717 7718 7697 7719 7698 +7697 7718 7719 7698 7720 7699 7698 7719 7720 +7721 7700 7722 7700 7720 7722 7700 7699 7720 +7700 7721 7701 7701 7721 7723 7701 7723 7702 +7702 7723 7724 7702 7724 7704 7709 7725 7710 +7710 7725 7711 7711 7725 7726 7727 7728 7726 +7728 7711 7726 7728 7712 7711 7712 7729 7713 +7712 7728 7729 7713 7730 7714 7713 7729 7730 +7714 7731 7715 7714 7730 7731 7715 7732 7716 +7715 7731 7732 7716 7733 7717 7716 7732 7733 +7717 7734 7718 7717 7733 7734 7718 7735 7719 +7718 7734 7735 7736 7720 7737 7720 7735 7737 +7720 7719 7735 7720 7736 7722 7727 7738 7728 +7728 7738 7729 7729 7738 7739 7729 7739 7730 +7730 7739 7740 7730 7740 7731 7731 7740 7741 +7731 7741 7732 7732 7741 7742 7743 7744 7742 +7744 7732 7742 7744 7733 7732 7733 7745 7734 +7733 7744 7745 7746 7735 7747 7735 7745 7747 +7735 7734 7745 7735 7746 7737 7744 7743 7748 +7748 7743 7749 7744 7750 7745 7744 7748 7750 +7747 7745 7751 7751 7745 7750 7748 7749 7752 +7752 7749 7753 7748 7754 7750 7748 7752 7754 +7751 7750 7755 7755 7750 7754 7756 7757 7758 +7756 7758 7753 7758 7752 7753 7758 7759 7752 +7752 7760 7754 7752 7759 7760 7755 7754 7761 +7754 7762 7761 7754 7760 7762 7762 7763 7761 +7764 7765 7766 7764 7766 7757 7766 7758 7757 +7766 7767 7758 7758 7768 7759 7758 7767 7768 +7759 7769 7760 7759 7768 7769 7760 7770 7762 +7760 7769 7770 7763 7762 7771 7771 7762 7770 +7766 7765 7772 7772 7765 7773 7766 7774 7767 +7766 7772 7774 7767 7775 7768 7767 7774 7775 +7768 7776 7769 7768 7775 7776 7769 7777 7770 +7769 7776 7777 7771 7770 7778 7770 7779 7778 +7770 7777 7779 7779 7780 7778 7772 7773 7781 +7781 7773 7782 7781 7288 7287 7774 7781 7287 +7772 7781 7774 7774 7287 7291 7774 7291 7775 +7775 7291 7293 7775 7293 7776 7776 7293 7294 +7783 7776 7294 7777 7776 7783 7777 7784 7779 +7777 7783 7784 7780 7779 7785 7779 7786 7785 +7779 7784 7786 7786 7787 7785 7788 7789 7790 +7788 7790 7782 7782 7790 7781 7790 7296 7781 +7296 7295 7781 7781 7295 7288 7301 7783 7294 +7783 7301 7303 7791 7783 7303 7784 7783 7791 +7784 7792 7786 7784 7791 7792 7787 7786 7793 +7793 7786 7792 7794 7795 7304 7304 7795 7305 +7796 7304 7307 7796 7794 7304 7308 7790 7789 +7796 7308 7789 7307 7308 7796 7790 7308 7296 +7310 7797 7791 7310 7791 7303 7791 7798 7792 +7791 7797 7798 7793 7792 7799 7799 7792 7798 +7800 7801 7802 7795 7800 7802 7311 7795 7802 +7305 7795 7311 7313 7797 7310 7797 7313 7314 +7803 7797 7314 7798 7797 7803 7799 7798 7804 +7798 7805 7804 7798 7803 7805 7806 7807 7805 +7805 7807 7804 7806 7808 7807 7802 7801 7809 +7809 7801 7810 7809 7315 7311 7809 7311 7802 +7317 7803 7314 7803 7317 7318 7811 7803 7318 +7805 7803 7811 7805 7812 7806 7805 7811 7812 +7808 7806 7813 7806 7814 7813 7806 7812 7814 +7814 7815 7813 7809 7810 7319 7810 7320 7319 +7810 7816 7320 7809 7319 7315 7322 7811 7318 +7811 7322 7323 7817 7811 7323 7812 7811 7817 +7812 7818 7814 7812 7817 7818 7815 7814 7819 +7814 7820 7819 7814 7818 7820 7820 7821 7819 +7816 7324 7320 7822 7324 7816 7326 7817 7323 +7817 7326 7328 7817 7328 7818 7818 7328 7329 +7823 7818 7329 7820 7818 7823 7821 7820 7824 +7820 7825 7824 7820 7823 7825 7826 7827 7825 +7825 7827 7824 7826 7828 7827 7822 7330 7324 +7829 7330 7822 7332 7823 7329 7823 7332 7334 +7823 7334 7825 7825 7334 7335 7830 7825 7335 +7826 7825 7830 7828 7826 7831 7826 7832 7831 +7826 7830 7832 7832 7833 7831 7829 7336 7330 +7834 7336 7829 7338 7830 7335 7830 7338 7340 +7830 7340 7832 7835 7836 7341 7341 7836 7340 +7836 7833 7340 7833 7832 7340 7837 7838 7835 +7835 7838 7836 7839 7840 7837 7837 7840 7838 +7839 7841 7840 7842 7343 7843 7343 7834 7843 +7343 7336 7834 7844 7842 7843 7344 7845 7835 +7344 7835 7341 7835 7846 7837 7835 7845 7846 +7837 7847 7839 7837 7846 7847 7841 7839 7848 +7839 7849 7848 7839 7847 7849 7849 7850 7848 +7851 7346 7842 7842 7346 7343 7842 7844 7852 +7842 7852 7851 7347 7845 7344 7845 7347 7349 +7845 7349 7846 7846 7349 7351 7846 7351 7847 +7847 7351 7352 7853 7847 7352 7849 7847 7853 +7850 7849 7854 7849 7855 7854 7849 7853 7855 +7855 7856 7854 7857 7354 7851 7851 7354 7346 +7851 7852 7858 7851 7858 7857 7356 7853 7352 +7853 7356 7357 7859 7853 7357 7855 7853 7859 +7856 7855 7860 7860 7855 7859 7861 7359 7358 +7354 7857 7358 7857 7861 7358 7857 7862 7861 +7857 7858 7863 7857 7863 7862 7360 7859 7357 +7360 7361 7859 7361 7860 7859 7361 7864 7860 +7861 7363 7359 7363 7861 7365 7861 7865 7365 +7861 7862 7865 7862 7863 7866 7867 7862 7866 +7865 7862 7867 7868 7869 7867 7868 7867 7866 +7870 7871 7869 7870 7869 7868 7872 7871 7870 +7361 7367 7873 7874 7361 7873 7864 7361 7874 +7368 7873 7367 7865 7370 7365 7370 7865 7372 +7372 7865 7867 7372 7867 7374 7374 7867 7869 +7374 7869 7376 7376 7869 7871 7875 7876 7378 +7875 7378 7376 7872 7875 7376 7871 7872 7376 +7877 7878 7876 7877 7876 7875 7879 7880 7878 +7879 7878 7877 7881 7880 7879 7874 7873 7380 +7379 7874 7380 7882 7874 7379 7380 7873 7368 +7876 7381 7378 7381 7876 7382 7382 7876 7878 +7382 7878 7383 7383 7878 7880 7385 7383 7880 +7881 7385 7880 7883 7385 7881 7884 7885 7886 +7885 7882 7886 7882 7386 7886 7882 7379 7386 +7883 7887 7385 7385 7887 7387 7888 7889 7388 +7387 7888 7388 7887 7888 7387 7890 7891 7889 +7890 7889 7888 7892 7893 7891 7892 7891 7890 +7894 7895 7893 7894 7893 7892 7896 7895 7894 +7884 7886 7390 7389 7884 7390 7897 7884 7389 +7390 7886 7386 7889 7392 7388 7392 7889 7393 +7393 7889 7891 7393 7891 7394 7394 7891 7893 +7394 7893 7396 7396 7893 7895 7896 7898 7895 +7898 7396 7895 7898 7398 7396 7899 7900 7400 +7398 7899 7400 7898 7899 7398 7901 7902 7900 +7901 7900 7899 7903 7904 7902 7903 7902 7901 +7905 7906 7904 7905 7904 7903 7907 7908 7906 +7907 7906 7905 7909 7908 7907 7910 7911 7912 +7913 7914 7912 7913 7912 7911 7402 7401 7914 +7913 7402 7914 7915 7402 7913 7389 7402 7897 +7897 7402 7915 7900 7405 7400 7405 7900 7406 +7406 7900 7902 7406 7902 7408 7408 7902 7904 +7408 7904 7410 7410 7904 7906 7410 7906 7412 +7412 7906 7908 7909 7916 7908 7916 7412 7908 +7916 7413 7412 7912 7415 7910 7415 7916 7910 +7415 7413 7916 7415 7912 7417 7417 7912 7914 +7417 7914 7401 7917 7420 7421 7422 7917 7421 +7918 7424 7425 7918 7425 7919 7425 7920 7919 +7425 7426 7920 7921 7426 7427 7921 7920 7426 +7922 7427 7428 7922 7921 7427 7923 7428 7429 +7923 7922 7428 7429 7430 7923 7923 7430 7924 +7925 7433 7432 7434 7926 7432 7432 7926 7925 +7435 7927 7434 7434 7927 7926 7436 7928 7435 +7435 7928 7927 7437 7929 7436 7436 7929 7928 +7438 7930 7437 7437 7930 7929 7439 7930 7438 +7931 7443 7444 7931 7444 7932 7444 7933 7932 +7444 7445 7933 7933 7445 7420 7934 7933 7420 +7917 7934 7420 7933 7934 7935 7934 7917 7422 +7936 7934 7422 7446 7936 7422 7937 7934 7936 +7938 7446 7447 7938 7936 7446 7447 7424 7918 +7939 7447 7918 7938 7447 7939 7939 7918 7919 +7940 7924 7430 7940 7430 7448 7448 7449 7940 +7940 7449 7941 7942 7943 7450 7450 7943 7451 +7450 7433 7925 7944 7450 7925 7942 7450 7944 +7944 7925 7926 7944 7926 7945 7945 7926 7927 +7945 7927 7946 7946 7927 7928 7946 7928 7947 +7947 7928 7929 7947 7929 7948 7948 7929 7930 +7948 7930 7949 7949 7930 7439 7452 7949 7439 +7950 7949 7452 7951 7452 7453 7951 7950 7452 +7952 7453 7454 7952 7951 7453 7454 7455 7952 +7952 7455 7953 7954 7458 7457 7954 7457 7955 +7457 7956 7955 7457 7459 7956 7459 7443 7931 +7957 7459 7931 7956 7459 7957 7957 7931 7932 +7958 7935 7934 7958 7934 7937 7959 7941 7449 +7959 7449 7460 7460 7461 7959 7959 7461 7960 +7943 7462 7451 7961 7462 7943 7962 7953 7455 +7962 7455 7463 7963 7463 7464 7963 7962 7463 +7964 7464 7465 7964 7963 7464 7965 7465 7466 +7965 7964 7465 7966 7466 7467 7966 7965 7466 +7967 7467 7468 7967 7966 7467 7468 7458 7954 +7968 7468 7954 7967 7468 7968 7968 7954 7955 +7469 7960 7461 7469 7969 7960 7970 7971 7972 +7961 7970 7972 7470 7961 7972 7462 7961 7470 +7471 7969 7469 7471 7973 7969 7972 7971 7472 +7971 7473 7472 7971 7974 7473 7972 7472 7470 +7474 7973 7471 7474 7975 7973 7974 7476 7473 +7976 7476 7974 7977 7978 7478 7977 7478 7477 +7474 7478 7975 7975 7478 7978 7976 7979 7476 +7476 7979 7479 7480 7479 7979 7480 7979 7980 +7481 7977 7477 7481 7981 7977 7980 7482 7480 +7982 7482 7980 7483 7981 7481 7483 7983 7981 +7982 7484 7482 7984 7484 7982 7485 7983 7483 +7485 7985 7983 7984 7986 7484 7484 7986 7486 +7487 7486 7986 7487 7986 7987 7488 7985 7485 +7488 7988 7985 7987 7489 7487 7989 7489 7987 +7490 7988 7488 7490 7990 7988 7989 7491 7489 +7991 7491 7989 7492 7990 7490 7492 7992 7990 +7991 7993 7491 7491 7993 7493 7494 7493 7993 +7494 7993 7994 7495 7992 7492 7495 7995 7992 +7994 7496 7494 7996 7496 7994 7497 7995 7495 +7497 7997 7995 7998 7999 7498 7498 7999 7499 +7498 7496 7996 7498 7996 7998 7500 7997 7497 +7500 8000 7997 7999 7501 7499 8001 7501 7999 +8002 8003 8004 8003 8000 8004 8000 7502 8004 +8000 7500 7502 8001 7503 7501 8005 7503 8001 +8002 8004 7505 7504 8002 7505 8006 8002 7504 +7505 8004 7502 8005 7506 7503 8007 7506 8005 +7507 8006 7504 7507 8008 8006 8007 7508 7506 +8009 7508 8007 7509 8008 7507 7509 8010 8008 +8009 7510 7508 8011 7510 8009 7511 8010 7509 +7511 8012 8010 8011 7512 7510 8013 7512 8011 +8014 8012 7511 8014 7511 7513 7513 7514 8014 +8014 7514 8015 8013 7515 7512 8016 7515 8013 +7516 8015 7514 7516 8017 8015 8016 7517 7515 +8018 7517 8016 7518 8017 7516 7518 8019 8017 +8018 7519 7517 8020 7519 8018 7520 8019 7518 +7520 8021 8019 7522 7519 8022 7519 8023 8022 +7519 8020 8023 8022 7523 7522 7524 8021 7520 +7524 8024 8021 8022 8023 7525 8023 7526 7525 +8023 8025 7526 8022 7525 7523 7527 8026 8027 +8024 7527 8027 7524 7527 8024 8026 8028 8027 +8025 7529 7526 8029 7529 8025 7530 8030 7531 +8030 8031 7531 7531 8031 7527 8031 8032 7527 +8032 8026 7527 8026 8032 8028 8033 7533 7532 +8034 8033 7532 7529 8034 7532 8029 8034 7529 +7534 8035 8030 7534 8030 7530 8031 8030 8036 +8036 8030 8035 8037 7535 8033 8033 7535 7533 +8033 8034 8038 8033 8038 8037 7536 8039 8035 +7536 8035 7534 8036 8035 8040 8040 8035 8039 +8037 7538 7535 7539 7538 8037 8038 7539 8037 +8041 7539 8038 7541 8042 7542 8043 7543 7542 +8043 7542 8042 7543 8043 8044 8045 7543 8044 +7544 7543 8045 7544 8046 7545 8045 8046 7544 +7545 8047 7546 8046 8047 7545 7546 8048 7547 +8047 8048 7546 7547 8049 7548 8048 8049 7547 +7549 7548 8049 7549 8049 8050 8051 8052 8039 +7536 8051 8039 7551 8051 7536 8039 8052 8040 +8041 7552 7539 8053 7552 8041 7554 8054 7555 +8054 7556 7555 7558 8055 7559 7559 8055 8056 +8057 7559 8056 7560 7559 8057 7541 7560 8042 +7560 8058 8042 7560 8057 8058 8042 8058 8043 +8043 8058 8059 8043 8059 8044 8050 8060 7549 +7549 8060 7561 7561 8061 7562 8060 8061 7561 +7562 8062 7563 8061 8062 7562 7563 8063 7564 +8062 8063 7563 7564 8064 7565 8063 8064 7564 +8064 8065 8066 7566 8064 8066 7565 8064 7566 +8066 7567 7566 8067 8068 7569 8067 7569 7568 +7551 7569 8051 8051 7569 8068 8053 7570 7552 +8069 7570 8053 7572 8070 7573 7573 8070 8071 +8072 7573 8071 7574 7573 8072 7554 7574 8054 +7574 8073 8054 7574 8072 8073 8054 8073 7556 +8073 7575 7556 8073 8074 7575 7558 7575 8055 +7575 8075 8055 7575 8074 8075 8055 8075 8056 +8065 8076 8066 8066 8076 7567 8076 7576 7567 +8076 8077 7576 7576 8078 7577 8077 8078 7576 +7568 7577 8067 8067 7577 8078 8069 8079 7570 +7570 8079 7578 8079 8080 8081 7579 8079 8081 +7578 8079 7579 8081 7580 7579 7582 8082 7583 +7583 8082 8083 8084 7583 8083 7584 7583 8084 +7572 7584 8070 7584 8085 8070 7584 8084 8085 +8070 8085 8071 8080 8086 8081 8081 8086 7580 +8086 7585 7580 8086 8087 7585 7582 7585 8082 +7585 8088 8082 7585 8087 8088 8082 8088 8083 +8089 8090 8091 8092 8093 8091 8091 8093 8089 +8094 8095 8092 8092 8095 8093 8096 8097 8094 +8094 8097 8095 8098 8099 8096 8096 8099 8097 +8100 8101 8098 8098 8101 8099 8102 8103 8100 +8100 8103 8101 7587 7586 8102 7586 8103 8102 +7586 8104 8103 8105 8106 7589 8106 7586 7589 +8106 8104 7586 8107 8108 8105 8105 8108 8106 +8109 8110 8107 8107 8110 8108 8111 8112 8109 +8109 8112 8110 8113 8114 8111 8111 8114 8112 +8115 8116 8113 8113 8116 8114 8117 8118 8115 +8115 8118 8116 8119 8120 8117 8117 8120 8118 +8119 8121 8120 8122 8123 8124 8122 8124 8090 +8090 8124 8091 8124 7591 8091 7591 7590 8091 +8091 7590 7594 8091 7594 8092 8092 7594 7596 +8092 7596 8094 8094 7596 7598 8094 7598 8096 +8096 7598 7600 8096 7600 8098 8098 7600 7602 +8098 7602 8100 8100 7602 7604 8100 7604 8102 +8102 7604 7587 7606 8105 7589 8105 7606 7609 +8105 7609 8107 8107 7609 7611 8107 7611 8109 +8109 7611 7613 8109 7613 8111 8111 7613 7615 +8111 7615 8113 8113 7615 7617 8113 7617 8115 +8115 7617 7619 8115 7619 8117 8117 7619 7621 +8117 7621 8119 8125 8121 8119 7621 8125 8119 +7623 8125 7621 8126 8127 7624 8127 7623 7624 +8127 8125 7623 8126 8128 8127 8129 8130 8131 +8129 8131 8123 8123 8131 8124 8131 7626 8124 +7626 7625 8124 8124 7625 7591 7635 8126 7624 +8132 8133 7637 7637 8133 7635 8133 8128 7635 +8128 8126 7635 8134 8135 8132 8132 8135 8133 +8134 8136 8135 8131 8130 7638 8130 7639 7638 +8130 8137 7639 8131 7638 7626 7649 8138 8132 +7649 8132 7637 8132 8139 8134 8132 8138 8139 +8136 8134 8140 8134 8141 8140 8134 8139 8141 +8141 8142 8140 8143 8144 8145 8137 8143 8145 +7651 8137 8145 7639 8137 7651 7663 8146 7662 +7666 8138 7649 7664 7662 8146 8138 7666 7667 +8147 8138 7667 8139 8138 8147 8139 8148 8141 +8139 8147 8148 8142 8141 8149 8149 8141 8148 +8145 8144 8150 8150 8144 8151 8150 7669 7651 +8150 7651 8145 7684 8152 7683 8146 7663 7683 +8152 8146 7683 8153 8146 8152 8154 7685 7664 +8146 8154 7664 8153 8154 8146 8147 7667 7685 +8154 8147 7685 8155 8147 8154 8156 8148 8157 +8148 8155 8157 8148 8147 8155 8148 8156 8149 +8158 8159 8160 8158 8160 8151 8151 8160 8150 +8160 7687 8150 7687 7686 8150 8150 7686 7669 +7704 8161 7703 8162 7705 7703 8162 7703 8161 +8163 7706 7705 8163 7705 8162 8164 7707 7706 +8164 7706 8163 8152 7684 7707 8164 8152 7707 +8165 8152 8164 8152 8166 8153 8152 8165 8166 +8153 8167 8154 8153 8166 8167 8154 8168 8155 +8154 8167 8168 8157 8155 8169 8155 8170 8169 +8155 8168 8170 8170 8171 8169 8160 8159 8172 +8172 8159 8173 8174 7708 7687 8160 8174 7687 +8172 8174 8160 8174 7709 7708 7722 8175 7721 +8176 7723 7721 8176 7721 8175 8177 7724 7723 +8177 7723 8176 8161 7704 7724 8177 8161 7724 +8178 8161 8177 8161 8179 8162 8161 8178 8179 +8162 8180 8163 8162 8179 8180 8163 8181 8164 +8163 8180 8181 8164 8182 8165 8164 8181 8182 +8165 8183 8166 8165 8182 8183 8166 8184 8167 +8166 8183 8184 8167 8185 8168 8167 8184 8185 +8168 8186 8170 8168 8185 8186 8171 8170 8187 +8187 8170 8186 8172 8173 8188 8188 8173 8189 +8172 8190 8174 8172 8188 8190 8191 7725 7709 +8174 8191 7709 8190 8191 8174 8192 7726 7725 +8192 7725 8191 8192 7727 7726 7737 8193 7736 +8175 7722 7736 8193 8175 7736 8194 8175 8193 +8175 8195 8176 8175 8194 8195 8176 8196 8177 +8176 8195 8196 8177 8197 8178 8177 8196 8197 +8178 8198 8179 8178 8197 8198 8179 8199 8180 +8179 8198 8199 8180 8200 8181 8180 8199 8200 +8181 8201 8182 8181 8200 8201 8182 8202 8183 +8182 8201 8202 8183 8203 8184 8183 8202 8203 +8184 8204 8185 8184 8203 8204 8185 8205 8186 +8185 8204 8205 8187 8186 8206 8186 8207 8206 +8186 8205 8207 8207 8208 8206 8188 8189 8209 +8209 8189 8210 8188 8211 8190 8188 8209 8211 +8190 8212 8191 8190 8211 8212 8191 8213 8192 +8191 8212 8213 8214 7738 7727 8192 8214 7727 +8213 8214 8192 8215 7739 7738 8215 7738 8214 +8216 7740 7739 8216 7739 8215 8217 7741 7740 +8217 7740 8216 8218 7742 7741 8218 7741 8217 +8218 7743 7742 7747 8219 7746 8193 7737 7746 +8219 8193 7746 8220 8193 8219 8193 8221 8194 +8193 8220 8221 8194 8222 8195 8194 8221 8222 +8195 8223 8196 8195 8222 8223 8196 8224 8197 +8196 8223 8224 8197 8225 8198 8197 8224 8225 +8198 8226 8199 8198 8225 8226 8199 8227 8200 +8199 8226 8227 8200 8228 8201 8200 8227 8228 +8201 8229 8202 8201 8228 8229 8202 8230 8203 +8202 8229 8230 8203 8231 8204 8203 8230 8231 +8204 8232 8205 8204 8231 8232 8205 8233 8207 +8205 8232 8233 8208 8207 8234 8234 8207 8233 +8235 8236 8237 8235 8237 8210 8237 8209 8210 +8237 8238 8209 8209 8239 8211 8209 8238 8239 +8211 8240 8212 8211 8239 8240 8212 8241 8213 +8212 8240 8241 8213 8242 8214 8213 8241 8242 +8214 8243 8215 8214 8242 8243 8215 8244 8216 +8215 8243 8244 8216 8245 8217 8216 8244 8245 +8217 8246 8218 8217 8245 8246 8246 7749 7743 +8246 7743 8218 7751 8247 8219 7751 8219 7747 +8219 8248 8220 8219 8247 8248 8220 8249 8221 +8220 8248 8249 8221 8250 8222 8221 8249 8250 +8222 8251 8223 8222 8250 8251 8223 8252 8224 +8223 8251 8252 8224 8253 8225 8224 8252 8253 +8225 8254 8226 8225 8253 8254 8226 8255 8227 +8226 8254 8255 8227 8256 8228 8227 8255 8256 +8228 8257 8229 8228 8256 8257 8229 8258 8230 +8229 8257 8258 8230 8259 8231 8230 8258 8259 +8231 8260 8232 8231 8259 8260 8261 8233 8262 +8233 8260 8262 8233 8232 8260 8233 8261 8234 +8236 8263 8237 8237 8263 8238 8238 8263 8264 +8265 8266 8264 8266 8238 8264 8266 8239 8238 +8239 8267 8240 8239 8266 8267 8240 8268 8241 +8240 8267 8268 8241 8269 8242 8241 8268 8269 +8242 8270 8243 8242 8269 8270 8243 8271 8244 +8243 8270 8271 8244 8272 8245 8244 8271 8272 +8245 8273 8246 8245 8272 8273 8273 7753 7749 +8273 7749 8246 7755 8274 8247 7755 8247 7751 +8247 8275 8248 8247 8274 8275 8248 8276 8249 +8248 8275 8276 8249 8277 8250 8249 8276 8277 +8250 8278 8251 8250 8277 8278 8251 8279 8252 +8251 8278 8279 8252 8280 8253 8252 8279 8280 +8253 8281 8254 8253 8280 8281 8254 8282 8255 +8254 8281 8282 8255 8283 8256 8255 8282 8283 +8256 8284 8257 8256 8283 8284 8257 8285 8258 +8257 8284 8285 8286 8259 8287 8259 8285 8287 +8259 8258 8285 8259 8286 8260 8260 8286 8288 +8260 8288 8262 8289 8290 8291 8292 8293 8291 +8293 8266 8291 8265 8289 8291 8265 8291 8266 +8294 8295 8293 8295 8266 8293 8295 8267 8266 +8267 8296 8268 8267 8295 8296 8268 8297 8269 +8268 8296 8297 8269 8298 8270 8269 8297 8298 +8270 8299 8271 8270 8298 8299 8271 8300 8272 +8271 8299 8300 8300 7757 7756 8273 8300 7756 +8272 8300 8273 8273 7756 7753 7761 8274 7755 +8274 7761 7763 8301 8274 7763 8275 8274 8301 +8275 8302 8276 8275 8301 8302 8276 8303 8277 +8276 8302 8303 8277 8304 8278 8277 8303 8304 +8278 8305 8279 8278 8304 8305 8279 8306 8280 +8279 8305 8306 8280 8307 8281 8280 8306 8307 +8281 8308 8282 8281 8307 8308 8282 8309 8283 +8282 8308 8309 8283 8310 8284 8283 8309 8310 +8284 8311 8285 8284 8310 8311 8287 8285 8312 +8312 8285 8311 8291 8290 8313 8313 8290 8314 +8292 8291 8315 8291 8316 8315 8291 8313 8316 +8317 8318 8316 8318 8295 8316 8294 8315 8316 +8294 8316 8295 8295 8318 8296 8296 8318 8319 +8320 8321 8319 8321 8296 8319 8321 8297 8296 +8297 8322 8298 8297 8321 8322 8298 8323 8299 +8298 8322 8323 8323 7765 7764 8300 8323 7764 +8299 8323 8300 8300 7764 7757 7771 8324 8301 +7771 8301 7763 8301 8325 8302 8301 8324 8325 +8302 8326 8303 8302 8325 8326 8303 8327 8304 +8303 8326 8327 8304 8328 8305 8304 8327 8328 +8305 8329 8306 8305 8328 8329 8306 8330 8307 +8306 8329 8330 8307 8331 8308 8307 8330 8331 +8308 8332 8309 8308 8331 8332 8309 8333 8310 +8309 8332 8333 8310 8334 8311 8310 8333 8334 +8312 8311 8335 8335 8311 8334 8314 8336 8313 +8337 8338 8336 8338 8313 8336 8338 8316 8313 +8317 8316 8339 8316 8340 8339 8316 8338 8340 +8340 8341 8339 8320 8342 8321 8343 8344 8342 +8344 8321 8342 8344 8322 8321 8322 8345 8323 +8322 8344 8345 8345 7773 7765 8345 7765 8323 +7778 8324 7771 8324 7778 7780 8346 8324 7780 +8325 8324 8346 8325 8347 8326 8325 8346 8347 +8326 8348 8327 8326 8347 8348 8327 8349 8328 +8327 8348 8349 8328 8350 8329 8328 8349 8350 +8329 8351 8330 8329 8350 8351 8330 8352 8331 +8330 8351 8352 8331 8353 8332 8331 8352 8353 +8332 8354 8333 8332 8353 8354 8333 8355 8334 +8333 8354 8355 8335 8334 8356 8356 8334 8355 +8337 8357 8338 8358 8359 8357 8359 8338 8357 +8359 8340 8338 8341 8340 8360 8360 8340 8359 +8344 8343 8361 8361 8343 8362 8344 8363 8345 +8344 8361 8363 8363 7782 7773 8363 7773 8345 +7785 8346 7780 8346 7785 7787 8364 8346 7787 +8347 8346 8364 8347 8365 8348 8347 8364 8365 +8348 8366 8349 8348 8365 8366 8349 8367 8350 +8349 8366 8367 8350 8368 8351 8350 8367 8368 +8351 8369 8352 8351 8368 8369 8352 8370 8353 +8352 8369 8370 8353 8371 8354 8353 8370 8371 +8354 8372 8355 8354 8371 8372 8356 8355 8373 +8373 8355 8372 8374 8375 8376 8374 8376 8358 +8376 8359 8358 8376 8377 8359 8360 8359 8378 +8359 8379 8378 8359 8377 8379 8380 8381 8379 +8379 8381 8378 8381 8380 8362 8380 8361 8362 +8380 8382 8361 8382 7789 7788 8363 8382 7788 +8361 8382 8363 8363 7788 7782 7793 8383 8364 +7793 8364 7787 8364 8384 8365 8364 8383 8384 +8365 8385 8366 8365 8384 8385 8366 8386 8367 +8366 8385 8386 8367 8387 8368 8367 8386 8387 +8368 8388 8369 8368 8387 8388 8369 8389 8370 +8369 8388 8389 8370 8390 8371 8370 8389 8390 +8371 8391 8372 8371 8390 8391 8373 8372 8392 +8372 8393 8392 8372 8391 8393 8393 8394 8392 +8375 8395 8376 8396 8397 8395 8397 8376 8395 +8397 8377 8376 8377 8398 8379 8377 8397 8398 +8398 7795 7794 8380 8398 7794 8379 8398 8380 +8380 7794 7796 8380 7796 8382 8382 7796 7789 +7799 8399 8383 7799 8383 7793 8383 8400 8384 +8383 8399 8400 8384 8401 8385 8384 8400 8401 +8385 8402 8386 8385 8401 8402 8386 8403 8387 +8386 8402 8403 8387 8404 8388 8387 8403 8404 +8388 8405 8389 8388 8404 8405 8389 8406 8390 +8389 8405 8406 8390 8407 8391 8390 8406 8407 +8408 8393 8409 8393 8407 8409 8393 8391 8407 +8393 8408 8394 8396 8410 8397 8411 7801 7800 +8397 8411 7800 8398 8397 7800 8410 8411 8397 +8398 7800 7795 7804 8399 7799 8399 7804 7807 +8399 7807 8400 8400 7807 7808 8412 8400 7808 +8401 8400 8412 8401 8413 8402 8401 8412 8413 +8402 8414 8403 8402 8413 8414 8403 8415 8404 +8403 8414 8415 8404 8416 8405 8404 8415 8416 +8405 8417 8406 8405 8416 8417 8406 8418 8407 +8406 8417 8418 8409 8407 8419 8419 8407 8418 +8411 7810 7801 8420 7810 8411 7813 8412 7808 +8412 7813 7815 8421 8412 7815 8413 8412 8421 +8413 8422 8414 8413 8421 8422 8414 8423 8415 +8414 8422 8423 8415 8424 8416 8415 8423 8424 +8416 8425 8417 8416 8424 8425 8417 8426 8418 +8417 8425 8426 8419 8418 8427 8418 8428 8427 +8418 8426 8428 8428 8429 8427 8420 7816 7810 +8430 7816 8420 7819 8421 7815 8421 7819 7821 +8431 8421 7821 8422 8421 8431 8422 8432 8423 +8422 8431 8432 8423 8433 8424 8423 8432 8433 +8424 8434 8425 8424 8433 8434 8425 8435 8426 +8425 8434 8435 8426 8436 8428 8426 8435 8436 +8429 8428 8437 8428 8438 8437 8428 8436 8438 +8438 8439 8437 8430 7822 7816 8440 7822 8430 +7824 8431 7821 8431 7824 7827 8431 7827 8432 +8432 7827 7828 8441 8432 7828 8433 8432 8441 +8433 8442 8434 8433 8441 8442 8434 8443 8435 +8434 8442 8443 8435 8444 8436 8435 8443 8444 +8436 8445 8438 8436 8444 8445 8439 8438 8446 +8446 8438 8445 8440 7829 7822 8447 7829 8440 +7831 8441 7828 8441 7831 7833 8448 8441 7833 +8442 8441 8448 8442 8449 8443 8442 8448 8449 +8443 8450 8444 8443 8449 8450 8444 8451 8445 +8444 8450 8451 8446 8445 8452 8445 8453 8452 +8445 8451 8453 8453 8454 8452 8447 7834 7829 +8455 7834 8447 7836 8448 7833 8448 7836 7838 +8448 7838 8449 8449 7838 7840 8449 7840 8450 +8450 7840 7841 8456 8450 7841 8451 8450 8456 +8451 8457 8453 8451 8456 8457 8454 8453 8458 +8458 8453 8457 8455 8459 7834 7834 8459 7843 +7844 7843 8459 7844 8459 8460 7848 8456 7841 +8456 7848 7850 8461 8456 7850 8457 8456 8461 +8458 8457 8462 8462 8457 8461 8463 7852 8464 +7852 8460 8464 7852 7844 8460 8465 8463 8464 +7854 8461 7850 8466 8467 7856 7856 8467 7854 +8467 8462 7854 8462 8461 7854 8466 8468 8467 +8469 7858 8463 8463 7858 7852 8463 8465 8470 +8463 8470 8469 7860 8471 8466 7860 8466 7856 +8468 8466 8472 8472 8466 8471 8473 7863 8469 +8469 7863 7858 8469 8470 8474 8475 8469 8474 +8473 8469 8475 8476 8475 8474 7864 8477 8471 +7864 8471 7860 8472 8471 8478 8478 8471 8477 +8473 7866 7863 7866 8473 7868 7868 8473 8475 +8476 8479 8475 8479 7868 8475 8479 7870 7868 +8480 8481 7872 7870 8480 7872 8479 8480 7870 +8482 8483 8481 8482 8481 8480 8484 8483 8482 +8485 8486 8477 7864 8485 8477 7874 8485 7864 +8477 8486 8478 8481 7875 7872 7875 8481 7877 +7877 8481 8483 8484 8487 8483 8487 7877 8483 +8487 7879 7877 7881 7879 8487 7881 8487 8488 +8489 8490 8491 8490 8485 8491 8485 7882 8491 +8485 7874 7882 8492 7883 8493 7883 8488 8493 +7883 7881 8488 8494 8495 8492 8494 8492 8493 +8496 8495 8494 8489 8491 7885 7884 8489 7885 +8497 8489 7884 7885 8491 7882 8492 7887 7883 +7887 8492 7888 7888 8492 8495 8496 8498 8495 +8498 7888 8495 8498 7890 7888 7890 8499 7892 +8498 8499 7890 7892 8500 7894 8499 8500 7892 +7896 7894 8500 7896 8500 8501 8502 8503 8504 +8505 8506 8504 8505 8504 8503 8507 8508 8506 +8507 8506 8505 8509 8508 8507 8510 8511 8512 +8513 8514 8512 8513 8512 8511 8515 8516 8514 +8515 8514 8513 8515 8497 8516 8497 7897 8516 +8497 7884 7897 8501 8517 7896 7896 8517 7898 +7898 8518 7899 8517 8518 7898 8504 7901 8502 +7901 8518 8502 7901 7899 8518 7901 8504 7903 +7903 8504 8506 7903 8506 7905 7905 8506 8508 +8509 8519 8508 8519 7905 8508 8519 7907 7905 +8520 8521 7909 7907 8520 7909 8519 8520 7907 +8521 8520 8510 8521 8510 8512 7910 8521 8512 +7911 7910 8512 7911 8512 7913 7913 8512 8514 +7913 8514 7915 7915 8514 8516 7915 8516 7897 +8521 7916 7909 7916 8521 7910 8522 7919 7920 +7921 8523 7920 7920 8523 8522 7922 8524 7921 +7921 8524 8523 7923 8525 7922 7922 8525 8524 +7924 8525 7923 8526 7932 7933 7935 8526 7933 +8527 7937 7936 7938 8528 7936 7936 8528 8527 +8528 7938 8529 7938 8530 8529 7938 7939 8530 +7939 7919 8522 8531 7939 8522 8530 7939 8531 +8531 8522 8523 8531 8523 8532 8532 8523 8524 +8532 8524 8533 8533 8524 8525 8533 8525 8534 +8534 8525 7924 7940 8534 7924 8535 8534 7940 +7940 7941 8535 8535 7941 8536 8537 7943 7942 +8537 7942 8538 7942 8539 8538 7942 7944 8539 +8540 7944 7945 8540 8539 7944 8541 7945 7946 +8541 8540 7945 8542 7946 7947 8542 8541 7946 +8543 7947 7948 8543 8542 7947 8544 7948 7949 +8544 8543 7948 8545 7949 7950 8545 8544 7949 +8546 7950 7951 8546 8545 7950 8547 7951 7952 +8547 8546 7951 7952 7953 8547 8547 7953 8548 +8549 7955 7956 8549 7956 8550 7956 8551 8550 +7956 7957 8551 7957 7932 8526 8552 7957 8526 +8551 7957 8552 8552 8526 7935 7958 8552 7935 +8553 8552 7958 7958 7937 8527 8554 7958 8527 +8553 7958 8554 8554 8527 8528 8554 8528 8555 +8555 8528 8529 7941 7959 8556 8557 7941 8556 +8536 7941 8557 7960 8556 7959 8558 8559 8537 +8559 7943 8537 8559 7961 7943 8558 8537 8538 +8560 8548 7953 8560 7953 7962 8561 7962 7963 +8561 8560 7962 8562 7963 7964 8562 8561 7963 +8563 7964 7965 8563 8562 7964 8564 7965 7966 +8564 8563 7965 8565 7966 7967 8565 8564 7966 +8566 7967 7968 8566 8565 7967 7968 7955 8549 +8567 7968 8549 8566 7968 8567 8567 8549 8550 +8568 8557 8556 8556 7960 8568 7960 8569 8568 +7960 7969 8569 8570 8571 7970 7970 8571 7971 +7970 7961 8559 7970 8559 8570 7973 8569 7969 +7973 8572 8569 8571 7974 7971 8573 7974 8571 +8574 8575 8576 8575 8572 8576 8572 7975 8576 +8572 7973 7975 8573 7976 7974 8577 7976 8573 +8574 8576 7978 7977 8574 7978 8578 8574 7977 +7978 8576 7975 8577 8579 7976 7976 8579 7979 +7980 7979 8579 7980 8579 8580 7981 8578 7977 +7981 8581 8578 8580 7982 7980 8582 7982 8580 +7983 8581 7981 7983 8583 8581 8582 7984 7982 +8584 7984 8582 7985 8583 7983 7985 8585 8583 +8584 8586 7984 7984 8586 7986 7987 7986 8586 +7987 8586 8587 7988 8585 7985 7988 8588 8585 +8587 7989 7987 8589 7989 8587 7990 8588 7988 +7990 8590 8588 8589 7991 7989 8591 7991 8589 +7992 8590 7990 7992 8592 8590 8591 8593 7991 +7991 8593 7993 7994 7993 8593 7994 8593 8594 +7995 8592 7992 7995 8595 8592 8596 8597 8598 +8594 8596 8598 7996 8594 8598 7994 8594 7996 +7997 8595 7995 7997 8599 8595 8598 8597 7998 +8597 7999 7998 8597 8600 7999 8598 7998 7996 +8000 8599 7997 8000 8601 8599 8600 8001 7999 +8602 8001 8600 8603 8604 8003 8603 8003 8002 +8000 8003 8601 8601 8003 8604 8602 8005 8001 +8605 8005 8602 8006 8603 8002 8006 8606 8603 +8605 8007 8005 8607 8007 8605 8008 8606 8006 +8008 8608 8606 8607 8009 8007 8609 8009 8607 +8010 8608 8008 8010 8610 8608 8609 8011 8009 +8611 8011 8609 8012 8612 8613 8610 8012 8613 +8010 8012 8610 8612 8614 8613 8611 8013 8011 +8615 8013 8611 8014 8612 8012 8014 8015 8612 +8015 8614 8612 8015 8616 8614 8615 8016 8013 +8617 8016 8615 8017 8616 8015 8017 8618 8616 +8617 8018 8016 8619 8018 8617 8019 8618 8017 +8019 8620 8618 8619 8020 8018 8621 8020 8619 +8021 8620 8019 8021 8622 8620 8623 8624 8625 +8621 8623 8625 8023 8621 8625 8020 8621 8023 +8024 8622 8021 8024 8626 8622 8624 8627 8625 +8023 8625 8627 8628 8023 8627 8025 8023 8628 +8024 8027 8629 8630 8024 8629 8626 8024 8630 +8028 8629 8027 8628 8029 8025 8631 8029 8628 +8630 8629 8032 8031 8630 8032 8632 8630 8031 +8032 8629 8028 8631 8034 8029 8633 8034 8631 +8036 8632 8031 8036 8634 8632 8633 8038 8034 +8635 8038 8633 8040 8634 8036 8040 8636 8634 +8635 8041 8038 8637 8041 8635 8044 8638 8045 +8639 8046 8045 8639 8045 8638 8046 8639 8640 +8641 8046 8640 8047 8046 8641 8047 8642 8048 +8641 8642 8047 8048 8643 8049 8642 8643 8048 +8050 8049 8643 8050 8643 8644 8645 8646 8052 +8645 8052 8051 8040 8052 8636 8636 8052 8646 +8637 8053 8041 8647 8053 8637 8648 8649 8057 +8648 8057 8056 8057 8650 8058 8649 8650 8057 +8058 8651 8059 8650 8651 8058 8044 8059 8638 +8059 8652 8638 8059 8651 8652 8638 8652 8639 +8639 8652 8653 8639 8653 8640 8644 8654 8050 +8050 8654 8060 8060 8655 8061 8654 8655 8060 +8061 8656 8062 8655 8656 8061 8062 8657 8063 +8656 8657 8062 8657 8658 8659 8064 8657 8659 +8063 8657 8064 8659 8065 8064 8660 8661 8068 +8660 8068 8067 8051 8068 8645 8645 8068 8661 +8647 8069 8053 8662 8069 8647 8663 8664 8072 +8663 8072 8071 8072 8665 8073 8664 8665 8072 +8073 8666 8074 8665 8666 8073 8074 8667 8075 +8666 8667 8074 8056 8075 8648 8648 8075 8667 +8658 8668 8659 8659 8668 8065 8668 8076 8065 +8668 8669 8076 8076 8670 8077 8669 8670 8076 +8077 8671 8078 8670 8671 8077 8067 8078 8660 +8660 8078 8671 8662 8672 8069 8069 8672 8079 +8080 8079 8672 8080 8672 8673 8674 8675 8084 +8674 8084 8083 8084 8676 8085 8675 8676 8084 +8071 8085 8663 8663 8085 8676 8673 8677 8080 +8080 8677 8086 8086 8678 8087 8677 8678 8086 +8087 8679 8088 8678 8679 8087 8083 8088 8674 +8674 8088 8679 8680 8681 8682 8683 8684 8682 +8682 8684 8680 8685 8686 8683 8683 8686 8684 +8687 8688 8685 8685 8688 8686 8689 8690 8687 +8687 8690 8688 8689 8691 8690 8692 8693 8694 +8695 8696 8694 8694 8696 8692 8697 8698 8695 +8695 8698 8696 8699 8700 8697 8697 8700 8698 +8701 8702 8699 8699 8702 8700 8703 8704 8701 +8701 8704 8702 8703 8705 8704 8706 8707 8708 +8090 8089 8708 8089 8706 8708 8089 8709 8706 +8710 8089 8093 8710 8709 8089 8095 8682 8681 +8710 8095 8681 8093 8095 8710 8682 8095 8097 +8682 8097 8683 8683 8097 8099 8683 8099 8685 +8685 8099 8101 8685 8101 8687 8687 8101 8103 +8687 8103 8689 8711 8691 8689 8103 8711 8689 +8104 8711 8103 8712 8104 8106 8712 8711 8104 +8713 8106 8108 8713 8712 8106 8714 8108 8110 +8714 8713 8108 8715 8110 8112 8715 8714 8110 +8114 8694 8693 8715 8114 8693 8112 8114 8715 +8694 8114 8116 8694 8116 8695 8695 8116 8118 +8695 8118 8697 8697 8118 8120 8697 8120 8699 +8699 8120 8121 8716 8699 8121 8701 8699 8716 +8701 8717 8703 8701 8716 8717 8705 8703 8718 +8718 8703 8717 8719 8720 8721 8719 8721 8707 +8707 8721 8708 8721 8123 8708 8123 8122 8708 +8708 8122 8090 8125 8716 8121 8716 8125 8127 +8716 8127 8717 8722 8723 8128 8128 8723 8127 +8723 8718 8127 8718 8717 8127 8724 8725 8722 +8722 8725 8723 8726 8727 8724 8724 8727 8725 +8726 8728 8727 8721 8720 8129 8720 8130 8129 +8720 8729 8130 8721 8129 8123 8133 8722 8128 +8722 8133 8135 8722 8135 8724 8724 8135 8136 +8730 8724 8136 8726 8724 8730 8728 8726 8731 +8726 8732 8731 8726 8730 8732 8733 8734 8732 +8732 8734 8731 8733 8735 8734 8736 8737 8738 +8729 8736 8738 8137 8729 8738 8130 8729 8137 +8140 8730 8136 8730 8140 8142 8739 8730 8142 +8732 8730 8739 8732 8740 8733 8732 8739 8740 +8735 8733 8741 8741 8733 8740 8738 8737 8143 +8737 8144 8143 8737 8742 8144 8738 8143 8137 +8149 8743 8739 8149 8739 8142 8739 8744 8740 +8739 8743 8744 8741 8740 8745 8740 8746 8745 +8740 8744 8746 8746 8747 8745 8748 8749 8750 +8751 8752 8750 8750 8752 8748 8742 8752 8751 +8151 8742 8751 8144 8742 8151 8157 8753 8156 +8743 8149 8156 8753 8743 8156 8754 8743 8753 +8743 8755 8744 8743 8754 8755 8744 8756 8746 +8744 8755 8756 8747 8746 8757 8757 8746 8756 +8750 8749 8758 8758 8749 8759 8758 8159 8158 +8751 8758 8158 8750 8758 8751 8751 8158 8151 +8169 8753 8157 8753 8169 8171 8760 8753 8171 +8754 8753 8760 8754 8761 8755 8754 8760 8761 +8755 8762 8756 8755 8761 8762 8757 8756 8763 +8763 8756 8762 8758 8759 8764 8764 8759 8765 +8764 8173 8159 8764 8159 8758 8187 8766 8760 +8187 8760 8171 8760 8767 8761 8760 8766 8767 +8768 8762 8769 8762 8767 8769 8762 8761 8767 +8762 8768 8763 8764 8765 8770 8770 8765 8771 +8770 8189 8173 8770 8173 8764 8206 8766 8187 +8766 8206 8208 8772 8766 8208 8767 8766 8772 +8769 8767 8773 8773 8767 8772 8770 8771 8774 +8774 8771 8775 8774 8210 8189 8774 8189 8770 +8234 8776 8772 8234 8772 8208 8773 8772 8777 +8777 8772 8776 8774 8775 8235 8775 8236 8235 +8775 8778 8236 8774 8235 8210 8262 8779 8261 +8776 8234 8261 8779 8776 8261 8780 8776 8779 +8777 8776 8781 8781 8776 8780 8263 8236 8782 +8236 8783 8782 8236 8778 8783 8784 8264 8263 +8784 8263 8782 8784 8265 8264 8287 8785 8286 +8786 8288 8286 8786 8286 8785 8779 8262 8288 +8786 8779 8288 8787 8779 8786 8779 8788 8780 +8779 8787 8788 8781 8780 8789 8789 8780 8788 +8783 8790 8782 8791 8290 8289 8782 8791 8289 +8784 8782 8289 8790 8791 8782 8265 8784 8289 +8293 8292 8792 8792 8294 8293 8312 8793 8785 +8312 8785 8287 8785 8794 8786 8785 8793 8794 +8786 8795 8787 8786 8794 8795 8787 8796 8788 +8787 8795 8796 8789 8788 8797 8797 8788 8796 +8791 8314 8290 8798 8314 8791 8315 8792 8292 +8294 8792 8315 8318 8317 8799 8800 8319 8318 +8800 8318 8799 8800 8320 8319 8335 8801 8793 +8335 8793 8312 8793 8802 8794 8793 8801 8802 +8794 8803 8795 8794 8802 8803 8804 8796 8805 +8796 8803 8805 8796 8795 8803 8796 8804 8797 +8336 8314 8806 8314 8807 8806 8314 8798 8807 +8806 8337 8336 8339 8799 8317 8799 8339 8341 +8808 8799 8341 8800 8799 8808 8809 8342 8320 +8800 8809 8320 8808 8809 8800 8809 8343 8342 +8356 8810 8801 8356 8801 8335 8801 8811 8802 +8801 8810 8811 8802 8812 8803 8802 8811 8812 +8805 8803 8813 8813 8803 8812 8807 8814 8806 +8814 8815 8816 8814 8816 8357 8806 8814 8357 +8337 8806 8357 8816 8358 8357 8360 8817 8808 +8360 8808 8341 8808 8818 8809 8808 8817 8818 +8818 8362 8343 8818 8343 8809 8373 8819 8810 +8373 8810 8356 8810 8820 8811 8810 8819 8820 +8811 8821 8812 8811 8820 8821 8813 8812 8822 +8822 8812 8821 8823 8824 8825 8823 8825 8815 +8815 8825 8816 8825 8375 8816 8375 8374 8816 +8816 8374 8358 8378 8817 8360 8817 8378 8381 +8817 8381 8818 8818 8381 8362 8392 8819 8373 +8819 8392 8394 8826 8819 8394 8820 8819 8826 +8820 8827 8821 8820 8826 8827 8822 8821 8828 +8828 8821 8827 8824 8829 8825 8829 8830 8831 +8829 8831 8395 8825 8829 8395 8375 8825 8395 +8831 8396 8395 8409 8832 8408 8826 8394 8408 +8832 8826 8408 8833 8826 8832 8834 8827 8835 +8827 8833 8835 8827 8826 8833 8827 8834 8828 +8830 8836 8831 8836 8837 8838 8836 8838 8410 +8831 8836 8410 8396 8831 8410 8838 8411 8410 +8419 8839 8832 8419 8832 8409 8832 8840 8833 +8832 8839 8840 8835 8833 8841 8841 8833 8840 +8838 8837 8842 8842 8837 8843 8842 8420 8411 +8842 8411 8838 8427 8839 8419 8839 8427 8429 +8844 8839 8429 8840 8839 8844 8841 8840 8845 +8840 8846 8845 8840 8844 8846 8846 8847 8845 +8843 8848 8842 8420 8842 8848 8849 8420 8848 +8430 8420 8849 8437 8844 8429 8844 8437 8439 +8850 8844 8439 8846 8844 8850 8847 8846 8851 +8851 8846 8850 8849 8440 8430 8852 8440 8849 +8446 8853 8850 8446 8850 8439 8851 8850 8854 +8854 8850 8853 8852 8447 8440 8855 8447 8852 +8452 8853 8446 8856 8857 8454 8454 8857 8452 +8857 8854 8452 8854 8853 8452 8856 8858 8857 +8855 8455 8447 8859 8455 8855 8458 8860 8856 +8458 8856 8454 8858 8856 8861 8861 8856 8860 +8859 8862 8455 8455 8862 8459 8460 8459 8862 +8460 8862 8863 8462 8864 8860 8462 8860 8458 +8861 8860 8865 8865 8860 8864 8863 8866 8460 +8460 8866 8464 8465 8464 8866 8465 8866 8867 +8467 8864 8462 8467 8468 8864 8468 8865 8864 +8468 8868 8865 8867 8470 8465 8869 8470 8867 +8472 8868 8468 8472 8870 8868 8869 8871 8470 +8470 8871 8474 8872 8873 8476 8474 8872 8476 +8871 8872 8474 8874 8873 8872 8875 8876 8877 +8876 8870 8877 8870 8478 8877 8870 8472 8478 +8873 8479 8476 8874 8878 8873 8878 8479 8873 +8878 8480 8479 8480 8879 8482 8878 8879 8480 +8484 8482 8879 8484 8879 8880 8881 8882 8883 +8883 8882 8875 8883 8875 8877 8485 8883 8877 +8486 8485 8877 8486 8877 8478 8880 8884 8484 +8484 8884 8487 8488 8487 8884 8488 8884 8885 +8881 8883 8490 8489 8881 8490 8886 8881 8489 +8490 8883 8485 8885 8887 8488 8488 8887 8493 +8493 8888 8494 8887 8888 8493 8496 8494 8888 +8496 8888 8889 8890 8891 8892 8891 8886 8892 +8886 8497 8892 8886 8489 8497 8889 8893 8496 +8496 8893 8498 8498 8894 8499 8893 8894 8498 +8499 8895 8500 8894 8895 8499 8896 8897 8501 +8500 8896 8501 8895 8896 8500 8898 8899 8897 +8898 8897 8896 8503 8502 8899 8898 8503 8899 +8900 8503 8898 8503 8901 8505 8900 8901 8503 +8505 8902 8507 8901 8902 8505 8509 8507 8902 +8509 8902 8903 8904 8905 8906 8511 8510 8906 +8905 8511 8906 8907 8511 8905 8511 8908 8513 +8907 8908 8511 8892 8515 8890 8515 8908 8890 +8515 8513 8908 8515 8892 8497 8897 8517 8501 +8517 8897 8518 8518 8897 8899 8518 8899 8502 +8903 8909 8509 8509 8909 8519 8906 8520 8904 +8520 8909 8904 8520 8519 8909 8520 8906 8510 +8910 8529 8530 8910 8530 8911 8530 8912 8911 +8530 8531 8912 8913 8531 8532 8913 8912 8531 +8914 8532 8533 8914 8913 8532 8915 8533 8534 +8915 8914 8533 8916 8534 8535 8916 8915 8534 +8535 8536 8916 8916 8536 8917 8918 8919 8539 +8539 8919 8538 8920 8539 8540 8920 8918 8539 +8921 8540 8541 8921 8920 8540 8922 8541 8542 +8922 8921 8541 8923 8542 8543 8923 8922 8542 +8924 8543 8544 8924 8923 8543 8925 8544 8545 +8925 8924 8544 8926 8545 8546 8926 8925 8545 +8927 8926 8928 8926 8547 8928 8926 8546 8547 +8548 8928 8547 8929 8930 8551 8551 8930 8550 +8931 8551 8552 8931 8929 8551 8932 8552 8553 +8932 8931 8552 8933 8553 8554 8933 8932 8553 +8934 8554 8555 8934 8933 8554 8555 8529 8910 +8935 8555 8910 8934 8555 8935 8935 8910 8911 +8557 8917 8536 8557 8936 8917 8937 8938 8939 +8559 8558 8939 8558 8937 8939 8558 8940 8937 +8558 8538 8919 8558 8919 8940 8941 8927 8928 +8941 8928 8548 8560 8941 8548 8942 8941 8560 +8943 8560 8561 8943 8942 8560 8944 8561 8562 +8944 8943 8561 8945 8562 8563 8945 8944 8562 +8946 8563 8564 8946 8945 8563 8947 8564 8565 +8947 8946 8564 8948 8565 8566 8948 8947 8565 +8949 8566 8567 8949 8948 8566 8567 8550 8930 +8567 8930 8949 8950 8936 8557 8950 8557 8568 +8568 8569 8950 8950 8569 8951 8939 8938 8570 +8938 8571 8570 8938 8952 8571 8939 8570 8559 +8572 8951 8569 8572 8953 8951 8952 8573 8571 +8954 8573 8952 8955 8956 8575 8955 8575 8574 +8572 8575 8953 8953 8575 8956 8957 8577 8958 +8577 8954 8958 8577 8573 8954 8959 8957 8958 +8578 8955 8574 8578 8960 8955 8957 8579 8577 +8580 8579 8957 8959 8580 8957 8961 8580 8959 +8581 8960 8578 8581 8962 8960 8961 8582 8580 +8963 8582 8961 8583 8962 8581 8583 8964 8962 +8963 8584 8582 8965 8584 8963 8585 8964 8583 +8585 8966 8964 8965 8967 8584 8584 8967 8586 +8587 8586 8967 8587 8967 8968 8588 8966 8585 +8588 8969 8966 8968 8589 8587 8970 8589 8968 +8590 8969 8588 8590 8971 8969 8970 8591 8589 +8972 8591 8970 8592 8971 8590 8592 8973 8971 +8972 8974 8591 8591 8974 8593 8594 8593 8974 +8594 8974 8975 8595 8973 8592 8595 8976 8973 +8977 8978 8596 8596 8978 8597 8596 8594 8975 +8596 8975 8977 8599 8976 8595 8599 8979 8976 +8978 8600 8597 8980 8600 8978 8981 8982 8983 +8982 8979 8983 8979 8601 8983 8979 8599 8601 +8980 8602 8600 8984 8602 8980 8981 8983 8604 +8603 8981 8604 8985 8981 8603 8604 8983 8601 +8984 8605 8602 8986 8605 8984 8606 8985 8603 +8606 8987 8985 8986 8607 8605 8988 8607 8986 +8608 8987 8606 8608 8989 8987 8988 8609 8607 +8990 8609 8988 8610 8989 8608 8610 8991 8989 +8990 8611 8609 8992 8611 8990 8993 8991 8610 +8993 8610 8613 8613 8614 8993 8993 8614 8994 +8992 8615 8611 8995 8615 8992 8616 8994 8614 +8616 8996 8994 8995 8617 8615 8997 8617 8995 +8618 8996 8616 8618 8998 8996 8997 8619 8617 +8999 8619 8997 8620 8998 8618 8620 9000 8998 +9001 9002 9003 8999 9001 9003 8621 8999 9003 +8619 8999 8621 8622 9000 8620 8622 9004 9000 +9003 9002 8623 9002 8624 8623 9002 9005 8624 +9003 8623 8621 8626 9004 8622 8626 9006 9004 +9005 9007 8624 8624 9007 8627 8628 8627 9007 +8628 9007 9008 8630 9006 8626 8630 9009 9006 +9008 8631 8628 9010 8631 9008 8632 9009 8630 +8632 9011 9009 9010 8633 8631 9012 8633 9010 +8634 9011 8632 8634 9013 9011 9012 8635 8633 +9014 8635 9012 8636 9013 8634 8636 9015 9013 +9014 8637 8635 9016 8637 9014 8640 9017 8641 +9018 8642 8641 9018 8641 9017 9019 8643 8642 +9019 8642 9018 9019 8644 8643 9020 9021 8646 +9020 8646 8645 8636 8646 9015 9015 8646 9021 +9016 8647 8637 9022 8647 9016 8648 9023 8649 +8649 9023 9024 9025 8649 9024 8650 8649 9025 +8650 9026 8651 9025 9026 8650 8651 9027 8652 +9026 9027 8651 8652 9028 8653 9027 9028 8652 +8640 8653 9017 8653 9029 9017 8653 9028 9029 +9017 9029 9018 9018 9029 9030 9018 9030 9019 +9019 9030 9031 9019 9031 8644 9031 8654 8644 +9031 9032 8654 8654 9033 8655 9032 9033 8654 +8655 9034 8656 9033 9034 8655 9034 9035 9036 +8657 9034 9036 8656 9034 8657 9036 8658 8657 +8660 9037 8661 9037 9038 8661 9038 8645 8661 +9038 9020 8645 9022 8662 8647 9039 8662 9022 +9040 9041 8664 9040 8664 8663 9041 9042 9043 +8665 9041 9043 8664 9041 8665 8665 9043 9044 +9045 8665 9044 8666 8665 9045 8666 9046 8667 +9045 9046 8666 8648 8667 9023 8667 9047 9023 +8667 9046 9047 9023 9047 9024 9035 9048 9036 +9036 9048 8658 9048 8668 8658 9048 9049 8668 +8668 9050 8669 9049 9050 8668 8669 9051 8670 +9050 9051 8669 8670 9052 8671 9051 9052 8670 +8660 8671 9037 8671 9053 9037 8671 9052 9053 +9037 9053 9038 9039 9054 8662 8662 9054 8672 +8673 8672 9054 8673 9054 9055 8674 9056 8675 +8675 9056 9057 9058 8675 9057 8676 8675 9058 +8663 8676 9040 9040 8676 9058 9042 9059 9043 +9043 9059 9044 9055 9060 8673 8673 9060 8677 +8677 9061 8678 9060 9061 8677 8678 9062 8679 +9061 9062 8678 8674 8679 9056 8679 9063 9056 +8679 9062 9063 9056 9063 9057 9064 9065 9066 +8681 8680 9066 8680 9064 9066 8680 9067 9064 +9068 8680 8684 9068 9067 8680 9069 8684 8686 +9069 9068 8684 9070 8686 8688 9070 9069 8686 +9071 8688 8690 9071 9070 8688 8690 8691 9071 +9071 8691 9072 9073 9074 8692 8692 9074 8693 +9075 8692 8696 9075 9073 8692 9076 8696 8698 +9076 9075 8696 9077 8698 8700 9077 9076 8698 +9078 8700 8702 9078 9077 8700 9079 9078 9080 +9078 8704 9080 9078 8702 8704 8705 9080 8704 +9081 9082 8706 8706 9082 8707 9083 8706 8709 +9083 9081 8706 8710 9066 9065 9083 8710 9065 +8709 8710 9083 9066 8710 8681 9084 9072 8691 +9084 8691 8711 9085 8711 8712 9085 9084 8711 +9086 8712 8713 9086 9085 8712 9087 8713 8714 +9087 9086 8713 9088 8714 8715 9088 9087 8714 +8715 8693 9074 8715 9074 9088 9089 9079 9080 +8705 8718 9080 8718 9090 9080 9090 9089 9080 +9090 8718 9091 9092 9093 9091 9091 9093 9090 +9094 9095 9092 9092 9095 9093 9094 9096 9095 +9097 9098 8719 8719 9098 8720 8719 8707 9082 +8719 9082 9097 8723 9091 8718 9091 8723 8725 +9091 8725 9092 9092 8725 8727 9092 8727 9094 +9099 9100 8728 8728 9100 8727 9100 9096 8727 +9096 9094 8727 9099 9101 9100 9098 8729 8720 +9102 8729 9098 8731 9099 8728 9103 9101 9099 +8731 9103 9099 8734 9103 8731 9104 9105 8735 +9105 8734 8735 9105 9103 8734 9104 9106 9105 +9107 9108 8736 8736 9108 8737 8736 8729 9102 +8736 9102 9107 8741 9109 9104 8741 9104 8735 +9106 9104 9110 9110 9104 9109 9108 8742 8737 +9111 8742 9108 8745 9109 8741 9112 9113 8747 +8747 9113 8745 9113 9110 8745 9110 9109 8745 +9112 9114 9113 9115 9116 8748 8748 9116 8749 +9117 8748 8752 9117 9115 8748 8752 8742 9111 +8752 9111 9117 8757 9118 9112 8757 9112 8747 +9114 9112 9119 9119 9112 9118 9116 8759 8749 +9120 8759 9116 9121 9122 9118 8757 9121 9118 +8763 9121 8757 9118 9122 9119 9120 8765 8759 +9123 8765 9120 8769 9124 8768 9124 9125 8768 +9125 8763 8768 9125 9121 8763 9126 9127 9128 +9123 9126 9128 8771 9123 9128 8765 9123 8771 +8773 9129 9124 8773 9124 8769 9125 9124 9130 +9130 9124 9129 9127 9131 9128 8771 9128 9131 +9132 8771 9131 8775 8771 9132 8777 9133 9129 +8777 9129 8773 9130 9129 9134 9134 9129 9133 +9132 8778 8775 9135 8778 9132 8781 9136 9133 +8781 9133 8777 9134 9133 9137 9137 9133 9136 +9135 8783 8778 9138 8783 9135 8789 9139 9136 +8789 9136 8781 9137 9136 9140 9140 9136 9139 +9138 9141 8783 8783 9141 8790 8791 8790 9141 +8791 9141 9142 8797 9143 9139 8797 9139 8789 +9140 9139 9144 9144 9139 9143 9142 8798 8791 +9145 8798 9142 8805 9146 8804 9146 9147 8804 +8804 9147 8797 9147 9148 8797 9148 9143 8797 +9143 9148 9144 9145 8807 8798 9149 8807 9145 +8813 9150 9146 8813 9146 8805 9147 9146 9151 +9151 9146 9150 8814 8807 9152 8807 9153 9152 +8807 9149 9153 9152 8815 8814 8822 9154 9150 +8822 9150 8813 9151 9150 9155 9155 9150 9154 +9156 9157 9153 9157 8824 9153 8823 9152 9153 +8823 9153 8824 8823 8815 9152 9157 9156 9158 +8828 9159 9154 8828 9154 8822 9155 9154 9160 +9160 9154 9159 9157 8829 8824 8830 8829 9157 +9158 8830 9157 9161 8830 9158 8835 9162 8834 +9162 9163 8834 8834 9163 8828 9163 9164 8828 +9164 9159 8828 9159 9164 9160 9161 9165 8830 +8830 9165 8836 8837 8836 9165 8837 9165 9166 +8841 9167 9162 8841 9162 8835 9163 9162 9168 +9168 9162 9167 9166 8843 8837 9169 8843 9166 +8845 9167 8841 8845 8847 9167 8847 9168 9167 +8847 9170 9168 9169 9171 8843 8843 9171 8848 +8849 8848 9171 8849 9171 9172 8851 9173 9174 +9170 8851 9174 8847 8851 9170 9173 9175 9174 +9172 8852 8849 9176 8852 9172 8854 9177 9173 +8854 9173 8851 9175 9173 9178 9178 9173 9177 +9176 8855 8852 9179 8855 9176 8857 9177 8854 +8857 8858 9177 8858 9178 9177 8858 9180 9178 +9179 8859 8855 9181 8859 9179 8861 9180 8858 +8861 9182 9180 8862 8859 9183 8859 9184 9183 +8859 9181 9184 9183 8863 8862 8865 9182 8861 +8865 9185 9182 9184 9186 9183 9186 9187 9188 +9186 9188 8866 9183 9186 8866 8863 9183 8866 +9188 8867 8866 8868 9185 8865 8868 9189 9185 +9188 9187 9190 9190 9187 9191 9190 8869 8867 +9190 8867 9188 8870 9189 8868 8870 9192 9189 +9191 9193 9190 9190 9193 8869 9193 8871 8869 +9193 9194 8871 9194 9195 9196 8872 9194 9196 +8871 9194 8872 9196 8874 8872 9197 9198 8876 +9197 8876 8875 8870 8876 9192 9192 8876 9198 +9195 9199 9196 9196 9199 8874 9199 8878 8874 +9199 9200 8878 9200 9201 9202 8879 9200 9202 +8878 9200 8879 9202 8880 8879 9203 9204 8882 +9203 8882 8881 8875 8882 9197 9197 8882 9204 +9201 9205 9202 9205 9206 9207 9205 9207 8884 +9202 9205 8884 8880 9202 8884 9207 8885 8884 +8886 9203 8881 8886 9208 9203 9206 9209 9207 +9207 9209 8885 9209 8887 8885 9209 9210 8887 +9210 9211 9212 8888 9210 9212 8887 9210 8888 +9212 8889 8888 9213 9214 8891 9213 8891 8890 +8886 8891 9208 9208 8891 9214 9211 9215 9212 +9212 9215 8889 9215 8893 8889 9215 9216 8893 +8893 9217 8894 9216 9217 8893 8894 9218 8895 +9217 9218 8894 8895 9219 8896 9218 9219 8895 +8896 9220 8898 9219 9220 8896 8898 9221 8900 +9220 9221 8898 8900 9222 8901 9221 9222 8900 +8901 9223 8902 9222 9223 8901 8903 8902 9223 +8903 9223 9224 9225 9226 8905 9225 8905 8904 +8905 9227 8907 9226 9227 8905 8907 9228 8908 +9227 9228 8907 8890 8908 9213 9213 8908 9228 +9224 9229 8903 8903 9229 8909 8904 8909 9225 +9225 8909 9229 9230 9231 8912 8912 9231 8911 +9232 8912 8913 9232 9230 8912 9233 8913 8914 +9233 9232 8913 9234 8914 8915 9234 9233 8914 +9235 8915 8916 9235 9234 8915 8916 8917 9235 +9235 8917 9236 9237 9238 8918 8918 9238 8919 +9239 8918 8920 9239 9237 8918 9240 8920 8921 +9240 9239 8920 9241 8921 8922 9241 9240 8921 +9242 8922 8923 9242 9241 8922 9243 8923 8924 +9243 9242 8923 9244 8924 8925 9244 9243 8924 +9245 8925 8926 9245 9244 8925 8926 8927 9245 +9245 8927 9246 9247 9248 8929 8929 9248 8930 +9249 9247 9250 9247 8931 9250 9247 8929 8931 +8932 9251 8931 8931 9251 9250 9251 8932 9252 +8932 9253 9252 8932 8933 9253 9254 8933 8934 +9254 9253 8933 9255 8934 8935 9255 9254 8934 +8935 8911 9231 8935 9231 9255 8936 9236 8917 +8936 9256 9236 9257 9258 8937 8937 9258 8938 +9259 8937 8940 9259 9257 8937 8940 8919 9238 +8940 9238 9259 9260 9246 8927 9260 8927 8941 +9261 8941 8942 9261 9260 8941 9262 8942 8943 +9262 9261 8942 9263 8943 8944 9263 9262 8943 +9264 8944 8945 9264 9263 8944 9265 8945 8946 +9265 9264 8945 9266 8946 8947 9266 9265 8946 +9267 8947 8948 9267 9266 8947 9268 8948 8949 +9268 9267 8948 8949 8930 9248 8949 9248 9268 +9269 9249 9250 9269 9250 9251 9269 9251 9270 +9270 9251 9252 9271 9256 8936 9271 8936 8950 +8950 8951 9271 9271 8951 9272 9258 8952 8938 +9273 8952 9258 8953 9272 8951 8953 9274 9272 +9273 8954 8952 9275 8954 9273 9276 9277 8956 +9276 8956 8955 8953 8956 9274 9274 8956 9277 +9275 9278 8954 8954 9278 8958 8959 8958 9278 +8959 9278 9279 8960 9276 8955 8960 9280 9276 +9279 8961 8959 9281 8961 9279 8962 9280 8960 +8962 9282 9280 9281 8963 8961 9283 8963 9281 +8964 9282 8962 8964 9284 9282 9283 8965 8963 +9285 8965 9283 8966 9284 8964 8966 9286 9284 +9285 9287 8965 8965 9287 8967 8968 8967 9287 +8968 9287 9288 8969 9286 8966 8969 9289 9286 +9288 8970 8968 9290 8970 9288 8971 9289 8969 +8971 9291 9289 9290 8972 8970 9292 8972 9290 +8973 9291 8971 8973 9293 9291 9292 9294 8972 +8972 9294 8974 8975 8974 9294 8975 9294 9295 +8976 9293 8973 8976 9296 9293 9297 9298 8977 +8977 9298 8978 8977 8975 9295 8977 9295 9297 +8979 9296 8976 8979 9299 9296 9298 8980 8978 +9300 8980 9298 9301 9302 8982 9301 8982 8981 +8979 8982 9299 9299 8982 9302 9300 8984 8980 +9303 8984 9300 8985 9301 8981 8985 9304 9301 +9303 8986 8984 9305 8986 9303 8987 9304 8985 +8987 9306 9304 9305 8988 8986 9307 8988 9305 +8989 9306 8987 8989 9308 9306 9307 8990 8988 +9309 8990 9307 8991 9310 9311 9308 8991 9311 +8989 8991 9308 9310 9312 9311 9309 8992 8990 +9313 8992 9309 8993 9310 8991 8993 8994 9310 +8994 9312 9310 8994 9314 9312 9313 8995 8992 +9315 8995 9313 8996 9314 8994 8996 9316 9314 +9315 8997 8995 9317 8997 9315 8998 9316 8996 +8998 9318 9316 9317 8999 8997 9319 8999 9317 +9000 9318 8998 9000 9320 9318 9321 9322 9001 +9001 9322 9002 9001 8999 9319 9001 9319 9321 +9004 9320 9000 9004 9323 9320 9324 9005 9325 +9005 9322 9325 9005 9002 9322 9326 9324 9325 +9006 9323 9004 9006 9327 9323 9324 9007 9005 +9008 9007 9324 9326 9008 9324 9328 9008 9326 +9009 9327 9006 9009 9329 9327 9328 9010 9008 +9330 9010 9328 9011 9329 9009 9011 9331 9329 +9330 9012 9010 9332 9012 9330 9013 9331 9011 +9013 9333 9331 9332 9014 9012 9334 9014 9332 +9015 9333 9013 9015 9335 9333 9334 9016 9014 +9336 9016 9334 9020 9337 9021 9337 9338 9021 +9338 9015 9021 9338 9335 9015 9336 9022 9016 +9339 9022 9336 9024 9340 9025 9025 9340 9341 +9342 9025 9341 9026 9025 9342 9026 9343 9027 +9342 9343 9026 9027 9344 9028 9343 9344 9027 +9028 9345 9029 9344 9345 9028 9029 9346 9030 +9345 9346 9029 9346 9347 9348 9031 9346 9348 +9030 9346 9031 9349 9032 9031 9349 9031 9348 +9032 9349 9350 9351 9032 9350 9033 9032 9351 +9351 9352 9353 9034 9351 9353 9033 9351 9034 +9353 9035 9034 9354 9355 9337 9020 9354 9337 +9038 9354 9020 9337 9355 9338 9339 9039 9022 +9356 9039 9339 9040 9357 9041 9357 9042 9041 +9358 9359 9045 9358 9045 9044 9045 9360 9046 +9359 9360 9045 9046 9361 9047 9360 9361 9046 +9024 9047 9340 9047 9362 9340 9047 9361 9362 +9340 9362 9341 9347 9363 9348 9348 9363 9349 +9349 9363 9364 9349 9364 9350 9352 9365 9353 +9353 9365 9035 9365 9048 9035 9365 9366 9048 +9048 9367 9049 9366 9367 9048 9049 9368 9050 +9367 9368 9049 9050 9369 9051 9368 9369 9050 +9051 9370 9052 9369 9370 9051 9052 9371 9053 +9370 9371 9052 9038 9053 9354 9354 9053 9371 +9356 9372 9039 9039 9372 9054 9055 9054 9372 +9055 9372 9373 9374 9375 9058 9374 9058 9057 +9040 9058 9357 9058 9376 9357 9058 9375 9376 +9357 9376 9042 9376 9059 9042 9376 9377 9059 +9044 9059 9358 9358 9059 9377 9373 9378 9055 +9055 9378 9060 9060 9379 9061 9378 9379 9060 +9061 9380 9062 9379 9380 9061 9062 9381 9063 +9380 9381 9062 9057 9063 9374 9374 9063 9381 +9382 9383 9064 9064 9383 9065 9384 9064 9067 +9384 9382 9064 9385 9067 9068 9385 9384 9067 +9386 9068 9069 9386 9385 9068 9387 9069 9070 +9387 9386 9069 9388 9070 9071 9388 9387 9070 +9071 9072 9388 9388 9072 9389 9390 9074 9073 +9075 9391 9073 9073 9391 9390 9391 9075 9392 +9075 9393 9392 9075 9076 9393 9394 9076 9077 +9394 9393 9076 9395 9077 9078 9395 9394 9077 +9078 9079 9395 9395 9079 9396 9397 9398 9081 +9081 9398 9082 9399 9081 9083 9399 9397 9081 +9083 9065 9383 9083 9383 9399 9400 9389 9072 +9400 9072 9084 9401 9084 9085 9401 9400 9084 +9402 9085 9086 9402 9401 9085 9403 9086 9087 +9403 9402 9086 9404 9087 9088 9404 9403 9087 +9088 9074 9390 9405 9088 9390 9404 9088 9405 +9405 9390 9391 9405 9391 9406 9406 9391 9392 +9407 9396 9079 9407 9079 9089 9408 9089 9090 +9408 9407 9089 9409 9090 9093 9409 9408 9090 +9410 9093 9095 9410 9409 9093 9095 9096 9410 +9410 9096 9411 9412 9413 9097 9097 9413 9098 +9097 9082 9398 9097 9398 9412 9414 9411 9096 +9414 9096 9100 9415 9416 9101 9416 9100 9101 +9416 9414 9100 9415 9417 9416 9413 9102 9098 +9418 9102 9413 9103 9415 9101 9419 9417 9415 +9103 9419 9415 9105 9419 9103 9105 9106 9419 +9419 9106 9420 9421 9422 9107 9107 9422 9108 +9107 9102 9418 9107 9418 9421 9110 9423 9424 +9420 9110 9424 9106 9110 9420 9423 9425 9424 +9426 9427 9428 9429 9430 9428 9428 9430 9426 +9422 9430 9429 9111 9422 9429 9108 9422 9111 +9113 9423 9110 9113 9114 9423 9114 9425 9423 +9114 9431 9425 9428 9427 9115 9427 9116 9115 +9427 9432 9116 9428 9115 9117 9428 9117 9429 +9429 9117 9111 9119 9431 9114 9119 9433 9431 +9432 9120 9116 9434 9120 9432 9435 9436 9122 +9435 9122 9121 9119 9122 9433 9433 9122 9436 +9434 9123 9120 9437 9123 9434 9125 9435 9121 +9125 9438 9435 9439 9440 9126 9126 9440 9127 +9126 9123 9437 9126 9437 9439 9130 9441 9442 +9438 9130 9442 9125 9130 9438 9441 9443 9442 +9440 9444 9127 9127 9444 9131 9132 9131 9444 +9132 9444 9445 9134 9446 9441 9134 9441 9130 +9443 9441 9447 9447 9441 9446 9445 9135 9132 +9448 9135 9445 9449 9450 9446 9134 9449 9446 +9137 9449 9134 9446 9450 9447 9448 9138 9135 +9451 9138 9448 9140 9449 9137 9140 9452 9449 +9451 9453 9138 9138 9453 9141 9142 9141 9453 +9142 9453 9454 9144 9452 9140 9144 9455 9452 +9454 9145 9142 9456 9145 9454 9147 9457 9148 +9457 9458 9148 9458 9144 9148 9458 9455 9144 +9459 9149 9460 9149 9456 9460 9149 9145 9456 +9461 9459 9460 9462 9463 9457 9147 9462 9457 +9151 9462 9147 9457 9463 9458 9464 9153 9459 +9459 9153 9149 9459 9461 9465 9459 9465 9464 +9155 9462 9151 9155 9466 9462 9464 9156 9153 +9158 9156 9464 9465 9158 9464 9467 9158 9465 +9468 9469 9470 9469 9466 9470 9466 9160 9470 +9466 9155 9160 9471 9161 9472 9161 9467 9472 +9161 9158 9467 9473 9471 9472 9468 9470 9164 +9163 9468 9164 9474 9468 9163 9164 9470 9160 +9471 9165 9161 9166 9165 9471 9473 9166 9471 +9475 9166 9473 9168 9474 9163 9168 9476 9474 +9477 9169 9478 9169 9475 9478 9169 9166 9475 +9479 9477 9478 9170 9480 9481 9476 9170 9481 +9168 9170 9476 9480 9482 9481 9477 9171 9169 +9172 9171 9477 9479 9172 9477 9483 9172 9479 +9174 9480 9170 9174 9175 9480 9175 9482 9480 +9175 9484 9482 9483 9176 9172 9485 9176 9483 +9178 9484 9175 9178 9486 9484 9485 9179 9176 +9487 9179 9485 9180 9486 9178 9180 9488 9486 +9487 9181 9179 9489 9181 9487 9182 9488 9180 +9182 9490 9488 9489 9184 9181 9491 9184 9489 +9185 9490 9182 9185 9492 9490 9493 9494 9495 +9495 9496 9186 9491 9495 9186 9184 9491 9186 +9495 9491 9493 9496 9187 9186 9189 9492 9185 +9189 9497 9492 9494 9498 9495 9499 9500 9498 +9500 9495 9498 9500 9496 9495 9500 9191 9187 +9500 9187 9496 9192 9497 9189 9192 9501 9497 +9499 9502 9500 9502 9503 9504 9502 9504 9193 +9500 9502 9193 9191 9500 9193 9505 9194 9193 +9505 9193 9504 9505 9195 9194 9506 9507 9198 +9506 9198 9197 9192 9198 9501 9501 9198 9507 +9503 9508 9504 9504 9508 9505 9505 9508 9509 +9509 9510 9511 9509 9511 9199 9505 9509 9199 +9195 9505 9199 9512 9200 9199 9512 9199 9511 +9512 9201 9200 9203 9513 9204 9513 9514 9204 +9514 9197 9204 9514 9506 9197 9510 9515 9511 +9511 9515 9512 9512 9515 9516 9512 9516 9201 +9516 9205 9201 9516 9517 9205 9206 9205 9517 +9206 9517 9518 9519 9520 9513 9203 9519 9513 +9208 9519 9203 9513 9520 9514 9209 9206 9521 +9206 9522 9521 9206 9518 9522 9523 9210 9209 +9523 9209 9521 9523 9211 9210 9524 9525 9214 +9524 9214 9213 9208 9214 9519 9519 9214 9525 +9522 9526 9521 9521 9526 9523 9523 9526 9527 +9523 9527 9211 9527 9215 9211 9527 9528 9215 +9528 9529 9530 9216 9528 9530 9215 9528 9216 +9531 9217 9216 9531 9216 9530 9532 9218 9217 +9532 9217 9531 9218 9532 9533 9534 9218 9533 +9219 9218 9534 9219 9535 9220 9534 9535 9219 +9220 9536 9221 9535 9536 9220 9221 9537 9222 +9536 9537 9221 9537 9538 9539 9223 9537 9539 +9222 9537 9223 9539 9224 9223 9540 9541 9226 +9540 9226 9225 9226 9542 9227 9541 9542 9226 +9227 9543 9228 9542 9543 9227 9213 9228 9524 +9524 9228 9543 9529 9544 9530 9530 9544 9531 +9531 9544 9545 9531 9545 9532 9532 9545 9546 +9532 9546 9533 9538 9547 9539 9539 9547 9224 +9547 9229 9224 9547 9548 9229 9225 9229 9540 +9540 9229 9548 9549 9550 9230 9230 9550 9231 +9551 9230 9232 9551 9549 9230 9552 9232 9233 +9552 9551 9232 9553 9233 9234 9553 9552 9233 +9554 9234 9235 9554 9553 9234 9235 9236 9554 +9554 9236 9555 9556 9557 9237 9237 9557 9238 +9558 9237 9239 9558 9556 9237 9559 9239 9240 +9559 9558 9239 9560 9240 9241 9560 9559 9240 +9561 9241 9242 9561 9560 9241 9562 9242 9243 +9562 9561 9242 9563 9243 9244 9563 9562 9243 +9564 9244 9245 9564 9563 9244 9245 9246 9564 +9564 9246 9565 9566 9567 9247 9247 9567 9248 +9247 9249 9566 9566 9249 9568 9569 9570 9253 +9253 9570 9252 9571 9253 9254 9571 9569 9253 +9572 9254 9255 9572 9571 9254 9255 9231 9550 +9255 9550 9572 9256 9555 9236 9256 9573 9555 +9574 9575 9257 9257 9575 9258 9576 9257 9259 +9576 9574 9257 9259 9238 9557 9259 9557 9576 +9577 9565 9246 9577 9246 9260 9578 9260 9261 +9578 9577 9260 9579 9261 9262 9579 9578 9261 +9580 9262 9263 9580 9579 9262 9581 9263 9264 +9581 9580 9263 9582 9264 9265 9582 9581 9264 +9583 9265 9266 9583 9582 9265 9584 9266 9267 +9584 9583 9266 9585 9267 9268 9585 9584 9267 +9268 9248 9567 9268 9567 9585 9586 9568 9249 +9586 9249 9269 9587 9269 9270 9587 9586 9269 +9270 9252 9570 9270 9570 9587 9588 9573 9256 +9588 9256 9271 9271 9272 9588 9588 9272 9589 +9575 9273 9258 9590 9273 9575 9591 9592 9593 +9592 9589 9593 9589 9274 9593 9589 9272 9274 +9590 9275 9273 9594 9275 9590 9591 9593 9277 +9276 9591 9277 9595 9591 9276 9277 9593 9274 +9594 9596 9275 9275 9596 9278 9279 9278 9596 +9279 9596 9597 9280 9595 9276 9280 9598 9595 +9597 9281 9279 9599 9281 9597 9282 9598 9280 +9282 9600 9598 9599 9283 9281 9601 9283 9599 +9284 9600 9282 9284 9602 9600 9603 9285 9604 +9285 9601 9604 9285 9283 9601 9605 9603 9604 +9286 9602 9284 9286 9606 9602 9603 9287 9285 +9288 9287 9603 9605 9288 9603 9607 9288 9605 +9289 9606 9286 9289 9608 9606 9607 9290 9288 +9609 9290 9607 9291 9608 9289 9291 9610 9608 +9609 9292 9290 9611 9292 9609 9293 9610 9291 +9293 9612 9610 9294 9292 9613 9292 9614 9613 +9292 9611 9614 9613 9295 9294 9296 9612 9293 +9296 9615 9612 9613 9614 9297 9614 9298 9297 +9614 9616 9298 9613 9297 9295 9299 9615 9296 +9299 9617 9615 9616 9300 9298 9618 9300 9616 +9619 9620 9302 9619 9302 9301 9299 9302 9617 +9617 9302 9620 9618 9303 9300 9621 9303 9618 +9304 9619 9301 9304 9622 9619 9621 9305 9303 +9623 9305 9621 9306 9622 9304 9306 9624 9622 +9623 9307 9305 9625 9307 9623 9308 9624 9306 +9308 9626 9624 9625 9309 9307 9627 9309 9625 +9628 9626 9308 9628 9308 9311 9311 9312 9628 +9628 9312 9629 9627 9313 9309 9630 9313 9627 +9314 9629 9312 9314 9631 9629 9630 9315 9313 +9632 9315 9630 9316 9631 9314 9316 9633 9631 +9632 9317 9315 9634 9317 9632 9318 9633 9316 +9318 9635 9633 9634 9319 9317 9636 9319 9634 +9320 9635 9318 9320 9637 9635 9638 9322 9321 +9639 9638 9321 9319 9639 9321 9636 9639 9319 +9323 9637 9320 9323 9640 9637 9638 9325 9322 +9326 9325 9638 9639 9326 9638 9641 9326 9639 +9327 9640 9323 9327 9642 9640 9641 9328 9326 +9643 9328 9641 9329 9642 9327 9329 9644 9642 +9643 9330 9328 9645 9330 9643 9331 9644 9329 +9331 9646 9644 9645 9332 9330 9647 9332 9645 +9333 9646 9331 9333 9648 9646 9647 9334 9332 +9649 9334 9647 9335 9648 9333 9335 9650 9648 +9649 9336 9334 9651 9336 9649 9338 9650 9335 +9338 9652 9650 9651 9339 9336 9653 9339 9651 +9654 9655 9342 9654 9342 9341 9342 9656 9343 +9655 9656 9342 9343 9657 9344 9656 9657 9343 +9344 9658 9345 9657 9658 9344 9345 9659 9346 +9658 9659 9345 9347 9346 9659 9347 9659 9660 +9661 9662 9351 9661 9351 9350 9352 9351 9662 +9352 9662 9663 9354 9664 9355 9664 9665 9355 +9665 9338 9355 9665 9652 9338 9653 9356 9339 +9666 9356 9653 9667 9668 9359 9667 9359 9358 +9359 9669 9360 9668 9669 9359 9360 9670 9361 +9669 9670 9360 9361 9671 9362 9670 9671 9361 +9341 9362 9654 9654 9362 9671 9660 9672 9347 +9347 9672 9363 9363 9673 9364 9672 9673 9363 +9350 9364 9661 9661 9364 9673 9663 9674 9352 +9352 9674 9365 9365 9675 9366 9674 9675 9365 +9366 9676 9367 9675 9676 9366 9367 9677 9368 +9676 9677 9367 9368 9678 9369 9677 9678 9368 +9369 9679 9370 9678 9679 9369 9370 9680 9371 +9679 9680 9370 9354 9371 9664 9371 9681 9664 +9371 9680 9681 9664 9681 9665 9666 9682 9356 +9356 9682 9372 9373 9372 9682 9373 9682 9683 +9684 9685 9375 9684 9375 9374 9375 9686 9376 +9685 9686 9375 9376 9687 9377 9686 9687 9376 +9358 9377 9667 9667 9377 9687 9683 9688 9373 +9373 9688 9378 9378 9689 9379 9688 9689 9378 +9379 9690 9380 9689 9690 9379 9380 9691 9381 +9690 9691 9380 9374 9381 9684 9684 9381 9691 +9692 9383 9382 9692 9382 9693 9382 9694 9693 +9382 9384 9694 9695 9384 9385 9695 9694 9384 +9696 9385 9386 9696 9695 9385 9697 9386 9387 +9697 9696 9386 9698 9697 9699 9697 9388 9699 +9697 9387 9388 9389 9699 9388 9700 9392 9393 +9700 9393 9701 9393 9702 9701 9393 9394 9702 +9703 9394 9395 9703 9702 9394 9395 9396 9703 +9703 9396 9704 9705 9398 9397 9705 9397 9706 +9397 9707 9706 9397 9399 9707 9399 9383 9692 +9708 9399 9692 9707 9399 9708 9708 9692 9693 +9709 9698 9699 9709 9699 9389 9400 9709 9389 +9710 9709 9400 9711 9400 9401 9711 9710 9400 +9712 9711 9713 9711 9402 9713 9711 9401 9402 +9403 9714 9402 9402 9714 9713 9404 9715 9403 +9403 9715 9714 9405 9716 9404 9404 9716 9715 +9406 9717 9405 9405 9717 9716 9718 9717 9406 +9718 9406 9392 9719 9718 9392 9700 9719 9392 +9719 9700 9701 9720 9704 9396 9720 9396 9407 +9721 9407 9408 9721 9720 9407 9722 9408 9409 +9722 9721 9408 9723 9722 9724 9722 9410 9724 +9722 9409 9410 9411 9724 9410 9725 9413 9412 +9726 9725 9412 9726 9412 9398 9727 9726 9398 +9705 9727 9398 9727 9705 9706 9728 9712 9713 +9728 9713 9714 9728 9714 9729 9729 9714 9715 +9729 9715 9730 9730 9715 9716 9730 9716 9731 +9731 9716 9717 9731 9717 9732 9732 9717 9718 +9733 9723 9724 9733 9724 9411 9414 9733 9411 +9734 9733 9414 9735 9414 9416 9735 9734 9414 +9416 9417 9735 9735 9417 9736 9737 9738 9725 +9738 9413 9725 9738 9418 9413 9737 9725 9726 +9739 9736 9417 9739 9417 9419 9419 9420 9739 +9739 9420 9740 9741 9742 9421 9421 9742 9422 +9421 9418 9738 9421 9738 9741 9743 9740 9420 +9743 9420 9424 9424 9425 9743 9743 9425 9744 +9745 9427 9426 9745 9426 9746 9426 9747 9746 +9426 9430 9747 9430 9422 9742 9430 9742 9747 +9431 9744 9425 9431 9748 9744 9749 9750 9745 +9750 9427 9745 9750 9432 9427 9749 9745 9746 +9433 9751 9752 9748 9433 9752 9431 9433 9748 +9751 9753 9752 9750 9434 9432 9754 9434 9750 +9435 9755 9436 9755 9756 9436 9436 9756 9433 +9756 9757 9433 9757 9751 9433 9751 9757 9753 +9754 9437 9434 9758 9437 9754 9438 9759 9755 +9438 9755 9435 9756 9755 9760 9760 9755 9759 +9761 9440 9439 9762 9761 9439 9437 9762 9439 +9758 9762 9437 9442 9759 9438 9442 9443 9759 +9443 9760 9759 9443 9763 9760 9761 9444 9440 +9445 9444 9761 9762 9445 9761 9764 9445 9762 +9447 9763 9443 9447 9765 9763 9764 9448 9445 +9766 9448 9764 9767 9768 9450 9767 9450 9449 +9447 9450 9765 9765 9450 9768 9769 9451 9770 +9451 9766 9770 9451 9448 9766 9771 9769 9770 +9772 9773 9774 9773 9767 9774 9767 9452 9774 +9767 9449 9452 9769 9453 9451 9775 9776 9454 +9775 9454 9453 9771 9775 9453 9769 9771 9453 +9777 9776 9775 9772 9774 9778 9772 9778 9779 +9455 9778 9452 9452 9778 9774 9780 9456 9776 +9776 9456 9454 9776 9777 9781 9782 9776 9781 +9780 9776 9782 9783 9782 9781 9779 9778 9784 +9779 9784 9785 9458 9784 9455 9455 9784 9778 +9780 9460 9456 9460 9780 9461 9780 9786 9461 +9780 9782 9786 9782 9783 9787 9782 9787 9786 +9785 9784 9463 9462 9785 9463 9788 9785 9462 +9463 9784 9458 9789 9465 9786 9786 9465 9461 +9786 9787 9790 9791 9786 9790 9789 9786 9791 +9792 9791 9790 9466 9788 9462 9466 9793 9788 +9794 9467 9789 9789 9467 9465 9795 9789 9791 +9794 9789 9795 9791 9792 9796 9791 9796 9795 +9797 9798 9469 9797 9469 9468 9466 9469 9793 +9793 9469 9798 9794 9472 9467 9472 9794 9473 +9794 9799 9473 9794 9795 9799 9795 9796 9800 +9795 9800 9799 9474 9797 9468 9474 9801 9797 +9802 9475 9799 9799 9475 9473 9799 9800 9803 +9799 9803 9802 9476 9801 9474 9476 9804 9801 +9802 9478 9475 9479 9478 9802 9803 9479 9802 +9805 9479 9803 9476 9481 9806 9807 9476 9806 +9804 9476 9807 9482 9806 9481 9805 9483 9479 +9808 9483 9805 9807 9806 9809 9807 9809 9810 +9484 9809 9482 9482 9809 9806 9808 9485 9483 +9811 9485 9808 9810 9809 9812 9810 9812 9813 +9486 9812 9484 9484 9812 9809 9814 9815 9816 +9811 9814 9816 9487 9811 9816 9485 9811 9487 +9813 9812 9817 9813 9817 9818 9488 9817 9486 +9486 9817 9812 9816 9815 9819 9819 9815 9820 +9819 9489 9487 9819 9487 9816 9818 9817 9821 +9818 9821 9822 9490 9821 9488 9488 9821 9817 +9819 9820 9823 9823 9820 9824 9823 9491 9489 +9823 9489 9819 9822 9821 9825 9822 9825 9826 +9492 9825 9490 9490 9825 9821 9827 9828 9829 +9827 9829 9824 9824 9829 9823 9829 9494 9823 +9494 9493 9823 9823 9493 9491 9826 9825 9830 +9826 9830 9831 9497 9830 9492 9492 9830 9825 +9828 9832 9829 9832 9833 9834 9832 9834 9498 +9829 9832 9498 9494 9829 9498 9834 9499 9498 +9835 9836 9837 9837 9836 9831 9830 9837 9831 +9838 9837 9830 9501 9838 9497 9497 9838 9830 +9834 9833 9839 9839 9833 9840 9841 9502 9499 +9834 9841 9499 9839 9841 9834 9841 9503 9502 +9842 9835 9837 9506 9843 9507 9843 9837 9507 +9837 9838 9507 9843 9842 9837 9507 9838 9501 +9840 9844 9839 9845 9846 9844 9846 9839 9844 +9846 9841 9839 9847 9508 9503 9841 9847 9503 +9846 9847 9841 9848 9509 9508 9848 9508 9847 +9848 9510 9509 9514 9843 9506 9514 9849 9843 +9845 9850 9846 9846 9850 9847 9847 9850 9851 +9847 9851 9848 9848 9851 9852 9852 9853 9854 +9852 9854 9515 9848 9852 9515 9510 9848 9515 +9855 9516 9515 9855 9515 9854 9856 9517 9516 +9856 9516 9855 9856 9518 9517 9519 9857 9520 +9857 9858 9520 9858 9514 9520 9858 9849 9514 +9853 9859 9854 9854 9859 9855 9855 9859 9860 +9855 9860 9856 9856 9860 9861 9518 9856 9861 +9862 9518 9861 9522 9518 9862 9524 9863 9525 +9863 9864 9525 9525 9864 9519 9864 9865 9519 +9865 9857 9519 9857 9865 9858 9526 9522 9866 +9522 9867 9866 9522 9862 9867 9868 9527 9526 +9868 9526 9866 9869 9528 9527 9869 9527 9868 +9869 9529 9528 9533 9870 9534 9871 9535 9534 +9871 9534 9870 9872 9536 9535 9872 9535 9871 +9873 9537 9536 9873 9536 9872 9873 9538 9537 +9540 9874 9541 9541 9874 9875 9876 9541 9875 +9542 9541 9876 9542 9877 9543 9876 9877 9542 +9524 9543 9863 9543 9878 9863 9543 9877 9878 +9863 9878 9864 9867 9879 9866 9866 9879 9868 +9868 9879 9880 9868 9880 9869 9869 9880 9881 +9869 9881 9529 9881 9544 9529 9881 9882 9544 +9544 9883 9545 9882 9883 9544 9545 9884 9546 +9883 9884 9545 9533 9546 9870 9546 9885 9870 +9546 9884 9885 9870 9885 9871 9871 9885 9886 +9871 9886 9872 9872 9886 9887 9872 9887 9873 +9873 9887 9888 9873 9888 9538 9888 9547 9538 +9888 9889 9547 9547 9890 9548 9889 9890 9547 +9540 9548 9874 9548 9891 9874 9548 9890 9891 +9874 9891 9875 9892 9893 9894 9895 9896 9894 +9894 9896 9892 9895 9897 9896 9898 9899 9900 +9550 9549 9900 9549 9898 9900 9549 9901 9898 +9902 9549 9551 9902 9901 9549 9552 9894 9893 +9902 9552 9893 9551 9552 9902 9894 9552 9553 +9894 9553 9895 9903 9897 9895 9553 9903 9895 +9554 9903 9553 9554 9555 9903 9903 9555 9904 +9905 9906 9907 9557 9556 9907 9556 9905 9907 +9556 9908 9905 9909 9556 9558 9909 9908 9556 +9910 9558 9559 9910 9909 9558 9911 9559 9560 +9911 9910 9559 9912 9560 9561 9912 9911 9560 +9913 9561 9562 9913 9912 9561 9914 9562 9563 +9914 9913 9562 9915 9914 9916 9914 9564 9916 +9914 9563 9564 9565 9916 9564 9917 9918 9566 +9566 9918 9567 9566 9568 9917 9917 9568 9919 +9920 9921 9569 9569 9921 9570 9922 9569 9571 +9922 9920 9569 9572 9900 9899 9922 9572 9899 +9571 9572 9922 9900 9572 9550 9573 9904 9555 +9573 9923 9904 9924 9925 9574 9574 9925 9575 +9576 9907 9906 9924 9576 9906 9574 9576 9924 +9907 9576 9557 9926 9915 9916 9926 9916 9565 +9577 9926 9565 9927 9926 9577 9928 9577 9578 +9928 9927 9577 9929 9578 9579 9929 9928 9578 +9930 9579 9580 9930 9929 9579 9931 9580 9581 +9931 9930 9580 9932 9581 9582 9932 9931 9581 +9933 9582 9583 9933 9932 9582 9934 9583 9584 +9934 9933 9583 9935 9584 9585 9935 9934 9584 +9585 9567 9918 9585 9918 9935 9936 9919 9568 +9936 9568 9586 9937 9586 9587 9937 9936 9586 +9587 9570 9921 9587 9921 9937 9938 9923 9573 +9938 9573 9588 9588 9589 9938 9938 9589 9939 +9925 9590 9575 9940 9590 9925 9941 9942 9592 +9941 9592 9591 9589 9592 9939 9939 9592 9942 +9940 9594 9590 9943 9594 9940 9595 9941 9591 +9595 9944 9941 9596 9594 9945 9594 9946 9945 +9594 9943 9946 9945 9597 9596 9598 9944 9595 +9598 9947 9944 9946 9948 9945 9597 9945 9948 +9949 9597 9948 9599 9597 9949 9600 9947 9598 +9600 9950 9947 9949 9601 9599 9951 9601 9949 +9602 9950 9600 9602 9952 9950 9951 9953 9601 +9601 9953 9604 9605 9604 9953 9605 9953 9954 +9606 9952 9602 9606 9955 9952 9954 9607 9605 +9956 9607 9954 9608 9955 9606 9608 9957 9955 +9956 9609 9607 9958 9609 9956 9610 9957 9608 +9610 9959 9957 9958 9611 9609 9960 9611 9958 +9612 9959 9610 9612 9961 9959 9960 9614 9611 +9962 9614 9960 9615 9961 9612 9615 9963 9961 +9962 9616 9614 9964 9616 9962 9965 9966 9967 +9966 9963 9967 9963 9617 9967 9963 9615 9617 +9964 9618 9616 9968 9618 9964 9965 9967 9620 +9619 9965 9620 9969 9965 9619 9620 9967 9617 +9968 9621 9618 9970 9621 9968 9622 9969 9619 +9622 9971 9969 9970 9623 9621 9972 9623 9970 +9624 9973 9974 9971 9624 9974 9622 9624 9971 +9973 9975 9974 9972 9625 9623 9976 9625 9972 +9626 9977 9973 9626 9973 9624 9975 9973 9978 +9978 9973 9977 9976 9627 9625 9979 9627 9976 +9628 9977 9626 9628 9629 9977 9629 9978 9977 +9629 9980 9978 9979 9630 9627 9981 9630 9979 +9631 9980 9629 9631 9982 9980 9981 9632 9630 +9983 9632 9981 9633 9982 9631 9633 9984 9982 +9983 9634 9632 9985 9634 9983 9635 9984 9633 +9635 9986 9984 9985 9636 9634 9987 9636 9985 +9637 9986 9635 9637 9988 9986 9987 9639 9636 +9989 9639 9987 9640 9988 9637 9640 9990 9988 +9989 9641 9639 9991 9641 9989 9642 9990 9640 +9642 9992 9990 9991 9643 9641 9993 9643 9991 +9644 9992 9642 9644 9994 9992 9993 9645 9643 +9995 9645 9993 9646 9994 9644 9646 9996 9994 +9995 9647 9645 9997 9647 9995 9648 9996 9646 +9648 9998 9996 9997 9649 9647 9999 9649 9997 +9650 9998 9648 9650 10000 9998 9999 9651 9649 +10001 9651 9999 9652 10000 9650 9652 10002 10000 +10001 9653 9651 10003 9653 10001 10004 10005 10006 +9655 9654 10006 10005 9655 10006 10007 9655 10005 +9655 10008 9656 10007 10008 9655 9656 10009 9657 +10008 10009 9656 9657 10010 9658 10009 10010 9657 +10010 10011 10012 9659 10010 10012 9658 10010 9659 +9660 9659 10012 10013 10014 10015 10016 10017 10013 +10016 10013 10015 9662 9661 10017 10016 9662 10017 +10018 9662 10016 10019 10020 9663 9662 10019 9663 +10018 10019 9662 10021 10020 10019 9665 10002 9652 +9665 10022 10002 10003 9666 9653 10023 9666 10003 +10024 10025 9668 10024 9668 9667 9668 10026 9669 +10025 10026 9668 9669 10027 9670 10026 10027 9669 +10006 9671 10004 9671 10027 10004 9671 9670 10027 +9671 10006 9654 10011 10028 10012 10014 10013 10028 +10013 9672 10028 9660 10012 10028 9660 10028 9672 +9672 10013 9673 9673 10013 10017 9673 10017 9661 +10020 9674 9663 10021 10029 10020 10029 9674 10020 +10029 9675 9674 9675 10030 9676 10029 10030 9675 +9676 10031 9677 10030 10031 9676 10031 10032 10033 +9678 10031 10033 9677 10031 9678 10034 9679 9678 +10034 9678 10033 9679 10034 10035 10036 9679 10035 +9680 9679 10036 10036 10037 10038 9681 10036 10038 +9680 10036 9681 10038 10039 9681 10039 9665 9681 +10039 10022 9665 10023 10040 9666 9666 10040 9682 +9683 9682 10040 9683 10040 10041 10042 10043 9685 +10042 9685 9684 9685 10044 9686 10043 10044 9685 +9686 10045 9687 10044 10045 9686 9667 9687 10024 +10024 9687 10045 10032 10046 10033 10033 10046 10034 +10034 10046 10047 10034 10047 10035 10037 10048 10038 +10038 10048 10039 10041 10049 9683 9683 10049 9688 +9688 10050 9689 10049 10050 9688 9689 10051 9690 +10050 10051 9689 9690 10052 9691 10051 10052 9690 +9684 9691 10042 10042 9691 10052 10053 9693 9694 +9695 10054 9694 9694 10054 10053 9696 10055 9695 +9695 10055 10054 9697 10056 9696 9696 10056 10055 +9698 10056 9697 10057 9701 9702 9703 10058 9702 +9702 10058 10057 9704 10058 9703 10059 9706 9707 +10059 9707 10060 9707 10061 10060 9707 9708 10061 +9708 9693 10053 10062 9708 10053 10061 9708 10062 +10062 10053 10054 10062 10054 10063 10063 10054 10055 +10063 10055 10064 10064 10055 10056 10064 10056 10065 +10066 10065 10067 10067 10065 9709 10065 10056 9709 +10056 9698 9709 9710 10068 9709 9709 10068 10067 +9711 10069 9710 9710 10069 10068 9712 10069 9711 +10070 9718 9719 9701 10057 9719 10057 10070 9719 +10057 10071 10070 10072 10057 10058 10071 10057 10072 +9720 10073 9704 10073 10058 9704 10073 10072 10058 +9721 10074 9720 9720 10074 10073 9722 10075 9721 +9721 10075 10074 9723 10075 9722 10076 10077 9727 +9727 10077 9726 9727 9706 10059 10078 9727 10059 +10076 9727 10078 10078 10059 10060 10079 10066 10067 +10079 10067 10068 10079 10068 10080 10080 10068 10069 +10080 10069 10081 10081 10069 9712 9728 10081 9712 +10082 10081 9728 10083 10082 10084 10082 9729 10084 +10082 9728 9729 9730 10085 9729 9729 10085 10084 +9731 10086 9730 9730 10086 10085 9732 10087 9731 +9731 10087 10086 10088 10087 9732 10088 9732 9718 +10089 10088 9718 10070 10089 9718 10089 10070 10071 +10089 10071 10090 10091 10092 10090 10071 10091 10090 +10072 10091 10071 10073 10093 10094 10091 10073 10094 +10072 10073 10091 10093 10073 10074 10093 10074 10095 +10095 10074 10075 10095 10075 10096 10096 10075 9723 +9733 10096 9723 10097 10096 9733 10098 9733 9734 +10098 10097 9733 10099 10098 10100 10098 9735 10100 +10098 9734 9735 9736 10100 9735 10101 10102 9737 +9737 10102 9738 9737 9726 10077 9737 10077 10101 +10103 10083 10084 10103 10084 10085 10103 10085 10104 +10104 10085 10086 10104 10086 10105 10105 10086 10087 +10105 10087 10106 10106 10087 10088 10107 10092 10091 +10107 10091 10094 10108 10099 10100 10108 10100 9736 +9739 10108 9736 10109 10108 9739 9739 9740 10109 +10109 9740 10110 10111 9742 9741 10112 10111 9741 +9738 10112 9741 10102 10112 9738 10113 10110 9740 +10113 9740 9743 9743 9744 10113 10113 9744 10114 +10115 10116 9747 9747 10116 9746 9747 9742 10111 +10117 9747 10111 10115 9747 10117 10117 10111 10112 +9748 10118 10119 10114 9748 10119 9744 9748 10114 +10118 10120 10119 10121 9750 9749 10122 10121 9749 +9746 10122 9749 10116 10122 9746 10118 9748 9752 +10123 10124 10125 10124 10123 9753 10123 10120 9753 +10118 9752 9753 10118 9753 10120 10126 10127 10121 +10127 9750 10121 10127 9754 9750 10126 10121 10122 +10125 10124 9757 9756 10125 9757 10128 10125 9756 +9757 10124 9753 10127 9758 9754 10129 9758 10127 +9760 10128 9756 9760 10130 10128 10129 9762 9758 +10131 9762 10129 9763 10130 9760 9763 10132 10130 +10133 9764 10134 9764 10131 10134 9764 9762 10131 +10135 10133 10134 10136 10137 10138 10137 10132 10138 +10132 9765 10138 10132 9763 9765 10139 9766 10133 +10133 9766 9764 10133 10135 10140 10133 10140 10139 +10141 10142 10143 10144 10143 10142 10136 10138 9768 +9767 10136 9768 10145 10136 9767 9768 10138 9765 +10139 9770 9766 9771 9770 10139 10140 9771 10139 +10146 9771 10140 10147 10148 10149 10149 10148 10141 +10143 10149 10141 10150 10149 10143 10151 9773 9772 +10143 10151 9772 10150 10143 9772 10151 10143 10144 +9767 9773 10145 10145 9773 10151 10146 10152 9771 +9771 10152 9775 9777 9775 10152 9777 10152 10153 +10147 10149 10154 10147 10154 10155 10156 10149 10150 +10154 10149 10156 9779 10156 9772 9772 10156 10150 +10153 10157 9777 9777 10157 9781 9783 9781 10157 +9783 10157 10158 10155 10154 10159 10155 10159 10160 +10161 10154 10156 10159 10154 10161 9785 10161 9779 +9779 10161 10156 10158 9787 9783 10162 9787 10158 +10160 10159 10163 10160 10163 10164 10165 10159 10161 +10163 10159 10165 9788 10165 9785 9785 10165 10161 +10162 10166 9787 9787 10166 9790 9792 9790 10166 +9792 10166 10167 10164 10163 10168 10164 10168 10169 +10170 10163 10165 10168 10163 10170 9793 10170 9788 +9788 10170 10165 10171 9796 10172 9796 10167 10172 +9796 9792 10167 10173 10171 10172 10174 10175 10176 +10176 10175 10169 10168 10176 10169 10177 10176 10168 +9797 10177 9798 10177 10170 9798 10177 10168 10170 +9798 10170 9793 10178 9800 10171 10171 9800 9796 +10171 10173 10179 10171 10179 10178 10174 10176 10180 +10174 10180 10181 10182 10176 10177 10180 10176 10182 +9801 10182 9797 9797 10182 10177 10183 10184 10178 +10184 9800 10178 10184 9803 9800 10183 10178 10179 +10181 10180 10185 10181 10185 10186 10187 10180 10182 +10185 10180 10187 9804 10187 9801 9801 10187 10182 +10184 9805 9803 10188 9805 10184 10186 10185 10189 +10186 10189 10190 10191 10185 10187 10189 10185 10191 +9807 10191 9804 9804 10191 10187 10188 9808 9805 +10192 9808 10188 10193 10190 10189 10194 10195 10193 +10189 10194 10193 10191 10194 10189 9810 10194 9807 +9807 10194 10191 10196 10197 10198 10192 10196 10198 +9811 10192 10198 9808 10192 9811 10195 10194 10199 +10195 10199 10200 9813 10199 9810 9810 10199 10194 +10198 10197 9814 10197 9815 9814 10197 10201 9815 +10198 9814 9811 10200 10199 10202 10200 10202 10203 +9818 10202 9813 9813 10202 10199 10201 9820 9815 +10204 9820 10201 10203 10202 10205 10203 10205 10206 +9822 10205 9818 9818 10205 10202 10204 9824 9820 +10207 9824 10204 10208 10206 10205 10205 9822 10208 +9822 10209 10208 9822 9826 10209 10210 10211 10212 +9828 9827 10212 9827 10210 10212 9827 10213 10210 +9827 9824 10207 9827 10207 10213 9831 10209 9826 +9831 10214 10209 10212 10211 10215 10215 10211 10216 +10217 9832 9828 10212 10217 9828 10215 10217 10212 +10217 9833 9832 10218 10219 9836 10218 9836 9835 +9831 9836 10214 10214 9836 10219 10215 10216 10220 +10220 10216 10221 10215 10222 10217 10215 10220 10222 +10222 9840 9833 10222 9833 10217 10223 10218 9835 +10223 9835 9842 9842 9843 10223 10223 9843 10224 +10221 10225 10220 10226 10227 10225 10227 10220 10225 +10227 10222 10220 10228 9844 9840 10222 10228 9840 +10227 10228 10222 10228 9845 9844 9849 10224 9843 +9849 10229 10224 10226 10230 10227 10231 10232 10230 +10232 10227 10230 10232 10228 10227 10233 9850 9845 +10228 10233 9845 10232 10233 10228 10234 9851 9850 +10234 9850 10233 10235 9852 9851 10235 9851 10234 +10235 9853 9852 9858 10229 9849 9858 10236 10229 +10231 10237 10232 10232 10237 10233 10233 10237 10238 +10233 10238 10234 10234 10238 10239 10234 10239 10235 +10235 10239 10240 10240 10241 10242 10240 10242 9859 +10235 10240 9859 9853 10235 9859 10243 9860 9859 +10243 9859 10242 10244 9861 9860 10244 9860 10243 +10244 9862 9861 9864 10245 9865 10245 10246 9865 +10246 9858 9865 10246 10236 9858 10241 10247 10242 +10242 10247 10243 10243 10247 10248 10249 10250 10248 +10250 10243 10248 10250 10244 10243 10250 9867 9862 +10250 9862 10244 9875 10251 9876 9876 10251 10252 +10253 9876 10252 9877 9876 10253 9877 10254 9878 +10253 10254 9877 9864 9878 10245 9878 10255 10245 +9878 10254 10255 10245 10255 10246 10249 10256 10250 +10250 10256 9867 10256 9879 9867 10256 10257 9879 +9879 10258 9880 10257 10258 9879 9880 10259 9881 +10258 10259 9880 10259 10260 10261 9882 10259 10261 +9881 10259 9882 10262 9883 9882 10262 9882 10261 +10263 9884 9883 10263 9883 10262 10264 9885 9884 +10264 9884 10263 9885 10264 10265 10266 9885 10265 +9886 9885 10266 9886 10267 9887 10266 10267 9886 +9887 10268 9888 10267 10268 9887 10268 10269 10270 +9889 10268 10270 9888 10268 9889 9889 10270 10271 +10272 9889 10271 9890 9889 10272 9890 10273 9891 +10272 10273 9890 9875 9891 10251 9891 10274 10251 +9891 10273 10274 10251 10274 10252 10260 10275 10261 +10261 10275 10262 10262 10275 10276 10262 10276 10263 +10263 10276 10277 10263 10277 10264 10264 10277 10278 +10264 10278 10265 10269 10279 10270 10270 10279 10271 +10280 10281 10282 9893 9892 10282 9892 10280 10282 +9892 10283 10280 10284 9892 9896 10284 10283 9892 +10285 10286 9897 10286 9896 9897 10286 10284 9896 +10285 10287 10286 10288 10289 9898 9898 10289 9899 +10290 9898 9901 10290 10288 9898 9902 10282 10281 +10290 9902 10281 9901 9902 10290 10282 9902 9893 +9903 10285 9897 9903 9904 10285 9904 10287 10285 +9904 10291 10287 10292 10293 9905 9905 10293 9906 +10294 9905 9908 10294 10292 9905 10295 9908 9909 +10295 10294 9908 10296 9909 9910 10296 10295 9909 +10297 9910 9911 10297 10296 9910 10298 9911 9912 +10298 10297 9911 10299 9912 9913 10299 10298 9912 +10300 9913 9914 10300 10299 9913 9914 9915 10300 +10300 9915 10301 10302 10303 9917 9917 10303 9918 +9917 9919 10302 10302 9919 10304 10305 10306 10307 +9921 9920 10307 9920 10305 10307 9920 10308 10305 +10309 9920 9922 10309 10308 9920 9922 9899 10289 +9922 10289 10309 9923 10310 10311 10291 9923 10311 +9904 9923 10291 10310 10312 10311 10313 10314 9924 +9924 10314 9925 9924 9906 10293 9924 10293 10313 +10315 10301 9915 10315 9915 9926 10316 9926 9927 +10316 10315 9926 10317 9927 9928 10317 10316 9927 +10318 9928 9929 10318 10317 9928 10319 9929 9930 +10319 10318 9929 10320 9930 9931 10320 10319 9930 +10321 9931 9932 10321 10320 9931 10322 9932 9933 +10322 10321 9932 10323 9933 9934 10323 10322 9933 +10324 9934 9935 10324 10323 9934 9935 9918 10303 +9935 10303 10324 10325 10304 9919 10325 9919 9936 +9937 10307 10306 10325 9937 10306 9936 9937 10325 +10307 9937 9921 9938 10310 9923 9938 9939 10310 +9939 10312 10310 9939 10326 10312 10314 9940 9925 +10327 9940 10314 10328 10329 9942 10328 9942 9941 +9939 9942 10326 10326 9942 10329 10327 9943 9940 +10330 9943 10327 9944 10328 9941 9944 10331 10328 +10330 9946 9943 10332 9946 10330 9947 10331 9944 +9947 10333 10331 10332 10334 9946 9946 10334 9948 +9949 9948 10334 9949 10334 10335 9950 10333 9947 +9950 10336 10333 10335 9951 9949 10337 9951 10335 +9952 10336 9950 9952 10338 10336 10337 10339 9951 +9951 10339 9953 9954 9953 10339 9954 10339 10340 +9955 10338 9952 9955 10341 10338 10340 9956 9954 +10342 9956 10340 9957 10341 9955 9957 10343 10341 +10342 9958 9956 10344 9958 10342 9959 10343 9957 +9959 10345 10343 10344 9960 9958 10346 9960 10344 +9961 10345 9959 9961 10347 10345 10346 9962 9960 +10348 9962 10346 9963 10347 9961 9963 10349 10347 +10348 9964 9962 10350 9964 10348 10351 10352 9966 +10351 9966 9965 9963 9966 10349 10349 9966 10352 +10350 9968 9964 10353 9968 10350 9969 10351 9965 +9969 10354 10351 10353 9970 9968 10355 9970 10353 +9971 10354 9969 9971 10356 10354 10355 9972 9970 +10357 9972 10355 10358 10356 9971 10358 9971 9974 +9974 9975 10358 10358 9975 10359 10357 9976 9972 +10360 9976 10357 9978 10359 9975 9978 10361 10359 +10360 9979 9976 10362 9979 10360 9980 10361 9978 +9980 10363 10361 10362 9981 9979 10364 9981 10362 +9982 10363 9980 9982 10365 10363 10364 9983 9981 +10366 9983 10364 9984 10365 9982 9984 10367 10365 +10366 9985 9983 10368 9985 10366 9986 10367 9984 +9986 10369 10367 10368 9987 9985 10370 9987 10368 +9988 10369 9986 9988 10371 10369 10370 9989 9987 +10372 9989 10370 9990 10371 9988 9990 10373 10371 +10372 9991 9989 10374 9991 10372 9992 10373 9990 +9992 10375 10373 10374 9993 9991 10376 9993 10374 +9994 10375 9992 9994 10377 10375 10376 9995 9993 +10378 9995 10376 9996 10377 9994 9996 10379 10377 +10378 9997 9995 10380 9997 10378 9998 10379 9996 +9998 10381 10379 10380 9999 9997 10382 9999 10380 +10000 10381 9998 10000 10383 10381 10382 10001 9999 +10384 10001 10382 10002 10383 10000 10002 10385 10383 +10384 10003 10001 10386 10003 10384 10387 10388 10005 +10387 10005 10004 10005 10389 10007 10388 10389 10005 +10007 10390 10008 10389 10390 10007 10008 10391 10009 +10390 10391 10008 10009 10392 10010 10391 10392 10009 +10393 10394 10011 10010 10393 10011 10392 10393 10010 +10015 10014 10394 10393 10015 10394 10395 10015 10393 +10015 10396 10016 10395 10396 10015 10016 10397 10018 +10396 10397 10016 10018 10398 10019 10397 10398 10018 +10021 10019 10398 10021 10398 10399 10022 10385 10002 +10022 10400 10385 10386 10023 10003 10401 10023 10386 +10402 10403 10404 10025 10024 10404 10403 10025 10404 +10405 10025 10403 10025 10406 10026 10405 10406 10025 +10026 10407 10027 10406 10407 10026 10004 10027 10387 +10387 10027 10407 10394 10028 10011 10028 10394 10014 +10399 10408 10021 10021 10408 10029 10029 10409 10030 +10408 10409 10029 10030 10410 10031 10409 10410 10030 +10411 10412 10032 10031 10411 10032 10410 10411 10031 +10413 10412 10411 10035 10414 10036 10414 10037 10036 +10039 10400 10022 10039 10415 10400 10401 10416 10023 +10023 10416 10040 10041 10040 10416 10041 10416 10417 +10418 10419 10420 10421 10043 10042 10422 10420 10419 +10043 10421 10423 10424 10043 10423 10044 10043 10424 +10404 10045 10402 10045 10424 10402 10045 10044 10424 +10045 10404 10024 10412 10046 10032 10413 10425 10412 +10425 10046 10412 10425 10047 10046 10047 10425 10035 +10425 10426 10035 10426 10414 10035 10426 10425 10427 +10414 10426 10037 10426 10428 10037 10428 10048 10037 +10426 10429 10428 10039 10048 10415 10415 10048 10428 +10417 10430 10041 10041 10430 10049 10049 10431 10050 +10430 10431 10049 10050 10432 10051 10431 10432 10050 +10420 10052 10418 10052 10432 10418 10052 10051 10432 +10420 10422 10052 10422 10433 10052 10421 10042 10052 +10421 10052 10433 10421 10433 10423 10427 10434 10426 +10426 10434 10429 10435 10060 10061 10062 10436 10061 +10061 10436 10435 10063 10437 10062 10062 10437 10436 +10064 10438 10063 10063 10438 10437 10065 10439 10064 +10064 10439 10438 10066 10439 10065 10440 10077 10076 +10440 10076 10441 10076 10442 10441 10076 10078 10442 +10078 10060 10435 10443 10078 10435 10442 10078 10443 +10443 10435 10436 10443 10436 10444 10444 10436 10437 +10444 10437 10445 10445 10437 10438 10445 10438 10446 +10447 10448 10446 10438 10447 10446 10439 10447 10438 +10079 10449 10066 10449 10439 10066 10449 10447 10439 +10080 10450 10079 10079 10450 10449 10081 10451 10080 +10080 10451 10450 10082 10452 10081 10081 10452 10451 +10083 10452 10082 10453 10088 10089 10090 10454 10089 +10089 10454 10453 10092 10454 10090 10455 10094 10093 +10095 10456 10093 10093 10456 10455 10096 10457 10095 +10095 10457 10456 10097 10458 10096 10096 10458 10457 +10098 10459 10097 10097 10459 10458 10099 10459 10098 +10460 10102 10101 10461 10460 10101 10461 10101 10077 +10462 10461 10077 10440 10462 10077 10462 10440 10441 +10463 10448 10447 10463 10447 10449 10463 10449 10464 +10464 10449 10450 10464 10450 10465 10465 10450 10451 +10465 10451 10466 10467 10468 10466 10451 10467 10466 +10452 10467 10451 10103 10469 10083 10469 10452 10083 +10469 10467 10452 10104 10470 10103 10103 10470 10469 +10105 10471 10104 10104 10471 10470 10106 10472 10105 +10105 10472 10471 10088 10453 10106 10453 10472 10106 +10453 10473 10472 10474 10453 10454 10473 10453 10474 +10107 10475 10092 10475 10454 10092 10475 10474 10454 +10476 10475 10107 10476 10107 10094 10477 10476 10094 +10455 10477 10094 10477 10455 10456 10477 10456 10478 +10478 10456 10457 10478 10457 10479 10479 10457 10458 +10479 10458 10480 10480 10458 10459 10480 10459 10481 +10481 10459 10099 10108 10481 10099 10482 10481 10108 +10483 10482 10484 10482 10109 10484 10482 10108 10109 +10110 10484 10109 10485 10486 10487 10488 10485 10460 +10485 10112 10460 10112 10102 10460 10485 10487 10112 +10488 10460 10461 10489 10468 10467 10490 10491 10489 +10467 10490 10489 10469 10490 10467 10492 10469 10470 +10490 10469 10492 10493 10470 10471 10492 10470 10493 +10494 10471 10472 10493 10471 10494 10495 10472 10473 +10494 10472 10495 10474 10496 10497 10495 10474 10497 +10473 10474 10495 10496 10474 10475 10496 10475 10498 +10498 10475 10476 10499 10483 10484 10499 10484 10110 +10113 10499 10110 10500 10499 10113 10113 10114 10500 +10500 10114 10501 10502 10116 10115 10486 10503 10487 +10503 10115 10487 10115 10117 10487 10115 10503 10502 +10487 10117 10112 10504 10505 10506 10506 10505 10491 +10490 10506 10491 10507 10506 10490 10508 10490 10492 +10507 10490 10508 10509 10492 10493 10508 10492 10509 +10510 10493 10494 10509 10493 10510 10495 10511 10512 +10510 10495 10512 10494 10495 10510 10511 10495 10497 +10513 10501 10114 10513 10114 10119 10119 10120 10513 +10513 10120 10514 10515 10516 10502 10516 10116 10502 +10516 10122 10116 10515 10502 10503 10504 10506 10517 +10504 10517 10518 10519 10506 10507 10517 10506 10519 +10520 10507 10508 10519 10507 10520 10521 10508 10509 +10520 10508 10521 10522 10509 10510 10521 10509 10522 +10510 10512 10523 10510 10523 10522 10524 10525 10123 +10524 10123 10125 10120 10123 10514 10514 10123 10525 +10526 10127 10126 10527 10526 10126 10122 10527 10126 +10516 10527 10122 10528 10518 10517 10529 10530 10528 +10517 10529 10528 10519 10529 10517 10531 10519 10520 +10529 10519 10531 10532 10520 10521 10531 10520 10532 +10533 10521 10522 10532 10521 10533 10522 10523 10534 +10535 10522 10534 10533 10522 10535 10536 10535 10534 +10128 10524 10125 10128 10537 10524 10538 10129 10526 +10526 10129 10127 10526 10527 10539 10526 10539 10538 +10530 10529 10540 10530 10540 10541 10542 10529 10531 +10540 10529 10542 10543 10531 10532 10542 10531 10543 +10544 10532 10533 10543 10532 10544 10545 10533 10535 +10544 10533 10545 10535 10536 10546 10547 10535 10546 +10545 10535 10547 10548 10547 10546 10549 10550 10551 +10550 10537 10551 10537 10130 10551 10537 10128 10130 +10552 10131 10538 10538 10131 10129 10538 10539 10553 +10538 10553 10552 10541 10540 10554 10541 10554 10555 +10556 10540 10542 10554 10540 10556 10557 10542 10543 +10556 10542 10557 10558 10543 10544 10557 10543 10558 +10559 10544 10545 10558 10544 10559 10560 10545 10547 +10559 10545 10560 10547 10548 10561 10547 10561 10560 +10549 10551 10562 10549 10562 10563 10132 10562 10130 +10130 10562 10551 10552 10134 10131 10564 10565 10135 +10564 10135 10134 10553 10564 10134 10552 10553 10134 +10566 10565 10564 10567 10555 10554 10568 10569 10567 +10554 10568 10567 10556 10568 10554 10570 10556 10557 +10568 10556 10570 10571 10557 10558 10570 10557 10571 +10572 10558 10559 10571 10558 10572 10573 10559 10560 +10572 10559 10573 10560 10561 10574 10575 10560 10574 +10573 10560 10575 10576 10575 10574 10577 10578 10579 +10580 10581 10579 10580 10579 10578 10582 10583 10581 +10582 10581 10580 10584 10585 10583 10584 10583 10582 +10586 10587 10585 10586 10585 10584 10587 10586 10563 +10587 10563 10562 10136 10587 10562 10137 10136 10562 +10137 10562 10132 10588 10140 10565 10565 10140 10135 +10565 10566 10589 10590 10565 10589 10588 10565 10590 +10591 10590 10589 10592 10569 10568 10592 10568 10570 +10592 10570 10593 10594 10595 10593 10570 10594 10593 +10571 10594 10570 10596 10571 10572 10594 10571 10596 +10597 10572 10573 10596 10572 10597 10598 10573 10575 +10597 10573 10598 10575 10576 10599 10600 10575 10599 +10598 10575 10600 10601 10602 10600 10601 10600 10599 +10602 10601 10577 10579 10602 10577 10603 10602 10579 +10604 10579 10581 10603 10579 10604 10605 10581 10583 +10604 10581 10605 10141 10605 10142 10605 10585 10142 +10605 10583 10585 10142 10585 10144 10585 10606 10144 +10585 10587 10606 10145 10606 10136 10136 10606 10587 +10607 10146 10588 10588 10146 10140 10608 10588 10590 +10607 10588 10608 10590 10591 10609 10610 10590 10609 +10608 10590 10610 10611 10612 10610 10611 10610 10609 +10613 10612 10611 10614 10595 10594 10615 10616 10614 +10594 10615 10614 10596 10615 10594 10617 10596 10597 +10615 10596 10617 10618 10597 10598 10617 10597 10618 +10619 10598 10600 10618 10598 10619 10620 10600 10602 +10619 10600 10620 10621 10602 10603 10620 10602 10621 +10622 10603 10604 10621 10603 10622 10147 10622 10148 +10622 10605 10148 10622 10604 10605 10148 10605 10141 +10606 10151 10144 10151 10606 10145 10607 10152 10146 +10152 10607 10153 10607 10623 10153 10607 10608 10623 +10624 10608 10610 10623 10608 10624 10625 10610 10612 +10624 10610 10625 10612 10613 10626 10627 10612 10626 +10625 10612 10627 10628 10627 10626 10616 10615 10629 +10616 10629 10630 10631 10615 10617 10629 10615 10631 +10632 10617 10618 10631 10617 10632 10633 10618 10619 +10632 10618 10633 10634 10619 10620 10633 10619 10634 +10635 10620 10621 10634 10620 10635 10636 10621 10622 +10635 10621 10636 10155 10636 10147 10147 10636 10622 +10623 10157 10153 10157 10623 10158 10623 10637 10158 +10623 10624 10637 10638 10624 10625 10637 10624 10638 +10639 10625 10627 10638 10625 10639 10627 10628 10640 +10627 10640 10639 10630 10629 10641 10630 10641 10642 +10643 10629 10631 10641 10629 10643 10644 10631 10632 +10643 10631 10644 10645 10632 10633 10644 10632 10645 +10646 10633 10634 10645 10633 10646 10647 10634 10635 +10646 10634 10647 10648 10635 10636 10647 10635 10648 +10160 10648 10155 10155 10648 10636 10649 10162 10637 +10637 10162 10158 10650 10637 10638 10649 10637 10650 +10639 10651 10652 10650 10639 10652 10638 10639 10650 +10651 10639 10640 10642 10641 10653 10642 10653 10654 +10655 10641 10643 10653 10641 10655 10656 10643 10644 +10655 10643 10656 10657 10644 10645 10656 10644 10657 +10658 10645 10646 10657 10645 10658 10659 10646 10647 +10658 10646 10659 10660 10647 10648 10659 10647 10660 +10164 10660 10160 10160 10660 10648 10649 10166 10162 +10166 10649 10167 10649 10661 10167 10649 10650 10661 +10650 10652 10662 10650 10662 10661 10654 10653 10663 +10654 10663 10664 10665 10653 10655 10663 10653 10665 +10666 10655 10656 10665 10655 10666 10667 10656 10657 +10666 10656 10667 10668 10657 10658 10667 10657 10668 +10669 10658 10659 10668 10658 10669 10670 10659 10660 +10669 10659 10670 10169 10670 10164 10164 10670 10660 +10661 10172 10167 10173 10172 10661 10662 10173 10661 +10671 10173 10662 10664 10663 10672 10664 10672 10673 +10674 10663 10665 10672 10663 10674 10675 10665 10666 +10674 10665 10675 10676 10666 10667 10675 10666 10676 +10677 10667 10668 10676 10667 10677 10678 10668 10669 +10677 10668 10678 10174 10678 10175 10678 10670 10175 +10678 10669 10670 10175 10670 10169 10671 10179 10173 +10679 10179 10671 10673 10672 10680 10673 10680 10681 +10682 10672 10674 10680 10672 10682 10683 10674 10675 +10682 10674 10683 10684 10675 10676 10683 10675 10684 +10685 10676 10677 10684 10676 10685 10686 10677 10678 +10685 10677 10686 10181 10686 10174 10174 10686 10678 +10687 10184 10183 10688 10687 10183 10179 10688 10183 +10679 10688 10179 10689 10681 10680 10690 10691 10689 +10680 10690 10689 10682 10690 10680 10692 10682 10683 +10690 10682 10692 10693 10683 10684 10692 10683 10693 +10694 10684 10685 10693 10684 10694 10695 10685 10686 +10694 10685 10695 10186 10695 10181 10181 10695 10686 +10696 10697 10687 10697 10184 10687 10697 10188 10184 +10696 10687 10688 10698 10691 10690 10699 10700 10698 +10690 10699 10698 10692 10699 10690 10701 10692 10693 +10699 10692 10701 10702 10693 10694 10701 10693 10702 +10703 10694 10695 10702 10694 10703 10190 10703 10186 +10186 10703 10695 10697 10192 10188 10704 10192 10697 +10705 10700 10699 10706 10707 10705 10699 10706 10705 +10701 10706 10699 10708 10701 10702 10706 10701 10708 +10709 10702 10703 10708 10702 10709 10193 10710 10190 +10710 10703 10190 10710 10709 10703 10195 10710 10193 +10711 10712 10196 10196 10712 10197 10196 10192 10704 +10196 10704 10711 10713 10707 10706 10714 10715 10713 +10706 10714 10713 10708 10714 10706 10716 10708 10709 +10714 10708 10716 10717 10709 10710 10716 10709 10717 +10200 10717 10195 10195 10717 10710 10712 10201 10197 +10718 10201 10712 10719 10715 10714 10720 10721 10719 +10714 10720 10719 10716 10720 10714 10722 10716 10717 +10720 10716 10722 10203 10722 10200 10200 10722 10717 +10723 10724 10725 10718 10723 10725 10204 10718 10725 +10201 10718 10204 10726 10721 10720 10727 10728 10726 +10720 10727 10726 10722 10727 10720 10206 10727 10203 +10203 10727 10722 10729 10730 10731 10729 10731 10724 +10731 10725 10724 10731 10732 10725 10732 10207 10204 +10732 10204 10725 10733 10728 10727 10734 10733 10735 +10735 10733 10208 10733 10727 10208 10727 10206 10208 +10209 10735 10208 10731 10730 10210 10730 10211 10210 +10730 10736 10211 10731 10210 10213 10731 10213 10732 +10732 10213 10207 10734 10735 10737 10734 10737 10738 +10214 10737 10209 10209 10737 10735 10739 10740 10741 +10736 10739 10741 10216 10736 10741 10211 10736 10216 +10738 10737 10219 10218 10738 10219 10742 10738 10218 +10219 10737 10214 10741 10740 10743 10743 10740 10744 +10743 10221 10216 10743 10216 10741 10745 10742 10218 +10745 10218 10223 10223 10224 10745 10745 10224 10746 +10743 10744 10747 10747 10744 10748 10749 10225 10221 +10743 10749 10221 10747 10749 10743 10749 10226 10225 +10229 10746 10224 10229 10750 10746 10748 10751 10747 +10752 10753 10751 10753 10747 10751 10753 10749 10747 +10754 10230 10226 10749 10754 10226 10753 10754 10749 +10754 10231 10230 10236 10750 10229 10236 10755 10750 +10752 10756 10753 10753 10756 10754 10754 10756 10757 +10757 10758 10759 10757 10759 10237 10754 10757 10237 +10231 10754 10237 10760 10238 10237 10760 10237 10759 +10761 10239 10238 10761 10238 10760 10762 10240 10239 +10762 10239 10761 10762 10241 10240 10246 10755 10236 +10246 10763 10755 10758 10764 10759 10759 10764 10760 +10760 10764 10765 10760 10765 10761 10761 10765 10766 +10761 10766 10762 10762 10766 10767 10762 10767 10241 +10767 10247 10241 10767 10768 10247 10247 10769 10248 +10768 10769 10247 10249 10248 10769 10249 10769 10770 +10252 10771 10253 10772 10254 10253 10772 10253 10771 +10254 10772 10773 10774 10254 10773 10255 10254 10774 +10246 10255 10763 10763 10255 10774 10770 10775 10249 +10249 10775 10256 10775 10776 10777 10257 10775 10777 +10256 10775 10257 10778 10258 10257 10778 10257 10777 +10779 10259 10258 10779 10258 10778 10779 10260 10259 +10265 10780 10266 10266 10780 10781 10782 10266 10781 +10267 10266 10782 10782 10783 10784 10268 10782 10784 +10267 10782 10268 10784 10269 10268 10271 10785 10272 +10786 10273 10272 10786 10272 10785 10273 10786 10787 +10788 10273 10787 10274 10273 10788 10252 10274 10771 +10274 10789 10771 10274 10788 10789 10771 10789 10772 +10772 10789 10790 10772 10790 10773 10776 10791 10777 +10777 10791 10778 10778 10791 10792 10778 10792 10779 +10779 10792 10793 10779 10793 10260 10793 10275 10260 +10793 10794 10275 10275 10795 10276 10794 10795 10275 +10276 10796 10277 10795 10796 10276 10277 10797 10278 +10796 10797 10277 10265 10278 10780 10278 10798 10780 +10278 10797 10798 10780 10798 10781 10783 10799 10784 +10784 10799 10269 10799 10279 10269 10799 10800 10279 +10271 10279 10785 10279 10801 10785 10279 10800 10801 +10785 10801 10786 10786 10801 10802 10786 10802 10787 +10803 10804 10805 10281 10280 10805 10280 10803 10805 +10280 10806 10803 10807 10280 10283 10807 10806 10280 +10808 10283 10284 10808 10807 10283 10809 10284 10286 +10809 10808 10284 10286 10287 10809 10809 10287 10810 +10811 10812 10813 10814 10815 10813 10813 10815 10811 +10816 10817 10814 10814 10817 10815 10818 10819 10816 +10816 10819 10817 10820 10821 10818 10818 10821 10819 +10822 10823 10820 10820 10823 10821 10822 10824 10823 +10825 10826 10827 10289 10288 10827 10288 10825 10827 +10288 10828 10825 10290 10805 10804 10828 10290 10804 +10288 10290 10828 10805 10290 10281 10291 10810 10287 +10291 10829 10810 10830 10831 10832 10293 10292 10832 +10292 10830 10832 10292 10833 10830 10834 10292 10294 +10834 10833 10292 10295 10813 10812 10834 10295 10812 +10294 10295 10834 10813 10295 10296 10813 10296 10814 +10814 10296 10297 10814 10297 10816 10816 10297 10298 +10816 10298 10818 10818 10298 10299 10818 10299 10820 +10820 10299 10300 10820 10300 10822 10300 10301 10822 +10301 10824 10822 10301 10835 10824 10836 10837 10838 +10838 10303 10836 10303 10302 10836 10839 10840 10836 +10839 10836 10302 10304 10839 10302 10841 10842 10305 +10305 10842 10306 10843 10305 10308 10843 10841 10305 +10309 10827 10826 10843 10309 10826 10308 10309 10843 +10827 10309 10289 10844 10829 10291 10844 10291 10311 +10311 10312 10844 10844 10312 10845 10832 10831 10313 +10831 10314 10313 10831 10846 10314 10832 10313 10293 +10847 10835 10301 10847 10301 10315 10848 10315 10316 +10848 10847 10315 10849 10316 10317 10849 10848 10316 +10850 10317 10318 10850 10849 10317 10851 10318 10319 +10851 10850 10318 10852 10319 10320 10852 10851 10319 +10853 10320 10321 10853 10852 10320 10854 10321 10322 +10854 10853 10321 10855 10322 10323 10855 10854 10322 +10324 10838 10837 10855 10324 10837 10323 10324 10855 +10839 10856 10840 10303 10838 10324 10856 10839 10304 +10325 10856 10304 10857 10856 10325 10325 10306 10842 +10325 10842 10857 10858 10859 10860 10859 10845 10860 +10845 10326 10860 10845 10312 10326 10846 10327 10314 +10861 10327 10846 10858 10860 10329 10328 10858 10329 +10862 10858 10328 10329 10860 10326 10861 10330 10327 +10863 10330 10861 10331 10862 10328 10331 10864 10862 +10863 10332 10330 10865 10332 10863 10333 10864 10331 +10333 10866 10864 10865 10867 10332 10332 10867 10334 +10335 10334 10867 10335 10867 10868 10336 10866 10333 +10336 10869 10866 10868 10337 10335 10870 10337 10868 +10338 10869 10336 10338 10871 10869 10870 10872 10337 +10337 10872 10339 10340 10339 10872 10340 10872 10873 +10341 10871 10338 10341 10874 10871 10873 10342 10340 +10875 10342 10873 10343 10874 10341 10343 10876 10874 +10875 10344 10342 10877 10344 10875 10345 10876 10343 +10345 10878 10876 10877 10346 10344 10879 10346 10877 +10347 10878 10345 10347 10880 10878 10879 10348 10346 +10881 10348 10879 10349 10880 10347 10349 10882 10880 +10881 10350 10348 10883 10350 10881 10884 10885 10352 +10884 10352 10351 10349 10352 10882 10882 10352 10885 +10883 10353 10350 10886 10353 10883 10354 10884 10351 +10354 10887 10884 10886 10355 10353 10888 10355 10886 +10356 10889 10890 10887 10356 10890 10354 10356 10887 +10889 10891 10890 10888 10357 10355 10892 10357 10888 +10358 10889 10356 10358 10359 10889 10359 10891 10889 +10359 10893 10891 10892 10360 10357 10894 10360 10892 +10361 10893 10359 10361 10895 10893 10894 10362 10360 +10896 10362 10894 10363 10895 10361 10363 10897 10895 +10896 10364 10362 10898 10364 10896 10365 10897 10363 +10365 10899 10897 10898 10366 10364 10900 10366 10898 +10367 10899 10365 10367 10901 10899 10900 10368 10366 +10902 10368 10900 10369 10901 10367 10369 10903 10901 +10902 10370 10368 10904 10370 10902 10371 10903 10369 +10371 10905 10903 10904 10372 10370 10906 10372 10904 +10373 10905 10371 10373 10907 10905 10906 10374 10372 +10908 10374 10906 10375 10907 10373 10375 10909 10907 +10908 10376 10374 10910 10376 10908 10377 10909 10375 +10377 10911 10909 10910 10378 10376 10912 10378 10910 +10379 10911 10377 10379 10913 10911 10912 10380 10378 +10914 10380 10912 10381 10913 10379 10381 10915 10913 +10914 10382 10380 10916 10382 10914 10383 10915 10381 +10383 10917 10915 10916 10384 10382 10918 10384 10916 +10919 10920 10921 10922 10923 10921 10922 10921 10920 +10924 10923 10922 10385 10917 10383 10385 10925 10917 +10918 10386 10384 10926 10386 10918 10927 10928 10388 +10927 10388 10387 10388 10929 10389 10928 10929 10388 +10389 10930 10390 10929 10930 10389 10921 10391 10919 +10391 10930 10919 10391 10390 10930 10391 10921 10392 +10392 10921 10923 10924 10931 10923 10931 10392 10923 +10931 10393 10392 10393 10932 10395 10931 10932 10393 +10395 10933 10396 10932 10933 10395 10396 10934 10397 +10933 10934 10396 10397 10935 10398 10934 10935 10397 +10936 10937 10399 10398 10936 10399 10935 10936 10398 +10938 10937 10936 10400 10925 10385 10400 10939 10925 +10926 10401 10386 10940 10401 10926 10941 10942 10403 +10941 10403 10402 10403 10943 10405 10942 10943 10403 +10405 10944 10406 10943 10944 10405 10406 10945 10407 +10944 10945 10406 10387 10407 10927 10927 10407 10945 +10937 10408 10399 10938 10946 10937 10946 10408 10937 +10946 10409 10408 10409 10947 10410 10946 10947 10409 +10410 10948 10411 10947 10948 10410 10413 10411 10948 +10413 10948 10949 10415 10939 10400 10415 10950 10939 +10416 10401 10951 10401 10952 10951 10401 10940 10952 +10951 10417 10416 10953 10954 10955 10419 10418 10955 +10954 10419 10955 10956 10419 10954 10957 10958 10422 +10419 10957 10422 10956 10957 10419 10424 10423 10958 +10957 10424 10958 10959 10424 10957 10402 10424 10941 +10941 10424 10959 10949 10960 10413 10413 10960 10425 +10427 10425 10960 10427 10960 10961 10962 10963 10428 +10962 10428 10429 10415 10428 10950 10950 10428 10963 +10952 10964 10951 10951 10964 10417 10964 10430 10417 +10964 10965 10430 10430 10966 10431 10965 10966 10430 +10955 10432 10953 10432 10966 10953 10432 10431 10966 +10432 10955 10418 10958 10433 10422 10433 10958 10423 +10961 10967 10427 10427 10967 10434 10429 10434 10962 +10962 10434 10967 10968 10441 10442 10443 10969 10442 +10442 10969 10968 10444 10970 10443 10443 10970 10969 +10445 10971 10444 10444 10971 10970 10971 10445 10972 +10445 10973 10972 10445 10446 10973 10446 10448 10973 +10973 10448 10974 10975 10461 10462 10441 10968 10462 +10968 10975 10462 10968 10976 10975 10969 10977 10978 +10976 10969 10978 10968 10969 10976 10977 10969 10970 +10977 10970 10979 10979 10970 10971 10979 10971 10980 +10980 10971 10972 10448 10463 10981 10982 10448 10981 +10974 10448 10982 10464 10983 10463 10463 10983 10981 +10465 10984 10464 10464 10984 10983 10466 10985 10465 +10465 10985 10984 10468 10985 10466 10986 10476 10477 +10478 10987 10477 10477 10987 10986 10479 10988 10478 +10478 10988 10987 10480 10989 10479 10479 10989 10988 +10481 10990 10480 10480 10990 10989 10482 10991 10481 +10481 10991 10990 10483 10991 10482 10992 10486 10485 +10992 10485 10993 10485 10994 10993 10485 10488 10994 +10488 10461 10975 10995 10488 10975 10994 10488 10995 +10995 10975 10976 10995 10976 10996 10996 10976 10978 +10997 10982 10981 10997 10981 10983 10997 10983 10998 +10998 10983 10984 10998 10984 10999 11000 11001 10999 +10984 11000 10999 10985 11000 10984 10489 11002 10468 +11002 10985 10468 11002 11000 10985 10491 11002 10489 +11003 10497 10496 10498 11004 10496 10496 11004 11003 +10476 10986 10498 10986 11004 10498 10986 11005 11004 +10987 11006 11007 11005 10987 11007 10986 10987 11005 +11006 10987 10988 11006 10988 11008 11008 10988 10989 +11008 10989 11009 11009 10989 10990 11009 10990 11010 +11010 10990 10991 11010 10991 11011 11011 10991 10483 +10499 11011 10483 11012 11011 10499 11013 11012 11014 +11012 10500 11014 11012 10499 10500 10501 11014 10500 +11015 10503 10992 10992 10503 10486 10992 10993 11016 +10992 11016 11015 11017 11018 11019 11019 11018 11001 +11000 11019 11001 11020 11019 11000 10504 11020 10505 +11020 11002 10505 11020 11000 11002 10505 11002 10491 +11021 10512 10511 10497 11003 10511 11003 11021 10511 +11003 11022 11021 11023 11003 11004 11022 11003 11023 +11024 11004 11005 11023 11004 11024 11005 11007 11025 +11026 11005 11025 11024 11005 11026 11027 11026 11025 +11028 11013 11014 11029 11028 11030 11030 11028 10513 +11028 11014 10513 11014 10501 10513 10514 11030 10513 +11031 10516 10515 11032 11031 10515 11032 10515 10503 +11033 11032 10503 11015 11033 10503 11033 11015 11016 +11017 11019 11034 11017 11034 11035 11036 11019 11020 +11034 11019 11036 10518 11036 10504 10504 11036 11020 +11037 10523 11021 11021 10523 10512 11038 11021 11022 +11037 11021 11038 11039 11022 11023 11038 11022 11039 +11040 11023 11024 11039 11023 11040 11041 11024 11026 +11040 11024 11041 11026 11027 11042 11026 11042 11041 +11043 11044 11045 11045 11044 11029 11045 11029 11030 +10524 11045 11030 10525 10524 11030 10525 11030 10514 +11046 10527 11031 11031 10527 10516 11031 11032 11047 +11031 11047 11046 11035 11034 11048 11035 11048 11049 +11050 11034 11036 11048 11034 11050 10528 11051 10518 +11051 11036 10518 11051 11050 11036 10530 11051 10528 +11037 10534 10523 10534 11037 10536 11037 11052 10536 +11037 11038 11052 11053 11038 11039 11052 11038 11053 +11054 11039 11040 11053 11039 11054 11055 11040 11041 +11054 11040 11055 11041 11042 11056 11057 11041 11056 +11055 11041 11057 11058 11057 11056 11043 11045 11059 +11043 11059 11060 10537 11059 10524 10524 11059 11045 +11061 10539 11046 11046 10539 10527 11046 11047 11062 +11063 11046 11062 11061 11046 11063 11064 11063 11062 +11049 11048 11065 11049 11065 11066 11067 11048 11050 +11065 11048 11067 11068 11050 11051 11067 11050 11068 +10541 11068 10530 10530 11068 11051 11052 10546 10536 +10546 11052 10548 11052 11069 10548 11052 11053 11069 +11070 11053 11054 11069 11053 11070 11071 11054 11055 +11070 11054 11071 11072 11055 11057 11071 11055 11072 +11057 11058 11073 11074 11057 11073 11072 11057 11074 +11075 11074 11073 11076 11077 11078 11078 11077 11060 +11078 11060 11059 10549 11078 11059 10550 10549 11059 +10550 11059 10537 11079 10553 11061 11061 10553 10539 +11080 11061 11063 11079 11061 11080 11063 11064 11081 +11082 11063 11081 11080 11063 11082 11083 11082 11081 +11066 11065 11084 11066 11084 11085 11086 11065 11067 +11084 11065 11086 11087 11067 11068 11086 11067 11087 +10555 11087 10541 10541 11087 11068 11088 10561 11069 +11069 10561 10548 11089 11069 11070 11088 11069 11089 +11090 11070 11071 11089 11070 11090 11091 11071 11072 +11090 11071 11091 11092 11072 11074 11091 11072 11092 +11074 11075 11093 11094 11074 11093 11092 11074 11094 +11095 11096 11094 11095 11094 11093 11096 11095 11076 +11078 11096 11076 11097 11096 11078 10563 11097 10549 +10549 11097 11078 11079 10564 10553 10564 11079 10566 +11079 11098 10566 11079 11080 11098 11099 11080 11082 +11098 11080 11099 11082 11083 11100 11101 11082 11100 +11099 11082 11101 11102 11103 11101 11102 11101 11100 +11104 11105 11103 11104 11103 11102 11106 11105 11104 +11085 11084 11107 11085 11107 11108 11109 11084 11086 +11107 11084 11109 11110 11086 11087 11109 11086 11110 +10567 11111 10555 11111 11087 10555 11111 11110 11087 +10569 11111 10567 11088 10574 10561 10574 11088 10576 +11088 11112 10576 11088 11089 11112 11113 11089 11090 +11112 11089 11113 10577 11113 10578 11113 11091 10578 +11113 11090 11091 10578 11091 10580 10580 11091 11092 +10580 11092 10582 10582 11092 11094 10582 11094 10584 +10584 11094 11096 10584 11096 10586 10586 11096 11097 +10586 11097 10563 11098 10589 10566 10589 11098 10591 +11098 11114 10591 11098 11099 11114 11115 11099 11101 +11114 11099 11115 11116 11101 11103 11115 11101 11116 +11117 11103 11105 11116 11103 11117 11105 11106 11118 +11119 11105 11118 11117 11105 11119 11120 11119 11118 +11108 11107 11121 11108 11121 11122 11123 11107 11109 +11121 11107 11123 11124 11109 11110 11123 11109 11124 +11125 11110 11111 11124 11110 11125 10592 11126 10569 +11126 11111 10569 11126 11125 11111 10593 11127 10592 +10592 11127 11126 10595 11127 10593 11112 10599 10576 +10599 11112 10601 10601 11112 11113 10601 11113 10577 +11114 10609 10591 10609 11114 10611 10611 11114 11115 +10611 11115 10613 11115 11128 10613 11115 11116 11128 +11129 11116 11117 11128 11116 11129 11130 11117 11119 +11129 11117 11130 11119 11120 11131 11119 11131 11130 +11132 11122 11121 11133 11134 11132 11121 11133 11132 +11123 11133 11121 11135 11123 11124 11133 11123 11135 +11136 11124 11125 11135 11124 11136 11137 11125 11126 +11136 11125 11137 11138 11126 11127 11137 11126 11138 +10614 11139 10595 11139 11127 10595 11139 11138 11127 +10616 11139 10614 11128 10626 10613 10626 11128 10628 +11128 11140 10628 11128 11129 11140 11141 11129 11130 +11140 11129 11141 11130 11131 11142 11130 11142 11141 +11134 11133 11143 11134 11143 11144 11145 11133 11135 +11143 11133 11145 11146 11135 11136 11145 11135 11146 +11147 11136 11137 11146 11136 11147 11148 11137 11138 +11147 11137 11148 11149 11138 11139 11148 11138 11149 +10630 11149 10616 10616 11149 11139 11150 10640 11140 +11140 10640 10628 11141 11151 11152 11150 11141 11152 +11140 11141 11150 11151 11141 11142 11153 11144 11143 +11154 11155 11153 11143 11154 11153 11145 11154 11143 +11156 11145 11146 11154 11145 11156 11157 11146 11147 +11156 11146 11157 11158 11147 11148 11157 11147 11158 +11159 11148 11149 11158 11148 11159 10642 11159 10630 +10630 11159 11149 11160 10652 10651 11161 11160 10651 +11161 10651 10640 11162 11161 10640 11150 11162 10640 +11162 11150 11152 11155 11154 11163 11155 11163 11164 +11165 11154 11156 11163 11154 11165 11166 11156 11157 +11165 11156 11166 11167 11157 11158 11166 11157 11167 +11168 11158 11159 11167 11158 11168 10654 11168 10642 +10642 11168 11159 11169 10662 11160 11160 10662 10652 +11160 11161 11170 11160 11170 11169 11164 11163 11171 +11164 11171 11172 11173 11163 11165 11171 11163 11173 +11174 11165 11166 11173 11165 11174 11175 11166 11167 +11174 11166 11175 11176 11167 11168 11175 11167 11176 +10664 11176 10654 10654 11176 11168 11177 10671 11169 +11169 10671 10662 11169 11170 11178 11169 11178 11177 +11179 11172 11171 11180 11181 11179 11171 11180 11179 +11173 11180 11171 11182 11173 11174 11180 11173 11182 +11183 11174 11175 11182 11174 11183 11184 11175 11176 +11183 11175 11184 10673 11184 10664 10664 11184 11176 +11185 11186 11177 11186 10671 11177 11186 10679 10671 +11185 11177 11178 11181 11180 11187 11181 11187 11188 +11189 11180 11182 11187 11180 11189 11190 11182 11183 +11189 11182 11190 11191 11183 11184 11190 11183 11191 +10681 11191 10673 10673 11191 11184 11192 11193 11194 +11186 11192 11194 10688 11186 11194 10679 11186 10688 +11195 11188 11187 11196 11197 11195 11187 11196 11195 +11189 11196 11187 11198 11189 11190 11196 11189 11198 +11199 11190 11191 11198 11190 11199 10689 11200 10681 +11200 11191 10681 11200 11199 11191 10691 11200 10689 +11194 11193 10696 11193 10697 10696 11193 11201 10697 +11194 10696 10688 11197 11196 11202 11197 11202 11203 +11204 11196 11198 11202 11196 11204 11205 11198 11199 +11204 11198 11205 11206 11199 11200 11205 11199 11206 +10698 11207 10691 11207 11200 10691 11207 11206 11200 +10700 11207 10698 11208 11209 11210 11201 11208 11210 +10704 11201 11210 10697 11201 10704 11211 11203 11202 +11212 11213 11211 11202 11212 11211 11204 11212 11202 +11214 11204 11205 11212 11204 11214 11215 11205 11206 +11214 11205 11215 11216 11206 11207 11215 11206 11216 +10705 11217 10700 11217 11207 10700 11217 11216 11207 +10707 11217 10705 11218 11219 11220 11218 11220 11209 +11209 11220 11210 11220 10712 11210 10712 10711 11210 +11210 10711 10704 11221 11213 11212 11221 11212 11214 +11221 11214 11222 11223 11224 11222 11214 11223 11222 +11215 11223 11214 11225 11215 11216 11223 11215 11225 +11226 11216 11217 11225 11216 11226 10713 11227 10707 +11227 11217 10707 11227 11226 11217 10715 11227 10713 +11220 11219 11228 11228 11219 11229 11228 10718 10712 +11228 10712 11220 11230 11224 11223 11231 11232 11230 +11223 11231 11230 11225 11231 11223 11233 11225 11226 +11231 11225 11233 11234 11226 11227 11233 11226 11234 +10719 11235 10715 11235 11227 10715 11235 11234 11227 +10721 11235 10719 11236 11237 11238 11236 11238 11229 +11229 11238 11228 11238 10724 11228 10724 10723 11228 +11228 10723 10718 11239 11232 11231 11239 11231 11233 +11239 11233 11240 11240 11233 11234 11240 11234 11241 +11242 11243 11241 11234 11242 11241 11235 11242 11234 +10726 11244 10721 11244 11235 10721 11244 11242 11235 +10728 11244 10726 11245 11246 11247 11245 11247 11237 +11237 11247 11238 11247 10730 11238 10730 10729 11238 +11238 10729 10724 11248 11243 11242 11249 11250 11248 +11242 11249 11248 11244 11249 11242 10733 11251 10728 +11251 11244 10728 11251 11249 11244 10734 11251 10733 +11252 11253 11254 11252 11254 11246 11254 11247 11246 +11254 11255 11247 11255 10736 10730 11255 10730 11247 +11250 11249 11256 11250 11256 11257 11258 11249 11251 +11256 11249 11258 10738 11258 10734 10734 11258 11251 +11254 11253 11259 11259 11253 11260 11259 10740 10739 +11255 11259 10739 11254 11259 11255 11255 10739 10736 +11261 11257 11256 11262 11263 11261 11256 11262 11261 +11258 11262 11256 10742 11262 10738 10738 11262 11258 +11259 11260 11264 11264 11260 11265 11264 10744 10740 +11264 10740 11259 11266 11263 11262 11267 11266 11268 +11268 11266 10745 11266 11262 10745 11262 10742 10745 +10746 11268 10745 11264 11265 11269 11269 11265 11270 +11269 10748 10744 11269 10744 11264 11271 11267 11268 +11268 10746 11271 10746 11272 11271 10746 10750 11272 +11269 11270 11273 11273 11270 11274 11275 10751 10748 +11269 11275 10748 11273 11275 11269 11275 10752 10751 +10755 11276 11277 11272 10755 11277 10750 10755 11272 +11276 11278 11277 11274 11279 11273 11280 11281 11279 +11281 11273 11279 11281 11275 11273 11282 10756 10752 +11275 11282 10752 11281 11282 11275 11283 10757 10756 +11283 10756 11282 11283 10758 10757 10763 11284 11276 +10763 11276 10755 11278 11276 11285 11285 11276 11284 +11280 11286 11281 11281 11286 11282 11282 11286 11287 +11282 11287 11283 11283 11287 11288 11288 11289 11290 +11288 11290 10764 11283 11288 10764 10758 11283 10764 +11291 10765 10764 11291 10764 11290 11292 10766 10765 +11292 10765 11291 11293 10767 10766 11293 10766 11292 +11294 10768 10767 11294 10767 11293 11295 10769 10768 +11295 10768 11294 11295 10770 10769 10773 11296 10774 +11296 11297 10774 10774 11297 10763 11297 11298 10763 +11298 11284 10763 11284 11298 11285 11289 11299 11290 +11290 11299 11291 11291 11299 11300 11291 11300 11292 +11292 11300 11301 11292 11301 11293 11293 11301 11302 +11293 11302 11294 11294 11302 11303 11294 11303 11295 +11295 11303 11304 11304 11305 11306 11304 11306 10775 +11295 11304 10775 10770 11295 10775 11306 10776 10775 +10781 11307 10782 11307 10783 10782 10787 11308 10788 +11309 10789 10788 11309 10788 11308 10789 11309 11310 +11311 10789 11310 10790 10789 11311 10773 10790 11296 +10790 11312 11296 10790 11311 11312 11296 11312 11297 +11305 11313 11306 11306 11313 10776 11313 10791 10776 +11313 11314 10791 10791 11315 10792 11314 11315 10791 +10792 11316 10793 11315 11316 10792 10793 11317 10794 +11316 11317 10793 11317 11318 11319 10795 11317 11319 +10794 11317 10795 11320 10796 10795 11320 10795 11319 +11321 10797 10796 11321 10796 11320 10797 11321 11322 +11323 10797 11322 10798 10797 11323 10781 10798 11307 +10798 11324 11307 10798 11323 11324 11324 11325 11326 +11324 11326 10799 11307 11324 10799 10783 11307 10799 +10799 11326 11327 11328 10799 11327 10800 10799 11328 +10800 11329 10801 11328 11329 10800 10801 11330 10802 +11329 11330 10801 10787 10802 11308 10802 11331 11308 +10802 11330 11331 11308 11331 11309 11309 11331 11332 +11309 11332 11310 11318 11333 11319 11319 11333 11320 +11320 11333 11334 11320 11334 11321 11321 11334 11335 +11321 11335 11322 11325 11336 11326 11326 11336 11327 +11337 11338 10803 10803 11338 10804 11339 10803 10806 +11339 11337 10803 11340 10806 10807 11340 11339 10806 +11341 10807 10808 11341 11340 10807 11342 10808 10809 +11342 11341 10808 10809 10810 11342 11342 10810 11343 +11344 11345 11346 11347 11348 11346 11346 11348 11344 +10812 10811 11347 10811 11348 11347 10811 11349 11348 +11350 10811 10815 11350 11349 10811 11351 10815 10817 +11351 11350 10815 11352 10817 10819 11352 11351 10817 +11353 10819 10821 11353 11352 10819 11354 11353 11355 +11353 10823 11355 11353 10821 10823 10824 11355 10823 +11356 11357 11358 10826 10825 11358 10825 11356 11358 +10825 11359 11356 11360 10825 10828 11360 11359 10825 +10828 10804 11338 10828 11338 11360 10829 11361 11362 +11343 10829 11362 10810 10829 11343 11361 11363 11362 +11364 11365 10830 10830 11365 10831 10833 11346 11345 +11364 10833 11345 10830 10833 11364 11346 10833 10834 +11346 10834 11347 11347 10834 10812 11366 11354 11355 +10824 10835 11355 10835 11367 11355 11367 11366 11355 +11367 10835 11368 11369 11370 11368 11368 11370 11367 +11371 11372 11369 11369 11372 11370 11373 11374 11371 +11371 11374 11372 11375 11376 11373 11373 11376 11374 +11377 11378 11375 11375 11378 11376 11379 11380 11377 +11377 11380 11378 11381 11382 11379 11379 11382 11380 +11383 11384 11381 11381 11384 11382 10837 10836 11383 +10836 11384 11383 10836 11385 11384 11386 11387 10840 +11387 10836 10840 11387 11385 10836 11388 11389 11386 +11386 11389 11387 10842 10841 11388 10841 11389 11388 +10841 11390 11389 10843 11358 11357 11390 10843 11357 +10841 10843 11390 11358 10843 10826 10844 11361 10829 +10844 10845 11361 10845 11363 11361 10845 11391 11363 +11365 10846 10831 11392 10846 11365 10847 11368 10835 +11368 10847 10848 11368 10848 11369 11369 10848 10849 +11369 10849 11371 11371 10849 10850 11371 10850 11373 +11373 10850 10851 11373 10851 11375 11375 10851 10852 +11375 10852 11377 11377 10852 10853 11377 10853 11379 +11379 10853 10854 11379 10854 11381 11381 10854 10855 +11381 10855 11383 11383 10855 10837 10856 11386 10840 +11386 10856 10857 11386 10857 11388 11388 10857 10842 +10858 11393 10859 11393 11394 10859 11394 10845 10859 +11394 11391 10845 11392 10861 10846 11395 10861 11392 +11396 11397 11393 10858 11396 11393 10862 11396 10858 +11393 11397 11394 11395 10863 10861 11398 10863 11395 +10864 11396 10862 10864 11399 11396 11400 10865 11401 +10865 11398 11401 10865 10863 11398 11402 11400 11401 +10866 11399 10864 10866 11403 11399 11400 10867 10865 +10868 10867 11400 11402 10868 11400 11404 10868 11402 +10869 11403 10866 10869 11405 11403 11404 10870 10868 +11406 10870 11404 10871 11405 10869 10871 11407 11405 +11406 11408 10870 10870 11408 10872 10873 10872 11408 +10873 11408 11409 10874 11407 10871 10874 11410 11407 +11409 10875 10873 11411 10875 11409 10876 11410 10874 +10876 11412 11410 11411 10877 10875 11413 10877 11411 +10878 11412 10876 10878 11414 11412 11413 10879 10877 +11415 10879 11413 10880 11414 10878 10880 11416 11414 +11415 10881 10879 11417 10881 11415 11418 11419 11420 +11419 11416 11420 11416 10882 11420 11416 10880 10882 +11417 10883 10881 11421 10883 11417 11418 11420 10885 +10884 11418 10885 11422 11418 10884 10885 11420 10882 +11421 10886 10883 11423 10886 11421 10887 11422 10884 +10887 11424 11422 11423 10888 10886 11425 10888 11423 +11426 11424 10887 11426 10887 10890 10890 10891 11426 +11426 10891 11427 11425 10892 10888 11428 10892 11425 +10893 11427 10891 10893 11429 11427 11428 10894 10892 +11430 10894 11428 10895 11429 10893 10895 11431 11429 +11430 10896 10894 11432 10896 11430 10897 11431 10895 +10897 11433 11431 11432 10898 10896 11434 10898 11432 +10899 11433 10897 10899 11435 11433 11434 10900 10898 +11436 10900 11434 10901 11435 10899 10901 11437 11435 +11438 11439 11440 11436 11438 11440 10902 11436 11440 +10900 11436 10902 10903 11437 10901 10903 11441 11437 +11439 11442 11440 10902 11440 11442 11443 10902 11442 +10904 10902 11443 10905 11444 11445 11441 10905 11445 +10903 10905 11441 11444 11446 11445 11443 10906 10904 +11447 10906 11443 11448 11449 11444 10905 11448 11444 +10907 11448 10905 11444 11449 11446 11447 10908 10906 +11450 10908 11447 10909 11448 10907 10909 11451 11448 +11450 10910 10908 11452 10910 11450 10911 11451 10909 +10911 11453 11451 11452 10912 10910 11454 10912 11452 +10913 11453 10911 10913 11455 11453 11454 10914 10912 +11456 10914 11454 10915 11455 10913 10915 11457 11455 +11456 10916 10914 11458 10916 11456 10917 11459 11460 +11457 10917 11460 10915 10917 11457 11459 11461 11460 +11458 10918 10916 11462 10918 11458 11463 11464 11465 +11466 11467 11465 11466 11465 11464 10920 10919 11467 +11466 10920 11467 11468 10920 11466 11468 11469 11470 +10922 11468 11470 10920 11468 10922 11470 10924 10922 +10925 11471 11459 10925 11459 10917 11461 11459 11472 +11472 11459 11471 11462 10926 10918 11473 10926 11462 +11474 11475 11476 10928 10927 11476 11475 10928 11476 +11477 10928 11475 11465 10929 11463 10929 11477 11463 +10929 10928 11477 10929 11465 10930 10930 11465 11467 +10930 11467 10919 11469 11478 11470 11470 11478 10924 +11478 10931 10924 11478 11479 10931 10931 11480 10932 +11479 11480 10931 10932 11481 10933 11480 11481 10932 +10933 11482 10934 11481 11482 10933 10934 11483 10935 +11482 11483 10934 10935 11484 10936 11483 11484 10935 +11485 11486 10938 10936 11485 10938 11484 11485 10936 +11487 11486 11485 11488 11489 11471 10925 11488 11471 +10939 11488 10925 11471 11489 11472 11473 10940 10926 +11490 10940 11473 11491 11492 11493 10942 10941 11493 +11492 10942 11493 11494 10942 11492 10942 11495 10943 +11494 11495 10942 10943 11496 10944 11495 11496 10943 +11476 10945 11474 10945 11496 11474 10945 10944 11496 +10945 11476 10927 11486 10946 10938 11487 11497 11486 +11497 10946 11486 11497 10947 10946 10947 11498 10948 +11497 11498 10947 11499 11500 10949 10948 11499 10949 +11498 11499 10948 11501 11500 11499 10950 11488 10939 +10950 11502 11488 11490 10952 10940 11503 10952 11490 +11504 11505 10954 11504 10954 10953 10954 11506 10956 +11505 11506 10954 10956 11507 10957 11506 11507 10956 +11493 10959 11491 10959 11507 11491 10959 10957 11507 +10959 11493 10941 11500 10960 10949 11508 11509 10961 +11508 10961 10960 11501 11508 10960 11500 11501 10960 +10963 10962 11509 11508 10963 11509 11510 10963 11508 +10950 10963 11502 11502 10963 11510 11503 11511 10952 +10952 11511 10964 10964 11512 10965 11511 11512 10964 +10965 11513 10966 11512 11513 10965 10953 10966 11504 +11504 10966 11513 11509 10967 10961 10967 11509 10962 +11514 10972 10973 10974 11514 10973 11515 10978 10977 +10979 11516 10977 10977 11516 11515 10980 11517 10979 +10979 11517 11516 10972 11514 10980 11514 11517 10980 +11514 11518 11517 10982 11518 10974 10974 11518 11514 +11519 10993 10994 10995 11520 10994 10994 11520 11519 +10996 11521 10995 10995 11521 11520 10978 11515 10996 +11515 11521 10996 11515 11522 11521 11523 11515 11516 +11522 11515 11523 11524 11516 11517 11523 11516 11524 +11525 11517 11518 11524 11517 11525 10997 11526 10982 +11526 11518 10982 11526 11525 11518 10998 11527 10997 +10997 11527 11526 10999 11528 10998 10998 11528 11527 +11001 11528 10999 11529 11007 11006 11008 11530 11006 +11006 11530 11529 11009 11531 11008 11008 11531 11530 +11010 11532 11009 11009 11532 11531 11011 11533 11010 +11010 11533 11532 11012 11534 11011 11011 11534 11533 +11013 11534 11012 11535 11016 11519 11519 11016 10993 +11536 11519 11520 11535 11519 11536 11537 11520 11521 +11536 11520 11537 11538 11521 11522 11537 11521 11538 +11539 11522 11523 11538 11522 11539 11540 11523 11524 +11539 11523 11540 11541 11524 11525 11540 11524 11541 +11542 11525 11526 11541 11525 11542 11543 11526 11527 +11542 11526 11543 11017 11543 11018 11543 11528 11018 +11543 11527 11528 11018 11528 11001 11529 11025 11007 +11025 11529 11027 11529 11544 11027 11529 11530 11544 +11545 11530 11531 11544 11530 11545 11546 11531 11532 +11545 11531 11546 11547 11532 11533 11546 11532 11547 +11548 11533 11534 11547 11533 11548 11028 11549 11013 +11549 11534 11013 11549 11548 11534 11029 11549 11028 +11550 11032 11033 11016 11535 11033 11535 11550 11033 +11535 11551 11550 11552 11535 11536 11551 11535 11552 +11553 11536 11537 11552 11536 11553 11554 11537 11538 +11553 11537 11554 11555 11538 11539 11554 11538 11555 +11556 11539 11540 11555 11539 11556 11557 11540 11541 +11556 11540 11557 11558 11541 11542 11557 11541 11558 +11559 11542 11543 11558 11542 11559 11035 11559 11017 +11017 11559 11543 11560 11042 11544 11544 11042 11027 +11561 11544 11545 11560 11544 11561 11562 11545 11546 +11561 11545 11562 11563 11546 11547 11562 11546 11563 +11564 11547 11548 11563 11547 11564 11043 11564 11044 +11564 11549 11044 11564 11548 11549 11044 11549 11029 +11565 11047 11550 11550 11047 11032 11566 11550 11551 +11565 11550 11566 11567 11551 11552 11566 11551 11567 +11568 11552 11553 11567 11552 11568 11569 11553 11554 +11568 11553 11569 11570 11554 11555 11569 11554 11570 +11571 11555 11556 11570 11555 11571 11572 11556 11557 +11571 11556 11572 11573 11557 11558 11572 11557 11573 +11574 11558 11559 11573 11558 11574 11049 11574 11035 +11035 11574 11559 11560 11056 11042 11056 11560 11058 +11560 11575 11058 11560 11561 11575 11576 11561 11562 +11575 11561 11576 11577 11562 11563 11576 11562 11577 +11578 11563 11564 11577 11563 11578 11060 11578 11043 +11043 11578 11564 11565 11062 11047 11062 11565 11064 +11565 11579 11064 11565 11566 11579 11580 11566 11567 +11579 11566 11580 11581 11567 11568 11580 11567 11581 +11582 11568 11569 11581 11568 11582 11583 11569 11570 +11582 11569 11583 11584 11570 11571 11583 11570 11584 +11585 11571 11572 11584 11571 11585 11586 11572 11573 +11585 11572 11586 11587 11573 11574 11586 11573 11587 +11066 11587 11049 11049 11587 11574 11575 11073 11058 +11073 11575 11075 11575 11588 11075 11575 11576 11588 +11589 11576 11577 11588 11576 11589 11076 11589 11077 +11589 11578 11077 11589 11577 11578 11077 11578 11060 +11579 11081 11064 11081 11579 11083 11579 11590 11083 +11579 11580 11590 11591 11580 11581 11590 11580 11591 +11592 11581 11582 11591 11581 11592 11593 11582 11583 +11592 11582 11593 11594 11583 11584 11593 11583 11594 +11595 11584 11585 11594 11584 11595 11596 11585 11586 +11595 11585 11596 11597 11586 11587 11596 11586 11597 +11085 11597 11066 11066 11597 11587 11588 11093 11075 +11093 11588 11095 11095 11588 11589 11095 11589 11076 +11590 11100 11083 11100 11590 11102 11102 11590 11591 +11102 11591 11104 11104 11591 11592 11104 11592 11106 +11592 11598 11106 11592 11593 11598 11599 11593 11594 +11598 11593 11599 11600 11594 11595 11599 11594 11600 +11601 11595 11596 11600 11595 11601 11602 11596 11597 +11601 11596 11602 11108 11602 11085 11085 11602 11597 +11598 11118 11106 11118 11598 11120 11598 11603 11120 +11598 11599 11603 11604 11599 11600 11603 11599 11604 +11605 11600 11601 11604 11600 11605 11606 11601 11602 +11605 11601 11606 11122 11606 11108 11108 11606 11602 +11607 11131 11603 11603 11131 11120 11608 11603 11604 +11607 11603 11608 11609 11604 11605 11608 11604 11609 +11610 11605 11606 11609 11605 11610 11132 11611 11122 +11611 11606 11122 11611 11610 11606 11134 11611 11132 +11612 11142 11607 11607 11142 11131 11613 11607 11608 +11612 11607 11613 11614 11608 11609 11613 11608 11614 +11615 11609 11610 11614 11609 11615 11616 11610 11611 +11615 11610 11616 11144 11616 11134 11134 11616 11611 +11617 11152 11151 11142 11612 11151 11612 11617 11151 +11612 11618 11617 11619 11612 11613 11618 11612 11619 +11620 11613 11614 11619 11613 11620 11621 11614 11615 +11620 11614 11621 11622 11615 11616 11621 11615 11622 +11153 11623 11144 11623 11616 11144 11623 11622 11616 +11155 11623 11153 11624 11161 11162 11152 11617 11162 +11617 11624 11162 11617 11625 11624 11618 11626 11627 +11625 11618 11627 11617 11618 11625 11628 11629 11626 +11618 11628 11626 11619 11628 11618 11630 11619 11620 +11628 11619 11630 11631 11620 11621 11630 11620 11631 +11632 11621 11622 11631 11621 11632 11633 11622 11623 +11632 11622 11633 11164 11633 11155 11155 11633 11623 +11634 11170 11624 11624 11170 11161 11625 11635 11636 +11634 11625 11636 11624 11625 11634 11635 11625 11627 +11637 11629 11628 11637 11628 11630 11637 11630 11638 +11639 11640 11638 11630 11639 11638 11631 11639 11630 +11641 11631 11632 11639 11631 11641 11642 11632 11633 +11641 11632 11642 11172 11642 11164 11164 11642 11633 +11643 11644 11634 11644 11170 11634 11644 11178 11170 +11643 11634 11636 11645 11640 11639 11646 11647 11645 +11639 11646 11645 11641 11646 11639 11648 11641 11642 +11646 11641 11648 11179 11649 11172 11649 11642 11172 +11649 11648 11642 11181 11649 11179 11650 11651 11185 +11185 11651 11186 11185 11178 11644 11185 11644 11650 +11652 11647 11646 11653 11654 11652 11646 11653 11652 +11648 11653 11646 11655 11648 11649 11653 11648 11655 +11188 11655 11181 11181 11655 11649 11656 11657 11192 +11192 11657 11193 11192 11186 11651 11192 11651 11656 +11654 11653 11658 11654 11658 11659 11660 11653 11655 +11658 11653 11660 11195 11661 11188 11661 11655 11188 +11661 11660 11655 11197 11661 11195 11657 11201 11193 +11662 11201 11657 11663 11659 11658 11664 11665 11663 +11658 11664 11663 11660 11664 11658 11666 11660 11661 +11664 11660 11666 11203 11666 11197 11197 11666 11661 +11667 11668 11208 11208 11668 11209 11208 11201 11662 +11208 11662 11667 11669 11665 11664 11670 11671 11669 +11664 11670 11669 11666 11670 11664 11211 11672 11203 +11672 11666 11203 11672 11670 11666 11213 11672 11211 +11673 11674 11218 11218 11674 11219 11218 11209 11668 +11218 11668 11673 11675 11671 11670 11676 11677 11675 +11670 11676 11675 11672 11676 11670 11221 11678 11213 +11678 11672 11213 11678 11676 11672 11222 11679 11221 +11221 11679 11678 11224 11679 11222 11680 11681 11682 +11674 11680 11682 11229 11674 11682 11219 11674 11229 +11683 11677 11676 11683 11676 11678 11683 11678 11684 +11685 11686 11684 11678 11685 11684 11679 11685 11678 +11230 11687 11224 11687 11679 11224 11687 11685 11679 +11232 11687 11230 11682 11681 11236 11681 11237 11236 +11681 11688 11237 11682 11236 11229 11689 11686 11685 +11690 11691 11689 11685 11690 11689 11687 11690 11685 +11239 11692 11232 11692 11687 11232 11692 11690 11687 +11240 11693 11239 11239 11693 11692 11241 11694 11240 +11240 11694 11693 11243 11694 11241 11695 11696 11697 +11246 11245 11697 11245 11695 11697 11245 11698 11695 +11245 11237 11688 11245 11688 11698 11699 11691 11690 +11700 11701 11699 11690 11700 11699 11692 11700 11690 +11702 11692 11693 11700 11692 11702 11703 11693 11694 +11702 11693 11703 11248 11704 11243 11704 11694 11243 +11704 11703 11694 11250 11704 11248 11705 11706 11707 +11705 11707 11696 11696 11707 11697 11707 11253 11697 +11253 11252 11697 11697 11252 11246 11708 11701 11700 +11708 11700 11702 11708 11702 11709 11710 11711 11709 +11702 11710 11709 11703 11710 11702 11712 11703 11704 +11710 11703 11712 11257 11712 11250 11250 11712 11704 +11707 11706 11713 11713 11706 11714 11713 11260 11253 +11713 11253 11707 11715 11711 11710 11715 11710 11712 +11715 11712 11716 11717 11716 11718 11718 11716 11261 +11716 11712 11261 11712 11257 11261 11263 11718 11261 +11713 11714 11719 11719 11714 11720 11719 11265 11260 +11719 11260 11713 11721 11717 11718 11721 11718 11263 +11266 11721 11263 11722 11721 11266 11266 11267 11722 +11722 11267 11723 11719 11720 11724 11724 11720 11725 +11724 11270 11265 11724 11265 11719 11726 11723 11267 +11726 11267 11271 11271 11272 11726 11726 11272 11727 +11724 11725 11728 11728 11725 11729 11728 11274 11270 +11728 11270 11724 11730 11727 11272 11730 11272 11277 +11277 11278 11730 11730 11278 11731 11728 11729 11732 +11732 11729 11733 11734 11279 11274 11728 11734 11274 +11732 11734 11728 11734 11280 11279 11285 11735 11736 +11731 11285 11736 11278 11285 11731 11735 11737 11736 +11733 11738 11732 11739 11740 11738 11740 11732 11738 +11740 11734 11732 11741 11286 11280 11734 11741 11280 +11740 11741 11734 11742 11287 11286 11742 11286 11741 +11743 11288 11287 11743 11287 11742 11743 11289 11288 +11297 11744 11298 11735 11285 11298 11744 11735 11298 +11745 11735 11744 11737 11735 11746 11746 11735 11745 +11739 11747 11740 11740 11747 11741 11741 11747 11748 +11741 11748 11742 11742 11748 11749 11742 11749 11743 +11743 11749 11750 11743 11750 11289 11750 11299 11289 +11750 11751 11299 11299 11752 11300 11751 11752 11299 +11300 11753 11301 11752 11753 11300 11301 11754 11302 +11753 11754 11301 11302 11755 11303 11754 11755 11302 +11755 11756 11757 11304 11755 11757 11303 11755 11304 +11757 11305 11304 11310 11758 11311 11759 11312 11311 +11759 11311 11758 11744 11297 11312 11759 11744 11312 +11760 11744 11759 11761 11745 11762 11745 11760 11762 +11745 11744 11760 11745 11761 11746 11756 11763 11757 +11757 11763 11305 11763 11313 11305 11763 11764 11313 +11313 11765 11314 11764 11765 11313 11314 11766 11315 +11765 11766 11314 11315 11767 11316 11766 11767 11315 +11316 11768 11317 11767 11768 11316 11318 11317 11768 +11318 11768 11769 11322 11770 11323 11771 11324 11323 +11771 11323 11770 11771 11325 11324 11327 11772 11328 +11773 11329 11328 11773 11328 11772 11774 11330 11329 +11774 11329 11773 11775 11331 11330 11775 11330 11774 +11776 11332 11331 11776 11331 11775 11758 11310 11332 +11776 11758 11332 11777 11758 11776 11778 11759 11779 +11759 11777 11779 11759 11758 11777 11759 11778 11760 +11760 11778 11780 11760 11780 11762 11769 11781 11318 +11318 11781 11333 11333 11782 11334 11781 11782 11333 +11334 11783 11335 11782 11783 11334 11322 11335 11770 +11335 11784 11770 11335 11783 11784 11770 11784 11771 +11771 11784 11785 11785 11786 11787 11785 11787 11336 +11771 11785 11336 11325 11771 11336 11787 11788 11336 +11336 11788 11327 11788 11789 11327 11789 11772 11327 +11772 11789 11773 11773 11789 11790 11773 11790 11774 +11774 11790 11791 11774 11791 11775 11775 11791 11792 +11775 11792 11776 11776 11792 11793 11776 11793 11777 +11777 11793 11794 11777 11794 11779 11786 11795 11787 +11787 11795 11788 11796 11797 11798 11799 11800 11798 +11798 11800 11796 11801 11802 11799 11799 11802 11800 +11803 11804 11801 11801 11804 11802 11803 11805 11804 +11806 11807 11808 11809 11810 11808 11808 11810 11806 +11338 11337 11809 11337 11810 11809 11337 11811 11810 +11812 11337 11339 11812 11811 11337 11813 11339 11340 +11813 11812 11339 11814 11340 11341 11814 11813 11340 +11815 11814 11816 11814 11342 11816 11814 11341 11342 +11343 11816 11342 11817 11818 11344 11344 11818 11345 +11819 11344 11348 11819 11817 11344 11820 11348 11349 +11820 11819 11348 11350 11798 11797 11820 11350 11797 +11349 11350 11820 11798 11350 11351 11798 11351 11799 +11799 11351 11352 11799 11352 11801 11801 11352 11353 +11801 11353 11803 11353 11354 11803 11354 11805 11803 +11354 11821 11805 11822 11823 11824 11824 11825 11822 +11826 11827 11828 11357 11356 11828 11356 11826 11828 +11356 11829 11826 11359 11808 11807 11829 11359 11807 +11356 11359 11829 11808 11359 11360 11808 11360 11809 +11809 11360 11338 11830 11815 11816 11830 11816 11343 +11362 11830 11343 11831 11830 11362 11362 11363 11831 +11831 11363 11832 11833 11834 11364 11364 11834 11365 +11364 11345 11818 11364 11818 11833 11835 11821 11354 +11835 11354 11366 11836 11366 11367 11836 11835 11366 +11837 11367 11370 11837 11836 11367 11838 11370 11372 +11838 11837 11370 11839 11372 11374 11839 11838 11372 +11840 11374 11376 11840 11839 11374 11841 11376 11378 +11841 11840 11376 11842 11378 11380 11842 11841 11378 +11843 11380 11382 11843 11842 11380 11844 11382 11384 +11844 11843 11382 11385 11824 11823 11844 11385 11823 +11384 11385 11844 11845 11825 11824 11385 11845 11824 +11387 11845 11385 11846 11387 11389 11846 11845 11387 +11390 11828 11827 11846 11390 11827 11389 11390 11846 +11828 11390 11357 11391 11847 11848 11832 11391 11848 +11363 11391 11832 11847 11849 11848 11834 11392 11365 +11850 11392 11834 11851 11852 11847 11391 11851 11847 +11394 11851 11391 11847 11852 11849 11850 11395 11392 +11853 11395 11850 11396 11854 11397 11854 11855 11397 +11855 11394 11397 11855 11851 11394 11853 11398 11395 +11856 11398 11853 11857 11858 11854 11396 11857 11854 +11399 11857 11396 11854 11858 11855 11856 11859 11398 +11398 11859 11401 11402 11401 11859 11402 11859 11860 +11403 11857 11399 11403 11861 11857 11860 11404 11402 +11862 11404 11860 11405 11861 11403 11405 11863 11861 +11862 11406 11404 11864 11406 11862 11407 11863 11405 +11407 11865 11863 11864 11866 11406 11406 11866 11408 +11409 11408 11866 11409 11866 11867 11410 11865 11407 +11410 11868 11865 11867 11411 11409 11869 11411 11867 +11412 11868 11410 11412 11870 11868 11869 11413 11411 +11871 11413 11869 11414 11870 11412 11414 11872 11870 +11871 11415 11413 11873 11415 11871 11416 11872 11414 +11416 11874 11872 11873 11417 11415 11875 11417 11873 +11876 11877 11419 11876 11419 11418 11416 11419 11874 +11874 11419 11877 11875 11421 11417 11878 11421 11875 +11422 11876 11418 11422 11879 11876 11878 11423 11421 +11880 11423 11878 11424 11881 11882 11879 11424 11882 +11422 11424 11879 11881 11883 11882 11880 11425 11423 +11884 11425 11880 11426 11881 11424 11426 11427 11881 +11427 11883 11881 11427 11885 11883 11884 11428 11425 +11886 11428 11884 11429 11885 11427 11429 11887 11885 +11886 11430 11428 11888 11430 11886 11431 11887 11429 +11431 11889 11887 11888 11432 11430 11890 11432 11888 +11433 11889 11431 11433 11891 11889 11890 11434 11432 +11892 11434 11890 11435 11891 11433 11435 11893 11891 +11892 11436 11434 11894 11436 11892 11437 11893 11435 +11437 11895 11893 11896 11897 11438 11438 11897 11439 +11438 11436 11894 11438 11894 11896 11441 11895 11437 +11441 11898 11895 11897 11899 11439 11439 11899 11442 +11443 11442 11899 11443 11899 11900 11901 11898 11441 +11901 11441 11445 11445 11446 11901 11901 11446 11902 +11900 11447 11443 11903 11447 11900 11904 11905 11449 +11904 11449 11448 11446 11449 11902 11902 11449 11905 +11903 11450 11447 11906 11450 11903 11451 11904 11448 +11451 11907 11904 11906 11452 11450 11908 11452 11906 +11453 11907 11451 11453 11909 11907 11908 11454 11452 +11910 11454 11908 11455 11909 11453 11455 11911 11909 +11910 11456 11454 11912 11456 11910 11457 11913 11914 +11911 11457 11914 11455 11457 11911 11913 11915 11914 +11912 11458 11456 11916 11458 11912 11460 11913 11457 +11460 11461 11913 11461 11915 11913 11461 11917 11915 +11916 11462 11458 11918 11462 11916 11919 11920 11921 +11464 11463 11921 11920 11464 11921 11922 11464 11920 +11464 11923 11466 11922 11923 11464 11466 11924 11468 +11923 11924 11466 11469 11468 11924 11469 11924 11925 +11472 11917 11461 11472 11926 11917 11918 11473 11462 +11927 11473 11918 11928 11929 11930 11931 11932 11930 +11931 11930 11929 11475 11474 11932 11931 11475 11932 +11933 11475 11931 11921 11477 11919 11477 11933 11919 +11477 11475 11933 11477 11921 11463 11925 11934 11469 +11469 11934 11478 11478 11935 11479 11934 11935 11478 +11479 11936 11480 11935 11936 11479 11480 11937 11481 +11936 11937 11480 11481 11938 11482 11937 11938 11481 +11482 11939 11483 11938 11939 11482 11483 11940 11484 +11939 11940 11483 11484 11941 11485 11940 11941 11484 +11487 11485 11941 11487 11941 11942 11488 11943 11489 +11943 11944 11489 11944 11472 11489 11944 11926 11472 +11945 11490 11946 11490 11927 11946 11490 11473 11927 +11947 11945 11946 11948 11949 11492 11948 11492 11491 +11492 11950 11494 11949 11950 11492 11930 11495 11928 +11495 11950 11928 11495 11494 11950 11495 11930 11496 +11496 11930 11932 11496 11932 11474 11942 11951 11487 +11487 11951 11497 11497 11952 11498 11951 11952 11497 +11498 11953 11499 11952 11953 11498 11501 11499 11953 +11501 11953 11954 11955 11956 11943 11488 11955 11943 +11502 11955 11488 11943 11956 11944 11957 11958 11945 +11958 11490 11945 11958 11503 11490 11957 11945 11947 +11959 11960 11505 11959 11505 11504 11505 11961 11506 +11960 11961 11505 11506 11962 11507 11961 11962 11506 +11491 11507 11948 11948 11507 11962 11954 11963 11501 +11501 11963 11508 11508 11964 11510 11963 11964 11508 +11502 11510 11955 11955 11510 11964 11958 11965 11503 +11503 11965 11511 11511 11966 11512 11965 11966 11511 +11512 11967 11513 11966 11967 11512 11504 11513 11959 +11959 11513 11967 11968 11627 11626 11629 11968 11626 +11969 11636 11635 11627 11968 11635 11968 11969 11635 +11968 11970 11969 11637 11971 11629 11971 11968 11629 +11971 11970 11968 11638 11972 11637 11637 11972 11971 +11640 11972 11638 11973 11644 11643 11974 11973 11643 +11974 11643 11636 11975 11974 11636 11969 11975 11636 +11975 11969 11970 11975 11970 11976 11976 11970 11971 +11976 11971 11977 11978 11979 11977 11971 11978 11977 +11972 11978 11971 11645 11980 11640 11980 11972 11640 +11980 11978 11972 11647 11980 11645 11981 11651 11650 +11982 11981 11650 11982 11650 11644 11983 11982 11644 +11973 11983 11644 11983 11973 11974 11984 11979 11978 +11985 11986 11984 11978 11985 11984 11980 11985 11978 +11652 11987 11647 11987 11980 11647 11987 11985 11980 +11654 11987 11652 11988 11989 11656 11656 11989 11657 +11656 11651 11981 11990 11656 11981 11988 11656 11990 +11990 11981 11982 11991 11986 11985 11992 11993 11991 +11985 11992 11991 11987 11992 11985 11659 11992 11654 +11654 11992 11987 11989 11662 11657 11994 11662 11989 +11995 11993 11992 11996 11995 11997 11997 11995 11663 +11995 11992 11663 11992 11659 11663 11665 11997 11663 +11998 11999 11667 11667 11999 11668 11667 11662 11994 +11667 11994 11998 12000 11996 11997 12001 12000 12002 +12002 12000 11669 12000 11997 11669 11997 11665 11669 +11671 12002 11669 12003 12004 11673 11673 12004 11674 +11673 11668 11999 11673 11999 12003 12005 12001 12002 +12006 12005 12007 12007 12005 11675 12005 12002 11675 +12002 11671 11675 11677 12007 11675 12008 12009 11680 +11680 12009 11681 11680 11674 12004 11680 12004 12008 +12010 12006 12007 12010 12007 11677 11683 12010 11677 +12011 12010 11683 12012 12011 12013 12011 11684 12013 +12011 11683 11684 11686 12013 11684 12014 12015 12016 +12017 12018 12016 12016 12018 12014 12009 12018 12017 +11688 12009 12017 11681 12009 11688 12019 12012 12013 +12020 12019 12021 12021 12019 11689 12019 12013 11689 +12013 11686 11689 11691 12021 11689 12022 12023 12024 +12025 12026 12024 12024 12026 12022 12026 12025 12015 +12015 12025 12016 12025 11696 12016 11696 11695 12016 +12016 11695 11698 12016 11698 12017 12017 11698 11688 +12027 12020 12021 12028 12027 12029 12029 12027 11699 +12027 12021 11699 12021 11691 11699 11701 12029 11699 +12024 12023 12030 12030 12023 12031 12030 11706 11705 +12025 12030 11705 12024 12030 12025 12025 11705 11696 +12028 12029 12032 12028 12032 12033 11708 12034 11701 +12034 12029 11701 12034 12032 12029 11709 12035 11708 +11708 12035 12034 11711 12035 11709 12030 12031 12036 +12036 12031 12037 12036 11714 11706 12036 11706 12030 +12038 12033 12032 12038 12032 12034 12038 12034 12039 +12039 12034 12035 12039 12035 12040 12040 12035 11711 +11715 12040 11711 12041 12040 11715 12042 11715 11716 +12042 12041 11715 11716 11717 12042 12042 11717 12043 +12036 12037 12044 12044 12037 12045 12044 11720 11714 +12044 11714 12036 12046 12043 11717 12046 11717 11721 +12047 11721 11722 12047 12046 11721 11722 11723 12047 +12047 11723 12048 12044 12045 12049 12049 12045 12050 +12049 11725 11720 12049 11720 12044 12051 12048 11723 +12051 11723 11726 12052 12053 11727 12053 11726 11727 +12053 12051 11726 12052 12054 12053 12049 12050 12055 +12055 12050 12056 12055 11729 11725 12055 11725 12049 +11730 12052 11727 12057 12058 11731 11731 12058 11730 +12058 12054 11730 12054 12052 11730 12057 12059 12058 +12055 12056 12060 12060 12056 12061 12060 11733 11729 +12060 11729 12055 11736 12057 11731 11736 11737 12057 +11737 12059 12057 11737 12062 12059 12061 12063 12060 +12063 12064 12065 12063 12065 11738 12060 12063 11738 +11733 12060 11738 12065 11739 11738 11746 12066 12067 +12062 11746 12067 11737 11746 12062 12066 12068 12067 +12064 12069 12065 12065 12069 11739 12069 11747 11739 +12069 12070 11747 12070 12071 12072 11748 12070 12072 +11747 12070 11748 12073 11749 11748 12073 11748 12072 +12074 11750 11749 12074 11749 12073 12075 11751 11750 +12075 11750 12074 12076 11752 11751 12076 11751 12075 +12077 11753 11752 12077 11752 12076 12078 11754 11753 +12078 11753 12077 11754 12078 12079 12080 11754 12079 +11755 11754 12080 11756 11755 12080 11756 12080 12081 +11762 12082 11761 12066 11746 11761 12082 12066 11761 +12083 12066 12082 12068 12066 12084 12066 12085 12084 +12066 12083 12085 12085 12086 12084 12071 12087 12072 +12072 12087 12073 12073 12087 12088 12073 12088 12074 +12074 12088 12089 12074 12089 12075 12075 12089 12090 +12075 12090 12076 12076 12090 12091 12076 12091 12077 +12077 12091 12092 12077 12092 12078 12078 12092 12093 +12078 12093 12079 12081 12094 11756 11756 12094 11763 +11763 12095 11764 12094 12095 11763 11764 12096 11765 +12095 12096 11764 11765 12097 11766 12096 12097 11765 +11766 12098 11767 12097 12098 11766 11767 12099 11768 +12098 12099 11767 12100 12101 11769 11768 12100 11769 +12099 12100 11768 12102 12101 12100 11779 12103 11778 +12104 11780 11778 12104 11778 12103 12082 11762 11780 +12104 12082 11780 12105 12082 12104 12082 12106 12083 +12082 12105 12106 12107 12085 12108 12085 12106 12108 +12085 12083 12106 12085 12107 12086 12101 11781 11769 +12102 12109 12101 12109 11781 12101 12109 11782 11781 +11782 12110 11783 12109 12110 11782 11783 12111 11784 +12110 12111 11783 12111 12112 12113 11785 12111 12113 +11784 12111 11785 12113 11786 11785 11788 12114 11789 +12115 11790 11789 12115 11789 12114 12116 11791 11790 +12116 11790 12115 12117 11792 11791 12117 11791 12116 +12118 11793 11792 12118 11792 12117 11793 12118 12119 +12120 11793 12119 11794 11793 12120 11779 11794 12103 +11794 12121 12103 11794 12120 12121 12103 12121 12104 +12104 12121 12122 12104 12122 12105 12105 12122 12123 +12105 12123 12106 12106 12123 12124 12106 12124 12108 +12112 12125 12113 12113 12125 11786 12125 11795 11786 +12125 12126 11795 11788 11795 12114 11795 12127 12114 +11795 12126 12127 12114 12127 12115 12115 12127 12128 +12115 12128 12116 12116 12128 12129 12116 12129 12117 +12117 12129 12130 12117 12130 12118 12118 12130 12131 +12118 12131 12119 12132 12133 12134 12135 12136 12134 +12134 12136 12132 12135 12137 12136 12138 12139 12140 +11797 11796 12140 11796 12138 12140 11796 12141 12138 +12142 11796 11800 12142 12141 11796 12143 11800 11802 +12143 12142 11800 12144 11802 11804 12144 12143 11802 +11804 11805 12144 12144 11805 12145 12146 12147 12148 +12149 12150 12148 12148 12150 12146 11807 11806 12149 +11806 12150 12149 11806 12151 12150 12152 11806 11810 +12152 12151 11806 12153 11810 11811 12153 12152 11810 +12154 11811 11812 12154 12153 11811 11813 12134 12133 +12154 11813 12133 11812 11813 12154 12134 11813 11814 +12134 11814 12135 12155 12156 11815 11815 12156 11814 +12156 12137 11814 12137 12135 11814 12155 12157 12156 +12158 12159 12160 11818 11817 12160 11817 12158 12160 +11817 12161 12158 12162 11817 11819 12162 12161 11817 +11820 12140 12139 12162 11820 12139 11819 11820 12162 +12140 11820 11797 11821 12163 12164 12145 11821 12164 +11805 11821 12145 12165 12166 12163 12163 12166 12164 +12167 12168 12165 12165 12168 12166 12169 12170 12167 +12167 12170 12168 12171 12172 12169 12169 12172 12170 +12173 12174 12171 12171 12174 12172 12175 12176 12173 +12173 12176 12174 12177 12178 12175 12175 12178 12176 +12177 12179 12178 12180 12181 12182 12182 11823 12180 +11823 11822 12180 12183 12184 12180 12183 12180 11822 +12185 12183 12186 12183 11822 12186 11825 12187 12186 +11825 12186 11822 12188 12189 12187 12187 12189 12186 +12189 12188 12147 12147 12188 12148 12188 11827 12148 +11827 11826 12148 12148 11826 11829 12148 11829 12149 +12149 11829 11807 11830 12155 11815 12190 12157 12155 +11830 12190 12155 11831 12190 11830 12191 12192 11832 +12192 11831 11832 12192 12190 11831 12191 12193 12192 +12160 12159 11833 12159 11834 11833 12159 12194 11834 +12160 11833 11818 11835 12163 11821 12163 11835 11836 +12163 11836 12165 12165 11836 11837 12165 11837 12167 +12167 11837 11838 12167 11838 12169 12169 11838 11839 +12169 11839 12171 12171 11839 11840 12171 11840 12173 +12173 11840 11841 12173 11841 12175 12175 11841 11842 +12175 11842 12177 12195 12179 12177 11842 12195 12177 +11843 12195 11842 11844 12182 12181 12195 11844 12181 +11843 11844 12195 12183 12196 12184 11823 12182 11844 +11825 11845 12187 12183 12185 12196 12187 11845 11846 +12187 11846 12188 12188 11846 11827 12191 11832 11848 +12197 12198 12199 12198 12197 11849 12197 12193 11849 +12191 11848 11849 12191 11849 12193 12194 11850 11834 +12200 11850 12194 12199 12198 11852 11851 12199 11852 +12201 12199 11851 11852 12198 11849 12200 11853 11850 +12202 11853 12200 11855 12201 11851 11855 12203 12201 +12202 11856 11853 12204 11856 12202 11857 12205 11858 +12205 12206 11858 12206 11855 11858 12206 12203 11855 +12204 12207 11856 11856 12207 11859 11860 11859 12207 +11860 12207 12208 12209 12210 12205 11857 12209 12205 +11861 12209 11857 12205 12210 12206 12208 11862 11860 +12211 11862 12208 11863 12209 11861 11863 12212 12209 +12213 11864 12214 11864 12211 12214 11864 11862 12211 +12215 12213 12214 11865 12212 11863 11865 12216 12212 +12213 11866 11864 11867 11866 12213 12215 11867 12213 +12217 11867 12215 11868 12216 11865 11868 12218 12216 +12217 11869 11867 12219 11869 12217 11870 12218 11868 +11870 12220 12218 12221 12222 12223 12219 12221 12223 +11871 12219 12223 11869 12219 11871 11872 12220 11870 +11872 12224 12220 12222 12225 12223 11871 12223 12225 +12226 11871 12225 11873 11871 12226 11874 12224 11872 +11874 12227 12224 12226 11875 11873 12228 11875 12226 +12229 12230 11877 12229 11877 11876 11874 11877 12227 +12227 11877 12230 12228 11878 11875 12231 11878 12228 +11879 12229 11876 11879 12232 12229 12231 11880 11878 +12233 11880 12231 12234 12232 11879 12234 11879 11882 +11882 11883 12234 12234 11883 12235 12233 11884 11880 +12236 11884 12233 11885 12235 11883 11885 12237 12235 +12236 11886 11884 12238 11886 12236 11887 12237 11885 +11887 12239 12237 12238 11888 11886 12240 11888 12238 +11889 12239 11887 11889 12241 12239 12240 11890 11888 +12242 11890 12240 11891 12241 11889 11891 12243 12241 +12242 11892 11890 12244 11892 12242 11893 12243 11891 +11893 12245 12243 12244 11894 11892 12246 11894 12244 +11895 12245 11893 11895 12247 12245 12248 12249 11896 +11896 12249 11897 11896 11894 12246 11896 12246 12248 +11898 12250 12251 12247 11898 12251 11895 11898 12247 +12250 12252 12251 12249 12253 11897 11897 12253 11899 +11900 11899 12253 11900 12253 12254 12250 11898 11901 +12255 12256 12257 12256 12255 11902 12255 12252 11902 +12250 11901 11902 12250 11902 12252 12254 11903 11900 +12258 11903 12254 12257 12256 11905 11904 12257 11905 +12259 12257 11904 11905 12256 11902 12258 11906 11903 +12260 11906 12258 11907 12259 11904 11907 12261 12259 +12260 11908 11906 12262 11908 12260 11909 12261 11907 +11909 12263 12261 12262 11910 11908 12264 11910 12262 +11911 12265 12266 12263 11911 12266 11909 11911 12263 +12265 12267 12266 12264 11912 11910 12268 11912 12264 +11914 12265 11911 11914 11915 12265 11915 12267 12265 +11915 12269 12267 12268 11916 11912 12270 11916 12268 +12271 12272 12273 12274 12273 12272 11917 12269 11915 +11917 12275 12269 12270 11918 11916 12276 11918 12270 +12277 12278 12279 11920 11919 12279 12278 11920 12279 +12280 11920 12278 11920 12281 11922 12280 12281 11920 +11922 12282 11923 12281 12282 11922 12273 11924 12271 +11924 12282 12271 11924 11923 12282 12283 12284 11925 +12283 11925 11924 12274 12283 11924 12273 12274 11924 +12285 12286 12284 12285 12284 12283 12287 12288 12286 +12287 12286 12285 12289 12290 12288 12289 12288 12287 +12291 12292 12290 12291 12290 12289 12293 12294 12292 +12293 12292 12291 12295 12294 12293 11926 12275 11917 +11926 12296 12275 12276 11927 11918 12297 11927 12276 +12298 12299 11929 12298 11929 11928 11929 12300 11931 +12299 12300 11929 12279 11933 12277 11933 12300 12277 +11933 11931 12300 11933 12279 11919 12284 11934 11925 +11934 12284 11935 11935 12284 12286 11935 12286 11936 +11936 12286 12288 11936 12288 11937 11937 12288 12290 +11937 12290 11938 11938 12290 12292 11938 12292 11939 +11939 12292 12294 12295 12301 12294 12301 11939 12294 +12301 11940 11939 11940 12302 11941 12301 12302 11940 +12303 12304 11942 11941 12303 11942 12302 12303 11941 +12305 12304 12303 11944 12296 11926 11944 12306 12296 +11946 11927 12307 11927 12308 12307 11927 12297 12308 +12307 11947 11946 12309 12310 12311 11949 11948 12311 +12310 11949 12311 12312 11949 12310 11949 12313 11950 +12312 12313 11949 11928 11950 12298 12298 11950 12313 +12304 11951 11942 12305 12314 12304 12314 11951 12304 +12314 11952 11951 12314 12315 12316 11953 12314 12316 +11952 12314 11953 12316 11954 11953 12317 12318 11956 +12317 11956 11955 11944 11956 12306 12306 11956 12318 +12319 12320 12308 12320 11958 12308 11957 12307 12308 +11957 12308 11958 11957 11947 12307 12320 12319 12321 +12322 12323 11960 12322 11960 11959 11960 12324 11961 +12323 12324 11960 12311 11962 12309 11962 12324 12309 +11962 11961 12324 11962 12311 11948 12315 12325 12316 +12316 12325 11954 12325 11963 11954 12325 12326 11963 +11963 12327 11964 12326 12327 11963 11955 11964 12317 +12317 11964 12327 12320 11965 11958 12321 12328 12320 +12328 11965 12320 12328 11966 11965 11966 12329 11967 +12328 12329 11966 11959 11967 12322 12322 11967 12329 +12330 11974 11975 11976 12331 11975 11975 12331 12330 +11977 12332 11976 11976 12332 12331 11979 12332 11977 +12333 12334 11983 11983 12334 11982 11983 11974 12330 +12335 11983 12330 12333 11983 12335 12336 12337 12335 +12330 12336 12335 12331 12336 12330 12338 12331 12332 +12336 12331 12338 11984 12339 11979 12339 12332 11979 +12339 12338 12332 11986 12339 11984 12340 11989 11988 +12340 11988 12341 11988 12342 12341 11988 11990 12342 +11990 11982 12334 11990 12334 12342 12343 12337 12336 +12343 12336 12338 12343 12338 12344 12344 12338 12339 +12344 12339 12345 12346 12345 12347 12347 12345 11991 +12345 12339 11991 12339 11986 11991 11993 12347 11991 +12348 12349 12340 12349 11989 12340 12349 11994 11989 +12348 12340 12341 12350 12346 12347 12351 12350 12352 +12352 12350 11995 12350 12347 11995 12347 11993 11995 +11996 12352 11995 12353 12354 11998 11998 12354 11999 +11998 11994 12349 11998 12349 12353 12355 12351 12352 +12356 12355 12357 12357 12355 12000 12355 12352 12000 +12352 11996 12000 12001 12357 12000 12358 12359 12360 +12004 12003 12360 12003 12358 12360 12003 12361 12358 +12003 11999 12354 12003 12354 12361 12362 12356 12357 +12363 12362 12364 12364 12362 12005 12362 12357 12005 +12357 12001 12005 12006 12364 12005 12365 12366 12367 +12365 12367 12359 12359 12367 12360 12367 12009 12360 +12009 12008 12360 12360 12008 12004 12368 12363 12364 +12368 12364 12006 12010 12368 12006 12369 12368 12010 +12370 12369 12371 12369 12011 12371 12369 12010 12011 +12012 12371 12011 12372 12373 12374 12015 12014 12374 +12014 12372 12374 12014 12375 12372 12018 12367 12366 +12375 12018 12366 12014 12018 12375 12367 12018 12009 +12376 12370 12371 12377 12376 12378 12378 12376 12019 +12376 12371 12019 12371 12012 12019 12020 12378 12019 +12379 12380 12381 12023 12022 12381 12022 12379 12381 +12022 12382 12379 12026 12374 12373 12382 12026 12373 +12022 12026 12382 12374 12026 12015 12383 12377 12378 +12384 12383 12385 12385 12383 12027 12383 12378 12027 +12378 12020 12027 12028 12385 12027 12381 12380 12386 +12386 12380 12387 12386 12031 12023 12386 12023 12381 +12388 12384 12385 12385 12028 12388 12028 12389 12388 +12028 12033 12389 12386 12387 12390 12390 12387 12391 +12390 12037 12031 12390 12031 12386 12392 12389 12033 +12392 12033 12038 12393 12038 12039 12393 12392 12038 +12394 12039 12040 12394 12393 12039 12395 12040 12041 +12395 12394 12040 12396 12395 12397 12395 12042 12397 +12395 12041 12042 12043 12397 12042 12390 12391 12398 +12398 12391 12399 12398 12045 12037 12398 12037 12390 +12400 12396 12397 12400 12397 12043 12046 12400 12043 +12401 12400 12046 12402 12046 12047 12402 12401 12046 +12403 12404 12048 12404 12047 12048 12404 12402 12047 +12403 12405 12404 12398 12399 12406 12406 12399 12407 +12406 12050 12045 12406 12045 12398 12051 12403 12048 +12408 12405 12403 12051 12408 12403 12053 12408 12051 +12053 12054 12408 12408 12054 12409 12406 12407 12410 +12410 12407 12411 12410 12056 12050 12410 12050 12406 +12412 12409 12054 12412 12054 12058 12058 12059 12412 +12412 12059 12413 12411 12414 12410 12056 12410 12414 +12415 12056 12414 12061 12056 12415 12062 12416 12417 +12413 12062 12417 12059 12062 12413 12416 12418 12417 +12063 12061 12419 12061 12420 12419 12061 12415 12420 +12419 12064 12063 12067 12416 12062 12421 12422 12068 +12068 12422 12067 12422 12418 12067 12418 12416 12067 +12423 12424 12421 12421 12424 12422 12423 12425 12424 +12420 12426 12419 12426 12427 12428 12426 12428 12069 +12419 12426 12069 12064 12419 12069 12429 12070 12069 +12429 12069 12428 12429 12071 12070 12430 12431 12080 +12430 12080 12079 12081 12080 12431 12081 12431 12432 +12084 12421 12068 12421 12084 12086 12433 12421 12086 +12423 12421 12433 12425 12423 12434 12423 12435 12434 +12423 12433 12435 12435 12436 12434 12427 12437 12428 +12428 12437 12429 12429 12437 12438 12429 12438 12071 +12438 12087 12071 12438 12439 12087 12087 12440 12088 +12439 12440 12087 12088 12441 12089 12440 12441 12088 +12089 12442 12090 12441 12442 12089 12090 12443 12091 +12442 12443 12090 12091 12444 12092 12443 12444 12091 +12092 12445 12093 12444 12445 12092 12079 12093 12430 +12430 12093 12445 12432 12446 12081 12081 12446 12094 +12094 12447 12095 12446 12447 12094 12095 12448 12096 +12447 12448 12095 12096 12449 12097 12448 12449 12096 +12097 12450 12098 12449 12450 12097 12098 12451 12099 +12450 12451 12098 12099 12452 12100 12451 12452 12099 +12453 12454 12102 12100 12453 12102 12452 12453 12100 +12455 12454 12453 12108 12456 12107 12433 12086 12107 +12456 12433 12107 12457 12433 12456 12433 12458 12435 +12433 12457 12458 12436 12435 12459 12459 12435 12458 +12454 12109 12102 12455 12460 12454 12460 12109 12454 +12460 12110 12109 12110 12461 12111 12460 12461 12110 +12112 12111 12461 12112 12461 12462 12119 12463 12120 +12464 12121 12120 12464 12120 12463 12465 12122 12121 +12465 12121 12464 12122 12465 12466 12467 12122 12466 +12123 12122 12467 12467 12468 12469 12124 12467 12469 +12123 12467 12124 12456 12108 12124 12469 12456 12124 +12470 12456 12469 12471 12457 12472 12457 12470 12472 +12457 12456 12470 12457 12471 12458 12458 12471 12473 +12458 12473 12459 12462 12474 12112 12112 12474 12125 +12125 12475 12126 12474 12475 12125 12126 12476 12127 +12475 12476 12126 12127 12477 12128 12476 12477 12127 +12128 12478 12129 12477 12478 12128 12129 12479 12130 +12478 12479 12129 12130 12480 12131 12479 12480 12130 +12119 12131 12463 12131 12481 12463 12131 12480 12481 +12463 12481 12464 12464 12481 12482 12464 12482 12465 +12465 12482 12483 12465 12483 12466 12468 12484 12469 +12469 12484 12470 12470 12484 12485 12470 12485 12472 +12486 12487 12488 12489 12490 12488 12488 12490 12486 +12133 12132 12489 12132 12490 12489 12132 12491 12490 +12492 12132 12136 12492 12491 12132 12136 12137 12492 +12492 12137 12493 12494 12495 12496 12497 12498 12496 +12496 12498 12494 12139 12138 12497 12138 12498 12497 +12138 12499 12498 12500 12138 12141 12500 12499 12138 +12501 12141 12142 12501 12500 12141 12502 12142 12143 +12502 12501 12142 12503 12143 12144 12503 12502 12143 +12144 12145 12503 12503 12145 12504 12505 12506 12507 +12508 12509 12507 12507 12509 12505 12508 12510 12509 +12511 12512 12513 12147 12146 12513 12146 12511 12513 +12146 12514 12511 12515 12146 12150 12515 12514 12146 +12516 12150 12151 12516 12515 12150 12517 12151 12152 +12517 12516 12151 12153 12488 12487 12517 12153 12487 +12152 12153 12517 12488 12153 12154 12488 12154 12489 +12489 12154 12133 12518 12493 12137 12518 12137 12156 +12519 12520 12157 12520 12156 12157 12520 12518 12156 +12519 12521 12520 12522 12523 12158 12158 12523 12159 +12161 12496 12495 12522 12161 12495 12158 12161 12522 +12496 12161 12162 12496 12162 12497 12497 12162 12139 +12524 12504 12145 12524 12145 12164 12525 12164 12166 +12525 12524 12164 12526 12166 12168 12526 12525 12166 +12527 12168 12170 12527 12526 12168 12528 12170 12172 +12528 12527 12170 12529 12172 12174 12529 12528 12172 +12176 12507 12506 12529 12176 12506 12174 12176 12529 +12507 12176 12178 12507 12178 12508 12530 12531 12179 +12179 12531 12178 12531 12510 12178 12510 12508 12178 +12181 12180 12530 12180 12531 12530 12180 12532 12531 +12533 12534 12184 12534 12180 12184 12534 12532 12180 +12185 12186 12533 12186 12534 12533 12186 12535 12534 +12189 12513 12512 12535 12189 12512 12186 12189 12535 +12513 12189 12147 12190 12519 12157 12536 12521 12519 +12190 12536 12519 12192 12536 12190 12192 12193 12536 +12536 12193 12537 12523 12194 12159 12538 12194 12523 +12195 12530 12179 12530 12195 12181 12196 12533 12184 +12533 12196 12185 12539 12540 12197 12539 12197 12199 +12193 12197 12537 12537 12197 12540 12538 12200 12194 +12541 12200 12538 12201 12539 12199 12201 12542 12539 +12541 12202 12200 12543 12202 12541 12203 12542 12201 +12203 12544 12542 12543 12204 12202 12545 12204 12543 +12206 12544 12203 12206 12546 12544 12545 12547 12204 +12204 12547 12207 12208 12207 12547 12208 12547 12548 +12549 12550 12210 12549 12210 12209 12206 12210 12546 +12546 12210 12550 12548 12211 12208 12551 12211 12548 +12212 12549 12209 12212 12552 12549 12551 12553 12211 +12211 12553 12214 12215 12214 12553 12215 12553 12554 +12216 12552 12212 12216 12555 12552 12554 12217 12215 +12556 12217 12554 12218 12555 12216 12218 12557 12555 +12556 12219 12217 12558 12219 12556 12220 12557 12218 +12220 12559 12557 12560 12561 12221 12221 12561 12222 +12221 12219 12558 12221 12558 12560 12224 12559 12220 +12224 12562 12559 12561 12563 12222 12222 12563 12225 +12226 12225 12563 12226 12563 12564 12565 12566 12567 +12566 12562 12567 12562 12227 12567 12562 12224 12227 +12564 12228 12226 12568 12228 12564 12565 12567 12230 +12229 12565 12230 12569 12565 12229 12230 12567 12227 +12568 12231 12228 12570 12231 12568 12232 12569 12229 +12232 12571 12569 12570 12233 12231 12572 12233 12570 +12573 12571 12232 12573 12232 12234 12234 12235 12573 +12573 12235 12574 12572 12236 12233 12575 12236 12572 +12237 12574 12235 12237 12576 12574 12575 12238 12236 +12577 12238 12575 12239 12576 12237 12239 12578 12576 +12577 12240 12238 12579 12240 12577 12241 12578 12239 +12241 12580 12578 12579 12242 12240 12581 12242 12579 +12243 12580 12241 12243 12582 12580 12581 12244 12242 +12583 12244 12581 12245 12582 12243 12245 12584 12582 +12585 12586 12587 12583 12585 12587 12246 12583 12587 +12244 12583 12246 12247 12584 12245 12247 12588 12584 +12589 12590 12586 12590 12249 12586 12248 12587 12586 +12248 12586 12249 12248 12246 12587 12590 12589 12591 +12592 12588 12247 12592 12247 12251 12251 12252 12592 +12592 12252 12593 12590 12253 12249 12254 12253 12590 +12591 12254 12590 12594 12254 12591 12595 12596 12255 +12595 12255 12257 12252 12255 12593 12593 12255 12596 +12594 12258 12254 12597 12258 12594 12259 12595 12257 +12259 12598 12595 12597 12260 12258 12599 12260 12597 +12261 12598 12259 12261 12600 12598 12599 12262 12260 +12601 12262 12599 12263 12600 12261 12263 12602 12600 +12601 12264 12262 12603 12264 12601 12604 12602 12263 +12604 12263 12266 12266 12267 12604 12604 12267 12605 +12603 12268 12264 12606 12268 12603 12269 12605 12267 +12269 12607 12605 12606 12270 12268 12608 12270 12606 +12609 12610 12611 12612 12613 12611 12612 12611 12610 +12272 12271 12613 12612 12272 12613 12614 12272 12612 +12274 12272 12614 12274 12614 12615 12275 12607 12269 +12275 12616 12607 12608 12276 12270 12617 12276 12608 +12618 12619 12620 12278 12277 12620 12619 12278 12620 +12621 12278 12619 12278 12622 12280 12621 12622 12278 +12611 12281 12609 12281 12622 12609 12281 12280 12622 +12281 12611 12282 12282 12611 12613 12282 12613 12271 +12615 12623 12274 12274 12623 12283 12283 12624 12285 +12623 12624 12283 12285 12625 12287 12624 12625 12285 +12287 12626 12289 12625 12626 12287 12289 12627 12291 +12626 12627 12289 12291 12628 12293 12627 12628 12291 +12629 12630 12295 12293 12629 12295 12628 12629 12293 +12631 12630 12629 12296 12616 12275 12296 12632 12616 +12617 12297 12276 12633 12297 12617 12634 12635 12636 +12299 12298 12636 12635 12299 12636 12637 12299 12635 +12620 12300 12618 12300 12637 12618 12300 12299 12637 +12300 12620 12277 12630 12301 12295 12631 12638 12630 +12638 12301 12630 12638 12302 12301 12302 12639 12303 +12638 12639 12302 12640 12641 12305 12303 12640 12305 +12639 12640 12303 12642 12641 12640 12643 12644 12645 +12644 12632 12645 12632 12306 12645 12632 12296 12306 +12633 12308 12297 12646 12308 12633 12647 12648 12310 +12647 12310 12309 12310 12649 12312 12648 12649 12310 +12636 12313 12634 12313 12649 12634 12313 12312 12649 +12313 12636 12298 12641 12314 12305 12315 12314 12641 +12642 12315 12641 12650 12315 12642 12651 12652 12653 +12653 12652 12643 12653 12643 12645 12317 12653 12645 +12318 12317 12645 12318 12645 12306 12646 12654 12308 +12308 12654 12319 12655 12656 12321 12319 12655 12321 +12654 12655 12319 12657 12658 12656 12657 12656 12655 +12323 12322 12658 12657 12323 12658 12659 12323 12657 +12323 12660 12324 12659 12660 12323 12309 12324 12647 +12647 12324 12660 12650 12661 12315 12315 12661 12325 +12325 12662 12326 12661 12662 12325 12653 12327 12651 +12327 12662 12651 12327 12326 12662 12327 12653 12317 +12656 12328 12321 12328 12656 12329 12329 12656 12658 +12329 12658 12322 12663 12334 12333 12663 12333 12664 +12333 12665 12664 12333 12335 12665 12335 12337 12665 +12665 12337 12666 12667 12341 12342 12668 12667 12342 +12668 12342 12334 12669 12668 12334 12663 12669 12334 +12669 12663 12664 12670 12666 12337 12670 12337 12343 +12671 12670 12672 12670 12344 12672 12670 12343 12344 +12345 12673 12344 12344 12673 12672 12346 12673 12345 +12674 12675 12348 12348 12675 12349 12348 12341 12667 +12676 12348 12667 12674 12348 12676 12676 12667 12668 +12677 12671 12672 12677 12672 12673 12677 12673 12678 +12679 12678 12680 12680 12678 12350 12678 12673 12350 +12673 12346 12350 12351 12680 12350 12681 12682 12683 +12354 12353 12683 12353 12681 12683 12353 12684 12681 +12353 12349 12675 12353 12675 12684 12685 12679 12680 +12686 12685 12687 12687 12685 12355 12685 12680 12355 +12680 12351 12355 12356 12687 12355 12688 12689 12690 +12359 12358 12690 12358 12688 12690 12358 12691 12688 +12361 12683 12682 12691 12361 12682 12358 12361 12691 +12683 12361 12354 12692 12686 12687 12693 12692 12694 +12694 12692 12362 12692 12687 12362 12687 12356 12362 +12363 12694 12362 12695 12696 12697 12698 12699 12697 +12697 12699 12695 12699 12698 12689 12689 12698 12690 +12698 12366 12690 12366 12365 12690 12690 12365 12359 +12700 12693 12694 12700 12694 12363 12368 12700 12363 +12701 12700 12368 12702 12701 12703 12701 12369 12703 +12701 12368 12369 12370 12703 12369 12704 12705 12706 +12707 12708 12706 12706 12708 12704 12708 12707 12696 +12696 12707 12697 12707 12373 12697 12373 12372 12697 +12697 12372 12375 12697 12375 12698 12698 12375 12366 +12702 12703 12709 12702 12709 12710 12376 12711 12370 +12711 12703 12370 12711 12709 12703 12377 12711 12376 +12712 12713 12714 12712 12714 12705 12705 12714 12706 +12714 12380 12706 12380 12379 12706 12706 12379 12382 +12706 12382 12707 12707 12382 12373 12715 12710 12709 +12715 12709 12711 12715 12711 12716 12717 12716 12718 +12718 12716 12383 12716 12711 12383 12711 12377 12383 +12384 12718 12383 12714 12713 12719 12719 12713 12720 +12719 12387 12380 12719 12380 12714 12721 12717 12718 +12722 12721 12723 12723 12721 12388 12721 12718 12388 +12718 12384 12388 12389 12723 12388 12719 12720 12724 +12724 12720 12725 12724 12391 12387 12724 12387 12719 +12726 12722 12723 12726 12723 12389 12392 12726 12389 +12727 12726 12392 12728 12392 12393 12728 12727 12392 +12729 12393 12394 12729 12728 12393 12730 12394 12395 +12730 12729 12394 12395 12396 12730 12730 12396 12731 +12724 12725 12732 12732 12725 12733 12732 12399 12391 +12732 12391 12724 12734 12731 12396 12734 12396 12400 +12735 12400 12401 12735 12734 12400 12736 12401 12402 +12736 12735 12401 12737 12402 12404 12737 12736 12402 +12404 12405 12737 12737 12405 12738 12732 12733 12739 +12739 12733 12740 12739 12407 12399 12739 12399 12732 +12741 12738 12405 12741 12405 12408 12742 12743 12409 +12743 12408 12409 12743 12741 12408 12742 12744 12743 +12740 12745 12739 12407 12739 12745 12746 12407 12745 +12411 12407 12746 12412 12742 12409 12412 12413 12742 +12413 12744 12742 12413 12747 12744 12746 12748 12411 +12411 12748 12414 12415 12414 12748 12415 12748 12749 +12750 12747 12413 12750 12413 12417 12751 12752 12418 +12752 12417 12418 12752 12750 12417 12753 12754 12751 +12751 12754 12752 12753 12755 12754 12749 12420 12415 +12756 12420 12749 12422 12751 12418 12751 12422 12424 +12751 12424 12753 12757 12758 12425 12425 12758 12424 +12758 12755 12424 12755 12753 12424 12759 12760 12757 +12757 12760 12758 12761 12762 12759 12759 12762 12760 +12763 12764 12761 12761 12764 12762 12763 12765 12764 +12756 12766 12420 12420 12766 12426 12767 12768 12427 +12426 12767 12427 12766 12767 12426 12769 12768 12767 +12770 12771 12772 12773 12431 12430 12774 12772 12771 +12773 12432 12431 12775 12776 12777 12778 12779 12777 +12778 12777 12776 12780 12781 12779 12780 12779 12778 +12782 12781 12780 12434 12757 12425 12757 12434 12436 +12783 12757 12436 12759 12757 12783 12759 12784 12761 +12759 12783 12784 12761 12785 12763 12761 12784 12785 +12765 12763 12786 12786 12763 12785 12768 12437 12427 +12769 12787 12768 12787 12437 12768 12787 12438 12437 +12438 12788 12439 12787 12788 12438 12439 12789 12440 +12788 12789 12439 12440 12790 12441 12789 12790 12440 +12441 12791 12442 12790 12791 12441 12442 12792 12443 +12791 12792 12442 12443 12793 12444 12792 12793 12443 +12772 12445 12770 12445 12793 12770 12445 12444 12793 +12772 12774 12445 12774 12794 12445 12773 12430 12445 +12773 12445 12794 12773 12794 12432 12794 12446 12432 +12794 12795 12446 12446 12796 12447 12795 12796 12446 +12777 12448 12775 12448 12796 12775 12448 12447 12796 +12448 12777 12449 12449 12777 12779 12449 12779 12450 +12450 12779 12781 12782 12797 12781 12797 12450 12781 +12797 12451 12450 12451 12798 12452 12797 12798 12451 +12452 12799 12453 12798 12799 12452 12455 12453 12799 +12455 12799 12800 12459 12801 12783 12459 12783 12436 +12783 12802 12784 12783 12801 12802 12784 12803 12785 +12784 12802 12803 12786 12785 12804 12804 12785 12803 +12800 12805 12455 12455 12805 12460 12460 12806 12461 +12805 12806 12460 12462 12461 12806 12462 12806 12807 +12466 12808 12467 12808 12468 12467 12472 12809 12471 +12810 12473 12471 12810 12471 12809 12801 12459 12473 +12810 12801 12473 12811 12801 12810 12801 12812 12802 +12801 12811 12812 12813 12803 12814 12803 12812 12814 +12803 12802 12812 12803 12813 12804 12474 12462 12815 +12462 12816 12815 12462 12807 12816 12474 12815 12817 +12818 12474 12817 12475 12474 12818 12475 12819 12476 +12818 12819 12475 12476 12820 12477 12819 12820 12476 +12477 12821 12478 12820 12821 12477 12478 12822 12479 +12821 12822 12478 12479 12823 12480 12822 12823 12479 +12480 12824 12481 12823 12824 12480 12481 12825 12482 +12824 12825 12481 12482 12826 12483 12825 12826 12482 +12483 12826 12466 12826 12827 12466 12827 12808 12466 +12827 12826 12828 12829 12484 12468 12808 12829 12468 +12827 12829 12808 12484 12829 12830 12831 12484 12830 +12485 12484 12831 12472 12485 12809 12485 12832 12809 +12485 12831 12832 12809 12832 12810 12810 12832 12833 +12810 12833 12811 12811 12833 12834 12811 12834 12812 +12812 12834 12835 12812 12835 12814 12816 12836 12815 +12815 12836 12817 12828 12837 12827 12827 12837 12829 +12829 12837 12838 12829 12838 12830 12839 12840 12841 +12842 12843 12841 12841 12843 12839 12844 12845 12842 +12842 12845 12843 12844 12846 12845 12847 12848 12849 +12850 12851 12849 12849 12851 12847 12852 12853 12850 +12850 12853 12851 12487 12486 12852 12486 12853 12852 +12486 12854 12853 12855 12486 12490 12855 12854 12486 +12856 12490 12491 12856 12855 12490 12857 12856 12858 +12856 12492 12858 12856 12491 12492 12493 12858 12492 +12859 12860 12494 12494 12860 12495 12861 12494 12498 +12861 12859 12494 12862 12498 12499 12862 12861 12498 +12863 12499 12500 12863 12862 12499 12864 12500 12501 +12864 12863 12500 12865 12501 12502 12865 12864 12501 +12866 12502 12503 12866 12865 12502 12867 12868 12504 +12868 12503 12504 12868 12866 12503 12869 12870 12867 +12867 12870 12868 12871 12872 12869 12869 12872 12870 +12873 12874 12871 12871 12874 12872 12874 12873 12840 +12873 12841 12840 12873 12875 12841 12841 12876 12842 +12841 12875 12876 12876 12506 12505 12844 12876 12505 +12842 12876 12844 12877 12846 12844 12505 12877 12844 +12509 12877 12505 12878 12879 12510 12879 12509 12510 +12879 12877 12509 12880 12881 12878 12878 12881 12879 +12882 12883 12880 12880 12883 12881 12884 12885 12882 +12882 12885 12883 12512 12511 12884 12511 12885 12884 +12511 12886 12885 12887 12511 12514 12887 12886 12511 +12515 12849 12848 12887 12515 12848 12514 12515 12887 +12849 12515 12516 12849 12516 12850 12850 12516 12517 +12850 12517 12852 12852 12517 12487 12888 12857 12858 +12888 12858 12493 12518 12888 12493 12889 12888 12518 +12890 12518 12520 12890 12889 12518 12520 12521 12890 +12890 12521 12891 12892 12893 12522 12522 12893 12523 +12522 12495 12860 12522 12860 12892 12524 12867 12504 +12867 12524 12525 12867 12525 12869 12869 12525 12526 +12869 12526 12871 12871 12526 12527 12871 12527 12873 +12873 12527 12528 12873 12528 12875 12875 12528 12529 +12875 12529 12876 12876 12529 12506 12531 12878 12510 +12878 12531 12532 12878 12532 12880 12880 12532 12534 +12880 12534 12882 12882 12534 12535 12882 12535 12884 +12884 12535 12512 12894 12891 12521 12894 12521 12536 +12536 12537 12894 12894 12537 12895 12893 12538 12523 +12896 12538 12893 12897 12898 12540 12897 12540 12539 +12537 12540 12895 12895 12540 12898 12896 12541 12538 +12899 12541 12896 12542 12897 12539 12542 12900 12897 +12899 12543 12541 12901 12543 12899 12544 12900 12542 +12544 12902 12900 12903 12545 12904 12545 12901 12904 +12545 12543 12901 12905 12903 12904 12546 12902 12544 +12546 12906 12902 12903 12547 12545 12548 12547 12903 +12905 12548 12903 12907 12548 12905 12908 12909 12550 +12908 12550 12549 12546 12550 12906 12906 12550 12909 +12910 12551 12911 12551 12907 12911 12551 12548 12907 +12912 12910 12911 12552 12908 12549 12552 12913 12908 +12910 12553 12551 12554 12553 12910 12912 12554 12910 +12914 12554 12912 12555 12913 12552 12555 12915 12913 +12914 12556 12554 12916 12556 12914 12557 12915 12555 +12557 12917 12915 12916 12558 12556 12918 12558 12916 +12559 12917 12557 12559 12919 12917 12920 12561 12560 +12921 12920 12560 12558 12921 12560 12918 12921 12558 +12562 12919 12559 12562 12922 12919 12920 12563 12561 +12564 12563 12920 12921 12564 12920 12923 12564 12921 +12924 12925 12566 12924 12566 12565 12562 12566 12922 +12922 12566 12925 12923 12568 12564 12926 12568 12923 +12569 12924 12565 12569 12927 12924 12926 12570 12568 +12928 12570 12926 12571 12927 12569 12571 12929 12927 +12928 12572 12570 12930 12572 12928 12931 12929 12571 +12931 12571 12573 12573 12574 12931 12931 12574 12932 +12930 12575 12572 12933 12575 12930 12576 12932 12574 +12576 12934 12932 12933 12577 12575 12935 12577 12933 +12578 12934 12576 12578 12936 12934 12935 12579 12577 +12937 12579 12935 12580 12936 12578 12580 12938 12936 +12937 12581 12579 12939 12581 12937 12582 12938 12580 +12582 12940 12938 12939 12583 12581 12941 12583 12939 +12584 12940 12582 12584 12942 12940 12943 12586 12585 +12944 12943 12585 12583 12944 12585 12941 12944 12583 +12588 12942 12584 12588 12945 12942 12943 12589 12586 +12591 12589 12943 12944 12591 12943 12946 12591 12944 +12947 12945 12588 12947 12588 12592 12592 12593 12947 +12947 12593 12948 12946 12594 12591 12949 12594 12946 +12950 12951 12596 12950 12596 12595 12593 12596 12948 +12948 12596 12951 12949 12597 12594 12952 12597 12949 +12598 12950 12595 12598 12953 12950 12952 12599 12597 +12954 12599 12952 12600 12953 12598 12600 12955 12953 +12954 12601 12599 12956 12601 12954 12602 12957 12958 +12955 12602 12958 12600 12602 12955 12957 12959 12958 +12956 12603 12601 12960 12603 12956 12604 12957 12602 +12604 12605 12957 12605 12959 12957 12605 12961 12959 +12960 12606 12603 12962 12606 12960 12607 12961 12605 +12607 12963 12961 12962 12608 12606 12964 12608 12962 +12965 12966 12967 12610 12609 12967 12966 12610 12967 +12968 12610 12966 12610 12969 12612 12968 12969 12610 +12969 12970 12971 12614 12969 12971 12612 12969 12614 +12971 12615 12614 12972 12973 12974 12975 12976 12974 +12975 12974 12973 12977 12978 12976 12977 12976 12975 +12979 12980 12978 12979 12978 12977 12981 12982 12980 +12981 12980 12979 12983 12982 12981 12616 12963 12607 +12616 12984 12963 12964 12617 12608 12985 12617 12964 +12986 12987 12619 12986 12619 12618 12619 12988 12621 +12987 12988 12619 12967 12622 12965 12622 12988 12965 +12622 12621 12988 12622 12967 12609 12970 12989 12971 +12971 12989 12615 12989 12623 12615 12989 12990 12623 +12974 12624 12972 12624 12990 12972 12624 12623 12990 +12624 12974 12625 12625 12974 12976 12625 12976 12626 +12626 12976 12978 12626 12978 12627 12627 12978 12980 +12627 12980 12628 12628 12980 12982 12983 12991 12982 +12991 12628 12982 12991 12629 12628 12992 12993 12631 +12629 12992 12631 12991 12992 12629 12994 12995 12993 +12994 12993 12992 12996 12995 12994 12632 12984 12616 +12632 12997 12984 12985 12633 12617 12998 12633 12985 +12999 13000 12635 12999 12635 12634 12635 13001 12637 +13000 13001 12635 12618 12637 12986 12986 12637 13001 +12993 12638 12631 12638 12993 12639 12639 12993 12995 +12996 13002 12995 13002 12639 12995 13002 12640 12639 +12642 12640 13002 12642 13002 13003 13004 13005 12644 +13004 12644 12643 12632 12644 12997 12997 12644 13005 +12998 12646 12633 13006 12646 12998 13007 13008 13009 +13010 13011 13009 13010 13009 13008 12648 12647 13011 +13010 12648 13011 13012 12648 13010 12648 13013 12649 +13012 13013 12648 12634 12649 12999 12999 12649 13013 +13014 12650 13015 12650 13003 13015 12650 12642 13003 +13016 13017 13014 13016 13014 13015 12652 12651 13017 +13016 12652 13017 13018 12652 13016 12643 12652 13004 +13004 12652 13018 13006 13019 12646 12646 13019 12654 +12654 13020 12655 13019 13020 12654 12655 13021 12657 +13020 13021 12655 13009 12659 13007 12659 13021 13007 +12659 12657 13021 12659 13009 12660 12660 13009 13011 +12660 13011 12647 13014 12661 12650 12661 13014 12662 +12662 13014 13017 12662 13017 12651 13022 12664 12665 +12666 13022 12665 13023 12668 12669 12664 13022 12669 +13022 13023 12669 13022 13024 13023 12670 13025 12666 +13025 13022 12666 13025 13024 13022 12671 13025 12670 +13026 12675 12674 12676 13027 12674 12674 13027 13026 +12668 13023 12676 13023 13027 12676 13023 13028 13027 +13024 13029 13030 13028 13024 13030 13023 13024 13028 +13031 13032 13029 13024 13031 13029 13025 13031 13024 +12677 13033 12671 13033 13025 12671 13033 13031 13025 +12678 13034 12677 12677 13034 13033 12679 13034 12678 +13035 13036 12681 12681 13036 12682 13037 12681 12684 +13037 13035 12681 12684 12675 13026 13038 12684 13026 +13037 12684 13038 13038 13026 13027 13038 13027 13039 +13039 13027 13028 13039 13028 13040 13040 13028 13030 +13041 13032 13031 13041 13031 13033 13041 13033 13042 +13043 13044 13042 13033 13043 13042 13034 13043 13033 +12685 13045 12679 13045 13034 12679 13045 13043 13034 +12686 13045 12685 13046 13047 12688 12688 13047 12689 +13048 12688 12691 13048 13046 12688 12691 12682 13036 +12691 13036 13048 13049 13044 13043 13049 13043 13045 +13049 13045 13050 13051 13050 13052 13052 13050 12692 +13050 13045 12692 13045 12686 12692 12693 13052 12692 +13053 13054 13055 13056 13057 13055 13055 13057 13053 +12696 12695 13056 12695 13057 13056 12695 13058 13057 +13059 12695 12699 13059 13058 12695 12699 12689 13047 +12699 13047 13059 13060 13051 13052 13060 13052 12693 +12700 13060 12693 13061 13060 12700 13062 13061 13063 +13061 12701 13063 13061 12700 12701 12702 13063 12701 +13064 13065 13066 13064 13066 13054 13054 13066 13055 +13066 12705 13055 12705 12704 13055 13055 12704 12708 +13055 12708 13056 13056 12708 12696 13067 13062 13063 +13063 12702 13067 12702 13068 13067 12702 12710 13068 +13066 13065 12712 13065 12713 12712 13065 13069 12713 +13066 12712 12705 13070 13068 12710 13070 12710 12715 +13071 12715 12716 13071 13070 12715 12716 12717 13071 +13071 12717 13072 13069 12720 12713 13073 12720 13069 +13074 13072 12717 13074 12717 12721 12721 12722 13074 +13074 12722 13075 13076 13077 13078 13073 13076 13078 +12725 13073 13078 12720 13073 12725 13079 13075 12722 +13079 12722 12726 13080 12726 12727 13080 13079 12726 +13081 12727 12728 13081 13080 12727 13082 12728 12729 +13082 13081 12728 13083 12729 12730 13083 13082 12729 +12730 12731 13083 13083 12731 13084 13077 13085 13078 +12725 13078 13085 13086 12725 13085 12733 12725 13086 +13087 13084 12731 13087 12731 12734 13088 12734 12735 +13088 13087 12734 13089 12735 12736 13089 13088 12735 +13090 12736 12737 13090 13089 12736 13091 13092 12738 +13092 12737 12738 13092 13090 12737 13091 13093 13092 +13086 12740 12733 13094 12740 13086 12741 13091 12738 +12741 12743 13091 12743 13095 13091 13095 13093 13091 +12743 13096 13095 12744 13096 12743 13094 13097 12740 +12740 13097 12745 12746 12745 13097 12746 13097 13098 +13099 13095 13096 12744 12747 13096 12747 13100 13096 +13100 13099 13096 13100 12747 13101 13102 13103 13101 +13101 13103 13100 13104 13105 13102 13102 13105 13103 +13106 13107 13104 13104 13107 13105 13108 13109 13106 +13106 13109 13107 13110 13111 13108 13108 13111 13109 +13112 13113 13110 13110 13113 13111 13114 13115 13112 +13112 13115 13113 13116 13117 13114 13114 13117 13115 +13118 13119 13116 13116 13119 13117 13118 13120 13119 +12748 12746 13121 12746 13122 13121 12746 13098 13122 +13121 12749 12748 12750 13101 12747 13101 12750 12752 +13101 12752 13102 13102 12752 12754 13102 12754 13104 +13104 12754 12755 13123 13104 12755 13106 13104 13123 +13106 13124 13108 13106 13123 13124 13108 13125 13110 +13108 13124 13125 13110 13126 13112 13110 13125 13126 +13112 13127 13114 13112 13126 13127 13114 13128 13116 +13114 13127 13128 13116 13129 13118 13116 13128 13129 +13120 13118 13130 13118 13131 13130 13118 13129 13131 +13132 13133 13131 13131 13133 13130 13132 13134 13133 +13122 13135 13121 12749 13121 13135 13136 12749 13135 +12756 12749 13136 12758 13123 12755 13123 12758 12760 +13123 12760 13124 13124 12760 12762 13124 12762 13125 +13125 12762 12764 13125 12764 13126 13126 12764 12765 +13137 13126 12765 13127 13126 13137 13127 13138 13128 +13127 13137 13138 13128 13139 13129 13128 13138 13139 +13129 13140 13131 13129 13139 13140 13131 13141 13132 +13131 13140 13141 13134 13132 13142 13132 13143 13142 +13132 13141 13143 13144 13145 13143 13143 13145 13142 +13144 13146 13145 13136 13147 12756 12756 13147 12766 +12766 13148 12767 13147 13148 12766 13149 13150 12769 +12767 13149 12769 13148 13149 12767 13151 13152 13150 +13151 13150 13149 13153 13154 13152 13153 13152 13151 +13155 13156 13154 13155 13154 13153 13157 13158 13156 +13157 13156 13155 13159 13160 13158 13159 13158 13157 +13161 13162 13160 13161 13160 13159 12771 12770 13162 +13161 12771 13162 13163 12771 13161 13164 13165 12774 +12771 13164 12774 13163 13164 12771 13166 13167 13165 +13166 13165 13164 13168 13169 13167 13168 13167 13166 +12776 12775 13169 13168 12776 13169 13170 12776 13168 +12776 13171 12778 13170 13171 12776 12778 13172 12780 +13171 13172 12778 13173 13174 12782 12780 13173 12782 +13172 13173 12780 13175 13174 13173 12786 13176 13137 +12786 13137 12765 13137 13177 13138 13137 13176 13177 +13138 13178 13139 13138 13177 13178 13139 13179 13140 +13139 13178 13179 13140 13180 13141 13140 13179 13180 +13141 13181 13143 13141 13180 13181 13143 13182 13144 +13143 13181 13182 13146 13144 13183 13183 13144 13182 +13150 12787 12769 12787 13150 12788 12788 13150 13152 +12788 13152 12789 12789 13152 13154 12789 13154 12790 +12790 13154 13156 12790 13156 12791 12791 13156 13158 +12791 13158 12792 12792 13158 13160 12792 13160 12793 +12793 13160 13162 12793 13162 12770 13165 12794 12774 +12794 13165 12795 12795 13165 13167 12795 13167 12796 +12796 13167 13169 12796 13169 12775 13174 12797 12782 +13175 13184 13174 13184 12797 13174 13184 12798 12797 +13184 13185 13186 12799 13184 13186 12798 13184 12799 +13186 12800 12799 12804 13187 13176 12804 13176 12786 +13176 13188 13177 13176 13187 13188 13177 13189 13178 +13177 13188 13189 13178 13190 13179 13178 13189 13190 +13179 13191 13180 13179 13190 13191 13192 13181 13193 +13181 13191 13193 13181 13180 13191 13181 13192 13182 +13182 13192 13194 13182 13194 13183 13185 13195 13186 +13186 13195 12800 13195 12805 12800 13195 13196 12805 +13196 13197 13198 12806 13196 13198 12805 13196 12806 +13198 12807 12806 12814 13199 12813 13187 12804 12813 +13199 13187 12813 13200 13187 13199 13201 13188 13202 +13188 13200 13202 13188 13187 13200 13188 13201 13189 +13189 13201 13203 13189 13203 13190 13190 13203 13204 +13190 13204 13191 13191 13204 13205 13191 13205 13193 +13197 13206 13198 12807 13198 13206 13207 12807 13206 +12816 12807 13207 12817 13208 12818 13209 12819 12818 +13209 12818 13208 13210 12820 12819 13210 12819 13209 +13211 12821 12820 13211 12820 13210 13212 12822 12821 +13212 12821 13211 13213 12823 12822 13213 12822 13212 +13214 12824 12823 13214 12823 13213 13215 12825 12824 +13215 12824 13214 13216 12826 12825 13216 12825 13215 +13216 12828 12826 12830 13217 12831 13218 12832 12831 +13218 12831 13217 13219 12833 12832 13219 12832 13218 +13220 12834 12833 13220 12833 13219 12834 13220 13221 +13222 12834 13221 12835 12834 13222 12814 12835 13199 +12835 13223 13199 12835 13222 13223 13199 13223 13200 +13200 13223 13224 13200 13224 13202 13207 13225 12816 +12816 13225 12836 12817 12836 13208 12836 13226 13208 +12836 13225 13226 13208 13226 13209 13209 13226 13227 +13209 13227 13210 13210 13227 13228 13210 13228 13211 +13211 13228 13229 13211 13229 13212 13212 13229 13230 +13212 13230 13213 13213 13230 13231 13213 13231 13214 +13214 13231 13232 13214 13232 13215 13215 13232 13233 +13215 13233 13216 13216 13233 13234 13216 13234 12828 +13234 12837 12828 13234 13235 12837 12837 13236 12838 +13235 13236 12837 12830 12838 13217 12838 13237 13217 +12838 13236 13237 13217 13237 13218 13218 13237 13238 +13218 13238 13219 13219 13238 13239 13219 13239 13220 +13220 13239 13240 13220 13240 13221 13241 13242 13243 +13244 13245 13243 13243 13245 13241 13244 13246 13245 +13247 13248 13249 13250 13251 13249 13249 13251 13247 +13252 13253 13250 13250 13253 13251 13253 13252 13242 +13252 13243 13242 13252 13254 13243 13243 13255 13244 +13243 13254 13255 13246 13244 13256 13244 13257 13256 +13244 13255 13257 13258 13259 13257 13257 13259 13256 +13258 13260 13259 13261 13262 13263 13264 13265 13263 +13263 13265 13261 13264 13266 13265 13267 13268 13269 +13270 13271 13269 13269 13271 13267 12840 12839 13270 +12839 13271 13270 12839 13272 13271 13273 12839 12843 +13273 13272 12839 13274 12843 12845 13274 13273 12843 +13275 13276 12846 13276 12845 12846 13276 13274 12845 +13277 13278 13275 13275 13278 13276 13277 13279 13278 +13280 13281 13282 13283 13284 13282 13282 13284 13280 +13284 13283 13248 13283 13249 13248 13283 13285 13249 +13285 12848 12847 13250 13285 12847 13249 13285 13250 +13250 12847 12851 13250 12851 13252 13252 12851 12853 +13252 12853 13254 13254 12853 12854 13254 12854 13255 +13255 12854 12855 13255 12855 13257 13257 12855 12856 +13257 12856 13258 13286 13287 12857 12857 13287 12856 +13287 13260 12856 13260 13258 12856 13288 13289 13286 +13286 13289 13287 13288 13290 13289 13291 13292 12859 +12859 13292 12860 13293 12859 12861 13293 13291 12859 +13294 12861 12862 13294 13293 12861 13295 12862 12863 +13295 13294 12862 12864 13263 13262 13295 12864 13262 +12863 12864 13295 13263 12864 12865 13263 12865 13264 +13296 13266 13264 12865 13296 13264 12866 13296 12865 +13297 12866 12868 13297 13296 12866 13298 12868 12870 +13298 13297 12868 12872 13269 13268 13298 12872 13268 +12870 12872 13298 13269 12872 12874 13269 12874 13270 +13270 12874 12840 12877 13275 12846 13275 12877 12879 +13275 12879 13277 13299 13279 13277 12879 13299 13277 +12881 13299 12879 13300 12881 12883 13300 13299 12881 +12885 13282 13281 13300 12885 13281 12883 12885 13300 +13282 12885 12886 13282 12886 13283 13283 12886 12887 +13283 12887 13285 13285 12887 12848 12888 13286 12857 +13286 12888 12889 13286 12889 13288 13301 13290 13288 +12889 13301 13288 12890 13301 12889 12890 12891 13301 +13301 12891 13302 13303 13304 12892 12892 13304 12893 +12892 12860 13292 12892 13292 13303 12891 12894 13305 +13306 12891 13305 13302 12891 13306 12895 13305 12894 +13304 12896 12893 13307 12896 13304 13306 13305 12898 +12897 13306 12898 13308 13306 12897 12898 13305 12895 +13307 12899 12896 13309 12899 13307 12900 13308 12897 +12900 13310 13308 13309 12901 12899 13311 12901 13309 +12902 13310 12900 12902 13312 13310 13311 13313 12901 +12901 13313 12904 12905 12904 13313 12905 13313 13314 +12906 13312 12902 12906 13315 13312 13314 12907 12905 +13316 12907 13314 13317 13318 12909 13317 12909 12908 +12906 12909 13315 13315 12909 13318 13316 13319 12907 +12907 13319 12911 12912 12911 13319 12912 13319 13320 +12913 13317 12908 12913 13321 13317 13320 12914 12912 +13322 12914 13320 12915 13321 12913 12915 13323 13321 +13322 12916 12914 13324 12916 13322 12917 13323 12915 +12917 13325 13323 13326 13327 13328 13324 13326 13328 +12918 13324 13328 12916 13324 12918 12919 13325 12917 +12919 13329 13325 13327 13330 13328 12918 13328 13330 +13331 12918 13330 12921 12918 13331 12922 13329 12919 +12922 13332 13329 13331 12923 12921 13333 12923 13331 +13334 13335 12925 13334 12925 12924 12922 12925 13332 +13332 12925 13335 13333 12926 12923 13336 12926 13333 +12927 13334 12924 12927 13337 13334 13336 12928 12926 +13338 12928 13336 12929 13337 12927 12929 13339 13337 +13338 12930 12928 13340 12930 13338 13341 13339 12929 +13341 12929 12931 12931 12932 13341 13341 12932 13342 +13340 12933 12930 13343 12933 13340 12934 13342 12932 +12934 13344 13342 13343 12935 12933 13345 12935 13343 +12936 13344 12934 12936 13346 13344 13345 12937 12935 +13347 12937 13345 12938 13346 12936 12938 13348 13346 +13347 12939 12937 13349 12939 13347 12940 13348 12938 +12940 13350 13348 13349 12941 12939 13351 12941 13349 +12942 13350 12940 12942 13352 13350 13351 12944 12941 +13353 12944 13351 12945 13352 12942 12945 13354 13352 +13353 12946 12944 13355 12946 13353 12945 12947 13356 +13357 12945 13356 13354 12945 13357 12948 13356 12947 +13355 12949 12946 13358 12949 13355 13357 13356 12951 +12950 13357 12951 13359 13357 12950 12951 13356 12948 +13358 12952 12949 13360 12952 13358 12953 13359 12950 +12953 13361 13359 13360 12954 12952 13362 12954 13360 +12955 13361 12953 12955 13363 13361 13362 12956 12954 +13364 12956 13362 13365 13363 12955 13365 12955 12958 +12958 12959 13365 13365 12959 13366 13364 12960 12956 +13367 12960 13364 12961 13366 12959 12961 13368 13366 +13367 12962 12960 13369 12962 13367 13370 13371 13372 +13373 13374 13372 13373 13372 13371 13375 13374 13373 +12963 13368 12961 12963 13376 13368 13369 12964 12962 +13377 12964 13369 13378 13379 13380 12966 12965 13380 +13379 12966 13380 13381 12966 13379 13372 12968 13370 +12968 13381 13370 12968 12966 13381 12968 13372 12969 +12969 13372 13374 13382 13383 12970 13382 12970 12969 +13375 13382 12969 13374 13375 12969 13384 13385 13383 +13384 13383 13382 12973 12972 13385 13384 12973 13385 +13386 12973 13384 12973 13387 12975 13386 13387 12973 +12975 13388 12977 13387 13388 12975 12977 13389 12979 +13388 13389 12977 12979 13390 12981 13389 13390 12979 +13391 13392 12983 12981 13391 12983 13390 13391 12981 +13393 13394 13392 13393 13392 13391 13395 13394 13393 +12984 13376 12963 12984 13396 13376 13377 12985 12964 +13397 12985 13377 13398 13399 13400 13401 13402 13400 +13401 13400 13399 12987 12986 13402 13401 12987 13402 +13403 12987 13401 13380 12988 13378 12988 13403 13378 +12988 12987 13403 12988 13380 12965 13383 12989 12970 +12989 13383 12990 12990 13383 13385 12990 13385 12972 +13392 12991 12983 12991 13392 12992 12992 13392 13394 +13395 13404 13394 13404 12992 13394 13404 12994 12992 +12996 12994 13404 12996 13404 13405 12997 13396 12984 +12997 13406 13396 13397 12998 12985 13407 12998 13397 +13408 13409 13410 13411 13412 13410 13411 13410 13409 +13412 13411 13398 13412 13398 13400 12999 13412 13400 +13000 12999 13400 13000 13400 13001 13001 13400 13402 +13001 13402 12986 13405 13413 12996 12996 13413 13002 +13003 13002 13413 13003 13413 13414 13415 13416 13005 +13415 13005 13004 12997 13005 13406 13406 13005 13416 +13417 13006 13418 13006 13407 13418 13006 12998 13407 +13419 13417 13418 13420 13421 13422 13423 13008 13007 +13424 13422 13421 13008 13423 13425 13426 13008 13425 +13010 13008 13426 13410 13012 13408 13012 13426 13408 +13012 13010 13426 13012 13410 13013 13013 13410 13412 +13013 13412 12999 13414 13427 13003 13003 13427 13015 +13015 13428 13016 13427 13428 13015 13016 13429 13018 +13428 13429 13016 13004 13018 13415 13415 13018 13429 +13417 13019 13006 13419 13430 13417 13430 13019 13417 +13430 13020 13019 13422 13021 13420 13021 13430 13420 +13021 13020 13430 13422 13424 13021 13424 13431 13021 +13423 13007 13021 13423 13021 13431 13423 13431 13425 +13432 13030 13029 13032 13432 13029 13433 13434 13035 +13035 13434 13036 13435 13035 13037 13435 13433 13035 +13436 13037 13038 13436 13435 13037 13437 13038 13039 +13437 13436 13038 13438 13039 13040 13438 13437 13039 +13438 13040 13030 13439 13438 13030 13432 13439 13030 +13438 13439 13440 13041 13441 13032 13441 13432 13032 +13441 13439 13432 13042 13442 13041 13041 13442 13441 +13044 13442 13042 13443 13444 13445 13047 13046 13445 +13046 13443 13445 13046 13446 13443 13447 13046 13048 +13447 13446 13046 13048 13036 13434 13048 13434 13447 +13448 13440 13439 13448 13439 13441 13448 13441 13449 +13449 13441 13442 13449 13442 13450 13451 13450 13452 +13452 13450 13049 13450 13442 13049 13442 13044 13049 +13050 13453 13049 13049 13453 13452 13051 13453 13050 +13454 13455 13053 13053 13455 13054 13456 13053 13057 +13456 13454 13053 13457 13057 13058 13457 13456 13057 +13059 13445 13444 13457 13059 13444 13058 13059 13457 +13445 13059 13047 13458 13451 13452 13458 13452 13453 +13458 13453 13459 13459 13453 13051 13060 13459 13051 +13460 13459 13060 13461 13460 13462 13460 13061 13462 +13460 13060 13061 13062 13462 13061 13463 13464 13064 +13064 13464 13065 13064 13054 13455 13064 13455 13463 +13465 13461 13462 13466 13465 13467 13467 13465 13067 +13465 13462 13067 13462 13062 13067 13068 13467 13067 +13464 13069 13065 13468 13069 13464 13469 13466 13467 +13469 13467 13068 13070 13469 13068 13470 13469 13070 +13471 13470 13472 13470 13071 13472 13470 13070 13071 +13072 13472 13071 13473 13474 13475 13468 13473 13475 +13073 13468 13475 13069 13468 13073 13476 13471 13472 +13476 13472 13072 13074 13476 13072 13477 13476 13074 +13074 13075 13477 13477 13075 13478 13475 13474 13076 +13474 13077 13076 13474 13479 13077 13475 13076 13073 +13480 13478 13075 13480 13075 13079 13481 13079 13080 +13481 13480 13079 13482 13080 13081 13482 13481 13080 +13483 13081 13082 13483 13482 13081 13484 13082 13083 +13484 13483 13082 13485 13486 13084 13486 13083 13084 +13486 13484 13083 13485 13487 13486 13479 13488 13077 +13077 13488 13085 13086 13085 13488 13086 13488 13489 +13087 13485 13084 13490 13487 13485 13087 13490 13485 +13088 13490 13087 13491 13088 13089 13491 13490 13088 +13492 13089 13090 13492 13491 13089 13493 13090 13092 +13493 13492 13090 13092 13093 13493 13493 13093 13494 +13495 13094 13496 13094 13489 13496 13094 13086 13489 +13497 13495 13496 13095 13498 13499 13494 13095 13499 +13093 13095 13494 13498 13500 13499 13501 13502 13503 +13504 13505 13503 13503 13505 13501 13506 13507 13504 +13504 13507 13505 13506 13508 13507 13495 13097 13094 +13098 13097 13495 13497 13098 13495 13509 13098 13497 +13099 13498 13095 13510 13500 13498 13099 13510 13498 +13100 13510 13099 13511 13100 13103 13511 13510 13100 +13512 13103 13105 13512 13511 13103 13513 13105 13107 +13513 13512 13105 13514 13107 13109 13514 13513 13107 +13515 13109 13111 13515 13514 13109 13113 13503 13502 +13515 13113 13502 13111 13113 13515 13503 13113 13115 +13503 13115 13504 13504 13115 13117 13504 13117 13506 +13117 13119 13506 13119 13516 13506 13516 13508 13506 +13119 13517 13516 13120 13517 13119 13518 13122 13519 +13122 13509 13519 13122 13098 13509 13520 13518 13519 +13521 13522 13523 13524 13525 13523 13524 13523 13522 +13526 13527 13525 13526 13525 13524 13528 13529 13527 +13528 13527 13526 13530 13529 13528 13531 13516 13517 +13531 13517 13120 13130 13531 13120 13532 13531 13130 +13533 13130 13133 13533 13532 13130 13534 13535 13134 +13535 13133 13134 13535 13533 13133 13536 13537 13534 +13534 13537 13535 13536 13538 13537 13518 13135 13122 +13539 13540 13136 13539 13136 13135 13520 13539 13135 +13518 13520 13135 13541 13542 13540 13541 13540 13539 +13543 13544 13542 13543 13542 13541 13545 13546 13544 +13545 13544 13543 13547 13548 13546 13547 13546 13545 +13549 13550 13548 13549 13548 13547 13550 13549 13521 +13523 13550 13521 13551 13550 13523 13552 13523 13525 +13551 13523 13552 13553 13525 13527 13552 13525 13553 +13554 13527 13529 13553 13527 13554 13529 13530 13555 +13556 13529 13555 13554 13529 13556 13557 13558 13556 +13557 13556 13555 13559 13560 13558 13559 13558 13557 +13561 13560 13559 13142 13534 13134 13534 13142 13145 +13534 13145 13536 13562 13563 13146 13146 13563 13145 +13563 13538 13145 13538 13536 13145 13562 13564 13563 +13540 13147 13136 13147 13540 13148 13148 13540 13542 +13148 13542 13149 13149 13542 13544 13149 13544 13151 +13151 13544 13546 13151 13546 13153 13153 13546 13548 +13153 13548 13155 13155 13548 13550 13155 13550 13157 +13157 13550 13551 13157 13551 13159 13159 13551 13552 +13159 13552 13161 13161 13552 13553 13161 13553 13163 +13163 13553 13554 13163 13554 13164 13164 13554 13556 +13164 13556 13166 13166 13556 13558 13166 13558 13168 +13168 13558 13560 13561 13565 13560 13565 13168 13560 +13565 13170 13168 13170 13566 13171 13565 13566 13170 +13171 13567 13172 13566 13567 13171 13172 13568 13173 +13567 13568 13172 13175 13173 13568 13175 13568 13569 +13183 13570 13562 13183 13562 13146 13564 13562 13571 +13571 13562 13570 13569 13572 13175 13175 13572 13184 +13185 13184 13572 13185 13572 13573 13193 13574 13192 +13575 13194 13192 13575 13192 13574 13575 13576 13194 +13194 13576 13183 13576 13577 13183 13577 13570 13183 +13570 13577 13571 13573 13578 13185 13185 13578 13195 +13195 13579 13196 13578 13579 13195 13197 13196 13579 +13197 13579 13580 13202 13581 13201 13582 13203 13201 +13582 13201 13581 13583 13204 13203 13583 13203 13582 +13204 13583 13584 13585 13204 13584 13205 13204 13585 +13193 13205 13574 13205 13586 13574 13205 13585 13586 +13574 13586 13575 13575 13586 13587 13575 13587 13576 +13580 13588 13197 13197 13588 13206 13207 13206 13588 +13207 13588 13589 13221 13590 13222 13591 13223 13222 +13591 13222 13590 13223 13591 13592 13593 13223 13592 +13224 13223 13593 13202 13224 13581 13224 13594 13581 +13224 13593 13594 13581 13594 13582 13582 13594 13595 +13582 13595 13583 13583 13595 13596 13583 13596 13584 +13589 13597 13207 13207 13597 13225 13597 13598 13599 +13226 13597 13599 13225 13597 13226 13600 13227 13226 +13600 13226 13599 13601 13228 13227 13601 13227 13600 +13602 13229 13228 13602 13228 13601 13603 13230 13229 +13603 13229 13602 13604 13231 13230 13604 13230 13603 +13605 13232 13231 13605 13231 13604 13232 13605 13606 +13607 13232 13606 13233 13232 13607 13233 13608 13234 +13607 13608 13233 13608 13609 13610 13235 13608 13610 +13234 13608 13235 13611 13236 13235 13611 13235 13610 +13612 13237 13236 13612 13236 13611 13613 13238 13237 +13613 13237 13612 13614 13239 13238 13614 13238 13613 +13615 13240 13239 13615 13239 13614 13615 13616 13240 +13240 13616 13221 13616 13617 13221 13617 13590 13221 +13590 13617 13591 13591 13617 13618 13591 13618 13592 +13598 13619 13599 13599 13619 13600 13600 13619 13620 +13600 13620 13601 13601 13620 13621 13601 13621 13602 +13602 13621 13622 13602 13622 13603 13603 13622 13623 +13603 13623 13604 13604 13623 13624 13604 13624 13605 +13605 13624 13625 13605 13625 13606 13609 13626 13610 +13610 13626 13611 13611 13626 13627 13611 13627 13612 +13612 13627 13628 13612 13628 13613 13613 13628 13629 +13613 13629 13614 13614 13629 13630 13614 13630 13615 +13615 13630 13631 13615 13631 13616 13632 13633 13634 +13635 13636 13634 13634 13636 13632 13637 13638 13635 +13635 13638 13636 13637 13639 13638 13640 13641 13642 +13643 13644 13642 13642 13644 13640 13644 13643 13633 +13643 13634 13633 13643 13645 13634 13645 13242 13241 +13635 13645 13241 13634 13645 13635 13635 13241 13245 +13635 13245 13637 13646 13647 13246 13246 13647 13245 +13647 13639 13245 13639 13637 13245 13648 13649 13646 +13646 13649 13647 13648 13650 13649 13651 13652 13653 +13654 13655 13653 13653 13655 13651 13654 13656 13655 +13657 13658 13659 13660 13661 13659 13659 13661 13657 +13660 13662 13661 13663 13664 13665 13666 13667 13665 +13665 13667 13663 13667 13666 13641 13641 13666 13642 +13666 13248 13642 13248 13247 13642 13642 13247 13251 +13642 13251 13643 13643 13251 13253 13643 13253 13645 +13645 13253 13242 13256 13646 13246 13646 13256 13259 +13646 13259 13648 13259 13260 13648 13260 13650 13648 +13260 13668 13650 13669 13670 13261 13261 13670 13262 +13671 13261 13265 13671 13669 13261 13265 13266 13671 +13671 13266 13672 13673 13674 13675 13268 13267 13675 +13267 13673 13675 13267 13676 13673 13677 13267 13271 +13677 13676 13267 13272 13653 13652 13677 13272 13652 +13271 13272 13677 13653 13272 13273 13653 13273 13654 +13678 13656 13654 13273 13678 13654 13274 13678 13273 +13276 13659 13658 13678 13276 13658 13274 13276 13678 +13659 13276 13278 13659 13278 13660 13679 13680 13279 +13279 13680 13278 13680 13662 13278 13662 13660 13278 +13681 13682 13679 13679 13682 13680 13682 13681 13664 +13664 13681 13665 13681 13281 13665 13281 13280 13665 +13665 13280 13284 13665 13284 13666 13666 13284 13248 +13683 13668 13260 13683 13260 13287 13684 13287 13289 +13684 13683 13287 13685 13686 13290 13686 13289 13290 +13686 13684 13289 13685 13687 13686 13688 13689 13291 +13291 13689 13292 13690 13291 13293 13690 13688 13291 +13691 13293 13294 13691 13690 13293 13692 13294 13295 +13692 13691 13294 13295 13262 13670 13295 13670 13692 +13693 13672 13266 13693 13266 13296 13694 13296 13297 +13694 13693 13296 13298 13675 13674 13694 13298 13674 +13297 13298 13694 13675 13298 13268 13299 13679 13279 +13679 13299 13300 13679 13300 13681 13681 13300 13281 +13301 13685 13290 13695 13696 13302 13302 13696 13301 +13696 13687 13301 13687 13685 13301 13695 13697 13696 +13698 13699 13303 13303 13699 13304 13303 13292 13689 +13303 13689 13698 13700 13701 13695 13302 13700 13695 +13306 13700 13302 13695 13701 13697 13699 13307 13304 +13702 13307 13699 13308 13700 13306 13308 13703 13700 +13702 13309 13307 13704 13309 13702 13310 13703 13308 +13310 13705 13703 13704 13311 13309 13706 13311 13704 +13312 13705 13310 13312 13707 13705 13706 13708 13311 +13311 13708 13313 13314 13313 13708 13314 13708 13709 +13315 13707 13312 13315 13710 13707 13709 13316 13314 +13711 13316 13709 13712 13713 13318 13712 13318 13317 +13315 13318 13710 13710 13318 13713 13711 13714 13316 +13316 13714 13319 13320 13319 13714 13320 13714 13715 +13321 13712 13317 13321 13716 13712 13715 13322 13320 +13717 13322 13715 13323 13716 13321 13323 13718 13716 +13717 13324 13322 13719 13324 13717 13325 13718 13323 +13325 13720 13718 13721 13722 13326 13326 13722 13327 +13326 13324 13719 13326 13719 13721 13329 13720 13325 +13329 13723 13720 13722 13724 13327 13327 13724 13330 +13331 13330 13724 13331 13724 13725 13726 13727 13728 +13727 13723 13728 13723 13332 13728 13723 13329 13332 +13725 13333 13331 13729 13333 13725 13726 13728 13335 +13334 13726 13335 13730 13726 13334 13335 13728 13332 +13729 13336 13333 13731 13336 13729 13337 13730 13334 +13337 13732 13730 13731 13338 13336 13733 13338 13731 +13339 13732 13337 13339 13734 13732 13733 13340 13338 +13735 13340 13733 13736 13734 13339 13736 13339 13341 +13341 13342 13736 13736 13342 13737 13735 13343 13340 +13738 13343 13735 13344 13737 13342 13344 13739 13737 +13738 13345 13343 13740 13345 13738 13346 13739 13344 +13346 13741 13739 13740 13347 13345 13742 13347 13740 +13348 13741 13346 13348 13743 13741 13742 13349 13347 +13744 13349 13742 13350 13743 13348 13350 13745 13743 +13746 13747 13748 13744 13746 13748 13351 13744 13748 +13349 13744 13351 13352 13745 13350 13352 13749 13745 +13747 13750 13748 13351 13748 13750 13751 13351 13750 +13353 13351 13751 13354 13749 13352 13354 13752 13749 +13751 13355 13353 13753 13355 13751 13357 13752 13354 +13357 13754 13752 13753 13358 13355 13755 13358 13753 +13359 13754 13357 13359 13756 13754 13755 13360 13358 +13757 13360 13755 13361 13756 13359 13361 13758 13756 +13757 13362 13360 13759 13362 13757 13363 13758 13361 +13363 13760 13758 13759 13364 13362 13761 13364 13759 +13762 13760 13363 13762 13363 13365 13365 13366 13762 +13762 13366 13763 13761 13367 13364 13764 13367 13761 +13368 13763 13366 13368 13765 13763 13764 13369 13367 +13766 13369 13764 13767 13768 13769 13371 13370 13769 +13768 13371 13769 13770 13371 13768 13371 13771 13373 +13770 13771 13371 13375 13373 13771 13375 13771 13772 +13376 13765 13368 13376 13773 13765 13766 13377 13369 +13774 13377 13766 13775 13776 13777 13379 13378 13777 +13776 13379 13777 13778 13379 13776 13769 13381 13767 +13381 13778 13767 13381 13379 13778 13381 13769 13370 +13772 13779 13375 13375 13779 13382 13382 13780 13384 +13779 13780 13382 13384 13781 13386 13780 13781 13384 +13386 13782 13387 13781 13782 13386 13387 13783 13388 +13782 13783 13387 13388 13784 13389 13783 13784 13388 +13389 13785 13390 13784 13785 13389 13390 13786 13391 +13785 13786 13390 13391 13787 13393 13786 13787 13391 +13788 13789 13395 13393 13788 13395 13787 13788 13393 +13790 13789 13788 13396 13773 13376 13396 13791 13773 +13774 13397 13377 13792 13397 13774 13793 13794 13399 +13793 13399 13398 13399 13795 13401 13794 13795 13399 +13777 13403 13775 13403 13795 13775 13403 13401 13795 +13403 13777 13378 13789 13404 13395 13405 13404 13789 +13790 13405 13789 13796 13405 13790 13797 13798 13799 +13798 13791 13799 13791 13406 13799 13791 13396 13406 +13792 13407 13397 13800 13407 13792 13801 13802 13409 +13801 13409 13408 13409 13803 13411 13802 13803 13409 +13398 13411 13793 13793 13411 13803 13796 13804 13405 +13405 13804 13413 13805 13806 13414 13413 13805 13414 +13804 13805 13413 13807 13808 13806 13807 13806 13805 +13809 13810 13808 13809 13808 13807 13810 13809 13797 +13810 13797 13799 13415 13810 13799 13416 13415 13799 +13416 13799 13406 13800 13811 13407 13407 13811 13418 +13812 13813 13419 13418 13812 13419 13811 13812 13418 +13421 13420 13813 13812 13421 13813 13814 13421 13812 +13815 13816 13424 13421 13815 13424 13814 13815 13421 +13426 13425 13816 13815 13426 13816 13817 13426 13815 +13408 13426 13801 13801 13426 13817 13806 13427 13414 +13427 13806 13428 13428 13806 13808 13428 13808 13429 +13429 13808 13810 13429 13810 13415 13813 13430 13419 +13430 13813 13420 13816 13431 13424 13431 13816 13425 +13818 13819 13433 13433 13819 13434 13820 13433 13435 +13820 13818 13433 13821 13435 13436 13821 13820 13435 +13822 13436 13437 13822 13821 13436 13823 13437 13438 +13823 13822 13437 13438 13440 13823 13823 13440 13824 +13825 13826 13827 13444 13443 13827 13443 13825 13827 +13443 13828 13825 13829 13443 13446 13829 13828 13443 +13830 13446 13447 13830 13829 13446 13447 13434 13819 +13447 13819 13830 13440 13448 13831 13832 13440 13831 +13824 13440 13832 13449 13833 13448 13448 13833 13831 +13450 13834 13449 13449 13834 13833 13451 13834 13450 +13835 13836 13837 13455 13454 13837 13454 13835 13837 +13454 13838 13835 13839 13454 13456 13839 13838 13454 +13457 13827 13826 13839 13457 13826 13456 13457 13839 +13827 13457 13444 13840 13832 13831 13841 13842 13840 +13831 13841 13840 13833 13841 13831 13834 13843 13844 +13841 13834 13844 13833 13834 13841 13845 13843 13846 +13846 13843 13458 13843 13834 13458 13834 13451 13458 +13459 13847 13458 13458 13847 13846 13460 13848 13459 +13459 13848 13847 13461 13848 13460 13837 13836 13463 +13836 13464 13463 13836 13849 13464 13837 13463 13455 +13850 13842 13841 13850 13841 13844 13851 13845 13846 +13851 13846 13847 13851 13847 13852 13852 13847 13848 +13852 13848 13853 13854 13853 13855 13855 13853 13465 +13853 13848 13465 13848 13461 13465 13466 13855 13465 +13849 13468 13464 13856 13468 13849 13857 13854 13855 +13857 13855 13466 13469 13857 13466 13858 13857 13469 +13859 13469 13470 13859 13858 13469 13470 13471 13859 +13859 13471 13860 13861 13474 13473 13862 13861 13473 +13468 13862 13473 13856 13862 13468 13863 13860 13471 +13863 13471 13476 13864 13863 13865 13863 13477 13865 +13863 13476 13477 13478 13865 13477 13866 13479 13861 +13861 13479 13474 13861 13862 13867 13861 13867 13866 +13868 13864 13865 13868 13865 13478 13480 13868 13478 +13869 13868 13480 13870 13480 13481 13870 13869 13480 +13871 13481 13482 13871 13870 13481 13872 13482 13483 +13872 13871 13482 13873 13483 13484 13873 13872 13483 +13874 13484 13486 13874 13873 13484 13875 13876 13487 +13876 13486 13487 13876 13874 13486 13877 13878 13875 +13875 13878 13876 13877 13879 13878 13866 13488 13479 +13880 13881 13489 13880 13489 13488 13867 13880 13488 +13866 13867 13488 13882 13881 13880 13490 13875 13487 +13875 13490 13491 13875 13491 13877 13883 13879 13877 +13491 13883 13877 13492 13883 13491 13884 13492 13493 +13884 13883 13492 13493 13494 13884 13884 13494 13885 +13881 13496 13489 13497 13496 13881 13882 13497 13881 +13886 13497 13882 13887 13885 13494 13887 13494 13499 +13888 13889 13500 13889 13499 13500 13889 13887 13499 +13888 13890 13889 13891 13892 13893 13894 13895 13893 +13893 13895 13891 13502 13501 13894 13501 13895 13894 +13501 13896 13895 13897 13501 13505 13897 13896 13501 +13898 13505 13507 13898 13897 13505 13507 13508 13898 +13898 13508 13899 13900 13509 13901 13509 13886 13901 +13509 13497 13886 13902 13903 13900 13902 13900 13901 +13904 13905 13903 13904 13903 13902 13906 13907 13905 +13906 13905 13904 13908 13909 13907 13908 13907 13906 +13910 13911 13909 13910 13909 13908 13912 13913 13911 +13912 13911 13910 13914 13915 13913 13914 13913 13912 +13916 13917 13915 13916 13915 13914 13918 13919 13917 +13918 13917 13916 13920 13919 13918 13510 13888 13500 +13921 13890 13888 13510 13921 13888 13511 13921 13510 +13922 13511 13512 13922 13921 13511 13923 13512 13513 +13923 13922 13512 13514 13893 13892 13923 13514 13892 +13513 13514 13923 13893 13514 13515 13893 13515 13894 +13894 13515 13502 13516 13924 13925 13899 13516 13925 +13508 13516 13899 13926 13927 13924 13924 13927 13925 +13928 13929 13926 13926 13929 13927 13930 13931 13928 +13928 13931 13929 13930 13932 13931 13900 13519 13509 +13519 13900 13520 13900 13933 13520 13900 13903 13933 +13934 13903 13905 13933 13903 13934 13935 13905 13907 +13934 13905 13935 13936 13907 13909 13935 13907 13936 +13937 13909 13911 13936 13909 13937 13938 13911 13913 +13937 13911 13938 13521 13938 13522 13938 13915 13522 +13938 13913 13915 13522 13915 13524 13524 13915 13917 +13524 13917 13526 13526 13917 13919 13920 13939 13919 +13939 13526 13919 13939 13528 13526 13940 13941 13530 +13528 13940 13530 13939 13940 13528 13942 13943 13941 +13942 13941 13940 13944 13945 13943 13944 13943 13942 +13946 13945 13944 13531 13924 13516 13924 13531 13532 +13924 13532 13926 13926 13532 13533 13926 13533 13928 +13928 13533 13535 13928 13535 13930 13947 13932 13930 +13535 13947 13930 13537 13947 13535 13948 13949 13538 +13949 13537 13538 13949 13947 13537 13948 13950 13949 +13933 13539 13520 13539 13933 13541 13541 13933 13934 +13541 13934 13543 13543 13934 13935 13543 13935 13545 +13545 13935 13936 13545 13936 13547 13547 13936 13937 +13547 13937 13549 13549 13937 13938 13549 13938 13521 +13941 13555 13530 13555 13941 13557 13557 13941 13943 +13557 13943 13559 13559 13943 13945 13951 13952 13561 +13951 13561 13559 13946 13951 13559 13945 13946 13559 +13953 13954 13952 13953 13952 13951 13955 13954 13953 +13563 13948 13538 13956 13957 13564 13564 13957 13563 +13957 13950 13563 13950 13948 13563 13956 13958 13957 +13952 13565 13561 13565 13952 13566 13566 13952 13954 +13955 13959 13954 13959 13566 13954 13959 13567 13566 +13567 13960 13568 13959 13960 13567 13569 13568 13960 +13569 13960 13961 13962 13963 13956 13564 13962 13956 +13571 13962 13564 13956 13963 13958 13961 13964 13569 +13569 13964 13572 13573 13572 13964 13573 13964 13965 +13576 13966 13577 13966 13967 13577 13967 13571 13577 +13967 13962 13571 13965 13968 13573 13573 13968 13578 +13578 13969 13579 13968 13969 13578 13580 13579 13969 +13580 13969 13970 13584 13971 13585 13972 13586 13585 +13972 13585 13971 13586 13972 13973 13974 13586 13973 +13587 13586 13974 13576 13587 13966 13587 13975 13966 +13587 13974 13975 13966 13975 13967 13588 13580 13976 +13580 13977 13976 13580 13970 13977 13976 13589 13588 +13592 13978 13593 13979 13594 13593 13979 13593 13978 +13980 13595 13594 13980 13594 13979 13981 13596 13595 +13981 13595 13980 13981 13982 13596 13596 13982 13584 +13982 13983 13584 13983 13971 13584 13971 13983 13972 +13972 13983 13984 13972 13984 13973 13977 13985 13976 +13985 13986 13987 13985 13987 13597 13976 13985 13597 +13589 13976 13597 13987 13598 13597 13606 13988 13607 +13989 13608 13607 13989 13607 13988 13989 13609 13608 +13616 13990 13617 13991 13618 13617 13991 13617 13990 +13991 13992 13618 13618 13992 13592 13992 13993 13592 +13993 13978 13592 13978 13993 13979 13979 13993 13994 +13979 13994 13980 13980 13994 13995 13980 13995 13981 +13981 13995 13996 13981 13996 13982 13986 13997 13987 +13987 13997 13598 13997 13619 13598 13997 13998 13619 +13619 13999 13620 13998 13999 13619 13620 14000 13621 +13999 14000 13620 13621 14001 13622 14000 14001 13621 +13622 14002 13623 14001 14002 13622 13623 14003 13624 +14002 14003 13623 13624 14004 13625 14003 14004 13624 +13606 13625 13988 13625 14005 13988 13625 14004 14005 +14006 14007 14005 14007 13988 14005 14007 13989 13988 +14008 13626 13609 13989 14008 13609 14007 14008 13989 +14009 13627 13626 14009 13626 14008 14010 13628 13627 +14010 13627 14009 14011 13629 13628 14011 13628 14010 +14012 13630 13629 14012 13629 14011 14013 13631 13630 +14013 13630 14012 14013 14014 13631 13631 14014 13616 +14014 14015 13616 14015 13990 13616 13990 14015 13991 +13991 14015 14016 13991 14016 13992 14007 14006 14017 +14017 14006 14018 14007 14019 14008 14007 14017 14019 +14008 14020 14009 14008 14019 14020 14009 14021 14010 +14009 14020 14021 14010 14022 14011 14010 14021 14022 +14011 14023 14012 14011 14022 14023 14012 14024 14013 +14012 14023 14024 14014 14013 14025 14025 14013 14024 +14018 14026 14017 14017 14026 14019 14019 14026 14027 +14019 14027 14020 14020 14027 14028 14020 14028 14021 +14021 14028 14029 14021 14029 14022 14022 14029 14030 +14022 14030 14023 14023 14030 14031 14023 14031 14024 +14024 14031 14032 14024 14032 14025 14033 14034 14035 +14036 14037 14035 14035 14037 14033 14038 14039 14036 +14036 14039 14037 13633 13632 14038 13632 14039 14038 +13632 14040 14039 14041 13632 13636 14041 14040 13632 +14042 13636 13638 14042 14041 13636 14043 14044 13639 +14044 13638 13639 14044 14042 13638 14043 14045 14044 +14046 14047 14048 14048 14049 14046 14050 14051 14052 +14053 14054 14052 14052 14054 14050 14055 14056 14053 +14053 14056 14054 14057 14058 14055 14055 14058 14056 +14059 14060 14057 14057 14060 14058 14060 14059 14034 +14059 14035 14034 14059 14061 14035 14061 13641 13640 +14036 14061 13640 14035 14061 14036 14036 13640 13644 +14036 13644 14038 14038 13644 13633 13647 14043 13639 +14062 14045 14043 13647 14062 14043 13649 14062 13647 +13649 13650 14062 14062 13650 14063 14064 14065 14066 +14064 14066 14047 14047 14066 14048 14066 13652 14048 +13652 13651 14048 14067 14049 14048 13651 14067 14048 +13655 14067 13651 14068 14069 13656 14069 13655 13656 +14069 14067 13655 14069 14068 14051 14051 14068 14052 +14068 13658 14052 13658 13657 14052 14052 13657 13661 +14052 13661 14053 14053 13661 13662 14070 14053 13662 +14055 14053 14070 14055 14071 14057 14055 14070 14071 +14071 13664 13663 14059 14071 13663 14057 14071 14059 +14059 13663 13667 14059 13667 14061 14061 13667 13641 +13668 14072 14073 14063 13668 14073 13650 13668 14063 +14072 14074 14073 14075 14076 14077 14078 14079 14077 +14077 14079 14075 13670 13669 14078 13669 14079 14078 +13669 14080 14079 14081 14080 14082 14080 13671 14082 +14080 13669 13671 14083 14082 14084 14082 13671 14084 +13672 14085 14084 13672 14084 13671 14086 14087 14085 +14085 14087 14084 13674 13673 14086 13673 14087 14086 +13673 14088 14087 14089 13673 13676 14089 14088 13673 +13677 14066 14065 14089 13677 14065 13676 13677 14089 +14066 13677 13652 13678 14068 13656 14068 13678 13658 +13680 14070 13662 14070 13680 13682 14070 13682 14071 +14071 13682 13664 13683 14072 13668 14090 14074 14072 +13683 14090 14072 13684 14090 13683 14091 13684 13686 +14091 14090 13684 13686 13687 14091 14091 13687 14092 +14093 14094 13688 13688 14094 13689 14095 13688 13690 +14095 14093 13688 13691 14077 14076 14095 13691 14076 +13690 13691 14095 14077 13691 13692 14077 13692 14078 +14078 13692 13670 14096 14081 14082 13672 13693 14085 +14082 14083 14096 14085 13693 13694 14085 13694 14086 +14086 13694 13674 14097 14092 13687 14097 13687 13696 +13696 13697 14097 14097 13697 14098 14099 14100 13698 +13698 14100 13699 13698 13689 14094 13698 14094 14099 +14101 14102 13701 14101 13701 13700 13697 13701 14098 +14098 13701 14102 14100 13702 13699 14103 13702 14100 +13703 14101 13700 13703 14104 14101 14103 13704 13702 +14105 13704 14103 13705 14104 13703 13705 14106 14104 +14105 13706 13704 14107 13706 14105 13707 14106 13705 +13707 14108 14106 14107 14109 13706 13706 14109 13708 +13709 13708 14109 13709 14109 14110 14111 14112 14113 +14112 14108 14113 14108 13710 14113 14108 13707 13710 +14110 13711 13709 14114 13711 14110 14111 14113 13713 +13712 14111 13713 14115 14111 13712 13713 14113 13710 +14114 14116 13711 13711 14116 13714 13715 13714 14116 +13715 14116 14117 13716 14115 13712 13716 14118 14115 +14117 13717 13715 14119 13717 14117 13718 14118 13716 +13718 14120 14118 14121 14122 14123 14119 14121 14123 +13719 14119 14123 13717 14119 13719 13720 14120 13718 +13720 14124 14120 14123 14122 13721 14122 13722 13721 +14122 14125 13722 14123 13721 13719 13723 14124 13720 +13723 14126 14124 14125 14127 13722 13722 14127 13724 +13725 13724 14127 13725 14127 14128 14129 14130 13727 +14129 13727 13726 13723 13727 14126 14126 13727 14130 +14128 13729 13725 14131 13729 14128 13730 14129 13726 +13730 14132 14129 14131 13731 13729 14133 13731 14131 +13732 14132 13730 13732 14134 14132 14133 13733 13731 +14135 13733 14133 13734 14134 13732 13734 14136 14134 +14135 13735 13733 14137 13735 14135 14138 14136 13734 +14138 13734 13736 13736 13737 14138 14138 13737 14139 +14137 13738 13735 14140 13738 14137 13739 14139 13737 +13739 14141 14139 14140 13740 13738 14142 13740 14140 +13741 14141 13739 13741 14143 14141 14142 13742 13740 +14144 13742 14142 13743 14143 13741 13743 14145 14143 +14144 13744 13742 14146 13744 14144 13745 14145 13743 +13745 14147 14145 14148 14149 13746 13746 14149 13747 +13746 13744 14146 13746 14146 14148 13749 14147 13745 +13749 14150 14147 14149 14151 13747 13747 14151 13750 +13751 13750 14151 13751 14151 14152 14153 14154 14155 +14154 14150 14155 14150 13752 14155 14150 13749 13752 +14152 13753 13751 14156 13753 14152 14157 14153 14155 +14155 13752 14157 13752 14158 14157 13752 13754 14158 +14156 13755 13753 14159 13755 14156 13756 14158 13754 +13756 14160 14158 14159 13757 13755 14161 13757 14159 +13758 14160 13756 13758 14162 14160 14161 13759 13757 +14163 13759 14161 13760 14162 13758 13760 14164 14162 +14163 13761 13759 14165 13761 14163 14166 14164 13760 +14166 13760 13762 13762 13763 14166 14166 13763 14167 +14165 13764 13761 14168 13764 14165 13765 14167 13763 +13765 14169 14167 14168 13766 13764 14170 13766 14168 +14171 14172 14173 13768 13767 14173 14172 13768 14173 +14174 13768 14172 13768 14175 13770 14174 14175 13768 +14175 14176 14177 13771 14175 14177 13770 14175 13771 +14177 13772 13771 13773 14169 13765 13773 14178 14169 +14170 13774 13766 14179 13774 14170 14180 14181 14182 +13776 13775 14182 14181 13776 14182 14183 13776 14181 +14173 13778 14171 13778 14183 14171 13778 13776 14183 +13778 14173 13767 14176 14184 14177 14177 14184 13772 +14184 13779 13772 14184 14185 13779 13779 14186 13780 +14185 14186 13779 13780 14187 13781 14186 14187 13780 +13781 14188 13782 14187 14188 13781 13782 14189 13783 +14188 14189 13782 13783 14190 13784 14189 14190 13783 +13784 14191 13785 14190 14191 13784 13785 14192 13786 +14191 14192 13785 13786 14193 13787 14192 14193 13786 +13787 14194 13788 14193 14194 13787 13790 13788 14194 +13790 14194 14195 13791 14178 13773 13791 14196 14178 +14179 13792 13774 14197 13792 14179 14198 14199 13794 +14198 13794 13793 14182 13795 14180 13795 14199 14180 +13795 13794 14199 13795 14182 13775 14200 13796 14201 +13796 14195 14201 13796 13790 14195 14202 14200 14201 +14203 14204 13798 14203 13798 13797 13791 13798 14196 +14196 13798 14204 14205 13800 14206 13800 14197 14206 +13800 13792 14197 14207 14205 14206 14208 14209 13802 +14208 13802 13801 13802 14210 13803 14209 14210 13802 +13793 13803 14198 14198 13803 14210 14200 13804 13796 +14202 14211 14200 14211 13804 14200 14211 13805 13804 +13805 14212 13807 14211 14212 13805 13807 14213 13809 +14212 14213 13807 13797 13809 14203 14203 13809 14213 +14205 13811 13800 14207 14214 14205 14214 13811 14205 +14214 13812 13811 13812 14215 13814 14214 14215 13812 +13814 14216 13815 14215 14216 13814 13815 14217 13817 +14216 14217 13815 13801 13817 14208 14208 13817 14217 +14218 13819 13818 13820 14219 13818 13818 14219 14218 +13821 14220 13820 13820 14220 14219 13822 14221 13821 +13821 14221 14220 13823 14222 13822 13822 14222 14221 +13824 14222 13823 14223 13826 13825 14223 13825 14224 +13825 14225 14224 13825 13828 14225 14226 13828 13829 +14226 14225 13828 14227 13829 13830 14227 14226 13829 +13830 13819 14218 14228 13830 14218 14227 13830 14228 +14228 14218 14219 14228 14219 14229 14229 14219 14220 +14229 14220 14230 14230 14220 14221 14230 14221 14231 +14232 14233 14231 14221 14232 14231 14222 14232 14221 +13832 14232 13824 13824 14232 14222 14234 13836 13835 +13838 14235 13835 13835 14235 14234 14235 13838 14236 +13838 14237 14236 13838 13839 14237 13839 13826 14223 +14238 13839 14223 14237 13839 14238 14238 14223 14224 +14239 14233 14232 14240 14239 14241 14241 14239 13840 +14239 14232 13840 14232 13832 13840 13842 14241 13840 +14242 13844 13843 13845 14242 13843 14243 13849 14234 +14234 13849 13836 14235 14244 14245 14243 14235 14245 +14234 14235 14243 14244 14235 14236 14240 14241 14246 +14240 14246 14247 13850 14248 13842 14248 14241 13842 +14248 14246 14241 13844 14242 13850 14242 14248 13850 +14242 14249 14248 13851 14250 13845 14250 14242 13845 +14250 14249 14242 13852 14251 13851 13851 14251 14250 +13853 14252 13852 13852 14252 14251 13854 14252 13853 +14253 13856 14243 14243 13856 13849 14243 14245 14254 +14243 14254 14253 14255 14247 14246 14256 14257 14255 +14246 14256 14255 14248 14256 14246 14258 14248 14249 +14256 14248 14258 14259 14249 14250 14258 14249 14259 +14251 14260 14261 14259 14251 14261 14250 14251 14259 +14260 14251 14252 14260 14252 14262 14262 14252 13854 +13857 14262 13854 14263 14262 13857 14264 14263 14265 +14263 13858 14265 14263 13857 13858 13859 14266 13858 +13858 14266 14265 13860 14266 13859 14267 13862 14253 +14253 13862 13856 14253 14254 14268 14253 14268 14267 +14257 14256 14269 14257 14269 14270 14271 14256 14258 +14269 14256 14271 14272 14258 14259 14271 14258 14272 +14259 14261 14273 14259 14273 14272 14274 14264 14265 +14274 14265 14266 14274 14266 14275 14275 14266 13860 +13863 14275 13860 14276 14275 13863 13863 13864 14276 +14276 13864 14277 14278 13867 14267 14267 13867 13862 +14267 14268 14279 14280 14267 14279 14278 14267 14280 +14281 14282 14280 14281 14280 14279 14283 14284 14282 +14283 14282 14281 14285 14286 14284 14285 14284 14283 +14287 14288 14286 14287 14286 14285 14289 14290 14288 +14289 14288 14287 14291 14292 14290 14291 14290 14289 +14293 14294 14292 14293 14292 14291 14295 14296 14294 +14295 14294 14293 14297 14298 14296 14297 14296 14295 +14299 14298 14297 14300 14301 14302 14302 14301 14270 +14269 14302 14270 14303 14302 14269 14271 14304 14305 +14303 14271 14305 14269 14271 14303 14304 14271 14272 +14304 14272 14306 14306 14272 14273 14307 14277 13864 +14307 13864 13868 14308 14307 14309 14307 13869 14309 +14307 13868 13869 13870 14310 13869 13869 14310 14309 +13871 14311 13870 13870 14311 14310 13872 14312 13871 +13871 14312 14311 14312 13872 14313 13872 14314 14313 +13872 13873 14314 14315 13873 13874 14315 14314 13873 +14316 13874 13876 14316 14315 13874 14317 13876 13878 +14317 14316 13876 14318 14319 13879 14319 13878 13879 +14319 14317 13878 14318 14320 14319 14278 13880 13867 +13880 14278 13882 14278 14321 13882 14278 14280 14321 +14322 14280 14282 14321 14280 14322 14323 14282 14284 +14322 14282 14323 14324 14284 14286 14323 14284 14324 +14325 14286 14288 14324 14286 14325 14326 14288 14290 +14325 14288 14326 14327 14290 14292 14326 14290 14327 +14328 14292 14294 14327 14292 14328 14329 14294 14296 +14328 14294 14329 14330 14296 14298 14329 14296 14330 +14298 14299 14331 14332 14298 14331 14330 14298 14332 +14332 14331 14300 14302 14332 14300 14333 14332 14302 +14334 14302 14303 14333 14302 14334 14303 14305 14335 +14303 14335 14334 14336 14308 14309 14336 14309 14310 +14336 14310 14337 14337 14310 14311 14337 14311 14338 +14338 14311 14312 14338 14312 14339 14339 14312 14313 +13883 14318 13879 13883 13884 14318 13884 14340 14318 +14340 14320 14318 13884 14341 14340 13885 14341 13884 +14342 13886 14321 14321 13886 13882 14343 14321 14322 +14342 14321 14343 14344 14322 14323 14343 14322 14344 +14345 14323 14324 14344 14323 14345 14346 14324 14325 +14345 14324 14346 14347 14325 14326 14346 14325 14347 +14348 14326 14327 14347 14326 14348 14349 14327 14328 +14348 14327 14349 14350 14328 14329 14349 14328 14350 +14351 14329 14330 14350 14329 14351 14352 14330 14332 +14351 14330 14352 14353 14332 14333 14352 14332 14353 +14354 14333 14334 14353 14333 14354 14334 14335 14355 +14356 14334 14355 14354 14334 14356 14357 14356 14355 +14358 14340 14341 14358 14341 13885 13887 14358 13885 +14359 14358 13887 14360 13887 13889 14360 14359 13887 +14361 14362 13890 14362 13889 13890 14362 14360 13889 +14363 14364 14361 14361 14364 14362 14365 14366 14363 +14363 14366 14364 13892 13891 14365 13891 14366 14365 +13891 14367 14366 14368 13891 13895 14368 14367 13891 +14369 13895 13896 14369 14368 13895 14370 13896 13897 +14370 14369 13896 14371 13897 13898 14371 14370 13897 +13898 13899 14371 14371 13899 14372 14342 13901 13886 +13901 14342 13902 13902 14342 14343 13902 14343 13904 +13904 14343 14344 13904 14344 13906 13906 14344 14345 +13906 14345 13908 13908 14345 14346 13908 14346 13910 +13910 14346 14347 13910 14347 13912 13912 14347 14348 +13912 14348 13914 13914 14348 14349 13914 14349 13916 +13916 14349 14350 13916 14350 13918 13918 14350 14351 +13918 14351 13920 14351 14373 13920 14351 14352 14373 +14374 14352 14353 14373 14352 14374 14375 14353 14354 +14374 14353 14375 14376 14354 14356 14375 14354 14376 +14356 14357 14377 14378 14356 14377 14376 14356 14378 +14379 14378 14377 13921 14361 13890 14361 13921 13922 +14361 13922 14363 14363 13922 13923 14363 13923 14365 +14365 13923 13892 14380 14372 13899 14380 13899 13925 +14381 13925 13927 14381 14380 13925 14382 13927 13929 +14382 14381 13927 14383 13929 13931 14383 14382 13929 +13931 13932 14383 14383 13932 14384 14373 13939 13920 +13939 14373 13940 13940 14373 14374 13940 14374 13942 +13942 14374 14375 13942 14375 13944 13944 14375 14376 +13944 14376 13946 14376 14385 13946 14376 14378 14385 +14378 14379 14386 14387 14378 14386 14385 14378 14387 +14388 14387 14386 14389 14384 13932 14389 13932 13947 +14390 13947 13949 14390 14389 13947 14391 14392 13950 +14392 13949 13950 14392 14390 13949 14391 14393 14392 +14385 13951 13946 13951 14385 13953 13953 14385 14387 +14394 14395 13955 14394 13955 13953 14388 14394 13953 +14387 14388 13953 14396 14397 14395 14396 14395 14394 +14398 14397 14396 13957 14391 13950 13957 13958 14391 +13958 14393 14391 13958 14399 14393 14395 13959 13955 +13959 14395 13960 13960 14395 14397 13961 13960 14397 +14398 13961 14397 14400 13961 14398 13962 14401 13963 +14401 14402 13963 14402 13958 13963 14402 14399 13958 +14400 14403 13961 13961 14403 13964 13965 13964 14403 +13965 14403 14404 13967 14405 14401 13967 14401 13962 +14402 14401 14406 14406 14401 14405 14404 14407 13965 +13965 14407 13968 14407 14408 14409 13969 14407 14409 +13968 14407 13969 14409 13970 13969 14410 14411 13974 +14410 13974 13973 13974 14412 13975 14411 14412 13974 +13967 13975 14405 13975 14413 14405 13975 14412 14413 +14405 14413 14406 14408 14414 14409 13970 14409 14414 +14415 13970 14414 13977 13970 14415 13982 14416 13983 +13983 14416 14417 14418 13983 14417 13984 13983 14418 +13973 13984 14410 14410 13984 14418 14415 14419 13977 +13977 14419 13985 13986 13985 14419 13986 14419 14420 +13992 14421 13993 14422 13994 13993 14422 13993 14421 +14423 13995 13994 14423 13994 14422 13995 14423 14424 +14425 13995 14424 13996 13995 14425 13982 13996 14416 +13996 14426 14416 13996 14425 14426 14416 14426 14417 +14420 14427 13986 13986 14427 13997 14427 14428 14429 +13998 14427 14429 13997 14427 13998 14430 13999 13998 +14430 13998 14429 14431 14000 13999 14431 13999 14430 +14432 14001 14000 14432 14000 14431 14433 14002 14001 +14433 14001 14432 14434 14003 14002 14434 14002 14433 +14435 14004 14003 14435 14003 14434 14436 14005 14004 +14436 14004 14435 14436 14006 14005 14014 14437 14015 +14438 14016 14015 14438 14015 14437 14438 14439 14016 +14016 14439 13992 14439 14440 13992 14440 14421 13992 +14421 14440 14422 14422 14440 14441 14422 14441 14423 +14423 14441 14442 14423 14442 14424 14428 14443 14429 +14429 14443 14430 14430 14443 14444 14430 14444 14431 +14431 14444 14445 14431 14445 14432 14432 14445 14446 +14432 14446 14433 14433 14446 14447 14433 14447 14434 +14434 14447 14448 14434 14448 14435 14435 14448 14449 +14450 14451 14449 14451 14435 14449 14451 14436 14435 +14451 14018 14006 14451 14006 14436 14025 14452 14437 +14025 14437 14014 14437 14453 14438 14437 14452 14453 +14439 14438 14454 14454 14438 14453 14451 14450 14455 +14455 14450 14456 14457 14026 14018 14451 14457 14018 +14455 14457 14451 14458 14027 14026 14458 14026 14457 +14459 14028 14027 14459 14027 14458 14460 14029 14028 +14460 14028 14459 14461 14030 14029 14461 14029 14460 +14462 14031 14030 14462 14030 14461 14463 14032 14031 +14463 14031 14462 14463 14464 14032 14032 14464 14025 +14464 14465 14025 14465 14452 14025 14452 14465 14453 +14453 14465 14466 14453 14466 14454 14467 14468 14469 +14469 14470 14467 14471 14472 14473 14474 14475 14473 +14473 14475 14471 14476 14477 14474 14474 14477 14475 +14477 14476 14456 14476 14455 14456 14476 14478 14455 +14455 14479 14457 14455 14478 14479 14457 14480 14458 +14457 14479 14480 14458 14481 14459 14458 14480 14481 +14481 14034 14033 14460 14481 14033 14459 14481 14460 +14460 14033 14037 14460 14037 14461 14461 14037 14039 +14461 14039 14462 14462 14039 14040 14462 14040 14463 +14482 14464 14463 14040 14482 14463 14041 14482 14040 +14483 14041 14042 14483 14482 14041 14484 14042 14044 +14484 14483 14042 14044 14045 14484 14484 14045 14485 +14469 14468 14486 14486 14468 14487 14046 14488 14047 +14488 14469 14047 14469 14486 14047 14469 14488 14470 +14489 14473 14049 14473 14488 14049 14488 14046 14049 +14473 14472 14488 14473 14490 14474 14473 14489 14490 +14490 14051 14050 14476 14490 14050 14474 14490 14476 +14476 14050 14054 14476 14054 14478 14478 14054 14056 +14478 14056 14479 14479 14056 14058 14479 14058 14480 +14480 14058 14060 14480 14060 14481 14481 14060 14034 +14491 14485 14045 14491 14045 14062 14062 14063 14491 +14491 14063 14492 14493 14494 14495 14493 14495 14487 +14487 14495 14486 14495 14065 14486 14065 14064 14486 +14486 14064 14047 14067 14489 14049 14489 14067 14069 +14489 14069 14490 14490 14069 14051 14496 14492 14063 +14496 14063 14073 14073 14074 14496 14496 14074 14497 +14498 14499 14075 14075 14499 14076 14500 14075 14079 +14500 14498 14075 14501 14079 14080 14501 14500 14079 +14502 14503 14081 14503 14080 14081 14503 14501 14080 +14083 14084 14502 14084 14503 14502 14084 14504 14503 +14505 14084 14087 14505 14504 14084 14506 14087 14088 +14506 14505 14087 14089 14495 14494 14506 14089 14494 +14088 14089 14506 14495 14089 14065 14507 14497 14074 +14507 14074 14090 14508 14090 14091 14508 14507 14090 +14091 14092 14508 14508 14092 14509 14510 14511 14093 +14093 14511 14094 14512 14093 14095 14512 14510 14093 +14095 14076 14499 14095 14499 14512 14096 14502 14081 +14502 14096 14083 14092 14097 14513 14514 14092 14513 +14509 14092 14514 14098 14513 14097 14515 14516 14099 +14099 14516 14100 14099 14094 14511 14099 14511 14515 +14514 14513 14102 14101 14514 14102 14517 14514 14101 +14102 14513 14098 14516 14103 14100 14518 14103 14516 +14104 14517 14101 14104 14519 14517 14518 14105 14103 +14520 14105 14518 14106 14519 14104 14106 14521 14519 +14522 14107 14523 14107 14520 14523 14107 14105 14520 +14524 14522 14523 14108 14521 14106 14108 14525 14521 +14522 14109 14107 14110 14109 14522 14524 14110 14522 +14526 14110 14524 14527 14528 14112 14527 14112 14111 +14108 14112 14525 14525 14112 14528 14526 14114 14110 +14529 14114 14526 14115 14527 14111 14115 14530 14527 +14529 14531 14114 14114 14531 14116 14117 14116 14531 +14117 14531 14532 14118 14530 14115 14118 14533 14530 +14532 14119 14117 14534 14119 14532 14120 14533 14118 +14120 14535 14533 14536 14537 14121 14121 14537 14122 +14121 14119 14534 14121 14534 14536 14124 14535 14120 +14124 14538 14535 14537 14125 14122 14539 14125 14537 +14126 14538 14124 14126 14540 14538 14539 14541 14125 +14125 14541 14127 14128 14127 14541 14128 14541 14542 +14543 14544 14130 14543 14130 14129 14126 14130 14540 +14540 14130 14544 14542 14131 14128 14545 14131 14542 +14132 14543 14129 14132 14546 14543 14545 14133 14131 +14547 14133 14545 14134 14546 14132 14134 14548 14546 +14547 14135 14133 14549 14135 14547 14136 14548 14134 +14136 14550 14548 14549 14137 14135 14551 14137 14549 +14552 14550 14136 14552 14136 14138 14138 14139 14552 +14552 14139 14553 14551 14140 14137 14554 14140 14551 +14141 14553 14139 14141 14555 14553 14554 14142 14140 +14556 14142 14554 14143 14555 14141 14143 14557 14555 +14556 14144 14142 14558 14144 14556 14145 14557 14143 +14145 14559 14557 14558 14146 14144 14560 14146 14558 +14147 14559 14145 14147 14561 14559 14562 14563 14148 +14148 14563 14149 14148 14146 14560 14148 14560 14562 +14150 14561 14147 14150 14564 14561 14563 14565 14149 +14149 14565 14151 14152 14151 14565 14152 14565 14566 +14153 14567 14154 14567 14568 14154 14568 14150 14154 +14568 14564 14150 14566 14156 14152 14569 14156 14566 +14157 14567 14153 14157 14158 14567 14158 14568 14567 +14158 14570 14568 14569 14159 14156 14571 14159 14569 +14160 14570 14158 14160 14572 14570 14571 14161 14159 +14573 14161 14571 14162 14572 14160 14162 14574 14572 +14573 14163 14161 14575 14163 14573 14164 14574 14162 +14164 14576 14574 14575 14165 14163 14577 14165 14575 +14578 14576 14164 14578 14164 14166 14166 14167 14578 +14578 14167 14579 14577 14168 14165 14580 14168 14577 +14169 14579 14167 14169 14581 14579 14580 14170 14168 +14582 14170 14580 14583 14584 14585 14172 14171 14585 +14584 14172 14585 14586 14172 14584 14172 14587 14174 +14586 14587 14172 14174 14588 14175 14587 14588 14174 +14176 14175 14588 14176 14588 14589 14590 14591 14592 +14593 14594 14592 14593 14592 14591 14595 14596 14594 +14595 14594 14593 14597 14598 14596 14597 14596 14595 +14599 14600 14598 14599 14598 14597 14601 14602 14600 +14601 14600 14599 14603 14604 14602 14603 14602 14601 +14605 14606 14604 14605 14604 14603 14607 14606 14605 +14178 14581 14169 14178 14608 14581 14582 14179 14170 +14609 14179 14582 14610 14611 14181 14610 14181 14180 +14585 14183 14583 14183 14611 14583 14183 14181 14611 +14183 14585 14171 14589 14612 14176 14176 14612 14184 +14592 14185 14590 14185 14612 14590 14185 14184 14612 +14185 14592 14186 14186 14592 14594 14186 14594 14187 +14187 14594 14596 14187 14596 14188 14188 14596 14598 +14188 14598 14189 14189 14598 14600 14189 14600 14190 +14190 14600 14602 14190 14602 14191 14191 14602 14604 +14191 14604 14192 14192 14604 14606 14607 14613 14606 +14613 14192 14606 14613 14193 14192 14193 14614 14194 +14613 14614 14193 14195 14194 14614 14195 14614 14615 +14616 14617 14618 14617 14608 14618 14608 14196 14618 +14608 14178 14196 14609 14197 14179 14619 14197 14609 +14620 14621 14622 14623 14624 14622 14623 14622 14621 +14199 14198 14624 14623 14199 14624 14625 14199 14623 +14180 14199 14610 14610 14199 14625 14615 14626 14195 +14195 14626 14201 14627 14628 14202 14201 14627 14202 +14626 14627 14201 14629 14630 14628 14629 14628 14627 +14631 14632 14630 14631 14630 14629 14632 14631 14616 +14632 14616 14618 14203 14632 14618 14204 14203 14618 +14204 14618 14196 14619 14633 14197 14197 14633 14206 +14634 14635 14207 14206 14634 14207 14633 14634 14206 +14636 14635 14634 14637 14638 14639 14639 14638 14620 +14639 14620 14622 14208 14639 14622 14209 14208 14622 +14209 14622 14210 14210 14622 14624 14210 14624 14198 +14628 14211 14202 14211 14628 14212 14212 14628 14630 +14212 14630 14213 14213 14630 14632 14213 14632 14203 +14635 14214 14207 14636 14640 14635 14640 14214 14635 +14640 14215 14214 14215 14641 14216 14640 14641 14215 +14639 14217 14637 14217 14641 14637 14217 14216 14641 +14217 14639 14208 14642 14224 14225 14226 14643 14225 +14225 14643 14642 14227 14644 14226 14226 14644 14643 +14228 14645 14227 14227 14645 14644 14229 14646 14228 +14228 14646 14645 14230 14647 14229 14229 14647 14646 +14231 14648 14230 14230 14648 14647 14233 14648 14231 +14649 14236 14237 14238 14650 14237 14237 14650 14649 +14224 14642 14238 14642 14650 14238 14642 14651 14650 +14652 14642 14643 14651 14642 14652 14653 14643 14644 +14652 14643 14653 14654 14644 14645 14653 14644 14654 +14655 14645 14646 14654 14645 14655 14656 14646 14647 +14655 14646 14656 14657 14647 14648 14656 14647 14657 +14239 14658 14233 14658 14648 14233 14658 14657 14648 +14240 14658 14239 14659 14245 14244 14236 14649 14244 +14649 14659 14244 14649 14660 14659 14661 14649 14650 +14660 14649 14661 14662 14650 14651 14661 14650 14662 +14663 14651 14652 14662 14651 14663 14664 14652 14653 +14663 14652 14664 14665 14653 14654 14664 14653 14665 +14666 14654 14655 14665 14654 14666 14667 14655 14656 +14666 14655 14667 14668 14656 14657 14667 14656 14668 +14669 14657 14658 14668 14657 14669 14247 14669 14240 +14240 14669 14658 14670 14254 14659 14659 14254 14245 +14671 14659 14660 14670 14659 14671 14672 14660 14661 +14671 14660 14672 14673 14661 14662 14672 14661 14673 +14674 14662 14663 14673 14662 14674 14675 14663 14664 +14674 14663 14675 14676 14664 14665 14675 14664 14676 +14677 14665 14666 14676 14665 14677 14678 14666 14667 +14677 14666 14678 14679 14667 14668 14678 14667 14679 +14680 14668 14669 14679 14668 14680 14255 14681 14247 +14681 14669 14247 14681 14680 14669 14257 14681 14255 +14682 14261 14260 14262 14683 14260 14260 14683 14682 +14263 14684 14262 14262 14684 14683 14264 14684 14263 +14685 14268 14670 14670 14268 14254 14686 14670 14671 +14685 14670 14686 14687 14671 14672 14686 14671 14687 +14688 14672 14673 14687 14672 14688 14689 14673 14674 +14688 14673 14689 14690 14674 14675 14689 14674 14690 +14691 14675 14676 14690 14675 14691 14692 14676 14677 +14691 14676 14692 14693 14677 14678 14692 14677 14693 +14694 14678 14679 14693 14678 14694 14695 14679 14680 +14694 14679 14695 14696 14680 14681 14695 14680 14696 +14270 14696 14257 14257 14696 14681 14697 14698 14682 +14698 14261 14682 14698 14273 14261 14697 14682 14683 +14697 14683 14699 14699 14683 14684 14699 14684 14700 +14700 14684 14264 14274 14700 14264 14701 14700 14274 +14702 14701 14703 14701 14275 14703 14701 14274 14275 +14276 14704 14275 14275 14704 14703 14277 14704 14276 +14685 14279 14268 14279 14685 14281 14281 14685 14686 +14281 14686 14283 14283 14686 14687 14283 14687 14285 +14285 14687 14688 14285 14688 14287 14287 14688 14689 +14287 14689 14289 14289 14689 14690 14289 14690 14291 +14291 14690 14691 14291 14691 14293 14293 14691 14692 +14293 14692 14295 14295 14692 14693 14295 14693 14297 +14297 14693 14694 14297 14694 14299 14694 14705 14299 +14694 14695 14705 14300 14705 14301 14705 14696 14301 +14705 14695 14696 14301 14696 14270 14706 14305 14304 +14306 14707 14304 14304 14707 14706 14273 14698 14708 +14707 14273 14708 14709 14707 14708 14306 14273 14707 +14710 14709 14708 14711 14702 14703 14711 14703 14704 +14711 14704 14712 14713 14712 14714 14714 14712 14307 +14712 14704 14307 14704 14277 14307 14308 14714 14307 +14715 14313 14314 14715 14314 14716 14314 14717 14716 +14314 14315 14717 14718 14315 14316 14718 14717 14315 +14719 14316 14317 14719 14718 14316 14720 14317 14319 +14720 14719 14317 14319 14320 14720 14720 14320 14721 +14705 14331 14299 14331 14705 14300 14722 14335 14706 +14706 14335 14305 14723 14706 14707 14722 14706 14723 +14724 14707 14709 14723 14707 14724 14709 14710 14725 +14709 14725 14724 14726 14713 14714 14726 14714 14308 +14336 14726 14308 14727 14726 14336 14728 14336 14337 +14728 14727 14336 14729 14337 14338 14729 14728 14337 +14730 14338 14339 14730 14729 14338 14339 14313 14715 +14731 14339 14715 14730 14339 14731 14731 14715 14716 +14340 14732 14733 14721 14340 14733 14320 14340 14721 +14734 14735 14732 14732 14735 14733 14736 14737 14734 +14734 14737 14735 14736 14738 14737 14722 14355 14335 +14355 14722 14357 14722 14739 14357 14722 14723 14739 +14740 14723 14724 14739 14723 14740 14724 14725 14741 +14724 14741 14740 14358 14732 14340 14732 14358 14359 +14732 14359 14734 14734 14359 14360 14734 14360 14736 +14742 14738 14736 14360 14742 14736 14362 14742 14360 +14743 14362 14364 14743 14742 14362 14744 14364 14366 +14744 14743 14364 14745 14366 14367 14745 14744 14366 +14746 14367 14368 14746 14745 14367 14747 14368 14369 +14747 14746 14368 14748 14369 14370 14748 14747 14369 +14749 14370 14371 14749 14748 14370 14371 14372 14749 +14749 14372 14750 14739 14377 14357 14377 14739 14379 +14739 14751 14379 14739 14740 14751 14740 14741 14752 +14753 14740 14752 14751 14740 14753 14754 14753 14752 +14755 14750 14372 14755 14372 14380 14756 14380 14381 +14756 14755 14380 14757 14381 14382 14757 14756 14381 +14758 14382 14383 14758 14757 14382 14759 14760 14384 +14760 14383 14384 14760 14758 14383 14761 14762 14759 +14759 14762 14760 14761 14763 14762 14751 14386 14379 +14386 14751 14388 14751 14764 14388 14751 14753 14764 +14753 14754 14765 14766 14753 14765 14764 14753 14766 +14767 14768 14766 14767 14766 14765 14769 14768 14767 +14389 14759 14384 14759 14389 14390 14759 14390 14761 +14770 14763 14761 14390 14770 14761 14392 14770 14390 +14392 14393 14770 14770 14393 14771 14764 14394 14388 +14394 14764 14396 14396 14764 14766 14396 14766 14398 +14766 14772 14398 14766 14768 14772 14768 14769 14773 +14768 14773 14772 14399 14774 14775 14771 14399 14775 +14393 14399 14771 14774 14776 14775 14777 14400 14772 +14772 14400 14398 14772 14773 14778 14772 14778 14777 +14402 14779 14774 14402 14774 14399 14776 14774 14780 +14780 14774 14779 14777 14403 14400 14781 14782 14404 +14781 14404 14403 14778 14781 14403 14777 14778 14403 +14783 14782 14781 14784 14785 14786 14785 14787 14779 +14406 14785 14779 14402 14406 14779 14786 14785 14406 +14779 14787 14780 14782 14407 14404 14408 14407 14782 +14783 14408 14782 14788 14408 14783 14789 14790 14411 +14789 14411 14410 14411 14791 14412 14790 14791 14411 +14786 14413 14784 14413 14791 14784 14413 14412 14791 +14413 14786 14406 14788 14792 14408 14408 14792 14414 +14415 14414 14792 14415 14792 14793 14794 14795 14418 +14794 14418 14417 14410 14418 14789 14789 14418 14795 +14793 14796 14415 14415 14796 14419 14420 14419 14796 +14420 14796 14797 14798 14799 14425 14798 14425 14424 +14425 14800 14426 14799 14800 14425 14417 14426 14794 +14794 14426 14800 14797 14801 14420 14420 14801 14427 +14428 14427 14801 14428 14801 14802 14439 14803 14440 +14804 14441 14440 14804 14440 14803 14441 14804 14805 +14806 14441 14805 14442 14441 14806 14424 14442 14798 +14798 14442 14806 14802 14807 14428 14428 14807 14443 +14807 14808 14809 14444 14807 14809 14443 14807 14444 +14810 14445 14444 14810 14444 14809 14811 14446 14445 +14811 14445 14810 14812 14447 14446 14812 14446 14811 +14813 14448 14447 14813 14447 14812 14814 14449 14448 +14814 14448 14813 14814 14450 14449 14454 14815 14803 +14454 14803 14439 14816 14804 14817 14804 14815 14817 +14804 14803 14815 14804 14816 14805 14808 14818 14809 +14819 14820 14818 14820 14809 14818 14820 14810 14809 +14821 14811 14822 14811 14820 14822 14811 14810 14820 +14823 14824 14821 14824 14811 14821 14824 14812 14811 +14812 14825 14813 14812 14824 14825 14813 14826 14814 +14813 14825 14826 14826 14456 14450 14826 14450 14814 +14464 14827 14465 14828 14466 14465 14828 14465 14827 +14828 14829 14466 14466 14829 14454 14829 14830 14454 +14830 14815 14454 14815 14830 14817 14831 14832 14833 +14831 14833 14819 14819 14833 14820 14833 14468 14820 +14468 14467 14820 14834 14835 14470 14470 14835 14467 +14835 14822 14467 14822 14820 14467 14835 14834 14823 +14823 14834 14824 14834 14472 14824 14472 14471 14824 +14824 14471 14475 14824 14475 14825 14825 14475 14477 +14825 14477 14826 14826 14477 14456 14482 14827 14464 +14827 14482 14483 14827 14483 14828 14836 14829 14828 +14483 14836 14828 14484 14836 14483 14484 14485 14836 +14836 14485 14837 14833 14832 14838 14838 14832 14839 +14838 14487 14468 14838 14468 14833 14488 14834 14470 +14834 14488 14472 14840 14837 14485 14840 14485 14491 +14491 14492 14840 14840 14492 14841 14842 14843 14844 +14842 14844 14839 14839 14844 14838 14844 14494 14838 +14494 14493 14838 14838 14493 14487 14845 14841 14492 +14845 14492 14496 14496 14497 14845 14845 14497 14846 +14847 14848 14498 14498 14848 14499 14849 14498 14500 +14849 14847 14498 14850 14500 14501 14850 14849 14500 +14851 14501 14503 14851 14850 14501 14852 14503 14504 +14852 14851 14503 14853 14504 14505 14853 14852 14504 +14506 14844 14843 14853 14506 14843 14505 14506 14853 +14844 14506 14494 14854 14846 14497 14854 14497 14507 +14855 14854 14856 14854 14508 14856 14854 14507 14508 +14509 14856 14508 14857 14858 14510 14510 14858 14511 +14859 14510 14512 14859 14857 14510 14512 14499 14848 +14512 14848 14859 14855 14856 14860 14855 14860 14861 +14514 14860 14509 14509 14860 14856 14862 14863 14515 +14515 14863 14516 14515 14511 14858 14515 14858 14862 +14864 14861 14860 14860 14514 14864 14514 14865 14864 +14514 14517 14865 14863 14518 14516 14866 14518 14863 +14519 14865 14517 14519 14867 14865 14866 14520 14518 +14868 14520 14866 14521 14867 14519 14521 14869 14867 +14868 14870 14520 14520 14870 14523 14524 14523 14870 +14524 14870 14871 14525 14869 14521 14525 14872 14869 +14871 14526 14524 14873 14526 14871 14874 14875 14528 +14874 14528 14527 14525 14528 14872 14872 14528 14875 +14873 14529 14526 14876 14529 14873 14530 14874 14527 +14530 14877 14874 14876 14878 14529 14529 14878 14531 +14532 14531 14878 14532 14878 14879 14533 14877 14530 +14533 14880 14877 14879 14534 14532 14881 14534 14879 +14535 14880 14533 14535 14882 14880 14883 14884 14536 +14536 14884 14537 14536 14534 14881 14536 14881 14883 +14538 14882 14535 14538 14885 14882 14884 14539 14537 +14886 14539 14884 14887 14888 14889 14888 14885 14889 +14885 14540 14889 14885 14538 14540 14886 14890 14539 +14539 14890 14541 14542 14541 14890 14542 14890 14891 +14887 14889 14544 14543 14887 14544 14892 14887 14543 +14544 14889 14540 14891 14545 14542 14893 14545 14891 +14546 14892 14543 14546 14894 14892 14893 14547 14545 +14895 14547 14893 14548 14894 14546 14548 14896 14894 +14895 14549 14547 14897 14549 14895 14550 14896 14548 +14550 14898 14896 14897 14551 14549 14899 14551 14897 +14900 14898 14550 14900 14550 14552 14552 14553 14900 +14900 14553 14901 14899 14554 14551 14902 14554 14899 +14555 14901 14553 14555 14903 14901 14902 14556 14554 +14904 14556 14902 14557 14903 14555 14557 14905 14903 +14904 14558 14556 14906 14558 14904 14559 14905 14557 +14559 14907 14905 14908 14909 14910 14906 14908 14910 +14560 14906 14910 14558 14906 14560 14561 14907 14559 +14561 14911 14907 14910 14909 14562 14909 14563 14562 +14909 14912 14563 14910 14562 14560 14564 14911 14561 +14564 14913 14911 14565 14563 14914 14563 14915 14914 +14563 14912 14915 14914 14566 14565 14568 14913 14564 +14568 14916 14913 14915 14917 14914 14566 14914 14917 +14918 14566 14917 14569 14566 14918 14570 14916 14568 +14570 14919 14916 14918 14571 14569 14920 14571 14918 +14572 14919 14570 14572 14921 14919 14920 14573 14571 +14922 14573 14920 14574 14921 14572 14574 14923 14921 +14922 14575 14573 14924 14575 14922 14576 14925 14926 +14923 14576 14926 14574 14576 14923 14925 14927 14926 +14924 14577 14575 14928 14577 14924 14578 14925 14576 +14578 14579 14925 14579 14927 14925 14579 14929 14927 +14928 14580 14577 14930 14580 14928 14931 14932 14933 +14934 14935 14933 14934 14933 14932 14936 14935 14934 +14937 14938 14939 14940 14941 14939 14940 14939 14938 +14942 14943 14941 14942 14941 14940 14944 14945 14943 +14944 14943 14942 14946 14947 14945 14946 14945 14944 +14948 14949 14947 14948 14947 14946 14950 14951 14949 +14950 14949 14948 14952 14953 14951 14952 14951 14950 +14954 14953 14952 14955 14956 14957 14956 14929 14957 +14929 14581 14957 14929 14579 14581 14930 14582 14580 +14958 14582 14930 14959 14960 14961 14584 14583 14961 +14960 14584 14961 14962 14584 14960 14584 14963 14586 +14962 14963 14584 14933 14587 14931 14587 14963 14931 +14587 14586 14963 14587 14933 14588 14588 14933 14935 +14964 14965 14589 14964 14589 14588 14936 14964 14588 +14935 14936 14588 14965 14964 14937 14965 14937 14939 +14590 14965 14939 14591 14590 14939 14591 14939 14593 +14593 14939 14941 14593 14941 14595 14595 14941 14943 +14595 14943 14597 14597 14943 14945 14597 14945 14599 +14599 14945 14947 14599 14947 14601 14601 14947 14949 +14601 14949 14603 14603 14949 14951 14603 14951 14605 +14605 14951 14953 14966 14967 14607 14966 14607 14605 +14954 14966 14605 14953 14954 14605 14968 14967 14966 +14955 14957 14969 14955 14969 14970 14608 14969 14581 +14581 14969 14957 14971 14609 14972 14609 14958 14972 +14609 14582 14958 14973 14971 14972 14974 14975 14976 +14977 14978 14976 14977 14976 14975 14978 14977 14959 +14978 14959 14961 14610 14978 14961 14611 14610 14961 +14611 14961 14583 14965 14612 14589 14612 14965 14590 +14967 14613 14607 14968 14979 14967 14979 14613 14967 +14979 14614 14613 14980 14981 14615 14614 14980 14615 +14979 14980 14614 14982 14981 14980 14983 14984 14985 +14985 14984 14970 14985 14970 14969 14616 14985 14969 +14617 14616 14969 14617 14969 14608 14986 14619 14971 +14971 14619 14609 14971 14973 14987 14971 14987 14986 +14988 14989 14990 14621 14620 14990 14989 14621 14990 +14991 14621 14989 14976 14623 14974 14623 14991 14974 +14623 14621 14991 14623 14976 14625 14625 14976 14978 +14625 14978 14610 14981 14626 14615 14982 14992 14981 +14992 14626 14981 14992 14627 14626 14627 14993 14629 +14992 14993 14627 14985 14631 14983 14631 14993 14983 +14631 14629 14993 14631 14985 14616 14986 14633 14619 +14987 14994 14986 14994 14633 14986 14994 14634 14633 +14636 14634 14994 14636 14994 14995 14996 14997 14998 +14998 14997 14988 14998 14988 14990 14637 14998 14990 +14638 14637 14990 14638 14990 14620 14995 14999 14636 +14636 14999 14640 14998 14641 14996 14641 14999 14996 +14641 14640 14999 14641 14998 14637 15000 14698 14697 +14699 15001 14697 14697 15001 15000 14700 15002 14699 +14699 15002 15001 14701 15003 14700 14700 15003 15002 +14702 15003 14701 15004 15005 15006 15006 15007 15004 +15000 14708 14698 14708 15000 14710 15000 15008 14710 +15000 15001 15008 15009 15001 15002 15008 15001 15009 +15010 15002 15003 15009 15002 15010 14711 15011 14702 +15011 15003 14702 15011 15010 15003 14712 15012 14711 +14711 15012 15011 14713 15012 14712 15013 14716 14717 +15013 14717 15014 14717 15015 15014 14717 14718 15015 +15016 14718 14719 15016 15015 14718 14720 15006 15005 +15016 14720 15005 14719 14720 15016 15017 15018 14721 +14721 15018 14720 15018 15007 14720 15007 15006 14720 +15019 15020 15017 15017 15020 15018 15021 15022 15019 +15019 15022 15020 15023 15024 15021 15021 15024 15022 +15023 15025 15024 15026 14725 15008 15008 14725 14710 +15009 15027 15028 15026 15009 15028 15008 15009 15026 +15027 15009 15010 15027 15010 15029 15029 15010 15011 +15029 15011 15030 15031 15032 15030 15011 15031 15030 +15012 15031 15011 14726 15033 14713 15033 15012 14713 +15033 15031 15012 14727 15034 14726 14726 15034 15033 +14728 15035 14727 14727 15035 15034 14729 15036 14728 +14728 15036 15035 14730 15037 14729 14729 15037 15036 +15037 14730 15038 14730 15039 15038 14730 14731 15039 +14731 14716 15013 15040 14731 15013 15039 14731 15040 +15040 15013 15014 14733 15017 14721 15017 14733 14735 +15017 14735 15019 15019 14735 14737 15019 14737 15021 +15021 14737 14738 15041 15021 14738 15023 15021 15041 +15025 15023 15042 15023 15043 15042 15023 15041 15043 +15044 15045 15043 15043 15045 15042 15046 15047 15044 +15044 15047 15045 15046 15048 15047 15049 14741 15026 +15026 14741 14725 15026 15028 15050 15026 15050 15049 +15032 15031 15051 15032 15051 15052 15053 15031 15033 +15051 15031 15053 15054 15033 15034 15053 15033 15054 +15055 15034 15035 15054 15034 15055 15056 15035 15036 +15055 15035 15056 15037 15057 15058 15056 15037 15058 +15036 15037 15056 15057 15037 15038 14742 15041 14738 +15041 14742 14743 15041 14743 15043 15043 14743 14744 +15043 14744 15044 15044 14744 14745 15044 14745 15046 +15059 15048 15046 14745 15059 15046 14746 15059 14745 +15060 14746 14747 15060 15059 14746 15061 14747 14748 +15061 15060 14747 15062 15061 15063 15061 14749 15063 +15061 14748 14749 14750 15063 14749 15049 14752 14741 +15064 15065 14754 15064 14754 14752 15050 15064 14752 +15049 15050 14752 15066 15065 15064 15067 15052 15051 +15067 15051 15053 15067 15053 15068 15068 15053 15054 +15068 15054 15069 15069 15054 15055 15069 15055 15070 +15070 15055 15056 15070 15056 15071 15071 15056 15058 +15072 15062 15063 15072 15063 14750 14755 15072 14750 +15073 15072 14755 15074 14755 14756 15074 15073 14755 +15075 14756 14757 15075 15074 14756 15076 14757 14758 +15076 15075 14757 15077 14758 14760 15077 15076 14758 +15078 14760 14762 15078 15077 14760 14762 14763 15078 +15078 14763 15079 15065 14765 14754 15066 15080 15065 +15080 14765 15065 15080 14767 14765 14769 14767 15080 +14769 15080 15081 15082 15079 14763 15082 14763 14770 +14770 14771 15082 15082 14771 15083 15084 14773 15085 +14773 15081 15085 14773 14769 15081 15086 15084 15085 +15087 15083 14771 15087 14771 14775 14775 14776 15087 +15087 14776 15088 15089 14778 15084 15084 14778 14773 +15084 15086 15090 15084 15090 15089 15091 15092 15093 +15092 15088 15093 15088 14780 15093 15088 14776 14780 +15089 14781 14778 14783 14781 15089 15090 14783 15089 +15094 14783 15090 15095 15096 15097 14785 14784 15097 +15096 14785 15097 15098 14785 15096 15093 14787 15091 +14787 15098 15091 14787 14785 15098 14787 15093 14780 +15099 14788 15100 14788 15094 15100 14788 14783 15094 +15101 15099 15100 15102 15103 14790 15102 14790 14789 +15097 14791 15095 14791 15103 15095 14791 14790 15103 +14791 15097 14784 15099 14792 14788 14793 14792 15099 +15101 14793 15099 15104 14793 15101 15105 15106 14795 +15105 14795 14794 14789 14795 15102 15102 14795 15106 +15104 15107 14793 14793 15107 14796 14797 14796 15107 +14797 15107 15108 15109 15110 14799 15109 14799 14798 +14799 15111 14800 15110 15111 14799 14794 14800 15105 +15105 14800 15111 15108 15112 14797 14797 15112 14801 +14802 14801 15112 14802 15112 15113 15114 15115 14806 +15114 14806 14805 14798 14806 15109 15109 14806 15115 +15113 15116 14802 14802 15116 14807 14808 14807 15116 +14808 15116 15117 14817 15118 14816 15118 15119 14816 +15119 14805 14816 15119 15114 14805 15117 15120 14808 +14808 15120 14818 14819 14818 15120 14819 15120 15121 +14822 15122 14821 15122 14823 14821 14829 15123 14830 +15118 14817 14830 15123 15118 14830 15124 15118 15123 +15119 15118 15125 15125 15118 15124 15126 15127 14831 +14831 15127 14832 14831 14819 15121 14831 15121 15126 +14835 15122 14822 15122 14835 14823 14836 15123 14829 +15124 15123 15128 15123 14837 15128 14837 15129 15128 +14837 15123 14836 15124 15128 15125 15130 15131 15132 +15127 15130 15132 14839 15127 15132 14832 15127 14839 +15133 15129 14837 15133 14837 14840 14840 14841 15133 +15133 14841 15134 15135 15136 15137 15135 15137 15131 +15131 15137 15132 15137 14843 15132 14843 14842 15132 +15132 14842 14839 15138 15134 14841 15138 14841 14845 +14845 14846 15138 15138 14846 15139 15140 14848 14847 +15140 14847 15141 14847 15142 15141 14847 14849 15142 +15143 15142 15144 15142 14850 15144 15142 14849 14850 +15144 14850 15145 14850 15146 15145 14850 14851 15146 +15147 14851 14852 15147 15146 14851 14853 15137 15136 +15147 14853 15136 14852 14853 15147 15137 14853 14843 +14846 14854 15148 15149 14846 15148 15139 14846 15149 +14855 15148 14854 15150 14858 14857 15150 14857 15151 +14857 15152 15151 14857 14859 15152 14859 14848 15140 +15153 14859 15140 15152 14859 15153 15153 15140 15141 +15154 15143 15144 15154 15144 15145 15155 15149 15148 +15148 14855 15155 14855 15156 15155 14855 14861 15156 +15157 14863 14862 15158 15157 14862 15158 14862 14858 +15159 15158 14858 15150 15159 14858 15159 15150 15151 +15160 15156 14861 15160 14861 14864 14864 14865 15160 +15160 14865 15161 15162 15163 15157 15163 14863 15157 +15163 14866 14863 15162 15157 15158 14867 15161 14865 +14867 15164 15161 15163 14868 14866 15165 14868 15163 +14869 15164 14867 14869 15166 15164 15165 15167 14868 +14868 15167 14870 14871 14870 15167 14871 15167 15168 +14872 15166 14869 14872 15169 15166 15168 14873 14871 +15170 14873 15168 15171 15172 14875 15171 14875 14874 +14872 14875 15169 15169 14875 15172 15170 14876 14873 +15173 14876 15170 14877 15171 14874 14877 15174 15171 +15173 15175 14876 14876 15175 14878 14879 14878 15175 +14879 15175 15176 14880 15174 14877 14880 15177 15174 +15178 15179 15180 15176 15178 15180 14881 15176 15180 +14879 15176 14881 14882 15177 14880 14882 15181 15177 +15180 15179 14883 15179 14884 14883 15179 15182 14884 +15180 14883 14881 14885 15181 14882 14885 15183 15181 +15182 14886 14884 15184 14886 15182 15185 15186 14888 +15185 14888 14887 14885 14888 15183 15183 14888 15186 +15184 15187 14886 14886 15187 14890 14891 14890 15187 +14891 15187 15188 14892 15185 14887 14892 15189 15185 +15188 14893 14891 15190 14893 15188 14894 15189 14892 +14894 15191 15189 15190 14895 14893 15192 14895 15190 +14896 15191 14894 14896 15193 15191 15192 14897 14895 +15194 14897 15192 14898 15193 14896 14898 15195 15193 +15194 14899 14897 15196 14899 15194 15197 15195 14898 +15197 14898 14900 14900 14901 15197 15197 14901 15198 +15196 14902 14899 15199 14902 15196 14903 15198 14901 +14903 15200 15198 15199 14904 14902 15201 14904 15199 +14905 15200 14903 14905 15202 15200 15201 14906 14904 +15203 14906 15201 14907 15202 14905 14907 15204 15202 +15205 15206 14908 14908 15206 14909 14908 14906 15203 +14908 15203 15205 14911 15204 14907 14911 15207 15204 +15206 14912 14909 15208 14912 15206 14913 15207 14911 +14913 15209 15207 15208 14915 14912 15210 14915 15208 +14916 15209 14913 14916 15211 15209 15210 15212 14915 +14915 15212 14917 14918 14917 15212 14918 15212 15213 +14919 15211 14916 14919 15214 15211 15213 14920 14918 +15215 14920 15213 14921 15214 14919 14921 15216 15214 +15215 14922 14920 15217 14922 15215 14923 15216 14921 +14923 15218 15216 15217 14924 14922 15219 14924 15217 +14923 14926 15220 15221 14923 15220 15218 14923 15221 +14927 15220 14926 15219 14928 14924 15222 14928 15219 +15223 15224 15225 15226 15225 15224 15227 15228 15229 +15230 15231 15229 15230 15229 15228 15232 15233 15231 +15232 15231 15230 15234 15235 15233 15234 15233 15232 +15236 15237 15235 15236 15235 15234 15238 15239 15237 +15238 15237 15236 15240 15239 15238 15221 15220 15241 +15221 15241 15242 14929 15241 14927 14927 15241 15220 +15222 14930 14928 15243 14930 15222 15244 15245 15246 +15247 15248 15246 15247 15246 15245 14932 14931 15248 +15247 14932 15248 15249 14932 15247 15225 14934 15223 +14934 15249 15223 14934 14932 15249 15250 15251 14936 +15250 14936 14934 15226 15250 14934 15225 15226 14934 +14938 14937 15251 15250 14938 15251 15252 14938 15250 +15229 14940 15227 14940 15252 15227 14940 14938 15252 +14940 15229 14942 14942 15229 15231 14942 15231 14944 +14944 15231 15233 14944 15233 14946 14946 15233 15235 +14946 15235 14948 14948 15235 15237 14948 15237 14950 +14950 15237 15239 15240 15253 15239 15253 14950 15239 +15253 14952 14950 15254 15255 14954 14952 15254 14954 +15253 15254 14952 15256 15255 15254 15242 15241 14956 +14955 15242 14956 15257 15242 14955 14956 15241 14929 +15258 14958 15259 14958 15243 15259 14958 14930 15243 +15260 15258 15259 15261 15262 15263 14960 14959 15263 +15262 14960 15263 15264 14960 15262 15246 14962 15244 +14962 15264 15244 14962 14960 15264 14962 15246 14963 +14963 15246 15248 14963 15248 14931 15251 14964 14936 +14964 15251 14937 15255 14966 14954 15265 15266 14968 +15265 14968 14966 15256 15265 14966 15255 15256 14966 +15267 15266 15265 15268 15269 15270 15269 15257 15270 +15257 14970 15270 15257 14955 14970 15271 14972 15272 +14972 14958 15272 15272 14958 15258 15273 15272 15258 +15258 15260 15273 14972 15271 14973 15274 15275 14975 +15274 14975 14974 15263 14977 15261 14977 15275 15261 +14977 14975 15275 14977 15263 14959 15266 14979 14968 +15267 15276 15266 15276 14979 15266 15276 14980 14979 +15277 15278 14982 14980 15277 14982 15276 15277 14980 +15279 15280 15278 15279 15278 15277 15280 15279 15268 +15280 15268 15270 14983 15280 15270 14984 14983 15270 +14984 15270 14970 15272 15281 15271 14987 14973 15271 +15282 14987 15271 15281 15282 15271 14987 15282 15283 +15284 15283 15282 15285 15286 15287 14989 14988 15287 +15286 14989 15287 15288 14989 15286 14989 15289 14991 +15288 15289 14989 14974 14991 15274 15274 14991 15289 +15278 14992 14982 14992 15278 14993 14993 15278 15280 +14993 15280 14983 15283 14994 14987 15290 15291 14995 +15290 14995 14994 15284 15290 14994 15283 15284 14994 +15291 15290 15285 15291 15285 15287 14996 15291 15287 +14997 14996 15287 14997 15287 14988 15291 14999 14995 +14999 15291 14996 15292 15293 15294 15295 15296 15294 +15294 15296 15292 15297 15298 15295 15295 15298 15296 +15299 15300 15297 15297 15300 15298 15299 15301 15300 +15302 15303 15304 15005 15004 15304 15004 15302 15304 +15004 15305 15302 15306 15307 15007 15307 15004 15007 +15307 15305 15004 15308 15309 15306 15306 15309 15307 +15309 15308 15293 15308 15294 15293 15308 15310 15294 +15294 15311 15295 15294 15310 15311 15295 15312 15297 +15295 15311 15312 15297 15313 15299 15297 15312 15313 +15301 15299 15314 15314 15299 15313 15315 15316 15015 +15015 15316 15014 15016 15304 15303 15315 15016 15303 +15015 15016 15315 15304 15016 15005 15018 15306 15007 +15306 15018 15020 15306 15020 15308 15308 15020 15022 +15308 15022 15310 15310 15022 15024 15310 15024 15311 +15311 15024 15025 15317 15311 15025 15312 15311 15317 +15312 15318 15313 15312 15317 15318 15314 15313 15319 +15313 15320 15319 15313 15318 15320 15320 15321 15319 +15322 15028 15027 15029 15323 15027 15027 15323 15322 +15030 15324 15029 15029 15324 15323 15032 15324 15030 +15325 15038 15039 15325 15039 15326 15039 15327 15326 +15039 15040 15327 15040 15014 15316 15040 15316 15327 +15042 15317 15025 15317 15042 15045 15317 15045 15318 +15318 15045 15047 15318 15047 15320 15328 15329 15048 +15048 15329 15047 15329 15321 15047 15321 15320 15047 +15330 15331 15328 15328 15331 15329 15332 15333 15330 +15330 15333 15331 15332 15334 15333 15335 15050 15322 +15322 15050 15028 15336 15322 15323 15335 15322 15336 +15337 15323 15324 15336 15323 15337 15052 15337 15032 +15032 15337 15324 15338 15058 15057 15339 15338 15057 +15339 15057 15038 15340 15339 15038 15325 15340 15038 +15340 15325 15326 15059 15328 15048 15328 15059 15060 +15328 15060 15330 15330 15060 15061 15330 15061 15332 +15341 15342 15062 15062 15342 15061 15342 15334 15061 +15334 15332 15061 15341 15343 15342 15335 15064 15050 +15064 15335 15066 15335 15344 15066 15335 15336 15344 +15345 15336 15337 15344 15336 15345 15067 15346 15052 +15346 15337 15052 15346 15345 15337 15068 15347 15067 +15067 15347 15346 15069 15348 15068 15068 15348 15347 +15070 15349 15069 15069 15349 15348 15071 15350 15070 +15070 15350 15349 15351 15350 15071 15351 15071 15058 +15352 15351 15058 15338 15352 15058 15352 15338 15339 +15072 15341 15062 15353 15343 15341 15072 15353 15341 +15073 15353 15072 15354 15073 15074 15354 15353 15073 +15355 15074 15075 15355 15354 15074 15356 15075 15076 +15356 15355 15075 15357 15076 15077 15357 15356 15076 +15358 15077 15078 15358 15357 15077 15078 15079 15358 +15358 15079 15359 15344 15080 15066 15080 15344 15081 +15344 15360 15081 15344 15345 15360 15361 15345 15346 +15360 15345 15361 15362 15346 15347 15361 15346 15362 +15363 15347 15348 15362 15347 15363 15349 15364 15365 +15363 15349 15365 15348 15349 15363 15364 15349 15350 +15364 15350 15366 15366 15350 15351 15367 15359 15079 +15367 15079 15082 15082 15083 15367 15367 15083 15368 +15360 15085 15081 15360 15361 15369 15086 15360 15369 +15370 15086 15369 15360 15086 15085 15369 15361 15362 +15369 15362 15371 15371 15362 15363 15371 15363 15372 +15372 15363 15365 15083 15087 15373 15374 15083 15373 +15368 15083 15374 15088 15373 15087 15370 15090 15086 +15375 15090 15370 15376 15377 15378 15379 15378 15377 +15374 15373 15092 15091 15374 15092 15380 15374 15091 +15092 15373 15088 15381 15094 15382 15094 15375 15382 +15094 15090 15375 15383 15381 15382 15384 15385 15386 +15386 15385 15376 15386 15376 15378 15095 15386 15378 +15096 15095 15378 15379 15387 15378 15387 15096 15378 +15387 15098 15096 15091 15098 15380 15380 15098 15387 +15381 15100 15094 15101 15100 15381 15383 15101 15381 +15388 15101 15383 15389 15390 15391 15391 15390 15384 +15391 15384 15386 15102 15391 15386 15103 15102 15386 +15103 15386 15095 15392 15104 15393 15104 15388 15393 +15104 15101 15388 15394 15392 15393 15395 15396 15397 +15397 15396 15389 15397 15389 15391 15105 15397 15391 +15106 15105 15391 15106 15391 15102 15392 15107 15104 +15108 15107 15392 15394 15108 15392 15398 15108 15394 +15399 15400 15110 15399 15110 15109 15397 15111 15395 +15111 15400 15395 15111 15110 15400 15111 15397 15105 +15398 15401 15108 15108 15401 15112 15113 15112 15401 +15113 15401 15402 15403 15404 15115 15403 15115 15114 +15109 15115 15399 15399 15115 15404 15402 15405 15113 +15113 15405 15116 15117 15116 15405 15117 15405 15406 +15119 15403 15114 15119 15407 15403 15406 15408 15117 +15117 15408 15120 15121 15120 15408 15121 15408 15409 +15125 15407 15119 15125 15410 15407 15411 15412 15126 +15126 15412 15127 15126 15121 15409 15126 15409 15411 +15413 15414 15128 15413 15128 15129 15125 15128 15410 +15410 15128 15414 15415 15416 15130 15130 15416 15131 +15130 15127 15412 15130 15412 15415 15417 15413 15129 +15417 15129 15133 15133 15134 15417 15417 15134 15418 +15419 15420 15135 15135 15420 15136 15135 15131 15416 +15135 15416 15419 15421 15418 15134 15421 15134 15138 +15138 15139 15421 15421 15139 15422 15423 15424 15142 +15142 15424 15141 15142 15143 15423 15423 15143 15425 +15426 15427 15146 15146 15427 15145 15428 15146 15147 +15428 15426 15146 15147 15136 15420 15147 15420 15428 +15149 15422 15139 15149 15429 15422 15430 15431 15152 +15152 15431 15151 15432 15152 15153 15432 15430 15152 +15153 15141 15424 15153 15424 15432 15433 15425 15143 +15433 15143 15154 15154 15145 15427 15154 15427 15433 +15434 15429 15149 15434 15149 15155 15155 15156 15434 +15434 15156 15435 15436 15437 15159 15159 15437 15158 +15159 15151 15431 15159 15431 15436 15156 15160 15438 +15439 15156 15438 15435 15156 15439 15161 15438 15160 +15440 15163 15162 15441 15440 15162 15158 15441 15162 +15437 15441 15158 15442 15439 15438 15438 15161 15442 +15161 15443 15442 15161 15164 15443 15444 15165 15440 +15440 15165 15163 15440 15441 15445 15440 15445 15444 +15166 15443 15164 15166 15446 15443 15444 15167 15165 +15168 15167 15444 15445 15168 15444 15447 15168 15445 +15169 15446 15166 15169 15448 15446 15447 15170 15168 +15449 15170 15447 15450 15451 15172 15450 15172 15171 +15169 15172 15448 15448 15172 15451 15449 15173 15170 +15452 15173 15449 15174 15450 15171 15174 15453 15450 +15452 15454 15173 15173 15454 15175 15176 15175 15454 +15176 15454 15455 15177 15453 15174 15177 15456 15453 +15457 15458 15178 15178 15458 15179 15178 15176 15455 +15178 15455 15457 15181 15456 15177 15181 15459 15456 +15458 15182 15179 15460 15182 15458 15183 15459 15181 +15183 15461 15459 15460 15184 15182 15462 15184 15460 +15463 15464 15186 15463 15186 15185 15183 15186 15461 +15461 15186 15464 15462 15465 15184 15184 15465 15187 +15188 15187 15465 15188 15465 15466 15189 15463 15185 +15189 15467 15463 15466 15190 15188 15468 15190 15466 +15191 15467 15189 15191 15469 15467 15468 15192 15190 +15470 15192 15468 15193 15469 15191 15193 15471 15469 +15470 15194 15192 15472 15194 15470 15195 15471 15193 +15195 15473 15471 15472 15196 15194 15474 15196 15472 +15475 15473 15195 15475 15195 15197 15197 15198 15475 +15475 15198 15476 15474 15199 15196 15477 15199 15474 +15200 15476 15198 15200 15478 15476 15477 15201 15199 +15479 15201 15477 15202 15478 15200 15202 15480 15478 +15479 15203 15201 15481 15203 15479 15204 15480 15202 +15204 15482 15480 15483 15484 15205 15205 15484 15206 +15205 15203 15481 15205 15481 15483 15207 15482 15204 +15207 15485 15482 15484 15208 15206 15486 15208 15484 +15209 15485 15207 15209 15487 15485 15486 15210 15208 +15488 15210 15486 15211 15487 15209 15211 15489 15487 +15488 15490 15210 15210 15490 15212 15213 15212 15490 +15213 15490 15491 15214 15489 15211 15214 15492 15489 +15491 15215 15213 15493 15215 15491 15216 15492 15214 +15216 15494 15492 15493 15217 15215 15495 15217 15493 +15218 15494 15216 15218 15496 15494 15495 15219 15217 +15497 15219 15495 15498 15499 15500 15501 15502 15500 +15501 15500 15499 15503 15504 15502 15503 15502 15501 +15505 15504 15503 15221 15496 15218 15221 15506 15496 +15497 15222 15219 15507 15222 15497 15508 15509 15510 +15511 15512 15510 15511 15510 15509 15224 15223 15512 +15511 15224 15512 15513 15224 15511 15514 15515 15226 +15224 15514 15226 15513 15514 15224 15516 15517 15515 +15516 15515 15514 15228 15227 15517 15516 15228 15517 +15518 15228 15516 15500 15230 15498 15230 15518 15498 +15230 15228 15518 15230 15500 15232 15232 15500 15502 +15232 15502 15234 15234 15502 15504 15505 15519 15504 +15519 15234 15504 15519 15236 15234 15236 15520 15238 +15519 15520 15236 15521 15522 15240 15238 15521 15240 +15520 15521 15238 15523 15524 15522 15523 15522 15521 +15525 15524 15523 15242 15506 15221 15242 15526 15506 +15507 15243 15222 15527 15243 15507 15528 15529 15245 +15528 15245 15244 15510 15247 15508 15247 15529 15508 +15247 15245 15529 15247 15510 15249 15249 15510 15512 +15249 15512 15223 15515 15250 15226 15250 15515 15252 +15252 15515 15517 15252 15517 15227 15522 15253 15240 +15253 15522 15254 15254 15522 15524 15256 15254 15524 +15525 15256 15524 15530 15256 15525 15531 15532 15533 +15532 15526 15533 15526 15257 15533 15526 15242 15257 +15527 15534 15243 15243 15534 15259 15260 15259 15534 +15260 15534 15535 15536 15537 15262 15536 15262 15261 +15262 15538 15264 15537 15538 15262 15244 15264 15528 +15528 15264 15538 15530 15539 15256 15256 15539 15265 +15540 15541 15267 15265 15540 15267 15539 15540 15265 +15542 15543 15541 15542 15541 15540 15544 15545 15543 +15544 15543 15542 15545 15544 15531 15545 15531 15533 +15268 15545 15533 15269 15268 15533 15269 15533 15257 +15546 15272 15273 15547 15546 15273 15260 15547 15273 +15535 15547 15260 15548 15549 15550 15551 15552 15550 +15551 15550 15549 15275 15274 15552 15551 15275 15552 +15553 15275 15551 15261 15275 15536 15536 15275 15553 +15541 15276 15267 15276 15541 15277 15277 15541 15543 +15277 15543 15279 15279 15543 15545 15279 15545 15268 +15546 15281 15272 15547 15554 15546 15554 15281 15546 +15554 15282 15281 15555 15556 15284 15282 15555 15284 +15554 15555 15282 15286 15285 15556 15555 15286 15556 +15557 15286 15555 15550 15288 15548 15288 15557 15548 +15288 15286 15557 15288 15550 15289 15289 15550 15552 +15289 15552 15274 15556 15290 15284 15290 15556 15285 +15558 15559 15560 15560 15561 15558 15562 15563 15564 +15562 15564 15559 15564 15560 15559 15564 15565 15560 +15561 15560 15566 15560 15567 15566 15560 15565 15567 +15567 15568 15566 15569 15570 15571 15572 15573 15571 +15571 15573 15569 15574 15575 15572 15572 15575 15573 +15575 15574 15563 15574 15564 15563 15574 15576 15564 +15564 15577 15565 15564 15576 15577 15565 15578 15567 +15565 15577 15578 15568 15567 15579 15567 15580 15579 +15567 15578 15580 15580 15581 15579 15582 15583 15584 +15582 15584 15570 15584 15571 15570 15584 15585 15571 +15571 15586 15572 15571 15585 15586 15572 15587 15574 +15572 15586 15587 15574 15588 15576 15574 15587 15588 +15576 15589 15577 15576 15588 15589 15577 15590 15578 +15577 15589 15590 15578 15591 15580 15578 15590 15591 +15581 15580 15592 15592 15580 15591 15593 15594 15595 +15596 15597 15595 15595 15597 15593 15597 15596 15583 +15583 15596 15584 15596 15293 15584 15293 15292 15584 +15584 15292 15296 15584 15296 15585 15585 15296 15298 +15585 15298 15586 15586 15298 15300 15586 15300 15587 +15587 15300 15301 15598 15587 15301 15588 15587 15598 +15588 15599 15589 15588 15598 15599 15589 15600 15590 +15589 15599 15600 15590 15601 15591 15590 15600 15601 +15592 15591 15602 15602 15591 15601 15603 15604 15302 +15302 15604 15303 15605 15302 15305 15605 15603 15302 +15307 15595 15594 15605 15307 15594 15305 15307 15605 +15595 15307 15309 15595 15309 15596 15596 15309 15293 +15314 15606 15598 15314 15598 15301 15598 15607 15599 +15598 15606 15607 15599 15608 15600 15599 15607 15608 +15600 15609 15601 15600 15608 15609 15602 15601 15610 +15610 15601 15609 15611 15612 15315 15315 15612 15316 +15315 15303 15604 15315 15604 15611 15319 15606 15314 +15606 15319 15321 15613 15606 15321 15607 15606 15613 +15607 15614 15608 15607 15613 15614 15608 15615 15609 +15608 15614 15615 15610 15609 15616 15616 15609 15615 +15617 15618 15327 15327 15618 15326 15327 15316 15612 +15327 15612 15617 15329 15613 15321 15613 15329 15331 +15613 15331 15614 15614 15331 15333 15614 15333 15615 +15333 15334 15615 15334 15616 15615 15334 15619 15616 +15620 15621 15340 15340 15621 15339 15340 15326 15618 +15340 15618 15620 15622 15619 15334 15622 15334 15342 +15342 15343 15622 15622 15343 15623 15624 15351 15352 +15625 15624 15352 15339 15625 15352 15621 15625 15339 +15626 15623 15343 15626 15343 15353 15627 15353 15354 +15627 15626 15353 15628 15627 15629 15627 15355 15629 +15627 15354 15355 15356 15630 15355 15355 15630 15629 +15357 15631 15356 15356 15631 15630 15358 15632 15357 +15357 15632 15631 15359 15632 15358 15633 15365 15364 +15366 15634 15364 15364 15634 15633 15635 15634 15366 +15635 15366 15351 15636 15635 15351 15624 15636 15351 +15636 15624 15625 15637 15628 15629 15637 15629 15630 +15637 15630 15638 15639 15640 15638 15630 15639 15638 +15631 15639 15630 15641 15631 15632 15639 15631 15641 +15367 15642 15359 15642 15632 15359 15642 15641 15632 +15368 15642 15367 15643 15370 15369 15371 15644 15369 +15369 15644 15643 15644 15371 15645 15371 15646 15645 +15371 15372 15646 15372 15365 15633 15647 15372 15633 +15646 15372 15647 15647 15633 15634 15647 15634 15648 +15648 15634 15635 15640 15639 15649 15640 15649 15650 +15651 15639 15641 15649 15639 15651 15652 15641 15642 +15651 15641 15652 15374 15652 15368 15368 15652 15642 +15653 15375 15643 15643 15375 15370 15644 15654 15655 +15653 15644 15655 15643 15644 15653 15654 15644 15645 +15656 15657 15658 15658 15657 15650 15649 15658 15650 +15659 15658 15649 15376 15659 15377 15659 15651 15377 +15659 15649 15651 15377 15651 15379 15651 15660 15379 +15651 15652 15660 15380 15660 15374 15374 15660 15652 +15653 15382 15375 15383 15382 15653 15655 15383 15653 +15661 15383 15655 15662 15663 15664 15664 15663 15656 +15658 15664 15656 15665 15664 15658 15384 15665 15385 +15665 15659 15385 15665 15658 15659 15385 15659 15376 +15660 15387 15379 15387 15660 15380 15666 15388 15667 +15388 15661 15667 15388 15383 15661 15668 15666 15667 +15662 15664 15669 15662 15669 15670 15389 15669 15390 +15669 15665 15390 15669 15664 15665 15390 15665 15384 +15666 15393 15388 15671 15672 15394 15671 15394 15393 +15668 15671 15393 15666 15668 15393 15673 15672 15671 +15674 15675 15676 15676 15675 15670 15676 15670 15669 +15395 15676 15669 15396 15395 15669 15396 15669 15389 +15677 15398 15672 15672 15398 15394 15672 15673 15678 +15672 15678 15677 15679 15680 15681 15681 15680 15674 +15681 15674 15676 15399 15681 15676 15400 15399 15676 +15400 15676 15395 15677 15401 15398 15682 15683 15402 +15682 15402 15401 15678 15682 15401 15677 15678 15401 +15684 15683 15682 15679 15681 15404 15403 15679 15404 +15685 15679 15403 15404 15681 15399 15683 15405 15402 +15406 15405 15683 15684 15406 15683 15686 15406 15684 +15407 15685 15403 15407 15687 15685 15686 15688 15406 +15406 15688 15408 15689 15690 15409 15408 15689 15409 +15688 15689 15408 15691 15690 15689 15410 15687 15407 +15410 15692 15687 15693 15412 15411 15409 15690 15411 +15690 15693 15411 15690 15694 15693 15690 15691 15695 +15690 15695 15694 15696 15697 15414 15696 15414 15413 +15410 15414 15692 15692 15414 15697 15698 15416 15415 +15699 15698 15415 15699 15415 15412 15700 15699 15412 +15693 15700 15412 15700 15693 15694 15700 15694 15701 +15701 15694 15695 15702 15696 15413 15702 15413 15417 +15417 15418 15702 15702 15418 15703 15704 15705 15419 +15419 15705 15420 15419 15416 15698 15706 15419 15698 +15704 15419 15706 15706 15698 15699 15707 15703 15418 +15707 15418 15421 15421 15422 15707 15707 15422 15708 +15709 15424 15423 15425 15709 15423 15710 15427 15426 +15710 15426 15711 15426 15712 15711 15426 15428 15712 +15428 15420 15705 15428 15705 15712 15713 15714 15715 +15714 15708 15715 15708 15429 15715 15708 15422 15429 +15716 15431 15430 15716 15430 15717 15430 15718 15717 +15430 15432 15718 15432 15424 15709 15719 15432 15709 +15718 15432 15719 15719 15709 15425 15433 15719 15425 +15720 15719 15433 15433 15427 15710 15721 15433 15710 +15720 15433 15721 15721 15710 15711 15722 15713 15715 +15723 15722 15724 15724 15722 15434 15722 15715 15434 +15715 15429 15434 15435 15724 15434 15725 15726 15436 +15436 15726 15437 15436 15431 15716 15727 15436 15716 +15725 15436 15727 15727 15716 15717 15728 15723 15724 +15724 15435 15728 15435 15729 15728 15435 15439 15729 +15726 15441 15437 15730 15441 15726 15731 15729 15439 +15731 15439 15442 15442 15443 15731 15731 15443 15732 +15730 15445 15441 15733 15445 15730 15446 15732 15443 +15446 15734 15732 15733 15447 15445 15735 15447 15733 +15448 15734 15446 15448 15736 15734 15735 15449 15447 +15737 15449 15735 15738 15739 15451 15738 15451 15450 +15448 15451 15736 15736 15451 15739 15737 15452 15449 +15740 15452 15737 15453 15738 15450 15453 15741 15738 +15740 15742 15452 15452 15742 15454 15455 15454 15742 +15455 15742 15743 15456 15741 15453 15456 15744 15741 +15745 15746 15457 15457 15746 15458 15457 15455 15743 +15457 15743 15745 15459 15744 15456 15459 15747 15744 +15746 15460 15458 15748 15460 15746 15749 15750 15751 +15750 15747 15751 15747 15461 15751 15747 15459 15461 +15748 15462 15460 15752 15462 15748 15749 15751 15464 +15463 15749 15464 15753 15749 15463 15464 15751 15461 +15752 15754 15462 15462 15754 15465 15466 15465 15754 +15466 15754 15755 15467 15753 15463 15467 15756 15753 +15755 15468 15466 15757 15468 15755 15469 15756 15467 +15469 15758 15756 15757 15470 15468 15759 15470 15757 +15471 15758 15469 15471 15760 15758 15759 15472 15470 +15761 15472 15759 15473 15760 15471 15473 15762 15760 +15761 15474 15472 15763 15474 15761 15764 15762 15473 +15764 15473 15475 15475 15476 15764 15764 15476 15765 +15763 15477 15474 15766 15477 15763 15478 15765 15476 +15478 15767 15765 15766 15479 15477 15768 15479 15766 +15480 15767 15478 15480 15769 15767 15770 15771 15772 +15768 15770 15772 15481 15768 15772 15479 15768 15481 +15482 15769 15480 15482 15773 15769 15772 15771 15483 +15771 15484 15483 15771 15774 15484 15772 15483 15481 +15485 15773 15482 15485 15775 15773 15774 15486 15484 +15776 15486 15774 15487 15775 15485 15487 15777 15775 +15776 15488 15486 15778 15488 15776 15489 15777 15487 +15489 15779 15777 15778 15780 15488 15488 15780 15490 +15491 15490 15780 15491 15780 15781 15492 15779 15489 +15492 15782 15779 15781 15493 15491 15783 15493 15781 +15494 15782 15492 15494 15784 15782 15783 15495 15493 +15785 15495 15783 15496 15784 15494 15496 15786 15784 +15785 15497 15495 15787 15497 15785 15788 15789 15790 +15791 15792 15790 15791 15790 15789 15793 15794 15792 +15793 15792 15791 15795 15796 15794 15795 15794 15793 +15797 15798 15796 15797 15796 15795 15799 15800 15798 +15799 15798 15797 15499 15498 15800 15799 15499 15800 +15801 15499 15799 15499 15802 15501 15801 15802 15499 +15501 15803 15503 15802 15803 15501 15804 15805 15505 +15503 15804 15505 15803 15804 15503 15806 15807 15805 +15806 15805 15804 15808 15809 15807 15808 15807 15806 +15810 15809 15808 15506 15786 15496 15506 15811 15786 +15787 15507 15497 15812 15507 15787 15813 15814 15815 +15815 15814 15788 15815 15788 15790 15508 15815 15790 +15509 15508 15790 15509 15790 15511 15511 15790 15792 +15511 15792 15513 15513 15792 15794 15513 15794 15514 +15514 15794 15796 15514 15796 15516 15516 15796 15798 +15516 15798 15518 15518 15798 15800 15518 15800 15498 +15805 15519 15505 15519 15805 15520 15520 15805 15807 +15520 15807 15521 15521 15807 15809 15810 15816 15809 +15816 15521 15809 15816 15523 15521 15525 15523 15816 +15525 15816 15817 15818 15819 15820 15819 15811 15820 +15811 15526 15820 15811 15506 15526 15821 15527 15822 +15527 15812 15822 15527 15507 15812 15823 15821 15822 +15824 15825 15826 15827 15828 15826 15827 15826 15825 +15828 15827 15813 15828 15813 15815 15528 15828 15815 +15529 15528 15815 15529 15815 15508 15829 15530 15830 +15530 15817 15830 15530 15525 15817 15831 15832 15829 +15831 15829 15830 15833 15834 15832 15833 15832 15831 +15835 15836 15834 15835 15834 15833 15836 15835 15818 +15836 15818 15820 15531 15836 15820 15532 15531 15820 +15532 15820 15526 15821 15534 15527 15535 15534 15821 +15823 15535 15821 15837 15535 15823 15838 15839 15840 +15841 15842 15840 15841 15840 15839 15843 15844 15842 +15843 15842 15841 15844 15843 15824 15844 15824 15826 +15536 15844 15826 15537 15536 15826 15537 15826 15538 +15538 15826 15828 15538 15828 15528 15829 15539 15530 +15539 15829 15540 15540 15829 15832 15540 15832 15542 +15542 15832 15834 15542 15834 15544 15544 15834 15836 +15544 15836 15531 15845 15547 15846 15547 15837 15846 +15547 15535 15837 15847 15848 15845 15847 15845 15846 +15849 15850 15848 15849 15848 15847 15850 15849 15838 +15850 15838 15840 15548 15850 15840 15549 15548 15840 +15549 15840 15551 15551 15840 15842 15551 15842 15553 +15553 15842 15844 15553 15844 15536 15845 15554 15547 +15554 15845 15555 15555 15845 15848 15555 15848 15557 +15557 15848 15850 15557 15850 15548 15851 15852 15853 +15854 15855 15853 15853 15855 15851 15854 15856 15855 +15857 15858 15859 15857 15859 15852 15859 15853 15852 +15859 15860 15853 15860 15559 15558 15854 15860 15558 +15853 15860 15854 15861 15862 15561 15561 15862 15558 +15862 15856 15558 15856 15854 15558 15863 15864 15861 +15861 15864 15862 15863 15865 15864 15866 15867 15868 +15869 15870 15868 15868 15870 15866 15870 15869 15858 +15869 15859 15858 15869 15871 15859 15871 15563 15562 +15860 15871 15562 15859 15871 15860 15860 15562 15559 +15566 15861 15561 15861 15566 15568 15872 15861 15568 +15863 15861 15872 15865 15863 15873 15873 15863 15872 +15874 15875 15876 15874 15876 15867 15867 15876 15868 +15876 15570 15868 15570 15569 15868 15868 15569 15573 +15868 15573 15869 15869 15573 15575 15869 15575 15871 +15871 15575 15563 15579 15872 15568 15579 15581 15872 +15581 15873 15872 15581 15877 15873 15878 15879 15880 +15878 15880 15875 15875 15880 15876 15880 15583 15876 +15583 15582 15876 15876 15582 15570 15592 15881 15882 +15877 15592 15882 15581 15592 15877 15881 15883 15882 +15884 15885 15593 15593 15885 15594 15597 15880 15879 +15884 15597 15879 15593 15597 15884 15880 15597 15583 +15602 15886 15881 15602 15881 15592 15883 15881 15887 +15887 15881 15886 15888 15889 15603 15603 15889 15604 +15890 15603 15605 15890 15888 15603 15605 15594 15885 +15605 15885 15890 15610 15891 15886 15610 15886 15602 +15887 15886 15892 15892 15886 15891 15893 15894 15611 +15611 15894 15612 15611 15604 15889 15611 15889 15893 +15616 15895 15891 15616 15891 15610 15892 15891 15896 +15896 15891 15895 15897 15898 15617 15617 15898 15618 +15617 15612 15894 15617 15894 15897 15619 15899 15895 +15619 15895 15616 15896 15895 15900 15900 15895 15899 +15901 15621 15620 15902 15901 15620 15618 15902 15620 +15898 15902 15618 15622 15899 15619 15622 15623 15899 +15623 15900 15899 15623 15903 15900 15904 15905 15901 +15905 15621 15901 15905 15625 15621 15904 15901 15902 +15906 15903 15623 15906 15623 15626 15907 15906 15908 +15906 15627 15908 15906 15626 15627 15628 15908 15627 +15909 15910 15636 15636 15910 15635 15636 15625 15905 +15636 15905 15909 15911 15907 15908 15912 15911 15913 +15913 15911 15637 15911 15908 15637 15908 15628 15637 +15638 15914 15637 15637 15914 15913 15640 15914 15638 +15915 15645 15646 15915 15646 15916 15646 15917 15916 +15646 15647 15917 15918 15647 15648 15918 15917 15647 +15648 15635 15910 15648 15910 15918 15912 15913 15919 +15912 15919 15920 15921 15913 15914 15919 15913 15921 +15650 15921 15640 15640 15921 15914 15922 15655 15654 +15645 15915 15654 15915 15922 15654 15915 15923 15922 +15915 15916 15924 15915 15924 15923 15925 15926 15927 +15927 15926 15920 15919 15927 15920 15928 15927 15919 +15656 15928 15657 15928 15921 15657 15928 15919 15921 +15657 15921 15650 15929 15661 15922 15922 15661 15655 +15923 15930 15931 15929 15923 15931 15922 15923 15929 +15930 15923 15924 15932 15933 15934 15934 15933 15925 +15927 15934 15925 15935 15934 15927 15662 15935 15663 +15935 15928 15663 15935 15927 15928 15663 15928 15656 +15929 15667 15661 15668 15667 15929 15931 15668 15929 +15936 15668 15931 15937 15938 15939 15939 15938 15932 +15934 15939 15932 15940 15939 15934 15941 15934 15935 +15940 15934 15941 15670 15941 15662 15662 15941 15935 +15936 15942 15668 15668 15942 15671 15673 15671 15942 +15673 15942 15943 15937 15939 15944 15937 15944 15945 +15946 15939 15940 15944 15939 15946 15674 15946 15675 +15946 15941 15675 15946 15940 15941 15675 15941 15670 +15943 15678 15673 15947 15678 15943 15945 15944 15948 +15945 15948 15949 15679 15948 15680 15948 15946 15680 +15948 15944 15946 15680 15946 15674 15947 15950 15678 +15678 15950 15682 15684 15682 15950 15684 15950 15951 +15949 15948 15952 15949 15952 15953 15685 15952 15679 +15679 15952 15948 15954 15686 15955 15686 15951 15955 +15686 15684 15951 15956 15954 15955 15953 15952 15957 +15953 15957 15958 15687 15957 15685 15685 15957 15952 +15954 15688 15686 15956 15959 15954 15959 15688 15954 +15959 15689 15688 15691 15689 15959 15691 15959 15960 +15961 15962 15963 15964 15963 15962 15958 15957 15965 +15958 15965 15966 15692 15965 15687 15687 15965 15957 +15960 15695 15691 15967 15695 15960 15968 15961 15963 +15969 15970 15971 15964 15968 15963 15972 15973 15970 +15972 15970 15969 15974 15975 15973 15974 15973 15972 +15976 15975 15974 15977 15978 15979 15979 15978 15966 +15979 15966 15965 15696 15979 15965 15697 15696 15965 +15697 15965 15692 15980 15699 15700 15701 15981 15700 +15700 15981 15980 15695 15967 15982 15981 15695 15982 +15983 15981 15982 15701 15695 15981 15984 15983 15982 +15985 15971 15970 15985 15970 15973 15985 15973 15986 +15986 15973 15975 15986 15975 15987 15987 15975 15976 +15988 15977 15979 15989 15988 15990 15990 15988 15702 +15988 15979 15702 15979 15696 15702 15703 15990 15702 +15991 15705 15704 15706 15992 15704 15704 15992 15991 +15699 15980 15706 15980 15992 15706 15980 15993 15992 +15981 15994 15995 15993 15981 15995 15980 15981 15993 +15994 15981 15983 15994 15983 15996 15996 15983 15984 +15997 15989 15990 15998 15997 15999 15999 15997 15707 +15997 15990 15707 15990 15703 15707 15708 15999 15707 +16000 15711 15712 15705 15991 15712 15991 16000 15712 +15991 16001 16000 15992 16002 16003 16001 15992 16003 +15991 15992 16001 16002 15992 15993 16002 15993 16004 +16004 15993 15995 15998 15999 15714 15713 15998 15714 +16005 15998 15713 15714 15999 15708 16006 15717 15718 +15719 16007 15718 15718 16007 16006 15720 16008 15719 +15719 16008 16007 15721 16009 15720 15720 16009 16008 +16010 16009 15721 16010 15721 15711 16011 16010 15711 +16000 16011 15711 16011 16000 16001 16011 16001 16012 +16012 16001 16003 16013 16005 15713 16013 15713 15722 +15722 15723 16013 16013 15723 16014 16015 15726 15725 +16015 15725 16016 15725 16017 16016 15725 15727 16017 +15727 15717 16006 16018 15727 16006 16017 15727 16018 +16018 16006 16007 16018 16007 16019 16019 16007 16008 +16019 16008 16020 16020 16008 16009 16020 16009 16021 +16021 16009 16010 16022 16014 15723 16022 15723 15728 +15728 15729 16022 16022 15729 16023 16024 16025 16015 +16025 15726 16015 16025 15730 15726 16024 16015 16016 +15729 15731 16026 16027 15729 16026 16023 15729 16027 +15732 16026 15731 16025 15733 15730 16028 15733 16025 +16029 16027 16026 16026 15732 16029 15732 16030 16029 +15732 15734 16030 16028 15735 15733 16031 15735 16028 +16032 16033 16034 16033 16030 16034 16030 15736 16034 +16030 15734 15736 16031 15737 15735 16035 15737 16031 +16032 16034 15739 15738 16032 15739 16036 16032 15738 +15739 16034 15736 16035 15740 15737 16037 15740 16035 +15741 16036 15738 15741 16038 16036 15742 15740 16039 +15740 16040 16039 15740 16037 16040 16039 15743 15742 +15744 16038 15741 15744 16041 16038 16039 16040 15745 +16040 15746 15745 16040 16042 15746 16039 15745 15743 +15747 16041 15744 15747 16043 16041 16042 15748 15746 +16044 15748 16042 16045 16046 15750 16045 15750 15749 +15747 15750 16043 16043 15750 16046 16044 15752 15748 +16047 15752 16044 15753 16045 15749 15753 16048 16045 +16047 16049 15752 15752 16049 15754 15755 15754 16049 +15755 16049 16050 15756 16048 15753 15756 16051 16048 +16050 15757 15755 16052 15757 16050 15758 16051 15756 +15758 16053 16051 16052 15759 15757 16054 15759 16052 +15760 16053 15758 15760 16055 16053 16054 15761 15759 +16056 15761 16054 15762 16055 15760 15762 16057 16055 +16056 15763 15761 16058 15763 16056 16059 16057 15762 +16059 15762 15764 15764 15765 16059 16059 15765 16060 +16058 15766 15763 16061 15766 16058 15767 16060 15765 +15767 16062 16060 16061 15768 15766 16063 15768 16061 +15769 16062 15767 15769 16064 16062 16065 16066 15770 +15770 16066 15771 15770 15768 16063 15770 16063 16065 +15773 16064 15769 15773 16067 16064 16066 15774 15771 +16068 15774 16066 15775 16067 15773 15775 16069 16067 +16068 15776 15774 16070 15776 16068 15777 16069 15775 +15777 16071 16069 16070 15778 15776 16072 15778 16070 +15779 16071 15777 15779 16073 16071 16072 16074 15778 +15778 16074 15780 15781 15780 16074 15781 16074 16075 +16076 16077 16078 16079 16080 16078 16079 16078 16077 +16081 16080 16079 15782 16073 15779 15782 16082 16073 +16075 15783 15781 16083 15783 16075 16084 16085 16086 +16087 16088 16086 16087 16086 16085 16089 16090 16088 +16089 16088 16087 16091 16092 16090 16091 16090 16089 +16092 16091 16076 16078 16092 16076 16093 16092 16078 +16094 16078 16080 16093 16078 16094 16080 16081 16095 +16096 16080 16095 16094 16080 16096 16097 16098 16096 +16097 16096 16095 16099 16100 16098 16099 16098 16097 +16101 16102 16100 16101 16100 16099 16103 16102 16101 +15784 16082 15782 15784 16104 16082 16083 15785 15783 +16105 15785 16083 16106 16107 16108 16109 16110 16108 +16109 16108 16107 16110 16109 16084 16086 16110 16084 +16111 16110 16086 16112 16086 16088 16111 16086 16112 +16113 16088 16090 16112 16088 16113 16114 16090 16092 +16113 16090 16114 16115 16092 16093 16114 16092 16115 +16116 16093 16094 16115 16093 16116 16117 16094 16096 +16116 16094 16117 16118 16096 16098 16117 16096 16118 +16119 16098 16100 16118 16098 16119 16120 16100 16102 +16119 16100 16120 16102 16103 16121 16122 16102 16121 +16120 16102 16122 16123 16122 16121 16124 16125 16126 +16125 16104 16126 16104 15786 16126 16104 15784 15786 +16105 15787 15785 16127 15787 16105 16128 16129 16130 +16130 16129 16106 16108 16130 16106 16131 16130 16108 +15788 16131 15789 16131 16110 15789 16131 16108 16110 +15789 16110 15791 15791 16110 16111 15791 16111 15793 +15793 16111 16112 15793 16112 15795 15795 16112 16113 +15795 16113 15797 15797 16113 16114 15797 16114 15799 +15799 16114 16115 15799 16115 15801 15801 16115 16116 +15801 16116 15802 15802 16116 16117 15802 16117 15803 +15803 16117 16118 15803 16118 15804 15804 16118 16119 +15804 16119 15806 15806 16119 16120 15806 16120 15808 +15808 16120 16122 16132 16133 15810 16132 15810 15808 +16123 16132 15808 16122 16123 15808 16134 16133 16132 +16124 16126 16135 16124 16135 16136 15811 16135 15786 +15786 16135 16126 16137 15812 16138 15812 16127 16138 +15812 15787 16127 16139 16137 16138 16140 16141 16142 +16142 16141 16128 16130 16142 16128 16143 16142 16130 +15813 16143 15814 16143 16131 15814 16143 16130 16131 +15814 16131 15788 16133 15816 15810 16144 16145 15817 +16144 15817 15816 16134 16144 15816 16133 16134 15816 +16146 16147 16145 16146 16145 16144 16148 16149 16147 +16148 16147 16146 16150 16151 16149 16150 16149 16148 +16151 16150 16136 16151 16136 16135 15818 16151 16135 +15819 15818 16135 15819 16135 15811 16137 15822 15812 +15823 15822 16137 16139 15823 16137 16152 15823 16139 +16153 16154 16155 16155 16154 16140 16155 16140 16142 +15824 16155 16142 15825 15824 16142 15825 16142 15827 +15827 16142 16143 15827 16143 15813 16145 15830 15817 +15830 16145 15831 15831 16145 16147 15831 16147 15833 +15833 16147 16149 15833 16149 15835 15835 16149 16151 +15835 16151 15818 16156 15837 16157 15837 16152 16157 +15837 15823 16152 16158 16159 16156 16158 16156 16157 +16160 16161 16159 16160 16159 16158 15839 15838 16161 +16160 15839 16161 16162 15839 16160 15839 16163 15841 +16162 16163 15839 16155 15843 16153 15843 16163 16153 +15843 15841 16163 15843 16155 15824 16156 15846 15837 +15846 16156 15847 15847 16156 16159 15847 16159 15849 +15849 16159 16161 15849 16161 15838 16164 16165 16166 +16166 16167 16164 16168 16169 16170 16171 16172 16170 +16170 16172 16168 16173 16174 16171 16171 16174 16172 +16174 16173 16165 16173 16166 16165 16173 16175 16166 +16167 16166 16176 16166 16177 16176 16166 16175 16177 +16177 16178 16176 16179 16180 16181 16179 16181 16169 +16169 16181 16170 16181 15852 16170 15852 15851 16170 +16170 15851 15855 16170 15855 16171 16171 15855 15856 +16182 16171 15856 16173 16171 16182 16173 16183 16175 +16173 16182 16183 16175 16184 16177 16175 16183 16184 +16178 16177 16185 16177 16186 16185 16177 16184 16186 +16186 16187 16185 16188 16189 16190 16188 16190 16180 +16180 16190 16181 16190 15858 16181 15858 15857 16181 +16181 15857 15852 15862 16182 15856 16182 15862 15864 +16182 15864 16183 16183 15864 15865 16191 16183 15865 +16184 16183 16191 16184 16192 16186 16184 16191 16192 +16187 16186 16193 16186 16194 16193 16186 16192 16194 +16194 16195 16193 16196 16197 16198 15867 15866 16198 +15866 16196 16198 15866 16199 16196 15870 16190 16189 +16199 15870 16189 15866 15870 16199 16190 15870 15858 +15873 16200 16191 15873 16191 15865 16191 16201 16192 +16191 16200 16201 16202 16194 16203 16194 16201 16203 +16194 16192 16201 16194 16202 16195 16204 16205 16206 +16204 16206 16197 16197 16206 16198 16206 15875 16198 +15875 15874 16198 16198 15874 15867 15877 16207 16200 +15877 16200 15873 16200 16208 16201 16200 16207 16208 +16203 16201 16209 16209 16201 16208 16210 16211 16212 +16210 16212 16205 16205 16212 16206 16212 15879 16206 +15879 15878 16206 16206 15878 15875 15882 16207 15877 +16207 15882 15883 16213 16207 15883 16208 16207 16213 +16209 16208 16214 16214 16208 16213 16215 16216 16217 +16218 16219 16217 16217 16219 16215 16219 16218 16211 +16211 16218 16212 16218 15885 16212 15885 15884 16212 +16212 15884 15879 16220 16221 16213 15883 16220 16213 +15887 16220 15883 16213 16221 16214 16217 16216 15888 +16216 15889 15888 16216 16222 15889 16217 15888 15890 +16217 15890 16218 16218 15890 15885 15892 16220 15887 +15892 16223 16220 16224 16225 15893 15893 16225 15894 +15893 15889 16222 15893 16222 16224 15896 16223 15892 +15896 16226 16223 16227 15898 15897 16228 16227 15897 +15894 16228 15897 16225 16228 15894 15900 16226 15896 +15900 16229 16226 16230 16231 16227 16231 15898 16227 +16231 15902 15898 16230 16227 16228 15903 16229 15900 +15903 16232 16229 16233 15905 15904 16234 16233 15904 +15902 16234 15904 16231 16234 15902 15903 15906 16235 +16236 15903 16235 16232 15903 16236 15907 16235 15906 +16237 15910 15909 16238 16237 15909 16238 15909 15905 +16239 16238 15905 16233 16239 15905 16239 16233 16234 +16236 16235 16240 16236 16240 16241 15911 16242 15907 +16242 16235 15907 16242 16240 16235 15912 16242 15911 +16243 15916 15917 16243 15917 16244 15917 16245 16244 +15917 15918 16245 15918 15910 16237 16246 15918 16237 +16245 15918 16246 16246 16237 16238 16247 16248 16249 +16250 16251 16249 16250 16249 16248 16251 16250 16241 +16240 16251 16241 16252 16251 16240 16253 16240 16242 +16252 16240 16253 15920 16253 15912 15912 16253 16242 +16254 15924 16243 16243 15924 15916 16243 16244 16255 +16243 16255 16254 16256 16257 16258 16258 16257 16247 +16249 16258 16247 16259 16258 16249 16260 16249 16251 +16259 16249 16260 16261 16251 16252 16260 16251 16261 +15925 16261 15926 16261 16253 15926 16261 16252 16253 +15926 16253 15920 16262 15931 15930 15924 16254 15930 +16254 16262 15930 16254 16263 16262 16254 16255 16264 +16254 16264 16263 16256 16258 16265 16256 16265 16266 +16267 16258 16259 16265 16258 16267 16268 16259 16260 +16267 16259 16268 15932 16268 15933 16268 16261 15933 +16268 16260 16261 15933 16261 15925 16269 15936 16262 +16262 15936 15931 16270 16262 16263 16269 16262 16270 +16263 16264 16271 16263 16271 16270 16266 16265 16272 +16266 16272 16273 16274 16265 16267 16272 16265 16274 +15937 16274 15938 16274 16268 15938 16274 16267 16268 +15938 16268 15932 16269 15942 15936 15942 16269 15943 +16269 16275 15943 16269 16270 16275 16270 16271 16276 +16270 16276 16275 16273 16272 16277 16273 16277 16278 +16279 16272 16274 16277 16272 16279 15945 16279 15937 +15937 16279 16274 16280 15947 16275 16275 15947 15943 +16275 16276 16281 16282 16275 16281 16280 16275 16282 +16283 16282 16281 16278 16277 16284 16278 16284 16285 +16286 16277 16279 16284 16277 16286 15949 16286 15945 +15945 16286 16279 16280 15950 15947 15950 16280 15951 +16280 16287 15951 16280 16282 16287 16282 16283 16288 +16282 16288 16287 16285 16284 16289 16285 16289 16290 +16291 16284 16286 16289 16284 16291 15953 16291 15949 +15949 16291 16286 16287 15955 15951 16292 16293 15956 +16292 15956 15955 16288 16292 15955 16287 16288 15955 +16294 16293 16292 16290 16289 16295 16290 16295 16296 +16297 16289 16291 16295 16289 16297 15958 16297 15953 +15953 16297 16291 16293 15959 15956 16298 16299 15960 +16298 15960 15959 16294 16298 15959 16293 16294 15959 +16300 16301 16299 16300 16299 16298 15962 15961 16301 +16300 15962 16301 16302 15962 16300 15964 15962 16302 +15964 16302 16303 16304 16305 16306 16306 16305 16296 +16295 16306 16296 16307 16306 16295 16308 16295 16297 +16307 16295 16308 15966 16308 15958 15958 16308 16297 +16309 15967 16299 16299 15967 15960 16310 16299 16301 +16309 16299 16310 16310 16301 15961 16311 16310 15961 +15968 16311 15961 16312 16310 16311 15969 15971 16313 +16314 16313 15971 16303 15968 15964 15968 16303 16311 +15969 16315 15972 16313 16315 15969 15972 16316 15974 +16315 16316 15972 15976 15974 16316 15976 16316 16317 +16318 16304 16306 16319 16320 16318 16306 16319 16318 +16307 16319 16306 15977 16319 15978 16319 16308 15978 +16319 16307 16308 15978 16308 15966 16309 15982 15967 +16309 16310 16321 15984 16309 16321 16322 15984 16321 +16309 15984 15982 16321 16310 16312 16323 16314 15971 +16323 15971 15985 16324 15985 15986 16324 16323 15985 +16325 15986 15987 16325 16324 15986 15987 15976 16317 +15987 16317 16325 16326 16320 16319 16326 16319 15977 +15988 16326 15977 16327 16326 15988 15988 15989 16327 +16327 15989 16328 16329 15995 15994 15996 16330 15994 +15994 16330 16329 15984 16322 16331 16330 15984 16331 +16332 16330 16331 15996 15984 16330 16333 16332 16331 +15989 15997 16334 16335 15989 16334 16328 15989 16335 +15998 16334 15997 16336 16003 16002 16004 16337 16002 +16002 16337 16336 15995 16329 16004 16329 16337 16004 +16329 16338 16337 16339 16329 16330 16338 16329 16339 +16340 16330 16332 16339 16330 16340 16332 16333 16341 +16332 16341 16340 16342 16335 16334 16334 15998 16342 +15998 16343 16342 15998 16005 16343 16344 16010 16011 +16012 16345 16011 16011 16345 16344 16003 16336 16012 +16336 16345 16012 16336 16346 16345 16347 16336 16337 +16346 16336 16347 16348 16337 16338 16347 16337 16348 +16349 16338 16339 16348 16338 16349 16350 16339 16340 +16349 16339 16350 16340 16341 16351 16352 16340 16351 +16350 16340 16352 16353 16352 16351 16005 16013 16354 +16355 16005 16354 16343 16005 16355 16014 16354 16013 +16356 16016 16017 16356 16017 16357 16017 16358 16357 +16017 16018 16358 16359 16358 16360 16358 16019 16360 +16358 16018 16019 16020 16361 16019 16019 16361 16360 +16361 16020 16362 16020 16363 16362 16020 16021 16363 +16021 16010 16344 16364 16021 16344 16363 16021 16364 +16364 16344 16345 16364 16345 16365 16365 16345 16346 +16365 16346 16366 16366 16346 16347 16366 16347 16367 +16368 16369 16367 16347 16368 16367 16348 16368 16347 +16370 16348 16349 16368 16348 16370 16371 16349 16350 +16370 16349 16371 16372 16350 16352 16371 16350 16372 +16352 16353 16373 16374 16352 16373 16372 16352 16374 +16375 16374 16373 16376 16355 16354 16377 16376 16378 +16378 16376 16022 16376 16354 16022 16354 16014 16022 +16023 16378 16022 16379 16025 16024 16380 16379 16024 +16380 16024 16016 16381 16380 16016 16356 16381 16016 +16381 16356 16357 16382 16359 16360 16382 16360 16361 +16382 16361 16383 16383 16361 16362 16384 16369 16368 +16385 16386 16384 16368 16385 16384 16370 16385 16368 +16387 16370 16371 16385 16370 16387 16388 16371 16372 +16387 16371 16388 16389 16372 16374 16388 16372 16389 +16374 16375 16390 16391 16374 16390 16389 16374 16391 +16392 16391 16390 16393 16377 16378 16378 16023 16393 +16023 16394 16393 16023 16027 16394 16395 16396 16379 +16396 16025 16379 16396 16028 16025 16395 16379 16380 +16397 16386 16385 16397 16385 16387 16397 16387 16398 +16399 16400 16398 16387 16399 16398 16388 16399 16387 +16401 16388 16389 16399 16388 16401 16402 16389 16391 +16401 16389 16402 16391 16392 16403 16391 16403 16402 +16027 16029 16404 16405 16027 16404 16394 16027 16405 +16030 16404 16029 16396 16031 16028 16406 16031 16396 +16407 16400 16399 16408 16409 16407 16399 16408 16407 +16401 16408 16399 16410 16401 16402 16408 16401 16410 +16402 16403 16411 16402 16411 16410 16405 16404 16033 +16032 16405 16033 16412 16405 16032 16033 16404 16030 +16406 16035 16031 16413 16035 16406 16414 16409 16408 +16414 16408 16410 16414 16410 16415 16415 16410 16411 +16036 16412 16032 16036 16416 16412 16413 16037 16035 +16417 16037 16413 16038 16416 16036 16038 16418 16416 +16417 16040 16037 16419 16040 16417 16041 16418 16038 +16041 16420 16418 16419 16042 16040 16421 16042 16419 +16043 16420 16041 16043 16422 16420 16421 16044 16042 +16423 16044 16421 16424 16425 16046 16424 16046 16045 +16043 16046 16422 16422 16046 16425 16423 16047 16044 +16426 16047 16423 16048 16424 16045 16048 16427 16424 +16049 16047 16428 16047 16429 16428 16047 16426 16429 +16428 16050 16049 16051 16427 16048 16051 16430 16427 +16429 16431 16428 16050 16428 16431 16432 16050 16431 +16052 16050 16432 16053 16430 16051 16053 16433 16430 +16432 16054 16052 16434 16054 16432 16055 16433 16053 +16055 16435 16433 16434 16056 16054 16436 16056 16434 +16057 16435 16055 16057 16437 16435 16436 16058 16056 +16438 16058 16436 16057 16059 16439 16440 16057 16439 +16437 16057 16440 16060 16439 16059 16438 16061 16058 +16441 16061 16438 16442 16440 16439 16439 16060 16442 +16060 16443 16442 16060 16062 16443 16441 16063 16061 +16444 16063 16441 16064 16443 16062 16064 16445 16443 +16446 16447 16065 16065 16447 16066 16065 16063 16444 +16065 16444 16446 16067 16445 16064 16067 16448 16445 +16447 16068 16066 16449 16068 16447 16069 16448 16067 +16069 16450 16448 16449 16070 16068 16451 16070 16449 +16071 16450 16069 16071 16452 16450 16451 16072 16070 +16453 16072 16451 16454 16455 16456 16457 16458 16456 +16457 16456 16455 16459 16460 16458 16459 16458 16457 +16461 16462 16460 16461 16460 16459 16463 16464 16462 +16463 16462 16461 16465 16466 16464 16465 16464 16463 +16467 16468 16466 16467 16466 16465 16469 16470 16468 +16469 16468 16467 16471 16472 16470 16471 16470 16469 +16473 16472 16471 16073 16452 16071 16073 16474 16452 +16453 16475 16072 16072 16475 16074 16075 16074 16475 +16075 16475 16476 16477 16478 16479 16479 16478 16454 +16456 16479 16454 16480 16479 16456 16481 16456 16458 +16480 16456 16481 16482 16458 16460 16481 16458 16482 +16483 16460 16462 16482 16460 16483 16076 16483 16077 +16483 16464 16077 16483 16462 16464 16077 16464 16079 +16079 16464 16466 16079 16466 16081 16466 16484 16081 +16466 16468 16484 16485 16468 16470 16484 16468 16485 +16486 16470 16472 16485 16470 16486 16472 16473 16487 +16488 16472 16487 16486 16472 16488 16489 16490 16488 +16489 16488 16487 16491 16490 16489 16492 16493 16494 +16493 16474 16494 16474 16082 16494 16474 16073 16082 +16476 16083 16075 16495 16083 16476 16496 16497 16498 +16498 16497 16477 16479 16498 16477 16499 16498 16479 +16084 16499 16085 16499 16480 16085 16499 16479 16480 +16085 16480 16087 16087 16480 16481 16087 16481 16089 +16089 16481 16482 16089 16482 16091 16091 16482 16483 +16091 16483 16076 16484 16095 16081 16095 16484 16097 +16097 16484 16485 16097 16485 16099 16099 16485 16486 +16099 16486 16101 16101 16486 16488 16101 16488 16103 +16488 16500 16103 16488 16490 16500 16490 16491 16501 +16490 16501 16500 16492 16494 16502 16492 16502 16503 +16104 16502 16082 16082 16502 16494 16495 16105 16083 +16504 16105 16495 16505 16506 16507 16508 16509 16507 +16508 16507 16506 16509 16508 16496 16509 16496 16498 +16106 16509 16498 16107 16106 16498 16107 16498 16109 +16109 16498 16499 16109 16499 16084 16500 16121 16103 +16510 16511 16123 16510 16123 16121 16501 16510 16121 +16500 16501 16121 16512 16513 16511 16512 16511 16510 +16514 16513 16512 16515 16516 16517 16517 16516 16503 +16517 16503 16502 16124 16517 16502 16125 16124 16502 +16125 16502 16104 16518 16127 16519 16127 16504 16519 +16127 16105 16504 16520 16518 16519 16521 16522 16523 +16523 16522 16505 16507 16523 16505 16524 16523 16507 +16128 16524 16129 16524 16509 16129 16524 16507 16509 +16129 16509 16106 16511 16132 16123 16132 16511 16134 +16511 16525 16134 16511 16513 16525 16513 16514 16526 +16527 16513 16526 16525 16513 16527 16528 16529 16527 +16528 16527 16526 16529 16528 16515 16517 16529 16515 +16530 16529 16517 16136 16530 16124 16124 16530 16517 +16518 16138 16127 16139 16138 16518 16520 16139 16518 +16531 16139 16520 16521 16523 16532 16521 16532 16533 +16140 16532 16141 16532 16524 16141 16532 16523 16524 +16141 16524 16128 16525 16144 16134 16144 16525 16146 +16146 16525 16527 16146 16527 16148 16148 16527 16529 +16148 16529 16150 16150 16529 16530 16150 16530 16136 +16534 16152 16535 16152 16531 16535 16152 16139 16531 +16536 16534 16535 16537 16538 16539 16540 16541 16539 +16540 16539 16538 16542 16543 16541 16542 16541 16540 +16543 16542 16533 16543 16533 16532 16153 16543 16532 +16154 16153 16532 16154 16532 16140 16534 16157 16152 +16536 16544 16534 16544 16157 16534 16544 16158 16157 +16539 16160 16537 16160 16544 16537 16160 16158 16544 +16160 16539 16162 16162 16539 16541 16162 16541 16163 +16163 16541 16543 16163 16543 16153 16545 16546 16547 +16547 16548 16545 16549 16550 16551 16552 16553 16551 +16551 16553 16549 16553 16552 16546 16552 16547 16546 +16552 16554 16547 16164 16555 16165 16555 16547 16165 +16547 16554 16165 16547 16555 16548 16556 16557 16167 +16557 16164 16167 16557 16555 16164 16558 16559 16556 +16556 16559 16557 16558 16560 16559 16561 16562 16563 +16561 16563 16550 16550 16563 16551 16563 16169 16551 +16169 16168 16551 16551 16168 16172 16551 16172 16552 +16552 16172 16174 16552 16174 16554 16554 16174 16165 +16176 16556 16167 16556 16176 16178 16564 16556 16178 +16558 16556 16564 16560 16558 16565 16558 16566 16565 +16558 16564 16566 16566 16567 16565 16568 16569 16570 +16568 16570 16562 16562 16570 16563 16570 16180 16563 +16180 16179 16563 16563 16179 16169 16185 16564 16178 +16564 16185 16187 16571 16564 16187 16566 16564 16571 +16567 16566 16572 16566 16573 16572 16566 16571 16573 +16573 16574 16572 16575 16576 16577 16578 16579 16577 +16577 16579 16575 16579 16578 16569 16569 16578 16570 +16578 16189 16570 16189 16188 16570 16570 16188 16180 +16193 16571 16187 16571 16193 16195 16580 16571 16195 +16573 16571 16580 16574 16573 16581 16581 16573 16580 +16582 16583 16584 16582 16584 16576 16576 16584 16577 +16584 16197 16577 16197 16196 16577 16577 16196 16199 +16577 16199 16578 16578 16199 16189 16203 16585 16202 +16585 16586 16202 16202 16586 16195 16586 16587 16195 +16587 16580 16195 16580 16587 16581 16584 16583 16204 +16583 16205 16204 16583 16588 16205 16584 16204 16197 +16209 16589 16585 16209 16585 16203 16586 16585 16590 +16590 16585 16589 16591 16592 16210 16210 16592 16211 +16210 16205 16588 16210 16588 16591 16593 16594 16589 +16209 16593 16589 16214 16593 16209 16589 16594 16590 +16595 16216 16215 16595 16215 16596 16215 16597 16596 +16215 16219 16597 16219 16211 16592 16219 16592 16597 +16220 16598 16221 16598 16599 16221 16599 16214 16221 +16599 16593 16214 16600 16601 16595 16601 16216 16595 +16601 16222 16216 16600 16595 16596 16602 16603 16598 +16220 16602 16598 16223 16602 16220 16598 16603 16599 +16604 16225 16224 16605 16604 16224 16222 16605 16224 +16601 16605 16222 16226 16602 16223 16226 16606 16602 +16607 16608 16604 16608 16225 16604 16608 16228 16225 +16607 16604 16605 16609 16610 16611 16610 16606 16611 +16606 16229 16611 16606 16226 16229 16612 16613 16230 +16230 16613 16231 16230 16228 16608 16230 16608 16612 +16609 16611 16614 16609 16614 16615 16232 16614 16229 +16229 16614 16611 16613 16234 16231 16616 16234 16613 +16615 16614 16617 16615 16617 16618 16236 16617 16232 +16232 16617 16614 16619 16620 16621 16238 16239 16621 +16239 16619 16621 16239 16622 16619 16239 16234 16616 +16239 16616 16622 16618 16617 16623 16618 16623 16624 +16241 16623 16236 16236 16623 16617 16625 16244 16245 +16620 16626 16621 16626 16245 16621 16245 16246 16621 +16245 16626 16625 16621 16246 16238 16627 16628 16629 +16248 16247 16629 16628 16248 16629 16630 16248 16628 +16623 16250 16624 16250 16630 16624 16250 16248 16630 +16250 16623 16241 16631 16632 16625 16632 16244 16625 +16632 16255 16244 16631 16625 16626 16633 16634 16635 +16635 16634 16627 16635 16627 16629 16256 16635 16629 +16257 16256 16629 16257 16629 16247 16632 16264 16255 +16636 16264 16632 16633 16635 16637 16633 16637 16638 +16266 16637 16256 16256 16637 16635 16639 16271 16640 +16271 16636 16640 16271 16264 16636 16641 16639 16640 +16638 16637 16642 16638 16642 16643 16273 16642 16266 +16266 16642 16637 16644 16276 16639 16639 16276 16271 +16639 16641 16645 16639 16645 16644 16643 16642 16646 +16643 16646 16647 16278 16646 16273 16273 16646 16642 +16644 16281 16276 16648 16649 16283 16648 16283 16281 +16645 16648 16281 16644 16645 16281 16650 16649 16648 +16647 16646 16651 16647 16651 16652 16285 16651 16278 +16278 16651 16646 16653 16288 16649 16649 16288 16283 +16649 16650 16654 16655 16649 16654 16653 16649 16655 +16656 16657 16655 16656 16655 16654 16658 16657 16656 +16652 16651 16659 16652 16659 16660 16290 16659 16285 +16285 16659 16651 16653 16292 16288 16292 16653 16294 +16653 16661 16294 16653 16655 16661 16662 16655 16657 +16661 16655 16662 16657 16658 16663 16664 16657 16663 +16662 16657 16664 16665 16664 16663 16660 16659 16666 +16660 16666 16667 16296 16666 16290 16290 16666 16659 +16661 16298 16294 16298 16661 16300 16300 16661 16662 +16300 16662 16302 16302 16662 16664 16668 16669 16303 +16668 16303 16302 16665 16668 16302 16664 16665 16302 +16670 16671 16669 16670 16669 16668 16672 16671 16670 +16673 16674 16675 16675 16674 16667 16675 16667 16666 +16304 16675 16666 16305 16304 16666 16305 16666 16296 +16676 16312 16311 16311 16303 16676 16303 16669 16676 +16313 16314 16676 16313 16676 16669 16313 16669 16315 +16315 16669 16671 16672 16677 16671 16677 16315 16671 +16677 16316 16315 16317 16316 16677 16317 16677 16678 +16679 16680 16681 16682 16683 16681 16682 16681 16680 +16684 16685 16683 16684 16683 16682 16673 16675 16686 +16684 16687 16685 16686 16675 16304 16318 16686 16304 +16688 16686 16318 16318 16320 16688 16688 16320 16689 +16690 16322 16321 16312 16676 16321 16676 16690 16321 +16676 16691 16690 16323 16692 16314 16692 16676 16314 +16692 16691 16676 16324 16693 16323 16323 16693 16692 +16325 16694 16324 16324 16694 16693 16317 16678 16695 +16694 16317 16695 16696 16694 16695 16325 16317 16694 +16697 16698 16696 16697 16696 16695 16679 16681 16699 +16697 16700 16698 16699 16681 16683 16699 16683 16701 +16701 16683 16685 16701 16685 16702 16702 16685 16687 +16320 16326 16703 16704 16320 16703 16689 16320 16704 +16327 16705 16326 16326 16705 16703 16328 16705 16327 +16690 16331 16322 16331 16690 16333 16690 16706 16333 +16690 16691 16706 16707 16691 16692 16706 16691 16707 +16708 16692 16693 16707 16692 16708 16709 16693 16694 +16708 16693 16709 16710 16694 16696 16709 16694 16710 +16698 16711 16712 16710 16698 16712 16696 16698 16710 +16711 16698 16700 16713 16704 16703 16714 16715 16713 +16703 16714 16713 16705 16714 16703 16335 16714 16328 +16328 16714 16705 16716 16341 16706 16706 16341 16333 +16717 16706 16707 16716 16706 16717 16718 16707 16708 +16717 16707 16718 16719 16708 16709 16718 16708 16719 +16720 16709 16710 16719 16709 16720 16710 16712 16721 +16710 16721 16720 16722 16715 16714 16723 16722 16724 +16724 16722 16342 16722 16714 16342 16714 16335 16342 +16343 16724 16342 16716 16351 16341 16351 16716 16353 +16716 16725 16353 16716 16717 16725 16726 16717 16718 +16725 16717 16726 16727 16718 16719 16726 16718 16727 +16728 16719 16720 16727 16719 16728 16720 16721 16729 +16720 16729 16728 16730 16723 16724 16724 16343 16730 +16343 16731 16730 16343 16355 16731 16732 16357 16358 +16359 16732 16358 16733 16362 16363 16364 16734 16363 +16363 16734 16733 16365 16735 16364 16364 16735 16734 +16366 16736 16365 16365 16736 16735 16367 16737 16366 +16366 16737 16736 16369 16737 16367 16725 16373 16353 +16373 16725 16375 16725 16738 16375 16725 16726 16738 +16739 16726 16727 16738 16726 16739 16740 16727 16728 +16739 16727 16740 16728 16729 16741 16728 16741 16740 +16742 16731 16355 16742 16355 16376 16376 16377 16742 +16742 16377 16743 16744 16745 16381 16381 16745 16380 +16381 16357 16732 16746 16381 16732 16744 16381 16746 +16746 16732 16359 16382 16746 16359 16747 16746 16382 +16748 16382 16383 16748 16747 16382 16383 16362 16733 +16749 16383 16733 16748 16383 16749 16749 16733 16734 +16749 16734 16750 16751 16752 16750 16734 16751 16750 +16735 16751 16734 16753 16735 16736 16751 16735 16753 +16754 16736 16737 16753 16736 16754 16384 16755 16369 +16755 16737 16369 16755 16754 16737 16386 16755 16384 +16738 16390 16375 16390 16738 16392 16738 16756 16392 +16738 16739 16756 16757 16739 16740 16756 16739 16757 +16740 16741 16758 16740 16758 16757 16759 16743 16377 +16759 16377 16393 16393 16394 16759 16759 16394 16760 +16761 16762 16395 16395 16762 16396 16395 16380 16745 +16395 16745 16761 16763 16752 16751 16763 16751 16753 +16763 16753 16764 16764 16753 16754 16764 16754 16765 +16766 16767 16765 16754 16766 16765 16755 16766 16754 +16397 16768 16386 16768 16755 16386 16768 16766 16755 +16398 16769 16397 16397 16769 16768 16400 16769 16398 +16770 16403 16756 16756 16403 16392 16757 16771 16772 +16770 16757 16772 16756 16757 16770 16771 16757 16758 +16405 16760 16394 16405 16773 16760 16762 16406 16396 +16774 16406 16762 16775 16767 16766 16776 16777 16775 +16766 16776 16775 16768 16776 16766 16778 16768 16769 +16776 16768 16778 16407 16779 16400 16779 16769 16400 +16779 16778 16769 16409 16779 16407 16780 16411 16770 +16770 16411 16403 16770 16772 16781 16770 16781 16780 +16412 16773 16405 16412 16782 16773 16774 16413 16406 +16783 16413 16774 16784 16777 16776 16785 16786 16784 +16776 16785 16784 16778 16785 16776 16787 16778 16779 +16785 16778 16787 16414 16788 16409 16788 16779 16409 +16788 16787 16779 16415 16789 16414 16414 16789 16788 +16411 16780 16415 16780 16789 16415 16780 16790 16789 +16780 16781 16791 16792 16780 16791 16790 16780 16792 +16793 16792 16791 16416 16782 16412 16416 16794 16782 +16783 16417 16413 16795 16417 16783 16786 16785 16796 +16786 16796 16797 16798 16785 16787 16796 16785 16798 +16799 16787 16788 16798 16787 16799 16800 16788 16789 +16799 16788 16800 16801 16789 16790 16800 16789 16801 +16802 16790 16792 16801 16790 16802 16792 16793 16803 +16804 16792 16803 16802 16792 16804 16805 16804 16803 +16418 16794 16416 16418 16806 16794 16795 16419 16417 +16807 16419 16795 16797 16796 16808 16797 16808 16809 +16810 16796 16798 16808 16796 16810 16811 16798 16799 +16810 16798 16811 16812 16799 16800 16811 16799 16812 +16813 16800 16801 16812 16800 16813 16814 16801 16802 +16813 16801 16814 16815 16802 16804 16814 16802 16815 +16804 16805 16816 16804 16816 16815 16420 16806 16418 +16420 16817 16806 16807 16421 16419 16818 16421 16807 +16819 16809 16808 16820 16821 16819 16808 16820 16819 +16810 16820 16808 16822 16810 16811 16820 16810 16822 +16823 16811 16812 16822 16811 16823 16824 16812 16813 +16823 16812 16824 16814 16825 16826 16824 16814 16826 +16813 16814 16824 16825 16814 16815 16825 16815 16827 +16827 16815 16816 16828 16829 16830 16829 16817 16830 +16817 16422 16830 16817 16420 16422 16818 16423 16421 +16831 16423 16818 16832 16821 16820 16833 16834 16832 +16820 16833 16832 16822 16833 16820 16835 16822 16823 +16833 16822 16835 16824 16836 16837 16835 16824 16837 +16823 16824 16835 16836 16824 16826 16828 16830 16425 +16424 16828 16425 16838 16828 16424 16425 16830 16422 +16831 16426 16423 16839 16426 16831 16840 16834 16833 +16840 16833 16835 16840 16835 16841 16841 16835 16837 +16427 16838 16424 16427 16842 16838 16839 16429 16426 +16843 16429 16839 16430 16842 16427 16430 16844 16842 +16843 16845 16429 16429 16845 16431 16432 16431 16845 +16432 16845 16846 16433 16844 16430 16433 16847 16844 +16846 16434 16432 16848 16434 16846 16435 16847 16433 +16435 16849 16847 16848 16436 16434 16850 16436 16848 +16437 16849 16435 16437 16851 16849 16850 16438 16436 +16852 16438 16850 16440 16851 16437 16440 16853 16851 +16852 16441 16438 16854 16441 16852 16855 16856 16857 +16858 16859 16857 16858 16857 16856 16860 16859 16858 +16861 16853 16440 16861 16440 16442 16442 16443 16861 +16861 16443 16862 16863 16864 16865 16854 16863 16865 +16444 16854 16865 16441 16854 16444 16866 16867 16868 +16868 16867 16855 16857 16868 16855 16869 16868 16857 +16870 16857 16859 16869 16857 16870 16859 16860 16871 +16872 16859 16871 16870 16859 16872 16873 16874 16872 +16873 16872 16871 16875 16874 16873 16445 16862 16443 +16445 16876 16862 16865 16864 16446 16864 16447 16446 +16864 16877 16447 16865 16446 16444 16878 16879 16880 +16880 16879 16866 16868 16880 16866 16881 16880 16868 +16882 16868 16869 16881 16868 16882 16883 16869 16870 +16882 16869 16883 16884 16870 16872 16883 16870 16884 +16885 16872 16874 16884 16872 16885 16874 16875 16886 +16874 16886 16885 16448 16876 16445 16448 16887 16876 +16877 16449 16447 16888 16449 16877 16889 16890 16891 +16891 16890 16878 16880 16891 16878 16892 16891 16880 +16893 16880 16881 16892 16880 16893 16894 16881 16882 +16893 16881 16894 16895 16882 16883 16894 16882 16895 +16896 16883 16884 16895 16883 16896 16897 16884 16885 +16896 16884 16897 16885 16886 16898 16899 16885 16898 +16897 16885 16899 16900 16899 16898 16901 16902 16903 +16902 16887 16903 16887 16450 16903 16887 16448 16450 +16888 16451 16449 16904 16451 16888 16905 16906 16907 +16907 16906 16889 16891 16907 16889 16908 16907 16891 +16909 16891 16892 16908 16891 16909 16910 16892 16893 +16909 16892 16910 16911 16893 16894 16910 16893 16911 +16912 16894 16895 16911 16894 16912 16913 16895 16896 +16912 16895 16913 16914 16896 16897 16913 16896 16914 +16915 16897 16899 16914 16897 16915 16899 16900 16916 +16917 16899 16916 16915 16899 16917 16918 16917 16916 +16901 16903 16919 16901 16919 16920 16452 16919 16450 +16450 16919 16903 16921 16453 16922 16453 16904 16922 +16453 16451 16904 16923 16921 16922 16924 16925 16926 +16926 16925 16905 16907 16926 16905 16927 16926 16907 +16454 16927 16455 16927 16908 16455 16927 16907 16908 +16455 16908 16457 16457 16908 16909 16457 16909 16459 +16459 16909 16910 16459 16910 16461 16461 16910 16911 +16461 16911 16463 16463 16911 16912 16463 16912 16465 +16465 16912 16913 16465 16913 16467 16467 16913 16914 +16467 16914 16469 16469 16914 16915 16469 16915 16471 +16471 16915 16917 16928 16929 16473 16928 16473 16471 +16918 16928 16471 16917 16918 16471 16930 16931 16929 +16930 16929 16928 16932 16931 16930 16920 16919 16933 +16920 16933 16934 16474 16933 16452 16452 16933 16919 +16921 16475 16453 16476 16475 16921 16923 16476 16921 +16935 16476 16923 16936 16937 16938 16938 16937 16924 +16926 16938 16924 16939 16938 16926 16477 16939 16478 +16939 16927 16478 16939 16926 16927 16478 16927 16454 +16929 16487 16473 16487 16929 16489 16489 16929 16931 +16940 16941 16491 16940 16491 16489 16932 16940 16489 +16931 16932 16489 16942 16941 16940 16934 16933 16493 +16492 16934 16493 16943 16934 16492 16493 16933 16474 +16944 16495 16945 16495 16935 16945 16495 16476 16935 +16946 16944 16945 16947 16948 16949 16949 16948 16936 +16938 16949 16936 16950 16949 16938 16496 16950 16497 +16950 16939 16497 16950 16938 16939 16497 16939 16477 +16951 16501 16941 16941 16501 16491 16941 16942 16952 +16953 16941 16952 16951 16941 16953 16954 16955 16953 +16954 16953 16952 16956 16957 16955 16956 16955 16954 +16958 16959 16957 16958 16957 16956 16958 16943 16959 +16943 16503 16959 16943 16492 16503 16960 16504 16944 +16944 16504 16495 16944 16946 16961 16944 16961 16960 +16962 16963 16964 16964 16963 16947 16964 16947 16949 +16505 16964 16949 16506 16505 16949 16506 16949 16508 +16508 16949 16950 16508 16950 16496 16951 16510 16501 +16510 16951 16512 16512 16951 16953 16512 16953 16514 +16953 16965 16514 16953 16955 16965 16966 16955 16957 +16965 16955 16966 16515 16966 16516 16966 16959 16516 +16966 16957 16959 16516 16959 16503 16960 16519 16504 +16520 16519 16960 16961 16520 16960 16967 16520 16961 +16968 16969 16970 16970 16969 16962 16970 16962 16964 +16521 16970 16964 16522 16521 16964 16522 16964 16505 +16965 16526 16514 16526 16965 16528 16528 16965 16966 +16528 16966 16515 16971 16531 16972 16531 16967 16972 +16531 16520 16967 16973 16974 16971 16973 16971 16972 +16975 16976 16974 16975 16974 16973 16977 16978 16976 +16977 16976 16975 16978 16977 16968 16970 16978 16968 +16979 16978 16970 16533 16979 16521 16521 16979 16970 +16971 16535 16531 16535 16971 16536 16971 16980 16536 +16971 16974 16980 16537 16980 16538 16980 16976 16538 +16980 16974 16976 16538 16976 16540 16540 16976 16978 +16540 16978 16542 16542 16978 16979 16542 16979 16533 +16980 16544 16536 16544 16980 16537 16981 16982 16983 +16546 16545 16983 16545 16981 16983 16545 16984 16981 +16545 16548 16984 16984 16548 16985 16986 16987 16549 +16549 16987 16550 16553 16983 16982 16986 16553 16982 +16549 16553 16986 16983 16553 16546 16988 16985 16548 +16988 16548 16555 16989 16555 16557 16989 16988 16555 +16990 16557 16559 16990 16989 16557 16991 16992 16560 +16992 16559 16560 16992 16990 16559 16991 16993 16992 +16994 16995 16561 16561 16995 16562 16561 16550 16987 +16561 16987 16994 16565 16991 16560 16996 16997 16567 +16567 16997 16565 16997 16993 16565 16993 16991 16565 +16996 16998 16997 16999 17000 16568 16568 17000 16569 +16568 16562 16995 16568 16995 16999 16572 16996 16567 +16572 16574 16996 16574 16998 16996 16574 17001 16998 +17002 17003 16575 16575 17003 16576 17004 16575 16579 +17004 17002 16575 16579 16569 17000 16579 17000 17004 +16581 17001 16574 16581 17005 17001 17006 16583 16582 +17007 17006 16582 16576 17007 16582 17003 17007 16576 +17008 17009 16587 17008 16587 16586 16581 16587 17005 +17005 16587 17009 17010 17011 17006 17011 16583 17006 +17011 16588 16583 17010 17006 17007 16590 17008 16586 +16590 17012 17008 17013 17014 16591 16591 17014 16592 +16591 16588 17011 16591 17011 17013 16593 17015 16594 +17015 17016 16594 17016 16590 16594 17016 17012 16590 +17017 17018 16597 16597 17018 16596 16597 16592 17014 +16597 17014 17017 17019 17020 17015 16593 17019 17015 +16599 17019 16593 17015 17020 17016 17021 17022 16600 +16600 17022 16601 16600 16596 17018 16600 17018 17021 +17023 17024 16603 17023 16603 16602 16599 16603 17019 +17019 16603 17024 17022 16605 16601 17025 16605 17022 +17026 17027 17028 17027 17023 17028 17023 16606 17028 +17023 16602 16606 17029 17030 16607 16607 17030 16608 +16607 16605 17025 16607 17025 17029 17026 17028 16610 +16609 17026 16610 17031 17026 16609 16610 17028 16606 +17032 16613 16612 17033 17032 16612 16608 17033 16612 +17030 17033 16608 17034 17035 17036 17035 17031 17036 +17031 16615 17036 17031 16609 16615 17037 17038 17032 +17038 16613 17032 17038 16616 16613 17037 17032 17033 +17034 17036 17039 17034 17039 17040 16618 17039 16615 +16615 17039 17036 17041 16620 16619 17041 16619 17042 +16619 17043 17042 16619 16622 17043 16622 16616 17038 +16622 17038 17043 17040 17039 17044 17040 17044 17045 +16624 17044 16618 16618 17044 17039 17046 17047 17041 +17047 16620 17041 17047 16626 16620 17046 17041 17042 +17048 17049 17050 16628 16627 17050 17049 16628 17050 +17051 16628 17049 17044 16630 17045 16630 17051 17045 +16630 16628 17051 16630 17044 16624 17052 16632 16631 +17053 17052 16631 16626 17053 16631 17047 17053 16626 +17048 17050 16634 16633 17048 16634 17054 17048 16633 +16634 17050 16627 17055 16636 17052 17052 16636 16632 +17052 17053 17056 17052 17056 17055 16638 17054 16633 +16638 17057 17054 17055 16640 16636 17058 17059 16641 +17058 16641 16640 17056 17058 16640 17055 17056 16640 +17060 17059 17058 17061 17062 17063 17062 17057 17063 +17057 16643 17063 17057 16638 16643 17064 16645 17059 +17059 16645 16641 17059 17060 17065 17066 17059 17065 +17064 17059 17066 17067 17068 17066 17067 17066 17065 +17069 17068 17067 17061 17063 17070 17061 17070 17071 +16647 17070 16643 16643 17070 17063 17064 16648 16645 +16648 17064 16650 17064 17072 16650 17064 17066 17072 +17073 17066 17068 17072 17066 17073 17068 17069 17074 +17068 17074 17073 17071 17070 17075 17071 17075 17076 +16652 17075 16647 16647 17075 17070 17072 16654 16650 +16654 17072 16656 16656 17072 17073 17077 17078 16658 +17077 16658 16656 17074 17077 16656 17073 17074 16656 +17079 17080 17078 17079 17078 17077 17081 17080 17079 +17076 17075 17082 17076 17082 17083 16660 17082 16652 +16652 17082 17075 17078 16663 16658 16663 17078 16665 +17078 17084 16665 17078 17080 17084 17080 17081 17085 +17086 17080 17085 17084 17080 17086 17087 17086 17085 +17083 17082 17088 17083 17088 17089 16667 17088 16660 +16660 17088 17082 17084 16668 16665 16668 17084 16670 +16670 17084 17086 17090 17091 16672 17090 16672 16670 +17087 17090 16670 17086 17087 16670 17092 17093 17091 +17092 17091 17090 17094 17093 17092 17095 17096 17097 +17098 17099 17097 17098 17097 17096 17099 17098 17089 +17099 17089 17088 16673 17099 17088 16674 16673 17088 +16674 17088 16667 17091 16677 16672 16677 17091 16678 +17091 17100 16678 17091 17093 17100 17093 17094 17101 +17102 17093 17101 17100 17093 17102 16680 16679 17102 +17101 16680 17102 17103 16680 17101 17097 16682 17095 +16682 17103 17095 16682 16680 17103 16682 17097 16684 +16684 17097 17099 16687 16684 17104 16684 17099 17104 +16673 16686 17104 16673 17104 17099 16688 17105 16686 +16686 17105 17104 16689 17105 16688 17100 16695 16678 +16695 17100 16697 16697 17100 17102 16700 16697 17106 +16697 17102 17106 16679 16699 17106 16679 17106 17102 +16701 17107 16699 16699 17107 17106 16702 17108 16701 +16701 17108 17107 16687 17104 16702 17104 17108 16702 +17104 17109 17108 17110 17104 17105 17109 17104 17110 +16704 17110 16689 16689 17110 17105 17111 16712 16711 +16700 17106 16711 17106 17111 16711 17106 17112 17111 +17113 17106 17107 17112 17106 17113 17114 17107 17108 +17113 17107 17114 17115 17108 17109 17114 17108 17115 +17116 17109 17110 17115 17109 17116 16713 17117 16704 +17117 17110 16704 17117 17116 17110 16715 17117 16713 +17118 16721 17111 17111 16721 16712 17119 17111 17112 +17118 17111 17119 17120 17112 17113 17119 17112 17120 +17121 17113 17114 17120 17113 17121 17122 17114 17115 +17121 17114 17122 17116 17123 17124 17122 17116 17124 +17115 17116 17122 17125 17126 17123 17116 17125 17123 +17117 17125 17116 16722 17127 16715 17127 17117 16715 +17127 17125 17117 16723 17127 16722 17128 16729 17118 +17118 16729 16721 17129 17118 17119 17128 17118 17129 +17130 17119 17120 17129 17119 17130 17131 17120 17121 +17130 17120 17131 17122 17132 17133 17131 17122 17133 +17121 17122 17131 17132 17122 17124 17134 17126 17125 +17134 17125 17127 17134 17127 17135 17136 17135 17137 +17137 17135 16730 17135 17127 16730 17127 16723 16730 +16731 17137 16730 17138 16741 17128 17128 16741 16729 +17139 17128 17129 17138 17128 17139 17140 17129 17130 +17139 17129 17140 17131 17141 17142 17140 17131 17142 +17130 17131 17140 17141 17131 17133 17143 17136 17137 +17144 17143 17145 17145 17143 16742 17143 17137 16742 +17137 16731 16742 16743 17145 16742 17146 16745 16744 +16746 17147 16744 16744 17147 17146 16747 17148 16746 +16746 17148 17147 16748 17149 16747 16747 17149 17148 +16749 17150 16748 16748 17150 17149 16750 17151 16749 +16749 17151 17150 16752 17151 16750 17152 16758 17138 +17138 16758 16741 17153 17138 17139 17152 17138 17153 +17154 17139 17140 17153 17139 17154 17140 17142 17155 +17140 17155 17154 17156 17144 17145 17157 17156 17158 +17158 17156 16759 17156 17145 16759 17145 16743 16759 +16760 17158 16759 17159 16762 16761 17160 17159 16761 +17160 16761 16745 17161 17160 16745 17146 17161 16745 +17161 17146 17147 17161 17147 17162 17162 17147 17148 +17162 17148 17163 17163 17148 17149 17163 17149 17164 +17164 17149 17150 17164 17150 17165 17166 17167 17165 +17150 17166 17165 17151 17166 17150 16763 17168 16752 +17168 17151 16752 17168 17166 17151 16764 17169 16763 +16763 17169 17168 16765 17170 16764 16764 17170 17169 +16767 17170 16765 17171 16772 16771 16758 17152 16771 +17152 17171 16771 17152 17172 17171 17173 17152 17153 +17172 17152 17173 17174 17153 17154 17173 17153 17174 +17154 17155 17175 17154 17175 17174 17176 17157 17158 +17158 16760 17176 16760 17177 17176 16760 16773 17177 +17178 16774 17159 17159 16774 16762 17159 17160 17179 +17159 17179 17178 17180 17167 17166 17180 17166 17168 +17180 17168 17181 17182 17183 17181 17168 17182 17181 +17169 17182 17168 17184 17169 17170 17182 17169 17184 +16775 17185 16767 17185 17170 16767 17185 17184 17170 +16777 17185 16775 17186 16781 17171 17171 16781 16772 +17187 17171 17172 17186 17171 17187 17188 17172 17173 +17187 17172 17188 17174 17189 17190 17188 17174 17190 +17173 17174 17188 17189 17174 17175 16782 17177 16773 +16782 17191 17177 17192 17193 17178 17193 16774 17178 +17193 16783 16774 17192 17178 17179 17194 17183 17182 +17195 17196 17194 17182 17195 17194 17184 17195 17182 +17197 17184 17185 17195 17184 17197 16784 17198 16777 +17198 17185 16777 17198 17197 17185 16786 17198 16784 +17186 16791 16781 16791 17186 16793 17186 17199 16793 +17186 17187 17199 17200 17187 17188 17199 17187 17200 +17188 17190 17201 17188 17201 17200 16794 17191 16782 +16794 17202 17191 17193 16795 16783 17203 16795 17193 +17204 17196 17195 17205 17206 17204 17195 17205 17204 +17197 17205 17195 17207 17197 17198 17205 17197 17207 +16797 17207 16786 16786 17207 17198 17199 16803 16793 +17199 17200 17208 16805 17199 17208 17209 16805 17208 +17199 16805 16803 17208 17200 17201 16806 17202 16794 +16806 17210 17202 17203 16807 16795 17211 16807 17203 +17212 17213 17214 17214 17213 17206 17205 17214 17206 +17215 17214 17205 17216 17205 17207 17215 17205 17216 +16809 17216 16797 16797 17216 17207 17209 16816 16805 +17217 16816 17209 16817 17210 16806 16817 17218 17210 +17211 16818 16807 17219 16818 17211 17220 17212 17214 +17220 17214 17215 17220 17215 17221 17222 17223 17221 +17215 17222 17221 17216 17222 17215 16819 17224 16809 +17224 17216 16809 17224 17222 17216 16821 17224 16819 +17225 16826 16825 16827 17226 16825 16825 17226 17225 +16816 17217 17227 17226 16816 17227 17228 17226 17227 +16827 16816 17226 17229 17228 17227 17230 17231 16829 +17230 16829 16828 16817 16829 17218 17218 16829 17231 +17219 16831 16818 17232 16831 17219 17223 17222 17233 +17223 17233 17234 17235 17222 17224 17233 17222 17235 +16832 17236 16821 17236 17224 16821 17236 17235 17224 +16834 17236 16832 17237 16837 16836 16826 17225 16836 +17225 17237 16836 17225 17238 17237 17239 17225 17226 +17238 17225 17239 17240 17226 17228 17239 17226 17240 +17228 17229 17241 17228 17241 17240 16838 17230 16828 +16838 17242 17230 17232 16839 16831 17243 16839 17232 +17244 17234 17233 17245 17246 17244 17233 17245 17244 +17235 17245 17233 17247 17235 17236 17245 17235 17247 +16840 17248 16834 17248 17236 16834 17248 17247 17236 +16841 17249 16840 16840 17249 17248 16837 17237 16841 +17237 17249 16841 17237 17250 17249 17251 17237 17238 +17250 17237 17251 17252 17238 17239 17251 17238 17252 +17253 17239 17240 17252 17239 17253 17240 17241 17254 +17240 17254 17253 16842 17242 16838 16842 17255 17242 +17243 16843 16839 17256 16843 17243 17246 17245 17257 +17246 17257 17258 17259 17245 17247 17257 17245 17259 +17260 17247 17248 17259 17247 17260 17261 17248 17249 +17260 17248 17261 17262 17249 17250 17261 17249 17262 +17263 17250 17251 17262 17250 17263 17264 17251 17252 +17263 17251 17264 17265 17252 17253 17264 17252 17265 +17253 17254 17266 17253 17266 17265 16844 17255 16842 +16844 17267 17255 17256 17268 16843 16843 17268 16845 +16846 16845 17268 16846 17268 17269 17258 17257 17270 +17258 17270 17271 17272 17257 17259 17270 17257 17272 +17273 17259 17260 17272 17259 17273 17274 17260 17261 +17273 17260 17274 17275 17261 17262 17274 17261 17275 +17276 17262 17263 17275 17262 17276 17277 17263 17264 +17276 17263 17277 17278 17264 17265 17277 17264 17278 +17265 17266 17279 17265 17279 17278 16847 17267 16844 +16847 17280 17267 17269 16848 16846 17281 16848 17269 +17271 17270 17282 17271 17282 17283 17284 17270 17272 +17282 17270 17284 17285 17272 17273 17284 17272 17285 +17286 17273 17274 17285 17273 17286 17287 17274 17275 +17286 17274 17287 17288 17275 17276 17287 17275 17288 +17289 17276 17277 17288 17276 17289 17290 17277 17278 +17289 17277 17290 17278 17279 17291 17278 17291 17290 +16849 17280 16847 16849 17292 17280 17281 16850 16848 +17293 16850 17281 17283 17282 17294 17283 17294 17295 +17296 17282 17284 17294 17282 17296 17297 17284 17285 +17296 17284 17297 17298 17285 17286 17297 17285 17298 +17299 17286 17287 17298 17286 17299 17300 17287 17288 +17299 17287 17300 17301 17288 17289 17300 17288 17301 +17302 17289 17290 17301 17289 17302 17290 17291 17303 +17290 17303 17302 16851 17292 16849 16851 17304 17292 +17293 16852 16850 17305 16852 17293 17295 17294 17306 +17295 17306 17307 17308 17294 17296 17306 17294 17308 +17309 17296 17297 17308 17296 17309 17310 17297 17298 +17309 17297 17310 17311 17298 17299 17310 17298 17311 +17312 17299 17300 17311 17299 17312 17313 17300 17301 +17312 17300 17313 17314 17301 17302 17313 17301 17314 +17302 17303 17315 17316 17302 17315 17314 17302 17316 +17317 17316 17315 16853 17304 16851 16853 17318 17304 +17305 16854 16852 17319 16854 17305 17307 17306 17320 +17307 17320 17321 17322 17306 17308 17320 17306 17322 +17323 17308 17309 17322 17308 17323 16855 17323 16856 +17323 17310 16856 17323 17309 17310 16856 17310 16858 +16858 17310 17311 16858 17311 16860 17311 17324 16860 +17311 17312 17324 17325 17312 17313 17324 17312 17325 +17326 17313 17314 17325 17313 17326 17327 17314 17316 +17326 17314 17327 17316 17317 17328 17316 17328 17327 +16853 16861 17329 17330 16853 17329 17318 16853 17330 +16862 17329 16861 17331 17332 16863 16863 17332 16864 +16863 16854 17319 16863 17319 17331 17333 17334 17335 +17335 17334 17321 17320 17335 17321 17336 17335 17320 +17337 17320 17322 17336 17320 17337 16866 17337 16867 +17337 17323 16867 17337 17322 17323 16867 17323 16855 +17324 16871 16860 16871 17324 16873 16873 17324 17325 +16873 17325 16875 17325 17338 16875 17325 17326 17338 +17339 17326 17327 17338 17326 17339 17327 17328 17340 +17341 17327 17340 17339 17327 17341 17342 17341 17340 +17343 17330 17329 17329 16862 17343 16862 17344 17343 +16862 16876 17344 17332 16877 16864 17345 16877 17332 +17346 17347 17348 17348 17347 17333 17335 17348 17333 +17349 17348 17335 17350 17335 17336 17349 17335 17350 +16878 17350 16879 17350 17337 16879 17350 17336 17337 +16879 17337 16866 17351 16886 17338 17338 16886 16875 +17352 17338 17339 17351 17338 17352 17353 17339 17341 +17352 17339 17353 17341 17342 17354 17341 17354 17353 +17355 17356 17357 17358 17359 17357 17358 17357 17356 +17358 17344 17359 17344 16887 17359 17344 16876 16887 +17345 16888 16877 17360 16888 17345 17346 17348 17361 +17346 17361 17362 17363 17348 17349 17361 17348 17363 +16889 17363 16890 17363 17350 16890 17363 17349 17350 +16890 17350 16878 17351 16898 16886 16898 17351 16900 +17351 17364 16900 17351 17352 17364 17365 17352 17353 +17364 17352 17365 17353 17354 17366 17367 17353 17366 +17365 17353 17367 17368 17367 17366 17355 17357 17369 +17355 17369 17370 16901 17369 16902 17369 17359 16902 +17369 17357 17359 16902 17359 16887 17371 16904 17372 +16904 17360 17372 16904 16888 17360 17373 17371 17372 +17374 17375 17376 17376 17375 17362 17361 17376 17362 +17377 17376 17361 16905 17377 16906 17377 17363 16906 +17377 17361 17363 16906 17363 16889 17364 16916 16900 +16916 17364 16918 17364 17378 16918 17364 17365 17378 +17379 17365 17367 17378 17365 17379 17367 17368 17380 +17367 17380 17379 17370 17369 17381 17370 17381 17382 +16920 17381 16901 16901 17381 17369 17371 16922 16904 +16923 16922 17371 17373 16923 17371 17383 16923 17373 +17384 17385 17386 17386 17385 17374 17376 17386 17374 +17387 17386 17376 16924 17387 16925 17387 17377 16925 +17387 17376 17377 16925 17377 16905 17378 16928 16918 +16928 17378 16930 16930 17378 17379 17388 17389 16932 +17388 16932 16930 17380 17388 16930 17379 17380 16930 +17390 17389 17388 17391 17392 17393 17393 17392 17382 +17381 17393 17382 17394 17393 17381 16934 17394 16920 +16920 17394 17381 17383 16935 16923 17395 16935 17383 +17384 17386 17396 17384 17396 17397 16936 17396 16937 +17396 17387 16937 17396 17386 17387 16937 17387 16924 +17389 16940 16932 17398 17399 16942 17398 16942 16940 +17390 17398 16940 17389 17390 16940 17400 17401 17399 +17400 17399 17398 17401 17400 17391 17393 17401 17391 +17402 17401 17393 17403 17393 17394 17402 17393 17403 +16943 17403 16934 16934 17403 17394 17395 17404 16935 +16935 17404 16945 16946 16945 17404 16946 17404 17405 +17406 17407 17408 17408 17407 17397 17408 17397 17396 +16947 17408 17396 16948 16947 17396 16948 17396 16936 +17399 16952 16942 16952 17399 16954 16954 17399 17401 +16954 17401 16956 16956 17401 17402 16956 17402 16958 +16958 17402 17403 16958 17403 16943 17409 16961 17410 +16961 17405 17410 16961 16946 17405 17411 17412 17409 +17411 17409 17410 17413 17412 17411 17414 17415 17416 +17416 17415 17406 17416 17406 17408 16962 17416 17408 +16963 16962 17408 16963 17408 16947 17417 16967 17409 +17409 16967 16961 17418 17409 17412 17417 17409 17418 +17412 17413 17419 17420 17412 17419 17418 17412 17420 +17421 17422 17420 17421 17420 17419 17422 17421 17414 +17422 17414 17416 16968 17422 17416 16969 16968 17416 +16969 17416 16962 17417 16972 16967 16972 17417 16973 +16973 17417 17418 16973 17418 16975 16975 17418 17420 +16975 17420 16977 16977 17420 17422 16977 17422 16968 +17423 17424 16981 16981 17424 16982 17425 16981 16984 +17425 17423 16981 16984 16985 17425 17425 16985 17426 +17427 17428 16986 16986 17428 16987 16986 16982 17424 +16986 17424 17427 17429 17426 16985 17429 16985 16988 +17430 16988 16989 17430 17429 16988 17431 16989 16990 +17431 17430 16989 17432 16990 16992 17432 17431 16990 +16992 16993 17432 17432 16993 17433 17434 16995 16994 +17435 17434 16994 16987 17435 16994 17428 17435 16987 +17436 17433 16993 17436 16993 16997 16997 16998 17436 +17436 16998 17437 17438 17000 16999 17439 17438 16999 +17439 16999 16995 17440 17439 16995 17434 17440 16995 +17440 17434 17435 17001 17437 16998 17001 17441 17437 +17442 17003 17002 17442 17002 17443 17002 17444 17443 +17002 17004 17444 17004 17000 17438 17445 17004 17438 +17444 17004 17445 17445 17438 17439 17446 17447 17448 +17447 17441 17448 17441 17005 17448 17441 17001 17005 +17449 17450 17442 17450 17003 17442 17450 17007 17003 +17449 17442 17443 17446 17448 17009 17008 17446 17009 +17451 17446 17008 17009 17448 17005 17452 17453 17010 +17010 17453 17011 17010 17007 17450 17010 17450 17452 +17012 17451 17008 17012 17454 17451 17455 17456 17013 +17013 17456 17014 17013 17011 17453 17013 17453 17455 +17457 17458 17459 17458 17454 17459 17454 17016 17459 +17454 17012 17016 17460 17461 17017 17017 17461 17018 +17017 17014 17456 17017 17456 17460 17462 17463 17464 +17465 17464 17463 17457 17459 17020 17019 17457 17020 +17466 17457 17019 17020 17459 17016 17467 17022 17021 +17468 17467 17021 17018 17468 17021 17461 17468 17018 +17469 17470 17471 17471 17470 17462 17464 17471 17462 +17472 17471 17464 17473 17024 17023 17464 17473 17023 +17472 17464 17023 17473 17464 17465 17019 17024 17466 +17466 17024 17473 17474 17475 17467 17475 17022 17467 +17475 17025 17022 17474 17467 17468 17469 17471 17476 +17469 17476 17477 17026 17476 17027 17476 17472 17027 +17476 17471 17472 17027 17472 17023 17478 17479 17029 +17029 17479 17030 17029 17025 17475 17029 17475 17478 +17477 17476 17480 17477 17480 17481 17031 17480 17026 +17026 17480 17476 17479 17033 17030 17482 17033 17479 +17483 17484 17485 17485 17484 17481 17485 17481 17480 +17034 17485 17480 17035 17034 17480 17035 17480 17031 +17486 17487 17037 17037 17487 17038 17037 17033 17482 +17037 17482 17486 17483 17485 17488 17483 17488 17489 +17040 17488 17034 17034 17488 17485 17490 17491 17043 +17043 17491 17042 17043 17038 17487 17043 17487 17490 +17492 17493 17494 17494 17493 17489 17488 17494 17489 +17495 17494 17488 17045 17495 17040 17040 17495 17488 +17496 17047 17046 17497 17496 17046 17042 17497 17046 +17491 17497 17042 17492 17494 17049 17048 17492 17049 +17498 17492 17048 17049 17494 17051 17051 17494 17495 +17051 17495 17045 17499 17053 17496 17496 17053 17047 +17496 17497 17500 17496 17500 17499 17501 17502 17503 +17502 17498 17503 17498 17054 17503 17498 17048 17054 +17504 17056 17499 17499 17056 17053 17499 17500 17505 +17499 17505 17504 17501 17503 17506 17501 17506 17507 +17057 17506 17054 17054 17506 17503 17504 17058 17056 +17508 17509 17060 17508 17060 17058 17505 17508 17058 +17504 17505 17058 17510 17511 17509 17510 17509 17508 +17512 17511 17510 17507 17506 17062 17061 17507 17062 +17513 17507 17061 17062 17506 17057 17509 17065 17060 +17065 17509 17067 17067 17509 17511 17069 17067 17511 +17512 17069 17511 17514 17069 17512 17071 17513 17061 +17071 17515 17513 17516 17074 17517 17074 17514 17517 +17074 17069 17514 17518 17519 17516 17518 17516 17517 +17520 17521 17519 17520 17519 17518 17522 17521 17520 +17523 17524 17525 17524 17515 17525 17515 17076 17525 +17515 17071 17076 17516 17077 17074 17077 17516 17079 +17079 17516 17519 17079 17519 17081 17519 17526 17081 +17519 17521 17526 17521 17522 17527 17528 17521 17527 +17526 17521 17528 17529 17528 17527 17523 17525 17530 +17523 17530 17531 17083 17530 17076 17076 17530 17525 +17526 17085 17081 17085 17526 17087 17526 17532 17087 +17526 17528 17532 17528 17529 17533 17534 17528 17533 +17532 17528 17534 17535 17536 17534 17535 17534 17533 +17537 17538 17536 17537 17536 17535 17539 17540 17538 +17539 17538 17537 17540 17539 17531 17530 17540 17531 +17541 17540 17530 17089 17541 17083 17083 17541 17530 +17532 17090 17087 17090 17532 17092 17092 17532 17534 +17092 17534 17094 17534 17542 17094 17534 17536 17542 +17543 17536 17538 17542 17536 17543 17095 17543 17096 +17543 17540 17096 17543 17538 17540 17096 17540 17098 +17098 17540 17541 17098 17541 17089 17542 17101 17094 +17101 17542 17103 17103 17542 17543 17103 17543 17095 +17544 17124 17123 17126 17544 17123 17545 17133 17132 +17124 17544 17132 17544 17545 17132 17544 17546 17545 +17134 17547 17126 17547 17544 17126 17547 17546 17544 +17135 17548 17134 17134 17548 17547 17136 17548 17135 +17549 17142 17141 17133 17545 17141 17545 17549 17141 +17545 17550 17549 17551 17545 17546 17550 17545 17551 +17552 17546 17547 17551 17546 17552 17553 17547 17548 +17552 17547 17553 17143 17554 17136 17554 17548 17136 +17554 17553 17548 17144 17554 17143 17555 17155 17549 +17549 17155 17142 17556 17549 17550 17555 17549 17556 +17557 17550 17551 17556 17550 17557 17552 17558 17559 +17557 17552 17559 17551 17552 17557 17558 17552 17553 +17558 17553 17560 17560 17553 17554 17560 17554 17561 +17562 17561 17563 17563 17561 17156 17561 17554 17156 +17554 17144 17156 17157 17563 17156 17564 17160 17161 +17162 17565 17161 17161 17565 17564 17163 17566 17162 +17162 17566 17565 17164 17567 17163 17163 17567 17566 +17165 17568 17164 17164 17568 17567 17167 17568 17165 +17569 17175 17555 17555 17175 17155 17570 17555 17556 +17569 17555 17570 17571 17556 17557 17570 17556 17571 +17557 17559 17572 17557 17572 17571 17573 17562 17563 +17574 17573 17575 17575 17573 17176 17573 17563 17176 +17563 17157 17176 17177 17575 17176 17576 17179 17564 +17564 17179 17160 17577 17564 17565 17576 17564 17577 +17578 17565 17566 17577 17565 17578 17579 17566 17567 +17578 17566 17579 17580 17567 17568 17579 17567 17580 +17180 17581 17167 17581 17568 17167 17581 17580 17568 +17181 17582 17180 17180 17582 17581 17183 17582 17181 +17583 17190 17189 17175 17569 17189 17569 17583 17189 +17569 17584 17583 17585 17569 17570 17584 17569 17585 +17586 17570 17571 17585 17570 17586 17571 17572 17587 +17571 17587 17586 17574 17575 17588 17574 17588 17589 +17191 17588 17177 17177 17588 17575 17590 17193 17192 +17591 17590 17192 17591 17192 17179 17592 17591 17179 +17576 17592 17179 17592 17576 17577 17592 17577 17593 +17593 17577 17578 17593 17578 17594 17594 17578 17579 +17594 17579 17595 17596 17597 17595 17579 17596 17595 +17580 17596 17579 17598 17580 17581 17596 17580 17598 +17599 17581 17582 17598 17581 17599 17194 17600 17183 +17600 17582 17183 17600 17599 17582 17196 17600 17194 +17601 17201 17583 17583 17201 17190 17602 17583 17584 +17601 17583 17602 17603 17584 17585 17602 17584 17603 +17586 17604 17605 17603 17586 17605 17585 17586 17603 +17604 17586 17587 17589 17588 17606 17589 17606 17607 +17202 17606 17191 17191 17606 17588 17608 17609 17590 +17609 17193 17590 17609 17203 17193 17608 17590 17591 +17610 17597 17596 17611 17612 17610 17596 17611 17610 +17598 17611 17596 17613 17598 17599 17611 17598 17613 +17614 17599 17600 17613 17599 17614 17204 17615 17196 +17615 17600 17196 17615 17614 17600 17206 17615 17204 +17616 17209 17208 17201 17601 17208 17601 17616 17208 +17601 17617 17616 17618 17601 17602 17617 17601 17618 +17603 17619 17620 17618 17603 17620 17602 17603 17618 +17619 17603 17605 17607 17606 17621 17607 17621 17622 +17210 17621 17202 17202 17621 17606 17609 17211 17203 +17623 17211 17609 17624 17612 17611 17625 17626 17624 +17611 17625 17624 17613 17625 17611 17627 17613 17614 +17625 17613 17627 17212 17627 17213 17627 17615 17213 +17627 17614 17615 17213 17615 17206 17628 17217 17616 +17616 17217 17209 17629 17616 17617 17628 17616 17629 +17618 17630 17631 17629 17618 17631 17617 17618 17629 +17630 17618 17620 17632 17633 17634 17634 17633 17622 +17621 17634 17622 17635 17634 17621 17218 17635 17210 +17210 17635 17621 17623 17219 17211 17636 17219 17623 +17637 17626 17625 17638 17639 17637 17625 17638 17637 +17627 17638 17625 17220 17640 17212 17640 17627 17212 +17640 17638 17627 17221 17641 17220 17220 17641 17640 +17223 17641 17221 17628 17227 17217 17227 17628 17229 +17628 17642 17229 17628 17629 17642 17629 17631 17643 +17629 17643 17642 17644 17632 17634 17230 17645 17231 +17645 17634 17231 17634 17635 17231 17645 17644 17634 +17231 17635 17218 17636 17232 17219 17646 17232 17636 +17647 17639 17638 17648 17649 17647 17638 17648 17647 +17640 17648 17638 17650 17640 17641 17648 17640 17650 +17234 17650 17223 17223 17650 17641 17651 17241 17642 +17642 17241 17229 17642 17643 17652 17642 17652 17651 +17242 17645 17230 17242 17653 17645 17646 17243 17232 +17654 17243 17646 17649 17648 17655 17649 17655 17656 +17657 17648 17650 17655 17648 17657 17244 17658 17234 +17658 17650 17234 17658 17657 17650 17246 17658 17244 +17659 17254 17651 17651 17254 17241 17651 17652 17660 +17661 17651 17660 17659 17651 17661 17662 17661 17660 +17255 17653 17242 17255 17663 17653 17654 17256 17243 +17664 17256 17654 17656 17655 17665 17656 17665 17666 +17667 17655 17657 17665 17655 17667 17668 17657 17658 +17667 17657 17668 17258 17668 17246 17246 17668 17658 +17669 17266 17659 17659 17266 17254 17670 17659 17661 +17669 17659 17670 17661 17662 17671 17661 17671 17670 +17267 17663 17255 17267 17672 17663 17664 17673 17256 +17256 17673 17268 17269 17268 17673 17269 17673 17674 +17675 17666 17665 17676 17677 17675 17665 17676 17675 +17667 17676 17665 17678 17667 17668 17676 17667 17678 +17271 17678 17258 17258 17678 17668 17679 17279 17669 +17669 17279 17266 17680 17669 17670 17679 17669 17680 +17670 17671 17681 17682 17670 17681 17680 17670 17682 +17683 17682 17681 17280 17672 17267 17280 17684 17672 +17674 17281 17269 17685 17281 17674 17677 17676 17686 +17677 17686 17687 17688 17676 17678 17686 17676 17688 +17283 17688 17271 17271 17688 17678 17689 17291 17679 +17679 17291 17279 17690 17679 17680 17689 17679 17690 +17691 17680 17682 17690 17680 17691 17682 17683 17692 +17693 17682 17692 17691 17682 17693 17694 17693 17692 +17292 17684 17280 17292 17695 17684 17685 17293 17281 +17696 17293 17685 17687 17686 17697 17687 17697 17698 +17699 17686 17688 17697 17686 17699 17295 17699 17283 +17283 17699 17688 17700 17303 17689 17689 17303 17291 +17701 17689 17690 17700 17689 17701 17702 17690 17691 +17701 17690 17702 17703 17691 17693 17702 17691 17703 +17693 17694 17704 17693 17704 17703 17304 17695 17292 +17304 17705 17695 17696 17305 17293 17706 17305 17696 +17707 17708 17709 17709 17708 17698 17697 17709 17698 +17710 17709 17697 17711 17697 17699 17710 17697 17711 +17307 17711 17295 17295 17711 17699 17700 17315 17303 +17315 17700 17317 17700 17712 17317 17700 17701 17712 +17713 17701 17702 17712 17701 17713 17714 17702 17703 +17713 17702 17714 17703 17704 17715 17703 17715 17714 +17318 17705 17304 17318 17716 17705 17717 17718 17719 +17706 17717 17719 17319 17706 17719 17305 17706 17319 +17707 17709 17720 17707 17720 17721 17722 17709 17710 +17720 17709 17722 17723 17710 17711 17722 17710 17723 +17321 17723 17307 17307 17723 17711 17724 17328 17712 +17712 17328 17317 17725 17712 17713 17724 17712 17725 +17726 17713 17714 17725 17713 17726 17714 17715 17727 +17714 17727 17726 17330 17716 17318 17330 17728 17716 +17719 17718 17331 17718 17332 17331 17718 17729 17332 +17719 17331 17319 17730 17731 17732 17732 17731 17721 +17720 17732 17721 17733 17732 17720 17734 17720 17722 +17733 17720 17734 17333 17734 17334 17734 17723 17334 +17734 17722 17723 17334 17723 17321 17724 17340 17328 +17340 17724 17342 17724 17735 17342 17724 17725 17735 +17736 17725 17726 17735 17725 17736 17726 17727 17737 +17738 17726 17737 17736 17726 17738 17739 17740 17738 +17739 17738 17737 17741 17742 17740 17741 17740 17739 +17743 17742 17741 17330 17343 17744 17745 17330 17744 +17728 17330 17745 17344 17744 17343 17729 17345 17332 +17746 17345 17729 17730 17732 17747 17730 17747 17748 +17749 17732 17733 17747 17732 17749 17346 17749 17347 +17749 17734 17347 17749 17733 17734 17347 17734 17333 +17750 17354 17735 17735 17354 17342 17751 17735 17736 +17750 17735 17751 17752 17736 17738 17751 17736 17752 +17753 17738 17740 17752 17738 17753 17754 17740 17742 +17753 17740 17754 17755 17356 17355 17742 17755 17355 +17754 17742 17355 17755 17742 17743 17744 17358 17745 +17358 17755 17745 17358 17356 17755 17358 17744 17344 +17756 17360 17757 17360 17746 17757 17360 17345 17746 +17758 17756 17757 17759 17760 17761 17761 17760 17748 +17747 17761 17748 17762 17761 17747 17763 17747 17749 +17762 17747 17763 17362 17763 17346 17346 17763 17749 +17750 17366 17354 17366 17750 17368 17750 17764 17368 +17750 17751 17764 17765 17751 17752 17764 17751 17765 +17766 17752 17753 17765 17752 17766 17767 17753 17754 +17766 17753 17767 17370 17767 17355 17355 17767 17754 +17756 17372 17360 17373 17372 17756 17758 17373 17756 +17768 17373 17758 17769 17770 17771 17771 17770 17759 +17761 17771 17759 17772 17771 17761 17773 17761 17762 +17772 17761 17773 17374 17773 17375 17773 17763 17375 +17773 17762 17763 17375 17763 17362 17774 17380 17764 +17764 17380 17368 17775 17764 17765 17774 17764 17775 +17776 17765 17766 17775 17765 17776 17777 17766 17767 +17776 17766 17777 17382 17777 17370 17370 17777 17767 +17778 17383 17779 17383 17768 17779 17383 17373 17768 +17780 17778 17779 17769 17771 17781 17769 17781 17782 +17783 17771 17772 17781 17771 17783 17384 17783 17385 +17783 17773 17385 17783 17772 17773 17385 17773 17374 +17774 17388 17380 17388 17774 17390 17774 17784 17390 +17774 17775 17784 17785 17775 17776 17784 17775 17785 +17391 17785 17392 17785 17777 17392 17785 17776 17777 +17392 17777 17382 17786 17395 17778 17778 17395 17383 +17778 17780 17787 17788 17778 17787 17786 17778 17788 +17789 17788 17787 17790 17791 17792 17792 17791 17782 +17781 17792 17782 17793 17792 17781 17794 17781 17783 +17793 17781 17794 17397 17794 17384 17384 17794 17783 +17784 17398 17390 17398 17784 17400 17400 17784 17785 +17400 17785 17391 17786 17404 17395 17404 17786 17405 +17786 17795 17405 17786 17788 17795 17788 17789 17796 +17788 17796 17795 17790 17792 17797 17790 17797 17798 +17799 17792 17793 17797 17792 17799 17406 17799 17407 +17799 17794 17407 17799 17793 17794 17407 17794 17397 +17795 17410 17405 17796 17800 17795 17800 17410 17795 +17800 17411 17410 17801 17802 17413 17411 17801 17413 +17800 17801 17411 17802 17801 17798 17797 17802 17798 +17803 17802 17797 17414 17803 17415 17803 17799 17415 +17803 17797 17799 17415 17799 17406 17802 17419 17413 +17419 17802 17421 17421 17802 17803 17421 17803 17414 +17804 17424 17423 17425 17805 17423 17423 17805 17804 +17426 17805 17425 17806 17807 17427 17427 17807 17428 +17427 17424 17804 17808 17427 17804 17806 17427 17808 +17809 17810 17808 17804 17809 17808 17805 17809 17804 +17429 17811 17426 17811 17805 17426 17811 17809 17805 +17430 17812 17429 17429 17812 17811 17431 17813 17430 +17430 17813 17812 17432 17814 17431 17431 17814 17813 +17433 17814 17432 17815 17435 17816 17435 17807 17816 +17435 17428 17807 17817 17815 17816 17818 17810 17809 +17818 17809 17811 17818 17811 17819 17819 17811 17812 +17819 17812 17820 17820 17812 17813 17820 17813 17821 +17821 17813 17814 17821 17814 17822 17823 17822 17824 +17824 17822 17436 17822 17814 17436 17814 17433 17436 +17437 17824 17436 17825 17439 17440 17435 17815 17440 +17815 17825 17440 17815 17826 17825 17815 17817 17827 +17815 17827 17826 17823 17824 17828 17823 17828 17829 +17441 17828 17437 17437 17828 17824 17830 17443 17444 +17445 17831 17444 17444 17831 17830 17439 17825 17445 +17825 17831 17445 17825 17832 17831 17833 17825 17826 +17832 17825 17833 17826 17827 17834 17835 17826 17834 +17833 17826 17835 17836 17835 17834 17829 17828 17447 +17446 17829 17447 17837 17829 17446 17447 17828 17441 +17838 17450 17449 17443 17830 17449 17830 17838 17449 +17830 17839 17838 17840 17830 17831 17839 17830 17840 +17841 17831 17832 17840 17831 17841 17842 17832 17833 +17841 17832 17842 17843 17833 17835 17842 17833 17843 +17835 17836 17844 17845 17835 17844 17843 17835 17845 +17846 17845 17844 17847 17848 17849 17848 17837 17849 +17837 17451 17849 17837 17446 17451 17850 17453 17452 +17450 17838 17452 17838 17850 17452 17838 17851 17850 +17852 17838 17839 17851 17838 17852 17853 17839 17840 +17852 17839 17853 17854 17840 17841 17853 17840 17854 +17855 17841 17842 17854 17841 17855 17856 17842 17843 +17855 17842 17856 17857 17843 17845 17856 17843 17857 +17845 17846 17858 17859 17845 17858 17857 17845 17859 +17860 17859 17858 17847 17849 17861 17847 17861 17862 +17454 17861 17451 17451 17861 17849 17863 17456 17455 +17864 17863 17455 17864 17455 17453 17865 17864 17453 +17850 17865 17453 17865 17850 17851 17865 17851 17866 +17866 17851 17852 17866 17852 17867 17867 17852 17853 +17867 17853 17868 17868 17853 17854 17868 17854 17869 +17870 17871 17869 17854 17870 17869 17855 17870 17854 +17872 17855 17856 17870 17855 17872 17873 17856 17857 +17872 17856 17873 17874 17857 17859 17873 17857 17874 +17859 17860 17875 17859 17875 17874 17876 17877 17878 +17878 17877 17862 17878 17862 17861 17457 17878 17861 +17458 17457 17861 17458 17861 17454 17879 17461 17460 +17880 17879 17460 17880 17460 17456 17881 17880 17456 +17863 17881 17456 17881 17863 17864 17882 17871 17870 +17883 17884 17882 17870 17883 17882 17872 17883 17870 +17885 17872 17873 17883 17872 17885 17886 17873 17874 +17885 17873 17886 17887 17463 17462 17874 17887 17462 +17886 17874 17462 17887 17874 17875 17878 17888 17465 +17887 17878 17465 17463 17887 17465 17876 17878 17887 +17466 17888 17457 17457 17888 17878 17889 17890 17879 +17890 17461 17879 17890 17468 17461 17889 17879 17880 +17884 17883 17891 17884 17891 17892 17893 17883 17885 +17891 17883 17893 17469 17893 17470 17893 17886 17470 +17893 17885 17886 17470 17886 17462 17888 17473 17465 +17473 17888 17466 17894 17475 17474 17895 17894 17474 +17468 17895 17474 17890 17895 17468 17892 17891 17896 +17892 17896 17897 17898 17891 17893 17896 17891 17898 +17477 17898 17469 17469 17898 17893 17899 17479 17478 +17900 17899 17478 17900 17478 17475 17901 17900 17475 +17894 17901 17475 17901 17894 17895 17897 17896 17902 +17897 17902 17903 17904 17896 17898 17902 17896 17904 +17481 17904 17477 17477 17904 17898 17905 17906 17899 +17906 17479 17899 17906 17482 17479 17905 17899 17900 +17903 17902 17907 17903 17907 17908 17483 17907 17484 +17907 17904 17484 17907 17902 17904 17484 17904 17481 +17909 17910 17486 17486 17910 17487 17486 17482 17906 +17486 17906 17909 17908 17907 17911 17908 17911 17912 +17489 17911 17483 17483 17911 17907 17913 17491 17490 +17914 17913 17490 17487 17914 17490 17910 17914 17487 +17915 17916 17917 17917 17916 17912 17917 17912 17911 +17492 17917 17911 17493 17492 17911 17493 17911 17489 +17918 17497 17913 17913 17497 17491 17913 17914 17919 +17913 17919 17918 17915 17917 17920 17915 17920 17921 +17498 17920 17492 17492 17920 17917 17922 17500 17918 +17918 17500 17497 17918 17919 17923 17918 17923 17922 +17921 17920 17502 17501 17921 17502 17924 17921 17501 +17502 17920 17498 17925 17505 17922 17922 17505 17500 +17922 17923 17926 17927 17922 17926 17925 17922 17927 +17928 17927 17926 17929 17930 17931 17930 17924 17931 +17924 17507 17931 17924 17501 17507 17925 17508 17505 +17508 17925 17510 17510 17925 17927 17512 17510 17927 +17928 17512 17927 17932 17512 17928 17929 17931 17933 +17929 17933 17934 17513 17933 17507 17507 17933 17931 +17935 17514 17936 17514 17932 17936 17514 17512 17932 +17937 17938 17935 17937 17935 17936 17939 17940 17938 +17939 17938 17937 17941 17940 17939 17934 17933 17942 +17934 17942 17943 17515 17942 17513 17513 17942 17933 +17935 17517 17514 17517 17935 17518 17518 17935 17938 +17518 17938 17520 17520 17938 17940 17944 17945 17522 +17944 17522 17520 17941 17944 17520 17940 17941 17520 +17946 17947 17945 17946 17945 17944 17948 17949 17947 +17948 17947 17946 17950 17951 17949 17950 17949 17948 +17952 17953 17951 17952 17951 17950 17953 17952 17943 +17953 17943 17942 17523 17953 17942 17524 17523 17942 +17524 17942 17515 17945 17527 17522 17527 17945 17529 +17945 17954 17529 17945 17947 17954 17955 17947 17949 +17954 17947 17955 17956 17949 17951 17955 17949 17956 +17957 17951 17953 17956 17951 17957 17531 17957 17523 +17523 17957 17953 17954 17533 17529 17533 17954 17535 +17535 17954 17955 17535 17955 17537 17537 17955 17956 +17537 17956 17539 17539 17956 17957 17539 17957 17531 +17958 17559 17558 17560 17959 17558 17558 17959 17958 +17561 17960 17560 17560 17960 17959 17562 17960 17561 +17961 17572 17958 17958 17572 17559 17962 17958 17959 +17961 17958 17962 17963 17959 17960 17962 17959 17963 +17573 17964 17562 17964 17960 17562 17964 17963 17960 +17574 17964 17573 17965 17587 17961 17961 17587 17572 +17966 17961 17962 17965 17961 17966 17967 17962 17963 +17966 17962 17967 17968 17963 17964 17967 17963 17968 +17589 17968 17574 17574 17968 17964 17969 17591 17592 +17593 17970 17592 17592 17970 17969 17594 17971 17593 +17593 17971 17970 17595 17972 17594 17594 17972 17971 +17597 17972 17595 17973 17605 17604 17587 17965 17604 +17965 17973 17604 17965 17974 17973 17975 17965 17966 +17974 17965 17975 17976 17966 17967 17975 17966 17976 +17977 17967 17968 17976 17967 17977 17607 17977 17589 +17589 17977 17968 17978 17609 17608 17591 17969 17608 +17969 17978 17608 17969 17979 17978 17970 17980 17981 +17979 17970 17981 17969 17970 17979 17982 17983 17980 +17970 17982 17980 17971 17982 17970 17984 17971 17972 +17982 17971 17984 17610 17985 17597 17985 17972 17597 +17985 17984 17972 17612 17985 17610 17986 17620 17619 +17605 17973 17619 17973 17986 17619 17973 17987 17986 +17988 17973 17974 17987 17973 17988 17989 17974 17975 +17988 17974 17989 17990 17975 17976 17989 17975 17990 +17991 17976 17977 17990 17976 17991 17622 17991 17607 +17607 17991 17977 17992 17993 17978 17993 17609 17978 +17993 17623 17609 17992 17978 17979 17992 17979 17994 +17994 17979 17981 17995 17983 17982 17995 17982 17984 +17995 17984 17996 17997 17998 17996 17984 17997 17996 +17985 17997 17984 17624 17999 17612 17999 17985 17612 +17999 17997 17985 17626 17999 17624 18000 17631 17630 +17620 17986 17630 17986 18000 17630 17986 18001 18000 +18002 17986 17987 18001 17986 18002 18003 17987 17988 +18002 17987 18003 18004 17988 17989 18003 17988 18004 +18005 17989 17990 18004 17989 18005 17632 18005 17633 +18005 17991 17633 18005 17990 17991 17633 17991 17622 +17993 17636 17623 18006 17636 17993 18007 17998 17997 +18008 18009 18007 17997 18008 18007 17999 18008 17997 +17637 18010 17626 18010 17999 17626 18010 18008 17999 +17639 18010 17637 18011 17643 18000 18000 17643 17631 +18012 18000 18001 18011 18000 18012 18013 18001 18002 +18012 18001 18013 18014 18002 18003 18013 18002 18014 +18015 18003 18004 18014 18003 18015 18016 18004 18005 +18015 18004 18016 17644 18017 17632 18017 18005 17632 +18017 18016 18005 17645 18017 17644 18006 17646 17636 +18018 17646 18006 18019 18009 18008 18020 18021 18019 +18008 18020 18019 18010 18020 18008 17647 18022 17639 +18022 18010 17639 18022 18020 18010 17649 18022 17647 +18023 17652 18011 18011 17652 17643 18024 18011 18012 +18023 18011 18024 18025 18012 18013 18024 18012 18025 +18026 18013 18014 18025 18013 18026 18015 18027 18028 +18026 18015 18028 18014 18015 18026 18029 18030 18027 +18015 18029 18027 18016 18029 18015 18031 18016 18017 +18029 18016 18031 17653 18031 17645 17645 18031 18017 +18018 17654 17646 18032 17654 18018 18021 18020 18033 +18021 18033 18034 18035 18020 18022 18033 18020 18035 +17656 18035 17649 17649 18035 18022 18023 17660 17652 +17660 18023 17662 18023 18036 17662 18023 18024 18036 +18037 18024 18025 18036 18024 18037 18026 18038 18039 +18037 18026 18039 18025 18026 18037 18038 18026 18028 +18040 18030 18029 18041 18042 18040 18029 18041 18040 +18031 18041 18029 17663 18041 17653 17653 18041 18031 +18032 17664 17654 18043 17664 18032 18044 18034 18033 +18045 18046 18044 18033 18045 18044 18035 18045 18033 +17666 18045 17656 17656 18045 18035 18047 17671 18036 +18036 17671 17662 18048 18036 18037 18047 18036 18048 +18037 18039 18049 18037 18049 18048 18042 18041 18050 +18042 18050 18051 17672 18050 17663 17663 18050 18041 +18043 18052 17664 17664 18052 17673 17674 17673 18052 +17674 18052 18053 18046 18045 18054 18046 18054 18055 +17675 18056 17666 18056 18045 17666 18056 18054 18045 +17677 18056 17675 18047 17681 17671 17681 18047 17683 +18047 18057 17683 18047 18048 18057 18048 18049 18058 +18048 18058 18057 18051 18050 18059 18051 18059 18060 +17684 18059 17672 17672 18059 18050 18053 17685 17674 +18061 17685 18053 18055 18054 18062 18055 18062 18063 +18064 18054 18056 18062 18054 18064 17687 18064 17677 +17677 18064 18056 18057 17692 17683 17694 17692 18057 +18058 17694 18057 18065 17694 18058 18060 18059 18066 +18060 18066 18067 17695 18066 17684 17684 18066 18059 +18061 17696 17685 18068 17696 18061 18063 18062 18069 +18063 18069 18070 18071 18062 18064 18069 18062 18071 +17698 18071 17687 17687 18071 18064 18072 17704 18073 +17704 18065 18073 17704 17694 18065 18074 18072 18073 +18067 18066 18075 18067 18075 18076 17705 18075 17695 +17695 18075 18066 18068 17706 17696 18077 17706 18068 +18070 18069 18078 18070 18078 18079 17707 18078 17708 +18078 18071 17708 18078 18069 18071 17708 18071 17698 +18080 17715 18072 18072 17715 17704 18072 18074 18081 +18072 18081 18080 18076 18075 18082 18076 18082 18083 +17716 18082 17705 17705 18082 18075 18084 18085 17717 +17717 18085 17718 17717 17706 18077 17717 18077 18084 +18086 18087 18088 18089 18090 18088 18089 18088 18087 +18090 18089 18079 18078 18090 18079 18091 18090 18078 +17721 18091 17707 17707 18091 18078 18092 17727 18080 +18080 17727 17715 18080 18081 18093 18094 18080 18093 +18092 18080 18094 18095 18094 18093 18083 18082 18096 +18083 18096 18097 17728 18096 17716 17716 18096 18082 +18085 17729 17718 18098 17729 18085 18086 18088 18099 +18086 18099 18100 18101 18088 18090 18099 18088 18101 +17730 18101 17731 18101 18091 17731 18101 18090 18091 +17731 18091 17721 18092 17737 17727 17737 18092 17739 +17739 18092 18094 18095 18102 18094 18102 17739 18094 +18102 17741 17739 18096 18103 17743 18102 18096 17743 +17741 18102 17743 18097 18096 18102 17745 18103 17728 +17728 18103 18096 18104 17746 18105 17746 18098 18105 +17746 17729 18098 18106 18104 18105 18100 18099 18107 +18100 18107 18108 18109 18099 18101 18107 18099 18109 +17748 18109 17730 17730 18109 18101 18103 17755 17743 +17755 18103 17745 18104 17757 17746 17758 17757 18104 +18106 17758 18104 18110 17758 18106 18108 18107 18111 +18108 18111 18112 17759 18111 17760 18111 18109 17760 +18111 18107 18109 17760 18109 17748 18110 17768 17758 +18113 17768 18110 18114 18115 18116 18116 18115 18112 +18116 18112 18111 17769 18116 18111 17770 17769 18111 +17770 18111 17759 18113 18117 17768 17768 18117 17779 +17780 17779 18117 17780 18117 18118 18119 18120 18121 +18121 18120 18114 18116 18121 18114 18122 18121 18116 +17782 18122 17769 17769 18122 18116 18118 18123 17780 +17780 18123 17787 18124 18125 17789 17787 18124 17789 +18123 18124 17787 18125 18124 18119 18121 18125 18119 +18126 18125 18121 17790 18126 17791 18126 18122 17791 +18126 18121 18122 17791 18122 17782 18127 17796 18125 +18125 17796 17789 18128 18125 18126 18127 18125 18128 +17798 18128 17790 17790 18128 18126 18127 17800 17796 +17800 18127 17801 17801 18127 18128 17801 18128 17798 +18129 17807 17806 17808 18130 17806 17806 18130 18129 +17810 18130 17808 18129 17816 17807 17816 18129 17817 +18129 18131 17817 18129 18130 18131 17818 18132 17810 +18132 18130 17810 18132 18131 18130 17819 18133 17818 +17818 18133 18132 17820 18134 17819 17819 18134 18133 +17821 18135 17820 17820 18135 18134 17822 18136 17821 +17821 18136 18135 17823 18136 17822 18137 17827 18131 +18131 17827 17817 18138 18131 18132 18137 18131 18138 +18139 18132 18133 18138 18132 18139 18140 18133 18134 +18139 18133 18140 18141 18134 18135 18140 18134 18141 +18142 18135 18136 18141 18135 18142 17829 18142 17823 +17823 18142 18136 18137 17834 17827 17834 18137 17836 +18137 18143 17836 18137 18138 18143 18144 18138 18139 +18143 18138 18144 18145 18139 18140 18144 18139 18145 +18146 18140 18141 18145 18140 18146 18147 18141 18142 +18146 18141 18147 17837 18147 17829 17829 18147 18142 +18143 17844 17836 17844 18143 17846 18143 18148 17846 +18143 18144 18148 18149 18144 18145 18148 18144 18149 +18150 18145 18146 18149 18145 18150 17847 18150 17848 +18150 18147 17848 18150 18146 18147 17848 18147 17837 +18148 17858 17846 17858 18148 17860 18148 18151 17860 +18148 18149 18151 18152 18149 18150 18151 18149 18152 +17862 18152 17847 17847 18152 18150 18153 17864 17865 +17866 18154 17865 17865 18154 18153 17867 18155 17866 +17866 18155 18154 17868 18156 17867 17867 18156 18155 +17869 18157 17868 17868 18157 18156 17871 18157 17869 +18158 17875 18151 18151 17875 17860 17876 18158 17877 +18158 18152 17877 18158 18151 18152 17877 18152 17862 +18159 17880 17881 18160 18159 17881 18160 17881 17864 +18161 18160 17864 18153 18161 17864 18161 18153 18154 +18161 18154 18162 18162 18154 18155 18162 18155 18163 +18163 18155 18156 18163 18156 18164 18164 18156 18157 +18164 18157 18165 18166 18165 18167 18167 18165 17882 +18165 18157 17882 18157 17871 17882 17884 18167 17882 +18158 17887 17875 17887 18158 17876 18168 17890 17889 +18169 18168 17889 18169 17889 17880 18170 18169 17880 +18159 18170 17880 18170 18159 18160 18171 18166 18167 +18167 17884 18171 17884 18172 18171 17884 17892 18172 +18173 18174 18168 18174 17890 18168 18174 17895 17890 +18173 18168 18169 17897 18172 17892 17897 18175 18172 +18176 18177 17901 17901 18177 17900 17901 17895 18174 +17901 18174 18176 17903 18175 17897 17903 18178 18175 +18179 18180 17905 17905 18180 17906 17905 17900 18177 +17905 18177 18179 17908 18178 17903 17908 18181 18178 +18182 18183 17909 17909 18183 17910 17909 17906 18180 +17909 18180 18182 17912 18181 17908 17912 18184 18181 +18183 17914 17910 18185 17914 18183 18186 18187 17916 +18186 17916 17915 17912 17916 18184 18184 17916 18187 +18185 17919 17914 18188 17919 18185 17921 18186 17915 +17921 18189 18186 18190 17923 18191 17923 18188 18191 +17923 17919 18188 18192 18190 18191 18193 18194 18195 +18194 18189 18195 18189 17924 18195 18189 17921 17924 +18190 17926 17923 17928 17926 18190 18192 17928 18190 +18196 17928 18192 18193 18195 17930 17929 18193 17930 +18197 18193 17929 17930 18195 17924 18198 17932 18199 +17932 18196 18199 17932 17928 18196 18200 18201 18198 +18200 18198 18199 18202 18201 18200 17934 18197 17929 +17934 18203 18197 18198 17936 17932 17936 18198 17937 +17937 18198 18201 18202 18204 18201 18204 17937 18201 +18204 17939 17937 18205 18206 17941 17939 18205 17941 +18204 18205 17939 18207 18206 18205 18208 18209 18210 +18209 18203 18210 18203 17943 18210 18203 17934 17943 +18206 17944 17941 18207 18211 18206 18211 17944 18206 +18211 17946 17944 17946 18212 17948 18211 18212 17946 +17948 18213 17950 18212 18213 17948 18210 17952 18208 +17952 18213 18208 17952 17950 18213 17952 18210 17943 +18214 17981 17980 17983 18214 17980 18215 17993 17992 +17994 18216 17992 17992 18216 18215 17981 18214 17994 +18214 18216 17994 18214 18217 18216 17995 18218 17983 +18218 18214 17983 18218 18217 18214 17996 18219 17995 +17995 18219 18218 17998 18219 17996 18220 18006 18215 +18215 18006 17993 18216 18221 18222 18220 18216 18222 +18215 18216 18220 18221 18216 18217 18221 18217 18223 +18223 18217 18218 18223 18218 18224 18225 18226 18224 +18218 18225 18224 18219 18225 18218 18007 18227 17998 +18227 18219 17998 18227 18225 18219 18009 18227 18007 +18228 18018 18220 18220 18018 18006 18220 18222 18229 +18220 18229 18228 18230 18226 18225 18231 18232 18230 +18225 18231 18230 18227 18231 18225 18019 18233 18009 +18233 18227 18009 18233 18231 18227 18021 18233 18019 +18234 18028 18027 18030 18234 18027 18235 18032 18228 +18228 18032 18018 18228 18229 18236 18228 18236 18235 +18237 18232 18231 18238 18239 18237 18231 18238 18237 +18233 18238 18231 18034 18238 18021 18021 18238 18233 +18240 18039 18038 18028 18234 18038 18234 18240 18038 +18234 18241 18240 18040 18242 18030 18242 18234 18030 +18242 18241 18234 18042 18242 18040 18243 18043 18235 +18235 18043 18032 18235 18236 18244 18235 18244 18243 +18239 18238 18245 18239 18245 18246 18044 18247 18034 +18247 18238 18034 18247 18245 18238 18046 18247 18044 +18248 18049 18240 18240 18049 18039 18249 18240 18241 +18248 18240 18249 18250 18241 18242 18249 18241 18250 +18051 18250 18042 18042 18250 18242 18243 18052 18043 +18053 18052 18243 18244 18053 18243 18251 18053 18244 +18246 18245 18252 18246 18252 18253 18254 18245 18247 +18252 18245 18254 18055 18254 18046 18046 18254 18247 +18255 18058 18248 18248 18058 18049 18256 18248 18249 +18255 18248 18256 18257 18249 18250 18256 18249 18257 +18060 18257 18051 18051 18257 18250 18251 18061 18053 +18258 18061 18251 18253 18252 18259 18253 18259 18260 +18261 18252 18254 18259 18252 18261 18063 18261 18055 +18055 18261 18254 18262 18065 18255 18255 18065 18058 +18263 18255 18256 18262 18255 18263 18264 18256 18257 +18263 18256 18264 18067 18264 18060 18060 18264 18257 +18258 18068 18061 18265 18068 18258 18260 18259 18266 +18260 18266 18267 18268 18259 18261 18266 18259 18268 +18070 18268 18063 18063 18268 18261 18262 18073 18065 +18073 18262 18074 18262 18269 18074 18262 18263 18269 +18270 18263 18264 18269 18263 18270 18076 18270 18067 +18067 18270 18264 18265 18077 18068 18271 18077 18265 +18267 18266 18272 18267 18272 18273 18274 18266 18268 +18272 18266 18274 18079 18274 18070 18070 18274 18268 +18275 18081 18269 18269 18081 18074 18276 18269 18270 +18275 18269 18276 18083 18276 18076 18076 18276 18270 +18277 18085 18084 18278 18277 18084 18077 18278 18084 +18271 18278 18077 18279 18280 18281 18281 18280 18273 +18281 18273 18272 18086 18281 18272 18087 18086 18272 +18087 18272 18089 18089 18272 18274 18089 18274 18079 +18275 18093 18081 18093 18275 18095 18275 18282 18095 +18275 18276 18282 18097 18282 18083 18083 18282 18276 +18283 18098 18277 18277 18098 18085 18277 18278 18284 +18277 18284 18283 18279 18281 18285 18279 18285 18286 +18100 18285 18086 18086 18285 18281 18282 18102 18095 +18102 18282 18097 18283 18105 18098 18106 18105 18283 +18284 18106 18283 18287 18106 18284 18286 18285 18288 +18286 18288 18289 18108 18288 18100 18100 18288 18285 +18290 18110 18291 18110 18287 18291 18110 18106 18287 +18292 18290 18291 18293 18294 18295 18295 18294 18289 +18288 18295 18289 18296 18295 18288 18112 18296 18108 +18108 18296 18288 18297 18113 18290 18290 18113 18110 +18290 18292 18298 18299 18290 18298 18297 18290 18299 +18300 18301 18299 18300 18299 18298 18301 18300 18293 +18295 18301 18293 18302 18301 18295 18114 18302 18115 +18302 18296 18115 18302 18295 18296 18115 18296 18112 +18297 18117 18113 18117 18297 18118 18297 18303 18118 +18297 18299 18303 18304 18299 18301 18303 18299 18304 +18119 18304 18120 18304 18302 18120 18304 18301 18302 +18120 18302 18114 18303 18123 18118 18123 18303 18124 +18124 18303 18304 18124 18304 18119 18305 18306 18161 +18161 18306 18160 18307 18161 18162 18307 18305 18161 +18308 18162 18163 18308 18307 18162 18309 18163 18164 +18309 18308 18163 18310 18309 18311 18309 18165 18311 +18309 18164 18165 18166 18311 18165 18312 18313 18170 +18170 18313 18169 18170 18160 18306 18170 18306 18312 +18314 18310 18311 18315 18314 18316 18316 18314 18171 +18314 18311 18171 18311 18166 18171 18172 18316 18171 +18317 18318 18173 18173 18318 18174 18173 18169 18313 +18173 18313 18317 18319 18315 18316 18316 18172 18319 +18172 18320 18319 18172 18175 18320 18321 18322 18176 +18176 18322 18177 18176 18174 18318 18176 18318 18321 +18178 18320 18175 18178 18323 18320 18324 18325 18179 +18179 18325 18180 18179 18177 18322 18179 18322 18324 +18181 18323 18178 18181 18326 18323 18327 18328 18182 +18182 18328 18183 18182 18180 18325 18182 18325 18327 +18329 18330 18331 18330 18326 18331 18326 18184 18331 +18326 18181 18184 18328 18185 18183 18332 18185 18328 +18329 18331 18187 18186 18329 18187 18333 18329 18186 +18187 18331 18184 18332 18188 18185 18334 18188 18332 +18335 18336 18337 18336 18333 18337 18333 18189 18337 +18333 18186 18189 18334 18338 18188 18188 18338 18191 +18192 18191 18338 18192 18338 18339 18335 18337 18194 +18193 18335 18194 18340 18335 18193 18194 18337 18189 +18341 18196 18342 18196 18339 18342 18196 18192 18339 +18343 18344 18341 18343 18341 18342 18345 18344 18343 +18197 18340 18193 18197 18346 18340 18341 18199 18196 +18199 18341 18200 18200 18341 18344 18347 18348 18202 +18347 18202 18200 18345 18347 18200 18344 18345 18200 +18349 18348 18347 18350 18351 18352 18351 18346 18352 +18346 18203 18352 18346 18197 18203 18348 18204 18202 +18349 18353 18348 18353 18204 18348 18353 18205 18204 +18354 18355 18207 18205 18354 18207 18353 18354 18205 +18356 18357 18355 18356 18355 18354 18358 18359 18357 +18358 18357 18356 18359 18358 18350 18359 18350 18352 +18208 18359 18352 18209 18208 18352 18209 18352 18203 +18355 18211 18207 18211 18355 18212 18212 18355 18357 +18212 18357 18213 18213 18357 18359 18213 18359 18208 +18360 18222 18221 18223 18361 18221 18221 18361 18360 +18224 18362 18223 18223 18362 18361 18226 18362 18224 +18363 18229 18360 18360 18229 18222 18364 18360 18361 +18363 18360 18364 18365 18361 18362 18364 18361 18365 +18230 18366 18226 18366 18362 18226 18366 18365 18362 +18232 18366 18230 18367 18236 18363 18363 18236 18229 +18368 18363 18364 18367 18363 18368 18369 18364 18365 +18368 18364 18369 18370 18365 18366 18369 18365 18370 +18237 18371 18232 18371 18366 18232 18371 18370 18366 +18239 18371 18237 18372 18244 18367 18367 18244 18236 +18368 18373 18374 18372 18368 18374 18367 18368 18372 +18373 18368 18369 18373 18369 18375 18376 18377 18375 +18369 18376 18375 18370 18376 18369 18378 18370 18371 +18376 18370 18378 18246 18378 18239 18239 18378 18371 +18379 18380 18372 18380 18244 18372 18380 18251 18244 +18379 18372 18374 18377 18376 18381 18377 18381 18382 +18383 18376 18378 18381 18376 18383 18253 18383 18246 +18246 18383 18378 18384 18258 18385 18258 18380 18385 +18258 18251 18380 18386 18384 18385 18382 18381 18387 +18382 18387 18388 18389 18381 18383 18387 18381 18389 +18260 18389 18253 18253 18389 18383 18390 18265 18384 +18384 18265 18258 18384 18386 18391 18384 18391 18390 +18392 18388 18387 18393 18394 18392 18387 18393 18392 +18389 18393 18387 18267 18393 18260 18260 18393 18389 +18395 18396 18390 18396 18265 18390 18396 18271 18265 +18395 18390 18391 18394 18393 18397 18394 18397 18398 +18273 18397 18267 18267 18397 18393 18396 18278 18271 +18399 18278 18396 18400 18401 18402 18402 18401 18398 +18402 18398 18397 18279 18402 18397 18280 18279 18397 +18280 18397 18273 18403 18284 18404 18284 18399 18404 +18284 18278 18399 18405 18403 18404 18406 18407 18408 +18408 18407 18400 18402 18408 18400 18409 18408 18402 +18286 18409 18279 18279 18409 18402 18410 18287 18403 +18403 18287 18284 18403 18405 18411 18412 18403 18411 +18410 18403 18412 18412 18411 18406 18408 18412 18406 +18413 18412 18408 18414 18408 18409 18413 18408 18414 +18289 18414 18286 18286 18414 18409 18410 18291 18287 +18291 18410 18292 18410 18415 18292 18410 18412 18415 +18416 18412 18413 18415 18412 18416 18293 18416 18294 +18416 18414 18294 18416 18413 18414 18294 18414 18289 +18415 18298 18292 18298 18415 18300 18300 18415 18416 +18300 18416 18293 18417 18418 18305 18305 18418 18306 +18419 18305 18307 18419 18417 18305 18420 18307 18308 +18420 18419 18307 18421 18308 18309 18421 18420 18308 +18309 18310 18421 18421 18310 18422 18423 18424 18312 +18312 18424 18313 18312 18306 18418 18312 18418 18423 +18425 18422 18310 18425 18310 18314 18314 18315 18425 +18425 18315 18426 18427 18428 18317 18317 18428 18318 +18317 18313 18424 18317 18424 18427 18315 18319 18429 +18430 18315 18429 18426 18315 18430 18320 18429 18319 +18431 18432 18321 18321 18432 18322 18321 18318 18428 +18321 18428 18431 18433 18430 18429 18429 18320 18433 +18320 18434 18433 18320 18323 18434 18435 18436 18437 +18325 18324 18437 18324 18435 18437 18324 18438 18435 +18324 18322 18432 18324 18432 18438 18326 18434 18323 +18326 18439 18434 18437 18436 18327 18436 18328 18327 +18436 18440 18328 18437 18327 18325 18441 18442 18330 +18441 18330 18329 18326 18330 18439 18439 18330 18442 +18440 18332 18328 18443 18332 18440 18444 18445 18446 +18445 18441 18446 18441 18333 18446 18441 18329 18333 +18447 18334 18448 18334 18443 18448 18334 18332 18443 +18449 18447 18448 18444 18446 18336 18335 18444 18336 +18450 18444 18335 18336 18446 18333 18447 18338 18334 +18451 18452 18339 18451 18339 18338 18449 18451 18338 +18447 18449 18338 18453 18452 18451 18340 18450 18335 +18340 18454 18450 18452 18342 18339 18453 18455 18452 +18455 18342 18452 18455 18343 18342 18456 18457 18345 +18343 18456 18345 18455 18456 18343 18458 18457 18456 +18459 18460 18461 18460 18454 18461 18454 18346 18461 +18454 18340 18346 18457 18347 18345 18462 18463 18349 +18462 18349 18347 18458 18462 18347 18457 18458 18347 +18464 18465 18463 18464 18463 18462 18466 18467 18465 +18466 18465 18464 18468 18469 18467 18468 18467 18466 +18469 18468 18459 18469 18459 18461 18350 18469 18461 +18351 18350 18461 18351 18461 18346 18463 18353 18349 +18353 18463 18354 18354 18463 18465 18354 18465 18356 +18356 18465 18467 18356 18467 18358 18358 18467 18469 +18358 18469 18350 18470 18374 18373 18375 18471 18373 +18373 18471 18470 18377 18471 18375 18472 18380 18379 +18374 18470 18379 18470 18472 18379 18470 18473 18472 +18474 18470 18471 18473 18470 18474 18382 18474 18377 +18377 18474 18471 18472 18385 18380 18385 18472 18386 +18472 18475 18386 18472 18473 18475 18476 18473 18474 +18475 18473 18476 18388 18476 18382 18382 18476 18474 +18477 18391 18475 18475 18391 18386 18478 18475 18476 +18477 18475 18478 18392 18479 18388 18479 18476 18388 +18479 18478 18476 18394 18479 18392 18480 18396 18395 +18391 18477 18395 18477 18480 18395 18477 18481 18480 +18482 18477 18478 18481 18477 18482 18483 18478 18479 +18482 18478 18483 18398 18483 18394 18394 18483 18479 +18484 18399 18480 18480 18399 18396 18485 18480 18481 +18484 18480 18485 18486 18481 18482 18485 18481 18486 +18400 18486 18401 18486 18483 18401 18486 18482 18483 +18401 18483 18398 18484 18404 18399 18404 18484 18405 +18484 18487 18405 18484 18485 18487 18406 18487 18407 +18487 18486 18407 18487 18485 18486 18407 18486 18400 +18487 18411 18405 18411 18487 18406 18488 18489 18417 +18417 18489 18418 18490 18417 18419 18490 18488 18417 +18491 18419 18420 18491 18490 18419 18492 18491 18493 +18491 18421 18493 18491 18420 18421 18422 18493 18421 +18494 18495 18423 18423 18495 18424 18423 18418 18489 +18423 18489 18494 18496 18492 18493 18497 18496 18498 +18498 18496 18425 18496 18493 18425 18493 18422 18425 +18426 18498 18425 18499 18500 18427 18427 18500 18428 +18427 18424 18495 18427 18495 18499 18501 18497 18498 +18498 18426 18501 18426 18502 18501 18426 18430 18502 +18503 18504 18431 18431 18504 18432 18431 18428 18500 +18431 18500 18503 18430 18433 18505 18506 18430 18505 +18502 18430 18506 18434 18505 18433 18507 18508 18435 +18435 18508 18436 18509 18435 18438 18509 18507 18435 +18438 18432 18504 18438 18504 18509 18510 18511 18512 +18512 18511 18506 18505 18512 18506 18513 18512 18505 +18439 18513 18434 18434 18513 18505 18508 18440 18436 +18514 18440 18508 18510 18512 18515 18510 18515 18516 +18441 18515 18442 18515 18513 18442 18515 18512 18513 +18442 18513 18439 18514 18443 18440 18517 18443 18514 +18516 18515 18445 18444 18516 18445 18518 18516 18444 +18445 18515 18441 18517 18519 18443 18443 18519 18448 +18449 18448 18519 18449 18519 18520 18450 18518 18444 +18450 18521 18518 18520 18522 18449 18449 18522 18451 +18523 18524 18453 18451 18523 18453 18522 18523 18451 +18525 18526 18524 18525 18524 18523 18527 18526 18525 +18528 18529 18530 18529 18521 18530 18521 18454 18530 +18521 18450 18454 18524 18455 18453 18455 18524 18456 +18456 18524 18526 18531 18532 18458 18531 18458 18456 +18527 18531 18456 18526 18527 18456 18533 18534 18532 +18533 18532 18531 18535 18536 18534 18535 18534 18533 +18537 18538 18536 18537 18536 18535 18538 18537 18528 +18538 18528 18530 18459 18538 18530 18460 18459 18530 +18460 18530 18454 18532 18462 18458 18462 18532 18464 +18464 18532 18534 18464 18534 18466 18466 18534 18536 +18466 18536 18468 18468 18536 18538 18468 18538 18459 +18539 18540 18488 18488 18540 18489 18541 18488 18490 +18541 18539 18488 18542 18490 18491 18542 18541 18490 +18491 18492 18542 18542 18492 18543 18544 18545 18494 +18494 18545 18495 18494 18489 18540 18494 18540 18544 +18546 18543 18492 18546 18492 18496 18496 18497 18546 +18546 18497 18547 18548 18549 18499 18499 18549 18500 +18499 18495 18545 18499 18545 18548 18550 18547 18497 +18550 18497 18501 18501 18502 18550 18550 18502 18551 +18552 18553 18503 18503 18553 18504 18503 18500 18549 +18503 18549 18552 18506 18551 18502 18506 18554 18551 +18555 18556 18507 18507 18556 18508 18557 18507 18509 +18557 18555 18507 18509 18504 18553 18509 18553 18557 +18558 18559 18511 18558 18511 18510 18506 18511 18554 +18554 18511 18559 18556 18514 18508 18560 18514 18556 +18516 18558 18510 18516 18561 18558 18562 18517 18563 +18517 18560 18563 18517 18514 18560 18564 18562 18563 +18518 18561 18516 18518 18565 18561 18562 18519 18517 +18566 18567 18520 18566 18520 18519 18564 18566 18519 +18562 18564 18519 18568 18567 18566 18521 18565 18518 +18521 18569 18565 18567 18522 18520 18568 18570 18567 +18570 18522 18567 18570 18523 18522 18523 18571 18525 +18570 18571 18523 18572 18573 18527 18525 18572 18527 +18571 18572 18525 18574 18573 18572 18575 18576 18577 +18529 18528 18577 18576 18529 18577 18578 18529 18576 +18521 18529 18569 18569 18529 18578 18573 18531 18527 +18574 18579 18573 18579 18531 18573 18579 18533 18531 +18533 18580 18535 18579 18580 18533 18577 18537 18575 +18537 18580 18575 18537 18535 18580 18537 18577 18528 +18581 18582 18539 18539 18582 18540 18583 18539 18541 +18583 18581 18539 18584 18541 18542 18584 18583 18541 +18542 18543 18584 18584 18543 18585 18586 18587 18588 +18545 18544 18588 18544 18586 18588 18544 18589 18586 +18544 18540 18582 18544 18582 18589 18590 18585 18543 +18590 18543 18546 18546 18547 18590 18590 18547 18591 +18588 18587 18548 18587 18549 18548 18587 18592 18549 +18588 18548 18545 18593 18591 18547 18593 18547 18550 +18550 18551 18593 18593 18551 18594 18595 18596 18597 +18553 18552 18597 18552 18595 18597 18552 18598 18595 +18552 18549 18592 18552 18592 18598 18554 18594 18551 +18554 18599 18594 18600 18601 18555 18555 18601 18556 +18557 18597 18596 18600 18557 18596 18555 18557 18600 +18597 18557 18553 18602 18603 18559 18602 18559 18558 +18554 18559 18599 18599 18559 18603 18604 18560 18605 +18560 18601 18605 18560 18556 18601 18606 18604 18605 +18561 18602 18558 18561 18607 18602 18604 18563 18560 +18564 18563 18604 18606 18564 18604 18608 18564 18606 +18565 18607 18561 18565 18609 18607 18608 18610 18564 +18564 18610 18566 18568 18566 18610 18568 18610 18611 +18612 18613 18614 18613 18609 18614 18609 18569 18614 +18609 18565 18569 18611 18615 18568 18568 18615 18570 +18570 18616 18571 18615 18616 18570 18571 18617 18572 +18616 18617 18571 18618 18619 18574 18572 18618 18574 +18617 18618 18572 18620 18621 18619 18620 18619 18618 +18576 18575 18621 18620 18576 18621 18622 18576 18620 +18614 18578 18612 18578 18622 18612 18578 18576 18622 +18578 18614 18569 18619 18579 18574 18579 18619 18580 +18580 18619 18621 18580 18621 18575 18623 18624 18625 +18582 18581 18625 18581 18623 18625 18581 18626 18623 +18627 18581 18583 18627 18626 18581 18628 18583 18584 +18628 18627 18583 18584 18585 18628 18628 18585 18629 +18630 18631 18586 18586 18631 18587 18589 18625 18624 +18630 18589 18624 18586 18589 18630 18625 18589 18582 +18632 18629 18585 18632 18585 18590 18590 18591 18632 +18632 18591 18633 18634 18635 18636 18631 18634 18636 +18592 18631 18636 18587 18631 18592 18637 18633 18591 +18637 18591 18593 18593 18594 18637 18637 18594 18638 +18639 18640 18595 18595 18640 18596 18598 18636 18635 +18639 18598 18635 18595 18598 18639 18636 18598 18592 +18641 18642 18643 18642 18638 18643 18638 18599 18643 +18638 18594 18599 18644 18601 18600 18645 18644 18600 +18596 18645 18600 18640 18645 18596 18641 18643 18603 +18602 18641 18603 18646 18641 18602 18603 18643 18599 +18644 18605 18601 18606 18605 18644 18645 18606 18644 +18647 18606 18645 18607 18646 18602 18607 18648 18646 +18649 18608 18650 18608 18647 18650 18608 18606 18647 +18651 18649 18650 18609 18648 18607 18609 18652 18648 +18649 18610 18608 18653 18654 18611 18653 18611 18610 +18651 18653 18610 18649 18651 18610 18655 18656 18654 +18655 18654 18653 18657 18656 18655 18658 18659 18660 +18613 18612 18660 18659 18613 18660 18661 18613 18659 +18609 18613 18652 18652 18613 18661 18654 18615 18611 +18615 18654 18616 18616 18654 18656 18657 18662 18656 +18662 18616 18656 18662 18617 18616 18617 18663 18618 +18662 18663 18617 18618 18664 18620 18663 18664 18618 +18660 18622 18658 18622 18664 18658 18622 18620 18664 +18622 18660 18612 18665 18666 18623 18623 18666 18624 +18667 18623 18626 18667 18665 18623 18668 18626 18627 +18668 18667 18626 18669 18627 18628 18669 18668 18627 +18628 18629 18669 18669 18629 18670 18671 18672 18630 +18630 18672 18631 18630 18624 18666 18630 18666 18671 +18673 18670 18629 18673 18629 18632 18632 18633 18673 +18673 18633 18674 18675 18676 18634 18634 18676 18635 +18634 18631 18672 18634 18672 18675 18633 18637 18677 +18678 18633 18677 18674 18633 18678 18638 18677 18637 +18679 18680 18639 18639 18680 18640 18639 18635 18676 +18639 18676 18679 18678 18677 18642 18641 18678 18642 +18681 18678 18641 18642 18677 18638 18680 18645 18640 +18682 18645 18680 18646 18681 18641 18646 18683 18681 +18682 18647 18645 18684 18647 18682 18648 18683 18646 +18648 18685 18683 18684 18686 18647 18647 18686 18650 +18687 18688 18651 18650 18687 18651 18686 18687 18650 +18689 18688 18687 18690 18691 18692 18691 18685 18692 +18685 18652 18692 18685 18648 18652 18688 18653 18651 +18689 18693 18688 18693 18653 18688 18693 18655 18653 +18694 18695 18657 18655 18694 18657 18693 18694 18655 +18696 18697 18695 18696 18695 18694 18698 18699 18697 +18698 18697 18696 18659 18658 18699 18698 18659 18699 +18700 18659 18698 18692 18661 18690 18661 18700 18690 +18661 18659 18700 18661 18692 18652 18695 18662 18657 +18662 18695 18663 18663 18695 18697 18663 18697 18664 +18664 18697 18699 18664 18699 18658 18701 18702 18703 +18666 18665 18703 18665 18701 18703 18665 18704 18701 +18705 18665 18667 18705 18704 18665 18706 18667 18668 +18706 18705 18667 18707 18668 18669 18707 18706 18668 +18708 18709 18670 18709 18669 18670 18709 18707 18669 +18708 18710 18709 18711 18712 18713 18711 18713 18702 +18702 18713 18703 18713 18672 18703 18672 18671 18703 +18703 18671 18666 18673 18708 18670 18673 18674 18708 +18674 18710 18708 18674 18714 18710 18715 18716 18717 +18715 18717 18712 18712 18717 18713 18717 18676 18713 +18676 18675 18713 18713 18675 18672 18678 18718 18719 +18714 18678 18719 18674 18678 18714 18718 18720 18719 +18717 18716 18679 18716 18680 18679 18716 18721 18680 +18717 18679 18676 18722 18723 18718 18678 18722 18718 +18681 18722 18678 18718 18723 18720 18721 18682 18680 +18724 18682 18721 18683 18725 18726 18722 18683 18726 +18681 18683 18722 18725 18727 18726 18728 18684 18729 +18684 18724 18729 18684 18682 18724 18730 18728 18729 +18685 18731 18725 18685 18725 18683 18727 18725 18732 +18732 18725 18731 18728 18686 18684 18730 18733 18728 +18733 18686 18728 18733 18687 18686 18734 18735 18689 +18687 18734 18689 18733 18734 18687 18736 18735 18734 +18737 18738 18739 18691 18690 18739 18738 18691 18739 +18740 18691 18738 18685 18691 18731 18691 18741 18731 +18691 18740 18741 18731 18741 18732 18735 18693 18689 +18736 18742 18735 18742 18693 18735 18742 18694 18693 +18694 18743 18696 18742 18743 18694 18696 18744 18698 +18743 18744 18696 18739 18700 18737 18700 18744 18737 +18700 18698 18744 18700 18739 18690 18745 18746 18701 +18701 18746 18702 18747 18701 18704 18747 18745 18701 +18748 18704 18705 18748 18747 18704 18749 18705 18706 +18749 18748 18705 18750 18706 18707 18750 18749 18706 +18751 18707 18709 18751 18750 18707 18709 18710 18751 +18751 18710 18752 18753 18754 18711 18711 18754 18712 +18711 18702 18746 18711 18746 18753 18714 18752 18710 +18714 18755 18752 18756 18757 18715 18715 18757 18716 +18715 18712 18754 18715 18754 18756 18714 18719 18758 +18759 18714 18758 18755 18714 18759 18720 18758 18719 +18757 18721 18716 18760 18721 18757 18759 18758 18723 +18722 18759 18723 18761 18759 18722 18723 18758 18720 +18760 18724 18721 18762 18724 18760 18763 18761 18722 +18763 18722 18726 18726 18727 18763 18763 18727 18764 +18762 18765 18724 18724 18765 18729 18730 18729 18765 +18730 18765 18766 18767 18768 18769 18768 18764 18769 +18764 18732 18769 18764 18727 18732 18766 18770 18730 +18730 18770 18733 18733 18771 18734 18770 18771 18733 +18772 18773 18736 18734 18772 18736 18771 18772 18734 +18774 18775 18773 18774 18773 18772 18776 18777 18775 +18776 18775 18774 18738 18737 18777 18776 18738 18777 +18778 18738 18776 18778 18779 18780 18740 18778 18780 +18738 18778 18740 18781 18767 18769 18740 18781 18769 +18741 18740 18769 18781 18740 18780 18741 18769 18732 +18773 18742 18736 18742 18773 18743 18743 18773 18775 +18743 18775 18744 18744 18775 18777 18744 18777 18737 +18779 18782 18780 18780 18782 18781 18783 18784 18745 +18745 18784 18746 18785 18745 18747 18785 18783 18745 +18786 18747 18748 18786 18785 18747 18787 18748 18749 +18787 18786 18748 18788 18749 18750 18788 18787 18749 +18789 18788 18790 18788 18751 18790 18788 18750 18751 +18752 18790 18751 18791 18792 18753 18753 18792 18754 +18753 18746 18784 18753 18784 18791 18793 18789 18790 +18790 18752 18793 18752 18794 18793 18752 18755 18794 +18795 18796 18756 18756 18796 18757 18756 18754 18792 +18756 18792 18795 18759 18794 18755 18759 18797 18794 +18796 18760 18757 18798 18760 18796 18761 18797 18759 +18761 18799 18797 18800 18762 18801 18762 18798 18801 +18762 18760 18798 18802 18800 18801 18761 18763 18803 +18804 18761 18803 18799 18761 18804 18764 18803 18763 +18800 18765 18762 18805 18806 18766 18805 18766 18765 +18802 18805 18765 18800 18802 18765 18807 18806 18805 +18808 18809 18810 18811 18810 18809 18804 18803 18768 +18767 18804 18768 18812 18804 18767 18768 18803 18764 +18806 18770 18766 18807 18813 18806 18813 18770 18806 +18813 18771 18770 18771 18814 18772 18813 18814 18771 +18772 18815 18774 18814 18815 18772 18774 18816 18776 +18815 18816 18774 18810 18778 18808 18778 18816 18808 +18778 18776 18816 18817 18818 18779 18817 18779 18778 +18811 18817 18778 18810 18811 18778 18817 18812 18818 +18812 18781 18818 18812 18767 18781 18818 18782 18779 +18782 18818 18781 18819 18820 18783 18783 18820 18784 +18821 18783 18785 18821 18819 18783 18822 18785 18786 +18822 18821 18785 18823 18786 18787 18823 18822 18786 +18824 18787 18788 18824 18823 18787 18788 18789 18824 +18824 18789 18825 18826 18827 18828 18792 18791 18828 +18791 18826 18828 18791 18829 18826 18791 18784 18820 +18791 18820 18829 18830 18825 18789 18830 18789 18793 +18793 18794 18830 18830 18794 18831 18828 18827 18795 +18827 18796 18795 18827 18832 18796 18828 18795 18792 +18797 18831 18794 18797 18833 18831 18832 18798 18796 +18834 18798 18832 18799 18833 18797 18799 18835 18833 +18834 18836 18798 18798 18836 18801 18802 18801 18836 +18802 18836 18837 18804 18835 18799 18804 18838 18835 +18837 18839 18802 18802 18839 18805 18840 18841 18807 +18805 18840 18807 18839 18840 18805 18842 18843 18841 +18842 18841 18840 18844 18845 18843 18844 18843 18842 +18846 18847 18845 18846 18845 18844 18809 18808 18847 +18846 18809 18847 18848 18809 18846 18849 18850 18811 +18809 18849 18811 18848 18849 18809 18849 18838 18850 +18838 18812 18850 18838 18804 18812 18841 18813 18807 +18813 18841 18814 18814 18841 18843 18814 18843 18815 +18815 18843 18845 18815 18845 18816 18816 18845 18847 +18816 18847 18808 18850 18817 18811 18817 18850 18812 +18851 18820 18819 18851 18819 18852 18819 18853 18852 +18819 18821 18853 18854 18821 18822 18854 18853 18821 +18855 18822 18823 18855 18854 18822 18856 18823 18824 +18856 18855 18823 18824 18825 18856 18856 18825 18857 +18858 18827 18826 18858 18826 18859 18826 18860 18859 +18826 18829 18860 18829 18820 18851 18861 18829 18851 +18860 18829 18861 18861 18851 18852 18825 18830 18862 +18863 18825 18862 18857 18825 18863 18831 18862 18830 +18864 18832 18858 18858 18832 18827 18858 18859 18865 +18858 18865 18864 18863 18862 18866 18863 18866 18867 +18833 18866 18831 18831 18866 18862 18868 18834 18864 +18864 18834 18832 18864 18865 18869 18864 18869 18868 +18870 18867 18866 18866 18833 18870 18833 18871 18870 +18833 18835 18871 18868 18836 18834 18872 18873 18837 +18872 18837 18836 18869 18872 18836 18868 18869 18836 +18874 18873 18872 18875 18876 18877 18878 18879 18877 +18878 18877 18876 18880 18881 18879 18880 18879 18878 +18880 18871 18881 18871 18838 18881 18871 18835 18838 +18873 18839 18837 18874 18882 18873 18882 18839 18873 +18882 18840 18839 18840 18883 18842 18882 18883 18840 +18842 18884 18844 18883 18884 18842 18877 18846 18875 +18846 18884 18875 18846 18844 18884 18846 18877 18848 +18848 18877 18879 18848 18879 18849 18849 18879 18881 +18849 18881 18838 18885 18852 18853 18885 18853 18886 +18853 18887 18886 18853 18854 18887 18888 18854 18855 +18888 18887 18854 18889 18888 18890 18888 18856 18890 +18888 18855 18856 18857 18890 18856 18891 18892 18860 +18860 18892 18859 18893 18860 18861 18893 18891 18860 +18861 18852 18885 18894 18861 18885 18893 18861 18894 +18894 18885 18886 18889 18890 18895 18889 18895 18896 +18863 18895 18857 18857 18895 18890 18892 18865 18859 +18897 18865 18892 18896 18895 18898 18896 18898 18899 +18867 18898 18863 18863 18898 18895 18900 18869 18901 +18869 18897 18901 18869 18865 18897 18902 18900 18901 +18903 18904 18905 18905 18904 18899 18898 18905 18899 +18906 18905 18898 18870 18907 18867 18907 18898 18867 +18907 18906 18898 18871 18907 18870 18900 18872 18869 +18908 18909 18874 18908 18874 18872 18902 18908 18872 +18900 18902 18872 18910 18911 18909 18910 18909 18908 +18912 18913 18911 18912 18911 18910 18913 18912 18903 +18913 18903 18905 18875 18913 18905 18876 18875 18905 +18876 18905 18878 18878 18905 18906 18878 18906 18880 +18880 18906 18907 18880 18907 18871 18909 18882 18874 +18882 18909 18883 18883 18909 18911 18883 18911 18884 +18884 18911 18913 18884 18913 18875 18914 18886 18887 +18888 18915 18887 18887 18915 18914 18889 18915 18888 +18916 18892 18891 18893 18917 18891 18891 18917 18916 +18917 18893 18918 18893 18919 18918 18893 18894 18919 +18919 18894 18886 18920 18919 18886 18914 18920 18886 +18919 18920 18921 18922 18914 18915 18920 18914 18922 +18896 18922 18889 18889 18922 18915 18923 18897 18916 +18916 18897 18892 18917 18924 18925 18923 18917 18925 +18916 18917 18923 18924 18917 18918 18921 18920 18926 +18921 18926 18927 18928 18920 18922 18926 18920 18928 +18899 18928 18896 18896 18928 18922 18923 18901 18897 +18929 18930 18902 18929 18902 18901 18925 18929 18901 +18923 18925 18901 18931 18932 18930 18931 18930 18929 +18932 18931 18927 18926 18932 18927 18933 18932 18926 +18903 18933 18904 18933 18928 18904 18933 18926 18928 +18904 18928 18899 18930 18908 18902 18908 18930 18910 +18910 18930 18932 18910 18932 18912 18912 18932 18933 +18912 18933 18903 18934 18918 18919 18921 18934 18919 +18935 18925 18924 18918 18934 18924 18934 18935 18924 +18934 18936 18935 18927 18936 18921 18921 18936 18934 +18935 18929 18925 18929 18935 18931 18931 18935 18936 +18931 18936 18927 +POINT_DATA 18937 +NORMALS Normals float +0.820881 0.481978 0.306353 0.806861 -0.318945 0.497243 0.999068 -0.0367114 0.022685 +0.843901 0.287051 -0.453247 0.833017 -0.497115 -0.242815 0.326945 0.915469 0.234572 +0.660664 0.59124 0.462556 0.818857 0.531967 0.215602 0.81356 0.578716 -0.0566431 +0.403418 0.914881 0.015722 0.832505 0.463947 -0.302802 0.494655 0.850986 -0.176461 +0.612872 0.670462 -0.418174 0.657735 0.414864 0.628708 0.725325 0.247952 0.642202 +0.908831 0.19202 0.370344 0.921106 0.0915609 0.378392 0.948625 -0.0417543 -0.313636 +0.726688 0.46955 -0.501446 0.876157 0.156515 -0.455908 0.737886 0.25779 -0.623753 +0.940944 0.0342561 0.336824 0.804429 -0.0110412 0.593946 0.996101 -0.0534415 0.0701955 +0.92701 -0.373651 0.0321931 0.902906 -0.379325 -0.202171 0.896856 -0.327555 -0.29725 +0.753513 -0.269173 -0.599803 0.848725 -0.345338 0.40051 0.718417 -0.503157 0.480323 +0.900564 -0.431103 0.0559907 0.866106 -0.472645 -0.162686 0.726467 -0.642704 -0.243262 +0.685844 -0.640658 -0.345217 0.708468 -0.611282 -0.352715 0.475691 -0.86443 0.162725 +0.45644 -0.888954 -0.0377305 0.544078 -0.800668 -0.250818 0.203044 0.905399 0.372862 +0.29984 0.712732 0.63412 0.458079 0.660735 0.594637 0.118037 0.986895 0.110024 +0.283145 0.958698 0.0269503 0.0903743 0.994537 -0.052248 0.73425 0.539337 -0.412303 +0.279049 0.944075 -0.175651 0.746037 0.324808 -0.581316 0.455506 0.823139 -0.339051 +0.233614 0.512286 -0.826431 0.263118 0.587884 0.764959 0.430722 0.379482 0.818824 +0.532585 0.441951 0.721826 0.73001 0.0942329 -0.676909 0.386925 0.0745315 -0.919094 +0.733561 0.186701 0.653476 0.552119 0.431126 0.713649 0.649212 -0.250219 -0.718271 +0.654914 -0.120865 -0.745975 0.593017 0.382641 0.708461 0.480491 -0.218071 0.849455 +0.80286 0.0286561 0.595479 0.841358 -0.340473 0.419755 0.63009 -0.706597 -0.322036 +0.671616 -0.698865 -0.246007 0.603748 -0.600517 -0.524278 0.572404 -0.546012 -0.611739 +0.598609 -0.677827 0.426869 0.526898 -0.821455 0.218153 0.451814 -0.891861 0.0211524 +0.36059 -0.932283 0.0287022 0.408136 -0.911906 -0.0430413 0.625256 -0.762061 -0.168278 +0.681216 -0.660589 -0.315543 0.388028 0.91893 0.0707243 0.535604 0.787163 0.305782 +0.599162 0.785414 -0.155341 0.521015 0.750999 -0.405641 0.200633 0.908761 0.365922 +0.246424 0.750258 0.613505 0.292099 0.885846 0.360493 0.37325 0.860555 -0.346596 +0.028281 0.995391 -0.0916311 -0.129975 0.89642 -0.423719 -0.139861 0.393586 -0.908586 +0.311964 0.599369 0.737181 0.290623 0.518629 0.804091 0.313164 0.455538 -0.833314 +0.309696 0.729529 0.609816 0.540922 0.308719 0.782366 0.486137 0.561658 0.669486 +0.630905 -0.00249555 -0.775856 0.582921 0.415524 -0.698243 0.0809249 0.0435169 -0.99577 +0.402225 -0.293485 0.867226 0.545984 -0.611407 -0.572785 0.637484 -0.556931 -0.532393 +0.563546 -0.433812 -0.703009 0.427448 -0.686976 0.587666 0.491531 -0.762423 0.420843 +0.578383 -0.765918 0.28079 0.610084 -0.790671 0.0513513 0.600914 -0.799238 -0.0109572 +0.543007 -0.839727 -0.00164102 0.558701 -0.829354 -0.00499726 0.618789 -0.770301 -0.154067 +0.648395 -0.653903 -0.389865 0.667477 -0.515491 -0.537348 0.482474 -0.846345 0.22565 +0.668317 -0.736493 0.104546 0.607591 -0.777169 -0.163835 0.406882 -0.912401 -0.0443927 +0.371743 -0.928192 0.0163097 0.427382 -0.87734 -0.218217 -0.169583 0.927564 0.332967 +0.0767222 0.793887 0.603206 0.382127 0.799748 0.463014 -0.311249 0.949369 -0.0426988 +0.37208 0.718337 -0.587833 0.0793898 0.912462 -0.401385 -0.166697 0.826904 -0.537068 +0.143493 0.874536 0.463247 0.226153 0.515502 0.826506 0.335076 0.706649 0.623195 +-0.0588973 0.926769 0.370986 -0.078164 0.877279 -0.473573 -0.147828 0.821182 -0.551188 +-0.248595 0.738188 -0.62712 0.0496819 0.45386 0.889687 -0.176562 0.900104 -0.398294 +0.134843 0.812288 -0.567456 0.438194 0.512908 -0.738181 0.0577455 0.334312 -0.940692 +0.18017 0.871327 0.45643 0.390862 0.300777 0.869919 0.557719 0.472929 0.682119 +0.0716636 0.822882 0.563675 -0.269922 -0.512524 -0.815145 -0.584893 0.162215 -0.794725 +-0.300649 0.111338 -0.947214 0.552682 -0.231317 0.800647 0.261573 -0.312102 0.91333 +0.725893 -0.350187 -0.591986 -0.305792 0.574935 -0.758907 -0.023291 0.269387 -0.96275 +0.369249 -0.59667 0.712489 0.214881 -0.744922 0.631599 0.3821 -0.724622 0.573517 +0.858513 -0.331024 -0.391635 0.779445 -0.352174 -0.518111 0.352143 -0.191578 -0.916129 +0.316995 -0.843509 0.433598 0.469224 -0.780936 0.412271 0.48091 -0.748559 0.456492 +0.63612 -0.709312 0.30369 0.639373 -0.760842 -0.111002 0.56615 -0.721277 -0.399041 +0.494293 -0.829266 -0.260753 0.342061 -0.933543 -0.107202 0.312796 -0.942037 0.121348 +0.790643 -0.61223 -0.00759356 0.258172 -0.818001 0.514024 0.158962 -0.612614 -0.774232 +0.482702 -0.689595 -0.539868 0.340521 -0.931863 0.125209 0.41855 -0.875851 -0.240212 +0.336305 -0.939673 -0.0625529 -0.4807 0.775209 0.409851 -0.60462 0.792756 0.0772826 +-0.513596 0.839768 -0.176095 0.0312949 0.90331 0.427846 0.080089 0.464737 0.881819 +-0.0160371 0.998237 0.0571406 -0.319532 0.936563 0.144043 -0.369311 0.929272 0.00798695 +-0.271766 0.924004 -0.268998 -0.0712348 0.872568 -0.48327 -0.0398607 0.805757 -0.590903 +0.177355 0.528189 0.830398 -0.0935278 0.884978 -0.456142 -0.118023 0.868423 -0.481572 +-0.274147 -0.0534176 -0.960203 0.0489435 0.836711 0.545453 0.0655267 0.395079 0.916307 +0.198733 0.872588 0.4462 -0.255461 -0.00854179 -0.966782 -0.371097 -0.500027 -0.782471 +0.13707 -0.242725 0.960363 -0.192404 0.548335 -0.813824 -0.323178 0.218442 -0.920782 +0.22153 -0.650914 0.72611 0.29023 -0.72108 0.629135 -0.249887 -0.368725 -0.89532 +0.424146 -0.700129 0.574386 0.478229 -0.740608 0.472013 0.485584 -0.70576 0.51586 +0.550945 -0.756668 -0.352013 0.567181 -0.786586 -0.244107 0.624339 -0.741005 -0.247211 +0.349819 -0.911327 0.217048 0.420592 -0.833384 0.358573 -0.127021 -0.709778 -0.692878 +-0.377275 -0.582982 -0.71958 0.273182 -0.921876 0.274803 0.0960656 -0.993979 0.0527049 +0.125851 -0.991696 -0.0264878 0.376823 -0.911899 -0.162617 0.52168 -0.810039 -0.267743 +0.0498547 -0.993065 -0.106468 0.00221345 0.923183 0.384354 0.150321 0.583351 0.798189 +0.00866876 0.999837 0.015853 -0.00803804 0.99989 -0.0124676 -0.0521521 0.99863 -0.00424207 +-0.105282 0.993549 -0.0421422 -0.147658 0.921483 -0.359258 -0.213709 0.790572 -0.573867 +0.189183 0.65142 0.734753 0.28225 0.730411 0.62196 0.350317 0.644814 0.679333 +-0.385375 0.699121 -0.602258 -0.468778 0.143071 -0.871652 -0.56515 0.256471 -0.78411 +0.0920418 0.756792 0.647143 -0.006192 0.403412 0.914997 -0.135788 0.0447959 -0.989725 +0.0223535 0.015673 0.999627 -0.45352 0.336568 -0.825252 -0.307164 0.134882 -0.942049 +0.402146 -0.589521 0.700531 0.163542 -0.38344 0.908971 -0.581542 -0.107413 -0.806394 +-0.305628 -0.221726 -0.925975 0.359616 -0.613565 0.703004 0.154705 -0.777896 0.609052 +0.42125 -0.711656 0.562223 0.751123 -0.653141 0.0960202 -0.0253025 -0.463567 -0.8857 +0.218905 -0.911863 0.347255 0.136936 -0.983791 0.115771 0.294787 -0.947839 0.121249 +0.333789 -0.942628 0.00618619 0.351036 -0.936216 0.0165135 0.534023 -0.843833 0.0525849 +0.779333 -0.625696 -0.033843 0.593355 -0.754934 0.279293 0.323829 -0.72688 -0.605624 +0.519498 -0.72211 -0.456815 0.455724 -0.887004 -0.0744239 0.0127569 0.940246 0.340257 +0.0462921 0.734161 0.677395 -0.0133986 0.999192 0.0378985 0.00954445 0.999887 -0.0116228 +0.0228354 0.998882 -0.0414038 -0.109546 0.961702 -0.251254 -0.162517 0.755204 -0.635023 +-0.401019 0.725751 -0.558989 0.0884321 0.685159 0.723005 0.0984171 0.723322 0.683461 +-0.407149 0.591713 -0.695777 -0.343821 0.529722 -0.775359 -0.5044 0.559726 -0.657486 +0.0196354 0.642336 0.766172 -0.00735439 0.391049 0.92034 -0.514103 0.493675 -0.701415 +-0.278826 0.295973 -0.913595 -0.00584147 -0.0021199 0.999981 -0.0731222 0.0684571 -0.994971 +0.0326336 -0.345289 0.937929 -0.0210668 -0.0296978 -0.999337 0.0718109 -0.643423 0.762135 +0.03222 -0.75848 0.650899 0.127073 -0.274252 -0.953225 0.0835818 -0.890729 0.446785 +0.302163 -0.897298 0.321796 0.47485 -0.780745 0.406146 0.539306 -0.800955 0.260038 +0.552782 -0.832356 0.0401961 0.54209 -0.840276 -0.00856878 0.541571 -0.816201 0.201285 +0.321546 -0.733321 -0.59904 0.293739 -0.539817 -0.788869 0.384265 -0.910944 0.150067 +0.391917 -0.919925 -0.0118256 0.417087 -0.905813 0.0744419 0.393775 -0.895243 0.208521 +0.418285 -0.905703 -0.0688524 0.135806 0.959501 0.246808 0.183526 0.743838 0.642668 +-0.0160463 0.999323 0.033109 -0.0151163 0.999864 0.00663334 -0.000949853 0.999218 -0.0395291 +-0.00580112 0.943077 -0.332523 0.0197569 0.760355 -0.649208 0.31976 0.614655 0.721077 +0.12317 0.629706 0.767007 -0.0541489 0.62286 -0.780457 -0.0284606 0.457297 -0.888859 +-0.00664235 0.593727 0.804639 -0.017277 0.365771 0.930544 -0.429959 0.521187 -0.737224 +-0.151607 0.440557 -0.88483 -0.00786864 0.0176022 0.999814 -0.28469 0.553084 -0.782975 +-0.11265 0.299298 -0.947486 0.127992 -0.290844 0.948171 0.0151271 0.0492909 -0.99867 +0.131901 -0.669156 0.731323 0.261403 -0.72563 0.636498 0.035585 -0.067943 -0.997054 +0.392443 -0.709149 0.585745 0.481583 -0.739039 0.471061 0.469959 -0.731098 0.494605 +0.340902 -0.581994 -0.738288 0.179124 -0.371177 -0.911121 0.287393 -0.915439 0.281737 +0.150218 -0.982387 0.111128 0.337357 -0.934399 0.114409 0.457443 -0.887848 0.0497132 +0.445263 -0.86933 0.214492 0.594863 -0.79079 0.144185 0.573762 -0.816917 0.0586865 +0.422243 -0.766628 -0.483727 0.212714 -0.628882 -0.747837 0.369356 -0.887689 -0.274925 +0.13187 0.891502 0.433399 0.236777 0.530138 0.814181 0.356534 0.694527 0.624912 +0.0222432 0.999667 0.0130711 0.0126238 0.999835 0.0130618 0.0383213 0.999112 0.0174927 +0.0123547 0.99821 -0.058509 -0.0234171 0.946812 -0.320935 -0.103696 0.768126 -0.631846 +0.05607 0.424202 0.90383 -0.00883827 0.593278 -0.804949 -0.0300862 0.396932 -0.917355 +0.0642263 0.56756 0.820823 5.54002e-05 0.379449 0.925213 0.0283297 0.408239 -0.912435 +-0.000277503 0.0425832 0.999093 -0.0641736 0.567469 -0.82089 0.0189759 0.386243 -0.922202 +0.00461872 -0.0629399 0.998007 0.00102194 0.0613941 -0.998113 0.409633 -0.591915 0.694145 +0.178762 -0.367441 0.912705 0.00170905 -0.0721141 -0.997395 0.379963 -0.606718 0.698227 +0.250385 -0.763724 0.595007 0.422361 -0.706111 0.568347 0.0477443 -0.353496 -0.934217 +0.228581 -0.911566 0.341757 0.131145 -0.980566 0.145916 0.300986 -0.912563 0.276832 +0.474156 -0.782901 0.402793 0.519287 -0.806075 0.283873 0.679681 -0.708799 0.188782 +0.747036 -0.635702 0.194476 0.120271 -0.731563 -0.671082 0.0949392 -0.620104 -0.778754 +0.363071 -0.896114 0.255264 0.460276 -0.789193 0.406596 0.236367 -0.820117 0.521094 +0.783361 -0.618989 0.0565624 0.304345 -0.778753 -0.548559 0.478665 -0.684467 -0.549895 +0.374457 -0.921446 -0.103536 0.0235588 0.895715 0.444004 0.0689081 0.499516 0.86356 +0.0491515 0.998213 0.0339854 0.0424661 0.999054 0.0093712 0.269792 0.949456 0.160453 +0.311008 0.94806 -0.0667505 -0.0969142 0.871268 -0.481144 -0.321878 0.766527 -0.555725 +0.181325 0.464345 0.866894 -0.138659 0.683764 -0.716408 0.0336928 0.483808 -0.874525 +0.298389 0.562915 0.770773 0.108598 0.320779 0.940908 0.254092 0.520443 -0.815215 +0.00688531 0.0321855 0.999458 0.183669 0.568459 -0.801948 0.158426 0.318077 -0.934734 +0.0399123 -0.0472816 0.998084 -0.00818681 0.018984 -0.999786 0.195494 -0.269486 0.942952 +-0.0245032 -0.0533422 -0.998276 0.243538 -0.595065 0.765889 0.293705 -0.755419 0.58573 +0.0878832 -0.269521 -0.958976 0.211136 -0.906051 0.366732 0.302105 -0.896199 0.324899 +0.457445 -0.762584 0.457394 0.449978 -0.766758 0.457823 0.265443 -0.705293 -0.657345 +0.268026 -0.572194 -0.775085 0.283405 -0.924965 0.25322 0.0813005 -0.989146 0.122394 +0.0398484 -0.910309 0.412008 0.0198985 -0.83192 0.554539 0.166018 -0.754488 -0.634969 +0.309523 -0.674441 -0.670317 0.139294 -0.989702 -0.0329793 0.435186 0.880454 0.188185 +0.499711 0.794591 0.34484 0.549549 0.826108 0.124668 0.380612 0.779848 -0.496962 +0.398607 0.879317 -0.260605 0.232518 0.613695 -0.754529 0.0767223 0.859127 0.505979 +0.159736 0.616804 0.770738 0.256231 0.939055 0.229177 0.3543 0.922355 0.154054 +0.104861 0.571709 -0.813728 -0.274896 0.73288 -0.62235 0.231575 0.597823 0.767451 +0.250559 0.439906 0.862382 0.364155 0.555615 0.747452 -0.0856275 0.844693 -0.528358 +0.0329539 0.801822 -0.596653 0.188031 0.674302 -0.714115 0.0845311 0.117641 0.989452 +0.376325 0.549491 -0.745948 0.317088 0.628587 -0.710165 0.214093 0.450311 -0.866824 +0.299015 0.128859 0.945508 0.0113994 0.0670258 -0.997686 0.38726 -0.0285045 0.92153 +-0.00797424 0.0013958 -0.999967 0.373267 -0.333581 0.865676 0.039904 -0.00609995 -0.999185 +0.323333 -0.598181 0.733236 0.224116 -0.543913 0.80866 0.0528166 -0.063169 -0.996604 +0.264401 -0.816881 0.512638 0.386 -0.821449 0.419793 0.473996 -0.754285 0.454293 +0.476494 -0.74244 0.470889 0.352026 -0.604431 -0.714662 0.1825 -0.365394 -0.912787 +0.283638 -0.915693 0.284703 0.107065 -0.990613 0.0849847 0.304889 -0.939201 0.157939 +0.224895 -0.90917 0.350473 0.452901 -0.738513 0.499478 0.343189 -0.721621 -0.601236 +0.37986 -0.686965 -0.619505 0.300436 -0.605462 -0.73699 0.414538 -0.908451 0.0536165 +0.315375 -0.822048 -0.474105 0.74875 0.610139 0.259044 0.590068 0.666926 0.455006 +0.625215 0.752253 0.207896 0.595598 0.751141 0.284694 0.680359 0.716954 0.151952 +0.600519 0.799241 0.0242947 0.415947 0.862055 -0.289567 0.421526 0.566714 -0.70792 +0.355599 0.778587 0.517061 0.272212 0.720313 0.638004 0.362996 0.688047 -0.628352 +0.569048 0.481901 -0.6663 0.572885 0.636921 -0.515882 0.315058 0.59911 0.736074 +0.183047 0.335031 0.924255 0.379474 0.759502 -0.528352 0.371749 0.683462 -0.628238 +0.383239 0.352711 0.853653 0.237146 0.637734 -0.732842 0.202031 0.29548 -0.933743 +0.54548 0.471399 0.69299 0.472787 0.217303 0.853963 0.604001 0.324415 0.727968 +0.0778279 0.046371 -0.995888 0.596262 0.0866953 0.798095 0.348997 -0.0262754 0.936755 +0.315825 0.158149 -0.935544 0.542955 -0.188033 0.818439 0.235627 -0.325452 0.91573 +0.29404 -0.0811732 -0.95234 0.35869 -0.558213 0.748157 0.184874 -0.652062 0.73528 +0.0599285 -0.0977474 -0.993405 0.233346 -0.755201 0.612553 0.212114 -0.785501 0.581374 +0.398046 -0.747185 0.532235 0.140295 -0.269999 -0.952585 0.216521 -0.91524 0.339786 +0.106209 -0.986906 0.121392 0.321259 -0.906815 0.272908 0.661635 -0.543692 0.51637 +0.697782 -0.590447 0.405552 0.738982 -0.604148 0.298179 0.337536 -0.686888 -0.643626 +0.298346 -0.51743 -0.802033 0.742972 -0.427244 0.515224 0.648717 -0.639436 0.412659 +0.829949 -0.474409 0.293464 0.749195 -0.654566 0.101247 0.586654 -0.702877 -0.402245 +0.455691 -0.70703 -0.54079 0.458314 -0.881868 0.110707 0.396645 -0.880293 -0.260302 +0.750514 0.561459 0.348559 0.781758 0.155614 0.603853 0.923216 0.334507 0.189149 +0.853385 0.432039 -0.291678 0.759388 0.641459 -0.108903 0.706752 0.547221 -0.448386 +0.935197 -0.00650485 0.354067 0.766445 -0.0374792 0.641215 0.997396 0.0701093 -0.0169196 +0.692012 0.494781 -0.525652 0.852538 0.359375 -0.379511 0.691803 0.505071 -0.516055 +0.930341 0.0228293 0.365983 0.78298 0.0638539 0.61876 0.997589 -0.000641001 0.0693993 +0.994312 0.0783149 -0.072185 0.69183 0.517611 -0.503439 0.862478 0.348811 -0.366692 +0.707561 0.511096 -0.487994 0.791239 0.442878 0.421663 0.712201 0.330358 0.619381 +0.923788 0.378914 0.0551392 0.913259 0.407337 -0.00588461 0.862438 0.407244 -0.300588 +0.741876 0.433557 -0.511515 0.850121 0.253443 -0.461585 0.764111 0.307167 -0.567259 +0.688013 0.476274 0.547541 0.626229 0.562572 0.539768 0.536627 0.607193 0.585959 +0.737446 0.395072 -0.547807 0.75642 0.257932 -0.601082 0.58578 0.485396 0.64904 +0.382742 0.390806 0.837126 0.482469 0.596967 0.640979 0.552719 0.657056 -0.512619 +0.639475 0.45684 -0.61836 0.548611 0.625172 -0.555144 0.379991 0.401787 0.833171 +0.45765 0.579383 -0.674442 0.556771 0.589812 -0.584917 0.540665 0.381767 -0.749624 +0.506065 0.5355 0.67612 0.368124 0.367649 0.854002 0.512171 0.5281 0.677341 +0.386905 0.0561119 -0.920411 0.0781957 0.049074 0.99573 0.446893 0.19566 -0.872928 +0.0418249 -0.0472513 0.998007 0.5823 0.115852 -0.804677 0.552867 0.409007 -0.725983 +0.510259 0.251467 -0.822435 0.191027 -0.296706 0.935668 0.547431 -0.274174 -0.790663 +0.389624 -0.145139 -0.909466 0.179099 -0.647492 0.740728 0.268162 -0.688802 0.673528 +0.296231 -0.28122 -0.912777 0.463863 -0.444166 -0.766516 0.318071 -0.652417 0.687883 +0.31146 -0.748322 0.585667 0.128692 -0.146978 -0.980732 0.182113 -0.918036 0.352201 +0.33506 -0.862396 0.379483 0.711032 -0.251053 0.656815 0.7303 -0.459147 0.505812 +0.282462 -0.635026 -0.718997 0.200862 -0.382455 -0.901877 0.8227 -0.364042 0.436622 +0.703715 -0.426976 0.56787 0.732786 -0.511144 0.449173 0.367631 -0.742784 -0.559571 +0.155088 -0.726864 -0.669042 0.100895 -0.748357 -0.655578 0.49778 -0.838443 0.221875 +0.389254 -0.887612 0.246224 0.329962 -0.943712 -0.023103 0.062933 -0.990796 -0.119848 +-0.0988611 -0.875129 -0.473683 0.33589 0.763488 0.551601 0.543092 0.315032 0.778335 +0.786592 0.522884 0.328428 0.755791 0.644491 0.115805 0.574663 0.777005 0.256956 +0.795943 0.582537 -0.164697 0.745199 0.66677 0.00981852 0.907812 0.403151 -0.115527 +0.685607 0.711734 -0.152899 0.864934 0.307076 -0.396981 0.58762 0.768946 -0.251843 +0.714882 0.479098 -0.509322 0.779797 0.242191 0.577286 0.649699 0.128533 0.749247 +0.846396 0.376696 -0.376449 0.909598 0.210799 -0.358043 0.80123 0.156904 -0.577418 +0.680417 0.0203108 0.732543 0.73252 0.0711344 0.677019 0.831565 0.393637 -0.391854 +0.724575 0.402072 -0.559758 0.804702 0.422324 -0.417249 0.693577 0.519192 -0.499389 +0.759783 0.10679 0.641346 0.739625 0.292347 0.606208 0.825642 0.381219 -0.415918 +0.680404 0.544599 -0.49037 0.847285 0.353127 -0.396748 0.678906 0.542493 -0.49476 +0.737898 0.370358 0.564217 0.742685 0.259583 0.61728 0.806916 0.287009 0.516249 +0.838045 0.219242 0.499613 0.872963 0.182186 -0.452487 0.765292 0.392777 -0.509956 +0.86329 -0.0548992 -0.501714 0.785353 0.0965063 -0.611479 0.760445 0.360747 0.539986 +0.605814 0.212325 0.766751 0.787744 0.0713429 -0.611857 0.789747 -0.281362 -0.545102 +0.751404 -0.0165449 -0.659635 0.472205 0.301787 0.828219 0.560491 0.613228 -0.556598 +0.656521 0.476819 -0.584485 0.659608 0.549125 -0.513205 0.559016 0.418574 0.715749 +0.372312 0.375991 0.848537 0.497682 0.516615 0.696722 0.663293 0.171631 -0.728413 +0.635711 0.492462 -0.594435 0.682924 0.324856 -0.65428 0.0320833 0.0534517 0.998055 +0.671198 0.0709563 -0.737874 0.732551 0.0876528 -0.675045 0.0089021 0.0326234 0.999428 +0.642011 0.323918 -0.694909 0.660271 0.118269 -0.741656 0.317151 0.08666 0.944407 +0.347468 -0.0813272 -0.934158 0.418385 -0.2412 0.875658 0.155503 -0.141444 -0.977657 +0.430672 -0.56807 0.701297 0.409203 -0.378405 0.830279 0.440864 -0.454068 -0.774249 +0.335559 -0.306449 -0.89078 0.554134 -0.551093 0.623885 0.535914 -0.679502 0.501071 +0.479615 -0.679637 0.555035 0.372181 -0.238298 -0.897048 0.528187 -0.347482 -0.774774 +0.351068 -0.862171 0.365256 0.430655 -0.779067 0.455621 0.680768 -0.197935 0.70525 +0.625506 -0.386753 0.677617 0.403288 -0.418742 -0.813642 0.691455 -0.193076 0.696141 +0.660636 -0.310841 0.683329 -0.0530996 -0.803462 -0.592983 0.144178 -0.857561 -0.493762 +-0.270632 -0.773335 -0.573333 0.276447 -0.851745 -0.445093 0.603979 -0.66232 0.443329 +0.525498 -0.842533 0.118278 0.454742 -0.867413 0.201999 0.671947 -0.685428 0.280492 +0.652942 -0.752024 0.0901446 0.662755 -0.674349 -0.32559 0.276922 -0.838224 -0.469781 +0.734375 -0.666916 -0.126162 0.748934 0.645923 0.147924 0.778677 0.487369 0.395137 +0.9068 0.413738 -0.0808327 0.739556 0.573544 -0.352284 0.737005 0.395157 0.548338 +0.779394 0.128146 0.613289 0.914105 0.218522 0.341556 0.992176 0.115365 -0.0477231 +0.75604 0.412452 -0.508219 0.854662 0.237358 -0.461751 0.748769 0.294962 -0.593585 +0.932226 0.0257008 0.360962 0.779639 0.0748715 0.621737 0.998402 0.00701648 0.0560759 +0.923345 0.167097 -0.345706 0.783351 0.225102 -0.579388 0.953577 0.102153 0.283293 +0.771 0.28534 0.569334 0.997119 -0.0186518 0.0735185 0.994345 0.0323522 -0.101147 +0.736442 0.36625 -0.568783 0.853951 0.205621 -0.478003 0.763556 0.306204 -0.568526 +0.708301 0.376259 0.597277 0.738962 0.256199 0.623135 0.909006 0.197708 0.366906 +0.997657 0.0362632 0.058014 0.999577 0.00364701 0.0288466 0.998701 -0.0145671 -0.0488266 +0.927078 0.0130902 -0.37464 0.809363 0.0794405 -0.581911 0.769551 0.629215 0.108995 +0.757349 0.519973 0.395034 0.920995 0.344941 -0.181061 0.783671 0.403087 -0.472632 +0.945776 0.0635147 0.318548 0.771424 -0.00911669 0.636257 0.998745 0.0161889 0.0473969 +0.999969 -0.00650684 0.00450595 0.998735 -0.00776097 -0.0496744 0.932399 -0.00957158 -0.361304 +0.820353 -0.00890732 -0.571788 0.665686 0.43507 0.606281 0.684119 0.252585 0.684238 +0.928846 0.224348 0.29481 0.911824 0.246775 -0.328146 0.788186 0.210543 -0.578304 +0.928427 -0.0369212 0.369675 0.625774 -0.237119 0.743089 0.999615 0.0137019 0.0241223 +0.999995 0.0027593 -0.00145406 0.998801 -0.000164695 -0.0489631 0.932704 0.00259484 -0.360635 +0.82294 0.000104283 -0.568128 0.932415 0.0344039 0.359749 0.7465 0.0191311 0.665111 +0.997297 0.0732661 -0.00553614 0.700191 0.429805 -0.570089 0.839775 0.382061 -0.385757 +0.653055 0.543555 -0.52732 0.833633 -0.195501 0.516561 0.50485 -0.277379 0.817427 +0.997375 -0.0262787 0.0674654 0.99973 0.0201667 -0.0115648 0.998669 0.0121558 -0.0501149 +0.932922 0.0169972 -0.359677 0.810198 0.00496943 -0.586135 0.933523 -0.0264637 0.35754 +0.742022 -0.0800243 0.665582 0.997707 -0.00491346 0.0675061 0.993308 0.0449049 -0.106413 +0.625626 0.503744 -0.595679 0.83315 0.180185 -0.522872 0.520659 0.309768 -0.795587 +0.695016 -0.346457 0.630016 0.749309 -0.218229 0.625229 0.93892 -0.151989 0.308752 +0.999969 -0.00560598 -0.00553797 0.998013 -0.0355579 -0.0520205 0.940601 -0.0332982 -0.337878 +0.782778 0.0025235 -0.622296 0.87054 -0.204795 0.447458 0.701991 -0.307147 0.642549 +0.990691 -0.103506 0.0884206 0.995446 -0.0755583 -0.0581291 0.89662 -0.221385 -0.383485 +0.644647 0.0611989 -0.762027 0.872338 -0.246633 0.422136 0.71592 -0.288828 0.635639 +0.993676 -0.100712 0.0496403 0.944362 -0.288125 -0.158631 0.905719 -0.337977 -0.25582 +0.788423 -0.194807 -0.583472 0.725818 -0.443448 0.525873 0.731723 -0.538074 0.418398 +0.867782 -0.418246 0.268373 0.902707 -0.407919 -0.136833 0.752372 -0.355914 -0.554313 +0.757864 -0.509784 -0.407139 0.679665 -0.462877 0.569035 0.741695 -0.558549 0.371366 +0.891998 -0.443732 0.0862637 0.797445 -0.557665 -0.230413 0.801385 -0.515594 -0.303224 +0.770123 -0.521351 -0.367565 0.818101 -0.5716 0.0631226 0.788747 -0.551925 -0.270658 +0.816912 -0.56118 -0.133159 0.498382 0.858731 0.11915 0.677413 0.607562 0.414706 +0.868738 0.485587 -0.0974643 0.646784 0.67604 -0.353044 0.653623 0.504953 0.563737 +0.66508 0.543885 0.511721 0.923738 0.269473 0.272199 0.995474 0.0950228 0.00166148 +0.720442 0.485783 -0.494953 0.856354 0.354173 -0.375793 0.67818 0.497677 -0.540731 +-0.146564 0.944714 0.293318 0.168533 0.821598 0.544585 0.546736 0.823986 0.148749 +0.619269 0.784215 -0.0388922 -0.034042 0.991248 0.12755 0.553019 0.745851 -0.371317 +0.324176 0.913367 -0.246312 0.112049 0.829881 -0.546573 0.437857 0.836507 0.329449 +0.698517 0.606429 0.379892 0.898818 0.40903 0.157546 0.944139 0.279254 0.174982 +0.560278 0.735373 0.381204 0.999371 0.025749 0.0243769 0.998937 -0.0134775 0.0440711 +0.994415 0.0454479 -0.0952517 0.681696 0.469093 -0.561464 0.843141 0.238292 -0.482006 +0.719449 0.335145 -0.608335 -0.102222 0.812613 0.573768 0.0979349 0.56795 0.817216 +0.310235 0.886964 -0.342124 0.951064 0.277962 -0.134959 0.491137 0.861172 0.131022 +0.999814 0.0027498 0.0190946 0.99966 0.0222584 0.0136104 0.999976 0.00522128 -0.00453975 +0.999868 -0.0144023 0.00749097 0.999999 0.00110091 0.00131802 0.923376 0.188891 -0.334212 +0.764483 0.248878 -0.594664 -0.134171 0.596329 0.791448 0.127902 0.283657 0.950358 +0.494156 0.379013 0.782406 0.853516 0.265312 -0.448464 0.996316 0.0363523 -0.0776722 +0.998666 -0.0444225 0.0263215 0.999893 -0.013663 0.00525142 0.999946 0.00940742 0.00439134 +0.999705 -0.0131447 0.0204315 0.997686 0.00838226 -0.06747 0.715735 0.396255 -0.575069 +0.85113 0.169957 -0.496681 0.761216 0.274394 -0.587587 0.643766 0.0876817 0.760183 +0.331747 0.121941 0.935454 0.848951 0.384623 -0.362421 0.996327 0.0678923 -0.052187 +0.999249 -0.0121703 0.0368008 0.999999 0.000315345 -0.00097408 0.999747 -0.0167551 -0.0150262 +0.953461 -0.26237 -0.148574 0.905273 -0.301393 -0.299405 0.817632 -0.278313 -0.504004 +0.669685 0.274238 0.690156 0.292065 0.372888 0.880712 0.839448 0.356865 -0.409847 +0.998678 0.0459571 -0.0230226 0.999677 -0.0100565 -0.023358 0.935211 -0.164509 -0.313556 +0.709792 -0.664626 -0.233383 0.616633 -0.495119 -0.612064 0.679961 -0.622063 -0.388189 +0.276938 0.556626 0.783245 0.4267 0.283869 0.858688 0.57785 0.395091 0.714138 +0.962849 -0.0321916 -0.268113 0.998579 -0.0266919 -0.0461308 0.886022 -0.202678 -0.416997 +0.536033 -0.262575 -0.802324 0.652823 0.0736544 0.753922 0.357393 0.0270085 0.933564 +0.889909 0.0900031 -0.44717 0.991535 0.0147412 -0.128998 0.96227 -0.135675 -0.235859 +0.578586 -0.344302 -0.739388 0.734064 -0.299381 -0.609525 0.598842 0.121834 0.791546 +0.31993 0.144671 0.936331 0.838324 0.343318 -0.423493 0.92963 0.0374375 -0.366589 +0.767941 -0.0817004 -0.635289 0.0386568 0.0233752 0.998979 0.772014 0.133959 -0.621329 +0.756073 0.00708836 -0.654449 0.0200673 0.0310242 0.999317 0.658663 0.0215309 -0.75213 +0.631325 -0.101951 -0.768788 0.372658 0.363219 0.853931 0.571648 -0.078088 -0.816774 +0.482148 -0.0800294 -0.872427 0.542739 0.479028 0.689903 0.460764 0.183049 0.868441 +0.590339 0.325914 0.738431 0.478558 -0.260258 -0.838599 0.441289 -0.176777 -0.879781 +0.588723 -0.0871357 0.803625 0.364207 -0.222632 0.904316 0.351301 -0.227918 -0.908098 +0.522109 -0.310743 -0.794255 0.529995 -0.429661 0.731093 0.408955 -0.397198 0.821577 +0.385599 -0.189791 -0.902936 0.51326 -0.574211 0.637844 0.43481 -0.434809 0.788594 +0.562123 -0.187944 -0.805416 0.405221 -0.205551 -0.890811 0.534162 -0.758938 0.372402 +0.566177 -0.736867 0.369419 0.756902 -0.247267 0.604945 0.657995 -0.370059 0.655819 +0.514086 -0.451583 -0.729238 0.361121 -0.300313 -0.882839 0.668462 -0.254655 0.698791 +0.719156 -0.234032 0.65425 0.631829 -0.471313 -0.615351 0.528675 -0.511381 -0.677489 +0.327989 -0.837565 -0.43693 0.240165 -0.788487 -0.566224 0.634587 -0.491826 0.59616 +0.562832 -0.666463 0.488924 0.634525 -0.688066 0.352057 0.728831 -0.66224 0.173907 +0.758296 -0.61504 0.216131 0.791521 -0.482651 -0.37489 0.808422 -0.306363 -0.50259 +0.55716 -0.216623 -0.801652 0.649377 -0.67738 0.345638 0.726684 -0.60722 0.32127 +0.735072 -0.57573 0.358055 0.817772 -0.519877 0.246933 0.838827 -0.540509 0.0649547 +0.856341 -0.451838 -0.250044 0.582895 -0.441411 -0.682195 0.641803 -0.559909 -0.524015 +0.73346 -0.620381 0.277782 0.77855 -0.627278 -0.0195572 0.694655 -0.636053 -0.335991 +0.77828 0.510449 0.365679 0.778947 0.179728 0.600782 0.916815 0.347968 0.195878 +0.849765 0.428706 -0.306773 0.804131 0.58701 -0.093767 0.712136 0.523852 -0.467378 +0.928735 0.0189349 0.37026 0.779543 -0.0144297 0.626183 0.995855 0.0814015 -0.0405903 +0.682484 0.476969 -0.55382 0.864856 0.227683 -0.44742 0.700327 0.329971 -0.632979 +0.771566 0.564005 0.294252 0.744727 0.511774 0.428333 0.838455 0.492815 0.232651 +0.801819 0.594514 -0.0603334 0.819964 0.570429 0.0476413 0.808433 0.491254 -0.324202 +0.732366 0.604431 -0.313534 0.650548 0.608965 -0.453816 0.923837 -0.00748646 0.382713 +0.811111 0.0751594 0.580043 0.999359 -0.0280811 0.0222042 0.937431 1.76853e-05 -0.348171 +0.771559 0.0561657 -0.633673 0.61412 0.421944 0.666949 0.491019 0.162169 0.855922 +0.816908 0.327635 0.474676 0.782821 0.481055 0.394686 0.807751 0.40122 -0.431926 +0.701468 0.50207 -0.505835 0.831738 0.381174 -0.403632 0.706128 0.498327 -0.503045 +0.948606 0.116827 0.294105 0.796369 0.268893 0.54175 0.999984 -0.00566712 0.000500992 +0.938602 -0.0133909 -0.344743 0.778367 -0.0139703 -0.627654 0.742292 0.186384 0.643633 +0.34301 -0.0154711 0.939204 0.91736 0.187001 -0.351398 0.70947 0.504897 -0.491661 +0.86503 0.350591 -0.3589 0.713204 0.509913 -0.480966 0.754137 0.326878 0.569586 +0.774176 0.247532 0.582563 0.898057 0.202694 0.390397 0.996349 0.0375272 0.0766818 +0.998981 0.017071 -0.0417853 0.937345 0.000756624 -0.348401 0.77559 -0.00392627 -0.631224 +0.629455 0.0453419 0.775713 0.343446 0.0913044 0.934724 0.865714 0.24496 -0.436502 +0.983695 0.08641 -0.157725 0.729946 0.4597 -0.505821 0.889038 0.189778 -0.416649 +0.739862 0.309377 -0.597403 0.9475 0.152079 0.281276 0.761796 0.294297 0.57711 +0.996917 -0.000257126 0.0784685 0.998721 0.00205702 -0.0505086 0.938262 0.0185265 -0.345431 +0.774651 0.0144702 -0.632224 0.637274 0.290938 0.713609 0.533244 0.443409 0.720444 +0.931428 0.115215 -0.345206 0.936739 0.00854035 -0.349924 0.805541 0.0559401 -0.589894 +0.763912 0.328648 0.555363 0.778012 0.239083 0.580979 0.822451 0.296261 0.485596 +0.925724 0.377601 -0.0212699 0.999557 0.0291757 -0.00590896 0.99886 -0.0349372 -0.0325395 +0.939278 -0.0450107 -0.340193 0.779596 -0.0140582 -0.626126 0.561826 0.47488 0.677377 +0.454968 0.219929 0.862923 0.636325 0.372647 0.675444 0.905901 0.0147188 -0.423233 +0.924008 -0.00769123 -0.382297 0.824836 -0.022824 -0.564912 0.658575 0.468022 0.589266 +0.704039 0.334592 0.6264 0.932098 0.147262 -0.330919 0.996709 -0.0508382 -0.0631441 +0.909461 -0.227815 -0.347823 0.729139 -0.331734 -0.598588 0.641639 0.0989653 0.760595 +0.326452 -0.0257264 0.944863 0.910075 -0.00182648 -0.41444 0.917921 0.0026125 -0.396754 +0.840158 0.00929173 -0.542262 0.762214 0.356077 0.540592 0.774037 0.18047 0.606876 +0.607341 0.379184 0.698109 0.831655 0.287735 -0.47493 0.942532 -0.0430363 -0.331334 +0.661971 -0.424527 -0.617714 0.764158 -0.282995 -0.579635 0.596596 -0.179481 0.782215 +0.339686 -0.228998 0.912235 0.898446 -0.0247136 -0.438388 0.922833 -0.0365874 -0.383458 +0.842579 -0.00917556 -0.538495 0.6552 0.0960992 0.749319 0.76929 0.00768669 0.638853 +0.716297 0.397267 -0.57367 0.663119 0.0952575 -0.742428 0.544223 -0.434188 0.717846 +0.405459 -0.401652 0.821145 0.84636 -0.0318695 -0.531657 0.871235 -0.184029 -0.455063 +0.823758 -0.168793 -0.541231 0.656914 -0.0233298 0.753605 0.768096 -0.00427423 0.640321 +0.5039 0.255925 -0.824977 0.589246 -0.308973 0.746542 0.36972 -0.216651 0.903532 +0.74359 -0.121317 -0.657539 0.781577 -0.35127 -0.515507 0.750974 -0.263493 -0.605483 +0.749631 -0.128419 0.649278 0.76315 -0.0365111 0.64519 0.753229 0.00806443 -0.657709 +0.566833 0.3364 -0.752021 0.467421 0.252237 -0.847287 0.595852 -0.297281 0.746046 +0.375757 -0.208032 0.903066 0.785757 -0.236151 -0.571681 0.752411 -0.088212 -0.65276 +0.852114 -0.235278 0.46749 0.725912 -0.263755 0.635205 0.800553 -0.377481 0.465427 +0.913034 -0.277975 -0.298494 0.836288 -0.000230761 -0.54829 0.392161 0.0528916 -0.918375 +0.627468 -0.389739 0.674083 0.729631 -0.273661 0.626697 0.716102 -0.435056 0.545823 +0.888564 -0.397269 -0.229415 0.882396 -0.398056 -0.250855 0.849079 -0.310639 -0.427281 +0.765357 -0.146541 -0.626701 0.658487 -0.489514 0.571638 0.682846 -0.49965 0.532983 +0.825868 -0.393623 0.403736 0.899116 -0.393307 0.192095 0.941968 -0.325472 -0.0822398 +0.990573 -0.124377 -0.057409 0.897038 -0.0905094 -0.432586 0.42026 -0.0264712 -0.907017 +0.838906 -0.264867 0.475482 0.722125 -0.317879 0.614401 0.932602 -0.349059 0.0917144 +0.979959 -0.155696 -0.124256 0.899308 -0.317086 -0.301168 0.736089 -0.401608 -0.544871 +0.752341 -0.478401 -0.452897 0.718358 -0.48842 0.495385 0.728304 -0.547066 0.412664 +0.846767 -0.451525 0.281266 0.915071 -0.40212 0.0307384 0.92655 -0.376171 -0.00112334 +0.908397 -0.331651 -0.254604 0.520453 -0.156792 -0.839372 0.731366 -0.426644 0.532052 +0.731693 -0.52567 0.433931 0.869941 -0.405128 0.2812 0.920598 -0.361892 -0.146746 +0.747115 -0.460819 -0.479025 0.762328 -0.46556 -0.449566 0.802544 -0.57705 0.151448 +0.829292 -0.558737 0.00940076 0.817927 -0.574567 -0.0294579 0.734292 -0.513372 -0.444144 +0.780055 -0.621024 0.0764339 0.716194 -0.628973 -0.302422 0.732727 0.6087 0.304294 +0.714705 0.592937 0.370975 0.932639 0.328224 0.149843 0.944091 0.329076 0.0199988 +0.765959 0.638735 0.0729692 0.834223 0.443674 -0.327452 0.751208 0.624965 -0.21238 +0.410455 0.549884 -0.72743 0.210139 0.848115 0.486357 0.253005 0.714411 0.652385 +0.494354 0.666341 0.558214 0.331328 0.900081 0.282976 0.857523 0.456983 -0.236262 +0.60214 0.798378 0.00454815 0.995171 0.0881564 -0.0431728 0.99747 0.0225215 0.0674211 +0.997317 0.0538294 -0.0496074 0.441321 0.545505 -0.712502 0.817161 0.224326 -0.530967 +0.319711 0.446495 -0.835719 0.10391 0.692776 0.713627 0.293966 0.727616 0.619806 +0.457915 0.633336 0.623859 0.857278 0.334749 -0.391174 0.999501 0.0198248 -0.0245855 +0.998822 -0.048492 0.0015367 0.933308 0.00121158 -0.359075 0.602019 0.270416 -0.751298 +-0.483829 0.764596 0.425796 -0.567375 0.790099 0.232012 -0.52271 0.801794 0.289657 +-0.293571 0.940947 -0.168628 -0.418341 0.809218 -0.412501 -0.0501073 0.86701 0.495764 +0.0953709 0.815105 0.571409 0.352881 0.785112 0.508993 0.141689 0.889425 0.434566 +0.938775 0.158447 -0.305934 0.999249 -0.0117692 -0.0369124 0.953329 -0.0325211 -0.300177 +0.771765 0.0116977 -0.6358 -0.417412 0.837477 0.352702 -0.262663 0.570232 0.77836 +-0.517002 0.810072 0.276572 -0.566809 0.782548 0.25758 -0.154269 0.98746 0.0335077 +0.848209 0.281014 -0.448968 0.991229 0.0571073 -0.119182 0.952841 -0.00511773 -0.303427 +0.778303 -0.014344 -0.627725 -0.414351 0.425693 0.804425 -0.38565 0.427544 0.817607 +0.945568 0.109972 -0.30628 0.930226 0.0312799 -0.365652 0.780344 -0.0062017 -0.62532 +-0.480443 0.44133 0.757893 -0.203841 0.309373 0.928837 0.958017 -0.257382 -0.126329 +0.929322 -0.0510157 -0.365729 0.672069 -0.122918 -0.730215 0.157818 0.58561 0.795081 +0.595242 -0.694056 -0.404937 0.636031 -0.73771 -0.226383 0.647664 -0.648981 -0.399194 +0.712628 -0.697568 0.0745651 0.576574 -0.736582 0.353567 0.83807 -0.545131 0.021693 +0.632295 -0.412467 -0.6558 0.329818 0.576585 0.747509 0.161653 0.296852 0.941142 +0.56319 -0.381867 -0.732799 0.748736 -0.596697 -0.2887 0.708325 -0.622323 -0.333152 +0.461176 -0.869361 -0.177565 0.0123611 0.0129077 0.99984 0.62049 -0.181045 -0.76303 +0.617598 -0.176105 -0.766525 -0.00318523 -0.0064435 0.999974 0.683682 0.0148103 -0.72963 +0.757895 -0.0157331 -0.652187 -0.000113516 -0.0117447 0.999931 0.679532 -0.0764241 -0.729654 +0.742257 -0.0171496 -0.669896 0.180395 0.319771 0.930163 0.59974 -0.30159 -0.741186 +0.691904 -0.321066 -0.646673 0.180998 0.60576 0.774786 0.310492 0.446758 0.839048 +0.444229 0.575008 0.687041 0.429383 -0.203626 -0.879867 0.561118 -0.390548 -0.729807 +0.105546 0.0718103 0.991818 0.45353 -0.0864419 -0.887039 0.0451673 -0.0834528 0.995488 +0.557707 -0.239803 -0.794643 0.525515 -0.208885 -0.824743 0.467721 -0.50805 0.723272 +0.317183 -0.277605 0.906824 0.572542 -0.27442 -0.772586 0.524236 -0.253005 -0.81312 +0.508072 -0.454662 0.731537 0.476954 -0.269319 0.836649 0.32028 -0.308145 -0.895806 +0.504048 -0.409313 -0.760525 0.724709 -0.526946 0.443988 0.547441 -0.402236 0.733835 +0.812112 -0.49382 0.310832 0.0837443 -0.529784 -0.843988 0.625369 -0.517982 0.583616 +0.639898 -0.548534 0.538184 0.759885 -0.376779 0.529729 0.509579 -0.564088 -0.649719 +0.431677 -0.569669 -0.69938 0.165343 -0.739938 -0.652038 0.585674 -0.565544 0.580643 +0.56521 -0.577081 0.589504 0.546905 -0.63165 0.549466 0.496633 -0.294818 -0.816357 +0.545212 -0.406987 -0.732875 0.60869 -0.562337 0.559709 0.556185 -0.688375 0.465615 +0.648463 -0.665955 0.368781 0.663598 -0.681416 0.308724 0.507757 -0.560331 -0.654379 +0.523319 -0.342814 -0.780139 0.480083 -0.831123 0.280632 0.559073 -0.710375 0.427556 +0.533774 -0.686443 0.493844 0.660959 -0.659171 0.358648 0.694469 -0.714077 0.0883548 +0.676079 -0.703418 -0.219362 0.61864 -0.610115 -0.495019 0.599643 -0.537011 -0.593336 +0.638465 -0.693962 0.332835 0.684937 -0.727753 -0.0351596 0.639139 -0.686499 -0.346728 +0.793724 0.567107 0.219983 0.772731 0.469416 0.427242 0.889504 0.435673 0.137739 +0.837667 0.454165 -0.303394 0.793724 0.554374 -0.250342 0.596081 0.611207 -0.520686 +0.762658 0.350854 0.543374 0.800216 0.15001 0.580648 0.836059 0.30621 0.455236 +0.882194 0.421724 0.209483 0.896243 0.387763 -0.215378 0.642746 0.527464 -0.555571 +0.830962 0.28151 -0.479849 0.708336 0.319245 -0.629557 0.741338 0.628249 0.236053 +0.665301 0.558847 0.49504 0.913922 0.387505 0.120774 0.833573 0.472781 -0.285718 +0.772985 0.622409 -0.12289 0.72652 0.525784 -0.442402 0.77134 0.224813 0.595393 +0.79186 0.0777427 0.605734 0.75589 0.408343 -0.511748 0.713895 0.312343 -0.626734 +0.558021 0.452098 0.695859 0.537822 0.223892 0.812785 0.824664 0.317242 0.468281 +0.896366 0.39487 0.201507 0.895117 0.438998 -0.0777641 0.690821 0.50831 -0.514185 +0.75142 0.508768 -0.420147 0.689631 0.533151 -0.49006 0.740737 0.118724 0.66122 +0.764455 0.251671 0.593524 0.620662 0.185396 -0.761845 0.615124 0.416261 -0.669588 +0.438113 0.243673 -0.865263 0.734587 0.385628 0.558277 0.460975 0.103413 0.881367 +0.751472 0.530331 -0.392477 0.686959 0.624104 -0.372264 0.818769 0.474739 -0.322861 +0.764067 0.597901 -0.242314 0.732491 0.556652 -0.391912 0.791542 0.316405 0.522828 +0.813678 0.163101 0.557966 0.856125 0.204479 0.474593 0.852718 0.0540852 0.519564 +0.629694 0.00221509 -0.77684 0.332795 0.0324205 -0.942442 0.0949988 -0.0128824 0.995394 +0.822624 0.41731 -0.38619 0.721543 0.493297 -0.485833 0.822071 0.396942 -0.408211 +0.704525 0.497212 -0.506384 0.630325 0.774053 0.0594391 0.850532 0.311416 0.423812 +0.928616 0.334262 -0.161062 0.818738 0.467376 -0.333508 0.866158 0.0818972 0.493014 +0.813775 0.0430929 0.579581 0.865889 0.167856 0.471234 0.631442 -0.0104487 -0.775353 +0.329793 -0.00990378 -0.944001 0.039911 -0.0435408 0.998254 0.805538 0.386218 -0.449382 +0.7183 0.44833 -0.53202 0.813868 0.278697 -0.509849 0.749737 0.322459 -0.577853 +0.976327 0.216212 -0.00615917 0.842563 0.0160927 0.538358 0.822484 0.195371 -0.534182 +0.801587 0.24862 0.543734 0.768457 0.430096 0.473805 0.631177 0.00190012 -0.775637 +0.336649 -0.00240543 -0.941627 0.188178 0.343789 0.919999 0.809625 0.210532 -0.54789 +0.778555 0.252568 -0.574509 0.945936 0.0495998 0.32054 0.828272 0.0531485 0.5578 +0.80585 0.537354 -0.24871 0.910125 0.395034 0.124984 0.836098 0.257601 0.484336 +0.627648 0.0211648 -0.77821 0.341189 0.00934513 -0.939948 0.138641 0.610829 0.779529 +0.284177 0.437017 0.853381 0.448639 0.5766 0.682829 0.830962 0.0130175 -0.556176 +0.786731 0.281383 -0.549434 0.82816 -0.0630208 -0.556938 0.819393 0.0978683 -0.564816 +0.941143 0.115818 0.317547 0.816745 0.261462 0.51436 0.94227 0.171725 0.287468 +0.644456 -0.0302409 -0.764043 0.340907 0.00382426 -0.940089 0.0768921 0.083333 0.993551 +0.804817 0.0484664 -0.591541 0.83058 -0.237435 -0.503747 0.780953 -0.0924515 -0.617709 +0.784291 0.328975 0.525988 0.817269 0.173905 0.549388 0.906643 0.204244 0.369166 +0.98192 0.101068 0.160058 0.918192 0.174465 0.355649 0.591515 -0.370058 -0.716357 +0.280213 -0.234108 -0.930953 0.0452208 -0.0260101 0.998638 0.891233 0.0122076 -0.453382 +0.755021 0.30318 -0.581399 0.843941 0.170277 -0.508694 0.804304 0.282576 -0.522728 +0.922483 0.0418154 0.383767 0.833842 0.00329715 0.551994 0.944345 0.147886 0.29384 +0.619118 -0.228093 -0.751443 0.457091 -0.444566 -0.770343 0.224386 -0.634232 -0.739866 +0.0620671 -0.0569386 0.996447 0.853463 -0.082301 -0.514614 0.867983 -0.181187 -0.462361 +0.833894 -0.0708307 -0.547361 0.908645 -0.028521 0.416594 0.828173 -0.0786689 0.554923 +0.926199 0.024678 0.376226 0.584338 0.0930208 -0.806161 0.377388 -0.0682491 -0.923537 +0.443195 -0.518152 0.731503 0.128342 -0.389019 0.912246 0.802247 -0.264444 -0.535228 +0.808533 -0.317484 -0.495458 0.789924 -0.284068 -0.543438 0.85086 -0.191402 0.489289 +0.788164 -0.285955 0.545003 0.910315 -0.042632 0.411714 0.550244 0.326813 -0.768391 +0.307073 0.144871 -0.940595 0.288223 -0.566669 0.77189 0.127537 -0.315498 0.940317 +0.816403 -0.240308 -0.525108 0.785148 -0.293739 -0.545216 0.795776 -0.264424 -0.544813 +0.752745 -0.347112 0.559364 0.745805 -0.219752 0.628875 0.798434 -0.15646 0.581398 +0.0543679 0.0297403 -0.998078 0.0310586 -0.0724963 0.996885 0.754727 -0.139692 -0.640994 +0.796411 -0.314926 -0.516286 0.76239 -0.256009 -0.594324 0.662255 -0.266272 0.700369 +0.696157 -0.299242 0.652548 0.0405408 0.0262652 -0.998833 0.592861 -0.28133 0.754565 +0.271868 -0.0926415 0.957865 0.769043 -0.194913 -0.608754 0.784021 -0.104326 -0.611904 +0.62536 -0.44538 0.64075 0.719033 -0.219869 0.659279 0.73817 -0.390246 0.550284 +0.321994 0.183364 -0.928815 0.726766 -0.18378 0.661843 0.406767 -0.0193092 0.913328 +0.832113 -0.374539 -0.409034 0.805023 -0.331333 -0.492095 0.723872 -0.298135 -0.622194 +0.844211 -0.261127 0.468102 0.728275 -0.264941 0.631998 0.808842 -0.40877 0.422708 +0.480242 0.00528842 -0.87712 0.883283 -0.0245197 0.468198 0.427586 0.00245136 0.903971 +0.858936 -0.316326 0.402699 0.806696 -0.457584 -0.373977 0.811933 -0.419082 -0.406368 +0.606929 -0.511114 -0.608604 0.608982 -0.567091 -0.554571 0.667958 -0.461501 0.583823 +0.625702 -0.637405 0.44968 0.83013 -0.458335 0.317513 0.800332 -0.537943 0.264739 +0.910639 -0.40587 0.077503 0.940761 -0.334856 -0.0532872 0.86221 -0.360141 -0.35622 +0.611657 -0.187889 -0.768488 0.915492 -0.00809498 0.402254 0.481843 0.0271118 0.875838 +0.996914 -0.0519747 0.0588346 0.874619 -0.481279 -0.0584179 0.739702 -0.660249 -0.130046 +0.834618 -0.49477 -0.242106 0.596129 -0.560394 -0.574969 0.63286 -0.558226 -0.536537 +0.673883 -0.663053 0.325947 0.716338 -0.612515 0.334193 0.721789 -0.519672 0.457124 +0.829996 -0.429617 0.355719 0.990782 -0.133381 0.0236593 0.929295 -0.0783518 -0.360933 +0.766972 -0.0704107 -0.637806 0.871691 -0.152441 0.465743 0.607065 -0.0477586 0.793216 +0.968506 0.00232823 -0.24898 0.819152 -0.547588 -0.170698 0.807224 -0.134764 -0.574655 +0.773851 -0.631952 -0.0423183 0.723608 -0.612666 -0.317854 0.697757 -0.485798 0.526437 +0.716915 -0.536146 0.445623 0.859326 -0.420094 0.291685 0.922516 -0.324675 -0.208688 +0.747982 -0.277102 -0.603107 0.645312 -0.466601 0.604861 0.582547 -0.660969 0.473031 +0.875666 -0.421495 0.23569 0.705161 0.28824 -0.647816 0.904015 -0.201432 -0.37707 +0.738296 -0.0805555 -0.669649 0.803293 -0.585417 0.109579 0.759642 -0.531907 -0.374191 +0.677853 -0.682055 0.274438 0.763554 -0.511872 -0.393665 0.161442 0.927131 0.338179 +0.381918 0.763098 0.521364 0.580836 0.750848 0.314415 0.633794 0.773482 -0.00553116 +0.298679 0.953527 0.0397109 0.604456 0.676278 -0.421048 0.384734 0.867059 -0.316527 +0.180007 0.545381 -0.818631 0.220662 0.879962 0.420683 0.174553 0.68152 0.710677 +0.525128 0.806955 0.270303 0.579793 0.726368 0.369093 0.320485 0.859561 0.398049 +-0.0236041 0.386732 -0.92189 -0.121072 0.785222 0.607263 -0.264452 0.695017 0.668593 +0.0516272 0.590919 -0.805077 0.0918873 0.643605 -0.759822 -0.348287 0.688976 0.635617 +-0.0883837 0.743182 0.663226 0.598616 0.158092 -0.785281 0.319602 0.608806 -0.726092 +0.199459 0.417489 -0.886521 -0.627141 0.775919 0.068152 -0.27401 0.792121 0.545401 +-0.266745 0.760659 0.591815 -0.242721 0.823542 0.512705 0.617703 -0.0279973 -0.785913 +0.314594 0.0304259 -0.948739 -0.612778 0.667765 0.422604 -0.651979 0.724107 0.22493 +-0.644054 0.677883 0.354498 -0.563914 0.807239 0.174258 -0.607283 0.794369 -0.0136031 +-0.405624 0.854427 0.32469 -0.254894 0.818922 0.514194 0.629139 0.0135747 -0.777175 +0.390602 0.0174224 -0.920395 -0.764097 0.58098 0.280389 -0.574831 0.376464 0.726529 +-0.703081 0.615711 0.355777 0.655836 -0.0519153 -0.753116 0.592529 0.157874 -0.789928 +-0.303156 0.636006 0.709643 -0.552686 0.395931 0.733333 0.614816 -0.347075 -0.708195 +0.711774 -0.297848 -0.636133 -0.131611 0.818755 0.558854 0.0494067 0.518806 0.853463 +0.301268 0.692009 0.656019 -0.442731 0.792521 0.419403 0.6495 -0.760317 0.00828451 +0.430881 -0.556733 -0.710204 0.541863 -0.519344 -0.660807 -0.292928 0.0733239 0.953319 +0.873694 -0.21415 -0.436805 0.863898 -0.323869 -0.385732 0.753818 -0.46946 -0.459745 +0.677806 -0.727219 -0.108317 0.527998 -0.751641 -0.395289 -0.299188 0.0865985 0.950256 +0.897727 -0.0294358 -0.439567 0.91639 -0.0503593 -0.397107 0.835525 -0.0604024 -0.546123 +-0.059038 -0.000641818 0.998256 0.873365 -0.00482281 -0.487043 0.870641 -0.162646 -0.464253 +0.851439 -0.114118 -0.511888 -0.0146629 0.0513855 0.998571 0.765305 -0.153844 -0.625013 +0.824078 -0.30627 -0.476544 0.758326 -0.272514 -0.592181 0.0216219 0.374985 0.926779 +0.670571 -0.314451 -0.671905 0.726911 -0.246294 -0.641046 -0.0752549 0.586739 0.806272 +0.01813 0.385098 0.922698 0.606952 -0.170581 -0.776216 0.707331 -0.348812 -0.614828 +0.701207 -0.41605 -0.578974 -0.0340553 0.0527158 0.998029 0.631458 -0.0682457 -0.772401 +0.749846 -0.107349 -0.652845 -0.00294177 -0.0555145 0.998454 0.593335 -0.286102 -0.752396 +0.703153 -0.286433 -0.650794 0.0600147 0.00597282 0.99818 0.506909 -0.280674 -0.815025 +0.593121 -0.367199 -0.7165 0.610589 -0.313892 0.727085 0.541443 -0.0136365 0.840627 +0.404698 -0.557429 -0.724909 0.514602 -0.404753 -0.755883 0.657738 -0.287582 0.696187 +0.694743 -0.284285 0.660693 0.198399 -0.558724 -0.805273 0.381708 -0.733236 -0.562729 +0.592104 -0.541715 0.596622 0.540166 -0.622622 0.566182 0.559361 -0.605297 0.566332 +0.34237 -0.456588 -0.821164 0.330964 -0.517373 -0.789169 0.557053 -0.593059 0.581354 +0.569861 -0.570973 0.590973 0.575336 -0.570107 0.586487 0.63107 -0.186109 -0.75307 +0.563417 -0.206166 -0.800035 0.567846 -0.56869 0.595099 0.53353 -0.636041 0.557492 +0.548079 -0.61371 0.568304 0.77946 -0.247348 -0.575553 0.730492 -0.0174599 -0.682698 +0.530216 -0.740585 0.412802 0.469289 -0.817247 0.334478 0.605119 -0.647459 0.46328 +0.504338 -0.692343 0.516047 0.748746 -0.503432 -0.431202 0.839323 -0.433038 -0.328656 +0.800447 -0.17868 -0.572152 0.63625 -0.547477 0.543558 0.681945 -0.547026 0.485503 +0.68526 -0.621395 0.379851 0.703606 -0.708112 0.0593077 0.641388 -0.733792 -0.22399 +0.646544 -0.651494 -0.396908 0.746897 -0.595844 -0.295153 0.782913 -0.509784 -0.356606 +0.764764 -0.622868 0.164839 0.77128 -0.628899 -0.0980457 0.755087 -0.589756 -0.286412 +0.826366 0.515057 0.227673 0.771897 0.537285 0.339852 0.881028 0.456599 0.123722 +0.86901 0.354802 -0.344872 0.778386 0.575917 -0.24987 0.699429 0.363788 -0.615189 +0.822922 0.458014 0.336187 0.812666 0.267289 0.517813 0.865131 0.393315 0.311211 +0.875252 0.418961 0.241673 0.799012 0.549076 0.245145 0.912504 0.385779 0.136058 +0.928102 0.309745 -0.206601 0.726505 0.232573 -0.646607 0.88672 0.217486 0.407954 +0.769697 0.280056 0.573703 0.879491 0.342968 0.329953 0.545649 0.607229 -0.57753 +0.604335 0.51642 -0.606703 0.570001 0.615742 -0.544024 0.67513 0.643555 0.360606 +0.632903 0.582274 0.510285 0.639639 0.710406 0.293572 0.796449 0.575123 -0.186823 +0.731304 0.678305 0.0713892 0.833624 0.434115 -0.341489 0.758577 0.625177 -0.183616 +0.72397 0.528695 -0.443113 0.797866 0.329962 0.504514 0.83309 0.153255 0.531483 +0.875762 0.204328 0.437368 0.895234 0.191547 0.402325 0.625785 0.240042 -0.742141 +0.600119 0.479764 -0.640066 0.432308 0.277088 -0.858098 0.5977 0.517981 0.611924 +0.521058 0.401821 0.75302 0.52925 0.594879 0.60499 0.814035 0.41776 -0.403514 +0.715769 0.467259 -0.518984 0.807754 0.443805 -0.388035 0.72344 0.506228 -0.469433 +0.908883 0.0389013 0.415233 0.818344 -0.0251024 0.57418 0.914793 0.0632474 0.39894 +0.597221 0.321812 -0.734686 0.291841 0.13144 -0.947392 0.528879 0.296927 0.795061 +0.816758 0.501208 -0.285827 0.730464 0.613334 -0.300405 0.815209 0.531259 -0.230647 +0.905988 0.41889 -0.0609591 0.795319 0.603815 -0.0536308 0.856141 0.443238 -0.265635 +0.794954 0.592549 -0.130134 0.703215 0.565507 -0.430919 0.817511 0.543737 0.189804 +0.83346 0.151379 0.531441 0.901775 0.425479 -0.0759561 0.700664 0.577252 -0.419345 +0.85764 0.017483 0.513954 0.795054 0.00563314 0.606513 0.894512 0.129338 0.427925 +0.0514034 0.0265245 -0.998326 0.537085 0.302394 0.787463 0.43484 -0.0975427 0.895209 +0.599338 0.12219 0.791115 0.859343 0.385938 -0.335531 0.984045 0.145956 -0.101744 +0.682874 0.513457 -0.519658 0.867202 0.343724 -0.360298 0.668365 0.523703 -0.528227 +0.945099 0.0797891 0.316892 0.806864 -0.013635 0.59058 0.660519 0.494695 -0.564794 +0.849777 0.46811 -0.242387 0.672709 0.606293 -0.424113 0.818817 0.307417 0.484803 +0.732986 0.448823 0.511165 0.016828 -0.00770546 -0.999829 0.569032 -0.339289 0.749057 +0.270212 -0.358147 0.893709 0.877921 0.310965 -0.364081 0.987936 0.0830646 -0.130698 +0.694486 0.464479 -0.549499 0.862015 0.22137 -0.455989 0.721495 0.330676 -0.608357 +0.89823 -0.0249443 0.438818 0.815075 -0.0157718 0.57914 0.963973 0.0932991 0.249102 +0.604079 0.795103 0.0538529 0.991803 0.105604 0.0719402 0.0271723 0.00193752 -0.999629 +0.00445387 -0.0101702 0.999938 0.940745 0.153644 -0.30231 0.931894 0.16425 -0.323412 +0.771431 0.235825 -0.591 0.901396 -0.025261 0.432258 0.821366 -0.0204959 0.570033 +0.918446 -0.00992968 0.395422 0.965544 0.252954 0.0611549 0.0371928 0.00889285 -0.999269 +0.0477086 0.381373 0.923189 0.859018 0.236249 -0.454174 0.987282 0.0619723 -0.146405 +0.730422 0.370507 -0.573767 0.866626 0.194714 -0.459397 0.741049 0.309116 -0.596065 +0.901021 0.0272144 0.432921 0.810741 0.0745259 0.580642 0.904886 -0.0072912 0.425591 +0.0519752 0.0230217 -0.998383 0.0708632 0.621831 0.779939 0.0357393 0.396289 0.91743 +0.956794 -0.00995561 -0.290597 0.930906 0.0160865 -0.364905 0.794271 0.0746293 -0.602962 +0.887818 0.190179 0.41906 0.804449 0.345936 0.482898 0.884843 0.163171 0.436381 +0.00669011 -0.0785129 -0.996891 -0.0232439 0.0382808 0.998997 0.905999 0.0614869 -0.41879 +0.987607 0.0158927 -0.156143 0.935567 -0.0125264 -0.352927 0.808032 -0.00768939 -0.589088 +0.78871 0.514893 0.33589 0.8149 0.111676 0.568742 0.913334 0.327134 0.242494 +0.933785 0.28398 0.217718 0.814011 0.540507 0.212692 0.889686 0.217947 0.401196 +-0.172195 -0.475687 -0.862596 -0.0156512 0.00425509 0.999868 0.929153 0.070281 -0.362953 +0.929149 -0.0240019 -0.368925 0.81045 -0.00950375 -0.585731 0.876091 -0.166735 0.452397 +0.739823 -0.314331 0.594859 0.992601 0.017544 0.120148 0.929558 0.0765094 0.360649 +-0.0516765 -0.340277 -0.938904 -0.305499 -0.584247 -0.751881 -0.0375268 -0.0912209 0.995123 +0.893038 -0.177412 -0.413531 0.877591 -0.205861 -0.43296 0.809958 -0.16994 -0.561327 +0.723027 -0.387272 0.57206 0.774205 -0.23717 0.586819 0.916751 -0.182324 0.355423 +0.933489 -0.0338075 0.35701 0.0307684 -0.0087038 -0.999489 -0.254881 -0.461045 0.849984 +0.884835 -0.218641 -0.411415 0.881182 -0.220503 -0.418206 0.772244 -0.343876 -0.534218 +0.761661 -0.301202 -0.573716 0.849094 -0.238946 0.471109 0.730712 -0.323445 0.601201 +0.914261 -0.165631 0.369721 -0.00875128 0.000795945 -0.999961 -0.115928 -0.588777 0.799939 +-0.172491 -0.322693 0.930654 0.899507 -0.205675 -0.385468 0.865552 -0.235421 -0.442037 +0.800348 -0.249457 -0.545173 0.709254 -0.459308 0.534785 0.681664 -0.531053 0.503306 +0.830777 -0.377112 0.409385 0.830671 -0.296113 0.47149 -0.00632173 0.011516 -0.999914 +-0.0133338 -0.00869937 0.999873 0.892185 -0.158737 -0.422858 0.890161 -0.196315 -0.411187 +0.802945 -0.301328 -0.514277 0.787744 -0.2634 -0.556847 0.67517 -0.53873 0.5039 +0.684345 -0.526094 0.504874 0.722423 -0.41453 0.553417 0.167335 0.302084 -0.93848 +0.0310384 0.0183842 0.999349 0.871416 -0.118289 -0.476069 0.85119 -0.246792 -0.463217 +0.82111 -0.227368 -0.523529 0.651378 -0.449779 0.611069 0.7331 -0.23185 0.639383 +0.639481 -0.312981 0.702216 0.551987 0.191436 -0.811581 0.36977 0.543588 -0.753513 +0.297836 0.428932 -0.852826 0.0168478 -0.0115714 0.999791 0.764191 -0.371469 -0.527279 +0.806204 -0.3503 -0.476787 0.740222 -0.435047 -0.512646 0.639566 -0.273123 0.718581 +0.712971 -0.320764 0.623524 0.582858 -0.0076995 -0.812537 0.328948 0.0506765 -0.942987 +0.0654814 0.0518216 0.996507 0.537255 -0.611926 -0.580434 0.623098 -0.53253 -0.572853 +0.709395 -0.499757 -0.496994 0.610263 -0.500098 0.614395 0.625985 -0.563398 0.539189 +0.520759 -0.650898 0.552397 0.621023 0.000480035 -0.783792 0.381644 0.0319381 -0.923758 +0.404299 0.337357 0.850137 0.538737 -0.579547 -0.611464 0.584363 -0.566222 -0.581303 +0.627422 -0.56861 -0.531999 0.621914 -0.58941 0.515577 0.6916 -0.518388 0.502954 +0.710601 -0.534007 0.458129 0.830424 -0.476106 0.289341 0.68674 -0.0348039 -0.72607 +0.593084 0.153129 -0.790445 0.527952 0.411734 0.742793 0.446359 -0.100769 0.889162 +0.651463 -0.00072676 0.75868 0.884914 -0.0257519 -0.465042 0.897122 -0.425767 -0.117872 +0.776041 -0.552092 -0.304886 0.623853 -0.582937 -0.520568 0.614398 -0.538248 -0.576892 +0.721416 -0.415522 0.553985 0.804678 -0.163435 0.570774 0.897061 -0.237929 0.372387 +0.858737 -0.360359 0.364296 0.68213 -0.287228 -0.672458 0.669963 -0.32861 -0.665706 +0.578487 -0.505971 0.639802 0.583703 -0.598764 0.548428 0.52509 -0.658242 0.539442 +0.884591 -0.259392 -0.387576 0.690926 -0.524072 -0.497966 0.736469 -0.483838 -0.472773 +0.881952 -0.195947 0.428678 0.789063 -0.247246 0.56236 0.986957 -0.0793322 0.140079 +0.945176 -0.305223 0.116108 0.849279 -0.427478 -0.30982 0.698578 -0.405291 -0.589684 +0.598529 -0.434338 -0.673137 0.616381 -0.564183 0.549337 0.699624 -0.568043 0.433421 +0.664131 -0.655443 0.359617 0.84917 -0.528039 0.00928816 0.758552 -0.223332 -0.612145 +0.747229 -0.374443 -0.549036 0.669595 -0.498898 0.550221 0.613589 -0.554456 0.562217 +0.869216 -0.345125 0.354053 0.992902 -0.118824 -0.00505272 0.895022 -0.241176 -0.375194 +0.727015 -0.327927 -0.603252 0.750583 -0.627263 0.207767 0.740308 -0.598677 0.305828 +0.727407 -0.561907 0.393876 0.931736 -0.11893 -0.343108 0.75784 0.24879 -0.603144 +0.575233 -3.03934e-05 -0.81799 0.650404 -0.526162 0.54784 0.731809 -0.564984 0.381116 +0.904693 -0.420773 0.0669406 0.661797 -0.40545 -0.630583 0.758148 -0.468152 -0.453922 +0.769311 -0.550307 -0.324535 0.743386 -0.650882 -0.154044 -0.389769 0.873089 0.292911 +-0.105592 0.829015 0.549167 -0.301658 0.953265 0.0169537 -0.148154 0.891696 -0.427701 +-0.267303 0.440633 -0.856967 -0.342657 0.776798 0.528366 -0.224248 0.526728 0.819921 +-0.201113 0.917954 0.341928 -0.271673 0.381096 -0.883719 -0.440907 0.612467 0.656114 +-0.523318 0.448171 0.724763 -0.159837 0.627663 -0.7619 -0.230374 0.704959 -0.670791 +-0.464112 0.681488 0.565839 -0.567319 0.610432 0.55274 -0.552443 0.516225 0.654461 +-0.258791 0.660218 -0.705081 -0.210022 0.36496 -0.907025 -0.521451 0.784953 0.334571 +-0.309922 0.756086 0.57644 -0.570306 0.715606 0.403311 0.0823604 0.0817475 -0.993244 +-0.765396 0.639403 0.0730221 -0.777965 0.62759 0.0300046 -0.759521 0.580042 0.294412 +-0.619325 0.595257 0.511962 -0.466077 0.708898 0.529373 0.334773 0.363028 -0.869562 +-0.799272 0.316072 0.511138 -0.748133 0.510172 0.42429 -0.692259 0.600434 0.40032 +-0.794025 0.607356 0.0253563 -0.880123 0.471964 -0.0513187 -0.808929 0.536448 -0.240536 +-0.723357 0.452211 0.521786 -0.797981 0.469189 0.378271 0.653691 0.133166 -0.744953 +0.577061 0.439708 -0.688228 0.396911 0.268422 -0.877731 -0.693227 0.476373 0.540837 +-0.696628 0.559757 0.448754 -0.644841 0.549341 0.531418 -0.788425 0.606617 0.101993 +0.639018 -0.246515 -0.728619 0.358784 -0.135224 -0.923574 -0.553085 0.265985 0.789524 +-0.689102 0.484715 0.538692 -0.569212 0.780413 0.258751 -0.604371 0.689665 0.398871 +0.505985 -0.572762 -0.64492 0.59325 -0.585842 -0.552127 0.552728 -0.533995 -0.639798 +-0.64968 0.036718 0.759321 -0.804507 0.0377855 0.592741 0.910525 -0.239324 -0.337146 +0.913755 -0.206883 -0.349645 0.701122 -0.277701 -0.656742 0.60567 -0.455869 -0.652186 +-0.6132 0.288088 0.735521 -0.727462 0.310346 0.611951 0.939088 -0.0107308 -0.34351 +0.989202 -0.0109389 -0.14615 0.705648 0.237001 -0.667751 0.878072 -0.0207796 -0.478078 +0.585481 0.113234 -0.802739 -0.55719 0.493737 0.667655 -0.302282 0.249453 0.919999 +0.94462 -0.13981 -0.296893 0.950032 -0.118892 -0.288624 0.712768 -0.184141 -0.676796 +0.821853 -0.278337 -0.497078 -0.0476941 0.0983517 0.994008 0.904159 -0.212087 -0.370832 +0.977255 -0.133562 -0.164721 0.901304 -0.238376 -0.361701 0.827207 -0.24026 -0.507941 +-0.135373 0.298576 0.944736 0.876223 -0.249107 -0.412529 0.89352 -0.315258 -0.319741 +0.79385 -0.36728 -0.484672 0.778418 -0.449635 -0.438057 -0.284481 0.548276 0.786425 +-0.180354 0.40087 0.898207 0.830626 -0.247819 -0.498644 0.807201 -0.359637 -0.468068 +0.771875 -0.467562 -0.430807 0.748906 -0.474887 -0.462194 -0.0666303 0.0389083 0.997019 +0.766604 -0.178819 -0.616719 0.764379 -0.425279 -0.484627 0.723591 -0.305068 -0.619153 +-0.0362519 0.00541026 0.999328 0.681235 -0.290885 -0.671792 0.737952 -0.214266 -0.639935 +0.208728 0.312178 0.92681 0.615756 -0.237323 -0.751347 0.701011 -0.346387 -0.623378 +0.717914 -0.385103 -0.579909 0.412409 0.680797 0.605338 0.621653 0.00606037 0.783269 +0.588329 0.329398 0.738489 0.609978 -0.444519 -0.655995 0.698579 -0.364648 -0.615645 +0.624962 -0.203046 0.753787 0.515707 -0.259996 0.816363 0.392392 -0.559148 -0.730331 +0.573829 -0.747203 -0.335272 0.601319 -0.674404 -0.428479 0.647812 -0.439757 0.622056 +0.637413 -0.579593 0.507716 0.543287 -0.625318 0.560193 0.568441 -0.175751 -0.803733 +0.605397 -0.241247 -0.758481 0.611581 -0.578109 0.540148 0.652044 -0.533645 0.538574 +0.55149 -0.583797 0.595852 0.670302 0.0344431 -0.741288 0.790196 0.012429 -0.612728 +0.584605 -0.566942 0.580357 0.620473 -0.546278 0.562666 0.547843 -0.599661 0.58333 +0.696476 0.00478811 -0.717564 0.786918 0.0308623 -0.616286 0.605058 -0.575264 0.550433 +0.669643 -0.60468 0.431207 0.623243 -0.692602 0.363141 0.643937 -0.700502 0.30764 +0.773186 -0.235183 -0.588959 0.67076 -0.160666 -0.724063 0.79653 0.530162 0.290634 +0.795362 -0.260169 0.547458 0.999495 0.0137082 -0.0286779 0.796255 0.29667 -0.527223 +0.714327 -0.65876 0.236164 0.673814 -0.580818 0.456755 0.761146 -0.262334 0.593159 +0.724949 -0.399394 0.56119 0.908161 -0.366054 -0.203095 0.896466 -0.382275 -0.224087 +0.825416 -0.309899 -0.471859 0.580645 -0.180994 -0.793784 0.833698 -0.470949 -0.288366 +0.825667 -0.247475 0.506981 0.732455 -0.295058 0.613556 0.898053 -0.362849 0.248679 +0.922615 -0.385427 -0.0151041 0.954912 -0.279259 -0.100783 0.994225 -0.086904 -0.062965 +0.92578 -0.158681 -0.343149 0.668967 -0.413206 -0.617854 0.658109 -0.397795 -0.639259 +0.721798 -0.424643 0.546522 0.698665 -0.571186 0.430829 0.857233 -0.42112 0.296328 +0.915522 -0.401505 0.0247744 0.922317 -0.385947 -0.0194147 0.93006 -0.367154 0.0136138 +0.964773 -0.248036 -0.0876989 0.396602 0.166002 -0.902857 0.758243 -0.632946 0.156358 +0.796511 -0.604595 0.00598265 0.814706 -0.579769 -0.0110837 0.7582 -0.64994 -0.0520591 +0.141825 -0.987995 0.0612562 0.389014 0.0447044 0.920146 0.952444 0.296744 0.0692346 +0.313036 0.877841 -0.362497 0.929837 0.366929 0.0276924 0.643239 0.738336 0.202741 +0.924588 0.379636 -0.0318323 0.821277 0.569654 0.0315917 0.818246 0.494117 -0.293807 +0.781003 0.543772 -0.307157 0.602569 0.611342 -0.513002 0.418815 0.883854 0.208317 +0.629082 0.624217 0.463259 0.868231 0.462278 0.180207 0.923372 0.382154 -0.0366415 +0.389433 0.91966 0.05067 0.816346 0.237198 -0.526608 0.584086 0.8112 -0.0282483 +0.626848 -0.0136698 -0.779022 0.686804 0.518067 0.509811 0.729361 0.499258 0.467733 +0.914398 0.277246 0.294978 0.998335 0.0341079 0.0465079 0.997324 0.071937 0.0130241 +0.994082 0.0953938 0.0519627 0.951492 0.303423 0.0509689 0.592337 0.598143 -0.539779 +0.777907 0.521151 -0.351086 0.602483 0.586964 -0.540821 0.684472 0.510889 0.520088 +0.72965 0.457262 0.508452 0.912043 0.275798 0.303502 0.997788 0.0185584 0.0638367 +0.999865 0.0144524 0.00780824 0.917854 0.185639 -0.350831 0.573092 0.335524 -0.747656 +0.726769 0.416019 0.546566 0.795825 0.170372 0.581065 0.913177 0.210757 0.348841 +0.997011 0.0485092 0.0601323 0.99825 0.0413413 0.0422785 0.962081 0.237506 0.134131 +0.919191 0.333954 0.208715 0.613713 0.191082 -0.766057 0.599672 0.46982 -0.647815 +0.413637 0.280507 -0.866153 0.751418 0.455256 0.477611 0.758953 0.522433 0.388656 +0.899722 0.304153 0.313036 0.998233 0.0389873 0.0448362 0.99991 0.00292789 0.0130889 +0.999191 -0.0382621 0.0124258 0.999158 0.0123931 -0.0391036 0.663129 0.49315 -0.563084 +0.855167 0.356913 -0.375901 0.664974 0.533882 -0.522283 0.939963 0.00898755 0.341159 +0.813559 -0.0409167 0.580041 0.995631 0.0196825 0.0912815 0.960615 0.16761 0.221641 +0.621701 0.259084 -0.739164 0.525298 0.406399 -0.747597 0.732947 0.567702 0.374838 +0.710049 0.33841 0.617503 0.865528 0.41037 0.287154 0.767409 0.62461 0.144724 +0.761275 0.64771 0.0305406 0.642775 0.765103 -0.0381867 0.638306 0.756274 -0.143583 +0.736275 0.652465 -0.17941 0.444059 0.89334 -0.0689679 0.89237 0.450656 -0.0241981 +0.442317 0.89618 0.034872 0.924021 0.38234 0.00133508 0.549695 0.834693 0.0334935 +0.925495 0.378591 0.0112698 0.624178 0.768975 0.138127 0.93366 0.357926 0.0129528 +0.7718 0.635217 0.0286937 0.94446 0.286854 0.160344 0.79951 0.583277 0.143429 +0.998378 0.044415 0.0356321 0.999802 0.0140529 0.0140841 0.999863 -0.00692497 -0.0150487 +0.999966 -0.00753739 -0.00344854 0.999467 -0.0226223 0.0235452 0.997241 0.0533178 -0.0516401 +0.666664 0.52815 -0.525944 0.85065 0.362996 -0.380301 0.655185 0.521394 -0.5467 +0.921874 -0.00744221 0.387418 0.815303 0.0378854 0.577793 0.992538 0.0231773 0.119714 +0.918151 0.170928 0.357467 0.535301 0.551718 -0.639578 0.572109 0.553858 -0.604924 +0.332182 0.360704 -0.87152 0.725957 0.438748 0.52961 0.699119 0.316663 0.64106 +0.857735 0.368745 -0.358215 0.992824 0.109751 -0.0474834 0.998256 0.0574313 0.0136834 +0.998733 0.0485621 -0.0131719 0.999743 0.0213763 0.00756738 0.999877 0.000326547 0.0156522 +0.999893 -0.00892326 -0.0115537 0.999958 -0.0042949 -0.00806625 0.999986 0.00301306 -0.00428883 +0.999985 -0.00247956 0.0049122 0.999446 -0.0247104 0.0223161 0.996767 0.0477189 -0.0646474 +0.661213 0.512302 -0.548037 0.847475 0.399072 -0.350039 0.732058 0.531174 -0.426551 +0.941601 0.124523 0.312861 0.810532 0.385802 0.440674 0.942485 0.134423 0.306027 +0.0514935 0.0231278 -0.998405 0.601073 0.340836 0.722871 0.47103 0.234903 0.850265 +0.6222 0.367836 0.69106 0.883215 0.427891 -0.191936 0.93804 0.345946 -0.0200692 +0.939163 0.301967 -0.163673 0.998291 0.0297026 -0.0503295 0.999224 -0.0358172 0.0163778 +0.999916 -0.0127362 0.00223355 0.999994 0.000633694 -0.00337482 0.999994 0.00339894 0.000827293 +0.999969 0.00498502 -0.00601545 0.999946 -0.00981765 -0.0034956 0.9993 -0.0143594 0.0345423 +0.9958 0.0853264 -0.0332008 0.788396 0.605303 -0.109728 0.90407 0.426575 -0.0262973 +0.996565 0.0675242 0.0479375 0.922135 0.015738 0.386548 0.00435734 -0.00199195 -0.999989 +0.580069 0.283839 0.763515 0.325732 0.160309 0.931772 0.842276 0.383819 -0.378488 +0.996485 0.0572787 -0.0611268 0.999476 -0.0216763 0.0240262 0.999977 -0.0025977 0.00629093 +0.999992 0.00381827 0.00130158 0.999975 0.00349101 -0.0061487 0.999779 0.00125851 -0.0209794 +0.998493 -0.0114034 0.0536909 0.945879 0.18321 0.267856 0.871057 0.362736 -0.331182 +0.951278 0.0133702 0.308046 0.0140594 -0.00491652 -0.999889 0.0735775 -0.0688612 0.994909 +0.847318 0.349134 -0.400198 0.998293 0.0395989 -0.0429239 0.999638 -0.0134832 0.0232842 +0.999989 0.00104278 0.0044792 0.999976 0.00289678 -0.00627816 0.999706 0.00311052 -0.0240406 +0.998854 -0.0022318 0.0478183 0.924888 0.00817669 0.380153 0.917691 0.375882 0.128669 +0.0201185 -0.00308406 -0.999793 0.398246 -0.505144 0.765656 -0.0439659 -0.469662 0.881751 +0.932062 0.159068 -0.325511 0.999995 -0.00325172 -9.09799e-05 0.999831 -0.00796428 0.0165768 +0.999975 -0.000341105 -0.00703823 0.999736 -0.00196852 -0.0228772 0.999046 0.000377766 0.0436592 +0.929453 -0.00693785 0.368874 -0.0151816 -0.00775609 -0.999855 0.109965 -0.64309 0.757854 +-0.000773209 -0.245663 0.969355 0.836761 0.268132 -0.477426 0.996195 0.040327 -0.0772609 +0.999501 -0.0157323 0.0274027 0.999957 -0.00834554 -0.00411568 0.99967 -0.00743154 -0.0245991 +0.998893 -0.0096848 0.046039 0.932137 -0.00920902 0.361989 -0.025091 0.0231833 -0.999416 +0.188036 0.46191 0.866765 0.935566 0.126601 -0.329679 0.999958 0.00194184 -0.00899004 +0.999895 -0.0143153 0.00234195 0.999888 0.013086 -0.00729158 0.998075 0.0427633 0.044919 +0.932981 0.00305736 0.359914 -0.0302175 -0.000838324 -0.999543 0.315708 0.60574 0.730348 +0.12043 0.325624 0.937799 0.920894 0.0120822 -0.389626 0.998815 -0.00362282 -0.0485373 +0.999635 0.00772132 0.0258945 0.998831 0.0410394 0.0255457 0.9447 0.292861 0.147561 +0.904373 0.252241 0.344215 -0.176702 -0.32687 -0.928403 0.00478458 0.0128444 0.999906 +0.923863 0.0022587 -0.382716 0.998096 -0.00101537 -0.0616739 0.998425 -0.00543873 0.0558416 +0.944096 0.178272 0.277311 -0.278119 -0.453338 -0.846838 -0.1682 -0.601779 -0.780751 +-0.408503 -0.564458 -0.717295 -0.0114342 -0.0127591 0.999853 0.926739 -0.0310872 -0.374418 +0.998038 0.00832741 -0.0620511 0.999503 -0.0111665 0.0294808 0.933562 -0.111773 0.340543 +-0.0561146 -0.079369 -0.995265 -0.123302 -0.326431 0.937144 0.946204 -0.154374 -0.284371 +0.999025 0.0011363 -0.044127 0.999869 0.0137383 -0.00851412 0.995461 -0.0438139 0.0844813 +0.839132 -0.274412 0.469633 -0.0190645 0.00622674 -0.999799 -0.405672 -0.566735 0.717106 +-0.32064 -0.595685 0.736444 -0.208907 -0.47614 0.854195 0.903528 -0.181144 -0.388361 +0.998268 -0.0271935 -0.0521624 0.999994 -0.00333836 -0.00065923 0.999799 0.0197392 -0.00365523 +0.999963 0.00559197 0.00657862 0.93301 -0.15643 0.32407 0.0182891 0.0211133 -0.99961 +-0.0447732 -0.0798274 0.995803 0.957024 -0.133939 -0.257227 0.998473 -0.00742479 -0.0547501 +0.999445 0.00317361 0.0331656 0.999502 0.0315259 -0.00101585 0.998952 0.0359777 -0.0282785 +0.998716 -0.0304533 0.0404766 0.850163 -0.34981 0.393517 0.296428 0.199582 -0.933969 +0.000788575 0.0371972 0.999308 0.918738 -0.154848 -0.363239 0.995201 -0.0560561 -0.0802092 +0.99813 -0.0489127 -0.0366629 0.958178 -0.286048 -0.00842493 0.995057 -0.0887481 0.0445468 +0.999349 -0.0322487 -0.0162063 0.999687 0.0109061 -0.022497 0.998331 -0.042499 0.0390965 +0.862572 -0.368554 0.346609 0.433262 0.320333 -0.84242 -0.0211061 -0.00137602 0.999776 +0.95136 -0.153293 -0.267238 0.992051 -0.0514115 -0.114858 0.915667 -0.289575 -0.278748 +0.785134 -0.600675 0.150843 0.769494 -0.549712 -0.325109 0.776527 -0.534493 0.333652 +0.828789 -0.491038 0.268309 0.92194 -0.383587 0.0537406 0.941014 -0.318928 0.113037 +0.999659 -0.0183111 0.0186181 0.998503 -0.0164527 0.0521561 0.862638 -0.2759 0.423951 +0.24471 0.580035 -0.776966 0.201227 0.287666 -0.936352 -0.0236446 0.00349247 0.999714 +0.920175 -0.272269 -0.281332 0.989364 -0.0881834 -0.115683 0.915508 -0.282263 -0.28666 +0.73876 -0.471549 -0.481535 0.720812 -0.472437 -0.507182 0.800133 -0.573666 0.175198 +0.806991 -0.587801 0.0570628 0.736424 -0.600849 0.310902 0.696861 -0.528904 0.484401 +0.858032 -0.377339 0.348421 0.996431 -0.0677428 0.0503653 0.948833 -0.136168 0.28491 +0.0878483 0.0591261 -0.994378 0.13106 0.318805 0.938715 0.892908 -0.345772 -0.28837 +0.986929 -0.132347 -0.0919541 0.905864 -0.305626 -0.293264 0.693845 -0.532532 -0.484757 +0.676079 -0.538581 -0.502839 0.690089 -0.487749 0.534675 0.628199 -0.640097 0.442314 +0.848957 -0.455813 0.267407 0.9192 -0.347632 0.184994 0.85017 -0.35444 0.389336 +0.369559 0.38952 -0.843624 0.132407 0.607597 0.783131 0.280676 0.437543 0.85427 +0.399302 0.584685 0.706188 0.832175 -0.396963 -0.387177 0.904406 -0.367022 -0.217587 +0.909037 -0.369379 -0.1929 0.615552 -0.545371 -0.568917 0.656038 -0.545335 -0.52175 +0.68878 -0.649894 0.321278 0.786301 -0.47621 0.393644 0.814542 -0.203445 0.54326 +0.925009 -0.216342 0.312338 0.861589 -0.353279 0.364497 0.65742 0.118301 -0.74418 +0.56774 0.441966 -0.694505 0.388479 0.249459 -0.887048 0.0865836 0.0596394 0.994458 +0.823193 -0.393392 -0.409385 0.837354 -0.391654 -0.381374 0.80465 -0.556811 -0.206156 +0.764182 -0.524644 -0.375198 0.73576 -0.589831 -0.332802 0.897244 -0.185355 0.400745 +0.76531 -0.244351 0.595478 0.985466 -0.0867067 0.146078 0.870566 -0.256112 0.420145 +0.573084 -0.417336 -0.70527 0.149814 -0.321136 -0.935108 0.505018 -0.550451 0.664802 +0.162436 -0.393202 0.90499 0.834898 -0.368291 -0.409032 0.83523 -0.394396 -0.383201 +0.723019 -0.512023 -0.463763 0.680705 -0.530264 -0.505431 0.667024 -0.47296 0.575663 +0.618346 -0.632602 0.466329 0.869304 -0.401976 0.287623 0.855103 -0.33709 0.393915 +0.618466 -0.221429 -0.753969 0.285337 -0.378704 -0.880435 0.0750298 -0.716987 -0.693037 +0.401599 -0.563276 0.722107 0.36005 -0.376215 0.853713 0.711812 -0.408525 0.571341 +0.79957 -0.100528 -0.5921 0.836745 -0.384203 -0.390187 0.62818 -0.547499 -0.552843 +0.654716 -0.540161 -0.528747 0.676192 -0.660253 0.32685 0.640301 -0.649499 0.410081 +0.641315 -0.602133 0.475554 0.5204 -0.627239 0.579444 0.626084 -0.27368 -0.73015 +0.335015 -0.145018 -0.930986 0.725754 -0.36746 0.581596 0.277793 -0.437576 0.855195 +0.81407 -0.513926 0.270498 0.839517 -0.483905 0.247078 0.663354 -0.3096 -0.681255 +0.497464 -0.22616 -0.837485 0.603314 -0.538658 0.588099 0.530717 -0.339273 0.776681 +0.698713 -0.423905 0.576285 0.723618 -0.47421 -0.5015 0.688545 -0.371892 -0.622576 +0.593701 -0.38518 -0.706509 0.547394 -0.710563 0.442108 0.593564 -0.708036 0.382579 +0.621027 -0.647446 0.441745 0.801382 -0.523725 0.288963 0.790948 -0.610002 0.0479508 +0.592295 -0.380154 -0.710401 0.799787 -0.319407 0.508253 0.500267 -0.360942 0.787054 +0.830612 -0.553369 0.0621784 0.842261 -0.439794 -0.311733 0.731971 -0.340774 -0.589993 +0.714483 -0.632124 0.299887 0.725707 -0.627474 -0.28218 0.772004 -0.58922 0.238391 +0.77171 -0.632776 -0.0637078 0.730527 -0.57161 -0.37362 -0.548606 0.752408 0.364574 +-0.653956 0.75291 0.0739375 -0.590878 0.762166 -0.264511 -0.631347 0.25339 0.732936 +-0.692662 0.410001 0.593396 -0.56753 0.780255 0.262891 -0.65685 0.739946 0.14501 +-0.766374 0.640853 -0.0444664 -0.671254 0.555115 -0.491188 -0.54654 0.265108 -0.794363 +-0.671923 0.238865 0.701044 -0.74381 0.179493 0.64384 -0.46062 0.586478 -0.666238 +-0.597837 0.407715 -0.690188 -0.587748 0.560345 0.58358 -0.630172 0.568067 0.529323 +-0.694382 0.423017 0.582143 -0.605353 0.562266 -0.563387 -0.668022 0.565668 -0.483495 +-0.516864 0.586619 -0.623483 -0.752443 0.536435 0.382188 -0.762406 0.544521 0.349618 +-0.598199 0.579607 0.553366 -0.564874 0.574973 0.591881 -0.576038 0.556624 -0.598623 +-0.202593 0.388985 -0.898692 -0.82517 0.391518 0.407196 -0.836363 0.383883 0.391319 +-0.622504 0.563476 0.543123 -0.636076 0.545675 0.54557 0.381591 0.566678 -0.730249 +-0.120362 0.633841 -0.764041 -0.0433913 0.4938 -0.868492 -0.839403 0.537104 0.0831949 +-0.813308 0.448338 0.370842 -0.838801 0.376466 0.393303 -0.669205 0.476984 0.569781 +-0.721654 0.472461 0.50596 0.124247 0.0957453 -0.987621 -0.766659 0.542499 0.343408 +-0.860399 0.508857 0.0278962 -0.815069 0.579165 -0.0151932 -0.814016 0.539329 0.215641 +-0.711147 0.488726 0.505389 -0.695402 0.559753 0.450659 0.396677 -0.00295344 -0.917953 +-0.904125 0.23691 0.355572 -0.828717 0.413185 0.377501 -0.74156 0.451709 0.496033 +-0.795148 0.544891 0.266145 -0.799432 0.600486 0.0180364 -0.63582 0.744406 0.20394 +-0.498318 0.821192 0.27807 0.507148 -0.623559 -0.594957 0.484884 -0.318273 -0.81461 +-0.895538 0.062215 0.440615 -0.860901 0.199554 0.468004 -0.754502 0.354682 0.552203 +0.654802 0.0975847 -0.749474 0.664091 -0.189458 -0.723249 0.615298 -0.493147 -0.614991 +-0.786507 0.385557 0.482444 -0.634235 0.433599 0.640107 -0.67178 0.519452 0.528092 +0.638608 0.139206 -0.756837 -0.509294 0.508492 0.694302 -0.491401 0.563722 0.663884 +-0.447328 0.73364 0.511537 0.851128 -0.063328 -0.521125 0.698869 0.357644 -0.619413 +0.856887 0.365232 -0.3638 0.667755 0.553776 -0.497429 -0.497751 0.512246 0.699891 +-0.284878 0.277506 0.917516 0.945594 -0.184413 -0.268035 0.986937 -0.00344485 -0.161069 +0.682121 0.417257 -0.600506 0.878165 -0.0745457 -0.472514 0.536228 0.0904054 -0.839217 +-0.105443 0.145006 0.983796 0.907177 -0.298866 -0.296158 0.984752 -0.12362 -0.122397 +0.914984 -0.29606 -0.27414 0.649368 -0.311898 -0.693571 0.766046 -0.469797 -0.438707 +-0.351194 0.534565 0.768702 -0.214375 0.387522 0.896588 0.915953 -0.284332 -0.283171 +0.981224 -0.138978 -0.133733 0.909102 -0.297559 -0.291535 0.762332 -0.476098 -0.438385 +0.765323 -0.458547 -0.451681 -0.181607 0.547566 0.816817 -0.176604 0.261021 0.949041 +0.910035 -0.190118 -0.368362 0.980163 -0.131265 -0.148495 0.913272 -0.288415 -0.287665 +0.742453 -0.480027 -0.467266 0.739994 -0.472355 -0.478841 0.0314142 0.103535 0.99413 +0.889856 -0.210557 -0.40475 0.898771 -0.297498 -0.322034 0.730334 -0.478393 -0.487599 +0.757219 -0.457573 -0.466097 0.197707 0.513166 0.835208 0.835607 -0.280486 -0.472323 +0.751115 -0.448275 -0.484641 0.777701 -0.454636 -0.434152 0.713735 -0.602247 -0.357604 +0.292335 0.748493 0.59523 0.470459 -0.0120842 0.882339 0.610758 0.223475 0.759628 +0.304978 0.828076 0.470403 0.721242 -0.434762 -0.539252 0.36128 -0.71504 -0.598493 +0.388596 -0.512773 -0.765544 0.558991 -0.302362 0.77208 0.585526 -0.0549765 0.808787 +0.866632 -0.261014 -0.425231 0.858169 -0.391576 -0.331986 0.713325 -0.374974 -0.592082 +0.593888 -0.457123 -0.662069 0.587702 0.230306 0.775606 0.547462 -0.170546 0.819268 +0.774095 -0.161433 0.61214 0.706535 -0.598876 0.377036 0.925809 0.0113489 -0.377822 +0.923617 -0.0156787 -0.382996 0.782317 -0.0677546 -0.619185 0.767957 -0.568018 0.295969 +0.728683 -0.527613 0.43663 0.827624 0.047022 0.55931 0.870838 -0.206366 0.446155 +0.931974 0.0332811 -0.360995 0.917093 0.0107858 -0.398528 0.686203 -0.106403 -0.719586 +0.770361 0.0515199 0.635523 0.548594 -0.1175 0.827791 0.872597 0.0639968 0.484231 +0.989221 -0.0233406 0.144558 0.889492 -0.311281 0.334527 0.88098 -0.0817423 -0.466039 +0.829371 -0.271338 -0.488384 0.592689 -0.318124 -0.739944 0.75209 -0.268242 0.602002 +0.760573 -0.179995 0.623803 0.93549 -0.113575 0.334603 0.97288 0.231237 0.00588654 +0.887252 -0.334297 0.317851 0.686727 -0.444898 -0.574867 0.290942 -0.605875 -0.740451 +0.588256 -0.268168 -0.762916 0.839305 0.00596888 0.543628 0.773711 -0.0318507 0.632737 +0.96521 -0.0263383 -0.260146 0.950597 -0.274511 0.144945 0.852941 -0.408326 0.32521 +0.345511 -0.110832 -0.931847 0.482526 -0.81811 -0.312833 0.879868 -0.201923 0.430185 +0.7576 -0.238279 0.607671 0.944149 -0.311186 0.108377 0.995228 -0.0786163 -0.0577993 +0.998421 -0.0556088 -0.00790856 0.937289 -0.159114 0.310117 0.841631 -0.346091 -0.414582 +0.649159 0.481501 -0.588854 0.920626 0.389935 -0.0199668 0.888139 0.45844 -0.0322659 +0.720116 0.679704 0.139411 0.870251 0.31936 -0.375062 0.745441 0.596034 -0.298431 +0.697888 0.35694 -0.620924 0.660945 -0.494238 0.564696 0.690041 -0.573846 0.441072 +0.843217 -0.446166 0.299868 0.920709 -0.389903 0.0164603 0.915264 -0.400985 -0.0387538 +0.901104 -0.371256 0.224008 0.847569 -0.345352 0.402938 0.47382 -0.323958 -0.818869 +0.766099 -0.633317 0.10955 0.730589 0.257012 0.632601 0.91811 0.13998 0.370784 +0.929631 0.368367 0.00957863 0.918631 0.292469 -0.265668 0.733256 0.226261 -0.641203 +0.793363 -0.589825 0.150604 0.813909 -0.577328 -0.0651435 0.652482 -0.74011 -0.1628 +0.458568 -0.697069 0.55119 -0.00500855 0.205966 0.978546 0.970958 -0.17845 0.159363 +0.786358 -0.603848 0.130416 0.72736 -0.660723 0.185453 0.919128 -0.391222 0.046353 +0.912912 -0.406056 -0.0413619 0.998037 0.0626241 1.23442e-05 0.411916 0.895461 0.168744 +-0.991253 0.0603488 -0.117368 0.552693 0.60041 -0.577961 0.613474 0.508882 -0.603894 +0.574973 0.600267 -0.555954 0.493861 0.822731 0.281451 0.709114 0.561954 0.425869 +0.896554 0.416177 0.151619 0.838544 0.528348 0.133016 0.658912 0.71606 0.23042 +0.676339 0.62538 0.389185 0.697696 0.703647 0.13454 0.593342 0.804587 0.0241996 +0.377682 0.925587 -0.0253993 -0.0955236 0.992648 -0.0743324 0.7256 -0.563471 -0.394974 +0.53151 -0.073389 -0.843867 0.760754 -0.647779 0.0404404 0.713874 -0.537281 0.449124 +0.823876 0.16127 0.543342 0.912049 -0.101525 0.397315 0.911115 0.27456 0.307387 +0.497808 0.51995 -0.694146 0.536528 0.586214 -0.607035 0.36484 0.358402 -0.859325 +0.680802 0.483587 0.550139 0.655607 0.512483 0.554563 0.92027 0.287333 0.265601 +0.981333 0.131575 0.140263 0.887613 0.340725 0.30992 0.740356 0.545473 -0.39285 +0.673251 0.580286 -0.458258 0.916483 0.397593 -0.0444872 0.803781 0.593633 -0.0391796 +0.927667 0.262937 0.265139 0.843086 0.511481 0.166115 0.871984 0.245683 0.423418 +0.217434 0.31516 -0.923795 0.275873 0.901726 0.332844 0.360339 0.722585 0.589938 +0.603831 0.723806 0.333906 0.639938 0.765002 -0.0724705 0.380393 0.92444 0.0266936 +0.605529 0.72173 -0.335321 0.458331 0.853875 -0.246635 0.52899 0.753828 -0.389762 +0.619612 0.519304 0.588562 0.567385 0.668389 0.480968 0.766873 0.555909 0.320734 +0.872657 0.482147 0.0774882 0.900478 0.366474 0.234174 0.827286 0.404835 -0.389494 +0.994542 0.104336 0.000128837 0.912523 0.0477212 0.406231 0.455355 0.557352 -0.69427 +0.309086 0.607128 -0.732025 0.196674 0.464005 -0.863723 0.251483 0.593449 0.764575 +0.393427 0.298325 0.869608 0.583226 0.450312 0.676067 0.496051 0.826918 -0.26484 +0.369132 0.928916 -0.0292752 0.382546 0.910987 0.154145 0.877412 0.357886 -0.319478 +0.948054 0.0135479 0.317821 0.0110052 0.0604816 -0.998109 0.63692 0.223918 0.737695 +0.296232 0.149555 0.943334 0.927182 0.330503 0.176353 0.0150715 0.0146292 -0.999779 +0.0784154 0.0461181 0.995853 0.315599 0.172781 -0.93303 0.00431855 0.00555212 0.999975 +0.38676 0.0138974 -0.922076 -0.163011 -0.329996 0.929801 0.296058 -0.131817 -0.946031 +-0.313329 -0.607625 0.729806 -0.316328 -0.58158 0.749467 -0.239116 -0.482517 0.842616 +0.0404591 -0.0284523 -0.998776 0.121234 0.256479 0.958917 0.0200836 0.0100323 -0.999748 +0.17073 0.615773 0.769204 0.26196 0.475394 0.839868 0.390121 0.590897 0.70615 +-0.00486404 -0.0174135 -0.999837 0.041478 0.0793575 0.995983 -0.0416527 -0.377311 -0.925149 +0.010594 -0.00326332 0.999939 -0.04996 -0.38291 -0.922434 -0.0662848 -0.611861 -0.788183 +0.0223216 -0.0337583 0.999181 -0.00495127 -0.0443303 -0.999005 -0.034059 -0.393246 0.918802 +-0.00466301 0.024499 -0.999689 -0.070033 -0.616811 0.783989 -0.0288813 -0.383461 0.923105 +0.161392 0.322622 -0.932667 0.0132656 -0.0272081 0.999542 0.585831 0.241734 -0.773542 +0.405794 0.51565 -0.75461 0.334451 0.425099 -0.841091 0.00799436 0.0209084 0.999749 +0.529227 0.395916 -0.750446 0.302511 0.181115 -0.93578 -0.0105497 -0.022098 0.9997 +0.888298 -0.452072 -0.0809815 0.0203027 0.0304437 -0.99933 -0.0136633 0.0262766 0.999561 +0.801614 -0.445489 -0.398691 0.892951 -0.448435 -0.039313 0.913176 -0.374726 0.160279 +0.906951 -0.420492 0.0250226 0.855806 -0.482905 0.185469 0.0952922 0.314648 -0.944413 +0.0314101 0.377877 0.925323 0.723941 -0.531153 -0.44021 0.795815 -0.53026 -0.292408 +0.795401 -0.588943 0.143123 0.776325 -0.571231 -0.266485 0.796876 -0.510491 0.323092 +0.83428 -0.486238 0.259902 0.916371 -0.398866 0.0342111 0.898429 -0.437884 0.0329035 +0.720783 -0.577851 0.382832 0.452071 0.552008 -0.700656 0.300295 0.624517 -0.720972 +0.186829 0.451117 -0.87269 0.0753174 0.61122 0.787869 0.030923 0.415774 0.908942 +0.524912 -0.580733 -0.622267 0.598111 -0.60261 -0.528322 0.729304 -0.598185 -0.332101 +0.788312 -0.542428 -0.290407 0.819603 -0.546565 0.171806 0.840989 -0.54017 -0.0308971 +0.80715 -0.585463 0.075775 0.695864 -0.62174 0.359462 0.704307 -0.535925 0.46555 +0.714338 -0.533379 0.453021 0.804206 -0.441353 0.398071 -0.0796115 -0.231713 -0.969521 +-0.0119771 -0.00581063 0.999911 0.806356 -0.500921 -0.314432 0.746525 -0.560641 -0.358304 +0.621031 -0.586975 -0.519404 0.613869 -0.539427 -0.576354 0.680806 -0.511663 0.524122 +0.685408 -0.504402 0.525161 0.717932 -0.397641 0.571363 -0.210595 -0.499558 -0.840293 +-0.189111 -0.599337 -0.777838 -0.313312 -0.587236 -0.746317 0.00136588 -0.376685 0.92634 +0.770241 -0.505604 -0.388708 0.816731 -0.497293 -0.29266 0.680996 -0.541036 -0.493482 +0.697484 -0.549821 -0.459579 0.605764 -0.537442 0.586691 0.617494 -0.553584 0.55879 +0.526196 -0.637579 0.562682 0.0170231 -0.0812535 -0.996548 0.0719847 -0.572395 0.816812 +0.029023 -0.390907 0.919972 0.557793 -0.597027 -0.576564 0.599061 -0.62376 -0.502046 +0.704776 -0.647522 -0.289838 0.700458 -0.632213 -0.331158 0.616518 -0.568851 0.544347 +0.540249 -0.689864 0.481891 0.640087 -0.679637 0.358304 0.669714 -0.679395 0.299844 +0.0531291 -0.00114228 -0.998587 0.153244 -0.315477 0.936478 0.582383 -0.195274 -0.789112 +0.552059 -0.410558 -0.725722 0.545501 -0.583078 -0.602037 0.477364 -0.836823 0.268051 +0.53575 -0.726981 0.4295 0.559035 -0.618492 0.55222 0.54812 -0.5644 0.617266 +0.630467 -0.222435 -0.743663 0.265039 -0.130583 -0.955355 0.376303 -0.723471 0.57878 +0.557857 -0.648944 0.517366 0.536119 -0.679269 0.501168 0.555015 -0.226431 -0.80043 +0.482086 -0.233547 -0.844422 0.543922 -0.532545 0.648495 0.381958 -0.386091 0.839668 +0.63673 -0.264579 -0.724274 0.345996 -0.158144 -0.924812 0.621426 -0.541473 0.566248 +0.561019 -0.664124 0.494163 0.665894 -0.658219 0.351188 0.625558 -0.775504 -0.0852649 +0.565639 -0.645853 -0.512763 0.539302 -0.440002 -0.718019 0.571206 -0.673633 0.46898 +0.266397 -0.522577 0.809905 0.641685 -0.765582 0.0460893 0.628022 -0.756802 -0.181218 +0.630137 -0.617495 -0.470773 0.565772 -0.45097 -0.69031 0.599128 -0.791093 0.12336 +0.450554 -0.848606 -0.277253 0.411091 -0.846254 0.338908 0.545184 -0.836558 -0.0542733 +0.426293 -0.859464 -0.282128 -0.844285 0.32769 0.42403 -0.780814 0.576843 0.23996 +-0.788186 0.602121 -0.127327 -0.85226 0.385464 -0.353653 -0.797758 0.2636 0.542306 +-0.70429 0.700777 0.11352 -0.601014 0.44187 0.665983 -0.700325 0.604467 -0.379691 +-0.797312 0.30064 -0.523363 -0.590405 0.390748 -0.706214 -0.772701 0.484999 0.409522 +-0.818137 0.414314 0.398742 -0.567682 0.270873 0.777409 -0.802739 0.288614 -0.521835 +-0.518354 0.359142 -0.776097 -0.771666 0.485256 -0.411167 -0.549427 0.571948 0.609102 +-0.604368 0.61538 0.506011 -0.688153 0.681838 0.248078 -0.662242 0.601016 0.447455 +-0.744671 0.600782 -0.290733 -0.66891 0.661052 -0.339956 -0.592133 0.696895 -0.404618 +-0.523374 0.598295 -0.606731 -0.559252 0.603402 0.568457 -0.58822 0.566339 0.577285 +-0.586218 0.534768 0.608582 -0.418163 0.719338 -0.5547 -0.255896 0.715548 -0.650007 +-0.330459 0.660656 -0.67404 -0.744345 0.382588 0.547337 -0.580664 0.434144 0.688729 +-0.602006 0.561235 0.567983 -0.297423 0.618437 -0.727376 -0.00569273 0.531678 -0.846927 +-0.687903 0.600665 0.407419 -0.708093 0.577096 0.406897 -0.54288 0.371391 0.753226 +0.591913 -0.142968 -0.79322 0.379171 0.449538 -0.808792 0.307449 0.145813 -0.940327 +-0.893111 0.2994 0.335727 -0.887754 0.449871 0.0975115 -0.793587 0.529492 0.299762 +-0.553488 0.60453 0.572883 -0.371552 0.863954 0.3399 -0.378044 0.905659 0.192002 +-0.37445 0.777088 0.505887 0.509684 -0.155079 -0.84627 0.529493 -0.369497 -0.763615 +-0.830702 0.458678 0.315513 -0.867432 0.462884 0.182485 -0.61577 0.507998 0.602301 +-0.696112 0.570028 0.436458 0.849043 -0.0818504 -0.521946 0.646098 0.324839 -0.690679 +0.890085 0.208675 -0.40522 0.704402 0.38603 -0.59565 -0.452295 0.693318 0.561016 +-0.387504 0.661889 0.641672 -0.424165 0.837816 0.343727 -0.481146 0.83741 0.259312 +0.825997 0.296853 -0.479173 0.908858 0.106587 -0.403256 0.806454 0.0488864 -0.589273 +-0.269157 0.774938 0.571861 -0.27744 0.583714 0.763089 -0.196204 0.837607 0.509821 +0.701179 0.41204 -0.581869 0.668087 0.1039 -0.736794 -0.388229 0.486355 0.782775 +-0.333625 0.508404 0.793864 0.465108 0.024385 -0.884918 -0.165893 0.539802 0.825283 +-0.167284 0.298563 0.939615 0.868946 -0.184149 -0.459372 0.580982 0.0331305 -0.813242 +-0.0332729 0.39348 0.918731 0.902973 -0.319118 -0.287755 0.984662 -0.0975332 -0.144667 +0.697391 0.272584 -0.662829 0.876161 -0.10327 -0.470826 0.547368 0.0875741 -0.832298 +0.0350885 0.58296 0.811743 0.0181376 0.390204 0.92055 0.828345 -0.400133 -0.392095 +0.889024 -0.397498 -0.227227 0.907394 -0.367301 -0.204269 0.614737 -0.396826 -0.681636 +0.680397 -0.547157 -0.487524 0.357933 0.357439 0.862624 0.832352 -0.373651 -0.409359 +0.827391 -0.377538 -0.415799 0.751824 -0.632715 -0.185563 0.667938 -0.595058 -0.446951 +0.724645 -0.624963 -0.290362 0.602356 0.52401 0.602147 0.570278 0.667914 0.478198 +0.45836 0.612959 0.643575 0.85714 -0.368212 -0.360182 0.806513 -0.437337 -0.397835 +0.617695 -0.514743 -0.594553 0.642098 -0.561913 -0.521502 0.406731 0.725946 0.554592 +0.224425 0.348447 0.910065 0.432582 -0.540575 -0.721562 0.624828 -0.773848 -0.103683 +0.821796 -0.566204 -0.0637449 0.749755 -0.582884 0.313231 0.38112 -0.744701 -0.547875 +0.533746 -0.553505 -0.639333 0.588278 -0.111106 0.80099 0.287181 0.0318737 0.957346 +0.651552 -0.137989 -0.745948 0.595339 -0.212097 -0.774975 0.754168 -0.644178 -0.127534 +0.0678529 -0.0325932 0.997163 0.670482 -0.0456112 -0.740523 0.728005 -0.021039 -0.685249 +0.695566 -0.461036 0.551029 0.173908 -0.379381 0.908749 0.903709 -0.25742 0.342117 +0.62379 -0.333783 -0.706735 0.703079 -0.285323 -0.651361 0.276073 -0.598874 0.751754 +0.314054 -0.414402 0.85419 0.422466 -0.559779 -0.712861 0.568588 -0.503001 -0.65092 +0.570849 -0.275002 0.773631 0.291036 -0.124277 0.948606 0.282182 -0.104875 -0.953611 +0.358781 -0.572654 -0.737118 0.633413 -0.0648491 0.771092 0.319111 -0.0303536 0.947231 +0.260777 -0.18005 -0.948461 0.433734 -0.89767 -0.0778718 0.357774 0.695345 -0.623292 +0.89618 0.426401 -0.122653 0.722603 -0.110462 0.68238 0.553613 0.734217 -0.392987 +0.362768 0.864212 -0.348621 0.274923 0.747363 -0.604869 0.617183 -0.231159 0.752097 +0.146492 -0.362401 0.920438 0.420844 0.858808 -0.292128 0.60261 0.204246 -0.771456 +0.453338 0.564007 -0.690204 0.307725 0.416501 -0.855472 0.374238 -0.568983 0.732259 +0.361914 -0.31988 0.875611 0.691152 -0.437919 0.574923 0.986413 0.0938668 -0.134831 +0.632897 0.255173 -0.730977 0.527746 0.421265 -0.737577 0.846672 0.327281 0.419564 +0.53591 0.467438 0.703066 0.980933 0.0122106 0.193964 0.995114 0.0985685 -0.00574649 +0.787025 0.268154 -0.555595 0.708479 -0.374109 -0.598415 0.457961 -0.83178 0.313708 +0.492721 -0.277604 0.824719 0.542493 0.546028 -0.638399 0.5829 0.531686 -0.614441 +0.358531 0.374317 -0.855185 0.412816 0.819727 0.397027 0.532008 0.627592 0.568416 +0.496317 0.651438 0.573845 0.399003 0.835837 0.377059 0.309837 0.509181 -0.802955 +-0.593435 0.784706 0.179084 0.9227 0.0885254 -0.375216 0.0766859 0.38714 0.918826 +0.999043 -0.0436813 -0.00211802 0.943004 -0.120337 0.310261 0.331681 0.241233 -0.912028 +0.57642 0.558167 0.596816 0.579073 0.581412 0.571519 0.555782 0.566012 0.608881 +0.168377 0.984514 0.0487936 0.413669 0.279273 -0.866536 0.230978 0.891921 0.388749 +0.220132 0.769199 0.599895 0.325118 0.925284 0.195315 0.371393 0.914056 -0.163 +0.542723 0.820327 -0.180318 0.381696 0.900539 -0.208176 0.545859 0.83513 0.0677947 +0.485687 0.873919 -0.0193329 0.568919 0.71347 0.409012 0.546226 0.761959 0.34793 +0.249343 0.555943 -0.792941 0.182779 0.283475 -0.9414 0.301196 0.617624 0.726513 +0.160461 0.278736 0.946868 0.0506357 0.0616229 -0.996814 0.0192482 0.0137717 0.99972 +0.372674 0.368778 -0.851538 -0.0164283 0.00732434 0.999838 0.609664 0.181307 -0.771646 +0.565838 0.452103 -0.689515 0.449581 0.303633 -0.84005 -0.0546875 -0.0759231 0.995613 +0.613317 -0.0709286 -0.786646 0.377347 0.0248151 -0.925739 -0.273823 -0.476154 0.835643 +0.606875 -0.306754 -0.733215 0.39843 -0.174187 -0.900507 -0.271709 -0.600381 0.752141 +-0.18984 -0.305834 0.932967 0.335157 -0.293926 -0.895141 0.524723 -0.464498 -0.713377 +0.0280408 0.373343 0.92727 0.0494043 -0.0105303 -0.998723 0.0826626 0.621804 0.778798 +0.0542294 0.388744 0.919748 -0.0203381 -0.0758443 -0.996912 0.011932 0.0455764 0.99889 +-0.188919 -0.466289 -0.864225 -0.00911698 -0.00196147 0.999957 -0.124187 -0.338494 -0.932738 +-0.293111 -0.603471 -0.741558 -0.0792234 -0.082511 0.993436 -0.0359221 -0.0345273 -0.998758 +-0.284704 -0.431741 0.855887 0.0499062 0.102577 -0.993472 -0.136247 -0.609038 0.781351 +-0.15831 -0.324959 0.932384 0.284235 0.46668 -0.837509 0.0004453 -0.00437376 0.99999 +0.213424 0.609415 -0.763586 0.188092 0.321403 -0.928074 0.00493949 0.00331696 0.999982 +0.0129139 -0.00245536 -0.999914 -0.00852366 -0.0330814 0.999416 -0.0354721 0.00857049 -0.999334 +0.0427787 0.0819995 0.995714 0.0435196 0.391523 -0.919139 0.207804 0.472475 0.856495 +0.900995 -0.433329 -0.0208259 0.069228 0.624676 -0.777809 0.0376552 0.344219 -0.938134 +0.321675 0.594996 0.736549 0.135646 0.348153 0.927572 0.810353 -0.479182 -0.337212 +0.871443 -0.4754 -0.120756 0.979937 -0.188391 0.0650573 0.932605 -0.335403 0.133237 +0.914115 -0.405243 0.0130804 0.922907 -0.384759 -0.0142431 0.865803 -0.458165 0.20117 +-0.0516482 -0.349872 -0.935373 0.0233116 -0.0379719 0.999007 0.806425 -0.42009 -0.416177 +0.82038 -0.423729 -0.383968 0.814508 -0.560093 0.151233 0.771012 -0.542229 -0.333958 +0.785131 -0.527783 0.324059 0.86276 -0.439727 0.249569 0.937181 -0.343504 0.0608036 +0.925513 -0.378477 -0.0134489 0.917553 -0.397447 -0.0114864 0.920317 -0.36398 0.143302 +0.841407 -0.398463 0.365051 -0.0753978 -0.372689 -0.924888 -0.0889326 -0.621995 -0.777954 +-0.0378788 -0.39897 0.916181 0.781492 -0.481744 -0.396475 0.837817 -0.452398 -0.305611 +0.735431 -0.498344 -0.459122 0.686868 -0.537535 -0.489151 0.769535 -0.607943 0.195505 +0.765399 -0.643546 -0.00360047 0.782145 -0.622668 -0.0230772 0.817217 -0.571145 0.0771275 +0.803105 -0.526672 0.278636 0.764404 -0.553337 0.330914 0.837396 -0.463421 0.289841 +0.742319 -0.515771 0.427719 -0.00978131 -0.0197569 -0.999757 -0.0243842 -0.586717 0.809425 +-0.0373872 -0.368471 0.928887 0.667019 -0.685949 -0.290793 0.678895 -0.698968 -0.224824 +0.641467 -0.6355 -0.429721 0.698696 -0.65521 -0.287268 0.684657 -0.645723 -0.338064 +0.750889 -0.611661 0.24907 0.638516 -0.649685 0.412561 0.613559 -0.637887 0.465452 +0.525195 -0.647068 0.552696 0.00844354 0.0133171 -0.999876 0.0119814 -0.0869539 0.99614 +0.526528 -0.514803 -0.676569 0.615062 -0.618968 -0.488444 0.669556 -0.695502 -0.260712 +0.66345 -0.681085 -0.309772 0.559825 -0.729861 0.392299 0.485808 -0.809034 0.330839 +0.620513 -0.640813 0.452021 0.544236 -0.649207 0.531354 0.045366 0.00285132 -0.998966 +0.467504 -0.575853 0.670695 0.172741 -0.359539 0.917002 0.481666 -0.287924 -0.827706 +0.583663 -0.426921 -0.690707 0.617059 -0.564757 0.547985 0.585523 -0.564499 0.581811 +0.549097 -0.527539 0.648224 0.0538816 -0.0428435 -0.997628 0.382621 -0.602635 0.700308 +0.34619 -0.537511 0.768918 0.532568 -0.617701 0.578634 0.501559 -0.651979 -0.568649 +0.563832 -0.401231 -0.721877 0.533306 -0.551922 0.641067 0.383093 -0.515337 0.766595 +0.443931 -0.688287 -0.573748 0.214594 -0.375533 -0.901623 0.619214 -0.547282 0.563077 +0.527761 -0.714754 0.458906 0.680091 -0.716599 0.154799 0.632888 -0.728835 -0.261253 +0.593611 -0.762972 -0.25593 0.554612 -0.704165 -0.443347 0.467127 -0.674495 0.571707 +0.489887 -0.773572 0.401992 0.629719 -0.763993 0.140598 0.553197 -0.746548 -0.369646 +0.424659 -0.811469 -0.401475 0.247423 -0.802687 -0.542656 0.616862 -0.772793 0.149242 +0.457044 -0.885907 -0.0792367 -0.654127 0.642553 0.399053 -0.669375 0.714844 -0.202325 +-0.757478 0.616555 0.21468 -0.551677 0.347956 0.75801 -0.651902 0.639843 -0.406972 +-0.430996 0.458324 -0.777291 -0.656474 0.709834 0.255298 -0.648417 0.721992 0.241419 +-0.595505 0.407163 0.692525 -0.571753 0.681893 -0.456202 -0.249 0.533374 -0.808401 +-0.621192 0.763341 -0.177286 -0.740566 0.408715 0.533398 -0.724977 0.430293 0.537826 +-0.756297 0.588303 0.286207 -0.728499 0.575485 0.371625 -0.411918 0.83963 -0.354042 +-0.489187 0.829865 -0.268367 -0.347512 0.838774 -0.419158 -0.397016 0.706883 -0.585401 +-0.744042 0.512576 0.428565 -0.831629 0.426138 0.356091 -0.766764 0.348657 0.538991 +-0.359943 0.680664 -0.638073 -0.261689 0.702948 -0.661349 -0.699576 0.666404 0.257874 +-0.485989 0.594505 0.640608 -0.700913 0.600653 0.384625 -0.255149 0.607579 -0.752162 +-0.323083 0.245228 -0.914046 -0.508226 0.652621 0.561953 -0.566529 0.674982 0.472698 +-0.401442 0.893508 0.201213 -0.360138 0.877948 0.315449 0.565403 -0.0376786 -0.823954 +0.178833 0.123687 -0.976074 -0.508935 0.663319 0.548628 -0.540904 0.584534 0.604767 +-0.572596 0.529723 0.625722 0.632564 0.254329 -0.731559 0.273815 0.707685 -0.651311 +0.251133 0.580623 -0.774474 -0.0975441 0.912778 0.396638 -0.079727 0.773762 0.628439 +-0.319783 0.94557 0.0602948 -0.407682 0.832046 0.376159 0.677237 -0.0206888 -0.735474 +0.505953 0.221674 -0.83359 -0.345365 0.63244 0.693356 -0.283679 0.686586 0.669422 +-0.093919 0.777393 0.621965 0.657999 -0.0329047 -0.752299 0.734469 -0.019849 -0.678352 +-0.156633 0.685687 0.710844 -0.171497 0.585514 0.792315 0.64276 0.0897152 -0.760796 +0.743767 0.00146117 -0.668437 0.0401868 0.616609 0.786243 0.0266901 0.413016 0.910333 +0.595899 0.293812 -0.747381 0.649333 0.136019 -0.748241 0.197984 0.461898 0.864553 +0.543325 -0.00417553 -0.839512 0.271825 0.59679 0.754952 0.376448 0.571206 0.729391 +0.51489 -0.584334 -0.62725 0.583678 -0.36175 -0.72695 0.512148 0.547001 0.662189 +0.382572 0.553642 0.739675 0.575645 0.549111 0.605896 0.633793 -0.733375 -0.2459 +0.628113 -0.751754 -0.200849 0.579229 -0.674172 -0.458243 0.560428 -0.564835 -0.605708 +0.20687 0.600291 0.772564 0.245318 0.480082 0.842224 0.47445 0.62787 0.61699 +0.510025 -0.657419 -0.554684 0.554217 -0.625046 -0.549691 0.561116 -0.741567 -0.367732 +0.469372 -0.827964 -0.306865 0.0117172 0.0420524 0.999047 0.86963 -0.420424 -0.258821 +0.21495 -0.508256 -0.83395 0.312778 -0.819172 -0.480758 0.38129 -0.833443 -0.399988 +-0.0263417 0.0107439 0.999595 0.884209 -0.0389752 -0.465462 0.876747 -0.416072 -0.241245 +0.449846 -0.46485 -0.762596 -0.019583 -0.0486547 0.998624 0.775229 -0.126641 -0.618855 +0.594388 -0.470307 -0.652314 0.58051 -0.390808 -0.714337 0.00859658 -0.352164 0.935899 +0.645954 -0.272555 -0.713062 0.454569 -0.282281 -0.844798 0.0440249 -0.60672 0.793696 +0.0543433 -0.362148 0.930535 0.500875 -0.631415 -0.59198 0.590606 -0.603769 -0.535395 +0.556791 -0.479012 -0.678624 -0.0103707 -0.0848274 0.996342 0.540189 0.255613 -0.801784 +0.465921 -0.144521 -0.872944 0.375865 -0.765125 -0.522791 -0.283682 -0.15534 0.946253 +0.984921 0.129125 -0.115142 0.245658 0.776391 -0.580404 -0.0230959 0.737506 -0.674945 +-0.386421 -0.308825 0.869084 0.0411905 0.669149 -0.741986 0.127564 0.636068 -0.761016 +-0.293992 -0.598265 0.745418 -0.125031 0.0966225 0.987437 0.476301 0.523525 -0.706441 +0.321337 0.630422 -0.706619 0.190274 0.429992 -0.882555 0.140134 0.885888 0.442228 +0.503341 0.75908 0.412851 0.435002 0.654244 0.618658 0.940272 -0.297343 -0.165757 +0.00895534 0.3203 -0.947274 0.534244 0.746504 0.396629 0.583943 0.593292 0.55409 +0.570112 0.604624 0.556239 0.479767 0.838804 0.257355 0.932022 0.277429 0.233171 +0.532386 0.20314 -0.821766 0.194344 0.517956 -0.833038 -0.0690458 0.443008 -0.893855 +0.362891 0.906934 0.213962 0.486179 0.776086 0.401646 0.550228 0.833284 0.0537221 +0.557324 0.760608 -0.332965 0.448212 0.872879 -0.192845 0.549195 0.726062 -0.413786 +0.595126 0.526452 0.607185 0.580872 0.627957 0.517936 0.580357 0.567919 0.583656 +0.536559 0.366501 -0.760119 0.280878 0.205586 -0.937466 0.214924 0.804884 0.553146 +0.234041 0.50535 0.83057 0.336961 0.729891 0.594741 0.345871 0.814304 0.466135 +0.447616 0.851237 -0.273925 0.450342 0.88601 0.110354 0.0780287 0.0957623 -0.992341 +0.0244063 0.0638548 0.997661 0.363625 0.35812 -0.859958 -0.0153446 -0.0229412 0.999619 +0.513842 0.532649 -0.672497 0.520062 0.535302 -0.665573 0.373678 0.382399 -0.845066 +-0.0104528 -0.00712415 0.99992 0.0880127 0.0590764 -0.994366 -0.122387 -0.319425 0.939675 +0.0431047 -0.015925 -0.998944 -0.376317 -0.591272 0.71329 -0.307647 -0.602462 0.736473 +-0.205907 -0.500976 0.84061 0.0493933 -0.0650915 -0.996656 0.000711004 -0.00021327 1 +0.405462 -0.550031 -0.730114 0.235913 -0.381299 -0.893844 0.2064 0.497251 0.842699 +0.180166 -0.274263 -0.944627 0.247215 -0.552439 -0.79605 0.313706 0.602623 0.733781 +0.13676 0.336068 0.931856 -0.170164 -0.352046 -0.920384 -0.0447533 -0.0385803 0.998253 +-0.286382 -0.482279 -0.827884 -0.282131 -0.574639 -0.76824 -0.377007 -0.570256 -0.729845 +-0.337783 -0.179369 0.923975 -0.038196 -0.0894225 -0.995261 -0.472212 -0.220816 0.853379 +0.10299 0.0719288 -0.992078 -0.428595 -0.572544 0.698927 -0.552071 -0.44914 0.702489 +-0.403869 -0.353549 0.843738 0.376048 0.362242 -0.852859 -0.0439224 -0.0485679 0.997854 +0.414031 0.58037 -0.701248 0.514215 0.5235 -0.679361 0.368815 0.3915 -0.843032 +0.0128549 0.020029 0.999717 0.0319383 0.0375985 -0.998782 -0.00445195 -0.0129061 0.999907 +-0.0342669 -0.0578792 -0.997735 0.0126285 0.00534136 0.999906 0.0310121 0.075892 -0.996634 +0.159492 0.33218 0.929634 0.253851 0.480247 -0.839597 0.113583 0.617012 0.778713 +0.268827 0.474922 0.837963 0.403401 0.56685 0.718295 0.161599 0.616358 -0.770707 +0.116448 0.253252 -0.960366 0.0109174 0.110611 0.993804 -0.188911 -0.467248 -0.863708 +-0.103072 -0.108248 0.988766 0.862501 -0.49901 -0.0841451 -0.127127 -0.32226 -0.938076 +-0.308266 -0.609906 -0.730059 -0.29342 -0.445354 0.84591 0.761999 -0.528809 -0.37379 +0.853933 -0.51604 -0.0670887 0.789103 -0.597091 0.144216 0.679941 -0.732072 0.0418413 +0.66585 -0.746071 -0.00455812 0.750779 -0.645873 0.138489 0.819046 -0.524868 0.231684 +-0.0122575 0.00438821 -0.999915 -0.15856 -0.597718 0.78587 -0.170919 -0.310695 0.935016 +0.661046 -0.683738 -0.309065 0.663438 -0.71167 -0.231034 0.61412 -0.641137 -0.460218 +0.713917 -0.654405 -0.249154 0.795442 -0.60603 0.000296177 0.775741 -0.630866 0.015293 +0.713377 -0.699437 -0.0433751 0.699731 -0.709295 0.0853067 0.711025 -0.627268 0.317771 +0.706217 -0.595811 0.382447 0.794955 -0.541688 0.273167 0.77746 -0.594994 0.203811 +0.700041 -0.657079 0.279625 -0.00498157 0.0211461 -0.999764 -0.00318429 -0.0445035 0.999004 +0.666523 -0.693177 -0.274323 0.66363 -0.714429 -0.221781 0.655841 -0.652796 -0.379118 +0.721747 -0.623422 -0.300708 0.679869 -0.627471 -0.379551 0.727289 -0.633377 0.264357 +0.73403 -0.667028 0.127568 0.658607 -0.67113 0.340325 0.647581 -0.651616 0.395014 +0.629644 -0.692702 0.351727 0.659615 -0.678412 0.32352 -0.0154394 0.0146895 -0.999773 +0.155172 -0.2679 0.950869 0.622843 -0.190606 -0.758773 0.705984 -0.405919 -0.580358 +0.737201 -0.600528 -0.309679 0.719649 -0.630985 -0.289764 0.680285 -0.656977 0.324951 +0.634731 -0.657008 0.406764 0.569973 -0.694336 0.43935 0.561324 -0.615447 0.553299 +-0.0132372 -0.04662 -0.998825 0.21783 -0.589954 0.777498 0.316561 -0.480688 0.817758 +0.629068 -0.247112 -0.737027 0.65249 -0.342667 -0.675897 0.445934 -0.690509 0.569508 +0.352595 -0.514975 0.781331 0.510935 -0.550344 0.660353 0.122646 -0.315275 -0.941042 +0.485403 -0.607524 0.628728 0.426875 -0.366665 0.82664 0.777566 -0.593777 -0.20693 +0.729933 -0.56208 -0.388926 0.611316 -0.579424 -0.539037 0.574438 -0.484284 -0.659916 +0.549743 -0.5363 0.640441 0.516758 -0.670572 0.532254 0.511646 -0.659253 0.551003 +0.510499 -0.718403 -0.472534 0.549964 -0.626061 -0.5528 0.383312 -0.678428 -0.626744 +0.642139 -0.54035 0.543765 0.737255 -0.519472 0.431976 0.826913 -0.538009 0.163588 +0.857848 -0.471554 -0.204288 0.615292 -0.551915 -0.562856 0.619268 -0.56246 -0.547856 +0.38846 -0.857085 0.338385 0.479974 -0.865703 0.142068 0.587067 -0.730979 -0.347882 +0.803312 -0.585859 0.107047 0.715841 -0.615011 -0.330656 -0.794281 0.29415 0.531595 +-0.824842 0.559081 0.0840481 -0.799947 0.292106 0.524175 -0.753213 0.635058 -0.171382 +-0.766436 0.365419 0.528247 -0.478662 0.534452 -0.696594 -0.887218 0.31933 0.332977 +-0.897705 0.362204 0.250867 -0.844089 0.307647 0.439167 -0.772365 0.2071 0.600468 +-0.409402 0.748188 -0.522115 -0.633272 0.649116 -0.421444 -0.603831 0.730911 -0.318054 +-0.601436 0.718547 -0.349234 0.483008 0.819823 0.30756 0.662126 0.579814 0.474768 +0.903343 0.402068 0.149376 0.837164 0.457113 -0.300339 0.658664 0.736625 -0.153445 +0.571231 0.62652 -0.530253 -0.863859 0.373605 0.337886 -0.882524 0.418353 0.214782 +-0.702687 0.420022 0.574292 -0.713301 0.543973 0.441921 -0.723676 0.641791 -0.253768 +-0.618886 0.681032 -0.391377 -0.707226 0.539245 -0.457216 -0.497196 0.645647 -0.579601 +0.577172 0.41133 0.705465 0.723604 -0.0314486 0.689498 0.88822 0.0360553 0.458001 +0.92303 -0.309285 0.22882 0.999743 0.0156803 0.0163937 0.61748 0.527187 -0.583774 +0.834714 0.374732 -0.403519 0.633521 0.57979 -0.512342 -0.62872 0.681885 0.373823 +-0.647351 0.729899 0.219509 -0.683001 0.614173 0.395349 -0.792337 0.534996 0.293227 +-0.759193 0.563728 0.325327 -0.800819 0.163401 -0.576184 -0.745374 -0.520525 -0.4165 +-0.961053 -0.131835 -0.242891 -0.804876 -0.570827 0.162267 -0.949747 0.127197 0.28601 +-0.841386 -0.0541139 0.537719 -0.641414 0.460724 -0.613451 -0.77053 0.197184 -0.606137 +-0.619187 0.349991 -0.702933 0.777168 -0.553342 0.299703 0.682255 -0.658821 0.316992 +0.673056 -0.591214 0.444367 0.824269 -0.43659 0.360513 0.997919 -0.0484447 -0.0425526 +0.70211 0.483514 -0.522739 0.867429 0.188691 -0.460394 0.761288 0.328071 -0.559295 +-0.615415 0.737961 0.276908 -0.604696 0.769813 0.204283 -0.640316 0.661862 0.389787 +-0.739232 0.588031 0.328262 -0.762609 0.586054 0.273802 -0.833984 0.536858 -0.127492 +-0.814117 0.47472 0.334447 -0.828293 -0.546647 -0.122913 -0.996277 -0.00842507 0.0857947 +-0.806724 0.248317 -0.536223 -0.77831 0.0340127 0.626958 -0.617429 0.202282 -0.760173 +-0.705592 0.450641 -0.546866 0.647194 -0.540321 0.537767 0.617495 -0.59206 0.517846 +0.844385 -0.445916 0.29694 0.914756 -0.350894 -0.200239 0.818631 -0.0817562 -0.568471 +-0.554518 0.606121 0.570199 -0.609643 0.595626 0.523034 -0.687162 0.663856 0.295133 +-0.715897 0.642766 0.272659 -0.800528 0.521624 0.295064 -0.395243 0.762275 -0.512562 +-0.166566 0.858512 -0.484987 -0.050727 0.594003 -0.802862 0.686772 -0.671821 0.277491 +0.760571 -0.574728 -0.302025 -0.486041 0.873094 -0.0383514 -0.321643 0.823004 0.468199 +-0.641202 0.749797 -0.163295 -0.582007 0.801315 -0.138424 -0.57534 0.778227 0.251688 +-0.515989 0.816421 0.259253 -0.541562 0.72814 0.420146 0.585865 0.236705 -0.775069 +-0.0768271 0.629936 -0.772838 -0.095393 0.477015 -0.873703 0.113683 0.834287 0.539483 +0.0981821 0.664204 0.741076 -0.492226 0.863514 0.109802 0.697371 -0.0442283 -0.715345 +0.373021 0.0460736 -0.926678 0.25997 0.680721 0.684861 0.212898 0.700578 0.681076 +0.65378 0.00169533 -0.756683 0.368897 -0.023421 -0.929175 0.286328 0.645679 0.707894 +0.36211 0.555268 0.748701 0.602467 0.0253802 -0.79774 0.371627 -0.0217857 -0.928127 +0.569921 0.556801 0.604287 0.596582 0.584609 0.549838 0.515219 0.574051 0.636408 +0.598831 -0.0410518 -0.799823 0.349772 -0.00762396 -0.936804 0.554932 0.534798 0.637214 +0.530453 0.440873 0.724052 0.551171 0.565049 0.613946 0.604885 -0.226165 -0.763521 +0.354495 -0.131623 -0.925748 0.375296 0.467934 0.800119 0.53742 -0.672523 -0.508815 +0.601594 -0.600624 -0.526627 0.556352 -0.460528 -0.691655 0.504446 0.477272 0.719545 +0.305513 0.115568 0.945148 0.5263 -0.689079 -0.498175 0.531676 -0.696425 -0.481989 +0.392251 -0.819748 -0.417315 0.351894 -0.864965 -0.35778 0.0037601 -0.0229091 0.999731 +0.460894 -0.575015 -0.675969 0.522006 -0.674123 -0.522558 0.373639 -0.837615 -0.39849 +0.352865 -0.849974 -0.391191 0.000844639 -0.00347874 0.999994 0.372389 -0.371657 -0.85041 +0.565082 -0.515687 -0.64401 -0.00705051 -0.0203087 0.999769 0.423695 -0.525352 -0.737894 +0.314897 -0.442718 -0.839548 -0.0652977 -0.380312 0.92255 0.240021 -0.430202 -0.870239 +0.114751 -0.602279 -0.789995 -0.0464995 -0.58362 0.810695 -0.130509 -0.407108 0.904008 +0.427063 -0.687267 -0.587606 0.096816 -0.497332 -0.862141 -0.406657 -0.393822 0.824339 +0.415761 -0.242182 -0.876636 0.282138 -0.723358 -0.6302 0.0538788 -0.832493 -0.55141 +-0.597777 -0.172933 0.782788 -0.559815 -0.381922 0.735352 -0.523524 -0.291863 0.800462 +0.165229 0.594993 -0.786564 -0.229738 0.436795 -0.86973 -0.58532 -0.370627 0.721135 +-0.567924 -0.413028 0.711948 -0.129413 0.738934 -0.661233 0.0144891 0.778861 -0.627029 +-0.581999 -0.475832 0.659439 -0.572784 0.278503 0.770944 -0.434774 -0.0519516 0.89904 +0.0708075 0.64181 -0.763588 0.000641848 0.322042 -0.946725 0.257963 0.915726 0.308059 +0.375584 0.700179 0.607195 0.586439 0.736157 0.337879 0.558731 0.778443 0.286089 +0.270089 0.956814 0.107512 0.0268109 0.994563 0.100627 -0.148288 0.802936 0.577325 +0.00816328 0.105672 -0.994368 0.165477 0.642786 0.74796 0.311705 0.696911 0.645876 +0.529603 0.589503 0.609924 -0.352371 0.56238 -0.74804 -0.104155 0.383999 -0.91744 +0.277287 0.83987 0.466617 0.321431 0.708364 0.628413 0.422658 0.747495 0.512456 +0.424362 0.899621 0.102953 0.127807 0.988732 -0.0779375 0.575821 0.812073 -0.0946947 +0.322073 0.935362 -0.146171 0.555129 0.717589 0.420592 0.358294 0.871545 0.334716 +-0.270119 0.56363 -0.780614 -0.017514 0.333725 -0.942508 0.19494 0.71709 0.669163 +0.0611938 0.417744 0.906502 0.374516 0.388699 -0.841814 -0.0134225 0.0145519 0.999804 +0.528383 0.529371 -0.66376 0.53004 0.521632 -0.668549 0.381085 0.375941 -0.844655 +-0.0013139 -0.00154903 0.999998 0.0495146 0.0592751 -0.997013 0.00525748 -0.030353 0.999525 +-0.0380845 -0.0161237 -0.999144 -0.00756216 -0.38154 0.924322 -0.0236611 0.00873514 -0.999682 +-0.0772267 -0.618584 0.781914 0.00121469 -0.365007 0.931004 0.000870347 -0.0506071 -0.998718 +0.165777 0.294646 0.941117 0.143613 -0.273827 -0.950996 0.1692 0.614072 0.7709 +0.270495 0.483779 0.83234 0.386563 0.585435 0.712625 0.152468 -0.460648 -0.874389 +0.285828 -0.557594 -0.779353 -0.00606097 0.030038 0.99953 -0.175462 -0.614562 -0.769108 +-0.373023 -0.421657 0.826474 -0.117137 -0.285411 -0.95122 -0.341874 -0.567218 -0.749257 +-0.605547 -0.214785 0.766277 -0.584452 -0.423748 0.691993 -0.442434 -0.29174 0.848021 +0.32877 0.158557 -0.931005 -0.619569 -0.313811 0.719484 -0.335112 -0.141318 0.93152 +0.483723 0.245373 -0.840122 -0.0492519 -0.0337132 0.998217 0.531158 0.524093 -0.665731 +0.555032 0.464691 -0.689929 0.399537 0.345545 -0.849099 0.0189273 0.0190433 0.999639 +0.0585338 0.0622595 -0.996342 0.000962065 0.00185746 0.999998 -0.0305669 -0.041059 -0.998689 +-0.00578447 -0.0178902 0.999823 -0.00353432 -0.00945331 -0.999949 0.0051691 0.0349684 0.999375 +0.165408 0.327025 -0.930427 -0.101427 0.30874 0.945723 0.376908 0.594379 -0.71039 +0.335753 0.599132 -0.72685 0.230588 0.466742 -0.853804 -0.342122 0.518398 0.783719 +-0.200269 0.376676 0.904438 -0.118564 -0.256168 -0.959333 -0.378452 0.0257846 0.925262 +-0.258628 -0.48115 -0.837619 -0.169127 -0.615934 -0.769429 -0.383874 -0.591344 -0.709192 +-0.475839 -0.218351 0.851998 -0.0341111 -0.0798627 -0.996222 -0.428175 -0.57375 0.698196 +-0.545886 -0.463873 0.697732 -0.396244 -0.359353 0.8449 0.0135217 0.0321177 -0.999393 +-0.0491115 -0.0376962 0.998082 0.661236 -0.705735 -0.254372 0.756573 -0.647532 0.0910986 +0.85719 -0.514298 0.0268892 0.795574 -0.588525 -0.143873 0.714822 -0.698777 -0.0272044 +0.688947 -0.67159 0.272615 -0.00600132 0.00392232 -0.999974 0.0206518 0.0153457 0.999669 +0.804383 -0.525304 -0.277532 0.752659 -0.525445 -0.396751 0.641979 -0.614925 -0.457962 +0.745958 -0.649692 -0.146449 0.769583 -0.58195 0.262822 0.782212 -0.518061 0.34606 +0.867719 -0.49444 0.0509145 0.686847 -0.695269 -0.211759 0.695092 -0.56908 -0.439312 +0.692581 -0.716109 0.0867147 0.655167 -0.655863 0.374965 0.561004 -0.727923 0.394212 +0.635281 -0.70116 0.323716 0.67748 -0.728327 0.102769 0.689557 -0.684852 0.23556 +-0.0293801 -0.00291937 -0.999564 0.0329005 -0.0422892 0.998564 0.829163 -0.357285 -0.429926 +0.830571 -0.486116 -0.27174 0.840421 -0.480066 -0.251452 0.579513 -0.524055 -0.624124 +0.642197 -0.532202 -0.551673 0.786697 -0.601396 -0.139391 0.500521 -0.835536 0.226622 +0.616216 -0.761347 0.201565 0.679562 -0.645078 0.349386 0.569391 -0.738772 0.360569 +0.6552 -0.68261 0.323661 0.640088 -0.728149 0.245125 0.00596921 -0.00058225 -0.999982 +0.383982 -0.565144 0.730185 0.312958 -0.293454 0.903295 0.750712 -0.480696 -0.453169 +0.760693 -0.609699 -0.22274 0.694923 -0.6103 -0.380285 0.723985 -0.61621 -0.31005 +0.490099 -0.83855 0.237985 0.506195 -0.817476 0.274773 0.355433 -0.773349 0.524975 +0.37928 -0.568642 0.729927 0.0792818 -0.0286909 -0.996439 0.520156 -0.458818 0.720364 +0.495368 -0.219199 0.840572 0.544637 -0.553671 -0.629936 0.445774 -0.494137 -0.746401 +0.460584 -0.729569 -0.50556 0.441524 -0.567243 0.695193 0.228633 -0.508178 0.83035 +0.490582 -0.541411 -0.682792 0.295842 -0.290641 -0.909948 0.71652 -0.40196 0.570112 +0.658395 -0.14119 0.739311 0.530545 -0.621518 -0.5764 0.564455 -0.61056 -0.555524 +0.538179 -0.55869 -0.631054 0.30936 -0.797827 0.517464 0.418708 -0.811288 0.408039 +0.4875 -0.751004 0.445349 0.580842 -0.757908 0.296981 0.611088 -0.785348 -0.0989903 +0.600265 -0.62428 -0.499956 0.525525 -0.487335 -0.697372 0.805155 -0.416644 0.422059 +0.692885 -0.470349 0.546518 0.733151 -0.63593 0.241006 0.604624 -0.780159 -0.160568 +0.43744 -0.692901 -0.57318 0.40578 -0.735738 -0.542247 0.349721 -0.926463 0.139144 +0.394259 -0.884468 -0.249552 0.474175 -0.864404 0.167223 0.399686 -0.916072 -0.0325987 +0.326854 -0.850459 -0.412172 -0.923683 0.36132 0.127507 0.0623639 0.963622 -0.259891 +0.493436 0.750669 0.439337 0.583263 0.778785 -0.230865 0.0641474 0.654541 -0.7533 +-0.957832 0.136081 0.253059 -0.85102 0.397571 -0.343077 0.349617 0.844651 0.405379 +0.588359 0.586726 0.556405 0.509593 0.588239 0.627925 0.372624 0.830781 0.413466 +0.511319 0.625613 -0.589204 0.310777 0.81975 -0.48107 0.517314 0.648778 -0.558098 +-0.882515 0.342361 0.322423 -0.921537 -0.369864 -0.118198 -0.954961 -0.257476 0.147498 +-0.95767 0.252514 -0.138223 -0.873905 0.431882 -0.223087 0.673086 0.525074 0.520819 +0.691816 0.529035 0.491439 0.841998 0.262277 0.471435 0.746487 0.546968 -0.378923 +0.56903 0.692892 -0.442837 0.811839 0.484744 -0.325485 0.549426 0.799276 -0.243493 +0.588515 0.670115 -0.452323 -0.902371 0.378995 0.205157 -0.946756 0.29009 0.139647 +-0.941669 -0.018801 -0.336016 -0.950611 -0.226999 0.211686 -0.998504 0.0530426 -0.013288 +-0.877099 0.260924 -0.403258 0.647906 0.425545 0.631767 0.727046 -0.0341974 0.685736 +0.906459 0.0477771 0.419583 0.924602 -0.372738 0.0785986 0.813939 -0.544939 0.201356 +0.663261 -0.693946 0.280221 0.911175 0.13204 -0.390289 0.644303 0.526215 -0.554951 +0.876675 0.181096 -0.445696 0.730935 0.330062 -0.597321 -0.876707 0.446475 0.179011 +-0.954021 0.270627 0.128862 -0.988425 0.123256 0.0884506 -0.93709 0.315537 0.149327 +-0.935446 0.329543 -0.127838 -0.892574 0.371531 -0.255491 -0.962585 0.252752 -0.0977046 +-0.962488 0.114142 0.246148 -0.906085 0.331952 -0.262331 0.82991 -0.524017 0.191457 +0.796269 -0.602661 -0.0524963 0.757787 -0.649544 0.0620549 0.629185 -0.667328 0.398496 +0.657963 -0.540052 0.524814 0.536037 -0.612942 0.580488 0.848387 -0.314977 -0.425476 +0.882912 -0.23368 -0.407259 0.835364 -0.0918322 -0.541973 -0.679954 0.661239 0.3169 +-0.780151 0.611557 0.131769 -0.876801 0.48082 0.00574346 -0.930779 0.365235 0.0159116 +-0.919343 0.342826 0.193076 -0.746004 0.663263 0.0596614 -0.67718 0.563899 0.4727 +-0.744877 0.594982 -0.301918 -0.821692 0.499487 -0.274472 -0.673601 0.635188 -0.377885 +-0.908694 0.37448 -0.184498 -0.965032 0.242812 0.0987663 -0.772032 0.235855 0.590202 +-0.824846 0.508242 0.247628 -0.649387 0.735387 -0.193656 -0.471141 0.829934 -0.298725 +-0.669442 0.577465 -0.467312 -0.447324 0.666502 -0.596386 0.628188 -0.535632 0.564338 +0.668685 -0.575306 0.471045 0.683827 -0.640325 0.349805 0.766065 -0.638489 -0.0740042 +0.82902 -0.488619 -0.271989 0.800469 -0.366962 -0.473907 0.767509 -0.469264 -0.436716 +-0.772905 0.627132 0.0965596 -0.537315 0.793306 0.286285 -0.564286 0.781418 0.266397 +-0.60434 0.67496 0.423323 -0.707561 0.655682 0.263511 -0.741157 0.671322 0.00354059 +-0.765242 0.642963 -0.0316788 -0.77936 0.625121 0.0426918 -0.706213 0.610591 0.358386 +-0.690887 0.644297 -0.327958 -0.733847 0.576188 -0.359828 -0.863447 0.456201 -0.215267 +-0.691577 0.6076 -0.390567 -0.77462 -0.283655 0.565247 -0.937754 0.164165 0.306051 +-0.785779 -0.195389 0.586834 -0.576383 0.559353 -0.59574 -0.633772 0.555953 -0.537818 +-0.487576 0.611975 -0.622701 0.715013 -0.674616 0.183438 0.719081 -0.689945 -0.0830596 +0.708771 -0.63922 -0.298397 0.28577 0.920031 0.268103 0.576471 0.625003 0.526358 +0.490198 0.640285 0.591388 -0.316457 0.93158 -0.178926 -0.758961 0.650656 0.025017 +-0.251521 0.795045 -0.551943 -0.342209 0.890431 0.300043 -0.453605 0.725064 0.518194 +-0.353117 0.923202 0.15168 -0.518887 0.815294 0.257006 -0.731837 0.64088 -0.231706 +-0.793965 0.469004 0.386853 -0.56974 0.550809 -0.609923 -0.255565 0.29205 -0.921625 +0.602305 0.544722 0.58353 0.609591 0.548914 0.57192 0.540371 0.585169 0.604629 +-0.380444 0.917485 0.116121 0.043986 0.0110858 -0.998971 0.647729 0.550199 0.527 +0.636509 0.552341 0.538308 0.562803 0.580778 0.588175 0.0535683 -0.00419928 -0.998555 +0.667665 0.530741 0.522051 0.636152 0.539765 0.55133 0.569925 0.565562 0.596092 +0.0812232 0.000701376 -0.996696 0.591024 0.507905 0.626676 0.441046 0.5317 0.723031 +0.675638 0.468158 0.56951 0.00646901 -0.0955332 -0.995405 0.272754 0.608509 0.745199 +0.496344 0.238302 0.834778 0.749218 0.387669 0.537014 0.769123 0.298293 0.565218 +0.414437 -0.709937 -0.569414 0.0720738 -0.46162 -0.884145 0.765139 0.0508365 0.641855 +0.359423 -0.220941 0.906642 0.391043 -0.779567 -0.489244 0.122511 -0.792977 -0.596807 +-0.00375844 -0.919859 -0.392232 -0.0903408 -0.872596 -0.480015 0.563928 -0.391814 0.726958 +0.292585 -0.328302 0.898116 0.452851 -0.722111 -0.522955 0.329246 -0.746054 -0.578793 +0.165046 -0.879447 -0.446466 -0.0501991 -0.866147 -0.497262 0.0313236 -0.01534 0.999392 +0.471499 -0.531539 -0.703673 0.431062 -0.601105 -0.672947 0.300724 -0.687504 -0.660987 +-0.0371021 -0.069356 0.996902 0.195403 -0.598193 -0.777164 0.165689 -0.67213 -0.721656 +-0.25418 -0.455371 0.853247 -0.321278 -0.566963 -0.758507 -0.216807 -0.784092 -0.581544 +-0.279138 -0.630811 0.723989 -0.423767 -0.562979 0.709561 -0.389429 -0.343196 -0.854729 +-0.506796 -0.462622 -0.727419 -0.300692 -0.59187 -0.747846 -0.547248 -0.47076 0.69203 +-0.514378 -0.567665 0.642784 -0.550766 -0.378597 0.743855 0.531732 0.0984787 -0.841168 +-0.115793 -0.418405 -0.900849 -0.500616 -0.239944 0.831752 -0.538777 -0.289504 -0.791143 +-0.598288 0.321491 -0.733959 -0.271393 0.449147 -0.851242 -0.552031 -0.54256 0.633159 +-0.568037 -0.498994 0.654476 -0.49038 -0.258562 0.83227 -0.258637 0.868931 -0.421979 +-0.0583152 0.894022 -0.444211 -0.0504269 0.787566 -0.614163 0.0726385 0.569156 0.819014 +-0.036987 0.622382 -0.781839 0.012382 0.370949 -0.928571 -0.0379705 0.940142 0.338662 +0.0349352 0.747359 0.663502 0.0637382 0.997656 0.0248893 0.0860651 0.996144 0.017004 +0.199891 0.898384 0.391088 0.244455 0.213898 -0.945774 0.396533 0.915118 0.0729474 +0.570475 0.70992 0.413004 0.574309 0.791265 -0.209924 0.0805189 0.816784 -0.571298 +-0.0291885 0.66345 0.747651 0.0507617 0.741203 0.669359 0.370645 0.271624 -0.888168 +0.437958 0.742005 0.507564 0.320595 0.704875 0.632747 0.174597 0.929979 -0.323503 +0.0159293 0.999812 0.0110589 0.0366598 0.935351 0.351815 0.27179 0.599133 -0.753107 +0.412797 0.562749 -0.716179 0.324502 0.591266 0.73831 0.11652 0.303336 0.945733 +0.538059 0.519644 -0.663674 0.542067 0.546668 -0.638214 0.36788 0.372076 -0.852187 +0.00212592 -0.00231516 0.999995 0.0592779 0.0489958 -0.997038 0.000354025 0.0126993 0.999919 +-0.0295672 -0.0285692 -0.999154 0.00660087 -0.0361286 0.999325 -0.0123409 -0.000409442 -0.999924 +0.00708207 -0.379928 0.924989 -0.00592123 0.0144154 -0.999879 0.0093856 -0.617307 0.786667 +0.00164903 -0.334012 0.942568 0.00703121 -0.00051329 -0.999975 0.0399485 0.338621 0.940075 +0.0392317 -0.0412348 -0.998379 0.0767505 0.625802 0.776196 0.0420615 0.373862 0.92653 +0.234242 -0.665512 -0.708678 0.209447 -0.403881 -0.890512 -0.121896 -0.300612 0.945925 +-0.00105056 -0.501588 -0.865106 0.137323 -0.793934 -0.592294 -0.27825 -0.707411 -0.649727 +0.217473 -0.766183 -0.604706 -0.46592 -0.548663 0.694181 -0.299917 -0.633461 0.713286 +-0.188488 -0.469339 0.862667 0.424571 0.141353 -0.894292 -0.0718401 -0.0847281 0.993811 +0.604886 0.249235 -0.756303 0.585532 0.390611 -0.710335 0.47148 0.280967 -0.835921 +-0.00303334 0.0212688 0.999769 0.611539 0.330918 -0.718688 0.323415 0.156312 -0.933257 +0.0205363 0.0181807 0.999624 0.0442045 0.0376454 -0.998313 0.00198668 0.00153609 0.999997 +-0.0255539 -0.0237189 -0.999392 -0.000854256 -0.00359589 0.999993 -0.0182889 -0.0336378 -0.999267 +0.0130543 -0.00614846 0.999896 0.0440162 0.0761538 -0.996124 0.00526148 0.00991779 0.999937 +0.270341 0.47812 -0.835654 -0.0191745 0.0311422 0.999331 0.258461 0.604292 -0.753677 +0.185769 0.306723 -0.933494 -0.565369 0.197678 0.8008 -0.265069 0.180851 0.947118 +-0.0282706 -0.373556 -0.927177 -0.614931 -0.0692218 0.785537 -0.38251 0.0123742 0.923868 +-0.0493596 -0.390088 -0.919454 -0.0829139 -0.620901 -0.779492 -0.614033 -0.31764 0.722543 +-0.325254 -0.1299 0.936662 0.0143429 -0.0262178 -0.999553 -0.0498111 -0.0414464 0.997898 +0.00420931 0.0134486 -0.999901 0.0159888 0.0264857 0.999521 -0.0224021 -0.0216824 -0.999514 +-0.00389811 0.0166812 0.999853 0.80527 -0.592778 -0.0124407 0.812192 -0.528761 0.246485 +0.664721 -0.670472 -0.329565 0.713619 -0.698285 -0.0560884 0.662028 -0.721269 0.203689 +0.0408299 0.048437 -0.997991 0.0168952 -0.0159804 0.99973 0.531743 -0.527342 -0.662691 +0.635295 -0.616218 -0.465484 0.731772 -0.680138 0.0438357 0.666985 -0.63915 0.382908 +0.805215 -0.526776 0.27228 0.789311 -0.607018 0.092285 0.629689 -0.602879 -0.489928 +0.679584 -0.676452 -0.283862 0.699064 -0.714898 0.015195 0.57176 -0.794732 0.203693 +0.524467 -0.803491 0.281667 0.536772 -0.778538 0.325199 0.578329 -0.760532 0.295171 +0.589659 -0.76407 0.261724 0.315083 0.185847 -0.93069 0.046451 -0.0372099 0.998227 +0.592738 -0.745271 -0.305342 0.601225 -0.76753 -0.222321 0.580624 -0.653027 -0.486243 +0.542621 -0.521817 -0.658232 0.724553 -0.626657 0.286921 0.71697 -0.653103 -0.243741 +0.419989 -0.887182 0.191093 0.3947 -0.884156 0.24996 0.452197 -0.819245 0.352641 +0.475635 -0.764342 0.435377 0.60484 -0.739997 0.294232 0.575128 -0.766024 0.287115 +0.396229 -3.22024e-05 -0.918152 0.59297 -0.277665 0.755836 0.285717 -0.110512 0.951921 +0.371326 -0.476323 -0.797015 0.395279 -0.748911 -0.53187 0.320278 -0.932358 -0.167721 +0.406189 -0.871128 -0.275947 0.451716 -0.857448 0.246446 0.514012 -0.736706 0.439381 +0.398751 -0.553351 0.7313 0.415271 -0.19794 -0.887902 0.611313 -0.0429233 0.790224 +0.355729 -0.0130703 0.934498 0.513115 -0.629508 -0.583466 0.51392 -0.40739 -0.75493 +0.429332 -0.666462 0.60951 0.479001 -0.731747 0.484876 0.48081 -0.710434 0.513911 +0.454991 -0.657344 -0.600734 0.333616 -0.446377 -0.83033 0.580863 -0.185832 0.792505 +0.265374 -0.37557 0.887989 0.325702 -0.682061 -0.65476 0.31173 -0.836408 -0.450828 +0.397978 -0.804024 -0.441768 0.271798 -0.923126 0.271963 0.0947669 -0.99299 0.0706332 +0.124709 -0.991421 -0.0391412 0.244383 -0.843641 -0.478066 0.362284 -0.77781 0.513578 +0.371892 -0.910863 0.178952 0.389088 -0.9212 -0.000138457 0.387716 -0.870668 -0.302677 +0.100439 0.979595 0.174086 0.29756 0.765066 0.57108 0.379018 0.750044 0.542015 +-0.343644 0.757133 -0.555571 -0.16317 0.831837 -0.530494 0.176732 0.896453 0.40637 +0.28038 0.742523 0.608315 0.508748 0.674885 0.534515 0.207707 0.894361 0.396202 +-0.167936 0.892884 -0.417798 0.0436501 0.993679 -0.103426 0.529575 0.689025 -0.494767 +0.369893 0.860771 -0.349647 0.558114 0.733371 -0.38817 0.16007 0.70469 0.691224 +0.343044 0.655901 0.672395 0.504465 0.570888 0.647767 0.509835 0.804737 -0.304082 +0.533921 0.636713 -0.55635 0.538277 0.728459 -0.4238 0.541621 0.628835 -0.557865 +0.245384 0.642233 0.726171 0.560378 0.287802 0.776625 0.7226 0.258519 0.641106 +0.736288 0.34892 -0.579772 0.566677 0.527109 -0.633272 0.392895 0.503349 -0.769593 +0.906845 -0.0147724 0.421205 0.69568 0.073656 0.714565 0.935854 -0.267551 0.229332 +0.917269 -0.396818 -0.0339747 0.922335 -0.385881 -0.0198588 0.872525 -0.444385 0.203032 +0.946394 -0.104109 -0.305778 0.396714 0.571708 -0.71817 0.781125 0.238618 -0.576979 +0.304058 0.451477 -0.838878 -0.656775 0.713927 0.242806 -0.67506 0.699859 -0.233435 +-0.86869 -0.0546259 0.492335 -0.938042 0.334423 0.0907626 -0.84545 0.504461 -0.175308 +0.877054 -0.206056 0.433955 0.739148 -0.274268 0.615173 0.987448 -0.139474 0.0741179 +0.934764 -0.313233 -0.167634 0.911426 -0.408699 -0.0476232 0.916157 -0.365554 0.1644 +0.827556 -0.400896 0.392979 0.885223 -0.392577 -0.249526 0.987329 -0.132994 -0.0865658 +0.919573 -0.0309929 -0.391695 0.5843 0.270164 -0.765248 -0.656714 0.686341 0.31251 +-0.710481 0.699977 0.0724489 -0.705359 0.708294 -0.0280793 -0.696445 0.717447 -0.0152956 +-0.678735 0.705527 0.203838 -0.644328 0.612728 0.457609 -0.739581 0.655572 0.152461 +-0.698241 0.640664 -0.319388 -0.715699 0.591067 -0.372041 -0.666937 0.647647 -0.36844 +-0.670571 0.65786 -0.342862 -0.90712 0.0926654 0.410545 -0.940828 0.3039 0.14996 +-0.779971 0.507946 -0.365563 0.772497 -0.435713 0.461955 0.751855 -0.580285 0.313024 +0.905147 -0.421467 0.0554516 0.832646 -0.516578 -0.199618 0.817955 -0.476559 -0.322245 +0.82877 -0.555597 0.0667187 0.778805 -0.543443 0.31326 0.76156 -0.53546 0.365115 +0.834441 -0.465648 0.294753 0.78004 -0.585735 0.220118 0.677395 -0.735536 0.011052 +0.734738 -0.666633 -0.12554 0.862414 -0.505442 -0.0277739 0.915663 -0.401661 -0.0151795 +0.914225 -0.35045 -0.203415 0.825949 -0.135153 -0.547304 0.316293 0.909013 0.271393 +0.628923 0.650232 0.42621 0.548076 0.640586 0.537831 0.0282076 0.997868 -0.0588506 +-0.0329985 0.914203 -0.40391 -0.0762584 0.812241 -0.578316 -0.397713 0.864823 0.30644 +-0.188839 0.790281 0.58292 -0.349642 0.936756 0.0154836 -0.43755 0.898683 0.0303142 +-0.649255 0.750664 0.122354 -0.734231 0.678851 0.00810123 -0.728352 0.684741 -0.0251546 +-0.71499 0.697109 0.053179 -0.664177 0.659105 0.352774 -0.69429 0.660151 -0.28664 +-0.647054 0.656469 -0.387775 -0.552234 0.747799 -0.368557 -0.611336 0.731813 -0.301194 +-0.726905 0.681131 0.0875758 -0.825562 0.487406 0.284398 -0.740335 0.670067 0.0539812 +-0.672103 0.56819 0.474803 -0.659587 0.650522 -0.37652 -0.641869 0.612066 -0.46193 +-0.542995 0.580957 -0.606337 0.785012 -0.606069 -0.12821 0.808648 -0.565005 0.163884 +0.79157 -0.611057 0.00510971 0.777428 -0.628636 -0.020547 0.805115 -0.593117 0.00130049 +0.826114 -0.56327 0.0161782 0.829302 -0.557036 -0.0443819 0.828517 -0.476322 -0.294411 +0.742199 0.617157 0.261262 0.702593 0.547441 0.454612 0.854305 0.4539 0.253256 +0.72805 0.540894 0.421162 0.743186 0.627211 0.232983 -0.190262 0.981684 0.00987499 +-0.432349 0.884333 -0.17615 -0.549819 0.835124 -0.0163326 -0.631409 0.759726 0.155364 +-0.568412 0.566224 -0.596907 -0.287215 0.282517 -0.915255 0.732817 0.41969 0.535574 +0.784328 0.164807 0.598054 0.841368 0.320126 0.435452 0.826396 0.410336 0.385607 +-0.0301104 0.0392688 -0.998775 0.758851 0.357339 0.544475 0.577021 0.225962 0.784849 +-0.0642941 -0.0402654 -0.997118 0.294774 0.469431 0.832312 -0.347629 -0.369582 -0.861721 +0.327006 0.552634 0.766592 0.150966 0.29919 0.942176 -0.427367 -0.590504 -0.68459 +-0.550511 -0.487137 -0.677964 -0.425628 -0.579435 -0.69505 0.0401978 -0.0529788 0.997786 +-0.290977 -0.758774 -0.582748 -0.197475 -0.89458 -0.400912 -0.272787 -0.795976 -0.540379 +-0.205229 -0.881588 -0.425069 0.417176 -0.544466 0.727682 0.164484 -0.355324 0.920158 +0.0514409 -0.744355 -0.665799 -0.157035 -0.746638 -0.64643 0.329108 -0.568931 0.753662 +0.155548 -0.2811 0.946989 0.0652867 -0.70516 -0.706036 -0.0289513 -0.689763 -0.723456 +-0.104652 -0.337998 0.93531 -0.142489 -0.780488 -0.608716 -0.204942 -0.797675 -0.567198 +-0.382221 -0.615021 0.689678 -0.313197 -0.570185 0.759472 -0.242755 -0.693785 0.678035 +-0.319131 -0.469328 -0.823339 -0.25782 -0.790255 -0.555901 -0.443733 -0.661964 -0.604074 +-0.328914 -0.826054 -0.457657 -0.358954 -0.671087 0.648686 -0.461839 -0.605687 0.647957 +-0.306959 -0.686344 0.659324 -0.368603 -0.455218 -0.810499 -0.439954 -0.561162 0.701097 +-0.562013 -0.227674 0.795177 -0.363154 -0.425315 0.828991 -0.76324 0.152119 -0.627952 +-0.570405 -0.751358 -0.331812 -0.832605 -0.191378 -0.519753 -0.499933 -0.767645 -0.400983 +-0.572813 -0.598969 -0.559573 -0.576947 -0.258178 0.774904 -0.52563 -0.407804 0.746598 +-0.47574 0.72624 -0.496232 -0.25353 0.60996 -0.75078 -0.594774 -0.496921 0.631912 +-0.447223 0.387798 0.805981 -0.714655 -0.0103653 0.6994 -0.061006 0.853459 -0.517577 +-0.143576 0.913176 -0.381438 -0.251097 0.765235 -0.592762 0.0672288 0.855964 0.512647 +0.244774 0.803372 0.542844 0.347252 0.720256 0.600539 -0.487807 0.775368 0.40106 +-0.269234 0.62604 -0.731838 -0.0490656 0.538007 -0.841511 -0.29586 0.869803 0.394854 +-0.170994 0.755743 0.63215 -0.1124 0.987949 0.106412 0.0643687 0.968861 0.239094 +0.540301 0.229023 -0.809706 -0.0379963 0.611652 -0.790214 -0.0164161 0.531918 -0.846637 +0.184233 0.910873 0.369281 0.329455 0.69355 0.640663 0.471709 0.680395 0.560851 +-0.163044 0.885354 -0.43539 -0.311793 0.741822 -0.593705 -0.199669 0.767756 0.608838 +-0.0222793 0.764001 0.64483 0.57876 0.312552 -0.753225 0.39234 0.423925 -0.816307 +0.187578 0.61397 0.766717 0.244824 0.435399 0.866308 0.416878 0.607495 0.676138 +-0.113909 0.943766 -0.310373 -0.0152115 0.999845 0.00886207 -0.0339223 0.925369 0.377546 +0.492837 0.577538 -0.650815 0.399523 0.612632 -0.681956 0.213981 0.466757 -0.858109 +0.0297634 0.064812 0.997454 0.0433126 0.07859 -0.995966 -0.00822853 -0.022531 0.999712 +-0.0282764 -0.0396108 -0.998815 -0.000575156 0.0198962 0.999802 -0.0128237 -0.0112705 -0.999854 +0.00179665 -0.0355285 0.999367 -0.00203139 0.00778384 -0.999968 -0.00331223 -0.380667 0.924706 +-0.0051162 0.00964751 -0.99994 0.00187495 -0.619936 0.78465 0.0415135 -0.2855 0.957479 +-0.000835558 0.024731 -0.999694 0.202774 0.457775 0.865635 0.0218865 -0.0363125 -0.999101 +0.313442 0.618188 0.720831 0.133438 0.296364 0.945707 0.0221306 -0.41231 -0.910775 +-0.033564 -0.380758 0.924066 0.552061 -0.203652 -0.808551 0.379184 -0.467413 -0.798589 +0.0683828 -0.727326 -0.682876 -0.060605 -0.620471 0.781884 -0.0284255 -0.384489 0.922692 +0.594199 0.323499 -0.736394 0.346843 0.105928 -0.931922 0.0223474 -0.0246224 0.999447 +0.0754115 0.0519865 -0.995796 0.0186867 0.026083 0.999485 0.00151419 -0.00506403 -0.999986 +-0.000178317 -0.00099195 1 -0.0206684 -0.014241 -0.999685 0.00162326 0.0017552 0.999997 +-0.0089852 -0.0155484 -0.999839 0.019207 0.0124026 0.999739 0.00919792 0.00544598 -0.999943 +-0.00824748 -0.0150534 0.999853 0.124942 0.324486 -0.937603 -0.0285286 -0.0131023 0.999507 +0.375002 0.592137 -0.713265 0.309269 0.602258 -0.735961 0.206254 0.501071 -0.840468 +-0.0136795 0.0190912 0.999724 -0.00156492 -0.00131615 -0.999998 -0.0322411 -0.0104265 0.999426 +-0.207667 -0.499597 -0.840998 -0.0372857 -0.00739244 0.999277 -0.120271 -0.320778 -0.939487 +-0.311466 -0.598477 -0.738115 -0.00654256 -0.00454304 0.999968 -0.00917273 -0.00451079 -0.999948 +0.020037 0.0132439 0.999712 -0.0160849 -0.0132248 -0.999783 0.00373913 0.00677404 0.99997 +0.0589799 0.0541166 -0.996791 -0.00712608 0.00578867 0.999958 0.381911 0.37251 -0.845801 +-0.0126191 0.00603234 0.999902 0.702422 -0.692612 0.163989 0.668912 -0.657361 -0.347035 +0.707955 -0.703932 -0.0572631 0.686089 -0.726568 0.0371712 0.618539 -0.763612 0.185219 +0.592287 0.183839 -0.784474 0.560689 0.448621 -0.695965 0.452257 0.301008 -0.839558 +-0.00454068 -0.0212961 0.999763 0.473924 -0.689436 -0.54779 0.649505 -0.670189 -0.359151 +0.738922 -0.652531 0.167921 0.702409 -0.611065 0.364994 0.664708 -0.67362 0.32311 +0.645376 -0.759134 -0.0848856 0.566626 -0.688254 -0.453035 0.69158 -0.660019 -0.293415 +0.713752 -0.700194 -0.0169088 0.703671 -0.709094 0.0450849 0.57228 -0.810871 0.122406 +0.458554 -0.885446 0.0755861 0.383576 -0.887476 0.255452 0.501283 -0.780335 0.373889 +0.428532 -0.779848 0.456287 0.59135 -0.0684709 -0.803503 0.370471 0.0220051 -0.928583 +0.0740362 0.0404794 0.996434 0.584387 -0.243266 -0.774153 0.538769 -0.481826 -0.691066 +0.458686 -0.855046 -0.241877 0.366812 -0.914478 -0.170821 0.34784 -0.856647 -0.381004 +0.607963 -0.793256 -0.0335552 0.5011 -0.816189 -0.287636 0.325065 -0.896581 0.300792 +0.316729 -0.851698 0.417485 0.354056 -0.555428 0.752425 0.519218 -0.598999 0.609601 +0.604267 -0.297004 -0.739358 0.376367 -0.152411 -0.913848 0.317409 -0.0459865 0.947173 +0.563998 -0.427461 -0.706529 0.433843 -0.324955 -0.840348 0.57436 -0.511516 0.63911 +0.52686 -0.685552 0.502432 0.521405 -0.673376 0.524119 0.434042 -0.586862 -0.68352 +0.522843 -0.415815 -0.744133 0.244074 -0.397362 0.884608 0.455576 -0.6271 -0.63182 +0.521163 -0.672116 -0.525974 0.378415 -0.840181 -0.388455 0.314816 -0.745698 -0.587218 +0.312405 -0.910199 0.27192 0.141221 -0.984901 0.100128 0.278739 -0.951361 0.131214 +0.336111 -0.941774 0.00952628 0.363728 -0.875004 -0.319483 0.374121 -0.749343 0.546369 +0.494062 -0.754035 0.432821 0.559866 -0.77971 0.28036 0.56567 -0.821114 0.0760822 +0.563377 -0.80749 -0.174831 0.549665 -0.673728 -0.493922 0.538326 -0.517764 -0.664925 +0.490506 -0.840309 0.230835 0.587347 -0.80796 -0.0471619 0.455508 -0.829546 -0.323058 +0.155087 0.987862 0.00877956 0.250589 0.802867 0.540935 -0.0640735 0.80938 -0.583779 +0.0587101 0.874499 0.481461 0.0487886 0.779717 0.624229 0.215599 0.85812 0.46599 +-0.0752371 0.928745 -0.363003 -0.0589425 0.997794 -0.0305351 0.028948 0.999531 -0.00996539 +0.602808 0.775578 -0.187353 0.302485 0.937488 -0.172101 0.574574 0.716669 -0.395285 +0.389699 0.868052 -0.307604 0.329914 0.722461 -0.607624 -0.00238786 0.692292 0.721614 +0.00987504 0.725113 0.688559 0.524145 0.557047 -0.644182 0.459791 0.697613 -0.54948 +0.524248 0.430475 -0.734749 -0.10139 0.684468 0.721958 0.199251 0.570084 0.797059 +0.495133 0.334778 -0.801728 0.072302 0.645237 0.760554 0.38631 0.253402 0.886877 +0.56529 0.334965 0.753821 0.243293 0.578931 -0.778234 0.430582 0.522926 -0.735627 +0.730504 -0.0921617 0.676661 0.386803 -0.00924134 0.922116 0.754792 0.331534 -0.566016 +0.585067 0.522936 -0.619867 0.682977 0.53652 -0.495669 -0.811262 0.582308 0.0526441 +-0.808118 0.544773 -0.22398 0.794399 -0.22877 0.562667 0.386024 -0.206171 0.899155 +0.804559 -0.410027 0.429607 0.861152 -0.460928 -0.214389 0.920252 -0.39006 -0.0314516 +0.855591 -0.466802 0.223739 0.942458 -0.117462 -0.31301 0.702341 0.4773 -0.528111 +0.852512 0.206745 -0.480083 0.740632 0.321706 -0.589889 0.373681 0.902716 0.213229 +0.642489 0.666071 0.37889 0.611519 0.720652 0.326658 0.604757 0.72936 0.319847 +0.261908 0.953046 0.15201 0.00808959 0.99985 0.0153169 -0.0454479 0.998148 -0.0404371 +-0.168637 0.887304 -0.429247 -0.265373 0.770153 -0.580037 -0.503438 0.762169 0.407 +-0.595922 0.800718 0.0610528 -0.551535 0.833513 -0.0326267 -0.572088 0.820188 0.00280463 +-0.612314 0.790601 0.00468835 -0.627183 0.77871 -0.0158459 -0.616638 0.787229 0.00522925 +-0.591136 0.780699 0.202653 -0.624955 0.680616 0.382352 -0.761366 0.64759 0.030789 +-0.810829 0.527514 -0.253545 -0.743299 0.593849 -0.307979 -0.785028 0.578257 -0.222151 +-0.675691 0.701132 -0.227719 -0.676586 0.682358 0.276803 -0.702234 0.672903 -0.232527 +0.687079 -0.446786 0.572979 0.575949 -0.703305 0.416706 0.823137 -0.500002 0.269155 +0.794504 -0.603081 0.0711044 0.859947 -0.508521 -0.04356 0.908936 -0.414731 -0.0428225 +0.917275 -0.366008 0.156987 0.908008 -0.372477 0.191785 0.921363 -0.388481 0.0131239 +0.924638 -0.380548 -0.0151145 0.922309 -0.386434 0.003729 0.91803 -0.39638 0.0101783 +0.912757 -0.408478 -0.00442586 0.940851 -0.325972 -0.0924223 0.990193 -0.110914 -0.0849446 +0.926646 -0.0258689 -0.375044 0.801417 0.0720658 -0.593748 0.729307 0.619394 0.290624 +0.787778 0.461333 0.40814 0.831188 0.474205 0.290268 0.706439 0.573554 0.414705 +0.71799 0.637382 0.279704 -0.129438 0.960621 -0.245874 -0.314304 0.939914 0.133317 +-0.386495 0.922256 0.00808574 -0.380915 0.924574 -0.00818275 -0.3732 0.927741 0.00423288 +-0.360223 0.932862 -0.0029153 -0.350553 0.936513 -0.00750081 -0.352475 0.934726 0.0452625 +-0.490385 0.85287 0.179265 -0.7355 0.627029 -0.256658 -0.721775 0.68718 -0.0826121 +-0.557567 0.82216 -0.11477 -0.420817 0.902525 -0.0914404 -0.408905 0.889241 -0.205056 +-0.4949 0.794392 -0.352159 -0.572365 0.811616 -0.116956 -0.654394 0.694231 0.299685 +-0.752681 0.626225 0.203258 -0.673911 0.66273 -0.326547 -0.686254 0.586028 -0.430845 +-0.544336 0.575574 -0.610256 0.547013 -0.812752 0.200528 0.669348 -0.732796 0.122403 +0.784656 -0.619758 -0.0146445 0.821536 -0.564762 0.0782424 0.791761 -0.52879 0.305772 +0.746886 -0.550265 0.373322 0.858691 -0.439233 0.264054 0.922342 -0.384017 0.0426229 +0.924284 -0.381298 -0.0176205 0.925626 -0.37843 -0.00267692 0.924439 -0.381326 0.00160994 +0.921694 -0.387909 0.0027148 0.925171 -0.379419 0.0100193 0.934755 -0.355275 -0.00341781 +0.917193 -0.320005 -0.23739 0.825675 -0.161797 -0.540446 0.78924 0.286563 0.543123 +0.794094 -0.177604 0.581267 0.883584 0.0807516 0.461257 0.866805 0.242849 0.435516 +-0.516123 0.847142 -0.126361 -0.583147 0.572811 -0.576044 -0.273171 0.258744 -0.926514 +0.77299 -0.61196 0.167309 0.770016 -0.638001 -0.00540037 0.771784 -0.635827 -0.00858246 +0.774555 -0.632506 0.000973885 0.774171 -0.632976 -0.000494012 0.773061 -0.634305 0.00590135 +0.767635 -0.640611 0.0188236 0.749567 -0.658293 -0.0692756 0.752234 -0.561776 -0.344313 +0.760458 -0.357273 0.542272 0.764802 -0.168009 0.621974 0.776971 -0.133447 0.61523 +-0.253258 -0.363141 -0.896654 0.585876 0.149269 0.796535 0.586079 0.178775 0.790286 +-0.514754 -0.418412 -0.748304 -0.386955 -0.70735 -0.591542 -0.481703 -0.556658 -0.676827 +0.251864 -0.0898019 0.963587 -0.393912 -0.54049 -0.743441 0.0791031 -0.0740524 0.994112 +-0.24197 -0.742844 -0.624206 -0.236941 -0.89191 -0.38517 -0.304967 -0.826246 -0.473616 +0.299881 -0.0958072 0.949154 -0.254089 -0.673042 -0.694589 -0.301629 -0.644629 -0.702477 +0.352771 -0.496281 0.793258 0.204303 -0.472152 0.857515 -0.25569 -0.726249 -0.638111 +-0.301877 -0.667808 -0.68037 0.0226724 -0.509566 0.860133 -0.452157 -0.739051 -0.499356 +-0.39313 -0.824076 -0.407857 -0.414266 -0.75503 -0.508245 -0.311384 -0.840719 -0.44298 +-0.0778385 -0.61339 0.785935 -0.0760298 -0.734643 0.67418 -0.413195 -0.537677 -0.734965 +-0.366865 -0.747366 -0.553944 -0.194235 -0.780863 0.59374 -0.321621 -0.683574 0.655199 +-0.608307 -0.683102 -0.404146 -0.697965 -0.482625 -0.529073 -0.465881 -0.584385 -0.664417 +-0.338758 -0.674721 0.65574 -0.477731 -0.495418 0.725488 -0.70087 -0.000880064 -0.713288 +-0.53461 -0.724861 -0.434476 -0.663225 -0.204773 -0.719862 -0.545958 -0.808339 -0.220266 +-0.521179 -0.594012 0.612798 -0.583545 -0.530436 0.614908 -0.630177 -0.585052 0.510482 +-0.426759 0.755629 -0.496892 -0.268967 0.835059 -0.47993 -0.39455 0.689695 -0.607166 +-0.631927 -0.486901 0.602989 -0.724589 0.125077 0.677736 -0.759434 -0.296817 0.57893 +0.0338044 -0.0597533 -0.997641 -0.456655 -0.760987 -0.460831 -0.998808 0.0431071 0.0228926 +-0.508548 0.478175 0.71605 -0.330386 0.772495 -0.542307 -0.565912 0.672287 -0.477257 +-0.466483 0.698456 -0.542728 -0.313551 0.907607 0.279171 -0.276684 0.740377 0.612608 +-0.3853 0.921199 0.0541828 -0.837595 0.503788 0.211264 -0.795262 -0.582928 0.166596 +-0.99292 0.069439 0.0963783 -0.606727 0.686167 -0.40132 -0.433435 0.457796 0.776246 +0.0283475 0.999033 0.03361 -0.430487 0.717363 -0.547788 -0.253953 0.709868 -0.656959 +-0.392005 0.632319 -0.66821 -0.418815 0.72481 0.547032 -0.526 0.738889 0.421149 +-0.462056 0.856884 0.228593 -0.36671 0.87803 0.307551 0.0199845 0.997338 0.0701316 +-0.351403 0.642177 -0.681267 -0.295219 0.669971 -0.681164 -0.373154 0.642431 -0.669357 +-0.167725 0.982783 -0.0775028 0.0475203 0.789206 0.612288 -0.166521 0.784232 -0.597705 +-0.393412 0.70844 -0.585952 -0.325883 0.811893 0.484387 -0.15815 0.778401 0.60752 +-0.416169 0.821902 0.388948 -0.331131 0.663144 -0.671262 -0.0977242 0.657435 -0.747148 +0.0128813 0.619673 0.784754 0.0353846 0.331814 0.942681 -0.20924 0.894279 -0.395581 +-0.0352097 0.997611 -0.0594433 -0.0181232 0.999775 0.0110194 -0.163113 0.930702 0.327395 +0.093062 0.617369 -0.78115 -0.00745078 0.33204 -0.943236 -0.00656732 0.0295856 0.999541 +-0.0333528 0.0158909 -0.999317 -0.00240705 -0.0160843 0.999868 0.00376446 -0.0142336 -0.999892 +0.000618658 0.0235623 0.999722 0.00385467 -0.0124775 -0.999915 -0.000773071 -0.0385749 0.999255 +-0.00980933 0.00735715 -0.999925 -0.0117309 -0.385516 0.922627 -0.00576266 0.0134025 -0.999894 +0.0064528 -0.589489 0.80775 0.161154 0.0268265 0.986565 0.00511978 0.0347884 -0.999382 +0.161419 0.600978 0.782796 0.270177 0.502777 0.821109 0.387078 0.587671 0.710502 +-0.0467307 -0.0800176 -0.995697 -0.00221996 0.00199108 0.999996 -0.209664 -0.500978 -0.83968 +-0.272782 -0.497247 0.823611 -0.0487285 -0.328481 -0.943253 -0.291741 -0.59341 -0.750167 +-0.167613 -0.60886 0.775368 -0.157461 -0.319031 0.934572 -0.00360353 0.00255064 -0.99999 +-0.00637956 -0.00052615 0.99998 -0.024444 -0.00385536 -0.999694 0.00144648 0.00957128 0.999953 +-0.0124137 -0.00901172 -0.999882 0.00157301 8.83345e-05 0.999999 0.000473907 -0.00113041 -0.999999 +0.0190608 0.0196725 0.999625 0.000230362 -0.0152714 -0.999883 -0.0494584 -0.0467257 0.997683 +-0.0018994 0.0334413 -0.999439 -0.335444 -0.168518 0.926865 0.000416937 0.381695 -0.924288 +-0.289157 0.122667 0.94939 0.0709547 0.619112 -0.782091 -0.0101296 0.362787 -0.931817 +-0.00900932 0.0261811 0.999617 -0.167804 -0.298484 -0.939547 0.00866881 0.000182022 0.999963 +-0.256542 -0.470469 -0.844301 -0.16276 -0.612187 -0.77378 -0.382648 -0.586055 -0.714227 +0.0186952 -0.00426958 0.999816 -0.037971 -0.0791926 -0.996136 0.0110502 0.0046881 0.999928 +-0.00521575 0.00935426 -0.999943 0.00277802 0.00110794 0.999996 0.0386035 0.0469661 -0.99815 +-0.000982149 -0.00145562 0.999999 0.346243 0.39407 -0.851367 -0.00180935 0.00221285 0.999996 +0.520295 0.523646 -0.674602 0.526742 0.526497 -0.667341 0.360118 0.378782 -0.852549 +-0.010659 -0.0061868 0.999924 0.0931265 0.0524818 -0.99427 -0.00801817 0.0171901 0.99982 +0.826456 -0.548097 0.12869 0.628528 -0.703372 -0.331995 0.709729 -0.700904 -0.0708443 +0.721794 -0.692007 0.0118589 0.699158 -0.714939 0.00628055 0.673688 -0.738998 -0.00514975 +0.613944 -0.772858 0.160508 0.0456462 -0.0202301 -0.998753 0.17213 0.306409 0.936208 +0.568875 -0.713727 -0.408626 0.554602 -0.808992 -0.194804 0.533294 -0.793368 -0.293539 +0.574495 -0.8151 -0.0746162 0.777009 -0.606129 0.169895 0.606562 -0.742789 0.283455 +0.7601 -0.644663 0.0815957 0.596074 -0.729982 -0.334398 0.532486 -0.808754 -0.24975 +0.647085 -0.749839 -0.137925 0.740205 -0.672381 -0.000676304 0.742977 -0.669297 0.00516585 +0.749114 -0.662312 -0.0130602 0.745771 -0.665723 0.0252463 0.607681 -0.782364 0.136497 +0.45267 -0.889157 0.0669978 0.279569 -0.8639 0.418949 0.351058 -0.586046 0.73028 +0.0561913 -0.051335 -0.997099 0.165434 0.551606 0.817534 0.410266 -0.0589984 0.910056 +0.518224 0.233565 0.822734 0.320587 -0.537234 -0.780131 0.31137 -0.860832 -0.402513 +0.45253 -0.863586 -0.22234 0.450269 -0.880878 0.145986 0.4801 -0.849142 -0.220142 +0.448559 -0.552486 0.702534 0.221542 -0.49848 0.838115 0.390456 -0.635631 -0.665971 +0.199834 -0.366116 -0.908859 0.55597 -0.368983 0.744814 0.364801 -0.4515 0.81429 +0.492528 -0.72246 -0.485251 0.51605 -0.712859 -0.474895 0.400143 -0.700484 -0.590937 +0.299661 -0.822587 0.483274 0.379445 -0.897415 0.225093 0.376001 -0.903849 0.204159 +0.549262 -0.776267 0.309386 0.511448 -0.819158 0.259618 0.567603 -0.818564 0.0882103 +0.514238 -0.834457 -0.198092 0.242194 -0.77062 -0.58948 0.10496 -0.764835 -0.635618 +0.38075 -0.624912 0.681553 0.228251 -0.54392 0.807498 0.478939 -0.664057 -0.574148 +0.411747 -0.650056 -0.638664 0.422687 -0.742279 -0.51996 0.45898 -0.872895 0.165507 +0.40738 -0.91153 -0.0561707 0.0447257 -0.883219 -0.466822 0.326145 -0.806162 0.493693 +0.484842 -0.780075 0.39549 0.490552 -0.767645 0.412407 0.652018 -0.74185 0.156624 +0.582639 -0.734291 -0.34835 0.405542 -0.793595 -0.453589 0.259185 -0.822283 -0.506631 +0.418718 -0.90509 -0.0740801 0.186192 0.900304 0.393428 0.304318 0.725978 0.616722 +0.361524 0.758965 0.541547 -0.154653 0.893989 -0.420554 -0.308455 0.739383 -0.598471 +0.126178 0.750898 0.648253 -0.00784878 0.75398 0.65685 -0.0858233 0.952729 -0.291448 +0.0389246 0.998767 -0.0308217 0.0175045 0.998926 0.042891 0.0295878 0.999152 -0.0286369 +0.161787 0.940408 -0.299095 0.369349 0.784974 -0.497391 -0.0745642 0.654608 0.752283 +-0.140996 0.624027 0.768577 0.369043 0.809819 -0.456072 0.746205 0.382226 -0.545052 +0.595066 0.67724 -0.432716 0.674327 0.570557 -0.468773 -0.315423 0.625582 0.713552 +-0.286065 0.703202 0.650902 0.722834 0.395732 -0.566487 0.718736 0.489841 -0.493432 +0.845124 0.397138 -0.357836 0.749463 0.516014 -0.41477 -0.343838 0.671709 0.656188 +-0.121995 0.361525 0.924347 0.826934 0.401875 -0.393289 0.769505 0.562445 -0.302519 +0.923166 0.333819 -0.190603 0.869011 0.289621 -0.401173 0.750581 0.55804 -0.353863 +0.713867 0.372022 -0.59329 0.0216936 0.0506195 0.998482 0.850232 0.359543 -0.384492 +0.923808 0.208124 -0.321347 0.783992 0.216301 -0.581868 0.0292603 -0.0229993 0.999307 +0.926142 0.149445 -0.346305 0.787126 0.488747 -0.376244 0.942422 0.300438 -0.146897 +0.938003 0.346603 0.00420048 0.794521 0.604098 -0.0616602 0.928775 0.368921 0.0356961 +0.789889 0.61285 0.022122 0.872697 0.487892 0.0190206 0.78382 0.620979 -0.00338051 +0.731257 0.660529 0.170188 0.675306 0.728014 -0.118142 0.57685 0.755539 0.31049 +0.303756 0.949164 0.0825781 0.0157739 0.998091 0.0597152 0.0317736 0.999038 0.0302086 +0.00165882 0.999031 -0.0439723 -0.166407 0.940709 -0.295592 -0.159944 0.803305 -0.573689 +-0.350504 0.759724 -0.547691 -0.722943 0.690572 -0.0215267 0.44651 -0.561307 0.696823 +0.121872 -0.422211 0.898268 0.925596 -0.00223498 -0.378507 0.997961 0.0233816 -0.059393 +0.998567 0.0451981 0.0286549 0.993943 0.0935662 0.0576424 0.885817 0.372511 0.2767 +-0.190394 0.902752 -0.385733 -0.00861173 0.999102 -0.0414752 -0.0353862 0.999334 -0.00896133 +-0.100704 0.994834 -0.012796 -0.309642 0.943825 -0.1154 -0.462739 0.886314 -0.0178816 +-0.497162 0.867534 0.0146472 -0.481981 0.876172 0.00404118 -0.469522 0.882908 0.00485309 +-0.489312 0.872045 -0.0105567 -0.616575 0.760248 -0.204593 -0.683955 0.6161 -0.390673 +-0.663575 0.67476 -0.323059 -0.706449 0.702977 -0.0821794 -0.656099 0.754153 0.0280657 +-0.625315 0.780336 0.0075872 -0.561802 0.807578 -0.179434 -0.811991 0.511563 0.281024 +-0.786947 0.611725 -0.0806635 0.291819 -0.828074 0.47868 0.431572 -0.830902 0.351209 +0.551721 -0.717813 0.424675 0.628707 -0.714741 0.306387 0.68355 -0.725349 0.0814092 +0.762461 -0.635745 0.12034 0.815748 -0.523187 0.246639 0.940535 -0.235746 -0.244575 +0.997914 0.0259735 -0.0590993 0.999788 -0.00703837 0.0193666 0.957666 -0.00178375 0.287876 +-0.757916 0.624835 -0.18747 -0.767404 0.640795 0.0217348 -0.771089 0.63555 0.0387017 +-0.757574 0.65161 -0.0385647 -0.621844 0.7512 -0.221377 -0.613632 0.770468 -0.172728 +-0.797707 0.597002 0.0851608 -0.731338 0.631579 -0.257396 -0.712952 0.673009 -0.196871 +-0.786214 0.577843 0.219008 -0.746905 0.550417 -0.373061 -0.535661 0.607907 -0.586103 +0.69099 -0.659568 0.295809 0.717536 -0.6903 0.0928909 0.718849 -0.631033 0.291639 +0.71844 -0.584866 0.376531 0.792033 -0.540692 0.283436 0.771146 -0.598606 0.216803 +0.699703 -0.714062 0.0230463 0.685005 -0.728373 -0.0154901 0.688194 -0.725526 0.000556581 +0.689323 -0.724454 0.000773045 0.690681 -0.723159 0.000984699 0.688201 -0.725484 0.00715589 +0.672593 -0.739673 -0.022428 0.73683 -0.666535 -0.113188 0.902901 -0.407961 0.13542 +0.995266 -0.0918941 0.0316546 0.999499 -0.0185311 -0.0256704 0.99558 -0.0531744 0.0774162 +0.85069 -0.270603 0.450667 -0.796726 0.593844 0.112149 -0.571269 0.428835 -0.699823 +-0.413456 -0.266693 -0.870591 0.738683 -0.609895 0.28701 0.751643 -0.658359 0.0399646 +0.721254 -0.692223 -0.0249082 0.724483 -0.689289 -0.00227778 0.726732 -0.686915 0.00297371 +0.728656 -0.684877 0.00220721 0.729159 -0.684342 0.00190416 0.71996 -0.693971 -0.00789712 +0.739623 -0.672778 -0.0180883 0.789186 -0.609064 0.0789079 0.780168 -0.544112 0.308675 +0.755339 -0.541773 0.368707 0.864805 -0.428979 0.260939 0.935059 -0.352083 0.0412658 +0.945997 -0.308055 0.100953 0.920614 -0.0807643 0.38203 -0.731671 -0.213568 -0.647337 +-0.59828 -0.652622 -0.464915 -0.593056 -0.425214 -0.683724 -0.562894 -0.695855 -0.446022 +0.779336 -0.605502 0.161254 0.78668 -0.614306 0.0613442 0.762554 -0.598488 0.24561 +0.758357 -0.515374 0.399104 0.748699 -0.375003 0.546647 -0.576126 -0.290992 -0.763808 +-0.651149 -0.443107 -0.616167 0.588634 -0.33984 0.733498 0.462375 0.0538838 0.885045 +-0.633929 -0.563143 -0.530097 -0.602753 -0.692531 -0.396346 -0.420128 -0.676034 -0.605368 +-0.553367 -0.778819 -0.295341 0.677596 0.110941 0.727018 0.451731 -0.239529 0.859398 +0.588596 -0.0106927 0.808357 -0.650491 -0.525051 -0.548802 -0.709726 -0.493775 -0.502469 +-0.492261 -0.610782 -0.620181 0.412047 -0.564021 0.71561 0.213303 -0.713618 0.667272 +0.364939 -0.648159 0.668363 -0.580038 -0.688334 -0.435605 -0.715305 -0.498153 -0.490084 +-0.465863 -0.637023 -0.614144 0.290163 -0.645446 0.706544 0.126916 -0.65091 0.74847 +-0.7515 -0.550504 -0.363583 -0.582046 -0.807845 -0.0927892 -0.868315 -0.491797 0.064533 +-0.747358 -0.652822 0.12361 -0.695032 -0.63154 -0.343639 -0.620672 -0.673462 -0.401517 +-0.496015 -0.684318 -0.534489 -0.551075 -0.789188 -0.271104 -0.110512 -0.730152 0.674289 +-0.368222 -0.75627 0.540803 -0.784792 -0.209211 -0.583379 -0.741982 -0.370824 -0.558527 +-0.906096 -0.242018 -0.347011 -0.983576 -0.167943 -0.0661237 -0.797427 -0.58767 0.136946 +-0.834064 -0.502746 -0.227118 -0.302619 -0.847607 0.435872 -0.557893 -0.696208 0.451719 +-0.438968 -0.694169 0.57047 -0.572923 -0.686513 0.447725 -0.752196 -0.464552 -0.467325 +-0.772574 -0.372581 -0.514113 -0.899413 -0.390043 -0.197289 -0.641051 -0.623259 0.447886 +-0.75475 -0.520242 0.399626 -0.725132 -0.605594 0.327781 -0.8131 -0.508447 0.283462 +-0.659557 -0.644638 0.386557 -0.752576 -0.560313 -0.345947 -0.779069 -0.557571 -0.286646 +-0.884016 -0.428252 -0.18739 -0.818571 -0.526464 -0.229733 -0.975593 -0.0821505 -0.203642 +-0.831239 -0.398641 0.387462 -0.719128 -0.632576 0.287579 -0.933263 -0.336239 0.126349 +-0.731513 -0.67943 0.057131 -0.912933 -0.405297 0.0478313 -0.761982 -0.647464 0.0131739 +-0.999841 0.00353109 0.0174822 -0.775705 -0.62997 -0.037683 -0.943554 -0.314473 0.103983 +-0.795443 -0.605296 0.0297983 -0.99739 -0.010114 0.0714917 -0.818342 -0.57471 -0.00492351 +-0.997823 -0.0480445 -0.0451776 -0.811501 -0.562776 -0.157322 -0.988356 -0.0485977 -0.144192 +-0.903991 0.382219 0.191596 -0.700735 0.708944 -0.0798041 -0.766391 0.527504 0.366583 +-0.508057 0.772399 -0.381154 -0.784094 0.219206 -0.580642 -0.683882 0.483564 -0.546325 +-0.886026 -0.0464835 0.461299 -0.965283 0.0557851 0.255182 0.78343 0.392991 -0.481451 +-0.675243 0.737564 -0.0067726 -0.725533 0.525109 0.444817 -0.258146 0.154741 -0.953633 +-0.506899 0.855126 0.108689 -0.788314 0.614795 0.024271 -0.773382 0.633257 -0.0294298 +-0.760578 0.645462 0.0699979 -0.711893 0.62321 0.323755 -0.849685 0.291113 -0.439647 +-0.779051 0.314051 -0.542633 -0.71807 0.556788 -0.417568 -0.628357 0.680451 0.377033 +-0.651155 0.758879 -0.0100119 -0.497986 0.866866 0.0235181 -0.516234 0.790596 0.329333 +0.478242 0.814234 0.329103 -0.523648 0.765867 0.37315 -0.105853 0.115554 -0.987645 +-0.35238 0.840989 0.410568 -0.730281 0.600932 -0.324915 -0.590472 0.767312 -0.250151 +-0.335934 0.850704 -0.40429 -0.382924 0.702754 -0.599589 -0.576737 0.759001 0.302146 +-0.573883 0.783414 0.238583 -0.59455 0.689146 0.414231 -0.718806 0.632515 0.288518 +-0.707885 0.705156 0.0406778 -0.673834 0.701287 0.23269 -0.761578 0.371756 -0.530845 +-0.795762 -0.584195 -0.159621 -0.980566 0.146295 0.130721 -0.668222 0.0151403 0.743807 +-0.377165 0.642308 -0.667224 -0.31977 0.67916 -0.660673 -0.0445384 0.995631 -0.0820695 +-0.00954625 0.804551 0.593807 -0.0491217 0.791115 -0.609691 -0.425515 0.712609 0.557786 +-0.510552 0.693066 0.508916 -0.404111 0.833039 0.377811 -0.388622 0.904736 0.174432 +-0.512647 0.819028 0.257655 -0.433971 0.855386 0.282814 -0.184736 0.777222 -0.601496 +-0.032553 0.770354 -0.636785 -0.0402712 0.63435 0.771996 0.00242618 0.322235 0.946657 +-0.0330473 0.924694 -0.379275 -0.00185979 0.99835 -0.0573871 0.0231342 0.999715 0.00588835 +-0.0362989 0.997806 0.0553675 -0.362966 0.850801 0.379991 -0.0330151 0.625662 -0.779396 +-0.0307592 0.313675 -0.949032 0.0047165 0.028526 0.999582 0.0444871 0.0849272 -0.995394 +0.00116344 -0.0156217 0.999877 0.298964 0.0260939 -0.953908 0.000111619 0.0240023 0.999712 +0.0471522 -0.0276663 -0.998505 -0.00590051 -0.0458167 0.998932 0.0146538 -0.0174604 -0.99974 +0.0202701 -0.388217 0.921345 -0.00575921 0.0241633 -0.999691 -0.0100307 -0.585335 0.81073 +0.0318079 0.0185395 0.999322 -0.0136208 0.00544952 -0.999892 0.0420852 0.612581 0.789287 +0.0250973 0.385846 0.922222 -0.164384 -0.321165 -0.932647 -0.14335 -0.292995 0.945307 +-0.271149 -0.462277 -0.844262 -0.162767 -0.601982 -0.781744 -0.396633 -0.568783 -0.720533 +-0.389385 -0.583482 0.712691 -0.321771 -0.611407 0.722942 -0.210027 -0.481447 0.850939 +-0.0555953 -0.0829268 -0.995004 -0.0431388 -0.0804028 0.995829 -0.00281006 0.0230207 -0.999731 +0.00492487 0.0291626 0.999563 0.00138144 0.00417137 -0.99999 0.00118512 0.00653281 0.999978 +0.00563854 0.00139707 -0.999983 0.0150392 0.0170093 0.999742 0.00636171 -0.000643274 -0.99998 +-0.0354763 -0.047076 0.998261 -0.00144869 -0.0238818 -0.999714 -0.350912 -0.407821 0.842937 +-0.0121484 0.0341614 -0.999342 -0.61375 -0.0823304 0.785196 -0.566827 -0.451131 0.689339 +-0.403946 -0.256472 0.878094 0.032844 0.383689 -0.922878 -0.455552 0.403133 0.793698 +-0.214523 0.330045 0.919266 -0.00752957 0.616123 -0.787614 0.0221028 0.338019 -0.94088 +0.0867809 0.618952 0.78062 -0.040704 0.284288 0.957874 -0.0387606 -0.342159 -0.938842 +0.0150814 0.00375631 0.999879 -0.0486016 -0.386995 -0.9208 -0.0771987 -0.620271 -0.780579 +-0.00241424 -0.0095862 0.999951 0.00131669 -0.040767 -0.999168 -0.00146699 0.000835184 0.999999 +0.0027402 0.0230495 -0.999731 -0.000792985 -0.000638931 0.999999 0.157065 0.322972 -0.933284 +0.00155402 0.000492961 0.999999 0.466842 0.55162 -0.691212 0.332038 0.607689 -0.721433 +0.235085 0.473089 -0.849071 0.00204284 -0.00126679 0.999997 0.0454237 0.0887282 -0.99502 +-0.00824561 -0.0212304 0.999741 -0.039502 -0.0281497 -0.998823 -0.0161497 0.0315682 0.999371 +-0.0222976 0.0110722 -0.99969 0.0216165 0.382627 0.92365 0.595959 -0.796494 -0.102124 +0.566079 -0.75689 -0.326606 0.612484 -0.785471 -0.0888738 0.65221 -0.757885 0.0152244 +0.66778 -0.744206 0.01506 0.665599 -0.746309 -0.000100248 0.664198 -0.747461 -0.0119423 +0.6296 -0.776835 -0.0114973 0.606204 -0.795023 -0.0213411 0.562851 -0.79894 0.211882 +0.0100487 -0.0401361 -0.999144 0.0637555 0.633579 0.771047 0.100571 0.360371 0.927372 +0.208406 -0.529489 -0.822319 0.290208 -0.836127 -0.465479 0.142028 -0.987086 -0.0740874 +0.290162 -0.955852 0.0463936 0.0431823 -0.998787 0.0236706 0.0533064 -0.998346 -0.0215404 +0.299235 -0.945362 -0.12942 0.358397 -0.933566 -0.00232963 0.347459 -0.937616 0.0122051 +0.336967 -0.941516 0.000147269 0.328366 -0.944513 -0.00843429 0.315072 -0.948832 -0.0211602 +0.347641 -0.937621 -0.00358602 0.473758 -0.879889 0.0367295 0.53426 -0.730909 0.424661 +0.395046 -0.530097 0.750291 0.0501072 -0.404077 -0.913351 0.426836 -0.528894 0.733541 +0.181534 -0.347212 0.920048 0.438521 -0.693106 -0.572105 0.278355 -0.527944 -0.802368 +0.451059 -0.670871 0.588623 0.517345 -0.739765 0.430236 0.59331 -0.740878 0.314775 +0.57371 -0.787551 0.224991 0.0655121 -0.734817 -0.675094 0.0337992 -0.856357 -0.515277 +-0.198184 -0.747536 -0.633966 0.0349375 -0.805272 -0.591875 0.31284 -0.598091 0.737847 +0.282348 -0.519642 0.806382 0.2572 -0.765774 -0.589439 0.292405 -0.810937 -0.506834 +0.254821 -0.783011 -0.567415 0.478203 -0.840996 0.253077 0.548179 -0.802558 0.235373 +0.508869 -0.86027 0.0314271 0.284374 -0.930574 -0.23057 0.386442 -0.706957 0.592346 +0.4706 -0.723938 0.504429 0.489907 -0.706372 0.51091 0.425107 -0.755662 -0.498257 +0.279762 -0.732727 -0.620358 0.209666 -0.824036 -0.526312 0.275912 -0.921231 0.274238 +0.0824993 -0.995298 -0.0507531 0.192044 -0.874336 -0.445709 -0.177952 0.983615 -0.0288803 +0.0507338 0.780545 0.623037 -0.224182 0.778128 -0.586736 -0.381212 0.708907 -0.593405 +-0.060061 0.645434 0.761451 -0.121763 0.634662 0.763137 -0.231225 0.861648 -0.451772 +0.301295 0.94363 0.137054 0.37436 0.926849 0.0284007 0.378529 0.925585 -0.00270586 +0.301975 0.94706 -0.109029 0.0989472 0.992305 -0.07443 0.542296 0.809216 -0.226017 +0.304843 0.934317 -0.184721 0.595915 0.763106 -0.250107 0.43584 0.880853 -0.184774 +0.583896 0.720866 -0.373387 -0.299612 0.605803 0.737045 -0.234906 0.495098 0.836479 +0.782134 0.51764 -0.346863 0.69813 0.614135 -0.368039 0.763995 0.53327 -0.363228 +0.744682 0.606431 -0.278731 0.448164 0.57489 -0.684581 -0.372075 0.612228 0.697665 +-0.289359 0.528742 0.797937 0.834151 0.494827 -0.243594 0.454289 0.710809 -0.537004 +0.731481 0.556833 -0.393539 0.578543 0.618869 -0.531309 -0.483765 0.522257 0.702295 +-0.446801 0.590651 0.671938 -0.300275 0.624375 0.721104 0.752456 0.418789 -0.508356 +0.62157 0.516515 -0.588951 0.673757 0.577768 -0.46069 -0.505814 0.525017 0.684477 +-0.300554 0.291011 0.908284 0.826733 0.463517 -0.318849 0.696587 0.647593 -0.308853 +0.763087 0.634806 -0.121327 0.647993 0.761646 -0.000299974 0.620467 0.765853 -0.168789 +0.63854 0.76732 -0.0590417 0.387628 0.920715 -0.0450413 0.636165 0.698736 -0.327202 +0.421323 0.887394 -0.187131 0.523115 0.705146 -0.478664 -0.113297 -0.037435 0.992856 +0.415693 0.83549 -0.359384 0.0486599 0.997691 -0.0473824 -0.0436561 0.998213 0.0408104 +-0.0126537 0.998722 -0.0489319 -0.0174289 0.925686 -0.37789 0.0215303 0.802599 -0.59613 +-0.283235 -0.41262 0.86575 -0.0164534 0.934773 -0.354864 -0.0257513 0.998075 -0.0564144 +-0.0269382 0.999407 -0.0214511 -0.276122 0.928668 -0.247654 -0.550271 0.707547 -0.443372 +-0.55981 0.759347 -0.331671 -0.712235 0.677183 -0.184782 -0.859289 0.510868 -0.0252058 +-0.884353 0.466515 0.0168399 -0.877471 0.479428 0.0139266 -0.874024 0.485542 -0.0182004 +-0.818363 0.53895 -0.199536 0.0583776 -0.7566 0.651267 -0.0103945 -0.609173 0.792969 +0.463617 -0.702494 0.539964 -0.688854 0.614577 -0.384417 -0.743024 0.587485 -0.320588 +-0.842418 0.496631 -0.209022 -0.73843 0.583044 -0.338793 -0.912515 0.406631 -0.0443475 +-0.925841 0.377121 0.0244422 -0.920263 0.391262 -0.00550786 -0.921809 0.351629 -0.163173 +-0.913273 0.35715 -0.1959 -0.921466 0.388135 -0.0158526 -0.937811 0.346723 0.0171316 +-0.951821 0.305321 0.0285666 -0.92546 0.377202 -0.0352514 -0.81446 0.542249 -0.206447 +-0.896799 0.440214 0.0443066 0.418865 -0.609725 0.672895 0.120862 -0.789495 0.601739 +0.59613 -0.720188 0.354907 0.6624 -0.746034 0.0682619 0.676655 -0.696123 0.239898 +-0.747997 0.610069 -0.261374 -0.844279 0.535715 -0.0142489 -0.836572 0.546623 0.0367577 +-0.821057 0.565629 -0.0770046 -0.768384 0.554606 -0.319372 -0.850064 0.457585 -0.260781 +-0.732037 0.567413 -0.377048 -0.906904 0.419171 -0.0426634 -0.922432 0.363352 -0.130746 +-0.997377 0.0699701 -0.0185449 -0.998947 0.0247344 0.0386403 -0.99649 0.0737232 -0.0396517 +-0.939891 0.334061 -0.0707713 -0.937107 -0.298428 -0.18103 -0.890734 0.0390356 -0.452846 +0.320629 -0.923986 0.208439 0.479358 -0.869603 0.118353 0.606176 -0.670197 0.428236 +0.524803 -0.707602 0.473161 0.675208 -0.650647 0.347495 0.670093 -0.738106 0.0785853 +0.651021 -0.758552 -0.027746 0.659149 -0.751975 -0.00747306 0.663172 -0.748454 0.0044485 +0.665219 -0.746635 0.00450379 0.663916 -0.747806 0.000681391 0.651151 -0.758817 -0.0141118 +0.637207 -0.770692 0.00102775 0.729412 -0.665212 0.159535 0.839673 -0.491478 0.231081 +-0.78904 0.598934 -0.136727 -0.803425 0.593162 -0.0516496 -0.788569 0.464916 -0.402506 +-0.962105 0.0551549 -0.267042 -0.793369 -0.110266 -0.598671 -0.999801 -0.0199531 0.000325533 +-0.998189 -0.0568401 -0.0197107 -0.950286 -0.167879 -0.262246 0.361378 -0.918489 0.160572 +0.3332 -0.94285 0.00351914 0.342889 -0.939328 -0.00947285 0.345712 -0.938335 0.00318195 +0.346112 -0.938175 0.00580366 0.344496 -0.938787 0.000878977 0.332809 -0.94287 -0.0153238 +0.323686 -0.945781 -0.0269507 0.348017 -0.931663 0.104346 0.391945 -0.822448 0.412259 +0.259871 -0.777126 0.573187 0.709907 -0.612697 0.347326 0.774489 -0.603274 0.190332 +0.758506 -0.634116 0.150218 0.848588 -0.488361 0.203475 -0.725522 -0.49706 -0.475971 +-0.747307 -0.433155 -0.503894 -0.911456 -0.271498 -0.30909 -0.997244 -0.0513585 -0.0535384 +-0.998184 -0.055036 -0.0245018 -0.944348 -0.301434 -0.131701 -0.905475 -0.275917 -0.322466 +0.52057 -0.794422 0.31289 0.640857 -0.74349 0.191112 0.689653 -0.653448 0.312066 +0.710615 -0.563618 0.421143 0.798736 -0.403811 0.446047 0.844929 -0.147616 0.514106 +-0.807669 -0.212508 -0.55001 -0.741401 -0.405049 -0.535032 -0.904666 -0.216222 -0.367189 +-0.995485 -0.0645982 -0.0695475 -0.996737 -0.0636092 -0.0496911 -0.931149 -0.275093 -0.239343 +0.658118 -0.529394 0.535372 0.5057 -0.447037 0.737852 -0.94187 -0.159479 -0.295715 +-0.776682 -0.291476 -0.558397 -0.992831 -0.0423659 -0.111766 -0.940451 -0.241468 -0.239261 +0.180123 -0.730025 0.659256 0.00967519 -0.754254 0.656512 -0.676548 -0.633219 -0.375921 +-0.759707 -0.400705 -0.512134 -0.793358 -0.504134 -0.341222 -0.877029 -0.426094 -0.221957 +-0.903666 -0.366715 -0.221151 0.0795069 -0.686017 0.723229 -0.0376244 -0.758012 0.651154 +-0.728855 -0.562686 -0.390071 -0.750985 -0.598305 -0.27938 -0.842981 -0.451859 -0.291903 +-0.660599 -0.73053 -0.173016 -0.683555 -0.58517 -0.436268 -0.587294 -0.794573 -0.154076 +-0.854348 -0.507744 -0.11084 -0.0270647 -0.916448 0.399237 -0.280723 -0.937674 0.204845 +-0.576781 -0.793137 0.195594 -0.416822 -0.891281 0.178543 -0.636622 -0.732668 0.240645 +-0.569636 -0.675662 0.46797 -0.72181 -0.573622 -0.387231 -0.734845 -0.620609 -0.27358 +-0.932378 -0.326379 -0.155395 -0.765999 -0.610896 -0.200129 -0.93525 -0.298686 -0.189987 +-0.869211 -0.267941 -0.415548 -0.583044 -0.692546 0.424782 -0.660271 -0.718555 0.218452 +-0.446064 -0.860461 0.246238 -0.633661 -0.72007 0.282794 -0.58812 -0.690737 0.42071 +-0.685682 -0.598405 -0.41443 -0.734245 -0.608343 -0.301336 -0.908656 -0.390091 -0.148907 +-0.763877 -0.605427 -0.223496 -0.944364 -0.292989 -0.149443 -0.983484 -0.157978 -0.0883297 +-0.94159 -0.300963 -0.151093 -0.899206 -0.272585 -0.342236 -0.67726 -0.646867 0.350545 +-0.673203 -0.710606 0.204542 -0.706758 -0.702605 0.0827004 -0.675821 -0.737034 0.00674328 +-0.69821 -0.715686 -0.0172142 -0.683068 -0.728728 -0.0487208 -0.697968 -0.687661 -0.199908 +-0.791132 -0.597656 -0.130068 -0.862105 -0.506547 -0.0136328 -0.911878 -0.406801 -0.054702 +-0.902615 -0.385007 -0.192496 -0.861194 0.465529 0.204028 -0.857672 0.469269 -0.210204 +-0.845308 0.478259 0.238165 -0.832912 0.552895 0.0237653 -0.770582 0.622736 0.135657 +-0.675138 0.737607 0.0111486 -0.659174 0.751972 -0.00538137 -0.751044 0.64635 0.134777 +-0.870751 0.491424 0.0171873 -0.870231 0.361017 -0.335209 -0.94167 0.0742177 0.328253 +-0.763815 0.604638 0.225833 -0.909382 0.409868 0.0709479 -0.900321 0.409011 0.148768 +-0.770747 0.609533 0.185523 -0.791246 0.508081 0.340269 -0.717959 0.696078 0.00301447 +-0.708343 0.705748 -0.0130027 -0.747975 0.663245 0.0252731 -0.772195 0.628508 -0.0932352 +-0.705436 0.595446 -0.384453 -0.617236 0.666235 -0.41851 -0.657478 0.682172 -0.319944 +-0.675901 0.731515 -0.0896836 -0.638055 0.712805 -0.291196 -0.633052 0.707087 0.315078 +-0.657035 0.746854 0.102533 -0.865147 0.488394 -0.113984 -0.789288 0.511779 0.339274 +-0.45386 0.626569 0.633579 -0.791999 0.60468 0.0842582 -0.782679 0.556008 0.279765 +-0.691711 0.667273 -0.276194 -0.736837 0.669684 -0.0927071 -0.639569 0.654527 -0.403171 +-0.561094 0.729522 -0.391116 -0.477049 0.6824 -0.553853 -0.279172 0.939852 -0.19683 +-0.176761 0.820384 0.543806 -0.372426 0.784036 -0.496575 -0.510542 0.68378 0.521335 +-0.523782 0.715959 0.46158 -0.455836 0.871821 0.179282 -0.440659 0.894362 0.0770464 +-0.514466 0.83152 0.209524 -0.566793 0.82374 -0.0140708 -0.548056 0.804629 -0.228489 +-0.484634 0.8253 0.289843 -0.252992 0.892733 -0.372858 -0.0226791 0.888075 -0.459139 +0.00653646 0.78259 -0.622503 -0.205191 0.766545 0.608528 -0.0071424 0.358096 0.933658 +-0.103401 0.948882 -0.298212 0.0294006 0.998512 -0.0459328 0.0297443 0.999404 0.0175426 +0.0422032 0.998976 -0.0162868 -0.0396643 0.997519 0.0581648 -0.371456 0.861102 0.347165 +-0.0102392 0.631143 -0.775599 0.048271 0.396418 -0.9168 0.00612695 0.0214918 0.99975 +0.371487 0.448381 -0.812989 0.00287563 -0.0174978 0.999843 0.629681 -0.144873 -0.763226 +0.591013 0.351256 -0.72617 0.445191 0.077428 -0.892082 -0.000367302 0.0200018 0.9998 +0.336969 -0.302363 -0.891644 0.511476 -0.420096 -0.749608 -0.0039015 -0.0428828 0.999072 +0.0309961 -0.0433324 -0.99858 0.198482 -0.272732 0.941394 0.0156465 0.0182692 -0.999711 +0.171737 -0.553759 0.814775 0.289994 0.140268 0.946693 0.000897525 -0.0154004 -0.999881 +0.423227 0.520904 0.741309 0.142033 0.298542 0.943769 -0.0402044 -0.378579 -0.924695 +-0.00645251 -0.368023 0.929794 -0.0522148 -0.387328 -0.920462 -0.0706777 -0.614782 -0.785524 +-0.0843716 -0.62523 0.775866 -0.0218965 -0.380637 0.924465 0.00526642 -0.0313516 -0.999495 +0.00503588 -0.0352312 0.999367 0.00605842 0.0227444 -0.999723 -0.000501218 0.0231671 0.999732 +-0.00119419 0.00252208 -0.999996 0.00585425 0.0176701 0.999827 -0.00182067 0.00277791 -0.999994 +0.00543514 -0.00302687 0.999981 -0.00469605 -0.00619947 -0.99997 -0.155337 -0.327338 0.932051 +-0.0151828 -0.0347512 -0.999281 -0.449851 -0.566303 0.690605 -0.326807 -0.62179 0.711741 +-0.227613 -0.458983 0.858794 0.0332409 0.0770513 -0.996473 0.0898114 0.244309 0.965529 +0.258961 0.477366 -0.839679 0.276497 0.600065 0.750647 0.265752 0.506226 0.820433 +0.313612 0.573093 0.757108 0.173765 0.614339 -0.769671 0.125863 0.256142 -0.95841 +0.0376658 0.0768957 0.996327 -0.187406 -0.461501 -0.867119 -0.0135802 -0.0274531 0.999531 +-0.12439 -0.327717 -0.936552 -0.311266 -0.603438 -0.73415 -0.00470617 -0.00379014 0.999982 +-0.0187395 -0.0260586 -0.999485 -6.79366e-05 0.00231276 0.999997 0.0433563 0.0894122 -0.995051 +0.0003867 0.000758129 1 0.266367 0.474259 -0.839123 0.00200615 0.00278767 0.999994 +0.240232 0.596938 -0.765476 0.168319 0.327068 -0.92989 0.000233379 -0.00655679 0.999978 +-0.00718622 -0.00690036 -0.99995 -0.00759342 -0.0317319 0.999468 -0.0167712 -0.0155447 -0.999739 +0.0379412 0.08125 0.995971 0.0017433 0.0308376 -0.999523 0.198402 0.482259 0.853266 +0.00743552 -0.0242817 -0.999677 0.3095 0.613269 0.726712 0.124963 0.294996 0.947292 +0.474762 -0.545941 -0.690325 0.545941 -0.663926 -0.511029 0.407661 -0.89648 -0.1736 +0.354241 -0.934699 -0.0291726 0.303265 -0.942653 -0.139413 0.0483818 -0.997933 -0.0423 +0.00530429 -0.999834 -0.0174052 0.00530731 -0.999922 0.0113382 0.0165754 -0.999829 0.00820297 +0.016661 -0.999859 -0.00203458 0.0156221 -0.999853 -0.00705614 0.0333127 -0.999437 -0.00390664 +0.0619184 -0.997681 0.0282715 0.262468 -0.922285 0.283727 0.338236 -0.554183 0.760577 +0.529082 -0.594352 0.605655 -0.0231871 -0.414369 -0.909814 0.160644 -0.260858 0.951917 +0.368223 -0.541406 -0.755838 0.543858 -0.501229 -0.673043 0.57518 -0.498326 0.648721 +0.493924 -0.711586 0.499684 0.518351 -0.679751 0.518894 0.269615 -0.730759 -0.627136 +0.238433 -0.781208 -0.576944 0.0275627 -0.761595 -0.647467 0.244275 -0.566003 0.787382 +0.287679 -0.535183 0.794241 0.32026 -0.772354 -0.548545 0.292062 -0.778643 -0.555352 +0.267349 -0.802283 -0.533728 0.284441 -0.911874 0.295937 0.316009 -0.86748 0.384208 +0.233092 -0.806817 0.542877 0.517098 -0.772374 0.368846 0.610173 -0.789944 0.060636 +0.543799 -0.819162 -0.182364 0.35454 -0.716864 -0.600339 0.231372 -0.640078 -0.732644 +0.398818 -0.627179 0.669021 0.213855 -0.572845 0.791274 0.0585402 -0.803252 -0.592756 +0.0835014 -0.89532 -0.437527 0.211914 -0.854622 -0.474039 0.341789 -0.905314 0.252164 +0.334869 -0.940388 -0.059444 0.276597 -0.863133 -0.422488 0.225947 -0.886983 0.402752 +0.0914736 -0.99449 0.0512163 0.0143829 -0.999512 -0.0277247 0.00637642 -0.929188 -0.369552 +-0.260295 0.948709 -0.179435 -0.317578 0.735089 0.598989 -0.329552 0.770125 -0.546171 +0.329204 0.81216 0.481686 0.391601 0.784952 0.480103 0.606177 0.762748 0.225311 +0.645806 0.76344 0.00966717 0.573743 0.78034 0.248773 0.618824 0.779266 -0.0989994 +0.649826 0.748337 -0.133109 0.565135 0.765429 -0.307801 0.532554 0.807408 -0.253926 +0.562644 0.736834 -0.374844 -0.333533 0.61894 0.711104 -0.233882 0.457496 0.857902 +-0.0108987 0.999878 -0.0111954 0.43685 0.865517 -0.245036 0.370922 0.927245 -0.0513268 +0.35761 0.925687 -0.123368 0.625725 0.754635 -0.197471 0.347203 0.923249 -0.164505 +0.595912 0.672173 -0.439399 0.380702 0.856584 -0.348324 0.240329 0.568799 -0.786581 +-0.445772 0.50322 0.740309 -0.380084 0.434181 0.816715 0.259426 0.519779 -0.813958 +-0.52313 0.480071 0.704179 -0.423404 0.557424 0.714149 0.232094 0.80179 -0.550695 +0.51274 0.664478 -0.54366 0.358648 0.83864 -0.409944 0.507412 0.676288 -0.534011 +-0.341824 0.624776 0.702005 -0.283113 0.487602 0.825889 0.659576 0.707943 -0.25254 +0.568605 0.697184 -0.436603 0.629986 0.710185 -0.314253 0.492704 0.824781 -0.277452 +0.513542 0.733388 -0.44544 -0.464558 0.472909 0.748694 -0.437839 0.436093 0.786206 +0.426148 0.843181 -0.327786 0.143536 0.984099 -0.104628 0.508796 0.671357 -0.538894 +0.35129 0.868306 -0.3502 0.518683 0.66768 -0.534015 -0.57744 0.350344 0.737443 +-0.43802 0.0776039 0.895609 0.365613 0.849239 -0.380947 0.0533457 0.997745 -0.040726 +-0.0419624 0.99864 -0.0309277 -0.0148923 0.931741 -0.362818 -0.083984 0.812323 -0.577129 +-0.39173 -0.576025 0.717456 -0.517171 -0.384519 0.764644 -0.432162 -0.353038 0.829819 +-0.0096684 0.922155 -0.3867 -0.021362 0.994216 -0.105253 -0.278931 0.920587 -0.273346 +-0.501365 0.670413 -0.546973 -0.534528 0.685851 -0.49385 -0.1019 -0.395249 0.912905 +-0.473372 0.675708 -0.5651 -0.217728 0.796537 -0.564025 -0.555173 0.707298 -0.437621 +-0.657966 0.692073 -0.296844 -0.0935175 -0.725666 0.681662 -0.334144 -0.717793 0.610836 +-0.389488 0.846736 -0.362404 -0.552765 0.71067 -0.4352 -0.644495 0.537713 -0.54359 +-0.7359 0.526599 -0.425611 -0.897396 0.427826 -0.107912 -0.93308 0.358209 0.0323861 +-0.937471 0.34795 0.00884528 -0.858547 0.469091 -0.207003 -0.16045 -0.886666 0.433681 +0.215363 -0.91532 0.340307 0.109828 -0.808355 0.57836 0.441671 -0.734138 0.515721 +-0.703566 0.493538 -0.511288 -0.819503 0.496575 -0.286056 -0.671669 0.608738 -0.422255 +-0.915076 0.376404 -0.14476 -0.995676 0.0917922 -0.0142981 -0.996992 0.0764668 -0.0126339 +-0.93507 0.315792 -0.160995 -0.917407 0.394242 -0.0542032 -0.926711 0.162194 -0.338968 +0.227802 -0.913073 0.338236 0.0724619 -0.996009 0.0521083 0.0651612 -0.997874 0.000854096 +0.0554624 -0.998432 -0.00752661 0.052278 -0.998628 -0.00303816 0.0453244 -0.99897 0.00213038 +0.0388954 -0.999243 -0.000768766 0.0297449 -0.999465 -0.0135684 0.0406291 -0.999157 -0.00592437 +0.0940403 -0.994867 0.0373526 0.166562 -0.915676 0.36578 0.22321 -0.765369 0.603645 +-0.705655 0.636547 -0.311221 -0.737851 0.590283 -0.327326 -0.853195 0.385986 -0.350817 +-0.745872 0.514071 -0.423563 -0.999327 -0.000750007 -0.0366686 -0.998675 -0.0309441 -0.0411139 +-0.916662 -0.255003 -0.307742 0.458866 -0.691198 0.55829 0.522868 -0.731583 0.437488 +0.62539 -0.705572 0.333251 0.675962 -0.672487 0.30139 -0.739145 0.361 -0.568633 +-0.944978 0.0386313 -0.324846 -0.817386 -0.116057 -0.564279 -0.939653 -0.188337 -0.285624 +0.476669 -0.83235 0.282807 0.574961 -0.677441 0.458796 0.510636 -0.718774 0.47182 +0.557596 -0.637146 0.532101 -0.688663 -0.542435 -0.481153 -0.778498 -0.386806 -0.49429 +-0.902583 -0.313684 -0.294868 -0.979991 -0.102109 -0.170852 -0.90508 -0.177015 -0.386647 +0.271273 -0.892714 0.359822 0.00541688 -0.878563 0.477596 -0.0791021 -0.791242 0.606366 +-0.611381 -0.611815 -0.501892 -0.617471 -0.534682 -0.576927 -0.709357 -0.597874 -0.373309 +-0.825192 -0.555266 -0.103625 -0.894514 -0.387731 -0.222508 -0.143651 -0.745194 0.651191 +-0.310173 -0.706755 0.635838 -0.628154 -0.654955 -0.420067 -0.653456 -0.693427 -0.30357 +-0.636516 -0.699748 -0.324345 -0.658738 -0.711279 -0.245249 -0.623183 -0.687308 -0.373164 +-0.197462 -0.880473 0.431017 -0.0922328 -0.993193 0.0711379 -0.100161 -0.994461 0.0318563 +-0.286946 -0.858235 0.425551 -0.423219 -0.696774 0.57913 -0.187257 -0.781464 0.595188 +-0.627717 -0.640495 -0.442422 -0.660057 -0.679509 -0.3203 -0.637715 -0.687972 -0.34643 +-0.673243 -0.691384 -0.262167 -0.633375 -0.685556 -0.358955 -0.287355 -0.880647 0.37668 +-0.477704 -0.877775 0.0361937 -0.67154 -0.721431 0.169033 -0.70977 -0.704421 -0.00409199 +-0.723069 -0.690358 0.0240292 -0.722737 -0.690997 0.0131985 -0.704875 -0.705852 -0.0701698 +-0.691226 -0.693542 -0.202991 -0.644857 -0.67649 -0.355697 -0.815255 0.497059 0.29714 +-0.807181 0.554619 0.202131 -0.727601 0.68584 0.0148041 -0.68105 0.732102 0.0140549 +-0.642912 0.765917 -0.00596879 -0.639377 0.722159 -0.263976 -0.602063 0.707717 0.369671 +-0.622714 0.781459 0.0393615 -0.704792 0.699678 0.117129 -0.778063 0.583179 0.233494 +-0.743486 0.64636 0.171606 -0.70177 0.609042 0.369576 -0.720116 0.692005 0.0506218 +-0.583505 0.796886 0.156507 -0.510209 0.85569 -0.0864979 -0.576169 0.692893 -0.433507 +-0.521847 0.687958 -0.50437 -0.654857 0.670487 -0.348726 -0.67743 0.732202 -0.0704902 +-0.6409 0.755953 -0.133352 -0.54397 0.827434 0.139462 -0.577776 0.760013 0.297582 +-0.550613 0.832679 0.0589144 -0.543094 0.819444 0.183198 -0.547237 0.789025 0.279232 +-0.492227 0.868506 -0.058398 -0.609929 0.728689 -0.311447 -0.551636 0.783441 -0.286211 +-0.637099 0.76913 0.0504427 -0.558409 0.614612 0.557163 -0.519846 0.761018 0.388087 +-0.423444 0.905759 0.0172347 -0.445343 0.894147 0.046589 -0.507103 0.822444 0.257745 +-0.368304 0.919519 -0.13725 -0.305014 0.952098 -0.0218018 -0.241906 0.963455 -0.11505 +-0.0261417 0.994693 -0.0995093 -0.00398162 0.93257 -0.360967 0.0183345 0.776626 -0.629696 +-0.00663905 0.904287 0.426873 0.00946307 0.40442 0.914524 -0.283932 0.957576 -0.0493113 +-0.0996971 0.994576 -0.0296467 -0.0489142 0.998726 -0.012381 -0.0406508 0.999025 0.0172126 +-0.0381117 0.99927 -0.00248523 -0.0206324 0.999478 -0.0248487 -0.0887337 0.99523 0.0405383 +-0.343106 0.931412 0.121452 -0.400525 0.91398 0.0649699 -0.524066 0.830644 0.18811 +-0.706704 0.659056 -0.257324 -0.696102 0.675825 0.242285 0.0133037 0.647261 -0.762152 +0.128725 0.635077 -0.761647 0.00701269 0.0183338 0.999807 0.44462 0.502654 -0.741385 +0.303723 0.64967 -0.696909 0.213324 0.443857 -0.870335 0.00821897 -0.0175465 0.999812 +0.105922 0.0468661 -0.993269 0.00186232 0.0101466 0.999947 0.391277 -0.567462 -0.724493 +0.24247 -0.378429 -0.893308 0.00877479 -0.0115516 0.999895 0.16864 -0.289117 -0.942323 +0.222475 -0.592105 -0.774543 0.0283612 -0.0306875 0.999127 0.00964264 -0.0172304 -0.999805 +0.555283 -0.163748 0.815382 0.287275 0.0764698 0.954791 -0.0557231 -0.0663112 -0.996242 +0.030155 -0.0115894 0.999478 -0.21093 -0.464036 -0.860337 0.023788 -0.38589 0.922238 +-0.129779 -0.331683 -0.934421 -0.317984 -0.593466 -0.739381 0.0127502 -0.614558 0.788768 +0.0432138 -0.361672 0.931303 -0.021598 -0.00524204 -0.999753 0.0124165 -0.0506561 0.998639 +-0.0161867 0.0119656 -0.999797 -0.00520052 0.0237269 0.999705 -0.0218773 5.04189e-05 -0.999761 +0.00766127 0.0341189 0.999388 -0.0217311 0.00241408 -0.999761 -0.0388049 -0.0744992 0.996466 +-0.0302169 -0.0172131 -0.999395 -0.267745 -0.484619 0.832741 -0.0169625 -0.00168568 -0.999855 +-0.248987 -0.609215 0.752902 -0.130823 -0.262454 0.956035 0.151747 0.335862 -0.929607 +0.243415 0.470608 0.848102 0.384015 0.590173 -0.71009 0.329726 0.604316 -0.725316 +0.221631 0.4649 -0.857175 0.346301 0.598364 0.722521 0.170942 0.323786 0.93056 +-0.119895 -0.256565 -0.959062 -0.00109473 -0.00542585 0.999985 -0.259639 -0.475743 -0.840391 +-0.168585 -0.615203 -0.770133 -0.384975 -0.588152 -0.711247 -0.00921861 -0.0172786 0.999808 +-0.0414433 -0.0912394 -0.994966 0.000816664 0.0034416 0.999994 0.0178425 0.0347069 -0.999238 +0.000726768 0.00188215 0.999998 0.126749 0.329896 -0.93547 0.000493837 0.00155283 0.999999 +0.372373 0.580422 -0.724188 0.308608 0.595842 -0.74144 0.201279 0.479841 -0.853955 +0.00109529 0.0031373 0.999994 0.0357622 0.0791516 -0.996221 -0.0034394 -0.0131882 0.999907 +-0.00943328 -0.0375316 -0.999251 0.00910321 0.000774218 0.999958 0.00295945 -0.000103983 -0.999996 +0.158467 0.319958 0.934085 0.0101039 0.0367523 -0.999273 0.16461 0.605006 0.779019 +0.258147 0.490097 0.832566 0.383886 0.582886 0.716154 -0.045615 -0.074546 -0.996174 +0.0482313 0.0778764 0.995796 0.570459 -0.789884 -0.225078 0.581651 -0.813203 -0.0195947 +0.577369 -0.802738 -0.149188 0.564265 -0.742733 -0.360489 0.469531 -0.84961 -0.240214 +0.319698 -0.931834 -0.171694 0.0711558 -0.996916 -0.0330816 0.0209347 -0.999725 0.0105274 +0.0256994 -0.99967 -0.000618349 0.034091 -0.999358 0.0110452 0.031323 -0.999165 0.026224 +0.248823 -0.960163 0.127177 0.231523 -0.885334 0.403213 0.383161 -0.546621 0.744576 +-0.201767 -0.483849 -0.851575 0.0250966 -0.079107 0.99655 0.420576 -0.662307 -0.620053 +0.39154 -0.670527 -0.630151 0.499925 -0.816693 -0.288251 0.598075 -0.798135 -0.0727102 +0.490974 -0.82797 -0.270945 0.437104 -0.575583 0.691118 0.25071 -0.535636 0.806373 +-0.0396688 -0.651068 -0.757982 -0.287122 -0.639942 -0.712765 0.348001 -0.53314 0.77114 +0.157708 -0.379584 0.911616 0.237464 -0.801633 -0.548631 0.268638 -0.798578 -0.538616 +0.21753 -0.813199 -0.539804 0.226555 -0.89414 0.386247 0.132281 -0.980183 0.147457 +0.186019 -0.899902 0.394428 0.273987 -0.767675 0.579315 0.0912879 -0.726718 -0.680844 +0.0977343 -0.627925 -0.772113 0.299382 -0.612024 0.731981 0.125741 -0.675027 0.726999 +0.138412 -0.814234 -0.563796 0.248431 -0.853442 -0.45817 0.190808 -0.909515 -0.36929 +0.135768 -0.946013 0.294322 0.0410265 -0.999135 0.00682273 0.0443041 -0.934848 -0.352273 +0.083722 -0.851778 0.517171 0.293651 -0.94172 0.164112 0.329865 -0.926552 -0.180806 +0.192289 -0.912358 -0.361425 0.357605 0.839935 0.408202 0.367332 0.74962 0.550579 +0.604992 0.790277 0.0972017 0.601253 0.731536 -0.321481 0.671588 0.733817 -0.102384 +0.570535 0.718022 -0.398665 -0.551494 0.833715 0.0278096 0.129223 0.735766 0.664793 +0.0438019 0.790217 0.611259 0.506027 0.809194 -0.298568 0.625496 0.764831 -0.154232 +0.578638 0.792718 -0.191771 0.630383 0.772513 -0.0764238 0.710138 0.702091 -0.0526484 +0.573395 0.762573 -0.299501 0.710102 0.63685 -0.300294 0.603152 0.700827 -0.380855 +-0.469927 0.509051 0.721134 -0.60079 0.620888 0.503537 -0.52831 0.813098 0.244459 +-0.10715 0.89257 0.437994 0.433716 0.869205 -0.237429 0.321225 0.865726 -0.383841 +0.270332 0.548787 -0.791046 -0.688646 0.423578 0.588514 -0.493294 0.316162 0.810372 +0.415468 0.699072 -0.581967 0.405013 0.682362 -0.60856 0.516535 0.687553 -0.510355 +-0.511613 0.526352 0.679121 -0.588487 0.403922 0.700379 0.370145 0.847248 -0.381004 +0.514436 0.700395 -0.494775 0.40637 0.832535 -0.376495 0.501629 0.709954 -0.4943 +-0.446077 0.54762 0.707903 -0.569247 0.615476 0.545112 -0.583402 0.536689 0.609596 +0.417203 0.819105 -0.393712 0.487431 0.707013 -0.51239 0.39618 0.83039 -0.391783 +0.48985 0.705967 -0.511525 -0.669545 0.464262 0.579802 -0.476661 0.302876 0.825264 +0.329004 0.876125 -0.352366 0.0999879 0.988475 -0.113661 0.526235 0.668257 -0.525841 +0.356799 0.86202 -0.360022 0.527079 0.674879 -0.516455 -0.637387 -0.144278 0.756916 +-0.596598 0.0792235 0.798621 0.374904 0.854201 -0.36026 0.0517298 0.995029 -0.0850931 +-0.162478 0.897686 -0.409587 -0.300611 0.797026 -0.523816 -0.443823 -0.0518741 0.894612 +-0.128213 0.94047 -0.314765 -0.153937 0.946272 -0.284383 -0.175124 0.809036 -0.561064 +-0.403496 0.757858 -0.512681 -0.500977 -0.318083 0.804889 -0.329218 0.777711 -0.53552 +-0.222019 0.84561 -0.485438 -0.220505 0.796117 -0.563538 -0.329289 -0.760094 0.560202 +-0.565217 -0.707419 0.424368 -0.487927 -0.705244 0.514352 -0.575404 -0.644319 0.503748 +-0.444323 0.667735 -0.59725 -0.716221 0.231397 -0.658394 -0.733461 0.4208 -0.533818 +-0.325038 -0.761521 0.560746 -0.193898 -0.59815 0.777573 -0.835986 0.316729 -0.448119 +-0.715764 0.365431 -0.595098 -0.721668 0.538234 -0.435315 0.00869704 -0.930184 0.36699 +-0.0333251 -0.99867 0.0393338 -0.0640383 -0.997775 -0.0185256 -0.0590359 -0.998254 -0.00177881 +-0.0242731 -0.999643 0.0111634 0.00965037 -0.999932 -0.0065125 0.000537597 -0.999941 -0.0108736 +-0.0118552 -0.999626 -0.0246454 -0.0225583 -0.999433 0.0249918 0.13717 -0.948894 0.284226 +0.314639 -0.545903 0.776525 0.447692 -0.675948 0.585377 -0.704687 0.58722 -0.398232 +-0.688485 0.633679 -0.352761 -0.706828 0.565462 -0.425025 -0.708808 0.567412 -0.419089 +-0.816408 0.489361 -0.3066 0.599979 -0.474557 0.644066 0.532634 -0.68652 0.494966 +0.536769 -0.657621 0.528595 -0.718763 0.455956 -0.524866 -0.847016 0.252766 -0.467625 +-0.788337 0.095137 -0.607844 -0.886786 0.109769 -0.448957 0.299832 -0.910353 0.285233 +0.286111 -0.855926 0.430734 0.0875373 -0.80414 0.58796 0.449962 -0.717148 0.532196 +-0.705788 -0.381793 -0.596739 -0.742484 -0.406525 -0.532405 0.211618 -0.914996 0.343511 +0.0582385 -0.996665 0.0571518 -0.0932942 -0.944383 0.315337 -0.299475 -0.75624 0.581735 +-0.653906 -0.636492 -0.409005 -0.718969 -0.645425 -0.257897 -0.661336 -0.687002 -0.301102 +-0.755059 -0.614649 -0.228237 -0.631842 -0.661165 -0.40452 -0.245264 -0.824977 0.509175 +-0.399136 -0.791964 0.462043 -0.444167 -0.734029 0.513729 -0.563355 -0.719446 0.406237 +-0.62439 -0.642528 -0.444179 -0.655299 -0.683234 -0.322141 -0.670808 -0.66425 -0.329831 +-0.669244 -0.698456 -0.25352 -0.664919 -0.665723 -0.338668 -0.409204 -0.874382 0.260786 +-0.35307 -0.935259 0.025135 -0.430499 -0.825666 0.364619 -0.445364 -0.755485 0.480514 +-0.510346 -0.789402 -0.341162 -0.54308 -0.786271 -0.29469 -0.605935 -0.772651 -0.189347 +-0.584487 -0.785742 -0.202449 -0.692312 -0.721564 -0.00705042 -0.696218 -0.717631 -0.0169417 +-0.712536 -0.700386 -0.0418426 -0.690564 -0.696386 -0.195367 -0.668024 -0.663274 -0.337359 +-0.336599 -0.927114 0.164803 -0.61657 0.729137 0.296986 -0.64105 0.763474 0.0785012 +-0.606869 0.771186 0.192305 -0.57314 0.758744 0.309545 -0.505343 0.859216 -0.0798482 +-0.645256 0.659889 -0.384956 -0.485774 0.698528 -0.525435 -0.684819 0.728473 -0.0187154 +-0.518198 0.585567 0.623363 -0.592134 0.62753 0.505553 -0.420721 0.895086 0.147702 +-0.369601 0.929151 0.00856866 -0.351479 0.920523 0.170586 -0.350308 0.923403 0.156879 +-0.338783 0.937877 0.0749116 -0.459105 0.867899 0.189669 -0.624855 0.68961 -0.366052 +-0.414459 0.895834 0.160328 -0.123399 0.991322 0.0453143 -0.0747805 0.997163 0.00864207 +-0.0482047 0.99748 -0.0520659 0.00488936 0.945897 -0.324431 -0.0598803 0.756031 -0.651791 +-0.0763342 0.894765 0.439963 0.0126235 0.389989 0.920733 -0.284004 0.951748 -0.116264 +-0.384353 0.922654 -0.0313332 -0.373316 0.927609 0.0132945 -0.37391 0.927407 0.0103632 +-0.382106 0.924113 0.00306132 -0.370566 0.928774 -0.00769085 -0.34658 0.937959 -0.0107391 +-0.361237 0.932362 0.0144349 -0.401046 0.916056 -0.00176316 -0.487834 0.872767 -0.0171707 +-0.653682 0.729505 -0.201302 -0.680366 0.619382 -0.39175 -0.696607 0.648608 -0.306669 +-0.722259 0.686537 0.0837178 -0.627002 0.587068 0.512074 -0.588861 0.467683 0.659177 +-0.00721192 0.673316 -0.73932 0.0200767 0.739064 -0.673336 -0.0349136 0.0219797 0.999149 +-0.72034 0.681922 -0.126855 -0.6842 0.66404 0.301533 0.0766376 0.660515 -0.746891 +0.0129266 0.364978 -0.930927 -0.00495979 -0.00399542 0.99998 -0.00542519 -0.0170548 -0.99984 +0.00225813 -0.00575201 0.999981 0.138954 -0.308469 -0.941031 -0.018677 -0.0070391 0.999801 +0.185319 -0.387583 -0.903015 0.301593 -0.607762 -0.73462 0.0163126 3.09695e-05 0.999867 +0.0335177 -0.0669695 -0.997192 0.0142191 0.0143513 0.999796 -0.107466 -0.327268 -0.938801 +-0.0260835 -0.0364488 0.998995 -0.271216 -0.460467 -0.845229 -0.102655 -0.630698 -0.769208 +-0.409911 -0.555585 -0.723393 0.151656 -0.276844 0.948872 -0.0044964 -0.0839479 -0.99646 +0.182069 -0.586905 0.788919 0.214442 -0.404119 0.889214 0.057745 0.0214921 -0.9981 +0.0353631 -0.0526777 0.997985 0.0451285 0.00325526 -0.998976 0.0116119 0.0111044 0.999871 +0.0423063 0.000300029 -0.999105 -0.00942167 0.0114319 0.99989 0.0399069 -0.002892 -0.999199 +-0.117951 -0.324242 0.938592 0.0331163 -0.0226809 -0.999194 -0.374343 -0.594941 0.711276 +-0.30074 -0.611167 0.732141 -0.181033 -0.466316 0.865897 0.0886361 0.0746684 -0.993261 +0.121746 0.256452 0.958859 0.297111 0.447273 -0.843607 0.158822 0.617354 0.770487 +0.249459 0.478358 0.841988 0.378467 0.59764 0.706816 0.234305 0.613097 -0.754463 +0.187571 0.307512 -0.932874 0.0309714 0.0773248 0.996525 -0.0320517 -0.373944 -0.926897 +-0.0138072 -0.0334901 0.999344 -0.0510717 -0.38998 -0.919406 -0.0828886 -0.621221 -0.779239 +-0.00132141 -0.00482067 0.999988 0.00694107 -0.0451851 -0.998955 0.00167454 0.00396285 0.999991 +0.00476458 0.055892 -0.998425 0.00034588 0.000890107 0.999999 -0.000327422 0.381782 -0.924253 +0.000676725 0.00242372 0.999997 0.0680988 0.610646 -0.78897 0.00359393 0.376628 -0.926358 +-0.000467219 0.000298531 1 -0.0128932 0.0333438 -0.999361 -0.0092867 -0.0224057 0.999706 +-0.00439426 -0.0277708 -0.999605 -0.00724961 0.0287326 0.999561 0.00635383 0.00994424 -0.99993 +0.0463969 0.384929 0.921779 -0.00716851 0.000266783 -0.999974 0.0895988 0.611078 0.786483 +0.0499148 0.395282 0.917203 -0.162154 -0.316085 -0.934771 -0.0134133 0.0475399 0.998779 +0.568089 -0.758179 -0.320062 0.575285 -0.781834 -0.24038 0.568204 -0.739534 -0.360879 +0.474096 -0.857066 -0.201671 0.369437 -0.929164 -0.0130327 0.362242 -0.932084 -0.000472913 +0.354817 -0.93442 0.0310452 0.386512 -0.902393 0.190514 0.537061 -0.771242 0.341689 +0.575938 -0.767927 0.280329 0.566167 -0.761719 0.315022 -0.261627 -0.502043 -0.82432 +-0.165971 -0.584058 -0.794562 -0.396074 -0.556363 -0.73047 0.00110161 -0.0710606 0.997471 +0.257558 -0.815844 -0.517747 0.524091 -0.848997 0.0673206 0.536845 -0.785893 0.306872 +0.625307 -0.759925 0.177493 0.586256 -0.737665 -0.334894 0.437088 -0.814548 -0.3814 +0.379277 -0.902853 -0.202495 0.46699 -0.844642 -0.261726 0.514055 -0.811062 0.279152 +0.546822 -0.690637 0.473293 0.410736 -0.612748 0.675156 0.0718491 -0.320388 -0.944558 +0.0130606 -0.340586 0.940123 0.334501 -0.768391 -0.545605 0.28635 -0.741705 -0.606529 +0.310623 -0.803997 -0.507052 0.428971 -0.901629 -0.0552162 0.227294 -0.866861 0.443722 +0.294896 -0.902352 0.314319 0.497078 -0.728873 0.470805 0.421578 -0.757931 0.497809 +0.182297 -0.755966 -0.628716 0.30308 -0.55114 -0.777424 0.0748804 -0.650265 0.756008 +0.00650909 -0.751064 0.660197 0.357736 -0.761033 -0.54116 0.289524 -0.780407 -0.554203 +0.139905 -0.917555 -0.372183 0.213061 -0.857726 -0.46788 0.274494 -0.907235 0.318713 +0.0060325 -0.998477 0.0548462 -0.00704611 -0.999907 0.0116997 0.143161 -0.947188 -0.286949 +0.148614 -0.792058 0.592079 0.173735 -0.769853 0.614119 0.483444 -0.796536 0.36306 +0.596314 -0.796631 -0.0989393 0.559839 -0.711648 -0.424426 0.381872 -0.793752 -0.473426 +0.241627 -0.821538 -0.516422 0.359172 -0.91271 0.194822 0.432108 -0.856208 -0.283179 +0.405078 0.908162 0.10561 0.562998 0.709477 0.423881 0.638099 0.768137 -0.0528805 +0.612055 0.663987 -0.429547 0.446486 0.700566 0.556649 0.166852 0.636519 0.752996 +0.626839 0.750902 -0.207891 0.683459 0.630545 -0.367827 0.671063 0.741387 0.00452415 +0.738277 0.672661 -0.0497328 0.720987 0.692103 -0.0342229 0.76219 0.646639 0.0304094 +0.697394 0.658088 -0.283833 0.756792 0.640448 -0.13074 0.72168 0.558139 -0.40946 +0.000438574 0.595575 0.803299 -0.126913 0.669695 0.731712 0.609476 0.762287 -0.217848 +0.710516 0.61088 -0.349274 0.549755 0.708562 -0.442391 0.723129 0.626402 -0.291042 +0.463004 0.443927 -0.767174 -0.759311 0.38704 0.523112 -0.554928 0.448181 0.700848 +-0.392752 0.711001 0.583287 0.365824 0.519407 -0.772262 -0.815716 0.245044 0.523985 +-0.801554 0.296686 0.519123 -0.58315 0.227622 0.779823 0.356348 0.765638 -0.53555 +0.501378 0.715724 -0.486168 0.575789 0.706711 -0.411128 0.588523 0.698833 -0.406537 +-0.766893 0.388456 0.51086 -0.635118 0.39573 0.663342 -0.712627 0.484328 0.507532 +0.464875 0.830703 -0.306306 0.490843 0.70419 -0.51302 0.575193 0.705102 -0.414709 +0.527875 0.657673 -0.537415 -0.765394 0.390052 0.511891 -0.705735 0.398156 0.586012 +-0.705925 0.469104 0.53067 0.373047 0.842313 -0.38903 0.495196 0.702957 -0.510522 +0.412145 0.818761 -0.399709 0.509952 0.682426 -0.523682 -0.798094 0.212804 0.563702 +-0.791805 0.295055 0.534778 -0.734175 0.357512 0.577211 0.330932 0.878959 -0.343388 +0.126753 0.987436 -0.094359 0.53362 0.683523 -0.498042 0.367822 0.860847 -0.351638 +0.502907 0.717707 -0.481645 -0.727645 0.111678 0.676802 -0.686588 0.348884 0.637869 +-0.729385 0.284724 0.622037 0.256953 0.790271 -0.55628 0.126674 0.734958 -0.666177 +-0.329647 0.735036 -0.592498 -0.763492 -0.193689 0.616089 -0.782316 -0.0818027 0.617487 +-0.0786036 0.904639 -0.418866 0.0198511 0.987167 -0.158451 -0.0292636 0.937539 -0.346648 +0.0669626 0.793079 -0.605427 -0.754911 -0.529162 0.387423 -0.755384 -0.426874 0.497166 +-0.762144 -0.522797 0.381862 -0.696537 -0.540096 0.472368 -0.18844 0.777318 -0.600223 +-0.337791 0.523747 -0.78204 -0.434016 0.70516 -0.560695 -0.592149 -0.554063 0.585128 +-0.500099 -0.540361 0.676691 -0.369325 -0.354143 0.859175 -0.631634 0.449011 -0.632003 +-0.730675 0.18778 -0.656394 -0.650991 0.322767 -0.687046 -0.371381 -0.519313 0.769668 +-0.684651 0.288673 -0.669269 -0.693917 0.277915 -0.664261 -0.2687 -0.834278 0.481437 +-0.343113 -0.938367 0.0417172 -0.375241 -0.92641 -0.0309684 -0.384079 -0.923267 0.00781356 +-0.325971 -0.935854 0.133868 -0.0870286 -0.995311 0.0422176 -0.0386568 -0.999147 -0.0145392 +-0.013902 -0.999696 -0.0203601 0.0103008 -0.99965 0.0243772 0.015274 -0.908045 0.418593 +0.00567375 -0.569207 0.822175 -0.627539 0.520714 -0.578837 -0.635694 0.563074 -0.528053 +-0.704823 0.556927 -0.439384 -0.814847 0.509043 -0.277309 0.423959 -0.542984 0.724864 +-0.0750465 -0.491604 0.867579 -0.702132 0.481208 -0.524833 -0.894355 0.216518 -0.391471 +-0.802596 0.0927319 -0.589271 -0.861229 0.301076 -0.409436 0.0767169 -0.87222 0.483059 +-0.0143468 -0.993284 0.114806 -0.0680245 -0.944332 0.321886 -0.317056 -0.742264 0.590355 +-0.784896 -0.439085 -0.437199 -0.759985 -0.399124 -0.512954 -0.916324 -0.18451 -0.355397 +-0.182347 -0.871916 0.454437 -0.0542672 -0.99527 0.0805756 -0.0523253 -0.997965 0.0364477 +-0.334065 -0.86589 0.372338 -0.446324 -0.723423 0.526739 -0.544364 -0.731595 0.410411 +-0.696996 -0.609562 -0.377665 -0.738935 -0.598623 -0.309235 -0.629287 -0.698638 -0.340446 +-0.757828 -0.594371 -0.26911 -0.630803 -0.672843 -0.386485 -0.462214 -0.823009 0.330174 +-0.605296 -0.770915 0.198258 -0.621958 -0.772 0.131086 -0.641247 -0.753063 0.147306 +-0.609653 -0.791773 0.037664 -0.491076 -0.841454 0.225387 -0.568288 -0.786003 0.243407 +-0.545422 -0.720074 0.428961 -0.578454 -0.692837 -0.430543 -0.657597 -0.709047 -0.254594 +-0.695873 -0.705556 -0.133985 -0.70705 -0.705862 -0.0428746 -0.698407 -0.713518 -0.0558495 +-0.741684 -0.647119 -0.176472 -0.652629 -0.680746 -0.332658 -0.643255 -0.742131 0.18832 +-0.627681 -0.771414 -0.104576 -0.513702 -0.820247 -0.251604 -0.713598 0.70028 -0.0196621 +-0.623221 0.744679 0.238848 -0.588028 0.80855 0.0216558 -0.549787 0.823234 0.141491 +-0.53482 0.771202 0.345276 -0.4572 0.855484 0.243137 -0.328154 0.9268 0.182639 +-0.136446 0.990598 0.00990145 -0.403866 0.864998 -0.297775 -0.198637 0.814512 -0.545081 +-0.539819 0.753625 -0.375026 -0.635968 0.764375 0.106186 -0.553646 0.724401 0.410754 +-0.452816 0.864545 0.217991 -0.359173 0.932911 0.025916 -0.34327 0.938695 -0.0318964 +-0.228944 0.920626 -0.316278 -0.330941 0.765739 -0.551473 -0.517629 0.85518 -0.0269695 +-0.274484 0.453484 0.847945 -0.683069 0.705312 -0.18961 -0.562572 0.76142 -0.322106 +-0.58745 0.803634 -0.0952602 -0.60154 0.798685 0.0158324 -0.608619 0.793419 0.0082948 +-0.609473 0.792804 0.00207174 -0.599837 0.800095 0.00666936 -0.584199 0.811564 0.00875142 +-0.582416 0.81287 0.00581869 -0.596723 0.802224 -0.0189103 -0.707506 0.692518 -0.140907 +-0.771911 0.592128 -0.231381 -0.752612 0.411346 0.514169 -0.722038 0.383859 0.575598 +-0.757217 0.590141 0.27992 -0.606673 0.77047 0.195765 -0.52684 0.804808 0.273356 +-0.311606 0.92351 -0.223676 -0.454912 0.851294 0.261445 -0.109659 0.759553 -0.641135 +-0.0400704 0.749674 -0.660593 -0.311691 0.127348 0.941611 -0.415372 0.893853 -0.168796 +-0.35728 0.933561 -0.0285418 -0.369033 0.929376 0.00865761 -0.368928 0.929441 0.00572037 +-0.356737 0.934137 0.0112811 -0.352081 0.93592 0.0095867 -0.400608 0.916151 -0.0134315 +-0.509211 0.860369 -0.0216729 -0.663173 0.726479 -0.180082 -0.756919 0.574645 -0.311217 +-0.826192 0.492467 -0.273648 -0.757182 0.544573 -0.360716 -0.792802 0.570504 -0.214453 +-0.767889 0.629211 0.120164 -0.790977 0.519166 0.323763 -0.768039 0.274464 0.578606 +-0.00481954 0.649954 -0.759959 -0.0449538 0.333494 -0.94168 -0.033511 0.0139353 0.999341 +-0.744015 0.601336 -0.291267 -0.754458 0.656347 -0.000965581 -0.597808 0.776752 0.198196 +-0.600098 0.706407 0.375328 -0.0136288 0.039582 -0.999123 -0.0205537 -0.00634481 0.999769 +0.0309132 -0.0657406 -0.997358 0.0365835 0.0253483 0.999009 0.448023 -0.514642 -0.73104 +0.299726 -0.330955 -0.894781 0.268084 -0.0467293 0.962261 0.380718 -0.234061 -0.894578 +0.559126 -0.36756 -0.743154 0.0122313 -0.0209949 0.999705 0.411113 -0.514664 -0.752401 +0.0130194 -0.00762278 0.999886 0.194303 -0.344372 -0.918506 0.352136 -0.55279 -0.755263 +0.0319142 -0.0271456 0.999122 0.372436 -0.0237055 -0.927755 0.367869 -0.557989 0.743856 +0.166033 -0.399908 0.901392 0.383708 0.0149866 -0.923333 0.29549 -0.587077 0.753675 +0.123039 -0.299064 0.946267 0.375423 -0.00154558 -0.926852 0.0052271 -0.0178371 0.999827 +0.375309 0.000175141 -0.9269 0.00817181 -0.00897919 0.999926 0.370599 -0.00873868 -0.928752 +-0.0488832 -0.385441 0.921437 0.376533 0.0014732 -0.926402 -0.0780298 -0.626735 0.775315 +-0.0436424 -0.34926 0.936009 0.466352 0.199469 -0.861817 0.043273 0.346027 0.937226 +0.418008 0.589934 -0.690831 0.540342 0.469076 -0.698568 0.397452 0.381301 -0.83465 +0.0807113 0.623166 0.777914 0.0469143 0.391672 0.918908 0.00499444 -0.0329052 -0.999446 +-0.0108747 0.0271512 0.999572 -0.212026 -0.495982 -0.842049 -0.00743887 -0.021714 0.999737 +-0.120975 -0.325576 -0.937745 -0.310778 -0.597843 -0.738919 0.000839764 0.00129576 0.999999 +-0.0110231 -0.0242828 -0.999644 0.000911691 0.0025263 0.999996 -0.00947465 0.0499405 -0.998707 +0.000223671 0.000706869 1 0.0327146 0.378563 -0.924997 0.000258483 0.00256577 0.999997 +-0.012459 0.604157 -0.796768 0.019508 0.37687 -0.926061 -0.0034659 -0.00642919 0.999973 +-0.00146238 0.0322644 -0.999478 -0.00583218 -0.0265277 0.999631 -0.00321524 -0.0182071 -0.999829 +0.0411859 0.0840035 0.995614 0.010019 0.022014 -0.999708 0.210978 0.491076 0.845182 +0.0106231 -0.0279759 -0.999552 0.292306 0.577031 0.762622 0.114989 0.330486 0.93678 +-0.0366861 -0.382255 -0.923328 0.0118919 0.0208045 0.999713 0.566281 -0.767321 -0.300905 +0.578548 -0.812222 -0.0746834 0.576174 -0.817295 0.0071879 0.575359 -0.817901 -0.000178218 +0.548186 -0.821939 0.154622 -0.0472084 -0.380759 -0.923468 -0.0737449 -0.598186 -0.797957 +0.00449273 -0.0529798 0.998586 0.527643 -0.830398 -0.178974 0.464664 -0.742371 -0.482672 +0.512968 -0.713194 -0.477722 0.458809 -0.855411 -0.240347 0.37655 -0.905117 -0.197417 +0.374514 -0.91546 -0.147213 0.418238 -0.908328 -0.00415368 0.419681 -0.907664 -0.00378573 +0.405897 -0.913808 0.0142473 0.325433 -0.923537 0.202909 0.288135 -0.87366 0.392042 +0.164186 -0.801863 0.574507 0.405181 -0.723206 0.559286 0.0357336 -0.0844873 -0.995784 +0.00488555 -0.348836 0.937171 0.26469 -0.785965 -0.558747 0.460546 -0.828033 -0.31978 +0.466331 -0.877624 -0.110957 0.147005 -0.986393 -0.073613 0.372014 -0.871243 -0.320222 +0.279288 -0.824104 0.492799 0.379755 -0.818207 0.431651 0.520047 -0.693809 0.498177 +0.49909 -0.715756 0.488469 0.380283 -0.673476 -0.633889 0.208928 -0.347051 -0.914278 +-0.0167404 -0.66345 0.748033 0.0399834 -0.722524 0.690189 0.0692894 -0.793318 -0.604851 +0.0322464 -0.924848 -0.378967 0.177188 -0.931755 -0.316919 0.301504 -0.910717 0.282293 +0.032001 -0.99911 0.0274878 0.0181639 -0.999559 0.0235018 0.0155196 -0.999716 0.0180705 +0.0457836 -0.996816 -0.065279 0.190177 -0.828397 -0.526869 0.0282257 -0.694965 0.718489 +0.0660705 -0.745215 0.663543 0.451371 -0.719745 -0.527476 0.295538 -0.698966 -0.651233 +0.149868 -0.741426 -0.654085 0.0356968 -0.930969 0.363349 0.0761191 -0.996041 -0.0459161 +0.189951 -0.871069 -0.452943 0.461062 0.865868 0.194149 0.670348 0.646051 0.365036 +0.852485 0.505669 0.132547 0.840433 0.465575 -0.27733 0.515987 0.847903 -0.121728 +0.577548 0.638835 -0.50826 0.295564 0.782171 0.548498 0.239617 0.542228 0.80534 +0.410439 0.736327 0.537924 0.454135 0.856784 0.244302 0.707592 0.682939 -0.181406 +0.587826 0.801029 -0.113199 0.822458 0.542445 0.171223 0.724516 0.6713 0.15631 +0.899623 0.435825 0.0271211 0.919861 0.3591 -0.157805 0.619199 0.525267 -0.583684 +0.821184 0.418516 -0.387946 0.605516 0.56801 -0.557417 0.0134454 0.398946 0.916876 +0.843361 0.464303 -0.270489 0.600183 0.566808 -0.564366 0.752052 0.500501 -0.428855 +0.584703 0.388157 -0.71236 -0.0392765 0.553727 0.831771 -0.0552764 0.490927 0.869445 +0.562365 0.25985 -0.784999 -0.515227 0.467054 0.718611 -0.282685 0.365405 0.886887 +0.602597 0.755577 -0.256864 0.681986 0.569896 -0.458382 0.65091 0.717367 -0.248395 +0.7054 0.632768 -0.319398 0.58709 0.639889 -0.49585 -0.523245 0.344934 0.779253 +-0.521467 0.293867 0.801071 0.611967 0.764484 -0.202634 0.579069 0.708346 -0.40364 +0.596448 0.765348 -0.241851 0.482053 0.842153 -0.241666 0.568494 0.661558 -0.489035 +-0.725676 0.370245 0.579925 -0.53384 0.278446 0.798425 0.505299 0.685658 -0.523972 +0.598908 0.539553 -0.591771 0.601847 0.657712 -0.452985 -0.825639 0.298049 0.479049 +-0.777762 0.328079 0.536144 -0.567075 0.246627 0.785876 0.447469 0.838406 -0.311203 +0.496128 0.697993 -0.516394 0.576391 0.70617 -0.411215 0.608898 0.670967 -0.42314 +-0.771134 0.416275 0.481733 -0.625534 0.415633 0.66027 -0.693191 0.499945 0.519173 +0.412889 0.875747 -0.250179 0.322431 0.924677 -0.202511 0.401899 0.684125 -0.608646 +0.2839 0.843282 -0.456374 0.279702 0.709463 -0.646861 -0.803925 -0.0100138 0.594647 +-0.759244 0.201876 0.618704 -0.739076 0.441136 0.509083 0.419903 0.744862 -0.518519 +0.452903 0.67236 -0.585501 0.547579 0.661019 -0.513041 -0.820858 -0.324165 0.470223 +-0.778679 -0.221678 0.586956 -0.757589 -0.285112 0.587171 -0.670841 -0.141119 0.728051 +0.327494 0.876303 -0.353328 0.175416 0.930448 -0.321709 0.321389 0.745776 -0.583547 +-0.667609 -0.421598 0.613639 -0.447669 -0.218512 0.86709 0.180596 0.63959 -0.747201 +0.198061 0.359313 -0.911957 -0.370213 -0.34496 0.862522 -0.635366 0.298886 -0.712024 +-0.232591 0.149341 -0.96104 -0.575605 -0.787295 0.221009 -0.522042 -0.732807 0.436425 +-0.443565 -0.894911 0.0488412 -0.568055 -0.822703 -0.0217433 -0.387286 -0.921438 -0.0310078 +-0.570082 -0.821168 -0.0262412 -0.40397 -0.914771 0.00128976 -0.58764 -0.805423 0.0772853 +-0.496158 -0.843209 0.206944 -0.578853 -0.750974 0.317753 -0.571901 -0.724925 0.383944 +-0.610235 0.298048 -0.734017 -0.376244 0.234383 -0.896384 -0.457177 -0.850624 0.259669 +-0.371668 -0.92741 0.0421039 -0.317277 -0.94304 0.100051 -0.0343959 -0.996419 0.0772362 +0.0249752 -0.912567 0.408164 -0.0986485 -0.698835 0.708448 -0.601378 0.455967 -0.656078 +-0.770327 0.135717 -0.623038 -0.725425 0.409605 -0.553157 -0.412155 -0.662734 0.625229 +-0.396068 -0.685831 0.610546 -0.539828 -0.654506 0.529346 -0.930836 -0.0867133 -0.354999 +-0.769358 -0.300615 -0.563666 -0.836256 0.289636 -0.465604 -0.348883 -0.86789 0.35362 +-0.139981 -0.986387 0.0862859 -0.376786 -0.854745 0.356993 -0.473213 -0.704296 0.529185 +-0.577429 -0.703973 0.413518 -0.689794 -0.553581 -0.466618 -0.697564 -0.445687 -0.561042 +-0.845422 -0.38701 -0.368082 -0.900797 -0.377602 -0.214433 -0.918023 -0.183294 -0.351621 +-0.442365 -0.846458 0.296347 -0.315984 -0.939301 0.133671 -0.125585 -0.988907 0.0793136 +-0.345861 -0.928886 0.132481 -0.588608 -0.797115 0.134717 -0.468645 -0.883386 -0.00143063 +-0.641608 -0.763731 0.0710918 -0.665388 -0.734766 0.131825 -0.621489 -0.722072 0.303915 +-0.586081 -0.710716 0.389091 -0.621107 -0.63318 -0.461854 -0.6573 -0.68553 -0.313057 +-0.670192 -0.678752 -0.300232 -0.697175 -0.685703 -0.209188 -0.790906 -0.57578 -0.207233 +-0.835837 -0.481216 -0.26421 -0.514136 -0.805851 0.293716 -0.572607 -0.791842 0.212383 +-0.633167 -0.770377 0.0749532 -0.68831 -0.6998 -0.191074 -0.615003 -0.696978 -0.368771 +-0.581144 0.750274 0.315215 -0.567593 0.780578 0.261795 -0.595637 0.725323 0.345143 +-0.493301 0.858144 0.14228 -0.268449 0.90396 -0.332853 -0.451267 0.802648 -0.390019 +-0.591585 0.737094 0.32668 -0.608324 0.789416 0.0822475 -0.593805 0.804598 0.0042125 +-0.535281 0.830584 -0.153634 -0.796295 0.476646 0.372456 -0.833595 0.299748 0.463972 +-0.71159 0.549108 0.438315 -0.55053 0.739966 0.38648 -0.521545 0.821709 0.229748 +-0.52779 0.797307 0.292814 -0.519843 0.820567 0.237553 -0.106891 0.954187 -0.279466 +-0.051511 0.998309 -0.0269391 -0.321165 0.93742 0.134521 -0.37289 0.927358 0.0309829 +-0.359509 0.933141 0.0013931 -0.269742 0.928218 -0.256225 -0.0809302 0.857053 -0.508833 +-0.217598 0.63637 -0.740057 -0.666029 0.52372 0.531152 -0.245619 0.923923 0.293322 +-0.327338 0.74138 0.585837 -0.0323173 0.999411 -0.0115402 -0.0158432 0.999869 -0.0032811 +-0.0205297 0.999764 0.00712954 -0.0179609 0.999798 0.00898035 -0.0426131 0.998902 -0.0194459 +-0.108687 0.992996 -0.0463202 -0.386748 0.883948 -0.262797 -0.468849 0.783513 -0.407785 +-0.613485 0.729564 -0.302278 -0.658229 0.68218 -0.31838 -0.726776 0.411477 0.549986 +-0.444231 0.736003 0.510841 -0.435533 0.535196 0.723793 -0.175466 0.608155 -0.774183 +-0.205147 0.36241 -0.909161 -0.162914 0.648562 0.743523 -0.173869 0.247861 0.953066 +-0.371818 0.894757 -0.247309 -0.455717 0.760523 -0.462522 -0.256155 0.791488 -0.554916 +-0.570625 0.711768 -0.409602 -0.660021 0.748229 -0.0672668 -0.602686 0.772485 0.200092 +-0.550298 0.756755 0.352836 -0.50884 0.828701 0.233103 -0.544889 0.701924 0.458692 +-0.0490165 0.0604461 -0.996967 0.0114851 0.0523393 0.998563 -0.317151 0.915461 -0.247681 +-0.339561 0.939704 0.0406875 -0.453721 0.853599 0.255941 0.00605195 -0.00959934 -0.999936 +0.204699 0.297889 0.932395 0.0557146 -0.0331152 -0.997897 0.13504 0.542695 0.829003 +0.328296 -0.184768 0.926327 0.563981 0.170554 0.807983 0.590737 -0.182908 -0.785859 +0.266193 -0.183005 -0.946388 0.416069 -0.506494 0.755215 0.158315 -0.279504 0.947002 +0.521116 -0.407911 -0.749698 0.158335 -0.374875 -0.913455 -0.0019908 -0.0321931 0.99948 +0.566125 -0.168303 -0.806955 0.301496 -0.41795 -0.856982 0.0716345 -0.705121 -0.70546 +-0.00035275 -0.0262568 0.999655 0.605305 0.0167187 -0.795818 0.362633 -0.00743298 -0.931902 +0.0058892 -0.356239 0.934376 0.608764 0.00517891 -0.793335 0.373661 0.0192611 -0.927365 +0.0794938 -0.610104 0.788324 0.0125819 -0.381647 0.924223 0.607441 -0.00240561 -0.794361 +0.376026 -0.00299026 -0.926604 0.00698383 -0.0150906 0.999862 0.606563 -0.00307344 -0.79503 +0.375475 -0.00132415 -0.926832 -0.0368032 -0.0549146 0.997813 0.603992 -0.0142115 -0.796864 +0.376451 -0.00676354 -0.926412 -0.261184 -0.468561 0.843939 0.611007 0.0590664 -0.789418 +0.381152 0.00301416 -0.924508 -0.166553 -0.61909 0.767456 -0.123325 -0.256788 0.958567 +0.617689 0.309592 -0.722919 0.327604 0.129662 -0.935876 0.190756 0.462327 0.865948 +0.0397257 0.0377418 -0.998498 0.313018 0.603457 0.733389 0.123295 0.325093 0.93761 +-0.175138 -0.339531 -0.924146 0.00602722 -0.000924251 0.999981 -0.256807 -0.468029 -0.845576 +-0.165788 -0.603204 -0.780166 -0.383623 -0.579442 -0.719083 -0.00361402 -0.0124442 0.999916 +-0.0388989 -0.087034 -0.995446 0.00103402 0.00344524 0.999994 0.00224696 -0.000739562 -0.999997 +0.000447858 0.00139982 0.999999 0.0443438 0.0863058 -0.995281 -0.000132447 0.000605133 1 +0.26597 0.467189 -0.843204 -0.000848903 0.00121857 0.999999 0.17483 0.600582 -0.780215 +0.166385 0.323473 -0.931494 0.00186526 -0.00605636 0.99998 0.0109022 0.0017321 -0.999939 +0.0274948 0.0123314 0.999546 -0.000676521 -0.00613884 -0.999981 0.0156397 0.29782 0.954494 +0.00860652 0.0308158 -0.999488 -0.136853 0.63314 0.761843 0.0641192 0.460552 0.885314 +0.327285 0.578334 0.747271 -0.0477366 -0.0860196 -0.995149 -0.0126002 0.101984 0.994706 +-0.209316 -0.471603 -0.856608 -0.0128374 0.0210609 0.999696 -0.132086 -0.304174 -0.943415 +-0.321126 -0.586114 -0.743874 0.024072 -0.0482006 0.998548 0.58918 -0.737037 -0.331125 +0.57785 -0.783419 -0.228787 0.565858 -0.73939 -0.364837 0.480505 -0.853436 -0.201895 +0.410694 -0.911739 -0.00791252 0.41425 -0.910096 0.0110653 0.416807 -0.908529 -0.0291118 +0.383123 -0.922956 0.0370003 0.23959 -0.920636 0.308262 0.327919 -0.770612 0.546467 +-0.000801785 -0.0272022 -0.99963 -0.0275048 -0.370455 0.928443 0.507088 -0.775119 -0.376898 +0.585681 -0.785341 -0.200543 0.554198 -0.72313 -0.412247 0.357789 -0.863533 -0.355383 +0.0926898 -0.989727 -0.108854 0.297304 -0.944068 -0.142639 0.462092 -0.848743 -0.257114 +0.456653 -0.674872 0.579668 0.534176 -0.66113 0.526843 0.532021 -0.652182 0.540012 +0.0303103 -0.387289 -0.92146 0.0135928 -0.637933 0.769972 0.12926 -0.58315 0.802015 +0.138064 -0.770407 -0.622424 0.2685 -0.862465 -0.429024 0.0744254 -0.989846 -0.121099 +0.29954 -0.945665 -0.126462 0.451682 -0.846683 -0.281267 0.295601 -0.91206 0.284195 +0.0302259 -0.99925 0.0242248 0.0161169 -0.999773 0.0139331 0.0184693 -0.999728 0.0142159 +0.283885 -0.951762 0.116444 0.294639 -0.935616 -0.19445 0.035664 -0.887549 -0.45933 +0.195948 -0.655833 0.729031 0.28342 -0.751756 0.595429 0.0597918 -0.764802 -0.641485 +0.201568 -0.770955 -0.604151 0.101977 -0.951868 0.289046 -0.0118397 -0.999922 0.00396394 +0.00729075 -0.930247 -0.366861 0.338466 0.919495 0.199924 0.529317 0.754155 0.388682 +0.574834 0.808442 0.126439 0.643635 0.763775 0.0487954 0.444834 0.893481 0.061767 +0.609698 0.735618 0.295185 0.523404 0.822351 0.223132 0.545233 0.834604 0.0784613 +0.483275 0.875439 0.00720672 0.506599 0.615791 -0.603456 0.55293 0.756417 -0.349429 +0.608629 0.563767 -0.558335 0.426527 0.646532 0.632512 0.160131 0.361226 0.918626 +0.551468 0.612043 -0.566822 0.595811 0.549074 -0.586112 0.551691 0.61068 -0.568073 +0.00696203 0.334941 0.942213 0.736773 0.393312 -0.549975 0.582679 0.559095 -0.589829 +0.61118 0.559363 -0.559975 0.0585917 0.584093 0.809569 -0.0192993 0.408306 0.912641 +0.835881 0.39576 -0.380365 0.602309 0.570052 -0.558806 0.768633 0.496203 -0.403714 +0.621049 0.546051 -0.562252 -0.0480159 0.100641 0.993763 0.536929 0.64871 -0.539335 +0.586734 0.569698 -0.575488 0.578993 0.673077 -0.460145 -0.556762 0.227178 0.799004 +-0.262238 0.135895 0.955387 0.484715 0.815855 -0.315328 0.541708 0.594775 -0.593966 +0.556452 0.735147 -0.387195 0.602141 0.622257 -0.500223 -0.569302 0.316852 0.758617 +-0.377096 0.17107 0.910238 0.642564 0.733217 -0.222496 0.665417 0.637094 -0.389015 +0.643862 0.711422 -0.281639 0.677004 0.667866 -0.309226 0.63454 0.655327 -0.409763 +-0.540194 0.352589 0.764115 -0.512621 0.287013 0.809224 0.645608 0.732097 -0.217311 +0.691654 0.634272 -0.345418 0.563367 0.731024 -0.384996 0.697333 0.653377 -0.294661 +0.430787 0.576017 -0.694714 -0.755568 0.302411 0.58109 -0.586757 0.217988 0.77987 +0.284377 0.80627 -0.518708 0.36967 0.782055 -0.501732 -0.857974 0.0352955 0.512479 +-0.803605 0.107948 0.585291 -0.656669 0.170833 0.734575 0.380512 0.808133 -0.449591 +0.523182 0.689079 -0.501449 0.569523 0.704765 -0.423023 0.542786 0.65516 -0.525498 +-0.727002 0.134562 0.673321 -0.624685 0.16988 0.762174 0.371173 0.845532 -0.383804 +0.376216 0.610547 -0.696918 0.291908 0.817979 -0.495681 0.229155 0.518798 -0.823612 +-0.674087 0.214396 0.706853 -0.517432 0.0125168 0.855633 0.299007 0.0939204 -0.949618 +-0.515301 -0.646398 0.562703 -0.592318 -0.353818 0.723859 -0.708048 -0.554814 0.436862 +-0.00843202 -0.0225826 -0.999709 -0.514533 -0.823991 0.237265 -0.447496 -0.893635 0.0341086 +-0.386174 -0.922271 -0.0169235 -0.403938 -0.914037 0.0370296 -0.513657 -0.82417 0.238536 +-0.586994 -0.790933 0.17281 -0.451167 -0.889743 0.069321 -0.579405 -0.813426 0.0512614 +-0.479919 -0.862046 0.162957 -0.596913 -0.748008 0.290135 -0.53896 -0.709452 0.454093 +-0.0632903 -0.00801929 -0.997963 -0.367618 -0.847437 0.383025 -0.0241886 -0.904787 0.425178 +-0.045273 -0.806454 0.58956 -0.633056 0.261647 -0.728547 -0.538529 -0.0391641 -0.841696 +-0.116034 -0.90409 0.411287 -0.399307 -0.827306 0.395119 -0.51417 -0.696021 0.501183 +-0.558973 -0.714971 0.41996 -0.667452 -0.273528 -0.692596 -0.715853 -0.389898 -0.579253 +-0.436336 -0.860946 0.2615 -0.40134 -0.913848 0.0617137 -0.534854 -0.810461 0.238922 +-0.591957 -0.769851 0.23857 -0.534703 -0.800742 0.27001 -0.566455 -0.761414 0.315242 +-0.560612 -0.736076 0.379349 -0.543741 -0.746053 -0.384383 -0.445971 -0.892468 -0.0679069 +-0.629769 -0.776724 -0.00951686 -0.592785 -0.697075 0.403352 -0.652773 -0.622341 -0.431948 +-0.717702 -0.643098 -0.267074 -0.658172 -0.685041 -0.312295 -0.756376 -0.614499 -0.224248 +-0.632852 -0.649992 -0.420724 -0.432014 -0.866703 0.249378 -0.346335 -0.937878 0.0209242 +-0.433927 -0.88498 -0.168871 -0.692817 -0.624269 0.360961 -0.638841 -0.734474 0.228978 +-0.721776 -0.687097 0.0832899 -0.688507 -0.723621 -0.0482839 -0.695179 -0.681863 -0.227571 +-0.665074 -0.6553 -0.358131 -0.754037 0.644888 0.124689 -0.755573 0.627396 -0.188368 +-0.576809 0.765472 0.285209 -0.573687 0.785559 0.231906 -0.538492 0.72641 0.427031 +-0.416869 0.847845 0.327688 -0.43456 0.889046 -0.144066 -0.796929 0.593934 -0.110207 +-0.303482 0.819664 -0.485849 -0.494805 0.816024 0.298786 -0.707056 0.613244 0.352143 +-0.0357172 0.998583 0.0394461 0.0327311 0.999463 -0.00139758 -0.00775699 0.999968 0.00195341 +-0.0429348 0.999078 -0.000541048 -0.0953399 0.994949 -0.0314205 -0.164197 0.880672 -0.44436 +-0.254093 0.560857 -0.787957 -0.0696652 0.941151 0.330728 -0.0124398 0.762867 0.646435 +-0.0250911 0.999244 0.029684 -0.0149585 0.999819 -0.0117747 -0.0213877 0.999756 0.00548902 +-0.0286675 0.999572 -0.0058774 -0.0189375 0.99857 -0.0499985 -0.261295 0.916027 -0.304334 +-0.471875 0.728673 -0.496357 -0.451864 0.747846 -0.48636 -0.444337 0.815723 0.370351 +-0.252117 0.546798 0.798404 -0.514996 0.818853 0.253494 -0.443173 0.832905 0.331462 +-0.468023 0.883716 0.00105591 -0.371623 0.551357 -0.746929 -0.239972 0.532654 -0.811599 +-0.0107903 0.639764 0.768495 -0.00775679 0.312611 0.949849 -0.287617 0.916699 -0.27738 +-0.140541 0.978116 -0.15342 -0.216849 0.884971 -0.412072 -0.267734 0.788122 -0.55424 +-0.568501 0.772751 0.282247 -0.554023 0.779039 0.293525 -0.35615 0.555476 0.751401 +-0.279413 0.546499 -0.789473 -0.109846 0.28364 -0.952619 -0.00547055 0.0653141 0.99785 +-0.135913 0.951168 -0.277143 -0.0344947 0.998375 -0.0453582 -0.0822981 0.995843 0.0390381 +-0.299904 0.941353 0.154637 -0.300985 0.807884 0.506687 0.000499888 0.0587964 -0.99827 +0.02473 0.357489 0.93359 0.00934161 -0.0389056 -0.999199 0.0884648 0.579243 0.81034 +0.186535 -0.0384309 0.981696 -0.112743 -0.305479 -0.945501 0.234969 -0.607353 0.758888 +0.206691 -0.401493 0.892234 -0.204785 -0.511998 -0.834219 -0.195539 -0.589536 -0.783716 +-0.312228 -0.587194 -0.746804 0.0293077 -0.0268542 0.99921 0.0172698 -0.0780816 -0.996797 +0.00564684 -0.0341334 0.999401 0.0490859 0.0307916 -0.99832 -0.0135579 -0.357023 0.933997 +0.0433505 0.00443936 -0.99905 -0.0172261 -0.611356 0.791168 -0.00281511 -0.369877 0.929076 +0.0420027 -0.00311155 -0.999113 -0.00742597 -0.0408694 0.999137 0.0426372 -0.000376196 -0.99909 +-0.130187 -0.299095 0.945301 0.0425822 -0.00571939 -0.999076 -0.389037 -0.591859 0.705941 +-0.322591 -0.59686 0.734638 -0.20136 -0.467351 0.860835 0.0357695 -0.0040543 -0.999352 +0.11997 0.251581 0.960372 0.0140015 0.0244434 -0.999603 0.167894 0.611995 0.772835 +0.260827 0.475027 0.840428 0.386694 0.58666 0.711546 -0.011087 -0.0428619 -0.99902 +0.0377602 0.0782825 0.996216 -0.0443822 -0.388679 -0.920304 -0.0104179 -0.0315251 0.999449 +-0.0485634 -0.382706 -0.922593 -0.0732077 -0.609874 -0.78911 -0.00166331 -0.00500391 0.999986 +0.00609825 -0.042167 -0.999092 0.00117158 0.00361269 0.999993 0.0144687 0.0224892 -0.999642 +5.09646e-05 0.000422562 1 0.122707 0.325172 -0.93766 -0.00046755 0.000331633 1 +0.388141 0.573384 -0.721511 0.308964 0.596822 -0.740503 0.19914 0.477705 -0.855652 +0.00235992 0.00410578 0.999989 0.0375851 0.0819293 -0.995929 0.0103621 0.00365767 0.99994 +-0.0096889 -0.0317078 -0.99945 -0.043624 -0.0376821 0.998337 -0.00161686 0.0032328 -0.999994 +-0.37855 -0.0173629 0.925418 -0.0283945 -0.0153557 -0.999479 -0.434456 0.536883 0.72319 +-0.385709 0.441563 0.810093 -0.145498 -0.321848 -0.935545 -0.523498 0.521808 0.673547 +-0.282355 0.305185 0.909471 -0.277113 -0.454063 -0.846779 -0.116058 -0.637134 -0.761965 +-0.41497 -0.558466 -0.718272 -0.0451983 0.0648902 0.996868 -0.000589281 -0.0528462 -0.998603 +-0.0963907 -0.073049 0.992659 0.586595 -0.751211 -0.302635 0.594106 -0.801793 -0.0645405 +0.574273 -0.818539 0.01433 0.559682 -0.828352 -0.0242529 0.565609 -0.824606 -0.010574 +0.595336 -0.754523 0.276171 0.056219 0.00686391 -0.998395 -0.296293 -0.442724 0.84629 +0.504505 -0.69054 -0.518295 0.503025 -0.673826 -0.541225 0.352317 -0.866669 -0.353211 +0.0882603 -0.994792 -0.0509771 0.0764484 -0.996487 -0.0341892 0.304461 -0.945958 -0.111651 +0.362457 -0.931972 -0.00729319 0.345256 -0.938419 -0.0129663 0.383847 -0.923372 -0.00670413 +0.517164 -0.849672 0.102954 0.589356 -0.64971 0.480142 0.532715 -0.663869 0.524874 +0.560482 -0.614078 0.555668 0.0103119 -0.400928 -0.916052 -0.164317 -0.60194 0.781453 +-0.186708 -0.385534 0.903606 0.423178 -0.716499 -0.554571 0.505821 -0.701397 -0.502182 +0.362355 -0.873906 -0.324018 0.138091 -0.990238 -0.0189722 0.350234 -0.934586 -0.06233 +0.401019 -0.823707 -0.400863 0.289807 -0.911954 0.290435 0.028279 -0.999262 0.0259821 +-0.0023397 -0.999961 -0.0084981 -0.0132201 -0.999389 0.0323413 0.169263 -0.951979 0.25512 +0.257196 -0.789365 0.557453 0.495449 -0.794317 0.351556 0.585226 -0.8048 -0.0990353 +0.37303 -0.737077 -0.56353 0.119308 -0.71779 -0.685962 0.34837 -0.640674 0.684234 +0.147234 -0.457502 0.876934 0.137111 -0.792265 -0.594573 0.324987 -0.826086 -0.460397 +0.406052 -0.807511 -0.427841 0.353172 -0.916381 0.188453 0.281272 -0.882207 -0.37762 +0.228472 -0.88186 0.412461 0.123428 -0.987923 0.0936702 0.30683 -0.95057 -0.0476613 +0.200933 -0.913472 -0.353829 0.701175 0.642367 0.309382 0.741912 0.505106 0.440948 +0.914045 0.373905 0.157213 0.938949 0.343914 0.00991863 0.74833 0.659789 0.0684119 +0.937182 0.348835 -0.00194912 0.750844 0.660356 -0.0127708 0.934461 0.356041 0.00423009 +0.750146 0.661231 -0.00737562 0.929485 0.368831 0.00458648 0.745878 0.666056 -0.00600614 +0.936665 0.349505 -0.0224633 0.772573 0.634669 0.0180473 0.937733 0.346124 0.029242 +0.800566 0.599239 0.00250494 0.937079 0.349023 -0.00818221 0.787294 0.616576 0.00133682 +0.957283 0.282064 -0.0636291 0.758561 0.651558 -0.00758993 0.884461 0.203097 -0.420097 +0.758806 0.621888 -0.193568 0.745525 0.365932 -0.557033 0.739472 0.407994 0.535464 +0.813822 0.0825775 0.575217 0.910501 0.208411 0.357145 0.995332 0.0810153 0.0524549 +0.997306 0.0733534 -0.000671698 0.998306 0.0581224 0.00275887 0.999174 0.0398349 -0.00800606 +0.999976 -0.00460369 -0.00521358 0.999439 0.00751338 -0.0326339 0.963151 -0.267957 -0.0232357 +0.999235 -0.0391107 0.000452303 0.954388 -0.260998 -0.144995 0.898844 -0.336607 -0.28067 +0.799319 -0.145663 -0.582986 0.872382 -0.18111 0.454036 0.748941 -0.322085 0.579093 +0.991801 -0.0749137 0.103532 0.998305 -0.0552374 -0.0183527 0.997598 -0.0675979 -0.0151176 +0.996929 -0.0692134 -0.0366488 0.941135 -0.292346 -0.1697 0.881034 -0.432869 -0.190799 +0.786949 -0.607094 0.110217 0.756991 -0.598557 -0.262096 0.786702 -0.567308 0.243437 +0.859546 -0.507601 0.0593549 0.796069 -0.555266 -0.240736 0.792807 -0.541171 -0.280342 +0.797128 -0.489337 -0.353748 0.696566 -0.476663 0.536272 0.694139 -0.575909 0.431857 +0.842779 -0.447381 0.299289 0.919602 -0.391319 0.0346707 0.926161 -0.376256 -0.0256263 +0.895159 -0.415112 -0.162396 0.779264 -0.566335 -0.26835 0.742097 -0.56734 -0.356955 +0.774774 -0.559844 -0.293769 0.79741 -0.598412 -0.0777196 0.778627 -0.605829 0.163434 +0.795633 -0.605709 0.00923256 0.792446 -0.603653 -0.087359 0.755014 -0.590342 -0.285395 +0.794285 0.521977 0.310889 0.728275 0.523729 0.441954 0.900335 0.40843 0.15027 +0.923659 0.38281 -0.0176105 0.836668 0.544833 0.0560591 0.859303 0.417197 -0.29588 +0.799342 0.577608 -0.165594 0.7177 0.553244 -0.42288 0.669423 0.486003 0.561849 +0.57741 0.625272 0.525008 0.768812 0.560078 0.308609 0.81533 0.574482 0.0721592 +0.868111 0.496247 -0.0110819 0.880156 0.392287 -0.267276 0.73118 0.427476 -0.53164 +0.838687 0.250806 -0.483426 0.749978 0.303858 -0.58754 0.175629 0.889033 0.422818 +0.256067 0.488991 0.833857 0.39315 0.743407 0.54109 0.135593 0.98315 0.122602 +0.304464 0.917378 0.256359 0.754061 0.404161 -0.517732 0.740617 0.286691 -0.607697 +0.302756 0.195433 0.932815 0.548021 0.607359 -0.575143 0.613275 0.51676 -0.597372 +0.621658 0.560866 -0.546782 0.277237 0.402865 0.872261 0.545793 0.58982 -0.595165 +0.583108 0.581151 -0.56767 0.607033 0.559213 -0.564616 0.378639 0.516279 0.768172 +0.135699 0.30993 0.941026 0.546052 0.563696 -0.619737 0.579557 0.572719 -0.579747 +0.637667 0.554523 -0.534682 -0.010972 0.0244968 0.99964 0.629506 0.742868 -0.227747 +0.675255 0.632007 -0.380262 0.634746 0.727761 -0.259734 0.740914 0.639496 -0.205162 +0.702383 0.605255 -0.374599 -0.0488104 0.0142973 0.998706 0.69391 0.676214 -0.247434 +0.687371 0.629087 -0.363002 0.686901 0.671551 -0.277826 0.726075 0.649342 -0.226208 +0.712419 0.594089 -0.373521 -0.0782962 -0.0102325 0.996878 0.675195 0.696932 -0.241655 +0.691126 0.63563 -0.343975 0.650178 0.639494 -0.410264 0.683264 0.628663 -0.371395 +0.596133 0.441946 -0.670305 -0.621678 0.15428 0.767929 -0.470023 -0.0381421 0.88183 +0.541166 0.333494 -0.771959 -0.68356 0.102821 0.722615 -0.714667 0.0269715 0.698944 +0.332546 0.761114 -0.556883 0.589208 0.778857 -0.214979 0.583341 0.780213 -0.22579 +0.651783 0.721261 -0.234439 0.761467 0.634479 -0.132685 0.703699 0.578919 -0.411898 +-0.63954 0.3091 0.703879 -0.708797 0.217251 0.671125 0.527605 0.666344 -0.526895 +0.634164 0.533691 -0.559473 0.645605 0.568203 -0.510235 -0.694361 0.317347 0.645874 +-0.602981 0.179677 0.777258 0.385082 0.739684 -0.551887 0.157679 0.450875 -0.878549 +-0.797223 -0.118518 0.591937 -0.72222 -0.173053 0.669664 -0.803919 -0.473918 0.359327 +-0.786305 -0.453298 0.419816 0.482703 -0.279826 -0.829877 0.285588 0.515858 -0.80767 +0.00101358 -0.00989378 -0.99995 -0.781427 -0.579311 0.231885 -0.749233 -0.608909 0.260536 +-0.694281 -0.71112 0.110827 -0.754687 -0.653631 0.0566818 -0.611603 -0.791008 -0.0157549 +-0.735235 -0.677806 0.00279397 -0.628588 -0.773518 0.0809057 -0.715257 -0.628475 0.305657 +-0.5963 -0.732217 0.329066 -0.602196 -0.687134 0.406457 -0.00436141 -0.319842 -0.947461 +-0.295993 -0.516708 -0.803368 -0.472758 -0.849569 0.233948 -0.426225 -0.903003 0.0540289 +-0.527158 -0.769971 0.359513 -0.465438 -0.665751 0.583218 -0.370249 -0.558627 0.742194 +-0.38495 -0.341908 -0.85727 -0.253258 -0.793403 0.553509 -0.355025 -0.769591 0.530742 +-0.442824 -0.539256 -0.716317 -0.527875 -0.516943 -0.673883 -0.693196 -0.412424 -0.591089 +-0.232492 -0.899002 0.371137 -0.361637 -0.927415 0.095505 -0.605959 -0.77991 0.156696 +-0.624512 -0.769022 0.136346 -0.647019 -0.742061 0.175249 -0.616206 -0.783966 0.0754102 +-0.469617 -0.852691 0.228865 -0.582002 -0.763952 0.278659 -0.522898 -0.77485 0.355225 +-0.629693 -0.656753 -0.414924 -0.464887 -0.737475 -0.489909 -0.689494 -0.587941 -0.422993 +-0.833524 -0.32376 -0.447681 -0.521085 -0.799507 0.298763 -0.593118 -0.776106 0.214172 +-0.61877 -0.763602 0.18449 -0.736762 -0.676125 0.0061132 -0.64116 -0.760723 -0.101071 +-0.662025 -0.740834 -0.113524 -0.578514 -0.759307 -0.29795 -0.44146 -0.886687 0.137473 +-0.401971 -0.897957 0.179146 -0.638951 -0.733555 0.231601 -0.593177 -0.800166 -0.0887394 +-0.687927 -0.721405 -0.0795746 -0.684661 -0.682876 -0.254793 -0.65321 -0.664084 -0.363744 +-0.456832 0.741375 0.491597 -0.570502 0.749252 0.336375 -0.337233 0.923329 -0.183675 +-0.439436 0.79872 -0.411026 -0.468389 0.70943 0.526613 -0.490306 0.673039 0.553732 +-0.355879 0.858357 0.369558 -0.0783205 0.995525 0.0528743 -0.0186308 0.999777 -0.00989554 +-0.0159482 0.998887 -0.0443924 -0.121682 0.961536 -0.246255 -0.246539 0.735607 -0.630952 +-0.314937 0.710868 -0.628873 -0.414364 0.873517 0.255479 -0.16265 0.7993 0.578502 +-0.370506 0.928612 0.0201079 -0.369806 0.929057 -0.00980948 -0.375233 0.926873 0.0102898 +-0.368366 0.929331 -0.0254992 -0.380152 0.893588 -0.238713 -0.549554 0.72666 -0.412257 +-0.486356 0.715736 -0.501178 -0.547593 0.682665 0.48385 -0.597243 0.777552 0.196756 +-0.54713 0.741975 0.387456 -0.424653 0.820067 0.383615 -0.381818 0.830349 0.405876 +-0.183943 0.959772 -0.212137 -0.349654 0.868138 0.352248 -0.31175 0.602818 -0.734454 +-0.170695 0.628964 -0.758464 -0.240066 0.748509 0.618144 -0.0355053 0.321227 0.946336 +-0.299501 0.911653 -0.281404 -0.0381225 0.994279 -0.099775 -0.142141 0.947098 -0.287752 +-0.141972 0.815118 -0.561628 -0.313632 0.791096 -0.525169 -0.536906 0.739491 0.406061 +-0.351636 0.579311 0.735358 -0.462886 0.827661 0.317355 -0.084999 0.602163 -0.793836 +-0.0396175 0.354836 -0.934089 0.0197894 0.108801 0.993867 -0.192249 0.896948 -0.398153 +-0.0338553 0.998074 -0.051988 0.00290551 0.999984 -0.00490413 0.0337056 0.9994 -0.00800733 +-0.00716607 0.999879 0.0138281 -0.200882 0.919302 0.338422 -0.00422398 0.0796726 -0.996812 +0.181624 0.457319 0.870558 0.00351597 -0.0489226 -0.998796 0.28954 0.622508 0.727084 +0.18244 0.326948 0.927265 -0.0509602 -0.39287 -0.918181 0.348008 -0.591919 0.726995 +0.186991 -0.404469 0.895231 -0.0771539 -0.371651 -0.925161 -0.0870547 -0.609577 -0.787932 +0.296962 -0.575917 0.761665 0.126754 -0.262164 0.956663 -0.0136965 -0.0277277 -0.999522 +0.00223528 -0.0743382 0.997231 -0.0148579 0.0198074 -0.999693 0.000269283 -0.359385 0.933189 +-0.0221271 -0.000903099 -0.999755 -0.00604414 -0.604374 0.796678 0.00523403 -0.356622 0.934234 +-0.0224322 -0.00183573 -0.999747 0.0025424 -0.0697613 0.99756 -0.0219675 -0.000295161 -0.999759 +-0.0105307 -0.358349 0.933528 -0.0227488 -0.00538822 -0.999727 -0.0805291 -0.61669 0.783077 +-0.0200003 -0.343854 0.93881 -0.0167978 0.000557825 -0.999859 0.0454819 0.342915 0.938265 +0.00225295 0.0273698 -0.999623 0.0793458 0.619802 0.780736 0.0507989 0.387664 0.9204 +-0.0325081 -0.0819827 -0.996103 -0.00959661 0.0266342 0.999599 -0.186716 -0.478279 -0.85813 +-0.00780091 -0.0226179 0.999714 -0.115717 -0.324395 -0.938817 -0.29498 -0.600098 -0.743552 +0.00040208 0.000998705 0.999999 -0.0111607 -0.0231062 -0.999671 0.000758848 0.00251139 0.999997 +-0.00543445 0.0412913 -0.999132 -0.000238718 -0.000339841 1 0.0475724 0.386011 -0.921267 +0.000789135 0.00200292 0.999998 0.07636 0.611694 -0.787401 0.0377618 0.383025 -0.922966 +0.00470113 0.00974932 0.999941 -0.00855414 0.0280147 -0.999571 -0.0135297 -0.0266224 0.999554 +-0.0090127 -0.023951 -0.999672 0.0296589 -0.270159 0.962359 -0.00752621 0.000922135 -0.999971 +-0.530704 0.282317 0.799156 -0.301528 -0.51485 0.802503 0.0010228 -0.0206401 0.999786 +0.0336548 0.0154292 -0.999314 0.271022 0.508071 0.817564 -0.11792 0.37541 0.919327 +0.4192 -0.282393 -0.862859 -0.502063 0.490242 0.712457 -0.326618 0.337786 0.882735 +0.200573 -0.346498 -0.916357 0.365916 -0.548087 -0.752135 -0.533566 0.412998 0.738065 +-0.386132 0.221608 0.895428 0.380391 -0.00571359 -0.924808 -0.46408 -0.180425 0.867223 +0.288636 -0.156538 -0.944556 -0.432811 -0.565831 0.70179 -0.541554 -0.481468 0.689135 +-0.393405 -0.35316 0.848829 0.511694 -0.675067 -0.531463 0.54675 -0.699754 -0.459792 +0.461603 -0.857936 -0.225541 0.306126 -0.940044 -0.150345 0.0797443 -0.995899 -0.0427263 +0.0370152 -0.999298 -0.00579699 0.0129352 -0.999916 -0.000277518 0.0313303 -0.998465 0.045685 +0.25908 -0.919628 0.295232 0.113336 -0.812581 0.571722 0.453836 -0.708932 0.539859 +0.0158231 -0.458885 -0.888355 -0.0360643 -0.0820967 0.995972 0.50402 -0.690462 -0.51887 +0.520474 -0.754611 -0.399586 0.454603 -0.887061 0.0803632 0.549752 -0.786764 0.28067 +0.558568 -0.813375 0.162553 0.572372 -0.756868 -0.3155 0.401466 -0.801368 -0.443434 +0.415525 -0.822454 -0.38847 0.206415 -0.904657 0.372813 0.0347945 -0.998724 0.0365935 +-0.0026837 -0.999967 -0.0076886 0.000236488 -0.999967 -0.00817519 0.0348518 -0.998566 0.0406396 +0.174079 -0.897762 0.404624 0.31095 -0.725611 0.61384 0.0399863 -0.753196 -0.65658 +-0.0137328 -0.638928 -0.769144 0.0148571 -0.37311 0.927668 0.420081 -0.731403 -0.537198 +0.519139 -0.713723 -0.470206 0.395042 -0.809526 -0.434292 0.444285 -0.812672 -0.377061 +0.448534 -0.891912 -0.0575303 0.156585 -0.956312 0.246879 0.0123805 -0.956256 -0.292268 +0.0989785 -0.830034 0.54886 0.328377 -0.895014 0.301859 0.48855 -0.78802 0.374625 +0.593856 -0.79514 0.12283 0.575206 -0.740344 -0.347891 0.394895 -0.813026 -0.427839 +0.3846 -0.830021 -0.403915 0.435686 -0.896564 -0.0796906 0.522643 0.850311 0.0617682 +0.725464 0.619741 0.299372 0.834021 0.548376 -0.0607648 0.651951 0.700944 -0.289201 +0.741212 0.621504 0.25365 0.664234 0.562066 0.492823 0.865585 0.449346 0.221021 +0.753527 0.643079 0.136551 0.752713 0.657375 0.035802 0.674453 0.738308 0.00382747 +0.72143 0.692117 -0.0226592 0.673641 0.738997 -0.00952378 0.699335 0.714636 -0.0150659 +0.67257 0.739954 -0.0108705 0.697624 0.716365 -0.0118677 0.687071 0.724927 0.0491366 +0.700076 0.713894 0.015782 0.797513 0.588051 0.134792 0.716491 0.661205 0.222371 +0.845093 0.534494 -0.0115489 0.732199 0.615985 -0.290598 0.753783 0.648379 -0.106844 +0.682547 0.729533 -0.0437121 0.714416 0.69505 -0.0807161 0.801296 0.568205 -0.187265 +0.732192 0.681024 0.0100866 0.933174 0.358061 -0.0312863 0.760989 0.648762 0.00185181 +0.942559 0.333813 0.0123351 0.764272 0.644888 0.002552 0.941525 0.336843 0.00815403 +0.756212 0.654317 0.00370452 0.930946 0.365013 0.01022 0.753644 0.657121 -0.0145988 +0.901724 0.431662 -0.0236886 0.650028 0.748087 -0.133528 0.825953 0.471123 -0.309589 +0.527131 0.819955 -0.223175 0.554575 0.667307 -0.497139 0.649097 0.535159 0.540627 +0.66706 0.524825 0.528763 0.844277 0.380582 0.377298 0.786088 0.478353 0.391463 +0.935559 0.131957 -0.327591 0.99861 0.050786 -0.0141 0.999143 0.039996 0.0106547 +0.999143 0.0389304 0.0140555 0.998867 0.0290928 0.037668 0.995792 0.0487443 -0.0776012 +0.625597 0.522872 -0.578994 0.860799 0.210733 -0.463268 0.728904 0.363158 -0.580358 +0.604282 0.467603 0.645128 0.569235 0.207513 0.795556 0.87948 0.223157 0.420376 +0.875013 0.235499 0.422957 0.953419 -0.15832 -0.256761 0.999478 0.001953 -0.0322423 +0.99988 0.00202083 0.0153258 0.999962 0.00370122 0.00789397 0.999503 -0.0306618 -0.00728878 +0.996389 -0.0797341 -0.0291914 0.94978 -0.238984 -0.201999 0.815639 0.205157 -0.540965 +0.915157 0.0389108 0.401214 0.679104 0.199709 0.706353 0.931319 -0.0962007 0.35127 +0.874839 -0.481795 -0.0503071 0.902146 -0.391282 -0.181745 0.956985 -0.250861 -0.145771 +0.998232 -0.043879 -0.0401002 0.998837 -0.0478778 -0.00568977 0.998842 -0.0439033 0.0196526 +0.998716 -0.0505778 -0.00293342 0.997289 -0.0493778 -0.0545508 0.890017 -0.333137 -0.311269 +0.79329 -0.608743 0.0110853 0.689379 -0.575128 -0.440437 0.739815 -0.577752 0.344785 +0.950805 -0.215745 -0.222315 0.789146 0.332756 -0.516258 0.767366 0.117587 -0.630335 +0.955629 0.12643 0.266061 0.782351 0.292316 0.54998 0.996827 -0.0368421 0.070557 +0.855997 -0.36329 0.367818 0.888649 -0.404887 -0.215337 0.953047 -0.270552 -0.136026 +0.997028 -0.0767729 0.00640089 0.957959 -0.286221 0.0197826 0.994151 -0.10677 -0.0162539 +0.995367 -0.0941384 -0.019567 0.997338 -0.0571811 -0.0452539 0.95348 -0.265382 -0.142999 +0.932277 -0.361596 -0.010414 0.930254 -0.366852 0.00687509 0.901644 -0.397751 -0.169799 +0.713516 -0.485173 -0.505472 0.704533 -0.523475 -0.479173 0.805693 -0.524042 -0.276114 +0.702558 0.306494 0.642242 0.690413 0.00345964 0.723407 0.889199 0.0506455 0.454708 +0.993428 -0.0508404 0.102547 0.999619 0.0220031 -0.0166704 0.999526 -0.0218678 0.0216625 +0.937873 -0.305136 0.165183 0.932356 -0.360701 0.0246545 0.931928 -0.361754 -0.0253819 +0.950818 -0.284451 -0.122609 0.998799 -0.04254 -0.0243144 0.999494 -0.0314833 0.00451762 +0.999664 0.00159209 -0.0258699 0.999069 -0.0428513 -0.00504972 0.947465 -0.277384 -0.159276 +0.937684 -0.345819 -0.0340378 0.913926 -0.378335 -0.146976 0.798567 -0.577116 -0.170964 +0.761067 -0.540791 -0.358221 0.792993 -0.608945 0.0186514 0.789402 -0.61085 -0.0608788 +0.740966 -0.595874 -0.309682 0.745567 -0.385079 0.543915 0.711254 -0.586959 0.386777 +0.859536 -0.420404 0.290618 0.928935 -0.339868 0.146864 0.997611 -0.066002 0.0204004 +0.999029 -0.0439408 -0.00334771 0.997233 -0.0738094 -0.00889812 0.996781 -0.0800255 -0.00488672 +0.998884 -0.0455574 0.0124582 0.999656 0.025437 -0.00633391 0.999919 0.0114531 -0.00559775 +0.999085 -0.0211882 -0.0371552 0.902984 -0.297013 -0.310489 0.801048 -0.580404 -0.14647 +0.735229 -0.536482 -0.414278 0.801048 -0.594144 -0.0729096 0.768113 -0.574675 -0.282402 +0.766861 -0.602851 0.220213 0.729994 -0.599275 0.3286 0.720518 -0.57822 0.382773 +0.842552 -0.472393 0.258749 0.928855 -0.368574 0.0371813 0.935056 -0.35384 -0.021604 +0.933564 -0.35839 -0.00380928 0.94266 -0.30203 0.14202 0.998807 -0.0327152 0.0362581 +0.99876 -0.00737016 -0.0492266 0.898067 -0.288776 -0.331788 0.695291 -0.508953 -0.507482 +0.677647 -0.520984 -0.519009 0.778434 -0.601383 0.179942 0.797355 -0.603502 0.00310385 +0.802086 -0.59659 -0.0271818 0.78632 -0.616143 0.0454856 0.743555 -0.548208 0.382876 +0.778454 -0.210167 0.591472 0.941134 -0.193865 0.276919 0.949359 -0.140713 -0.280922 +0.695922 -0.471907 -0.541292 0.730362 -0.311849 -0.607718 0.938328 -0.00297475 0.345734 +0.781131 0.0744635 0.619911 0.937799 0.018101 -0.346707 0.781669 -0.0659524 -0.620196 +0.94714 0.139736 0.288791 0.732715 0.310377 0.605635 0.940359 0.167168 -0.296278 +0.764952 0.194523 -0.61401 0.705068 0.474447 0.527047 0.712716 0.50325 0.488647 +0.906219 0.270339 0.325089 0.997155 -0.00577172 0.0751566 0.997069 0.0371791 -0.0668724 +0.711003 0.384721 -0.588613 0.842906 0.22391 -0.489259 0.731015 0.324098 -0.600481 +0.744314 0.401698 0.533512 0.803669 0.140919 0.578151 0.895171 0.232668 0.380177 +0.992946 0.110962 0.041787 0.998643 0.0471694 0.0220517 0.999844 -0.00281202 -0.0174314 +0.932724 0.166607 -0.319795 0.766598 0.232825 -0.598431 0.868145 0.0926214 0.487592 +0.781695 0.0632688 0.620443 0.941902 0.284637 0.178332 0.927043 0.373714 -0.0304658 +0.936627 0.311388 -0.160519 0.992088 0.0940253 -0.083187 0.701866 0.435351 -0.563786 +0.860013 0.356766 -0.364823 0.679532 0.534638 -0.502393 0.704025 0.399384 0.587232 +0.703823 0.300773 0.643559 0.870274 0.329921 -0.365752 0.987969 0.0803583 -0.132136 +0.692405 0.488664 -0.530833 0.876233 0.18236 -0.446049 0.722077 0.313333 -0.616788 +0.713941 0.444227 0.54125 0.526944 0.735551 0.425787 0.503391 0.607839 0.614108 +0.932434 0.117604 -0.341665 0.92831 0.00513277 -0.371771 0.795555 0.0439146 -0.604288 +0.465827 0.794577 0.389427 0.463497 0.514035 0.721761 0.378556 0.902312 0.206222 +0.832115 0.294868 -0.469721 0.922187 0.1979 -0.332274 0.819869 0.156111 -0.550858 +0.701119 0.209118 0.681691 0.519882 0.202952 0.829779 0.582346 0.286408 0.760818 +0.83847 0.388601 -0.382043 0.780018 0.510192 -0.362321 0.926864 0.336532 -0.166341 +0.866041 0.429384 -0.256129 0.776228 0.600434 -0.19222 0.703019 0.5777 -0.414761 +0.547233 0.249089 0.799056 0.34285 0.198972 0.918076 0.864973 0.368544 -0.340584 +0.98951 0.128265 -0.0664682 0.692883 0.502719 -0.5169 0.861934 0.350989 -0.36589 +0.675276 0.499535 -0.542648 0.040032 0.0206162 0.998986 0.88553 0.415734 -0.207368 +0.923156 0.337881 -0.183354 0.685987 0.497996 -0.530492 0.819791 0.438352 -0.368497 +0.712444 0.530593 -0.459232 -0.00457394 -0.00306235 0.999985 0.828801 0.492569 -0.26545 +0.726152 0.60893 -0.319229 0.812435 0.527935 -0.247456 0.797137 0.51076 -0.32202 +0.778321 0.586706 -0.22359 0.718938 0.554152 -0.419576 0.00606857 -0.013718 0.999888 +0.811537 0.507811 -0.289026 0.684766 0.522638 -0.507882 0.756151 0.501158 -0.420804 +0.685934 0.534817 -0.493422 -0.107718 -0.302496 0.947044 0.889976 0.320086 -0.324788 +0.724605 0.572803 -0.383202 0.914777 0.403619 -0.016584 0.812004 0.580859 -0.0570198 +0.847308 0.454603 -0.274601 0.795838 0.592059 -0.126916 0.71253 0.557407 -0.426144 +-0.602193 -0.190789 0.775218 -0.343351 -0.533443 0.773012 -0.302759 -0.485998 0.819844 +0.801999 0.504729 -0.319448 0.921919 0.386972 -0.0178296 0.920469 0.355795 -0.161698 +0.697904 0.495293 -0.517316 0.829905 0.378031 -0.410305 0.669194 0.496455 -0.552912 +-0.706786 0.0592457 0.704943 -0.558798 -0.254438 0.789308 0.822905 0.379485 -0.422869 +0.683842 0.496977 -0.534204 0.826564 0.304027 -0.473665 0.712798 0.352632 -0.606275 +-0.807634 0.138966 0.573076 -0.7661 0.0173817 0.642487 0.726026 0.431398 -0.53552 +0.672601 0.151348 -0.724363 -0.853325 -0.0849312 0.514415 -0.872852 -0.295233 0.388545 +-0.830727 -0.143569 0.537848 0.350209 -0.128983 -0.927748 -0.852097 -0.467701 0.234919 +-0.76788 -0.539237 0.345808 -0.756359 -0.644046 0.114565 -0.647108 -0.743481 0.16878 +-0.671752 -0.739465 0.0440584 -0.462557 -0.864926 0.194793 -0.645915 -0.716891 0.262416 +-0.608262 -0.672961 0.420882 -0.528576 0.263666 -0.8069 -0.342578 -0.136433 -0.92953 +-0.703345 -0.613562 0.358954 -0.623987 -0.765373 0.15762 -0.646611 -0.742362 0.17548 +-0.609385 -0.791842 0.0404364 -0.450039 -0.832965 0.321924 -0.565322 -0.690503 0.45124 +-0.257938 -0.552309 0.792731 -0.369586 -0.38632 -0.845082 -0.366014 -0.57058 0.735168 +-0.385376 -0.412749 -0.825302 -0.511808 -0.504092 -0.695661 -0.496717 -0.511899 -0.700879 +-0.355491 -0.756849 0.548458 -0.517993 -0.801141 0.299759 -0.55837 -0.814901 0.155436 +-0.408243 -0.912629 0.0211229 -0.587686 -0.808076 -0.0404733 -0.335911 -0.94187 -0.00666257 +-0.652609 -0.756182 0.0478581 -0.394798 -0.871021 0.292331 -0.561095 -0.726209 0.397232 +-0.198318 -0.788331 0.582413 -0.275782 -0.559019 -0.781948 -0.278107 -0.901429 0.331786 +-0.383589 -0.903244 0.192381 -0.576664 -0.796623 0.181249 -0.596307 -0.788673 -0.149709 +-0.6813 -0.731928 -0.0105231 -0.736242 -0.674085 -0.0596432 -0.712218 -0.70138 -0.0284908 +-0.648322 -0.74387 0.162283 -0.626863 -0.773188 0.096042 -0.496932 -0.826589 0.264213 +-0.525701 -0.800495 0.287831 -0.533166 -0.806859 0.254388 -0.603047 -0.700301 -0.381986 +-0.682348 -0.657155 -0.320233 -0.701925 -0.664782 -0.255667 -0.612631 -0.771604 -0.171201 +-0.671959 -0.667394 -0.321023 -0.440924 -0.841935 -0.311018 -0.477958 -0.876345 0.0597954 +-0.726325 0.680749 -0.0950436 -0.49749 0.681454 0.536772 -0.533941 0.704409 0.46767 +-0.466928 0.861394 0.199947 -0.383344 0.923513 0.0130798 -0.354424 0.934147 -0.0418715 +-0.267716 0.9077 -0.323125 -0.356248 0.755351 -0.550029 -0.527652 0.776299 0.344882 +-0.618175 0.784963 0.0411361 -0.608746 0.793231 -0.0146027 -0.612413 0.79051 0.00663481 +-0.617177 0.78682 0.00275335 -0.599598 0.782795 -0.166475 -0.462909 0.7369 0.492639 +-0.49619 0.758194 0.42301 -0.571501 0.812169 0.117337 -0.782396 0.622029 -0.0306146 +-0.625715 0.75004 -0.214289 -0.577854 0.808176 0.11374 -0.689996 0.687733 0.225675 +-0.368204 0.902558 0.223191 -0.425715 0.818495 0.385789 -0.186223 0.719839 -0.668694 +-0.146428 0.616453 -0.773657 -0.300832 0.800985 0.517613 -0.134992 0.430386 0.892494 +-0.353117 0.922913 0.153426 -0.379613 0.925055 -0.0129335 -0.377309 0.926069 -0.00589911 +-0.381687 0.924257 0.00798373 -0.382428 0.923723 -0.0220325 -0.284498 0.946115 -0.154683 +-0.0309041 0.999503 -0.00625127 -0.0164751 0.998007 -0.0609187 -0.024845 0.939228 -0.342395 +-0.0767759 0.801664 -0.592825 -0.487533 0.673966 0.555051 -0.534016 0.684299 0.49655 +-0.402215 0.80865 0.429311 -0.419145 0.83086 0.366045 -0.512411 0.858435 0.0229056 +-0.172685 0.612279 -0.771553 -0.203135 0.390925 -0.897727 0.18937 0.368415 0.91017 +-0.0468904 0.92693 -0.372294 -0.00997369 0.998392 -0.0557946 0.00340304 0.99969 0.0246667 +0.0102938 0.999923 0.00687776 0.0263852 0.999305 -0.0263261 -0.0281528 0.999161 0.0297327 +-0.349697 0.859311 0.373224 -0.0301215 0.083018 -0.996093 0.268252 0.583976 0.766168 +0.274573 0.500478 0.821055 0.366776 0.577875 0.729065 -0.0504689 -0.0794712 -0.995559 +0.0651845 0.035291 0.997249 -0.193509 -0.48796 -0.851146 -0.0398152 -0.396586 0.917134 +-0.126983 -0.322124 -0.938143 -0.305073 -0.602743 -0.737313 0.0818798 -0.600314 0.795562 +-0.014612 -0.359181 0.933154 -0.0110708 -0.00102031 -0.999938 -0.00325456 -0.0795485 0.996826 +0.000211356 0.0123559 -0.999924 0.00775742 -0.364121 0.931319 -0.00446916 -0.00314514 -0.999985 +0.00208642 -0.60159 0.798802 0.00522184 -0.349711 0.936843 -0.00408667 -0.00102904 -0.999991 +0.00635757 -0.0758596 0.997098 -0.00449 -0.00112813 -0.999989 0.00520035 -0.354897 0.934891 +-0.00442998 -0.00335091 -0.999985 0.0118706 -0.611383 0.791246 0.052028 -0.280286 0.958506 +0.00702732 0.016496 -0.999839 0.207586 0.458331 0.8642 -0.000327268 0.00314486 -0.999995 +0.311275 0.605342 0.732577 0.123097 0.325526 0.937486 -0.172164 -0.32529 -0.92981 +0.00669192 -0.00226398 0.999975 -0.268 -0.475553 -0.83787 -0.255724 -0.593422 -0.763188 +-0.368414 -0.580369 -0.726253 -0.00343661 -0.0136556 0.999901 -0.0420311 -0.0801996 -0.995892 +0.00105438 0.00330169 0.999994 0.000624656 -0.0042183 -0.999991 0.00036137 0.00128815 0.999999 +0.0358933 0.083401 -0.995869 0.000223852 -0.000102034 1 0.249056 0.472146 -0.845606 +0.00170828 0.00448237 0.999988 0.156703 0.602076 -0.78291 0.151687 0.320919 -0.934881 +-0.00557929 0.0026818 0.999981 0.00625861 0.00158633 -0.999979 -0.0063149 -0.0286212 0.99957 +-0.00729376 -0.0187072 -0.999798 0.0888879 0.0266267 0.995686 0.00680725 0.0253357 -0.999656 +0.500356 -0.283592 0.818058 0.419074 0.00371037 0.907945 -0.0387504 0.269392 -0.962251 +0.0391055 0.0348754 0.998626 0.58152 -0.143096 -0.800848 0.221744 0.622482 -0.750564 +-0.106447 -0.0335554 -0.993752 -0.0326258 0.0102788 0.999415 0.583541 -0.16538 -0.795065 +0.299396 -0.384 -0.873445 -0.0545448 -0.710277 -0.701806 -0.601978 0.0920004 0.793195 +-0.301554 0.161811 0.939618 0.604616 -0.0223238 -0.796204 0.376189 -0.019555 -0.926337 +-0.615707 -0.318604 0.720691 -0.333717 -0.105998 0.936695 0.506272 -0.355404 -0.785733 +0.349907 -0.21025 -0.912886 -0.0506669 -0.025124 0.9984 0.604638 -0.727354 -0.324608 +0.596202 -0.769329 -0.229513 0.58835 -0.724196 -0.359702 0.475854 -0.849472 -0.227947 +0.315827 -0.93296 -0.172738 0.0683784 -0.996505 -0.0479849 0.00336274 -0.998292 0.0583242 +-0.0110222 -0.932584 0.360785 0.0248148 -0.802937 0.595547 0.175895 -0.574647 -0.799276 +0.185802 -0.598357 -0.779388 0.0136337 -0.0116795 0.999839 0.631516 -0.767512 -0.110061 +0.424363 -0.752651 -0.503421 0.290882 -0.736126 -0.611152 0.169581 -0.919794 -0.353865 +0.315063 -0.936551 -0.15365 0.481138 -0.840911 -0.247738 0.00410823 -0.929454 0.368915 +-0.0152077 -0.998751 0.0475931 0.00455851 -0.999884 -0.0145103 0.0333446 -0.999127 0.0251485 +0.298101 -0.903606 0.307624 0.530529 -0.669954 0.519326 0.458602 -0.696597 0.551758 +-0.0180821 -0.760464 -0.649128 0.0742483 -0.669855 -0.73877 0.00758571 -0.363569 0.931536 +0.46563 -0.727 -0.504639 0.288821 -0.748483 -0.596956 0.148127 -0.927104 -0.34429 +0.307302 -0.951462 0.0168958 0.09161 -0.994537 -0.0500322 0.226636 -0.861695 -0.454002 +0.293258 -0.889726 0.34984 0.0338404 -0.999387 -0.00897661 0.0330223 -0.938857 -0.342721 +0.140681 -0.760122 0.634369 0.160373 -0.779899 0.60501 0.388596 -0.753677 0.530061 +0.425205 -0.750483 -0.505941 0.303911 -0.753807 -0.582592 0.184007 -0.901321 -0.392124 +0.176691 -0.874501 -0.451696 0.235806 -0.908891 0.343966 0.115657 -0.99222 -0.0460805 +0.238871 -0.858873 -0.453076 0.360671 0.897471 0.253894 0.578432 0.645958 0.49815 +0.612575 0.736432 0.287088 0.659161 0.752002 0.000641591 0.459482 0.885551 0.0683787 +0.694003 0.719914 -0.00916906 0.593519 0.792124 0.14239 0.720417 0.693434 -0.0121621 +0.731654 0.680941 0.0316461 0.722676 0.691156 -0.00653909 0.740625 0.671827 -0.0110681 +0.710067 0.69767 0.0951975 0.742899 0.669254 0.0141735 0.628045 0.712212 0.313549 +0.536772 0.843713 -0.00484526 0.111859 0.992768 -0.0435808 0.645292 0.734971 -0.208365 +0.293645 0.947143 -0.129201 0.649486 0.760274 -0.0123082 0.356729 0.933043 -0.046638 +0.635989 0.771554 0.0148908 0.356455 0.934037 0.0226924 0.642213 0.766515 -0.0041198 +0.440717 0.897203 0.0281998 0.66786 0.744227 0.00941335 0.575547 0.799164 0.173446 +0.690864 0.722973 0.00397607 0.726183 0.68618 0.0425929 0.682854 0.725925 -0.0821232 +0.727582 0.685721 -0.0202802 0.623099 0.717224 -0.31199 0.72256 0.662587 -0.197193 +0.686988 0.632326 -0.358066 0.591689 0.55091 0.588559 0.596745 0.566033 0.568773 +0.568376 0.590031 0.573421 0.531378 0.794218 -0.294713 0.497828 0.6345 -0.591251 +0.598741 0.678163 -0.42615 0.570293 0.664483 -0.482937 0.534623 0.527631 0.660139 +0.375751 0.390579 0.840392 0.544215 0.577942 0.608122 0.880356 0.341314 -0.329359 +0.679014 0.604905 -0.415969 0.862885 0.423077 -0.276469 0.751438 0.627472 -0.204008 +0.713829 0.575992 -0.398348 0.432732 0.361201 0.826 0.845656 0.247384 -0.472934 +0.982574 0.0881824 -0.163625 0.719169 0.461771 -0.519195 0.888295 0.15378 -0.432764 +0.745079 0.289195 -0.601019 0.495957 0.413032 0.763827 0.432677 0.282343 0.856197 +0.589854 0.331799 0.736194 0.891042 -0.443292 -0.0976566 0.914953 -0.0964585 -0.391863 +0.881062 -0.186651 -0.434616 0.811772 -0.125225 -0.57039 0.607885 0.312134 0.730102 +0.560724 0.435181 0.704419 0.898256 -0.326035 -0.294682 0.994995 -0.0996614 -0.00723982 +0.956233 -0.292441 -0.00978892 0.919087 0.024431 -0.393297 0.794921 -0.326336 -0.511473 +0.769743 -0.113945 -0.628103 0.720409 0.442757 0.533832 0.793108 0.14292 0.592076 +0.770521 0.192675 0.607597 0.890523 -0.400123 -0.216498 0.92799 -0.371575 -0.0276891 +0.92131 -0.388823 0.00212953 0.946524 -0.297037 -0.125944 0.99866 -0.0349672 -0.038144 +0.999842 -0.00154473 -0.0177093 0.998607 -0.0163967 0.0501509 0.996208 0.0314259 -0.0811325 +0.73955 0.288934 -0.607934 0.831083 0.246066 -0.498752 0.765539 0.307115 -0.565358 +0.924189 -0.0150165 0.381639 0.782097 -0.0116298 0.623048 0.825831 -0.323087 0.462188 +0.89032 -0.39393 -0.228361 0.925411 -0.377127 -0.0372859 0.946916 -0.299661 -0.116416 +0.998052 -0.0495248 -0.0379531 0.998643 -0.0505934 -0.0123225 0.998783 -0.0487769 0.00736041 +0.999087 -0.0425766 0.00355211 0.999956 -0.00445822 -0.00823832 0.999816 0.0184336 0.00526597 +0.999726 0.0121258 0.0200396 0.999836 0.0179873 0.00189153 0.926643 0.193203 -0.322499 +0.768527 0.2756 -0.577418 0.945293 0.027273 0.325081 0.766218 0.0811827 0.637431 +0.994561 -0.0573568 0.0869374 0.930794 -0.330853 0.155434 0.846321 -0.479591 0.231806 +0.901603 -0.303717 -0.308005 0.997755 -0.0486363 -0.0460427 0.998391 -0.0565727 -0.00378709 +0.999155 -0.0406452 0.00607181 0.999972 -0.000373642 -0.00754301 0.999752 0.021741 0.0047992 +0.999756 0.0220613 0.000582116 0.999835 0.0181017 0.00155767 0.999894 0.0133626 0.00585805 +0.999891 0.0128594 0.00720218 0.999852 0.0129841 0.0113183 0.999609 0.00412452 0.027638 +0.995468 0.030994 -0.0899047 0.723528 0.395552 -0.565726 0.870514 0.179159 -0.458374 +0.696851 0.316847 -0.643433 0.957411 0.107496 0.267972 0.73469 0.332449 0.591361 +0.999233 0.0380718 0.0091875 0.999674 0.00358507 -0.0252659 0.996015 -0.0792126 0.0409818 +0.930547 -0.32555 0.167627 0.922761 -0.384284 0.0289427 0.927106 -0.374046 -0.0237651 +0.926547 -0.376001 0.0115939 0.854586 -0.441541 0.273358 0.898672 -0.172789 -0.403153 +0.997709 -0.0512227 -0.0441986 0.99994 -0.0079273 -0.00760498 0.99972 0.0227192 0.00663696 +0.999808 0.0190369 0.00458895 0.99991 0.0122623 0.00545066 0.999969 0.0070382 0.00362829 +0.999989 0.00455168 -0.000566972 0.999989 0.00468662 4.01627e-05 0.999957 0.00750128 0.0054389 +0.999902 0.0102536 0.00951212 0.999935 -0.00712078 -0.00893134 0.998438 -0.0557897 0.00294104 +0.996542 -0.0711634 -0.0428877 0.911175 -0.218783 -0.349133 0.750952 -0.0795696 -0.655545 +0.579541 0.339544 0.740838 0.517813 -0.158438 0.840694 0.86886 0.017803 0.494737 +0.924675 -0.318278 0.208988 0.998289 -0.032291 0.0487434 0.998461 0.0404528 -0.0379334 +0.998791 0.0429476 -0.0239362 0.999931 -0.00576234 0.0102824 0.998968 -0.0453085 -0.00316626 +0.998467 -0.0496229 -0.0245284 0.998176 -0.0397246 0.0454558 0.933657 -0.0659014 0.352055 +0.925842 0.0344238 -0.37634 0.999051 -0.0144909 -0.0410681 0.999668 0.00515546 0.0252526 +0.999935 0.00959499 0.00618175 0.99998 0.00631659 -0.000107747 0.999995 0.0030169 -0.00045409 +1 -0.000440091 -0.000683764 0.999992 -0.00311657 -0.00246245 0.999991 -0.00320198 0.00294199 +0.999931 0.00256581 0.011514 0.999935 -0.00803298 -0.00804787 0.998867 -0.0320604 -0.0351826 +0.944 -0.269916 -0.18976 0.896644 -0.408254 -0.171344 0.726897 -0.413234 -0.548505 +0.751724 -0.526374 -0.397292 0.783894 -0.552578 0.283138 0.784291 -0.494532 0.374601 +0.848827 0.0235228 0.528148 0.938908 -0.233014 0.25329 0.996281 -0.0770678 0.0385361 +0.999975 -0.00204026 -0.00683759 0.999297 0.0289152 -0.0238643 0.999732 0.0132871 -0.0189382 +0.999284 0.0375689 0.00441961 0.99576 0.07805 0.0486932 0.894591 0.184571 0.40699 +0.839033 0.266567 -0.474306 0.996131 0.0407862 -0.0778399 0.999375 -0.0082922 0.0343679 +0.999976 0.000346085 0.0069608 0.999993 -0.000315597 -0.00377812 0.999996 -0.00239681 -0.00170862 +0.999997 -0.00207208 -0.00164017 0.999993 -0.00163235 -0.00323917 0.999989 -0.00167022 0.0044566 +0.999821 0.00142568 0.0188882 0.999022 0.0041271 -0.044019 0.945408 -0.128211 -0.29961 +0.79723 -0.548736 -0.251621 0.784816 -0.355593 -0.50756 0.761289 -0.588798 -0.27158 +0.798774 0.156273 0.580981 0.620611 0.189845 0.760789 0.978361 0.202063 -0.0444932 +0.790372 -0.155077 -0.592674 0.795243 -0.601607 0.0752232 0.835919 0.165927 0.523171 +0.950042 -0.228682 0.212425 0.942896 -0.312073 0.116438 0.997907 -0.0554521 0.033263 +0.999408 0.0316151 -0.0135888 0.998276 0.0479751 0.0338258 0.897742 0.314051 0.308919 +0.93195 0.154355 -0.328091 0.999975 -0.00452566 0.00546588 0.99978 -0.0108249 0.0179909 +0.999989 0.000419776 -0.00467528 0.999996 -0.00019685 -0.00264608 0.999999 -0.000750503 -0.000932999 +0.999996 -0.000149402 -0.00285576 0.999991 -0.000240348 0.00423233 0.999744 -0.00064314 0.0226372 +0.998835 0.00373282 -0.0481033 0.92036 -0.00645482 -0.391019 0.78806 -0.0616133 -0.612507 +0.903317 -0.0848572 0.420498 0.351024 -0.242867 0.904322 0.728212 0.622607 -0.286477 +0.941867 0.20245 -0.268143 0.803379 -0.0708043 -0.591243 0.820375 -0.54086 -0.185619 +0.722457 -0.634868 0.273859 0.682651 -0.55667 0.473399 0.841236 -0.405391 0.357744 +0.994735 -0.0503158 0.089273 0.946804 0.171261 0.272456 0.839535 0.30365 -0.45053 +0.994864 0.059208 -0.0820988 0.999246 -0.0208323 0.0327508 0.999971 -0.00528508 0.00541233 +0.99999 0.00196308 -0.00405553 0.999999 0.000452649 -0.00125659 0.999997 0.000128069 -0.00258129 +0.999996 -0.000655029 0.00289511 0.999757 -0.00311071 0.0218443 0.999066 -0.00480104 -0.0429507 +0.923762 -0.00376513 -0.38295 0.794214 0.0085741 -0.607577 0.587102 -0.530672 0.611308 +0.561055 -0.658402 0.50172 0.89383 -0.341258 0.290881 0.673207 0.413516 -0.613022 +0.865116 0.206909 -0.456907 0.720267 0.336427 -0.606656 0.69064 -0.45173 0.564762 +0.790515 -0.106126 0.603178 0.912436 -0.163388 0.375186 0.931363 0.0154928 0.363761 +0.845929 0.360316 -0.393163 0.998246 0.0438284 -0.0397866 0.99969 -0.0126857 0.0214199 +1 0.000535661 -0.000749749 0.999994 0.00176046 -0.00303177 0.999996 0.000205327 -0.0027481 +1 -0.000482193 0.000706102 0.999806 -0.00301655 0.0194387 0.999301 0.00376607 -0.0371908 +0.921769 0.034808 -0.386175 0.800034 0.0114508 -0.599846 0.668302 -0.431779 0.605755 +0.805981 0.181824 0.563325 0.992804 0.119018 0.0132003 0.7874 0.250959 -0.563045 +0.884912 0.172241 0.432739 0.7867 0.344235 0.51245 0.906757 0.126071 0.402365 +0.936738 0.115434 -0.33045 0.999978 -0.00618237 -0.00251544 0.999909 -0.0067383 0.0116881 +0.999985 0.00351969 -0.00421146 0.99999 0.00178898 -0.00406225 0.999999 0.00112397 -1.96422e-05 +0.999853 6.4675e-05 0.0171715 0.999828 0.0177562 -0.00540385 0.934855 0.143991 -0.324519 +0.786509 0.193843 -0.586369 0.719505 0.38587 0.577423 0.728838 0.172096 0.662705 +0.900146 0.0734925 0.429344 0.996168 0.0523227 0.0700848 0.776966 0.515402 -0.361503 +0.884997 0.44222 -0.145676 0.778559 0.573562 0.254701 0.799569 0.591475 0.104147 +0.825294 0.481879 0.294418 0.928406 0.0724753 -0.36443 0.99888 0.0123872 -0.0456637 +0.999217 -0.0242288 0.0312806 0.999975 -0.00438246 0.00550778 0.99997 0.00475306 -0.00614963 +0.999996 0.00284738 -0.000256924 0.999889 0.00138376 0.0148601 0.999756 -0.0199381 0.00951968 +0.994951 -0.00251131 -0.10033 0.776795 0.327058 -0.538166 0.883889 -0.0708856 -0.462293 +0.775194 0.122308 -0.61977 0.711182 -0.378086 0.592681 0.627686 -0.611143 0.482197 +0.849375 -0.289724 0.44116 0.887753 0.376692 0.26457 0.854171 0.419916 -0.306698 +0.924878 0.329972 -0.188993 0.998414 0.0468257 -0.0312389 0.999323 -0.0268264 0.0251792 +0.999967 -0.00701276 0.00398259 0.99999 0.00382918 -0.00246319 0.999932 0.000998506 0.0116547 +0.999554 -0.00427885 -0.029538 0.946318 -0.111992 -0.303217 0.750095 -0.298601 -0.590081 +0.788192 -0.291879 -0.541812 0.572388 -0.549219 0.608877 0.506296 -0.149697 0.849267 +0.838828 0.389166 -0.380679 0.996543 0.0546749 -0.0625503 0.999239 -0.0311477 0.0234629 +0.999891 -0.0123931 0.00797186 0.99996 -0.00313448 0.00842808 0.999768 0.014619 -0.0157914 +0.927405 0.166592 -0.334913 0.803081 0.0573077 -0.593108 0.268286 -0.168676 0.948457 +0.857368 0.36579 -0.362101 0.994902 0.0877081 -0.049769 0.999985 0.00253202 0.00494446 +0.999626 -0.0236521 0.013759 0.999544 -0.0197883 0.0227913 0.997323 0.0556008 -0.0474987 +0.701373 0.459889 -0.544589 0.849162 0.377799 -0.369043 0.68984 0.545557 -0.475907 +0.024457 -0.043855 0.998739 0.861778 0.455908 -0.222457 0.932163 0.317024 -0.174833 +0.998119 0.036399 -0.0493264 0.998853 -0.0415885 0.0237478 0.998392 -0.039586 0.0405821 +0.995615 0.0528902 -0.0771615 0.70709 0.478567 -0.520573 0.863258 0.232721 -0.447912 +0.689179 0.331497 -0.644315 0.0181338 0.00305657 0.999831 0.84912 0.382775 -0.363976 +0.995494 0.0819303 -0.0477403 0.999241 0.0148438 0.0360033 0.999987 0.00167615 0.00470157 +0.938634 0.137674 -0.316248 0.711604 0.220504 -0.667081 0.0105142 -0.0257709 0.999613 +0.859876 0.464989 -0.210708 0.928557 0.371006 -0.0117034 0.94195 0.306378 -0.137342 +0.99052 0.0673516 -0.119723 0.680902 0.438824 -0.58635 0.886015 0.185796 -0.424803 +0.710848 0.327832 -0.622272 0.0258668 -0.362997 0.931431 0.831903 0.370028 -0.413541 +0.918264 0.0859707 -0.386524 0.779536 0.0542153 -0.624006 -0.049796 -0.644627 0.762874 +-0.0905188 -0.660496 0.745353 0.772228 0.388049 -0.503072 0.739173 0.261553 -0.620655 +-0.627807 -0.182745 0.756613 -0.326276 -0.585785 0.74189 -0.192912 -0.420473 0.886559 +0.624007 0.197441 -0.756064 0.608479 0.436246 -0.662905 0.44616 0.272444 -0.852476 +-0.778955 -0.21534 0.588947 -0.224946 -0.358489 0.906027 0.627193 -0.0830592 -0.774422 +0.335323 0.0021827 -0.942101 -0.846457 -0.408494 0.341531 -0.428834 -0.681838 0.59262 +-0.732208 -0.546167 0.406906 -0.508865 -0.710168 0.486536 0.57692 -0.344219 -0.740727 +0.181304 -0.312476 -0.932463 -0.467316 -0.801644 0.372804 -0.329457 -0.920604 0.209634 +-0.64632 -0.726494 0.233402 -0.362183 -0.889882 0.277368 -0.579184 -0.73029 0.362246 +-0.134716 -0.776616 0.615402 -0.219147 -0.519379 -0.825966 0.0273575 -0.567764 -0.822737 +-0.248373 -0.759571 0.601134 -0.082848 -0.69851 0.710788 -0.405543 -0.371811 -0.83504 +-0.527342 -0.482378 -0.699444 -0.528714 -0.493286 -0.690746 -0.599387 -0.749471 0.281119 +-0.313549 -0.877391 0.363143 -0.396129 -0.874121 0.281059 -0.578825 -0.732586 0.35816 +-0.126295 -0.82977 0.543628 -0.408481 -0.416916 -0.811988 -0.24429 -0.925158 0.290525 +-0.0788012 -0.996336 0.0332495 -0.175275 -0.932532 0.315693 -0.360198 -0.751668 0.552496 +-0.561937 -0.732935 -0.383449 -0.368108 -0.900841 -0.230176 -0.624885 -0.750397 -0.215458 +-0.33659 -0.941613 -0.00851021 -0.669992 -0.740345 0.0547692 -0.403734 -0.866505 0.293545 +-0.567868 -0.727592 0.384886 -0.190042 -0.802355 0.565783 -0.628222 -0.658054 -0.415093 +-0.378345 -0.894801 -0.237035 -0.633146 -0.716979 -0.291664 -0.488038 -0.869769 -0.0729406 +-0.628057 -0.775756 -0.0612153 -0.517603 -0.779197 -0.353468 -0.558708 -0.699219 -0.446024 +-0.264593 -0.884955 0.383204 -0.403748 -0.90957 -0.098337 -0.342918 -0.865687 0.364682 +-0.480731 -0.859798 -0.172178 -0.617167 -0.74249 -0.260408 -0.604621 0.735255 0.306322 +-0.613799 0.787355 0.0576414 -0.605937 0.795512 0.000644293 -0.542605 0.825388 -0.155931 +-0.622728 0.772278 0.125681 -0.561789 0.77523 0.288809 -0.572883 0.783095 0.242008 +-0.552075 0.722456 0.416258 -0.386874 0.822544 0.416832 -0.355481 0.92143 0.156846 +-0.382306 0.923993 0.00892304 -0.365275 0.930832 0.0112064 -0.24494 0.935138 -0.255972 +-0.0110853 0.831548 -0.555342 -0.0239077 0.55556 -0.831132 -0.324098 0.640214 0.696482 +-0.313159 0.750687 0.581722 -0.145191 0.939186 0.311207 -0.0432386 0.999058 -0.00359204 +-0.038831 0.999141 -0.0144941 -0.043601 0.999049 0.000510743 -0.0464883 0.998882 0.00861685 +-0.0321386 0.999376 0.0146369 -0.0122633 0.999908 -0.00579813 -0.0239512 0.999212 -0.0316529 +-0.145737 0.909351 -0.389668 -0.284497 0.763511 -0.579752 -0.468556 0.709577 0.526266 +-0.305574 0.732692 0.608101 -0.155039 0.924412 0.348462 -0.300767 0.953648 -0.00973937 +-0.111966 0.9937 0.00485334 -0.313649 0.940775 0.128711 -0.435493 0.848127 0.301706 +-0.349003 0.560387 -0.751108 -0.14625 0.433704 -0.889107 -0.0450885 0.803697 0.593329 +0.0596338 0.598135 0.799173 -0.118286 0.947073 -0.298431 0.00416844 0.999188 -0.0400648 +0.0024499 0.999776 0.0210307 -0.00300842 0.999928 0.0116492 0.00207737 0.999936 -0.0110824 +0.0153219 0.99914 -0.0385179 -0.0524517 0.994453 0.0911654 -0.200173 0.863063 0.463739 +-0.286143 0.544904 -0.788164 -0.133805 0.283792 -0.949504 0.324808 0.594906 0.735246 +0.176824 0.336338 0.924992 -0.154429 -0.285321 -0.945909 -0.0483352 -0.0893837 0.994824 +-0.258292 -0.476278 -0.840503 -0.165405 -0.609503 -0.775337 -0.380072 -0.585473 -0.716077 +-0.284261 -0.482087 0.828726 -0.0363888 -0.0809712 -0.996052 -0.194344 -0.600329 0.775781 +-0.174903 -0.307648 0.935287 0.0131739 0.031052 -0.999431 -0.00912899 -0.0461458 0.998893 +0.0049108 0.00520986 -0.999974 0.0155041 -0.367683 0.929822 0.00205135 -0.0034537 -0.999992 +0.00996279 -0.603311 0.797444 0.0106034 -0.34551 0.938355 0.00271221 -0.000778346 -0.999996 +0.00504666 -0.0782678 0.99692 0.00164401 -0.00257765 -0.999995 -0.00711903 -0.361594 0.932308 +0.00428664 0.00305765 -0.999986 0.0124348 -0.581449 0.813488 0.16178 0.0272086 0.986452 +0.0141071 0.0280499 -0.999507 0.156603 0.594151 0.788962 0.25915 0.480636 0.837753 +0.383321 0.581234 0.717797 -0.0434072 -0.0767705 -0.996104 0.0372958 0.0845545 0.995721 +-0.242189 -0.481847 -0.842121 -0.00991654 -0.0328994 0.999409 -0.160637 -0.321067 -0.933334 +-0.331295 -0.591473 -0.735121 -0.00113308 -0.00660414 0.999977 -0.00791042 -0.0133659 -0.999879 +0.00129354 0.00322737 0.999994 0.00568161 0.0126412 -0.999904 0.000330502 0.000615471 1 +0.159455 0.323957 -0.932537 0.00100171 0.000948409 0.999999 0.372131 0.58142 -0.723511 +0.333689 0.590768 -0.734605 0.242648 0.48076 -0.84261 -0.00240125 -0.000737747 0.999997 +0.0447498 0.0775352 -0.995985 -0.0177926 -0.0153973 0.999723 -0.0119928 -0.0303487 -0.999467 +0.0394961 0.0581989 0.997523 -0.00178875 -0.0121254 -0.999925 0.417746 0.347181 0.839615 +-0.0153653 0.0120554 -0.999809 0.54064 0.399637 0.740269 0.30148 -0.186758 0.935003 +0.620336 0.0959062 0.77845 -0.0371733 0.0376636 -0.998599 0.37916 -0.453011 0.806857 +0.117933 -0.301505 0.946143 -0.542 0.171475 -0.822698 -0.237689 -0.0698737 -0.968825 +-0.0127917 -0.0332577 0.999365 0.0282849 -0.0130942 -0.999514 -0.0315646 0.0135505 0.99941 +0.0531504 -0.0277993 -0.9982 -0.0101276 -0.00277772 0.999945 0.03755 -0.356623 -0.933494 +0.0179764 0.032158 0.999321 0.602093 -0.731017 -0.321088 0.595974 -0.762419 -0.252056 +0.577895 -0.724697 -0.375302 0.474867 -0.85212 -0.219984 0.351866 -0.935896 0.0170026 +0.203425 -0.910678 0.35956 0.364317 -0.77633 0.514379 0.152808 -0.638149 -0.754596 +0.0630338 -0.617066 -0.784383 -0.00010759 -0.0328195 0.999461 0.208642 -0.77428 -0.597461 +0.284935 -0.850044 -0.442986 0.0575665 -0.99552 -0.0750082 0.0583321 -0.998294 -0.00234735 +0.322878 -0.933782 -0.154274 0.464709 -0.841884 -0.274365 0.146455 -0.953613 0.26301 +-0.00949319 -0.997888 0.0642667 0.0302705 -0.99933 0.0205707 0.315906 -0.898402 0.305085 +0.525502 -0.671966 0.521833 0.530012 -0.672233 0.516904 0.180639 -0.742585 -0.644932 +0.300154 -0.640331 -0.707025 -0.0110395 -0.33198 0.943222 0.0217222 -0.799019 -0.600913 +0.0791964 -0.92763 -0.365007 0.277975 -0.958163 -0.0682174 0.0221678 -0.999737 -0.00585701 +0.167388 -0.929867 -0.32761 0.317476 -0.895217 0.312723 0.0327872 -0.997863 0.0565279 +0.0159689 -0.999799 0.0121574 0.146776 -0.937856 -0.314457 0.0319439 -0.637559 0.769739 +0.000147173 -0.774221 0.632915 0.224651 -0.790611 -0.569619 0.254325 -0.813026 -0.523744 +0.0603578 -0.885845 -0.460039 0.0182064 -0.936321 0.350673 0.00872262 -0.998599 0.0522007 +0.162915 -0.92804 -0.334963 0.357502 0.905621 0.228128 0.548717 0.735697 0.397063 +0.631006 0.757248 0.168544 0.659892 0.751361 0.000186443 0.341393 0.939468 0.0291682 +0.659053 0.752091 0.00270241 0.344477 0.938773 0.00637976 0.653581 0.755756 -0.0407947 +0.347995 0.937228 -0.0224303 0.537096 0.756026 -0.374101 0.416439 0.865058 -0.279736 +0.202287 0.794108 -0.573125 0.166308 0.926406 0.337806 0.36344 0.784833 0.501945 +0.614834 0.761824 0.203969 0.667956 0.744197 0.00236542 0.321206 0.945668 0.0503934 +0.672067 0.7386 -0.0528679 0.359117 0.933263 -0.00745711 0.797224 0.588168 -0.135985 +0.377075 0.926181 -0.0015319 0.897715 0.429274 -0.099156 0.507424 0.861297 -0.0262326 +0.847049 0.32915 -0.417334 0.305493 0.87776 -0.369068 0.179235 0.542566 -0.820669 +0.194869 0.915389 0.352263 0.338621 0.718513 0.607515 0.529968 0.663262 0.528411 +0.107378 0.984387 0.139474 0.321893 0.901473 0.289364 0.17654 0.942548 -0.283615 +0.0183421 0.999549 -0.0237604 0.0482048 0.998811 -0.00728642 0.0561277 0.997615 0.0401641 +0.17298 0.941351 0.289718 0.83619 0.504741 -0.21453 0.901358 0.30439 -0.308058 +0.56572 0.440124 -0.697317 0.185939 0.596798 0.78055 0.285487 0.430993 0.856003 +0.504512 0.56552 0.652422 0.681125 0.705357 -0.196317 0.69156 0.619471 -0.371485 +0.781773 0.597811 -0.17735 0.740099 0.670455 -0.0523738 0.821633 0.49218 -0.287538 +0.726126 0.61721 -0.302973 0.607139 0.621151 -0.495533 0.195491 0.376 0.905764 +0.830555 0.39866 -0.388907 0.594294 0.698978 -0.397799 0.908342 0.36106 -0.211069 +0.857095 0.339799 -0.387201 0.577091 0.759706 -0.299689 0.674135 0.43557 -0.596508 +0.126634 0.534136 0.83586 0.260118 0.396588 0.880373 0.389408 0.522076 0.758813 +0.939107 0.10608 -0.32684 0.933593 0.165787 -0.317677 0.733246 0.187145 -0.653702 +0.261011 0.43313 0.862712 0.954174 -0.147478 -0.26039 0.996457 0.0191472 -0.0818966 +0.674809 0.437246 -0.594516 0.866521 0.211017 -0.452341 0.723571 0.310733 -0.616352 +0.370887 0.834528 0.407439 0.524281 0.376592 0.763746 0.723364 0.603782 0.334949 +0.674081 0.578559 0.459223 0.383762 0.822031 0.420705 0.887048 0.0403425 -0.459911 +0.997786 -0.0127095 -0.0652766 0.99867 -0.0508448 -0.00859212 0.928314 -0.0401171 -0.369626 +0.68641 -0.111973 -0.718543 0.780797 0.217036 0.585876 0.357502 -0.00383222 0.933904 +0.936982 0.147754 -0.316597 0.99908 -0.0207386 -0.0375437 0.911379 -0.203092 -0.35797 +0.537969 -0.256829 -0.802888 0.641732 0.00104528 0.766928 0.360974 -0.00563704 0.932559 +0.813558 0.279209 -0.510065 0.947719 -0.0698247 -0.311374 0.573602 -0.374862 -0.728327 +0.763115 -0.311227 -0.566386 0.635433 -0.00785258 0.772116 0.552758 -0.00710411 0.833311 +0.75856 0.0878904 -0.645648 0.750756 -0.0732501 -0.656506 0.620675 0.214507 0.754155 +0.357659 0.175484 0.917216 0.784713 -0.140684 -0.603683 0.738016 0.00635332 -0.674754 +0.100563 -0.0227114 0.994671 0.897858 -0.308387 -0.314242 0.896424 -0.0105047 -0.443073 +0.754317 0.0201687 -0.6562 0.68815 -0.437175 0.579075 0.181737 -0.367748 0.911994 +0.89306 -0.279264 0.352782 0.901791 -0.244907 -0.356082 0.954127 -0.276083 -0.115847 +0.995207 -0.0648901 -0.0731554 0.931935 -0.0555907 -0.358339 0.782573 -0.0165195 -0.62234 +0.276074 -0.534746 0.798642 0.262998 -0.269726 0.926326 0.996524 -0.016265 0.081706 +0.932995 0.000500512 -0.35989 0.998046 -0.0469346 -0.0412495 0.997828 -0.0615026 -0.0236151 +0.909919 -0.194186 -0.366523 0.758051 -0.294223 -0.582058 0.0449444 -0.0872379 0.995173 +0.89737 0.0294147 -0.440298 0.890887 -0.44116 0.108157 0.936813 0.0057482 -0.349784 +0.997271 0.0145861 -0.0723768 0.967254 -0.0973601 -0.234393 0.658947 -0.390897 -0.642641 +0.745397 -0.279819 -0.60505 0.46879 -0.603649 0.644859 0.302721 -0.299227 0.904888 +0.961 0.12442 -0.24698 0.871471 -0.275613 0.40568 0.937746 0.0515601 -0.343474 +0.997601 0.007587 -0.0688143 0.949315 0.00840179 -0.314213 0.762107 -0.0512318 -0.64542 +0.620683 -0.373448 0.689412 0.699169 0.162133 0.69633 0.666575 0.178537 0.723742 +0.873525 0.419587 -0.24678 0.929232 0.188335 0.317895 0.846482 0.255629 -0.467035 +0.993928 0.0234932 -0.107499 0.9509 -0.0427158 -0.306536 0.7768 -0.00520164 -0.629726 +0.620035 0.345209 0.704548 0.30657 -0.102536 0.946309 0.666231 0.174716 0.72499 +0.914769 -0.0514722 -0.400686 0.907977 -0.204052 -0.365979 0.717996 -0.367867 -0.590893 +0.331383 -0.557568 0.76112 -0.0383184 -0.630206 0.775482 0.460079 -0.678193 0.573046 +0.882208 -0.148822 -0.446723 0.915332 -0.139507 -0.377763 0.635635 -0.411971 -0.652877 +0.760694 -0.288198 -0.581624 0.419023 -0.583156 0.695952 0.358849 -0.37329 0.855501 +0.580232 -0.337916 0.741042 0.853874 0.283987 -0.436178 0.913675 0.169357 -0.369481 +0.781916 0.0890813 -0.616985 0.540245 -0.239931 0.806578 0.209756 -0.393065 0.895267 +0.804896 0.384231 -0.452226 0.721481 0.36945 -0.585637 0.810391 0.235978 -0.536265 +0.531736 0.294941 -0.793894 0.349557 -0.478105 0.805745 0.111348 -0.298388 0.947927 +0.738918 0.181738 -0.648823 0.622513 0.216402 -0.752095 -0.00427953 -0.0210082 0.99977 +0.657282 0.307105 -0.688235 0.704714 0.289963 -0.647534 -0.00784535 -0.0234417 0.999694 +0.62673 0.17027 -0.760407 0.62095 0.4318 -0.654194 0.462693 0.247542 -0.851257 +0.109131 -0.291084 0.950453 0.632706 0.0643311 -0.771715 0.360137 0.0385823 -0.932101 +0.144384 -0.670272 0.727934 0.243713 -0.72371 0.645637 0.599153 0.311407 -0.737592 +0.317834 0.122046 -0.940259 0.277118 -0.622904 0.731571 0.313799 -0.274932 0.908814 +0.0920088 0.0342813 -0.995168 0.409414 -0.41578 0.8121 -0.0997782 -0.31305 -0.944481 +0.199741 -0.836092 0.510934 -0.060161 -0.893998 0.444014 -0.348942 -0.758306 0.550647 +0.154651 -0.786626 0.597748 -0.320596 -0.728165 -0.605801 -0.172116 -0.668173 -0.723824 +-0.458104 -0.608955 -0.647544 -0.0126663 -0.913036 0.407683 0.104744 -0.93467 0.339736 +0.257011 -0.718339 0.646479 -0.287728 -0.424576 -0.858456 -0.100584 -0.65546 -0.748502 +-0.484714 -0.595721 -0.640444 0.280399 -0.611226 0.74012 0.302631 -0.744652 0.594901 +-0.206112 -0.344905 -0.915728 0.141839 -0.909651 0.390405 0.104024 -0.916363 0.386598 +0.282587 -0.787976 0.547026 -0.323327 -0.741932 -0.587365 -0.180821 -0.578099 -0.795679 +-0.412336 -0.572145 -0.708963 0.152868 -0.93829 0.310232 0.0748241 -0.997094 0.0142922 +-0.00995966 -0.999758 0.0196146 -0.327294 -0.92396 0.197932 -0.568469 -0.816382 0.101799 +-0.315778 -0.94238 -0.11047 -0.546653 -0.782181 -0.298938 -0.0624533 -0.996226 -0.0602723 +-0.0458922 -0.998066 0.0419205 -0.0510913 -0.933449 0.355052 0.0933233 -0.811481 0.576878 +-0.290951 -0.766215 -0.572941 -0.158406 -0.911052 -0.380647 -0.444516 -0.738601 -0.506827 +-0.0416361 -0.994001 -0.101139 -0.169034 -0.955931 -0.240048 -0.135673 -0.989191 0.0556192 +-0.458924 0.708151 0.536573 -0.311035 0.729733 0.608891 -0.140781 0.939283 0.312936 +-0.0495408 0.99877 0.0021426 -0.0569779 0.998137 0.0218027 -0.0523024 0.998487 -0.0169914 +-0.024968 0.892646 -0.450065 -0.00851329 0.479869 -0.877299 -0.0143661 0.925739 0.377891 +-0.0857757 0.79257 0.603718 0.0190099 0.99925 0.0337266 0.0297064 0.999352 -0.0202991 +0.0253719 0.999675 -0.00238543 0.0248588 0.999671 0.00632997 0.0244617 0.999507 0.0197022 +0.0190346 0.999316 -0.0316978 -0.139729 0.950916 -0.276105 -0.164952 0.78974 -0.590848 +-0.355446 0.75237 -0.554614 -0.0353107 0.91189 0.408912 -0.0446661 0.773548 0.632162 +-0.296295 0.951386 -0.0841111 -0.293744 0.944483 0.147194 -0.038293 0.99901 0.0226555 +-0.0730281 0.993965 0.0818545 -0.2124 0.857326 0.468911 -0.0150817 0.378174 -0.925612 +0.139734 0.87768 0.458424 0.255735 0.542642 0.800087 0.290423 0.729203 0.61961 +0.0128899 0.883844 0.467605 -0.210749 0.888938 -0.406662 -0.0358821 0.998103 -0.0500221 +-0.0065129 0.999979 -1.83608e-05 0.00663315 0.999888 0.0134234 -0.000337655 0.999973 0.0073006 +-0.00317138 0.999982 -0.00506414 0.00214409 0.999586 -0.028698 0.010039 0.999279 0.0366042 +-0.0034908 0.924684 0.380719 -0.0761428 0.573203 -0.815868 -0.00325614 0.341931 -0.939719 +0.0279669 0.0594934 0.997837 -0.0361057 -0.331635 -0.942717 -0.147729 -0.358522 0.921758 +-0.0505926 -0.387787 -0.92036 -0.07839 -0.618658 -0.78174 -0.399389 -0.574257 0.714645 +-0.329852 -0.602357 0.726886 -0.221971 -0.477138 0.850335 0.0103088 -0.0276597 -0.999564 +-0.0424453 -0.0578464 0.997423 0.00885273 0.0225405 -0.999707 0.0152411 -0.00614521 0.999865 +0.00055195 -0.000864074 -1 -0.0405612 -0.373833 0.926609 9.54934e-05 -0.00249266 -0.999997 +-5.46826e-05 -0.604544 0.796572 -0.0267574 -0.354768 0.934571 0.000359204 -0.00124082 -0.999999 +-0.00526052 -0.0824033 0.996585 -0.000314255 -0.00278681 -0.999996 0.0251332 -0.371807 0.92797 +0.00475706 0.0117425 -0.99992 -0.0100861 -0.58086 0.813941 0.0305588 0.0146652 0.999425 +-0.00589392 -0.00308862 -0.999978 0.0417035 0.605091 0.795063 0.0374327 0.395506 0.9177 +-0.150627 -0.320466 -0.935207 -0.0154949 0.0375522 0.999175 -0.246892 -0.472323 -0.846141 +-0.155788 -0.601533 -0.78351 -0.370174 -0.581902 -0.724128 -0.0084891 -0.0223155 0.999715 +-0.037511 -0.0853396 -0.995646 0.000724749 -0.000974308 0.999999 -0.010271 -0.000960315 -0.999947 +0.000915595 0.00199855 0.999998 0.0303446 0.0805794 -0.996286 0.000523738 0.000818222 0.999999 +0.254823 0.485024 -0.836551 0.000539279 0.00039025 1 0.251447 0.595945 -0.762643 +0.165052 0.325229 -0.93112 -0.00613975 -0.0122269 0.999906 -0.000325878 -0.00107134 -0.999999 +0.0062064 0.0128489 0.999898 -0.00709249 -0.017878 -0.999815 0.1614 0.340144 0.926419 +0.00513871 -0.00405656 -0.999979 0.087769 0.609792 0.787687 0.23704 0.347513 0.907219 +0.439305 0.57002 0.694325 0.00875474 -0.00527118 -0.999948 -0.240221 -0.341616 0.908621 +-0.00963475 0.0168832 -0.999811 -0.105162 -0.597271 0.795115 -0.169203 -0.33946 0.925277 +-0.0284843 -0.000563888 -0.999594 0.00334432 -0.0141955 0.999894 -0.00790372 0.0227624 -0.99971 +0.0217115 0.0121469 0.999691 -0.0155592 -0.0453118 -0.998852 0.010896 0.00643443 0.99992 +0.103612 -0.292084 -0.950764 0.00153573 0.0185831 0.999826 0.579653 -0.747691 -0.323975 +0.609363 -0.788395 -0.0843218 0.584923 -0.781384 0.217495 0.236704 -0.519405 -0.82109 +0.286558 -0.574779 -0.766495 -0.00378068 -0.0425152 0.999089 0.440528 -0.692774 -0.570963 +0.541184 -0.672312 -0.505091 0.358911 -0.851483 -0.382309 0.0196006 -0.999487 -0.0253173 +-0.017616 -0.99968 0.0181694 0.0623531 -0.996783 -0.050353 0.337409 -0.933607 -0.12055 +0.536128 -0.842604 0.050836 0.55946 -0.797168 0.226997 0.390899 -0.900319 -0.191373 +0.0924808 -0.9956 0.015094 0.318301 -0.897796 0.304378 0.518702 -0.694105 0.499166 +0.529539 -0.676476 0.511829 0.363823 -0.673943 -0.642988 0.292811 -0.544661 -0.785879 +-0.00176824 -0.335646 0.941987 0.190519 -0.824382 -0.533008 0.474345 -0.877642 0.0688616 +0.577624 -0.754793 0.310865 0.589157 -0.794499 0.147193 0.43092 -0.784119 -0.446616 +0.139033 -0.942706 -0.303273 0.0480158 -0.997285 -0.055828 0.378042 -0.844293 -0.379808 +0.312838 -0.898705 0.307346 0.032246 -0.998943 0.0327593 -0.0042169 -0.999931 0.0109216 +-0.0440132 -0.99891 0.0155416 0.011883 -0.997824 -0.0648542 0.225484 -0.816687 -0.531205 +-0.00884633 -0.626323 0.779513 -0.00462003 -0.777779 0.628521 0.361358 -0.739747 -0.567622 +0.294701 -0.728195 -0.618776 0.281449 -0.807053 -0.519088 0.378083 -0.912143 -0.158265 +-0.0114308 -0.937903 0.34671 -0.0164046 -0.996986 0.0758285 0.0338361 -0.994469 -0.0994285 +0.195836 -0.858217 -0.474459 0.141849 0.906436 0.397809 0.273956 0.79375 0.543054 +0.420754 0.760751 0.49419 0.0804943 0.995091 0.0575804 0.0739396 0.997258 0.00301659 +0.0508796 0.998628 0.0123955 0.0355311 0.998041 -0.0514922 0.0599455 0.935241 -0.348901 +-0.0552768 0.808703 -0.585614 -0.389934 0.871345 0.297842 -0.129695 0.807959 0.574788 +-0.281095 0.941839 0.184184 0.00435385 0.998558 0.0534974 0.0542059 0.997569 -0.0437974 +-0.114169 0.94991 -0.290923 -0.137907 0.799977 -0.583968 -0.283352 0.742543 -0.606911 +-0.0449698 0.919518 0.390466 0.0243006 0.788551 0.614489 0.102021 0.943253 0.316014 +0.0213379 0.925045 -0.379256 -0.0195746 0.998672 -0.0476454 -0.0568054 0.998295 0.0134536 +-0.0762744 0.996786 0.0244845 -0.247005 0.920033 0.304183 -0.161684 0.926549 -0.339654 +0.198949 0.949993 -0.240693 0.641174 0.742547 -0.193697 0.661948 0.749439 0.0128715 +0.339094 0.940096 -0.0351428 0.634753 0.691416 -0.345011 0.337039 0.912113 -0.233357 +0.255882 0.734661 -0.62833 -0.0961407 0.616648 0.781346 0.010944 0.366935 0.930182 +0.532611 0.663811 -0.525053 0.461178 0.696951 -0.549158 0.515178 0.685709 -0.514194 +-0.0087431 0.407842 0.913011 0.400661 0.828116 -0.392038 0.608579 0.251082 -0.752721 +0.39322 0.74639 -0.536918 0.316169 0.502404 -0.804753 -0.00675621 0.554302 0.832288 +0.0597481 0.387697 0.919848 0.63209 0.267755 -0.727166 0.527295 0.428584 -0.733673 +0.24591 0.602732 0.759106 0.63248 0.102829 -0.767721 0.59965 0.472542 -0.645851 +0.396987 0.268661 -0.877623 0.419788 0.797445 0.433428 0.191056 0.349256 0.917343 +0.39781 0.879787 0.260237 0.497971 -0.374824 -0.782005 0.340379 -0.209768 -0.916591 +0.0530965 0.00378668 0.998582 0.269999 -0.388439 -0.881031 0.209254 -0.566083 -0.797348 +0.247692 0.295923 0.922539 0.592768 -0.181255 -0.784712 0.294383 -0.182736 -0.938055 +0.100145 0.563988 0.819688 0.238511 -0.138985 0.961143 0.594288 0.15047 0.790051 +0.669506 0.00394925 -0.742797 0.587929 0.147929 -0.795271 0.403233 -0.483448 0.776969 +0.151737 -0.243016 0.958081 0.670321 0.00673532 -0.742041 0.664119 -0.0725507 -0.744098 +0.00778496 -0.0892597 0.995978 0.633158 0.024211 -0.773644 0.487889 -0.0281717 -0.872451 +-0.0414194 -0.387708 0.920851 0.638854 -0.0256315 -0.768901 0.337163 -0.0280892 -0.941027 +-0.000892426 -0.589778 0.807565 -0.0562117 -0.334819 0.940604 0.632926 -0.258387 -0.729822 +0.29171 -0.132282 -0.947316 -0.0728052 -0.0810229 0.99405 0.634241 -0.197244 -0.747551 +0.583908 -0.432565 -0.686978 0.558479 -0.494909 -0.665707 -0.301692 -0.202417 0.93167 +0.61876 0.026935 -0.785118 0.479916 -0.0714848 -0.874397 0.664751 -0.103871 0.739809 +-0.12397 -0.171684 0.977321 0.622615 -0.0255517 -0.782111 0.339719 -0.0284006 -0.940098 +-0.529357 -0.362146 0.767224 -0.383002 -0.504245 0.773981 -0.471779 -0.375419 0.7978 +0.534799 -0.429804 -0.727501 0.126285 -0.345742 -0.929793 -0.327059 -0.451429 0.830207 +0.634322 -0.112817 -0.764793 0.297654 -0.392906 -0.870073 0.0687409 -0.72436 -0.685986 +-0.0662541 -0.583583 0.809346 -0.207176 -0.390696 0.896903 0.612569 0.307446 -0.728173 +0.340435 0.151675 -0.927954 -0.292985 -0.432798 0.852553 0.403719 0.426221 -0.809535 +-0.0779248 -0.600111 0.796112 -0.165669 -0.337511 0.926628 0.622189 0.223643 -0.750243 +0.568704 0.405479 -0.715655 0.359481 0.2804 -0.890028 -0.0346456 -0.0149004 0.999289 +0.616796 0.334774 -0.712383 0.272039 0.149655 -0.950578 0.0116877 0.0057368 0.999915 +0.0303927 0.0278599 -0.99915 0.0837923 -0.0447553 0.995478 -0.0116368 -0.00640395 -0.999912 +0.408323 -0.579169 0.705575 0.298385 -0.270558 0.915295 -0.0329691 0.0286311 -0.999046 +0.660662 -0.349331 0.66445 0.77449 -0.180422 0.606312 0.60906 -0.33506 0.718874 +-0.0634317 -0.03844 -0.997246 0.651209 -0.232191 0.722505 0.7343 -0.32128 0.597983 +-0.0637351 -0.362579 -0.929771 0.589402 -0.646687 0.48415 0.393147 -0.868497 0.301907 +0.617908 -0.679745 0.39514 0.384948 -0.786234 0.483375 0.11375 -0.687727 -0.717002 +-0.0463057 -0.609434 -0.791484 0.580015 -0.694262 0.426125 0.433355 -0.841302 0.323133 +0.578014 -0.696156 0.425754 0.492272 -0.670401 0.555185 0.142519 -0.415593 -0.898315 +0.311753 -0.62733 -0.71363 0.441463 -0.668144 0.59891 0.38863 -0.559433 0.732121 +0.453439 -0.651438 0.608295 0.0413756 -0.459083 -0.887429 0.502841 -0.76538 0.401678 +0.433886 -0.845992 0.309903 0.577279 -0.714512 0.395247 0.429415 -0.74702 0.507507 +-0.0644527 -0.769082 -0.635892 0.0812853 -0.640682 -0.763491 0.417065 -0.817956 0.396239 +0.41044 -0.887671 0.208755 0.390303 -0.920682 -0.00302089 0.331835 -0.923351 -0.193152 +0.0704231 -0.996893 -0.035276 0.00232283 -0.999981 0.00574882 0.0516753 -0.998639 -0.00710562 +0.0764808 -0.995031 0.0637557 0.176886 -0.896742 0.405666 0.299915 -0.757965 0.579258 +-0.0796478 -0.807873 -0.58395 0.0158212 -0.928814 -0.370209 -0.0169898 -0.996636 -0.0801785 +0.0148094 -0.947089 -0.320628 0.147631 -0.988089 -0.0434183 0.0116613 0.934246 0.356438 +-0.0668916 0.798089 0.598816 0.0241211 0.998712 0.0446454 0.056795 0.998386 -0.000192749 +0.025105 0.99932 -0.0270206 0.0043337 0.902103 -0.4315 -0.0079357 0.446357 -0.89482 +0.0271386 0.92927 0.368403 0.025254 0.812796 0.582 -0.00299532 0.998643 0.0519891 +0.00251224 0.999778 -0.0209055 0.00152243 0.999994 -0.00326927 0.00453622 0.999946 0.00937963 +0.00831416 0.999745 0.0210024 -0.000259053 0.998497 -0.0548003 -0.0380732 0.927169 -0.372703 +-0.0824791 0.789596 -0.608058 -0.18795 0.982138 -0.00898903 -0.00380047 0.804746 0.593607 +-0.185293 0.798256 -0.573109 -0.507617 0.777551 -0.371133 -0.590258 0.797425 0.125337 +-0.376302 0.75076 0.542914 -0.122542 0.946814 0.297536 0.0341528 0.998024 0.0527474 +0.033976 0.934138 0.355291 -0.0127054 0.388021 -0.921563 0.0514567 0.887107 0.458686 +0.0540327 0.48927 0.870457 0.0714986 0.921168 0.382542 -0.0446806 0.925777 -0.37542 +-0.00471027 0.998441 -0.0556172 0.00867152 0.999729 0.0216066 0.0073691 0.999937 0.00845344 +0.00315306 0.999992 0.00253979 0.000438906 0.999977 -0.00673196 -0.00170654 0.99969 -0.024855 +-0.000700772 0.998985 0.0450402 0.00837886 0.92477 0.380436 -0.343355 0.923905 -0.168838 +-0.289179 0.860495 0.419433 0.00670434 0.585606 -0.810568 -0.0436345 0.283503 -0.957978 +-0.0190667 -0.0170728 0.999672 -0.204046 -0.45586 -0.866347 0.0170067 -0.405619 0.913884 +-0.121554 -0.325599 -0.937662 -0.306982 -0.607819 -0.732337 -0.0772592 -0.619688 0.781037 +0.0231285 -0.35993 0.932692 -0.00555651 0.00159733 -0.999983 0.0297786 -0.0071161 0.999531 +0.00406078 0.0132055 -0.999905 -0.043689 -0.0542801 0.99757 -0.000960223 -0.003395 -0.999994 +-0.269957 -0.459743 0.846026 -0.000656084 -0.00164901 -0.999998 -0.178389 -0.600767 0.779266 +-0.169169 -0.310548 0.935383 -0.00104869 -0.00223485 -0.999997 -0.0151479 -0.0520034 0.998532 +-0.000730355 -0.000511378 -1 0.202012 -0.265353 0.942751 0.00754667 0.0215628 -0.999739 +0.17177 -0.553487 0.814952 0.289414 0.131588 0.948116 0.00839479 -0.0288171 -0.999549 +0.420221 0.503478 0.754933 0.137148 0.330186 0.933899 -0.0380619 -0.382243 -0.923278 +0.00149055 0.0142924 0.999897 -0.0504583 -0.388242 -0.920175 -0.0777636 -0.612146 -0.786912 +-0.00448213 -0.0102518 0.999937 -0.0100896 -0.0531346 -0.998536 0.000969699 0.00127028 0.999999 +0.0315688 0.0332427 -0.998949 0.000467611 0.0010574 0.999999 0.175292 0.304244 -0.936327 +0.000452662 0.00131679 0.999999 0.351429 0.590884 -0.726191 0.302831 0.60299 -0.738036 +0.214497 0.486121 -0.847158 -0.000469823 -0.00238364 0.999997 0.0318827 0.0780379 -0.99644 +-0.00196047 -0.0164311 0.999863 -0.0157472 -0.0271222 -0.999508 0.00454274 0.0383236 0.999255 +-0.000533374 -0.00294226 -0.999996 -0.12144 0.302956 0.945235 0.00514486 0.000161948 -0.999987 +-0.38837 0.46298 0.796755 -0.311063 0.195 0.930169 0.00727412 -0.00340693 -0.999968 +-0.440441 -0.565974 0.696911 -0.53787 -0.401547 0.741253 -0.421108 -0.355211 0.834562 +0.00720603 -0.00150496 -0.999973 -0.0463077 -0.0537395 0.997481 0.00303236 0.00603442 -0.999977 +0.0183003 0.0204458 0.999623 -0.00590273 0.0121987 -0.999908 0.00340456 -0.00016764 0.999994 +0.00491694 -0.00805632 -0.999955 -0.00198878 0.000324246 0.999998 0.0320792 -0.0420241 -0.998601 +-0.00102279 0.016509 0.999863 0.471094 -0.510658 -0.719235 0.286539 -0.29285 -0.912214 +0.00150694 -0.0419104 0.99912 0.507166 -0.678372 -0.531597 0.323744 -0.717966 -0.616209 +0.106109 -0.937236 -0.332159 -0.00730747 -0.999918 -0.0105048 -0.0375586 -0.999287 0.00386542 +0.00862045 -0.998723 0.0497739 0.267913 -0.925956 0.266134 0.238849 -0.808053 0.538518 +0.52164 -0.785392 0.333244 0.604808 -0.781088 -0.155272 0.355131 -0.786772 -0.504848 +0.24352 -0.969746 0.0170287 0.0871442 -0.814292 0.573877 0.425119 -0.728265 0.537498 +0.40105 -0.567187 -0.719345 0.529994 -0.470669 -0.705392 0.00137524 -0.335895 0.941898 +0.579325 -0.796748 -0.171975 0.0774639 -0.785138 -0.614457 0.00586606 -0.925817 -0.377926 +-0.0167205 -0.999851 -0.00428498 0.0358572 -0.996212 -0.0792155 0.174213 -0.871823 -0.457792 +0.306686 -0.937108 0.166647 0.219235 -0.882528 -0.416029 0.22375 -0.890582 0.39598 +0.0509427 -0.998143 0.0334086 0.00502294 -0.999951 -0.00851644 -0.00436243 -0.999969 -0.0065885 +0.0197194 -0.999779 -0.00733637 -0.010952 -0.999866 -0.0121364 0.180316 -0.943267 -0.278806 +0.00134155 -0.629137 0.777294 -0.00491329 -0.780299 0.625387 0.0738731 -0.786416 -0.613264 +0.0313356 -0.931744 -0.361761 0.278 -0.959938 -0.0351484 0.035656 -0.915437 -0.400878 +0.000299306 -0.940529 0.339714 1.48289e-05 -0.997732 0.067317 -0.00565891 -0.998347 -0.0571915 +0.00754993 -0.923961 -0.382411 -0.270571 0.867437 0.417545 -0.0703664 0.820807 0.566855 +-0.0829162 0.991786 0.0973885 -0.0475198 0.998739 -0.0161812 -0.0664809 0.997741 -0.00969052 +-0.077216 0.995421 -0.0563435 -0.165564 0.9069 -0.387454 -0.308028 0.759153 -0.573415 +-0.588307 0.71785 0.372273 -0.666234 0.716957 0.205194 -0.57936 0.629124 0.518213 +-0.35724 0.852866 0.380788 -0.0797557 0.996814 -5.97421e-05 -0.0304256 0.924568 -0.379801 +-0.0206075 0.801068 -0.598219 -0.296255 0.8381 0.458063 -0.292776 0.717062 0.632538 +-0.205101 0.898672 0.387715 -0.170888 0.938596 -0.299726 -0.0467841 0.994382 -0.0949484 +-0.274115 0.957213 -0.0927598 -0.354177 0.935061 -0.01483 -0.401712 0.914896 0.0399037 +-0.646123 0.746994 0.156607 -0.690361 0.644826 0.328027 -0.0498719 0.930613 -0.362591 +-0.0540907 0.997378 -0.0480644 -0.00611439 0.999343 -0.0357293 -0.102391 0.924635 -0.366833 +-0.248933 0.797848 -0.549064 -0.329738 0.608828 0.721527 -0.161445 0.407784 0.898692 +-0.0391086 0.925839 -0.375889 0.430948 0.735951 -0.522168 0.277044 0.844732 -0.457902 +0.0843876 0.787757 -0.610178 -0.0697237 0.471595 0.879055 0.18842 0.772239 -0.606749 +0.146704 0.646295 -0.748853 -0.0795449 0.627142 0.774833 0.202497 0.614007 0.762883 +0.457374 0.531099 -0.713263 0.320796 0.618605 -0.717229 0.186004 0.424496 -0.886118 +-0.020385 0.863747 0.503512 0.112795 0.562999 0.818724 0.520733 0.814103 0.257046 +0.535022 0.748815 0.391187 0.295797 0.869588 0.395375 0.12618 0.0952699 -0.987422 +-0.0520992 0.0537954 0.997192 0.443282 -0.323599 -0.835933 -0.0440217 0.0288799 0.998613 +0.252958 -0.333501 -0.908179 0.332111 -0.535632 -0.776403 -0.107469 0.288915 0.951304 +0.471954 0.174998 -0.86408 -0.365626 0.477666 0.798845 -0.257362 0.152491 0.954207 +0.663941 0.133357 -0.735798 0.578863 0.429079 -0.693404 0.466686 0.281826 -0.838319 +-0.107631 -0.542839 0.832912 -0.206415 -0.288549 0.93495 0.569496 -0.320193 -0.757067 +0.326411 -0.20581 -0.92255 -0.0987632 -0.109966 0.989016 0.250566 -0.286729 -0.924664 +0.0806521 -0.569627 -0.817937 -0.269943 -0.432501 0.860275 -0.259856 -0.139231 -0.955557 +-0.281013 -0.632971 0.721373 -0.394745 -0.337989 0.854365 -0.339004 -0.0683381 -0.9383 +-0.539999 -0.188835 0.820209 0.400079 -0.590752 -0.700678 0.0667674 -0.40958 -0.909828 +-0.619333 -0.270224 0.73716 -0.660737 -0.325311 0.676461 -0.769032 -0.290815 0.569225 +0.0939811 -0.297558 -0.950067 -0.210685 -0.530089 -0.821351 -0.60617 -0.302328 0.735633 +-0.7007 -0.194201 0.686517 -0.113933 -0.315982 -0.9419 -0.585963 -0.206781 0.783511 +-0.209088 -0.518877 -0.828884 -0.218668 -0.593422 -0.774619 -0.292931 -0.593307 -0.749786 +-0.429827 -0.540691 0.723119 -0.58145 -0.434122 0.68808 -0.586107 -0.420238 0.692732 +-0.0115943 -0.0569982 -0.998307 -0.504229 -0.292972 0.812355 -0.0950567 0.2941 -0.951036 +-0.434031 -0.552312 0.711737 -0.534123 -0.436206 0.72418 -0.387245 -0.352108 0.852092 +0.407643 0.509554 -0.757748 0.138891 0.590958 -0.794656 -0.140205 0.339874 -0.929961 +-0.0611255 -0.0743609 0.995356 -0.276911 0.0427237 -0.959945 0.0591468 0.0631099 0.996252 +-0.336239 -0.00116579 -0.941776 0.310016 0.124725 0.942515 -0.367543 -0.0104272 -0.929948 +0.42344 0.0482109 0.90464 -0.363481 0.00283958 -0.931597 0.626615 0.105053 0.772216 +-0.394025 0.00452523 -0.919089 0.74181 0.351371 0.571188 0.758739 0.141102 0.635929 +0.838341 -0.0265084 0.544502 0.831637 -0.248004 0.496865 -0.406296 -0.0731744 -0.910807 +0.687215 -0.402964 0.604447 0.656942 -0.521294 0.544683 0.751582 -0.36641 0.548515 +-0.313567 -0.407443 -0.85771 0.664143 -0.538561 0.518523 0.701125 -0.585751 0.406596 +0.652904 -0.668969 0.355242 0.671486 -0.683368 0.286556 -0.07804 -0.439296 -0.894946 +-0.486268 -0.373653 -0.78989 0.712297 -0.654556 0.253354 0.670951 -0.645437 0.365014 +0.647262 -0.64762 0.402046 0.659382 -0.673789 0.333501 0.638375 -0.734191 0.231172 +0.476898 -0.495661 -0.725871 0.285009 -0.359813 -0.888428 0.674084 -0.681687 0.284452 +0.559729 -0.797684 0.224508 0.328899 -0.789561 0.518091 0.350771 -0.506273 0.787812 +0.445449 -0.555469 -0.702161 0.54447 -0.461549 -0.700375 0.63495 -0.648638 0.419651 +0.468417 -0.415919 0.779485 0.650184 -0.737626 0.182122 0.174592 -0.753928 -0.633332 +0.374131 -0.644193 -0.667114 0.709713 -0.669498 0.21927 0.604402 -0.774176 0.188014 +0.495589 -0.817876 0.292352 0.528765 -0.780141 0.334347 0.548542 -0.783047 0.293154 +0.558022 -0.82609 0.078657 0.567108 -0.800328 -0.194584 0.557763 -0.734398 -0.38673 +0.475393 -0.854522 -0.209269 0.384203 -0.923011 -0.020945 0.364455 -0.930526 0.0359658 +0.369625 -0.899761 0.231966 0.557241 -0.719669 0.414197 0.440001 -0.728419 0.525172 +0.182756 -0.777051 -0.602322 0.294441 -0.842716 -0.450705 0.0595391 -0.992997 -0.102043 +0.148108 -0.952905 -0.264644 0.413932 -0.893325 0.175014 0.420619 -0.906878 -0.0255319 +0.514745 -0.812586 -0.27339 0.314935 -0.902514 0.293743 0.216587 -0.919411 -0.328289 +0.156488 0.943713 0.291406 0.16877 0.8191 0.548262 0.0485763 0.994455 0.093274 +0.287081 0.948896 0.131073 0.32658 0.938036 -0.115908 0.0345485 0.880234 -0.47328 +0.0140474 0.435208 -0.90022 0.125909 0.956011 0.264935 0.160073 0.829164 0.535596 +-0.0202581 0.999062 0.0382733 -0.024746 0.999526 -0.0182987 -0.0242367 0.999704 -0.00204238 +-0.0218534 0.999634 0.0159457 -0.0356972 0.999348 0.00545961 -0.0567938 0.997257 -0.0474678 +-0.170349 0.896526 -0.408927 -0.286798 0.76254 -0.579897 -0.0106595 0.999894 -0.00991285 +0.00937559 0.806901 0.590612 0.0116089 0.792408 -0.609881 0.0276936 0.926415 0.375484 +-0.0490203 0.792537 0.607851 0.039713 0.993195 0.109481 0.207578 0.911077 0.356161 +0.0338684 0.400295 -0.91576 0.0186504 0.816079 0.577639 -0.0107364 0.444963 0.895485 +0.18105 0.878089 0.442924 -0.118239 0.947779 -0.296199 0.00284037 0.999161 -0.0408491 +0.000593746 0.999823 0.0188067 -0.00294408 0.999977 0.00617467 -0.00481502 0.999988 -0.000476124 +-0.0053425 0.999958 -0.00750243 -0.00337661 0.999659 -0.0259081 -0.00397746 0.998845 0.0478794 +-0.00934017 0.919623 0.39269 -0.0286362 0.945511 -0.324329 0.00638764 0.949867 0.312588 +-0.0149673 0.562451 -0.826695 -0.152371 -0.0166059 -0.988184 -0.00714623 -0.00438004 0.999965 +-0.257038 -0.483908 -0.836519 -0.16874 -0.583427 -0.794443 -0.377441 -0.582644 -0.719767 +0.165409 -0.300088 0.939461 -0.0376559 -0.0838018 -0.995771 0.191867 -0.583924 0.788809 +0.224292 -0.367878 0.902418 0.0114042 0.0324757 -0.999408 0.0245878 -0.0539501 0.998241 +0.00262397 0.00540948 -0.999982 -0.124525 -0.317779 0.939952 -0.000576368 -0.00391972 -0.999992 +-0.390106 -0.576879 0.717655 -0.322083 -0.585589 0.743874 -0.211888 -0.476865 0.853055 +-0.000299121 -0.00154803 -0.999999 -0.0467899 -0.0723111 0.996284 -0.00128356 -0.0033309 -0.999994 +0.0197692 0.0204769 0.999595 0.0013573 0.00532175 -0.999985 0.0420264 -0.0216036 0.998883 +0.00969374 0.0301853 -0.999497 0.545845 -0.174785 0.819453 0.285405 0.0531427 0.956933 +-0.0369035 -0.0819197 -0.995955 0.0332026 0.0318021 0.998943 -0.198431 -0.476854 -0.856292 +-0.0110055 -0.0140161 0.999841 -0.134336 -0.3391 -0.93111 -0.309639 -0.599208 -0.738291 +-0.00110513 -0.00193564 0.999997 0.0215042 0.0196936 -0.999575 0.000838131 0.0013258 0.999999 +0.306348 0.211734 -0.928073 9.65741e-05 0.000635673 1 0.438157 0.307455 -0.844683 +9.71002e-05 0.0014909 0.999999 0.270477 0.563886 -0.780304 0.203558 0.301532 -0.931473 +0.000512559 -0.00310511 0.999995 -0.00353845 0.00559981 -0.999978 0.00480531 -0.0114565 0.999923 +-0.00738708 -0.0107556 -0.999915 -0.00859013 0.00972682 0.999916 0.00234892 0.00376687 -0.99999 +-0.0340963 0.0274785 0.999041 0.00111954 0.000701769 -0.999999 -0.628052 -0.0984286 0.771922 +-0.280862 0.0472734 0.958583 0.000849348 -0.00203943 -0.999998 -0.0484856 -0.0390576 0.99806 +0.00257759 -0.00151837 -0.999996 0.0205265 0.0198298 0.999593 0.00209589 0.000771468 -0.999998 +0.00387035 0.00413235 0.999984 -0.00197802 0.00553043 -0.999983 -0.00308852 -0.00561866 0.999979 +-0.0064357 0.0103438 -0.999926 -0.00115807 0.00197251 0.999997 0.0152972 -0.00982462 -0.999835 +0.0007818 0.0186513 0.999826 0.0401339 -0.0271752 -0.998825 0.000216465 -0.0416599 0.999132 +-0.117019 -0.787337 -0.605315 -0.137435 -0.94847 -0.285509 0.0139767 -0.999159 -0.0385465 +0.0251571 -0.999647 0.00856512 0.0269347 -0.997646 0.0630568 0.165874 -0.896123 0.411642 +0.283787 -0.763175 0.580541 -0.141768 -0.816504 -0.559663 -0.26641 -0.963849 -0.00463523 +-0.309035 -0.778434 0.546386 0.471407 -0.645331 -0.601102 0.292324 -0.329596 -0.897727 +-0.0108482 -0.340787 0.940078 -0.033744 -0.786227 -0.617016 -0.0282086 -0.931061 -0.363771 +-0.0132744 -0.999132 -0.0394747 -0.0523455 -0.998565 -0.0113143 -0.216192 -0.894308 -0.391759 +-0.00817377 -0.966705 0.255763 -0.286369 -0.928383 -0.236851 -0.173182 -0.855169 0.488564 +-0.0257337 -0.996138 0.0839432 0.013697 -0.999394 -0.0319943 0.0195119 -0.999693 0.0152537 +0.297037 -0.948882 0.106732 0.334223 -0.935146 -0.117462 0.107866 -0.994141 -0.00700849 +0.43825 -0.859334 -0.263594 -0.0109254 -0.632359 0.774598 -0.0166929 -0.778719 0.627151 +-0.00393323 -0.806122 -0.591737 0.0332837 -0.935027 -0.35301 -0.0245926 -0.996229 -0.0832088 +-0.0537164 -0.943811 -0.326091 -0.00277694 -0.938918 0.34413 0.00902331 -0.997931 0.063651 +-0.00315107 -0.997583 -0.0694183 -0.000840973 -0.927607 -0.373557 -0.422733 0.729901 0.53716 +-0.527733 0.724824 0.442864 -0.449037 0.869651 0.205118 -0.349253 0.936814 0.0200383 +-0.339937 0.939398 -0.0444348 -0.347778 0.912598 -0.214975 -0.604982 0.661314 -0.443464 +-0.431362 0.72361 -0.538809 -0.499353 0.613266 0.612006 -0.612638 0.613188 0.498674 +-0.531267 0.833338 0.152656 -0.238829 0.909112 -0.341286 -0.433542 0.700059 -0.567414 +-0.445452 0.675482 0.58762 -0.502535 0.714949 0.486113 -0.39825 0.83103 0.388312 +-0.471065 0.794904 0.382394 -0.584036 0.700515 -0.410098 -0.447046 0.888044 -0.107367 +-0.358764 0.912094 -0.198426 -0.547855 0.76145 -0.346482 -0.540508 0.796205 -0.271863 +-0.599369 0.795918 -0.0852676 -0.641231 0.767279 -0.0102526 -0.684956 0.728551 0.00702153 +-0.698954 0.689481 0.189944 -0.615563 0.582811 0.530485 -0.624983 0.535964 0.567572 +-0.377335 0.855693 -0.354129 -0.324045 0.943828 -0.064681 -0.35275 0.914966 -0.195972 +-0.54989 0.742343 -0.382817 -0.427698 0.768871 -0.475303 -0.354298 0.5535 0.753731 +-0.391019 0.670294 0.630722 -0.405148 0.727227 0.554072 -0.536905 0.806615 -0.247195 +-0.641509 0.753834 -0.142129 -0.723394 0.689887 -0.0275122 -0.709381 0.703563 0.0421708 +-0.653328 0.678935 0.334979 -0.344131 0.891152 -0.295673 -0.143483 0.977352 -0.155552 +-0.143593 0.917989 -0.3697 -0.29488 0.738957 -0.605796 -0.27193 0.61502 0.740138 +-0.212794 0.50976 0.833585 -0.119967 0.757025 -0.642278 -0.0801999 0.754982 -0.650822 +-0.271629 0.609583 0.744732 -0.156122 0.763696 0.626414 0.044712 0.624656 -0.779619 +0.0550163 0.420743 -0.90551 -0.424628 0.571204 0.702437 -0.439982 0.613091 0.656152 +-0.248135 0.820418 0.515115 0.373298 0.41616 -0.829132 -0.524491 0.508699 0.68274 +-0.28512 0.261328 0.922179 0.517674 -0.20088 -0.831662 0.569415 0.405263 -0.715213 +0.125676 -0.179037 -0.975782 -0.0657699 0.0668982 0.99559 0.565843 -0.0926882 -0.819287 +0.31842 -0.409753 -0.854816 0.0551053 -0.719947 -0.691838 -0.0975127 0.0536722 0.993786 +0.588776 0.34618 -0.730413 0.322141 0.129734 -0.93776 -0.52542 -0.159802 0.835702 +-0.313177 0.0118789 0.949621 0.00990375 -0.0340786 -0.99937 -0.346608 -0.180359 0.920507 +-0.171832 -0.451077 -0.875787 -0.435261 -0.21882 0.873307 -0.244238 -0.508916 -0.82544 +-0.297367 -0.607927 -0.736205 -0.425725 -0.621639 0.657513 -0.519357 -0.42644 0.740552 +-0.444445 -0.52381 0.726699 -0.798963 -0.35935 -0.482209 -0.359999 -0.660534 -0.658859 +-0.533297 -0.398969 -0.745934 -0.388312 -0.623367 0.678695 -0.677233 -0.440721 0.58917 +-0.23702 -0.729216 0.641923 -0.649381 -0.0401634 -0.759402 -0.795432 -0.0227549 -0.605616 +-0.825664 -0.260595 0.500369 -0.415389 -0.607336 0.677197 -0.754428 -0.264557 0.600706 +-0.307712 -0.380618 0.872034 -0.57552 -0.152631 -0.803418 -0.642343 -0.170556 -0.747199 +-0.797657 -0.2628 0.542844 -0.338997 -0.348669 0.873791 -0.463171 -0.104009 -0.880145 +-0.893537 -0.055997 0.445483 -0.418392 -0.524928 0.741214 -0.799184 -0.212413 0.562304 +-0.329373 -0.464605 0.821983 -0.388374 -0.409655 -0.825439 -0.829032 -0.209901 0.518312 +-0.897868 -0.0182474 0.439887 -0.630067 -0.231842 0.741125 -0.392156 -0.331036 -0.858271 +-0.50672 -0.511059 -0.694301 -0.741019 -0.174373 0.648448 -0.706597 0.0869623 0.702252 +-0.484223 0.0315526 -0.874375 -0.589307 -0.319182 0.742186 -0.434853 -0.127758 0.891393 +-0.610949 0.0391543 -0.790702 -0.0828642 -0.0417672 0.995685 -0.49053 0.381789 -0.783337 +-0.70668 0.129238 -0.695629 0.0675046 0.0606377 0.995875 -0.644077 0.0670253 -0.762019 +-0.752869 0.016249 -0.65797 0.398557 0.368034 0.840061 -0.66049 -0.0237336 -0.750459 +-0.725879 -0.0258388 -0.687337 0.534424 0.461182 0.70831 0.436129 0.166895 0.884273 +0.611052 0.297576 0.733528 -0.66747 -0.0134089 -0.744516 -0.72147 0.0403349 -0.69127 +0.577644 0.0997611 0.81017 0.331181 0.0686027 0.94107 -0.635656 0.0205216 -0.7717 +-0.601649 0.157922 -0.782993 0.599685 0.28101 0.749274 0.308769 0.381486 0.871281 +-0.599782 -0.0147602 -0.800027 -0.490622 -0.0588131 -0.869386 0.451196 0.676895 0.58158 +0.749276 -0.0267637 0.661717 0.649341 0.166209 0.742112 -0.689616 -0.123551 -0.713558 +-0.411249 -0.32355 -0.852168 0.694944 -0.391925 0.602866 0.784788 -0.266615 0.559486 +0.712836 -0.426635 0.556639 -0.34327 0.0812864 -0.935713 -0.294961 -0.537482 -0.790007 +-0.771533 -0.127963 -0.623187 -0.761034 -0.230621 -0.606334 0.824929 -0.264007 0.499793 +0.778205 -0.287341 0.558419 0.800376 -0.419625 0.428151 -0.421757 0.494759 -0.759826 +-0.247076 0.214229 -0.945018 0.757502 -0.402668 0.513856 0.742657 -0.539346 0.396948 +0.849817 -0.424734 0.312108 0.799386 -0.568115 0.195517 0.68968 -0.669635 0.275556 +0.0661087 0.0283106 -0.997411 0.765434 -0.632075 0.120799 0.720207 -0.686876 0.0974801 +0.668291 -0.654208 0.354118 0.628302 -0.652223 0.424076 0.672169 -0.66459 0.326357 +0.684445 -0.7251 0.0759302 0.607246 -0.767803 0.204282 0.527372 -0.499963 -0.686961 +0.281341 -0.284498 -0.916465 0.69129 -0.667815 0.275936 0.76395 -0.643167 0.0521148 +0.69145 -0.566402 0.448426 0.536004 -0.219892 0.815075 0.451863 -0.689773 -0.565715 +0.575382 -0.63331 -0.517547 0.52484 -0.529912 -0.666135 0.636627 -0.694705 0.334798 +0.533124 -0.508348 0.676285 0.604375 -0.796279 0.0259181 0.568132 -0.806454 0.163886 +0.607948 -0.732333 -0.306737 0.600652 -0.794208 -0.0919323 0.574676 -0.818316 0.0103392 +0.582458 -0.792089 0.182584 0.432822 -0.699466 -0.568693 0.51081 -0.687359 -0.516343 +0.363029 -0.866398 -0.342876 0.12957 -0.988569 -0.0770883 0.36071 -0.870812 -0.334028 +0.398066 -0.903753 0.157399 0.404093 -0.913714 0.042833 0.398505 -0.895193 0.199557 +0.41302 -0.89245 0.181516 0.41047 -0.910178 0.05559 0.459821 -0.884492 -0.0789822 +0.533601 -0.803073 -0.265224 0.390613 -0.917942 -0.0693119 0.387474 -0.918148 0.08288 +0.406643 -0.882518 0.236228 0.392732 -0.919634 -0.00588148 0.133962 -0.988188 -0.0744259 +0.367378 -0.845043 -0.388503 0.39006 0.892663 0.225847 0.522362 0.75519 0.396013 +0.595698 0.798778 0.0842472 0.6067 0.737204 -0.297399 0.46585 0.869274 -0.165369 +0.540446 0.705034 -0.459178 0.205246 0.893367 0.399713 0.281151 0.80432 0.523472 +0.329413 0.793956 0.511 0.122565 0.980624 0.15282 0.302312 0.90678 0.293867 +0.369423 0.726803 -0.579037 0.202215 0.354518 -0.912922 0.16507 0.920069 0.355281 +0.278515 0.760244 0.586905 0.315551 0.806021 0.500757 0.0488452 0.996705 0.0647516 +0.0469057 0.998897 0.0021585 0.0414476 0.998959 -0.0190528 0.0432996 0.999054 -0.00402303 +0.043781 0.998864 0.0187958 0.0329269 0.999262 -0.0197989 -0.160221 0.945854 -0.282293 +-0.143258 0.787757 -0.599096 -0.374131 0.749727 -0.545834 0.0154328 0.999577 -0.0246703 +0.0158913 0.796834 0.603989 -0.0624043 0.786841 -0.613992 0.15716 0.937141 0.311556 +0.173092 0.814489 0.553757 0.140673 0.944326 0.297421 0.25834 0.467897 -0.845182 +0.168982 0.719028 0.674125 0.00137806 0.413228 0.910627 -0.227861 0.895355 -0.382648 +-0.0595451 0.997291 -0.0431849 -0.027054 0.999632 -0.0020662 -0.0155334 0.999809 0.0118592 +-0.0214942 0.999763 0.00345922 -0.0228981 0.999736 -0.00190769 -0.0225745 0.99973 -0.00543582 +-0.0162957 0.999663 -0.0202226 0.00577789 0.998257 0.0587279 0.03861 0.916357 0.398496 +0.0199479 0.900842 -0.433689 0.197947 0.926743 0.319319 0.171126 0.571693 -0.802424 +0.135809 0.0097163 -0.990687 0.00986076 0.0299087 0.999504 -0.0552989 -0.397203 -0.916063 +-0.0734715 -0.593971 -0.801124 0.039427 -0.0654637 0.997076 0.00621255 -0.0304446 -0.999517 +0.373082 -0.551607 0.746015 0.179707 -0.401573 0.898023 0.00682056 0.0242532 -0.999682 +0.309412 -0.553661 0.773126 0.128606 -0.309648 0.942114 0.000406668 -0.000132568 -1 +-0.00202246 -0.418899 0.908031 0.000496468 -0.0034059 -0.999994 -0.0793146 -0.594312 0.800314 +-0.0133723 -0.37203 0.928124 0.000196447 -0.00241171 -0.999997 0.00417385 -0.0304765 0.999527 +-0.000795938 -0.0034106 -0.999994 -0.0074896 0.0327023 0.999437 0.00363767 0.0125292 -0.999915 +0.0019985 -0.00959837 0.999952 -0.00844532 -0.00194401 -0.999962 0.0140537 0.0057168 0.999885 +-0.155976 -0.319461 -0.934675 -0.0118779 -0.0105291 0.999874 -0.266865 -0.482475 -0.834267 +-0.165066 -0.603905 -0.779777 -0.380819 -0.575657 -0.723599 -0.00273398 -0.00369656 0.999989 +-0.0435741 -0.076294 -0.996133 0.0013459 0.00113703 0.999998 0.17562 0.345834 -0.921714 +0.000284505 0.000355639 1 0.581493 0.236543 -0.778404 0.410548 0.518439 -0.750114 +0.338656 0.419015 -0.84246 -0.000111819 0.000508771 1 0.533454 0.381664 -0.754824 +0.307659 0.179385 -0.934434 0.000243897 0.00187707 0.999998 0.0297819 0.0188633 -0.999378 +0.00186032 -0.000291481 0.999998 -0.0136083 -0.0163746 -0.999773 0.00270057 -0.00778779 0.999966 +0.00132815 0.00184856 -0.999997 0.00274091 -0.0108376 0.999938 -0.000982661 0.0006974 -0.999999 +-0.00994126 0.0114573 0.999885 -0.00800912 -0.00292951 -0.999964 -0.0127765 -0.00518692 0.999905 +-0.00912498 -0.000737644 -0.999958 0.0160435 0.0136293 0.999778 -0.00555213 0.00141808 -0.999984 +0.00537994 0.0045447 0.999975 -0.00036293 0.00165943 -0.999999 -0.00181411 -0.00392949 0.999991 +0.0012275 0.00243605 -0.999996 -0.000820967 -0.00417281 0.999991 -0.00734495 0.00269095 -0.999969 +0.000399608 0.0035515 0.999994 -0.0224813 0.00733875 -0.99972 -0.000126744 0.0182835 0.999833 +-0.0108296 -0.00333823 -0.999936 -0.00642574 -0.0473731 0.998857 -0.28692 -0.771527 -0.567823 +-0.141996 -0.912258 -0.384216 -0.372461 -0.750708 -0.545629 -0.0190239 -0.999287 -0.0326183 +-0.0085689 -0.99977 -0.019668 -0.0336542 -0.999328 0.0145362 -0.0132761 -0.954868 0.296733 +-0.262469 -0.836944 0.480245 0.2977 -0.767999 0.567056 -0.544809 -0.808379 -0.222947 +-0.586061 -0.751379 -0.303253 -0.422932 -0.882204 -0.206989 -0.303457 -0.935509 -0.18093 +-0.50015 -0.78018 -0.375724 -0.09166 -0.993832 0.062426 -0.291488 -0.842649 0.452745 +-0.380266 -0.707251 0.595982 -0.23943 -0.755871 0.609371 0.0171538 -0.0292554 -0.999425 +0.027264 -0.341433 0.93951 -0.17574 -0.803106 -0.56933 -0.13996 -0.95457 -0.263075 +0.0103465 -0.99714 -0.0748603 -0.116241 -0.955647 -0.270603 -0.181895 -0.965307 0.18734 +-0.172488 -0.928459 0.328956 -0.012946 -0.999912 0.00283593 0.0545467 -0.962043 0.26739 +-0.0869638 -0.792495 0.603647 0.459254 -0.804549 0.376546 0.623794 -0.776509 -0.0889649 +0.597975 -0.751794 -0.277907 0.498774 -0.862676 0.083758 0.611528 -0.767473 0.192403 +0.633895 -0.773396 -0.00603167 0.315859 -0.423293 -0.84915 -0.00202531 -0.636423 0.771337 +0.0782666 -0.775055 0.627028 0.157888 -0.81889 -0.551807 0.252357 -0.837499 -0.484677 +0.0159903 -0.988829 -0.148197 -0.193832 -0.905327 -0.377904 0.232871 -0.916807 0.324402 +0.420888 -0.883186 -0.206968 0.374284 -0.838287 -0.396468 0.0219705 -0.935329 0.353096 +-0.015968 -0.999152 0.03794 -0.0519213 -0.99689 -0.0592902 -0.231562 -0.952269 -0.198902 +-0.637204 0.706765 0.307334 -0.655536 0.750461 0.0841529 -0.646882 0.762377 -0.0180199 +-0.666651 0.700367 -0.255074 -0.894385 0.40006 0.200068 -0.843681 0.506515 -0.177893 +-0.485747 0.713806 0.504511 -0.496386 0.744965 0.445677 -0.486933 0.839909 0.239685 +-0.642055 0.754072 0.138351 -0.756788 0.653333 0.0207064 -0.776158 0.621202 -0.108101 +-0.709086 0.599398 -0.371375 -0.726945 0.57189 -0.380122 -0.679811 0.65472 -0.330452 +-0.685658 0.72418 -0.0737368 -0.608744 0.780328 -0.143245 -0.783239 0.512384 0.352136 +-0.921712 0.376518 -0.0931694 -0.988572 0.144623 -0.0425297 -0.867668 0.492771 -0.0657952 +-0.662703 0.748861 0.00559325 -0.617144 0.773008 -0.146945 -0.320703 0.825467 0.464493 +-0.350456 0.659462 0.665049 -0.442038 0.81298 0.379033 -0.72811 0.667653 -0.155224 +-0.711284 0.702467 -0.0247915 -0.614891 0.761924 -0.203422 -0.553348 0.783092 -0.283855 +-0.680648 0.636784 -0.362249 -0.633949 0.710245 -0.30604 -0.687044 0.721799 -0.0835237 +-0.684102 0.729305 0.0108809 -0.646452 0.736265 0.200034 -0.64213 0.637841 0.425239 +-0.785555 0.590998 0.183371 -0.786859 0.551366 -0.277215 -0.91137 0.407662 -0.0567082 +-0.744215 0.55535 -0.371122 -0.709034 0.682512 0.177337 -0.697676 0.561419 0.445036 +-0.713023 0.701017 -0.0131672 -0.59757 0.787362 -0.151561 -0.452565 0.889295 -0.0658691 +-0.354395 0.90779 -0.224325 -0.53485 0.753629 -0.382072 -0.421492 0.779843 -0.462805 +-0.331353 0.646393 0.6873 -0.215132 0.533733 0.817831 -0.741592 0.644523 -0.186098 +-0.754424 0.656144 -0.0178873 -0.760902 0.644318 0.0766996 -0.751725 0.595209 0.283964 +-0.757274 0.643685 0.110481 -0.40933 0.788912 -0.45833 -0.259753 0.836548 -0.482405 +-0.310217 0.737998 -0.59927 -0.317541 0.692937 0.647306 -0.0827989 0.610443 0.787721 +-0.140829 0.732046 -0.66654 0.0888441 0.680386 -0.727449 -0.203705 0.774705 0.598612 +-0.287351 0.55174 0.782951 -0.0532324 0.8109 0.582759 0.449537 0.46067 -0.76531 +0.268119 0.638166 -0.721704 0.0923187 0.252818 -0.963099 -0.485211 0.471551 0.736349 +-0.430207 0.427793 0.79493 -0.207285 -0.542275 -0.81423 -0.253292 -0.626679 -0.736965 +-0.184834 -0.535281 -0.824203 -0.529296 0.416497 0.739172 -0.338014 0.211838 0.91699 +-0.0469761 -0.117013 -0.992019 -0.333314 0.0220295 0.942559 -0.317523 -0.124802 -0.940002 +-0.424214 -0.204468 0.882176 -0.443305 -0.296091 -0.846056 -0.635648 -0.240354 0.733608 +-0.606723 -0.446703 0.657529 -0.701285 -0.364897 0.612413 -0.545706 -0.444043 -0.710655 +-0.605997 -0.419412 -0.675915 -0.44414 -0.516854 -0.731848 -0.624076 -0.309386 0.717502 +-0.672527 -0.158988 0.722793 -0.794741 -0.467222 -0.387414 -0.735359 -0.674026 -0.0702594 +-0.850323 -0.524326 -0.0450889 -0.73183 -0.62606 0.26921 -0.442576 -0.580374 -0.683588 +-0.547251 -0.26903 0.792552 -0.824971 -0.132058 -0.54953 -0.790526 -0.344066 -0.506644 +-0.878252 -0.232091 -0.4181 -0.921987 -0.195153 -0.334448 -0.67565 -0.728154 -0.11528 +-0.285879 -0.63373 0.718791 -0.35368 -0.683492 0.638552 -0.909339 -0.0144159 -0.415806 +-0.817477 0.00763037 -0.575911 -0.930349 -0.00779536 -0.366591 -0.183449 -0.642327 0.744152 +-0.315528 -0.35497 0.880022 -0.916246 0.0284986 -0.399602 -0.819758 0.0203147 -0.57235 +-0.915278 0.0318367 -0.401563 -0.455003 -0.334888 0.825119 -0.919043 -0.0320794 -0.392849 +-0.814013 -0.0925888 -0.573419 -0.90153 -0.00232205 -0.43271 -0.225559 -0.580078 0.782708 +-0.411197 -0.557401 0.721263 -0.907056 -0.165177 -0.387254 -0.780249 -0.274894 -0.561823 +-0.867051 -0.175301 -0.46636 -0.614313 -0.0845465 0.78452 -0.52358 -0.488292 0.698166 +-0.478994 -0.267029 0.836218 -0.819667 -0.168059 -0.547633 -0.780406 -0.343138 -0.522707 +-0.889815 -0.205209 -0.407577 -0.855384 -0.263081 -0.446213 -0.615923 0.300451 0.728263 +-0.374323 0.157592 0.913809 -0.923986 -0.0416899 -0.380146 -0.819866 -0.0133137 -0.5724 +-0.874805 -0.0671831 -0.479795 -0.451762 0.433632 0.779663 -0.35424 0.292081 0.888371 +-0.917744 0.015331 -0.396878 -0.806073 0.0820631 -0.586099 -0.906088 0.00947076 -0.422983 +-0.000372054 0.0762527 0.997088 -0.848108 0.202081 -0.489771 -0.748121 0.319417 -0.581625 +-0.903646 0.0300724 -0.427223 0.1684 0.327964 0.92956 -0.717216 0.395649 -0.57364 +-0.793246 0.174994 -0.583214 -0.751961 0.181344 -0.633774 0.173946 0.587985 0.789947 +0.273114 0.426569 0.862234 0.404432 0.580885 0.706405 -0.694862 0.02526 -0.7187 +-0.755637 -0.0217421 -0.654629 0.130161 0.0779423 0.988425 -0.688495 -0.0124786 -0.725134 +-0.743668 0.0262554 -0.668034 0.337458 0.157852 0.928011 -0.697681 0.179508 -0.693554 +-0.707691 0.285709 -0.646176 0.324379 0.546956 0.771762 -0.575326 0.47075 -0.668875 +-0.386601 0.0931741 -0.917528 0.583991 0.662364 0.469284 0.565438 0.247856 0.786669 +0.762251 0.169594 0.62467 0.52583 0.782784 0.332794 -0.253533 -0.398915 -0.881242 +0.868451 -0.0347489 0.494555 0.675036 0.0865844 0.732687 0.829205 -0.264377 0.492468 +-0.153434 0.00769896 -0.988129 -0.327702 -0.496299 -0.803927 0.870281 -0.185964 0.456102 +0.731465 -0.271254 0.625604 0.913649 -0.184199 0.362377 -0.00836375 0.584525 -0.811333 +-0.0472329 0.397752 -0.916276 0.737531 -0.463335 0.491293 0.709751 -0.513732 0.482009 +0.840874 -0.376549 0.38877 0.824088 -0.384847 0.415658 -0.014113 0.0232829 -0.999629 +0.71854 -0.48632 0.497184 0.674087 -0.597708 0.433995 0.818231 -0.479052 0.317817 +0.773077 -0.605375 0.189401 0.705461 -0.708515 0.0181826 0.678827 -0.688273 0.255878 +0.0583355 -0.0549079 -0.996786 0.696646 -0.661885 0.276753 0.724996 -0.68872 0.0067693 +0.694208 -0.716358 0.0700469 0.661647 -0.651721 0.370787 0.564324 -0.739463 0.36706 +0.615268 -0.712141 0.338083 0.737768 -0.641185 0.211141 0.860485 -0.394469 0.322429 +0.47101 -0.662452 -0.5825 0.250151 -0.368382 -0.895388 0.403982 -0.885354 0.230101 +0.375261 -0.827424 0.41779 0.22694 -0.767326 0.599757 0.658505 -0.494733 0.567107 +0.507022 -0.688459 -0.518607 0.526096 -0.719798 -0.452895 0.435529 -0.877922 -0.198918 +0.350458 -0.913036 -0.208674 0.265652 -0.814539 -0.515708 0.355041 -0.799013 0.485308 +0.394423 -0.897018 0.199474 0.416124 -0.908515 0.0379527 0.428039 -0.880977 0.201651 +0.498662 -0.805322 0.320611 0.543532 -0.795291 0.268488 0.546025 -0.83202 0.0979759 +0.540395 -0.841411 0.00049124 0.568397 -0.805128 -0.16939 0.535627 -0.711883 -0.454231 +0.377027 -0.855676 -0.354498 0.172669 -0.98498 -0.000598693 0.354923 -0.890527 0.284588 +0.458393 -0.783152 0.420177 0.612662 -0.781302 0.119217 0.584976 -0.750835 -0.306677 +0.466019 -0.854067 -0.231076 0.353942 -0.909198 -0.219282 0.448435 -0.812183 -0.373182 +0.388198 -0.906114 0.168108 0.391791 -0.919464 0.0329455 0.397445 -0.916587 -0.0436402 +0.417683 -0.856113 -0.304321 0.426133 -0.903119 -0.0527868 0.286159 0.909782 0.300683 +0.490571 0.729985 0.47588 0.48827 0.728461 0.480559 0.0429838 0.996217 0.0755207 +0.0991005 0.993727 -0.0518215 0.438769 0.582697 -0.684066 0.300803 0.833777 -0.462962 +0.190814 0.490369 -0.850369 0.216005 0.891961 0.397175 0.224958 0.816383 0.531896 +0.29012 0.884138 0.366238 0.195158 0.347686 -0.917076 0.214295 0.936652 0.277058 +0.254427 0.772209 0.582203 0.0731636 0.992316 0.0997792 0.262352 0.957274 0.121642 +0.356467 0.934241 0.011179 0.366338 0.930373 -0.0142007 0.367506 0.929954 0.0112025 +0.374409 0.927245 0.00593673 0.299972 0.893521 -0.334122 0.213391 0.611823 -0.761668 +-0.158903 0.939958 -0.30204 0.185557 0.789213 0.585415 -0.32833 0.761251 -0.559193 +0.199317 0.891967 0.405793 0.274057 0.799563 0.534408 0.32475 0.787843 0.523298 +0.122282 0.977809 0.170108 0.22129 0.909186 0.352719 0.376749 0.578227 -0.72368 +0.341206 0.569814 -0.74759 0.200898 0.411878 -0.888818 0.328807 0.591642 0.736102 +0.129443 0.308913 0.942241 -0.0037006 0.917645 -0.397385 0.0261692 0.999038 -0.0351998 +0.0532231 0.998374 0.0204129 0.051025 0.998684 0.00521443 0.045945 0.998944 -0.0002568 +0.04493 0.998987 -0.0024036 0.0440951 0.999016 -0.00484626 0.05437 0.99852 0.000967129 +0.0609249 0.996421 0.0585891 0.22176 0.897499 0.381206 0.0209259 0.999109 -0.0366408 +-0.170936 -0.416766 -0.892798 0.00425863 0.0217201 0.999755 -0.120934 -0.334907 -0.934458 +-0.29768 -0.592821 -0.748298 0.0137136 -0.0566232 0.998301 -0.014799 0.000349388 -0.99989 +-0.0282668 -0.363089 0.931325 -0.00316608 0.0176751 -0.999839 0.086732 -0.573064 0.814908 +-0.0115962 -0.427722 0.903836 -0.00303711 0.000563502 -0.999995 0.000616502 -0.415341 0.909666 +0.000382571 -0.0021833 -0.999998 0.0164895 -0.585697 0.810363 0.0119113 -0.366813 0.930219 +0.000269648 -0.00395721 -0.999992 0.00147288 -0.0408492 0.999164 5.25334e-05 -0.00153161 -0.999999 +-0.00651709 0.0236173 0.9997 0.00787855 0.0214548 -0.999739 -0.0106938 0.00341963 0.999937 +0.00854478 -0.0275047 -0.999585 -0.00860798 -0.00534831 0.999949 -0.0388537 -0.382591 -0.923101 +-0.00146311 -0.00230563 0.999996 -0.0621746 -0.405526 -0.911967 -0.0778243 -0.618544 -0.781886 +0.00216408 0.000351105 0.999998 0.0423707 0.0473618 -0.997979 0.000899903 -0.000123662 1 +0.254571 0.504803 -0.824844 1.02055e-05 -0.000254517 1 0.227326 0.608252 -0.760495 +0.172247 0.317801 -0.93238 0.000182851 0.000793315 1 0.0237136 0.0133753 -0.999629 +0.000721016 0.00238987 0.999997 -0.0192293 -0.0232885 -0.999544 -0.0035913 -0.00283056 0.99999 +-0.0006005 0.000892352 -0.999999 -0.0109401 -0.0112316 0.999877 -0.000763707 -0.000394613 -1 +0.0184564 0.00531395 0.999816 -0.0131205 -0.00946075 -0.999869 0.012094 -0.0139541 0.99983 +0.0125159 0.00911372 -0.99988 -0.00251536 0.0108307 0.999938 0.00255019 -0.0128936 -0.999914 +-0.00307784 0.00861841 0.999958 -0.00903254 0.00806053 -0.999927 -0.00231738 0.000264922 0.999997 +-0.00104509 0.00790706 -0.999968 -0.000314962 -0.00319884 0.999995 -0.00463261 -0.00470255 -0.999978 +0.000321159 -0.00327529 0.999995 -0.0218778 -0.0151978 -0.999645 6.6403e-05 0.00324994 0.999995 +0.00349055 0.0145355 -0.999888 -0.00373854 0.0136565 0.9999 0.0229845 0.0220321 -0.999493 +0.00292484 -0.0343864 0.999404 -0.0584798 -0.810582 -0.582698 -0.0511303 -0.925829 -0.374468 +-0.0515376 -0.997123 -0.0555807 -0.0402841 -0.999063 -0.0158074 -0.277622 -0.949593 -0.145602 +-0.349077 -0.925727 0.145515 -0.301651 -0.949096 0.0906868 -0.563422 -0.814938 -0.13576 +-0.0474118 -0.997758 -0.0472274 -0.0394176 -0.998846 -0.0274257 0.00287377 -0.999996 0.000525568 +-0.152257 -0.92655 0.343982 -0.336227 -0.712709 0.615627 0.0106894 -0.0384611 -0.999203 +0.169179 -0.241597 0.955515 -0.296696 -0.760252 -0.577917 -0.172496 -0.910263 -0.376387 +-0.341266 -0.77143 -0.53706 -0.0198274 -0.999489 -0.0250644 -0.00779866 -0.998354 -0.0568121 +-0.024526 -0.935206 -0.353255 -0.239102 -0.850602 0.468301 -0.0482175 -0.997022 0.0601919 +-0.020942 -0.99877 0.0449397 -0.264065 -0.829813 0.49161 -0.314049 -0.774875 0.548581 +-0.272423 -0.754323 0.597312 0.783163 -0.621626 0.0153981 0.00776294 -0.357517 -0.933875 +0.197606 -0.610534 0.766942 0.312055 -0.761522 0.568072 -0.584991 -0.697731 -0.41347 +-0.419918 -0.904334 -0.0764724 -0.599468 -0.799833 0.0301053 0.366625 -0.778223 -0.509859 +-0.573375 -0.738384 0.355007 0.154084 -0.768115 -0.621496 -0.16458 -0.859476 -0.483957 +0.112251 -0.927511 0.356543 0.760656 -0.648649 -0.0256271 0.169367 -0.789288 0.590203 +0.211392 -0.752726 -0.623472 0.417673 -0.733552 -0.536144 0.113769 -0.960535 0.253828 +-0.0310166 -0.999471 0.00980685 -0.326275 -0.881718 -0.34076 -0.185005 -0.960359 -0.208529 +0.311302 -0.9466 -0.0839003 -0.61587 0.72989 0.296588 -0.671182 0.738769 0.0611114 +-0.681837 0.731492 -0.00429705 -0.739866 0.654816 -0.154321 -0.839155 0.489558 -0.236964 +-0.451288 0.608917 0.652349 -0.588068 0.671386 0.451017 -0.546511 0.804852 0.231387 +-0.65709 0.74203 0.132759 -0.704402 0.708359 0.0452156 -0.552653 0.830042 0.0748671 +-0.426159 0.803017 -0.416596 -0.748473 0.558374 -0.35778 -0.44554 0.557752 -0.700291 +-0.808353 0.564042 -0.168591 -0.710406 0.703711 -0.010701 -0.736482 0.663499 -0.13177 +-0.778604 0.57842 -0.243324 -0.90313 0.383986 0.19212 -0.851814 0.493657 -0.17526 +-0.649999 0.684522 0.330046 -0.70453 0.70755 0.0548772 -0.69219 0.721427 -0.0203872 +-0.671746 0.740764 0.00506093 -0.616681 0.774655 -0.14005 -0.262866 0.699187 0.66486 +-0.505286 0.667137 0.54737 -0.621112 0.512688 0.592765 -0.739576 0.629156 -0.239145 +-0.731949 0.681106 0.0185687 -0.733718 0.678041 -0.0437944 -0.714485 0.636645 -0.290163 +-0.793548 0.46005 -0.398292 -0.76245 0.478235 -0.435847 -0.919284 0.338287 -0.201193 +-0.938388 0.34409 -0.0320759 -0.942968 0.332666 0.0120129 -0.95592 0.265562 0.125277 +-0.987723 0.119324 0.100821 -0.933864 0.328245 0.14196 -0.772049 0.635324 -0.0174127 +-0.624393 0.677864 0.388115 -0.711352 0.642286 0.28539 -0.718337 0.695405 0.0200956 +-0.708968 0.705227 -0.00448402 -0.710676 0.703373 -0.014374 -0.588739 0.784957 -0.192946 +-0.444014 0.834801 -0.325513 -0.532131 0.750003 -0.392852 -0.431129 0.758021 -0.48942 +-0.174681 0.789783 0.587987 -0.145566 0.534785 0.832355 0.151839 0.676233 0.72087 +-0.432844 0.827599 0.357388 -0.750556 0.316485 -0.580089 -0.952675 0.0324026 -0.30226 +-0.802851 -0.16129 -0.573947 -0.999068 0.0375572 -0.0212646 -0.998719 0.0486651 0.0138764 +-0.995865 0.0791047 0.0446682 -0.904862 0.282645 0.318333 -0.802645 0.558992 0.208058 +-0.764403 0.51427 0.388864 -0.747301 0.597769 0.290194 -0.381553 0.816996 -0.432361 +-0.204903 0.86536 -0.457347 -0.189772 0.771889 -0.606774 -0.408882 0.567413 0.714744 +-0.488138 0.464756 0.738731 -0.799972 -0.119605 -0.587996 -0.780542 -0.337149 -0.526389 +-0.895624 -0.202765 -0.39591 -0.99793 -0.0459068 -0.045045 -0.999633 -0.0270304 -0.00202868 +-0.998918 -0.0169393 0.0433244 -0.949812 0.0865631 0.300606 -0.737113 0.446112 0.507592 +-0.755988 0.284378 0.589586 -0.268531 0.588704 -0.762442 -0.338178 -0.0843192 -0.937297 +-0.570127 0.434755 0.697096 -0.414274 0.320085 0.852011 -0.871957 0.175844 -0.456913 +-0.761684 0.321534 -0.562541 -0.995483 0.0265607 -0.0911454 -0.999792 0.00298872 0.0201696 +-0.999754 -0.00946715 0.0200373 -0.927578 -0.172726 0.331308 -0.7871 -0.0899998 0.610224 +-0.379937 -0.413466 -0.827462 -0.393283 -0.525012 -0.754779 -0.592663 0.169891 0.78733 +-0.593499 0.20224 0.779011 -0.726687 0.402634 -0.556607 -0.930176 0.167545 -0.326651 +-0.787062 0.167774 -0.593621 -0.999918 0.0124533 -0.00302632 -0.999862 0.0140486 -0.0088139 +-0.998157 -0.0472272 0.0380953 -0.690151 -0.467124 0.552709 -0.849277 -0.3732 0.37343 +-0.672778 -0.548699 0.496285 -0.481587 -0.257985 -0.837566 -0.628313 -0.0422078 0.776815 +-0.768279 0.0116895 0.640008 -0.920314 0.0293736 -0.390076 -0.792865 0.0570806 -0.606718 +-0.99945 -0.0227542 -0.024128 -0.999699 -0.0136918 0.020361 -0.999295 0.0197021 -0.031969 +-0.995467 -0.0507445 0.0804409 -0.689318 -0.481237 0.541528 -0.854091 -0.227882 0.467545 +-0.711816 -0.331371 0.619283 -0.694241 -0.296735 -0.655727 -0.632069 -0.350858 -0.690932 +-0.606509 -0.3168 -0.729235 -0.620261 -0.302857 0.72357 -0.748803 -0.256603 0.611105 +-0.861511 0.187664 -0.471786 -0.727707 0.296579 -0.618452 -0.99373 0.0708095 -0.0865274 +-0.999711 0.000581202 0.0240135 -0.999929 -0.0114689 0.00325626 -0.99999 0.0043167 0.00126162 +-0.930189 -0.166425 0.327186 -0.763066 -0.215203 0.609441 -0.646946 -0.318106 -0.693015 +-0.735913 -0.387776 -0.555033 -0.643421 -0.477093 0.598659 -0.762192 -0.349218 0.545078 +-0.677627 -0.510474 0.529375 -0.69906 0.463112 -0.544832 -0.84619 0.44309 -0.29603 +-0.696769 0.581597 -0.419831 -0.93188 0.334083 -0.141381 -0.998239 0.0483236 -0.0344132 +-0.999984 -0.00467103 -0.00324197 -0.997803 -0.0425654 0.0507695 -0.696299 -0.415484 0.58527 +-0.84396 -0.389557 0.368749 -0.692844 -0.537431 0.480765 -0.763664 -0.537033 -0.358348 +-0.779037 -0.55504 -0.291603 -0.910858 -0.380235 -0.160495 -0.821935 -0.563196 -0.085047 +-0.921165 -0.387403 -0.0370615 -0.791085 -0.610952 0.0303593 -0.878459 -0.477675 0.0116569 +-0.779705 -0.625848 0.0193336 -0.817708 -0.575456 0.0142997 -0.803133 -0.562135 -0.197439 +-0.773161 -0.456076 -0.440701 -0.903588 -0.052166 0.425214 -0.677051 -0.516685 0.524059 +-0.858209 -0.373988 0.351582 -0.700939 -0.54359 0.46173 -0.764694 0.606142 -0.218711 +-0.718611 0.619319 -0.316295 -0.845701 0.401425 -0.351637 -0.70185 0.551351 -0.451019 +-0.997378 0.047787 -0.0543508 -0.99998 0.00588058 -0.00244229 -0.997604 -0.0527248 0.0447915 +-0.72139 -0.621074 0.306372 -0.937523 -0.331202 0.106557 -0.795197 -0.596799 -0.107204 +-0.951894 -0.278064 -0.128755 -0.99644 -0.0542665 -0.0645175 -0.991681 -0.109807 -0.0671602 +-0.883075 -0.398466 -0.247798 -0.818705 -0.529381 -0.222435 -0.788017 -0.331794 0.518596 +-0.993401 -0.072003 0.0892789 -0.720389 -0.612233 0.325901 -0.937483 -0.32667 0.120055 +-0.76311 -0.607311 0.220991 -0.832851 -0.469056 0.293846 -0.697287 -0.589167 0.408256 +-0.697854 0.434057 -0.569732 -0.930788 0.166947 -0.325212 -0.779608 0.154381 -0.606941 +-0.999855 0.0095206 -0.0141356 -0.999899 0.0131972 -0.00531427 -0.999874 -0.0117065 0.0107287 +-0.999422 -0.0290554 0.0176294 -0.998598 0.00184709 -0.0529033 -0.956535 -0.118204 -0.266588 +-0.796234 -0.405314 0.449146 -0.999506 -0.0298374 -0.00988523 -0.996864 -0.0491573 0.062007 +-0.69069 -0.492445 0.529572 -0.865536 -0.150348 0.477748 -0.73571 -0.259116 0.625771 +-0.925012 0.0187319 -0.379475 -0.79386 -0.00416735 -0.608086 -0.99934 -0.00427315 -0.0360598 +-0.999905 -0.00319903 0.0133849 -0.999948 0.0101594 7.78474e-05 -0.999739 0.0146947 0.0175017 +-0.998886 0.0163545 -0.0442575 -0.934189 -0.00742035 -0.356702 -0.9286 -0.1154 0.352682 +-0.996494 0.0813933 0.0193882 -0.897546 0.229465 0.376507 -0.743477 0.276274 0.609027 +-0.927257 -0.00746344 -0.374352 -0.795208 -0.00421684 -0.606322 -0.999042 -0.00474141 -0.0435142 +-0.999801 -0.003383 0.0196686 -0.999962 0.000571113 0.00865618 -0.99981 0.00376972 0.0191174 +-0.998914 0.0058018 -0.0462261 -0.92977 0.0170928 -0.367743 -0.747293 -0.382477 0.543382 +-0.95713 0.0557247 0.28425 -0.654165 0.409922 0.635635 -0.760033 0.275864 0.588429 +-0.926565 0.00146445 -0.376133 -0.794947 -0.000693084 -0.606679 -0.99897 0.00100479 -0.0453592 +-0.99977 0.00134073 0.02142 -0.999925 0.00427437 0.0114863 -0.999822 0.00445101 0.018348 +-0.998425 -0.0113014 -0.0549463 -0.931317 -0.0137804 -0.363948 -0.711242 -0.415635 0.566905 +-0.682617 -0.0790291 0.726491 -0.928399 -0.00752459 -0.37151 -0.795159 -0.0142597 -0.606234 +-0.999046 -0.000999388 -0.0436516 -0.999718 0.00277777 0.0235834 -0.999867 0.00671405 0.0148478 +-0.999921 -0.0120304 -0.00375651 -0.997635 -0.0441967 -0.0526384 -0.902079 -0.16856 -0.397292 +-0.325855 0.0799391 0.942034 -0.926227 0.00369751 -0.376948 -0.785322 0.0617401 -0.616001 +-0.999266 -0.0141422 -0.0355912 -0.999509 -0.00473276 0.0309743 -0.999798 0.00504604 0.0194738 +-0.998971 0.00146516 -0.0453217 -0.945978 -0.156153 -0.28415 -0.0669519 0.180352 0.981321 +-0.858785 0.203908 -0.47001 -0.719682 0.320955 -0.615667 -0.994507 0.0488519 -0.0925757 +-0.99887 -0.022355 0.0419366 -0.99947 -0.00917162 0.0312392 -0.998351 0.00283 -0.0573308 +-0.925976 -0.0348252 -0.375972 -0.304327 0.554141 0.774798 -0.0722375 0.443534 0.893342 +-0.685389 0.481148 -0.54657 -0.849344 0.369969 -0.376481 -0.660317 0.542016 -0.519808 +-0.997046 0.0591066 -0.0490402 -0.998392 -0.0315833 0.0470698 -0.999304 -0.00709786 -0.0366229 +-0.922745 0.0203598 -0.384872 0.165112 0.57957 0.798021 0.0119626 0.33912 0.940667 +-0.658282 0.539764 -0.524709 -0.843036 0.381694 -0.378945 -0.660816 0.531289 -0.530146 +-0.997804 0.0510898 -0.0421637 -0.999684 -0.0171877 0.018371 -0.938505 0.12714 -0.321004 +0.22865 0.442906 0.866922 -0.65963 0.530504 -0.532404 -0.847002 0.376503 -0.375278 +-0.664132 0.526036 -0.531239 -0.998415 0.0485399 -0.0284787 -0.998241 0.0200474 -0.0558004 +-0.852388 0.265159 -0.450695 0.282138 0.569429 0.772106 0.350617 0.407745 0.843096 +-0.666562 0.524734 -0.52948 -0.854942 0.371131 -0.362403 -0.673859 0.518249 -0.526623 +-0.995492 0.0445387 -0.0837404 -0.941414 0.0157988 -0.336883 0.456957 0.240996 0.85622 +-0.680707 0.517405 -0.518585 -0.851917 0.357459 -0.382702 -0.686493 0.516713 -0.511601 +-0.925199 0.0327534 -0.378066 0.686933 0.415841 0.595986 0.712066 0.506315 0.486424 +0.693229 0.401755 0.598353 -0.6763 0.528237 -0.513405 -0.732223 0.389497 -0.558696 +-0.687592 0.531656 -0.494529 0.652983 0.517106 0.553367 0.580687 0.439945 0.685019 +0.741423 0.556484 0.37499 0.709515 0.424286 0.562645 -0.654393 0.490513 -0.575471 +-0.779047 0.11658 -0.616031 -0.591796 0.330627 -0.735162 0.529778 0.278702 0.801037 +-0.554306 -0.160476 -0.816696 -0.599951 -0.153997 -0.785076 0.676106 0.354329 0.646012 +0.723588 0.201573 0.660143 0.627684 0.280578 0.726147 -0.421999 0.260724 -0.868297 +0.761215 -0.0680281 0.644921 0.743069 -0.0622243 0.666316 -0.297302 0.565871 -0.769027 +-0.199368 0.40278 -0.89332 0.840436 -0.252543 0.479467 0.725351 -0.301409 0.618884 +0.798658 -0.397865 0.451497 -0.0176436 0.0776421 -0.996825 0.7582 -0.436935 0.483964 +0.742212 -0.556362 0.373608 0.825121 -0.458103 0.330631 0.72654 -0.557972 0.401008 +0.285766 0.130284 -0.949402 0.740517 -0.625148 0.246626 0.670897 -0.640717 0.373334 +0.628035 -0.657273 0.416609 0.65187 -0.677788 0.340103 0.705065 -0.704731 0.0789772 +0.720893 -0.69242 -0.0294609 0.676747 -0.711545 0.188988 0.4882 -0.397958 -0.776718 +0.684663 -0.666961 0.293936 0.730874 -0.682295 0.0172363 0.727945 -0.685517 0.012737 +0.58718 -0.801104 0.115987 0.464557 -0.880961 0.0899669 0.255087 -0.90246 0.347126 +0.327151 -0.760536 0.560854 0.598874 -0.736895 -0.313584 0.563352 -0.80028 -0.205393 +0.522903 -0.753842 -0.397862 0.432701 -0.726461 -0.533876 0.45291 -0.689865 0.564764 +0.540044 -0.698946 0.468856 0.596282 -0.738217 0.315411 0.573315 -0.817512 0.0546231 +0.538132 -0.833126 0.127729 0.448411 -0.706789 -0.547154 0.525567 -0.746335 -0.408368 +0.401265 -0.912869 0.0752144 0.600334 -0.702454 0.382307 0.405052 -0.762312 0.50479 +0.544535 -0.779552 -0.309486 0.551906 -0.795712 -0.249485 0.543842 -0.766467 -0.341708 +0.495646 -0.828223 -0.261499 0.518707 -0.813863 -0.26186 0.486712 -0.81583 -0.312303 +0.376873 -0.916336 0.13526 0.344663 -0.938643 0.0125058 0.285927 -0.945975 0.152897 +0.142954 -0.984139 0.105044 0.330493 -0.933789 0.137158 0.422005 -0.906409 -0.0182733 +0.386025 -0.892552 -0.233101 0.482551 -0.805149 -0.34479 0.553991 -0.800001 0.23042 +0.168394 -0.985708 -0.00475849 0.350547 -0.925277 -0.144843 0.525103 -0.811572 -0.256161 +0.1591 0.911001 0.380479 0.285519 0.747553 0.599703 0.42755 0.748837 0.506404 +0.0327466 0.997975 0.0545397 -0.00662612 0.999413 0.0336124 -0.0532914 0.998012 -0.0336527 +-0.000757961 0.89227 -0.451501 -0.0198751 0.483603 -0.875062 0.387259 0.900557 0.197556 +0.516316 0.798934 0.308419 0.548328 0.822716 0.149916 0.579485 0.814961 -0.00590664 +0.398889 0.916315 0.035428 0.581123 0.813811 -0.00292271 0.426355 0.90454 -0.005286 +0.59816 0.800539 0.0366404 0.405431 0.913791 0.0247304 0.600333 0.79881 -0.0387739 +0.428842 0.903378 0.00187089 0.518916 0.721098 -0.459069 0.182928 0.898048 -0.400059 +0.0778693 0.575857 -0.813833 0.332087 0.88206 0.334197 0.43017 0.795929 0.425969 +0.344367 0.788509 0.509573 0.216856 0.888996 0.40331 0.178019 0.520123 -0.835333 +0.227737 0.811588 0.538016 0.237481 0.482065 0.843336 0.356956 0.732261 0.57998 +0.368457 0.874118 0.316476 0.39062 0.902965 0.179084 0.23577 0.598108 -0.76595 +0.317659 0.942002 0.10828 0.510454 0.749846 0.420913 0.3871 0.708395 0.590195 +-0.347502 0.743431 -0.571448 0.181881 0.84104 -0.509481 0.271008 0.881891 0.385775 +0.24901 0.810717 0.529841 0.293419 0.895452 0.33477 0.225452 0.53283 -0.815637 +0.109757 -0.0152288 -0.993842 0.0325902 0.070801 0.996958 0.283205 0.804873 -0.521511 +0.349238 0.935722 -0.0495808 0.371476 0.928037 0.0274456 0.369281 0.929312 0.00337307 +0.367748 0.929923 -0.00211357 0.371257 0.92853 0.000665586 0.370372 0.928841 -0.00885996 +0.363913 0.931417 0.00550029 0.392424 0.905557 0.161151 -0.0511097 0.900697 -0.43143 +0.359986 0.911922 0.196999 -0.0688721 -0.474635 -0.877484 0.136892 -0.603905 -0.785213 +-0.332936 -0.565991 -0.754193 -0.00483939 0.000198739 0.999988 0.014445 -0.0977912 -0.995102 +-0.0490278 -0.0861532 0.995075 0.0225002 0.00523534 -0.999733 -0.277827 -0.462191 0.842135 +-0.01055 0.0153291 -0.999827 -0.19374 -0.577141 0.79333 -0.172483 -0.34965 0.920866 +-0.00737869 0.0057709 -0.999956 -0.000141752 -0.365689 0.930737 -0.00199379 0.000664256 -0.999998 +0.018538 -0.58846 0.808314 0.0115707 -0.376279 0.926434 -0.000541374 -0.00486939 -0.999988 +0.00110192 -0.0479015 0.998851 0.00218164 0.0037639 -0.999991 -0.00695235 0.0179673 0.999814 +0.0102781 0.0299965 -0.999497 -0.0103894 0.00307002 0.999941 -0.0357257 -0.0798467 -0.996167 +-0.00647179 0.000782757 0.999979 -0.197441 -0.477799 -0.855994 -0.00120601 0.00237408 0.999996 +-0.121744 -0.326949 -0.937167 -0.312825 -0.609137 -0.728761 0.000592239 -0.000105623 1 +0.152508 0.326448 -0.93283 0.000345684 -0.001141 0.999999 0.348304 0.595718 -0.723743 +0.320783 0.607395 -0.726753 0.229685 0.486398 -0.843008 0.000142328 -0.000295353 1 +0.0295566 0.0729549 -0.996897 0.000838329 0.00150912 0.999998 -0.0230059 -0.0325067 -0.999207 +-0.00146697 -0.00134699 0.999998 -0.00300647 -0.0023252 -0.999993 -0.0121681 -0.0134271 0.999836 +0.0021154 0.00279793 -0.999994 0.0353204 0.0326309 0.998843 -0.013392 -0.0154663 -0.999791 +0.277395 -0.0479781 0.959557 0.0279354 0.0320986 -0.999094 0.0320057 -0.0325884 0.998956 +0.28678 0.0604332 -0.956088 0.00550244 -0.0108081 0.999926 0.0437677 -0.0285322 -0.998634 +-0.00717502 0.0129329 0.999891 0.0170632 -0.00793701 -0.999823 -0.00084097 0.00422202 0.999991 +-0.00784186 0.00564837 -0.999953 0.00015636 -0.00274126 0.999996 -0.0219435 -0.0200312 -0.999559 +7.2998e-05 -0.003297 0.999995 0.0472984 0.0434019 -0.997937 -0.00105343 0.000945366 0.999999 +0.331118 0.16891 -0.928348 -0.0012084 0.0125911 0.99992 -0.562201 -0.738588 -0.372046 +-0.441173 -0.865098 -0.238688 -0.601917 -0.788203 -0.128187 -0.170095 -0.935637 0.309277 +-0.469596 -0.783809 0.406354 0.117285 -0.806205 0.579894 0.301914 -0.0776415 -0.950168 +0.0264251 0.0145306 0.999545 -0.130759 -0.804071 -0.579976 -0.249936 -0.868924 -0.427203 +-0.28671 -0.93314 -0.216906 -0.377379 -0.913069 -0.154567 -0.0528133 -0.955429 0.290459 +0.0174838 -0.999835 -0.00495469 0.0438005 -0.999033 -0.00371019 0.0486999 -0.998601 -0.0206223 +-0.0222181 -0.998916 0.0408979 -0.350052 -0.848758 0.396326 -0.480604 -0.688254 0.543439 +-0.532776 -0.676027 0.509056 0.041981 -0.0707774 -0.996608 0.0192901 -0.0330063 0.999269 +-0.241253 -0.784582 -0.571164 -0.156889 -0.948195 -0.276245 -0.0591538 -0.99742 -0.0406796 +-0.0481407 -0.998426 -0.0287857 -0.201047 -0.91873 -0.339875 -0.358674 -0.856474 0.371222 +-0.0438021 -0.998351 0.0370948 -0.159397 -0.942505 0.293731 -0.365955 -0.71101 0.600451 +0.0101795 -0.408567 -0.912672 0.368123 -0.60666 0.704592 0.169735 -0.434695 0.884438 +-0.295064 -0.750364 -0.591516 -0.15592 -0.917669 -0.365476 -0.440672 -0.746308 -0.498831 +-0.109753 -0.993709 -0.0222866 -0.395689 -0.91732 -0.0442077 -0.541894 -0.799035 -0.260565 +-0.278961 -0.834636 -0.474936 -0.337896 -0.630394 0.698877 -0.451113 -0.694089 0.561014 +-0.16413 -0.79634 -0.582155 -0.162611 -0.76614 -0.621762 0.210968 -0.894534 0.394083 +0.0421966 -0.998649 -0.0303096 -0.115907 -0.946778 -0.300295 -0.268851 -0.956448 0.113691 +-0.799385 0.51249 0.31359 -0.759679 0.621378 0.191773 -0.691134 0.721661 0.0392365 +-0.653142 0.731999 0.193863 -0.590771 0.736624 0.329204 -0.430893 0.859306 -0.275542 +-0.566673 0.433216 -0.700861 -0.70739 0.588307 0.391784 -0.798384 0.531552 0.282905 +-0.772261 0.616822 0.152131 -0.751017 0.554544 0.358406 -0.752267 0.653503 0.0838304 +-0.727029 0.608423 0.3182 -0.631503 0.650526 -0.421924 -0.575234 0.451724 -0.681946 +-0.672289 0.73987 -0.0248874 -0.747392 0.650935 -0.132995 -0.863189 0.447679 -0.233426 +-0.825921 0.473403 0.306175 -0.779762 0.572163 0.254166 -0.68548 0.727988 0.012263 +-0.684431 0.729077 0.000651934 -0.669905 0.742446 -0.00135181 -0.61314 0.775009 -0.153037 +-0.598677 0.30589 0.740282 -0.636291 0.466424 0.614478 -0.570665 0.780749 0.254503 +-0.694847 0.697812 0.173913 -0.785292 0.543285 0.296915 -0.712423 0.679788 -0.174192 +-0.747484 0.66218 -0.0527803 -0.769671 0.491997 -0.406873 -0.898499 0.118388 -0.422711 +-0.806262 -0.11498 -0.580277 -0.892561 0.0556695 -0.447478 -0.899875 0.372881 0.226239 +-0.954786 0.255935 0.151263 -0.91923 -0.00218128 0.393715 -0.798614 0.574337 0.17987 +-0.794319 0.180863 0.579954 -0.745956 0.66592 -0.0100299 -0.757078 0.653305 0.00491815 +-0.758435 0.651507 -0.0177451 -0.62607 0.756989 -0.187095 -0.47317 0.82765 -0.301839 +-0.710354 0.555212 -0.432593 -0.459684 0.708633 -0.535286 -0.632023 0.269929 0.72642 +-0.731882 0.184451 0.655993 -0.808707 -0.187772 -0.557435 -0.795136 -0.331534 -0.507784 +-0.874449 -0.218647 -0.43305 -0.882114 -0.187174 -0.432251 -0.903052 0.145342 0.404194 +-0.995975 0.0805293 0.0393478 -0.997659 0.0620064 -0.0288332 -0.995598 -0.0441417 0.0826798 +-0.719921 -0.331451 0.609798 -0.836599 -0.245689 0.489632 -0.739255 -0.331814 0.586004 +-0.662348 0.379059 -0.646227 -0.740776 -0.165762 -0.650979 -0.63037 -0.143244 -0.762965 +-0.639169 0.144005 0.755464 -0.699262 0.359832 0.6177 -0.698677 0.398277 0.594328 +-0.932323 -0.0340437 -0.36002 -0.803214 -0.00920706 -0.595619 -0.924417 0.0737152 -0.374192 +-0.851204 -0.288911 0.438157 -0.993679 -0.0561229 0.0972255 -0.998706 0.0115694 -0.0495219 +-0.999939 -0.00473678 -0.00994839 -0.927497 -0.188453 0.322855 -0.762147 -0.251756 0.596449 +-0.731392 -0.275729 -0.62373 -0.768048 -0.33246 -0.547332 -0.585106 -0.408837 -0.700359 +-0.788651 -0.172002 0.590292 -0.774949 -0.0989675 0.624227 -0.940618 0.000968802 -0.339466 +-0.799496 0.01009 -0.600586 -0.992302 0.0436844 -0.115878 -0.860825 0.269676 -0.431573 +-0.842783 -0.362593 0.397798 -0.998457 -0.0406605 0.0378218 -0.998493 0.0351549 -0.0421357 +-0.998524 -0.027101 0.0470704 -0.704673 -0.423276 0.56945 -0.85326 -0.352327 0.384464 +-0.680201 -0.510631 0.525912 -0.620315 -0.329753 -0.711669 -0.658035 -0.345088 -0.669256 +-0.925587 -0.0837875 0.369144 -0.699571 -0.463742 0.54364 -0.857892 -0.354525 0.371932 +-0.676025 -0.532133 0.50973 -0.936174 -0.00709439 -0.351464 -0.795445 -0.00340237 -0.606016 +-0.997665 -0.0102223 -0.0675289 -0.934157 0.0463395 -0.35384 -0.928831 -0.148671 0.339367 +-0.999629 0.0266071 0.00582257 -0.998455 0.0316765 -0.0456619 -0.992753 -0.103167 0.0616263 +-0.672453 -0.514134 0.532422 -0.818802 -0.443 0.365123 -0.694645 -0.524359 0.492459 +-0.701134 -0.550109 -0.453642 -0.720992 -0.436574 -0.538121 -0.669323 -0.442088 -0.597131 +-0.855224 -0.281862 0.434909 -0.987389 -0.0937586 0.127567 -0.67759 -0.516838 0.523211 +-0.873547 -0.361442 0.325999 -0.692978 -0.539978 0.477708 -0.933011 -0.0133592 -0.3596 +-0.794489 -0.0121033 -0.607159 -0.999006 -0.00148947 -0.0445601 -0.946678 0.11246 -0.301915 +-0.839993 -0.295556 0.455036 -0.993222 -0.0956851 0.0659858 -0.997167 -0.0337051 -0.0672431 +-0.953242 -0.265326 -0.144676 -0.903605 -0.426072 0.044282 -0.686823 -0.646886 0.33138 +-0.740278 -0.660453 0.125661 -0.761208 -0.64344 -0.0809163 -0.767734 -0.600349 -0.223976 +-0.817925 -0.371303 -0.439469 -0.870821 -0.355576 0.339465 -0.988189 -0.127426 0.0851221 +-0.738068 -0.599349 0.309898 -0.941017 -0.320586 0.108218 -0.790608 -0.609857 0.054885 +-0.92316 -0.383947 0.018995 -0.780632 -0.609266 0.139312 -0.85226 -0.438094 0.285879 +-0.695795 -0.574291 0.431346 -0.926224 0.0115441 -0.376798 -0.786614 0.0763238 -0.61271 +-0.999627 -0.0216086 -0.0167187 -0.998944 0.0385196 -0.0250628 -0.845625 0.363299 -0.391065 +-0.870348 -0.462856 0.1681 -0.892682 -0.397751 -0.211926 -0.860005 -0.459901 0.221093 +-0.929106 -0.330916 0.165096 -0.996519 -0.0829356 0.00844341 -0.995207 -0.0700085 0.0682795 +-0.694538 -0.46299 0.550688 -0.859325 -0.205925 0.468141 -0.720791 -0.318014 0.615895 +-0.852191 0.202165 -0.482596 -0.739668 0.30723 -0.59875 -0.996496 0.0316106 -0.077431 +-0.998313 -0.0301365 0.0496229 -0.998417 0.047294 -0.0304281 -0.926629 0.334669 -0.171334 +-0.851412 0.473898 -0.224763 -0.858371 -0.361552 0.363975 -0.9944 -0.0182127 0.104101 +-0.936666 0.0370229 0.348262 -0.792656 -0.0645138 0.606246 -0.722602 0.372633 -0.582229 +-0.92745 0.176585 -0.329628 -0.762214 0.254869 -0.595039 -0.999991 -0.00282167 0.00327597 +-0.998912 -0.0294579 0.0361423 -0.999843 0.00569156 0.0168089 -0.992724 0.0857777 -0.0845084 +-0.853371 0.293181 -0.431049 -0.91005 -0.0689785 0.408719 -0.878855 0.220395 0.423132 +-0.812742 0.167046 0.558163 -0.841465 0.242103 -0.483034 -0.733498 0.331809 -0.593197 +-0.995565 0.046056 -0.082028 -0.99938 -0.0141922 0.03223 -0.999329 -0.0162512 0.0328268 +-0.998975 -0.0213696 -0.0399131 -0.921863 0.02599 -0.386644 -0.772645 0.368492 0.516946 +-0.746 0.423637 0.513825 -0.670529 0.500024 0.548057 -0.722423 0.371964 -0.582878 +-0.933723 0.15235 -0.323961 -0.780333 0.159991 -0.604552 -0.999963 0.0085174 -0.000803561 +-0.999473 -0.00204074 0.0323864 -0.999029 -0.00256985 -0.0439849 -0.926316 -0.0125588 -0.376538 +-0.648889 0.189766 0.736839 -0.67675 0.366538 0.638482 -0.668888 0.515362 0.535715 +-0.920694 0.0394399 -0.388287 -0.786932 0.0763588 -0.612297 -0.999644 -0.00773458 -0.025532 +-0.999086 -0.00568578 0.0423625 -0.999009 0.00341195 -0.0443698 -0.926245 0.00266425 -0.376913 +-0.603619 0.0442752 0.796043 -0.642943 0.188495 0.742357 -0.853564 0.19195 -0.484338 +-0.744772 0.296225 -0.597967 -0.996305 0.0328755 -0.0793471 -0.998519 -0.0137032 0.0526446 +-0.999222 -0.00448164 -0.0391864 -0.924958 -0.00354741 -0.380054 -0.446584 0.332978 0.830475 +-0.462652 0.280255 0.841077 -0.7243 0.370473 -0.581497 -0.927794 0.176368 -0.328775 +-0.766845 0.254033 -0.58942 -0.999818 -0.00150703 0.0190388 -0.999707 -0.00977612 -0.0221506 +-0.921931 0.00649453 -0.387299 -0.254284 0.596037 0.761629 0.00990662 0.479073 0.877719 +-0.840296 0.237233 -0.487467 -0.746659 0.316652 -0.585005 -0.997115 0.0333194 -0.0682053 +-0.999677 -0.0129055 0.0218762 -0.937448 0.125196 -0.324834 0.147734 0.620357 0.77028 +0.282919 0.46868 0.836837 0.350599 0.515455 0.781912 -0.729651 0.355488 -0.584156 +-0.929747 0.160539 -0.331357 -0.777243 0.226488 -0.587024 -0.999498 -0.0241559 0.0205064 +-0.999387 0.0256372 -0.0238416 -0.846123 0.358417 -0.394478 0.180132 0.362965 0.914226 +-0.851567 0.231501 -0.470363 -0.742641 0.307921 -0.5947 -0.993466 0.0795699 -0.0818227 +-0.999072 -0.0173883 0.0394139 -0.998539 0.0368781 -0.0394887 -0.850521 0.375556 -0.368201 +0.158349 0.640646 0.751331 0.339985 0.68331 0.64614 0.45922 0.576747 0.675633 +-0.725101 0.436886 -0.532315 -0.856701 0.425686 -0.2913 -0.725285 0.559944 -0.40053 +-0.934866 0.325266 -0.142224 -0.999701 0.0166015 -0.0179408 -0.998991 0.0213895 -0.0394813 +-0.85535 0.361886 -0.370695 0.204981 0.619344 0.757889 0.447767 0.36839 0.814735 +0.534963 0.46017 0.708561 -0.777793 0.592859 -0.208702 -0.731618 0.602968 -0.318063 +-0.851355 0.389718 -0.351162 -0.712787 0.523596 -0.466672 -0.997051 0.068518 -0.0345555 +-0.995834 0.0823458 -0.0391548 -0.847141 0.376485 -0.374982 0.650706 0.332384 0.682717 +0.506837 0.456579 0.731199 -0.69215 0.488657 -0.53117 -0.854007 0.434576 -0.286034 +-0.723702 0.537022 -0.433431 -0.928392 0.371264 -0.0158238 -0.919251 0.362163 -0.154325 +-0.820582 0.408946 -0.399261 0.481452 0.516982 0.707767 0.394342 0.562768 0.726489 +0.561609 0.532379 0.633378 -0.805493 0.578446 -0.128765 -0.80557 0.591148 -0.040024 +-0.74822 0.597934 -0.287474 -0.814787 0.427515 -0.391603 -0.729509 0.516299 -0.44861 +-0.825702 0.401776 -0.395971 0.408543 0.746881 0.524653 0.645298 0.578839 0.498534 +0.652739 0.494135 0.57425 -0.741336 0.385104 -0.549651 -0.906063 0.204914 -0.370216 +-0.81872 0.152062 -0.553691 -0.862509 0.287512 -0.416432 0.657576 0.443422 0.609074 +0.709879 0.244341 0.660583 0.832075 0.287791 0.474159 0.76884 0.266351 0.581328 +-0.885368 0.180016 -0.428622 -0.809522 0.228975 -0.540596 -0.905559 0.0441653 -0.421916 +0.794798 0.194398 0.574896 0.726115 0.0333982 0.686762 -0.688284 0.47297 -0.550058 +-0.740634 0.327125 -0.5869 -0.553075 0.653006 -0.517389 0.708425 0.0312718 0.705093 +0.704671 -0.00635401 0.709506 -0.417585 0.710901 -0.565901 -0.219234 0.75402 -0.619185 +-0.400237 0.647126 -0.648875 0.75758 -0.137617 0.638071 0.735904 -0.0707699 0.673377 +-0.282746 0.63951 -0.7149 0.0590413 0.534722 -0.842963 0.837859 -0.240521 0.490044 +0.723993 -0.293104 0.624439 0.804152 -0.395936 0.443367 0.591324 0.151609 -0.792055 +0.400835 0.500494 -0.767357 0.395213 0.428205 -0.812679 0.753985 -0.42843 0.497949 +0.743944 -0.545032 0.386636 0.840531 -0.446079 0.307442 0.788747 -0.584537 0.190248 +0.682467 -0.676051 0.277838 0.621228 -0.184246 -0.761662 0.600699 0.114723 -0.791201 +0.765908 -0.631672 0.119897 0.711935 -0.696724 0.0878911 0.657568 -0.657459 0.367901 +0.564243 -0.733015 0.379892 0.619868 -0.706926 0.340615 0.638251 -0.764112 0.0936462 +0.631439 -0.775084 -0.0230177 0.608888 -0.793075 -0.0169572 0.590641 -0.806919 -0.00499523 +0.595269 -0.748808 0.291448 0.497513 -0.711036 -0.496898 0.578813 -0.610836 -0.540236 +0.6668 -0.411389 -0.621399 0.410721 -0.898759 0.153429 0.371823 -0.928078 0.0204735 +0.372874 -0.927529 -0.0256015 0.386181 -0.922372 -0.00965464 0.494348 -0.863044 0.1038 +0.572833 -0.663243 0.481633 0.521763 -0.672579 0.524786 0.542393 -0.629098 0.556817 +0.742046 -0.670225 0.0128816 0.615362 -0.725603 -0.307945 0.629125 -0.731057 -0.264117 +0.641854 -0.689573 -0.33543 0.591785 -0.758829 -0.271973 0.590455 -0.699935 -0.401813 +0.288568 -0.91468 0.282999 0.0545853 -0.997622 0.0420782 0.0590247 -0.998146 -0.0148706 +0.0414967 -0.998487 0.0360952 0.160947 -0.94747 0.276399 0.106887 -0.817736 0.565583 +0.472578 -0.814929 0.335501 0.569608 -0.819622 0.0613748 0.557491 -0.80667 -0.196183 +0.526826 -0.75058 -0.398852 0.50382 -0.843187 -0.187622 0.623347 -0.759317 0.18675 +0.587667 -0.693834 0.416224 0.647214 -0.744069 0.165756 0.642829 -0.661522 -0.386211 +0.409031 -0.895659 -0.174611 0.325209 -0.945544 -0.0136471 0.416324 -0.90842 -0.0380481 +0.632062 -0.752747 -0.184034 0.715972 -0.628211 -0.304523 0.338107 -0.905677 0.255799 +0.363625 -0.929912 -0.0551407 0.437525 -0.851034 -0.290368 0.723619 -0.670444 0.163951 +0.148474 0.960324 0.23608 0.245632 0.814021 0.526341 0.00269068 0.997836 0.0656945 +0.011701 0.999917 -0.00540934 -0.017389 0.999229 -0.0351977 -0.17062 0.86755 -0.467167 +-0.148525 0.656904 -0.7392 0.245614 0.921728 0.300151 0.366529 0.777095 0.511645 +0.508614 0.82954 0.230598 0.528572 0.809817 0.254576 0.273348 0.950216 0.149571 +0.142138 0.985247 0.0953204 0.316581 0.932836 0.172027 0.40859 0.912337 0.0263503 +0.417231 0.908799 0.00138219 0.188094 0.941823 -0.278551 0.0453998 0.639178 -0.767718 +-0.217692 0.746125 -0.629212 0.444936 0.808127 0.385957 0.475058 0.717912 0.508845 +0.47715 0.76018 0.44097 0.431282 0.870266 0.237979 0.379234 0.87573 0.298796 +0.306103 0.563043 -0.767648 0.137628 0.761039 -0.633938 0.284871 0.546368 -0.787611 +0.204418 0.707666 0.67633 0.0615802 0.405702 0.911929 -0.173083 0.907279 -0.383258 +0.228386 0.939736 -0.254431 0.559375 0.796234 -0.230461 0.57762 0.816117 0.0175423 +0.428823 0.903313 -0.0116547 0.573785 0.81895 0.00956345 0.418439 0.908146 0.0133815 +0.574144 0.818754 -0.000568704 0.416811 0.908993 -0.000319414 0.578632 0.815572 0.00513689 +0.416029 0.909341 0.00422331 0.580672 0.81413 -0.00353512 0.401885 0.915665 -0.00674619 +0.580706 0.813892 -0.0189987 0.372303 0.92789 -0.0202312 0.592281 0.801664 0.0808627 +0.375871 0.925338 0.0497138 0.608282 0.739986 0.287078 0.411314 0.895046 0.172374 +0.448328 0.893545 0.0240638 0.544943 0.828529 -0.128751 0.390906 0.899166 -0.196703 +0.510571 0.78023 0.361328 0.421401 0.888717 0.180564 0.387971 0.835966 0.388123 +0.447335 0.0295243 -0.893879 -0.00815905 0.0295618 0.99953 0.435359 -0.543012 -0.718053 +0.383049 -0.421089 -0.822167 -0.0175242 -0.0233713 0.999573 0.292355 -0.304335 -0.906592 +0.531724 -0.517284 -0.670586 -0.131481 -0.329254 0.935042 0.0497585 -0.0333784 -0.998203 +-0.400284 -0.561034 0.724579 -0.320018 -0.59322 0.738701 -0.204867 -0.452422 0.867954 +0.0140195 -0.0100036 -0.999852 -0.0291801 -0.10749 0.993778 -0.0102885 0.0113937 -0.999882 +-0.0276092 -0.342843 0.938987 -0.00485727 0.00498409 -0.999976 0.00244398 -0.593115 0.805114 +-0.0322112 -0.390169 0.92018 -0.00108952 -0.00282552 -0.999995 -0.0145621 -0.0496288 0.998662 +0.00428884 0.0105991 -0.999935 0.0161887 0.0257926 0.999536 -0.00783118 -0.00188179 -0.999968 +0.00317113 -0.0130525 0.99991 -0.156907 -0.319105 -0.93464 -0.0110445 0.00656301 0.999918 +-0.271673 -0.494248 -0.82578 -0.168565 -0.608748 -0.775249 -0.385382 -0.580765 -0.717073 +-0.00507521 0.0068396 0.999964 0.00411518 -0.00704124 -0.999967 -0.000825539 0.0016219 0.999998 +0.281563 0.499348 -0.819374 7.56068e-05 -0.00142031 0.999999 0.259333 0.602101 -0.755129 +0.174329 0.32535 -0.929385 0.000421899 0.00027918 1 -0.000924362 -0.00446388 -0.99999 +0.0010268 0.0013346 0.999999 -0.00706863 -0.0148545 -0.999865 -0.00487391 -0.0103893 0.999934 +0.00374183 0.00669318 -0.999971 0.00222539 0.0142481 0.999896 -0.00780702 -0.012327 -0.999894 +0.196347 0.308723 0.930665 -0.00558669 0.00942771 -0.99994 0.144179 0.553212 0.820469 +0.295038 -0.166945 0.940788 0.55579 0.172116 0.81331 0.178194 0.334424 -0.925423 +0.404863 -0.506025 0.761593 0.142255 -0.297369 0.944106 0.551695 -0.163157 -0.817932 +0.443516 0.496136 -0.74642 0.292533 0.137258 -0.946353 -0.00333011 -0.048602 0.998813 +0.189025 -0.280273 -0.941125 0.0687965 -0.553199 -0.830204 -0.000901177 0.0209027 0.999781 +-0.00799094 -0.0427919 -0.999052 0.000583518 0.00509734 0.999987 -0.0181573 -0.00767298 -0.999806 +-0.000412248 -0.00288406 0.999996 0.0601 0.0588943 -0.996453 -0.000251632 -0.00330345 0.999994 +0.380937 0.378243 -0.843694 0.00136011 0.0014496 0.999998 0.611733 0.0891883 -0.78602 +0.560336 0.457034 -0.690756 0.448563 0.280751 -0.84851 0.0058027 0.0178767 0.999823 +-0.516667 -0.764149 -0.386175 -0.34364 -0.913325 -0.218516 -0.543888 -0.813821 -0.204646 +-0.276228 -0.951226 -0.137357 -0.57129 -0.757969 -0.314819 0.0952462 -0.938644 0.331475 +0.152825 -0.753729 0.639169 0.346146 -0.769662 0.536473 0.58992 -0.274953 -0.759207 +0.406072 -0.141151 -0.902874 -0.036606 -0.00866118 0.999292 -0.519885 -0.744811 -0.418302 +-0.458146 -0.792599 -0.402355 -0.4019 -0.728676 -0.554533 -0.410551 -0.807662 -0.423238 +0.201768 -0.898154 0.390652 -0.0250823 -0.998318 0.0522641 -0.0290151 -0.999545 -0.008195 +-0.0421191 -0.999092 -0.00643434 -0.031761 -0.999471 -0.00699953 0.0127074 -0.999611 -0.0248204 +-0.0352045 -0.996703 0.0731019 -0.295739 -0.841141 0.452791 -0.434832 -0.70017 0.566289 +-0.21902 -0.774375 0.593611 0.31154 -0.316932 -0.895822 0.512807 -0.428126 -0.744135 +-0.284393 -0.0577159 0.956969 -0.292777 -0.787437 -0.542425 -0.178098 -0.9004 -0.396939 +-0.322598 -0.766616 -0.555185 -0.0929437 -0.985983 -0.138559 -0.250809 -0.954101 -0.163663 +-0.366439 -0.9196 -0.141624 -0.168843 -0.938173 0.302197 -0.00934886 -0.996581 0.0820934 +-0.212889 -0.862143 0.459769 -0.409667 -0.643144 0.646946 -0.165094 -0.760808 0.627626 +0.0426732 -0.506941 -0.860924 -0.0634165 -0.435418 0.897992 -0.0569981 -0.803765 -0.592211 +0.0173419 -0.940446 -0.339502 -0.00608093 -0.995493 -0.094638 -0.160368 -0.956183 -0.244939 +-0.352772 -0.91608 0.190654 -0.547192 -0.817273 -0.180682 -0.0181465 -0.855921 -0.516788 +0.0632055 -0.920934 0.38456 0.0312355 -0.998868 -0.0358791 -0.000114043 -0.913844 -0.406065 +-0.839398 0.542918 0.0255128 -0.909833 0.367656 0.19244 -0.93991 0.337518 0.0514884 +-0.956096 0.274359 0.102995 -0.913768 0.213586 -0.345557 -0.907349 0.283685 0.310227 +-0.91385 0.403593 0.0446235 -0.949378 0.294829 0.108427 -0.998195 0.0514662 0.0309606 +-0.998673 0.0508209 0.00831153 -0.999656 0.020032 0.0169363 -0.999348 0.0334015 -0.0137225 +-0.938922 0.28678 -0.190216 -0.920303 0.387541 -0.0534211 -0.929159 0.141679 -0.341454 +-0.889093 -0.0486641 0.455133 -0.953184 0.294275 0.0695877 -0.933369 0.358326 -0.0206049 +-0.944746 0.327401 0.0162603 -0.929163 0.369653 -0.00350908 -0.859382 0.477034 -0.18412 +-0.953048 0.134877 0.271124 -0.999188 0.0293519 0.027601 -0.999294 0.0345928 -0.0146852 +-0.999993 0.000417664 0.00372953 -0.999797 -0.0176388 -0.0097127 -0.999777 -0.0206758 0.00416336 +-0.999203 -0.0276644 0.0287843 -0.998937 0.00312578 -0.0459974 -0.955139 -0.103389 -0.277524 +-0.93424 -0.135984 0.329704 -0.99864 0.051545 0.00780646 -0.998481 0.0550075 -0.00296885 +-0.999854 0.0113107 0.0127846 -0.996029 0.0367592 -0.0810901 -0.909265 0.0586959 -0.412057 +-0.912452 0.151017 0.380295 -0.998676 0.0439599 0.0267211 -0.99997 0.00209266 -0.00741928 +-0.999705 -0.0226254 -0.00883518 -0.999868 -0.0151977 -0.00581896 -0.999952 -0.00824235 -0.00527568 +-0.999991 -0.00348461 0.00241881 -0.999727 -0.00379218 0.0230446 -0.999023 -0.00658683 -0.0437061 +-0.924291 -0.0442079 -0.37912 -0.831622 -0.303683 0.464954 -0.993291 -0.064086 0.0962631 +-0.999663 0.016801 -0.0198124 -0.998558 -0.016021 -0.0512292 -0.951773 -0.154226 -0.265222 +-0.855412 -0.289655 0.429384 -0.994573 -0.0449743 0.0938137 -0.999044 0.0252695 -0.0356795 +-0.99974 0.0161428 -0.0160933 -0.999969 0.00685804 -0.00382643 -0.999995 0.00201612 -0.00244612 +-0.999982 0.0010373 0.00593092 -0.99974 0.00342289 0.0225574 -0.998998 0.00475484 -0.0445076 +-0.9242 0.00946071 -0.381791 -0.849667 -0.371161 0.374574 -0.994263 -0.100836 0.0356735 +-0.996278 -0.0425676 -0.0749526 -0.912817 -0.176229 -0.368387 -0.843453 -0.388385 0.371138 +-0.994317 -0.097662 0.042374 -0.999475 -0.026565 -0.0185357 -0.99998 -0.0039389 0.00486495 +-0.99969 0.0212965 -0.0128991 -0.999977 0.00673953 0.000371683 -0.99971 -0.000754386 0.0240833 +-0.999092 0.000450165 -0.0426083 -0.925627 0.00456267 -0.37841 -0.880872 -0.43708 0.181729 +-0.919283 -0.333816 -0.20853 -0.8595 -0.47105 0.198422 -0.923456 -0.382921 0.0244844 +-0.932076 -0.323781 0.162481 -0.997874 -0.0478976 0.0441956 -0.999674 0.0197742 -0.0161654 +-0.999868 0.00212509 0.0160788 -0.99911 -0.00606182 -0.0417368 -0.925331 -0.00716425 -0.379093 +-0.844594 -0.358934 0.397274 -0.997861 -0.0419767 0.0501125 -0.999957 0.0091275 -0.00138108 +-0.999283 -5.80292e-05 -0.0378591 -0.923824 0.00880043 -0.382715 -0.936861 -0.129355 0.324897 +-0.999794 -0.00673097 0.0191469 -0.999806 0.0124677 -0.0152167 -0.937622 0.124926 -0.324434 +-0.925808 0.00396344 0.377974 -0.998879 -0.00823703 0.0466233 -0.999943 -0.00621287 0.00865782 +-0.995977 0.0438596 -0.0781456 -0.837326 0.26962 -0.475595 -0.944793 0.182597 0.272074 +-0.998719 -0.0223083 0.0454139 -0.99974 -0.0215171 0.00750605 -1 -0.000409177 -0.000114156 +-0.931671 0.163404 -0.324482 -0.824072 0.398894 0.40223 -0.915828 0.395674 0.0685721 +-0.995879 0.0846449 -0.0325738 -0.999986 -0.00306401 -0.00429015 -0.998492 -0.0333275 0.0436192 +-0.996683 0.0289633 -0.0760553 -0.842675 0.251632 -0.476005 -0.805025 0.304381 0.509202 +-0.799246 0.430557 0.419316 -0.805181 0.576525 -0.138935 -0.764008 0.555606 0.328014 +-0.847214 0.46235 -0.261652 -0.785797 0.539501 -0.302426 -0.936947 0.307452 -0.166142 +-0.998972 0.0404698 -0.0204395 -0.999463 -0.0310425 -0.0104755 -0.922265 0.0441477 -0.38403 +-0.707832 0.404154 0.579339 -0.729415 0.517972 0.446831 -0.649104 0.532241 0.543492 +-0.771369 0.581391 -0.258793 -0.734446 0.59435 -0.327623 -0.855141 0.395281 -0.335391 +-0.707952 0.541153 -0.453825 -0.995382 0.0702685 -0.0654007 -0.945947 0.103543 -0.307349 +-0.5528 0.525486 0.646743 -0.443685 0.520671 0.729415 -0.685157 0.518946 -0.511131 +-0.859369 0.358944 -0.364204 -0.706629 0.4951 -0.505522 -0.98957 0.083255 -0.117553 +-0.847591 0.258575 -0.463389 -0.335695 0.683475 0.648205 -0.157972 0.745003 0.648086 +-0.732936 0.399623 -0.550551 -0.938016 0.159329 -0.307799 -0.790744 0.18015 -0.585039 +-0.935404 0.150863 -0.319781 -0.184921 0.758398 0.625009 0.0216672 0.407502 0.912947 +-0.891902 0.196739 -0.407191 -0.753694 0.263541 -0.602073 -0.987769 0.0657076 -0.141404 +-0.860133 0.234177 -0.453136 -0.000736048 0.367301 0.930102 -0.663889 0.485805 -0.568546 +-0.874085 0.333835 -0.352887 -0.643604 0.551887 -0.530278 -0.921971 0.178048 -0.343902 +-0.0262411 0.619003 0.78495 0.0125179 0.732064 0.681121 -0.625961 0.560668 -0.542055 +-0.842246 0.373892 -0.388364 -0.652935 0.529393 -0.541681 -0.825628 0.384666 -0.41276 +0.0216623 0.713864 0.699949 0.219789 0.567278 0.793655 -0.643135 0.532363 -0.550424 +-0.808924 0.497321 -0.313549 -0.579504 0.676068 -0.455091 -0.796969 0.545714 -0.258914 +-0.838898 0.485341 -0.246364 0.18728 0.603229 0.775268 0.28577 0.471838 0.834089 +0.403826 0.532022 0.744229 -0.524486 0.812101 -0.255746 -0.586027 0.702872 -0.403167 +-0.821876 0.4349 -0.367942 -0.646467 0.571953 -0.504926 -0.82567 0.400153 -0.397678 +0.268019 0.595342 0.757452 -0.650221 0.525511 -0.548681 -0.842756 0.380439 -0.380827 +-0.661691 0.526376 -0.533941 -0.906006 0.37187 -0.202152 -0.927608 0.373184 -0.0166538 +-0.882187 0.436063 -0.177751 0.47794 0.785011 0.394121 0.577464 0.624079 0.526366 +0.552446 0.597632 0.581068 0.401356 0.831171 0.384797 -0.659034 0.524708 -0.538847 +-0.850937 0.434628 -0.294966 -0.737926 0.515369 -0.43573 -0.9416 0.313389 -0.123196 +-0.986474 0.126376 -0.104397 -0.868862 0.261157 -0.420566 0.683327 0.457367 0.569104 +0.72066 0.153935 0.675983 0.670405 0.378962 0.637922 -0.784557 0.580782 -0.217169 +-0.759936 0.576705 -0.299848 -0.846504 0.480552 -0.22913 -0.580523 0.758596 -0.295846 +-0.836253 0.34528 -0.425984 0.706927 0.0658459 0.704215 0.697811 -0.015876 0.716106 +-0.433399 0.871726 -0.228603 -0.452722 0.724942 -0.519136 -0.230425 0.803037 -0.549578 +-0.424408 0.708319 -0.564059 0.691988 -0.0154718 0.721743 0.699072 0.00216182 0.715048 +-0.198271 0.880891 -0.42979 -0.0702477 0.849045 -0.523629 0.0793643 0.803515 -0.58997 +0.69058 -0.00421806 0.723244 0.718208 0.0202286 0.695534 0.0639251 0.772604 -0.631662 +0.259144 0.750288 -0.608205 0.763769 -0.124552 0.633359 0.757682 -0.0308988 0.651892 +0.309608 0.662753 -0.681837 0.446632 0.539995 -0.71339 0.854748 -0.232452 0.464083 +0.745088 -0.255548 0.616067 0.808832 -0.381783 0.447249 0.767177 -0.055714 -0.639011 +0.572544 0.40203 -0.714538 0.453788 0.302483 -0.838201 0.657495 -0.496435 0.566791 +0.584445 -0.667938 0.46074 0.805567 -0.494208 0.326835 0.761625 -0.623215 0.177567 +0.705246 -0.708814 0.0145459 0.674159 -0.716163 0.180611 0.814094 -0.446557 -0.371265 +0.837111 -0.316463 -0.446202 0.64892 0.0785385 -0.756793 0.550276 -0.807643 0.211917 +0.663258 -0.737552 0.126906 0.729949 -0.682416 0.0385015 0.597572 -0.790299 0.13541 +0.489046 -0.871531 0.0355955 0.415366 -0.909099 0.0317847 0.422394 -0.905685 -0.0362974 +0.335519 -0.929235 -0.154756 0.271634 -0.922245 0.275098 0.120412 -0.820582 0.5587 +0.444788 -0.713818 0.540951 0.792045 -0.550172 -0.264529 0.803344 -0.530263 -0.271035 +0.837555 -0.48727 -0.247122 0.777241 -0.346172 -0.525416 0.775195 -0.499303 -0.387001 +0.209196 -0.902108 0.377411 0.0577737 -0.996893 0.0535403 0.0237224 -0.99966 0.0107751 +0.00378298 -0.999767 -0.0212621 -0.0142441 -0.999899 -8.88138e-06 -0.126257 -0.940199 0.316361 +-0.327957 -0.719412 0.612283 0.574932 -0.772859 -0.268592 0.637915 -0.745629 0.192621 +0.623808 -0.705094 -0.337203 0.631252 -0.774171 -0.0467014 0.622916 -0.781431 0.036613 +0.670044 -0.742313 -0.00345765 0.75789 -0.635543 -0.147266 0.787799 -0.572256 -0.227806 +0.74037 -0.621516 -0.256067 0.695408 -0.628692 -0.348073 0.740073 -0.611988 -0.278858 +-0.171079 -0.870099 0.462233 -0.0147043 -0.996215 0.0856672 0.112727 -0.990889 -0.0736977 +0.428236 -0.901128 -0.0676832 0.616395 -0.65703 0.434014 0.525802 -0.715872 0.459412 +0.715174 -0.68218 0.152175 0.659313 -0.664659 -0.351475 0.729361 -0.613334 -0.303074 +0.756735 -0.653708 -0.00430784 0.752485 -0.657211 0.0428966 0.743955 -0.667379 -0.0337086 +0.627192 -0.749014 -0.21356 0.566951 -0.778066 -0.270518 0.424504 -0.90384 -0.0535783 +0.371426 0.897788 0.236682 0.500812 0.765113 0.404709 0.550754 0.820261 0.154409 +0.569906 0.821645 0.010307 0.355577 0.933773 0.0404049 0.569549 0.821374 -0.0309742 +0.359438 0.932997 -0.0178951 0.484476 0.726453 -0.48739 0.405373 0.856571 -0.319311 +0.212872 0.560677 -0.800204 0.356327 0.91848 0.171539 0.553432 0.772793 0.310652 +0.541579 0.8116 0.219084 0.443535 0.831213 0.335205 0.267028 0.953741 0.138111 +0.0972902 0.994258 0.0445603 0.0563452 0.996404 0.0632859 0.274604 0.95293 0.128516 +0.264816 0.889583 -0.372177 0.135537 0.747961 -0.649757 -0.31348 0.728567 -0.609032 +0.208805 0.938797 0.273973 0.224806 0.783259 0.579627 0.107363 0.988699 0.104628 +0.288811 0.915033 0.281609 0.302096 0.563658 -0.768784 0.193672 0.981007 -0.0107623 +0.503508 0.775174 0.381556 0.630706 0.759652 -0.15855 0.457502 0.803304 -0.381307 +0.522816 0.729112 0.441654 0.537718 0.68969 0.484961 0.530972 0.668347 0.520942 +0.517901 0.803412 0.29378 -0.0182429 0.881866 -0.471147 0.218379 0.872572 -0.436954 +0.449815 0.620748 -0.642135 0.305618 0.597337 0.741476 0.118852 0.296877 0.947491 +0.213878 0.835675 -0.505869 0.336366 0.940447 -0.0491707 0.402435 0.91521 0.0208952 +0.413818 0.910275 0.0123926 0.403301 0.915062 -0.003058 0.400055 0.916491 0.000735535 +0.401829 0.915462 -0.0215354 0.331297 0.93643 -0.115507 0.15255 0.988015 0.0235443 +0.322438 0.899774 0.294007 0.513948 0.807686 -0.288965 0.430724 0.891985 0.137259 +0.469499 -0.29529 -0.83209 0.577974 0.340198 -0.741763 0.113008 -0.205637 -0.972082 +0.00756943 0.0117976 0.999902 0.108414 -0.396215 -0.911735 -0.166198 -0.573517 -0.802157 +0.000406344 -0.0456372 0.998958 0.530775 -0.4876 -0.693199 0.306851 -0.306547 -0.901039 +0.00217341 -0.384108 0.923286 0.321984 -0.311822 -0.89392 0.513299 -0.487755 -0.70613 +-0.0645515 -0.603907 0.794437 0.0091937 -0.33378 0.942606 0.0344396 -0.0396281 -0.998621 +-0.0215583 -0.109331 0.993772 0.00909276 -0.00388887 -0.999951 -0.245751 -0.444105 0.861613 +-0.00557974 0.0100698 -0.999934 -0.172008 -0.595222 0.784935 -0.17218 -0.342035 0.923778 +-0.000319693 0.00231297 -0.999997 0.0303065 0.0250656 0.999226 0.00830712 0.0193587 -0.999778 +0.281853 -0.0418975 0.958543 0.00861306 -0.0278554 -0.999575 0.0335563 -0.0340309 0.998857 +-0.0398803 -0.384878 -0.922106 0.0107778 -0.0110883 0.99988 -0.038901 -0.37796 -0.925004 +-0.0771756 -0.620757 -0.780196 -0.00262977 0.0126216 0.999917 0.134471 0.297432 -0.945226 +0.000249265 0.00357636 0.999994 0.377987 0.587839 -0.715242 0.310385 0.612449 -0.727027 +0.199862 0.481947 -0.853102 -8.698e-05 -0.00142457 0.999999 0.0356818 0.0780741 -0.996309 +0.00080827 0.0015395 0.999998 -0.00942928 -0.0325163 -0.999427 0.000404631 -0.00127168 0.999999 +0.00112805 -0.00246875 -0.999996 -0.00949799 -0.0235094 0.999678 -0.000762519 -0.000130969 -1 +-0.0136759 0.0344952 0.999311 -0.0151232 -0.0291532 -0.99946 0.0335678 0.390889 0.919826 +0.0367857 0.085059 -0.995697 0.0875579 0.586703 0.805055 0.0157289 0.00838439 0.999841 +0.260001 0.493138 -0.830189 0.0617681 -0.607738 0.791732 -0.0145676 -0.397086 0.917666 +0.205146 0.579779 -0.788525 -0.0684851 -0.17414 -0.982337 -0.00251503 -0.0444094 0.99901 +-0.120675 -0.3385 -0.933196 -0.305617 -0.563655 -0.767392 0.00597402 0.0222005 0.999736 +-0.0298862 -0.0270547 -0.999187 0.000426995 0.00187874 0.999998 0.0416593 0.0580394 -0.997445 +-0.000853826 -0.00263707 0.999996 0.34577 0.394362 -0.851423 0.0003593 -0.00183764 0.999998 +0.513351 0.534105 -0.671717 0.525647 0.529059 -0.666177 0.358713 0.38298 -0.851265 +0.00347914 0.00517924 0.999981 0.103245 0.0468615 -0.993551 -0.0091002 -0.0117592 0.999889 +-0.484879 -0.715266 -0.503276 -0.282601 -0.917776 -0.278969 -0.473599 -0.740917 -0.476178 +-0.11301 -0.990366 -0.0800204 -0.0999789 -0.993847 -0.0476713 -0.359272 -0.889128 0.283505 +-0.389883 -0.721944 0.571654 0.0551226 -0.0678873 -0.996169 0.0362136 -0.268639 0.96256 +-0.335806 -0.839116 -0.427923 -0.299985 -0.879894 -0.368505 -0.520214 -0.822666 -0.229344 +-0.411459 -0.881019 -0.233467 -0.608077 -0.738534 -0.291221 -0.413618 -0.82887 -0.37669 +-0.214328 -0.884941 0.413453 -0.334727 -0.939529 0.0724126 -0.380281 -0.924559 -0.0239994 +-0.373004 -0.927765 -0.0109698 -0.38359 -0.923467 0.00824793 -0.323415 -0.938727 0.119139 +-0.0367796 -0.998046 0.0505077 -0.131364 -0.940495 0.313389 -0.325847 -0.728674 0.602377 +0.411159 -0.538667 -0.735382 0.213971 -0.354163 -0.910376 -0.503183 0.290491 0.813893 +-0.305999 -0.519145 0.798031 0.0235856 0.017257 0.999573 -0.225569 -0.822863 -0.521551 +-0.158754 -0.935471 -0.315739 -0.154468 -0.941236 -0.300356 -0.217493 -0.867459 0.44745 +-0.0227659 -0.993271 0.11355 0.128628 -0.910175 0.393746 0.270723 -0.787017 0.554359 +0.149981 -0.630137 -0.761862 0.133133 -0.613256 -0.778583 0.27309 0.536051 0.798794 +-0.217549 0.0644431 0.97392 0.0356986 -0.811077 -0.583848 -0.00469506 -0.93142 -0.363916 +-0.00801514 -0.991893 -0.126826 -0.181165 -0.914965 -0.360582 0.0925278 -0.963454 0.251387 +-0.0310492 -0.932692 -0.359336 -0.235945 -0.798453 0.553898 -0.0902177 -0.99582 0.0142397 +-0.0214707 -0.92253 -0.385328 -0.852825 0.515063 0.0860224 -0.7559 0.540931 0.368793 +-0.840082 0.538854 0.0624371 -0.808557 0.545333 -0.221014 -0.814025 0.510535 -0.276978 +-0.479541 0.592429 0.647355 -0.60756 0.609526 0.509262 -0.779951 0.568196 0.262355 +-0.827222 0.560654 -0.03702 -0.751916 0.580512 -0.312454 -0.813117 0.439046 -0.382204 +-0.734346 0.500544 -0.458467 -0.822809 0.407743 -0.395892 -0.356255 0.757308 0.547327 +-0.362656 0.708229 0.605716 -0.41756 0.755088 0.505455 -0.72264 0.466106 -0.510428 +-0.83218 0.378666 -0.405079 -0.70618 0.516466 -0.484327 -0.817205 0.314128 -0.483218 +-0.32241 0.784851 0.529207 -0.143641 0.469103 0.871384 -0.298058 0.821634 0.485879 +-0.643035 0.554569 -0.528166 -0.730987 0.37207 -0.572033 -0.614768 0.549779 -0.565512 +-0.036623 0.417626 0.907881 -0.577317 0.568148 -0.58644 -0.558611 0.592284 -0.580649 +-0.54374 0.601875 -0.584887 0.0544965 0.634902 0.770668 -0.0062389 0.744641 0.667436 +-0.564297 0.588229 -0.579272 -0.574086 0.573728 -0.584176 -0.560236 0.580791 -0.590607 +0.184948 0.807054 0.560766 0.170821 0.69509 0.698334 -0.580333 0.55764 -0.593507 +-0.512077 0.676997 -0.528633 -0.534651 0.631513 -0.561551 0.256161 0.620703 0.741019 +0.376074 0.569498 0.730918 -0.357695 0.860219 -0.363424 -0.376539 0.843121 -0.383883 +-0.503555 0.676045 -0.537954 -0.53027 0.649028 -0.545505 0.516107 0.7997 0.306779 +0.607713 0.676924 0.415282 0.588188 0.747913 0.30767 0.524244 0.706035 0.476112 +0.552647 0.741014 0.381417 -0.567846 0.574658 -0.589338 -0.559725 0.5891 -0.582811 +-0.554707 0.588185 -0.588505 0.56893 0.736391 0.366124 0.756556 0.233766 0.610718 +0.77731 0.526042 0.345063 0.694288 0.592029 0.409225 0.542674 0.760194 0.357226 +-0.573231 0.543311 -0.613367 -0.547527 0.335226 -0.766705 -0.727496 0.38214 -0.569841 +0.800314 0.213386 0.560325 0.720574 -0.0106091 0.693297 -0.760596 0.379237 -0.526947 +-0.405886 0.557899 -0.723882 -0.763739 0.606367 -0.221409 -0.61166 0.767419 -0.192197 +0.708187 0.00473276 0.706009 0.699686 -0.0185094 0.714211 -0.43324 0.866906 -0.246529 +-0.355566 0.934438 -0.0199326 -0.247057 0.962089 -0.115532 -0.119438 0.983695 -0.134457 +-0.209582 0.887871 -0.409586 -0.278597 0.798389 -0.533815 0.703355 -0.0116732 0.710743 +0.714687 0.0196067 0.69917 -0.132733 0.953629 -0.270136 0.115957 0.925564 -0.360395 +0.309505 0.774837 -0.551212 0.711499 0.02459 0.702257 0.710595 -0.00836794 0.703552 +0.250929 0.811436 -0.527832 0.358862 0.742622 -0.565447 0.283013 0.803759 -0.523331 +0.304391 0.735543 -0.605246 0.688051 -0.00392395 0.725652 0.554888 -0.1995 0.80765 +0.429123 0.549493 -0.716876 0.352662 0.642312 -0.680489 0.253931 0.486223 -0.836126 +0.640771 -0.231479 0.732005 0.438832 -0.319071 0.840012 0.350867 0.414142 -0.839868 +0.595742 -0.518096 0.613732 0.575263 -0.625214 0.527428 0.540636 -0.627511 0.560306 +0.860642 0.170287 -0.479894 0.559095 0.530336 -0.637304 0.66114 0.58476 -0.470053 +0.547683 -0.715902 0.433044 0.544591 -0.785721 0.293366 0.533911 -0.779082 0.328589 +0.61543 -0.732721 0.290457 0.658493 -0.749054 0.0728372 0.642113 -0.766606 -0.00252833 +0.607047 -0.794587 -0.0112424 0.58458 -0.811133 0.0181301 0.580984 -0.813898 0.00530593 +0.566545 -0.793826 -0.221059 0.379093 -0.766237 -0.518815 0.146405 -0.988883 0.0259801 +0.0491031 -0.803968 0.592642 0.893617 -0.368481 -0.256262 0.979289 -0.0702775 -0.189879 +0.763396 0.431699 -0.480482 0.905306 -0.0654024 -0.419695 0.804923 0.180664 -0.565208 +0.38612 -0.909276 0.155331 0.358864 -0.933312 0.0120941 0.367386 -0.929956 -0.0144904 +0.375176 -0.926951 0.00220078 0.385128 -0.922436 0.0280864 0.340189 -0.93588 -0.0916517 +0.265485 -0.857466 -0.44076 0.0430948 -0.922588 0.383373 0.00842898 -0.999055 0.0426383 +-0.0471514 -0.99862 -0.0231425 -0.0482563 -0.998817 -0.0060837 -0.00175967 -0.999813 -0.019234 +-0.0355775 -0.99897 0.0281559 -0.36812 -0.846372 0.384891 -0.503662 -0.67562 0.538389 +-0.555842 -0.665142 0.498624 0.645288 -0.698348 -0.3097 0.73869 -0.651294 -0.173647 +0.821408 -0.569098 -0.03764 0.874858 -0.450218 -0.178683 0.783667 -0.311161 -0.537629 +0.76134 -0.519584 -0.387807 -0.402113 -0.834484 0.376751 -0.0910196 -0.992741 0.0786168 +0.190123 -0.945848 0.263105 0.17002 -0.793643 0.584143 0.418472 -0.708679 0.568027 +0.588772 -0.743359 -0.317434 0.661255 -0.745253 -0.0856704 0.665977 -0.745423 0.0286041 +0.653604 -0.756799 0.00751729 0.613528 -0.769495 -0.17737 0.547305 -0.747818 -0.375799 +0.515217 -0.823277 -0.238257 0.615929 -0.768308 -0.17417 0.611181 -0.782609 -0.118243 +0.585859 -0.808518 -0.0553806 0.642059 -0.75471 -0.134807 0.208053 -0.91119 0.355594 +0.0527768 -0.997889 0.0378443 0.275977 -0.954009 -0.117062 0.328506 -0.94443 -0.0116645 +0.31694 -0.948314 0.0158053 0.319797 -0.946608 -0.0407858 0.415529 -0.872621 -0.256648 +0.382426 0.905954 0.181651 0.528971 0.774292 0.347364 0.59986 0.783064 0.164251 +0.581118 0.758716 -0.294367 0.35079 0.913389 -0.206559 0.197412 0.755004 -0.625297 +0.321306 0.904469 0.280531 0.507206 0.744414 0.434269 0.471213 0.725275 0.50193 +0.0351597 0.998167 0.0492681 0.035356 0.998928 0.0298649 0.0317333 0.999182 -0.0250623 +0.0839849 0.900877 -0.425873 0.177996 0.673428 -0.717504 0.366297 0.900049 0.236088 +0.611305 0.651414 0.449406 0.593004 0.722519 0.355405 0.524736 0.79571 0.302487 +0.296478 0.945593 0.133998 0.124583 0.991549 0.0361968 0.112873 0.992365 0.0497065 +0.268355 0.952321 0.145156 0.376267 0.908522 0.181686 0.4393 0.735638 -0.515609 +0.436419 0.672163 -0.59811 0.550293 0.668062 -0.500869 0.240338 0.80925 0.536052 +0.240407 0.483096 0.841916 0.347401 0.73557 0.581591 0.382348 0.871546 0.306949 +0.385107 0.905631 0.177552 0.547556 0.807926 -0.217802 0.383907 0.853927 -0.351317 +0.557598 0.829858 0.0204739 0.409803 0.912138 -0.0081121 0.544738 0.838549 0.00985539 +0.395158 0.918571 0.00883699 0.530253 0.847806 -0.00752735 0.385682 0.922542 -0.0128526 +0.537126 0.843482 0.00582124 0.392985 0.919419 0.0152335 0.564409 0.825419 0.0112211 +0.462263 0.885518 0.0465929 0.595007 0.799323 -0.0839617 0.58997 0.79699 0.129396 +0.555364 0.767776 -0.319516 0.733381 0.619427 -0.280111 0.632991 0.688315 -0.354322 +0.350514 0.927619 0.129088 0.446261 0.792099 0.416449 0.383636 0.758969 0.526107 +-0.593402 0.803474 -0.0479957 0.204625 0.858279 -0.470623 -0.178766 0.808774 0.560292 +0.262094 0.936235 0.234029 0.547039 0.833368 -0.0790349 0.624556 0.690631 0.364635 +0.544732 0.755218 0.364572 0.352053 0.852227 -0.386999 0.437696 0.548324 -0.712575 +0.287083 0.833102 -0.472783 0.129557 0.312816 -0.940936 0.0348993 0.0650946 0.997269 +0.398871 0.916302 -0.0359487 -0.0780142 0.993536 -0.0824636 -0.176068 -0.570911 -0.801911 +-0.330392 -0.573616 -0.749537 -0.24892 -0.489458 -0.835745 -0.0014639 -0.00702641 0.999974 +-0.0260805 -0.0892081 -0.995672 0.0127036 -0.0438163 0.998959 0.0547057 -0.0071312 -0.998477 +-0.0299695 -0.377862 0.925377 0.420652 -0.549404 -0.721947 0.233262 -0.371032 -0.898846 +0.023345 -0.585908 0.810042 -0.0212532 -0.365018 0.930758 0.170247 -0.292258 -0.941064 +0.159649 -0.594802 -0.78786 -0.16125 -0.345269 0.924547 -0.00265061 -0.0400265 -0.999195 +-0.375578 -0.576529 0.725641 -0.32711 -0.577028 0.748356 -0.245789 -0.490241 0.836213 +-0.00355452 0.0153391 -0.999876 -0.0385515 -0.0648428 0.997151 0.00188328 0.0095782 -0.999952 +0.207646 0.332467 0.919972 0.0101663 0.0278473 -0.99956 0.108925 0.572224 0.812832 +0.255587 -0.153001 0.954602 0.558389 0.1634 0.813328 -0.0363493 -0.081033 -0.996048 +0.384533 -0.501992 0.774686 0.120638 -0.307381 0.943909 -0.197621 -0.480368 -0.854513 +-0.00139902 -0.0416762 0.99913 -0.125115 -0.295097 -0.94724 -0.309468 -0.610862 -0.728751 +0.00369132 0.016218 0.999862 -0.00484705 0.371908 -0.928257 0.00131207 0.00166009 0.999998 +0.0756897 0.619451 -0.781378 0.0122783 0.377919 -0.925757 -0.000147323 -0.00108815 0.999999 +-0.00923026 0.0331683 -0.999407 0.000890455 0.00272522 0.999996 -0.00205738 -0.0233853 -0.999724 +-0.00186222 -0.00735221 0.999971 0.00196951 -0.000130667 -0.999998 -0.00725838 -0.0313988 0.999481 +-0.00541836 -0.0117711 -0.999916 0.0374059 0.0892358 0.995308 0.00585193 0.00198479 -0.999981 +0.205938 0.501539 0.840267 0.134656 0.334772 -0.932628 0.276281 0.556575 0.783513 +-0.0411548 0.0337076 0.998584 0.358093 0.558822 -0.747989 0.316739 0.607512 -0.728427 +0.141628 0.288848 -0.946841 -0.152394 -0.570948 0.806718 -0.180685 -0.341991 0.922169 +-0.182424 -0.56962 -0.801408 -0.362447 -0.602278 -0.711262 -0.262082 -0.490593 -0.831043 +-0.00397275 -0.00867384 0.999955 -0.0533319 -0.104555 -0.993088 0.00683581 0.013148 0.99989 +0.00580695 0.0359838 -0.999336 -0.000804668 -0.00168255 0.999998 0.159262 0.330101 -0.930413 +-0.000676214 -0.00176291 0.999998 0.467687 0.552096 -0.69026 0.332408 0.606731 -0.722068 +0.234045 0.472295 -0.8498 0.000386198 6.90391e-05 1 0.0461624 0.0937222 -0.994528 +0.000125857 0.00647707 0.999979 -0.549591 -0.739897 -0.387945 -0.402141 -0.914379 -0.0468242 +-0.57829 -0.811662 0.0823716 -0.196511 -0.756643 0.623598 -0.0257212 -0.00454974 -0.999659 +0.0182018 0.0019489 0.999832 -0.479749 -0.756296 -0.44481 -0.278135 -0.914635 -0.293399 +-0.475342 -0.741999 -0.472745 -0.13235 -0.985279 -0.108209 -0.277573 -0.945744 -0.168882 +-0.370916 -0.912581 -0.172095 -0.412678 -0.829561 0.376199 -0.591547 -0.781884 0.196795 +-0.415396 -0.909334 0.0236222 -0.580492 -0.814245 -0.00591749 -0.417673 -0.90857 -0.00705391 +-0.575211 -0.81793 -0.0110499 -0.410848 -0.911629 -0.011651 -0.574896 -0.818041 -0.0174488 +-0.404547 -0.914507 0.0042867 -0.595024 -0.801732 0.0563245 -0.460811 -0.862409 0.209533 +-0.602781 -0.730445 0.321099 -0.517932 -0.702673 0.48785 -0.0355269 -0.091263 -0.995193 +0.0478132 -0.0264713 0.998505 -0.218627 -0.818559 -0.53119 -0.196213 -0.892556 -0.406009 +-0.307001 -0.882186 -0.35707 -0.378092 -0.859458 0.344061 -0.121821 -0.988677 0.0876165 +-0.358569 -0.856156 0.372055 -0.472289 -0.719107 0.509733 -0.510069 -0.712102 0.482432 +-0.167782 -0.404794 -0.898883 0.522701 -0.275043 0.806929 0.105186 -0.252208 0.961939 +-0.258351 -0.807829 -0.529781 -0.1661 -0.906965 -0.387072 -0.303966 -0.801903 -0.51435 +-0.0779098 -0.980839 -0.178565 -0.181442 -0.907612 -0.378577 -0.176077 -0.862582 0.474287 +-0.028465 -0.621564 0.782846 0.342161 -0.76457 0.546223 -0.220367 -0.750321 -0.623263 +-0.285384 -0.617004 -0.73339 -0.383248 -0.715378 0.584257 -0.432943 -0.594151 0.677898 +-0.560652 -0.709695 0.426617 -0.154661 -0.83216 -0.532532 -0.243939 -0.920723 -0.304571 +-0.139016 -0.943991 -0.299258 0.202429 -0.888096 0.412684 -0.012813 -0.999918 0.00025999 +-0.222702 -0.904241 -0.364353 -0.456683 -0.847201 0.271459 -0.363001 -0.906847 -0.214149 +-0.754975 0.641884 0.134157 -0.91106 0.412232 0.00580678 -0.877286 0.439081 -0.19385 +-0.26108 0.879945 0.396904 -0.34527 0.670708 0.65646 -0.330844 0.930438 -0.157565 +-0.745267 0.610192 -0.268782 -0.229713 0.791753 -0.566003 -0.886436 0.454193 -0.0891126 +-0.754165 0.530989 -0.386375 -0.400196 0.632587 0.663081 -0.505192 0.710379 0.490044 +-0.444092 0.739193 0.506336 -0.552737 0.774012 -0.308849 -0.649657 0.736237 -0.189474 +-0.6358 0.662511 -0.396026 -0.639814 0.592495 -0.489477 -0.550717 0.606112 -0.57388 +-0.419904 0.741664 0.523083 -0.141274 0.587558 0.796754 -0.599398 0.577125 -0.554661 +-0.567923 0.573473 -0.590418 -0.572625 0.578344 -0.58105 0.130347 0.778164 0.614386 +0.198514 0.759797 0.619113 -0.570836 0.577221 -0.583919 -0.578019 0.571987 -0.582 +-0.585058 0.56808 -0.578784 0.371174 0.828724 0.418864 0.588857 0.598492 0.543189 +0.467403 0.641695 0.60808 0.297642 0.836429 0.460212 -0.585543 0.557501 -0.5885 +-0.521462 0.679843 -0.515646 -0.550123 0.63149 -0.54643 0.618 0.535697 0.575417 +0.598945 0.633268 0.490139 0.56837 0.590667 0.572772 -0.365513 0.855758 -0.366167 +-0.411482 0.824085 -0.389315 -0.640342 0.565439 -0.519848 -0.529895 0.656598 -0.536741 +0.493924 0.77177 0.400512 0.520994 0.714434 0.467065 0.519825 0.706904 0.479655 +0.508648 0.813816 0.281035 -0.629272 0.536101 -0.562683 -0.665136 0.530866 -0.525143 +-0.581756 0.573669 -0.576597 0.481699 0.741408 0.467205 0.651457 0.318891 0.688413 +0.64472 0.40135 0.65058 0.475273 0.836794 0.27183 -0.604494 0.578542 -0.547609 +-0.606035 0.548657 -0.575931 -0.561106 0.523877 -0.640868 0.692339 0.0283172 0.721017 +0.725838 -0.00267764 0.687861 -0.56902 0.495032 -0.656627 -0.412398 0.589609 -0.69447 +0.709086 -0.0285767 0.704543 0.730695 0.000678684 0.682703 -0.142996 0.943218 -0.29982 +-0.0355208 0.999342 -0.0073501 -0.0360346 0.999115 -0.0216753 -0.146977 0.947853 -0.282794 +-0.169606 0.804397 -0.569368 -0.322856 0.782997 -0.531676 0.718407 0.0172618 0.695408 +0.721631 -0.00745308 0.692238 -0.213741 0.899827 -0.380297 -0.0590963 0.998054 -0.0198987 +0.0309656 0.996782 -0.0739436 0.364888 0.744332 -0.559309 0.26528 0.858816 -0.438248 +0.253661 0.778016 -0.574758 0.70065 0.00383937 0.713495 0.563356 -0.249341 0.787692 +0.267759 0.808401 -0.524207 0.286026 0.755055 -0.589984 0.604076 -0.203852 0.770414 +0.345969 -0.463078 0.816005 0.279381 0.622556 -0.731007 0.222445 0.510539 -0.830583 +0.375658 -0.490845 0.786099 0.242123 -0.352242 0.904048 0.467625 0.583411 -0.664047 +0.40834 0.684872 -0.603497 0.564621 0.686866 -0.457623 0.496082 -0.586649 0.640114 +0.185977 -0.480913 0.856817 0.746529 0.52778 -0.405146 0.648885 0.651462 -0.393124 +0.81997 0.46608 -0.332293 0.688311 0.645573 -0.330853 0.627984 0.636721 -0.447462 +0.201299 -0.717889 0.666419 -0.108518 -0.759363 0.641554 0.500207 -0.734218 0.459038 +0.575052 -0.785012 0.230374 0.194178 -0.821898 -0.535517 0.303731 -0.945009 0.121266 +0.416677 -0.773726 0.477209 0.936192 -0.0243099 -0.350648 0.695877 0.486642 -0.528143 +0.875688 0.171639 -0.451343 0.739711 0.321561 -0.591123 0.142469 -0.928336 0.343359 +0.217787 -0.960589 0.172735 -0.00431993 -0.999458 0.0326343 0.00822066 -0.999965 0.00162454 +-0.000393283 -0.999998 -0.00196683 0.00459732 -0.999981 0.00415061 -0.00325838 -0.999978 0.00581682 +0.0030271 -0.998542 -0.0539015 -0.172293 -0.961446 -0.21433 0.0969516 -0.949218 0.299309 +-0.0495195 -0.998528 0.0221164 -0.290711 -0.938925 -0.184139 -0.388779 -0.920994 -0.0249324 +-0.322901 -0.937838 0.127255 -0.0340433 -0.999355 0.0114432 -0.0226287 -0.99632 0.0826737 +-0.271367 -0.86095 0.430261 -0.46761 -0.696005 0.544902 -0.0543465 -0.808583 0.585867 +0.811017 -0.483632 -0.329168 0.880884 -0.398771 -0.255001 0.901722 -0.320783 -0.289822 +0.84042 -0.0854349 -0.535159 -0.160553 -0.918251 0.361992 0.0288522 -0.914769 0.402947 +0.258719 -0.643078 0.720774 0.538919 -0.787314 -0.299504 0.571202 -0.815089 -0.0967384 +0.601395 -0.784398 -0.151801 0.636826 -0.743006 -0.205902 0.562275 -0.82562 0.0468843 +0.705254 -0.647429 0.288882 0.757324 -0.64832 0.0783693 0.800063 -0.571796 -0.181516 +0.732454 -0.633048 -0.250522 0.678154 -0.645442 -0.351444 0.776995 -0.541271 -0.321411 +0.372473 -0.915036 0.154836 0.0599406 -0.998072 0.0161137 0.0281023 -0.999532 -0.0120433 +0.0616112 -0.998099 -0.00161605 0.0689333 -0.997602 0.00615097 0.0646707 -0.997872 0.00828583 +0.113512 -0.992656 -0.0418311 0.340209 -0.932944 -0.117783 0.433819 -0.900512 -0.0296581 +0.49594 -0.841748 -0.213316 0.660662 -0.74857 -0.0562977 0.55016 -0.790834 -0.268153 +0.458845 0.862527 0.213329 0.480729 0.815379 0.32258 0.510791 0.824203 0.244504 +0.537968 0.787218 0.30146 0.280465 0.944286 0.172229 0.0747005 0.997133 -0.0120797 +-0.0970261 0.909643 -0.403902 -0.301535 0.79583 -0.525101 0.244078 0.915504 0.31981 +0.380344 0.760004 0.527004 0.547682 0.806405 0.223057 0.584601 0.761983 0.278608 +0.273594 0.952341 0.134878 0.0284412 0.999513 0.0128628 0.0130782 0.999582 0.0257891 +0.0101311 0.999941 0.00384488 -0.00742236 0.999747 -0.0212531 0.104892 0.937067 -0.333021 +0.38139 0.758927 -0.527799 0.626577 0.772671 0.101885 0.623924 0.712558 0.320905 +0.642311 0.753032 0.142756 0.652598 0.712504 -0.257785 0.473163 0.864198 -0.171108 +0.584835 0.68295 -0.437661 0.334253 0.900584 0.27789 0.491469 0.739844 0.459444 +0.459761 0.737252 0.495055 0.126106 0.983295 0.131259 0.271012 0.952225 0.140781 +0.327463 0.944248 0.0341018 0.342915 0.929004 0.139145 -0.0255657 0.932704 -0.359735 +0.452632 0.722964 -0.521964 0.294215 0.850149 -0.436675 0.105991 0.792497 -0.600595 +0.153893 0.70652 0.690758 0.051577 0.40111 0.914577 0.355419 0.834176 -0.421696 +0.403632 0.913103 -0.0576494 0.388923 0.921261 0.00418482 0.420788 0.907146 0.00474783 +0.442338 0.896845 0.00225302 0.505684 0.84627 0.167662 0.676799 0.723981 0.133397 +0.682323 0.710369 -0.172658 0.486763 0.873069 -0.028507 0.627201 0.739176 0.245435 +0.558957 0.829177 0.00577118 0.486035 0.847001 0.215311 0.212012 0.759604 -0.61486 +-0.027225 0.00899546 -0.999589 -0.0104423 0.0310418 0.999464 -0.0736264 -0.399483 -0.913779 +-0.088627 -0.62691 -0.774035 0.00513063 0.00786155 0.999956 0.00389327 -0.0106785 -0.999935 +-0.0436898 -0.086151 0.995324 0.0177475 -0.0238599 -0.999558 -0.267619 -0.461515 0.845804 +0.0251762 -0.368583 -0.929254 -0.163083 -0.578252 0.799393 -0.197188 -0.373911 0.90626 +0.0221354 -0.379728 -0.924833 -0.00115865 -0.610638 -0.791909 -0.272382 -0.468172 0.840609 +-0.00454264 -0.0478642 -0.998843 -0.246979 -0.579667 0.776523 -0.178437 -0.346591 0.920888 +-0.000669816 0.0188624 -0.999822 -0.0156056 0.0321743 0.999361 0.0036253 0.0166635 -0.999855 +-0.108357 0.318204 0.94181 -0.00866263 -0.00417225 -0.999954 -0.419395 0.50584 0.753813 +-0.26023 0.145925 0.954456 -0.156739 -0.319561 -0.934513 -0.114576 -0.574102 0.810727 +-0.218657 -0.313784 0.923975 -0.263619 -0.492709 -0.829303 -0.164442 -0.605157 -0.778938 +-0.382308 -0.581902 -0.717796 -0.0152528 -0.0145373 0.999778 -0.0456098 -0.038078 -0.998233 +0.00597822 0.0137827 0.999887 -8.29193e-05 0.397259 -0.917706 0.000325288 -0.0021766 0.999998 +-0.0109625 0.609609 -0.792626 -0.00843578 0.368817 -0.929464 -7.54556e-05 -0.000274447 1 +-0.00209651 0.040058 -0.999195 0.000386742 0.0025148 0.999997 0.00159678 -0.018706 -0.999824 +-0.00147695 -0.0111514 0.999937 -0.000245142 -0.00428837 -0.999991 0.0146608 0.00289876 0.999888 +-0.00597619 -0.019017 -0.999801 0.0143796 0.300081 0.953805 0.00375212 0.0423527 -0.999096 +-0.236875 0.482501 0.843257 -0.1629 0.138846 0.976824 0.308025 0.580501 0.75375 +-0.00805789 0.392392 -0.919763 -0.565886 -0.389669 0.726588 -0.421643 -0.492881 0.761108 +-0.444618 -0.351391 0.823917 0.0743418 0.606848 -0.791334 -0.0358816 0.0187748 -0.99918 +-0.0506969 -0.0550125 0.997198 -0.0572399 -0.422964 -0.904337 -0.0716562 -0.610942 -0.788426 +0.0166232 0.022314 0.999613 0.00257591 -0.0633638 -0.997987 0.00215453 0.00117858 0.999997 +0.0491749 0.101717 -0.993597 -0.00111273 -0.00239468 0.999997 0.26621 0.47914 -0.836395 +-0.000152678 -0.00096054 0.999999 0.239525 0.596098 -0.766352 0.166376 0.324619 -0.931097 +-0.00102679 -0.000990323 0.999999 -0.00399396 0.00787155 -0.999961 -0.0046642 0.00414182 0.999981 +-0.495158 -0.790615 -0.360204 -0.411393 -0.874067 -0.258385 -0.549969 -0.799426 -0.24177 +-0.356846 -0.922394 -0.147819 -0.575996 -0.753811 -0.316225 0.127628 -0.887008 0.443765 +0.30917 -0.748122 0.587135 -0.0227941 -0.0178858 -0.99958 -0.00332466 0.0257743 0.999662 +-0.255675 -0.789501 -0.55796 -0.185796 -0.90391 -0.385262 -0.384581 -0.797565 -0.464744 +-0.135157 -0.977436 -0.162333 -0.292723 -0.913081 -0.283893 0.0578108 -0.937687 0.342638 +-0.307177 -0.941389 -0.139386 -0.418396 -0.907123 -0.045534 -0.418183 -0.908363 9.60036e-05 +-0.414867 -0.909863 0.00591735 -0.379341 -0.910932 0.162182 -0.422303 -0.817911 0.390746 +-0.486348 -0.699339 0.523823 -0.561307 -0.714856 0.417031 -0.156238 -0.30471 -0.939544 +0.0188904 -0.0257167 0.999491 -0.264741 -0.819991 -0.507472 -0.227805 -0.869956 -0.437358 +-0.279693 -0.798122 -0.533641 -0.206766 -0.890554 -0.405168 -0.425985 -0.866076 0.261629 +-0.348549 -0.919322 0.182648 -0.317008 -0.811072 0.491597 -0.39518 -0.608499 0.688158 +-0.189191 -0.511994 0.837895 -0.356092 -0.692026 -0.627932 -0.163451 -0.651127 -0.741159 +-0.47408 -0.589592 -0.653934 -0.0273992 -0.430897 0.901985 -0.245451 -0.842171 -0.480106 +-0.253359 -0.928631 -0.271024 -0.147867 -0.940165 -0.306961 -0.314045 -0.556673 0.769085 +-0.513757 -0.724174 -0.460029 -0.339605 -0.773535 -0.535082 -0.483507 -0.638135 -0.599171 +-0.0655446 -0.878648 0.472949 -0.329215 -0.932631 0.147703 -0.616356 -0.783846 0.0754361 +-0.429789 -0.88839 -0.161382 -0.521151 -0.793023 -0.315463 -0.382435 -0.88127 -0.27768 +-0.375004 -0.886199 -0.272071 -0.212089 -0.851379 0.479762 -0.368898 -0.924309 -0.097808 +-0.429714 0.590974 0.682712 -0.585577 0.699858 0.409021 -0.35821 0.919099 -0.164142 +-0.457329 0.707668 -0.538568 -0.527152 0.683637 0.504728 -0.557967 0.605712 0.567261 +-0.6453 0.470969 0.601479 -0.422278 0.749772 -0.509435 -0.645132 0.635954 -0.423518 +-0.636735 0.702446 -0.318022 -0.67502 0.671068 -0.30662 -0.496348 0.788824 0.362487 +-0.24611 0.787335 0.565273 -0.566522 0.707358 0.422727 -0.73076 0.635966 -0.248067 +-0.658237 0.64841 -0.382476 -0.685017 0.556361 -0.470334 -0.565159 0.594582 -0.571897 +0.155329 0.897316 0.413154 0.30127 0.780699 0.54749 0.36935 0.749022 0.550042 +-0.181583 0.916934 0.355332 -0.603157 0.555306 -0.572571 -0.635021 0.540518 -0.551895 +-0.570737 0.57225 -0.588888 0.195705 0.891287 0.409031 0.330358 0.76288 0.555767 +0.521937 0.660035 0.540311 0.190735 0.889342 0.415561 -0.602792 0.556326 -0.571965 +-0.511183 0.67378 -0.533584 -0.547999 0.626477 -0.554277 0.399041 0.891967 0.212513 +0.6673 0.669124 0.327084 0.579166 0.738899 0.344376 0.556541 0.704069 0.441077 +0.356059 0.83974 0.40995 -0.367459 0.853161 -0.370257 -0.416993 0.793672 -0.442946 +-0.528377 0.388479 -0.754919 -0.726122 0.438337 -0.529724 0.705737 0.654357 0.271574 +0.659634 0.592519 0.46239 0.661416 0.663711 0.349308 0.646286 0.684382 0.337542 +0.722825 0.649727 0.235329 -0.747554 0.399859 -0.530355 -0.406296 0.578258 -0.707489 +-0.743823 0.514743 -0.426342 0.544278 0.467298 0.696702 0.416024 0.164588 0.894335 +0.688756 0.389138 0.611708 -0.448911 0.823395 -0.347132 -0.446443 0.764603 -0.464834 +-0.260027 0.740996 -0.619121 -0.518376 0.613408 -0.595833 0.651815 0.0577988 0.756173 +0.390499 0.033951 0.919977 -0.393988 0.605858 -0.691166 -0.171958 0.7487 -0.640217 +-0.459864 0.625505 -0.630293 0.657135 -0.0110108 0.753692 0.563101 -0.0203086 0.826138 +-0.191442 0.907616 -0.373607 -0.0168808 0.999037 -0.0404982 0.00518149 0.999854 0.0162683 +0.0211791 0.999437 -0.0260389 -0.00296929 0.931334 -0.364154 0.0634627 0.786804 -0.613931 +0.677321 0.0102827 0.735615 0.450807 -0.0682811 0.890006 -0.0309641 0.926123 -0.375949 +-0.0431227 0.998722 -0.026372 -0.00681183 0.999524 0.0300988 0.162848 0.939381 -0.301737 +0.308974 0.751979 -0.58229 0.604407 -0.167484 0.778872 0.19586 -0.355613 0.913881 +0.248352 0.789633 -0.561072 0.270749 0.648125 -0.711779 0.092862 0.774863 -0.625271 +0.010352 0.573663 -0.819026 0.353491 -0.508177 0.785367 0.2107 -0.539689 0.815071 +0.232128 0.640931 -0.731658 0.257143 -0.552035 0.79318 0.140423 -0.439888 0.887006 +0.370516 0.764631 -0.527311 0.673251 0.708567 -0.211344 0.587955 0.781952 -0.207029 +0.68381 0.66616 -0.297715 0.70551 0.68444 -0.183841 0.696612 0.585357 -0.414836 +0.0134189 -0.582817 0.812492 0.760548 0.526315 -0.38021 0.693552 0.617333 -0.371329 +0.915377 0.402432 -0.0115609 0.810487 0.581455 -0.0708639 0.936181 0.348384 -0.0468262 +0.826588 0.56176 0.0343196 0.858648 0.286369 -0.425107 0.810972 0.555735 -0.182982 +0.755751 0.377607 -0.535027 -0.0157526 -0.890119 0.455457 -0.365104 -0.826744 0.428011 +-0.411716 -0.762584 0.498955 -0.57231 -0.70724 0.415058 -0.514854 -0.775298 -0.365838 +-0.479064 -0.875025 -0.0694944 -0.560381 -0.828217 0.00545384 0.541536 -0.827283 -0.149471 +-0.515396 -0.786761 0.339668 -0.537473 -0.755634 -0.374352 -0.381111 -0.900075 -0.211233 +-0.572898 -0.796503 -0.193315 -0.351475 -0.935462 -0.0371096 -0.614173 -0.786145 0.0690517 +-0.473596 -0.853276 0.218236 -0.615673 -0.71634 0.328334 -0.551109 -0.691502 0.467017 +0.944222 0.146242 -0.295057 0.997177 0.0479719 -0.0577654 0.93789 -0.00541848 -0.34689 +0.794043 0.0640114 -0.604482 -0.360137 -0.891109 0.27609 -0.342382 -0.938074 0.0528362 +-0.361601 -0.932135 -0.0192376 -0.354735 -0.934967 1.14684e-05 -0.350936 -0.936332 0.0112195 +-0.346457 -0.937977 -0.0129115 -0.394432 -0.902387 -0.173553 0.0659763 -0.970495 0.231919 +-0.354042 -0.933611 0.0549931 -0.390021 -0.903345 -0.17847 -0.382066 -0.860056 0.338125 +-0.00566165 -0.996361 0.0850423 0.149221 -0.910435 0.3858 0.289289 -0.788083 0.543357 +0.933773 -0.253354 -0.252746 0.992637 -0.0577231 -0.106489 0.919356 -0.206284 -0.33501 +0.805459 -0.164136 -0.569469 0.153139 -0.89771 0.413117 0.148398 -0.865558 0.478317 +0.238383 -0.540928 0.806579 0.786035 -0.60996 0.10049 0.621554 -0.720263 -0.308045 +0.755284 -0.63754 -0.151951 0.854182 -0.517254 -0.0531193 0.884739 -0.436813 -0.162581 +0.722065 -0.417042 -0.551994 0.709649 -0.564118 -0.4221 0.577468 -0.772305 0.264717 +0.232762 -0.599127 0.766073 0.567937 -0.72736 -0.38522 0.455316 -0.863946 -0.21514 +0.331682 -0.942991 -0.0274865 0.314586 -0.949173 0.0102982 0.317353 -0.948299 0.00389038 +0.313253 -0.949651 0.00603527 0.31053 -0.95049 0.0118445 0.34624 -0.93813 -0.00553162 +0.38066 -0.924004 0.0362436 0.516757 -0.851709 0.086916 0.765447 -0.626454 0.147126 +0.567369 -0.758053 0.321633 0.743056 -0.665083 0.0743805 0.578008 -0.747784 -0.32669 +0.509599 -0.823102 -0.250622 0.619912 -0.773046 -0.134571 0.725223 -0.687844 -0.0303724 +0.622859 -0.760957 -0.181633 0.530995 -0.805467 -0.263185 0.468778 -0.883085 -0.0201968 +0.48153 -0.865649 0.137044 0.540181 -0.81111 -0.224286 0.468907 0.86348 0.185819 +0.732721 0.600929 0.319382 0.855278 0.48549 0.181106 0.82508 0.565013 -0.00206679 +0.42706 0.903616 0.0331246 0.769873 0.618632 -0.156812 0.491295 0.847821 -0.199574 +0.564894 0.750725 -0.342501 0.712914 0.633528 0.300661 0.585782 0.74236 0.325211 +0.607056 0.778326 0.160287 0.63159 0.771661 0.0750638 0.7464 0.657448 0.103194 +0.593804 0.763025 0.255323 0.692083 0.715108 0.0981895 0.628302 0.775309 0.0642817 +0.486205 0.873751 0.0128204 0.238028 0.897169 -0.372062 0.196128 0.797738 -0.570217 +-0.366984 0.726538 -0.580918 -0.00459631 0.947194 0.320629 0.0640314 0.770217 0.634559 +0.0260989 0.999312 0.0263483 0.00412297 0.999981 0.0045558 0.0065582 0.999658 0.0253118 +0.254349 0.95037 0.179177 0.31815 0.940414 -0.120005 0.0430299 0.996158 -0.0762683 +0.547669 0.818442 -0.173813 0.292081 0.944064 -0.153073 0.578442 0.815699 -0.00633781 +0.473643 0.880716 0.00104716 0.623968 0.779398 0.0565968 0.606589 0.773547 0.183507 +0.779563 0.61388 0.124226 0.742313 0.627259 0.235621 0.825957 0.552952 0.109723 +0.861558 0.477545 0.172244 0.54841 0.802728 -0.234253 0.590096 0.784756 -0.189589 +0.509291 0.813814 -0.279875 0.570663 0.821168 -0.00512611 0.446959 0.892728 -0.0571381 +0.593356 0.801339 0.0760552 0.474649 0.878664 0.0515553 0.580699 0.760745 0.289924 +0.524854 0.828171 0.196626 0.509154 0.853395 0.111709 0.431139 0.884507 0.178231 +-0.0745085 0.910369 -0.407033 0.291909 0.934604 -0.203233 0.0649816 0.915724 -0.396518 +-0.177912 0.593201 -0.78515 0.0043106 0.620311 0.784344 -0.00520164 0.347398 0.937703 +-0.164667 0.580969 -0.797094 -0.306056 -0.200495 -0.930662 -0.00232512 0.0461987 0.998929 +-0.155908 -0.324538 -0.932935 -0.438514 -0.522908 -0.73094 -0.00926603 -0.0218151 0.999719 +-0.00958753 0.0171584 -0.999807 -0.129788 -0.329663 0.935135 0.00172846 -0.0278118 -0.999612 +-0.385222 -0.553728 0.738234 -0.314987 -0.582237 0.749522 -0.20982 -0.472994 0.855717 +-0.00181044 -0.375034 -0.927009 -0.151114 -0.374253 0.914932 -0.00116871 -0.379256 -0.925291 +0.00717644 -0.607044 -0.794636 -0.362673 -0.565837 0.740471 -0.302186 -0.561009 0.770683 +-0.202546 -0.47654 0.855502 0.00364286 -0.045988 -0.998935 -0.0477819 -0.117085 0.991972 +0.0024181 0.020512 -0.999787 0.0361069 0.0785047 0.99626 0.00807696 0.0244197 -0.999669 +0.324248 -0.0263989 0.945604 0.00757353 -0.0303746 -0.99951 -0.593476 -0.1527 0.790234 +-0.326346 0.0283665 0.944825 -0.0399801 -0.38338 -0.922725 -0.0385337 -0.0511575 0.997947 +-0.0512935 -0.404061 -0.913293 -0.0769147 -0.614081 -0.785486 0.00810753 0.0295281 0.999531 +0.00857035 0.0117778 -0.999894 0.00242009 0.0071664 0.999971 0.00624123 0.391944 -0.919968 +-0.000489041 -0.00367434 0.999993 -0.00302484 0.608885 -0.793253 -0.000235876 0.373714 -0.927544 +0.000295057 0.000742267 1 0.000789704 0.0433186 -0.999061 0.000419518 0.00143751 0.999999 +0.000721119 -0.018976 -0.99982 -0.00330664 -0.0139843 0.999897 -0.00140845 -0.00868643 -0.999961 +-0.0213842 -0.00769621 0.999742 -0.00133932 -0.0182324 -0.999833 -0.00486035 0.0465991 0.998902 +0.00361407 0.0505696 -0.998714 -0.616535 -0.127615 0.776917 -0.279361 0.0509997 0.958831 +-0.187211 0.272847 -0.943667 -0.0649884 -0.0376449 0.997176 -0.179486 0.542965 -0.82035 +-0.297734 -0.139354 -0.944423 0.016805 0.0275759 0.999478 -0.14047 -0.343482 -0.928595 +-0.423023 -0.502882 -0.753765 0.00539732 0.00757266 0.999957 0.00990577 -0.010255 -0.999898 +-0.000898276 -0.00393678 0.999992 0.13124 0.334733 -0.933129 -0.000100281 -0.00138016 0.999999 +0.370991 0.579825 -0.725375 0.308547 0.597624 -0.74003 0.201814 0.477919 -0.854906 +0.000162301 -0.00100212 1 0.0345059 0.0776508 -0.996383 -0.00216443 -0.00458002 0.999987 +-0.00461299 -0.013904 -0.999893 -0.00430043 0.00282464 0.999987 -0.464733 -0.742625 -0.482216 +-0.409664 -0.817238 -0.405337 -0.473806 -0.750564 -0.46061 -0.436978 -0.865785 -0.243859 +-0.297619 -0.88495 0.358172 -0.205947 -0.76968 0.604301 0.382969 -0.707268 0.594228 +-0.555887 -0.732192 -0.393554 -0.384524 -0.897233 -0.21706 -0.567915 -0.803428 -0.178815 +-0.35679 -0.933736 -0.0289564 -0.580001 -0.814612 -0.00246248 -0.361748 -0.932259 0.00565509 +-0.57348 -0.819215 -0.00274207 -0.368166 -0.929614 0.016471 -0.574837 -0.814974 0.0733515 +-0.468375 -0.860707 0.199521 -0.560499 -0.772724 0.297889 -0.561136 -0.746229 0.358145 +0.00607478 -0.0287805 -0.999567 0.00449107 0.0357879 0.999349 -0.246936 -0.828073 -0.503307 +-0.287706 -0.886653 -0.362037 -0.296671 -0.895318 -0.332252 0.0585829 -0.908481 0.4138 +-0.412051 -0.897998 -0.154318 -0.485122 -0.823451 0.294254 -0.532761 -0.800801 0.273647 +-0.589301 -0.784082 0.194783 -0.471806 -0.825109 0.310796 -0.491982 -0.794003 0.357088 +-0.222313 -0.76809 0.600512 -0.03104 -0.342769 -0.938907 -0.0269726 -0.0485563 0.998456 +-0.328295 -0.800568 -0.501311 -0.369386 -0.842882 -0.391284 -0.348651 -0.877153 -0.33022 +-0.21524 -0.759808 0.613485 -0.184126 -0.648206 0.738868 -0.0385094 -0.757166 -0.652087 +-0.0244437 -0.637664 -0.769927 -0.244311 -0.465706 0.850547 -0.196389 -0.849449 -0.489763 +0.0193641 -0.873969 -0.485596 -0.509104 -0.823549 -0.250159 -0.340865 -0.912971 -0.224264 +-0.504166 -0.809322 -0.301355 -0.367871 -0.897343 -0.243815 -0.378849 -0.881881 -0.280641 +-0.557489 -0.816672 0.149178 -0.390926 -0.856151 0.337909 -0.313828 -0.936707 -0.155214 +-0.545949 -0.78138 -0.3023 -0.0887825 -0.888281 -0.450638 -0.191632 -0.849877 0.490903 +-0.113318 -0.992188 0.0521733 -0.275159 -0.944864 -0.177539 -0.869863 0.489838 0.0582909 +-0.837786 0.419479 0.349503 -0.91828 0.394414 0.0346447 -0.800188 0.452958 -0.393101 +-0.49496 0.791407 0.358732 -0.702825 0.682496 0.200589 -0.820678 0.535094 0.200407 +-0.810973 0.567036 -0.144197 -0.73003 0.64708 0.219872 -0.843687 0.46614 -0.26628 +-0.773258 0.541372 -0.330134 -0.824463 0.494859 -0.274547 -0.834098 0.495985 -0.24141 +0.0393976 0.933315 0.356892 0.0942396 0.815287 0.571337 0.000134955 0.99622 0.0868646 +-0.287114 0.951418 0.111215 -0.4378 0.898696 0.026016 -0.522082 0.816677 0.245906 +-0.763215 0.592021 -0.258873 -0.735958 0.602405 -0.308987 -0.807956 0.522865 -0.271695 +-0.694042 0.631717 -0.34531 -0.750128 0.518986 -0.409831 0.155198 0.939781 0.304507 +0.198768 0.818575 0.538912 0.0315604 0.909995 0.413417 -0.695822 0.64512 -0.315676 +-0.646343 0.634375 -0.424039 -0.561037 0.714034 -0.418799 -0.534894 0.654028 -0.534916 +0.280333 0.907142 0.313859 0.371393 0.727822 0.576491 0.597538 0.73334 0.324285 +0.593329 0.756533 0.274987 0.279437 0.950068 0.138873 0.147162 0.98284 0.111219 +0.253922 0.892518 0.37274 -0.396922 0.837401 -0.375781 -0.383545 0.845585 -0.371321 +-0.54248 0.6452 -0.53799 -0.494323 0.626674 -0.602433 0.24112 0.609102 0.75555 +0.261159 0.394642 0.880939 0.479487 0.617807 0.623222 -0.557846 0.509682 -0.655006 +-0.387833 0.589684 -0.708419 0.127079 0.107254 0.986077 -0.203793 0.879595 -0.429861 +-0.198651 0.894191 -0.401198 -0.26611 0.761511 -0.591005 0.456591 0.358235 0.814366 +-0.177653 0.729523 -0.660481 -0.0644685 0.800033 -0.596482 0.503392 0.410548 0.760294 +0.264268 -0.172152 0.948961 0.646238 0.119379 0.753741 -0.00959923 0.940417 -0.339889 +0.0197419 0.998403 -0.052934 0.0203889 0.999437 0.026659 0.0215602 0.999745 -0.00669996 +0.1342 0.935541 -0.326733 0.314859 0.717645 -0.621167 0.36109 -0.430321 0.827307 +0.0929712 -0.351418 0.931591 0.212157 0.852771 -0.477254 0.0826963 0.992849 -0.0860927 +0.000527119 0.998599 0.0529178 0.0246111 0.99804 -0.0575347 0.264317 0.684773 -0.679134 +0.25273 0.836662 -0.485926 0.20718 0.60594 -0.768058 -0.296906 -0.470183 0.831128 +0.126116 0.558159 -0.820094 -0.116536 0.706129 -0.698427 -0.105656 -0.582645 0.80583 +-0.214433 -0.432162 0.875931 0.0693606 0.823298 -0.563355 0.226292 0.931932 -0.283364 +0.552433 0.806927 -0.209012 0.637781 0.7696 -0.0308486 0.303872 0.952598 0.0147765 +0.770993 0.618276 -0.152659 0.350712 0.936466 0.00569581 0.87587 0.481301 -0.034655 +0.433084 0.901338 -0.00526427 0.887691 0.45982 0.0238773 0.483978 0.874549 0.0304923 +0.896012 0.44385 -0.0126207 0.58848 0.807969 0.0296251 0.924279 0.381286 0.0181458 +0.638751 0.754976 0.148354 0.95149 0.306534 0.0265174 0.770932 0.632844 0.0719177 +0.922443 0.382543 -0.0525344 0.728251 0.678182 -0.0985849 0.692783 0.621406 -0.365932 +0.273077 0.814233 -0.512302 -0.127554 0.818604 -0.560015 0.246707 -0.57767 0.778096 +-0.0708805 -0.490308 0.868662 0.858759 0.466886 -0.211072 0.927864 0.372787 0.00993763 +0.917011 0.398708 -0.0110979 0.924377 0.330124 -0.191169 0.997015 0.0677647 -0.0370033 +0.999496 0.0122447 0.0292824 0.998554 0.0515021 -0.0154342 0.179049 0.938618 -0.294851 +0.897399 0.407059 -0.170229 0.822089 0.510353 -0.25241 0.514964 0.830406 -0.212693 +0.610599 0.709041 -0.352746 -0.142539 -0.696335 0.703421 -0.267217 -0.60971 0.746223 +0.938657 0.138768 -0.315701 0.999943 0.00168913 -0.0105309 0.999458 -0.0194754 0.0265321 +0.99994 0.00346572 0.010411 0.998228 0.0489194 -0.0338715 0.706762 0.631076 -0.319737 +0.952161 0.263871 -0.154149 0.888776 0.206121 -0.409379 0.703794 0.629994 -0.328301 +0.713609 0.324439 -0.620888 -0.25578 -0.850417 0.459747 -0.350987 -0.932281 0.0875233 +-0.5828 -0.799114 0.147514 -0.644085 -0.747437 0.162765 -0.719618 -0.693715 0.0301474 +-0.662685 -0.748898 8.191e-05 -0.722132 -0.691752 -0.0020214 -0.655293 -0.755369 -0.00295314 +-0.743432 -0.668735 -0.0101215 -0.650139 -0.759687 0.0139833 -0.778013 -0.628127 0.0123229 +-0.648777 -0.760791 0.0169011 -0.803702 -0.59468 -0.0204516 -0.63825 -0.764314 -0.0919862 +-0.721234 -0.683323 -0.113538 -0.571672 -0.770557 -0.281837 -0.660683 -0.75045 -0.017977 +-0.652705 -0.744417 0.140779 -0.606235 -0.767845 0.207106 -0.749549 -0.660564 0.0427929 +-0.666886 -0.739534 -0.0913894 -0.54543 -0.838062 -0.0125657 -0.592013 -0.742592 -0.313174 +-0.125457 -0.99207 0.00763665 -0.039604 -0.998494 -0.0379669 -0.0658555 -0.995143 0.0731634 +-0.292352 -0.855172 0.428031 -0.440696 -0.712906 0.545484 -0.12937 -0.763121 0.633174 +0.918868 -0.0409341 -0.392436 0.998322 -0.0377528 -0.0439061 0.998734 -0.0405244 0.0298021 +0.998886 -0.0471623 0.00172211 0.997873 -0.0622732 -0.0192755 0.955806 -0.254842 -0.146598 +0.911785 -0.31998 -0.25741 0.734957 -0.280176 -0.617527 -0.0034625 -0.868986 0.494824 +0.162861 -0.798328 0.579783 0.367421 -0.776413 0.51204 0.856909 -0.284393 -0.429915 +0.940385 -0.281593 -0.190737 0.946292 -0.322935 0.0156617 0.93013 -0.366953 -0.0142956 +0.888095 -0.434156 -0.150986 0.741809 -0.628883 -0.232863 0.704452 -0.624979 -0.336376 +0.71471 -0.601514 -0.356891 0.214083 -0.893978 0.393664 0.20121 -0.90397 0.377297 +0.237148 -0.74246 0.62651 0.326308 -0.711912 0.621855 0.650834 -0.617899 -0.441153 +0.779909 -0.5484 -0.301661 0.769821 -0.62065 -0.148893 0.71736 -0.605985 -0.343769 +0.744594 -0.667139 -0.0224793 0.619668 -0.760932 -0.192336 0.537023 -0.799218 -0.269922 +0.316314 -0.614847 0.722433 0.167896 -0.612332 0.772567 0.603574 -0.722561 -0.337051 +0.6644 -0.742011 -0.0894013 0.671504 -0.740979 0.00567997 0.66518 -0.746662 0.00552658 +0.665794 -0.746126 0.00381721 0.667901 -0.744131 0.0132723 0.627995 -0.778021 -0.0175025 +0.554188 -0.83239 0.00130983 0.531274 -0.823539 0.198826 0.596585 -0.730689 -0.331934 +0.656982 -0.748565 -0.0895817 0.692609 -0.721097 0.0176716 0.686751 -0.708605 -0.162022 +0.597974 -0.672421 -0.436207 0.455372 -0.857192 -0.240538 0.519674 -0.809946 -0.271894 +0.54814 -0.789518 -0.27605 0.15171 -0.876037 0.457759 0.562368 -0.826763 0.0143475 +0.769392 -0.638705 -0.0095804 0.762621 -0.646845 0.000934721 0.759601 -0.650379 0.00354562 +0.764718 -0.644189 0.0150938 0.74929 -0.660567 -0.0470656 0.585493 -0.78491 -0.202768 +0.420446 -0.906759 0.0318459 0.377272 -0.896082 0.233887 0.355607 -0.917225 0.179562 +0.418744 -0.907893 -0.0196045 0.623887 -0.767024 -0.1498 0.71319 -0.700933 0.0073735 +0.705921 -0.707723 -0.0283467 0.664843 -0.670366 -0.329534 0.794572 0.554878 0.246508 +0.778677 0.516188 0.356668 0.894896 0.43233 0.110684 0.829716 0.51354 -0.21874 +0.776266 0.581848 -0.242621 0.677532 0.626946 -0.384563 0.727141 0.637612 0.254395 +0.686521 0.602253 0.407407 0.861719 0.467147 0.198026 0.780931 0.604049 0.158969 +0.768531 0.636716 0.0628733 0.798458 0.580561 0.159417 0.783202 0.595338 0.17935 +0.862078 0.505071 0.0415299 0.869406 0.494088 0.00332597 0.705439 0.639431 -0.305752 +0.719454 0.675217 -0.16269 0.581556 0.754994 -0.302946 0.695617 0.661653 -0.279878 +0.444216 0.611753 -0.654546 0.274159 0.894556 0.352996 0.33755 0.776828 0.5316 +0.592756 0.791084 0.151086 0.550691 0.732306 -0.400584 0.40304 0.88319 -0.239864 +0.228991 0.776512 -0.58702 0.34378 0.907794 0.240261 0.541085 0.74213 0.395563 +0.611534 0.749416 0.253774 0.616997 0.726787 0.301821 0.359177 0.912016 0.198036 +0.409998 0.890339 0.197986 0.327933 0.942175 -0.069032 0.153912 0.985715 -0.0683944 +0.574385 0.779318 -0.25049 0.472215 0.87996 -0.0517951 0.59224 0.723291 -0.355107 +0.699613 0.642844 -0.311918 0.589476 0.689133 -0.421442 0.659961 0.682786 0.313456 +0.569532 0.713358 0.408354 0.68917 0.69768 0.195672 0.76293 0.613941 0.202522 +0.70134 0.671775 0.238412 0.780075 0.572991 0.251324 0.535654 0.815076 -0.220739 +0.433829 0.796723 -0.420743 0.612378 0.741005 -0.27551 0.453584 0.871587 -0.186006 +0.547209 0.736312 -0.398004 -0.0189553 0.948338 0.316695 -0.0233222 0.78145 0.623532 +0.0009151 0.999305 0.0372551 0.0307596 0.99941 0.015288 0.141136 0.950546 0.276663 +0.208578 0.928853 -0.30615 0.084072 0.996238 0.0210031 0.344542 0.889223 0.300954 +0.466267 0.852712 -0.235536 0.430503 0.890303 0.148417 0.439285 0.682383 -0.584279 +0.361729 0.154333 -0.91942 -0.00957313 0.627136 0.778851 -0.00946937 0.33064 0.943709 +-0.604097 0.146884 -0.783257 -0.320605 -0.105162 -0.941357 0.00317992 0.0582645 0.998296 +-0.03066 -0.0524205 -0.998154 0.00334622 -0.053875 0.998542 0.0110281 0.0536657 -0.998498 +0.00202937 -0.38062 0.924729 0.00904679 -0.0281412 -0.999563 -0.061054 -0.590335 0.804846 +-0.000819698 -0.405172 0.91424 -0.0133548 -0.380157 -0.924826 0.00369499 -0.418429 0.908242 +-0.00948886 -0.383807 -0.923365 -0.0744347 -0.612871 -0.786669 -0.0730564 -0.575069 0.814837 +-0.0209 -0.388174 0.921349 0.000514737 -0.0391646 -0.999233 0.0119942 -0.0373301 0.999231 +0.00665209 0.0234667 -0.999703 0.235533 0.331722 0.913501 0.0106577 0.0301013 -0.99949 +0.242552 0.538439 0.807002 0.30249 -0.160255 0.939584 0.591622 0.164389 0.789278 +-0.0374432 -0.083711 -0.995786 0.433824 -0.516761 0.738075 0.128419 -0.303634 0.944095 +-0.198984 -0.476952 -0.856109 0.00732229 -0.0382311 0.999242 -0.122267 -0.344312 -0.93086 +-0.308736 -0.600553 -0.737576 0.00275299 0.0254868 0.999671 -0.00632702 0.0408657 -0.999145 +-0.000370374 0.00364034 0.999993 0.00416303 0.386893 -0.922115 -0.000139645 -0.0031288 0.999995 +0.00125915 0.607633 -0.794217 0.000226538 0.37444 -0.927251 0.00079298 0.00160139 0.999998 +0.000207737 0.0435772 -0.99905 -0.00128291 -0.00209193 0.999997 -0.000375072 -0.0200096 -0.9998 +-0.00880104 -0.0199877 0.999762 -0.000732 -0.0091801 -0.999958 0.0460094 0.02559 0.998613 +0.00927785 -0.00633474 -0.999937 0.276654 -0.0394471 0.96016 0.00318995 0.021032 -0.999774 +0.0165952 -0.050738 0.998574 -0.0619541 0.015574 -0.997958 0.0237391 0.000952874 0.999718 +-0.537808 0.168648 -0.826027 -0.287968 -0.0500137 -0.956333 0.000607279 0.019728 0.999805 +-0.0270254 -0.0568008 -0.99802 -0.00142954 0.00265204 0.999995 0.014586 0.0443924 -0.998908 +0.000261819 -0.00319574 0.999995 0.00941277 0.383202 -0.923616 0.000621669 -0.00102126 0.999999 +0.0761832 0.611092 -0.787885 0.00856723 0.372254 -0.928091 -0.000789457 -0.00255449 0.999996 +-0.00900172 0.0371229 -0.99927 -0.00314824 -0.00706651 0.99997 -0.511695 -0.751549 -0.416345 +-0.366243 -0.909975 -0.194455 -0.584095 -0.798939 -0.143285 -0.347605 -0.910615 0.223497 +-0.548006 -0.775394 0.313772 -0.198823 -0.753686 0.62644 0.0123001 0.00585482 -0.999907 +0.00530623 0.012045 0.999913 -0.476045 -0.73076 -0.489255 -0.408513 -0.813902 -0.413135 +-0.482617 -0.726353 -0.489378 -0.4094 -0.810334 -0.419225 -0.406008 -0.817251 0.408973 +-0.575433 -0.811997 0.0976612 -0.282096 -0.952997 -0.110535 -0.585669 -0.748277 -0.311565 +-0.0238347 -0.998734 -0.044291 -0.0332833 -0.999418 -0.00751121 -0.0262337 -0.999644 0.00483625 +-0.0219185 -0.99974 -0.00625191 -0.0722229 -0.996889 0.0315496 -0.320058 -0.934462 0.156026 +-0.587831 -0.773391 0.237322 -0.455584 -0.863787 0.21521 -0.611714 -0.733252 0.296897 +-0.530155 -0.726466 0.437245 -0.0218709 -0.0714652 -0.997203 0.00916268 0.0205808 0.999746 +-0.401992 -0.7714 -0.493299 -0.264076 -0.890057 -0.371566 -0.538705 -0.771189 -0.339212 +-0.412335 -0.877766 -0.243938 -0.475558 -0.799921 -0.366021 -0.386248 -0.850898 -0.356069 +-0.44087 -0.745333 0.500112 -0.196319 -0.592966 0.780928 -0.175442 -0.407917 -0.896004 +-0.151933 -0.319338 0.935382 -0.349822 -0.814942 -0.462055 -0.218274 -0.901045 -0.3748 +-0.529287 -0.810733 -0.250135 -0.321949 -0.852812 -0.41117 -0.542898 -0.745066 -0.387477 +-0.157419 -0.722583 0.673122 -0.36497 -0.739627 0.565464 -0.248275 -0.764879 -0.594406 +-0.267241 -0.621643 -0.736303 -0.452495 -0.713573 0.534848 -0.458604 -0.657012 0.598347 +-0.594649 -0.735949 0.323685 -0.528444 -0.778897 -0.337736 -0.444933 -0.893305 -0.0635693 +-0.540022 -0.841358 -0.0222086 0.311187 -0.780557 -0.542119 -0.542177 -0.777104 0.319613 +0.161688 -0.760135 -0.629327 -0.13763 -0.859894 -0.491569 -0.34548 -0.840057 0.418267 +-0.389508 -0.919137 0.0589193 -0.358127 -0.899259 -0.251155 -0.45258 -0.890701 0.0426864 +-0.161946 -0.956389 -0.243091 -0.841562 0.533664 -0.0835244 -0.0740646 0.933213 0.351608 +-0.0185556 0.80991 0.586261 -0.0303731 0.998715 0.0405628 -0.0391687 0.99917 -0.0112053 +-0.0506773 0.997494 -0.0493705 -0.310059 0.940345 -0.14005 -0.763165 0.61176 -0.208158 +-0.44426 0.598048 -0.667062 -0.789732 0.584776 -0.185366 -0.673223 0.686296 -0.275261 +0.306357 0.904959 0.295287 0.477386 0.747877 0.461284 0.382249 0.773178 0.506044 +0.0571772 0.994295 0.0900403 -0.0169751 0.92333 0.383633 -0.786198 0.578027 -0.218581 +-0.777186 0.622604 -0.0913513 -0.688971 0.643995 -0.33255 -0.674267 0.633249 -0.379947 +-0.65743 0.663793 -0.356601 -0.644372 0.71828 -0.262408 0.122898 0.957902 0.259462 +0.24684 0.757089 0.604886 0.082879 0.993277 0.0808179 0.276233 0.911951 0.303381 +-0.662462 0.693542 -0.283095 -0.568132 0.794667 -0.213848 -0.451268 0.8406 -0.299581 +-0.629116 0.621102 -0.467381 -0.531044 0.656183 -0.536112 0.31844 0.57671 0.752331 +0.126239 0.299046 0.945852 -0.624425 0.532259 -0.571658 -0.514111 0.69122 -0.507843 +-0.526956 0.632684 -0.567475 -0.0168619 0.286288 0.957995 -0.290085 0.896066 -0.336031 +-0.173497 0.914428 -0.365679 -0.227159 0.802279 -0.552039 -0.285533 0.803516 -0.522335 +-0.206003 0.522696 0.827255 0.0642735 0.379899 0.922792 0.357959 0.588247 0.725141 +-0.266789 0.785031 -0.559061 -0.218145 0.803874 -0.553353 -0.262708 0.800633 -0.53849 +-0.290814 -0.190866 0.937549 -0.145896 0.948429 -0.281418 -0.0445281 0.998305 -0.0374662 +-0.039034 0.999054 0.0191584 -0.0237219 0.999268 0.030022 0.0598187 0.996075 -0.0652415 +0.465979 0.696667 -0.545453 0.365029 0.8413 -0.398708 0.579 0.698013 -0.421352 +-0.307236 -0.609446 0.730877 -0.425003 -0.556144 0.714197 0.447558 0.84279 -0.298994 +0.342457 0.931871 -0.119752 0.0377635 0.996216 -0.0782852 0.00601101 0.914897 -0.403643 +0.0383945 0.709025 -0.704137 -0.42928 -0.51416 0.742535 -0.51056 -0.515636 0.688075 +-0.402158 -0.381419 0.832339 0.442744 0.620425 -0.647341 0.360225 0.693612 -0.623811 +0.537237 0.650828 -0.536469 0.372271 0.903912 0.210611 0.537648 0.771715 0.339692 +0.590222 0.783984 0.192371 0.619585 0.78374 -0.0431944 0.326281 0.945168 0.0140769 +0.428354 0.742632 -0.514792 0.383044 0.864337 -0.325882 0.207508 0.612973 -0.762368 +-0.407123 -0.39831 0.821949 0.375399 0.848371 -0.373287 0.123059 0.991066 -0.0514347 +0.0650222 0.996995 0.0421065 0.0818381 0.995797 0.0411266 0.28082 0.946878 0.156726 +0.417825 0.891108 0.177055 0.132264 0.61185 -0.779837 -0.26677 0.700406 -0.662016 +-0.371651 -0.647321 0.665471 -0.525315 -0.478786 0.703426 -0.623381 -0.613572 0.484691 +-0.786483 -0.508331 -0.350775 -0.761743 -0.577783 -0.293112 -0.897168 -0.409394 -0.16579 +-0.772365 -0.630822 -0.0742669 -0.924583 -0.380637 -0.016193 -0.679335 -0.717165 0.155494 +-0.892101 -0.447506 0.0623932 -0.581499 -0.777509 0.239455 -0.800162 -0.546616 0.246884 +-0.615591 -0.696356 0.368966 -0.0645101 0.851554 -0.520283 0.347828 0.936188 -0.050672 +0.406846 0.913377 -0.0147939 0.615586 0.754065 -0.228996 0.512815 0.822523 -0.245921 +0.606741 0.749811 -0.263911 0.434872 0.868734 -0.237039 0.550301 0.705463 -0.446644 +-0.63267 -0.642784 0.431924 -0.601719 -0.763387 0.234893 -0.797697 -0.601279 0.0462878 +-0.638024 -0.769717 -0.021452 -0.796249 -0.604215 -0.0301878 -0.676796 -0.736141 -0.0066207 +-0.781396 -0.624024 0.00373785 -0.692208 -0.721685 0.00435033 -0.77664 -0.629936 -0.00338732 +-0.696374 -0.71644 -0.0421494 -0.789928 -0.586914 -0.177613 -0.802532 -0.577137 -0.151179 +-0.894876 -0.445332 -0.0295905 -0.92159 -0.386556 -0.0352962 -0.914823 -0.403761 -0.00873244 +-0.885574 -0.463004 0.0372292 -0.688465 -0.659799 0.301132 -0.764558 -0.629898 0.136673 +-0.697313 -0.709211 0.103804 -0.689713 -0.719576 0.0806648 -0.701282 -0.648618 0.295801 +-0.598633 -0.736576 0.314792 -0.60192 -0.690076 0.401856 0.630952 0.0849849 -0.771153 +0.613381 0.511933 -0.601405 0.616328 0.0346376 -0.786727 -0.434457 -0.867048 0.243876 +-0.266976 -0.954924 0.129786 -0.0343338 -0.997783 0.0570199 -0.0230456 -0.93289 0.359422 +-0.0112681 -0.731593 0.681648 0.599759 -0.230761 -0.766184 0.470614 -0.195809 -0.860338 +0.0342774 -0.675225 0.736815 0.0340181 -0.776417 0.6293 0.590453 -0.745024 -0.31033 +0.616437 -0.752006 -0.233436 0.611594 -0.637417 -0.468671 0.560949 -0.462072 -0.686895 +0.0524479 -0.927575 0.369937 0.0960032 -0.904353 0.415846 0.201625 -0.650581 0.732183 +0.633862 -0.716803 -0.290539 0.684298 -0.725709 -0.0712944 0.67225 -0.722982 -0.159301 +0.601956 -0.666527 -0.439761 0.473236 -0.845771 -0.246412 0.489987 -0.830226 -0.265778 +0.446783 -0.851373 -0.274862 0.233848 -0.653778 0.719645 0.271149 -0.682581 0.678646 +0.607995 -0.73504 -0.300097 0.627316 -0.747632 -0.217993 0.60014 -0.674259 -0.430357 +0.470235 -0.852479 -0.228383 0.616378 -0.778256 -0.119982 0.717484 -0.694931 -0.0478367 +0.69911 -0.647052 -0.304252 0.408778 -0.700269 0.585256 0.517524 -0.739151 0.431074 +0.584159 -0.749854 0.310608 0.619513 -0.783341 0.0508014 0.641229 -0.767303 -0.00838879 +0.637691 -0.770293 2.84863e-05 0.63899 -0.769123 0.0119048 0.63419 -0.77316 -0.00519378 +0.598186 -0.774633 -0.205224 0.575796 -0.716271 -0.394228 0.534634 -0.831529 -0.150751 +0.427924 -0.880933 0.202085 0.683752 -0.634343 0.360683 0.568828 -0.768363 0.293349 +0.598392 -0.795996 0.0912009 0.628244 -0.777269 -0.0340743 0.680537 -0.732713 -6.22295e-05 +0.696756 -0.717101 0.0172637 0.679947 -0.708556 -0.188734 0.635631 -0.628289 -0.448583 +0.679896 -0.676749 -0.282404 0.669544 -0.663471 -0.333941 0.398563 -0.904402 0.152331 +0.376757 -0.926299 0.00485761 0.375053 -0.926998 -0.00306384 0.374727 -0.927126 0.00404699 +0.368345 -0.929668 0.00635988 0.371482 -0.926518 -0.0597214 0.49745 -0.836931 -0.228233 +0.772606 -0.605512 0.190879 0.78574 -0.61855 -0.0030299 0.786248 -0.617901 -0.00369371 +0.784154 -0.619965 0.0273174 0.775868 -0.628254 -0.057676 0.722127 -0.595889 -0.351354 +0.423869 0.888617 0.175199 0.67016 0.647659 0.362524 0.756873 0.606092 0.244533 +0.7324 0.671665 0.111608 0.365752 0.92864 0.0620793 0.665479 0.708429 -0.235088 +0.420256 0.889734 -0.178206 0.568037 0.653282 -0.500557 0.707107 0.628386 0.324238 +0.698886 0.608265 0.37626 0.648156 0.693852 0.313788 0.799555 0.557984 0.222185 +0.715686 0.641852 0.275354 0.821368 0.484695 0.300708 0.666825 0.702764 -0.247924 +0.577193 0.699125 -0.421987 0.602413 0.669851 -0.434049 0.442099 0.808482 -0.388466 +0.262759 0.571411 -0.777462 0.561013 0.827652 0.0160441 0.545059 0.741689 0.390908 +0.585207 0.810552 0.0231958 0.494957 0.820032 -0.287341 0.527832 0.807357 -0.263756 +0.546476 0.791796 -0.272806 0.65281 0.694773 0.301877 0.548652 0.720807 0.423578 +0.726196 0.676446 0.122723 0.725497 0.640525 -0.251756 0.672432 0.733074 -0.102166 +0.68577 0.567469 -0.455739 0.753927 0.576758 0.314554 0.728492 0.557402 0.398249 +0.629959 0.684839 0.366261 0.640986 0.677334 0.361047 0.736665 0.619046 0.272226 +0.272626 0.524898 -0.806323 0.215289 0.927069 0.306909 0.283393 0.761718 0.582644 +-0.00532326 0.975516 -0.219865 0.311069 0.796192 -0.518955 0.292252 0.915588 0.276202 +0.477052 0.749038 0.459744 0.439223 0.763145 0.474018 0.111226 0.986683 0.11868 +0.308089 0.896154 0.319357 0.410672 0.911182 -0.0331132 0.613055 0.783371 -0.102437 +0.70518 0.601014 -0.376169 0.591806 0.721256 0.359939 0.792841 0.600832 0.101998 +0.249933 0.823673 -0.509016 0.13422 0.984057 -0.116693 0.453134 0.676027 -0.581083 +0.278623 0.888067 -0.365658 0.226471 0.722397 -0.65334 0.00741876 0.940691 0.339184 +-0.00822162 0.790214 0.612776 0.0254187 0.995543 0.0908204 0.256037 0.956206 0.14183 +0.362673 0.891811 0.270447 0.305734 0.851104 -0.426789 0.371976 0.920315 0.121051 +0.556874 -0.177633 -0.81138 0.468673 0.497102 -0.73023 0.285001 0.0743448 -0.95564 +0.00642758 0.630546 0.776125 -0.00102748 0.337853 0.941198 0.157624 -0.289143 -0.94422 +0.115664 -0.583202 -0.80405 0.00660925 0.0656881 0.997818 0.0181606 -0.0313396 -0.999344 +0.0136322 -0.0578029 0.998235 0.0174624 0.0514988 -0.99852 -0.0332794 -0.377195 0.925536 +-0.0420187 -0.080622 -0.995859 0.0186235 -0.574329 0.818413 -0.0158378 -0.416073 0.909193 +-0.209343 -0.479181 -0.852385 0.00637756 -0.406899 0.913451 -0.130352 -0.327212 -0.935917 +-0.31772 -0.595955 -0.73749 0.0166007 -0.582319 0.812791 -0.0028992 -0.39531 0.918543 +-0.00929592 -0.00579858 -0.99994 0.0432422 0.0382632 0.998332 0.00876923 0.0256338 -0.999633 +0.224482 0.501374 0.835603 -0.00862256 -0.00541248 -0.999948 0.317844 0.560035 0.765073 +0.134492 -0.044558 0.989912 -0.157264 -0.321644 -0.933709 0.0467319 -0.604718 0.795067 +0.00354375 -0.390161 0.92074 -0.25903 -0.478166 -0.839203 -0.163206 -0.600979 -0.782424 +-0.380983 -0.574172 -0.724692 -0.00274859 -0.0461739 0.99893 -0.0390188 -0.104597 -0.993749 +-8.47525e-05 0.0234351 0.999725 0.0102955 0.0724015 -0.997322 -0.000163412 0.00355131 0.999994 +0.00195684 0.381159 -0.924507 0.000356466 -0.00225677 0.999997 0.000163628 0.606187 -0.795322 +-0.00022068 0.374721 -0.927137 0.00077468 0.00119557 0.999999 -0.000164695 0.0434074 -0.999057 +-0.00470189 -0.00963218 0.999943 -0.000992023 -0.0210133 -0.999779 0.00576897 0.0133854 0.999894 +0.00246491 -0.00249158 -0.999994 0.208777 0.309764 0.927609 0.014178 0.00886616 -0.99986 +0.153362 0.557871 0.815635 0.281451 -0.166622 0.944999 0.567389 0.169015 0.805918 +-0.0234526 -0.0373715 -0.999026 0.389636 -0.511247 0.766036 0.136332 -0.30064 0.943944 +-0.274016 0.0439632 -0.96072 -0.00565562 -0.0463358 0.99891 -0.0505591 0.014951 -0.998609 +-0.00567096 0.0200831 0.999782 -0.00249586 -0.00364128 -0.99999 -0.0014621 0.00562927 0.999983 +-0.00404983 0.0357574 -0.999352 0.000434658 -0.0022047 0.999997 -0.0205113 0.370726 -0.928516 +-0.000222228 -0.00234518 0.999997 -0.0249186 0.602592 -0.797661 -0.0154359 0.368351 -0.929559 +-0.00301982 -0.00542521 0.999981 0.01391 0.0623316 -0.997959 0.00279482 0.000202521 0.999996 +-0.489793 -0.712708 -0.502146 -0.263872 -0.917403 -0.297899 -0.47074 -0.737887 -0.483659 +-0.0157569 -0.999864 0.00485024 0.0978129 -0.915679 0.389825 0.295764 -0.765283 0.571722 +0.0150283 -0.0128582 -0.999804 0.0246427 0.0340617 0.999116 -0.49605 -0.744707 -0.446482 +-0.389445 -0.829843 -0.399617 -0.507623 -0.712205 -0.484854 -0.413721 -0.81196 -0.411772 +0.0541315 -0.953537 0.296374 -0.0708102 -0.997331 0.0177918 -0.0324602 -0.99924 -0.0215815 +-0.0144121 -0.999871 0.00710116 -0.0167752 -0.999843 0.00574693 -0.0153799 -0.999875 -0.0035589 +-0.00381059 -0.999876 -0.0153054 -0.0108873 -0.999871 -0.0118458 -0.0638944 -0.996053 0.0616167 +-0.272881 -0.843057 0.463455 -0.414773 -0.603571 0.68093 -0.178859 -0.537139 0.824312 +-0.147127 -0.308587 -0.939749 -0.0141669 -0.0403713 0.999084 -0.0452081 -0.825465 -0.56264 +-0.0439355 -0.870777 -0.489712 -0.285331 -0.88134 -0.376598 -0.249104 -0.517904 0.818366 +-0.364969 -0.72158 -0.58832 -0.212141 -0.629661 -0.747344 -0.429236 -0.61037 -0.665737 +-0.267676 -0.485884 0.832026 -0.0547278 -0.815982 -0.575481 -0.0721024 -0.910514 -0.407143 +-0.0607184 -0.913174 -0.403021 -0.211006 -0.805041 0.554423 -0.35408 -0.895716 0.268925 +-0.520266 -0.83377 0.184802 -0.413442 -0.910304 -0.0203138 -0.549183 -0.831724 -0.0814442 +-0.387993 -0.852104 -0.351256 -0.505193 -0.745259 -0.435166 -0.281367 -0.807695 0.518132 +-0.338075 -0.936809 0.0899634 -0.572704 -0.80815 -0.137492 -0.065998 -0.997691 0.0160496 +-0.268316 -0.961206 -0.0639525 -0.473728 -0.826855 -0.303138 -0.0416671 -0.879536 -0.474004 +-0.392088 0.874654 0.285039 -0.176439 0.80694 0.563664 -0.334806 0.94177 0.0312065 +-0.347996 0.937448 -0.00945993 -0.342328 0.938539 -0.0442195 -0.225208 0.875988 -0.426529 +-0.476959 0.383458 -0.790867 -0.0428815 0.888204 0.457444 0.144705 0.791963 0.593174 +0.415051 0.764461 0.493287 -0.00379815 0.99567 0.092879 0.0103604 0.99993 0.00579714 +-0.20145 0.935816 0.289252 -0.73469 0.528204 -0.425712 -0.534063 0.349667 -0.769747 +-0.869512 0.493053 -0.0291272 -0.780727 0.616875 -0.0996529 -0.232797 0.876202 0.42199 +0.0202807 0.506682 0.861894 0.313516 0.739287 0.595955 -0.332163 0.916525 0.222825 +-0.0734275 0.946539 0.314122 -0.636596 0.494142 -0.592089 -0.846238 0.528568 -0.0670608 +-0.760932 0.552437 -0.340288 -0.728839 0.653721 -0.203576 -0.785776 0.578138 0.219799 +-0.750257 0.578312 -0.32042 -0.6761 0.657741 -0.332063 -0.692218 0.713415 -0.108965 +-0.67773 0.692321 -0.247737 -0.353586 0.0646354 0.933166 -0.830919 0.530605 0.167426 +-0.742466 0.644311 -0.183323 -0.720201 0.690552 -0.0666984 -0.630808 0.661775 -0.405136 +-0.556158 0.695239 -0.455336 -0.537658 0.632717 -0.557309 -0.46021 0.0383405 0.886982 +-0.395167 0.843698 -0.363341 -0.383701 0.897562 -0.217155 -0.273699 0.899965 -0.339341 +-0.37515 0.788117 -0.48799 -0.566141 0.18601 0.803047 -0.581367 0.169305 0.795832 +-0.27111 0.803828 -0.529489 -0.209198 0.764522 -0.609707 -0.323597 0.750189 -0.576629 +-0.619584 -0.336745 0.709027 -0.739095 -0.236465 0.630732 -0.279763 0.840395 -0.464187 +-0.401089 0.889501 -0.218895 -0.382444 0.92374 -0.020988 -0.376097 0.925617 0.0422339 +-0.328403 0.927346 0.179389 -0.0683559 0.995614 0.0638718 0.0680279 0.995784 -0.0615365 +0.607322 0.763851 -0.218382 0.332897 0.926147 -0.177291 0.615175 0.733611 -0.288747 +0.465573 0.857541 -0.218781 0.526599 0.721859 -0.449013 -0.584283 -0.519523 0.623466 +-0.515591 -0.539653 0.665538 -0.583484 -0.411981 0.69987 0.405965 0.848092 -0.340489 +-0.0934614 0.836934 -0.539265 -0.301022 0.787164 -0.538293 -0.571099 -0.294849 0.7661 +-0.0216634 0.946344 -0.322434 0.518118 0.686342 -0.51038 0.385613 0.835111 -0.39229 +0.540702 0.7166 -0.440596 0.387132 0.895731 0.218618 0.535068 0.769426 0.348836 +0.534027 0.807894 0.249244 0.532847 0.794115 0.292327 0.277031 0.948958 0.15077 +0.0511792 0.996966 0.0586482 0.053639 0.998219 -0.0261139 0.0721079 0.912515 -0.402638 +0.120081 0.692885 -0.710979 -0.773391 -0.523463 0.357564 -0.694916 -0.5592 0.452092 +-0.806204 -0.590398 0.0382803 -0.903844 -0.427406 0.0197449 -0.824044 -0.566163 -0.0202915 +-0.911887 -0.41013 -0.0159933 -0.82132 -0.570468 -0.000938158 -0.909918 -0.414735 0.0065915 +-0.82309 -0.567866 0.00709022 -0.91188 -0.410355 0.00911583 -0.826313 -0.563155 0.00791937 +-0.89837 -0.438591 -0.0238438 -0.81488 -0.579629 -0.000530898 -0.841735 -0.537696 -0.0486329 +-0.786804 -0.617097 -0.0114587 -0.724812 -0.676504 -0.130345 -0.755626 -0.654992 -0.0037735 +-0.648364 -0.758662 0.0636937 -0.727295 -0.656458 0.200262 -0.595482 -0.734639 0.325125 +-0.656296 -0.650397 0.382438 0.439016 0.848752 -0.294761 0.348023 0.937397 -0.0129397 +0.367433 0.91403 0.17188 0.508827 0.807313 -0.298899 0.327789 0.854695 -0.402556 +0.598503 0.799187 0.0556264 0.368818 0.929402 0.0135668 0.605966 0.794005 -0.0486 +0.353056 0.935601 -0.00164174 0.515228 0.763443 -0.389481 0.403181 0.865087 -0.298445 +0.210178 0.790627 -0.575095 -0.87446 -0.448918 0.183826 -0.93162 -0.363084 -0.0159633 +-0.926538 -0.376148 -0.0062378 -0.921985 -0.387006 0.0130612 -0.925449 -0.377029 -0.0373308 +-0.868786 -0.452045 -0.202156 -0.50988 -0.808523 0.293792 -0.592373 -0.779639 0.20312 +-0.64703 -0.74186 0.176058 -0.739851 -0.672264 0.026117 -0.69991 -0.712686 0.0469443 +-0.744149 -0.657308 0.119119 -0.691442 -0.672286 0.26446 -0.713542 -0.573442 0.40252 +0.258745 0.907064 -0.332094 0.513289 0.701189 -0.494843 0.352031 0.876132 -0.329343 +0.511784 0.693319 -0.507332 -0.699393 -0.606176 0.378682 -0.602869 -0.750207 0.271549 +-0.705346 -0.60764 0.365048 -0.572343 -0.75635 0.316794 -0.61813 -0.696277 0.364848 +0.620916 -0.127108 -0.773503 0.617175 0.366302 -0.696361 0.476413 0.0963557 -0.873926 +-0.424427 -0.875907 0.229453 -0.26927 -0.898438 0.346848 -0.253485 -0.607818 0.752531 +0.549326 -0.27545 -0.788904 0.584534 -0.289145 -0.7581 -0.0260626 -0.644903 0.76382 +-0.0750767 -0.755572 0.650749 0.517476 -0.642263 -0.565435 0.542669 -0.366557 -0.755743 +0.0181738 -0.904804 0.425441 0.133318 -0.868858 0.476772 0.281127 -0.57528 0.768128 +0.583581 -0.761534 -0.281955 0.557272 -0.798989 -0.225974 0.374049 -0.797276 -0.473749 +0.178487 -0.934587 -0.307718 0.342382 -0.850362 -0.399572 0.401764 -0.586477 0.7033 +0.255443 -0.493763 0.831232 0.61361 -0.738223 -0.280194 0.662043 -0.748992 -0.0266549 +0.688527 -0.725182 0.00640577 0.665558 -0.726399 -0.171399 0.613174 -0.649401 -0.449773 +0.53449 -0.714411 -0.451595 0.314181 -0.831315 -0.458482 0.362856 -0.58871 0.722327 +0.177012 -0.735508 0.653983 0.432869 -0.670907 0.602086 0.765431 -0.633836 -0.111207 +0.809469 -0.560116 0.176151 0.668992 -0.678185 -0.304163 0.678943 -0.695844 -0.234172 +0.646447 -0.617992 -0.447427 0.700286 -0.677704 -0.224312 0.720879 -0.693025 -0.00697348 +0.567272 -0.800578 -0.193074 0.43378 -0.843944 -0.315584 0.308116 -0.858678 0.409557 +0.399925 -0.893712 0.203317 0.402271 -0.915446 0.0117314 0.377282 -0.92601 -0.0128379 +0.367736 -0.92993 0.000701217 0.363068 -0.931762 -0.000925338 0.375638 -0.926764 -0.00229837 +0.448707 -0.893406 -0.02207 0.670183 -0.715782 0.19624 0.764632 -0.257831 0.590645 +0.926159 -0.24688 0.285097 0.929792 -0.318303 0.18485 0.924199 -0.381286 0.0218509 +0.925999 -0.377316 -0.0125554 0.923379 -0.383442 0.0185477 0.921731 -0.386557 -0.0314029 +0.849759 -0.387499 -0.357427 0.742512 -0.44876 -0.497283 0.753526 -0.573744 -0.320963 +0.728332 -0.587013 -0.353481 0.874921 -0.252038 0.413509 0.72975 -0.360172 0.581155 +0.940084 -0.333619 0.0702902 0.914736 -0.403845 -0.0129233 0.906213 -0.42239 -0.0191184 +0.908277 -0.418286 0.00835068 0.912312 -0.409359 0.0106 0.889459 -0.381217 -0.252064 +0.775794 -0.359939 -0.518254 0.802512 -0.565812 0.189291 0.795523 -0.605169 -0.0302214 +0.804475 -0.593914 -0.00921363 0.829103 -0.559076 -0.00473366 0.825746 -0.563247 0.0299547 +0.817705 -0.572979 -0.0552603 0.752428 -0.520571 -0.403556 0.796128 0.534581 0.283555 +0.740586 0.562456 0.367662 0.803405 0.545586 0.238485 0.749449 0.631066 0.200206 +0.805167 0.58292 0.109132 0.631762 0.714209 0.301302 0.669203 0.737958 0.087091 +0.443698 0.884408 -0.14476 0.0869131 0.996204 0.00482923 0.0680916 0.994751 -0.0763775 +0.443403 0.693324 -0.568063 0.261334 0.868717 -0.420756 0.123412 0.769752 -0.626299 +0.607062 0.794649 -0.00279095 0.701074 0.60893 0.37108 0.664884 0.739323 0.106449 +0.698535 0.674859 -0.237937 0.745767 0.584849 0.319034 0.735008 0.578035 0.354456 +0.644562 0.699111 0.30949 0.794051 0.591027 -0.142018 0.774381 0.628462 0.0732758 +0.897237 0.441017 -0.0216972 0.811721 0.58283 -0.0376699 0.849535 0.521154 0.0817879 +0.812751 0.582606 -0.00252884 0.808661 0.530781 0.253652 0.785939 0.592581 0.176489 +0.806787 0.526002 0.269102 0.233594 0.754562 -0.613246 0.132421 0.620316 -0.773093 +0.643944 0.495772 0.582706 0.539314 0.629123 0.559772 0.540667 0.638063 0.548229 +0.534445 0.810297 -0.24039 0.614845 0.752713 0.23535 0.667723 0.742264 -0.0564772 +0.866672 0.459831 -0.193482 0.890229 0.416816 0.183731 0.345455 0.700342 -0.624645 +0.299448 0.619164 -0.725924 0.191923 0.762599 -0.617744 0.274471 0.890381 0.363162 +0.367209 0.811393 0.454752 0.556905 0.794692 0.241496 0.519533 0.75052 0.408419 +0.293998 0.943259 0.15436 -0.0109339 0.958742 -0.284068 0.186893 0.791904 -0.581342 +-0.635256 0.768589 -0.0756303 -0.15409 0.81399 0.560068 0.178079 0.972466 0.150325 +0.533091 0.838661 -0.111632 0.556188 0.780573 0.285239 0.28786 0.949893 0.121821 +0.0957205 0.991978 0.0825609 0.271068 0.917191 0.292031 0.133683 0.942768 -0.30548 +0.0226984 0.926179 -0.376401 -0.269349 0.597307 -0.75543 0.0206043 0.929821 0.367434 +0.0140472 0.794462 0.607152 0.133847 0.942214 0.307113 -0.0472994 0.995946 -0.0765127 +-0.0819662 0.567545 -0.819252 -0.241582 -0.188472 -0.951902 0.0220843 0.630718 0.775697 +0.011696 0.338248 0.940984 -0.10773 -0.333429 -0.9366 -0.380934 -0.516552 -0.766853 +0.0140007 0.0708758 0.997387 0.00880928 -0.00833658 -0.999926 -0.0395213 -0.103324 0.993862 +-0.00127924 0.00334528 -0.999994 -0.267785 -0.474879 0.83832 -0.167847 -0.325857 -0.9304 +-0.174099 -0.574708 0.799625 -0.165725 -0.344652 0.923986 -0.266946 -0.469172 -0.841794 +-0.178318 -0.601008 -0.779097 -0.389767 -0.574472 -0.719766 -0.00953365 -0.368264 0.929672 +-0.0441293 -0.0839354 -0.995494 0.00638932 -0.597284 0.802004 0.0106599 -0.367902 0.929804 +0.00823581 0.030606 -0.999498 0.149759 0.288687 0.945638 0.0097323 0.0279053 -0.999563 +0.161983 0.616549 0.770473 0.252634 0.4167 0.873234 0.368547 0.600183 0.709897 +0.00741545 -0.0329839 -0.999428 0.0275692 -0.306158 0.951581 -0.0397597 -0.385336 -0.921919 +-0.0087978 -0.600421 0.799635 -0.00904986 -0.382862 0.923761 -0.0505327 -0.391747 -0.918684 +-0.0766404 -0.611677 -0.787387 -9.19753e-05 -0.0401689 0.999193 0.009529 -0.051016 -0.998652 +0.000427627 0.0231982 0.999731 0.00777413 0.0640328 -0.997917 4.35722e-05 0.00365924 0.999993 +6.25286e-05 0.376148 -0.92656 0.000849672 -0.00117776 0.999999 -0.000480992 0.605836 -0.795589 +-0.000107395 0.375116 -0.926978 0.000445112 -0.00147874 0.999999 -0.000323829 0.0427891 -0.999084 +-0.00911863 -0.021681 0.999723 -0.00049057 -0.0206363 -0.999787 -0.0141357 0.0348915 0.999291 +0.00279371 0.00498101 -0.999984 0.0273843 0.391691 0.919689 -0.00864224 -0.0165155 -0.999826 +0.0565933 0.578752 0.813538 0.0288436 -0.00975189 0.999536 -0.0443198 -0.280499 -0.958831 +0.0522829 -0.610529 0.790266 0.0373683 -0.38283 0.923063 0.0032163 -0.002013 -0.999993 +0.276323 -0.52356 -0.805935 -0.5173 -0.292471 -0.804277 0.0061625 -0.0587606 0.998253 +-0.264497 0.522129 -0.810816 0.0573895 0.279654 -0.958384 -0.00525988 0.0235585 0.999709 +0.00938195 -0.00484 -0.999944 -0.00399739 0.00811559 0.999959 -0.0139851 0.0265852 -0.999549 +-0.000997334 -0.000851374 0.999999 0.0509325 0.377719 -0.924518 -0.00194219 -0.00490669 0.999986 +0.00657604 0.608056 -0.793867 0.0545929 0.398912 -0.915363 0.00286602 -0.000657031 0.999996 +0.00371214 0.0579432 -0.998313 0.0209369 0.0206256 0.999568 -0.489899 -0.722647 -0.487627 +-0.282343 -0.913649 -0.292452 -0.479717 -0.730743 -0.485681 -0.0804844 -0.996654 -0.0142766 +0.162976 -0.946469 0.278631 0.142891 -0.772973 0.618139 0.419804 -0.736127 0.530925 +-0.0720744 -0.0863324 -0.993656 -0.0163494 -0.0008144 0.999866 -0.278575 -0.745274 -0.605775 +-0.273286 -0.822775 -0.498354 -0.407304 -0.771146 -0.489323 -0.40998 -0.796151 -0.44504 +0.278758 -0.863145 0.421039 -0.307782 -0.944322 -0.1163 -0.332024 -0.942532 -0.0373461 +-0.333002 -0.942926 -0.000204532 -0.329534 -0.944131 0.00491404 -0.335111 -0.942165 0.00515536 +-0.342285 -0.939588 -0.00383268 -0.351338 -0.936248 0.00098568 -0.290756 -0.951758 0.0980711 +-0.012545 -0.998368 0.055716 -0.0314417 -0.89885 0.437126 -0.18373 -0.672854 0.716597 +-0.217358 -0.420568 -0.88084 -0.183956 -0.318922 0.929757 -0.13757 -0.831942 -0.537538 +-0.286153 -0.813613 -0.506113 -0.438925 -0.677917 0.589723 -0.423147 -0.682309 0.596155 +-0.550392 -0.715848 0.429687 -0.288869 -0.752507 -0.591852 -0.269877 -0.625372 -0.732173 +-0.47171 -0.691276 0.547382 -0.410226 -0.702641 0.581387 -0.506834 -0.711497 0.486714 +-0.22428 -0.791459 -0.568587 -0.322243 -0.86525 -0.384061 -0.287224 -0.90296 -0.319633 +-0.440538 -0.850997 0.285884 -0.409155 -0.911923 0.0314386 -0.378654 -0.892498 -0.245088 +-0.351217 -0.858913 0.372713 -0.0342984 -0.998481 0.043127 -0.0250749 -0.999597 -0.0133278 +-0.227661 -0.888538 -0.398335 -0.541473 0.748522 0.382782 -0.638852 0.766665 0.0639728 +-0.625146 0.780252 -0.0200103 -0.737222 0.664326 -0.123187 -0.722283 0.59191 -0.357699 +-0.325586 0.736626 0.592769 -0.55119 0.697625 0.457721 -0.507286 0.836479 0.207279 +-0.40854 0.91257 -0.0176416 -0.44356 0.895974 0.022052 -0.678708 0.721173 0.138796 +-0.780971 0.624368 0.0157498 -0.776047 0.626997 -0.0680105 -0.733167 0.606771 -0.307076 +-0.71875 0.393358 -0.573295 -0.727699 0.505657 -0.463427 -0.691895 0.372422 0.618533 +-0.728663 0.542426 0.41812 -0.631849 0.671683 0.386793 -0.546658 0.797587 0.254991 +-0.61008 0.654665 0.446337 -0.649734 0.419106 -0.634189 -0.739993 0.25367 -0.622947 +-0.705972 0.429826 -0.562897 -0.891353 0.449334 0.0599086 -0.726297 0.124065 0.676092 +-0.760978 0.435983 0.480448 -0.741615 0.291688 0.60409 -0.822764 0.27511 -0.497368 +-0.739201 0.328303 -0.588047 -0.9483 0.304724 -0.0887174 -0.99665 0.0746548 0.0334005 +-0.925423 0.355241 -0.131894 -0.815175 0.560432 0.146305 -0.684034 0.717135 -0.133478 +-0.647471 -0.0168907 0.761903 -0.765495 0.0702139 0.6396 -0.725523 0.387336 -0.568847 +-0.928009 0.154263 -0.33912 -0.775354 0.0903084 -0.625036 -0.998389 0.0385246 0.0416616 +-0.93576 0.180771 0.30278 -0.67229 0.714938 0.192066 -0.595755 0.465931 0.654205 +-0.566105 0.798102 -0.206296 -0.475342 0.831865 -0.286444 -0.545008 0.764368 -0.344541 +-0.557572 0.779909 -0.284351 -0.52693 0.814525 -0.242683 -0.78952 -0.211434 0.576154 +-0.776517 -0.207278 0.595027 -0.925504 -0.102637 -0.364565 -0.718222 -0.291868 -0.631641 +-0.955598 0.255607 0.146622 -0.899169 0.323997 0.294143 -0.628522 0.228218 0.743557 +-0.511732 0.825194 -0.239134 -0.489789 0.8174 -0.303255 -0.315688 0.845776 -0.430121 +-0.379503 0.720688 -0.580161 -0.837378 -0.292908 0.461522 -0.767042 -0.536576 0.351756 +-0.935583 -0.318694 0.152048 -0.791985 -0.609229 0.0399955 -0.936451 -0.350562 -0.012879 +-0.802654 -0.596269 -0.0145056 -0.937695 -0.347435 -0.00414352 -0.801321 -0.598233 0.00114289 +-0.937709 -0.347413 0.00260158 -0.799162 -0.601112 0.00208775 -0.937378 -0.348312 0.000415865 +-0.798511 -0.601979 0.000634746 -0.937299 -0.348525 0.000908408 -0.798708 -0.601718 0.000826186 +-0.936864 -0.34968 -0.00295918 -0.798296 -0.602182 -0.0100287 -0.935985 -0.351705 -0.015343 +-0.796113 -0.605132 0.00453888 -0.931055 -0.36226 0.0436503 -0.776043 -0.599269 0.196552 +-0.83595 -0.479842 0.266343 -0.75967 -0.552752 0.342588 -0.728321 -0.595284 -0.339389 +-0.729813 -0.459432 -0.506256 -0.901815 -0.309854 -0.301197 -0.932235 0.125009 0.339574 +-0.800035 0.569673 0.188195 -0.771594 0.374711 0.514038 -0.779579 0.504542 0.371071 +-0.457061 0.697694 -0.551651 -0.513193 0.725377 -0.45876 -0.568666 0.76117 -0.311832 +-0.553924 0.830324 -0.0610719 -0.569628 0.821706 0.0179665 -0.586891 0.794556 0.155691 +-0.556781 0.709672 0.431695 -0.384581 0.844877 0.37186 -0.0859935 0.993897 0.0690903 +0.0119608 0.999772 -0.0177061 0.0834332 0.99499 -0.0550804 0.270988 0.695689 -0.665268 +0.288816 0.835138 -0.468114 0.216976 0.575875 -0.788219 -0.928036 -0.188682 0.321167 +-0.996668 -0.0808203 -0.0110185 -0.996707 -0.0803163 -0.0111199 -0.996556 -0.082841 0.0038158 +-0.996625 -0.0820868 0.000937934 -0.996673 -0.0815066 0.000662762 -0.996893 -0.0787678 0.000764707 +-0.997257 -0.0735198 -0.00860428 -0.997899 -0.0643931 -0.00718746 -0.995514 -0.0797671 0.0508834 +-0.770913 -0.618704 0.151322 -0.961162 -0.272887 -0.0412396 -0.999584 -0.0278954 -0.00738949 +-0.931643 -0.159778 0.326361 -0.773238 -0.0719306 0.630023 -0.397447 0.90712 -0.138457 +-0.379278 0.924991 -0.0232537 -0.399228 0.916388 0.0291526 -0.454704 0.844005 0.28443 +0.125839 0.599588 -0.790354 -0.309171 0.665083 -0.679763 -0.865523 -0.401621 0.299283 +-0.942547 -0.334069 -0.00144515 -0.941769 -0.335883 -0.0159206 -0.939988 -0.341196 0.00292241 +-0.940934 -0.338573 0.00340476 -0.942616 -0.333855 0.00399659 -0.944277 -0.329108 0.00540154 +-0.946323 -0.323191 -0.00456383 -0.944918 -0.326262 0.0261362 -0.946402 -0.279425 0.162002 +-0.997046 -0.0607933 0.0469453 -0.998792 0.00475201 -0.0489094 -0.997185 -0.0570314 0.048677 +-0.696886 -0.453268 0.555786 -0.843233 -0.38097 0.379235 -0.688166 -0.529152 0.496414 +-0.0621555 0.8639 -0.499813 0.335467 0.939502 -0.0692658 0.331904 0.931608 -0.148143 +0.568903 0.80437 -0.171286 0.372626 0.905327 -0.203795 0.566088 0.819028 0.0934724 +0.409234 0.912401 0.0071305 0.557296 0.776612 0.293761 0.30953 0.941701 0.131873 +0.0694713 0.997414 0.0184388 0.0289442 0.999385 0.0198026 -0.01311 0.999365 0.0331267 +-0.0426244 0.997755 -0.0516606 -0.02679 0.918688 -0.394074 -0.0785885 0.797136 -0.598664 +-0.820419 -0.53356 0.20549 -0.928474 -0.371116 0.0144507 -0.952461 -0.304141 -0.017799 +-0.914082 -0.360781 0.185177 -0.689651 -0.517246 0.506791 -0.812714 -0.425212 0.398359 +-0.692213 -0.508478 0.512144 0.0518163 0.920426 -0.387468 0.0568067 0.998199 -0.0192708 +0.0515117 0.998664 0.00398733 0.185589 0.932415 -0.310094 0.385217 0.757038 -0.527732 +-0.817282 -0.48253 0.314984 -0.686548 -0.521167 0.506989 -0.763288 -0.484172 0.427748 +-0.712556 -0.518287 0.472909 0.291123 0.868478 -0.401239 0.327253 0.920999 -0.211342 +0.449399 0.553342 -0.701322 0.290873 0.842027 -0.454294 0.236695 0.51935 -0.821128 +-0.710321 -0.623959 0.325759 -0.629481 -0.747053 0.213694 -0.748468 -0.619237 0.237364 +-0.59189 -0.696267 0.406052 -0.609732 -0.477056 0.632964 0.405777 0.307294 -0.860765 +-0.404065 -0.340134 0.849141 0.60754 -0.240771 -0.756918 0.568954 -0.158593 -0.806932 +-0.341032 -0.601806 0.722169 -0.331137 -0.688164 0.645584 0.494316 -0.501184 -0.710258 +0.18335 -0.518219 -0.835363 0.00563679 -0.819993 0.572345 -0.0111832 -0.600338 0.799668 +0.325592 -0.691806 0.644511 0.0648281 -0.800904 -0.595274 -0.0330884 -0.927689 -0.371884 +-0.0252597 -0.989177 -0.144538 -0.0722843 -0.888022 -0.454083 0.321264 -0.615947 0.719305 +0.134601 -0.617709 0.774802 0.465335 -0.715344 -0.521294 0.321566 -0.700872 -0.63669 +0.148349 -0.745622 -0.649647 0.0899374 -0.643484 0.760158 0.01109 -0.73372 0.679361 +0.692978 -0.666234 -0.275525 0.690264 -0.723239 -0.0214923 0.606763 -0.778662 -0.159765 +0.355392 -0.798337 -0.486163 0.167566 -0.918439 -0.358317 0.203932 -0.872916 -0.443205 +0.206859 -0.805496 0.555325 0.198398 -0.769043 0.607627 0.507292 -0.784927 0.355731 +0.596485 -0.800749 0.0548349 0.604085 -0.796477 -0.0265579 0.606141 -0.795277 -0.0112832 +0.600407 -0.79969 -0.00263507 0.594676 -0.803935 0.00700892 0.690474 -0.710179 0.137443 +0.765608 -0.566287 0.305227 0.873414 -0.314481 -0.37182 0.837577 -0.380004 -0.392506 +0.734236 -0.462721 -0.496777 0.742499 -0.602034 -0.293684 0.559209 -0.785897 -0.263916 +0.289138 -0.853939 -0.43265 0.450235 -0.84717 0.28212 0.663075 -0.739651 0.115098 +0.764227 -0.644791 -0.0141931 0.763762 -0.645497 -0.00130068 0.773922 -0.633165 -0.0121186 +0.765027 -0.642112 0.0492524 0.743871 -0.55456 0.372987 0.81128 -0.123573 0.57145 +0.799549 -0.299278 0.520724 0.85485 -0.382373 -0.350745 0.863176 -0.377534 -0.335255 +0.75155 -0.35863 -0.553676 0.935027 0.151489 0.320588 0.800321 0.370071 0.471734 +0.933735 -0.267523 0.237846 0.943452 -0.33148 0.00432824 0.916475 -0.40007 0.00414579 +0.992691 -0.0437787 0.112461 0.999071 0.0229365 0.0364916 0.996049 0.0623539 -0.0632358 +0.919117 -0.0235291 -0.393282 0.789713 -0.255611 -0.557688 0.780159 -0.546087 -0.305189 +0.753378 -0.524457 -0.396694 0.786051 0.542816 -0.295762 0.760366 0.360325 0.540379 +0.774682 0.625992 0.0894488 0.611962 0.723669 -0.31907 0.803352 0.550899 0.226133 +0.682847 0.622551 0.382298 0.866532 0.493512 0.0746175 0.865436 0.493421 0.0869327 +0.810773 0.583264 0.0495008 0.817294 0.528446 0.229727 0.804476 0.568743 0.171316 +0.872747 0.487241 0.030136 0.936159 0.351399 0.0111404 0.954177 0.298981 0.0124924 +0.931041 0.308909 0.194264 0.527475 0.776827 -0.343961 0.28952 0.946429 -0.143003 +0.0316901 0.929229 -0.368142 0.000440951 0.752313 -0.658806 0.695993 0.386991 0.60484 +0.650955 0.313174 0.691505 0.610836 0.791699 0.00959836 0.804698 0.57673 -0.140867 +0.867392 0.460103 0.189567 0.00984529 0.689123 -0.724577 -0.00109618 0.703764 -0.710433 +0.677719 0.432268 0.594846 0.577959 0.628363 0.520695 0.5477 0.560993 0.620735 +0.0844863 0.690147 -0.718721 -0.0294961 0.754888 -0.655191 -0.0291152 0.92047 0.389728 +0.0710283 0.815341 0.574608 0.0430076 0.998138 0.0432637 0.0312741 0.998734 -0.0393905 +0.595394 0.777577 0.202188 0.279958 0.956888 -0.0773922 0.0671337 0.996681 0.0460426 +0.0935698 0.994776 0.0408053 0.283364 0.920655 0.268513 -0.00116052 0.893971 -0.448124 +-0.192391 0.861091 -0.470647 -0.477058 0.349394 -0.806436 -0.0651964 0.911142 0.406903 +-0.0304166 0.799316 0.60014 -0.0128928 0.910064 0.414268 -0.546325 0.118072 -0.829209 +-0.456444 0.0515429 -0.888258 -0.0841162 0.658631 0.74775 0.00153166 0.337234 0.941419 +-0.0581346 -0.0259131 -0.997972 -0.00473405 0.0435702 0.999039 0.0112855 0.0462555 -0.998866 +-0.114455 -0.35199 0.928979 0.00673603 -0.0427861 -0.999062 -0.390805 -0.559074 0.731237 +-0.306972 -0.602368 0.736832 -0.197052 -0.453782 0.869052 -0.0286002 -0.386673 -0.921773 +-0.0338043 -0.110694 0.993279 -0.0403359 -0.379701 -0.924229 -0.00165806 -0.605707 -0.795686 +0.010053 -0.346937 0.937834 0.00765955 -0.0402094 -0.999162 -0.00141285 -0.608344 0.793673 +-0.00741112 -0.34598 0.938213 0.00471194 0.0301493 -0.999534 0.0378237 0.334652 0.941583 +0.0090983 0.0329991 -0.999414 0.07749 0.623983 0.777586 0.0471659 0.334393 0.941253 +-0.0372784 -0.0859681 -0.9956 -0.0046062 -0.335405 0.942063 -0.198779 -0.478788 -0.855131 +-0.00402987 -0.61635 0.787462 0.000287751 -0.376544 0.926399 -0.12207 -0.328317 -0.936647 +-0.308735 -0.596056 -0.741215 0.00148014 -0.0389234 0.999241 -0.00603133 -0.0224719 -0.999729 +0.000294682 0.0223033 0.999751 0.00382286 0.0552769 -0.998464 0.000243157 0.00410056 0.999992 +-0.00056645 0.373614 -0.927584 0.00132906 9.74511e-06 0.999999 -0.000307238 0.606183 -0.795325 +-0.000128218 0.375218 -0.926937 -0.00124088 -0.00740255 0.999972 -0.000309385 0.0421994 -0.999109 +-0.00975909 -0.0320308 0.999439 -0.000234596 -0.0196476 -0.999807 0.0355319 0.0857759 0.995681 +-0.00102854 0.00605352 -0.999981 0.195527 0.488419 0.850421 0.0127165 -0.00340664 -0.999913 +0.304788 0.54529 0.780873 0.237401 -0.0260219 0.971063 0.0313637 -0.0330859 -0.99896 +0.151068 -0.575041 0.804056 0.208961 -0.403202 0.890934 0.523947 0.28887 -0.80127 +0.274555 -0.0355934 -0.960912 0.0374819 -0.0489018 0.9981 0.0334076 0.026819 -0.999082 +0.00872989 0.00245747 0.999959 -0.0174149 -0.0440546 -0.998877 -0.00536752 0.012045 0.999913 +0.0375318 0.0792504 -0.996148 -0.00206406 0.000448878 0.999998 0.263649 0.495795 -0.827452 +0.00268086 -0.000569086 0.999996 0.202273 0.603848 -0.771008 0.0830708 0.284103 -0.955188 +0.0174435 0.0152858 0.999731 -0.334678 -0.138929 -0.932035 -0.0314235 -0.0226809 0.999249 +-0.449861 -0.719376 -0.529267 -0.327859 -0.890683 -0.314947 -0.46443 -0.747159 -0.475455 +-0.15288 -0.980732 -0.121625 -0.283054 -0.951196 -0.122912 -0.396266 -0.82397 0.405026 +-0.432616 -0.768089 0.472104 -0.591655 -0.691305 -0.414779 -0.681195 -0.665864 -0.3043 +-0.695966 -0.695006 -0.180551 -0.720799 -0.691295 -0.0506033 -0.669925 -0.742429 9.29614e-05 +-0.741333 -0.670647 0.0256369 -0.659023 -0.752018 0.0125247 -0.736109 -0.676804 0.00893254 +-0.658691 -0.752411 0.00221532 -0.734818 -0.678261 -0.00190399 -0.662722 -0.748855 -0.00407666 +-0.737877 -0.674677 -0.0186426 -0.668131 -0.743679 -0.0232848 -0.738408 -0.674345 0.00356683 +-0.661042 -0.748059 0.0585863 -0.715988 -0.63146 0.297691 -0.624258 -0.711619 0.322336 +-0.586881 -0.635148 0.502154 -0.437107 -0.290989 -0.851037 -0.26081 -0.177857 0.948865 +-0.334071 -0.737115 -0.587416 -0.396451 -0.744909 -0.536598 -0.318978 -0.938605 0.131431 +-0.369643 -0.86543 0.338224 -0.149542 -0.918864 0.365139 -0.301335 -0.755478 0.581765 +-0.641005 -0.560761 -0.524079 -0.641879 -0.449394 -0.621318 -0.470124 -0.580646 -0.664706 +-0.426929 -0.327429 0.842925 -0.643158 -0.59008 -0.488009 -0.471884 -0.822324 -0.317974 +-0.590771 -0.739139 -0.323516 -0.524056 -0.806009 -0.275163 -0.503407 -0.776411 -0.379168 +-0.472277 -0.818355 -0.327488 -0.283418 -0.81563 0.504403 -0.397075 -0.885728 0.240454 +-0.564885 -0.810845 0.153084 -0.484503 -0.874733 0.0100086 -0.566133 -0.821653 -0.0661733 +-0.545447 -0.769356 -0.332534 -0.528799 -0.733936 -0.426275 -0.316187 -0.740091 0.593541 +-0.318499 -0.815756 0.482805 -0.386334 -0.734682 0.557664 -0.204291 -0.77094 0.603254 +-0.548479 -0.607174 -0.574901 -0.609368 -0.500115 -0.615269 -0.548086 -0.618322 -0.563276 +-0.195111 -0.920686 0.338037 -0.297159 -0.917982 -0.262689 -0.905777 0.375669 0.196061 +-0.912051 0.408977 0.0300035 -0.899846 0.435981 -0.0141007 -0.918149 0.396235 -0.000118982 +-0.938934 0.344076 0.00376351 -0.941419 0.337057 -0.0110622 -0.884093 0.427755 -0.188162 +-0.898124 0.379532 0.222101 -0.946246 0.287579 0.148042 -0.99927 0.0314366 0.0216887 +-0.999546 0.0293236 0.00694861 -0.999736 0.0229436 -0.00125543 -0.99966 0.025516 -0.00543176 +-0.999547 0.0297711 -0.00440079 -0.999399 0.0307079 0.0161066 -0.994544 0.0811317 -0.0655753 +-0.854543 0.286082 -0.433489 -0.91563 0.168118 0.365181 -0.995948 0.0720718 0.053791 +-0.998451 0.0556222 0.00120077 -0.999894 0.0143789 -0.00214755 -0.999971 -0.00150227 -0.00743027 +-0.999996 -0.00278264 -0.000661259 -0.999998 -0.00150455 -0.00137476 -0.999988 -7.07379e-05 -0.00498465 +-0.999904 -0.00752838 0.0116311 -0.999998 0.000327327 -0.00214152 -0.932342 0.152812 -0.327699 +-0.945174 -0.0631747 0.320398 -0.997759 -0.0463334 0.0482709 -0.998202 -0.0579649 -0.0152386 +-0.998392 -0.0560881 -0.00818806 -0.9988 -0.0487955 -0.004165 -0.998966 -0.0454631 0.000134233 +-0.999021 -0.0442402 -0.000439305 -0.99907 -0.0426498 -0.00630883 -0.999177 -0.0397207 0.00821532 +-0.998929 -0.0206343 0.0414221 -0.99458 0.0426175 -0.0948421 -0.850163 0.247923 -0.464496 +-0.605094 0.660207 0.444958 -0.655793 0.754282 0.0315283 -0.603109 0.774254 -0.191808 +-0.864165 -0.435506 0.252097 -0.924695 -0.380345 0.0166478 -0.925405 -0.378801 -0.0116187 +-0.924177 -0.381965 0.00027825 -0.924784 -0.380489 0.0012691 -0.925713 -0.378226 0.00109587 +-0.926341 -0.376684 -0.00078342 -0.926778 -0.375467 -0.0103403 -0.923168 -0.382799 0.0350064 +-0.931107 -0.328589 0.158335 -0.993988 -0.109049 -0.00982786 -0.907655 -0.137257 -0.396639 +-0.7881 0.558667 0.258437 -0.583224 0.215318 0.783254 -0.755536 0.65331 -0.0484813 +-0.623418 0.7545 -0.205133 -0.558982 0.78494 -0.267223 -0.700374 0.633445 -0.328972 +-0.622443 0.722765 -0.300291 -0.612328 0.703327 -0.361089 -0.88299 -0.462557 -0.0798082 +-0.910919 0.235937 0.338467 -0.915313 0.250767 0.315149 -0.711851 -0.183906 0.677825 +-0.848522 -0.203527 0.488454 -0.746117 -0.32064 0.583523 -0.732769 0.627552 -0.263111 +-0.635623 0.646429 -0.422035 -0.677345 0.577589 -0.455626 -0.501352 0.659779 -0.559766 +-0.505932 0.692664 0.514051 -0.517533 0.683495 0.514775 -0.352289 0.856016 0.378325 +-0.0459774 0.995592 0.0817438 0.0256458 0.999536 0.0164362 0.424549 0.86735 -0.259736 +0.476595 0.588578 -0.653018 -0.858778 -0.283088 0.427038 -0.994405 -0.013186 0.10481 +-0.943339 -0.0115828 0.331629 -0.783822 -0.0940056 0.613829 -0.589403 0.745016 -0.312339 +-0.594554 0.803404 -0.0323695 -0.467743 0.883738 -0.0149426 -0.427595 0.903951 -0.00594263 +-0.397568 0.892638 0.212456 -0.392619 0.828121 0.400083 0.56328 0.778162 -0.277812 +0.626495 0.649797 -0.430428 0.669747 0.741388 -0.0422163 0.726957 0.686446 -0.0180375 +0.623915 0.723621 -0.295131 0.735568 0.65749 -0.163237 0.697592 0.611547 -0.37333 +-0.845774 -0.381191 0.373309 -0.92037 -0.192873 0.340175 -0.760313 -0.103418 0.641272 +0.514081 0.817569 -0.259425 0.457835 0.887386 -0.0541561 0.398418 0.917199 0.0028728 +0.289328 0.951269 -0.10666 0.0586227 0.997516 -0.0390608 0.00613952 0.999926 0.0104822 +-0.0234814 0.998924 -0.0399954 -0.174227 0.889261 -0.422918 -0.296027 0.781741 -0.548862 +-0.834967 -0.39979 0.378152 -0.662872 -0.45628 0.593641 -0.801331 -0.471472 0.368215 +-0.571769 -0.644665 0.507432 -0.122639 0.950567 -0.285276 -0.00930158 0.999588 -0.0271605 +-0.0128486 0.999284 0.035579 0.0414424 0.998731 -0.0286262 0.569635 0.788605 -0.231554 +0.317053 0.923434 -0.21621 0.532048 0.770288 -0.35154 0.43828 0.854191 -0.279764 +0.214619 0.787894 -0.577201 -0.843515 -0.474614 0.251444 -0.540725 -0.747732 0.385374 +-0.8213 -0.49399 0.285378 -0.537569 -0.742702 0.399266 -0.768005 -0.3256 0.551501 +-0.347244 -0.537691 0.768317 0.242605 0.780315 -0.576412 0.127206 0.649856 -0.749337 +-0.732704 -0.151485 0.663473 -0.5722 -0.217155 0.790842 0.550384 0.131733 -0.824454 +0.343046 0.573949 -0.743573 0.183682 0.3427 -0.921313 -0.604831 -0.361284 0.709686 +-0.69958 -0.281035 0.656969 0.320081 -0.547243 -0.773352 0.13121 -0.333815 -0.933462 +-0.568895 -0.472052 0.673443 -0.378429 -0.622458 0.685082 -0.409536 -0.395836 0.821945 +-0.499393 -0.757644 -0.420216 -0.324765 -0.808506 -0.490761 -0.365999 -0.674763 -0.640889 +-0.126726 -0.813126 -0.568125 -0.310366 -0.457214 0.833444 -0.0263709 -0.763202 -0.645621 +-0.0967702 -0.876056 -0.4724 -0.307899 -0.886652 -0.34503 -0.071669 -0.638355 0.766399 +-0.17785 -0.583852 0.79214 0.0485559 -0.763564 -0.643904 -0.0286893 -0.666506 -0.744947 +-0.0107569 -0.674262 0.738414 -0.0295778 -0.706397 0.707197 0.0585432 -0.810396 -0.58295 +-0.0145728 -0.914418 -0.404509 -0.0283694 -0.924919 -0.379105 0.0338129 -0.706983 0.706421 +0.0237243 -0.721878 0.691614 0.810781 -0.376784 -0.44796 0.748559 -0.531929 -0.395867 +0.44199 -0.559403 -0.701222 0.140765 -0.740621 -0.657012 0.257851 -0.805825 0.533065 +0.181069 -0.729888 0.659149 0.65352 -0.588629 0.475844 0.90327 -0.427008 0.042039 +0.857946 -0.511824 -0.0443285 0.952318 -0.304465 0.0197856 0.961042 -0.276006 0.0147873 +0.893445 -0.364288 0.262772 0.771689 -0.415033 -0.481916 0.615797 -0.47961 -0.625115 +0.717234 -0.486857 -0.498544 0.382656 -0.55366 0.739619 0.338454 0.0414504 0.940069 +0.95619 0.00713142 0.29266 0.761035 -0.592014 0.265225 0.804887 -0.111623 -0.582836 +0.774633 0.345938 0.529406 0.864862 0.254648 0.432628 0.953162 0.297916 0.0522284 +0.936089 0.171759 0.306978 0.88387 0.238135 -0.402574 0.923119 -0.0034563 -0.384498 +0.803915 -0.203355 -0.558898 0.740776 -0.431706 -0.514667 0.215432 0.558226 0.801232 +0.450233 0.387833 0.804286 0.547159 0.406801 0.731526 0.628576 0.73878 0.243098 +0.732676 0.603956 -0.313724 0.482575 0.684141 -0.546875 0.232594 0.469673 -0.85165 +0.677082 0.320961 0.662227 0.541759 0.409506 0.734032 -0.0243058 0.688282 -0.725036 +-0.00332188 0.724476 -0.689292 0.617961 0.488671 0.615893 0.57245 0.638448 0.514475 +0.569774 0.562475 0.599149 -0.0181805 0.682393 -0.73076 -0.156309 0.590065 -0.79208 +-0.284352 0.854167 0.435364 -0.185372 0.795324 0.577146 -0.102904 0.991844 0.0752082 +-0.310546 0.949932 0.0345122 -0.0136484 0.999779 0.0160105 0.0459869 0.998303 0.035716 +0.266669 0.919609 0.288456 -0.37606 0.731552 -0.568692 -0.515898 0.438139 -0.736127 +-0.584776 0.457777 -0.669685 -0.243857 0.847821 0.470886 -0.2753 0.738905 0.615004 +-0.182946 0.908367 0.376032 -0.595263 0.229027 -0.7702 -0.356495 0.471738 -0.806459 +-0.34711 0.664094 0.662189 -0.15912 0.370942 0.914922 -0.389465 0.478623 -0.786916 +-0.148017 0.262617 -0.95348 -0.0614461 0.00344608 0.998104 0.000592536 0.0838191 -0.996481 +-0.0916227 -0.357025 0.929591 0.0100104 -0.0588668 -0.998216 -0.0645672 -0.608508 0.790917 +-0.0250939 -0.354626 0.934672 0.0100989 -0.379621 -0.925087 0.0143975 -0.065909 0.997722 +0.00952883 -0.37478 -0.927065 0.00939532 -0.604444 -0.796592 0.00508736 -0.36225 0.932067 +0.00115226 -0.0442223 -0.999021 -0.00176212 -0.616275 0.787529 0.0421572 -0.289346 0.956296 +0.00263111 0.0334904 -0.999436 0.204907 0.45742 0.865321 -0.00938461 0.000884738 -0.999956 +0.312142 0.617828 0.721703 0.125876 0.290801 0.948467 -0.15749 -0.323764 -0.932938 +0.0128339 -0.368116 0.929691 -0.25891 -0.470075 -0.843798 -0.163555 -0.60246 -0.781212 +-0.381095 -0.575568 -0.723525 8.97508e-05 -0.614968 0.788552 0.00171271 -0.374585 0.927191 +-0.0382475 -0.0855875 -0.995596 0.00117496 -0.040112 0.999195 0.0104562 0.00872425 -0.999907 +0.000211293 0.021759 0.999763 0.00174008 0.0480023 -0.998846 0.000699363 0.00498475 0.999987 +-0.00164531 0.372865 -0.927884 0.00133415 -2.69881e-05 0.999999 -0.000784275 0.606457 -0.795116 +-0.00082092 0.375059 -0.927 -0.00319291 -0.0144104 0.999891 -0.000349759 0.0419518 -0.99912 +0.0095192 0.000417439 0.999955 -0.000837737 -0.0204611 -0.99979 0.156641 0.324227 0.93292 +-0.00371298 0.00295188 -0.999989 0.160842 0.607367 0.777969 0.277732 0.454407 0.846392 +0.376907 0.590367 0.713728 -0.00875528 0.0114972 -0.999896 0.0882717 0.0475666 0.99496 +0.00631249 -0.0125624 -0.999901 0.361275 -0.54884 0.753827 0.176743 -0.418377 0.890911 +0.0149265 0.0112284 -0.999826 0.296204 -0.576217 0.761733 0.123457 -0.293013 0.948104 +-0.0182188 -0.0216774 -0.999599 -0.00206867 -0.0378309 0.999282 0.00465979 -0.00400765 -0.999981 +-0.00429636 0.0161751 0.99986 -0.0414548 0.288006 -0.956731 -0.000494803 0.0043659 0.99999 +0.334053 0.563125 -0.755843 0.100393 0.614695 -0.78235 -0.199812 0.321399 -0.925623 +0.0133468 0.0139302 0.999814 -0.439846 -0.214818 -0.872003 -0.0329632 -0.0358017 0.998815 +-0.495678 -0.803628 -0.329371 -0.525264 -0.804876 -0.276175 -0.581498 -0.804886 -0.118402 +-0.662689 -0.743806 0.0871603 -0.610553 -0.747913 0.260482 -0.643543 -0.639477 0.420622 +-0.641054 -0.681251 -0.353477 -0.568254 -0.768258 -0.294733 -0.602862 -0.727255 -0.32811 +-0.581749 -0.783712 -0.217634 -0.690307 -0.722267 -0.0425104 -0.700782 -0.713147 -0.0180584 +-0.719286 -0.694229 0.0259606 -0.71451 -0.699369 0.0189556 -0.71435 -0.699781 0.00334182 +-0.715311 -0.698718 -0.0110935 -0.719244 -0.694632 -0.0132247 -0.718447 -0.695575 0.00303122 +-0.702828 -0.711256 -0.0121367 -0.631047 -0.764436 0.131974 -0.66665 -0.744 0.0451895 +-0.473606 -0.854925 0.211662 -0.630013 -0.734418 0.252415 -0.580522 -0.654057 0.484978 +-0.497778 -0.304289 -0.812173 -0.553854 -0.373454 -0.744162 -0.578629 -0.341733 -0.740545 +-0.275427 -0.183855 0.943577 -0.603284 -0.636167 -0.480978 -0.382026 -0.844703 -0.374878 +-0.515585 -0.678622 -0.52311 -0.348184 -0.897868 -0.269446 -0.373287 -0.908235 -0.189118 +-0.708571 -0.670619 0.21954 -0.6448 -0.735709 -0.20728 -0.57792 -0.537077 0.614457 +-0.535614 -0.640759 0.550042 -0.529461 -0.626939 0.571505 -0.648452 -0.329276 -0.686358 +-0.526227 -0.424723 -0.736679 -0.59377 -0.282789 0.753304 -0.126524 -0.570178 0.811719 +-0.189139 -0.545266 0.816647 -0.379421 -0.79052 -0.480747 -0.321171 -0.822982 -0.468563 +-0.578408 -0.770485 -0.267949 -0.492897 -0.743565 -0.451845 -0.586453 -0.688875 -0.426056 +-0.376798 -0.840903 0.388466 -0.386244 -0.818034 0.426187 -0.416743 -0.741281 0.526144 +-0.535727 -0.728312 0.427268 -0.557694 -0.65194 -0.513762 -0.556017 -0.51977 -0.648602 +-0.569788 -0.545816 -0.614351 -0.578716 -0.426404 0.695175 -0.416321 -0.475164 0.775175 +-0.531923 -0.634879 -0.560345 -0.311592 -0.885932 -0.343562 -0.548823 -0.615294 -0.565867 +-0.239329 -0.905632 -0.350075 -0.398136 -0.872208 0.284148 -0.428624 -0.895526 -0.119643 +-0.437239 -0.667681 0.602515 -0.443825 -0.757029 0.479507 -0.556754 -0.635663 -0.534749 +-0.572751 -0.515103 -0.637672 -0.59057 -0.555623 -0.585244 -0.378492 -0.91708 0.125332 +-0.867446 -0.0548466 0.494499 -0.957536 0.275296 0.0856546 -0.900065 0.432842 -0.0503042 +-0.80846 0.555549 -0.194315 0.395585 0.91323 0.0975839 0.550873 0.732785 0.399456 +0.583338 0.811979 -0.0201479 0.559864 0.712608 -0.422779 -0.903469 -0.122642 0.410734 +-0.931803 0.266892 0.24599 -0.900013 0.435173 -0.0245149 -0.827209 0.504467 -0.247464 +-0.853742 0.456027 -0.251324 -0.466295 0.712064 0.524914 -0.313613 0.74666 0.586639 +-0.129353 0.897237 0.422178 0.291339 0.883857 0.365947 0.716971 0.668134 -0.198872 +0.669886 0.628752 -0.394872 0.710995 0.703027 -0.0154807 0.728208 0.684577 -0.0326609 +0.794807 0.573189 -0.199339 0.771154 0.636613 0.00667093 0.804591 0.514978 -0.295686 +0.804938 0.567242 -0.174101 0.77066 0.520578 -0.367535 -0.733353 0.0689604 0.676342 +-0.82803 0.533754 0.171677 -0.746598 0.307394 0.59 -0.796685 0.599442 -0.0772227 +-0.762482 0.514107 -0.392828 -0.913104 0.24366 -0.32691 -0.789609 0.211991 -0.575828 +-0.915756 0.354678 -0.188666 -0.8978 0.440309 -0.00913676 -0.884566 0.466263 0.0119077 +-0.802736 0.596171 0.0139447 -0.73877 0.611661 0.283 -0.516191 0.672557 0.530296 +-0.355985 0.819201 0.44965 -0.238719 0.837154 0.492125 0.777457 0.584052 -0.233332 +0.744044 0.599301 -0.295358 0.70195 0.700984 -0.126049 0.597741 0.79716 -0.0851004 +0.770414 0.630686 -0.0932656 0.567626 0.760418 -0.315539 0.712673 0.630205 -0.308122 +0.598497 0.691487 -0.404533 -0.649489 -0.208238 0.731301 -0.716396 -0.17421 0.675595 +-0.868824 0.273379 -0.412806 -0.757546 0.360866 -0.543966 -0.940202 0.335211 -0.0604437 +-0.925122 0.379519 0.0107273 -0.930406 0.366318 0.0124665 -0.910592 0.387189 0.144594 +-0.593892 0.538946 0.597352 -0.662068 0.537507 0.522258 0.423452 0.870714 -0.250093 +0.342892 0.93824 -0.046154 0.23684 0.899216 -0.367855 0.124852 0.760727 -0.63695 +-0.310129 0.73872 -0.598425 -0.601029 -0.536037 0.592814 -0.495332 -0.700834 0.513301 +-0.602099 -0.613727 0.510701 -0.821479 0.537816 -0.189541 -0.828613 0.559383 0.0221755 +-0.819026 0.573755 -0.00163917 -0.804132 0.591835 0.0556989 -0.732462 0.616122 0.289642 +-0.0737932 0.891351 -0.447267 0.00564897 0.996437 -0.0841532 -0.0235426 0.999722 0.00154467 +-0.0379832 0.998858 0.0289814 -0.0502043 0.998291 0.029908 -0.00518545 0.998523 -0.0540829 +0.0744004 0.945581 -0.316767 -0.0398747 0.770145 -0.636621 -0.375509 -0.768083 0.518692 +-0.437254 -0.550613 0.711079 0.0285217 0.672503 -0.739545 -0.25294 0.54028 -0.80257 +-0.586935 -0.20298 0.783777 -0.560046 -0.412181 0.718649 -0.569917 -0.127464 0.811756 +-0.113989 0.587201 -0.801375 -0.357687 -0.0444634 -0.932783 -0.603895 -0.30448 0.736616 +-0.361287 -0.159491 0.918713 -0.410869 -0.605398 -0.681674 -0.569673 -0.36701 -0.735375 +-0.373201 -0.560172 -0.739546 -0.389447 -0.380678 0.838699 -0.493892 -0.719732 -0.48791 +-0.389398 -0.898648 -0.201993 -0.491009 -0.760538 -0.424842 -0.234063 -0.903583 -0.35882 +-0.417612 -0.532067 0.736549 -0.490644 -0.483506 0.724907 -0.380676 -0.414249 0.826731 +-0.241352 -0.692179 -0.680174 -0.380549 -0.752505 -0.537512 -0.339389 -0.459789 0.820615 +-0.249345 -0.753908 -0.607823 -0.282485 -0.651207 -0.704366 -0.165706 -0.631448 0.757506 +-0.253542 -0.574177 0.778484 -0.0290488 -0.788608 -0.614209 -0.0797726 -0.883983 -0.460663 +-0.231182 -0.867663 -0.440132 -0.0491817 -0.679938 0.731619 -0.0981662 -0.689128 0.71796 +0.0741809 -0.459899 -0.884867 -0.0586665 -0.625841 -0.777741 0.0131318 -0.723329 0.690379 +-0.0390492 -0.732074 0.680105 0.73369 -0.317326 -0.600835 0.305134 -0.247445 -0.9196 +0.0246509 -0.628116 0.777729 -0.0449098 0.0134227 0.998901 0.992456 0.0412896 -0.115441 +0.799754 -0.0680841 -0.596454 0.728359 -0.376616 -0.57241 0.402203 -0.266149 -0.876012 +-0.0469848 0.624458 0.779644 0.214349 0.589107 0.779107 0.320106 -0.449079 -0.834182 +0.0583647 0.227064 -0.972129 0.092686 0.715726 0.692203 0.317236 0.655404 0.685425 +0.550978 0.550089 0.627554 -0.00683932 0.679823 -0.733344 0.017817 0.741122 -0.671134 +0.166271 0.661739 0.731065 0.320674 0.720704 0.614617 0.49931 0.607678 0.617589 +0.0208757 0.659065 -0.751796 0.00269686 0.464848 -0.885386 -0.357657 0.744933 0.563166 +-0.289206 0.808217 0.512977 -0.272631 0.961578 -0.0322671 -0.176279 0.830797 -0.527923 +-0.519114 0.830752 -0.200928 -0.443137 0.760263 0.475004 -0.171477 0.912168 0.372216 +0.119769 0.956327 0.266635 -0.407729 0.529948 -0.743581 -0.154728 0.369314 -0.916333 +-0.370879 0.706503 0.602746 -0.312811 0.755952 0.575054 -0.170247 0.910932 0.375792 +-0.202848 0.855616 0.476207 -0.285329 0.954427 0.0874993 -0.336933 0.548229 -0.765455 +-0.271686 0.548159 -0.791017 -0.470675 0.519921 0.712844 -0.42166 0.59081 0.687856 +-0.274013 0.667431 0.692425 -0.225421 0.583801 -0.779975 -0.187205 0.397753 -0.898191 +-0.518832 0.432569 0.737358 -0.360992 0.200092 0.91085 -0.0320597 0.0829317 -0.996039 +-0.410262 -0.247552 0.877726 -0.0040169 -0.0391609 -0.999225 -0.166257 -0.581591 0.79631 +-0.174799 -0.288622 0.941352 0.0116514 -0.37625 -0.926445 -0.00469458 -0.0533321 0.998566 +0.00569506 -0.378108 -0.925744 0.0108717 -0.602105 -0.798343 -0.00829536 -0.377219 0.926087 +0.00229486 -0.0442001 -0.99902 0.00705025 -0.589274 0.807903 0.161 0.0247854 0.986643 +0.00921139 0.042317 -0.999062 0.157602 0.596618 0.786898 0.263074 0.500221 0.824967 +0.384748 0.586366 0.712842 0.00790375 -0.0260534 -0.999629 0.0411762 0.0407036 0.998322 +-0.0402224 -0.387162 -0.921134 -0.00825006 -0.39867 0.917057 -0.0500947 -0.384518 -0.921757 +-0.0766752 -0.612958 -0.786386 -0.00107739 -0.6127 0.790315 -0.000601234 -0.374068 0.927401 +0.00971279 -0.0334959 -0.999392 -0.000765843 -0.0416842 0.999131 0.00806188 -7.447e-05 -0.999968 +-0.000293834 0.0219801 0.999758 -0.000642622 0.041733 -0.999129 0.00106792 0.00633949 0.999979 +0.000217763 0.373116 -0.927785 0.000907229 -0.00241695 0.999997 -0.00106896 0.606655 -0.794964 +0.00114086 0.375877 -0.926669 -0.00734802 -0.0232818 0.999702 0.00143682 0.0424734 -0.999097 +-0.00738892 0.0272094 0.999602 0.000226385 -0.0222087 -0.999753 0.0403116 0.382164 0.923215 +-0.0028364 -0.00152167 -0.999995 0.0773072 0.61262 0.786588 0.0439657 0.409522 0.91124 +-0.00777777 0.00624609 -0.99995 -0.00523518 -0.0167623 0.999846 -0.0116603 0.00201325 -0.99993 +0.003023 -0.386247 0.92239 -0.0130623 -0.00473095 -0.999904 0.0796863 -0.605718 0.791679 +0.0100878 -0.382065 0.92408 -0.00745431 -0.0143363 -0.999869 -0.00659925 -0.0491464 0.99877 +-0.00859265 0.00110259 -0.999962 -0.00392421 0.0186243 0.999819 -0.00944216 0.0442633 -0.998975 +0.00907402 0.018401 0.999789 -0.449448 0.398156 -0.799667 -0.26534 0.144136 -0.95332 +0.00728013 -0.0112574 0.99991 -0.560798 -0.742639 -0.366051 -0.475564 -0.877561 -0.0610387 +-0.62588 -0.779 0.0378712 -0.574622 -0.717861 0.393046 -0.527678 -0.763271 -0.372791 +-0.515486 -0.801431 -0.303287 -0.614675 -0.767649 -0.181353 -0.587267 -0.779875 -0.216593 +-0.697129 -0.716812 -0.0138322 -0.72683 -0.686663 -0.014535 -0.712391 -0.701577 0.0169665 +-0.640994 -0.754494 0.140947 -0.659739 -0.751495 4.13419e-05 -0.432941 -0.899478 0.0591707 +-0.629092 -0.774462 0.0667214 -0.469123 -0.865032 0.177884 -0.605289 -0.741268 0.290081 +-0.555954 -0.760459 0.335585 -0.58841 -0.0855804 -0.804021 -0.357929 -0.189368 -0.914345 +-0.165447 -0.332608 0.928439 -0.356863 -0.799986 -0.482359 -0.362372 -0.81506 -0.452066 +-0.514108 -0.816125 -0.263881 -0.526236 -0.795796 -0.29964 -0.561466 -0.776289 -0.286586 +-0.542628 -0.80086 -0.253334 -0.651268 -0.746441 0.136658 -0.733475 -0.674819 -0.0814472 +-0.498162 -0.840387 -0.213503 -0.535432 -0.796493 0.280911 -0.520568 -0.759772 0.389558 +-0.499487 -0.594972 0.629699 -0.386282 -0.547026 0.742663 -0.402546 -0.387579 -0.8293 +-0.552195 0.120939 0.824897 -0.400055 -0.421575 0.813775 -0.381055 -0.140922 0.913749 +-0.284156 -0.754886 -0.591103 -0.19541 -0.892727 -0.40602 -0.429097 -0.747159 -0.507572 +-0.228062 -0.891929 -0.390449 -0.463777 -0.673432 0.575675 -0.499388 -0.704069 0.504874 +-0.542573 -0.719752 0.433095 -0.611548 -0.596399 -0.519921 -0.512143 -0.518641 -0.684632 +-0.543493 -0.536843 -0.645302 0.171853 -0.69629 0.696883 -0.20168 -0.195035 0.959837 +-0.524506 0.318922 0.789419 -0.222215 -0.812808 -0.538483 -0.31378 -0.805347 -0.50295 +-0.456854 -0.829346 0.321668 -0.587407 -0.781516 0.210202 -0.610037 -0.788515 0.0780988 +-0.704651 -0.687859 -0.174117 -0.616991 -0.691474 -0.375746 -0.0547406 -0.803415 0.592898 +-0.281296 -0.842199 0.459972 -0.43871 -0.652489 0.617893 -0.585252 -0.68654 0.431443 +-0.612589 -0.669563 -0.420023 -0.461056 -0.808764 -0.36514 -0.514281 -0.639344 -0.571625 +-0.393197 -0.888618 -0.236125 -0.310055 -0.90842 -0.280426 -0.695075 -0.683968 0.221492 +-0.659931 -0.716399 -0.226417 0.771064 0.579558 0.263768 0.705793 0.573543 0.415818 +0.906379 0.399799 0.136523 0.936821 0.349111 0.0220956 0.801178 0.595216 0.0618973 +0.945556 0.320487 -0.056665 0.8169 0.576769 0.00327612 0.855858 0.296892 -0.423512 +0.809879 0.552144 -0.198071 0.746427 0.378431 -0.54739 0.154213 0.915227 0.37226 +0.283928 0.724025 0.628628 0.414675 0.783795 0.462288 0.142089 0.98686 0.0769257 +0.690904 0.706765 -0.152105 0.453155 0.891431 -0.00100359 0.809387 0.567456 0.151285 +0.703727 0.686681 0.18231 0.874786 0.484461 0.00678587 0.927952 0.347697 -0.134212 +0.990436 0.130147 -0.0458049 0.920302 0.214252 -0.327323 0.759916 0.205913 -0.616544 +0.12116 0.740204 0.661376 -0.0576133 0.772302 0.632638 0.89371 0.412318 -0.17685 +0.932825 0.345077 -0.103729 0.694636 0.436017 -0.572163 0.772163 0.472508 -0.424854 +0.684857 0.536005 -0.493629 -0.904809 0.214645 0.36776 -0.930196 0.366958 -0.00875817 +-0.85937 0.410239 -0.305264 -0.411578 0.739372 0.532853 -0.309703 0.713038 0.629016 +-0.117481 0.769833 0.62734 0.630029 0.755761 -0.178574 0.690213 0.636899 -0.34346 +0.640671 0.767462 -0.0232941 0.765582 0.64012 -0.064276 0.589063 0.761242 -0.271138 +0.747186 0.641459 -0.173906 0.492657 0.542646 -0.680312 -0.843476 -0.277756 0.459783 +-0.998653 0.0165303 0.0491918 -0.921731 0.195287 -0.335073 -0.533278 0.534251 0.655889 +-0.430269 0.531155 0.729892 -0.313245 0.668785 0.674244 0.312706 0.76666 -0.560757 +0.372207 0.620434 -0.690307 0.228144 0.738818 -0.634113 -0.814539 -0.435204 0.383567 +-0.99984 -0.00810318 -0.0159352 -0.912283 0.341155 -0.226613 -0.905218 0.424918 -0.00494319 +-0.878547 0.475603 0.0442448 -0.803486 0.579569 0.136047 -0.688714 0.695535 0.204704 +-0.572176 0.637824 0.515554 -0.513251 0.524462 0.679348 0.109876 0.94476 -0.308797 +-0.0202966 0.999177 -0.0351068 -0.277235 0.956749 -0.0881635 -0.412936 0.910733 0.00703659 +-0.42555 0.904597 -0.0247122 -0.257583 0.91237 -0.318169 -0.416352 0.689434 -0.592733 +-0.844364 -0.273908 0.460461 -0.976773 0.211566 0.0339613 -0.913942 0.405079 0.0249173 +-0.86322 0.486115 0.136173 -0.717704 0.655818 0.234103 -0.721277 0.612963 0.322545 +-0.664737 0.673627 0.323034 -0.233426 0.60224 -0.763426 -0.516368 0.272009 -0.812019 +-0.796666 0.477485 0.370584 -0.43163 0.223334 0.873966 -0.616187 0.75542 0.222832 +-0.530897 0.809522 0.250644 -0.561243 0.194817 -0.804396 -0.586946 -0.078804 -0.805782 +-0.487001 -0.159998 0.858621 -0.705087 -0.55882 -0.436547 -0.76872 -0.63227 -0.0964606 +-0.688354 -0.718149 -0.102133 -0.752 -0.589647 0.294639 -0.435722 -0.621597 -0.650971 +-0.470589 -0.502582 0.72523 -0.535495 -0.440689 0.720443 -0.454877 -0.337576 0.824093 +-0.65681 -0.520855 -0.545262 -0.684616 -0.498285 -0.531989 -0.55541 -0.612879 -0.562049 +-0.529737 -0.84813 0.00744531 -0.384913 -0.420682 0.821504 -0.643489 -0.540125 -0.54239 +-0.688241 -0.505578 -0.520303 -0.52143 -0.593783 -0.612808 -0.465727 -0.527611 0.710441 +-0.512647 -0.555052 0.655065 -0.549685 -0.405962 0.730096 -0.546282 -0.619196 -0.564068 +-0.654552 -0.524835 -0.54416 -0.517509 -0.642298 -0.565366 -0.449082 -0.318565 0.83477 +-0.250416 -0.743523 -0.620052 -0.349363 -0.783142 -0.514426 -0.317446 -0.605106 0.73012 +-0.332061 -0.661653 0.672273 -0.128871 -0.275224 -0.952704 -0.314514 -0.594979 -0.739649 +-0.191787 -0.674014 0.713388 -0.176144 -0.623652 0.761598 -0.00683054 -0.0409229 -0.999139 +-0.151131 -0.719892 0.677433 -0.405911 0.0456565 0.912772 0.0293529 -0.370967 -0.928182 +-0.432231 0.605042 0.668655 -0.347534 0.678393 0.647304 -0.0085808 -0.0490739 -0.998758 +-0.0096969 -0.616544 -0.787261 -0.344195 0.683927 0.643253 -0.132878 0.706543 0.695083 +-0.0237404 0.672034 -0.74014 -0.0458859 0.751414 -0.658234 -0.211859 0.790185 0.575085 +0.0824402 0.789449 0.608254 -0.015106 0.639832 -0.768366 -0.0952987 0.351354 -0.93138 +-0.277833 0.946424 -0.164592 -0.187726 0.846853 0.497593 -0.407215 0.797608 -0.444969 +-0.281274 0.798767 0.531843 -0.165193 0.811024 0.561206 -0.0279252 0.897116 0.440913 +-0.323767 0.0805273 -0.942704 -0.291999 0.85002 0.43841 -0.21586 0.775504 0.593294 +-0.29658 0.881241 0.368042 -0.307286 0.951313 0.0240723 -0.352441 0.494474 -0.794532 +-0.267239 0.56576 -0.780064 -0.20136 0.749949 0.630104 -0.139147 0.50961 0.849079 +-0.328707 0.581379 -0.744278 -0.251052 0.549693 -0.79675 -0.584767 0.0861752 0.806611 +-0.297633 0.226418 0.927442 -0.283873 0.560956 -0.777653 -0.130146 0.292787 -0.947279 +-0.528286 -0.383624 0.757461 -0.312358 -0.127555 0.941362 0.00578874 0.0138549 -0.999887 +-0.0311921 -0.0179857 0.999352 -0.0012482 -0.388993 -0.92124 0.00626778 -0.0368805 0.9993 +-0.00780099 -0.383328 -0.923579 -0.0600953 -0.609696 -0.790354 0.0207613 -0.386106 0.922221 +0.0033471 -0.0315426 -0.999497 -0.0104196 -0.585358 0.810708 0.0314798 0.014941 0.999393 +0.0134768 0.0481925 -0.998747 0.0411258 0.605758 0.794585 0.0383451 0.41133 0.910679 +-0.0376074 -0.0819964 -0.995923 -0.0149388 -0.00540843 0.999874 -0.199278 -0.480187 -0.85423 +-0.0161077 -0.395783 0.918203 -0.122063 -0.322786 -0.938568 -0.308701 -0.597735 -0.739876 +-0.00953782 -0.611089 0.791505 -0.00913697 -0.37487 0.927032 -0.00612379 -0.00461086 -0.999971 +-0.00322691 -0.0423815 0.999096 0.0035975 -0.00955958 -0.999948 -0.00188236 0.0227716 0.999739 +-0.000338282 0.0398057 -0.999207 -0.000369307 0.00818273 0.999966 0.00869779 0.376358 -0.926433 +-0.00159407 -0.00781247 0.999968 0.00835939 0.607654 -0.794158 0.00971418 0.377015 -0.926156 +-0.00946209 -0.0318419 0.999448 0.00354479 0.0424434 -0.999093 0.0376177 0.0811192 0.995994 +0.00200189 -0.0233291 -0.999726 0.19986 0.476396 0.856214 -0.00017324 -0.00563443 -0.999984 +0.309865 0.599397 0.738043 0.121039 0.343524 0.931311 -0.00383491 0.00225843 -0.99999 +0.000513028 -0.0407534 0.999169 -0.00340727 0.00146227 -0.999993 -0.0133974 -0.385296 0.922696 +0.00370161 0.000795234 -0.999993 -0.0177752 -0.61022 0.792032 -0.00906693 -0.380519 0.924729 +0.0177174 0.000590605 -0.999843 -0.00318199 -0.0468058 0.998899 0.0354114 0.00267272 -0.999369 +0.00889698 0.0311259 0.999476 -0.00173888 0.0200718 -0.999797 0.0271045 0.035973 0.998985 +-0.496178 -0.788255 -0.363952 -0.386397 -0.902775 -0.18893 -0.567737 -0.804043 -0.176608 +-0.340511 -0.939945 -0.0235724 -0.630115 -0.772013 0.0833732 -0.45183 -0.865314 0.216984 +-0.602029 -0.727992 0.328008 -0.537483 -0.751141 0.383276 -0.104633 -0.0433367 -0.993566 +-0.0515648 -0.0977673 0.993872 -0.561075 -0.67641 -0.477142 -0.671316 -0.67831 -0.298713 +-0.648318 -0.72142 -0.243386 -0.621056 -0.769359 -0.149583 -0.583992 -0.760946 -0.282693 +-0.510396 -0.859829 0.0138041 -0.57363 -0.808558 0.131082 -0.628496 -0.771353 0.10004 +-0.625957 -0.77211 -0.109656 -0.589726 -0.752975 -0.291979 -0.507764 -0.819826 -0.264689 +-0.492261 -0.834371 0.247999 -0.413196 -0.908049 0.068676 -0.500574 -0.836925 0.221319 +-0.590947 -0.775163 0.223392 -0.44058 -0.81389 0.378777 -0.535485 -0.688816 0.48866 +-0.277538 -0.540737 0.794088 -0.354741 -0.355495 -0.864744 -0.279826 -0.461074 0.842086 +-0.301744 -0.763466 -0.571026 -0.419676 -0.738359 -0.527918 -0.36129 -0.557404 0.74751 +-0.603877 -0.617636 -0.503844 -0.513075 -0.533032 -0.672778 -0.533605 -0.54363 -0.647867 +-0.212774 -0.560622 0.800269 -0.245992 -0.351307 0.903367 -0.292888 -0.788111 -0.541386 +-0.317307 -0.868564 -0.380674 -0.319905 -0.884303 -0.340101 -0.356411 -0.769453 0.530012 +-0.568439 -0.791394 0.224884 -0.628727 -0.75842 0.171763 -0.71061 -0.703324 0.0192328 +-0.696433 -0.716008 -0.0480917 -0.696906 -0.687149 -0.205301 -0.662426 -0.647079 -0.377466 +-0.543877 -0.231953 0.806471 -0.501926 -0.230076 0.833748 -0.599901 -0.691064 -0.403175 +-0.698114 -0.660955 -0.275271 -0.644288 -0.736227 -0.207032 -0.628677 -0.7565 -0.180203 +-0.521358 -0.764924 -0.378256 -0.456978 -0.834744 -0.307204 -0.499846 -0.774717 0.387255 +-0.532752 -0.846132 0.015366 -0.698168 -0.69847 -0.157166 -0.493346 -0.827583 -0.267799 +0.726283 0.680931 -0.094058 0.717287 0.560124 0.414441 0.87472 0.476227 -0.0898441 +0.667561 0.620165 -0.412018 0.694024 0.47856 0.537877 0.740559 0.521804 0.423429 +0.909244 0.285362 0.303057 0.994194 0.107005 0.0113423 0.708527 0.518524 -0.478667 +0.863367 0.359641 -0.353914 0.663239 0.520942 -0.537339 0.745424 0.621482 0.241044 +0.649981 0.572511 0.499756 0.911753 0.391352 0.124697 0.920777 0.388958 0.0296971 +0.787813 0.615263 0.028333 0.945363 0.286162 0.156205 0.802334 0.5792 0.144181 +0.997883 0.0225703 0.0609872 0.99842 -0.0081775 0.0555905 0.995541 0.0403208 -0.0852784 +0.652207 0.456143 -0.605442 0.823489 0.200054 -0.53089 0.519246 0.282175 -0.806697 +0.597184 0.534921 0.597688 0.583276 0.599189 0.548418 0.722885 0.580457 0.374842 +0.857782 0.509888 0.0649898 0.895571 0.444675 -0.0147026 0.921857 0.332886 -0.198411 +0.999261 0.0308899 -0.0228704 0.999041 -0.0328284 0.02898 0.999287 -0.0303709 -0.0224164 +0.922388 0.0217583 -0.385651 0.600232 0.208509 -0.77217 0.350381 0.820119 0.45237 +0.350935 0.826322 0.440496 0.560128 0.789347 0.251373 0.613421 0.679997 0.401644 +0.502827 0.752007 0.426205 0.929919 0.144218 -0.338306 0.999642 -0.0267112 0.00123097 +0.999272 -0.0324608 -0.0200533 0.947941 -0.0289261 -0.31713 0.751666 0.0371471 -0.658497 +0.120428 0.924497 0.361666 0.0962843 0.771949 0.628351 0.143443 0.947178 0.286841 +0.839189 0.302738 -0.451787 0.996169 0.0657003 -0.057717 0.999486 0.032057 -1.18005e-05 +0.942283 0.0400472 -0.332413 0.771853 -0.00764569 -0.635755 0.0327849 0.656586 0.753539 +0.016808 0.728425 0.684919 0.867134 0.450255 -0.212951 0.9421 0.335332 -0.000583741 +0.893462 0.330141 -0.304519 0.667216 0.125105 -0.734283 -0.0838231 0.661468 0.745274 +-0.191326 0.632164 0.750842 0.454172 0.285379 -0.843971 -0.329499 0.607201 0.723006 +-0.296919 0.487442 0.821121 0.285298 0.582922 -0.760793 0.192886 0.590952 -0.783308 +-0.440774 0.598946 0.668567 -0.341478 0.476692 0.810036 0.0379795 0.865579 -0.49933 +-0.279489 0.917317 -0.283576 -0.517323 0.755619 -0.401766 -0.563016 0.775585 -0.285449 +-0.711457 0.679595 -0.178828 -0.892199 0.451441 -0.0135034 -0.816477 0.540768 -0.202326 +-0.603349 0.73548 0.308284 -0.598301 0.756227 0.264871 -0.449781 0.692018 0.56463 +-0.320799 0.642682 0.695735 -0.676695 0.649987 -0.345834 -0.741226 0.54607 -0.390375 +-0.943772 0.185137 -0.273897 -0.826817 0.10017 -0.553479 -0.996745 0.0548732 -0.0590695 +-0.792899 0.340187 -0.505553 -0.751621 0.268051 0.602672 -0.748999 0.521324 0.408927 +-0.588691 0.686034 0.427551 -0.417218 0.829465 0.371371 -0.298272 0.93598 0.187018 +-0.298067 0.87551 0.380314 -0.897697 -0.192944 -0.396122 -0.762314 -0.297072 -0.575001 +-0.953702 -0.300754 -0.000160762 -0.920749 -0.0128693 -0.389942 -0.765584 -0.126204 0.630836 +-0.711673 0.458181 0.532533 -0.668094 0.104902 0.736645 -0.714414 -0.508726 -0.480427 +-0.738487 -0.455396 -0.497245 -0.812533 -0.393397 -0.430149 -0.833591 -0.425206 -0.352598 +-0.866462 -0.44108 -0.233863 -0.546447 -0.173834 0.819254 -0.785837 -0.210345 -0.581562 +-0.743975 -0.399566 -0.535582 -0.818972 -0.330247 -0.469278 -0.839015 -0.370027 -0.398915 +-0.459153 -0.566815 0.684032 -0.544052 -0.43839 0.715417 -0.460459 -0.495396 0.736587 +-0.726355 -0.40873 -0.552583 -0.715634 -0.317457 -0.622164 -0.441646 -0.567758 0.694694 +-0.566051 -0.212335 0.796555 -0.279918 -0.401548 0.872012 -0.671621 -0.545412 -0.501449 +-0.747574 -0.436739 -0.500393 -0.529569 -0.60253 -0.597088 -0.524197 -0.317234 0.790304 +-0.361295 -0.35271 0.863169 -0.518877 -0.343877 -0.782633 -0.698851 -0.467601 -0.541254 +-0.484001 -0.612168 -0.625295 -0.358272 -0.583871 0.728516 -0.360167 -0.598279 0.71578 +-0.241485 -0.511328 0.82476 -0.364415 0.00508295 -0.931223 -0.29708 -0.543972 0.784753 +-0.358165 -0.0758675 -0.930571 -0.193816 -0.827986 0.526189 -0.373694 -0.701613 0.606705 +-0.72402 0.108444 0.681201 -0.379383 -0.419916 0.824463 -0.284601 -0.414092 -0.864598 +-0.739767 0.495137 0.455614 -0.844429 0.382878 0.374626 -0.555193 0.10678 0.824839 +-0.341211 -0.362213 -0.867396 -0.351224 -0.600217 -0.718597 -0.449266 0.673673 0.586792 +-0.603952 0.649558 0.461863 -0.749704 0.608471 0.260203 -0.749325 0.518132 0.412373 +-0.283367 0.570773 -0.770663 -0.261595 0.733196 -0.627688 -0.533286 0.766852 0.357133 +-0.241029 0.792987 0.559533 -0.599187 0.688818 0.408049 -0.286499 0.590375 -0.75457 +-0.455181 -0.0245018 -0.890062 -0.563012 0.813035 0.148296 -0.43066 0.727383 0.534272 +-0.601005 0.593187 0.535651 -0.442197 0.715686 0.540607 -0.410804 0.161317 -0.897339 +-0.577271 -0.339079 -0.742821 -0.604345 -0.0468689 -0.795343 -0.388341 0.739607 0.549702 +-0.500352 0.74608 0.439332 -0.47302 0.851542 0.226117 -0.561566 0.826559 -0.038008 +-0.499429 0.865763 0.0320128 -0.566355 0.725424 0.391154 -0.385307 0.561247 -0.73249 +-0.240971 0.708859 -0.662912 -0.339723 0.684632 -0.644877 -0.307628 0.804111 0.508694 +-0.142855 0.422101 0.895223 -0.401401 0.829459 0.388427 -0.304802 0.64097 -0.704452 +-0.280595 0.538115 -0.794794 -0.101734 0.0333874 0.994251 -0.232924 0.576574 -0.783141 +-0.23708 0.352716 -0.905199 -0.0314538 0.00451537 0.999495 -0.10711 -0.0498996 -0.992994 +0.0218379 0.025588 0.999434 -0.211907 -0.46355 -0.860359 0.00544519 -0.0488889 0.998789 +-0.123233 -0.330806 -0.935618 -0.301652 -0.619619 -0.724623 0.194827 -0.277369 0.940802 +-0.00357457 0.00573213 -0.999977 0.17106 -0.553441 0.815133 0.289001 0.129946 0.948468 +-0.00659562 0.00896711 -0.999938 0.418834 0.505208 0.754548 0.136135 0.349161 0.927121 +-0.159267 -0.322466 -0.933086 -0.00297235 -0.0318095 0.99949 -0.259664 -0.472021 -0.842479 +-0.164854 -0.602932 -0.780574 -0.381411 -0.576827 -0.722355 0.0242151 -0.387055 0.921739 +-0.0394428 -0.0787815 -0.996111 0.000303469 -0.609309 0.792933 0.0342158 -0.364328 0.930642 +0.00877431 0.0262039 -0.999618 0.0115916 -0.0499095 0.998687 0.000495892 -0.0167268 -0.99986 +-0.00529118 0.0226804 0.999729 0.00232378 0.0433233 -0.999058 -0.00353813 0.0103235 0.999941 +-0.0305968 0.37713 -0.925655 -0.00400539 -0.0126761 0.999912 -0.00127141 0.607899 -0.794014 +-0.0340111 0.36585 -0.930052 0.00957539 0.000655702 0.999954 -0.0121659 0.0488593 -0.998732 +0.157931 0.31993 0.934186 0.00391216 -0.0234457 -0.999717 0.16424 0.60094 0.782238 +0.259325 0.477556 0.839459 0.381572 0.574207 0.724354 0.00542533 -0.0072082 -0.999959 +0.0376886 0.10373 0.993891 0.00869811 0.00376458 -0.999955 -0.00903522 -0.0713102 0.997413 +0.0176343 0.0098632 -0.999796 0.00881756 -0.380933 0.924561 0.0291455 0.0137883 -0.99948 +-1.6208e-05 -0.606818 0.79484 0.00953455 -0.373772 0.927472 0.00287052 -0.00292958 -0.999992 +0.0171216 -0.0336571 0.999287 -0.0649431 -0.0367105 -0.997213 0.0317708 0.0485886 0.998313 +-0.309599 -0.142413 -0.940142 -0.0592377 -0.0536138 0.996803 -0.680841 -0.531514 -0.503933 +-0.731135 -0.603672 -0.317838 -0.650493 -0.690436 -0.316474 -0.765637 -0.640583 -0.0587653 +-0.657302 -0.753334 -0.0210402 -0.777163 -0.62904 0.0180301 -0.638572 -0.768859 0.03288 +-0.789804 -0.613028 0.0201503 -0.658422 -0.752338 0.0216232 -0.790967 -0.611713 -0.0133383 +-0.641399 -0.760291 -0.102788 -0.684633 -0.721537 -0.103252 -0.55247 -0.783467 -0.284529 +-0.609686 -0.791592 -0.0408132 -0.490087 -0.870642 -0.0423775 -0.477908 -0.877518 0.0395832 +-0.633224 -0.757896 0.156914 -0.590066 -0.793477 0.149054 -0.784395 -0.618815 0.0423496 +-0.617671 -0.783969 0.0622593 -0.717955 -0.627239 0.301848 -0.617173 -0.718436 0.320854 +-0.563565 -0.656775 0.50104 -0.420832 -0.280636 -0.862638 -0.377001 -0.379671 0.84482 +-0.686544 -0.571623 -0.449337 -0.705543 -0.474826 -0.52607 -0.516698 -0.622674 -0.587622 +-0.453581 -0.699308 0.552479 -0.478009 -0.55767 0.67861 -0.639163 -0.589295 -0.494168 +-0.603163 -0.464957 -0.648082 -0.530824 -0.548777 -0.645809 -0.392987 -0.556683 0.731891 +-0.508047 -0.507524 0.695922 -0.386244 -0.397834 0.832192 -0.641088 -0.610791 -0.464695 +-0.740409 -0.619462 -0.260884 -0.606429 -0.724161 -0.328381 -0.763388 -0.61418 -0.200053 +-0.547246 -0.717812 -0.430427 -0.609717 -0.755513 0.239677 -0.643829 -0.658363 0.389925 +-0.711806 -0.701813 0.028104 -0.696857 -0.717064 0.0144628 -0.707164 -0.707011 -0.00737289 +-0.711754 -0.69932 -0.0660103 -0.683279 -0.706734 -0.183457 -0.650579 -0.668476 -0.360398 +-0.425865 -0.426332 0.798048 -0.627051 -0.68173 -0.376897 -0.460973 -0.86415 -0.201861 +-0.619245 -0.734462 -0.277671 -0.488887 -0.8687 -0.0796849 -0.630248 -0.771227 -0.0894183 +-0.532131 -0.784235 -0.319079 -0.571722 -0.72821 -0.377948 -0.643392 -0.721252 0.256598 +-0.644713 -0.626137 0.438518 -0.741614 -0.670438 0.0228537 -0.655357 -0.747521 -0.108261 +-0.712542 -0.661859 -0.232868 -0.635986 -0.676967 -0.370455 0.728063 0.646733 0.22729 +0.666037 0.556694 0.496474 0.86773 0.45154 0.207739 0.83152 0.553972 -0.0411146 +0.733817 0.669087 -0.117622 0.702153 0.596204 -0.389258 0.656032 0.504916 0.560965 +0.634782 0.55651 0.536049 0.83803 0.396339 0.374996 0.81256 0.460543 0.357275 +0.726284 0.52806 -0.440072 0.686353 0.616857 -0.385236 0.782025 0.388493 -0.487351 +0.787968 0.547851 -0.281006 0.795295 0.378305 -0.473699 0.492245 0.810846 0.316581 +0.582249 0.640749 0.500427 0.599726 0.739854 0.30487 0.763557 0.606677 0.221186 +0.552952 0.760444 0.340541 0.817637 0.468895 0.334077 0.684317 0.423518 -0.593584 +0.695031 0.195379 -0.691924 0.568178 0.569023 0.594463 0.577386 0.561757 0.592499 +0.561748 0.584285 0.585705 0.575499 0.222215 -0.787033 0.592459 0.565674 0.57359 +0.548734 0.62423 0.556083 0.550974 0.60773 0.57192 0.601072 0.183056 -0.777948 +0.590424 0.357808 -0.723446 0.600314 0.152979 -0.784997 0.415246 0.75176 0.512277 +0.320021 0.779985 0.537783 0.616846 -0.00525267 -0.787066 0.448837 0.0321302 -0.893036 +0.383594 0.830329 0.40424 0.108717 0.686056 0.71938 0.370421 0.882835 0.288776 +0.598028 -0.0168663 -0.801298 0.37267 -0.0393434 -0.92713 -0.100648 0.597363 0.79563 +-0.134788 0.634718 0.760897 0.593641 0.0588526 -0.802575 0.35345 0.00470421 -0.935442 +-0.279884 0.605498 0.745008 -0.226619 0.488213 0.842788 0.509358 0.365602 -0.779031 +0.28962 0.137551 -0.947206 -0.457529 0.494922 0.738728 -0.351553 0.441068 0.825754 +-0.252298 0.298393 -0.920493 -0.498304 0.435404 0.749745 -0.40103 0.569307 0.71768 +-0.306399 0.749014 -0.58745 -0.608034 0.599153 -0.520874 -0.495747 0.716642 -0.490569 +-0.342588 0.641114 0.686736 -0.239901 0.743766 0.623907 -0.680532 0.493244 -0.541836 +-0.81839 0.0789588 -0.569212 -0.872841 0.246652 -0.421083 -0.861163 0.359183 -0.359703 +-0.752757 0.42923 0.499118 -0.665244 0.43428 0.607332 -0.412765 0.80157 0.432564 +-0.168184 0.935642 0.310304 -0.88471 -0.188981 -0.426114 -0.765022 -0.298966 -0.570403 +-0.902717 -0.124502 -0.411827 -0.808241 0.173606 0.562678 -0.818928 0.248156 0.51747 +-0.769544 0.226371 0.597125 -0.731427 -0.483303 -0.481075 -0.743453 -0.449937 -0.494807 +-0.818551 -0.384758 -0.426539 -0.832964 -0.380258 -0.401964 -0.744174 -0.117618 0.657549 +-0.775129 0.311575 0.549632 -0.681786 0.115695 0.722345 -0.763616 -0.462071 -0.450979 +-0.753321 -0.477042 -0.4527 -0.888227 -0.315931 -0.333527 -0.880917 -0.25652 -0.397722 +-0.583515 -0.23168 0.778354 -0.82173 -0.152749 -0.549025 -0.785787 -0.375735 -0.491285 +-0.895274 -0.242706 -0.373601 -0.977329 -0.143538 -0.155645 -0.92029 -0.176133 -0.349348 +-0.267034 -0.59437 0.758563 -0.354085 -0.352384 0.866285 -0.885385 -0.0605655 -0.460897 +-0.811415 0.0117647 -0.584353 -0.918977 -0.245804 -0.308321 -0.500877 -0.34482 0.793865 +-0.789485 -0.14546 -0.596284 -0.80083 0.0156478 -0.598687 -0.311237 -0.549605 0.775284 +-0.443018 -0.521847 0.728979 -0.646381 -0.00537793 -0.762996 -0.791418 0.0368583 -0.610164 +-0.451426 -0.678081 0.580017 -0.59333 -0.518824 0.615452 -0.554831 -0.635823 0.536556 +-0.628247 -0.00927517 -0.777959 -0.782625 -0.0894356 -0.616035 -0.390052 -0.740748 0.546948 +-0.42366 -0.353202 0.834123 -0.80891 0.454592 -0.372842 -0.594193 -0.786533 -0.168227 +-0.983345 0.0698403 0.167794 -0.346083 -0.329473 0.87845 -0.601672 -0.3296 -0.727568 +-0.736119 -0.33442 -0.588466 -0.529996 -0.213984 0.820558 -0.370256 0.811926 0.451317 +-0.323599 -0.885841 -0.33252 -0.865598 -0.27764 -0.416721 -0.360513 0.0481836 -0.931509 +-0.430082 -0.902579 0.0195028 -0.998758 0.0487677 -0.0101724 -0.559651 -0.780477 0.27865 +-0.866513 0.299909 0.399012 -0.377135 -0.219051 0.899881 -0.75487 0.224452 -0.616273 +-0.788239 -0.35962 -0.499352 -0.670983 -0.160731 -0.723842 -0.805864 0.528213 0.267534 +-0.810732 0.491664 0.317773 -0.694703 -0.124627 0.708418 -0.855201 0.279837 0.436259 +-0.621137 0.0301955 0.78312 -0.449202 0.845823 -0.287751 -0.434333 0.899714 0.0432475 +-0.308892 0.805362 0.505942 -0.469046 0.653278 -0.594326 -0.414511 0.554083 -0.721923 +-0.446804 0.669642 -0.59325 -0.73541 0.570048 0.366358 -0.812723 0.527971 0.246429 +-0.802615 0.542009 0.249071 -0.782453 0.596385 0.17914 -0.740573 0.593022 0.316033 +-0.7662 0.463181 0.445423 -0.579467 0.478334 -0.65986 -0.48377 0.158103 -0.860796 +-0.597964 0.131551 -0.790654 -0.755954 0.513483 0.406041 -0.839669 0.482198 0.249883 +-0.80493 0.536526 0.253431 -0.789006 0.521289 0.325158 -0.783914 0.545621 0.296271 +-0.21534 0.375966 -0.901265 -0.578925 0.759281 0.297216 -0.62393 0.78006 0.0470918 +-0.62557 0.779915 -0.0198809 -0.596121 0.780122 0.189863 -0.600333 0.681532 0.418467 +-0.719281 0.614405 0.324256 -0.747757 0.601645 0.28086 -0.233926 0.640805 -0.731196 +-0.295864 0.68659 -0.664122 -0.466509 0.571839 0.67481 -0.527717 0.655279 0.540485 +-0.398799 0.845175 0.355863 -0.46887 0.870572 0.149215 -0.662601 0.743454 -0.0907524 +-0.595599 0.782019 0.183597 -0.50557 0.813573 0.287226 -0.337401 0.62632 -0.702769 +-0.282833 0.523385 -0.803787 -0.551207 0.534917 0.640339 -0.266851 0.272591 0.924383 +-0.404394 0.565024 -0.719175 -0.412761 0.408914 -0.81389 -0.0163563 0.0492295 0.998654 +-0.509323 0.402124 -0.760846 -0.440971 -0.0886476 -0.893133 -0.00430519 0.0243581 0.999694 +-0.371998 -0.410951 -0.832308 -0.582005 -0.365799 -0.726265 -0.449814 -0.519308 -0.726627 +0.0188233 -0.0236864 0.999542 -0.0416731 -0.0557303 -0.997576 0.0433644 -0.0402417 0.998249 +0.0239829 0.0427439 -0.998798 0.54225 -0.176293 0.821514 0.284137 0.059079 0.956962 +0.0127626 -0.0256565 -0.999589 0.0292376 0.0524212 0.998197 -0.0402204 -0.386725 -0.921318 +-0.0140912 -0.0503962 0.99863 -0.052263 -0.385987 -0.921023 -0.0784756 -0.61304 -0.786145 +0.15734 -0.287668 0.944718 0.00559961 -0.0275201 -0.999606 0.177711 -0.585635 0.790854 +0.213314 -0.398306 0.892104 0.0031892 0.0172642 -0.999846 0.0373025 -0.048981 0.998103 +-0.00171547 -0.0189772 -0.999818 0.00894429 0.00241184 0.999957 -0.00335644 0.0357455 -0.999355 +-0.00435096 0.013178 0.999904 -0.162206 0.28894 -0.943506 -0.00721358 -0.0183074 0.999806 +-0.177106 0.587025 -0.789959 -0.209551 0.40095 -0.891811 -0.00772501 0.0264583 0.99962 +-0.0369833 0.0471902 -0.998201 0.0392919 0.383667 0.922635 -0.00154924 0.00299668 -0.999994 +0.0742426 0.610651 0.788412 0.048916 0.390527 0.919291 0.0260785 -0.000978846 -0.999659 +-0.00491668 0.0554771 0.998448 -0.00148395 -0.00524799 -0.999985 0.00697061 -0.0574812 0.998322 +-0.0282241 0.013142 -0.999515 -0.028792 -0.382862 0.923357 -0.0809158 -0.0320475 -0.996206 +0.00726432 -0.604954 0.796228 -0.00541876 -0.368944 0.929436 -0.800112 -0.47685 -0.363916 +-0.737261 -0.614382 -0.281036 -0.929551 -0.341256 -0.139571 -0.759994 -0.642833 0.0957903 +-0.878862 -0.409054 0.245512 -0.721742 -0.560234 0.406482 -0.330541 -0.166098 -0.929061 +-0.00609755 -0.0308821 0.999505 -0.771531 -0.54953 -0.320558 -0.796558 -0.536827 -0.278049 +-0.889416 -0.434189 -0.142895 -0.837647 -0.540926 -0.0758128 -0.918287 -0.395902 -0.0031333 +-0.8415 -0.540032 0.0155853 -0.917919 -0.396185 -0.0214915 -0.842526 -0.51314 -0.163824 +-0.945479 -0.287438 -0.153129 -0.99513 -0.0883523 -0.04371 -0.993442 -0.110109 0.0307898 +-0.771185 -0.542844 0.332557 -0.934337 -0.328945 0.137149 -0.837379 -0.542281 0.0687577 +-0.919908 -0.392016 -0.00968291 -0.850386 -0.525216 -0.0315068 -0.915099 -0.403223 -0.00221469 +-0.816842 -0.556373 0.152374 -0.826238 -0.477012 0.299651 -0.721901 -0.517647 0.459239 +-0.482321 -0.241865 -0.841943 -0.0877859 -0.0304128 0.995675 -0.685885 -0.550806 -0.475578 +-0.747927 -0.607065 -0.268474 -0.849422 -0.463301 -0.252655 -0.772812 -0.59503 -0.220682 +-0.828586 -0.51706 -0.214697 -0.881112 -0.47155 -0.035803 -0.918684 -0.394919 -0.00766474 +-0.923865 -0.382358 0.0166068 -0.92239 -0.386098 -0.0111712 -0.922955 -0.384842 -0.0071132 +-0.922566 -0.385488 -0.0164573 -0.889888 -0.45581 0.0183313 -0.88331 -0.468735 0.00711115 +-0.913466 -0.405938 -0.0281601 -0.918492 -0.390186 0.064248 -0.661777 -0.505977 0.553207 +-0.757722 -0.512008 0.404604 -0.636661 -0.561336 0.528739 -0.700233 -0.28931 -0.652666 +-0.637529 -0.347033 -0.687841 -0.596825 -0.315878 -0.737578 -0.37291 -0.35575 0.85696 +-0.706766 -0.498377 -0.502098 -0.686247 -0.523034 -0.50547 -0.830142 -0.410367 -0.377444 +-0.833788 -0.414822 -0.364308 -0.605762 -0.576284 0.548588 -0.519709 -0.626449 0.580917 +-0.621751 -0.578437 0.528049 -0.643271 -0.337924 -0.68703 -0.708733 -0.422698 -0.564822 +-0.520472 -0.502355 0.69047 -0.510312 -0.522701 0.682909 -0.377021 -0.391417 0.839433 +-0.633173 -0.545292 -0.549317 -0.683787 -0.500151 -0.531304 -0.726966 -0.552982 -0.4071 +-0.856591 -0.419098 -0.301012 -0.66472 -0.628131 0.404475 -0.702767 -0.682011 0.202434 +-0.741312 -0.666554 0.0784964 -0.693588 -0.720338 0.00706997 -0.730982 -0.682366 -0.00645084 +-0.688546 -0.723442 -0.0503623 -0.750875 -0.624781 -0.214093 -0.653256 -0.638657 -0.406663 +-0.365888 -0.400659 0.839999 -0.560276 -0.651319 -0.511737 -0.608731 -0.528051 -0.592122 +-0.556249 -0.604868 -0.569843 -0.667417 -0.712667 0.216008 -0.529283 -0.734903 0.424001 +-0.512998 -0.858374 0.00519732 -0.664645 -0.741635 -0.0906919 -0.506475 -0.839739 -0.195757 +-0.620048 -0.731354 -0.28401 -0.462409 -0.883385 -0.0762138 -0.415166 -0.897391 -0.149418 +0.541191 0.840206 -0.0341481 0.737569 0.622196 0.262421 0.88883 0.453509 -0.0656616 +0.651487 0.64397 -0.401082 0.684878 0.682863 0.254246 0.617743 0.598273 0.510356 +0.667528 0.701502 0.249604 0.794927 0.596103 -0.112927 0.762141 0.646304 0.0378454 +0.942029 0.308101 0.132871 0.807523 0.566339 0.164824 0.994384 0.105199 -0.0115496 +0.730281 0.482541 -0.483574 0.863107 0.359985 -0.3542 0.696792 0.491397 -0.522502 +0.594707 0.54873 0.587553 0.594516 0.543211 0.592851 0.566912 0.576693 0.588248 +0.848856 0.474429 -0.233156 0.954622 0.261218 -0.143046 0.992646 0.0861927 -0.0850028 +0.68529 0.524556 -0.505192 0.868458 0.34508 -0.355951 0.697566 0.535855 -0.475669 +0.602346 0.549345 0.579136 0.586683 0.652296 0.479909 0.550646 0.600344 0.579979 +0.95065 0.108639 -0.290622 0.992331 0.0273261 -0.120548 0.740997 0.394733 -0.543239 +0.892079 -0.0707176 -0.446311 0.732917 -0.144174 -0.664866 0.539012 0.756521 0.370327 +0.604436 0.607717 0.515108 0.56983 0.620181 0.539137 0.503473 0.82958 0.241478 +0.891205 0.0595676 -0.449672 0.950937 -0.124582 -0.283194 0.667788 -0.358305 -0.652439 +0.774404 -0.295691 -0.559344 0.503091 0.526261 0.685528 0.347217 0.535704 0.769715 +0.597463 0.549501 0.584026 0.80649 0.201227 -0.555951 0.890804 -0.0268875 -0.453591 +0.704435 -0.199488 -0.681157 0.186608 0.643937 0.741972 0.356475 0.734004 0.578069 +0.48317 0.63661 0.601062 0.777245 -0.104623 -0.620439 0.584879 -0.215487 -0.781973 +0.0130274 0.835338 0.549582 0.113232 0.605359 0.787857 0.514216 0.794742 0.322439 +0.522947 0.744323 0.415342 0.31113 0.864819 0.394064 0.394173 -0.228427 -0.890196 +0.544794 -0.422718 -0.72423 -0.139463 0.414412 0.89934 0.124877 -0.0516247 -0.990828 +-0.276296 0.518602 0.809143 -0.255498 0.529829 0.808704 0.0111 -0.0650945 -0.997817 +-0.344658 0.527716 0.776355 -0.295406 0.542817 0.786184 -0.315851 -0.104665 -0.943018 +-0.260341 0.538826 0.80118 -0.255369 0.390223 0.884597 -0.492996 0.0298506 -0.869519 +-0.390518 0.598746 0.699284 -0.238701 0.406726 0.881814 -0.717304 0.407042 -0.565502 +-0.674266 0.0871623 -0.733326 -0.332391 0.745691 0.577461 -0.211031 0.715303 0.666189 +-0.100562 0.770614 0.629319 -0.945565 -0.0481635 -0.32185 -0.790989 -0.300348 -0.533036 +-0.849592 0.2672 -0.454751 -0.659387 0.238956 0.712817 -0.528494 0.404856 0.746181 +-0.262581 0.8432 0.469111 -0.0536682 0.870147 0.489862 -0.694035 -0.587076 -0.416722 +-0.45739 -0.842102 -0.28576 -0.907697 -0.399257 -0.129152 -0.617487 -0.676491 -0.401335 +-0.943431 -0.286941 -0.166144 -0.984756 -0.132668 -0.11249 -0.910645 -0.147265 -0.386055 +-0.770155 0.143621 0.621478 -0.523913 0.0806733 0.847943 -0.718282 -0.244248 -0.651471 +-0.64234 -0.441222 -0.626675 -0.931021 -0.174916 -0.320319 -0.995269 -0.0631999 -0.0737969 +-0.994003 -0.100689 -0.0426615 -0.914717 -0.295902 -0.275199 -0.88923 0.132105 0.437971 +-0.836547 0.0291157 0.547121 -0.483552 -0.0121737 0.875231 -0.938562 -0.0365004 -0.343176 +-0.785048 -0.00472191 -0.619416 -0.993105 -0.0390354 -0.110537 -0.922965 -0.273334 -0.27097 +-0.764961 -0.290565 0.57481 -0.901501 -0.033965 0.431442 -0.629114 -0.152287 0.76225 +-0.928937 0.00930307 -0.370121 -0.780317 0.0195405 -0.625079 -0.942096 -0.144152 -0.30278 +-0.809621 -0.229286 0.540317 -0.740986 -0.192673 0.643287 -0.921753 0.00608082 -0.387729 +-0.777795 0.00997945 -0.628439 -0.916112 -0.0218017 -0.40033 -0.722733 -0.385913 0.573349 +-0.701004 -0.39217 0.595647 -0.833169 -0.273886 0.480433 -0.717066 -0.354574 0.600078 +-0.920868 0.0100861 -0.389744 -0.771911 0.0213223 -0.635373 -0.919292 0.0121197 -0.393389 +-0.745534 -0.404222 0.52989 -0.744289 -0.288428 0.602364 -0.922131 -0.00765192 -0.386801 +-0.793039 -0.0920144 -0.602181 -0.924602 0.0193696 -0.380441 -0.623593 -0.491444 0.607959 +-0.52414 -0.650202 0.550012 -0.632073 -0.57694 0.517324 -0.943977 -0.136577 -0.300422 +-0.803809 -0.295018 -0.516581 -0.936585 -0.0206605 -0.34983 -0.696959 -0.593065 0.403139 +-0.901716 -0.29227 0.318569 -0.71894 -0.689496 -0.0878664 -0.882828 -0.246882 -0.399579 +-0.528377 -0.735936 0.423339 -0.53737 0.159838 0.828061 -0.705253 0.0120348 -0.708853 +-0.733911 -0.35136 -0.58131 -0.900897 -0.05016 -0.431125 -0.98513 -0.0480779 -0.164944 +-0.928956 -0.162533 -0.332602 -0.767234 -0.132385 0.627556 -0.89204 0.425075 0.153547 +-0.527379 0.779663 0.337634 -0.74631 0.547872 0.377964 -0.383224 -0.725945 -0.571089 +-0.498896 0.316761 -0.8067 -0.304458 -0.949865 0.0711507 -0.0712986 -0.893369 0.44363 +-0.296898 -0.0452323 0.953837 -0.530945 0.623087 -0.574334 -0.84776 0.219176 -0.482975 +-0.404172 0.594775 -0.694901 -0.767301 0.259713 0.586344 -0.616533 -0.0255102 0.786916 +-0.398226 0.81343 -0.423967 -0.417458 0.908221 -0.029398 -0.353796 0.879631 0.317925 +-0.533738 0.712805 -0.455008 -0.384649 0.757858 -0.526969 -0.552888 0.406536 -0.727353 +-0.902322 0.392881 0.17737 -0.918986 0.211932 0.332491 -0.725162 -0.218696 0.652926 +-0.813478 -0.235455 0.531802 -0.742907 -0.292173 0.602265 -0.542497 0.424538 -0.724889 +-0.53352 0.375889 -0.75767 -0.864097 0.465773 0.190769 -0.953526 0.265304 0.142836 +-0.998359 0.0452001 0.0351613 -0.998611 0.0515834 0.0107367 -0.925142 -0.17431 0.337238 +-0.763729 -0.248227 0.595903 -0.531121 0.501788 -0.682729 -0.365934 0.420433 -0.830258 +-0.614618 0.718908 0.324679 -0.739376 0.650363 0.174215 -0.842146 0.539047 0.0147762 +-0.921233 0.371861 -0.114237 -0.998695 0.0383098 -0.0337662 -0.999475 -0.0313049 -0.008313 +-0.996942 -0.064154 0.0446175 -0.703832 -0.428759 0.56638 -0.843463 -0.379417 0.380279 +-0.680053 -0.519432 0.517415 -0.374269 0.598234 -0.708547 -0.272243 0.484112 -0.831576 +-0.477171 0.688966 0.545557 -0.49415 0.773222 0.39742 -0.482839 0.873406 -0.0634749 +-0.554352 0.772566 -0.309574 -0.583203 0.759307 -0.288664 -0.613994 0.785951 0.0727435 +-0.521407 0.753365 0.400719 -0.516431 0.821078 0.243167 -0.669251 0.73105 0.132927 +-0.814364 0.579119 0.0378464 -0.816462 0.574055 -0.0620463 -0.745188 0.580718 -0.327814 +-0.856615 0.372657 -0.356843 -0.70689 0.515597 -0.484217 -0.994728 0.0802253 -0.0638772 +-0.999669 0.0161048 -0.0200824 -0.993762 -0.0896575 0.0663192 -0.66335 -0.542973 0.514925 +-0.845352 -0.376882 0.378603 -0.671641 -0.536828 0.510601 -0.348205 0.636666 -0.688047 +-0.237542 0.712077 -0.660697 -0.364954 0.658956 -0.657713 -0.406557 0.553633 0.726775 +-0.408889 0.657216 0.633148 -0.486901 0.716533 0.499507 -0.440554 0.888499 -0.128378 +-0.475991 0.844168 0.246603 -0.693203 0.492036 -0.52666 -0.847196 0.443408 -0.292658 +-0.717952 0.553225 -0.422477 -0.929537 0.33416 -0.155876 -0.995366 0.0632477 -0.0724317 +-0.990511 -0.0779522 0.113192 -0.698144 -0.477859 0.533147 -0.85589 -0.226519 0.46491 +-0.735888 -0.33067 0.590868 -0.301349 0.634505 -0.711753 -0.211258 0.386599 -0.897726 +-0.214075 0.626179 0.749714 -0.152212 0.267324 0.951509 -0.780604 0.590766 -0.20409 +-0.724251 0.611674 -0.318299 -0.838045 0.403675 -0.367053 -0.701655 0.536614 -0.468749 +-0.993678 0.107245 -0.0331972 -0.932063 0.0228794 0.361573 -0.769856 -0.0622176 0.635178 +-0.607614 0.138971 -0.78198 -0.300217 -0.0158029 -0.95374 -0.0408788 0.044703 0.998164 +-0.678306 0.496287 -0.54185 -0.843019 0.4442 -0.303325 -0.694559 0.550446 -0.463246 +-0.907628 0.37556 0.187528 -0.779902 0.211106 0.589225 -0.0305388 -0.0623132 -0.997589 +-0.0190308 0.0041881 0.99981 -0.77848 0.617715 -0.111342 -0.774787 0.531115 0.34296 +0.041321 0.0220347 -0.998903 0.0116669 -0.0236024 0.999653 0.0366595 0.0382456 -0.998596 +0.0179006 0.00680725 0.999817 -0.0206999 -0.0853048 -0.99614 -0.0183475 0.000110941 0.999832 +-0.190286 -0.484102 -0.854071 0.00438081 -0.0019243 0.999989 -0.117559 -0.325784 -0.938107 +-0.299814 -0.600952 -0.740924 0.0389141 -0.0449003 0.998233 -0.00338413 -0.000638457 -0.999994 +0.36605 -0.556495 0.745869 0.167651 -0.405938 0.898391 0.00541158 0.00955859 -0.99994 +0.293798 -0.579622 0.76008 0.121959 -0.293641 0.948104 0.00546682 -0.014508 -0.99988 +-0.00164883 -0.0356553 0.999363 -0.0104375 -0.00188262 -0.999944 -0.00417876 0.0136051 0.999899 +-0.0343125 0.0511612 -0.998101 -0.0129904 -0.0274638 0.999538 -0.362245 0.562293 -0.743374 +-0.186095 0.407757 -0.893925 0.0298927 0.0778809 0.996514 -0.311579 0.580548 -0.752252 +-0.142095 0.29205 -0.945788 0.187708 0.478715 0.85767 0.0202917 0.0708338 -0.997282 +0.294909 0.599731 0.743876 0.121233 0.333832 0.934804 -0.0245345 -0.0553619 -0.998165 +0.0244589 0.0386981 0.998951 -0.29633 -0.122274 -0.947226 -0.0603749 -0.103681 0.992776 +-0.358898 -0.0436341 -0.932356 -0.291487 -0.445727 0.846382 -0.752974 -0.493487 -0.435317 +-0.775487 -0.561376 -0.288925 -0.898899 -0.40778 -0.160298 -0.829962 -0.554772 -0.0582277 +-0.914761 -0.403966 0.00496665 -0.833393 -0.551767 0.0317788 -0.898141 -0.439345 -0.0178294 +-0.816774 -0.576947 0.00349094 -0.84081 -0.538579 -0.0545055 -0.780694 -0.624842 -0.00945439 +-0.724889 -0.683105 -0.0889053 -0.774626 -0.632409 0.00356851 -0.772449 -0.61519 0.157685 +-0.809699 -0.586798 -0.00745784 -0.902572 -0.429944 0.022617 -0.830916 -0.556239 0.0133243 +-0.896647 -0.441767 0.0294472 -0.743883 -0.596864 0.30065 -0.805905 -0.541885 0.238489 +-0.644344 -0.668925 0.370621 -0.44704 -0.244971 -0.860317 -0.152593 -0.596552 0.787935 +-0.185366 -0.342426 0.921078 -0.691009 -0.493637 -0.528043 -0.713046 -0.473245 -0.517305 +-0.835628 -0.407788 -0.368015 -0.889041 -0.414557 -0.19429 -0.908477 -0.417405 -0.0210336 +-0.914901 -0.40288 -0.025385 -0.85233 -0.477807 -0.212685 -0.847835 -0.487224 0.209256 +-0.920017 -0.391655 -0.0132545 -0.899883 -0.434134 0.0416879 -0.699015 -0.644797 0.309216 +-0.809416 -0.510871 0.289579 -0.713135 -0.599591 0.363219 -0.815232 -0.350703 0.460873 +-0.713978 -0.385378 0.584567 -0.480549 -0.281182 -0.830668 -0.549663 -0.352578 -0.757337 +-0.545183 -0.337725 -0.767279 -0.294586 -0.200299 0.934398 -0.686959 -0.594745 -0.417571 +-0.708398 -0.488362 -0.509583 -0.690473 -0.58366 -0.427303 -0.82085 -0.485447 -0.300909 +-0.753778 -0.404489 0.517888 -0.736442 -0.301175 0.605761 -0.608147 -0.313905 -0.729123 +-0.52903 -0.434113 -0.729159 -0.454031 -0.207244 0.866548 -0.699406 -0.556097 -0.448984 +-0.763664 -0.605174 -0.224903 -0.846435 -0.471483 -0.247491 -0.758451 -0.619531 -0.20232 +-0.694538 -0.589213 -0.41285 -0.704081 -0.543547 0.456975 -0.752736 -0.529699 0.390906 +-0.77805 -0.578078 0.245894 -0.806053 -0.520829 0.281096 -0.757748 -0.551398 0.348967 +-0.738944 -0.517586 -0.431353 -0.674169 -0.521164 -0.52334 -0.693539 -0.431309 -0.577041 +-0.490286 -0.518157 0.700809 -0.525825 -0.446535 0.723958 -0.423157 -0.358727 0.832017 +-0.693122 -0.275887 -0.665934 -0.628852 -0.436518 -0.643426 -0.841105 -0.313576 -0.440695 +-0.815044 -0.413322 -0.406041 -0.826581 -0.507428 0.243475 -0.741167 -0.602684 0.295709 +-0.769784 -0.628607 0.110842 -0.795178 -0.602646 -0.0671487 -0.787358 -0.57735 -0.216182 +-0.824455 -0.361695 -0.435259 -0.413219 -0.387342 0.824146 -0.713126 -0.412835 -0.566585 +-0.734378 -0.399419 -0.548774 -0.508589 -0.653606 0.560479 -0.552641 -0.488557 0.675204 +-0.691814 -0.616488 0.375947 -0.640781 -0.712892 -0.28493 -0.710988 -0.64951 -0.269507 +-0.632042 -0.754227 -0.177945 -0.726487 -0.683143 -0.0743819 -0.613794 -0.78724 -0.0592504 +-0.768113 -0.612488 -0.18671 -0.616092 -0.688318 -0.382949 -0.550913 -0.83218 0.0630137 +0.494895 0.866617 0.0636677 0.708869 0.596218 0.376867 0.780808 0.624715 0.0084153 +0.275193 0.808279 -0.520532 0.295197 0.827733 0.477197 0.340285 0.734069 0.587664 +0.629797 0.713208 0.307718 0.7956 0.570699 0.20328 0.52828 0.729598 0.43429 +0.819277 0.486409 0.303629 0.302135 0.932352 -0.198578 0.72932 0.561844 -0.390415 +0.331168 0.923733 -0.192469 0.818119 0.474842 -0.324355 0.503632 0.835465 -0.219894 +0.603828 0.660806 -0.445788 0.068531 0.690567 0.720014 0.235746 0.672876 0.701186 +0.556036 0.598573 0.576658 0.832285 0.406103 -0.377336 0.624858 0.530528 -0.572793 +0.835638 0.399195 -0.377297 0.624736 0.577347 -0.525715 0.0769295 0.708065 0.701945 +0.336239 0.726513 0.599267 0.54453 0.585118 0.600936 0.836482 0.34484 -0.42589 +0.691545 0.466157 -0.551782 0.808734 0.224331 -0.543714 0.477669 0.271521 -0.835529 +0.178254 0.877701 0.444822 0.105262 0.674862 0.730398 0.565081 0.746105 0.352152 +0.586664 0.707266 0.394462 0.332576 0.863666 0.37878 0.788665 -0.119094 -0.603178 +0.391506 -0.189687 -0.900412 -0.129983 0.568807 0.812135 0.0679729 0.485193 0.871761 +0.406932 0.615678 0.674794 0.621803 -0.280907 -0.731062 0.467099 -0.418331 -0.778985 +0.272071 -0.594969 -0.7563 0.0461893 0.506064 0.861258 0.608918 -0.290447 -0.738146 +0.374706 -0.219988 -0.900667 0.0289642 0.649955 0.759421 0.0665921 0.764915 0.640679 +0.455769 -0.403299 -0.793489 0.477968 -0.389905 -0.787096 -0.291861 0.615737 0.731905 +-0.316043 0.708936 0.630497 -0.0907583 0.784502 0.613449 0.377485 -0.525974 -0.762139 +0.316326 -0.468447 -0.824921 -0.275206 0.602193 0.749416 -0.208311 0.384109 0.899481 +0.0806337 -0.311241 -0.946904 0.150897 -0.613904 -0.774824 -0.332478 0.526839 0.78224 +-0.212777 0.397266 0.892696 -0.37139 -0.36887 -0.852059 -0.143211 0.567554 0.810785 +-0.246111 0.537364 0.80664 -0.514119 -0.242912 -0.822603 -0.521496 -0.468311 -0.713251 +-0.572608 -0.315338 -0.756758 -0.26947 0.533239 0.801899 -0.191283 0.416601 0.888738 +-0.602888 -0.0677934 -0.794941 -0.637338 -0.115957 -0.76181 -0.0723047 0.56585 0.821332 +-0.651001 0.0158283 -0.758912 -0.708887 -0.0334014 -0.704531 -0.0429543 0.83897 0.54248 +-0.0294367 0.565502 0.824222 0.167016 0.755029 0.634064 0.0109784 0.839913 0.54261 +-0.651049 -0.216133 -0.727614 -0.630963 -0.40051 -0.664438 -0.485131 0.512362 0.708613 +-0.304015 0.48741 0.818539 -0.563414 -0.626709 -0.538331 -0.387945 -0.85138 -0.35306 +-0.489157 -0.638455 -0.594223 -0.426607 -0.803768 -0.414685 -0.516014 0.385896 0.764731 +-0.414273 0.177302 0.892716 -0.431437 -0.182998 -0.883388 -0.543683 -0.478534 -0.689502 +-0.63013 -0.382548 -0.675717 -0.418287 -0.147795 0.89621 -0.615773 -0.0748379 -0.784361 +-0.406443 -0.0480823 -0.91241 -0.664836 -0.230873 0.710416 -0.59563 -0.442239 0.670559 +-0.673658 -0.367843 0.640996 -0.631819 0.0123168 -0.775018 -0.629063 -0.123054 -0.767553 +-0.66369 -0.289233 0.689826 -0.704107 -0.284513 0.650604 -0.683164 0.00112523 -0.730264 +-0.71906 -0.00150372 -0.694947 -0.649735 -0.438404 0.621005 -0.761675 -0.38751 0.519315 +-0.735745 -0.502715 0.453824 -0.695299 0.0258422 -0.718256 -0.711691 0.0392647 -0.701394 +-0.834597 -0.314235 0.452443 -0.761629 -0.560504 0.325201 -0.912282 -0.368229 0.179302 +-0.768408 -0.554796 0.318985 -0.819579 -0.499172 0.281279 -0.666921 -0.606464 0.432918 +-0.685233 -0.0306251 -0.72768 -0.691382 -0.10532 -0.714772 -0.870637 -0.463951 0.163529 +-0.884475 -0.461176 0.0708579 -0.692901 -0.528952 0.489998 -0.746321 -0.558189 0.362532 +-0.663065 -0.575373 0.478844 -0.650792 -0.304147 -0.695675 -0.645952 -0.361431 -0.672394 +-0.700006 -0.56489 0.43691 -0.7249 -0.676088 0.132001 -0.71247 -0.611376 0.344393 +-0.553546 -0.779383 0.293511 -0.591588 -0.675156 0.440668 -0.713501 -0.00431085 -0.700641 +-0.606556 -0.381548 -0.697504 -0.650206 -0.165292 -0.74156 -0.936874 0.337975 0.0896692 +-0.36387 -0.760116 0.538351 -0.515128 -0.618748 0.593122 -0.845664 0.0690967 -0.529224 +-0.576926 -0.748245 -0.327545 -0.899989 -0.268978 -0.343032 -0.57792 -0.79955 -0.163489 +-0.998774 -0.048841 0.00810959 -0.761174 -0.610013 0.220225 -0.93966 0.212327 0.268246 +-0.617994 0.617106 -0.487098 -0.811292 0.00866502 0.584577 -0.28367 0.802936 -0.524238 +-0.506053 0.422461 -0.751956 -0.86257 -0.0508461 0.503377 -0.996364 0.0481979 0.0702489 +-0.888991 0.375117 -0.262646 -0.884448 0.463451 -0.0544434 -0.918745 0.301529 0.254929 +-0.772808 -0.449548 0.447968 -0.932899 -0.315028 0.174517 -0.79429 -0.537266 0.283633 +-0.905959 0.0520801 0.42015 -0.827573 0.538808 -0.157511 -0.799206 -0.090999 0.594129 +-0.791174 0.608617 0.0602409 -0.723769 0.580751 0.372674 -0.694012 0.545692 -0.469646 +-0.581201 0.40549 -0.705538 -0.741695 0.567321 -0.35782 -0.926263 -0.165101 0.33879 +-0.999917 -0.00305308 -0.0124868 -0.998997 0.024966 -0.0371612 -0.999378 0.0203006 0.0288477 +-0.950741 0.0965799 0.294555 -0.697071 0.406783 0.590441 -0.766698 0.310988 0.561658 +-0.673523 0.506178 -0.538657 -0.730664 0.406877 -0.548252 -0.700889 0.506873 -0.501831 +-0.835766 -0.304266 0.457074 -0.994162 -0.068573 0.083308 -0.9988 0.00107759 -0.0489726 +-0.999021 -0.0194079 0.0397593 -0.924364 -0.00348362 0.381495 -0.79799 0.0813039 0.597162 +-0.634095 0.535235 -0.558075 -0.629388 0.574017 -0.523809 -0.540845 0.561158 -0.626569 +-0.853071 -0.376028 0.361764 -0.998001 -0.0580491 0.0249886 -0.99969 0.0183914 0.0167611 +-0.923969 -0.0133195 0.382236 -0.790305 -0.0273128 0.612105 -0.567249 0.571579 -0.592896 +-0.396026 0.49964 -0.770404 -0.640228 0.759224 0.116988 -0.58715 0.755274 0.291232 +-0.731052 0.679365 -0.0634527 -0.891211 0.451377 -0.0447441 -0.92381 0.382799 -0.00625109 +-0.866505 0.453789 -0.207956 -0.863031 -0.350514 0.363754 -0.994972 -0.0248796 0.0970081 +-0.93733 0.0548799 0.344095 -0.799045 -0.00397658 0.601258 -0.33371 0.642732 -0.68959 +-0.285179 0.700369 -0.654336 -0.414796 0.869009 0.269754 -0.382476 0.704129 0.59826 +-0.54926 0.835462 -0.0177937 -0.434923 0.899918 -0.031449 -0.448651 0.893128 0.0321626 +-0.676109 0.733382 0.0708986 -0.759662 0.588688 -0.276332 -0.834451 0.486535 -0.258797 +-0.761311 0.525585 -0.379693 -0.911618 0.40886 -0.0422572 -0.911416 0.381638 -0.153862 +-0.824572 0.409599 -0.390269 -0.89324 -0.130748 0.430147 -0.868585 0.206102 0.450646 +-0.811497 0.181927 0.555315 -0.327481 0.633988 -0.700582 -0.251524 0.359717 -0.898521 +-0.397518 0.56787 0.720765 -0.234831 0.315279 0.919486 -0.815676 0.556778 -0.157071 +-0.828079 0.55905 -0.0418113 -0.764237 0.573284 -0.295445 -0.830284 0.411798 -0.375568 +-0.717912 0.521353 -0.461295 -0.901848 0.369542 -0.223851 -0.870196 0.448747 -0.203435 +-0.724167 0.120453 0.679024 -0.776029 0.372718 0.508783 -0.71098 0.296903 0.637461 +-0.0546952 0.0182064 -0.998337 -0.250619 0.132523 0.958972 -0.689203 0.507389 -0.517258 +-0.850946 0.370926 -0.371893 -0.687511 0.516474 -0.510473 -0.987189 0.128312 -0.0948405 +-0.86282 0.363329 -0.351473 -0.867323 0.0182174 0.497413 -0.771172 -0.0510714 0.634575 +-0.0250249 0.00317389 -0.999682 -0.0481895 0.025739 0.998506 -0.711253 0.42895 -0.556885 +-0.944315 0.137521 -0.298928 -0.789091 0.138606 -0.598435 -0.99741 0.0458637 -0.0554035 +-0.928801 0.329981 -0.168647 -0.951051 0.294895 0.0924092 -0.996863 -0.0547981 0.0571147 +-0.70837 -0.475724 0.521439 -0.849594 -0.395101 0.349406 -0.659169 -0.568844 0.491846 +-0.0260697 0.0158084 -0.999535 -0.0174726 0.0111046 0.999786 -0.9252 0.0168017 -0.379107 +-0.795672 0.0535441 -0.603357 -0.999007 -0.0416383 -0.0158456 -0.999939 -0.00747852 0.00814799 +-0.999212 0.0349144 -0.0188579 -0.999284 0.0354954 -0.0130794 -0.998099 -0.0170122 0.0592444 +-0.648346 -0.498986 0.575032 -0.83435 -0.13451 0.534572 -0.538543 -0.136393 0.831486 +-0.0494909 -0.00708915 -0.998749 0.00412441 -0.0142215 0.99989 -0.859851 0.196278 -0.471308 +-0.740291 0.307896 -0.597636 -0.994274 0.0389531 -0.0995092 -0.998415 -0.0464229 0.0318296 +-0.999343 -0.0161312 0.0324498 -0.999364 0.0319863 0.015786 -0.946329 0.289577 0.143549 +-0.895871 0.318107 0.310198 -0.53994 0.113864 0.833966 -0.178433 -0.327369 -0.927896 +-0.000908217 -0.00244583 0.999997 -0.693139 0.475813 -0.541443 -0.844167 0.369788 -0.388122 +-0.672539 0.526569 -0.520016 -0.993526 0.104384 -0.0448256 -0.996425 0.036602 0.0761402 +-0.898369 0.308394 0.312772 -0.79278 0.590041 0.152811 -0.734034 0.555193 0.391094 +-0.775797 0.486469 0.401855 -0.23942 -0.477925 -0.845143 -0.222688 -0.586209 -0.778954 +-0.364201 -0.585952 -0.723891 -0.0022767 0.0212184 0.999772 -0.671484 0.51417 -0.533608 +-0.844431 0.451274 -0.288595 -0.693569 0.556745 -0.457162 -0.901354 0.406478 0.149457 +-0.698376 0.499124 0.512979 -0.694553 0.52421 0.492747 -0.0130367 -0.0850553 -0.996291 +0.0115248 -0.0478287 0.998789 -0.783983 0.614154 -0.0904729 -0.740387 0.595562 0.311661 +0.0358786 0.0273808 -0.998981 0.0158094 -0.370117 0.928851 0.0222909 0.0029458 -0.999747 +0.0789562 -0.603686 0.793302 0.0101058 -0.381071 0.924491 0.0200653 -0.00968102 -0.999752 +-0.00516104 -0.0440211 0.999017 0.0238845 -0.0184603 -0.999544 -0.0105828 0.00548058 0.999929 +0.0126239 0.0452066 -0.998898 -0.00126195 0.00178183 0.999998 -0.131683 0.277057 -0.951787 +0.169954 0.323435 0.930863 -0.703338 -0.530627 -0.473023 -0.744747 -0.598342 -0.295532 +-0.900301 -0.412411 -0.139197 -0.781356 -0.615747 0.101679 -0.843168 -0.477743 0.246637 +-0.713107 -0.581611 0.391417 -0.229531 0.579612 -0.781899 -0.224064 0.389717 -0.893261 +0.254225 0.595926 0.761736 0.273326 0.481076 0.832982 0.366539 0.58043 0.727152 +-0.702218 -0.501082 -0.505773 -0.694824 -0.506985 -0.510084 -0.900242 -0.30507 -0.310639 +-0.997421 -0.0366239 -0.0617323 -0.995934 -0.0863916 0.0255172 -0.726356 -0.602371 0.330992 +-0.932552 -0.309106 0.186547 -0.774158 -0.573519 0.267874 -0.839782 -0.319698 0.438815 +-0.755608 -0.393281 0.52382 -0.0919857 0.0122755 -0.995685 0.0672275 0.102837 0.992424 +-0.738517 -0.470313 -0.483112 -0.72487 -0.489591 -0.484628 -0.909153 -0.280798 -0.30756 +-0.998833 -0.0158399 -0.0456195 -0.999695 0.0181646 -0.0167597 -0.998424 0.0554198 -0.00888228 +-0.999706 0.00787867 0.0229302 -0.930953 -0.172234 0.321966 -0.766557 -0.205601 0.608374 +-0.422861 -0.357494 -0.832698 -0.0731803 -0.0809528 0.994028 -0.745605 -0.476658 -0.465693 +-0.75936 -0.457411 -0.462761 -0.897371 -0.325355 -0.298109 -0.993165 -0.110467 -0.0376956 +-0.998466 -0.0526244 -0.017196 -0.999327 -0.0326014 0.0167892 -0.999583 -0.0284415 0.00508126 +-0.999492 -0.0132981 -0.0289509 -0.995824 -0.0842871 0.0350846 -0.703803 -0.411732 0.578912 +-0.819941 -0.418087 0.391025 -0.687432 -0.512653 0.514415 -0.752391 -0.157239 -0.639674 +-0.674561 -0.452523 -0.583258 -0.648164 -0.319185 -0.691379 -0.382584 -0.37618 0.843871 +-0.702172 -0.504696 -0.502231 -0.726939 -0.473804 -0.49706 -0.825279 -0.40683 -0.391668 +-0.890058 -0.41287 -0.193226 -0.924597 -0.380234 -0.0232997 -0.936804 -0.349263 0.0203249 +-0.937703 -0.347128 0.0146893 -0.934235 -0.356534 0.00940203 -0.936526 -0.350295 -0.0146094 +-0.941146 -0.335261 -0.0429437 -0.913511 -0.395065 0.097062 -0.655886 -0.55631 0.510229 +-0.757052 -0.506913 0.412203 -0.680342 -0.543324 0.491868 -0.658653 -0.0606421 -0.749999 +-0.74483 -0.0179607 -0.667013 -0.43229 -0.55334 0.711997 -0.523727 -0.502027 0.688244 +-0.385907 -0.382717 0.839407 -0.723902 -0.488936 -0.486732 -0.714131 -0.500589 -0.489313 +-0.82093 -0.399531 -0.407982 -0.818587 -0.419729 -0.392101 -0.663611 -0.657883 0.356104 +-0.647864 -0.723882 0.237206 -0.705091 -0.628751 0.327901 -0.642551 -0.711673 0.28399 +-0.590364 -0.63276 0.501085 -0.617597 -0.147514 -0.772537 -0.629155 -0.170649 -0.758316 +-0.195012 -0.320068 0.927107 -0.691599 -0.522352 -0.498838 -0.71305 -0.489291 -0.502149 +-0.832015 -0.404734 -0.379396 -0.832652 -0.378195 -0.404549 -0.602589 -0.531199 0.595579 +-0.751767 -0.415158 0.512337 -0.64164 -0.601849 0.475475 -0.5896 -0.365284 -0.720375 +-0.541924 -0.246479 0.803471 -0.355346 -0.500822 0.789244 -0.319146 -0.444931 0.836769 +-0.604804 -0.564777 -0.561462 -0.668151 -0.520771 -0.531386 -0.716242 -0.574678 -0.395908 +-0.835702 -0.455937 -0.306144 -0.818077 -0.448776 0.359653 -0.696436 -0.644983 0.3146 +-0.83278 -0.50142 0.23464 -0.771778 -0.631647 0.0733602 -0.896107 -0.443076 0.0259859 +-0.802394 -0.596783 -0.00382814 -0.856684 -0.511166 -0.0692913 -0.792331 -0.56842 -0.221608 +-0.724075 -0.546824 -0.420356 -0.597978 -0.300789 0.742932 -0.527857 -0.314817 0.788833 +-0.562461 -0.576537 -0.592656 -0.537952 -0.602005 -0.590082 -0.557535 -0.561559 -0.611396 +-0.869207 -0.468519 0.158019 -0.878248 -0.437192 -0.193761 -0.57515 -0.249148 0.779184 +-0.629696 -0.574717 -0.522669 -0.588325 -0.549052 -0.593646 -0.715386 -0.440913 -0.542051 +-0.780874 -0.527525 0.334595 -0.702252 -0.542238 0.461325 -0.826482 -0.562165 0.0299605 +-0.852716 -0.522254 -0.0112529 -0.799864 -0.600102 -0.0097511 -0.770092 -0.624045 -0.132385 +-0.738709 -0.652766 -0.167945 -0.794841 -0.559108 -0.235851 -0.83788 -0.338556 -0.428178 +-0.821149 -0.566027 -0.0729935 0.120596 0.941211 0.315561 0.303675 0.72727 0.615516 +0.458327 0.732123 0.503916 0.0632521 0.93822 -0.34021 -0.068066 0.793059 -0.60533 +0.125311 0.732921 0.668673 -0.0649955 0.646474 0.760163 -0.0413289 0.929103 -0.367505 +-0.0583499 0.998071 -0.0212053 0.0221231 0.99706 -0.0733634 0.476236 0.618312 -0.625211 +0.262391 0.87266 -0.411844 0.244772 0.692853 -0.678263 -0.287901 0.617434 0.732044 +-0.29981 0.662007 0.686921 0.506608 0.554318 -0.660364 0.359978 0.658444 -0.66096 +0.161445 0.46651 -0.869657 -0.33115 0.692299 0.641141 -0.108684 0.776143 0.621119 +-0.0203703 -0.0250997 -0.999477 -0.24531 0.843436 0.477953 -0.194158 0.569869 0.798469 +-0.173241 0.918148 0.35636 -0.192651 -0.480325 -0.855671 -0.285149 0.52738 0.80035 +-0.278208 0.505477 0.816758 -0.0504652 -0.341468 -0.938537 -0.301135 -0.589237 -0.749745 +-0.221047 0.59917 0.769502 -0.129193 0.584488 0.801051 -0.0298539 -0.132682 -0.990709 +-0.0744279 0.679896 0.729522 -0.0217431 0.731795 0.681178 0.329767 -0.576143 -0.747872 +0.0682346 -0.394048 -0.916553 -0.088447 0.680844 0.727069 -0.199801 0.615026 0.762773 +0.059062 -0.562929 -0.824392 -0.147117 -0.627811 -0.764336 -0.313278 0.594762 0.740348 +-0.209471 0.417488 0.884209 -0.462111 -0.633631 -0.620456 -0.121292 -0.724379 -0.678648 +-0.114409 0.486147 0.866355 -0.561298 -0.380037 -0.735198 -0.696806 -0.427667 -0.575814 +-0.514453 -0.535219 -0.669984 -0.0807131 0.601265 0.794963 -0.0819277 0.495247 0.864881 +-0.533988 -0.29135 -0.793708 -0.306727 0.512567 0.801993 -0.207159 0.443256 0.872129 +-0.671448 -0.247593 -0.698467 -0.58416 -0.371276 -0.721742 -0.628841 -0.290766 -0.72112 +-0.166957 0.587164 0.792063 -0.121636 0.631064 0.766135 -0.697268 -0.0609792 -0.714212 +-0.745138 -0.0564536 -0.664517 -0.389821 0.612777 0.687418 -0.400712 0.596505 0.695422 +-0.161409 0.742627 0.649963 -0.69707 -0.18708 -0.692166 -0.693219 -0.421402 -0.584695 +-0.555607 0.418815 0.718258 -0.429048 0.314049 0.84693 -0.391744 -0.392771 -0.832026 +-0.519151 -0.733263 -0.439098 -0.50872 -0.65134 -0.562992 -0.544947 -0.797112 -0.260086 +-0.589388 0.107587 0.800654 -0.581757 0.18292 0.792527 -0.367355 -0.166365 -0.915081 +-0.611636 -0.277637 0.740823 -0.692491 -0.220412 0.686932 -0.511151 -0.25593 -0.820502 +-0.631733 -0.488982 0.601507 -0.7639 -0.422219 0.488045 -0.645495 -0.644759 0.409416 +-0.47179 -0.188841 -0.861251 -0.555107 -0.361623 -0.749056 -0.612912 -0.300984 -0.73058 +-0.843255 -0.324327 0.42864 -0.721988 -0.633174 0.27897 -0.92234 -0.342347 0.179128 +-0.758601 -0.590599 0.275169 -0.840872 -0.289012 0.457609 -0.520311 -0.361053 0.773897 +-0.632549 -0.0720884 -0.771158 -0.426811 -0.050038 -0.902955 -0.883769 -0.416997 0.212287 +-0.909255 -0.266504 0.319737 -0.622335 -0.394106 0.676298 -0.654285 0.0212663 -0.755949 +-0.622566 -0.0982669 -0.776373 -0.687202 -0.612079 0.391296 -0.755778 -0.53423 0.37868 +-0.754662 -0.616838 0.223599 -0.812277 -0.489996 0.316403 -0.746381 -0.539991 0.389006 +-0.691368 -0.0201492 -0.722222 -0.714179 -0.0938211 -0.693647 -0.813771 -0.497563 0.300345 +-0.722363 -0.501198 0.476437 -0.774248 -0.491583 0.398605 -0.716443 -0.513372 0.472396 +-0.662261 -0.275343 -0.696847 -0.716383 -0.376604 -0.587337 -0.715009 -0.592079 0.371759 +-0.718372 -0.568643 0.400733 -0.775967 -0.569388 0.271426 -0.801711 -0.548685 0.237073 +-0.727294 -0.599712 0.33375 -0.613957 -0.709084 -0.34678 -0.702802 -0.648689 -0.292015 +-0.66109 -0.725885 -0.189869 -0.747027 -0.611139 -0.261646 -0.82678 -0.463762 -0.31837 +-0.822131 -0.519511 0.23283 -0.663592 -0.743872 0.0793661 -0.899055 -0.428415 -0.0903395 +-0.574309 -0.80012 -0.17314 -0.969955 0.203775 0.132905 -0.778781 0.557583 -0.287405 +-0.968087 0.183613 0.170569 -0.927714 0.373237 0.00640902 -0.950228 0.275454 0.145574 +-0.997211 0.0648588 0.0369214 -0.927774 0.18129 -0.326143 -0.8926 0.173782 0.416011 +-0.997829 0.0466272 0.0465017 -0.998623 0.0504703 -0.0142967 -0.99875 0.0462998 0.0188223 +-0.997944 0.0371647 0.0522152 -0.954748 0.297405 0.00256914 -0.827257 0.384948 -0.409219 +-0.933735 0.00598273 0.357916 -0.99923 -0.010185 0.0378841 -0.999917 -0.00980265 -0.00836116 +-0.999139 0.0153995 0.0385362 -0.899683 0.305424 0.311909 -0.791982 0.598108 -0.122605 +-0.750009 0.563999 0.345532 -0.798287 0.520642 -0.302769 -0.78412 0.541427 -0.303335 +-0.732975 0.53998 -0.413726 -0.933333 -0.0267263 0.358015 -0.99917 -0.000627923 0.0407211 +-0.999673 0.0213756 0.0140134 -0.901828 0.288146 0.321992 -0.726384 0.48719 0.48478 +-0.691607 0.506243 0.515168 -0.763357 0.589293 -0.264613 -0.666609 0.634784 -0.390745 +-0.681272 0.564124 -0.466512 -0.535274 0.579196 -0.614828 -0.937241 0.0177583 0.348231 +-0.996462 -0.0135516 0.0829491 -0.954536 0.118145 0.273685 -0.709448 0.462736 0.531562 +-0.748564 0.296076 0.593289 -0.568564 0.57732 -0.586036 -0.642241 0.540498 -0.543497 +-0.49318 0.616675 -0.613584 -0.576245 0.728908 0.369641 -0.628949 0.7767 0.0340709 +-0.619033 0.785261 -0.0127778 -0.594733 0.803515 -0.0256151 -0.710874 0.691536 -0.128206 +-0.814923 0.527891 -0.239232 -0.956453 0.153011 0.248568 -0.995948 0.00521061 0.0897751 +-0.93317 0.0382026 0.3574 -0.798684 0.0688004 0.597804 -0.585392 0.582067 -0.564371 +-0.579943 0.603252 -0.547498 -0.493592 0.59895 -0.630576 -0.574228 0.248894 0.779945 +-0.688446 0.457846 0.562511 -0.764379 0.577592 0.286554 -0.790057 0.612908 0.0124283 +-0.780674 0.624916 0.00529343 -0.7658 0.638459 -0.076944 -0.734586 0.606255 -0.304694 +-0.792043 0.532746 -0.298078 -0.762964 0.538652 -0.357408 -0.806447 0.525468 -0.271157 +-0.907122 0.415477 -0.0671422 -0.884887 0.434087 -0.168946 -0.922443 0.140518 0.359657 +-0.997098 0.0622513 0.0438155 -0.996966 0.0549897 0.0550851 -0.904493 0.191819 0.380917 +-0.806951 0.146475 0.572168 -0.536222 0.532936 -0.654557 -0.351682 0.172537 -0.920082 +-0.584339 0.296179 0.75553 -0.747511 0.333947 0.574201 -0.776981 0.579876 -0.245039 +-0.714702 0.616573 -0.33021 -0.839042 0.431069 -0.331946 -0.699014 0.547422 -0.460117 +-0.982051 0.161614 -0.0972486 -0.865396 0.362902 -0.34553 -0.867947 -0.27921 0.410743 +-0.990816 -0.0288986 0.132091 -0.958735 0.152669 0.239831 -0.772991 0.329386 0.542209 +-0.786191 0.294468 0.543316 -0.321655 0.0104957 -0.946799 -0.572116 0.469701 0.672357 +-0.288998 0.262534 0.920628 -0.676996 0.529377 -0.511308 -0.858468 0.348618 -0.376162 +-0.720702 0.478252 -0.501861 -0.988855 0.0740725 -0.129145 -0.850818 0.27915 -0.44518 +-0.82619 -0.295236 0.47984 -0.921894 0.14894 0.357672 -0.821534 0.220635 0.52574 +-0.360241 -0.00239954 -0.932856 -0.0296999 0.0377578 0.998845 -0.746722 0.324594 -0.580556 +-0.949226 0.0322883 -0.312933 -0.798329 -0.178324 -0.575214 -0.921772 0.0292235 -0.386629 +-0.749916 0.225425 0.62194 -0.444985 0.181054 0.877045 -0.387532 -0.0736778 -0.918907 +0.000948467 0.00511607 0.999986 -0.825989 0.199834 -0.527075 -0.771587 -0.295994 -0.563064 +-0.890002 -0.0394646 -0.454245 -0.920459 0.313379 -0.233556 -0.928881 0.137581 -0.343878 +-0.574685 -0.105744 0.811514 -0.348976 -0.421691 -0.836895 0.023216 -0.00516744 0.999717 +-0.816055 0.537102 -0.213483 -0.759894 0.539831 -0.362137 -0.911594 0.242169 -0.332191 +-0.793554 0.269014 -0.545805 -0.849991 0.348854 -0.394735 -0.895341 0.395272 0.205242 +-0.918329 0.234449 0.318913 -0.699613 -0.253341 0.668102 -0.844242 -0.197058 0.498422 +-0.692032 -0.318981 0.647566 -0.112679 -0.310968 -0.943717 -0.477433 -0.392746 -0.786008 +0.0198447 0.0206552 0.99959 -0.860893 0.208861 -0.463941 -0.746815 0.308158 -0.589327 +-0.985988 0.0978185 -0.135125 -0.858782 0.367034 -0.357463 -0.899975 0.320699 0.295292 +-0.998244 0.0555188 0.0206602 -0.997738 0.0590048 0.0322053 -0.93358 0.000833212 0.358368 +-0.763896 -0.100064 0.637534 -0.0244702 0.00110254 -0.9997 0.0161747 -0.0423416 0.998972 +-0.731959 0.363151 -0.576505 -0.943033 0.135934 -0.303662 -0.785634 0.169325 -0.59507 +-0.997339 0.0516298 -0.0514739 -0.93238 0.326504 -0.155123 -0.945379 0.305682 0.113212 +-0.998835 0.0396522 0.0275193 -0.999962 0.0059141 -0.00645206 -0.999291 -0.0182736 0.0329323 +-0.939825 -0.00182123 0.34165 -0.812466 0.00272185 0.583002 -0.0191979 0.00668214 -0.999793 +0.00860556 -0.378668 0.925492 -0.924591 0.0209396 -0.380386 -0.802371 -0.00198059 -0.596822 +-0.999524 -0.0197113 -0.0237246 -0.999931 0.00544235 0.0103998 -0.999501 0.0314046 0.00347555 +-0.999986 0.00275403 0.00456586 -0.999417 -0.0135583 -0.031336 -0.999143 -0.0108039 0.0399544 +-0.928668 -0.00429122 0.370887 -0.810527 0.0178381 0.58543 -0.0372759 -0.00704195 -0.99928 +-0.006654 -0.610628 0.791889 0.00146898 -0.377369 0.926062 -0.925925 -0.00929623 -0.377594 +-0.806479 -0.0150808 -0.59107 -0.999058 -0.00476816 -0.0431207 -0.999609 -0.00985869 0.0261691 +-0.999876 -0.0139525 0.00730543 -0.999887 -0.0127768 -0.00796906 -0.999665 -0.00620707 -0.025126 +-0.999074 0.000939633 0.0430138 -0.924454 0.00763755 0.381218 -0.794676 0.0157196 0.60683 +-0.0404808 -0.00781818 -0.99915 7.36261e-06 -0.0459032 0.998946 -0.929858 0.00159883 -0.367915 +-0.809957 0.00188477 -0.586486 -0.999102 0.0082207 -0.0415624 -0.999637 0.01331 0.023408 +-0.999892 0.0141335 0.00415092 -0.999905 0.011166 -0.00807022 -0.999723 0.00824322 -0.0220435 +-0.999261 -0.00111172 0.038428 -0.921531 -0.0279039 0.387302 -0.789518 -0.00219122 0.613723 +-0.0346188 -0.0144493 -0.999296 -0.011671 -0.00589484 0.999914 -0.931879 0.0172529 -0.362358 +-0.813877 0.0238039 -0.580549 -0.999322 0.0133058 -0.0343304 -0.999929 -0.00323773 0.0114407 +-0.99949 -0.0310387 0.00747395 -0.999914 -0.00674452 0.0112838 -0.999094 0.0332682 -0.0265406 +-0.999978 0.00583012 -0.00307714 -0.932193 -0.159411 0.324969 -0.77483 -0.176618 0.606996 +-0.0409195 0.00819515 -0.999129 0.0460056 0.0807075 0.995676 -0.931298 0.0047866 -0.364226 +-0.821613 -0.0521496 -0.567655 -0.998316 -0.0125922 -0.0566258 -0.999018 -0.0436616 -0.00749724 +-0.946218 -0.308577 -0.0972224 -0.930803 -0.332564 0.151679 -0.996242 -0.0784915 0.0366213 +-0.99946 0.00645477 -0.0322315 -0.99754 -0.0287114 0.0639487 -0.717787 -0.380455 0.583126 +-0.843852 -0.239119 0.480349 -0.732458 -0.334277 0.593098 -0.0863938 0.053555 -0.994821 +0.245558 0.476053 0.844437 -0.946693 -0.12319 -0.297652 -0.793065 -0.274502 -0.543779 +-0.994354 -0.0381178 -0.0990282 -0.906848 -0.318182 -0.276381 -0.843306 -0.484142 0.233328 +-0.936776 -0.297954 0.183503 -0.99929 -0.0272063 0.0260534 -0.936054 -0.158057 0.314356 +-0.76372 -0.249547 0.595363 -0.349742 0.559534 -0.751401 -0.364998 0.295918 -0.882728 +0.340223 0.594358 0.728688 0.182572 0.342296 0.921684 -0.769183 -0.46197 -0.441521 +-0.768002 -0.433174 -0.471734 -0.906959 -0.287391 -0.307948 -0.985407 -0.114798 -0.125675 +-0.903026 -0.303916 -0.30361 -0.943083 -0.143269 0.300115 -0.996489 -0.0261572 0.0795265 +-0.724147 -0.361583 0.587256 -0.853001 -0.222406 0.472148 -0.735235 -0.322566 0.596138 +-0.458175 0.433903 -0.77576 -0.468619 -0.151417 -0.870327 -0.0586337 -0.0623197 0.996332 +-0.817097 -0.205375 -0.538677 -0.792331 -0.364247 -0.489424 -0.895541 -0.24405 -0.372083 +-0.979114 -0.141949 -0.145552 -0.903731 -0.308851 -0.296447 -0.849221 -0.271969 0.452611 +-0.995786 -0.0375308 0.0836734 -0.945912 -0.139268 0.293009 -0.768726 -0.247882 0.589588 +-0.822784 -0.360277 -0.439577 -0.532941 -0.772483 -0.345315 -0.760301 -0.567964 -0.315213 +-0.59747 -0.686745 -0.414017 -0.676905 -0.554793 -0.483739 -0.387492 -0.394731 0.833089 +-0.88524 -0.221424 -0.409049 -0.759338 -0.298632 -0.578122 -0.900251 -0.293324 -0.321728 +-0.868633 -0.364387 0.335707 -0.988148 -0.107323 0.109755 -0.736588 -0.356676 0.574648 +-0.870523 -0.21025 0.444954 -0.734658 -0.316787 0.599937 -0.778287 -0.252973 -0.574694 +-0.763902 -0.0870539 -0.639434 -0.528313 -0.506409 0.681495 -0.526867 -0.52135 0.671272 +-0.375073 -0.372212 0.848987 -0.740245 -0.482741 -0.467973 -0.759967 -0.430522 -0.48693 +-0.811554 -0.387425 -0.437358 -0.838758 -0.376238 -0.393611 -0.868933 -0.427767 0.248941 +-0.919365 -0.265297 0.290491 -0.763201 -0.272953 0.585679 -0.763349 -0.10952 -0.636635 +-0.716715 -0.262011 -0.646274 -0.34018 -0.254396 0.905296 -0.797628 -0.20385 -0.567657 +-0.788467 -0.364111 -0.495724 -0.807171 -0.342939 -0.480486 -0.816599 -0.401881 -0.414315 +-0.611766 -0.542245 0.575946 -0.530996 -0.578219 0.61944 -0.613905 -0.556254 0.560091 +-0.743043 -0.54657 -0.386196 -0.77395 -0.410995 -0.481752 -0.870729 -0.316764 -0.376154 +-0.856746 -0.045467 -0.513731 -0.472912 -0.289246 0.832281 -0.702567 -0.431566 -0.565818 +-0.698402 -0.338215 -0.63075 -0.656972 -0.645091 0.390187 -0.669034 -0.724742 0.16475 +-0.758846 -0.644677 -0.0924375 -0.779867 -0.609999 -0.140389 -0.88611 -0.457662 -0.073176 +-0.87039 -0.256379 -0.420346 -0.266307 -0.570484 0.776935 -0.439487 -0.534163 0.722164 +-0.643918 -0.54496 -0.537018 -0.722903 -0.454321 -0.520581 -0.527623 -0.5796 -0.62103 +-0.845631 -0.392222 0.362036 -0.579958 -0.663143 0.473171 -0.56083 -0.79557 0.229211 +-0.788412 -0.561107 0.252123 -0.622394 -0.709696 0.330088 -0.645156 -0.597377 -0.476356 +-0.697838 -0.490337 -0.522103 -0.527588 -0.596059 -0.60528 -0.881564 -0.372133 0.29045 +-0.914088 -0.402312 0.050866 -0.704715 -0.63878 0.308768 -0.763888 -0.638186 0.0958825 +-0.73361 -0.674094 -0.0861006 -0.630419 -0.691782 -0.352149 -0.481065 0.858128 0.179424 +-0.14562 0.782746 0.605064 -0.263349 0.90963 -0.32128 -0.437661 0.720104 -0.538427 +-0.238538 0.784555 0.572339 -0.0472218 0.485815 0.872785 -0.378107 0.853104 -0.359511 +-0.364395 0.931201 -0.00900186 -0.360318 0.932814 -0.00542662 -0.161273 0.93332 -0.320789 +-0.289934 0.737228 -0.610273 -0.423895 0.500591 0.7548 -0.405997 0.229586 0.884566 +-0.262103 0.620914 -0.738761 -0.329218 0.192978 -0.924324 -0.412911 0.716842 0.56182 +-0.560335 0.635167 0.531589 -0.607323 0.460929 0.647072 -0.435582 -0.30059 -0.848478 +-0.429732 0.57548 0.695812 -0.589264 0.67118 0.449762 -0.553706 0.79015 0.262818 +-0.594688 0.696933 0.400787 -0.390168 -0.392828 -0.83274 -0.542089 -0.431156 -0.721279 +-0.472972 -0.524931 -0.707634 -0.693207 0.452538 0.560957 -0.410548 0.490388 0.768746 +-0.356192 -0.190758 -0.914734 -0.421493 0.627706 0.654469 -0.36788 0.655643 0.659391 +-0.405102 -0.276713 -0.871391 -0.349805 0.683447 0.640731 -0.304493 0.65426 0.692263 +-0.542154 -0.443954 -0.713425 -0.615832 -0.397303 -0.680368 -0.389707 -0.484015 -0.783491 +-0.348729 0.65939 0.666028 -0.225285 0.461249 0.858194 -0.770744 -0.564342 -0.295755 +-0.613101 -0.789956 0.00878189 -0.7929 -0.605339 -0.0698253 -0.740031 -0.65176 0.166021 +-0.366103 -0.576757 -0.730287 -0.431518 0.499534 0.751171 -0.372694 0.444097 0.814786 +-0.810339 -0.135913 -0.569981 -0.77534 -0.377238 -0.506497 -0.812439 -0.346039 -0.469255 +-0.834412 -0.328862 -0.442274 -0.607917 -0.785967 -0.112663 -0.522143 0.457325 0.719876 +-0.374101 0.587596 0.717481 -0.766095 -0.210776 -0.607183 -0.787718 -0.0904215 -0.609364 +-0.311118 0.602683 0.734832 -0.153227 0.431276 0.889113 -0.617711 -0.295555 -0.728753 +-0.706493 -0.318103 -0.632201 -0.0789562 0.456062 0.886439 -0.736097 -0.269629 -0.620856 +-0.742817 -0.368093 -0.559223 -0.639105 -0.37412 -0.671997 -0.00502538 0.623515 0.781796 +-0.142735 0.646897 0.7491 -0.77401 -0.195036 -0.602386 -0.717692 -0.308213 -0.624437 +-0.3548 0.549232 0.756612 -0.393102 0.454617 0.799246 -0.608672 -0.419482 -0.673464 +-0.731834 -0.403296 -0.549337 -0.747346 -0.490194 -0.448534 -0.839717 -0.31848 -0.439824 +-0.631279 0.171265 0.756409 -0.591378 0.230848 0.772646 -0.479203 -0.273905 -0.833871 +-0.68546 -0.0259447 0.727648 -0.740942 0.0117387 0.671466 -0.510818 -0.260673 -0.819216 +-0.548247 -0.307817 -0.777608 -0.544697 -0.286723 -0.788096 -0.677395 -0.297864 0.672616 +-0.753386 -0.273977 0.597785 -0.559538 -0.24885 -0.790564 -0.527553 -0.24166 -0.814425 +-0.690077 -0.60927 0.390619 -0.654603 -0.726893 0.207658 -0.688081 -0.67635 0.262862 +-0.623013 -0.666668 0.409156 -0.426832 -0.446525 0.786403 -0.460686 -0.0893927 -0.88305 +-0.421629 -0.415989 0.805718 -0.51423 -0.214364 -0.830431 -0.671827 -0.700723 0.240074 +-0.644076 -0.665778 0.376704 -0.72874 -0.683819 0.0364527 -0.788776 -0.586096 0.185265 +-0.737726 -0.6167 0.274666 -0.826525 -0.457628 0.327768 -0.6628 -0.586103 0.466025 +-0.711248 -0.290189 -0.640247 -0.587389 -0.348226 -0.730557 -0.611762 -0.295989 -0.733579 +-0.822516 -0.420939 0.382462 -0.710965 -0.47784 0.515943 -0.812066 -0.432968 0.391264 +-0.726112 -0.50752 0.463881 -0.752793 -0.198564 -0.627594 -0.76223 -0.377192 -0.526053 +-0.820637 -0.486815 0.299276 -0.753113 -0.580243 0.310064 -0.826305 -0.505564 0.248242 +-0.835195 -0.544548 0.0769239 -0.897174 -0.441107 0.0224442 -0.848204 -0.529504 -0.0132589 +-0.863559 -0.499167 -0.0714016 -0.836007 -0.522575 -0.167354 -0.816305 -0.493004 -0.300989 +-0.844593 -0.299214 -0.443997 -0.820408 -0.558234 0.123716 -0.839536 -0.512797 -0.179494 +-0.890108 0.448205 0.0825828 -0.912842 0.289562 0.287879 -0.993975 0.106973 0.023879 +-0.931288 0.339403 -0.132317 -0.848472 0.481234 -0.220246 -0.910762 0.14682 0.385949 +-0.996689 0.0487664 0.0650651 -0.997477 0.0433451 0.0562135 -0.952959 0.25218 0.168153 +-0.903898 0.427204 -0.021562 -0.768395 0.519629 -0.37357 -0.93919 0.0230047 0.342628 +-0.996064 -0.00593378 0.0884391 -0.958118 0.0996717 0.26847 -0.824474 0.523277 0.215464 +-0.80505 0.318331 0.500559 -0.794752 0.601837 -0.07849 -0.662278 0.636125 -0.395896 +-0.669393 0.568496 -0.478253 -0.55689 0.622944 -0.549376 -0.898958 0.310552 0.308921 +-0.924857 0.377704 0.0444863 -0.929482 0.3688 -0.00701523 -0.938072 0.345842 -0.0203422 +-0.884595 0.426551 -0.188535 -0.957698 0.129518 0.256983 -0.996268 -0.00159174 0.0862996 +-0.93592 0.0229701 0.351462 -0.797863 0.0560663 0.600226 -0.64857 0.48952 -0.582861 +-0.764958 0.175189 -0.619797 -0.627746 0.314168 -0.712204 -0.881463 0.35376 0.312854 +-0.919764 0.383967 0.0812565 -0.914582 0.403802 -0.022008 -0.931312 0.343436 -0.12128 +-0.989459 0.122809 -0.0767435 -0.944666 0.295536 -0.142354 -0.859923 0.45842 -0.224461 +-0.909233 0.180226 0.375252 -0.996273 0.0620586 0.0599122 -0.996916 0.0569897 0.0539431 +-0.906602 0.213624 0.36392 -0.795028 0.142842 0.589515 -0.627033 0.0586456 -0.776782 +-0.776165 0.0063997 -0.630498 -0.510015 0.542722 0.667336 -0.658062 0.577822 0.48278 +-0.804098 0.54751 0.231646 -0.83542 0.549052 0.0247919 -0.826887 0.558956 -0.0618517 +-0.754733 0.578438 -0.309495 -0.865186 0.363516 -0.345411 -0.71437 0.522204 -0.46581 +-0.997058 0.056627 -0.0516651 -0.997357 0.0691612 -0.0222852 -0.863237 0.356049 -0.357841 +-0.938998 0.180264 0.292896 -0.993434 0.0661596 0.0933329 -0.918404 0.292714 0.266181 +-0.725354 0.417744 0.54713 -0.710943 0.507705 0.486616 -0.626311 -0.0295103 -0.779014 +-0.774762 -0.101488 -0.624054 -0.615556 0.560962 0.553544 -0.297729 0.291277 0.909129 +-0.682433 0.501375 -0.531891 -0.861234 0.352093 -0.366479 -0.67787 0.510337 -0.529196 +-0.999412 0.0128218 -0.03181 -0.995111 -0.00549962 -0.09861 -0.881858 0.0497995 -0.468877 +-0.871873 0.149214 0.466448 -0.98747 0.101783 0.120597 -0.917039 0.282224 0.281759 +-0.693577 0.52317 0.495222 -0.717617 0.504928 0.47966 -0.718647 -0.121604 -0.68466 +-0.776221 -0.270989 -0.569251 -0.026353 0.0342182 0.999067 -0.678198 0.517668 -0.521601 +-0.854937 0.369344 -0.364236 -0.679585 0.521981 -0.515462 -0.946608 0.0287178 -0.321105 +-0.814103 -0.235921 0.530639 -0.93428 0.092481 0.344336 -0.736183 0.454593 0.501378 +-0.788936 0.306084 0.532815 -0.808591 0.156731 -0.567112 -0.805178 -0.263297 -0.531379 +-0.822914 -0.0825633 -0.562135 -0.728926 -0.236852 -0.642314 -0.0331642 0.00227451 0.999447 +-0.680672 0.531657 -0.50401 -0.831588 0.393987 -0.391453 -0.697386 0.520148 -0.493051 +-0.907536 0.336711 -0.251007 -0.85732 0.431527 -0.280691 -0.741215 -0.163911 0.650949 +-0.799129 0.0531033 0.59881 -0.764738 0.290412 -0.575184 -0.528777 0.0692435 -0.845932 +-0.0335495 0.012456 0.999359 -0.723218 0.420988 -0.547471 -0.927153 0.189622 -0.323158 +-0.813616 0.118471 -0.569204 -0.934228 0.186546 -0.304005 -0.77547 -0.179738 0.605261 +-0.758374 -0.203477 0.619246 -0.400898 0.0357351 -0.915425 -0.0411024 -0.0446496 0.998157 +-0.944182 0.01187 -0.32921 -0.802859 0.0696851 -0.592082 -0.993952 0.0199225 -0.107994 +-0.851958 0.244628 -0.462953 -0.897937 -0.0268747 0.439302 -0.725315 -0.380124 0.573955 +-0.83928 -0.22141 0.496575 -0.760253 -0.321269 0.564625 -0.365105 0.00240097 -0.930963 +-0.0669619 -0.357943 0.93134 -0.879755 0.191603 -0.435109 -0.738236 0.306214 -0.601032 +-0.993798 0.0232462 -0.108747 -0.935342 0.0253632 -0.352833 -0.923742 0.0107821 0.382865 +-0.920278 -0.17838 0.348237 -0.796124 -0.228923 0.560162 -0.332509 -0.0112481 -0.943033 +-0.000245278 -0.608647 0.793441 -0.0100321 -0.377609 0.925911 -0.743706 0.387815 -0.544519 +-0.923796 0.167874 -0.344122 -0.813094 0.182328 -0.552842 -0.928776 -0.000287439 -0.370642 +-0.932388 0.0233328 0.360706 -0.99643 -0.0418347 0.0733308 -0.718341 -0.434633 0.543212 +-0.857677 -0.370655 0.356378 -0.663379 -0.536545 0.521581 -0.335324 -0.00642543 -0.942081 +0.0105857 -0.0486326 0.998761 -0.909208 0.0177 -0.415965 -0.843905 -0.0826619 -0.530086 +-0.908151 0.00621676 -0.418596 -0.926404 -0.037641 0.374644 -0.999689 0.024957 -8.22934e-05 +-0.997718 -0.0497085 0.0456872 -0.637587 -0.486418 0.597394 -0.825839 -0.209148 0.523686 +-0.500868 -0.300041 0.811854 -0.321962 -0.0113909 -0.946684 0.0271104 0.0251115 0.999317 +-0.946592 -0.110595 -0.302874 -0.823612 -0.272715 -0.497283 -0.910197 -0.0017154 -0.414173 +-0.843084 -0.26054 0.470455 -0.99786 -0.0319826 0.0570295 -0.999215 0.0347389 -0.0190638 +-0.925813 -0.00438069 0.377956 -0.582689 -0.183289 0.791757 -0.306846 -0.00285464 -0.951755 +0.161897 0.326796 0.931125 -0.790016 -0.246731 -0.561247 -0.770176 -0.320709 -0.551339 +-0.922337 -0.192492 -0.335025 -0.991019 -0.0509292 -0.123642 -0.929309 -0.0107574 -0.369148 +-0.935592 -0.159666 0.31492 -0.99963 0.00866495 0.0257679 -0.94724 0.0212111 0.319822 +-0.754142 -0.0317313 0.655945 -0.38095 0.0344399 -0.923954 0.117828 0.605979 0.786706 +0.258296 0.480087 0.838332 0.392397 0.573121 0.719414 -0.950248 -0.146181 -0.275063 +-0.795796 -0.276572 -0.538718 -0.991379 -0.0767658 -0.106185 -0.920325 -0.154449 -0.359372 +-0.840925 -0.258744 0.475286 -0.994858 -0.0468168 0.0898055 -0.951123 -0.162304 0.262721 +-0.806355 -0.166989 0.567367 -0.533242 0.0250067 -0.845593 -0.0538578 0.0379859 0.997826 +-0.840189 -0.199913 -0.5041 -0.795823 -0.321902 -0.512879 -0.905218 -0.18987 -0.38017 +-0.984457 -0.0897461 -0.150961 -0.915851 -0.28129 -0.286518 -0.936071 -0.152326 0.317124 +-0.998707 -0.0367275 0.0351408 -0.77197 -0.509422 0.380199 -0.940161 -0.317175 0.12449 +-0.681201 -0.711102 0.174065 -0.898684 -0.437425 0.0320322 -0.593076 -0.777726 0.208334 +-0.803248 -0.577158 0.147244 -0.718822 -0.675388 0.164758 -0.651393 0.016841 -0.758554 +-0.645477 -0.272634 -0.713464 -0.387013 -0.389039 0.835984 -0.941709 -0.144127 -0.303993 +-0.814804 -0.279055 -0.508156 -0.936192 -0.167434 -0.309047 -0.836136 -0.259493 0.483259 +-0.997355 -0.0181069 0.0703874 -0.999854 0.00933468 -0.0143142 -0.997775 -0.0270445 -0.0609336 +-0.976252 -0.141287 -0.164226 -0.62035 -0.772802 -0.133952 -0.5156 -0.518351 0.682252 +-0.514912 -0.534843 0.669931 -0.358243 -0.375193 0.854922 -0.746465 -0.498442 -0.440846 +-0.761867 -0.424074 -0.489611 -0.915585 -0.280583 -0.288058 -0.980772 -0.12163 -0.152621 +-0.910002 -0.17632 -0.375243 -0.930731 -0.135909 0.339511 -0.999574 0.0288502 0.00447889 +-0.9962 0.03232 -0.0808771 -0.950433 0.0075817 -0.310836 -0.188625 -0.306287 0.933064 +-0.724128 -0.305467 -0.618327 -0.677616 -0.427411 -0.598462 -0.911271 -0.216975 -0.350009 +-0.982517 -0.122829 -0.139903 -0.913786 -0.28653 -0.287915 -0.830214 -0.305539 0.466251 +-0.996436 -0.0707461 0.045947 -0.995739 -0.0336605 -0.0858567 -0.912999 -0.136464 -0.384461 +-0.681662 -0.38898 0.619702 -0.348245 -0.567922 0.745781 -0.24997 -0.733803 0.631703 +-0.943515 -0.137238 -0.301572 -0.809576 -0.278111 -0.516953 -0.94553 -0.152019 -0.287858 +-0.87547 -0.445865 0.186431 -0.913077 -0.340326 -0.224652 -0.72382 -0.365054 0.585509 +-0.49754 -0.67747 0.541745 -0.827675 -0.244205 0.50529 -0.727033 -0.386572 0.567438 +-0.801752 -0.192922 -0.565663 -0.782818 -0.294948 -0.547907 -0.905767 -0.188079 -0.379754 +-0.982184 -0.0993656 -0.159501 -0.92687 -0.152543 -0.34299 -0.707512 -0.438241 0.554411 +-0.780505 -0.255935 0.570359 -0.890288 -0.236472 -0.38919 -0.793495 -0.393943 -0.46387 +-0.913399 -0.264946 -0.30904 -0.546284 -0.749684 0.37356 -0.639741 -0.76845 0.0147033 +-0.80116 -0.596473 -0.0486136 -0.860021 -0.417594 -0.293223 -0.743514 0.648501 0.163197 +-0.776477 0.586422 -0.230635 -0.603045 0.733675 0.313141 -0.409602 0.235646 0.881304 +-0.623461 0.770407 -0.1333 -0.82354 0.515853 -0.23596 -0.725936 0.582658 -0.365412 +-0.758087 0.605627 -0.241911 -0.635684 0.771737 -0.0180961 -0.626268 0.779162 0.0263809 +-0.630893 0.727795 -0.268864 -0.79133 -0.0502762 0.609319 -0.622905 -0.296643 0.723873 +-0.753844 -0.324721 0.571205 -0.798403 0.572209 -0.187429 -0.79239 0.609974 -0.0070925 +-0.775603 0.629647 0.0445494 -0.767333 0.639176 -0.05151 -0.68837 0.585352 -0.42838 +-0.782732 0.15096 -0.603773 -0.606808 0.349561 -0.713856 -0.917443 0.277818 0.284809 +-0.90651 -0.220653 0.359932 -0.7671 -0.261107 0.585987 -0.616185 -0.130812 -0.776662 +-0.626798 -0.218469 -0.747928 -0.916511 0.347377 0.198335 -0.949799 0.277702 0.144094 +-0.999348 0.0360576 -0.00205208 -0.998391 -0.0386631 0.0414721 -0.701849 -0.428895 0.568733 +-0.845622 -0.380225 0.374636 -0.675039 -0.52242 0.52096 -0.510889 -0.278146 -0.813405 +-0.881331 0.356054 0.310613 -0.994643 0.0966529 0.0366511 -0.998165 0.0592348 0.0126013 +-0.999882 0.0101989 -0.0114536 -0.999578 -0.00565701 -0.0284875 -0.996628 -0.0646095 0.0505884 +-0.662134 -0.546731 0.512507 -0.859914 -0.352474 0.369201 -0.656751 -0.542204 0.524112 +-0.733397 -0.262433 -0.627103 -0.707374 -0.343672 -0.617666 -0.564361 -0.338332 -0.753013 +-0.730537 0.517246 0.445839 -0.899207 0.433102 0.0620387 -0.939352 0.342933 0.0038871 +-0.949102 0.314747 -0.0117676 -0.950965 0.28364 -0.123345 -0.997596 0.0463029 -0.0515628 +-0.999914 0.0118044 -0.00576364 -0.994886 -0.0432145 0.0912937 -0.689151 -0.453961 0.564792 +-0.851506 -0.211585 0.47976 -0.701842 -0.305812 0.643348 -0.636527 -0.312507 -0.705105 +-0.673278 -0.338116 -0.657552 -0.509517 0.559721 0.653532 -0.568886 0.65079 0.502833 +-0.721823 0.643213 0.255436 -0.79119 0.611563 0.00304298 -0.787606 0.61611 0.00925383 +-0.773802 0.630344 -0.0624188 -0.720272 0.619717 -0.311702 -0.856369 0.387987 -0.340732 +-0.684698 0.578452 -0.443375 -0.998055 0.0474907 -0.0403871 -0.999895 -0.0144891 -0.000757429 +-0.940973 -0.282427 0.18656 -0.772069 -0.34506 0.533706 -0.60546 -0.700055 -0.378605 +-0.723693 -0.432425 -0.537845 -0.682653 -0.530317 -0.502741 -0.50033 0.5307 0.684126 +-0.55249 0.628346 0.547664 -0.551345 0.522751 0.650193 -0.640737 0.544576 -0.541196 +-0.927531 0.164414 -0.335642 -0.599575 0.297381 -0.743017 -0.997476 -0.0277893 -0.0653451 +-0.938675 -0.226142 -0.260288 -0.63124 -0.774562 0.0398754 -0.799724 -0.500733 -0.331222 +-0.563317 0.526485 0.636779 -0.39036 0.398759 0.829826 -0.907621 0.00430558 -0.419768 +-0.581956 0.0166153 -0.81305 -0.946721 -0.136798 -0.291557 -0.451129 0.58387 0.674966 +-0.319329 0.438575 0.840048 -0.889983 -0.0167982 -0.455684 -0.439783 0.00867473 -0.898062 +-0.933393 -0.0388448 -0.356748 -0.371758 0.566872 0.735155 -0.388553 0.657514 0.645525 +-0.260772 0.671831 0.693283 -0.821607 -0.0211531 -0.569662 -0.380853 0.0287157 -0.92419 +-0.911463 -0.137171 -0.387839 -0.21193 0.672007 0.709571 -0.136532 0.575379 0.806411 +-0.774622 -0.15348 -0.613518 -0.349951 -0.0761509 -0.933668 -0.0160517 0.627577 0.778389 +-0.0286409 0.48939 0.871594 -0.666222 -0.317294 -0.674887 -0.291291 -0.36694 -0.883462 +-0.542475 0.17838 0.820915 -0.243025 0.270156 0.931641 -0.398005 -0.309508 -0.863595 +-0.252995 -0.556158 -0.791633 -0.530909 -0.414338 -0.73923 -0.656059 -0.0282552 0.75418 +-0.366969 -0.049338 0.928924 -0.586864 -0.223928 -0.778105 -0.352907 -0.199497 -0.914143 +-0.806805 -0.246122 0.537113 -0.527319 -0.38759 0.756114 -0.45361 -0.377157 -0.807459 +-0.838941 -0.444011 0.314695 -0.633974 -0.480931 0.605626 -0.745406 -0.503265 0.437143 +-0.375412 -0.376908 0.846762 -0.424788 -0.163627 -0.890383 -0.51922 -0.46384 -0.717818 +-0.609471 -0.298202 -0.734589 -0.429323 -0.425356 0.796714 -0.577793 -0.0865977 -0.811576 +-0.383742 -0.095516 -0.918487 -0.685459 -0.703103 0.189188 -0.655583 -0.640626 0.399761 +-0.622582 -0.769245 0.143712 -0.667812 -0.744156 0.0160867 -0.433946 -0.874636 0.21611 +-0.634319 -0.731158 0.251094 -0.54672 -0.667797 0.505118 -0.610721 -0.248935 -0.751698 +-0.519802 -0.442187 -0.730942 -0.58443 -0.68483 0.435259 -0.755317 -0.544955 0.364032 +-0.505638 -0.857319 0.0966182 -0.856885 -0.514248 0.0360164 -0.477624 -0.87801 -0.0312038 +-0.8853 -0.464851 -0.0125472 -0.591281 -0.806393 -0.010807 -0.885208 -0.461792 -0.056167 +-0.573138 -0.753595 -0.321881 -0.755028 -0.466072 -0.461205 -0.870201 -0.454302 0.190683 +-0.924832 -0.380253 0.00965032 -0.929805 -0.366566 -0.0330527 -0.898232 -0.32187 -0.299296 +-0.897724 0.229585 0.376008 -0.936821 0.346687 0.0466347 -0.866846 0.455241 -0.203306 +-0.921652 0.0258562 0.387154 -0.996624 0.0329212 0.0752123 -0.993077 0.0824468 -0.083673 +-0.868087 0.278354 -0.411028 -0.939732 0.167823 0.29789 -0.997968 -0.0124764 0.0624823 +-0.998356 -0.0196454 -0.0538387 -0.93668 0.0277469 -0.349086 -0.791368 0.548374 0.270227 +-0.810982 0.582334 0.0565306 -0.89527 0.443151 -0.0459274 -0.866357 0.459221 -0.196322 +-0.890831 0.325379 0.317094 -0.998217 0.0374737 0.0464659 -0.999728 0.0111528 0.0204796 +-0.997739 -0.0225258 -0.0633197 -0.940951 -0.0336341 -0.336868 -0.711642 0.529737 0.461459 +-0.810601 0.543596 0.217782 -0.7821 0.612976 -0.112159 -0.776506 0.573395 0.26126 +-0.812306 0.531446 -0.240258 -0.795991 0.529907 -0.292569 -0.905562 0.379026 -0.190519 +-0.831504 0.401571 -0.383852 -0.903057 0.301942 0.305482 -0.998667 0.0368211 0.0361778 +-0.999826 0.00691865 0.0173235 -0.999572 -0.0282356 -0.00769987 -0.997525 -0.0392044 -0.0583746 +-0.906096 -0.169725 -0.387535 -0.756049 0.57927 0.30469 -0.419647 0.27799 0.86407 +-0.780926 0.575521 0.242756 -0.79494 0.554671 -0.245786 -0.772779 0.553177 -0.311139 +-0.833186 0.412248 -0.368583 -0.741498 0.511579 -0.434129 -0.806235 0.416957 -0.41968 +-0.908697 0.174161 0.379391 -0.998431 0.0419534 0.0370943 -0.999994 -0.000625385 -0.0035174 +-0.999815 -0.0173639 0.00827512 -0.999706 0.0145348 -0.0194198 -0.95988 -0.019751 -0.279715 +-0.369837 0.0154249 0.928969 -0.721748 0.49095 -0.487903 -0.825546 0.391717 -0.40624 +-0.729965 0.479312 -0.487249 -0.822285 0.403262 -0.40153 -0.939733 -0.0346781 0.340145 +-0.999594 0.00240289 0.0283871 -0.999924 0.00632281 -0.0105688 -0.999405 0.0282151 0.0198221 +-0.954561 0.293611 0.0510526 -0.990076 0.0948839 -0.103661 -0.828089 0.246411 -0.503538 +-0.350396 0.00512608 0.936587 -0.756783 0.387675 -0.526297 -0.901637 0.209342 -0.378452 +-0.796146 0.220025 -0.563685 -0.847873 0.274464 -0.453631 -0.85742 -0.264507 0.441437 +-0.996666 -0.0306455 0.0756146 -0.999198 0.0377163 0.0134824 -0.896478 0.310826 0.315776 +-0.756885 0.639257 -0.135924 -0.725 0.586195 0.361593 -0.845498 0.401913 -0.351566 +-0.75513 0.519246 -0.400202 -0.87684 0.114012 -0.467069 -0.337088 -0.0300583 0.940993 +-0.854044 0.208568 -0.47656 -0.751874 0.306749 -0.583601 -0.891637 0.0718794 -0.447008 +-0.92202 -0.150221 0.356809 -0.950686 0.13108 0.281095 -0.729391 0.44339 0.520955 +-0.756416 0.30855 0.576742 -0.722515 0.418012 -0.55067 -0.764651 0.162256 -0.623684 +-0.776143 0.158262 -0.610374 -0.399095 -0.283803 0.871882 -0.750434 0.359002 -0.554947 +-0.81305 0.231682 -0.53411 -0.810275 0.176652 -0.558792 -0.80821 -0.319086 0.494955 +-0.880855 -0.093538 0.464052 -0.812616 0.0720274 0.578332 -0.642001 0.0294313 -0.766139 +-0.776909 0.000466589 -0.629613 -0.174488 -0.5757 0.798827 -0.159915 -0.287502 0.944336 +-0.889412 0.0784727 -0.450321 -0.849528 -5.09453e-05 -0.527543 -0.813205 0.276591 -0.512051 +-0.681114 -0.423977 0.596932 -0.686992 -0.163777 0.707969 -0.629732 -0.00864837 -0.776764 +-0.777408 -0.00350613 -0.628987 -0.0138498 -0.0489329 0.998706 -0.910466 -0.0289243 -0.412571 +-0.83815 0.00713318 -0.545394 -0.908744 0.0197954 -0.416884 -0.537166 -0.228942 0.811811 +-0.629984 -0.00219721 -0.776605 -0.775626 0.00545705 -0.631169 -0.0226879 0.0406924 0.998914 +-0.865186 -0.0375853 -0.50004 -0.813858 -0.0512845 -0.578796 -0.876843 -0.159981 -0.453378 +-0.634465 -0.210508 0.743735 -0.58018 -0.360731 0.730249 -0.697612 -0.324404 0.638827 +-0.616402 -0.00119511 -0.787431 -0.771767 0.0171254 -0.635674 -0.0944545 0.322319 0.941907 +-0.798194 -0.241145 -0.552028 -0.801231 -0.360177 -0.477809 -0.76123 -0.222141 0.609247 +-0.737112 -0.226392 0.63672 -0.59913 -0.0125429 -0.800554 -0.769506 -0.0230023 -0.638225 +-0.299305 0.500778 0.812181 -0.200782 0.353743 0.913538 -0.753789 -0.265865 -0.600931 +-0.792692 -0.536452 -0.289585 -0.918408 -0.322987 -0.228489 -0.821916 -0.544026 -0.168788 +-0.869769 -0.387941 -0.304964 -0.815106 -0.285709 -0.503957 -0.797307 -0.461811 0.388628 +-0.696431 -0.420285 0.581674 -0.675963 -0.604431 0.421588 -0.694617 -0.609356 0.382352 +-0.742654 -0.245055 -0.623228 -0.72482 -0.465456 -0.507924 -0.456471 -0.172108 0.872933 +-0.92102 -0.0482768 -0.386513 -0.778875 0.0170288 -0.626948 -0.923875 -0.213798 -0.317405 +-0.587717 -0.807817 0.0449491 -0.828359 -0.427838 -0.361629 -0.501245 -0.531384 0.682924 +-0.52689 -0.483742 0.698842 -0.419596 -0.369499 0.829102 -0.864255 -0.0318991 -0.502042 +-0.767954 -0.0567771 -0.637983 -0.893064 -0.162975 -0.419376 -0.337941 -0.214082 0.916496 +-0.699028 -0.381567 -0.604786 -0.702033 -0.329334 -0.631418 -0.430439 -0.290206 0.854695 +-0.67756 -0.511791 -0.528188 -0.700072 -0.478276 -0.530236 -0.678327 -0.454444 -0.577368 +-0.237671 -0.597812 0.765594 -0.385807 -0.705153 0.594905 -0.790942 -0.503119 -0.348255 +-0.734361 -0.474962 -0.484896 -0.803093 -0.392192 -0.448584 -0.788621 -0.30355 -0.534728 +-0.311499 -0.851476 0.421849 -0.670998 -0.720249 0.176074 -0.946684 -0.314228 0.0710588 +-0.885989 -0.214606 -0.411056 -0.846202 -0.487815 0.214424 -0.895698 -0.292444 -0.334966 +-0.868003 -0.0332979 0.495441 -0.876318 0.45799 -0.149372 -0.954796 -0.105894 0.277762 +-0.936302 0.312793 -0.159685 -0.929696 0.366444 -0.037203 -0.942138 0.333691 0.0320433 +-0.944336 0.32898 0.00129397 -0.886158 0.394078 -0.243775 -0.832818 -0.307566 0.460236 +-0.996248 -0.0399689 0.0767632 -0.996987 0.0714847 -0.0301053 -0.998364 0.0550521 0.0154503 +-0.999068 0.0325518 -0.0283329 -0.940503 0.032595 -0.338218 -0.841771 -0.38705 0.376317 +-0.99541 -0.0698769 0.065393 -0.999734 -0.00423702 -0.0226685 -0.997909 -0.0518999 -0.0385349 +-0.889256 -0.163982 -0.427005 -0.833789 -0.351787 0.425489 -0.998644 -0.0516526 0.00650086 +-0.953949 -0.148093 -0.260865 -0.9315 -0.206968 0.299119 -0.921138 -0.178756 -0.345761 +-0.625648 0.749084 0.217802 -0.636728 0.770971 0.0134415 -0.618386 0.785842 0.00712244 +-0.618262 0.785969 -0.00229193 -0.661163 0.703984 -0.259365 -0.878089 -0.477501 -0.0308438 +-0.512374 0.62744 0.586338 -0.411829 0.639874 0.648813 -0.366362 0.87799 0.308078 +-0.315577 0.948702 0.0193758 -0.344986 0.938602 -0.00320193 -0.462822 0.88121 -0.0962497 +-0.544611 0.722735 -0.425503 -0.577457 0.605497 -0.547647 -0.554597 0.566077 -0.609901 +-0.451861 0.556078 0.697566 -0.406561 0.624317 0.667036 -0.272148 0.694853 0.665669 +-0.545404 0.532309 -0.647442 -0.313315 0.288639 -0.904722 -0.166332 0.674656 0.719147 +-0.203798 0.577525 0.790526 -0.075173 0.0468512 -0.996069 -0.0162359 0.682193 0.730992 +-0.0962519 0.703773 0.703874 -0.0170702 0.055899 -0.99829 0.0405009 0.727879 0.684509 +-0.0272123 0.716878 0.696667 -0.0410676 -0.0942059 -0.994705 0.0949236 0.648577 0.755207 +-0.00193914 0.3835 0.923539 -0.202346 -0.476364 -0.855648 -0.0440875 0.0314972 0.998531 +-0.200612 -0.338521 -0.919325 -0.28786 -0.579037 -0.762792 -0.180779 -0.346272 0.920551 +-0.369509 -0.377044 -0.849294 -0.431344 -0.521193 0.73641 -0.318901 -0.581191 0.748679 +-0.256977 -0.457666 0.851178 -0.3479 -0.344576 -0.871914 -0.504233 -0.521385 -0.688409 +-0.495152 -0.525128 -0.692145 -0.449113 -0.473124 0.757926 -0.138867 -0.0865266 -0.986524 +-0.686539 -0.674169 0.272323 -0.661168 -0.634171 0.400854 -0.696156 -0.641047 0.323149 +-0.654855 -0.686058 0.316999 -0.55347 -0.675141 0.487704 -0.382224 -0.325767 -0.864743 +-0.437179 -0.823584 0.361365 -0.402424 -0.825087 0.396595 -0.49554 -0.691369 0.525784 +-0.524079 -0.730394 0.438026 -0.551741 -0.67663 -0.487599 -0.538794 -0.506332 -0.673297 +-0.506945 -0.574343 -0.642758 -0.451434 -0.844125 0.289239 -0.355732 -0.93433 0.0219663 +-0.393557 -0.907184 -0.148761 -0.854202 0.517466 0.0506757 -0.775934 0.309536 0.549649 +-0.809637 0.520689 0.270871 -0.824994 0.564938 0.015129 -0.8132 0.537944 -0.222086 +-0.825689 0.493914 -0.272556 -0.721271 0.130279 0.680291 -0.81572 0.550015 0.179123 +-0.770579 0.359295 0.526417 -0.796357 0.600109 -0.0753897 -0.734471 0.598532 -0.319863 +-0.80699 0.451157 -0.381083 -0.719233 0.518528 -0.46242 -0.826299 0.406324 -0.39004 +-0.63145 0.00736113 0.775382 -0.791552 0.0399478 0.609795 -0.704932 0.499862 -0.503198 +-0.812795 0.397761 -0.425618 -0.710202 0.501918 -0.493651 -0.821212 0.320982 -0.471786 +-0.888173 0.450906 0.0885073 -0.638179 -0.0519314 0.768135 -0.791732 -0.00304922 0.610861 +-0.721277 0.422116 -0.54916 -0.800219 0.271946 -0.534504 -0.798699 0.160874 -0.579827 +-0.899494 0.180069 0.398102 -0.993056 0.111374 -0.0378987 -0.867324 0.299588 -0.397487 +-0.520203 -0.37708 0.76629 -0.562169 -0.00488525 0.827008 -0.886127 0.0627026 -0.459181 +-0.835842 -0.0237361 -0.548456 -0.823508 0.253594 -0.507469 -0.92213 0.0105939 0.386736 +-0.999997 -0.0013977 0.0022325 -0.929674 0.0269705 -0.367395 -0.104788 -0.0578009 0.992813 +-0.915654 -0.0192292 -0.401508 -0.844697 -0.014683 -0.535043 -0.909618 0.0296861 -0.414384 +-0.896285 -0.0582818 0.439632 -0.999853 0.00509268 -0.0163831 -0.934972 -0.00916193 -0.354604 +-0.284889 -0.135545 0.948929 -0.91463 0.0163636 -0.40396 -0.833196 0.0403599 -0.551503 +-0.916203 0.000919233 -0.400712 -0.812198 -0.251517 0.526377 -0.999033 -0.0381065 0.0219386 +-0.93263 0.00692447 -0.360767 -0.27157 0.108795 0.95625 -0.91612 -0.0603482 -0.396337 +-0.786511 -0.0934315 -0.610468 -0.915537 0.00932725 -0.402126 -0.951453 -0.150434 0.268527 +-0.944459 -0.00269925 -0.328617 -0.0957799 0.12598 0.987398 -0.786228 -0.432405 -0.441443 +-0.68515 -0.353928 -0.636635 -0.906888 -0.239419 -0.346745 -0.894121 -0.264895 0.361079 +-0.961905 -0.0257254 -0.272172 -0.552926 0.102998 0.82684 -0.299001 0.164924 0.939893 +-0.68503 -0.255632 -0.682192 -0.611148 -0.429744 -0.664694 -0.636567 -0.389976 -0.665358 +-0.915762 -0.378181 -0.135497 -0.600462 -0.316621 0.7343 -0.509567 -0.244841 0.82486 +-0.667543 -0.0887351 -0.739265 -0.752204 -0.0688381 -0.655324 -0.525882 -0.24423 0.814739 +-0.752879 -0.0985591 -0.650738 -0.734348 -0.269502 -0.622978 -0.743621 -0.355986 0.565952 +-0.603068 -0.391204 0.695175 -0.735691 -0.497064 0.460093 -0.757585 -0.520518 -0.39386 +-0.776844 -0.408354 -0.479333 -0.790368 -0.385218 -0.476367 -0.842464 -0.234429 -0.485075 +-0.744638 -0.396275 0.537104 -0.781593 -0.547907 0.298178 -0.921365 -0.371964 0.112822 +-0.827504 -0.555308 -0.0828891 -0.868378 -0.430094 -0.246856 -0.829632 -0.394356 -0.395215 +-0.798315 -0.569721 0.195223 -0.878021 -0.425355 -0.219437 0.00403355 0.932122 0.362122 +-0.0628158 0.749273 0.659276 -0.0174604 0.999028 0.0404799 -0.0404054 0.997528 -0.0574841 +-0.23353 0.934604 -0.268291 -0.187669 0.760894 -0.621144 -0.479665 0.680738 -0.55364 +0.00482999 0.68585 0.727727 -0.0710206 0.701411 0.70921 -0.41822 0.612555 -0.670722 +-0.408816 0.549793 -0.728421 -0.486153 0.564976 -0.666677 0.0105904 0.718425 0.695524 +-0.00494686 0.699423 0.71469 -0.47571 0.528999 -0.702752 -0.252451 0.298535 -0.920405 +0.017147 0.69925 0.714672 0.0411235 0.730468 0.681707 -0.0325201 0.0378085 -0.998756 +0.152287 0.817584 0.555306 0.211343 0.729919 0.65004 -0.148892 -0.305176 -0.940584 +0.333967 0.61172 0.717122 0.15411 0.308576 0.938633 -0.273853 -0.508196 -0.816543 +-0.200991 -0.60679 -0.769031 -0.318037 -0.580234 -0.749787 -0.00941623 -0.0751112 0.997131 +-0.374218 -0.412569 -0.830511 -0.239738 -0.489471 0.838417 -0.356262 -0.357083 -0.863464 +-0.51298 -0.508924 -0.691266 -0.508807 -0.506433 -0.696161 -0.302641 -0.618112 0.725497 +-0.422543 -0.540019 0.727899 -0.0949434 -0.10328 -0.990111 -0.655077 -0.714578 0.245463 +-0.526973 -0.740252 0.417524 -0.475777 -0.850318 0.224934 -0.63289 -0.730348 0.256985 +-0.588365 -0.643779 0.48926 -0.317782 -0.324216 -0.89101 -0.592466 -0.54764 0.590826 +-0.487115 -0.686371 0.540013 -0.580516 -0.658892 0.478396 -0.618399 -0.605945 -0.500414 +-0.520332 -0.52159 -0.676165 -0.540324 -0.536314 -0.648396 -0.427785 -0.847589 0.313995 +-0.576571 -0.789015 0.212182 -0.595987 -0.784744 0.170224 -0.7196 -0.693832 0.0278121 +-0.611259 -0.788173 -0.0717407 -0.710404 -0.674094 -0.202295 -0.611923 -0.699636 -0.368863 +-0.930271 0.200571 0.307192 -0.943821 0.329457 0.0257135 -0.871389 0.442881 -0.211039 +-0.935979 0.0153451 0.35172 -0.995435 0.0668099 0.0681589 -0.990907 0.117156 -0.0661703 +-0.862347 0.361918 -0.354079 -0.865089 0.108158 0.48982 -0.924835 0.300875 0.23271 +-0.951348 0.290276 -0.103325 -0.987654 0.0851211 -0.131506 -0.861558 0.275472 -0.426418 +-0.673032 0.408424 0.616618 -0.798418 0.596227 0.0839193 -0.734026 0.542864 0.40805 +-0.770143 0.521931 -0.366699 -0.93058 0.154222 -0.332019 -0.813802 0.163059 -0.557797 +-0.926603 0.0228875 -0.375344 -0.707914 0.404842 0.578758 -0.483958 0.0567175 0.873251 +-0.905778 0.0215185 -0.423206 -0.803516 0.0660891 -0.591603 -0.917118 -0.0288582 -0.397571 +-0.624509 -0.133647 0.769498 -0.608914 -0.327136 0.722638 -0.736813 -0.306432 0.602665 +-0.846006 0.202703 -0.493138 -0.740196 0.323141 -0.589652 -0.904114 0.0427336 -0.425148 +-0.557459 0.289336 0.778154 -0.763171 0.100374 0.638353 -0.716461 0.417792 -0.55869 +-0.791689 0.156242 -0.590608 -0.737181 0.112524 -0.666261 -0.698549 0.363223 0.616521 +-0.391612 0.210811 0.895655 -0.653383 -0.267012 -0.708375 -0.654205 -0.340521 -0.675323 +-0.518708 -0.113363 0.847402 -0.698084 -0.493551 -0.518735 -0.695414 -0.496758 -0.51926 +-0.674202 -0.435075 -0.596792 -0.725691 -0.42781 0.538842 -0.608901 -0.439716 0.660219 +-0.760647 -0.523011 0.384547 -0.792442 -0.524673 -0.311053 -0.767605 -0.458468 -0.447872 +-0.877423 -0.310973 -0.365274 -0.864278 -0.128805 -0.486244 -0.824768 -0.3177 0.467787 +-0.8012 -0.582523 0.136914 -0.94208 -0.335349 0.0050617 -0.980037 -0.145977 -0.134978 +-0.908755 -0.157056 -0.38665 -0.884074 -0.434578 0.171916 -0.915548 -0.34589 -0.205259 +0.0167456 0.934107 0.356601 0.0327608 0.748292 0.66256 0.0144946 0.999158 0.0383808 +-0.00425198 0.997759 -0.0667777 -0.131421 0.932344 -0.336842 -0.276012 0.714334 -0.643074 +0.00185707 0.676324 0.736602 0.0148813 0.706568 0.707489 -0.348488 0.584319 -0.73289 +-0.305812 0.517455 -0.799199 -0.00721296 0.714108 0.699998 0.00278405 0.706255 0.707952 +-0.502966 0.539129 -0.675548 -0.441805 0.378598 -0.813309 -0.0271223 0.689693 0.723593 +-0.00998953 0.753474 0.657402 -0.580384 0.449144 -0.679282 -0.442505 0.10056 -0.89111 +0.144808 0.878132 0.455977 0.261099 0.52583 0.809525 0.356671 0.718002 0.597712 +0.0230526 0.896718 0.442002 -0.355202 -0.380072 -0.854036 0.0514186 0.0639916 0.996625 +-0.41636 -0.542615 -0.729529 -0.470609 -0.584636 -0.660854 -0.160235 -0.353382 0.921654 +-0.367767 -0.332278 -0.868527 -0.565251 -0.479556 -0.671206 -0.51182 -0.516466 -0.686515 +-0.356513 -0.634417 0.685867 -0.334866 -0.595694 0.730078 -0.313409 -0.694414 0.647737 +-0.0764674 -0.080059 -0.993853 -0.443066 -0.694194 0.567263 -0.528062 -0.666879 0.52576 +-0.554158 -0.728285 0.403124 -0.312717 -0.331813 -0.890005 -0.511866 -0.805801 0.297789 +-0.576531 -0.711935 0.40095 -0.537936 -0.589232 0.602853 -0.593173 -0.614106 0.520595 +-0.624559 -0.608352 -0.489728 -0.534989 -0.521012 -0.665081 -0.543625 -0.541983 -0.640879 +-0.677446 -0.628145 0.382755 -0.654178 -0.735491 0.176362 -0.725956 -0.685701 0.0529239 +-0.704821 -0.709184 0.0168675 -0.723239 -0.690503 -0.0114197 -0.72508 -0.685457 -0.0663982 +-0.705388 -0.683954 -0.186104 -0.666569 -0.653326 -0.358958 -0.91262 0.361037 0.191772 +-0.872672 0.422102 -0.245507 -0.860895 -0.0778788 0.502787 -0.999321 0.0192441 0.0314272 +-0.929076 0.100077 -0.356094 -0.947781 -0.103412 0.30169 -0.937653 0.109568 -0.32985 +-0.951155 0.14605 0.27198 -0.997633 0.0373562 -0.0577386 -0.847744 0.265975 -0.458899 +-0.877846 0.138606 0.458447 -0.99944 0.0310943 0.0123844 -0.998572 -0.0298071 -0.0443372 +-0.918705 -0.131178 -0.372523 -0.860012 -0.286222 0.422441 -0.994876 -0.096192 0.0311318 +-0.9087 -0.299578 -0.290719 -0.891445 -0.445097 -0.0849324 -0.0153591 0.930447 0.366104 +-0.0146406 0.7459 0.665897 0.000381325 0.999974 0.00723645 -0.133028 0.950935 -0.279332 +-0.192051 0.733963 -0.651471 -0.420105 0.678417 -0.602713 0.0277553 0.691343 0.721994 +0.00478286 0.702893 0.71128 -0.360424 0.616355 -0.700144 -0.197083 0.708216 -0.67793 +-0.389327 0.642823 -0.6597 0.0725295 0.706381 0.704106 -0.00179015 0.695642 0.718386 +-0.284988 0.643901 -0.710051 -0.179486 0.36691 -0.912777 0.0348288 0.684521 0.728161 +-0.00267772 0.780291 0.625411 -0.763372 0.109304 -0.636644 -0.296904 -0.155214 -0.942208 +0.062719 0.884326 0.462638 0.0634872 0.483168 0.873223 0.0631194 0.936089 0.346055 +-0.520497 -0.323995 -0.790006 -0.21489 -0.575642 -0.788961 -0.773267 -0.307856 -0.554331 +-0.811091 -0.195617 -0.551239 -0.0748305 -0.0564642 0.995596 -0.735135 -0.379133 -0.561991 +-0.336255 -0.186772 -0.923065 -0.299124 -0.476902 0.826492 -0.0258184 -0.00395445 -0.999659 +-0.329441 -0.646902 0.687741 -0.340481 -0.693951 0.634432 -0.157849 -0.299147 -0.94106 +-0.276483 -0.834434 0.476736 -0.340773 -0.913324 0.222964 -0.589211 -0.789097 0.173653 +-0.407068 -0.893455 0.189823 -0.615436 -0.744758 0.258019 -0.553897 -0.666788 0.49859 +-0.565535 -0.656047 -0.499773 -0.3651 -0.737001 -0.568799 -0.541085 -0.570713 -0.617668 +-0.577081 -0.694142 0.430283 -0.654831 -0.728806 0.200095 -0.447402 -0.891908 0.0658057 +-0.631412 -0.775441 -0.00327159 -0.464271 -0.883814 -0.057666 -0.654722 -0.75246 -0.0717161 +-0.527511 -0.780981 -0.334367 -0.638675 -0.667909 -0.382089 0.0189224 0.92963 0.368009 +0.00962846 0.772678 0.634725 -0.00519738 0.99998 0.00370279 -0.0263333 0.931559 -0.362636 +-4.16247e-05 0.744048 -0.668126 0.19586 0.783979 0.589081 0.190866 0.713886 0.673748 +-0.0685592 0.679552 -0.730417 0.0153264 0.729418 -0.683896 0.307316 0.649198 0.695772 +0.151933 0.631356 0.760464 -0.0595106 0.671895 -0.738252 0.00463373 0.314605 -0.949211 +0.22149 0.754665 0.617594 0.169336 0.811018 0.559978 -0.00565114 -0.332585 -0.943056 +-0.0353678 0.820769 0.570164 -0.0536193 0.388724 0.919793 0.144287 0.900277 0.41071 +-0.0578587 -0.363958 -0.929617 -0.0542795 -0.627383 -0.776817 -0.36561 -0.317894 0.874799 +0.0134733 -0.00357217 -0.999903 -0.441435 -0.626494 0.642371 -0.513567 -0.519352 0.683024 +-0.405758 -0.564507 0.718813 -0.00143067 -0.0124275 -0.999922 -0.361357 -0.624589 0.692322 +-0.33391 -0.746087 0.576072 -0.222852 -0.737978 0.636966 -0.192012 -0.5027 -0.842866 +-0.154559 -0.94376 0.29228 -0.0949871 -0.992826 0.072617 -0.34485 -0.868418 0.356271 +-0.52071 -0.657685 0.544345 -0.520242 -0.720499 0.458508 -0.502275 -0.74497 -0.439021 +-0.399159 -0.828797 -0.392131 -0.497873 -0.701974 -0.509269 -0.318777 -0.814607 -0.484558 +-0.478799 -0.831476 0.281779 -0.431464 -0.893125 -0.12715 0.142575 0.949508 0.279474 +0.173158 0.798413 0.576674 -0.00611455 0.999979 -0.00223799 -0.00179859 0.931751 -0.363093 +-0.00611883 0.750376 -0.660983 0.223694 0.778718 0.586139 0.270013 0.560009 0.783252 +0.361952 0.6881 0.628895 0.284858 0.785705 0.549112 0.0162535 0.691444 -0.722248 +-0.0304711 0.731216 -0.681465 0.228077 0.571344 0.788382 0.0215699 0.667613 -0.744196 +-0.0602005 0.238902 -0.969176 0.232417 0.801459 0.551041 0.106838 0.675865 0.729241 +0.336895 0.771996 0.539003 0.264238 0.784518 0.560989 -0.194146 -0.446378 -0.87353 +-0.230524 0.704112 0.671628 -0.34703 0.240598 0.906467 -0.112913 -0.324753 -0.939035 +-0.306892 -0.631994 -0.711619 -0.503509 -0.531943 0.680819 -0.510247 -0.427887 0.74603 +-0.410033 -0.399307 0.820016 -0.00750579 0.00845429 -0.999936 -0.303954 -0.496243 0.813238 +-0.185176 -0.299971 -0.935803 -0.3634 -0.628545 0.687656 -0.436187 -0.683611 0.585164 +-0.488601 -0.723296 -0.487967 -0.381722 -0.731998 -0.56433 -0.416184 -0.637488 -0.648382 +-0.297125 -0.873745 0.38508 -0.279561 -0.948551 0.148647 -0.121069 -0.989088 0.083952 +-0.352618 -0.924542 0.144509 -0.557723 -0.826347 0.0780747 -0.427194 -0.887203 -0.174287 +-0.542825 -0.786238 -0.295247 -0.394926 -0.841872 -0.367811 0.18511 0.908632 0.374328 +0.312774 0.718415 0.621332 0.34706 0.769862 0.535595 0.0351619 0.997238 0.0654143 +0.01121 0.999453 -0.0311086 0.00499637 0.931386 -0.363998 0.0735512 0.749639 -0.657748 +0.190146 0.721996 0.665256 0.205627 0.600951 0.772383 -0.00831239 0.677545 -0.735435 +-0.150596 0.591519 -0.792103 0.22478 0.663529 0.713585 0.307468 0.708084 0.635673 +0.3332 0.633602 0.698231 -0.207561 0.586433 -0.782953 -0.444817 -0.0882419 -0.891264 +0.190892 0.74237 0.64222 0.0799095 0.553503 0.829005 -0.37976 -0.421967 -0.823241 +-0.581366 -0.385458 -0.716544 -0.475425 -0.54995 -0.686677 -0.590053 -0.0473288 0.805976 +-0.221615 0.122845 0.967365 -0.0215643 -0.00953378 -0.999722 -0.195738 -0.393861 0.898087 +0.0207946 -0.0193637 -0.999596 -0.435833 -0.643232 0.629526 -0.408731 -0.677989 0.610959 +-0.509573 -0.684502 0.521338 -0.207239 -0.499195 -0.841342 -0.556439 -0.688856 0.464601 +-0.581252 -0.769185 0.265518 -0.516807 -0.811679 0.27219 -0.575637 -0.763993 0.291473 +-0.548271 -0.757057 0.355335 -0.503727 -0.746633 -0.434509 -0.413838 -0.825724 -0.383299 +-0.492701 -0.714172 -0.497196 -0.341264 -0.797181 -0.498037 -0.448287 -0.860132 0.243334 +-0.368026 -0.929019 0.0384814 -0.410185 -0.895738 -0.171471 0.154622 0.948601 0.276131 +0.241076 0.717008 0.654051 0.00686561 0.998827 0.047938 0.00411544 0.99984 -0.0174281 +0.130167 0.942715 -0.307155 0.33621 0.723629 -0.602763 0.322276 0.634169 0.702829 +0.290845 0.666199 0.686722 0.21026 0.635215 -0.743164 0.214434 0.375136 -0.901827 +0.340531 0.673978 0.655586 0.334615 0.699449 0.631509 -0.531752 0.226674 -0.816002 +-0.00130841 0.00533643 -0.999985 0.335383 0.599569 0.72666 0.115929 0.351105 0.929132 +0.0178623 -0.0471608 -0.998728 -0.0471835 -0.074738 0.996086 0.323448 0.198452 -0.925202 +-0.246019 -0.506644 0.826309 0.150759 -0.311955 -0.93806 -0.272807 -0.791656 0.546679 +-0.394123 -0.828993 0.396785 -0.547632 -0.67734 0.491233 -0.494591 -0.710343 0.500793 +-0.485867 -0.723781 -0.489974 -0.113491 -0.861817 -0.494359 -0.389489 -0.637412 -0.664834 +-0.439855 -0.805428 0.397257 -0.384937 -0.897747 0.214183 -0.553961 -0.812935 0.179624 +-0.420184 -0.907416 -0.00640507 -0.556763 -0.827045 -0.0775331 -0.415279 -0.886107 -0.20581 +-0.558312 -0.772054 -0.303677 -0.407235 -0.833817 -0.372704 0.172935 0.913383 0.368545 +0.324867 0.694129 0.642375 0.4353 0.67543 0.595238 0.0281814 0.999157 0.0298446 +0.00636051 0.99998 -0.000373366 -0.0119334 0.999745 0.0191974 0.0294935 0.997183 -0.0689689 +0.409582 0.60666 -0.681327 0.250335 0.845994 -0.470772 0.157471 0.519937 -0.839565 +0.174123 0.675165 0.716822 0.302524 0.687224 0.660457 0.466835 0.631401 0.619192 +-0.0201394 0.300847 -0.95346 0.16898 0.645749 0.744617 0.246992 0.450785 0.857781 +0.41965 0.617091 0.665653 0.46862 -0.306166 -0.828648 0.258116 0.458563 -0.85035 +0.00788995 -0.0378481 -0.999252 0.0325557 0.0704534 0.996984 0.121363 -0.0176091 -0.992452 +-0.296205 -0.511664 -0.806513 -0.16821 -0.333394 0.92766 0.576919 0.18692 -0.795126 +0.359095 0.513386 -0.779414 0.183465 0.4011 -0.897474 -0.339954 -0.729935 0.592981 +-0.340507 -0.600286 0.723679 -0.250151 -0.744779 0.618651 0.515797 -0.433654 -0.738849 +0.184834 -0.28872 -0.939403 -0.192294 -0.920156 0.341081 -0.350655 -0.86117 0.368005 +-0.48556 -0.709967 0.510077 -0.52564 -0.73428 0.429576 -0.524919 -0.719157 -0.455274 +-0.363536 -0.879202 -0.30797 -0.513344 -0.703196 -0.491928 0.205901 -0.959811 -0.190701 +0.143047 -0.798936 -0.584157 -0.441915 -0.843298 0.305876 -0.401991 -0.91499 0.0345918 +-0.422686 -0.887534 -0.183357 0.00709311 0.94242 0.334356 0.0522779 0.748642 0.66091 +-0.000727646 0.999301 0.037362 -0.00628991 0.999898 -0.0128153 -0.019514 0.999809 0.00138966 +-0.0488175 0.99863 -0.0188405 -0.117746 0.889413 -0.441679 -0.190553 0.398018 -0.897369 +0.0523111 0.684304 0.727318 0.0478697 0.718606 0.693768 -0.0974139 0.0809387 -0.991947 +0.0873116 0.67099 0.736308 0.0606915 0.378073 0.923784 -0.505023 0.235853 -0.830256 +-0.280275 -0.0233147 -0.959636 -0.036615 -0.0429723 0.998405 -0.187407 0.237671 -0.953095 +-0.248052 -0.495469 0.832455 -0.150657 0.545655 -0.824356 -0.409287 -0.0371684 -0.911649 +-0.317192 -0.631961 0.707117 -0.342489 -0.666965 0.661709 -0.422556 -0.359187 -0.832125 +-0.560705 -0.317384 -0.764773 -0.250866 -0.628914 -0.735889 -0.206275 -0.882407 0.422857 +-0.12688 -0.984282 0.122843 -0.342408 -0.925648 0.161038 -0.552319 -0.816328 0.168976 +-0.428244 -0.903353 -0.0236562 -0.563515 -0.821194 -0.0899517 -0.40398 -0.897482 -0.176992 +-0.578313 -0.749275 -0.32271 -0.3422 -0.90211 0.262864 -0.271873 -0.737619 0.618064 +0.578859 -0.776706 0.248293 0.0331435 -0.616333 -0.786788 0.0414532 -0.993961 -0.101606 +-0.0410031 0.932281 0.359404 0.00223066 0.772485 0.635029 0.00986688 0.999365 0.0342246 +0.00882921 0.99993 -0.00792016 0.00791627 0.999499 -0.0306353 -0.140492 0.961444 -0.236403 +-0.215655 0.755356 -0.618814 -0.340692 0.713257 -0.61253 0.114376 0.801518 0.586931 +0.202089 0.721622 0.662135 -0.303566 0.552811 -0.776046 -0.155196 0.23295 -0.960025 +0.32767 0.627632 0.706194 0.0940128 0.299367 0.949495 -0.0672651 0.0432672 -0.996797 +-0.168596 -0.294046 0.940804 -0.0177707 0.0616111 -0.997942 -0.352508 -0.640316 0.682446 +-0.315694 -0.612566 0.724638 -0.296878 -0.702157 0.647178 -0.52183 0.214116 -0.825741 +-0.297895 -0.0389597 -0.953803 -0.433233 -0.689338 0.580623 -0.454624 -0.707178 0.541495 +-0.553734 -0.70803 0.438261 -0.532526 -0.76902 -0.353589 -0.462807 -0.88244 -0.0843118 +-0.560292 -0.828282 -0.00465412 -0.563777 -0.716629 0.410607 -0.45292 -0.477324 -0.753011 +-0.404089 -0.875408 0.265278 -0.376542 -0.925696 0.0361108 -0.396532 -0.917627 -0.0269027 +-0.432925 -0.885815 -0.16706 -0.696157 -0.65891 0.284962 -0.632727 -0.740601 -0.226201 +-0.294958 0.840482 0.454521 -0.170592 0.791991 0.586216 -0.0435434 0.994055 0.0997912 +0.0341574 0.999309 -0.0146201 0.0170079 0.998813 -0.045646 -0.0435411 0.932691 -0.35804 +-0.265326 0.589652 -0.762832 -0.0990613 0.825176 0.556122 0.0475295 0.524781 0.84991 +0.342593 0.719527 0.604079 -0.190931 0.89961 0.392742 -0.246358 0.541545 -0.803764 +-0.307453 0.248894 -0.918436 -0.300134 0.0305944 0.953406 -0.235854 0.159053 -0.958684 +-0.405321 -0.292992 0.865951 -0.0721033 0.0220265 -0.997154 -0.371844 -0.608769 0.700809 +-0.361459 -0.680047 0.637874 -0.435757 -0.428356 -0.791598 -0.278119 -0.843494 0.459531 +-0.384146 -0.891298 0.240871 -0.56217 -0.814248 0.144794 -0.41578 -0.90946 0.0030044 +-0.565636 -0.824622 -0.00733202 -0.487978 -0.870738 -0.0607598 -0.574194 -0.813513 -0.0921858 +-0.539338 -0.816674 -0.205326 -0.529449 -0.805664 -0.265687 -0.507551 -0.860694 -0.0399732 +-0.579783 -0.81473 -0.00822752 -0.6961 -0.712733 -0.0863446 -0.429071 0.729142 0.533151 +-0.508853 0.682993 0.524013 -0.372606 0.838773 0.39702 -0.078153 0.996119 0.040476 +-0.0236186 0.998277 -0.0537108 -0.148698 0.88708 -0.437011 -0.395676 0.330144 -0.856998 +-0.496289 0.266858 0.826126 -0.406385 0.500451 0.764461 -0.204102 0.909322 0.362595 +-0.348238 0.862427 0.367354 -0.538761 0.364637 -0.759458 -0.624991 0.188967 -0.757415 +-0.594865 -0.062309 0.801407 -0.458519 0.00725435 0.888655 -0.607383 0.260192 -0.750591 +-0.736611 0.335761 -0.587085 -0.572871 -0.374593 0.729039 -0.538969 -0.391763 0.745677 +-0.605487 0.330671 -0.723908 -0.529664 -0.110815 -0.840937 -0.554542 -0.556583 0.618626 +-0.452573 -0.706628 0.543925 -0.595819 -0.653645 0.466634 -0.582268 -0.699423 -0.414452 +-0.691655 -0.676158 -0.253818 -0.65871 -0.737751 -0.147734 -0.723287 -0.690288 -0.0189116 +-0.63235 -0.774578 -0.0127424 -0.790225 -0.568136 -0.229709 -0.628818 -0.671895 -0.391337 +-0.404921 -0.86918 0.283839 -0.373075 -0.927497 0.0237437 -0.424422 -0.886293 -0.185339 +-0.481536 0.70986 0.514025 -0.637247 0.676879 0.368445 -0.517743 0.835773 0.182827 +-0.403942 0.878953 -0.253519 -0.764147 0.222023 -0.605628 -0.565865 0.458263 -0.685413 +-0.484669 0.778574 0.398646 -0.377705 0.406079 0.832129 -0.349121 0.913125 0.210517 +-0.336742 0.866264 0.36904 -0.636935 0.0260834 -0.770476 -0.784208 -0.079126 -0.615432 +-0.502694 -0.119351 0.856186 -0.847619 -0.0307563 -0.529713 -0.749889 -0.323911 -0.576843 +-0.537872 -0.568901 0.622129 -0.605607 -0.454041 0.653519 -0.592049 -0.638062 0.492295 +-0.671019 -0.594469 -0.443103 -0.730469 -0.438403 -0.523657 -0.707002 -0.578258 -0.407144 +-0.916054 -0.38996 -0.0936764 -0.87215 0.0285032 -0.488407 -0.664568 -0.642969 0.38071 +-0.628115 -0.754721 0.189388 -0.738453 -0.671992 0.0558031 -0.612072 -0.786681 -0.0806241 +-0.740941 -0.64073 -0.201175 -0.583887 -0.718757 -0.377444 -0.926563 0.347501 0.143957 +-0.846836 0.487304 -0.21308 -0.715396 0.288256 0.636488 -0.749678 0.513034 0.418067 +-0.603066 0.712418 0.358849 -0.614592 0.788844 0.00137106 -0.943904 0.228324 -0.238564 +-0.820217 0.166312 -0.547344 -0.946118 0.0245617 -0.322889 -0.77492 -0.0361924 0.631022 +-0.769724 0.389252 0.505972 -0.775923 -0.0274141 0.630232 -0.879918 -0.202908 -0.429618 +-0.763814 -0.369682 -0.529078 -0.899826 -0.174398 -0.399874 -0.716562 -0.56999 0.402058 +-0.690478 -0.704795 0.1628 -0.786414 -0.605462 -0.122345 -0.802216 -0.55569 -0.218308 +-0.850461 -0.415816 -0.322202 -0.900488 0.433974 0.0280847 -0.943214 -0.169084 0.285935 +-0.912561 -0.205654 -0.353466 diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 8a2a75b..83ba8f6 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -14,6 +14,172 @@ The workflow builders are organized by anatomy: - Shared helper modules such as `alignment.py`, `boundary.py`, `materials.py`, and `model.py` contain reusable mechanics used by both anatomy workflows. +## Automatic Model QC + +Both anatomy workflows save one three-view anatomy panel (`*_qc_3d.png`) +after model generation. The FAIM adapter saves a corresponding solved SED +panel (`*_sed_3d.png`) after successful processing of a hip or spine model. +The views use `ogo.cli.Visualize.vis3d`: white background, opaque bone and +gold PMMA supports, with constrained surface-adjacent voxel layers in red. +Individual views are framed independently. Numerical BC checks remain in CSV; +the former automatic slice PNG is no longer generated. + +Hips rejected for insufficient shaft coverage also receive the anatomy panel +of their available aligned bone and supports. They retain `too_short` in the +shaft-geometry JSON, have no artificial distal BC, and are not solved. These +images do not imply that the requested shaft length was retained. + +SED uses a linear Jet scale, shared across the three views, from zero to the +99th percentile of bone-element SED. A bone-masked Gaussian (sigma 0.8 mm) +and light surface smoothing improve display only; FE arrays and results are +unchanged. Each PNG has a JSON sidecar recording display settings. Spine +generation retains `*_qc_body_mask.nii.gz` in model coordinates for consistent +body/process colouring after solving. Temporary individual renders are removed. + +Rendering needs a working graphics backend (X display, EGL/OSMesa, or native +macOS VTK). Graphics failures produce a warning rather than invalidate a +numerical model. Rendering is implemented in `ogo/fea/qc_render.py`. + +### Measurement-Based Audit And Gallery + +Generation writes `*_qc_metrics.csv` while the model is still in memory, +including hips rejected as `too_short`. These CSVs are independent of rendering: +an unavailable graphics backend does not prevent geometric measurement. +The file contains one row per model; missing measurements are blank, not zero. +When SED is loaded for the solved preview, finite/nonnegative SED checks are +added to the same row without replacing the generation measurements. + +```bash +ogoValidateFEA derivatives/fea --output derivatives/fea/qc --gallery +``` + +The command reuses generation measurement CSVs without opening `.n88model` files. +For older models without a CSV, it reads the model once, reconstructs available +measurements, and saves a sidecar with `measurement_source=legacy_backfill`. +Provide `--site hip` or `--site spine` if the loading axis cannot identify anatomy. +Recovered boundary sets are recorded and exist only in memory; models are not +rewritten. Hip coverage is recovered from `*_shaft_geometry.json`; its saved +crop plane also disambiguates distal nodes from combined GT/shaft constraints. +Spine anatomy is recovered from registered `*_qc_body_mask.nii.gz` and optional +`*_qc_process_mask.nii.gz`. When process is absent, it is measured as model bone +minus registered body, with that derivation explicitly recorded. Masks must +match the FE voxel lattice and have at least 95% overlap with model bone. +Native-space segmentations are not accepted without prior registration. + +For files stored elsewhere, supply `--evidence evidence.csv`: + +```csv +site,model_id,body_mask,process_mask,shaft_geometry +spine,sub-001_L1,registered/sub-001_body.nii.gz,, +hip,sub-001_LF,,,geometry/sub-001_shaft_geometry.json +``` + +Paths are relative to this CSV (absolute paths are also supported). Do not list +a Pistoia mask as `body_mask` unless its provenance confirms it is the registered +vertebral body: the filename alone does not identify the anatomical region. +Recovered evidence sources and derivations are recorded in the measurement CSV. +Legacy backfill is cached using its version and input paths, sizes, and modification +times. Adding or changing evidence automatically refreshes a legacy CSV once; +unchanged unresolved models are not repeatedly reopened. Use `--workers 4` for +bounded process-parallel backfill; auditing cached CSVs remains lightweight. + +Original scan coverage and other unrecoverable quantities remain missing and +require review. An unreadable model +is retained as a review row with its reconstruction error, not silently dropped. +The command writes +`qc_summary.csv`; `--gallery` additionally writes a standalone `gallery.html`. +Without a refreshed editable installation, use `python -m ogo.cli.ValidateFEA`. +The gallery supports anatomy, QC status, reason, subject search, and anatomy/SED +view selection, with expandable measurement tables. A separate camera selector +shows oblique/side, top, bottom, or all views in both thumbnails and the review +dialog. The fixed three-row Ogo renders are split once into cached PNGs in +`gallery_views/`; unrecognized legacy layouts remain full montages. Image files +remain external; retain their relative location and the view-assets folder when +copying the gallery and models together. + +For sharing, export one portable ZIP instead: + +```bash +ogoValidateFEA derivatives/fea --output derivatives/fea/qc --gallery-zip --workers 4 +``` + +Extract `gallery.zip` and open `gallery.html`. It includes `images/`, +`gallery_views/`, `qc_summary.csv`, and the initial `study_inclusion.csv`. +No original model directory or server is needed to review it. Model files are +not included; original model paths remain in the CSV as provenance. Both solve +stages and all available camera views are retained. Missing images remain +missing. Large cohorts use lazy-loaded image files rather than embedded HTML. +After review, export the updated inclusion CSV from the gallery; browser +decisions cannot rewrite the CSV inside the original ZIP. +The export popup directs users to browser downloads and explicitly states that +the project CSV is unchanged. Import the downloaded CSV to resume its decisions. + +Include and Exclude on each gallery card record a quick decision with reason +`manual_review`, reviewer, and UTC timestamp. Use Review for a specific reason +or explanation. The hip side view looks toward the distal cut to expose its +boundary while retaining the shaft profile, before and after solving. + +Hip distal coverage QC uses the recipe's central 90%-width support patch, not +the entire irregular bone cross-section. New measurements must show complete +coverage of that intended patch. Full-face area and coverage remain in the CSV +for inspection. Older CSVs without patch measurements use a minimum full-face +coverage of 89%; legacy backfill refreshes them with the explicit patch check. +Flat-cut, shaft-length, and X/Z fixation checks remain separate. + +Click a model name, image, or Review to open its measurements and views. Include +and Exclude override the study decision only; Reset to automatic clears that +override. Choose a reason and optionally add an explanation. The default reason +is `manual_review`. Reviewer name/initials and UTC review time are recorded; +missing reviewer names are explicitly recorded as `unspecified`. + +The gallery exports `study_inclusion.csv` with automatic QC, manual decisions, +reasons, notes, and final inclusion. Automatic passes default to inclusion, +failures to exclusion, and review cases to pending. A manually included too-short +model remains ineligible for solving. Browser autosave is a convenience scoped +to the audited dataset, not the durable study record: export the CSV before +closing. Import CSV resumes review in the gallery. To regenerate a gallery with +the same review decisions, use: + +```bash +ogoValidateFEA derivatives/fea --output derivatives/fea/qc --gallery \ + --reviews study_inclusion.csv +``` + +The CLI also writes an initial `study_inclusion.csv`. Browser exports reflect +subsequent clicks and do not overwrite the server-side file automatically. +Duplicate site/model-ID pairs are rejected; audit separate length runs/cohorts +separately. Unknown imported identities are rejected rather than ignored. + +Measurements include bone/disk volumes and face-connected components, disk axial +contact footprint area/fraction and connectivity, disk axial thickness percentiles, +boundary-node counts/extents/centroids, loading direction, and hip available, +requested, and measured shaft lengths and distal BC coverage. +For hip, `shaft_bc_node_count` is the distal node-set size; separate +`shaft_bc_fixed_x_node_count` and `shaft_bc_fixed_z_node_count` count those +same nodes actually fixed at zero displacement along each required axis. +Incomplete fixation is a failure, independently of distal face coverage. +Spine records +aligned body/process dimensions, volumes, components, and centroid offsets. +Registration rotation is recorded, but a large rotation alone does not fail QC. +Disk numbers follow spatial component order, not guaranteed anatomical names. +Contact area counts projected axial shared faces, not the whole endplate fraction. + +Policy version 1 fails construction violations: insufficient length, missing BC, +incorrect disk count, noncompressive loading, nonplanar axial/distal BC, +incorrect shaft length, incomplete distal section/BC, zero axial disk contact, +or invalid available SED. Disconnected bone/contact footprints, process-vector +orientation concerns, and missing required measurements trigger `review`. +`pass` means the implemented construction tests passed, not that every possible +anatomical error has been excluded. No strength-based thresholds are used. + +Registration surface distances, true support penetration/trim-depth distributions, +and solver force balance are not yet included. These need validated measurement +definitions before adding thresholds. Generation measurements are reused; +cached legacy measurements refresh when the backfill version or supplied +evidence changes. Legacy backfill does not generate missing preview images. +Failures before an FE mesh exists require the job manifest/log for participant flow. +Implementation: `ogo/fea/validation.py`; CLI: `ogo/cli/ValidateFEA.py`. + ## Inputs All FEA workflows expect a density-calibrated CT image and an aligned @@ -73,12 +239,16 @@ For spine, the maintained path is: Pistoia mask, and any lower-level overrides. 2. `spine.py::main` thresholds the requested body/process labels, crops around the vertebra, checks posterior-process orientation, and runs scaled ICP to - the bundled L4 body reference from `default_spine_reference_path`. The - default backend is VTK ICP through `alignment.py::estimate_rigid_icp_vtk`, - matching the original spine registration behavior. The deterministic NumPy - point-cloud ICP helper remains available as `--registration_backend numpy` - and is also used for alternate PCA-start rescue attempts when process-vector - QC flags an implausible orientation. + the selected reference surface from `default_spine_reference_path`. The + maintained default `--spine_icp_target body` uses only the vertebral body + label and the bundled `L4_BODY_SPINE_COMPRESSION_REF.vtk` surface. The + optional `--spine_icp_target vertebra` uses the body plus posterior-process + labels and requires a matching full-vertebra reference surface. The default + backend is VTK ICP through `alignment.py::estimate_rigid_icp_vtk`, matching + the original spine registration behavior. The deterministic NumPy point-cloud + ICP helper remains available as `--registration_backend numpy` and is also + used for alternate PCA-start rescue attempts when process-vector QC flags an + implausible orientation. 3. `spine.py` applies the transform and resamples the density image with cubic interpolation and labels/masks with nearest-neighbor interpolation. 4. `boundary.py::generate_bone_cap_mask` and related helpers generate superior @@ -98,16 +268,19 @@ For hip, the maintained short-femur path is: 2. `femur.py::main` selects the side-specific femur mask and aligns it to the bundled left or mirrored-right reference. The fixed rough crop for ICP stability is controlled by `DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM`. -3. In `greater_trochanter_length` mode, the rough crop is used only for the ICP +3. The rough crop is used only for the ICP estimate. The final model is then regenerated from the transformed full scan. -4. `femur.py::crop_vtk_images_to_greater_trochanter_length` applies the final - post-ICP flat shaft crop. The requested distal length is measured after the - generated GT support, not from the femoral head and not from the raw GT - landmark. -5. `femur.py::proximal_sideways_fall_fixture_plane` and +4. `femur.py::proximal_sideways_fall_fixture_plane` and `boundary.py::generate_projected_material_disk_vtk` create the femoral-head - and greater-trochanter PMMA supports. The distal crop face becomes the distal - shaft support. + and greater-trochanter PMMA supports on the full aligned femur. +5. `femur.py::crop_vtk_images_to_greater_trochanter_length` measures the actual + generated GT disk's distal voxel face and applies the final flat shaft crop + the requested length distal to it. Before accepting the length, the usable + distal face must clear the entire transformed native scan end, removing + oblique-tail coverage. The fixture box is not a substitute for the disk edge. + Proximal supports stay unchanged, and the external crop face becomes the + distal shaft support. `_shaft_geometry.json` records the available coverage + and the final crop coordinates. Insufficient coverage stops model generation. 6. `materials.py::build_femur_material_table` assigns bone and PMMA material definitions. The simple whole-femur path uses one trabecular-style bone region unless a compartment mask is supplied. @@ -115,13 +288,13 @@ For hip, the maintained short-femur path is: defaulting to `4%` displacement. The most important short-femur convention is the final shaft-length definition. -In `greater_trochanter_length` mode the requested value is the distance between +The requested value is the distance between the final GT support and the final distal shaft boundary condition. The audit in -`ogo/cli/CheckFEModelBC.py::audit_femur_sideways` reports the robust model-space +`ogo/cli/CheckFEModelBC.py::audit_femur_sideways` reports the model-space measurement as: ```text -p5(z of Greater_Trochanter_PMMA_Nodes) - median(z of Distal_Femur_Nodes) +min(z of Greater_Trochanter_PMMA_Nodes) - median(z of Distal_Femur_Nodes) ``` This is deliberately a boundary-condition measurement. It verifies the @@ -470,7 +643,7 @@ Available spine presets: | `none` | Pass only explicit lower-level options. | Spine registration can be switched without editing code. The maintained default -keeps the original VTK ICP behavior: +keeps the original VTK ICP behavior and uses the vertebral-body surface for ICP: ```bash ogoFEA spine image.nii.gz spine_labels.nii.gz \ @@ -480,6 +653,21 @@ ogoFEA spine image.nii.gz spine_labels.nii.gz \ --registration_iterations 75 ``` +For cases where the box-like vertebral body gives ambiguous rotations, ICP can +instead use the whole vertebra surface. This needs a reference surface generated +from the same aligned L4 reference, including body and posterior processes: + +```bash +ogoFEA spine image.nii.gz spine_labels.nii.gz \ + --vertebra L1:20:48 \ + --spine_icp_target vertebra \ + --reference_path /path/to/L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk +``` + +If `--reference_path` is omitted with `--spine_icp_target vertebra`, Ogo looks +for the bundled `ogo/dat/L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk` file and +fails clearly if it is not installed. + For sensitivity checks, the deterministic point-cloud ICP path can be selected: ```bash @@ -496,22 +684,29 @@ The femur workflow builds one sideways-fall model per side: 1. Read the calibrated image and whole-femur mask. 2. Pre-rotate the side to a stable starting orientation. -3. ICP-align to the left femur reference or the mirrored right reference. +3. ICP-align to the left femur reference or the mirrored right reference. The + maintained hip default uses evenly distributed surfaces (up to 40,000 points + per surface), four axial starting rotations, and bidirectional mean surface + distance to select a rigid NumPy ICP fit. Superior-inverting candidates are + rejected. The reference may be scaled to estimate registration; the native + bone is transformed rigidly and its physical dimensions are not scaled. + This fixed hip registration recipe has no backend-selection option. 4. Apply transform and isotropic resampling through the shared VTK reslice helper. The default output spacing is `1.0 x 1.0 x 1.0 mm`. 5. Smooth the transformed femur mask with one binary close/open pass only when at least one input spacing dimension is coarser than 2 mm. If a compartment mask is supplied, the derived cortical binary mask follows the same rule. -6. Standardize the distal shaft with a flat post-ICP model-grid cut. In - `greater_trochanter_length` mode, the fixed rough crop is used only to make - ICP stable; the final model is then generated from the transformed full scan. - The requested shaft length is measured after the generated - greater-trochanter support, not from the femoral-head support and not from - the raw GT landmark alone. -7. Generate PMMA fixtures for the femoral head and greater trochanter. The +6. Generate PMMA fixtures for the femoral head and greater trochanter. The femoral-head fixture is placed on the high-y side and the greater-trochanter fixture is placed on the low-y side. Both fixtures are anchored to the proximal model footprint so they do not overwrite bone voxels. +7. Standardize the distal shaft with a flat post-ICP model-grid cut. The + rough 120-mm crop is used only for ICP and the + final model uses the transformed full scan. The requested shaft length starts + at the actual generated GT disk's distal face. Crop all model-grid images + together, preserving the proximal fixtures and their coordinates. + The default retained shaft is 10 mm. Scans with less available coverage are + recorded as too short; they are not solved as shorter substitutes. 8. Convert density to material IDs and construct the N88 model. 9. Apply sideways-fall boundary conditions. @@ -534,29 +729,104 @@ is a label inside the same segmentation used as the femur mask, pass `--pistoia_mask_label LABEL` and omit `--pistoia_mask`; Ogo will use the main segmentation as the ROI source and keep only that label. -For the maintained short-femur cohort model, use: +The maintained 10-mm reporting configuration is: ```bash ogoFEA hip image.nii.gz femur_mask.nii.gz \ --side left \ - --femur_cut_mode greater_trochanter_length \ - --femur_greater_trochanter_distal_length 20 \ + --femur_greater_trochanter_distal_length 10 \ --femur_shaft_length 120 \ --run_pistoia \ - --critical_volume 10 \ + --critical_volume 11.2 \ --critical_strain 0.009 ``` +There is one maintained crop recipe. The command no longer exposes aspect-ratio, +lesser-trochanter or oblique-midpoint alternatives. `--femur_cut_mode` remains a +hidden compatibility argument accepting only `greater_trochanter_length`; +old unsupported modes fail rather than silently using a different definition. + +The 11.2% full-bone criterion is specific to the adopted 10-mm shaft protocol; +it is not automatically valid for other shaft lengths. Use `--no-solve` for +geometry inspection. Length-specific calibration does not change the material +law or critical strain. + The generated model can be audited with `ogoFEA check-bc`. For hip sideways fall, the post-GT-support shaft length is measured from final boundary-condition node sets as: ```text -p5(z of Greater_Trochanter_PMMA_Nodes) - median(z of Distal_Femur_Nodes) +min(z of Greater_Trochanter_PMMA_Nodes) - median(z of Distal_Femur_Nodes) ``` -The low GT percentile and distal-shaft median make this measurement robust to a -small number of edge voxels or support-surface bleed-through nodes. +The GT minimum is the distal edge; a percentile inside the disk would shift the +length origin. The distal median represents the flat external support plane. + +### Geometry Records And 3D QC + +Every generated hip model has three inspectable sidecars: + +| File suffix | Contents | +| --- | --- | +| `_shaft_geometry.json` | Full available shaft before cropping, requested length, retained voxel-face length, verified length from the saved N88 BC nodes, GT/cut coordinates, registration diagnostics and export paths. | +| `_surface.vtp` | External FE mesh surface in physical coordinates. `ModelRegion`: 0 bone, 1 GT support, 2 FH loading support. Material IDs are preserved. | +| `_3d.png` | Four opaque mesh views: blue bone, gold GT support, red FH loading support, green distal constraint nodes. | + +`_icp.json` records the selected transform and all starting-fit scores. Explicit +`--femur_icp_transform_out` overrides its path. A supplied transform remains +useful for fixed-registration length sweeps, but cohort regeneration should +estimate a fresh transform rather than reuse an unreviewed historical fit. + +`available_shaft_length_mm` is measured on the transformed full-scan model +after generating the actual connected GT support and before the final crop. +It measures **usable complete-section coverage**, not the distance to the +lowest corner of an angled scan end. Before any resampling, rough crop or +padding, `shaft_geometry.capture_distal_scan_face` records the physical voxel +corners of the first occupied distal femur slice. The inverse rigid ICP carries +this original full-scan face into the model frame. The most proximal point +(maximum aligned Z), plus a voxel-scale resampling clearance, sets the minimum +safe flat plane; it is snapped proximally to the model's voxel-face grid. +Background image padding and the separate 120-mm registration crop do not +define the usable scan end. + +The clearance is half the first resampling voxel's extent projected onto +aligned Z, plus half a final Z voxel. It bounds the two nearest-neighbor mask +resamplings and is recorded in the sidecar; there is no fitted distance or +percentile threshold. The final cut is the requested shaft length distal to the +unchanged GT disk. If that cut cannot clear the original scan end, generation +stops as `too_short`; it never substitutes a shorter shaft. No extra physical +pre-trim changes the proximal supports or the ICP input. + +Version 3 geometry records distinguish `raw_available_shaft_length_mm`, +`safe_distal_face_z_mm`, `oblique_trim_loss_mm`, and usable +`available_shaft_length_mm`. `distal_scan_boundary` records the original and +aligned limits and clearance. `complete_distal_section_verified` becomes true +only when the saved distal BC plane also clears that safe limit. This check +does not establish segmentation correctness or replace anatomical QC. +`measured_shaft_length_mm` is checked after rereading the saved N88 file. +For insufficient coverage, the JSON is still written with `status: too_short` +and a null final length. Registration failures are unmeasured, not assigned a +zero length. Coverage plots must use these records, state their denominator, +and distinguish missing measurements from genuinely short scans. Successful +geometry checks do not by themselves establish anatomical alignment quality. + +Implementation: `hip_registration.estimate_femur_icp` selects the rigid fit; +`shaft_geometry.measure_available_shaft` defines coverage; +`femur.crop_vtk_images_to_greater_trochanter_length` cuts all model-grid images; +`shaft_geometry.verify_model_shaft` verifies the saved BC nodes; and +`model_export.export_femur_model` writes the mesh and four-view image. +These helpers do not change the material law, loading endpoint or regional +Pistoia definitions. Whole-bone hip Pistoia defaults to 10% at strain 0.009. + +Use `--no-solve` to generate models and QC for inspection before solving. +`--threads` limits VTK/ITK generation as well as the FAIM solver. For an +I/O-heavy cohort generation pass, independent `--no-solve --threads 1` jobs +can use the CPU budget without multiplying each job's image-filter threads. +PNG rendering needs an X display or an EGL/OSMesa-enabled headless VTK build; +without one, the N88 model and VTP are preserved and the export record marks +PNG QC as unavailable. The surface export itself does not require a display. +On Groot the existing +`ogoloco-n88` environment provides headless VTK and the modeling dependencies. ## Materials diff --git a/ogo/fea/alignment.py b/ogo/fea/alignment.py index 19ba508..516821f 100644 --- a/ogo/fea/alignment.py +++ b/ogo/fea/alignment.py @@ -50,10 +50,8 @@ def sample_points(points, *, max_points=None, mode="linspace", offset=0): step = max(1, int(np.ceil(coordinates.shape[0] / int(max_points)))) start = int(offset) % step indices = np.arange(start, coordinates.shape[0], step, dtype=int) - if indices.size < int(max_points): - fallback = np.linspace(0, coordinates.shape[0] - 1, int(max_points), dtype=int) - indices = np.unique(np.concatenate([indices, fallback])) - indices = indices[: int(max_points)] + # A stride may yield fewer than the cap. Filling, sorting, then truncating + # biases the sample toward the beginning of a spatially ordered surface. else: indices = np.linspace(0, coordinates.shape[0] - 1, int(max_points), dtype=int) return coordinates[indices] @@ -128,6 +126,7 @@ def estimate_rigid_icp( convergence="delta", distance_mode="mean", initial_transform=None, + nearest_workers=None, ): """Estimate a rigid transform from ``moving_points`` to ``fixed_points``.""" import numpy as np @@ -153,7 +152,10 @@ def estimate_rigid_icp( distance_token = str(distance_mode).strip().lower() for used_iterations in range(1, max(1, int(iterations)) + 1): transformed = moving @ rotation.T + translation - matched = fixed[_nearest_indices(transformed, fixed)] + if nearest_workers is None: + matched = fixed[_nearest_indices(transformed, fixed)] + else: + matched = fixed[_nearest_indices(transformed, fixed, workers=nearest_workers)] step_rotation, step_translation = _kabsch(transformed, matched) rotation = step_rotation @ rotation translation = step_rotation @ translation + step_translation @@ -393,12 +395,12 @@ def _principal_axes(points): return axes -def _nearest_indices(query, target): +def _nearest_indices(query, target, *, workers=-1): try: from scipy.spatial import cKDTree except ImportError: return _nearest_indices_numpy(query, target) - return cKDTree(target).query(query, workers=-1)[1] + return cKDTree(target).query(query, workers=workers)[1] def _nearest_indices_numpy(query, target): diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index e570787..eb1726b 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -18,12 +18,10 @@ at least one input spacing dimension is coarser than 2 mm. If a compartment mask is supplied, the derived cortical binary mask follows the same rule. 4. Stabilize registration with a fixed 120 mm proximal rough crop after - isotropic resampling. For GT-length analyses, rerun the final model from the - transformed full scan and crop after ICP so the requested shaft length is - measured distal to the generated greater-trochanter PMMA support. The - post-ICP crop face becomes the distal support surface. Historical ratio, - lesser-trochanter, and fixed-length modes remain available for debugging and - sensitivity checks. + isotropic resampling. After ICP, apply the final flat distal crop on the + transformed full scan so the shaft extends the requested length distal to the edge + of the greater-trochanter PMMA support. This final crop face becomes the + distal support surface. 5. Generate two geometric PMMA fixtures from proximal contact planes: a femoral-head loading fixture on the high-y side and a greater-trochanter contact fixture on the low-y side. Defaults are 10 mm PMMA thickness and @@ -59,6 +57,13 @@ foreground_voxel_center_bounds_from_mask, ) from ogo.fea.image_io import write_vtk_image_with_sitk_geometry +from ogo.fea.hip_registration import estimate_femur_icp +from ogo.fea.shaft_geometry import ( + aligned_distal_scan_boundary, + capture_distal_scan_face, + measure_available_shaft, + verify_model_shaft, +) LEFT_FEMUR = 1 @@ -85,15 +90,18 @@ DEFAULT_FEMUR_PROXIMAL_REFERENCE_WIDTH = "max_xy" DEFAULT_FEMUR_REFERENCE_MIN_SCALE = (0.8, 0.8, 0.75) DEFAULT_FEMUR_REFERENCE_MAX_SCALE = (1.2, 1.2, 1.3) +DEFAULT_FEMUR_REGISTRATION_BACKEND = "numpy" +DEFAULT_FEMUR_REGISTRATION_LANDMARKS = 40000 +DEFAULT_FEMUR_REGISTRATION_ITERATIONS = 50 DEFAULT_PMMA_THICKNESS_MM = 10.0 DEFAULT_PMMA_INTRUSION_MM = 6.0 DEFAULT_FEMUR_INPUT_MARGIN_MM = DEFAULT_PMMA_THICKNESS_MM + DEFAULT_PMMA_INTRUSION_MM DEFAULT_FEMUR_SHAFT_LENGTH_MM = 120.0 DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM = 120.0 -DEFAULT_FEMUR_CUT_MODE = "post_icp_flat_ratio" -DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM = 175.0 +DEFAULT_FEMUR_CUT_MODE = "greater_trochanter_length" +DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM = 10.0 DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM = 0.0 -DEFAULT_FEMUR_PISTOIA_CRITICAL_VOLUME_PERCENT = 7.0 +DEFAULT_FEMUR_PISTOIA_CRITICAL_VOLUME_PERCENT = 11.2 DEFAULT_FEMUR_PISTOIA_CRITICAL_STRAIN = 0.009 PRE_ICP_CROP_MODES = {"bbox_ratio", "proximal_box_ratio"} ROUGH_PRE_ICP_CROP_MODES = {"post_icp_flat_ratio", "post_icp_oblique_ratio", "greater_trochanter_length"} @@ -240,6 +248,8 @@ def write_icp_transform(path, *, matrix, icp_transform, reference_scale, femur_s "femur_side": int(femur_side), "matrix": matrix_rows, "icp": { + **{key: value for key, value in icp_transform.items() + if key not in {"rotation", "translation"}}, "iterations": int(icp_transform["iterations"]), "mean_distance": float(icp_transform["mean_distance"]), }, @@ -927,61 +937,50 @@ def crop_vtk_images_to_greater_trochanter_length( vtk_images, vtk_mask, *, + gt_support_vtk, + distal_boundary=None, retained_length_mm=DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, gt_inclusion_length_mm=DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, labels=None, ): - """Apply the post-ICP crop for a requested shaft length after GT support. + """Crop a supported model a fixed distance distal to its actual GT disk. - The generated sideways-fall GT fixture uses a proximal z footprint with the - same long-axis length as the femoral-head fixture. Reserve that footprint - first, then keep ``retained_length_mm`` of shaft distal to it. Final QC - measures this on generated node sets as low-percentile GT-support z minus - median distal-shaft-support z. + The inputs must share the model grid and include the generated supports. + Length is measured between voxel faces, which become mesh-node planes. + No anatomical landmark or fixture-box surrogate is used for the origin. """ import numpy as np - from ogo.util.vtk_image import vtk_image_to_numpy + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy retained_length_mm = float(retained_length_mm) - if retained_length_mm <= 0.0: + if not np.isfinite(retained_length_mm) or retained_length_mm <= 0.0: raise ValueError("retained_length_mm must be positive.") - gt_inclusion_length_mm = float(gt_inclusion_length_mm) - if gt_inclusion_length_mm < 0.0: - raise ValueError("gt_inclusion_length_mm must be non-negative.") - - mask_data = vtk_image_to_numpy(vtk_mask) - active = _active_crop_mask(mask_data, labels) - if not np.any(active): - raise ValueError("Cannot apply greater-trochanter crop to an empty femur mask.") + if float(gt_inclusion_length_mm) != 0.0: + raise ValueError("Shaft length starts at the GT disk edge; no additional offset is supported.") + measurement = measure_available_shaft( + vtk_mask, gt_support_vtk, requested_length_mm=retained_length_mm, + distal_boundary=distal_boundary, labels=labels + ) + active = _active_crop_mask(vtk_image_to_numpy(vtk_mask), labels) spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) origin = np.asarray(vtk_mask.GetOrigin(), dtype=np.float64) - coords = np.argwhere(active) - lo = coords.min(axis=0).astype(np.int64) - hi = (coords.max(axis=0) + 1).astype(np.int64) - y_width_mm = float(hi[1] - lo[1]) * float(spacing[1]) - z_length_mm = float(hi[2] - lo[2]) * float(spacing[2]) - - landmark = detect_lesser_trochanter_cut_z(vtk_mask, distal_offset_mm=0.0) - gt_z = float(landmark["greater_trochanter_z"]) - gt_disk_distal_z = float(landmark["greater_trochanter_disk_distal_z"]) - support_length_mm = min(float(FEMORAL_HEAD_FIXTURE_LONG_AXIS_EXTENSION_MM), z_length_mm) - gt_support_distal_z = float(landmark["mask_z_max"]) - support_length_mm - distal_length_origin_z = gt_support_distal_z - gt_inclusion_length_mm - cut_z = distal_length_origin_z - retained_length_mm - available_mm = distal_length_origin_z - float(landmark["mask_z_min"]) - if available_mm < retained_length_mm: + gt_active = (vtk_image_to_numpy(gt_support_vtk) != 0) & active + extent_z = vtk_mask.GetExtent()[4] + gt_min_index = int(np.argwhere(gt_active)[:, 2].min()) + gt_support_distal_z = measurement["distal_length_origin_z"] + available_mm = measurement["available_shaft_length_mm"] + if not measurement["eligible_for_requested_length"]: raise ValueError( - "Femur scan is too short for the requested post-GT-support shaft length: " - "requested %.4f mm, available %.4f mm below the GT-support distal length origin." + "Femur scan is too short for the requested greater-trochanter distal shaft length: " + "requested %.4f mm, available %.4f mm below the GT support distal length origin." % (retained_length_mm, max(0.0, available_mm)) ) - z_centers = origin[2] + np.arange(active.shape[2], dtype=np.float64) * spacing[2] - keep = active & (z_centers[None, None, :] >= cut_z) - if not np.any(keep): - raise ValueError("Greater-trochanter distal crop removed the entire femur mask.") + cut_index = int(round(gt_min_index - retained_length_mm / spacing[2])) + cut_z = float(origin[2] + (extent_z + cut_index - 0.5) * spacing[2]) + keep = np.broadcast_to(np.arange(active.shape[2])[None, None, :] >= cut_index, active.shape) cropped_images, crop_face_image, meta = _crop_vtk_images_with_keep_mask( vtk_images, @@ -989,34 +988,31 @@ def crop_vtk_images_to_greater_trochanter_length( active=active, keep=keep, meta={ + **measurement, "enabled": True, "method": "greater_trochanter_length", - "retained_length_mm": retained_length_mm, + "retained_length_mm": float(gt_support_distal_z - cut_z), "requested_retained_length_mm": retained_length_mm, - "gt_inclusion_length_mm": gt_inclusion_length_mm, - "available_below_gt_disk_mm": float(gt_disk_distal_z - float(landmark["mask_z_min"])), - "available_below_gt_support_mm": float(available_mm), - "available_below_gt_inclusion_mm": float(available_mm), - "available_below_gt_mm": float(gt_z - float(landmark["mask_z_min"])), - "greater_trochanter_z": gt_z, - "greater_trochanter_disk_distal_z": gt_disk_distal_z, - "greater_trochanter_disk_proximal_z": float(landmark["greater_trochanter_disk_proximal_z"]), - "greater_trochanter_support_distal_z": float(gt_support_distal_z), - "greater_trochanter_support_length_mm": float(support_length_mm), - "distal_length_origin_z": float(distal_length_origin_z), - "lesser_trochanter_z": float(landmark["lesser_trochanter_z"]), + "available_below_gt_support_distal_edge_mm": float(available_mm), + "greater_trochanter_support_distal_edge_z": float(gt_support_distal_z), + "distal_length_origin_z": float(gt_support_distal_z), "cut_z_mm": float(cut_z), - "reference_axis": "greater_trochanter_support_distal_z_minus_optional_offset", - "reference_width_mm": y_width_mm, - "input_z_length_mm": z_length_mm, + "reference_axis": "generated_gt_disk_distal_face_z", "status": "cropped", - "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), - "crop_stage": "after ICP on aligned full-scan reference grid", + "crop_stage": "after ICP and support generation on full-scan model grid", }, ) - meta["aspect_ratio_z_over_y"] = ( - float(retained_length_mm) / float(y_width_mm) if y_width_mm > 0.0 else None - ) + # Select the external flat face even when coverage exactly equals the target + # and no occupied voxel was removed by this crop. + face_data = np.zeros(cropped_images[0].GetDimensions(), dtype=np.uint8) + face_index = cut_index - meta["crop_slices_xyz"][2][0] + if 0 <= face_index < face_data.shape[2]: + cropped_active = active[tuple(slice(lo, hi) for lo, hi in meta["crop_slices_xyz"])] + face_data[:, :, face_index] = cropped_active[:, :, face_index] + crop_face_image = numpy_to_vtk_image(face_data, crop_face_image) + meta["crop_face_voxels"] = int(np.count_nonzero(face_data)) + if not meta["crop_face_voxels"]: + raise ValueError("The requested shaft cut has no bone contact surface.") return cropped_images, crop_face_image, meta @@ -1211,8 +1207,10 @@ def _crop_vtk_images_with_keep_mask(vtk_images, vtk_mask, *, active, keep, meta) out_hi = (coords.max(axis=0) + 1).astype(np.int64) slices = tuple(slice(int(out_lo[axis]), int(out_hi[axis])) for axis in range(3)) spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) + extent = vtk_mask.GetExtent() + extent_offset = (extent[0], extent[2], extent[4]) origin = tuple( - float(vtk_mask.GetOrigin()[axis]) + float(out_lo[axis]) * float(spacing[axis]) + float(vtk_mask.GetOrigin()[axis]) + float(extent_offset[axis] + out_lo[axis]) * float(spacing[axis]) for axis in range(3) ) cropped_images = [] @@ -2309,6 +2307,7 @@ def expand_xz_footprint(bounds, *, x_extension_mm=0.0, z_extension_mm=0.0): import sys import argparse import time +import json from datetime import date from collections import OrderedDict import numpy as np @@ -2370,16 +2369,8 @@ def sidewaysFallFe(args): pmma_yield_compression = args.pmma_yield_compression pmma_yield_tension = args.pmma_yield_tension femur_shaft_length = args.femur_shaft_length - femur_cut_mode = args.femur_cut_mode - femur_bbox_ratio = args.femur_bbox_ratio - femur_bbox_crop_from = args.femur_bbox_crop_from - femur_experimental_crop_ratio = args.femur_experimental_crop_ratio femur_greater_trochanter_distal_length = args.femur_greater_trochanter_distal_length femur_greater_trochanter_inclusion_length = args.femur_greater_trochanter_inclusion_length - femur_proximal_reference_distance = args.femur_proximal_reference_distance - femur_proximal_reference_width = args.femur_proximal_reference_width - femur_lesser_trochanter_distal_offset = args.femur_lesser_trochanter_distal_offset - femur_lesser_trochanter_distal_offset_percent = args.femur_lesser_trochanter_distal_offset_percent femur_input_margin = args.femur_input_margin femur_icp_transform_in = args.femur_icp_transform_in femur_icp_transform_out = args.femur_icp_transform_out @@ -2428,31 +2419,17 @@ def sidewaysFallFe(args): ogo.message("PMMA Intrusion [mm]: %8.4f" % pmma_intrusion) ogo.message("PMMA Material ID: %d" % pmma_mat_id) ogo.message("Applied Displacement [mm]: %8.4f" % fe_displacement) - ogo.message("Femur Cut Mode: %s" % femur_cut_mode) + ogo.message("Femur Crop Recipe: registration-only rough crop, then a complete flat shaft distal to the GT disk edge") if femur_icp_transform_in: ogo.message("Femur ICP Transform In: %s" % femur_icp_transform_in) if femur_icp_transform_out: ogo.message("Femur ICP Transform Out: %s" % femur_icp_transform_out) - if femur_cut_mode == "bbox_ratio": - ogo.message("Femur BBox Ratio [reference, constrained, free]: %s" % str(femur_bbox_ratio)) - ogo.message("Femur BBox Crop From [reference, constrained, free]: %s" % str(femur_bbox_crop_from)) - if femur_cut_mode in {"proximal_box_ratio", "post_icp_flat_ratio", "post_icp_oblique_ratio"}: - ogo.message("Femur Crop Ratio: %8.4f" % femur_experimental_crop_ratio) - if femur_cut_mode == "proximal_box_ratio": - ogo.message("Femur Proximal Reference Distance [mm]: %8.4f" % femur_proximal_reference_distance) - ogo.message("Femur Proximal Reference Width: %s" % femur_proximal_reference_width) - if femur_cut_mode in {"post_icp_flat_ratio", "post_icp_oblique_ratio"}: - ogo.message("Femur Rough Pre-ICP Retained Length [mm]: %8.4f" % femur_shaft_length) - if femur_cut_mode == "greater_trochanter_length": - ogo.message("Femur Rough Pre-ICP Retained Length [mm]: %8.4f" % femur_shaft_length) - ogo.message( - "Femur Additional Offset Distal To GT Disk [mm]: %8.4f" - % femur_greater_trochanter_inclusion_length - ) - ogo.message( - "Femur Shaft Length Distal To GT Disk [mm]: %8.4f" - % femur_greater_trochanter_distal_length - ) + if args.femur_cut_mode != DEFAULT_FEMUR_CUT_MODE: + raise ValueError("Only the GT-disk-relative complete flat shaft recipe is supported.") + if femur_greater_trochanter_inclusion_length != 0: + raise ValueError("The shaft length origin must be the GT disk distal edge, without an extra offset.") + ogo.message("Femur Rough Pre-ICP Retained Length [mm]: %8.4f" % femur_shaft_length) + ogo.message("Femur Shaft Length Distal To GT Disk [mm]: %8.4f" % femur_greater_trochanter_distal_length) if compartment_mask is not None: ogo.message("Compartment Mask: %s" % compartment_mask) ogo.message("Cortical Label: %d" % cortical_label) @@ -2461,18 +2438,6 @@ def sidewaysFallFe(args): ogo.message("Pistoia ROI Mask: %s" % pistoia_mask) if pistoia_mask_label: ogo.message("Pistoia ROI Label(s): %s" % pistoia_mask_label) - if femur_cut_mode == "fixed_length": - ogo.message("Retained Proximal Femur Length [mm]: %8.4f" % femur_shaft_length) - elif femur_lesser_trochanter_distal_offset_percent is not None: - ogo.message( - "Lesser Trochanter Distal Cut Offset [%% greater-lesser distance]: %8.4f" - % femur_lesser_trochanter_distal_offset_percent - ) - else: - ogo.message( - "Lesser Trochanter Distal Cut Offset [mm]: %8.4f" - % femur_lesser_trochanter_distal_offset - ) ogo.message("Input Foreground Safety Margin [mm]: %8.4f" % femur_input_margin) ## @@ -2486,6 +2451,7 @@ def sidewaysFallFe(args): ogo.message("Reading bone mask...") maskData = ogo.readNii(mask) maskThres = ogo.maskThreshold(maskData, mask_threshold) + native_distal_face = capture_distal_scan_face(maskThres) compartmentData = None if compartment_mask is not None: ogo.message("Reading trabecular/cortical compartment mask...") @@ -2530,90 +2496,23 @@ def sidewaysFallFe(args): interpolation="nearest", ) - if femur_cut_mode in PRE_ICP_CROP_MODES: - ogo.message("Applying %s femur crop after isotropic resampling and before ICP..." % femur_cut_mode) - images_to_crop = [imageData, maskThres] - if compartmentData is not None: - images_to_crop.append(compartmentData) - if pistoiaMaskData is not None: - images_to_crop.append(pistoiaMaskData) - if femur_cut_mode == "bbox_ratio": - cropped_images, distal_crop_face, bbox_crop_meta = crop_vtk_images_to_bbox_ratio( - images_to_crop, - maskThres, - bbox_ratio=femur_bbox_ratio, - bbox_crop_from=femur_bbox_crop_from, - labels={1}, - ) - else: - cropped_images, distal_crop_face, bbox_crop_meta = crop_vtk_images_to_proximal_box_ratio( - images_to_crop, - maskThres, - ratio=femur_experimental_crop_ratio, - proximal_reference_distance_mm=femur_proximal_reference_distance, - reference_width=femur_proximal_reference_width, - labels={1}, - ) - imageData = cropped_images[0] - maskThres = cropped_images[1] - next_crop_index = 2 - if compartmentData is not None: - compartmentData = cropped_images[next_crop_index] - next_crop_index += 1 - if pistoiaMaskData is not None: - pistoiaMaskData = cropped_images[next_crop_index] - registration_mask = maskThres - ogo.message( - "%s crop slices xyz=%s; output shape xyz=%s; crop face voxels=%d." - % ( - femur_cut_mode, - bbox_crop_meta["crop_slices_xyz"], - bbox_crop_meta["output_shape_xyz"], - bbox_crop_meta["crop_face_voxels"], - ) - ) - elif femur_cut_mode in ROUGH_PRE_ICP_CROP_MODES: - ogo.message( - "Applying fixed-length rough femur crop after isotropic resampling and before ICP..." - ) - images_to_crop = [imageData, maskThres] - if compartmentData is not None: - images_to_crop.append(compartmentData) - if pistoiaMaskData is not None: - images_to_crop.append(pistoiaMaskData) - cropped_images, rough_crop_face, bbox_crop_meta = crop_vtk_images_to_fixed_proximal_length( - images_to_crop, - maskThres, - retained_length_mm=femur_shaft_length, - labels={1}, - ) - if femur_cut_mode == "post_icp_oblique_ratio": - distal_crop_face = thicken_z_crop_face_into_mask( - rough_crop_face, - cropped_images[1], - thickness_voxels=3, - ) - registration_mask = cropped_images[1] - if femur_cut_mode not in FULL_SCAN_AFTER_ROUGH_CROP_MODES: - imageData = cropped_images[0] - maskThres = cropped_images[1] - next_crop_index = 2 - if compartmentData is not None: - compartmentData = cropped_images[next_crop_index] - next_crop_index += 1 - if pistoiaMaskData is not None: - pistoiaMaskData = cropped_images[next_crop_index] - ogo.message( - "rough pre-ICP crop slices xyz=%s; output shape xyz=%s; retained length=%8.4f; status=%s." - % ( - bbox_crop_meta["crop_slices_xyz"], - bbox_crop_meta["output_shape_xyz"], - bbox_crop_meta["retained_length_mm"], - bbox_crop_meta["status"], - ) + ogo.message("Applying fixed-length rough femur crop after isotropic resampling and before ICP...") + cropped_images, _rough_crop_face, bbox_crop_meta = crop_vtk_images_to_fixed_proximal_length( + [imageData, maskThres], + maskThres, + retained_length_mm=femur_shaft_length, + labels={1}, + ) + registration_mask = cropped_images[1] + ogo.message( + "rough pre-ICP crop slices xyz=%s; output shape xyz=%s; retained length=%8.4f; status=%s." + % ( + bbox_crop_meta["crop_slices_xyz"], + bbox_crop_meta["output_shape_xyz"], + bbox_crop_meta["retained_length_mm"], + bbox_crop_meta["status"], ) - else: - registration_mask = maskThres + ) images_to_pad = [imageData, maskThres] if distal_crop_face is not None: @@ -2678,8 +2577,8 @@ def sidewaysFallFe(args): ogo.message("Aligning input with reference model...") sample_surface_points = surface_points_from_vtk_mask( registration_mask, - max_points=8000, - sample_mode="stride", + max_points=DEFAULT_FEMUR_REGISTRATION_LANDMARKS, + sample_mode="linspace", sample_offset=0, ) mask_surface = polydata_from_points(sample_surface_points) @@ -2701,7 +2600,7 @@ def sidewaysFallFe(args): ref_poly, reference_scale = scale_reference_point_cloud_to_sample( ref_poly, sample_surface_points, - max_points=8000, + max_points=DEFAULT_FEMUR_REGISTRATION_LANDMARKS, reference_sample_mode="linspace", min_scale=DEFAULT_FEMUR_REFERENCE_MIN_SCALE, max_scale=DEFAULT_FEMUR_REFERENCE_MAX_SCALE, @@ -2710,14 +2609,10 @@ def sidewaysFallFe(args): ogo.message("Sample femur axis lengths: %s" % str(reference_scale["sample_axis_lengths"])) ogo.message("Femur reference scale factors: %s" % str(reference_scale["scale_factors"])) - icp_transform = estimate_rigid_icp( + icp_transform = estimate_femur_icp( moving_points=polydata_points(ref_poly), fixed_points=sample_surface_points, - iterations=50, - tolerance=1.0e-4, - start_by_matching_centroids_only=True, - convergence="delta", - distance_mode="mean", + iterations=DEFAULT_FEMUR_REGISTRATION_ITERATIONS, ) icp = point_transform_to_vtk_matrix( icp_transform["rotation"], @@ -2727,6 +2622,8 @@ def sidewaysFallFe(args): "ICP reference-to-sample iterations=%d mean_distance=%0.4f" % (icp_transform["iterations"], icp_transform["mean_distance"]) ) + if not femur_icp_transform_out: + femur_icp_transform_out = str(N88_fileName).replace(".n88model", "_icp.json") if femur_icp_transform_out: try: write_icp_transform( @@ -2749,6 +2646,10 @@ def sidewaysFallFe(args): margin_voxels=int(round(femur_input_margin / max(iso_resolution, 1.0e-6))), ) output_spacing = (float(iso_resolution), float(iso_resolution), float(iso_resolution)) + distal_scan_boundary = aligned_distal_scan_boundary( + native_distal_face, icp, resampling_spacing=output_spacing, + output_spacing=output_spacing, + ) ogo.message( "Reference-frame output grid: origin=%s size=%s spacing=%s" % (output_origin, output_size, output_spacing) @@ -2821,142 +2722,7 @@ def sidewaysFallFe(args): f"{input_spacing} is <= {mask_smoothing_spacing_threshold} mm in all dimensions..." ) - if femur_cut_mode in PRE_ICP_CROP_MODES: - ogo.message("Using transformed %s crop face for distal shaft standardization." % femur_cut_mode) - try: - mask_z_min, mask_z_max = femur_z_coverage(mask_trans) - except ValueError as exc: - ogo.message(str(exc)) - sys.exit(1) - retained_length_mm = mask_z_max - mask_z_min - shaft_crop = { - "cut_mode": femur_cut_mode, - "cut_plane": "transformed input crop face", - "pre_icp_crop": bbox_crop_meta, - "mask_z_min": mask_z_min, - "mask_z_max": mask_z_max, - "retained_length_mm": retained_length_mm, - "warnings": [], - } - ogo.message( - "Transformed %s mask z coverage [%8.4f, %8.4f]; retained z-span=%8.4f" - % (femur_cut_mode, mask_z_min, mask_z_max, retained_length_mm) - ) - elif femur_cut_mode in {"post_icp_flat_ratio", "post_icp_oblique_ratio", "greater_trochanter_length"}: - if femur_cut_mode == "post_icp_oblique_ratio": - ogo.message("Applying oblique post-ICP femur crop from transformed rough-crop face...") - if distal_crop_face_trans is None: - ogo.message("post_icp_oblique_ratio cut mode requires a transformed rough crop-face mask.") - sys.exit(1) - elif femur_cut_mode == "greater_trochanter_length": - ogo.message("Applying flat post-ICP femur crop at fixed distance below detected GT...") - else: - ogo.message("Applying flat post-ICP femur crop in aligned reference coordinates...") - images_to_crop = [image_trans, mask_trans] - if compartment_trans is not None: - images_to_crop.append(compartment_trans) - if pistoia_mask_trans is not None: - images_to_crop.append(pistoia_mask_trans) - try: - if femur_cut_mode == "post_icp_oblique_ratio": - cropped_images, distal_crop_face_trans, shaft_crop = crop_vtk_images_to_oblique_post_icp_ratio( - images_to_crop, - mask_trans, - distal_crop_face_trans, - ratio=femur_experimental_crop_ratio, - labels={1}, - ) - elif femur_cut_mode == "greater_trochanter_length": - cropped_images, distal_crop_face_trans, shaft_crop = crop_vtk_images_to_greater_trochanter_length( - images_to_crop, - mask_trans, - retained_length_mm=femur_greater_trochanter_distal_length, - gt_inclusion_length_mm=femur_greater_trochanter_inclusion_length, - labels={1}, - ) - else: - cropped_images, distal_crop_face_trans, shaft_crop = crop_vtk_images_to_flat_post_icp_ratio( - images_to_crop, - mask_trans, - ratio=femur_experimental_crop_ratio, - labels={1}, - ) - except ValueError as exc: - ogo.message(str(exc)) - sys.exit(1) - image_trans = cropped_images[0] - mask_trans = cropped_images[1] - next_crop_index = 2 - if compartment_trans is not None: - compartment_trans = cropped_images[next_crop_index] - next_crop_index += 1 - if pistoia_mask_trans is not None: - pistoia_mask_trans = cropped_images[next_crop_index] - retained_length_mm = shaft_crop.get("target_length_mm", shaft_crop["retained_length_mm"]) - shaft_crop["pre_icp_crop"] = bbox_crop_meta - if femur_cut_mode == "greater_trochanter_length": - ogo.message( - "Post-ICP GT-length crop retained shaft z=%8.4f below GT-support origin z=%8.4f; " - "GT center z=%8.4f; GT disk distal z=%8.4f; GT support distal z=%8.4f; " - "cut z=%8.4f; available=%8.4f; slices xyz=%s." - % ( - retained_length_mm, - shaft_crop["distal_length_origin_z"], - shaft_crop["greater_trochanter_z"], - shaft_crop["greater_trochanter_disk_distal_z"], - shaft_crop["greater_trochanter_support_distal_z"], - shaft_crop["cut_z_mm"], - shaft_crop["available_below_gt_support_mm"], - shaft_crop["crop_slices_xyz"], - ) - ) - else: - ogo.message( - "Post-ICP %s crop retained z=%8.4f from y width=%8.4f; slices xyz=%s; status=%s." - % ( - "oblique" if femur_cut_mode == "post_icp_oblique_ratio" else "flat", - retained_length_mm, - shaft_crop["reference_width_mm"], - shaft_crop["crop_slices_xyz"], - shaft_crop["status"], - ) - ) - else: - ogo.message("Applying flat distal femur crop in reference coordinates...") - try: - image_trans, mask_trans, shaft_crop = standardize_femur_shaft_length( - image_trans, - mask_trans, - reference_bounds=ref_poly.GetBounds(), - retained_length_mm=femur_shaft_length, - cut_mode=femur_cut_mode, - lesser_trochanter_distal_offset_mm=femur_lesser_trochanter_distal_offset, - lesser_trochanter_distal_offset_percent=femur_lesser_trochanter_distal_offset_percent, - ) - except ValueError as exc: - ogo.message(str(exc)) - sys.exit(1) - retained_length_mm = shaft_crop["retained_length_mm"] - ogo.message( - "Distal cut z=%8.4f; input mask z coverage [%8.4f, %8.4f]; retained length=%8.4f" - % ( - shaft_crop["cut_z"], - shaft_crop["mask_z_min"], - shaft_crop["mask_z_max"], - retained_length_mm, - ) - ) - for warning in shaft_crop.get("warnings", []): - ogo.message("WARNING: Femur crop QC: %s" % warning) - if femur_cut_mode == "lesser_trochanter": - ogo.message( - "Detected greater trochanter z=%8.4f; lesser trochanter z=%8.4f" - % (shaft_crop["greater_trochanter_z"], shaft_crop["lesser_trochanter_z"]) - ) - if compartment_trans is not None: - compartment_trans = flat_crop_vtk_image_below_z(compartment_trans, shaft_crop["cut_z"]) - if pistoia_mask_trans is not None: - pistoia_mask_trans = flat_crop_vtk_image_below_z(pistoia_mask_trans, shaft_crop["cut_z"]) + # Keep the aligned anatomy intact until the actual support disks exist. # ogo.message("Writing out temp images...") # ogo.writeNii(image_trans, "temp_image.nii", image_pathname) @@ -3026,37 +2792,7 @@ def sidewaysFallFe(args): ) if distal_crop_face_change is not None: distal_crop_face_change = ogo.applyMask(distal_crop_face_change, ogo.cast2unsignchar(change)) - if femur_cut_mode in PRE_ICP_CROP_MODES: - ogo.message("Skipping model-grid distal shaft cut; pre-ICP crop face is already transformed.") - elif femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"}: - ogo.message("Skipping model-grid distal shaft cut; post-ICP crop face is already on the model grid.") - else: - model_z_min, model_z_max = femur_z_coverage(change) - model_cut_z = model_z_max - retained_length_mm - if model_z_min > model_cut_z: - if model_z_min - model_cut_z <= 2.0: - model_cut_z = model_z_min - else: - ogo.message( - "Femur scan does not include enough distal shaft after model-grid alignment " - "(required retained length %.1f mm, available approximately %.1f mm)." - % (retained_length_mm, max(0.0, model_z_max - model_z_min)) - ) - sys.exit(1) - change = flat_crop_vtk_image_below_z(change, model_cut_z) - femur_mask_on_model_grid = flat_crop_vtk_image_below_z( - femur_mask_on_model_grid, - model_cut_z, - ) - if pistoia_mask_on_model_grid is not None: - pistoia_mask_on_model_grid = flat_crop_vtk_image_below_z( - pistoia_mask_on_model_grid, - model_cut_z, - ) - ogo.message( - "Applied model-grid distal shaft cut z=%8.4f; model mask z coverage [%8.4f, %8.4f]" - % (model_cut_z, model_z_min, model_z_max) - ) + ogo.message("Skipping model-grid distal shaft cut; final post-ICP crop face is already on the model grid.") # Convert image data to hexahedral elements ogo.message("Meshing image data to elements...") @@ -3235,6 +2971,72 @@ def sidewaysFallFe(args): ogo.message("Performing Image connectivity...") conn2 = ogo.imageConnectivity(combinedImage) + # Freeze proximal supports before shortening the shaft. Measuring their + # generated voxel faces avoids mistaking the larger fixture box for a disk. + greaterTrochanterPMMA = ogo.applyMask(greaterTrochanterPMMA, ogo.cast2unsignchar(conn2)) + femoralHeadPMMA = ogo.applyMask(femoralHeadPMMA, ogo.cast2unsignchar(conn2)) + images_to_crop = [conn2, change, femur_mask_on_model_grid, femoralHeadPMMA, greaterTrochanterPMMA] + if pistoia_mask_on_model_grid is not None: + images_to_crop.append(pistoia_mask_on_model_grid) + shaft_crop = {} + try: + shaft_crop = measure_available_shaft( + conn2, greaterTrochanterPMMA, + requested_length_mm=femur_greater_trochanter_distal_length, + distal_boundary=distal_scan_boundary, + ) + shaft_crop.update(pre_icp_crop=bbox_crop_meta, + proximal_supports_frozen_before_crop=True, + registration={key: value for key, value in icp_transform.items() + if key not in {"rotation", "translation"}}, + reference_scale=reference_scale) + with open(str(N88_fileName).replace(".n88model", "_shaft_geometry.json"), "w") as stream: + json.dump(shaft_crop, stream, indent=2) + cropped_images, distal_crop_face_change, shaft_crop = crop_vtk_images_to_greater_trochanter_length( + images_to_crop, + conn2, + gt_support_vtk=greaterTrochanterPMMA, + distal_boundary=distal_scan_boundary, + retained_length_mm=femur_greater_trochanter_distal_length, + gt_inclusion_length_mm=femur_greater_trochanter_inclusion_length, + ) + except ValueError as exc: + # Preserve a visual explanation of coverage failure without inventing + # a distal boundary or sending an ineligible model to the solver. + if shaft_crop.get("status") == "too_short": + from ogo.fea.qc_render import try_export_model_qc + + available_model = ogo.applyTestBase(ogo.Image2Mesh(conn2), material_table) + from ogo.fea.validation import measure_model, write_measurements + + write_measurements(N88_fileName, measure_model( + available_model, "hip", shaft=shaft_crop, ineligible=True)) + shaft_crop["exports"] = {"qc_3d": try_export_model_qc( + N88_fileName, "hip", model=available_model, ineligible=True)} + with open(str(N88_fileName).replace(".n88model", "_shaft_geometry.json"), "w") as stream: + json.dump(shaft_crop, stream, indent=2) + ogo.message(str(exc)) + sys.exit(1) + conn2, change, femur_mask_on_model_grid, femoralHeadPMMA, greaterTrochanterPMMA = cropped_images[:5] + if pistoia_mask_on_model_grid is not None: + pistoia_mask_on_model_grid = cropped_images[5] + retained_length_mm = shaft_crop["retained_length_mm"] + shaft_crop["pre_icp_crop"] = bbox_crop_meta + shaft_crop["proximal_supports_frozen_before_crop"] = True + shaft_crop["registration"] = { + key: value for key, value in icp_transform.items() + if key not in {"rotation", "translation"} + } + shaft_crop["reference_scale"] = reference_scale + ogo.message( + "Generated GT-disk-edge crop: shaft=%.4f mm, GT distal face z=%.4f, " + "shaft cut face z=%.4f, available=%.4f mm." + % (retained_length_mm, shaft_crop["distal_length_origin_z"], + shaft_crop["cut_z_mm"], shaft_crop["available_below_gt_support_distal_edge_mm"]) + ) + with open(str(N88_fileName).replace(".n88model", "_shaft_geometry.json"), "w") as stream: + json.dump(shaft_crop, stream, indent=2) + # Convert image data to hexahedral elements ogo.message("Meshing image data to elements...") mesh2 = ogo.Image2Mesh(conn2) @@ -3317,116 +3119,33 @@ def sidewaysFallFe(args): model2.AddNodeSet(gt_pmma_visible_node_IDS) ## - # Distal Femur (df) support nodes. The bbox-ratio crop standardizes shaft - # length before registration; the support itself remains a bbox-relative - # recipe plane in the generated model frame. + # Distal Femur (df) support nodes on the final flat post-ICP crop face. ogo.message("Determining distal femur nodes...") - if femur_cut_mode in PRE_ICP_CROP_MODES: - if distal_crop_face_change is None: - ogo.message("%s cut mode requires a transformed distal crop-face mask." % femur_cut_mode) - sys.exit(1) - distal_plane = bbox_relative_oriented_contact_plane( - model_bounds, - center_fraction=DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, - size_fraction=DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, - normal=DISTAL_SHAFT_FIXTURE_NORMAL, - size_bounds_axes=("x", "y"), - shape="anatomy", - ) - ogo.message( - "Distal Femur bbox-relative oblique support plane: center=%s normal=%s outward=%s size=%s" - % ( - distal_plane["center"], - distal_plane["normal"], - distal_plane["outward_normal"], - distal_plane["size"], - ) - ) - distal_surface = projected_crop_face_surface_vtk( - change, - distal_plane, - intrusion=pmma_intrusion, - output_value=1, - ) - df_visible_node_IDS = interface_node_ids_from_voxel_mask( - model2, - distal_surface, - change, - name=DISTAL_FEMUR_NODE_SET, - direction=distal_plane["normal"], - ) - if df_visible_node_IDS.GetNumberOfTuples() == 0: - ogo.message("No distal femur nodes found on the bbox-relative oblique shaft support surface.") - sys.exit(1) - elif femur_cut_mode == "post_icp_oblique_ratio": - if not shaft_crop or not shaft_crop.get("plane_center"): - ogo.message("post_icp_oblique_ratio cut mode requires stored oblique crop-plane metadata.") - sys.exit(1) - distal_plane = { - "center": shaft_crop["plane_center"], - "normal": shaft_crop["plane_normal"], - "outward_normal": tuple(float(-value) for value in shaft_crop["plane_normal"]), - "u_axis": shaft_crop["plane_u_axis"], - "v_axis": shaft_crop["plane_v_axis"], - "size": shaft_crop["plane_size"], - "shape": shaft_crop.get("plane_shape", "anatomy"), - } - ogo.message( - "Distal Femur post-ICP oblique support plane: center=%s normal=%s outward=%s size=%s" - % ( - distal_plane["center"], - distal_plane["normal"], - distal_plane["outward_normal"], - distal_plane["size"], - ) - ) - distal_surface = projected_crop_face_surface_vtk( - change, - distal_plane, - intrusion=pmma_intrusion, - output_value=1, - ) - df_visible_node_IDS = interface_node_ids_from_voxel_mask( - model2, - distal_surface, - change, - name=DISTAL_FEMUR_NODE_SET, - direction=distal_plane["normal"], - ) - if df_visible_node_IDS.GetNumberOfTuples() == 0: - ogo.message("No distal femur nodes found on the post-ICP oblique shaft support surface.") - sys.exit(1) - elif femur_cut_mode in {"post_icp_flat_ratio", "greater_trochanter_length"}: - if distal_crop_face_change is None: - ogo.message("%s cut mode requires a distal crop-face mask." % femur_cut_mode) - sys.exit(1) - distal_support_direction = (0.0, 0.0, 1.0) - ogo.message( - "Distal Femur post-ICP straight support patch: fraction=%8.4f direction=%s" - % (POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, distal_support_direction) - ) - distal_surface = straight_crop_face_support_surface_vtk( - distal_crop_face_change, - change, - support_fraction=POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, - output_value=1, - ) - df_visible_node_IDS = interface_node_ids_from_voxel_mask( - model2, - distal_surface, - change, - name=DISTAL_FEMUR_NODE_SET, - direction=distal_support_direction, - ) - if df_visible_node_IDS.GetNumberOfTuples() == 0: - ogo.message("No distal femur nodes found on the post-ICP straight shaft support patch.") - sys.exit(1) - else: - distal_cut_z = model2_bounds[4] - ogo.message("Distal Femur Cut Plane z: %8.4f" % distal_cut_z) - df_visible_node_IDS = find_nodes_on_coordinate_plane(model2, "z", distal_cut_z) + if distal_crop_face_change is None: + ogo.message("Final femur crop requires a distal crop-face mask.") + sys.exit(1) + distal_support_direction = (0.0, 0.0, -1.0) + ogo.message( + "Distal Femur post-ICP straight support patch: fraction=%8.4f direction=%s" + % (POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, distal_support_direction) + ) + distal_surface = straight_crop_face_support_surface_vtk( + distal_crop_face_change, + change, + support_fraction=POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, + output_value=1, + ) + df_visible_node_IDS = directional_face_node_ids_from_voxel_mask( + model2, + distal_surface, + name=DISTAL_FEMUR_NODE_SET, + direction=distal_support_direction, + ) + if df_visible_node_IDS.GetNumberOfTuples() == 0: + ogo.message("No distal femur nodes found on the post-ICP straight shaft support patch.") + sys.exit(1) - ogo.message("-- found %d distal shaft interface nodes." + ogo.message("-- found %d distal shaft outer-face nodes." % df_visible_node_IDS.GetNumberOfTuples()) df_visible_node_IDS.SetName(DISTAL_FEMUR_NODE_SET) @@ -3481,7 +3200,22 @@ def sidewaysFallFe(args): ## # Write out n88model file ogo.message("Writing out n88model file: %s" % N88_fileName) + shaft_crop = verify_model_shaft(model2, shaft_crop) + from ogo.fea.validation import measure_model, write_measurements + + write_measurements(N88_fileName, measure_model( + model2, "hip", shaft=shaft_crop, registration_rotation=icp_transform["rotation"])) write_model(model2, N88_fileName) + saved_model = vtkbone.vtkboneN88ModelReader() + saved_model.SetFileName(str(N88_fileName)) + saved_model.Update() + shaft_crop = verify_model_shaft(saved_model.GetOutput(), shaft_crop) + + from ogo.fea.qc_render import try_export_model_qc + + shaft_crop["exports"] = {"qc_3d": try_export_model_qc(N88_fileName, "hip", model=model2)} + with open(str(N88_fileName).replace(".n88model", "_shaft_geometry.json"), "w") as stream: + json.dump(shaft_crop, stream, indent=2) ## ogo.message("Done writing n88model.") @@ -3566,28 +3300,14 @@ def main(): parser.add_argument("--fe_displacement", type=float, default=DEFAULT_FEMUR_FE_DISPLACEMENT, help="Sets the applied displacement endpoint for the sideways-fall model. The default reports the force at 4%% displacement. (default: %(default)s)") parser.add_argument("--femur_shaft_length", type=float, default=DEFAULT_FEMUR_SHAFT_LENGTH_MM, - help="Retained proximal femur length [mm] for --femur_cut_mode fixed_length. (default: %(default)s [mm])") - parser.add_argument("--femur_cut_mode", choices=["bbox_ratio", "proximal_box_ratio", "post_icp_oblique_ratio", "post_icp_flat_ratio", "greater_trochanter_length", "lesser_trochanter", "fixed_length"], + help="Registration-only proximal crop length [mm]; the solved model uses the full scan before its final GT-relative crop. (default: %(default)s)") + parser.add_argument("--femur_cut_mode", choices=[DEFAULT_FEMUR_CUT_MODE], default=DEFAULT_FEMUR_CUT_MODE, - help="Set the distal femur crop. post_icp_oblique_ratio uses a fixed rough pre-ICP crop, then an angled final ratio crop after ICP following the transformed rough-crop face. post_icp_flat_ratio keeps the flat aligned-frame comparison path. greater_trochanter_length uses the rough crop only for ICP, then crops the transformed full scan to a fixed shaft length distal to the detected GT-disk distal edge. (default: %(default)s)") - parser.add_argument("--femur_bbox_ratio", nargs=3, default=DEFAULT_FEMUR_BBOX_RATIO, - help="BBox-ratio crop in recipe order: reference constrained free. Use 'none' for a free axis. (default: %(default)s)") - parser.add_argument("--femur_bbox_crop_from", nargs=3, default=DEFAULT_FEMUR_BBOX_CROP_FROM, - help="BBox crop side in recipe order: reference constrained free. Values are min, max, center/null. (default: %(default)s)") - parser.add_argument("--femur_experimental_crop_ratio", type=float, default=DEFAULT_FEMUR_EXPERIMENTAL_RATIO, - help="Retained proximal femur length as a multiple of the proximal transverse max(x, y) width in proximal_box_ratio mode. (default: %(default)s)") + help=argparse.SUPPRESS) parser.add_argument("--femur_greater_trochanter_distal_length", type=float, default=DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, - help="Retained shaft length [mm] distal to the detected GT-disk distal edge in greater_trochanter_length mode. (default: %(default)s [mm])") + help="Retained complete flat shaft length [mm] from the actual GT disk distal edge. Insufficient coverage fails generation. (default: %(default)s)") parser.add_argument("--femur_greater_trochanter_inclusion_length", type=float, default=DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, - help="Optional extra distal offset [mm] below the detected GT-disk distal edge before measuring --femur_greater_trochanter_distal_length. The default makes the disk distal edge 0 mm. (default: %(default)s [mm])") - parser.add_argument("--femur_proximal_reference_distance", type=float, default=DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, - help="Proximal-most distance [mm] used to estimate transverse reference width in proximal_box_ratio mode. (default: %(default)s)") - parser.add_argument("--femur_proximal_reference_width", choices=["max_xy", "rms_xy"], default=DEFAULT_FEMUR_PROXIMAL_REFERENCE_WIDTH, - help="Transverse width summary for proximal_box_ratio. max_xy uses max(x width, y width); rms_xy uses sqrt((x^2 + y^2) / 2). (default: %(default)s)") - parser.add_argument("--femur_lesser_trochanter_distal_offset", type=float, default=DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, - help="Cut this many mm distal to the detected lesser trochanter in lesser_trochanter mode. (default: %(default)s [mm])") - parser.add_argument("--femur_lesser_trochanter_distal_offset_percent", type=float, default=None, - help="Optional percentage of the detected greater-to-lesser trochanter z distance to cut distal to the lesser trochanter. Overrides --femur_lesser_trochanter_distal_offset when set. (default: %(default)s)") + help=argparse.SUPPRESS) parser.add_argument("--femur_input_margin", type=float, default=DEFAULT_FEMUR_INPUT_MARGIN_MM, help="Pad the input image/mask as needed so femur foreground has this margin before ICP. (default: %(default)s [mm])") parser.add_argument("--femur_icp_transform_in", type=str, default=None, diff --git a/ogo/fea/gallery.py b/ogo/fea/gallery.py new file mode 100644 index 0000000..660abdf --- /dev/null +++ b/ogo/fea/gallery.py @@ -0,0 +1,136 @@ +"""Standalone HTML review gallery and portable inclusion CSV export.""" + +import csv +import hashlib +import html +import json +import os +import re +import shutil +import zipfile +from pathlib import Path +from tempfile import TemporaryDirectory +from urllib.parse import quote + +from ogo.fea.review import FIELDS, REASONS, identity, inclusion_row, write_inclusion + + +def _camera_images(path, output): + """Split the fixed three-row Ogo render, caching assets beside the gallery.""" + from PIL import Image + with Image.open(path) as image: + if image.size != (1400, 2550) or not path.name.endswith(('_qc_3d.png', '_sed_3d.png')): + return [] + stat = path.stat() + key = hashlib.sha256(f'{path.resolve()}:{stat.st_size}:{stat.st_mtime_ns}'.encode()).hexdigest()[:20] + directory = output.parent / (output.stem + '_views') + directory.mkdir(exist_ok=True) + assets = [] + for index, view in enumerate(('oblique', 'top', 'bottom')): + asset = directory / f'{key}_{view}.png' + if not asset.exists(): + image.crop((0, index * 850, 1400, (index + 1) * 850)).save(asset) + assets.append((view, asset)) + return assets + + +def generate_gallery(rows, output, decisions=None, image_root=None): + output = Path(output).absolute() + decisions = decisions or {} + cards = [] + escape = lambda value: html.escape(str(value), quote=True) + for index, row in enumerate(rows): + images = [] + for stage, key in [('anatomy', 'anatomy_image'), ('sed', 'sed_image')]: + path = Path(row.get(key) or '') + if image_root is not None and not path.is_absolute(): + path = Path(image_root) / path + if path.is_file(): + cameras = _camera_images(path, output) + for view, asset in [('all', path), *cameras]: + relative = quote(Path(os.path.relpath(asset, output.parent)).as_posix()) + source = f'src="{escape(relative)}"' + fallback = ' data-fallback="true"' if not cameras else '' + images.append(f'') + measurements = ''.join(f'{escape(key)}{escape(value)}' + for key, value in sorted(row.items()) if not key.endswith('_image')) + cards.append(f'

{"".join(images)}
Measurements{measurements}
') + reasons = sorted({reason for row in rows for reason in row['qc_reasons'].split(';') if reason}) + options = ''.join(f'' for reason in reasons) + manual_options = ''.join(f'' for reason in REASONS) + seeds = [inclusion_row(row, decisions.get(identity(row))) for row in rows] + serialized = json.dumps(rows, sort_keys=True) + fingerprint = hashlib.sha256(serialized.encode()).hexdigest() + data = json.dumps({'rows': rows, 'seeds': seeds, 'fields': FIELDS, 'reasons': REASONS, + 'storageKey': 'ogo-fea-review-v1-' + fingerprint, + 'imported': bool(decisions)}).replace('<', '\\u003c') + script = Path(__file__).with_name('gallery_review.js').read_text() + content = '''FEA QC review + +
+ + + + + +

+
CARDS

+ +
+ +
+''' + # Replace once so record text cannot become another template token. + tokens = {'SCRIPT': script, 'MANUAL_OPTIONS': manual_options, 'REASONS': options, + 'CARDS': ''.join(cards), 'DATA': data} + content = re.sub(r'SCRIPT|MANUAL_OPTIONS|REASONS|CARDS|DATA', lambda match: tokens[match[0]], content) + output.write_text(content) + return output + + +def export_gallery_zip(rows, output, decisions=None): + """Package the full gallery, images, and review CSVs without copying models. + + Image references are relative to the extracted HTML. Missing previews stay + missing; the archive contains no dependence on the original image folders. + """ + output = Path(output).absolute() + output.parent.mkdir(parents=True, exist_ok=True) + with TemporaryDirectory(prefix='ogo-gallery-', dir=output.parent) as temporary: + root = Path(temporary) + images = root / 'images' + images.mkdir() + portable, copied = [], {} + for row in rows: + record = dict(row) + for key in ('anatomy_image', 'sed_image'): + source = Path(row.get(key) or '') + record[key] = '' + if source.is_file(): + source = source.resolve() + if source not in copied: + # Stable identity preserves browser autosave across exports. + name = hashlib.sha256(str(source).encode()).hexdigest()[:20] + '_' + source.name + target = images / name + try: + target.symlink_to(source) + except OSError: + shutil.copyfile(source, target) + copied[source] = target.relative_to(root).as_posix() + record[key] = copied[source] + portable.append(record) + generate_gallery(portable, root / 'gallery.html', decisions, image_root=root) + write_inclusion(rows, root / 'study_inclusion.csv', decisions) + fields = sorted({key for row in rows for key in row}) + with (root / 'qc_summary.csv').open('w', newline='') as stream: + writer = csv.DictWriter(stream, fieldnames=fields) + writer.writeheader() + writer.writerows(rows) + temporary_zip = root / 'gallery.zip' + with zipfile.ZipFile(temporary_zip, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=True) as archive: + for source in sorted(root.rglob('*')): + if source.is_file() and source != temporary_zip: + compression = zipfile.ZIP_STORED if source.suffix.lower() in ('.png', '.jpg', '.jpeg') else zipfile.ZIP_DEFLATED + archive.write(source, source.relative_to(root).as_posix(), compress_type=compression) + temporary_zip.replace(output) + return output diff --git a/ogo/fea/gallery_review.js b/ogo/fea/gallery_review.js new file mode 100644 index 0000000..f59b43d --- /dev/null +++ b/ogo/fea/gallery_review.js @@ -0,0 +1,173 @@ +/* The exported CSV is the study record. Browser storage is only a convenience. */ +function decisionKey(row) { return JSON.stringify([row.site, row.model_id]); } + +function inclusionRecord(row, decision = {}) { + const manual = decision.manual_decision || 'automatic'; + if (!['automatic', 'include', 'exclude'].includes(manual)) throw new Error('Invalid manual decision'); + return { + model_id: row.model_id, site: row.site, model_path: row.model_path || '', + automatic_qc_status: row.qc_status, automatic_qc_reasons: row.qc_reasons || '', + manual_decision: manual, + manual_reason: manual === 'automatic' ? '' : (decision.manual_reason || 'manual_review'), + manual_note: manual === 'automatic' ? '' : (decision.manual_note || ''), + reviewer: decision.reviewer || '', review_timestamp: decision.review_timestamp || '', + final_inclusion: manual === 'automatic' ? ({pass: 'include', fail: 'exclude'}[row.qc_status] || 'pending') : manual, + solver_eligible: row.generation_status === 'too_short' ? 'False' : '', + qc_policy_version: row.qc_policy_version || '', measurement_source: row.measurement_source || '' + }; +} + +function encodeCSV(records, fields) { + const quote = value => '"' + String(value ?? '').replaceAll('"', '""') + '"'; + return [fields.map(quote).join(','), ...records.map(row => fields.map(key => quote(row[key])).join(','))].join('\r\n') + '\r\n'; +} + +function parseCSV(text) { + text = text.replace(/^\uFEFF/, ''); + const rows = []; let row = [], field = '', quoted = false; + for (let i = 0; i < text.length; i++) { + const c = text[i]; + if (c === '"') { + if (quoted && text[i + 1] === '"') { field += '"'; i++; } + else quoted = !quoted; + } else if (!quoted && c === ',') { row.push(field); field = ''; } + else if (!quoted && (c === '\r' || c === '\n')) { + if (c === '\r' && text[i + 1] === '\n') i++; + row.push(field); if (row.some(value => value !== '')) rows.push(row); + row = []; field = ''; + } else field += c; + } + if (quoted) throw new Error('Unterminated CSV quotation'); + if (row.length || field) { row.push(field); rows.push(row); } + const headers = rows.shift() || []; + if (new Set(headers).size !== headers.length) throw new Error('Duplicate CSV headers'); + if (!['model_id', 'site', 'manual_decision'].every(key => headers.includes(key))) throw new Error('Missing review CSV columns'); + return rows.map(values => { + if (values.length !== headers.length) throw new Error('Invalid CSV row length'); + return Object.fromEntries(headers.map((key, index) => [key, values[index]])); + }); +} + +function validateReviews(records, rows) { + const known = new Map(rows.map(row => [decisionKey(row), row])); + const decisions = new Map(); + for (const record of records) { + const key = decisionKey(record); + if (!known.has(key)) throw new Error('Unknown model in review CSV: ' + record.model_id); + if (decisions.has(key)) throw new Error('Duplicate model in review CSV: ' + record.model_id); + inclusionRecord(known.get(key), record); + decisions.set(key, record); + } + return decisions; +} + +function imageVisible(image, stage, camera) { + return image.stage === stage && (image.view === camera || image.fallback === 'true'); +} + +if (typeof module !== 'undefined') module.exports = {inclusionRecord, encodeCSV, parseCSV, validateReviews, imageVisible}; + +if (typeof document !== 'undefined') { + const data = JSON.parse(document.getElementById('review-data').textContent); + const rows = data.rows, $ = id => document.getElementById(id); + let decisions = validateReviews(data.seeds, rows), selected = null, autosave = true; + const message = text => { $('message').textContent = text; }; + try { + const saved = localStorage.getItem(data.storageKey); + if (saved && !data.imported) decisions = validateReviews(JSON.parse(saved), rows); + } catch (error) { autosave = false; } + function persist() { + try { localStorage.setItem(data.storageKey, JSON.stringify([...decisions.values()])); } + catch (error) { autosave = false; } + } + function updateImages(container) { + for (const image of container.querySelectorAll('img')) { + image.hidden = !imageVisible(image.dataset, $('stage').value, $('camera').value); + image.style.height = $('camera').value === 'all' || image.dataset.fallback === 'true' ? '560px' : '280px'; + } + } + function refresh() { + let visible = 0; + for (const card of document.querySelectorAll('article')) { + const row = rows[Number(card.dataset.index)], record = inclusionRecord(row, decisions.get(decisionKey(row))); + card.querySelector('.automatic').textContent = 'Automatic: ' + row.qc_status + (row.qc_reasons ? ': ' + row.qc_reasons : ''); + card.querySelector('.decision').textContent = 'Study: ' + record.final_inclusion + (record.manual_decision !== 'automatic' ? ' (manual)' : ''); + for (const button of card.querySelectorAll('.quick-decision')) { + button.setAttribute('aria-pressed', String(record.manual_decision === button.dataset.decision)); + button.style.fontWeight = record.manual_decision === button.dataset.decision ? '700' : '400'; + } + card.hidden = !row.model_id.toLowerCase().includes($('search').value.toLowerCase()) || + ($('site').value && row.site !== $('site').value) || + ($('status').value && row.qc_status !== $('status').value) || + ($('reason').value && !row.qc_reasons.split(';').includes($('reason').value)) || + ($('inclusion').value && record.final_inclusion !== $('inclusion').value); + if (!card.hidden) visible++; + updateImages(card); + } + $('review-stage').value = $('stage').value; + $('review-camera').value = $('camera').value; + updateImages($('review-images')); + $('message').textContent = visible + ' / ' + rows.length + ' models. ' + (autosave ? '' : 'Browser autosave unavailable. ') + 'Export CSV to preserve the study record.'; + } + function openReview(index) { + selected = rows[index]; + const record = inclusionRecord(selected, decisions.get(decisionKey(selected))); + $('review-title').textContent = selected.model_id + ': ' + selected.site; + $('review-auto').textContent = 'Automatic: ' + selected.qc_status + '; ' + selected.qc_reasons + + (selected.generation_status === 'too_short' ? '. Too short: manual inclusion does not make this model solver-eligible.' : ''); + $('manual-reason').value = data.reasons.includes(record.manual_reason) ? record.manual_reason : 'other'; + if (!record.manual_reason) $('manual-reason').value = 'manual_review'; + $('manual-note').value = record.manual_note; + const card = document.querySelector('article[data-index="' + index + '"]'); + $('review-images').replaceChildren(...[...card.querySelectorAll('img')].map(image => { + return image.cloneNode(); + })); + updateImages($('review-images')); + $('review-measurements').replaceChildren(card.querySelector('details').cloneNode(true)); + $('review-dialog').showModal(); + } + function decide(manual) { + const decision = {manual_decision: manual, manual_reason: $('manual-reason').value, + manual_note: $('manual-note').value, reviewer: $('reviewer').value.trim() || 'unspecified', + review_timestamp: new Date().toISOString()}; + decisions.set(decisionKey(selected), inclusionRecord(selected, decision)); + persist(); refresh(); $('review-dialog').close(); + } + for (const card of document.querySelectorAll('article')) { + for (const target of card.querySelectorAll('.review-open,img')) target.addEventListener('click', () => openReview(Number(card.dataset.index))); + for (const button of card.querySelectorAll('.quick-decision')) button.addEventListener('click', () => { + const row = rows[Number(card.dataset.index)]; + const decision = {manual_decision: button.dataset.decision, manual_reason: 'manual_review', + manual_note: '', reviewer: $('reviewer').value.trim() || 'unspecified', + review_timestamp: new Date().toISOString()}; + decisions.set(decisionKey(row), inclusionRecord(row, decision)); + persist(); refresh(); + }); + } + for (const id of ['search', 'site', 'status', 'reason', 'inclusion', 'stage', 'camera']) $(id).addEventListener('input', refresh); + for (const id of ['stage', 'camera']) $('review-' + id).addEventListener('change', () => { + $(id).value = $('review-' + id).value; + refresh(); + }); + $('close').onclick = () => $('review-dialog').close(); + for (const id of ['include', 'exclude']) $(id).onclick = () => decide(id); + $('reset').onclick = () => decide('automatic'); + $('export').onclick = () => { + const csv = encodeCSV(rows.map(row => inclusionRecord(row, decisions.get(decisionKey(row)))), data.fields); + const url = URL.createObjectURL(new Blob([csv], {type: 'text/csv;charset=utf-8'})); + const link = document.createElement('a'); link.href = url; link.download = 'study_inclusion.csv'; link.click(); + setTimeout(() => URL.revokeObjectURL(url), 1000); + message('CSV export started: check your browser downloads for study_inclusion.csv. The project CSV is unchanged.'); + window.alert('CSV export started.\n\nCheck your browser downloads for study_inclusion.csv.\n\nThis is a separate file: it does not update the project CSV or the CSV inside the gallery ZIP.'); + }; + $('import').onchange = async event => { + try { + const records = parseCSV(await event.target.files[0].text()); + const imported = validateReviews(records, rows); + for (const [key, record] of imported) decisions.set(key, record); + persist(); refresh(); message('Imported ' + imported.size + ' decisions. Automatic QC remains unchanged.'); + } catch (error) { message('Import rejected: ' + error.message); } + event.target.value = ''; + }; + refresh(); +} diff --git a/ogo/fea/hip_registration.py b/ogo/fea/hip_registration.py new file mode 100644 index 0000000..ebb250f --- /dev/null +++ b/ogo/fea/hip_registration.py @@ -0,0 +1,56 @@ +"""Deterministic rigid hip registration in the canonical superior-Z frame. + +Reference scaling, when used, is performed by the caller. This fit is rigid: +it never scales the participant's anatomy or its solved voxel dimensions. +""" + +import numpy as np +from scipy.spatial import cKDTree + +from ogo.fea.alignment import estimate_rigid_icp + + +def estimate_femur_icp(*, moving_points, fixed_points, iterations=50): + """Select the best non-inverted fit from four axial starting rotations. + + Bidirectional mean surface distance avoids choosing a fit that matches only + one part of the femur. Positive superior-axis projection excludes SI flips; + it does not replace visual alignment QC. KD-tree queries use one worker so + cohort-level parallelism remains bounded. + """ + moving = np.asarray(moving_points, dtype=float) + fixed = np.asarray(fixed_points, dtype=float) + if (moving.ndim != 2 or fixed.ndim != 2 or moving.shape[1] != 3 + or fixed.shape[1] != 3 or min(len(moving), len(fixed)) < 3 + or not np.all(np.isfinite(moving)) or not np.all(np.isfinite(fixed))): + raise ValueError("ICP requires finite (n, 3) surfaces with at least three points.") + fixed_tree = cKDTree(fixed) + trials = [] + best = None + for angle in (0, 90, 180, 270): + theta = np.deg2rad(angle) + rotation = np.array([[np.cos(theta), -np.sin(theta), 0], + [np.sin(theta), np.cos(theta), 0], [0, 0, 1.]]) + fit = estimate_rigid_icp( + moving_points=moving, fixed_points=fixed, iterations=iterations, + initial_transform={"rotation": rotation, + "translation": fixed.mean(axis=0) - moving.mean(axis=0) @ rotation.T}, + nearest_workers=1, + ) + transformed = moving @ fit["rotation"].T + fit["translation"] + forward = float(fixed_tree.query(transformed, workers=1)[0].mean()) + reverse = float(cKDTree(transformed).query(fixed, workers=1)[0].mean()) + score = (forward + reverse) / 2 + superior_projection = float(fit["rotation"][2, 2]) + valid = bool(superior_projection > 0 and np.isfinite(score)) + trials.append({"start_deg": angle, "forward_distance_mm": forward, + "reverse_distance_mm": reverse, "symmetric_distance_mm": score, + "superior_axis_projection": superior_projection, + "orientation_valid": valid, "iterations": int(fit["iterations"])}) + if valid and (best is None or score < best["symmetric_distance_mm"]): + best = dict(fit, mean_distance=forward, symmetric_distance_mm=score, + selected_start_deg=angle, superior_axis_projection=superior_projection) + if best is None: + raise ValueError("No femur ICP candidate preserved superior orientation.") + return dict(best, strategy="axial_multistart_rigid", candidates=trials, + reference_points=len(moving), sample_points=len(fixed), visual_qc_status="pending") diff --git a/ogo/fea/legacy_qc.py b/ogo/fea/legacy_qc.py new file mode 100644 index 0000000..a3d42f4 --- /dev/null +++ b/ogo/fea/legacy_qc.py @@ -0,0 +1,185 @@ +"""One-time measurement reconstruction for older Ogo voxel FE models. + +Only the measurement sidecar is written. Recovered node sets exist in memory; +the original model, constraints, and solved arrays are never modified on disk. +""" + +import csv +import hashlib +import json +from pathlib import Path + +import numpy as np + +from ogo.fea.validation import measure_model, model_grid, write_measurements + + +BACKFILL_VERSION = 3 +SUFFIXES = {'body_mask': '_qc_body_mask.nii.gz', + 'process_mask': '_qc_process_mask.nii.gz', + 'shaft_geometry': '_shaft_geometry.json'} + + +def load_evidence(path): + """Read explicit registered-mask/shaft paths keyed by site and model ID.""" + path = Path(path).absolute() + evidence = {} + with path.open(newline='') as stream: + reader = csv.DictReader(stream) + if not {'site', 'model_id'} <= set(reader.fieldnames or []): + raise ValueError('Evidence CSV requires site and model_id') + for row in reader: + key = row['site'], row['model_id'] + if key[0] not in ('hip', 'spine') or not key[1]: + raise ValueError('Evidence CSV has invalid site or model_id') + if key in evidence: + raise ValueError(f'Duplicate evidence identity: {key}') + evidence[key] = {name: str((path.parent / row[name]).absolute()) + for name in SUFFIXES if row.get(name)} + return evidence + + +def evidence_paths(path, evidence=None): + path = Path(path) + return {key: Path((evidence or {}).get(key) or path.with_name(path.stem + suffix)) + for key, suffix in SUFFIXES.items()} + + +def evidence_signature(path, evidence=None): + """Cheap cache key: version and metadata, without reopening model/mask data.""" + inputs = {'model': Path(path), **evidence_paths(path, evidence)} + metadata = {'version': BACKFILL_VERSION} + for key, source in inputs.items(): + stat = source.stat() if source.is_file() else None + metadata[key] = [str(source.absolute()), stat.st_size if stat else None, + stat.st_mtime_ns if stat else None] + return hashlib.sha256(json.dumps(metadata, sort_keys=True).encode()).hexdigest() + + +def needs_backfill(path, row, evidence=None): + if not row: + return True + if not row.get('measurement_source', '').startswith('legacy_backfill'): + return False + return row.get('legacy_evidence_signature') != evidence_signature(path, evidence) + + +def registered_anatomy(grid, spacing, origin, body_path, process_path=None): + """Recover anatomy on the FE lattice, rejecting misplaced or native masks. + + When only the registered body is saved, process is the remaining bone, not + an independently recovered segmentation. That distinction is recorded. + """ + import SimpleITK as sitk + import vtk + from vtk.util.numpy_support import numpy_to_vtk + + bone = (grid != 0) & (grid != 5000) + def read_mask(path): + image = sitk.ReadImage(str(path)) + if image.GetDimension() != 3 or image.GetNumberOfComponentsPerPixel() != 1: + raise ValueError('Registered mask must be a scalar 3D image') + active = np.argwhere(sitk.GetArrayFromImage(image).transpose(2, 1, 0) != 0) + xyz = np.asarray(image.GetOrigin()) + (active * image.GetSpacing()) @ np.asarray(image.GetDirection()).reshape(3, 3).T + continuous = (xyz - origin) / spacing + indices = np.rint(continuous).astype(int) + if not len(indices): + raise ValueError('Registered mask has no overlap with model bone') + if not np.allclose(indices, continuous, atol=1e-3): + raise ValueError('Registered mask does not match the model voxel lattice') + inside = np.all((indices >= 0) & (indices < grid.shape), axis=1) + candidate = np.zeros(grid.shape, dtype=bool) + candidate[tuple(indices[inside].T)] = True + overlap = np.count_nonzero(candidate & bone) / len(indices) + if overlap < .95: + raise ValueError(f'Registered mask/model bone overlap is only {overlap:.1%}') + return candidate & bone + body = read_mask(body_path) + process = read_mask(process_path) if process_path else bone & ~body + if np.any(body & process) or not process.any(): + raise ValueError('Body/process masks overlap or leave no process') + images = [] + for mask in (body, process): + image = vtk.vtkImageData() + image.SetDimensions(*grid.shape) + image.SetSpacing(*spacing) + image.SetOrigin(*origin) + image.GetPointData().SetScalars(numpy_to_vtk(mask.astype(np.uint8).ravel(order='F'), deep=True)) + images.append(image) + return images, {'body_mask_source': str(body_path), + 'process_mask_source': str(process_path) if process_path else 'model_bone_minus_registered_body'} + + +def backfill_measurements(path, site=None, evidence=None): + import vtkbone + from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtkIdTypeArray + + path = Path(path) + reader = vtkbone.vtkboneN88ModelReader() + reader.SetFileName(str(path)) + reader.Update() + model = reader.GetOutput() + if not model.GetNumberOfCells(): + raise ValueError('Empty or unreadable model') + constraints = model.GetConstraints() + if site is None: + top = constraints.GetItem('top_displacement') + if top is None: + raise ValueError('Cannot infer anatomy: provide --site hip or --site spine') + senses = np.unique(vtk_to_numpy(top.GetAttributes().GetArray('SENSE'))) + if len(senses) != 1 or senses[0] not in (1, 2): + raise ValueError('Cannot infer anatomy from loading axis; provide --site') + site = 'hip' if senses[0] == 1 else 'spine' + mapping = {'body_top': 'top_displacement', 'body_bottom': 'bottom_fixed_z'} if site == 'spine' else { + 'Femoral_Head_PMMA_Nodes': 'top_displacement', + 'Greater_Trochanter_PMMA_Nodes': 'bottom_fixed_y_PMMA'} + recovered = [] + for name, constraint_name in mapping.items(): + constraint = constraints.GetItem(constraint_name) + if model.GetNodeSet(name) is None and constraint is not None: + nodes = numpy_to_vtkIdTypeArray(np.unique(vtk_to_numpy(constraint.GetIndices())).astype(np.int64), deep=True) + nodes.SetName(name) + model.AddNodeSet(nodes) + recovered.append(name) + sources = evidence_paths(path, evidence) + shaft_path = sources['shaft_geometry'] + shaft = json.loads(shaft_path.read_text()) if shaft_path.exists() else None + # The union constraint alone is ambiguous; a saved crop face makes it + # possible to select the actual constrained distal nodes geometrically. + if site == 'hip' and model.GetNodeSet('Distal_Femur_Nodes') is None and shaft and 'cut_z_mm' in shaft: + fixed = constraints.GetItem('bottom_fixed_z') + if fixed is not None: + points = vtk_to_numpy(model.GetPoints().GetData()) + ids = vtk_to_numpy(fixed.GetIndices()).astype(int) + attrs = fixed.GetAttributes() + correct = (vtk_to_numpy(attrs.GetArray('SENSE')) == 2) & (vtk_to_numpy(attrs.GetArray('VALUE')) == 0) + ids = np.unique(ids[correct & np.isclose(points[ids, 2], float(shaft['cut_z_mm']), atol=1e-4, rtol=0)]) + if len(ids): + nodes = numpy_to_vtkIdTypeArray(ids.astype(np.int64), deep=True) + nodes.SetName('Distal_Femur_Nodes') + model.AddNodeSet(nodes) + recovered.append('Distal_Femur_Nodes') + images, provenance, error = (None, None), {}, '' + if site == 'spine' and sources['body_mask'].is_file(): + try: + grid, spacing, origin = model_grid(model) + process = sources['process_mask'] if sources['process_mask'].is_file() else None + images, provenance = registered_anatomy(grid, spacing, origin, sources['body_mask'], process) + except Exception as exc: + error = str(exc) + row = measure_model(model, site, shaft=shaft, body_image=images[0], process_image=images[1]) + missing = [str(sources[key]) for key in (evidence or {}) if key in sources and not sources[key].is_file()] + if missing: + error = '; '.join(filter(None, [error, 'Missing explicit evidence: ' + ', '.join(missing)])) + row.update(measurement_source='legacy_backfill', + recovered_boundary_sets=';'.join(recovered), + legacy_evidence_signature=evidence_signature(path, evidence), + legacy_backfill_version=BACKFILL_VERSION, legacy_evidence_error=error, + measurement_error='', **provenance) + row['legacy_evidence_sources'] = json.dumps({key: str(source) for key, source in sources.items() if source.is_file()}, sort_keys=True) + array = model.GetCellData().GetArray('StrainEnergyDensity') + if array is not None: + sed = vtk_to_numpy(array) + row.update(sed_available=True, sed_finite=bool(np.isfinite(sed).all()), + sed_nonnegative=bool((sed >= 0).all())) + return write_measurements(path, row) diff --git a/ogo/fea/model_export.py b/ogo/fea/model_export.py new file mode 100644 index 0000000..f15a9a1 --- /dev/null +++ b/ogo/fea/model_export.py @@ -0,0 +1,163 @@ +"""Inspectable hip surfaces and opaque four-view QC from the actual FE mesh. + +These are visualization artifacts, not replacement solver models. No mesh +smoothing, scaling, or voxel modification is performed during export. +""" + +from pathlib import Path +import os +import warnings + +import numpy as np +import vtk +from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + + +def export_femur_model(model, model_path, *, pmma_mat_id): + """Write a colored VTP surface and a four-view PNG beside an n88model. + + ModelRegion cell labels: 0 bone, 1 GT support, 2 FH loading support. The + supports are identified using their actual BC-node loading-axis positions. + Distal constraint nodes are shown in green in each QC view. + """ + prefix = Path(model_path).with_suffix("") + surface_path = prefix.with_name(prefix.name + "_surface.vtp") + png_path = prefix.with_name(prefix.name + "_3d.png") + points = vtk_to_numpy(model.GetPoints().GetData()) + bc = {} + for key, name in (("gt", "Greater_Trochanter_PMMA_Nodes"), + ("fh", "Femoral_Head_PMMA_Nodes"), + ("distal", "Distal_Femur_Nodes")): + ids = model.GetNodeSet(name) + if ids is None or not ids.GetNumberOfTuples(): + raise ValueError("Cannot export hip QC: missing %s." % name) + bc[key] = points[vtk_to_numpy(ids).astype(int)] + grid = vtk.vtkUnstructuredGrid() + grid.DeepCopy(model) + centers = vtk.vtkCellCenters() + centers.SetInputData(grid) + centers.Update() + y = vtk_to_numpy(centers.GetOutput().GetPoints().GetData())[:, 1] + material = grid.GetCellData().GetArray("MaterialID") or grid.GetCellData().GetScalars() + if material is None: + raise ValueError("Cannot export hip QC without cell material IDs.") + pmma = vtk_to_numpy(material) == pmma_mat_id + region = np.zeros(grid.GetNumberOfCells(), dtype=np.uint8) + gt_y, fh_y = bc["gt"][:, 1].mean(), bc["fh"][:, 1].mean() + near_gt = np.abs(y - gt_y) < np.abs(y - fh_y) + region[pmma & near_gt] = 1 + region[pmma & ~near_gt] = 2 + array = numpy_to_vtk(region, deep=True) + array.SetName("ModelRegion") + grid.GetCellData().AddArray(array) + geometry = vtk.vtkDataSetSurfaceFilter() + geometry.SetInputData(grid) + geometry.Update() + surface = geometry.GetOutput() + writer = vtk.vtkXMLPolyDataWriter() + writer.SetFileName(str(surface_path)) + writer.SetInputData(surface) + writer.SetDataModeToAppended() + writer.SetCompressorTypeToZLib() + if not writer.Write(): + raise RuntimeError("Could not write hip surface: %s" % surface_path) + outputs = {"surface_vtp": str(surface_path), "qc_3d_png": None, + "render_status": "unavailable", + "region_labels": {"0": "bone", "1": "GT support", "2": "FH loading support"}} + + colors = vtk.vtkLookupTable() + colors.SetNumberOfTableValues(3) + colors.Build() + for index, rgb in enumerate(((0.46, 0.65, 0.80), (0.89, 0.64, 0.25), (0.76, 0.24, 0.26))): + colors.SetTableValue(index, *rgb, 1) + normals = vtk.vtkPolyDataNormals() + normals.SetInputData(surface) + normals.SplittingOff() + normals.ConsistencyOn() + bounds = np.asarray(grid.GetBounds()).reshape(3, 2) + center = bounds.mean(axis=1) + size = np.ptp(bounds, axis=1) + radius = float(np.linalg.norm(size)) + window = vtk.vtkRenderWindow() + if window.GetClassName() == "vtkXOpenGLRenderWindow" and not os.environ.get("DISPLAY"): + window.Finalize() + outputs["render_message"] = "3D QC needs an X display or a headless EGL/OSMesa VTK build; surface VTP was saved." + warnings.warn(outputs["render_message"], RuntimeWarning, stacklevel=2) + return outputs + window.SetOffScreenRendering(1) + window.SetSize(1600, 1000) + window.SetMultiSamples(4) + views = (("Lateral", (3, 0, 0), (0, 0.5, 0.5, 1)), + ("Medial", (-3, 0, 0), (0.5, 0.5, 1, 1)), + ("Oblique anterior", (2.5, 0.4, -0.8), (0, 0, 0.5, 0.5)), + ("Oblique posterior", (-2.5, 0.4, 0.8), (0.5, 0, 1, 0.5))) + try: + for label, direction, viewport in views: + renderer = vtk.vtkRenderer() + renderer.SetBackground(1, 1, 1) + renderer.SetViewport(*viewport) + mapper = vtk.vtkPolyDataMapper() + mapper.SetInputConnection(normals.GetOutputPort()) + mapper.SetScalarModeToUseCellFieldData() + mapper.SetColorModeToMapScalars() + mapper.SelectColorArray("ModelRegion") + mapper.SetLookupTable(colors) + mapper.SetScalarRange(0, 2) + actor = vtk.vtkActor() + actor.SetMapper(mapper) + actor.GetProperty().SetOpacity(1) + actor.GetProperty().SetAmbient(0.3) + actor.GetProperty().SetDiffuse(0.7) + renderer.AddActor(actor) + cloud = vtk.vtkPolyData() + cloud_points = vtk.vtkPoints() + cloud_points.SetData(numpy_to_vtk(bc["distal"], deep=True)) + cloud.SetPoints(cloud_points) + vertices = vtk.vtkVertexGlyphFilter() + vertices.SetInputData(cloud) + node_mapper = vtk.vtkPolyDataMapper() + node_mapper.SetInputConnection(vertices.GetOutputPort()) + node_actor = vtk.vtkActor() + node_actor.SetMapper(node_mapper) + node_actor.GetProperty().SetColor(0.09, 0.39, 0.36) + node_actor.GetProperty().SetPointSize(3) + renderer.AddActor(node_actor) + text = vtk.vtkTextActor() + text.SetInput(label) + text.GetPositionCoordinate().SetCoordinateSystemToNormalizedViewport() + text.SetPosition(0.025, 0.94) + text.GetTextProperty().SetFontSize(23) + text.GetTextProperty().SetColor(0.15, 0.15, 0.15) + renderer.AddActor2D(text) + if label == "Lateral": + length_text = vtk.vtkTextActor() + length = float(bc["gt"][:, 2].min() - np.median(bc["distal"][:, 2])) + length_text.SetInput("GT disk edge to distal BC: %.1f mm" % length) + length_text.GetPositionCoordinate().SetCoordinateSystemToNormalizedViewport() + length_text.SetPosition(0.025, 0.035) + length_text.GetTextProperty().SetFontSize(22) + length_text.GetTextProperty().SetColor(0.15, 0.15, 0.15) + renderer.AddActor2D(length_text) + camera = renderer.GetActiveCamera() + camera.ParallelProjectionOn() + camera.SetFocalPoint(*center) + camera.SetPosition(*(center + np.asarray(direction) * radius)) + camera.SetViewUp(0, 1, 0) + window.AddRenderer(renderer) + # Fit the projected bounds in each viewport, including long shafts. + renderer.ResetCamera() + camera.SetParallelScale(camera.GetParallelScale() * 1.1) + renderer.ResetCameraClippingRange() + window.Render() + capture = vtk.vtkWindowToImageFilter() + capture.SetInput(window) + capture.Update() + png = vtk.vtkPNGWriter() + png.SetFileName(str(png_path)) + png.SetInputConnection(capture.GetOutputPort()) + png.Write() + if png.GetErrorCode(): + raise RuntimeError("Could not write hip QC: %s" % png_path) + finally: + window.Finalize() + return dict(outputs, qc_3d_png=str(png_path), render_status="rendered") diff --git a/ogo/fea/qc_render.py b/ogo/fea/qc_render.py new file mode 100644 index 0000000..1c1df42 --- /dev/null +++ b/ogo/fea/qc_render.py @@ -0,0 +1,370 @@ +"""Three-view QC of generated and solved hip/spine models using Ogo Visualize. + +Visualization only: reconstruct occupied voxel cells without changing the model. +The spine body ROI must be the aligned model-space mask, not the source mask. +SED smoothing is display-only. No model or analysis input is modified. +""" + +import argparse +import json +import os +import shutil +import warnings +from tempfile import TemporaryDirectory +from pathlib import Path +from time import perf_counter + +import nibabel as nib +from netCDF4 import Dataset +import numpy as np +from PIL import Image +import SimpleITK as sitk +import vtk +import vtkbone +from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtk + +from ogo.cli.Visualize import vis3d + + +def smooth_bone_sed(values, indices, bone, spacing): + """Normalized 0.8-mm Gaussian convolution, excluding PMMA and empty space.""" + from scipy.ndimage import gaussian_filter + + if not np.all(np.isfinite(values[bone])) or np.any(values[bone] < 0): + raise ValueError("Bone SED must be finite and nonnegative") + shape = tuple(indices.max(axis=0) + 1) + weights = np.zeros(shape, dtype=float) + energy = np.zeros(shape, dtype=float) + locations = tuple(indices[bone].T) + weights[locations] = 1 + energy[locations] = values[bone] + sigma = 0.8 / np.asarray(spacing) + numerator = gaussian_filter(energy, sigma, mode="constant", cval=0) + denominator = gaussian_filter(weights, sigma, mode="constant", cval=0) + return numerator[locations] / denominator[locations] + + +def preview(model_path, output_dir, site, body_mask=None, view="oblique", sed=False, + model_override=None, ineligible=False): + start = perf_counter() + if model_override is None: + reader = vtkbone.vtkboneN88ModelReader() + reader.SetFileName(str(model_path)) + reader.Update() + model = reader.GetOutput() + else: + model = model_override + if not model.GetNumberOfCells(): + raise ValueError("Empty FE model") + points = vtk_to_numpy(model.GetPoints().GetData()) + centers_filter = vtk.vtkCellCenters() + centers_filter.SetInputData(model) + centers_filter.Update() + centers = vtk_to_numpy(centers_filter.GetOutput().GetPoints().GetData()) + connectivity = vtk_to_numpy(model.GetCells().GetConnectivityArray()).reshape(-1, 8) + first_cell = points[connectivity[0]] + spacing = np.ptp(first_cell, axis=0) + origin = points.min(axis=0) + spacing / 2 + indices = np.rint((centers - origin) / spacing).astype(int) + if not np.allclose(origin + indices * spacing, centers, atol=1e-4): + raise ValueError("Model cells are not on a regular voxel grid") + materials = vtk_to_numpy(model.GetCellData().GetScalars()) + disk = materials == 5000 + sed_values = None + if sed: + from scipy.spatial import cKDTree + from matplotlib import colormaps + + array = model.GetCellData().GetArray("StrainEnergyDensity") + if array is None: + raise ValueError("SED rendering requires a solved model with StrainEnergyDensity") + sed_values = vtk_to_numpy(array) + if view == "oblique": + from ogo.fea.validation import write_measurements + + finite = np.isfinite(sed_values) + write_measurements(model_path, { + "sed_available": True, + "sed_finite": bool(np.all(finite)), + "sed_nonnegative": bool(np.all(sed_values[finite] >= 0)), + "bone_sed_p99": float(np.percentile(sed_values[~disk & finite], 99)) + if np.any(~disk & finite) else None, + }) + if not np.all(np.isfinite(sed_values)) or np.any(sed_values < 0): + raise ValueError("Nonfinite or negative SED values") + sed_max = float(np.percentile(sed_values[~disk], 99)) + if sed_max <= 0: + raise ValueError("No positive bone SED") + display_sed = smooth_bone_sed(sed_values, indices, ~disk, spacing) + assert np.all(np.isfinite(display_sed)) + bone_tree = cKDTree(centers[~disk]) + lookup = vtk.vtkLookupTable() + lookup.SetNumberOfTableValues(256) + lookup.SetRange(0, sed_max) + for i, rgba in enumerate(colormaps["jet"](np.linspace(0, 1, 256))): + lookup.SetTableValue(i, *rgba) + lookup.Build() + labels = np.ones(len(centers), dtype=np.uint8) + if site == "spine": + if body_mask is None: + raise ValueError("Provide the aligned vertebral-body mask") + mask = sitk.ReadImage(str(body_mask)) + direction = np.asarray(mask.GetDirection()).reshape(3, 3) + mask_indices = np.floor( + ((centers - mask.GetOrigin()) @ np.linalg.inv(direction).T) + / mask.GetSpacing() + 0.5 + ).astype(int) + valid = np.all((mask_indices >= 0) & (mask_indices < mask.GetSize()), axis=1) + body = np.zeros(len(centers), dtype=bool) + xyz = mask_indices[valid] + body[valid] = sitk.GetArrayFromImage(mask)[xyz[:, 2], xyz[:, 1], xyz[:, 0]] > 0 + labels[~body] = 2 + labels[disk] = 3 + node_sets = ("body_top", "body_bottom") if site == "spine" else ( + "Femoral_Head_PMMA_Nodes", "Greater_Trochanter_PMMA_Nodes") + for position, name in enumerate(node_sets): + nodes = model.GetNodeSet(name) + if nodes is None: + if ineligible: + continue + # N88 solver outputs may retain constraints but omit named node sets. + constraint_name = ("top_displacement", "bottom_fixed_z" if site == "spine" + else "bottom_fixed_y_PMMA")[position] + with Dataset(model_path) as dataset: + constraint = dataset.groups["Constraints"].groups[constraint_name] + node_ids = np.unique(constraint.variables["NodeNumber"][:]).astype(int) - 1 + else: + node_ids = vtk_to_numpy(nodes).astype(int) + membership = np.zeros(len(points), dtype=bool) + membership[node_ids] = True + # Four BC nodes identify the loaded outer face of a voxel cell. + labels[disk & (membership[connectivity].sum(axis=1) >= 4)] = 4 + if site == "hip": + nodes = model.GetNodeSet("Distal_Femur_Nodes") + if nodes is None: + if ineligible: + node_ids = np.array([], dtype=int) + else: + with Dataset(model_path) as dataset: + constraint = dataset.groups["Constraints"].groups["bottom_fixed_z"] + node_ids = np.unique(constraint.variables["NodeNumber"][:]).astype(int) - 1 + else: + node_ids = vtk_to_numpy(nodes).astype(int) + membership = np.zeros(len(points), dtype=bool) + membership[node_ids] = True + # Display the one-voxel layer adjoining the actual constrained shaft face. + labels[~disk & (membership[connectivity].sum(axis=1) >= 4)] = 5 + volume = np.zeros(tuple(indices.max(axis=0) + 1), dtype=np.uint8) + volume[tuple(indices.T)] = labels + assert np.count_nonzero(volume) == model.GetNumberOfCells() + affine = np.eye(4) + affine[:3, :3] = np.diag(spacing) + affine[:3, 3] = origin + output_dir.mkdir(parents=True, exist_ok=True) + prefix = output_dir / model_path.stem + nifti_path = Path(str(prefix) + "_debug_labels.nii.gz") + nib.save(nib.Nifti1Image(volume, affine), nifti_path) + palette = { + 1: ("Vertebral body" if site == "spine" else "Femur", [110, 175, 210]), + 2: ("Posterior process", [75, 115, 145]), + 3: ("PMMA disks", [230, 165, 65]), + 4: ("Loaded disk faces (one voxel layer)", [195, 55, 65]), + 5: ("Distal shaft BC (one voxel layer)", [195, 55, 65]), + } + def scene_setup(): + configured = False + filters = [] + + def rotate_camera(caller, event): + nonlocal configured + if configured: + return + configured = True + caller.SetBackground(1, 1, 1) + actors = caller.GetActors() + actors.InitTraversal() + present_labels = sorted(np.unique(labels)) + for actor_index in range(actors.GetNumberOfItems()): + actor = actors.GetNextActor() + mapper = actor.GetMapper() + mapper.Update() + smooth = vtk.vtkWindowedSincPolyDataFilter() + smooth.SetInputData(mapper.GetInput()) + smooth.SetNumberOfIterations(10) + smooth.SetPassBand(0.15) + smooth.BoundarySmoothingOff() + smooth.FeatureEdgeSmoothingOff() + smooth.NormalizeCoordinatesOn() + normals = vtk.vtkPolyDataNormals() + normals.SetInputConnection(smooth.GetOutputPort()) + normals.SplittingOff() + normals.Update() + mapper.SetInputConnection(normals.GetOutputPort()) + actor.GetProperty().SetInterpolationToPhong() + if sed and present_labels[actor_index] in (1, 2): + surface = normals.GetOutput() + xyz = vtk_to_numpy(surface.GetPoints().GetData()) + matrix = actor.GetMatrix() + transform = np.array([[matrix.GetElement(i, j) for j in range(4)] for i in range(4)]) + world = xyz @ transform[:3, :3].T + transform[:3, 3] + _, nearest = bone_tree.query(world) + values = numpy_to_vtk(display_sed[nearest], deep=True) + values.SetName("SED") + surface.GetPointData().SetScalars(values) + mapper.SetInputData(surface) + mapper.SetScalarModeToUsePointData() + mapper.SetLookupTable(lookup) + mapper.SetScalarRange(0, sed_max) + mapper.ScalarVisibilityOn() + actor.GetProperty().LightingOff() + filters.extend((smooth, normals)) + camera = caller.GetActiveCamera() + if site == "hip": + bounds = caller.ComputeVisiblePropBounds() + center = [(bounds[i] + bounds[i + 1]) / 2 for i in (0, 2, 4)] + distance = max(bounds[i + 1] - bounds[i] for i in (0, 2, 4)) * 3 + camera.SetFocalPoint(*center) + if view == "oblique": + # Look toward the distal (-Z) face without losing the shaft profile. + camera.SetPosition(center[0] + distance, center[1] + 0.18 * distance, + center[2] - 0.45 * distance) + camera.SetViewUp(0, 1, 0) + else: + camera.SetPosition(center[0], center[1] + (distance if view == "top" else -distance), + center[2]) + camera.SetViewUp(1 if view == "top" else -1, 0, 0) + camera.ParallelProjectionOn() + caller.ResetCamera() + elif view in ("top", "bottom"): + bounds = caller.ComputeVisiblePropBounds() + center = [(bounds[i] + bounds[i + 1]) / 2 for i in (0, 2, 4)] + distance = max(bounds[i + 1] - bounds[i] for i in (0, 2, 4)) * 3 + camera.SetFocalPoint(*center) + camera.SetPosition(center[0], center[1], center[2] + (distance if view == "top" else -distance)) + camera.SetViewUp(0, 1, 0) + camera.ParallelProjectionOn() + caller.ResetCamera() + caller.ResetCameraClippingRange() + if sed and view == "bottom": + bar = vtk.vtkScalarBarActor() + bar.SetLookupTable(lookup) + bar.SetTitle("SED (MPa)") + bar.SetNumberOfLabels(4) + bar.SetLabelFormat("%.3g") + bar.SetOrientationToHorizontal() + bar.SetPosition(0.25, 0.035) + bar.SetWidth(0.5) + bar.SetHeight(0.085) + for prop in (bar.GetTitleTextProperty(), bar.GetLabelTextProperty()): + prop.SetColor(0.1, 0.1, 0.1) + prop.SetFontFamilyToArial() + prop.ItalicOff() + prop.ShadowOff() + caller.AddActor2D(bar) + + return lambda renderer: rotate_camera(renderer, None) + + label_palette = {label: dict(RGB=color, LABEL=name, VIS=1) + for label, (name, color) in palette.items()} + suffix = "_ogo_visualize" + ("" if view == "oblique" else "_" + view) + if sed: + suffix += "_sed" + tif_path = Path(str(prefix) + suffix + ".tif") + render_start = perf_counter() + vis3d(str(nifti_path), [], None, 0, 1, 63.5, 20 if site == "hip" else 15, + 65 if site == "hip" else 35, False, str(tif_path), True, True, True, None, + renderer_setup=scene_setup(), label_palette=label_palette) + render_seconds = perf_counter() - render_start + png_path = Path(str(prefix) + suffix + ".png") + with Image.open(tif_path) as image: + image.save(png_path) + report = dict(source_model=str(model_path), labels={k: v[0] for k, v in palette.items()}, + occupied_voxels=int(np.count_nonzero(volume)), model_cells=model.GetNumberOfCells(), + label_counts={int(k): int(v) for k, v in zip(*np.unique(labels, return_counts=True))}, + render_seconds=render_seconds, total_seconds=perf_counter() - start, + smoothing="display-only windowed sinc, 10 iterations, passband 0.15", view=view, + visualization_only=True) + report["eligible_for_solve"] = not ineligible + if sed: + report.update(sed_range_mpa=[0, sed_max], sed_upper_limit="99th percentile of bone elements", + sed_mapping="nearest bone element at display-surface points", sed_scale="linear", + sed_display_smoothing="bone-masked normalized Gaussian, sigma 0.8 mm", + solved_sed_modified=False) + Path(str(prefix) + "_preview_" + view + ("_sed" if sed else "") + ".json").write_text(json.dumps(report, indent=2)) + return png_path + + +def panel(model_path, output_dir, site, body_mask=None, sed=False, + model_override=None, ineligible=False): + """Stack three opaque views, each independently framed for inspection.""" + rows = [] + for view in ("oblique", "top", "bottom"): + path = preview(model_path, output_dir, site, body_mask, view, sed, + model_override, ineligible) + with Image.open(path) as source: + rgb = source.convert("RGB") + pixels = np.asarray(rgb) + yy, xx = np.where(np.any(pixels < 245, axis=2)) + box = (max(0, xx.min() - 35), max(0, yy.min() - 35), + min(rgb.width, xx.max() + 36), min(rgb.height, yy.max() + 36)) + rows.append(rgb.crop(box)) + width, height = 1400, 850 + canvas = Image.new("RGB", (width, height * 3), "white") + for i, row in enumerate(rows): + row.thumbnail((width - 100, height - 100), Image.Resampling.LANCZOS) + canvas.paste(row, ((width - row.width) // 2, i * height + 70 + (height - 100 - row.height) // 2)) + path = output_dir / (model_path.stem + "_3view_panel" + ("_sed" if sed else "") + ".png") + canvas.save(path) + return path + + +def export_model_qc(model_path, site, body_mask=None, sed=False, + model=None, ineligible=False): + """Write one three-view PNG and its visualization settings beside a model. + + An ineligible hip can supply its uncut supported mesh without writing an + n88model or assigning an artificial distal boundary. Rendering errors are + raised here; pipeline callers use ``try_export_model_qc`` to warn instead. + """ + if site not in ("hip", "spine"): + raise ValueError("QC site must be hip or spine") + if ineligible and sed: + raise ValueError("Ineligible models cannot have solved SED QC") + window = vtk.vtkRenderWindow() + needs_display = window.GetClassName() == "vtkXOpenGLRenderWindow" + window.Finalize() + if needs_display and not os.environ.get("DISPLAY"): + raise RuntimeError("3D QC requires DISPLAY or an EGL/OSMesa VTK build") + model_path = Path(model_path) + if body_mask is None and site == "spine": + body_mask = model_path.with_name(model_path.stem + "_qc_body_mask.nii.gz") + suffix = "_sed_3d" if sed else "_qc_3d" + output = model_path.with_name(model_path.stem + suffix + ".png") + with TemporaryDirectory(prefix="ogo-qc-") as temporary: + temporary = Path(temporary) + rendered = panel(model_path, temporary, site, body_mask, sed, model, ineligible) + shutil.copyfile(rendered, output) + views = [json.loads((temporary / (model_path.stem + "_preview_" + view + + ("_sed" if sed else "") + ".json")).read_text()) + for view in ("oblique", "top", "bottom")] + output.with_suffix(".json").write_text(json.dumps({"site": site, "views": views}, indent=2)) + return str(output) + + +def try_export_model_qc(*args, **kwargs): + """Keep numerical model generation/solving usable if graphics are unavailable.""" + try: + return export_model_qc(*args, **kwargs) + except Exception as exc: + warnings.warn(f"3D model QC was not generated: {exc}", RuntimeWarning, stacklevel=2) + return None + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("model", type=Path) + parser.add_argument("--site", choices=("hip", "spine"), required=True) + parser.add_argument("--body-mask", type=Path) + parser.add_argument("--sed", action="store_true", help="Colour bone by solved linear SED using Jet") + args = parser.parse_args() + print(export_model_qc(args.model, args.site, args.body_mask, args.sed)) diff --git a/ogo/fea/review.py b/ogo/fea/review.py new file mode 100644 index 0000000..0ff77c7 --- /dev/null +++ b/ogo/fea/review.py @@ -0,0 +1,57 @@ +"""Study inclusion decisions, kept separate from measured model validity.""" + +import csv + + +REASONS = ('manual_review', 'segmentation', 'registration', 'support_contact', + 'insufficient_coverage', 'acceptable_after_inspection', 'other') +FIELDS = ('model_id', 'site', 'model_path', 'automatic_qc_status', 'automatic_qc_reasons', + 'manual_decision', 'manual_reason', 'manual_note', 'reviewer', 'review_timestamp', + 'final_inclusion', 'solver_eligible', 'qc_policy_version', 'measurement_source') + + +def identity(row): + return (row['site'], row['model_id']) + + +def inclusion_row(measurement, decision=None): + decision = decision or {} + manual = decision.get('manual_decision') or 'automatic' + if manual not in ('automatic', 'include', 'exclude'): + raise ValueError(f'Invalid manual decision: {manual}') + row = {key: measurement.get(key, '') for key in FIELDS} + status = measurement.get('qc_status', 'review') + row.update(automatic_qc_status=status, automatic_qc_reasons=measurement.get('qc_reasons', ''), + manual_decision=manual, + manual_reason=(decision.get('manual_reason') or 'manual_review') if manual != 'automatic' else '', + manual_note=decision.get('manual_note', '') if manual != 'automatic' else '', + reviewer=decision.get('reviewer', ''), review_timestamp=decision.get('review_timestamp', ''), + final_inclusion=manual if manual != 'automatic' else {'pass': 'include', 'fail': 'exclude'}.get(status, 'pending'), + solver_eligible='False' if measurement.get('generation_status') == 'too_short' else '') + return row + + +def load_decisions(path): + """Read exported decisions, rejecting duplicate or malformed identities.""" + decisions = {} + with open(path, newline='', encoding='utf-8-sig') as stream: + reader = csv.DictReader(stream) + if not {'model_id', 'site', 'manual_decision'} <= set(reader.fieldnames or []): + raise ValueError('Review CSV requires model_id, site, and manual_decision') + for row in reader: + if not row['model_id'] or row['site'] not in ('hip', 'spine', 'unknown'): + raise ValueError('Invalid model identity in review CSV') + key = identity(row) + if key in decisions: + raise ValueError(f'Duplicate review identity: {key}') + inclusion_row(row, row) # Validate the decision before applying it. + decisions[key] = row + return decisions + + +def write_inclusion(rows, path, decisions=None): + decisions = decisions or {} + with open(path, 'w', newline='') as stream: + writer = csv.DictWriter(stream, fieldnames=FIELDS) + writer.writeheader() + writer.writerows(inclusion_row(row, decisions.get(identity(row))) for row in rows) diff --git a/ogo/fea/shaft_geometry.py b/ogo/fea/shaft_geometry.py new file mode 100644 index 0000000..8234fac --- /dev/null +++ b/ogo/fea/shaft_geometry.py @@ -0,0 +1,164 @@ +"""Shaft coverage and final-model checks relative to the generated GT disk.""" + +import numpy as np +from vtk.util.numpy_support import vtk_to_numpy + +from ogo.util.vtk_image import vtk_image_to_numpy + + +def capture_distal_scan_face(vtk_mask, *, labels=None): + """Keep the native distal voxel-face footprint before cropping or padding. + + Background padding may precede the first occupied slice. Use that slice, + not the NIfTI bounding box or the separate 120-mm registration crop. + Coordinates are in Ogo's canonical RAS input frame. + """ + data = vtk_image_to_numpy(vtk_mask) + active = np.isin(data, list(labels)) if labels is not None else data != 0 + occupied_z = np.flatnonzero(np.any(active, axis=(0, 1))) + if not len(occupied_z): + raise ValueError("Cannot capture the distal scan boundary from an empty mask.") + first_z = int(occupied_z[0]) + xy = np.argwhere(active[:, :, first_z]) + extent = np.asarray(vtk_mask.GetExtent()).reshape(3, 2)[:, 0] + origin = np.asarray(vtk_mask.GetOrigin()) + spacing = np.asarray(vtk_mask.GetSpacing()) + z_face = float(origin[2] + (extent[2] + first_z - .5) * spacing[2]) + corners = [] + for offset in ((-.5, -.5), (.5, -.5), (.5, .5), (-.5, .5)): + corners.append(np.column_stack(( + origin[:2] + (xy + extent[:2] + offset) * spacing[:2], + np.full(len(xy), z_face), + ))) + return {"points_xyz": np.unique(np.concatenate(corners), axis=0), + "native_face_z_mm": z_face, + "native_distal_slice_index": int(extent[2] + first_z), + "native_face_voxels": int(len(xy)), + "native_face_at_image_boundary": first_z == 0} + + +def aligned_distal_scan_boundary(native_face, reference_to_input, *, + resampling_spacing, output_spacing): + """Carry the full scan end through the exact rigid ICP and bound resampling. + + ICP maps model/reference coordinates to input coordinates, so invert it. + The clearance is the projected half-voxel radius of the first resampling + grid plus half a final Z voxel. This bounds the two nearest-neighbor + mask resamplings without a cohort-specific distance or percentile. + """ + if hasattr(reference_to_input, "GetElement"): + matrix = np.array([[reference_to_input.GetElement(i, j) for j in range(4)] + for i in range(4)]) + else: + matrix = np.asarray(reference_to_input, dtype=float) + if (matrix.shape != (4, 4) or not np.all(np.isfinite(matrix)) + or not np.allclose(matrix[3], [0, 0, 0, 1]) + or not np.allclose(matrix[:3, :3].T @ matrix[:3, :3], np.eye(3), atol=1e-6) + or not np.isclose(np.linalg.det(matrix[:3, :3]), 1, atol=1e-6)): + raise ValueError("The distal scan boundary requires a finite rigid ICP matrix.") + inverse = np.linalg.inv(matrix) + if inverse[2, 2] <= 0: + raise ValueError("The distal input boundary is not inferior after ICP.") + first_spacing = np.asarray(resampling_spacing, dtype=float) + final_spacing = np.asarray(output_spacing, dtype=float) + if (first_spacing.shape != (3,) or final_spacing.shape != (3,) + or not np.all(np.isfinite([first_spacing, final_spacing])) + or np.any(first_spacing <= 0) or np.any(final_spacing <= 0)): + raise ValueError("Boundary resampling spacing must contain three positive values.") + points = np.asarray(native_face["points_xyz"], dtype=float) + if (points.ndim != 2 or points.shape[1] != 3 or not len(points) + or not np.all(np.isfinite(points))): + raise ValueError("The distal scan boundary has no finite physical face points.") + aligned_z = points @ inverse[2, :3] + inverse[2, 3] + clearance = .5 * np.dot(np.abs(inverse[2, :3]), first_spacing) + .5 * final_spacing[2] + return {**{key: value for key, value in native_face.items() if key != "points_xyz"}, + "source": "full native femur distal voxel face before resampling and rough crop", + "aligned_face_z_min_mm": float(aligned_z.min()), + "aligned_face_z_max_mm": float(aligned_z.max()), + "resampling_clearance_mm": float(clearance), + "required_flat_face_z_mm": float(aligned_z.max() + clearance)} + + +def measure_available_shaft(vtk_mask, gt_support_vtk, *, requested_length_mm, + distal_boundary=None, labels=None): + """Measure the full aligned model before cropping, including short scans. + + All coordinates refer to voxel faces. Coverage ends at the first flat + grid face that clears the transformed original scan end, not the lowest + corner of an oblique truncation. The GT origin is its connected disk edge. + """ + requested = float(requested_length_mm) + if not np.isfinite(requested) or requested <= 0: + raise ValueError("requested_length_mm must be positive.") + if distal_boundary is None: + raise ValueError("The transformed original distal scan boundary is required.") + required_face = float(distal_boundary["required_flat_face_z_mm"]) + if not np.isfinite(required_face): + raise ValueError("The distal scan boundary must be finite.") + spacing = np.asarray(vtk_mask.GetSpacing(), dtype=float) + origin = np.asarray(vtk_mask.GetOrigin(), dtype=float) + if (gt_support_vtk.GetExtent() != vtk_mask.GetExtent() + or not np.allclose(gt_support_vtk.GetOrigin(), origin) + or not np.allclose(gt_support_vtk.GetSpacing(), spacing)): + raise ValueError("GT support and model must share the same grid.") + data = vtk_image_to_numpy(vtk_mask) + active = np.isin(data, list(labels)) if labels is not None else data != 0 + if not np.any(active): + raise ValueError("Cannot measure shaft length from an empty model.") + disk = (vtk_image_to_numpy(gt_support_vtk) != 0) & active + if not np.any(disk): + raise ValueError("Cannot define shaft length from an empty GT support.") + gt_index = int(np.argwhere(disk)[:, 2].min()) + distal_index = int(np.argwhere(active)[:, 2].min()) + extent_z = vtk_mask.GetExtent()[4] + gt_face = float(origin[2] + (extent_z + gt_index - 0.5) * spacing[2]) + distal_face = float(origin[2] + (extent_z + distal_index - 0.5) * spacing[2]) + first_grid_face = origin[2] + (extent_z - .5) * spacing[2] + safe_index = int(np.ceil((required_face - first_grid_face) / spacing[2] - 1e-9)) + safe_face = float(max(distal_face, first_grid_face + safe_index * spacing[2])) + available = max(0.0, gt_face - safe_face) + eligible = available + 1e-6 >= requested + return {"type": "ogo.femur.shaft_geometry", "version": 3, + "definition": "generated GT disk minimum Z face minus complete-section safe flat face Z", + "available_shaft_length_mm": available, + "raw_available_shaft_length_mm": gt_face - distal_face, + "requested_shaft_length_mm": requested, + "measured_shaft_length_mm": None, + "eligible_for_requested_length": bool(eligible), + "status": "measured" if eligible else "too_short", + "full_model_distal_face_z_mm": distal_face, + "safe_distal_face_z_mm": safe_face, + "oblique_trim_loss_mm": safe_face - distal_face, + "distal_scan_boundary": dict(distal_boundary), + "complete_distal_section_verified": False, + "greater_trochanter_support_distal_edge_z": gt_face, + "distal_length_origin_z": gt_face, + "available_below_gt_support_distal_edge_mm": available} + + +def verify_model_shaft(model, metadata): + """Check the actual FE node sets before solving; return a verified sidecar.""" + points = vtk_to_numpy(model.GetPoints().GetData()) + coordinates = {} + for key, name in (("gt", "Greater_Trochanter_PMMA_Nodes"), + ("distal", "Distal_Femur_Nodes")): + ids = model.GetNodeSet(name) + if ids is None or ids.GetNumberOfTuples() == 0: + raise ValueError("Cannot verify shaft length: missing %s." % name) + coordinates[key] = points[vtk_to_numpy(ids).astype(int)] + gt_z = float(coordinates["gt"][:, 2].min()) + distal_z = float(np.median(coordinates["distal"][:, 2])) + span = float(np.ptp(coordinates["distal"][:, 2])) + length = gt_z - distal_z + retained = float(metadata["retained_length_mm"]) + if (abs(length - retained) > 0.01 or span > 0.01 + or abs(gt_z - metadata["distal_length_origin_z"]) > 0.01 + or abs(distal_z - metadata["cut_z_mm"]) > 0.01): + raise ValueError("Saved shaft BC nodes do not match the generated GT-disk crop.") + if distal_z < float(metadata["safe_distal_face_z_mm"]) - .01: + raise ValueError("Saved shaft cut does not clear the original distal scan boundary.") + return dict(metadata, status="verified", measured_shaft_length_mm=length, + complete_distal_section_verified=True, + gt_bc_distal_z_mm=gt_z, distal_bc_median_z_mm=distal_z, + distal_bc_z_span_mm=span, gt_bc_nodes=len(coordinates["gt"]), + distal_bc_nodes=len(coordinates["distal"])) diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 9e6caee..6fa638c 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -6,11 +6,14 @@ posterior-process labels are explicit and traceable. 2. Crop the vertebral body, posterior process, calibrated image, and combined vertebra mask to the same bounding box. -3. ICP-align the vertebral body to the bundled L4 compression reference. By - default the reference is PCA-scaled within 0.8,0.8,0.75 to 1.2,1.2,1.3 before - VTK rigid ICP; users can override the scale/reference explicitly. A - deterministic NumPy point-cloud ICP backend is kept for debugging and - process-orientation rescue attempts. +3. ICP-align the requested registration target to a bundled L4 compression + reference surface. The maintained body-only target uses the vertebral body; + the optional full-vertebra target uses body plus posterior-process labels + when the matching reference surface is available. By default the reference + is PCA-scaled within 0.8,0.8,0.75 to 1.2,1.2,1.3 before VTK rigid ICP; users + can override the scale/reference explicitly. A deterministic NumPy + point-cloud ICP backend is kept for debugging and process-orientation rescue + attempts. 4. Apply the ICP transform and set the final output spacing to 1.0 x 1.0 x 1.0 mm in one shared VTK reslice helper. Image data use cubic interpolation; body/process labels use nearest-neighbor interpolation. @@ -29,6 +32,7 @@ default. """ +import os from pathlib import Path @@ -63,12 +67,16 @@ DEFAULT_SPINE_REGISTRATION_LANDMARKS = 250 DEFAULT_SPINE_REGISTRATION_ITERATIONS = 75 DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC = True -DEFAULT_SPINE_REFERENCE_FILENAME = "L4_BODY_SPINE_COMPRESSION_REF.vtk" +SPINE_ICP_TARGETS = ("body", "vertebra") +DEFAULT_SPINE_ICP_TARGET = "body" +DEFAULT_SPINE_BODY_REFERENCE_FILENAME = "L4_BODY_SPINE_COMPRESSION_REF.vtk" +DEFAULT_SPINE_VERTEBRA_REFERENCE_FILENAME = "L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk" +DEFAULT_SPINE_REFERENCE_FILENAME = DEFAULT_SPINE_BODY_REFERENCE_FILENAME SPINE_CONTACT_SIZE_FRACTION = (1.6, 1.6) SPINE_SUPERIOR_CONTACT_CENTER_FRACTION = (0.5, 0.5, 1.05) SPINE_INFERIOR_CONTACT_CENTER_FRACTION = (0.5, 0.5, -0.05) -SPINE_ALIGNMENT_METHOD = "scaled ICP to reference vertebral body" +SPINE_ALIGNMENT_METHOD = "scaled ICP to spine reference surface" BENCHMARK_LINEAR_FE_DISPLACEMENT_MM = -0.2 BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM = -2.0 @@ -98,9 +106,16 @@ def solve_report_profile(preset=None): } -def default_spine_reference_path(): - """Return the bundled reference body used for spine ICP alignment.""" - return Path(__file__).resolve().parents[1] / "dat" / DEFAULT_SPINE_REFERENCE_FILENAME +def default_spine_reference_path(icp_target=DEFAULT_SPINE_ICP_TARGET): + """Return the bundled reference surface used for spine ICP alignment.""" + icp_target = str(icp_target).strip().lower() + if icp_target == "body": + filename = DEFAULT_SPINE_BODY_REFERENCE_FILENAME + elif icp_target == "vertebra": + filename = DEFAULT_SPINE_VERTEBRA_REFERENCE_FILENAME + else: + raise ValueError("spine_icp_target must be 'body' or 'vertebra'.") + return Path(__file__).resolve().parents[1] / "dat" / filename def pistoia_mask_output_path(output_file): @@ -1391,6 +1406,9 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, registration_backend = kwargs.get("registration_backend", DEFAULT_SPINE_REGISTRATION_BACKEND) registration_landmarks = kwargs.get("registration_landmarks", DEFAULT_SPINE_REGISTRATION_LANDMARKS) registration_iterations = kwargs.get("registration_iterations", DEFAULT_SPINE_REGISTRATION_ITERATIONS) + spine_icp_target = str(kwargs.get("spine_icp_target", DEFAULT_SPINE_ICP_TARGET)).strip().lower() + if spine_icp_target not in SPINE_ICP_TARGETS: + raise ValueError("spine_icp_target must be 'body' or 'vertebra'.") registration_retry_on_process_qc = kwargs.get( "registration_retry_on_process_qc", DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC, @@ -1486,11 +1504,18 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, registration_body = threshold(preprocessed_labels, body_label) isolated_vertebra = registration_body isolated_process = threshold(preprocessed_labels, process_label) + registration_target = registration_body + if spine_icp_target == "vertebra": + registration_target = combine_mask(registration_body.GetOutput(), isolated_process.GetOutput()) # Marching Cubes and Registration - ogo.message(f"Starting ICP registration to reference...: {reference_path} ") + if not os.path.exists(reference_path): + raise FileNotFoundError( + f"Spine ICP reference does not exist for target '{spine_icp_target}': {reference_path}" + ) + ogo.message(f"Starting ICP registration to {spine_icp_target} reference...: {reference_path} ") icp = get_icp_with_scaling( - registration_body, + registration_target, reference_path, process=isolated_process, scale_factors=registration_scale, @@ -1829,19 +1854,28 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, # Quality control and output. If quality control is activated output only provided if it passes if quality_control: - image_path = n88model_output_path.replace(".n88model", ".png") dataframe_path = n88model_output_path.replace(".n88model", "_BCcheck.csv") ogo.message(f"Starting QC...") test = check_image(boundary_masks, dataframe_path) - visualize_slice(image_with_pads, image_path) ogo.message(f"Writing n88model file: {n88model_output_path}") + from ogo.fea.validation import measure_model, write_measurements + + write_measurements(n88model_output_path, measure_model( + model, "spine", body_image=padded_mask1, process_image=padded_mask2, + registration_rotation=sample_to_reference_rotation)) writer = vtkbone.vtkboneN88ModelWriter() writer.SetInputData(model) writer.SetFileName(n88model_output_path) writer.Update() + # Retain the aligned body ROI so post-solve QC uses the identical anatomy. + body_qc_path = n88model_output_path.replace(".n88model", "_qc_body_mask.nii.gz") + write_vtk_image_with_sitk_geometry(padded_mask1, body_qc_path) + from ogo.fea.qc_render import try_export_model_qc + try_export_model_qc(n88model_output_path, "spine", body_mask=body_qc_path, model=model) + ###################################################################### MAIN @@ -1932,6 +1966,10 @@ def main(): help="Maximum spine ICP landmarks/sampled points. For vtk this maps to SetMaximumNumberOfLandmarks; for numpy this caps sampled surface points. (default: %(default)s)") parser.add_argument("--registration_iterations", type=int, default=DEFAULT_SPINE_REGISTRATION_ITERATIONS, help="Maximum spine ICP iterations. (default: %(default)s)") + parser.add_argument("--spine_icp_target", choices=SPINE_ICP_TARGETS, default=DEFAULT_SPINE_ICP_TARGET, + help=("Segmentation surface used for spine ICP. body uses only the vertebral body label; " + "vertebra uses body plus posterior-process labels and requires a matching reference " + "surface. (default: %(default)s)")) parser.add_argument("--top_node_set_id", type=int, default=DEFAULT_SPINE_TOP_NODE_SET_ID, help="ID for the top node set. (default: %(default)s)") parser.add_argument("--bottom_node_set_id", type=int, default=DEFAULT_SPINE_BOTTOM_NODE_SET_ID, @@ -1996,7 +2034,7 @@ def main(): # Set default reference path if not provided reference_path = args.reference_path if reference_path is None: - reference_path = str(default_spine_reference_path()) + reference_path = str(default_spine_reference_path(args.spine_icp_target)) # Extract all kwargs kwargs = vars(args).copy() # Convert parsed arguments to a dictionary diff --git a/ogo/fea/validation.py b/ogo/fea/validation.py new file mode 100644 index 0000000..c46148e --- /dev/null +++ b/ogo/fea/validation.py @@ -0,0 +1,323 @@ +"""Generation-time measurements and CSV-only FEA review. + +Measurements describe geometry, not strength. Thresholds below test construction +invariants; unusual anatomy is flagged for review, never silently excluded. +""" + +import csv +from datetime import datetime, timezone +import json +from pathlib import Path + +import numpy as np +from scipy.ndimage import label + + +def grid_measurements(materials, spacing, loading_axis): + """Measure voxel connectivity and axial disk contact on the model grid. + + Contact area counts shared bone/PMMA faces normal to the loading axis. + Components use face connectivity. PMMA material ID is 5000 in Ogo recipes. + """ + bone = (materials != 0) & (materials != 5000) + disks, count = label(materials == 5000) + components, bone_count = label(bone) + bone_sizes = np.bincount(components.ravel())[1:] + voxel_volume = float(np.prod(spacing)) + result = {'bone_component_count': bone_count, 'disk_component_count': count, + 'bone_volume_mm3': int(bone.sum()) * voxel_volume, + 'bone_largest_component_fraction': float(bone_sizes.max() / bone.sum()) if bone.any() else 0} + # Never wrap opposite image boundaries as np.roll would do. + neighbors = np.zeros(bone.shape, dtype=bool) + lower = [slice(None)] * 3 + upper = [slice(None)] * 3 + lower[loading_axis] = slice(None, -1) + upper[loading_axis] = slice(1, None) + lower, upper = tuple(lower), tuple(upper) + neighbors[lower] |= bone[upper] + neighbors[upper] |= bone[lower] + area = float(np.prod(np.delete(spacing, loading_axis))) + for component in range(1, count + 1): + disk = disks == component + contact = disk & neighbors + footprint = np.any(disk, axis=loading_axis) + contact_footprint = np.any(contact, axis=loading_axis) + prefix = f'disk_{component}_' + result[prefix + 'volume_mm3'] = int(disk.sum()) * voxel_volume + result[prefix + 'contact_area_mm2'] = int(contact_footprint.sum()) * area + result[prefix + 'contact_footprint_fraction'] = float(contact_footprint.sum() / footprint.sum()) + result[prefix + 'contact_component_count'] = label(contact_footprint)[1] + result[prefix + 'centroid_axis_mm'] = float(np.argwhere(disk)[:, loading_axis].mean() * spacing[loading_axis]) + depth = disk.sum(axis=loading_axis) * spacing[loading_axis] + result[prefix + 'axial_thickness_p50_mm'] = float(np.median(depth[footprint])) + result[prefix + 'axial_thickness_p95_mm'] = float(np.percentile(depth[footprint], 95)) + return result + + +def model_grid(model): + """Return material grid, spacing, and voxel-centre origin for a voxel mesh.""" + import vtk + from vtk.util.numpy_support import vtk_to_numpy + + points = vtk_to_numpy(model.GetPoints().GetData()) + cells = vtk_to_numpy(model.GetCells().GetConnectivityArray()).reshape(-1, 8) + spacing = np.ptp(points[cells[0]], axis=0) + centers = vtk.vtkCellCenters() + centers.SetInputData(model) + centers.Update() + xyz = vtk_to_numpy(centers.GetOutput().GetPoints().GetData()) + origin = xyz.min(axis=0) + indices = np.rint((xyz - origin) / spacing).astype(int) + if not np.allclose(origin + indices * spacing, xyz, atol=1e-4): + raise ValueError('QC requires a regular voxel FE grid') + grid = np.zeros(tuple(indices.max(axis=0) + 1), dtype=np.int32) + grid[tuple(indices.T)] = vtk_to_numpy(model.GetCellData().GetScalars()) + return grid, spacing, origin + + +def measure_model(model, site, shaft=None, ineligible=False, body_image=None, process_image=None, + registration_rotation=None): + """Extract geometry from the already-generated in-memory voxel FE mesh.""" + import vtk + from vtk.util.numpy_support import vtk_to_numpy + + if site not in ('hip', 'spine'): + raise ValueError('site must be hip or spine') + grid, spacing, origin = model_grid(model) + points = vtk_to_numpy(model.GetPoints().GetData()) + cells = vtk_to_numpy(model.GetCells().GetConnectivityArray()).reshape(-1, 8) + centers = vtk.vtkCellCenters() + centers.SetInputData(model) + centers.Update() + indices = np.rint((vtk_to_numpy(centers.GetOutput().GetPoints().GetData()) - origin) / spacing).astype(int) + axis = 1 if site == 'hip' else 2 + row = grid_measurements(grid, spacing, axis) + row.update(site=site, generation_status='too_short' if ineligible else 'generated', + measurement_schema_version=1, measurement_source='generation', + measurement_timestamp=datetime.now(timezone.utc).isoformat(), + node_count=len(points), element_count=len(cells)) + if registration_rotation is not None: + matrix = np.asarray(registration_rotation) + row['registration_rotation_deg'] = float(np.degrees(np.arccos(np.clip((np.trace(matrix) - 1) / 2, -1, 1)))) + centroids = {} + for name, image in [('body', body_image), ('process', process_image)]: + if image is None: + continue + from ogo.util.vtk_image import vtk_image_to_numpy + active = vtk_image_to_numpy(image) != 0 + locations = np.argwhere(active) + row[name + '_volume_mm3'] = float(len(locations) * np.prod(image.GetSpacing())) + row[name + '_component_count'] = label(active)[1] + if len(locations): + centroids[name] = np.asarray(image.GetOrigin()) + locations.mean(axis=0) * image.GetSpacing() + for dimension, extent in zip('xyz', (np.ptp(locations, axis=0) + 1) * image.GetSpacing()): + row[name + '_' + dimension + '_extent_mm'] = float(extent) + if len(centroids) == 2: + vector = centroids['process'] - centroids['body'] + row['process_body_axial_offset_mm'] = float(abs(vector[2])) + row['process_body_transverse_offset_mm'] = float(np.linalg.norm(vector[:2])) + names = ('Femoral_Head_PMMA_Nodes', 'Greater_Trochanter_PMMA_Nodes', 'Distal_Femur_Nodes') if site == 'hip' else ('body_top', 'body_bottom') + coordinates = [] + for name in names: + nodes = model.GetNodeSet(name) + selected = points[vtk_to_numpy(nodes).astype(int)] if nodes is not None else np.empty((0, 3)) + row[name + '_node_count'] = len(selected) + if len(selected): + coordinates.append(selected) + row[name + '_span_mm'] = float(np.ptp(selected[:, 2 if name == 'Distal_Femur_Nodes' else axis])) + for dimension, center in zip('xyz', selected.mean(axis=0)): + row[name + '_centroid_' + dimension + '_mm'] = float(center) + # Solved legacy files may omit all named sets while retaining constraints. + # Do not interpret absent audit evidence as a measured zero boundary count. + row['boundary_count'] = len(coordinates) if coordinates or ineligible else None + if site == 'hip' and len(coordinates) == 3: + row['distal_boundary_span_mm'] = row['Distal_Femur_Nodes_span_mm'] + row['measured_shaft_length_mm'] = float(np.min(coordinates[1][:, 2]) - np.median(coordinates[2][:, 2])) + distal_nodes = vtk_to_numpy(model.GetNodeSet('Distal_Femur_Nodes')).astype(int) + row['shaft_bc_node_count'] = len(distal_nodes) + for direction in ('x', 'z'): + fixed = model.GetConstraints().GetItem('bottom_fixed_' + direction) + if fixed is not None: + indices_fixed = vtk_to_numpy(fixed.GetIndices()).astype(int) + senses_fixed = vtk_to_numpy(fixed.GetAttributes().GetArray('SENSE')) + values_fixed = vtk_to_numpy(fixed.GetAttributes().GetArray('VALUE')) + correct = (senses_fixed == 'xyz'.index(direction)) & (values_fixed == 0) + row['shaft_bc_fixed_' + direction + '_node_count'] = len(np.intersect1d(distal_nodes, indices_fixed[correct])) + membership = np.zeros(len(points), dtype=bool) + membership[vtk_to_numpy(model.GetNodeSet('Distal_Femur_Nodes')).astype(int)] = True + face_cells = membership[cells].sum(axis=1) >= 4 + distal = indices[:, 2].min() + bone_cells = vtk_to_numpy(model.GetCellData().GetScalars()) != 5000 + cross_section = bone_cells & (indices[:, 2] == distal) + row['distal_boundary_area_mm2'] = float(np.count_nonzero(face_cells & bone_cells) * spacing[0] * spacing[1]) + row['distal_cross_section_area_mm2'] = float(cross_section.sum() * spacing[0] * spacing[1]) + row['distal_boundary_coverage_fraction'] = float(np.count_nonzero(face_cells & cross_section) / cross_section.sum()) if cross_section.any() else 0 + # Reuse the recipe's patch construction; 90% is a bounding-box width, + # not a prescribed fraction of the irregular bone cross-section area. + from vtk.util.numpy_support import numpy_to_vtk + from ogo.fea.femur import (POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, + straight_crop_face_support_surface_vtk) + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + template = vtk.vtkImageData() + template.SetDimensions(*grid.shape) + template.SetSpacing(*spacing) + template.SetOrigin(*origin) + template.GetPointData().SetScalars(numpy_to_vtk(grid.ravel(order='F'), deep=True)) + face = np.zeros(grid.shape, dtype=np.uint8) + face[tuple(indices[cross_section].T)] = 1 + patch = vtk_image_to_numpy(straight_crop_face_support_surface_vtk( + numpy_to_vtk_image(face, template), template)) != 0 + intended = patch[tuple(indices.T)] & cross_section + row['distal_support_patch_width_fraction'] = POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION + row['distal_support_patch_area_mm2'] = float(intended.sum() * spacing[0] * spacing[1]) + row['distal_support_patch_coverage_fraction'] = float( + np.count_nonzero(face_cells & intended) / intended.sum()) if intended.any() else 0 + # Compare the prescribed displacement with the actual support separation. + constraint = model.GetConstraints().GetItem('top_displacement') + if constraint is not None and len(coordinates) >= 2: + values = vtk_to_numpy(constraint.GetAttributes().GetArray('VALUE')) + senses = vtk_to_numpy(constraint.GetAttributes().GetArray('SENSE')) + row['loading_axis_matches'] = bool(np.all(senses == axis)) + row['loading_compressive'] = bool(np.median(values) * (np.mean(coordinates[0][:, axis]) - np.mean(coordinates[1][:, axis])) < 0) + if shaft: + for key in ('available_shaft_length_mm', 'requested_shaft_length_mm', 'measured_shaft_length_mm', + 'oblique_trim_loss_mm', 'complete_distal_section_verified'): + if key in shaft and (key != 'measured_shaft_length_mm' or key not in row): + row[key] = shaft[key] + for key, value in shaft.get('registration', {}).items(): + if isinstance(value, (str, int, float, bool)): + row['registration_' + key] = value + if 'reference_scale' in shaft: + row['registration_reference_scale'] = json.dumps(shaft['reference_scale']) + return row + + +def write_measurements(model_path, measurements): + """Write one row, merging new stage measurements without deleting geometry.""" + model_path = Path(model_path).absolute() + path = model_path.with_name(model_path.stem + '_qc_metrics.csv') + row = {} + if path.exists(): + with path.open(newline='') as stream: + row.update(next(csv.DictReader(stream))) + row.update(measurements) + row.update(model_id=model_path.stem, model_path=str(model_path), + anatomy_image=str(model_path.with_name(model_path.stem + '_qc_3d.png')), + sed_image=str(model_path.with_name(model_path.stem + '_sed_3d.png'))) + temporary = path.with_suffix('.csv.tmp') + with temporary.open('w', newline='') as stream: + writer = csv.DictWriter(stream, fieldnames=sorted(row)) + writer.writeheader() + writer.writerow(row) + temporary.replace(path) + return path + + +def evaluate(measurements): + """Apply versioned construction tests. Missing evidence requires review.""" + row = dict(measurements) + failures, review = [], [] + if row.get('measurement_error'): + review.append('legacy_measurement_error') + if row.get('site') not in ('hip', 'spine'): + review.append('unknown_anatomy') + if row.get('measurement_source') == 'legacy_backfill': + if row.get('site') == 'spine' and row.get('process_body_axial_offset_mm') in (None, ''): + review.append('legacy_body_process_metrics_missing') + if row.get('site') == 'hip' and row.get('available_shaft_length_mm') in (None, ''): + review.append('legacy_original_coverage_missing') + if row.get('legacy_evidence_error'): + review.append('legacy_evidence_error') + truth = lambda value: str(value).lower() == 'true' + def number(key): + try: + value = float(row[key]) + return value if np.isfinite(value) else None + except (KeyError, ValueError, TypeError): + return None + if row.get('generation_status') == 'too_short': + failures.append('too_short') + for key, expected, reason in [('disk_component_count', 2, 'disk_connectivity'), + ('boundary_count', 3 if row.get('site') == 'hip' else 2, 'missing_boundary')]: + value = number(key) + if value is None: + review.append('missing_' + key) + elif value != expected: + failures.append(reason) + if number('bone_component_count') is None: + review.append('missing_bone_connectivity') + elif number('bone_component_count') != 1: + review.append('disconnected_bone') + if 'loading_compressive' not in row: + review.append('missing_loading_direction') + elif not truth(row['loading_compressive']): + failures.append('noncompressive_loading') + if 'loading_axis_matches' not in row: + review.append('missing_loading_axis') + elif not truth(row['loading_axis_matches']): + failures.append('incorrect_loading_axis') + if row.get('site') == 'hip' and 'too_short' not in failures: + measured, requested = number('measured_shaft_length_mm'), number('requested_shaft_length_mm') + if measured is None or requested is None: + review.append('missing_shaft_length') + elif abs(measured - requested) > 0.01: + failures.append('shaft_length_mismatch') + if 'complete_distal_section_verified' not in row: + review.append('missing_distal_section_check') + elif not truth(row['complete_distal_section_verified']): + failures.append('incomplete_distal_section') + span = number('distal_boundary_span_mm') + if span is None: + review.append('missing_distal_planarity') + elif span > 1e-4: + failures.append('nonplanar_distal_boundary') + coverage = number('distal_support_patch_coverage_fraction') + minimum = 1 - 1e-6 + if coverage is None: + # Older CSVs only describe the full face. Allow the established + # inset patch without requiring model reopening for gallery export. + coverage = number('distal_boundary_coverage_fraction') + minimum = 0.89 + if coverage is None: + review.append('missing_distal_boundary_coverage') + elif coverage < minimum: + failures.append('incomplete_distal_boundary') + shaft_nodes = number('shaft_bc_node_count') + for direction in ('x', 'z'): + fixed_nodes = number('shaft_bc_fixed_' + direction + '_node_count') + if shaft_nodes is None or fixed_nodes is None: + review.append('missing_shaft_fixation_' + direction) + elif fixed_nodes != shaft_nodes: + failures.append('incomplete_shaft_fixation_' + direction) + for key in row: + if key.endswith('_contact_area_mm2') and number(key) == 0: + failures.append('no_axial_contact_' + key.split('_contact')[0]) + for component in (1, 2): + if number(f'disk_{component}_contact_area_mm2') is None: + review.append(f'missing_disk_{component}_contact') + elif number(f'disk_{component}_contact_component_count') != 1: + review.append(f'fragmented_disk_{component}_contact') + axial = number('process_body_axial_offset_mm') + for key in ('sed_finite', 'sed_nonnegative'): + if key in row and not truth(row[key]): + failures.append('invalid_' + key) + transverse = number('process_body_transverse_offset_mm') + if axial is not None and transverse is not None and axial > 0.75 * transverse + 2: + review.append('process_orientation') + if row.get('site') == 'spine': + for name in ('body_top', 'body_bottom'): + span = number(name + '_span_mm') + if span is None: + review.append('missing_' + name + '_planarity') + elif span > 1e-4: + failures.append('nonplanar_' + name) + row.update(qc_status='fail' if failures else 'review' if review else 'pass', + qc_reasons=';'.join(failures + review), qc_policy_version=1) + return row + + +def generate_gallery(rows, output, decisions=None): + """Build the standalone review gallery without opening any FE model.""" + from ogo.fea.gallery import generate_gallery as render + + return render(rows, output, decisions=decisions) diff --git a/ogo/util/faim.py b/ogo/util/faim.py index 570acc1..131075e 100644 --- a/ogo/util/faim.py +++ b/ogo/util/faim.py @@ -940,6 +940,19 @@ def run_faim_pipeline( ) if not dry_run: log_file.write_text(log_text) + profile = str(report_profile or "").strip().lower() + if profile in ("spine", "femur"): + from ogo.fea.qc_render import try_export_model_qc + from ogo.fea.validation import write_measurements + + write_measurements(model_file, { + "site": "hip" if profile == "femur" else "spine", + "solver_processing_complete": True, + }) + + qc_path = try_export_model_qc(model_file, "hip" if profile == "femur" else "spine", sed=True) + if qc_path is None: + warnings.append("Post-solve 3D SED QC unavailable; see rendering warning.") write_results_csv( output_file=results_csv, model_file=model_file, diff --git a/setup.cfg b/setup.cfg index 95fde32..a01cdde 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,6 +19,9 @@ keywords = medical imaging +[options.package_data] +ogo.fea = gallery_review.js + [entry_points] console_scripts = ogoAnalyzeBMD = ogo.cli.AnalyzeBMD:main @@ -33,6 +36,7 @@ console_scripts = ogoRepairNIfTI = ogo.cli.RepairNifti:main ogoVisualize = ogo.cli.Visualize:main ogoValidate = ogo.cli.Validate:main + ogoValidateFEA = ogo.cli.ValidateFEA:main ogoMergeLabels = ogo.cli.MergeLabels:main ogoImageCrop = ogo.cli.ImageCrop:main ogoGraphCuts = ogo.cli.GraphCuts:main diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index 12d5ffe..5387758 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -12,6 +12,25 @@ _SPEC.loader.exec_module(GenerateFEM) +def test_generation_thread_limit_applies_to_vtk_and_itk(): + vtk = pytest.importorskip("vtk") + sitk = pytest.importorskip("SimpleITK") + old_default = vtk.vtkMultiThreader.GetGlobalDefaultNumberOfThreads() + old_maximum = vtk.vtkMultiThreader.GetGlobalMaximumNumberOfThreads() + old_itk = sitk.ProcessObject.GetGlobalDefaultNumberOfThreads() + try: + GenerateFEM.limit_generation_threads(1) + assert vtk.vtkMultiThreader.GetGlobalDefaultNumberOfThreads() == 1 + assert vtk.vtkMultiThreader.GetGlobalMaximumNumberOfThreads() == 1 + assert sitk.ProcessObject.GetGlobalDefaultNumberOfThreads() == 1 + with pytest.raises(ValueError, match="positive"): + GenerateFEM.limit_generation_threads(0) + finally: + vtk.vtkMultiThreader.SetGlobalMaximumNumberOfThreads(old_maximum) + vtk.vtkMultiThreader.SetGlobalDefaultNumberOfThreads(old_default) + sitk.ProcessObject.SetGlobalDefaultNumberOfThreads(old_itk) + + BENCHMARK_LINEAR_ARGS = [ "--fe_displacement", "-0.2", @@ -50,7 +69,10 @@ "250", "--registration_iterations", "75", + "--spine_icp_target", + "body", ] +FEMUR_DEFAULT_REGISTRATION_ARGS = [] @pytest.fixture @@ -129,6 +151,31 @@ def test_spine_runs_each_requested_vertebra(monkeypatch, tmp_path, solve_calls): ] +def test_spine_can_request_full_vertebra_icp_target(monkeypatch, tmp_path, solve_calls): + monkeypatch.chdir(tmp_path) + calls = [] + monkeypatch.setattr(GenerateFEM, "run_spine_command", lambda argv: calls.append(list(argv))) + + GenerateFEM.main( + [ + "spine", + "density.nii.gz", + "spine_mask.nii.gz", + "--output_path", + "models", + "--vertebra", + "L1:2:1", + "--spine_icp_target", + "vertebra", + "--reference_path", + "L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk", + ] + ) + + assert calls[0][calls[0].index("--spine_icp_target") + 1] == "vertebra" + assert calls[0][calls[0].index("--reference_path") + 1] == "L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk" + + def test_hip_defaults_to_both_sides(monkeypatch, tmp_path, solve_calls): monkeypatch.chdir(tmp_path) calls = [] @@ -137,8 +184,24 @@ def test_hip_defaults_to_both_sides(monkeypatch, tmp_path, solve_calls): GenerateFEM.main(["hip", "density.nii.gz", "hip_mask.nii.gz", "--output_path", "models"]) assert calls == [ - ["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "1", "--output_path", "models"], - ["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "2", "--output_path", "models"], + [ + "density.nii.gz", + "hip_mask.nii.gz", + "--femur_side", + "1", + "--output_path", + "models", + *FEMUR_DEFAULT_REGISTRATION_ARGS, + ], + [ + "density.nii.gz", + "hip_mask.nii.gz", + "--femur_side", + "2", + "--output_path", + "models", + *FEMUR_DEFAULT_REGISTRATION_ARGS, + ], ] assert solve_calls == [ (Path("models/density_LF.n88model"), "hip", calls[0]), @@ -178,7 +241,7 @@ def test_hip_can_run_one_side(monkeypatch, solve_calls): GenerateFEM.main(["hip", "density.nii.gz", "hip_mask.nii.gz", "--side", "right"]) - assert calls == [["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "2"]] + assert calls == [["density.nii.gz", "hip_mask.nii.gz", "--femur_side", "2", *FEMUR_DEFAULT_REGISTRATION_ARGS]] assert solve_calls == [(Path("density_RF.n88model"), "hip", calls[0])] @@ -427,6 +490,30 @@ def test_hip_forwards_pistoia_mask_to_builder(monkeypatch, tmp_path, solve_calls assert calls[0][calls[0].index("--pistoia_mask") + 1] == "femoral_neck.nii.gz" +def test_hip_wrapper_matches_fixed_registration_builder(monkeypatch, tmp_path, solve_calls): + from ogo.fea import femur + + monkeypatch.chdir(tmp_path) + calls = [] + monkeypatch.setattr(femur, "sidewaysFallFe", lambda args: calls.append(args)) + + GenerateFEM.main( + [ + "hip", + "density.nii.gz", + "left_femur.nii.gz", + "--side", + "left", + "--femur_greater_trochanter_distal_length=15", + "--no-solve", + ] + ) + + assert calls[0].femur_greater_trochanter_distal_length == 15 + assert calls[0].femur_shaft_length == 120 + assert not hasattr(calls[0], "registration_backend") + + def test_hip_pistoia_mask_label_uses_bone_mask_when_mask_omitted(monkeypatch, tmp_path, solve_calls): monkeypatch.chdir(tmp_path) calls = [] @@ -570,7 +657,7 @@ def test_solve_model_sets_femur_displacement_from_percent(monkeypatch, tmp_path) assert calls[0]["report_profile"] == "femur" assert calls[0]["target_displacement"] == 4.0 assert calls[0]["solve_displacement_percent"] == 4.0 - assert calls[0]["critical_volume"] == 7.0 + assert calls[0]["critical_volume"] == 11.2 assert calls[0]["critical_strain"] == 0.009 @@ -736,6 +823,7 @@ def test_spine_modeling_metadata_records_materials_and_bcs(tmp_path): data = json.loads(path.read_text()) assert data["target"] == {"vertebra": "L1", "body_label": 2, "process_label": 3} + assert data["alignment"]["registration_target"] == "body" assert data["alignment"]["registration_backend"] == "vtk" assert data["alignment"]["registration_landmarks"] == 250 assert data["alignment"]["registration_iterations"] == 75 @@ -768,8 +856,6 @@ def test_femur_modeling_metadata_records_materials_shaft_and_bcs(tmp_path): "6", "--pmma_intrusion", "6", - "--femur_lesser_trochanter_distal_offset", - "50", "--compartment_mask", "trab_cort.nii.gz", ] @@ -778,9 +864,14 @@ def test_femur_modeling_metadata_records_materials_shaft_and_bcs(tmp_path): data = json.loads(path.read_text()) assert data["target"]["side"] == "left" + assert data["alignment"]["registration_backend"] == "numpy" + assert data["alignment"]["registration_landmarks"] == 40000 + assert data["alignment"]["registration_iterations"] == 50 assert data["geometry"]["model_coordinates"] == "preprocessed_image_physical_space" assert data["segmentation"]["compartment_labels"] == {"cortical": 1, "trabecular": 2} - assert data["shaft_standardization"]["lesser_trochanter_distal_offset_mm"] == 50.0 + assert "lesser_trochanter_distal_offset_mm" not in data["shaft_standardization"] + assert "bbox_ratio" not in data["shaft_standardization"] + assert "proximal_box_ratio" not in data["shaft_standardization"] assert data["materials"]["include_cortical_region"] is True assert data["materials"]["cortical"]["material_id_range"] == [129, 256] assert data["boundary_conditions"]["fixture_geometry"]["femoral_head"]["relative_to"] == "model_bbox" @@ -833,6 +924,9 @@ def test_femur_modeling_metadata_records_gt_length_study_mode(tmp_path): "flat post-ICP aligned-frame crop face at fixed shaft length below detected GT-disk distal edge" ) assert data["boundary_conditions"]["fixture_geometry"]["distal_shaft"]["support_fraction"] == 0.9 + assert shaft["coverage_definition"] == ( + "GT disk distal voxel face minus safe flat face clearing the full transformed native scan end" + ) def test_femur_modeling_metadata_records_absolute_displacement(tmp_path): diff --git a/tests/cli/test_gt_shaft_length_audit.py b/tests/cli/test_gt_shaft_length_audit.py new file mode 100644 index 0000000..5f3e887 --- /dev/null +++ b/tests/cli/test_gt_shaft_length_audit.py @@ -0,0 +1,21 @@ +"""The audit and crop share the actual GT disk-edge length definition.""" + +from ogo.cli.CheckFEModelBC import audit_femur_sideways +from ogo.fea.femur import DISTAL_FEMUR_NODE_SET, GREATER_TROCHANTER_NODE_SET + + +def test_shaft_length_uses_disk_edge_not_a_percentile_inside_the_disk(): + sets = { + GREATER_TROCHANTER_NODE_SET: { + "count": 100, "bounds": {"z_min": 40}, + "z_percentiles": {"p05": 44}, + }, + DISTAL_FEMUR_NODE_SET: { + "count": 100, "z_percentiles": {"p50": 20}, + "planarity": {"rms_mm": 0}, + }, + } + checks = audit_femur_sideways(sets, {}, 0.1) + length = next(check for check in checks if check["name"] == "post-GT-support shaft length is positive") + assert length["passed"] + assert "gt_z_min_minus_distal_z_median=20 mm" in length["detail"] diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index 856d771..508211c 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -13,7 +13,7 @@ null ], "cortical_label": 1, - "cut_mode": "post_icp_flat_ratio", + "cut_mode": "greater_trochanter_length", "distal_shaft_center_fraction": [ 0.5, 0.5, @@ -68,6 +68,9 @@ 0.8, 0.75 ], + "registration_backend": "numpy", + "registration_iterations": 50, + "registration_landmarks": 40000, "right_femur_code": 2, "rough_pre_icp_length_mm": 120.0, "shaft_length_mm": 120.0, @@ -149,6 +152,7 @@ "registration_landmarks": 250, "registration_max_scale": "1.2,1.2,1.3", "registration_min_scale": "0.8,0.8,0.75", + "spine_icp_target": "body", "superior_contact_center_fraction": [ 0.5, 0.5, diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 11ff021..c9cb23b 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -4,6 +4,40 @@ from ogo.fea.image_io import write_vtk_image_with_sitk_geometry +@pytest.mark.parametrize("options", [ + ["--femur_cut_mode", "post_icp_oblique_ratio"], + ["--femur_bbox_ratio", "1", "1.2", "none"], + ["--femur_lesser_trochanter_distal_offset", "20"], +]) +def test_builder_rejects_obsolete_crop_methods(monkeypatch, options): + monkeypatch.setattr("sys.argv", ["hip", "density.nii.gz", "mask.nii.gz"] + options) + with pytest.raises(SystemExit) as error: + femur.main() + assert error.value.code == 2 + + +def test_builder_has_one_gt_disk_relative_recipe(monkeypatch): + captured = [] + monkeypatch.setattr("sys.argv", ["hip", "density.nii.gz", "mask.nii.gz", + "--femur_greater_trochanter_distal_length", "15"]) + monkeypatch.setattr(femur, "sidewaysFallFe", captured.append) + femur.main() + args = captured[0] + assert args.femur_cut_mode == "greater_trochanter_length" + assert args.femur_greater_trochanter_distal_length == 15 + assert args.femur_shaft_length == 120 + assert not hasattr(args, "femur_bbox_ratio") + + +def test_builder_defaults_match_current_short_shaft_protocol(monkeypatch): + captured = [] + monkeypatch.setattr("sys.argv", ["hip", "density.nii.gz", "mask.nii.gz"]) + monkeypatch.setattr(femur, "sidewaysFallFe", captured.append) + femur.main() + assert captured[0].femur_greater_trochanter_distal_length == 10 + assert femur.DEFAULT_FEMUR_PISTOIA_CRITICAL_VOLUME_PERCENT == 11.2 + + def _vtk_image_from_array(data, *, origin=(0, 0, 0), spacing=(1, 1, 1)): vtk = pytest.importorskip("vtk") from vtk.util.numpy_support import numpy_to_vtk @@ -982,7 +1016,7 @@ def test_lesser_trochanter_cut_uses_distal_area_peak(): assert meta["cut_z"] == pytest.approx(61) -def test_greater_trochanter_length_crop_uses_gt_support_distal_origin(): +def test_greater_trochanter_length_crop_uses_gt_support_distal_edge_origin(): np = pytest.importorskip("numpy") vtk = pytest.importorskip("vtk") from ogo.util.vtk_image import vtk_image_to_numpy @@ -991,12 +1025,12 @@ def test_greater_trochanter_length_crop_uses_gt_support_distal_origin(): data = np.zeros((60, 80, 130), dtype=np.uint8) x = np.arange(data.shape[0])[:, None] y = np.arange(data.shape[1])[None, :] - for z in range(5, 126): + for z in range(10, 126): radius = 10 y_center = 35 - if 98 <= z <= 104: + if 58 <= z <= 64: radius = 19 - if 113 <= z <= 119: + if 73 <= z <= 79: y_center = 50 radius = 12 section = ((x - 30) ** 2 + (y - y_center) ** 2) <= radius**2 @@ -1011,24 +1045,20 @@ def test_greater_trochanter_length_crop_uses_gt_support_distal_origin(): cropped, crop_face, meta = femur.crop_vtk_images_to_greater_trochanter_length( [image], image, + gt_support_vtk=_vtk_image_from_array(data * ((np.arange(130) >= 74) & (np.arange(130) <= 79))[None, None, :]), retained_length_mm=10.0, + distal_boundary={"required_flat_face_z_mm": 9.5}, labels={1}, ) cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 assert meta["method"] == "greater_trochanter_length" - assert meta["greater_trochanter_z"] == pytest.approx(116) - assert meta["greater_trochanter_disk_distal_z"] == pytest.approx(113) - assert meta["greater_trochanter_support_distal_z"] == pytest.approx(45) - assert meta["greater_trochanter_support_length_mm"] == pytest.approx(80) - assert meta["gt_inclusion_length_mm"] == pytest.approx(0) - assert meta["distal_length_origin_z"] == pytest.approx(45) - assert meta["cut_z_mm"] == pytest.approx(35) - assert meta["available_below_gt_disk_mm"] == pytest.approx(108) - assert meta["available_below_gt_support_mm"] == pytest.approx(40) - assert meta["available_below_gt_mm"] == pytest.approx(111) + assert meta["greater_trochanter_support_distal_edge_z"] == pytest.approx(73.5) + assert meta["distal_length_origin_z"] == pytest.approx(73.5) + assert meta["cut_z_mm"] == pytest.approx(63.5) + assert meta["available_below_gt_support_distal_edge_mm"] == pytest.approx(64) assert meta["retained_length_mm"] == pytest.approx(10) - assert cropped[0].GetOrigin()[2] == pytest.approx(35) + assert cropped[0].GetOrigin()[2] == pytest.approx(64) assert np.argwhere(cropped_mask)[:, 2].min() == 0 assert vtk_image_to_numpy(crop_face).sum() > 0 @@ -1038,15 +1068,15 @@ def test_greater_trochanter_length_crop_rejects_short_femur(): vtk = pytest.importorskip("vtk") from vtk.util.numpy_support import numpy_to_vtk - data = np.zeros((60, 80, 130), dtype=np.uint8) + data = np.zeros((60, 80, 90), dtype=np.uint8) x = np.arange(data.shape[0])[:, None] y = np.arange(data.shape[1])[None, :] - for z in range(5, 126): + for z in range(10, 86): radius = 10 y_center = 35 - if 98 <= z <= 104: + if 58 <= z <= 64: radius = 19 - if 113 <= z <= 119: + if 73 <= z <= 79: y_center = 50 radius = 12 section = ((x - 30) ** 2 + (y - y_center) ** 2) <= radius**2 @@ -1058,11 +1088,13 @@ def test_greater_trochanter_length_crop_rejects_short_femur(): image.SetSpacing(1, 1, 1) image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) - with pytest.raises(ValueError, match="too short for the requested post-GT-support"): + with pytest.raises(ValueError, match="too short for the requested greater-trochanter"): femur.crop_vtk_images_to_greater_trochanter_length( [image], image, - retained_length_mm=50.0, + gt_support_vtk=_vtk_image_from_array(data * ((np.arange(90) >= 74) & (np.arange(90) <= 79))[None, None, :]), + retained_length_mm=80.0, + distal_boundary={"required_flat_face_z_mm": 9.5}, labels={1}, ) diff --git a/tests/fea/test_gt_disk_crop.py b/tests/fea/test_gt_disk_crop.py new file mode 100644 index 0000000..a550a79 --- /dev/null +++ b/tests/fea/test_gt_disk_crop.py @@ -0,0 +1,117 @@ +"""Shaft length is measured from the generated GT disk, not its fixture box.""" + +import numpy as np +import pytest + +from ogo.fea.femur import crop_vtk_images_to_greater_trochanter_length +from ogo.util.vtk_image import vtk_image_to_numpy + + +def vtk_image(data, *, origin=(0, 0, 0), spacing=(1, 1, 1)): + import vtk + from vtk.util.numpy_support import numpy_to_vtk + + image = vtk.vtkImageData() + image.SetDimensions(data.shape) + image.SetOrigin(*origin) + image.SetSpacing(*spacing) + image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) + return image + + +def images(distal_index=0, spacing=1.0): + bone = np.zeros((12, 12, 70), dtype=np.uint16) + bone[4:8, 4:8, distal_index:65] = 1 + disk = np.zeros_like(bone) + disk[3:9, 0:4, 40:46] = 5000 + geometry = {"origin": (2.0, 3.0, 7.0), "spacing": (spacing,) * 3} + return ( + vtk_image(np.maximum(bone, disk), **geometry), + vtk_image(disk, **geometry), + ) + + +def flat_boundary(image): + """Known complete flat faces for synthetic crop-only tests.""" + data = vtk_image_to_numpy(image) + index = int(np.argwhere(data != 0)[:, 2].min()) + z = image.GetOrigin()[2] + (image.GetExtent()[4] + index - .5) * image.GetSpacing()[2] + return {"required_flat_face_z_mm": z} + + +@pytest.mark.parametrize("spacing", [0.5, 1.0, 2.0]) +def test_length_uses_generated_disk_and_voxel_faces(spacing): + material, disk = images(spacing=spacing) + cropped, face, meta = crop_vtk_images_to_greater_trochanter_length( + [material, disk], material, gt_support_vtk=disk, retained_length_mm=20, + distal_boundary=flat_boundary(material), + ) + expected_gt_edge = 7.0 + (40 - 0.5) * spacing + assert meta["greater_trochanter_support_distal_edge_z"] == pytest.approx(expected_gt_edge) + assert meta["cut_z_mm"] == pytest.approx(expected_gt_edge - 20) + face_data = vtk_image_to_numpy(face) + assert np.count_nonzero(face_data) == 16 + assert np.all(np.argwhere(face_data)[:, 2] == 0) + assert cropped[0].GetOrigin()[2] - spacing / 2 == pytest.approx(expected_gt_edge - 20) + assert np.count_nonzero(vtk_image_to_numpy(cropped[1])) == 6 * 4 * 6 + + +def test_exactly_twenty_mm_coverage_still_has_a_distal_face(): + material, disk = images(distal_index=20) + _, face, meta = crop_vtk_images_to_greater_trochanter_length( + [material, disk], material, gt_support_vtk=disk, retained_length_mm=20, + distal_boundary=flat_boundary(material), + ) + assert meta["available_below_gt_support_distal_edge_mm"] == pytest.approx(20) + assert np.count_nonzero(vtk_image_to_numpy(face)) == 16 + + +def test_insufficient_coverage_is_not_solved_as_a_shorter_model(): + material, disk = images(distal_index=21) + with pytest.raises(ValueError, match="too short"): + crop_vtk_images_to_greater_trochanter_length( + [material, disk], material, gt_support_vtk=disk, retained_length_mm=20, + distal_boundary=flat_boundary(material), + ) + + +def test_empty_gt_support_cannot_define_length(): + material, disk = images() + disk = vtk_image(np.zeros((12, 12, 70), dtype=np.uint16), origin=(2, 3, 7)) + with pytest.raises(ValueError, match="empty GT"): + crop_vtk_images_to_greater_trochanter_length( + [material, disk], material, gt_support_vtk=disk, retained_length_mm=20, + distal_boundary=flat_boundary(material), + ) + + +def test_contact_canvas_extent_preserves_absolute_model_coordinates(): + material, disk = images() + for image in (material, disk): + image.SetExtent(10, 21, 20, 31, 30, 99) + cropped, _, meta = crop_vtk_images_to_greater_trochanter_length( + [material, disk], material, gt_support_vtk=disk, retained_length_mm=20, + distal_boundary=flat_boundary(material), + ) + assert meta["cut_z_mm"] == pytest.approx(7 + 30 + 40 - 0.5 - 20) + assert cropped[0].GetOrigin()[2] - 0.5 == pytest.approx(meta["cut_z_mm"]) + + +def test_partial_distal_face_is_rejected_even_with_twenty_mm_raw_span(): + material, disk = images() + with pytest.raises(ValueError, match="too short"): + crop_vtk_images_to_greater_trochanter_length( + [material, disk], material, gt_support_vtk=disk, retained_length_mm=20, + distal_boundary={"required_flat_face_z_mm": 30.5}, + ) + + +def test_eligible_crop_preserves_disks_and_clears_original_end(): + material, disk = images() + _, _, meta = crop_vtk_images_to_greater_trochanter_length( + [material, disk], material, gt_support_vtk=disk, retained_length_mm=20, + distal_boundary={"required_flat_face_z_mm": 19.2}, + ) + assert meta["retained_length_mm"] == 20 + assert meta["available_shaft_length_mm"] == 27 + assert meta["cut_z_mm"] >= meta["safe_distal_face_z_mm"] diff --git a/tests/fea/test_hip_registration.py b/tests/fea/test_hip_registration.py new file mode 100644 index 0000000..2658e01 --- /dev/null +++ b/tests/fea/test_hip_registration.py @@ -0,0 +1,58 @@ +"""Hip registration must cover both ends and retain superior orientation.""" + +import numpy as np +import pytest + +from ogo.fea import alignment + + +def test_public_hip_defaults_are_the_locked_ten_mm_recipe(): + from ogo.fea import femur + + assert femur.DEFAULT_FEMUR_CUT_MODE == "greater_trochanter_length" + assert femur.DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM == 10 + assert femur.DEFAULT_FEMUR_PISTOIA_CRITICAL_VOLUME_PERCENT == 11.2 + assert femur.DEFAULT_FEMUR_REGISTRATION_LANDMARKS == 40000 + + +def test_stride_sampling_does_not_truncate_the_proximal_end(): + points = np.column_stack([np.zeros(15106), np.zeros(15106), np.arange(15106)]) + sampled = alignment.sample_points(points, max_points=8000, mode="stride") + assert sampled[:, 2].max() >= 15104 + assert sampled[:, 2].min() <= 1 + assert len(sampled) <= 8000 + + +def test_multistart_recovers_axial_rotation_without_scaling_native_geometry(): + from ogo.fea.hip_registration import estimate_femur_icp + + rng = np.random.default_rng(42) + points = rng.normal(size=(800, 3)) * [5, 12, 30] + points[:200] += [15, 20, 35] + rotation = np.diag([-1.0, -1.0, 1.0]) + fixed = points @ rotation.T + [9, 6, 2] + fit = estimate_femur_icp(moving_points=points, fixed_points=fixed, iterations=50) + assert fit["symmetric_distance_mm"] < 1e-8 + assert np.linalg.det(fit["rotation"]) == pytest.approx(1) + assert fit["rotation"][2, 2] > 0 + assert len(fit["candidates"]) == 4 + again = estimate_femur_icp(moving_points=points, fixed_points=fixed, iterations=50) + np.testing.assert_allclose(fit["rotation"], again["rotation"]) + + +def test_superior_inversion_cannot_win_candidate_selection(monkeypatch): + from ogo.fea import hip_registration + + calls = iter([0, 1, 2, 3]) + + def fit(**kwargs): + number = next(calls) + rotation = np.diag([1., -1., -1.]) if number == 0 else np.eye(3) + return {"rotation": rotation, "translation": np.zeros(3), + "iterations": 1, "mean_distance": 0.0} + + monkeypatch.setattr(hip_registration, "estimate_rigid_icp", fit) + points = np.array([[0., 0., 0.], [1., 0., 1.], [0., 1., 2.]]) + result = hip_registration.estimate_femur_icp(moving_points=points, fixed_points=points) + assert not result["candidates"][0]["orientation_valid"] + assert result["selected_start_deg"] != 0 diff --git a/tests/fea/test_legacy_evidence.py b/tests/fea/test_legacy_evidence.py new file mode 100644 index 0000000..fc3d9f0 --- /dev/null +++ b/tests/fea/test_legacy_evidence.py @@ -0,0 +1,97 @@ +import csv +import json + +import numpy as np +import pytest + +from tests.fea.test_model_export import hip_model + + +def test_evidence_cache_retries_only_when_inputs_change(tmp_path): + from ogo.fea.legacy_qc import evidence_signature, needs_backfill + model = tmp_path / 'case.n88model' + model.touch() + row = {'measurement_source': 'legacy_backfill'} + assert needs_backfill(model, row) + row['legacy_evidence_signature'] = evidence_signature(model) + assert not needs_backfill(model, row) + model.with_name('case_shaft_geometry.json').write_text('{}') + assert needs_backfill(model, row) + assert not needs_backfill(model, {'measurement_source': 'generation'}) + + +def test_evidence_manifest_resolves_relative_paths_and_rejects_duplicates(tmp_path): + from ogo.fea.legacy_qc import load_evidence + path = tmp_path / 'sources.csv' + path.write_text('site,model_id,body_mask\nspine,case,body.nii.gz\n') + assert load_evidence(path)[('spine', 'case')]['body_mask'] == str(tmp_path / 'body.nii.gz') + path.write_text(path.read_text() + 'spine,case,other.nii.gz\n') + with pytest.raises(ValueError, match='Duplicate'): + load_evidence(path) + + +def test_registered_mask_geometry_and_process_recovery(tmp_path): + sitk = pytest.importorskip('SimpleITK') + from ogo.fea.legacy_qc import registered_anatomy + mask = sitk.GetImageFromArray(np.ones((2, 2, 2), dtype=np.uint8)) + body = tmp_path / 'body.nii.gz' + sitk.WriteImage(mask, str(body)) + grid = np.ones((3, 2, 2), dtype=np.int32) + images, provenance = registered_anatomy(grid, np.ones(3), np.zeros(3), body) + from ogo.util.vtk_image import vtk_image_to_numpy + assert vtk_image_to_numpy(images[0]).sum() == 8 + assert vtk_image_to_numpy(images[1]).sum() == 4 + assert provenance['process_mask_source'] == 'model_bone_minus_registered_body' + mask.SetOrigin((100, 0, 0)) + sitk.WriteImage(mask, str(body)) + with pytest.raises(ValueError, match='overlap'): + registered_anatomy(grid, np.ones(3), np.zeros(3), body) + + +def test_registered_mask_must_match_voxel_lattice(tmp_path): + sitk = pytest.importorskip('SimpleITK') + from ogo.fea.legacy_qc import registered_anatomy + mask = sitk.GetImageFromArray(np.ones((2, 2, 2), dtype=np.uint8)) + mask.SetOrigin((0.3, 0, 0)) + path = tmp_path / 'body.nii.gz' + sitk.WriteImage(mask, str(path)) + with pytest.raises(ValueError, match='lattice'): + registered_anatomy(np.ones((3, 2, 2), dtype=int), np.ones(3), np.zeros(3), path) + + +def test_zero_anatomy_offset_is_valid_evidence(): + from ogo.fea.validation import evaluate + row = evaluate({'site': 'spine', 'measurement_source': 'legacy_backfill', + 'process_body_axial_offset_mm': 0, + 'process_body_transverse_offset_mm': 10}) + assert 'legacy_body_process_metrics_missing' not in row['qc_reasons'] + + +def test_hip_distal_set_is_recovered_from_saved_plane_not_union(tmp_path, monkeypatch, hip_model): + import vtkbone + from ogo.fea.legacy_qc import backfill_measurements + model = hip_model + model.ApplyBoundaryCondition('Femoral_Head_PMMA_Nodes', 1, -.1, 'top_displacement') + model.ApplyBoundaryCondition('Greater_Trochanter_PMMA_Nodes', 1, 0, 'bottom_fixed_y_PMMA') + model.ApplyBoundaryCondition('Greater_Trochanter_PMMA_Nodes', 2, 0, 'bottom_fixed_z') + for sense, name in ((0, 'bottom_fixed_x'), (2, 'bottom_fixed_z')): + model.ApplyBoundaryCondition('Distal_Femur_Nodes', sense, 0, name) + model.GetNodeSets().RemoveItem(model.GetNodeSet('Distal_Femur_Nodes')) + class Reader: + def SetFileName(self, name): + pass + def Update(self): + pass + def GetOutput(self): + return model + monkeypatch.setattr(vtkbone, 'vtkboneN88ModelReader', Reader) + path = tmp_path / 'hip.n88model' + path.touch() + path.with_name('hip_shaft_geometry.json').write_text(json.dumps({'cut_z_mm': model.GetBounds()[4]})) + row = next(csv.DictReader(backfill_measurements(path, site='hip').open())) + assert 'Distal_Femur_Nodes' in row['recovered_boundary_sets'] + assert float(row['distal_boundary_span_mm']) == 0 + assert float(row['distal_boundary_coverage_fraction']) == 1 + assert float(row['distal_support_patch_coverage_fraction']) == 1 + assert float(row['distal_support_patch_width_fraction']) == 0.9 + assert row['shaft_bc_node_count'] == row['shaft_bc_fixed_z_node_count'] diff --git a/tests/fea/test_model_export.py b/tests/fea/test_model_export.py new file mode 100644 index 0000000..67ee111 --- /dev/null +++ b/tests/fea/test_model_export.py @@ -0,0 +1,73 @@ +"""Surface exports preserve model geometry and distinguish actual supports.""" + +import numpy as np +import pytest +import vtk +import vtkbone +from vtk.util.numpy_support import numpy_to_vtkIdTypeArray, vtk_to_numpy + +from tests.fea.test_gt_disk_crop import vtk_image + + +@pytest.fixture +def hip_model(): + import ogo.util.Helper as helper + + data = np.zeros((4, 8, 12), dtype=np.uint16) + data[1:3, 2:6, 1:11] = 1 + data[1:3, 1:2, 7:10] = 5000 + data[1:3, 6:7, 8:11] = 5000 + grid = helper.Image2Mesh(vtk_image(data)) + model = vtkbone.vtkboneFiniteElementModel() + model.ShallowCopy(grid) + xyz = vtk_to_numpy(model.GetPoints().GetData()) + for name, active in (("Femoral_Head_PMMA_Nodes", xyz[:, 1] == xyz[:, 1].max()), + ("Greater_Trochanter_PMMA_Nodes", xyz[:, 1] == xyz[:, 1].min()), + ("Distal_Femur_Nodes", xyz[:, 2] == xyz[:, 2].min())): + ids = numpy_to_vtkIdTypeArray(np.flatnonzero(active), deep=True) + ids.SetName(name) + model.AddNodeSet(ids) + return model + + +def test_hip_exports_are_colored_nonblank_and_in_physical_coordinates(tmp_path, hip_model): + from ogo.fea.model_export import export_femur_model + + model = hip_model + outputs = export_femur_model(model, tmp_path / "test.n88model", pmma_mat_id=5000) + reader = vtk.vtkXMLPolyDataReader() + reader.SetFileName(outputs["surface_vtp"]) + reader.Update() + surface = reader.GetOutput() + assert surface.GetNumberOfPolys() > 0 + assert set(vtk_to_numpy(surface.GetCellData().GetArray("ModelRegion"))) == {0, 1, 2} + assert surface.GetBounds() == pytest.approx(model.GetBounds()) + png = vtk.vtkPNGReader() + png.SetFileName(outputs["qc_3d_png"]) + png.Update() + pixels = vtk_to_numpy(png.GetOutput().GetPointData().GetScalars()) + assert (pixels.min(axis=1) < 200).sum() > 1000 + assert ((pixels[:, 2] > pixels[:, 0].astype(float) * 1.25) + & (pixels[:, 2] > pixels[:, 1])).sum() > 1000 + assert (pixels[:, 0].astype(float) > pixels[:, 1].astype(float) * 1.5).sum() > 100 + assert png.GetOutput().GetDimensions()[:2] == (1600, 1000) + + +def test_missing_display_preserves_surface_and_records_missing_png(tmp_path, hip_model, monkeypatch): + from pathlib import Path + from ogo.fea.model_export import export_femur_model + + class NoDisplayWindow: + def GetClassName(self): + return "vtkXOpenGLRenderWindow" + + def Finalize(self): + pass + + monkeypatch.delenv("DISPLAY", raising=False) + monkeypatch.setattr(vtk, "vtkRenderWindow", NoDisplayWindow) + with pytest.warns(RuntimeWarning, match="headless"): + outputs = export_femur_model(hip_model, tmp_path / "test.n88model", pmma_mat_id=5000) + assert Path(outputs["surface_vtp"]).is_file() + assert outputs["qc_3d_png"] is None + assert outputs["render_status"] == "unavailable" diff --git a/tests/fea/test_qc_render.py b/tests/fea/test_qc_render.py new file mode 100644 index 0000000..81a6db4 --- /dev/null +++ b/tests/fea/test_qc_render.py @@ -0,0 +1,94 @@ +"""Display smoothing must not alter solved values or blend supports into bone.""" + +import numpy as np +import pytest + + +def test_masked_sed_smoothing_excludes_disks_and_preserves_source(): + from ogo.fea.qc_render import smooth_bone_sed + + indices = np.indices((3, 3, 3)).reshape(3, -1).T + bone = indices[:, 0] < 2 + values = np.where(bone, 4.0, 1000.0) + original = values.copy() + display = smooth_bone_sed(values, indices, bone, np.ones(3)) + np.testing.assert_allclose(display, 4) + np.testing.assert_array_equal(values, original) + + +def test_sed_missing_or_invalid_is_rejected(): + from ogo.fea.qc_render import smooth_bone_sed + + with pytest.raises(ValueError, match="finite"): + smooth_bone_sed(np.array([np.nan]), np.zeros((1, 3), dtype=int), + np.ones(1, dtype=bool), np.ones(3)) + + +def test_qc_uses_existing_visualizer_extension(): + import inspect + from ogo.cli.Visualize import vis3d + + signature = inspect.signature(vis3d) + assert "renderer_setup" in signature.parameters + assert "label_palette" in signature.parameters + + +def test_ineligible_qc_is_anatomy_only(tmp_path): + from ogo.fea.qc_render import export_model_qc + + with pytest.raises(ValueError, match="Ineligible"): + export_model_qc(tmp_path / "short.n88model", "hip", sed=True, ineligible=True) + + +def test_render_failure_warns_without_interrupting_solver(monkeypatch): + from ogo.fea import qc_render + + def fail(*args, **kwargs): + raise RuntimeError("No graphics display") + + monkeypatch.setattr(qc_render, "export_model_qc", fail) + with pytest.warns(RuntimeWarning, match="No graphics display"): + assert qc_render.try_export_model_qc("model.n88model", "hip") is None + + +def test_too_short_branch_exports_before_exit(): + import inspect + from ogo.fea import femur + + source = inspect.getsource(femur) + branch = source.split('if shaft_crop.get("status") == "too_short":', 1)[1].split('sys.exit(1)', 1)[0] + assert 'model=available_model, ineligible=True' in branch + assert 'shaft_crop["exports"]' in branch + assert 'write_model(' not in branch + + +def test_export_keeps_only_panel_and_settings(tmp_path, monkeypatch): + import json + from PIL import Image + from ogo.fea import qc_render + + def fake_panel(model_path, temporary, site, body_mask, sed, model, ineligible): + assert ineligible + assert model is sentinel + image = temporary / "panel.png" + Image.new("RGB", (20, 60), "white").save(image) + for view in ("oblique", "top", "bottom"): + (temporary / (model_path.stem + "_preview_" + view + ".json")).write_text( + json.dumps({"eligible_for_solve": False, "view": view})) + return image + + class Window: + def GetClassName(self): + return "vtkEGLRenderWindow" + + def Finalize(self): + pass + + sentinel = object() + monkeypatch.setattr(qc_render.vtk, "vtkRenderWindow", Window) + monkeypatch.setattr(qc_render, "panel", fake_panel) + path = qc_render.export_model_qc(tmp_path / "short.n88model", "hip", + model=sentinel, ineligible=True) + assert path.endswith("short_qc_3d.png") + assert sorted(p.name for p in tmp_path.iterdir()) == ["short_qc_3d.json", "short_qc_3d.png"] + assert all(not v["eligible_for_solve"] for v in json.loads((tmp_path / "short_qc_3d.json").read_text())["views"]) diff --git a/tests/fea/test_review.py b/tests/fea/test_review.py new file mode 100644 index 0000000..e87d7f7 --- /dev/null +++ b/tests/fea/test_review.py @@ -0,0 +1,173 @@ +import csv +import json +from pathlib import Path +import shutil +import subprocess + +import pytest + +from ogo.fea.review import inclusion_row, load_decisions + + +def test_gallery_has_quick_decisions_on_each_card(tmp_path): + from ogo.fea.gallery import generate_gallery + output = tmp_path / 'gallery.html' + generate_gallery([{'site': 'hip', 'model_id': 'case', 'qc_status': 'fail', + 'qc_reasons': 'incomplete_distal_boundary'}], output) + content = output.read_text() + assert 'class="quick-decision" data-decision="include"' in content + assert 'class="quick-decision" data-decision="exclude"' in content + assert "manual_reason: 'manual_review'" in content + assert "window.alert('CSV export started." in content + assert 'does not update the project CSV' in content + + +def test_manual_decision_preserves_automatic_flags(): + row = inclusion_row({'model_id': 'case', 'site': 'hip', 'qc_status': 'fail', + 'qc_reasons': 'too_short', 'generation_status': 'too_short'}, + {'manual_decision': 'include', 'reviewer': 'MW'}) + assert row['final_inclusion'] == 'include' + assert row['automatic_qc_status'] == 'fail' + assert row['solver_eligible'] == 'False' + assert row['manual_reason'] == 'manual_review' + + +def test_review_is_pending_and_auto_reset_has_no_manual_reason(): + row = inclusion_row({'model_id': 'case', 'site': 'spine', 'qc_status': 'review'}, {}) + assert row['final_inclusion'] == 'pending' + assert row['manual_reason'] == '' + + +def test_import_requires_unambiguous_identity(tmp_path): + path = tmp_path / 'decisions.csv' + with path.open('w', newline='') as stream: + writer = csv.DictWriter(stream, fieldnames=['model_id', 'site', 'manual_decision']) + writer.writeheader() + writer.writerows([{'model_id': 'case', 'site': 'hip', 'manual_decision': 'include'}] * 2) + with pytest.raises(ValueError, match='Duplicate'): + load_decisions(path) + + +def test_command_backfills_only_missing_measurements(tmp_path, monkeypatch): + from ogo.cli import ValidateFEA + from ogo.fea.validation import write_measurements + write_measurements(tmp_path / 'existing.n88model', {'site': 'hip'}) + (tmp_path / 'existing.n88model').touch() + (tmp_path / 'legacy.n88model').touch() + calls = [] + def backfill(path, site=None, evidence=None): + from ogo.fea.legacy_qc import evidence_signature + calls.append(path.name) + return write_measurements(path, {'site': 'spine', 'measurement_source': 'legacy_backfill', + 'legacy_evidence_signature': evidence_signature(path, evidence)}) + monkeypatch.setattr(ValidateFEA, 'backfill_measurements', backfill) + ValidateFEA.main([str(tmp_path), '--output', str(tmp_path / 'audit'), '--gallery']) + assert calls == ['legacy.n88model'] + assert (tmp_path / 'audit' / 'study_inclusion.csv').exists() + calls.clear() + ValidateFEA.main([str(tmp_path), '--output', str(tmp_path / 'audit')]) + assert calls == [] + + +def test_javascript_csv_roundtrip_and_manual_provenance(): + node = shutil.which('node') + if not node: + pytest.skip('Node is required for browser-independent JavaScript tests') + from ogo.fea import gallery + script = Path(gallery.__file__).with_name('gallery_review.js') + code = '''const assert = require('node:assert/strict'); +const {inclusionRecord, encodeCSV, parseCSV, validateReviews, imageVisible} = require(SCRIPT); +assert.equal(imageVisible({stage:'anatomy',view:'top'},'anatomy','top'),true); +assert.equal(imageVisible({stage:'anatomy',view:'bottom'},'anatomy','top'),false); +assert.equal(imageVisible({stage:'sed',view:'top'},'anatomy','top'),false); +assert.equal(imageVisible({stage:'sed',view:'all'},'sed','all'),true); +assert.equal(imageVisible({stage:'sed',view:'all',fallback:'true'},'sed','top'),true); +const row = {site:'hip', model_id:'test', qc_status:'fail', qc_reasons:'too_short', generation_status:'too_short'}; +const decision = inclusionRecord(row, {manual_decision:'include', manual_note:'Quote "here",\nnew line'}); +assert.equal(decision.automatic_qc_status, 'fail'); +assert.equal(decision.solver_eligible, 'False'); +assert.equal(decision.manual_reason, 'manual_review'); +const records = parseCSV(encodeCSV([decision], Object.keys(decision))); +assert.equal(records[0].manual_note, decision.manual_note); +assert.equal(validateReviews(records,[row]).size,1); +assert.throws(()=>validateReviews([records[0],records[0]],[row]),/Duplicate/); +assert.throws(()=>validateReviews(records,[]),/Unknown/); +assert.equal(inclusionRecord(row,{manual_decision:'automatic'}).final_inclusion,'exclude'); +'''.replace('SCRIPT', json.dumps(str(script))) + # Keep a newline inside the JS string escaped, not an invalid literal. + code = code.replace('Quote "here",\nnew line', 'Quote "here",\\nnew line') + subprocess.run([node, '-e', code], check=True, capture_output=True, text=True) + + +def test_gallery_does_not_interpret_template_words_in_subject(tmp_path): + from ogo.fea.validation import evaluate, generate_gallery + row = evaluate({'site': 'hip', 'model_id': 'DATA_SCRIPT_CARDS', 'qc_reasons': ''}) + path = generate_gallery([row], tmp_path / 'review.html') + assert 'DATA_SCRIPT_CARDS' in path.read_text() + + +def test_legacy_anatomy_is_not_silently_certified(): + from ogo.fea.validation import evaluate + row = evaluate({'site': 'spine', 'measurement_source': 'legacy_backfill'}) + assert 'legacy_body_process_metrics_missing' in row['qc_reasons'] + + +def test_gallery_individual_views_keep_panel_order_and_cache(tmp_path): + from PIL import Image + from ogo.fea.gallery import generate_gallery + image = tmp_path / 'case_qc_3d.png' + panel = Image.new('RGB', (1400, 2550)) + for index, color in enumerate(('red', 'green', 'blue')): + panel.paste(color, (0, index * 850, 1400, (index + 1) * 850)) + panel.save(image) + row = {'site': 'hip', 'model_id': 'case', 'qc_status': 'review', + 'qc_reasons': '', 'anatomy_image': str(image)} + path = generate_gallery([row], tmp_path / 'gallery.html') + content = path.read_text() + assert 'id="camera"' in content + assert 'id="review-camera"' in content + assert 'id="review-stage"' in content + for view, color in zip(('oblique', 'top', 'bottom'), ((255, 0, 0), (0, 128, 0), (0, 0, 255))): + assert f'data-view="{view}"' in content + asset = next((tmp_path / 'gallery_views').glob(f'*_{view}.png')) + with Image.open(asset) as cropped: + assert cropped.size == (1400, 850) + assert cropped.getpixel((700, 425)) == color + modified = asset.stat().st_mtime_ns + generate_gallery([row], path) + assert asset.stat().st_mtime_ns == modified + + +def test_gallery_unknown_montage_is_not_split(tmp_path): + from PIL import Image + from ogo.fea.gallery import generate_gallery + image = tmp_path / 'legacy.png' + Image.new('RGB', (400, 400), 'white').save(image) + row = {'site': 'spine', 'model_id': 'legacy', 'qc_status': 'review', + 'qc_reasons': '', 'anatomy_image': str(image)} + content = generate_gallery([row], tmp_path / 'gallery.html').read_text() + assert 'data-fallback="true"' in content + assert not (tmp_path / 'gallery_views').exists() + + +def test_gallery_zip_is_portable_and_has_review_records(tmp_path): + import zipfile + from PIL import Image + from ogo.fea.gallery import export_gallery_zip + image = tmp_path / 'case_qc_3d.png' + Image.new('RGB', (1400, 2550), 'red').save(image) + row = {'site': 'hip', 'model_id': 'case', 'qc_status': 'review', + 'qc_reasons': '', 'anatomy_image': str(image), 'sed_image': str(image)} + path = export_gallery_zip([row], tmp_path / 'gallery.zip') + with zipfile.ZipFile(path) as archive: + assert {'gallery.html', 'qc_summary.csv', 'study_inclusion.csv'} <= set(archive.namelist()) + assert len([name for name in archive.namelist() if name.startswith('images/')]) == 1 + assert len([name for name in archive.namelist() if name.startswith('gallery_views/')]) == 3 + content = archive.read('gallery.html').decode() + assert 'src="images/' in content + assert str(tmp_path) not in content + assert 'data:image' not in content + archive.extractall(tmp_path / 'unpacked') + import re + for source in re.findall(r'src="([^"]+)"', content): + assert (tmp_path / 'unpacked' / source).is_file() diff --git a/tests/fea/test_shaft_geometry.py b/tests/fea/test_shaft_geometry.py new file mode 100644 index 0000000..37e0d12 --- /dev/null +++ b/tests/fea/test_shaft_geometry.py @@ -0,0 +1,118 @@ +"""The sidecar and saved BC nodes use the same generated-disk origin.""" + +from types import SimpleNamespace + +import numpy as np +import pytest +import vtk +from vtk.util.numpy_support import numpy_to_vtk, numpy_to_vtkIdTypeArray + +from tests.fea.test_gt_disk_crop import images, vtk_image + + +def test_short_scan_measurement_is_available_without_cropping(): + from ogo.fea.shaft_geometry import measure_available_shaft + + material, disk = images(distal_index=25) + result = measure_available_shaft(material, disk, requested_length_mm=20, + distal_boundary={"required_flat_face_z_mm": 31.5}) + assert result["available_shaft_length_mm"] == 15 + assert not result["eligible_for_requested_length"] + assert result["status"] == "too_short" + assert result["measured_shaft_length_mm"] is None + + +def model_with_nodes(distal_z): + xyz = np.array([[0, 0, 40.], [1, 0, 40.], [0, 0, distal_z], [1, 0, distal_z]]) + points = vtk.vtkPoints() + points.SetData(numpy_to_vtk(xyz)) + sets = {"Greater_Trochanter_PMMA_Nodes": numpy_to_vtkIdTypeArray(np.array([0, 1])), + "Distal_Femur_Nodes": numpy_to_vtkIdTypeArray(np.array([2, 3]))} + return SimpleNamespace(GetPoints=lambda: points, GetNodeSet=lambda name: sets[name]) + + +def test_final_measurement_uses_gt_minimum_and_distal_median(): + from ogo.fea.shaft_geometry import verify_model_shaft + + meta = {"requested_shaft_length_mm": 20., "retained_length_mm": 20., + "distal_length_origin_z": 40., "cut_z_mm": 20., + "safe_distal_face_z_mm": 19.} + verified = verify_model_shaft(model_with_nodes(20), meta) + assert verified["measured_shaft_length_mm"] == 20 + assert verified["status"] == "verified" + with pytest.raises(ValueError, match="shaft"): + verify_model_shaft(model_with_nodes(19), meta) + + +def test_oblique_end_reduces_usable_coverage_not_the_requested_length(): + from ogo.fea.shaft_geometry import measure_available_shaft + + material, disk = images() + result = measure_available_shaft( + material, disk, requested_length_mm=20, + distal_boundary={"required_flat_face_z_mm": 28.2}, + ) + assert result["raw_available_shaft_length_mm"] == 40 + assert result["safe_distal_face_z_mm"] == pytest.approx(28.5) + assert result["available_shaft_length_mm"] == 18 + assert not result["eligible_for_requested_length"] + assert result["requested_shaft_length_mm"] == 20 + assert result["status"] == "too_short" + + +def test_source_boundary_is_required_for_coverage(): + from ogo.fea.shaft_geometry import measure_available_shaft + + material, disk = images() + with pytest.raises(ValueError, match="distal.*boundary"): + measure_available_shaft(material, disk, requested_length_mm=20) + + +def test_capture_uses_full_native_voxel_faces_and_nonzero_extent(): + from ogo.fea.shaft_geometry import capture_distal_scan_face + + data = np.zeros((5, 6, 8), dtype=np.uint8) + data[1:3, 2:4, 2:] = 1 + data[4, 5, 0] = 2 # Another label must not define this femur's end. + image = vtk_image(data, origin=(10, 20, 30), spacing=(.5, .75, 2)) + image.SetExtent(5, 9, 10, 15, 20, 27) + face = capture_distal_scan_face(image, labels={1}) + points = face["points_xyz"] + assert points[:, 0].min() == pytest.approx(12.75) + assert points[:, 0].max() == pytest.approx(13.75) + assert points[:, 1].min() == pytest.approx(28.625) + assert points[:, 1].max() == pytest.approx(30.125) + assert np.all(points[:, 2] == 73) + assert face["native_distal_slice_index"] == 22 + assert face["native_face_voxels"] == 4 + + +def test_boundary_uses_inverse_icp_and_resampling_clearance(): + from ogo.fea.shaft_geometry import aligned_distal_scan_boundary + + angle = np.deg2rad(30) + matrix = np.eye(4) + matrix[:3, :3] = [[1, 0, 0], [0, np.cos(angle), -np.sin(angle)], + [0, np.sin(angle), np.cos(angle)]] + matrix[:3, 3] = [10, 20, 30] + points = np.array([[0, 0, 5], [0, 8, 5], [4, 8, 5], [4, 0, 5]], float) + boundary = aligned_distal_scan_boundary( + {"points_xyz": points, "native_face_voxels": 1}, matrix, + resampling_spacing=(1, 1, 1), output_spacing=(1, 1, 1), + ) + inverse = np.linalg.inv(matrix) + transformed = points @ inverse[:3, :3].T + inverse[:3, 3] + clearance = .5 * np.abs(inverse[2, :3]).sum() + .5 + assert boundary["aligned_face_z_min_mm"] == pytest.approx(transformed[:, 2].min()) + assert boundary["aligned_face_z_max_mm"] == pytest.approx(transformed[:, 2].max()) + assert boundary["resampling_clearance_mm"] == pytest.approx(clearance) + assert boundary["required_flat_face_z_mm"] == pytest.approx(transformed[:, 2].max() + clearance) + + +def test_planar_bc_nodes_alone_do_not_verify_complete_section(): + from ogo.fea.shaft_geometry import verify_model_shaft + + meta = {"retained_length_mm": 20., "distal_length_origin_z": 40., + "cut_z_mm": 20., "safe_distal_face_z_mm": 21.} + with pytest.raises(ValueError, match="original.*boundary"): + verify_model_shaft(model_with_nodes(20), meta) diff --git a/tests/fea/test_sideways_fall.py b/tests/fea/test_sideways_fall.py index 8d6c729..eb009b9 100644 --- a/tests/fea/test_sideways_fall.py +++ b/tests/fea/test_sideways_fall.py @@ -65,7 +65,7 @@ def test_sideways_fall_icp_path_uses_deterministic_point_cloud_transform(): if isinstance(node, ast.Attribute) and node.attr == "iterativeClosestPoint" ] - assert "estimate_rigid_icp" in called_names + assert "estimate_femur_icp" in called_names assert helper_calls == [] diff --git a/tests/fea/test_spine.py b/tests/fea/test_spine.py index 9a2366d..dcd2fdf 100644 --- a/tests/fea/test_spine.py +++ b/tests/fea/test_spine.py @@ -5,6 +5,12 @@ def test_default_spine_cap_geometry_matches_maintained_settings(): assert spine.DEFAULT_SPINE_PMMA_THICKNESS_MM == 10 assert spine.DEFAULT_SPINE_PMMA_INTRUSION_MM == 6 assert spine.DEFAULT_SPINE_REGISTRATION_BACKEND == "vtk" + assert spine.DEFAULT_SPINE_ICP_TARGET == "body" + + +def test_spine_reference_path_depends_on_icp_target(): + assert spine.default_spine_reference_path("body").name == "L4_BODY_SPINE_COMPRESSION_REF.vtk" + assert spine.default_spine_reference_path("vertebra").name == "L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk" def test_benchmark_presets_match_spinefe_notebook_settings(): diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py index 786d05b..6e65758 100644 --- a/tests/fea/test_spinefe_benchmark.py +++ b/tests/fea/test_spinefe_benchmark.py @@ -272,7 +272,8 @@ def test_spine_registration_uses_preprocessed_isotropic_body(): assert "SPINE_PREPROCESSING_CROP_MARGIN_MM" in source assert "resample_vtk_image_to_spacing" in source assert "registration_body = threshold(" in source - assert "get_icp_with_scaling(\n registration_body," in source + assert "registration_target = registration_body" in source + assert "get_icp_with_scaling(\n registration_target," in source assert source.index("resample_vtk_image_to_spacing") < source.index( "registration_body = threshold(" ) diff --git a/tests/fea/test_validation.py b/tests/fea/test_validation.py new file mode 100644 index 0000000..1563e84 --- /dev/null +++ b/tests/fea/test_validation.py @@ -0,0 +1,92 @@ +import csv + +import numpy as np + +from ogo.fea.validation import evaluate, grid_measurements, write_measurements, generate_gallery + + +def test_disk_contact_uses_faces_not_diagonal_neighbors(): + labels = np.zeros((4, 4, 4), dtype=int) + labels[1, 1, 1] = 1 + labels[1, 1, 2] = 5000 + labels[2, 2, 2] = 5000 + result = grid_measurements(labels, (2, 2, 2), loading_axis=2) + assert result['disk_1_contact_area_mm2'] == 4 + assert result['disk_component_count'] == 2 + + +def test_too_short_and_missing_checks_are_not_passes(): + assert evaluate({'site': 'hip', 'generation_status': 'too_short'})['qc_status'] == 'fail' + assert evaluate({'site': 'spine'})['qc_status'] == 'review' + + +def test_flat_boundary_and_length_checks(): + row = {'site': 'hip', 'generation_status': 'generated', 'bone_component_count': '1', + 'disk_component_count': '2', 'boundary_count': '3', 'loading_compressive': 'True', + 'loading_axis_matches': 'True', + 'shaft_bc_node_count': '100', 'shaft_bc_fixed_x_node_count': '100', + 'shaft_bc_fixed_z_node_count': '100', + 'measured_shaft_length_mm': '10', 'requested_shaft_length_mm': '10', + 'complete_distal_section_verified': 'True', 'distal_boundary_span_mm': '0', + 'distal_boundary_coverage_fraction': '1'} + for component in (1, 2): + row[f'disk_{component}_contact_area_mm2'] = '10' + row[f'disk_{component}_contact_component_count'] = '1' + assert evaluate(row)['qc_status'] == 'pass' + row['measured_shaft_length_mm'] = '8' + assert 'shaft_length_mismatch' in evaluate(row)['qc_reasons'] + + +def test_partial_shaft_fixation_is_flagged(): + row = {'site': 'hip', 'shaft_bc_node_count': 100, + 'shaft_bc_fixed_x_node_count': 99, 'shaft_bc_fixed_z_node_count': 100} + assert 'incomplete_shaft_fixation_x' in evaluate(row)['qc_reasons'] + assert 'incomplete_shaft_fixation_z' not in evaluate(row)['qc_reasons'] + + +def test_intended_distal_patch_coverage_takes_precedence(): + row = {'site': 'hip', 'distal_boundary_coverage_fraction': 0.945, + 'distal_support_patch_coverage_fraction': 1.0} + assert 'incomplete_distal_boundary' not in evaluate(row)['qc_reasons'] + row['distal_support_patch_coverage_fraction'] = 0.98 + assert 'incomplete_distal_boundary' in evaluate(row)['qc_reasons'] + + +def test_legacy_full_face_coverage_tolerates_existing_patch(): + row = {'site': 'hip', 'distal_boundary_coverage_fraction': 0.89} + assert 'incomplete_distal_boundary' not in evaluate(row)['qc_reasons'] + row['distal_boundary_coverage_fraction'] = 0.88 + assert 'incomplete_distal_boundary' in evaluate(row)['qc_reasons'] + + +def test_csv_and_gallery_escape_subject_names(tmp_path): + path = tmp_path / 'case.n88model' + csv_path = write_measurements(path, {'site': 'hip', 'generation_status': 'too_short'}) + with csv_path.open() as stream: + row = next(csv.DictReader(stream)) + row['model_id'] = '' + output = tmp_path / 'gallery.html' + generate_gallery([evaluate(row)], output) + content = output.read_text() + assert '<script>bad</script>' in content + assert 'too_short' in content + assert 'loading="lazy"' not in content # No fabricated image for a missing preview. + + +def test_command_uses_csv_only(tmp_path): + from ogo.cli.ValidateFEA import main + write_measurements(tmp_path / 'missing.n88model', {'site': 'hip', 'generation_status': 'too_short'}) + main([str(tmp_path), '--output', str(tmp_path / 'audit'), '--gallery']) + assert (tmp_path / 'audit' / 'gallery.html').exists() + with (tmp_path / 'audit' / 'qc_summary.csv').open() as stream: + assert next(csv.DictReader(stream))['qc_status'] == 'fail' + + +def test_solved_measurements_do_not_replace_geometry(tmp_path): + path = tmp_path / 'model.n88model' + csv_path = write_measurements(path, {'site': 'hip', 'available_shaft_length_mm': 18}) + write_measurements(path, {'sed_finite': False}) + with csv_path.open() as stream: + row = next(csv.DictReader(stream)) + assert row['available_shaft_length_mm'] == '18' + assert 'invalid_sed_finite' in evaluate(row)['qc_reasons'] diff --git a/tests/fea/test_workflow_locked_defaults.py b/tests/fea/test_workflow_locked_defaults.py index 92fa774..e6bfde1 100644 --- a/tests/fea/test_workflow_locked_defaults.py +++ b/tests/fea/test_workflow_locked_defaults.py @@ -33,6 +33,7 @@ def _locked_workflow_defaults(): "registration_iterations": spine.DEFAULT_SPINE_REGISTRATION_ITERATIONS, "registration_min_scale": spine.DEFAULT_SPINE_REGISTRATION_MIN_SCALE, "registration_max_scale": spine.DEFAULT_SPINE_REGISTRATION_MAX_SCALE, + "spine_icp_target": spine.DEFAULT_SPINE_ICP_TARGET, "reference_filename": spine.DEFAULT_SPINE_REFERENCE_FILENAME, "contact_size_fraction": list(spine.SPINE_CONTACT_SIZE_FRACTION), "superior_contact_center_fraction": list( @@ -76,6 +77,9 @@ def _locked_workflow_defaults(): "proximal_reference_distance_mm": femur.DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, "reference_min_scale": list(femur.DEFAULT_FEMUR_REFERENCE_MIN_SCALE), "reference_max_scale": list(femur.DEFAULT_FEMUR_REFERENCE_MAX_SCALE), + "registration_backend": femur.DEFAULT_FEMUR_REGISTRATION_BACKEND, + "registration_landmarks": femur.DEFAULT_FEMUR_REGISTRATION_LANDMARKS, + "registration_iterations": femur.DEFAULT_FEMUR_REGISTRATION_ITERATIONS, "pmma_thickness_mm": femur.DEFAULT_PMMA_THICKNESS_MM, "pmma_intrusion_mm": femur.DEFAULT_PMMA_INTRUSION_MM, "input_margin_mm": femur.DEFAULT_FEMUR_INPUT_MARGIN_MM, From a624cbdad1782a20bdc6d90588f90b10052b630f Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Thu, 8 Oct 2026 16:38:53 -0600 Subject: [PATCH 51/67] Initialize spine ICP by centroids and reject anatomical flips --- ogo/cli/CheckFEModelBC.py | 2 +- ogo/fea/README.md | 12 +++ ogo/fea/spine.py | 156 ++++++------------------------- tests/cli/test_CheckFEModelBC.py | 8 ++ tests/fea/test_spine.py | 61 ++++++++++++ 5 files changed, 112 insertions(+), 127 deletions(-) diff --git a/ogo/cli/CheckFEModelBC.py b/ogo/cli/CheckFEModelBC.py index e1112e6..ade6b4d 100644 --- a/ogo/cli/CheckFEModelBC.py +++ b/ogo/cli/CheckFEModelBC.py @@ -198,7 +198,7 @@ def audit_spine_compression(node_sets, constraints, tolerance): top = node_sets.get("body_top") bottom = node_sets.get("body_bottom") - if top and bottom: + if top and bottom and top.get("count", 0) > 0 and bottom.get("count", 0) > 0: z_gap = bottom["centroid"]["z"] - top["centroid"]["z"] applied = _single_value(constraints, "top_displacement") axis = _single_axis(constraints, "top_displacement") diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 64a09de..781f4f3 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -644,6 +644,18 @@ Available spine presets: Spine registration can be switched without editing code. The maintained default uses dense deterministic NumPy ICP on the vertebral-body surface: +Initialization matches centroids while retaining native physical anatomical +directions. PCA is used to estimate reference size, not to swap registration +axes. The rigid fit is rejected if the superior-inferior axis or body-to-process +direction changes by more than 60 degrees, or if posterior-process QC fails +on the final resampled and cleaned masks. This initialization applies to both +body-only and full-vertebra registration targets. +There are no axis-permutation retries or least-bad fallback models. Images must +have a superior-inferior physical z axis; NIfTI array ordering alone is not an +orientation check. The same checks apply to the optional VTK backend. +Boundary-contact QC remains separate: a correctly aligned model with a missing +support must not be solved. + ```bash ogoFEA spine image.nii.gz spine_labels.nii.gz \ --vertebra L1:20:48 \ diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index ac1c4b6..a896a83 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -11,9 +11,9 @@ the optional full-vertebra target uses body plus posterior-process labels when the matching reference surface is available. By default the reference is PCA-scaled within 0.8,0.8,0.75 to 1.2,1.2,1.3 before deterministic NumPy - rigid ICP (8,000 surface points, 50 iterations). Process-orientation QC - triggers alternate initialization attempts when needed. Reference scaling - does not scale the physical dimensions of the solved bone. + rigid ICP (8,000 surface points, 50 iterations), initialized by centroids + without PCA axis swaps. Anatomical orientation checks reject invalid fits. + Reference scaling does not scale the physical dimensions of the solved bone. 4. Apply the ICP transform and set the final output spacing to 1.0 x 1.0 x 1.0 mm in one shared VTK reslice helper. Image data use cubic interpolation; body/process labels use nearest-neighbor interpolation. @@ -67,7 +67,6 @@ DEFAULT_SPINE_REGISTRATION_BACKEND = "numpy" DEFAULT_SPINE_REGISTRATION_LANDMARKS = 8000 DEFAULT_SPINE_REGISTRATION_ITERATIONS = 50 -DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC = True SPINE_ICP_TARGETS = ("body", "vertebra") DEFAULT_SPINE_ICP_TARGET = "body" DEFAULT_SPINE_BODY_REFERENCE_FILENAME = "L4_BODY_SPINE_COMPRESSION_REF.vtk" @@ -77,7 +76,7 @@ SPINE_SUPERIOR_CONTACT_CENTER_FRACTION = (0.5, 0.5, 1.05) SPINE_INFERIOR_CONTACT_CENTER_FRACTION = (0.5, 0.5, -0.05) -SPINE_ALIGNMENT_METHOD = "scaled ICP to spine reference surface" +SPINE_ALIGNMENT_METHOD = "centroid-initialized scaled ICP to spine reference surface" BENCHMARK_LINEAR_FE_DISPLACEMENT_MM = -0.2 BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM = -2.0 @@ -735,67 +734,25 @@ def perform_marching_cubes(body_output): return mcubes -def _principal_axes_from_points(points): - centered = np.asarray(points, dtype=float) - np.asarray(points, dtype=float).mean(axis=0) - _, _, vh = np.linalg.svd(centered, full_matrices=False) - axes = vh.T - if np.linalg.det(axes) < 0: - axes[:, -1] *= -1 - return axes - - -def _proper_signed_axis_permutation_matrices(): - matrices = [] - for permutation in [ - (0, 1, 2), - (0, 2, 1), - (1, 0, 2), - (1, 2, 0), - (2, 0, 1), - (2, 1, 0), - ]: - for signs in [ - (-1, -1, -1), - (-1, -1, 1), - (-1, 1, -1), - (-1, 1, 1), - (1, -1, -1), - (1, -1, 1), - (1, 1, -1), - (1, 1, 1), - ]: - matrix = np.zeros((3, 3), dtype=float) - for row, col in enumerate(permutation): - matrix[row, col] = signs[row] - if np.linalg.det(matrix) > 0: - matrices.append(matrix) - return matrices - - -def _initial_transform_from_rotation(moving_points, fixed_points, rotation): - moving_center = np.asarray(moving_points, dtype=float).mean(axis=0) - fixed_center = np.asarray(fixed_points, dtype=float).mean(axis=0) - return { - "rotation": np.asarray(rotation, dtype=float), - "translation": fixed_center - np.asarray(rotation, dtype=float) @ moving_center, - } +def check_spine_registration_orientation(rotation, body_centroid=None, process_centroid=None): + """Reject axis swaps relative to native physical anatomical directions. - -def _spine_pca_initial_transforms(reference_points, sample_points): - moving_axes = _principal_axes_from_points(reference_points) - fixed_axes = _principal_axes_from_points(sample_points) - candidates = [] - seen = set() - for axis_rotation in _proper_signed_axis_permutation_matrices(): - rotation = fixed_axes @ axis_rotation @ moving_axes.T - if np.linalg.det(rotation) < 0: - continue - key = tuple(np.round(rotation, 8).ravel()) - if key in seen: - continue - seen.add(key) - candidates.append(_initial_transform_from_rotation(reference_points, sample_points, rotation)) - return candidates + Inputs must have a superior-inferior physical z axis, as in the maintained + CT workflow. A 60-degree guard allows clinical tilt but rejects sideways + and inverted fits. It is a rejection rule, not an alternative alignment. + """ + rotation = np.asarray(rotation, dtype=float) + if rotation.shape != (3, 3) or not np.all(np.isfinite(rotation)): + raise ValueError("spine registration orientation requires a finite 3x3 rotation") + if rotation[2, 2] < 0.5: + raise ValueError("spine registration orientation failed: superior-inferior axis swap") + if body_centroid is not None and process_centroid is not None: + offset = np.asarray(process_centroid) - np.asarray(body_centroid) + length = np.linalg.norm(offset) + if length > 1.e-6: + posterior_cosine = float(np.dot(offset, offset @ rotation) / length ** 2) + if posterior_cosine < 0.5: + raise ValueError("spine registration orientation failed: posterior direction swap") def _icp_transform_to_vtk(transform): @@ -826,9 +783,8 @@ def get_icp_with_scaling( reference_path, process=None, scale_factors=None, - min_scale=(0.8, 0.8, 0.75), - max_scale=(1.2, 1.2, 1.3), - retry_on_process_qc=DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC, + min_scale=DEFAULT_SPINE_REGISTRATION_MIN_SCALE, + max_scale=DEFAULT_SPINE_REGISTRATION_MAX_SCALE, backend=DEFAULT_SPINE_REGISTRATION_BACKEND, landmarks=DEFAULT_SPINE_REGISTRATION_LANDMARKS, iterations=DEFAULT_SPINE_REGISTRATION_ITERATIONS, @@ -881,14 +837,15 @@ def get_icp_with_scaling( fixed_points=sample_surface_points, iterations=iterations, tolerance=1.0e-4, - start_by_matching_centroids_only=False, + start_by_matching_centroids_only=True, convergence="delta", distance_mode="mean", ) else: raise ValueError("registration_backend must be 'vtk' or 'numpy'.") - if process is not None and retry_on_process_qc: + check_spine_registration_orientation(transform["rotation"]) + if process is not None: body_centroid = _mask_centroid_physical(body.GetOutput()) process_centroid = _mask_centroid_physical(process.GetOutput()) default_process_qc = _icp_process_orientation_metrics( @@ -901,57 +858,9 @@ def get_icp_with_scaling( f"axial_offset={default_process_qc['axial_offset_mm']:.2f} mm, " f"transverse_offset={default_process_qc['transverse_offset_mm']:.2f} mm" ) - if default_process_qc["status"] != "ok": - ogo.message( - "Default ICP failed process-vector QC; trying alternate PCA starts " - "with the deterministic point-cloud backend..." - ) - retry_candidates = [] - for initial in _spine_pca_initial_transforms(scaled_reference_points, sample_surface_points): - candidate = estimate_rigid_icp( - moving_points=scaled_reference_points, - fixed_points=sample_surface_points, - iterations=iterations, - tolerance=1.0e-4, - start_by_matching_centroids_only=False, - convergence="delta", - distance_mode="mean", - initial_transform=initial, - ) - process_qc = _icp_process_orientation_metrics( - candidate, - body_centroid, - process_centroid, - ) - retry_candidates.append((candidate, process_qc)) - valid = [ - (candidate, process_qc) - for candidate, process_qc in retry_candidates - if process_qc["status"] == "ok" - ] - if valid: - transform, chosen_qc = min(valid, key=lambda item: item[0]["mean_distance"]) - ogo.message( - "Selected alternate ICP start with process-vector QC: " - f"axial_offset={chosen_qc['axial_offset_mm']:.2f} mm, " - f"transverse_offset={chosen_qc['transverse_offset_mm']:.2f} mm, " - f"mean_distance={transform['mean_distance']:.4f}" - ) - else: - best_candidate, best_qc = min( - retry_candidates, - key=lambda item: ( - item[1]["axial_offset_mm"] / max(item[1]["transverse_offset_mm"], 1.0e-6), - item[0]["mean_distance"], - ), - ) - ogo.message( - "No alternate ICP start passed process-vector QC; " - "using least-bad candidate for downstream QC failure: " - f"axial_offset={best_qc['axial_offset_mm']:.2f} mm, " - f"transverse_offset={best_qc['transverse_offset_mm']:.2f} mm" - ) - transform = best_candidate + check_spine_registration_orientation(transform["rotation"], body_centroid, process_centroid) + # Mask-shape QC runs after resampling and morphology; the rigid + # transform checks above prevent axis flips before either operation. vtk_transform = _icp_transform_to_vtk(transform) ogo.message( @@ -1443,10 +1352,6 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, spine_icp_target = str(kwargs.get("spine_icp_target", DEFAULT_SPINE_ICP_TARGET)).strip().lower() if spine_icp_target not in SPINE_ICP_TARGETS: raise ValueError("spine_icp_target must be 'body' or 'vertebra'.") - registration_retry_on_process_qc = kwargs.get( - "registration_retry_on_process_qc", - DEFAULT_SPINE_REGISTRATION_RETRY_ON_PROCESS_QC, - ) mask_smoothing_spacing_threshold = kwargs.get( "mask_smoothing_spacing_threshold", DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, @@ -1555,7 +1460,6 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, scale_factors=registration_scale, min_scale=registration_min_scale, max_scale=registration_max_scale, - retry_on_process_qc=registration_retry_on_process_qc, backend=registration_backend, landmarks=registration_landmarks, iterations=registration_iterations, diff --git a/tests/cli/test_CheckFEModelBC.py b/tests/cli/test_CheckFEModelBC.py index 2c34cc2..c2ec1ad 100644 --- a/tests/cli/test_CheckFEModelBC.py +++ b/tests/cli/test_CheckFEModelBC.py @@ -35,3 +35,11 @@ def test_spine_bc_audit_requires_all_inferior_fixture_axes(): assert by_name["bottom_fixed_x fixes x"]["passed"] is True assert by_name["bottom_fixed_y fixes y"]["passed"] is True assert by_name["bottom_fixed_z fixes z"]["passed"] is True + + +def test_spine_bc_audit_reports_empty_support_without_crashing(): + empty = {"count": 0, "centroid": {"x": None, "y": None, "z": None}, + "bounds": {"z_min": None, "z_max": None}} + checks = audit_spine_compression( + {"body_top": _flat_set(10), "body_bottom": empty}, {}, tolerance=1.e-5) + assert any(c['name'] == 'body_bottom exists' and not c['passed'] for c in checks) diff --git a/tests/fea/test_spine.py b/tests/fea/test_spine.py index 35449f4..c1920e0 100644 --- a/tests/fea/test_spine.py +++ b/tests/fea/test_spine.py @@ -1,4 +1,65 @@ from ogo.fea import spine +import numpy as np +import pytest +from types import SimpleNamespace + + +def _mock_registration_inputs(monkeypatch): + points = np.array([[0, 0, 0], [40, 0, 0], [0, 35, 0], [0, 0, 30]], dtype=float) + monkeypatch.setattr(spine, "surface_points_from_vtk_mask", lambda *a, **k: points) + monkeypatch.setattr(spine.ogo, "readPolyData", lambda *a: None) + monkeypatch.setattr(spine, "polydata_points", lambda *a: points) + monkeypatch.setattr(spine, "sample_points", lambda *a, **k: points) + return SimpleNamespace(GetOutput=lambda: None) + + +def test_spine_icp_preserves_native_orientation_at_initialization(monkeypatch): + body = _mock_registration_inputs(monkeypatch) + calls = [] + + def estimate(**kwargs): + calls.append(kwargs) + return dict(rotation=np.eye(3), translation=np.zeros(3), iterations=1, mean_distance=0) + + monkeypatch.setattr(spine, "estimate_rigid_icp", estimate) + spine.get_icp_with_scaling(body, "reference.vtk") + assert len(calls) == 1 + assert calls[0]["start_by_matching_centroids_only"] is True + + +def test_spine_icp_rejects_invalid_process_without_axis_swapped_retries(monkeypatch): + body = _mock_registration_inputs(monkeypatch) + calls = [] + + def estimate(**kwargs): + calls.append(kwargs) + return dict(rotation=np.diag([-1., -1., 1.]), translation=np.zeros(3), iterations=1, mean_distance=0) + + monkeypatch.setattr(spine, "estimate_rigid_icp", estimate) + centroids = iter([np.zeros(3), np.array([0., 30., 0.])]) + monkeypatch.setattr(spine, "_mask_centroid_physical", lambda *a: next(centroids)) + monkeypatch.setattr(spine, "_icp_process_orientation_metrics", lambda *a: dict( + status="failed", axial_offset_mm=30, transverse_offset_mm=5)) + with pytest.raises(ValueError, match="orientation"): + spine.get_icp_with_scaling(body, "reference.vtk", process=body, scale_factors=1) + assert len(calls) == 1 + + +@pytest.mark.parametrize("rotation", [ + np.diag([1., -1., -1.]), + np.diag([-1., -1., 1.]), + np.array([[1., 0., 0.], [0., 0., -1.], [0., 1., 0.]]), +]) +def test_spine_registration_rejects_anatomical_axis_flips(rotation): + with pytest.raises(ValueError, match="orientation"): + spine.check_spine_registration_orientation(rotation, [0, 0, 0], [0, 30, 0]) + + +def test_spine_registration_accepts_small_anatomical_rotation(): + angle = np.deg2rad(32) + rotation = np.array([[1., 0., 0.], [0., np.cos(angle), -np.sin(angle)], + [0., np.sin(angle), np.cos(angle)]]) + spine.check_spine_registration_orientation(rotation, [0, 0, 0], [0, 30, 0]) def test_default_spine_cap_geometry_matches_maintained_settings(): From d3d17987869019a8183328ea11483bfd80fb1b6d Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 01:37:07 -0600 Subject: [PATCH 52/67] Clean disconnected vertebral body fragments before FEA registration --- ogo/fea/README.md | 13 +++++- ogo/fea/spine.py | 55 ++++++++++++++++++++--- ogo/fea/validation.py | 5 +++ tests/fea/test_spine_body_cleanup.py | 67 ++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 tests/fea/test_spine_body_cleanup.py diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 781f4f3..bfb62da 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -237,7 +237,15 @@ For spine, the maintained path is: 1. `GenerateFEM.py::build_spine_command` forwards the calibrated image, segmentation, target `LEVEL:BODY_LABEL:PROCESS_LABEL`, preset, optional Pistoia mask, and any lower-level overrides. -2. `spine.py::main` thresholds the requested body/process labels, crops around +2. `spine.py::clean_body_component` keeps the largest face-connected component + of the requested body label before registration and support construction. + Other labels, including the posterior process, are unchanged. The QC CSV + records the original component count, removed voxels, removed volume, and + removed body fraction. Removal of more than 5% triggers + `substantial_body_cleanup` for manual review, not automatic exclusion. This + conservative triage threshold is not a validated anatomical cutoff. Cleanup + cannot repair wrongly labelled anatomy connected to the main body. + `spine.py::main` then thresholds the body/process labels, crops around the vertebra, checks posterior-process orientation, and runs scaled ICP to the selected reference surface from `default_spine_reference_path`. The maintained default `--spine_icp_target body` uses only the vertebral body @@ -607,7 +615,8 @@ separate right reference file. The spine workflow builds one compression model per `--vertebra` target: -1. Threshold the labelled mask into vertebral body and posterior process. +1. Keep the largest face-connected body-label component, record the removed + volume, then threshold the labelled mask into body and posterior process. 2. Crop the image and masks to the vertebra. 3. Align the body to the scaled L4 vertebral-body reference using ICP. 4. Apply transform and isotropic resampling through the shared VTK reslice diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index a896a83..f102cd9 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -4,7 +4,9 @@ 1. Read the calibrated density image and labelled vertebra mask. The high-level wrapper uses ``--vertebra LEVEL:BODY_LABEL:PROCESS_LABEL`` so body and posterior-process labels are explicit and traceable. -2. Crop the vertebral body, posterior process, calibrated image, and combined +2. Retain the largest face-connected body-label component, leaving the process + and other labels unchanged. Record the removed volume for segmentation QC. + Crop the vertebral body, posterior process, calibrated image, and combined vertebra mask to the same bounding box. 3. ICP-align the requested registration target to a bundled L4 compression reference surface. The maintained body-only target uses the vertebral body; @@ -1298,6 +1300,38 @@ def get_mask_np(vtk_image): ###################################################################### VERTEBRA PIPELINE +def clean_body_component(mask_image, body_label): + """Remove detached body fragments before registration and cap placement. + + Face connectivity matches the voxel FE mesh. This does not repair errors + connected to the main body or alter the posterior-process segmentation. + Return a new label image and measurements of the cleanup for audit. + """ + from scipy.ndimage import label + from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + + data = vtk_image_to_numpy(mask_image, copy=True) + components, count = label(data == body_label) + if not count: + raise ValueError(f'No voxels found for body label {body_label}') + sizes = np.bincount(components.ravel()) + sizes[0] = 0 + retained = int(sizes.argmax()) + removed = (components != 0) & (components != retained) + removed_count = int(removed.sum()) + original_count = int(sizes.sum()) + data[removed] = 0 + volume = float(np.prod(mask_image.GetSpacing())) + metrics = { + 'input_body_component_count': int(count), + 'body_cleanup_removed_voxels': removed_count, + 'body_cleanup_removed_volume_mm3': removed_count * volume, + 'body_cleanup_removed_fraction': removed_count / original_count, + 'body_cleanup_original_volume_mm3': original_count * volume, + } + return numpy_to_vtk_image(data, mask_image), metrics + + def process_vertebra(input_mask, input_image, n88model_output_path, body_label, process_label, reference_path, **kwargs): pmma_mat_id = kwargs.get("pmma_mat_id", 5000) @@ -1388,10 +1422,17 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, check_vertebra_presence(mask_reader, body_label) check_vertebra_presence(mask_reader, process_label) + mask_image, body_cleanup = clean_body_component(mask_reader.GetOutput(), body_label) + ogo.message( + 'Body-label cleanup: %d components; removed %d voxels (%.2f%%); process label unchanged' + % (body_cleanup['input_body_component_count'], body_cleanup['body_cleanup_removed_voxels'], + 100 * body_cleanup['body_cleanup_removed_fraction']) + ) + # Threshold images to extract body, process and full vertebra ogo.message(f"Thresholding...") - body = threshold(mask_reader.GetOutput(), body_label) - process = threshold(mask_reader.GetOutput(), process_label) + body = threshold(mask_image, body_label) + process = threshold(mask_image, process_label) fullvertebra = combine_mask(body.GetOutput(), process.GetOutput()) # Crop everything to same BB @@ -1401,7 +1442,7 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, body.GetOutput(), process.GetOutput(), image_reader.GetOutput(), - mask_reader.GetOutput(), + mask_image, margin_mm=SPINE_PREPROCESSING_CROP_MARGIN_MM, extra_label_outputs=[] if pistoia_mask_reader is None @@ -1810,9 +1851,11 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, ogo.message(f"Writing n88model file: {n88model_output_path}") from ogo.fea.validation import measure_model, write_measurements - write_measurements(n88model_output_path, measure_model( + measurements = measure_model( model, "spine", body_image=padded_mask1, process_image=padded_mask2, - registration_rotation=sample_to_reference_rotation)) + registration_rotation=sample_to_reference_rotation) + measurements.update(body_cleanup) + write_measurements(n88model_output_path, measurements) writer = vtkbone.vtkboneN88ModelWriter() writer.SetInputData(model) writer.SetFileName(n88model_output_path) diff --git a/ogo/fea/validation.py b/ogo/fea/validation.py index c46148e..04d8a37 100644 --- a/ogo/fea/validation.py +++ b/ogo/fea/validation.py @@ -305,6 +305,11 @@ def number(key): if axial is not None and transverse is not None and axial > 0.75 * transverse + 2: review.append('process_orientation') if row.get('site') == 'spine': + # Review triage only: substantial cleanup can indicate a second vertebra + # or a disconnected anatomical region, not necessarily a failed model. + removed = number('body_cleanup_removed_fraction') + if removed is not None and removed > 0.05: + review.append('substantial_body_cleanup') for name in ('body_top', 'body_bottom'): span = number(name + '_span_mm') if span is None: diff --git a/tests/fea/test_spine_body_cleanup.py b/tests/fea/test_spine_body_cleanup.py new file mode 100644 index 0000000..eb1b960 --- /dev/null +++ b/tests/fea/test_spine_body_cleanup.py @@ -0,0 +1,67 @@ +import numpy as np +import pytest + +vtk = pytest.importorskip('vtk') + +from ogo.fea.spine import clean_body_component +from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + + +def image_for(data): + template = vtk.vtkImageData() + template.SetDimensions(*data.shape) + template.SetSpacing(0.8, 0.9, 1.5) + template.SetOrigin(10, -20, 30) + return numpy_to_vtk_image(data, template, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) + + +def test_body_cleanup_preserves_other_labels_and_geometry(): + data = np.zeros((8, 8, 8), dtype=np.uint8) + data[1:4, 1:4, 1:4] = 20 + data[6, 6, 6] = 20 + data[4:6, 1:4, 1:4] = 48 + data[7, 0, 0] = 99 + original = image_for(data) + cleaned, metrics = clean_body_component(original, 20) + expected = data.copy() + expected[6, 6, 6] = 0 + assert np.array_equal(vtk_image_to_numpy(cleaned), expected) + assert np.array_equal(vtk_image_to_numpy(original), data) + assert cleaned.GetSpacing() == original.GetSpacing() + assert cleaned.GetOrigin() == original.GetOrigin() + assert cleaned.GetExtent() == original.GetExtent() + assert metrics['input_body_component_count'] == 2 + assert metrics['body_cleanup_removed_voxels'] == 1 + assert metrics['body_cleanup_removed_fraction'] == pytest.approx(1 / 28) + assert metrics['body_cleanup_removed_volume_mm3'] == pytest.approx(0.8 * 0.9 * 1.5) + + +def test_single_body_component_is_unchanged(): + data = np.zeros((5, 5, 5), dtype=np.uint8) + data[1:4, 1:4, 1:4] = 20 + cleaned, metrics = clean_body_component(image_for(data), 20) + assert np.array_equal(vtk_image_to_numpy(cleaned), data) + assert metrics['body_cleanup_removed_fraction'] == 0 + + +def test_diagonal_body_fragments_are_not_face_connected(): + data = np.zeros((5, 5, 5), dtype=np.uint8) + data[1:3, 1:3, 1:3] = 20 + data[3, 3, 3] = 20 + cleaned, metrics = clean_body_component(image_for(data), 20) + assert vtk_image_to_numpy(cleaned)[3, 3, 3] == 0 + assert metrics['input_body_component_count'] == 2 + + +def test_empty_body_is_rejected(): + with pytest.raises(ValueError, match='body label'): + clean_body_component(image_for(np.zeros((5, 5, 5), dtype=np.uint8)), 20) + + +def test_substantial_body_cleanup_requires_review_not_exclusion(): + from ogo.fea.validation import evaluate + + row = {'site': 'spine', 'body_cleanup_removed_fraction': 0.06} + assert 'substantial_body_cleanup' in evaluate(row)['qc_reasons'] + row['body_cleanup_removed_fraction'] = 0.01 + assert 'substantial_body_cleanup' not in evaluate(row)['qc_reasons'] From 1ce8c73ed53ba32908fcc7abcef4ff8be0f08587 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 07:51:36 -0600 Subject: [PATCH 53/67] Use compact WebP previews for FEA QC and portable galleries --- ogo/fea/README.md | 16 +++++++----- ogo/fea/gallery.py | 23 +++++++++++------ ogo/fea/qc_images.py | 17 +++++++++++++ ogo/fea/qc_render.py | 15 ++++++++---- ogo/fea/validation.py | 5 ++-- tests/fea/test_qc_images.py | 49 +++++++++++++++++++++++++++++++++++++ tests/fea/test_qc_render.py | 21 ++++++++++++++-- tests/fea/test_review.py | 6 +++-- 8 files changed, 127 insertions(+), 25 deletions(-) create mode 100644 ogo/fea/qc_images.py create mode 100644 tests/fea/test_qc_images.py diff --git a/ogo/fea/README.md b/ogo/fea/README.md index bfb62da..fcb5bad 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -16,9 +16,9 @@ The workflow builders are organized by anatomy: ## Automatic Model QC -Both anatomy workflows save one three-view anatomy panel (`*_qc_3d.png`) +Both anatomy workflows save one three-view anatomy panel (`*_qc_3d.webp`) after model generation. The FAIM adapter saves a corresponding solved SED -panel (`*_sed_3d.png`) after successful processing of a hip or spine model. +panel (`*_sed_3d.webp`) after successful processing of a hip or spine model. The views use `ogo.cli.Visualize.vis3d`: white background, opaque bone and gold PMMA supports, with constrained surface-adjacent voxel layers in red. Individual views are framed independently. Numerical BC checks remain in CSV; @@ -32,7 +32,10 @@ images do not imply that the requested shaft length was retained. SED uses a linear Jet scale, shared across the three views, from zero to the 99th percentile of bone-element SED. A bone-masked Gaussian (sigma 0.8 mm) and light surface smoothing improve display only; FE arrays and results are -unchanged. Each PNG has a JSON sidecar recording display settings. Spine +unchanged. Review images use WebP (quality 85) at 1400 x 2550 pixels; individual +gallery views are 1400 x 850 pixels. These compact lossy images are for QC, not +quantitative analysis or publication. Each image has a JSON sidecar recording +display settings and compression. Existing PNG previews remain supported. Spine generation retains `*_qc_body_mask.nii.gz` in model coordinates for consistent body/process colouring after solving. Temporary individual renders are removed. @@ -92,7 +95,7 @@ Without a refreshed editable installation, use `python -m ogo.cli.ValidateFEA`. The gallery supports anatomy, QC status, reason, subject search, and anatomy/SED view selection, with expandable measurement tables. A separate camera selector shows oblique/side, top, bottom, or all views in both thumbnails and the review -dialog. The fixed three-row Ogo renders are split once into cached PNGs in +dialog. The fixed three-row Ogo renders are split once into cached WebP images in `gallery_views/`; unrecognized legacy layouts remain full montages. Image files remain external; retain their relative location and the view-assets folder when copying the gallery and models together. @@ -106,8 +109,9 @@ ogoValidateFEA derivatives/fea --output derivatives/fea/qc --gallery-zip --worke Extract `gallery.zip` and open `gallery.html`. It includes `images/`, `gallery_views/`, `qc_summary.csv`, and the initial `study_inclusion.csv`. No original model directory or server is needed to review it. Model files are -not included; original model paths remain in the CSV as provenance. Both solve -stages and all available camera views are retained. Missing images remain +not included; original model paths remain in the CSV as provenance. Legacy PNGs +are converted to compact WebP images inside the ZIP without modifying their +originals. Both solve stages and all available camera views are retained. Missing images remain missing. Large cohorts use lazy-loaded image files rather than embedded HTML. After review, export the updated inclusion CSV from the gallery; browser decisions cannot rewrite the CSV inside the original ZIP. diff --git a/ogo/fea/gallery.py b/ogo/fea/gallery.py index 660abdf..dc3af73 100644 --- a/ogo/fea/gallery.py +++ b/ogo/fea/gallery.py @@ -13,13 +13,14 @@ from urllib.parse import quote from ogo.fea.review import FIELDS, REASONS, identity, inclusion_row, write_inclusion +from ogo.fea.qc_images import save_review_image def _camera_images(path, output): """Split the fixed three-row Ogo render, caching assets beside the gallery.""" from PIL import Image with Image.open(path) as image: - if image.size != (1400, 2550) or not path.name.endswith(('_qc_3d.png', '_sed_3d.png')): + if image.size != (1400, 2550) or not path.stem.endswith(('_qc_3d', '_sed_3d')): return [] stat = path.stat() key = hashlib.sha256(f'{path.resolve()}:{stat.st_size}:{stat.st_mtime_ns}'.encode()).hexdigest()[:20] @@ -27,9 +28,9 @@ def _camera_images(path, output): directory.mkdir(exist_ok=True) assets = [] for index, view in enumerate(('oblique', 'top', 'bottom')): - asset = directory / f'{key}_{view}.png' + asset = directory / f'{key}_{view}.webp' if not asset.exists(): - image.crop((0, index * 850, 1400, (index + 1) * 850)).save(asset) + save_review_image(image.crop((0, index * 850, 1400, (index + 1) * 850)), asset) assets.append((view, asset)) return assets @@ -112,10 +113,16 @@ def export_gallery_zip(rows, output, decisions=None): # Stable identity preserves browser autosave across exports. name = hashlib.sha256(str(source).encode()).hexdigest()[:20] + '_' + source.name target = images / name - try: - target.symlink_to(source) - except OSError: - shutil.copyfile(source, target) + if source.suffix.lower() == '.png': + from PIL import Image + target = target.with_suffix('.webp') + with Image.open(source) as image: + save_review_image(image, target) + else: + try: + target.symlink_to(source) + except OSError: + shutil.copyfile(source, target) copied[source] = target.relative_to(root).as_posix() record[key] = copied[source] portable.append(record) @@ -130,7 +137,7 @@ def export_gallery_zip(rows, output, decisions=None): with zipfile.ZipFile(temporary_zip, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=True) as archive: for source in sorted(root.rglob('*')): if source.is_file() and source != temporary_zip: - compression = zipfile.ZIP_STORED if source.suffix.lower() in ('.png', '.jpg', '.jpeg') else zipfile.ZIP_DEFLATED + compression = zipfile.ZIP_STORED if source.suffix.lower() in ('.png', '.jpg', '.jpeg', '.webp') else zipfile.ZIP_DEFLATED archive.write(source, source.relative_to(root).as_posix(), compress_type=compression) temporary_zip.replace(output) return output diff --git a/ogo/fea/qc_images.py b/ogo/fea/qc_images.py new file mode 100644 index 0000000..0d8293e --- /dev/null +++ b/ogo/fea/qc_images.py @@ -0,0 +1,17 @@ +"""Compact review images; numerical models and source images are not modified.""" + +from pathlib import Path + + +def save_review_image(image, path): + """Save an RGB WebP at quality 85 without resizing the supplied image.""" + image.convert('RGB').save(path, format='WEBP', quality=85, method=4) + + +def qc_image_path(model_path, sed=False): + """Prefer current WebP previews, accepting existing legacy PNGs.""" + model_path = Path(model_path) + suffix = '_sed_3d' if sed else '_qc_3d' + path = model_path.with_name(model_path.stem + suffix + '.webp') + legacy = path.with_suffix('.png') + return legacy if not path.exists() and legacy.exists() else path diff --git a/ogo/fea/qc_render.py b/ogo/fea/qc_render.py index 1c1df42..ad18940 100644 --- a/ogo/fea/qc_render.py +++ b/ogo/fea/qc_render.py @@ -8,7 +8,6 @@ import argparse import json import os -import shutil import warnings from tempfile import TemporaryDirectory from pathlib import Path @@ -24,6 +23,7 @@ from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtk from ogo.cli.Visualize import vis3d +from ogo.fea.qc_images import save_review_image def smooth_bone_sed(values, indices, bone, spacing): @@ -320,7 +320,7 @@ def panel(model_path, output_dir, site, body_mask=None, sed=False, def export_model_qc(model_path, site, body_mask=None, sed=False, model=None, ineligible=False): - """Write one three-view PNG and its visualization settings beside a model. + """Write one three-view WebP and its visualization settings beside a model. An ineligible hip can supply its uncut supported mesh without writing an n88model or assigning an artificial distal boundary. Rendering errors are @@ -339,15 +339,20 @@ def export_model_qc(model_path, site, body_mask=None, sed=False, if body_mask is None and site == "spine": body_mask = model_path.with_name(model_path.stem + "_qc_body_mask.nii.gz") suffix = "_sed_3d" if sed else "_qc_3d" - output = model_path.with_name(model_path.stem + suffix + ".png") + output = model_path.with_name(model_path.stem + suffix + ".webp") with TemporaryDirectory(prefix="ogo-qc-") as temporary: temporary = Path(temporary) rendered = panel(model_path, temporary, site, body_mask, sed, model, ineligible) - shutil.copyfile(rendered, output) + with Image.open(rendered) as image: + save_review_image(image, output) views = [json.loads((temporary / (model_path.stem + "_preview_" + view + ("_sed" if sed else "") + ".json")).read_text()) for view in ("oblique", "top", "bottom")] - output.with_suffix(".json").write_text(json.dumps({"site": site, "views": views}, indent=2)) + output.with_suffix(".json").write_text(json.dumps( + {"site": site, "views": views, "image_format": "WEBP", "image_quality": 85}, indent=2)) + if model_path.with_name(model_path.stem + '_qc_metrics.csv').exists(): + from ogo.fea.validation import write_measurements + write_measurements(model_path, {'sed_image' if sed else 'anatomy_image': str(output)}) return str(output) diff --git a/ogo/fea/validation.py b/ogo/fea/validation.py index 04d8a37..64c586c 100644 --- a/ogo/fea/validation.py +++ b/ogo/fea/validation.py @@ -201,9 +201,10 @@ def write_measurements(model_path, measurements): with path.open(newline='') as stream: row.update(next(csv.DictReader(stream))) row.update(measurements) + from ogo.fea.qc_images import qc_image_path row.update(model_id=model_path.stem, model_path=str(model_path), - anatomy_image=str(model_path.with_name(model_path.stem + '_qc_3d.png')), - sed_image=str(model_path.with_name(model_path.stem + '_sed_3d.png'))) + anatomy_image=str(qc_image_path(model_path)), + sed_image=str(qc_image_path(model_path, sed=True))) temporary = path.with_suffix('.csv.tmp') with temporary.open('w', newline='') as stream: writer = csv.DictWriter(stream, fieldnames=sorted(row)) diff --git a/tests/fea/test_qc_images.py b/tests/fea/test_qc_images.py new file mode 100644 index 0000000..7c91676 --- /dev/null +++ b/tests/fea/test_qc_images.py @@ -0,0 +1,49 @@ +"""Compact QC images preserve dimensions and legacy preview discovery.""" + +from pathlib import Path + +import pytest +from PIL import Image + + +def test_review_image_is_webp_without_resizing(tmp_path): + from ogo.fea.qc_images import save_review_image + + path = tmp_path / 'case_qc_3d.webp' + save_review_image(Image.new('RGB', (1400, 2550), 'white'), path) + with Image.open(path) as image: + assert image.format == 'WEBP' + assert image.size == (1400, 2550) + + +@pytest.mark.parametrize('extension', ['.png', '.webp']) +def test_gallery_splits_both_formats_into_webp(tmp_path, extension): + from ogo.fea.gallery import _camera_images + + source = tmp_path / ('case_qc_3d' + extension) + Image.new('RGB', (1400, 2550), 'white').save(source) + assets = _camera_images(source, tmp_path / 'gallery.html') + assert [view for view, _ in assets] == ['oblique', 'top', 'bottom'] + for _, path in assets: + assert path.suffix == '.webp' + with Image.open(path) as image: + assert image.size == (1400, 850) + + +def test_measurements_discover_legacy_png_and_prefer_webp(tmp_path): + import csv + from ogo.fea.validation import write_measurements + + model = tmp_path / 'case.n88model' + png = tmp_path / 'case_qc_3d.png' + png.touch() + path = write_measurements(model, {'site': 'hip'}) + with path.open() as stream: + assert next(csv.DictReader(stream))['anatomy_image'] == str(png) + webp = png.with_suffix('.webp') + webp.touch() + path = write_measurements(model, {'site': 'hip'}) + with path.open() as stream: + row = next(csv.DictReader(stream)) + assert row['anatomy_image'] == str(webp) + assert Path(row['sed_image']).suffix == '.webp' diff --git a/tests/fea/test_qc_render.py b/tests/fea/test_qc_render.py index 81a6db4..7c2149e 100644 --- a/tests/fea/test_qc_render.py +++ b/tests/fea/test_qc_render.py @@ -1,5 +1,7 @@ """Display smoothing must not alter solved values or blend supports into bone.""" +from pathlib import Path + import numpy as np import pytest @@ -89,6 +91,21 @@ def Finalize(self): monkeypatch.setattr(qc_render, "panel", fake_panel) path = qc_render.export_model_qc(tmp_path / "short.n88model", "hip", model=sentinel, ineligible=True) - assert path.endswith("short_qc_3d.png") - assert sorted(p.name for p in tmp_path.iterdir()) == ["short_qc_3d.json", "short_qc_3d.png"] + assert path.endswith("short_qc_3d.webp") + assert sorted(p.name for p in tmp_path.iterdir()) == ["short_qc_3d.json", "short_qc_3d.webp"] + with Image.open(path) as image: + assert image.format == "WEBP" + assert image.size == (20, 60) assert all(not v["eligible_for_solve"] for v in json.loads((tmp_path / "short_qc_3d.json").read_text())["views"]) + from ogo.fea.validation import write_measurements + import csv + + legacy = tmp_path / "short_qc_3d.png" + legacy.touch() + Path(path).unlink() + metrics = write_measurements(tmp_path / "short.n88model", {"site": "hip", "retained_shaft_length_mm": 10}) + qc_render.export_model_qc(tmp_path / "short.n88model", "hip", model=sentinel, ineligible=True) + with metrics.open() as stream: + row = next(csv.DictReader(stream)) + assert row["anatomy_image"] == path + assert row["retained_shaft_length_mm"] == "10" diff --git a/tests/fea/test_review.py b/tests/fea/test_review.py index e87d7f7..f26b9bd 100644 --- a/tests/fea/test_review.py +++ b/tests/fea/test_review.py @@ -129,10 +129,10 @@ def test_gallery_individual_views_keep_panel_order_and_cache(tmp_path): assert 'id="review-stage"' in content for view, color in zip(('oblique', 'top', 'bottom'), ((255, 0, 0), (0, 128, 0), (0, 0, 255))): assert f'data-view="{view}"' in content - asset = next((tmp_path / 'gallery_views').glob(f'*_{view}.png')) + asset = next((tmp_path / 'gallery_views').glob(f'*_{view}.webp')) with Image.open(asset) as cropped: assert cropped.size == (1400, 850) - assert cropped.getpixel((700, 425)) == color + assert max(abs(a - b) for a, b in zip(cropped.getpixel((700, 425)), color)) <= 5 modified = asset.stat().st_mtime_ns generate_gallery([row], path) assert asset.stat().st_mtime_ns == modified @@ -163,6 +163,8 @@ def test_gallery_zip_is_portable_and_has_review_records(tmp_path): assert {'gallery.html', 'qc_summary.csv', 'study_inclusion.csv'} <= set(archive.namelist()) assert len([name for name in archive.namelist() if name.startswith('images/')]) == 1 assert len([name for name in archive.namelist() if name.startswith('gallery_views/')]) == 3 + assert not any(name.endswith('.png') for name in archive.namelist()) + assert archive.testzip() is None content = archive.read('gallery.html').decode() assert 'src="images/' in content assert str(tmp_path) not in content From 41f018fd7550f3bc83c7998e3a4a478c0d755a9a Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 09:45:00 -0600 Subject: [PATCH 54/67] Keep each hip support connected and add FEA review sorting --- ogo/fea/README.md | 8 ++++++++ ogo/fea/femur.py | 2 ++ ogo/fea/gallery.py | 26 ++++++++++++++++++++++++-- ogo/fea/gallery_review.js | 28 +++++++++++++++++++++++++--- tests/fea/test_review.py | 22 ++++++++++++++++++++++ tests/fea/test_sideways_fall.py | 15 +++++++++++++++ 6 files changed, 96 insertions(+), 5 deletions(-) diff --git a/ogo/fea/README.md b/ogo/fea/README.md index fcb5bad..4f3c9a3 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -100,6 +100,11 @@ dialog. The fixed three-row Ogo renders are split once into cached WebP images i remain external; retain their relative location and the view-assets folder when copying the gallery and models together. +Sort by patient ID, number of QC flags, or stiffness, reaction force and full or +regional failure load (lowest/highest first). Force sorting uses absolute +magnitudes; missing outcomes are last. Sorting never changes inclusion. Manual +exclusions are labelled "Manually excluded" independently of automatic QC. + For sharing, export one portable ZIP instead: ```bash @@ -810,6 +815,9 @@ estimate a fresh transform rather than reuse an unreviewed historical fit. `available_shaft_length_mm` is measured on the transformed full-scan model after generating the actual connected GT support and before the final crop. +Each proximal support retains its own largest 26-connected PMMA component +before the disks are combined. This removes disconnected shaft islands without +discarding either main disk; connected unwanted extensions still require QC. It measures **usable complete-section coverage**, not the distance to the lowest corner of an angled scan end. Before any resampling, rough crop or padding, `shaft_geometry.capture_distal_scan_face` records the physical voxel diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index eb1726b..00dcaa4 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -2942,6 +2942,7 @@ def sidewaysFallFe(args): thickness=pmma_thick, intrusion=pmma_intrusion, anatomy_constrained=True, + keep_largest_component=True, output_value=pmma_mat_id, ) greaterTrochanterPMMA = generate_projected_material_disk_vtk( @@ -2957,6 +2958,7 @@ def sidewaysFallFe(args): thickness=pmma_thick, intrusion=pmma_intrusion, anatomy_constrained=True, + keep_largest_component=True, output_value=pmma_mat_id, ) diff --git a/ogo/fea/gallery.py b/ogo/fea/gallery.py index dc3af73..90794b1 100644 --- a/ogo/fea/gallery.py +++ b/ogo/fea/gallery.py @@ -37,6 +37,21 @@ def _camera_images(path, output): def generate_gallery(rows, output, decisions=None, image_root=None): output = Path(output).absolute() + rows = [dict(row) for row in rows] + outcome_keys = ('stiffness_N_per_mm', 'reaction_force_N', 'pistoia_failure_load_N', + 'masked_pistoia_failure_load_N') + for row in rows: + if not row.get('model_path') or row.get('generation_status') in ('too_short', 'construction_failed'): + continue + model = Path(row['model_path']) + result = model.with_name(model.stem + '_results.csv') + if result.is_file(): + with result.open(newline='') as stream: + records = list(csv.DictReader(stream)) + if len(records) != 1: + raise ValueError(f'Expected one model-result row: {result}') + for key in outcome_keys: + row.setdefault(key, records[0].get(key, '')) decisions = decisions or {} cards = [] escape = lambda value: html.escape(str(value), quote=True) @@ -59,6 +74,12 @@ def generate_gallery(rows, output, decisions=None, image_root=None): reasons = sorted({reason for row in rows for reason in row['qc_reasons'].split(';') if reason}) options = ''.join(f'' for reason in reasons) manual_options = ''.join(f'' for reason in REASONS) + sort_options = '' + for key, name in [('stiffness_N_per_mm', 'Stiffness'), ('reaction_force_N', 'Reaction force'), + ('pistoia_failure_load_N', 'Full-bone failure load'), + ('masked_pistoia_failure_load_N', 'Regional failure load')]: + for direction, label in [('asc', 'lowest first'), ('desc', 'highest first')]: + sort_options += f'' seeds = [inclusion_row(row, decisions.get(identity(row))) for row in rows] serialized = json.dumps(rows, sort_keys=True) fingerprint = hashlib.sha256(serialized.encode()).hexdigest() @@ -70,6 +91,7 @@ def generate_gallery(rows, output, decisions=None, image_root=None):
+ @@ -82,9 +104,9 @@ def generate_gallery(rows, output, decisions=None, image_root=None):
''' # Replace once so record text cannot become another template token. - tokens = {'SCRIPT': script, 'MANUAL_OPTIONS': manual_options, 'REASONS': options, + tokens = {'SCRIPT': script, 'MANUAL_OPTIONS': manual_options, 'REASONS': options, 'SORT_OPTIONS': sort_options, 'CARDS': ''.join(cards), 'DATA': data} - content = re.sub(r'SCRIPT|MANUAL_OPTIONS|REASONS|CARDS|DATA', lambda match: tokens[match[0]], content) + content = re.sub(r'SCRIPT|MANUAL_OPTIONS|REASONS|SORT_OPTIONS|CARDS|DATA', lambda match: tokens[match[0]], content) output.write_text(content) return output diff --git a/ogo/fea/gallery_review.js b/ogo/fea/gallery_review.js index f59b43d..8d38603 100644 --- a/ogo/fea/gallery_review.js +++ b/ogo/fea/gallery_review.js @@ -65,7 +65,24 @@ function imageVisible(image, stage, camera) { return image.stage === stage && (image.view === camera || image.fallback === 'true'); } -if (typeof module !== 'undefined') module.exports = {inclusionRecord, encodeCSV, parseCSV, validateReviews, imageVisible}; +function sortIndices(indices, rows, mode) { + const patient = (a, b) => rows[a].model_id.localeCompare(rows[b].model_id, undefined, {numeric: true}); + const count = row => new Set((row.qc_reasons || '').split(';').filter(Boolean)).size; + return [...indices].sort((a, b) => { + if (mode === 'patient') return patient(a, b); + if (mode === 'flags') return count(rows[b]) - count(rows[a]) || patient(a, b); + const [key, order] = mode.split(':'); + const value = row => { + const raw = row[key]; + return raw === '' || raw == null || !Number.isFinite(Number(raw)) ? null : Math.abs(Number(raw)); + }; + const x = value(rows[a]), y = value(rows[b]); + if (x === null || y === null) return x === y ? patient(a, b) : x === null ? 1 : -1; + return (order === 'desc' ? y - x : x - y) || patient(a, b); + }); +} + +if (typeof module !== 'undefined') module.exports = {inclusionRecord, encodeCSV, parseCSV, validateReviews, imageVisible, sortIndices}; if (typeof document !== 'undefined') { const data = JSON.parse(document.getElementById('review-data').textContent); @@ -88,10 +105,15 @@ if (typeof document !== 'undefined') { } function refresh() { let visible = 0; + const cards = [...document.querySelectorAll('article')]; + const byIndex = new Map(cards.map(card => [Number(card.dataset.index), card])); + for (const index of sortIndices([...byIndex.keys()], rows, $('sort').value)) { + document.querySelector('main').append(byIndex.get(index)); + } for (const card of document.querySelectorAll('article')) { const row = rows[Number(card.dataset.index)], record = inclusionRecord(row, decisions.get(decisionKey(row))); card.querySelector('.automatic').textContent = 'Automatic: ' + row.qc_status + (row.qc_reasons ? ': ' + row.qc_reasons : ''); - card.querySelector('.decision').textContent = 'Study: ' + record.final_inclusion + (record.manual_decision !== 'automatic' ? ' (manual)' : ''); + card.querySelector('.decision').textContent = record.manual_decision === 'exclude' ? 'Manually excluded' : 'Study: ' + record.final_inclusion + (record.manual_decision !== 'automatic' ? ' (manual)' : ''); for (const button of card.querySelectorAll('.quick-decision')) { button.setAttribute('aria-pressed', String(record.manual_decision === button.dataset.decision)); button.style.fontWeight = record.manual_decision === button.dataset.decision ? '700' : '400'; @@ -144,7 +166,7 @@ if (typeof document !== 'undefined') { persist(); refresh(); }); } - for (const id of ['search', 'site', 'status', 'reason', 'inclusion', 'stage', 'camera']) $(id).addEventListener('input', refresh); + for (const id of ['search', 'site', 'status', 'reason', 'inclusion', 'stage', 'camera', 'sort']) $(id).addEventListener('input', refresh); for (const id of ['stage', 'camera']) $('review-' + id).addEventListener('change', () => { $(id).value = $('review-' + id).value; refresh(); diff --git a/tests/fea/test_review.py b/tests/fea/test_review.py index f26b9bd..21942de 100644 --- a/tests/fea/test_review.py +++ b/tests/fea/test_review.py @@ -9,6 +9,28 @@ from ogo.fea.review import inclusion_row, load_decisions +def test_gallery_sorting_is_numeric_and_puts_missing_results_last(tmp_path): + from ogo.fea import gallery + node = shutil.which('node') + if not node: + pytest.skip('Node is required') + script = Path(gallery.__file__).with_name('gallery_review.js') + code = '''const assert = require('node:assert/strict'); +const {sortIndices} = require(SCRIPT); +const rows = [{model_id:'case2',qc_reasons:'a;b',reaction_force_N:-100}, + {model_id:'case10',qc_reasons:'a',reaction_force_N:''}, + {model_id:'case1',qc_reasons:'a;a',reaction_force_N:-50}]; +assert.deepEqual(sortIndices([0,1,2],rows,'patient'),[2,0,1]); +assert.deepEqual(sortIndices([0,1,2],rows,'flags'),[0,2,1]); +assert.deepEqual(sortIndices([0,1,2],rows,'reaction_force_N:asc'),[2,0,1]); +assert.deepEqual(sortIndices([0,1,2],rows,'reaction_force_N:desc'),[0,2,1]); +'''.replace('SCRIPT', json.dumps(str(script))) + subprocess.run([node, '-e', code], check=True) + output = gallery.generate_gallery([{'site':'spine', 'model_id':'case', + 'qc_status':'pass', 'qc_reasons':''}], tmp_path / 'gallery.html') + assert 'id="sort"' in output.read_text() + + def test_gallery_has_quick_decisions_on_each_card(tmp_path): from ogo.fea.gallery import generate_gallery output = tmp_path / 'gallery.html' diff --git a/tests/fea/test_sideways_fall.py b/tests/fea/test_sideways_fall.py index eb009b9..64bf1c3 100644 --- a/tests/fea/test_sideways_fall.py +++ b/tests/fea/test_sideways_fall.py @@ -120,3 +120,18 @@ def test_sideways_fall_fits_contact_canvas_before_generating_pmma(): assert "projected_material_disk_required_bounds" in called_names assert "fit_vtk_images_to_physical_bounds" in called_names + + +def test_each_proximal_disk_keeps_its_own_largest_component(): + pytest.importorskip("vtk") + pytest.importorskip("vtkbone") + from ogo.fea import femur + + tree = ast.parse(textwrap.dedent(inspect.getsource(femur.sidewaysFallFe))) + calls = [node for node in ast.walk(tree) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "generate_projected_material_disk_vtk"] + assert len(calls) == 2 + for call in calls: + options = {keyword.arg: keyword.value for keyword in call.keywords} + assert ast.literal_eval(options["keep_largest_component"]) is True From 0642bcf3be877866ca6136af931d44ee3db13b4a Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:33:46 -0600 Subject: [PATCH 55/67] Preserve gallery scroll position when saving review decisions --- ogo/fea/gallery_review.js | 20 +++++++++++++++----- tests/fea/test_review.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/ogo/fea/gallery_review.js b/ogo/fea/gallery_review.js index 8d38603..4ea14b8 100644 --- a/ogo/fea/gallery_review.js +++ b/ogo/fea/gallery_review.js @@ -82,7 +82,13 @@ function sortIndices(indices, rows, mode) { }); } -if (typeof module !== 'undefined') module.exports = {inclusionRecord, encodeCSV, parseCSV, validateReviews, imageVisible, sortIndices}; +function preserveScroll(action, viewport = window) { + const x = viewport.scrollX, y = viewport.scrollY; + action(); + viewport.scrollTo({left: x, top: y, behavior: 'instant'}); +} + +if (typeof module !== 'undefined') module.exports = {inclusionRecord, encodeCSV, parseCSV, validateReviews, imageVisible, sortIndices, preserveScroll}; if (typeof document !== 'undefined') { const data = JSON.parse(document.getElementById('review-data').textContent); @@ -107,8 +113,11 @@ if (typeof document !== 'undefined') { let visible = 0; const cards = [...document.querySelectorAll('article')]; const byIndex = new Map(cards.map(card => [Number(card.dataset.index), card])); - for (const index of sortIndices([...byIndex.keys()], rows, $('sort').value)) { - document.querySelector('main').append(byIndex.get(index)); + const main = document.querySelector('main'); + const ordered = sortIndices([...byIndex.keys()], rows, $('sort').value); + for (const [position, index] of ordered.entries()) { + const card = byIndex.get(index); + if (main.children[position] !== card) main.insertBefore(card, main.children[position] || null); } for (const card of document.querySelectorAll('article')) { const row = rows[Number(card.dataset.index)], record = inclusionRecord(row, decisions.get(decisionKey(row))); @@ -153,7 +162,8 @@ if (typeof document !== 'undefined') { manual_note: $('manual-note').value, reviewer: $('reviewer').value.trim() || 'unspecified', review_timestamp: new Date().toISOString()}; decisions.set(decisionKey(selected), inclusionRecord(selected, decision)); - persist(); refresh(); $('review-dialog').close(); + persist(); + preserveScroll(() => { $('review-dialog').close(); refresh(); }); } for (const card of document.querySelectorAll('article')) { for (const target of card.querySelectorAll('.review-open,img')) target.addEventListener('click', () => openReview(Number(card.dataset.index))); @@ -163,7 +173,7 @@ if (typeof document !== 'undefined') { manual_note: '', reviewer: $('reviewer').value.trim() || 'unspecified', review_timestamp: new Date().toISOString()}; decisions.set(decisionKey(row), inclusionRecord(row, decision)); - persist(); refresh(); + persist(); preserveScroll(refresh); }); } for (const id of ['search', 'site', 'status', 'reason', 'inclusion', 'stage', 'camera', 'sort']) $(id).addEventListener('input', refresh); diff --git a/tests/fea/test_review.py b/tests/fea/test_review.py index 21942de..caf440d 100644 --- a/tests/fea/test_review.py +++ b/tests/fea/test_review.py @@ -9,6 +9,26 @@ from ogo.fea.review import inclusion_row, load_decisions +def test_review_decision_restores_scroll_after_refresh(): + node = shutil.which('node') + if not node: + pytest.skip('Node is required') + from ogo.fea import gallery + script = Path(gallery.__file__).with_name('gallery_review.js') + code = '''const assert = require('node:assert/strict'); +const {preserveScroll} = require(SCRIPT); +const viewport = {scrollX: 15, scrollY: 2400, + scrollTo(options) { this.scrollX = options.left; this.scrollY = options.top; }}; +preserveScroll(() => { viewport.scrollY = 0; }, viewport); +assert.equal(viewport.scrollX, 15); +assert.equal(viewport.scrollY, 2400); +'''.replace('SCRIPT', json.dumps(str(script))) + subprocess.run([node, '-e', code], check=True) + source = script.read_text() + assert 'persist(); preserveScroll(refresh);' in source + assert "preserveScroll(() => { $('review-dialog').close(); refresh(); });" in source + + def test_gallery_sorting_is_numeric_and_puts_missing_results_last(tmp_path): from ogo.fea import gallery node = shutil.which('node') From 075852c253d4b304b977b0b522c0a74eea419b72 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 10:42:52 -0600 Subject: [PATCH 56/67] Integrate paginated FEA review galleries and reproducible cohort recovery example --- examples/fea/cohort_recovery/README.md | 44 ++++ .../build_manual_spine_gallery.py | 51 ++++ .../fea/cohort_recovery/recover_spines.py | 222 ++++++++++++++++++ ogo/fea/README.md | 18 +- ogo/fea/gallery.py | 49 ++-- ogo/fea/gallery_review.js | 137 ++++++++--- tests/fea/test_cohort_recovery.py | 31 +++ tests/fea/test_review.py | 58 ++++- tests/fea/test_validation.py | 3 +- 9 files changed, 557 insertions(+), 56 deletions(-) create mode 100644 examples/fea/cohort_recovery/README.md create mode 100644 examples/fea/cohort_recovery/build_manual_spine_gallery.py create mode 100644 examples/fea/cohort_recovery/recover_spines.py create mode 100644 tests/fea/test_cohort_recovery.py diff --git a/examples/fea/cohort_recovery/README.md b/examples/fea/cohort_recovery/README.md new file mode 100644 index 0000000..06195f7 --- /dev/null +++ b/examples/fea/cohort_recovery/README.md @@ -0,0 +1,44 @@ +# Spine Cohort Recovery + +This study-specific example resegments selected cases on full native HU CTs, +isolates L1 (vertebral-level label 20), and maps body 2 / process 1 into the +calibrated QCT's physical grid. Only then are inputs cropped with 15 mm padding. +It preserves original study outputs and builds a separate after-only review ZIP. +Successful solving is not evidence of anatomical validity: review the new models. + +Use a GPU compute node, never an HPC login node. Install Ogo and spine-segment +separately and use the validated segmentation model bundle for your study. + +```bash +python examples/fea/cohort_recovery/recover_spines.py \ + --root /scratch/study/spine_recovery \ + --review-csv /scratch/study/study_inclusion.csv \ + --selection manual-exclusions \ + --library /scratch/study/ct-library \ + --qct-dir /scratch/study/spine_QCT \ + --segment-cli /path/to/spine-segment \ + --model-bundle /path/to/model-bundle-pytorch \ + --faim-bin-dir /path/to/n88/bin \ + --gpu-workers 2 --fea-workers 4 +``` + +Use `--selection incomplete` for `incomplete_fea` cases not already manually +excluded. Use `--preflight` to check selected inputs without starting inference. +Native layout: `sub-ID/ses-1/ct/sub-ID_ses-1_ct.nii.gz`; QCT: `ID_QCT.nii.gz`; +review model ID: `ID_QCT_L1`. This example is not a generic BIDS discovery tool. + +Defaults reproduce the study recovery: four CPU threads per solve, full-bone +critical volume 12%, body critical volume 35%, critical strain 0.007. Registration +and supports use the checked-out Ogo defaults. The launch record saves the Ogo +commit and segmentation bundle manifest hash. Preserve the bundle and its source +release separately. Do not change code or bundle mid-batch. + +Per-case status and logs support resuming the same command. Use a new output root +when changing the selected cases or processing settings. Existing completed stages +are retained. Failures stay visible in `after_review.zip`; old manual decisions +are provenance only, not new inclusion decisions. `models/` contains n88models, +individual result and measurement CSVs, and before/after-solve WebP previews. + +Participant lists, trained weights, scans, and study results are not included in +this repository. Local delivery/download utilities are operational helpers rather +than part of the scientific model-generation pipeline. diff --git a/examples/fea/cohort_recovery/build_manual_spine_gallery.py b/examples/fea/cohort_recovery/build_manual_spine_gallery.py new file mode 100644 index 0000000..e4f50db --- /dev/null +++ b/examples/fea/cohort_recovery/build_manual_spine_gallery.py @@ -0,0 +1,51 @@ +"""Package selected rerun spine cases, with fresh manual-review decisions.""" +import csv +import json +from pathlib import Path +import sys + +from ogo.fea.gallery import export_gallery_zip +from ogo.fea.validation import evaluate + + +def main(root): + with (root / 'rerun_selection.csv').open() as stream: + selected = list(csv.DictReader(stream)) + expected = len(selected) + rows = [] + for original in selected: + stem = original['model_id'] + subject = stem.removesuffix('_QCT_L1') + model = root / 'models' / subject / (stem + '.n88model') + metric = model.with_name(stem + '_qc_metrics.csv') + row = {} + if metric.is_file(): + with metric.open() as stream: + row = next(csv.DictReader(stream)) + row.update(model_id=stem, site='spine', model_path=str(model)) + status = json.loads((root / 'cases' / subject / 'status.json').read_text()) + row['rerun_stage'] = status['stage'] + row['previous_manual_decision'] = original['manual_decision'] + row['rerun_error'] = status.get('error', '') + if status['stage'] != 'complete': + row['generation_status'] = 'construction_failed' + for suffix, field in [('_qc_3d.webp', 'anatomy_image'), ('_sed_3d.webp', 'sed_image')]: + image = model.with_name(stem + suffix) + row[field] = str(image) if image.is_file() else '' + result = model.with_name(stem + '_results.csv') + if status['stage'] == 'complete' and result.is_file(): + with result.open() as stream: + row.update(next(csv.DictReader(stream))) + row = evaluate(row) + if status['stage'] != 'complete': + row.update(qc_status='fail', qc_reasons='rerun_failed;' + row['qc_reasons']) + rows.append(row) + if len(rows) != expected: + raise ValueError(f'Rerun gallery must retain all {expected} cases, including failures') + export_gallery_zip(rows, root / 'after_review.zip') + (root / 'gallery_ready.json').write_text(json.dumps({'cases': expected, 'zip': 'after_review.zip'})) + print(f'After-only gallery packaged: {expected} cases; original decisions not imported', flush=True) + + +if __name__ == '__main__': + main(Path(sys.argv[1])) diff --git a/examples/fea/cohort_recovery/recover_spines.py b/examples/fea/cohort_recovery/recover_spines.py new file mode 100644 index 0000000..37fa8e3 --- /dev/null +++ b/examples/fea/cohort_recovery/recover_spines.py @@ -0,0 +1,222 @@ +"""Resegment manually excluded L1 cases without replacing selected study models. + +Run on Groot, not an ARC login node. Two GPU workers segment full native CTs; +four CPU workers generate and solve the isolated L1 models. Completed stages +are resumable, and original review decisions are retained as provenance only. +""" + +import argparse +import csv +import fcntl +import hashlib +import json +import os +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path +import subprocess +import time +import sys + +import nibabel as nib +import numpy as np +import SimpleITK as sitk + +PYTHON = sys.executable +SOURCE = str(Path(__file__).resolve().parents[3]) +SEGMENT = BUNDLE = FAIM = LIBRARY = QCT = None + + +def save_json(path, value): + temporary = path.with_suffix('.tmp') + temporary.write_text(json.dumps(value, indent=2) + '\n') + temporary.replace(path) + + +def isolate_l1(level, parts, level_affine, parts_affine): + """The level map identifies L1=20; the parts map uses body=2/process=1.""" + if level.shape != parts.shape or not np.allclose(level_affine, parts_affine, atol=1e-5): + raise ValueError('Vertebral-level and body/process grids do not match') + selected = level == 20 + if set(np.unique(parts[selected])) - {0, 1, 2}: + raise ValueError('Unexpected process/body labels inside L1') + result = np.where(selected, parts, 0).astype(np.uint8) + if not np.any(result == 2) or not np.any(result == 1): + raise ValueError('L1 body or process is missing; do not model other levels') + return result + + +def run(command, log, environment): + with log.open('a') as stream: + stream.write('\n' + json.dumps(command) + '\n') + stream.flush() + subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, + env=environment, check=True) + + +def inputs(subject): + native = LIBRARY / f'sub-{subject}/ses-1/ct/sub-{subject}_ses-1_ct.nii.gz' + qct = QCT / f'{subject}_QCT.nii.gz' + for path in (native, qct): + if not path.is_file(): + raise FileNotFoundError(path) + return native, qct + + +def segment_case(row, root, gpu): + subject = row['model_id'].removesuffix('_QCT_L1') + folder = root / 'cases' / subject + folder.mkdir(parents=True, exist_ok=True) + status_path = folder / 'status.json' + status = json.loads(status_path.read_text()) if status_path.exists() else {'subject': subject} + if status.get('stage') in ('segmented', 'complete', 'fea_failed'): + return row + native, qct = inputs(subject) + status.update(stage='segmenting', original_review=row, native_ct=str(native), + calibrated_qct=str(qct), model_bundle=BUNDLE, started=time.time()) + save_json(status_path, status) + environment = dict(os.environ, CUDA_VISIBLE_DEVICES=str(gpu), OMP_NUM_THREADS='4', + OPENBLAS_NUM_THREADS='1', MKL_NUM_THREADS='1') + try: + output = folder / 'segmentation' + output.mkdir(exist_ok=True) + run([SEGMENT, '--output', str(output), '--device', 'cuda', + '--model-bundle', BUNDLE, '--overwrite', str(native)], folder / 'segmentation.log', environment) + stem = native.name.removesuffix('.nii.gz') + level = nib.load(output / f'{stem}_vertebral-level.nii.gz') + parts = nib.load(output / f'{stem}_process-body.nii.gz') + isolated = isolate_l1(np.asarray(level.dataobj), np.asarray(parts.dataobj), + level.affine, parts.affine) + native_header = nib.load(native) + if parts.shape != native_header.shape or not np.allclose(parts.affine, native_header.affine, atol=1e-5): + raise ValueError('Segmentation does not preserve the native CT grid') + label_path = folder / 'L1_native_body2_process1.nii.gz' + nib.save(nib.Nifti1Image(isolated, parts.affine), label_path) + reference = sitk.ReadImage(str(qct)) + labels = sitk.Resample(sitk.ReadImage(str(label_path)), reference, + sitk.Transform(), sitk.sitkNearestNeighbor, 0, sitk.sitkUInt8) + full_path = folder / 'L1_qct_full_body2_process1.nii.gz' + sitk.WriteImage(labels, str(full_path)) + array = sitk.GetArrayFromImage(labels) + if not np.any(array == 2) or not np.any(array == 1): + raise ValueError('L1 does not overlap calibrated QCT') + # Crop only after full-size inference and physical-coordinate mapping. + coordinates = np.argwhere(array > 0)[:, ::-1] + margin = np.ceil(15 / np.array(reference.GetSpacing())).astype(int) + start = np.maximum(0, coordinates.min(axis=0) - margin) + end = np.minimum(reference.GetSize(), coordinates.max(axis=0) + margin + 1) + size = (end - start).tolist() + sitk.WriteImage(sitk.RegionOfInterest(reference, size, start.tolist()), + str(folder / f'{subject}_QCT.nii.gz')) + sitk.WriteImage(sitk.RegionOfInterest(labels, size, start.tolist()), + str(folder / f'{subject}_labels.nii.gz')) + status.update(stage='segmented', body_voxels=int((array == 2).sum()), + process_voxels=int((array == 1).sum()), crop_start_xyz=start.tolist(), + crop_size_xyz=size, native_shape=list(parts.shape), + qct_size=list(reference.GetSize())) + except Exception as error: + status.update(stage='segmentation_failed', error=str(error)) + save_json(status_path, status) + return row + + +def solve_case(row, root): + subject = row['model_id'].removesuffix('_QCT_L1') + folder = root / 'cases' / subject + status_path = folder / 'status.json' + status = json.loads(status_path.read_text()) + if status['stage'] not in ('segmented', 'fea_failed', 'solving'): + return + output = root / 'models' / subject + output.mkdir(parents=True, exist_ok=True) + environment = dict(os.environ, PYTHONPATH=SOURCE, OMP_NUM_THREADS='4', + OPENBLAS_NUM_THREADS='1', MKL_NUM_THREADS='1', + PATH=str(Path(PYTHON).parent) + os.pathsep + os.environ['PATH']) + command = [PYTHON, '-m', 'ogo.cli.GenerateFEM', 'spine', + str(folder / f'{subject}_QCT.nii.gz'), str(folder / f'{subject}_labels.nii.gz'), + '--vertebra', 'L1:2:1', '--pistoia_mask_label', '2', '--output_path', str(output), + '--threads', '4', '--faim_bin_dir', FAIM, '--run_pistoia', '--require_pistoia', + '--critical_volume', '12', '--masked_critical_volume', '35', '--critical_strain', '0.007'] + status.update(stage='solving', fea_command=command, ogo_source=SOURCE) + save_json(status_path, status) + try: + run(command, folder / 'fea.log', environment) + prefix = output / f'{subject}_QCT_L1' + for suffix in ('.n88model', '_results.csv', '_qc_metrics.csv', '_qc_3d.webp', '_sed_3d.webp'): + if not Path(str(prefix) + suffix).is_file(): + raise FileNotFoundError(str(prefix) + suffix) + status.update(stage='complete', finished=time.time()) + status.pop('error', None) + except Exception as error: + status.update(stage='fea_failed', error=str(error)) + save_json(status_path, status) + + +def main(): + global SEGMENT, BUNDLE, FAIM, LIBRARY, QCT + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--root', type=Path, required=True) + parser.add_argument('--review-csv', type=Path, required=True) + parser.add_argument('--selection', choices=('manual-exclusions', 'incomplete'), default='manual-exclusions') + parser.add_argument('--library', type=Path, required=True) + parser.add_argument('--qct-dir', type=Path, required=True) + parser.add_argument('--segment-cli', required=True) + parser.add_argument('--model-bundle', required=True) + parser.add_argument('--faim-bin-dir', required=True) + parser.add_argument('--gpu-workers', type=int, choices=(1, 2), default=2) + parser.add_argument('--fea-workers', type=int, default=4) + parser.add_argument('--preflight', action='store_true') + args = parser.parse_args() + LIBRARY, QCT = args.library, args.qct_dir + SEGMENT, BUNDLE, FAIM = args.segment_cli, args.model_bundle, args.faim_bin_dir + if args.fea_workers < 1: + parser.error('--fea-workers must be positive') + root = args.root + with args.review_csv.open() as stream: + rows = [row for row in csv.DictReader(stream) + if row['site'] == 'spine' and ( + row['manual_decision'] == 'exclude' if args.selection == 'manual-exclusions' else + row['manual_decision'] != 'exclude' and + 'incomplete_fea' in row['automatic_qc_reasons'].split(';'))] + if not rows or len({row['model_id'] for row in rows}) != len(rows): + raise ValueError('Selection must contain nonempty, unique spine model IDs') + for row in rows: + inputs(row['model_id'].removesuffix('_QCT_L1')) + if args.preflight: + print(f'All {len(rows)} full-size native CTs and calibrated QCTs exist', flush=True) + return + root.mkdir(parents=True, exist_ok=True) + with (root / 'coordinator.lock').open('w') as lock: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + manifest = root / 'rerun_selection.csv' + if manifest.exists(): + with manifest.open() as stream: + saved = list(csv.DictReader(stream)) + if saved != rows: + raise ValueError('Selection changed; use a new recovery output root') + with manifest.open('w', newline='') as stream: + writer = csv.DictWriter(stream, fieldnames=list(rows[0])) + writer.writeheader() + writer.writerows(rows) + commit = subprocess.check_output(['git', '-C', SOURCE, 'rev-parse', 'HEAD'], text=True).strip() + save_json(root / 'launch.json', {'pid': os.getpid(), 'started': time.time(), 'cases': len(rows), + 'ogo_commit': commit, 'segmentation_cli': SEGMENT, 'selection': args.selection, + 'bundle_manifest_sha256': hashlib.sha256((Path(BUNDLE) / 'manifest.json').read_bytes()).hexdigest()}) + def segment_chunk(gpu): + for row in rows[gpu::args.gpu_workers]: + segment_case(row, root, gpu) + print('Segmentation', row['model_id'], flush=True) + with ThreadPoolExecutor(max_workers=args.gpu_workers) as pool: + list(pool.map(segment_chunk, range(args.gpu_workers))) + with ThreadPoolExecutor(max_workers=args.fea_workers) as pool: + list(pool.map(lambda row: solve_case(row, root), rows)) + states = [json.loads(path.read_text()) for path in (root / 'cases').glob('*/status.json')] + save_json(root / 'summary.json', {'cases': len(states), 'stages': { + stage: sum(item['stage'] == stage for item in states) + for stage in sorted({item['stage'] for item in states})}}) + run([PYTHON, str(Path(__file__).with_name('build_manual_spine_gallery.py')), str(root)], + root / 'gallery.log', dict(os.environ, PYTHONPATH=SOURCE)) + print('Rerun complete; build the after-only review gallery', flush=True) + + +if __name__ == '__main__': + main() diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 4f3c9a3..39331a3 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -92,7 +92,7 @@ is retained as a review row with its reconstruction error, not silently dropped. The command writes `qc_summary.csv`; `--gallery` additionally writes a standalone `gallery.html`. Without a refreshed editable installation, use `python -m ogo.cli.ValidateFEA`. -The gallery supports anatomy, QC status, reason, subject search, and anatomy/SED +The gallery supports anatomy, QC status, subject search, and anatomy/SED view selection, with expandable measurement tables. A separate camera selector shows oblique/side, top, bottom, or all views in both thumbnails and the review dialog. The fixed three-row Ogo renders are split once into cached WebP images in @@ -100,6 +100,15 @@ dialog. The fixed three-row Ogo renders are split once into cached WebP images i remain external; retain their relative location and the view-assets folder when copying the gallery and models together. +The initial page shows 50 models; select 100, 250, or 500 per page. Only that page +is rendered. Cached 420-pixel WebP thumbnails are loaded ahead of scrolling; +opening Review uses the original-resolution view. Measurement tables are built +only when expanded. Export always includes the entire cohort, not just the page. +The Exclude flags dropdown has checkboxes: checking a flag hides matching cases +without changing their QC or study decisions. "Not manually reviewed" refers to +cases without an explicit manual include/exclude decision. Include/exclude saves +preserve your scroll position. Filtering can remove a card from the current page. + Sort by patient ID, number of QC flags, or stiffness, reaction force and full or regional failure load (lowest/highest first). Force sorting uses absolute magnitudes; missing outcomes are last. Sorting never changes inclusion. Manual @@ -112,12 +121,17 @@ ogoValidateFEA derivatives/fea --output derivatives/fea/qc --gallery-zip --worke ``` Extract `gallery.zip` and open `gallery.html`. It includes `images/`, -`gallery_views/`, `qc_summary.csv`, and the initial `study_inclusion.csv`. +`gallery_views/`, `gallery_thumbnails/`, `qc_summary.csv`, and the initial `study_inclusion.csv`. No original model directory or server is needed to review it. Model files are not included; original model paths remain in the CSV as provenance. Legacy PNGs are converted to compact WebP images inside the ZIP without modifying their originals. Both solve stages and all available camera views are retained. Missing images remain missing. Large cohorts use lazy-loaded image files rather than embedded HTML. + +For study-specific full-size L1 resegmentation and recovery of excluded or +incomplete cases, see [the cohort recovery example](../../examples/fea/cohort_recovery/README.md). +It preserves prior results and builds a fresh review gallery; it does not +automatically reinstate rerun cases. After review, export the updated inclusion CSV from the gallery; browser decisions cannot rewrite the CSV inside the original ZIP. The export popup directs users to browser downloads and explicitly states that diff --git a/ogo/fea/gallery.py b/ogo/fea/gallery.py index 90794b1..6b54d83 100644 --- a/ogo/fea/gallery.py +++ b/ogo/fea/gallery.py @@ -53,7 +53,7 @@ def generate_gallery(rows, output, decisions=None, image_root=None): for key in outcome_keys: row.setdefault(key, records[0].get(key, '')) decisions = decisions or {} - cards = [] + image_groups = [] escape = lambda value: html.escape(str(value), quote=True) for index, row in enumerate(rows): images = [] @@ -65,14 +65,24 @@ def generate_gallery(rows, output, decisions=None, image_root=None): cameras = _camera_images(path, output) for view, asset in [('all', path), *cameras]: relative = quote(Path(os.path.relpath(asset, output.parent)).as_posix()) - source = f'src="{escape(relative)}"' - fallback = ' data-fallback="true"' if not cameras else '' - images.append(f'') - measurements = ''.join(f'{escape(key)}{escape(value)}' - for key, value in sorted(row.items()) if not key.endswith('_image')) - cards.append(f'

{"".join(images)}
Measurements{measurements}
') + from PIL import Image + directory = output.parent / (output.stem + '_thumbnails') + directory.mkdir(exist_ok=True) + stat = asset.stat() + key = hashlib.sha256(f'{asset.resolve()}:{stat.st_size}:{stat.st_mtime_ns}'.encode()).hexdigest()[:20] + thumbnail = directory / f'{key}.webp' + if not thumbnail.exists(): + with Image.open(asset) as image: + image.thumbnail((420, 560)) + image.convert('RGB').save(thumbnail, 'WEBP', quality=75, method=2) + images.append({'stage': stage, 'view': view, 'src': relative, + 'thumbnail': quote(Path(os.path.relpath(thumbnail, output.parent)).as_posix()), + 'fallback': 'true' if not cameras else ''}) + image_groups.append(images) reasons = sorted({reason for row in rows for reason in row['qc_reasons'].split(';') if reason}) - options = ''.join(f'' for reason in reasons) + reasons = ['manual_review_pending', 'too_short'] + [r for r in reasons if r not in ('manual_review_pending', 'too_short')] + labels = {'manual_review_pending': 'Not manually reviewed', 'too_short': 'Too short'} + options = ''.join(f'' for reason in reasons) manual_options = ''.join(f'' for reason in REASONS) sort_options = '' for key, name in [('stiffness_N_per_mm', 'Stiffness'), ('reaction_force_N', 'Reaction force'), @@ -83,7 +93,15 @@ def generate_gallery(rows, output, decisions=None, image_root=None): seeds = [inclusion_row(row, decisions.get(identity(row))) for row in rows] serialized = json.dumps(rows, sort_keys=True) fingerprint = hashlib.sha256(serialized.encode()).hexdigest() - data = json.dumps({'rows': rows, 'seeds': seeds, 'fields': FIELDS, 'reasons': REASONS, + columns, column_ids, values = [], {}, [] + for row in rows: + keys = tuple(row) + if keys not in column_ids: + column_ids[keys] = len(columns) + columns.append(keys) + values.append([column_ids[keys], list(row.values())]) + data = json.dumps({'rowColumns': columns, 'rowValues': values, 'images': image_groups, + 'seeds': seeds, 'fields': FIELDS, 'reasons': REASONS, 'storageKey': 'ogo-fea-review-v1-' + fingerprint, 'imported': bool(decisions)}).replace('<', '\\u003c') script = Path(__file__).with_name('gallery_review.js').read_text() @@ -92,12 +110,13 @@ def generate_gallery(rows, output, decisions=None, image_root=None):
- +
Exclude flags
REASONS
-

-
CARDS

+

+
+

@@ -105,8 +124,10 @@ def generate_gallery(rows, output, decisions=None, image_root=None): ''' # Replace once so record text cannot become another template token. tokens = {'SCRIPT': script, 'MANUAL_OPTIONS': manual_options, 'REASONS': options, 'SORT_OPTIONS': sort_options, - 'CARDS': ''.join(cards), 'DATA': data} - content = re.sub(r'SCRIPT|MANUAL_OPTIONS|REASONS|SORT_OPTIONS|CARDS|DATA', lambda match: tokens[match[0]], content) + 'DATA': data} + content = re.sub(r'SCRIPT|MANUAL_OPTIONS|REASONS|SORT_OPTIONS|DATA', lambda match: tokens[match[0]], content) + menu_style = '#reason-picker{position:relative}#reason-picker summary{cursor:pointer;border:1px solid #999;padding:6px}#reason-picker .flag-menu{position:absolute;top:100%;left:0;width:320px;max-width:85vw;max-height:55vh;overflow:auto;background:white;border:1px solid #aaa;padding:10px;z-index:3}#reason-picker label{display:flex;gap:6px;padding:5px 0}' + content = content.replace('', menu_style + '', 1) output.write_text(content) return output diff --git a/ogo/fea/gallery_review.js b/ogo/fea/gallery_review.js index 4ea14b8..136f00d 100644 --- a/ogo/fea/gallery_review.js +++ b/ogo/fea/gallery_review.js @@ -65,6 +65,13 @@ function imageVisible(image, stage, camera) { return image.stage === stage && (image.view === camera || image.fallback === 'true'); } +function pageIndices(matches, page, size) { return matches.slice(page * size, (page + 1) * size); } + +function excludedByFlags(row, decision, flags) { + if (flags.has('manual_review_pending') && (!decision.manual_decision || decision.manual_decision === 'automatic')) return true; + return (row.qc_reasons || '').split(';').some(reason => flags.has(reason)); +} + function sortIndices(indices, rows, mode) { const patient = (a, b) => rows[a].model_id.localeCompare(rows[b].model_id, undefined, {numeric: true}); const count = row => new Set((row.qc_reasons || '').split(';').filter(Boolean)).size; @@ -88,12 +95,22 @@ function preserveScroll(action, viewport = window) { viewport.scrollTo({left: x, top: y, behavior: 'instant'}); } -if (typeof module !== 'undefined') module.exports = {inclusionRecord, encodeCSV, parseCSV, validateReviews, imageVisible, sortIndices, preserveScroll}; +if (typeof module !== 'undefined') module.exports = {inclusionRecord, encodeCSV, parseCSV, validateReviews, imageVisible, pageIndices, excludedByFlags, sortIndices, preserveScroll}; if (typeof document !== 'undefined') { const data = JSON.parse(document.getElementById('review-data').textContent); - const rows = data.rows, $ = id => document.getElementById(id); + const rows = data.rowValues.map(([group, values]) => Object.fromEntries(data.rowColumns[group].map((key, i) => [key, values[i]]))); + for (const row of rows) Object.assign(row, (data.outcomes || {})[row.model_id] || {}); + const $ = id => document.getElementById(id); let decisions = validateReviews(data.seeds, rows), selected = null, autosave = true; + let page = 0; + const previewObserver = typeof IntersectionObserver === 'undefined' ? null : new IntersectionObserver(entries => { + for (const entry of entries) { + if (!entry.isIntersecting || entry.target.hidden) continue; + entry.target.src = entry.target.dataset.thumbnail || entry.target.dataset.src; + previewObserver.unobserve(entry.target); + } + }, {rootMargin: '1200px 0px'}); const message = text => { $('message').textContent = text; }; try { const saved = localStorage.getItem(data.storageKey); @@ -103,23 +120,70 @@ if (typeof document !== 'undefined') { try { localStorage.setItem(data.storageKey, JSON.stringify([...decisions.values()])); } catch (error) { autosave = false; } } - function updateImages(container) { + function updateImages(container, full = false) { for (const image of container.querySelectorAll('img')) { image.hidden = !imageVisible(image.dataset, $('stage').value, $('camera').value); image.style.height = $('camera').value === 'all' || image.dataset.fallback === 'true' ? '560px' : '280px'; + if (!image.hidden) { + if (full || !previewObserver) image.src = full ? image.dataset.src : (image.dataset.thumbnail || image.dataset.src); + else previewObserver.observe(image); + } else { + if (previewObserver) previewObserver.unobserve(image); + image.removeAttribute('src'); + } } } - function refresh() { - let visible = 0; - const cards = [...document.querySelectorAll('article')]; - const byIndex = new Map(cards.map(card => [Number(card.dataset.index), card])); - const main = document.querySelector('main'); - const ordered = sortIndices([...byIndex.keys()], rows, $('sort').value); - for (const [position, index] of ordered.entries()) { - const card = byIndex.get(index); - if (main.children[position] !== card) main.insertBefore(card, main.children[position] || null); + function measurementTable(row) { + const table = document.createElement('table'); + for (const [key, value] of Object.entries(row).sort(([a], [b]) => a.localeCompare(b))) { + if (key.endsWith('_image')) continue; + const tr = table.insertRow(), th = document.createElement('th'); + th.textContent = key; tr.append(th); tr.insertCell().textContent = value; } - for (const card of document.querySelectorAll('article')) { + return table; + } + function imagesFor(index) { + return (data.images[index] || []).map(asset => { + const image = document.createElement('img'); + Object.assign(image.dataset, asset); + image.loading = 'eager'; image.decoding = 'async'; + image.alt = asset.stage + ': ' + asset.view; + image.hidden = true; + return image; + }); + } + function makeCard(index) { + const card = document.createElement('article'); + card.dataset.index = index; + card.innerHTML = '

Measurements
'; + card.querySelector('.review-open').textContent = rows[index].model_id; + card.append(...imagesFor(index)); + card.querySelector('details').addEventListener('toggle', event => { + if (event.target.open && !event.target.querySelector('table')) event.target.append(measurementTable(rows[index])); + }); + return card; + } + function refresh() { + const pageSize = Number($('page-size').value); + const search = $('search').value.toLowerCase(); + const excluded = new Set([...$('reason-picker').querySelectorAll('input:checked')].map(input => input.dataset.qcReason)); + $('reason-summary').textContent = 'Exclude flags' + (excluded.size ? ' (' + excluded.size + ')' : ''); + let matches = []; + rows.forEach((row, index) => { + if (!row.model_id.toLowerCase().includes(search) || + ($('site').value && row.site !== $('site').value) || + ($('status').value && row.qc_status !== $('status').value) || + excludedByFlags(row, decisions.get(decisionKey(row)) || {}, excluded) || + ($('inclusion').value && inclusionRecord(row, decisions.get(decisionKey(row))).final_inclusion !== $('inclusion').value)) return; + matches.push(index); + }); + matches = sortIndices(matches, rows, $('sort').value); + const pages = Math.max(1, Math.ceil(matches.length / pageSize)); + page = Math.min(page, pages - 1); + if (previewObserver) previewObserver.disconnect(); + const cards = pageIndices(matches, page, pageSize).map(makeCard); + document.querySelector('main').replaceChildren(...cards); + for (const card of cards) { const row = rows[Number(card.dataset.index)], record = inclusionRecord(row, decisions.get(decisionKey(row))); card.querySelector('.automatic').textContent = 'Automatic: ' + row.qc_status + (row.qc_reasons ? ': ' + row.qc_reasons : ''); card.querySelector('.decision').textContent = record.manual_decision === 'exclude' ? 'Manually excluded' : 'Study: ' + record.final_inclusion + (record.manual_decision !== 'automatic' ? ' (manual)' : ''); @@ -127,18 +191,15 @@ if (typeof document !== 'undefined') { button.setAttribute('aria-pressed', String(record.manual_decision === button.dataset.decision)); button.style.fontWeight = record.manual_decision === button.dataset.decision ? '700' : '400'; } - card.hidden = !row.model_id.toLowerCase().includes($('search').value.toLowerCase()) || - ($('site').value && row.site !== $('site').value) || - ($('status').value && row.qc_status !== $('status').value) || - ($('reason').value && !row.qc_reasons.split(';').includes($('reason').value)) || - ($('inclusion').value && record.final_inclusion !== $('inclusion').value); - if (!card.hidden) visible++; updateImages(card); } + $('previous-page').disabled = page === 0; + $('next-page').disabled = page === pages - 1; + $('page-status').textContent = 'Page ' + (page + 1) + ' / ' + pages; $('review-stage').value = $('stage').value; $('review-camera').value = $('camera').value; - updateImages($('review-images')); - $('message').textContent = visible + ' / ' + rows.length + ' models. ' + (autosave ? '' : 'Browser autosave unavailable. ') + 'Export CSV to preserve the study record.'; + updateImages($('review-images'), true); + $('message').textContent = matches.length + ' / ' + rows.length + ' models. ' + (autosave ? '' : 'Browser autosave unavailable. ') + 'Export CSV to preserve the study record.'; } function openReview(index) { selected = rows[index]; @@ -149,12 +210,9 @@ if (typeof document !== 'undefined') { $('manual-reason').value = data.reasons.includes(record.manual_reason) ? record.manual_reason : 'other'; if (!record.manual_reason) $('manual-reason').value = 'manual_review'; $('manual-note').value = record.manual_note; - const card = document.querySelector('article[data-index="' + index + '"]'); - $('review-images').replaceChildren(...[...card.querySelectorAll('img')].map(image => { - return image.cloneNode(); - })); - updateImages($('review-images')); - $('review-measurements').replaceChildren(card.querySelector('details').cloneNode(true)); + $('review-images').replaceChildren(...imagesFor(index)); + updateImages($('review-images'), true); + $('review-measurements').replaceChildren(measurementTable(selected)); $('review-dialog').showModal(); } function decide(manual) { @@ -165,18 +223,31 @@ if (typeof document !== 'undefined') { persist(); preserveScroll(() => { $('review-dialog').close(); refresh(); }); } - for (const card of document.querySelectorAll('article')) { - for (const target of card.querySelectorAll('.review-open,img')) target.addEventListener('click', () => openReview(Number(card.dataset.index))); - for (const button of card.querySelectorAll('.quick-decision')) button.addEventListener('click', () => { + document.querySelector('main').addEventListener('click', event => { + const card = event.target.closest('article'); + if (!card) return; + if (event.target.closest('.review-open,img')) openReview(Number(card.dataset.index)); + const button = event.target.closest('.quick-decision'); + if (button) { const row = rows[Number(card.dataset.index)]; const decision = {manual_decision: button.dataset.decision, manual_reason: 'manual_review', manual_note: '', reviewer: $('reviewer').value.trim() || 'unspecified', review_timestamp: new Date().toISOString()}; decisions.set(decisionKey(row), inclusionRecord(row, decision)); persist(); preserveScroll(refresh); - }); - } - for (const id of ['search', 'site', 'status', 'reason', 'inclusion', 'stage', 'camera', 'sort']) $(id).addEventListener('input', refresh); + } + }); + $('previous-page').onclick = () => { page--; refresh(); window.scrollTo(0, 0); }; + $('next-page').onclick = () => { page++; refresh(); window.scrollTo(0, 0); }; + for (const id of ['search', 'site', 'status', 'inclusion', 'stage', 'camera', 'page-size', 'sort']) $(id).addEventListener('input', () => { page = 0; refresh(); }); + $('reason-picker').addEventListener('change', () => { page = 0; refresh(); }); + $('clear-flags').onclick = () => { + for (const input of $('reason-picker').querySelectorAll('input')) input.checked = false; + page = 0; refresh(); + }; + document.addEventListener('keydown', event => { + if (event.key === 'Escape') $('reason-picker').open = false; + }); for (const id of ['stage', 'camera']) $('review-' + id).addEventListener('change', () => { $(id).value = $('review-' + id).value; refresh(); diff --git a/tests/fea/test_cohort_recovery.py b/tests/fea/test_cohort_recovery.py new file mode 100644 index 0000000..6047a17 --- /dev/null +++ b/tests/fea/test_cohort_recovery.py @@ -0,0 +1,31 @@ +"""Verify the study example never substitutes neighboring vertebral levels.""" +import importlib.util +from pathlib import Path + +import numpy as np +import pytest + +pytest.importorskip('SimpleITK') +source = Path(__file__).resolve().parents[2] / 'examples/fea/cohort_recovery/recover_spines.py' +spec = importlib.util.spec_from_file_location('recover_spines', source) +recovery = importlib.util.module_from_spec(spec) +spec.loader.exec_module(recovery) + + +def test_only_l1_body_and_process_are_retained(): + level = np.array([20, 20, 19, 21]) + parts = np.array([2, 1, 2, 1]) + np.testing.assert_array_equal(recovery.isolate_l1(level, parts, np.eye(4), np.eye(4)), [2, 1, 0, 0]) + + +def test_mismatched_affine_is_rejected(): + shifted = np.eye(4) + shifted[0, 3] = 2 + with pytest.raises(ValueError, match='grids'): + recovery.isolate_l1(np.array([20, 20]), np.array([2, 1]), np.eye(4), shifted) + + +def test_missing_l1_and_unknown_part_are_rejected(): + for level, parts in [([19, 21], [2, 1]), ([20, 20], [2, 3])]: + with pytest.raises(ValueError): + recovery.isolate_l1(np.array(level), np.array(parts), np.eye(4), np.eye(4)) diff --git a/tests/fea/test_review.py b/tests/fea/test_review.py index caf440d..701617f 100644 --- a/tests/fea/test_review.py +++ b/tests/fea/test_review.py @@ -9,6 +9,51 @@ from ogo.fea.review import inclusion_row, load_decisions +def gallery_data(content): + import re + return json.loads(re.search(r'id="review-data">(.*?)', content, re.S)[1]) + + +def test_gallery_pages_filters_and_compact_data(tmp_path): + from ogo.fea.gallery import generate_gallery + rows = [{'site': 'spine', 'model_id': f'case{i}', 'qc_status': 'pass', 'qc_reasons': ''} + for i in range(501)] + content = generate_gallery(rows, tmp_path / 'gallery.html').read_text() + data = gallery_data(content) + restored = [dict(zip(data['rowColumns'][group], values)) for group, values in data['rowValues']] + assert restored == rows + assert len(data['seeds']) == 501 + assert '
' in content + assert 'id="page-size"' in content + assert 'value="500"' in content + assert 'data-qc-reason="manual_review_pending"' in content + assert 'data-qc-reason="too_short"' in content + assert "rootMargin: '1200px 0px'" in content + + +def test_gallery_pagination_and_flags_do_not_change_decisions(): + node = shutil.which('node') + if not node: + pytest.skip('Node is required') + from ogo.fea import gallery + script = Path(gallery.__file__).with_name('gallery_review.js') + code = '''const assert = require('node:assert/strict'); +const {pageIndices, excludedByFlags, inclusionRecord} = require(SCRIPT); +const ids = Array.from({length:501}, (_, i) => i); +assert.equal(pageIndices(ids,0,50).length,50); +assert.equal(pageIndices(ids,0,500).length,500); +assert.deepEqual(pageIndices(ids,1,500),[500]); +const row = {site:'hip',model_id:'case',qc_status:'fail',qc_reasons:'too_short'}; +const decision = {manual_decision:'include'}; +const before = inclusionRecord(row,decision); +assert.equal(excludedByFlags(row,decision,new Set(['too_short'])),true); +assert.equal(excludedByFlags(row,decision,new Set(['manual_review_pending'])),false); +assert.equal(excludedByFlags(row,{},new Set(['manual_review_pending'])),true); +assert.deepEqual(inclusionRecord(row,decision),before); +'''.replace('SCRIPT', json.dumps(str(script))) + subprocess.run([node, '-e', code], check=True) + + def test_review_decision_restores_scroll_after_refresh(): node = shutil.which('node') if not node: @@ -170,7 +215,7 @@ def test_gallery_individual_views_keep_panel_order_and_cache(tmp_path): assert 'id="review-camera"' in content assert 'id="review-stage"' in content for view, color in zip(('oblique', 'top', 'bottom'), ((255, 0, 0), (0, 128, 0), (0, 0, 255))): - assert f'data-view="{view}"' in content + assert any(item['view'] == view for item in gallery_data(content)['images'][0]) asset = next((tmp_path / 'gallery_views').glob(f'*_{view}.webp')) with Image.open(asset) as cropped: assert cropped.size == (1400, 850) @@ -188,7 +233,7 @@ def test_gallery_unknown_montage_is_not_split(tmp_path): row = {'site': 'spine', 'model_id': 'legacy', 'qc_status': 'review', 'qc_reasons': '', 'anatomy_image': str(image)} content = generate_gallery([row], tmp_path / 'gallery.html').read_text() - assert 'data-fallback="true"' in content + assert gallery_data(content)['images'][0][0]['fallback'] == 'true' assert not (tmp_path / 'gallery_views').exists() @@ -208,10 +253,11 @@ def test_gallery_zip_is_portable_and_has_review_records(tmp_path): assert not any(name.endswith('.png') for name in archive.namelist()) assert archive.testzip() is None content = archive.read('gallery.html').decode() - assert 'src="images/' in content + assert gallery_data(content)['images'][0][0]['src'].startswith('images/') assert str(tmp_path) not in content assert 'data:image' not in content archive.extractall(tmp_path / 'unpacked') - import re - for source in re.findall(r'src="([^"]+)"', content): - assert (tmp_path / 'unpacked' / source).is_file() + for group in gallery_data(content)['images']: + for asset in group: + for key in ('src', 'thumbnail'): + assert (tmp_path / 'unpacked' / asset[key]).is_file() diff --git a/tests/fea/test_validation.py b/tests/fea/test_validation.py index 1563e84..32931dd 100644 --- a/tests/fea/test_validation.py +++ b/tests/fea/test_validation.py @@ -68,7 +68,8 @@ def test_csv_and_gallery_escape_subject_names(tmp_path): output = tmp_path / 'gallery.html' generate_gallery([evaluate(row)], output) content = output.read_text() - assert '<script>bad</script>' in content + assert '\\u003cscript>bad\\u003c/script>' in content + assert '' not in content assert 'too_short' in content assert 'loading="lazy"' not in content # No fabricated image for a missing preview. From bda0243a41ddd0877565937d8194cd03e9089b65 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:50:44 -0600 Subject: [PATCH 57/67] Write compact three-view FEA debug previews --- ogo/fea/README.md | 5 +++-- ogo/fea/gallery.py | 11 ++++++----- ogo/fea/qc_images.py | 14 ++++++++++++++ ogo/fea/qc_render.py | 7 ++++--- tests/fea/test_qc_images.py | 28 ++++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 10 deletions(-) diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 39331a3..f8a9f46 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -32,8 +32,9 @@ images do not imply that the requested shaft length was retained. SED uses a linear Jet scale, shared across the three views, from zero to the 99th percentile of bone-element SED. A bone-masked Gaussian (sigma 0.8 mm) and light surface smoothing improve display only; FE arrays and results are -unchanged. Review images use WebP (quality 85) at 1400 x 2550 pixels; individual -gallery views are 1400 x 850 pixels. These compact lossy images are for QC, not +unchanged. Review images use WebP (quality 85) at 600 x 1092 pixels; individual +gallery views are 600 x 364 pixels. Legacy 1400 x 2550 panels remain supported. +These compact lossy images are for QC, not quantitative analysis or publication. Each image has a JSON sidecar recording display settings and compression. Existing PNG previews remain supported. Spine generation retains `*_qc_body_mask.nii.gz` in model coordinates for consistent diff --git a/ogo/fea/gallery.py b/ogo/fea/gallery.py index 6b54d83..285c16f 100644 --- a/ogo/fea/gallery.py +++ b/ogo/fea/gallery.py @@ -13,14 +13,14 @@ from urllib.parse import quote from ogo.fea.review import FIELDS, REASONS, identity, inclusion_row, write_inclusion -from ogo.fea.qc_images import save_review_image +from ogo.fea.qc_images import REVIEW_PANEL_SIZE, save_qc_panel, save_review_image def _camera_images(path, output): """Split the fixed three-row Ogo render, caching assets beside the gallery.""" from PIL import Image with Image.open(path) as image: - if image.size != (1400, 2550) or not path.stem.endswith(('_qc_3d', '_sed_3d')): + if image.size not in ((1400, 2550), REVIEW_PANEL_SIZE) or not path.stem.endswith(('_qc_3d', '_sed_3d')): return [] stat = path.stat() key = hashlib.sha256(f'{path.resolve()}:{stat.st_size}:{stat.st_mtime_ns}'.encode()).hexdigest()[:20] @@ -30,7 +30,8 @@ def _camera_images(path, output): for index, view in enumerate(('oblique', 'top', 'bottom')): asset = directory / f'{key}_{view}.webp' if not asset.exists(): - save_review_image(image.crop((0, index * 850, 1400, (index + 1) * 850)), asset) + height = image.height // 3 + save_review_image(image.crop((0, index * height, image.width, (index + 1) * height)), asset) assets.append((view, asset)) return assets @@ -156,11 +157,11 @@ def export_gallery_zip(rows, output, decisions=None): # Stable identity preserves browser autosave across exports. name = hashlib.sha256(str(source).encode()).hexdigest()[:20] + '_' + source.name target = images / name - if source.suffix.lower() == '.png': + if source.suffix.lower() in ('.png', '.webp'): from PIL import Image target = target.with_suffix('.webp') with Image.open(source) as image: - save_review_image(image, target) + save_qc_panel(image, target) else: try: target.symlink_to(source) diff --git a/ogo/fea/qc_images.py b/ogo/fea/qc_images.py index 0d8293e..59b1dc6 100644 --- a/ogo/fea/qc_images.py +++ b/ogo/fea/qc_images.py @@ -2,6 +2,20 @@ from pathlib import Path +REVIEW_PANEL_SIZE = (600, 1092) + + +def save_qc_panel(image, path): + """Save the three-view debug panel at review resolution, not publication size.""" + from PIL import Image + + if image.size in ((1400, 2550), REVIEW_PANEL_SIZE): + image = image.resize(REVIEW_PANEL_SIZE, Image.Resampling.LANCZOS) + else: + image = image.copy() + image.thumbnail(REVIEW_PANEL_SIZE, Image.Resampling.LANCZOS) + image.convert('RGB').save(path, format='WEBP', quality=85, method=2) + def save_review_image(image, path): """Save an RGB WebP at quality 85 without resizing the supplied image.""" diff --git a/ogo/fea/qc_render.py b/ogo/fea/qc_render.py index ad18940..52d42a0 100644 --- a/ogo/fea/qc_render.py +++ b/ogo/fea/qc_render.py @@ -23,7 +23,7 @@ from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtk from ogo.cli.Visualize import vis3d -from ogo.fea.qc_images import save_review_image +from ogo.fea.qc_images import REVIEW_PANEL_SIZE, save_qc_panel def smooth_bone_sed(values, indices, bone, spacing): @@ -344,12 +344,13 @@ def export_model_qc(model_path, site, body_mask=None, sed=False, temporary = Path(temporary) rendered = panel(model_path, temporary, site, body_mask, sed, model, ineligible) with Image.open(rendered) as image: - save_review_image(image, output) + save_qc_panel(image, output) views = [json.loads((temporary / (model_path.stem + "_preview_" + view + ("_sed" if sed else "") + ".json")).read_text()) for view in ("oblique", "top", "bottom")] output.with_suffix(".json").write_text(json.dumps( - {"site": site, "views": views, "image_format": "WEBP", "image_quality": 85}, indent=2)) + {"site": site, "views": views, "image_format": "WEBP", "image_quality": 85, + "image_size_pixels": list(REVIEW_PANEL_SIZE), "review_only": True}, indent=2)) if model_path.with_name(model_path.stem + '_qc_metrics.csv').exists(): from ogo.fea.validation import write_measurements write_measurements(model_path, {'sed_image' if sed else 'anatomy_image': str(output)}) diff --git a/tests/fea/test_qc_images.py b/tests/fea/test_qc_images.py index 7c91676..e4be943 100644 --- a/tests/fea/test_qc_images.py +++ b/tests/fea/test_qc_images.py @@ -16,6 +16,34 @@ def test_review_image_is_webp_without_resizing(tmp_path): assert image.size == (1400, 2550) +def test_default_qc_panel_is_compact_webp(tmp_path): + from ogo.fea.qc_images import save_qc_panel + + path = tmp_path / 'case_qc_3d.webp' + save_qc_panel(Image.new('RGB', (1400, 2550), 'white'), path) + with Image.open(path) as image: + assert image.format == 'WEBP' + assert image.size == (600, 1092) + + +def test_gallery_splits_compact_panel_without_mixing_views(tmp_path): + from ogo.fea.gallery import _camera_images + + source = tmp_path / 'case_qc_3d.webp' + panel = Image.new('RGB', (600, 1092)) + colors = ['red', 'green', 'blue'] + for index, color in enumerate(colors): + panel.paste(color, (0, index * 364, 600, (index + 1) * 364)) + panel.save(source, lossless=True) + assets = _camera_images(source, tmp_path / 'gallery.html') + assert [view for view, _ in assets] == ['oblique', 'top', 'bottom'] + for (_, path), color in zip(assets, colors): + with Image.open(path) as image: + assert image.size == (600, 364) + expected = Image.new('RGB', (1, 1), color).getpixel((0, 0)) + assert max(abs(a - b) for a, b in zip(image.getpixel((300, 182)), expected)) < 5 + + @pytest.mark.parametrize('extension', ['.png', '.webp']) def test_gallery_splits_both_formats_into_webp(tmp_path, extension): from ogo.fea.gallery import _camera_images From 8669883b6eb2239ecefdb740ab01c451a0630775 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:39:21 -0600 Subject: [PATCH 58/67] Simplify FEA guides and document workflow helpers --- README.rst | 8 + docs/fea/implementation.md | 46 + docs/fea/vertebra_fea_quickstart.md | 341 +---- ...7-28-masked-pistoia-length-study-design.md | 122 -- ogo/fea/README.md | 1171 ++--------------- ogo/fea/femur.py | 47 +- ogo/fea/gallery.py | 1 + ogo/fea/legacy_qc.py | 3 + ogo/fea/material_laws.py | 58 +- ogo/fea/qc_render.py | 1 + ogo/fea/review.py | 3 + ogo/fea/spine.py | 71 +- 12 files changed, 311 insertions(+), 1561 deletions(-) create mode 100644 docs/fea/implementation.md delete mode 100644 docs/superpowers/specs/2026-07-28-masked-pistoia-length-study-design.md diff --git a/README.rst b/README.rst index be57f01..b16d990 100644 --- a/README.rst +++ b/README.rst @@ -46,6 +46,14 @@ which was presented at the ASBMR in 2018 and published in 2020: For further information about the Bone Imaging Laboratory see https://bonelab.ucalgary.ca. Please contact us for further questions, comments or interest in contributing to this project. +Finite Element Modelling +======================== + +For hip and spine models, start with the `FEA run guide `_. +The `vertebra quickstart `_ covers installation +and a first L1 model; the `code map `_ locates the +registration, material laws, supports and QC functions. + Install ======= diff --git a/docs/fea/implementation.md b/docs/fea/implementation.md new file mode 100644 index 0000000..18583a4 --- /dev/null +++ b/docs/fea/implementation.md @@ -0,0 +1,46 @@ +# FEA code map + +Start at `ogo/cli/GenerateFEM.py`. It parses `ogoFEA`, calls the anatomy builder, +audits the model, runs FAIM/N88 and writes modelling metadata. + +| Task | File / function | +| --- | --- | +| CLI and site arguments | `GenerateFEM.build_parser`, `build_spine_command`, `build_femur_command` | +| Reporting endpoint and criteria | `GenerateFEM.solve_report_profile`, `critical_volume_percent`, `critical_strain` | +| Spine preparation and model | `spine.clean_body_component`, `get_icp_with_scaling`, `process_vertebra` | +| Hip preparation and model | `femur.sidewaysFallFe`, `crop_vtk_images_to_greater_trochanter_length` | +| Hip multistart alignment | `hip_registration.estimate_femur_icp` | +| Physical scan-end/shaft measurements | `shaft_geometry.capture_distal_scan_face`, `measure_available_shaft`, `verify_model_shaft` | +| Shared transforms | `alignment.py`, `image_io.py` | +| PMMA/contact geometry | `boundary.generate_bone_cap_mask`, `generate_projected_material_disk_vtk` | +| Density laws and material tables | `material_laws.py`, `materials.build_bone_pmma_material_table` | +| N88 mesh and file writing | `model.create_microfe_model`, `write_model` | +| Solver discovery, Pistoia and results | `ogo/util/faim.py` | +| Generation measurements / QC rules | `validation.measure_model`, `evaluate` | +| Legacy measurement recovery | `legacy_qc.backfill_measurements` | +| Three-view previews | `qc_render.py`, using `ogo/cli/Visualize.py` | +| Gallery and review records | `gallery.py`, `review.py`, `gallery_review.js` | + +Anatomy defaults are constants at the top of `spine.py` and `femur.py`. +Shared helpers own geometry/material logic; the CLI owns solving and reporting. +Changing a reporting criterion must not silently change model construction. + +Spine cleans the body label, crops, estimates centroid-initialized ICP, resamples, +builds caps/materials and applies compression. Hip captures the original distal +scan face, rough-crops for registration, transforms the full scan, builds proximal +supports, checks available coverage, then crops and constrains the shaft. +Reference scaling changes the registration target, not native bone dimensions. + +For hip, the saved-model length check is +`min(z of Greater_Trochanter_PMMA_Nodes) - median(z of Distal_Femur_Nodes)`. +Coverage also accounts for the most proximal transformed scan-end point and +resampling clearance, so the final face must be complete, not an oblique remnant. + +QC measurements are written during generation; validation reads these CSVs +without reopening models. Legacy recovery uses registered anatomy and saved +shaft geometry where available, recording what was recovered. Manual inclusion +is separate from the automatic construction checks. + +Tests are in `tests/fea/` and `tests/util/`; run focused tests for the module +being edited. Imaging/mesh tests require vtkbone and SimpleITK. Document changes +to defaults and preserve their matching CLI/provenance tests. diff --git a/docs/fea/vertebra_fea_quickstart.md b/docs/fea/vertebra_fea_quickstart.md index 6885400..35440f0 100644 --- a/docs/fea/vertebra_fea_quickstart.md +++ b/docs/fea/vertebra_fea_quickstart.md @@ -1,333 +1,74 @@ -# Vertebra FEA Quickstart +# L1 FEA quickstart -This guide shows how to check out the Ogo FEA branch and run a solved vertebra -compression model with FAIM/N88. It is intended for users who already have a -density-calibrated CT image, a labelled vertebra segmentation, and access to -the Numerics88 command-line tools. +## Install -## What This Workflow Does - -`ogoFEA spine` is the user-facing entry point for vertebral compression models. -For each requested vertebra it: - -1. Extracts the vertebral body and posterior process labels from a labelled - spine mask. -2. Crops the calibrated image and masks around the target vertebra. -3. Aligns the vertebral body to the bundled Ogo reference with ICP. -4. Resamples image and masks to isotropic model spacing. -5. Builds superior and inferior PMMA supports on stable vertebral-body contact - surfaces. -6. Converts density to finite-element material IDs. -7. Writes an `.n88model`, audits the boundary conditions, and records modeling - metadata. -8. Solves the model with FAIM/N88 and writes a compact `_results.csv`. -9. Optionally runs full-model and masked-ROI Pistoia postprocessing. - -## Fresh Setup On A New Computer - -Use this section when setting up a new workstation from scratch. - -### 1. Get Ogo - -Install Git and a conda-style Python distribution such as Miniforge or -Miniconda, then check out the FEA branch: +Install Git and conda first, then: ```bash -git clone git@github.com:Bonelab/Ogo.git +git clone https://github.com/Bonelab/Ogo.git cd Ogo git checkout ogo-fea -git pull origin ogo-fea -``` - -### 2. Create The Ogo Environment - -The most reliable setup is to create an environment that gets `vtkbone` from -the Numerics88 conda channel. This avoids the slow full `environment.yml` solve -and avoids the common plain-`requirements.txt` failure where `vtkbone` is -missing. - -```bash conda create -n ogo -c numerics88 -c conda-forge \ - git pandas nibabel pyyaml vtkbone pbr "setuptools<69" wheel python=3 + git pandas nibabel pyyaml vtkbone netcdf4 pbr "setuptools<69" wheel python=3 conda activate ogo -PBR_VERSION=0.0.1 pip install -e . -``` - -Then verify the command-line entry point: - -```bash -which ogoFEA +PBR_VERSION=0.0.1 python -m pip install -e . ogoFEA --help ``` -The repository also contains `environment.yml`, but it can take a long time to -solve on a new machine. Use it only if you need to reproduce that exact pinned -environment: - -```bash -conda env create -n ogo-pinned -f environment.yml -``` - -If `pip install -e .` fails with a PBR versioning error, first check whether -Git is available inside the active environment: - -```bash -which git -``` - -If this prints nothing, install Git into the environment and rerun the editable -install: - -```bash -conda install -c conda-forge git -PBR_VERSION=0.0.1 pip install -e . -``` - -`PBR_VERSION=0.0.1` is only used for package metadata during installation. It -does not change the checked-out Ogo code. - -### 3. Make N88/FAIM Discoverable - -Ogo builds the `.n88model` in Python, but solving and Pistoia require the -Numerics88/FAIM command-line tools. - -If the N88 commands are already on your `PATH`, this should print paths: - -```bash -which n88modelinfo -which n88pistoia -which n88tabulate -which faim -``` - -If they are not on `PATH`, decide how Ogo should find them: - -| Setup | How to run | -| --- | --- | -| N88 tools live in one directory | Pass `--faim_bin_dir /path/to/n88/bin`. | -| N88 tools live in a separate conda environment | Pass `--faim_env ENV_NAME`. | -| Individual tools have unusual names or locations | Pass explicit overrides such as `--n88pistoia_command /path/to/n88pistoia`. | - -On a machine with a separate N88 environment, a typical check is: - -```bash -conda run -n ogoloco-n88 n88modelinfo --help -conda run -n ogoloco-n88 n88pistoia --help -``` - -### 4. Verify With A Debug Model +`PBR_VERSION` supplies installation metadata only. The Numerics88 channel +provides vtkbone; solving additionally requires FAIM/N88 tools and a license. +Use tools already on `PATH`, or add `--faim_bin_dir /path/to/n88/bin` to commands. +A separate solver conda environment can be selected with `--faim_env NAME`. -Before the first solved run on a new computer, generate an unsolved debug model: +On ARC, use the shared installation instead: ```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --output_path derivatives/fea_test \ - --debug \ - --no-solve +source /work/boyd_lab/conda_init/ogo_fea.sh ``` -This confirms that Ogo can read the images, find the labels, align the -vertebra, build the PMMA supports, and write the `.n88model`. After inspecting -the debug output, run the solved command below in a clean output directory. +Run generation and solving on a compute node, not the login node. -## Required Inputs +## Generate and inspect -You need two images in the same image space: - -| Input | Requirement | -| --- | --- | -| Calibrated CT | Scalar density image in K2HPO4-equivalent units. | -| Labelled spine mask | Integer labels containing the vertebral body and posterior process. | - -The vertebra target is passed as: - -```text -LEVEL:BODY_LABEL:PROCESS_LABEL -``` - -For example, `L1:2:3` means: - -| Part | Meaning | -| --- | --- | -| `L1` | Output level name. | -| `2` | Label value for the L1 vertebral body. | -| `3` | Label value for the L1 posterior process. | - -If you want masked Pistoia for a region such as the vertebral body, provide a -binary ROI mask in the original CT image space. - -## Standard L1 Command - -This command generates, audits, solves, and postprocesses one L1 model: - -```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --output_path derivatives/fea \ - --threads 4 \ - --faim_bin_dir /path/to/n88/bin \ - --run_pistoia \ - --critical_volume 12 \ - --critical_strain 0.007 -``` - -Use `--threads 4` for a typical single-model run. This controls FAIM solver -threads for that one model; it does not launch multiple subjects in parallel. - -## Full And Masked Pistoia - -To report both full-model Pistoia and an ROI-masked Pistoia result, add -`--pistoia_mask`: - -```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --pistoia_mask sub-001_desc-L1Body_mask.nii.gz \ - --output_path derivatives/fea \ - --threads 4 \ - --faim_bin_dir /path/to/n88/bin \ - --run_pistoia \ - --critical_volume 12 \ - --critical_strain 0.007 -``` - -With this command, Ogo reports: - -| Result | Description | -| --- | --- | -| Full-model Pistoia | Uses the solved vertebra model and excludes PMMA. | -| Masked Pistoia | Uses only elements selected by the supplied ROI mask after transformation into model space. | - -The same `--critical_volume` and `--critical_strain` values are used for both -full-model and masked Pistoia in a single `ogoFEA` run. - -## N88 And FAIM Path Handling - -Ogo does not search the entire filesystem for N88. The commands are resolved in -this order: - -1. Explicit command overrides such as `--faim_command` or - `--n88pistoia_command`. -2. `--faim_bin_dir`, for example `/path/to/n88/bin`. -3. `--faim_install_root`, where Ogo checks common subdirectories. -4. The current shell `PATH`. - -If the N88 tools are in a separate conda environment, use: +Use density-calibrated QCT in mg/cm3 K2HPO4-equivalent units and a segmentation +in the same physical space. This example uses body label 20 and process label 48; +replace these with your dataset's labels. NIfTI geometry must preserve anatomical +orientation, with physical z along the superior-inferior axis. ```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --output_path derivatives/fea \ - --faim_env ogoloco-n88 \ - --threads 4 +ogoFEA spine qct.nii.gz labels.nii.gz --vertebra L1:20:48 \ + --output_path derivatives/fea --threads 4 --no-solve ``` -## Expected Outputs - -By default, outputs are written to the calibrated image directory. In most -projects you should pass `--output_path derivatives/fea`. - -Important outputs: - -| File | Meaning | -| --- | --- | -| `*_L1.n88model` | Generated finite-element model. | -| `*_L1_modeling.json` | Traceable metadata for inputs, preprocessing, materials, boundary conditions, and reporting. | -| `*_L1_results.csv` | Compact one-row results table for downstream analysis. | -| `*_L1_pistoia.txt` | Full-model Pistoia report, when Pistoia is run. | -| `*_L1_masked_pistoia.txt` | ROI-masked Pistoia report, when `--pistoia_mask` is supplied. | -| `*_L1_pistoia_mask.nii.gz` | ROI mask transformed into model space. | - -The `_results.csv` includes reaction force, stiffness, full-model Pistoia -fields, and `masked_pistoia_*` fields when a mask is supplied. - -## Model Defaults +Inspect `*_qc_3d.webp`: the body should be upright, processes posterior, and +both PMMA caps in contact with the body. Check `*_bc_audit.json` and +`*_qc_metrics.csv`. Registration/segmentation failures should be resolved +before interpreting mechanical results. -The default high-level spine preset is `benchmark-linear`. - -| Setting | Default | -| --- | --- | -| Model type | Linear elastic vertebral compression. | -| Resampling | 1.0 mm isotropic. | -| Density-to-modulus law | `E = 2980 * rho^1.05 MPa` for benchmark-linear spine models. | -| Bone Poisson's ratio | 0.3. | -| PMMA modulus | 2500 MPa. | -| PMMA Poisson's ratio | 0.3. | -| PMMA thickness | 10 mm. | -| Default target endpoint | 0.68% compression strain. | -| Full-model Pistoia critical strain | User-supplied; commonly `0.007`. | - -FAIM stores prescribed displacement in millimeters. Before solving, Ogo -converts the 0.68% endpoint to millimeters from the generated vertebral-body -height and updates the `.n88model`. - -## Quick Quality Checks - -For a new dataset, first generate a debug model without solving: +## Solve and report both regions ```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --output_path derivatives/fea \ - --debug \ - --no-solve +ogoFEA spine qct.nii.gz labels.nii.gz --vertebra L1:20:48 \ + --pistoia_mask_label 20 --critical_volume 12 --masked_critical_volume 35 \ + --critical_strain 0.007 --output_path derivatives/fea --threads 4 ``` -Check: - -- The vertebral body is upright in the model frame. -- The posterior process is posterior to the body, not rotated into the axial - direction. -- Superior and inferior PMMA supports contact the body surfaces without being - dominated by osteophytes. -- The boundary-condition audit passes. -- The `_modeling.json` records the expected labels, material law, displacement - endpoint, and N88 settings. +This reports reaction force at 0.68%, stiffness, full-bone Pistoia at 12% +critical volume, and body-only Pistoia at 35%, all in `*_results.csv`. +Critical volume is a percentage of evaluated bone; critical strain 0.007 means +0.7% effective strain. These criteria must be justified for the intended study. +The model-space ROI is saved as `*_pistoia_mask.nii.gz`. -## Common Problems +For a separate binary body mask, replace `--pistoia_mask_label 20` with +`--pistoia_mask body_mask.nii.gz`. The full-bone result is still included. +For full-bone Pistoia alone, omit the ROI and add `--run_pistoia`. -| Symptom | Likely cause | What to check | -| --- | --- | --- | -| Command cannot find `faim` or `n88pistoia` | N88 tools are not on `PATH`. | Pass `--faim_bin_dir`, `--faim_env`, or explicit command paths. | -| Missing masked Pistoia fields | No `--pistoia_mask` was supplied, or masked Pistoia failed. | Check `_results.csv`, `*_masked_pistoia.txt`, and the log. | -| Process appears rotated relative to the body | ICP alignment or label definition problem. | Run `--debug --no-solve` and inspect the model before solving. | -| PMMA support contacts osteophytes instead of a stable surface | Endplate surface is irregular. | Inspect debug output and `_modeling.json`; consider excluding the case if QC fails. | -| Existing solution is reused | The `.n88model` already contains a solution. | Remove old outputs or write to a clean `--output_path` before rerunning. | - -## Minimal Reproducible Example - -For a collaborator, the most useful handoff is: +## Review a cohort ```bash -git clone git@github.com:Bonelab/Ogo.git -cd Ogo -git checkout ogo-fea -python -m pip install -e . - -ogoFEA spine \ - image_K2HPO4.nii.gz \ - spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --pistoia_mask L1_body_mask.nii.gz \ - --output_path derivatives/fea \ - --threads 4 \ - --faim_bin_dir /path/to/n88/bin \ - --run_pistoia \ - --critical_volume 12 \ - --critical_strain 0.007 +ogoValidateFEA derivatives/fea --output derivatives/qc --gallery-zip --workers 4 ``` -The main file to send onward for tabulated analysis is -`derivatives/fea/*_L1_results.csv`; the main file for visual/model inspection is -`derivatives/fea/*_L1.n88model`. +Extract `gallery.zip`, open `gallery.html`, review and export the inclusion CSV. +Browser decisions do not overwrite project files. See the +[workflow guide](../../ogo/fea/README.md) for defaults, outputs and legacy models. diff --git a/docs/superpowers/specs/2026-07-28-masked-pistoia-length-study-design.md b/docs/superpowers/specs/2026-07-28-masked-pistoia-length-study-design.md deleted file mode 100644 index 85511b7..0000000 --- a/docs/superpowers/specs/2026-07-28-masked-pistoia-length-study-design.md +++ /dev/null @@ -1,122 +0,0 @@ -# Masked Pistoia and RETRO2 Femur Length Study Design - -## Goal - -Add a generic Pistoia region-of-interest mask workflow to Ogo so a solved FE -model can report both full-model Pistoia and masked-region Pistoia. The first -use case is the RETRO2 femur length study, where the mask is the femoral neck. -The same mechanism should also support spine models, for example vertebral-body -or trabecular-body-only Pistoia. - -## Inputs - -`ogo.cli.GenerateFEM` should accept a site-agnostic optional argument: - -```text ---pistoia_mask PATH -``` - -The mask is interpreted in the input image space. For hip this can be a -femoral-neck mask. For spine this can be a vertebral-body, trabecular-body, or -other ROI mask. The mask is optional; if it is not provided, the current full -model Pistoia behavior is unchanged. - -For the RETRO2 femur length study, inputs should come from the PI-preferred FEA -folder: - -```text -/home/matthias.walle/data/scratch/RETRO2/fea/QCT/hip_QCT/ -/home/matthias.walle/data/scratch/RETRO2/fea/segmentations/left_femur/ -/home/matthias.walle/data/scratch/RETRO2/fea/segmentations/femoral_neck_original_dim/ -``` - -Cases without a full hip QCT or without a reconstructed femoral-neck mask should -be reported in a manifest and skipped for masked femoral-neck Pistoia. - -## Pipeline Behavior - -The ROI mask must be carried through exactly the same spatial operations as the -FE model: - -- input resampling -- rough crop or crop-to-mask steps -- padding -- ICP/reference transformation -- reference-grid resampling -- final length crop - -ROI masks use nearest-neighbor interpolation. The transformed ROI mask should be -written beside the generated model for auditability. Its geometry must match the -final model grid so `ogo.util.faim.run_pistoia_with_image_mask()` can select -model elements directly. - -The existing post-hoc source-space masks must not be applied directly to -post-ICP models. Prior checks showed that source-space masks select zero -elements in existing length-crop `.n88model` files. - -## Pistoia Reporting - -When `--run_pistoia` is enabled, Ogo should continue to run full-model Pistoia -with the existing critical strain and critical volume settings. - -When `--pistoia_mask` is provided, Ogo should additionally run masked Pistoia -using the transformed model-space ROI mask. The lower-level mechanism should -reuse `ogo.util.faim.run_pistoia_with_image_mask()`, which temporarily excludes -elements outside the image mask and then restores the original model materials. - -The output CSV should include both full and masked values. Generic column names -avoid femur-specific assumptions: - -```text -pistoia_failure_load_N -pistoia_stiffness_N_per_mm -masked_pistoia_failure_load_N -masked_pistoia_stiffness_N_per_mm -masked_pistoia_critical_volume_pct -masked_pistoia_critical_ees -masked_pistoia_ees_at_crit_vol -masked_pistoia_mask_file -masked_pistoia_selected_elements -masked_pistoia_original_included_elements -``` - -If masked Pistoia fails and `--require_pistoia` is not set, the main solve -should still complete and report a warning, matching the current full-Pistoia -failure behavior. - -## RETRO2 Length Study Output - -Create a clean study folder: - -```text -/home/matthias.walle/data/scratch/RETRO2/fea/length_study_femoral_neck_pistoia/ -``` - -The folder should contain: - -- `models///...` for `.n88model` and sidecars -- `individual_results///...` for solver and Pistoia outputs -- `transformed_masks///...` for model-space Pistoia ROI masks -- `logs/` for generation and solve logs -- `tables/run_manifest.csv` -- `tables/results_summary.csv` - -`results_summary.csv` should retain the length-study fields from the existing -study and add the masked Pistoia fields above. - -## Validation - -Implementation should include unit or integration tests that verify: - -- `--pistoia_mask` appears in the hip and spine CLI. -- The femur pipeline resamples/transforms the ROI mask to the final model grid. -- The transformed ROI mask selects nonzero model elements in a generated model. -- Full Pistoia and masked Pistoia can both be reported in the same results CSV. -- Missing ROI masks produce manifest warnings rather than silent omission. - -Before running the full RETRO2 study, run a small smoke set with available -femoral-neck masks across several crop lengths and verify: - -- transformed ROI masks overlay the final model anatomy; -- selected-element counts are plausible and nonzero; -- full-model and masked Pistoia outputs are both present. diff --git a/ogo/fea/README.md b/ogo/fea/README.md index f8a9f46..5b5d762 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -1,1057 +1,150 @@ -# Ogo FEA Workflow +# Hip and spine FEA -This directory contains the maintained model-building code used by -`ogoFEA`. The user-facing entry point is the CLI in -`ogo/cli/GenerateFEM.py`; the lower-level spine and femur modules in this -directory should stay implementation details. +`ogoFEA` generates, checks and solves N88 models from calibrated QCT and +segmentations. Start with the [installation and L1 example](../../docs/fea/vertebra_fea_quickstart.md). +For implementation details, see the [code map](../../docs/fea/implementation.md). -The workflow builders are organized by anatomy: +## Run a model -- `spine.py` owns spine-compression defaults, preprocessing, model building, - and the lower-level spine command parser. -- `femur.py` owns hip sideways-fall defaults, preprocessing, model building, - and the lower-level femur command parser. -- Shared helper modules such as `alignment.py`, `boundary.py`, `materials.py`, - and `model.py` contain reusable mechanics used by both anatomy workflows. - -## Automatic Model QC - -Both anatomy workflows save one three-view anatomy panel (`*_qc_3d.webp`) -after model generation. The FAIM adapter saves a corresponding solved SED -panel (`*_sed_3d.webp`) after successful processing of a hip or spine model. -The views use `ogo.cli.Visualize.vis3d`: white background, opaque bone and -gold PMMA supports, with constrained surface-adjacent voxel layers in red. -Individual views are framed independently. Numerical BC checks remain in CSV; -the former automatic slice PNG is no longer generated. - -Hips rejected for insufficient shaft coverage also receive the anatomy panel -of their available aligned bone and supports. They retain `too_short` in the -shaft-geometry JSON, have no artificial distal BC, and are not solved. These -images do not imply that the requested shaft length was retained. - -SED uses a linear Jet scale, shared across the three views, from zero to the -99th percentile of bone-element SED. A bone-masked Gaussian (sigma 0.8 mm) -and light surface smoothing improve display only; FE arrays and results are -unchanged. Review images use WebP (quality 85) at 600 x 1092 pixels; individual -gallery views are 600 x 364 pixels. Legacy 1400 x 2550 panels remain supported. -These compact lossy images are for QC, not -quantitative analysis or publication. Each image has a JSON sidecar recording -display settings and compression. Existing PNG previews remain supported. Spine -generation retains `*_qc_body_mask.nii.gz` in model coordinates for consistent -body/process colouring after solving. Temporary individual renders are removed. - -Rendering needs a working graphics backend (X display, EGL/OSMesa, or native -macOS VTK). Graphics failures produce a warning rather than invalidate a -numerical model. Rendering is implemented in `ogo/fea/qc_render.py`. - -### Measurement-Based Audit And Gallery - -Generation writes `*_qc_metrics.csv` while the model is still in memory, -including hips rejected as `too_short`. These CSVs are independent of rendering: -an unavailable graphics backend does not prevent geometric measurement. -The file contains one row per model; missing measurements are blank, not zero. -When SED is loaded for the solved preview, finite/nonnegative SED checks are -added to the same row without replacing the generation measurements. - -```bash -ogoValidateFEA derivatives/fea --output derivatives/fea/qc --gallery -``` - -The command reuses generation measurement CSVs without opening `.n88model` files. -For older models without a CSV, it reads the model once, reconstructs available -measurements, and saves a sidecar with `measurement_source=legacy_backfill`. -Provide `--site hip` or `--site spine` if the loading axis cannot identify anatomy. -Recovered boundary sets are recorded and exist only in memory; models are not -rewritten. Hip coverage is recovered from `*_shaft_geometry.json`; its saved -crop plane also disambiguates distal nodes from combined GT/shaft constraints. -Spine anatomy is recovered from registered `*_qc_body_mask.nii.gz` and optional -`*_qc_process_mask.nii.gz`. When process is absent, it is measured as model bone -minus registered body, with that derivation explicitly recorded. Masks must -match the FE voxel lattice and have at least 95% overlap with model bone. -Native-space segmentations are not accepted without prior registration. - -For files stored elsewhere, supply `--evidence evidence.csv`: - -```csv -site,model_id,body_mask,process_mask,shaft_geometry -spine,sub-001_L1,registered/sub-001_body.nii.gz,, -hip,sub-001_LF,,,geometry/sub-001_shaft_geometry.json -``` - -Paths are relative to this CSV (absolute paths are also supported). Do not list -a Pistoia mask as `body_mask` unless its provenance confirms it is the registered -vertebral body: the filename alone does not identify the anatomical region. -Recovered evidence sources and derivations are recorded in the measurement CSV. -Legacy backfill is cached using its version and input paths, sizes, and modification -times. Adding or changing evidence automatically refreshes a legacy CSV once; -unchanged unresolved models are not repeatedly reopened. Use `--workers 4` for -bounded process-parallel backfill; auditing cached CSVs remains lightweight. - -Original scan coverage and other unrecoverable quantities remain missing and -require review. An unreadable model -is retained as a review row with its reconstruction error, not silently dropped. -The command writes -`qc_summary.csv`; `--gallery` additionally writes a standalone `gallery.html`. -Without a refreshed editable installation, use `python -m ogo.cli.ValidateFEA`. -The gallery supports anatomy, QC status, subject search, and anatomy/SED -view selection, with expandable measurement tables. A separate camera selector -shows oblique/side, top, bottom, or all views in both thumbnails and the review -dialog. The fixed three-row Ogo renders are split once into cached WebP images in -`gallery_views/`; unrecognized legacy layouts remain full montages. Image files -remain external; retain their relative location and the view-assets folder when -copying the gallery and models together. - -The initial page shows 50 models; select 100, 250, or 500 per page. Only that page -is rendered. Cached 420-pixel WebP thumbnails are loaded ahead of scrolling; -opening Review uses the original-resolution view. Measurement tables are built -only when expanded. Export always includes the entire cohort, not just the page. -The Exclude flags dropdown has checkboxes: checking a flag hides matching cases -without changing their QC or study decisions. "Not manually reviewed" refers to -cases without an explicit manual include/exclude decision. Include/exclude saves -preserve your scroll position. Filtering can remove a card from the current page. - -Sort by patient ID, number of QC flags, or stiffness, reaction force and full or -regional failure load (lowest/highest first). Force sorting uses absolute -magnitudes; missing outcomes are last. Sorting never changes inclusion. Manual -exclusions are labelled "Manually excluded" independently of automatic QC. - -For sharing, export one portable ZIP instead: +Inputs are NIfTI images in the same physical space. QCT density must be in +mg/cm3 K2HPO4-equivalent units, not HU. Spine labels identify the body and +posterior process separately; a hip mask contains the selected whole femur. +Label numbers are dataset-specific. ```bash -ogoValidateFEA derivatives/fea --output derivatives/fea/qc --gallery-zip --workers 4 -``` - -Extract `gallery.zip` and open `gallery.html`. It includes `images/`, -`gallery_views/`, `gallery_thumbnails/`, `qc_summary.csv`, and the initial `study_inclusion.csv`. -No original model directory or server is needed to review it. Model files are -not included; original model paths remain in the CSV as provenance. Legacy PNGs -are converted to compact WebP images inside the ZIP without modifying their -originals. Both solve stages and all available camera views are retained. Missing images remain -missing. Large cohorts use lazy-loaded image files rather than embedded HTML. - -For study-specific full-size L1 resegmentation and recovery of excluded or -incomplete cases, see [the cohort recovery example](../../examples/fea/cohort_recovery/README.md). -It preserves prior results and builds a fresh review gallery; it does not -automatically reinstate rerun cases. -After review, export the updated inclusion CSV from the gallery; browser -decisions cannot rewrite the CSV inside the original ZIP. -The export popup directs users to browser downloads and explicitly states that -the project CSV is unchanged. Import the downloaded CSV to resume its decisions. - -Include and Exclude on each gallery card record a quick decision with reason -`manual_review`, reviewer, and UTC timestamp. Use Review for a specific reason -or explanation. The hip side view looks toward the distal cut to expose its -boundary while retaining the shaft profile, before and after solving. +# L1: body label 20, process label 48; also evaluate body-only Pistoia. +ogoFEA spine qct.nii.gz labels.nii.gz --vertebra L1:20:48 \ + --pistoia_mask_label 20 --critical_volume 12 --masked_critical_volume 35 \ + --critical_strain 0.007 --threads 4 --output_path derivatives/fea -Hip distal coverage QC uses the recipe's central 90%-width support patch, not -the entire irregular bone cross-section. New measurements must show complete -coverage of that intended patch. Full-face area and coverage remain in the CSV -for inspection. Older CSVs without patch measurements use a minimum full-face -coverage of 89%; legacy backfill refreshes them with the explicit patch check. -Flat-cut, shaft-length, and X/Z fixation checks remain separate. - -Click a model name, image, or Review to open its measurements and views. Include -and Exclude override the study decision only; Reset to automatic clears that -override. Choose a reason and optionally add an explanation. The default reason -is `manual_review`. Reviewer name/initials and UTC review time are recorded; -missing reviewer names are explicitly recorded as `unspecified`. - -The gallery exports `study_inclusion.csv` with automatic QC, manual decisions, -reasons, notes, and final inclusion. Automatic passes default to inclusion, -failures to exclusion, and review cases to pending. A manually included too-short -model remains ineligible for solving. Browser autosave is a convenience scoped -to the audited dataset, not the durable study record: export the CSV before -closing. Import CSV resumes review in the gallery. To regenerate a gallery with -the same review decisions, use: - -```bash -ogoValidateFEA derivatives/fea --output derivatives/fea/qc --gallery \ - --reviews study_inclusion.csv +# Left femur with a separate femoral-neck mask. +ogoFEA hip qct.nii.gz left_femur.nii.gz --side left \ + --pistoia_mask femoral_neck.nii.gz --critical_volume 11.2 \ + --masked_critical_volume 35 --critical_strain 0.009 \ + --threads 4 --output_path derivatives/fea ``` -The CLI also writes an initial `study_inclusion.csv`. Browser exports reflect -subsequent clicks and do not overwrite the server-side file automatically. -Duplicate site/model-ID pairs are rejected; audit separate length runs/cohorts -separately. Unknown imported identities are rejected rather than ignored. - -Measurements include bone/disk volumes and face-connected components, disk axial -contact footprint area/fraction and connectivity, disk axial thickness percentiles, -boundary-node counts/extents/centroids, loading direction, and hip available, -requested, and measured shaft lengths and distal BC coverage. -For hip, `shaft_bc_node_count` is the distal node-set size; separate -`shaft_bc_fixed_x_node_count` and `shaft_bc_fixed_z_node_count` count those -same nodes actually fixed at zero displacement along each required axis. -Incomplete fixation is a failure, independently of distal face coverage. -Spine records -aligned body/process dimensions, volumes, components, and centroid offsets. -Registration rotation is recorded, but a large rotation alone does not fail QC. -Disk numbers follow spatial component order, not guaranteed anatomical names. -Contact area counts projected axial shared faces, not the whole endplate fraction. - -Policy version 1 fails construction violations: insufficient length, missing BC, -incorrect disk count, noncompressive loading, nonplanar axial/distal BC, -incorrect shaft length, incomplete distal section/BC, zero axial disk contact, -or invalid available SED. Disconnected bone/contact footprints, process-vector -orientation concerns, and missing required measurements trigger `review`. -`pass` means the implemented construction tests passed, not that every possible -anatomical error has been excluded. No strength-based thresholds are used. - -Registration surface distances, true support penetration/trim-depth distributions, -and solver force balance are not yet included. These need validated measurement -definitions before adding thresholds. Generation measurements are reused; -cached legacy measurements refresh when the backfill version or supplied -evidence changes. Legacy backfill does not generate missing preview images. -Failures before an FE mesh exists require the job manifest/log for participant flow. -Implementation: `ogo/fea/validation.py`; CLI: `ogo/cli/ValidateFEA.py`. +A regional mask enables both full-bone and masked Pistoia. Without a mask, +add `--run_pistoia` for full-bone results. `--require_pistoia` makes a failed +Pistoia calculation fatal. `--pistoia_mask_label` without a separate mask +selects labels from the input segmentation; repeat it for multiple labels. +Without label selection, all nonzero mask voxels are included. -## Inputs +Useful options: -All FEA workflows expect a density-calibrated CT image and an aligned -segmentation image. +- `--no-solve`: generate the model and QC without FAIM. +- `--dry-run`: print generator/solver commands without writing models. +- `--vertebra`: repeat for additional levels; format `LEVEL:BODY:PROCESS`. +- `--side`: `left`, `right` or `both` (default both); choose explicitly. +- `--threads`: per-job generation and solver threads, not parallel cases. -The calibrated image should be a scalar image in K2HPO4-equivalent density -units, with the same image space as the segmentation. During model generation, -the image is cropped, aligned to the model reference frame, resampled to -isotropic spacing, thresholded at -31 mg/cc, and converted to material IDs. +Use `ogoFEA spine --help` or `ogoFEA hip --help` for wrapper options. +Additional options are forwarded to the anatomy builder; its help is available +with `python -m ogo.fea.spine --help` or `python -m ogo.fea.femur --help`. -The segmentation depends on the model type: +## Solver setup -| Model | Required mask | Label convention | -| --- | --- | --- | -| Spine compression | Labelled vertebra mask | Provide the vertebral body label and posterior process label for each target as `LEVEL:BODY_LABEL:PROCESS_LABEL`. | -| Hip sideways fall | Whole-femur mask | Nonzero voxels are the femur by default. The wrapper chooses left/right from `--side`. | -| Hip with trab/cort compartments | Whole-femur mask plus `--compartment_mask` | Compartment mask defaults are cortical `1`, trabecular `2`; override with `--cortical_label` and `--trabecular_label` if needed. | - -The spine mask must contain both the body and posterior process labels. The -femur workflow currently requires a side-specific run because alignment uses a -side-specific femur reference frame. - -## Implementation Walkthrough - -The high-level command is `ogoFEA`, implemented in `ogo/cli/GenerateFEM.py`. -That file is intentionally the best starting point for a new developer because -it owns the user-facing arguments, the lower-level anatomy command construction, -the solve/reporting path, and the `_modeling.json` provenance record. +FAIM/N88 tools must be on `PATH`, or supplied through `--faim_bin_dir DIR`, +`--faim_install_root DIR` or `--faim_env ENV`. Individual command overrides +are also available. Ogo does not search the filesystem for installations. +Use `--faim_license_dir DIR` when the license is not already configured. +On ARC, `source /work/boyd_lab/conda_init/ogo_fea.sh` configures the shared +environment; run generation and solving on a compute node. -Use this map when tracing a run: +## Model conventions -| Question | Main code location | -| --- | --- | -| Which command-line options are exposed? | `ogo/cli/GenerateFEM.py::build_parser` | -| How does `ogoFEA spine` become a lower-level model-builder call? | `ogo/cli/GenerateFEM.py::build_spine_command` | -| How does `ogoFEA hip` become a lower-level model-builder call? | `ogo/cli/GenerateFEM.py::build_femur_command` | -| Where are solve endpoints, Pistoia settings, and result columns selected? | `ogo/cli/GenerateFEM.py::solve_report_profile`, `critical_volume_percent`, `solve_model` | -| Where is model provenance written? | `ogo/cli/GenerateFEM.py::write_modeling_metadata` | -| Where is the generated `.n88model` audited? | `ogo/cli/GenerateFEM.py::audit_generated_model`, `ogo/cli/CheckFEModelBC.py` | - -The anatomy-specific implementation is split below the CLI: +Both workflows use 1-mm voxels, cubic density interpolation and nearest-neighbor +labels. Reference scaling estimates registration; the solved subject geometry +is transformed rigidly, not scaled. -| Anatomy | Main file | What belongs there | +| Setting | Hip sideways fall | Spine compression | | --- | --- | --- | -| Spine compression | `ogo/fea/spine.py` | Vertebra labels, body/process QC, dense NumPy spine ICP, 1 mm resampling, stable PMMA cap generation, axial compression defaults. | -| Hip sideways fall | `ogo/fea/femur.py` | Side handling, femur reference alignment, rough pre-ICP crop, post-ICP GT-length crop, femoral-head/GT/distal supports, sideways-fall defaults. | -| Shared material tables | `ogo/fea/materials.py` | Bone material ID ranges, PMMA material, shared femur/spine material-table construction. | -| Material laws | `ogo/fea/material_laws.py` | Density-to-modulus and yield-strength functions such as `default_E` and `kopperdahl_trab_E`. | -| Shared geometry/BC helpers | `ogo/fea/boundary.py` | Resampling helpers, projected material disks, PMMA caps, contact footprint calculations. | -| N88 model writing | `ogo/fea/model.py` | `create_microfe_model` and `write_model`. | -| FAIM/N88 adapter | `ogo/util/faim.py` | Solver command resolution, running `faim`/N88 tools, Pistoia parsing, masked Pistoia, compact `_results.csv`. | - -For spine, the maintained path is: - -1. `GenerateFEM.py::build_spine_command` forwards the calibrated image, - segmentation, target `LEVEL:BODY_LABEL:PROCESS_LABEL`, preset, optional - Pistoia mask, and any lower-level overrides. -2. `spine.py::clean_body_component` keeps the largest face-connected component - of the requested body label before registration and support construction. - Other labels, including the posterior process, are unchanged. The QC CSV - records the original component count, removed voxels, removed volume, and - removed body fraction. Removal of more than 5% triggers - `substantial_body_cleanup` for manual review, not automatic exclusion. This - conservative triage threshold is not a validated anatomical cutoff. Cleanup - cannot repair wrongly labelled anatomy connected to the main body. - `spine.py::main` then thresholds the body/process labels, crops around - the vertebra, checks posterior-process orientation, and runs scaled ICP to - the selected reference surface from `default_spine_reference_path`. The - maintained default `--spine_icp_target body` uses only the vertebral body - label and the bundled `L4_BODY_SPINE_COMPRESSION_REF.vtk` surface. The - optional `--spine_icp_target vertebra` uses the body plus posterior-process - labels and requires a matching full-vertebra reference surface. The default - backend is deterministic NumPy ICP with 8,000 surface points and 50 - iterations. Alternate PCA-start rescue attempts are evaluated when - process-vector QC flags an implausible orientation. VTK ICP remains an - optional sensitivity check, not the maintained default. -3. `spine.py` applies the transform and resamples the density image with cubic - interpolation and labels/masks with nearest-neighbor interpolation. -4. `boundary.py::generate_bone_cap_mask` and related helpers generate superior - and inferior PMMA caps. The stable-contact settings are defined by - `DEFAULT_SPINE_STABLE_CONTACT_*` constants in `spine.py`. -5. `materials.py::build_spine_material_table` assigns bone and PMMA material - definitions. The default benchmark linear preset uses - `material_laws.kopperdahl_trab_E`. -6. `model.py::create_microfe_model` writes the N88 model structure, and the - high-level wrapper solves at the selected endpoint, defaulting to Crawford - `0.68%` strain for the maintained linear spine workflow. - -For hip, the maintained short-femur path is: - -1. `GenerateFEM.py::build_femur_command` forwards side, optional compartment - mask, optional Pistoia mask, and the femur crop settings. -2. `femur.py::main` selects the side-specific femur mask and aligns it to the - bundled left or mirrored-right reference. The fixed rough crop for ICP - stability is controlled by `DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM`. -3. The rough crop is used only for the ICP - estimate. The final model is then regenerated from the transformed full scan. -4. `femur.py::proximal_sideways_fall_fixture_plane` and - `boundary.py::generate_projected_material_disk_vtk` create the femoral-head - and greater-trochanter PMMA supports on the full aligned femur. -5. `femur.py::crop_vtk_images_to_greater_trochanter_length` measures the actual - generated GT disk's distal voxel face and applies the final flat shaft crop - the requested length distal to it. Before accepting the length, the usable - distal face must clear the entire transformed native scan end, removing - oblique-tail coverage. The fixture box is not a substitute for the disk edge. - Proximal supports stay unchanged, and the external crop face becomes the - distal shaft support. `_shaft_geometry.json` records the available coverage - and the final crop coordinates. Insufficient coverage stops model generation. -6. `materials.py::build_femur_material_table` assigns bone and PMMA material - definitions. The simple whole-femur path uses one trabecular-style bone - region unless a compartment mask is supplied. -7. The high-level wrapper solves and reports the maintained hip endpoint, - defaulting to `4%` displacement. - -The most important short-femur convention is the final shaft-length definition. -The requested value is the distance between -the final GT support and the final distal shaft boundary condition. The audit in -`ogo/cli/CheckFEModelBC.py::audit_femur_sideways` reports the model-space -measurement as: - -```text -min(z of Greater_Trochanter_PMMA_Nodes) - median(z of Distal_Femur_Nodes) -``` - -This is deliberately a boundary-condition measurement. It verifies the -mechanical model that FAIM/N88 will solve rather than an intermediate crop -coordinate. - -## Basic Commands - -### Recommended Spine Command - -For a standard solved L1 compression model, run the high-level wrapper. This -generates the `.n88model`, audits the boundary conditions, solves it with -FAIM/N88, runs optional Pistoia postprocessing, and writes a compact -`_results.csv` next to the model: - -```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --output_path derivatives/fea \ - --threads 4 \ - --faim_bin_dir /path/to/n88/bin \ - --run_pistoia \ - --critical_volume 12 \ - --critical_strain 0.007 -``` - -The `--vertebra` value is `LEVEL:BODY_LABEL:PROCESS_LABEL`. For example, -`L1:2:3` means that label `2` is the L1 vertebral body and label `3` is the L1 -posterior process. Repeat `--vertebra` to process more than one level from the -same image. - -To additionally report Pistoia for an ROI such as the vertebral body, pass a -mask in the same image space as the calibrated CT and segmentation: - -```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --pistoia_mask sub-001_desc-L1Body_mask.nii.gz \ - --output_path derivatives/fea \ - --threads 4 \ - --faim_bin_dir /path/to/n88/bin \ - --run_pistoia \ - --critical_volume 12 \ - --critical_strain 0.007 -``` - -Ogo transforms this ROI mask through the same preprocessing, alignment, -resampling, and padding steps as the model, then writes the model-space mask as -`_pistoia_mask.nii.gz`. The `_results.csv` includes both full-model -Pistoia fields and `masked_pistoia_*` fields when a mask is supplied. - -If the ROI is already a label inside the main segmentation, keep the workflow -less error-prone by selecting that label directly. For example, if label `20` -is the L1 vertebral body and label `48` is the posterior process, this command -uses the main segmentation as the Pistoia ROI source but keeps only label `20` -for masked Pistoia: - -```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:20:48 \ - --pistoia_mask_label 20 \ - --output_path derivatives/fea \ - --threads 4 \ - --run_pistoia \ - --critical_volume 12 \ - --critical_strain 0.007 -``` - -When `--pistoia_mask_label` is supplied without `--pistoia_mask`, the main -`bone_mask` is used as the ROI source. Repeat `--pistoia_mask_label` to keep a -multi-label ROI. Without any label selection, `--pistoia_mask` remains a binary -mask: every nonzero voxel in that mask is included. - -Generate and solve one L1 model: - -```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --output_path derivatives/fea -``` - -Generate L1 and L2 in one command: - -```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --vertebra L2:4:5 \ - --output_path derivatives/fea -``` - -Generate only the model files, without running FAIM: - -```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --output_path derivatives/fea \ - --no-solve -``` - -Generate the left femur: - -```bash -ogoFEA hip \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-leftFemur_mask.nii.gz \ - --side left \ - --output_path derivatives/fea -``` - -Generate both femurs from a mask containing both sides: - -```bash -ogoFEA hip \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-femur_mask.nii.gz \ - --side both \ - --output_path derivatives/fea -``` - -Generate a femur model with an explicit trabecular/cortical compartment mask: - -```bash -ogoFEA hip \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-leftFemur_mask.nii.gz \ - --side left \ - --compartment_mask sub-001_desc-leftFemur_compartments.nii.gz \ - --output_path derivatives/fea -``` - -Use `--dry-run` to print the lower-level command without generating files. - -## Solver Settings - -By default, `ogoFEA` generates each `.n88model`, audits the boundary -conditions, writes a `_modeling.json` record, then runs the FAIM/N88 solve and -postprocessing. - -Control the FAIM thread count with `--threads`: - -```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --threads 8 -``` - -This sets the FAIM solver command to use the multithreaded engine with -`--threads=8`. It is a per-solve thread count, not a request to run multiple -models in parallel. - -If Ogo and FAIM/N88 are installed in separate conda environments, activate the -Ogo environment first and pass the FAIM environment with `--faim_env`. Ogo -generates the `.n88model` in the current Python process, then launches each -FAIM/N88 subprocess through `conda run -n ENV_NAME`. - -For example, if Ogo is installed in `ogo-dev` and FAIM/N88 is installed in -`ogoloco-n88`: - -```bash -conda activate ogo-dev - -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --faim_env ogoloco-n88 \ - --threads 8 -``` - -The same pattern works for femur: - -```bash -ogoFEA hip \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-leftFemur_mask.nii.gz \ - --side left \ - --faim_env ogoloco-n88 \ - --threads 8 -``` - -Use `--conda_executable` if `conda` is not on `PATH` or if a specific conda -install should be used. For example: - -```bash -ogoFEA spine \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-spine_labels.nii.gz \ - --vertebra L1:2:3 \ - --faim_env ogoloco-n88 \ - --conda_executable /path/to/conda -``` - -Other available solver-location options are `--faim_bin_dir`, -`--faim_install_root`, `--faim_license_dir`, and explicit command overrides -such as `--faim_command`, `--n88postfaim_command`, and `--n88tabulate_command`. -Ogo does not search your filesystem for an arbitrary N88 installation. If the -N88 commands are not already on `PATH`, use `--faim_bin_dir`, `--faim_env`, or -the explicit command overrides. - -FAIM/N88 does not interpret prescribed displacement as percent strain in these -models. The `.n88model` stores the applied displacement in millimeters. For -spine, Ogo converts Crawford `0.68%` strain to millimeters from the generated -vertebral height before solving, updates the model displacement, and reports -that solved load directly. Femur reporting still converts the `4%` endpoint -from generated model geometry for `_results.csv`. - -## Outputs - -The default output directory is the calibrated image directory. Use -`--output_path` for a dedicated derivatives folder. - -Expected default outputs are: - -| Output | Meaning | +| Registration | NumPy ICP, 40,000 points, four axial starts | Centroid-initialized NumPy ICP, 8,000 points, 50 iterations | +| Target | Side-specific femur surface | L4 body surface | +| PMMA thickness / intrusion | 10 / 6 mm | 10 / 6 mm | +| Bone / PMMA Poisson ratio | 0.3 / 0.3 | 0.3 / 0.3 | +| PMMA modulus | 2500 MPa | 2500 MPa | +| Reporting endpoint | 4% of model span along loading axis | 0.68% of top-to-bottom BC-centroid distance | +| Full-bone Pistoia default | 11.2% volume, EES 0.009 | 2% volume, EES 0.007 | + +The spine command above explicitly uses 12%, rather than the generic 2% +fallback. Regional critical volume inherits the full-bone value unless set +with `--masked_critical_volume`; it is not automatically 35%. +Critical volume is a percentage; critical strain is a dimensionless fraction. +These are analysis choices, not universally validated failure thresholds. + +Hip registration uses a 120-mm proximal rough crop, then returns to the +transformed full scan. Proximal supports are built before the flat distal crop. +The default shaft is 10 mm distal to the generated GT support edge, after +clearing the transformed incomplete scan end. Final length is verified from +GT and distal BC nodes. Insufficient coverage is recorded as `too_short` and +is not solved. `--femur_greater_trochanter_distal_length` changes this length; +the 11.2% criterion is specific to the 10-mm protocol, not other lengths. + +Spine keeps the largest face-connected body component without changing process +labels. Registration rejects implausible axial/process rotations rather than +returning a least-bad fit. Caps use axial body contact, small-tip trimming, +gap closing and largest-component retention without overwriting bone. Default +trim fraction is 0.10, minimum shift 3 mm. See `boundary.py` and the +`DEFAULT_SPINE_STABLE_CONTACT_*` constants for the exact construction. +Optional `--registration_backend vtk` and `--spine_icp_target vertebra` are +available; whole-vertebra ICP requires a matching aligned reference surface. + +For linear materials, the hip converts QCT to ash density +`rho_ash = 1.06 * rho_QCT + 0.0389` (g/cm3), then uses +`E = 10500 * rho_ash^2.29` MPa. The default spine `benchmark-linear` preset +uses `E = 2980 * rho_QCT^1.05` MPa without ash conversion. Named laws are in +`material_laws.py`; `--elastic_E_func` overrides the law. Spine also supports +`benchmark-nonlinear` (yield materials, 4% endpoint) and `none` presets; +do not mix these with linear reference results. + +## Outputs and QC + +| Suffix | Contents | | --- | --- | -| `.n88model` | The generated finite-element model. | -| `_modeling.json` | Traceable record of inputs, alignment, image processing, material laws, boundary conditions, audit summary, and reporting settings. | -| `_results.csv` | One-row summary written after solve with stiffness, reaction force, and profile-specific force endpoints. | -| Solve/postprocessing files | Written when `--no-solve` is not used. These come from FAIM/N88 postprocessing. | - -Use `--debug` to write visual sidecars such as the boundary-condition audit -PNG. These images are intended for manual model inspection and are not part of -the minimal output contract. - -## Results CSV - -After a successful solve, `ogoFEA` writes one compact `_results.csv` next to the -model. This is the file intended for downstream analysis tables. - -Maintained spine and femur models write the same columns: - -| Field | Meaning | -| --- | --- | -| `model_file` | Source `.n88model`. | -| `analysis_file` | FAIM/N88 postprocessing text file used to tabulate reaction force. | -| `analysis_var` | N88 reaction-force variable tabulated from `analysis_file`. | -| `applied_displacement` | Prescribed displacement in the solved `.n88model`. | -| `reaction_force_N` | Reaction force from the solved model at the prescribed displacement. | -| `stiffness_N_per_mm` | Reaction force divided by applied displacement when computable. | -| `characteristic_length_mm` | Spine: distance between `body_top` and `body_bottom` centroids along the loading axis. Femur: full generated-model span along the loading axis. | - -## Reaction Force and Displacement Endpoint - -The reported `reaction_force_N` is the net support reaction from the solved -model at the prescribed displacement endpoint. For spine compression this is -tabulated from the axial `fz_ns1` reaction force. For hip sideways fall this is -tabulated from the lateral `fy_ns1` reaction force. The sign depends on the -model coordinate convention, so the compact CSV reports the force magnitude -used for stiffness and downstream comparisons. - -The reaction force and stiffness in `_results.csv` should be interpreted at -that endpoint. They are the relevant summary values for the chosen model -protocol: the force is the load carried by the model at the target normalized -displacement, and stiffness is the secant stiffness from zero displacement to -that same endpoint. Changing the endpoint can change both values, especially -for nonlinear material settings, so comparisons should use the same model type, -material law, boundary conditions, and displacement endpoint. - -Ogo chooses percent displacement endpoints so models of different physical -size are compared at a similar normalized deformation: - -| Model | Default endpoint | Length used for conversion | Reason | -| --- | --- | --- | --- | -| Spine compression | `0.68%` strain | Distance between `body_top` and `body_bottom` centroids along the loading axis | Matches the vertebral strength endpoint reported by Crawford et al. (2003), and keeps the solved displacement scaled to each vertebral body height. | -| Hip sideways fall | `4%` displacement | Full generated-model span along the loading axis | Keeps the sideways-fall load endpoint scaled to each generated femur model instead of using one fixed millimeter displacement for all femora. | - -FAIM/N88 stores prescribed displacement in millimeters, not percent strain. -Before solving, `ogoFEA` converts the percent endpoint to millimeters from the -generated model geometry and updates the `.n88model`. The exact percent -endpoint, converted millimeter displacement, and characteristic length are -recorded in `_modeling.json`; `_results.csv` stays compact for downstream -statistics. `stiffness_N_per_mm` is computed as -`reaction_force_N / applied_displacement`. - -## Reference Models - -The reference models define the common coordinate frame used before meshing and -boundary-condition placement. They are alignment targets, not subject-specific -geometry copied into the final model. - -For spine compression, the bundled vertebral-body reference is an L4 body. The -same reference is used for all requested vertebrae. Before ICP, Ogo estimates -principal-axis lengths for the segmented body and for the L4 reference, scales -the reference to match the target vertebra within the configured scale limits, -and then runs rigid ICP. This lets one L4 reference provide a stable model frame -for other vertebral levels while still adapting to their size. - -For hip sideways fall, the canonical bundled reference is a left femur. Left -femur models ICP-align directly to that reference. Right femur models use -`RT_FEMUR_SIDEWAYS_FALL_REF.vtk` when it is present; otherwise Ogo mirrors the -left femur reference across the x direction in memory and uses that mirrored -surface as the right-side ICP target. The mirrored reference keeps left and -right outputs in matching sideways-fall coordinate frames without requiring a -separate right reference file. - -## Spine Model - -The spine workflow builds one compression model per `--vertebra` target: - -1. Keep the largest face-connected body-label component, record the removed - volume, then threshold the labelled mask into body and posterior process. -2. Crop the image and masks to the vertebra. -3. Align the body to the scaled L4 vertebral-body reference using ICP. -4. Apply transform and isotropic resampling through the shared VTK reslice - helper. The default output spacing is `1.0 x 1.0 x 1.0 mm`. -5. Smooth body/process masks with one binary close/open pass only when at - least one input spacing dimension is coarser than 2 mm. -6. Generate superior and inferior PMMA caps fitted to the body surface. -7. Convert density to material IDs and construct the N88 model. -8. Apply axial compression boundary conditions. - -Default spine boundary conditions: - -| Region | Node set | Constraint | -| --- | --- | --- | -| Superior PMMA cap | `body_top` | Prescribed displacement in the compression direction. | -| Inferior PMMA cap | `body_bottom` | Fixed in all displacement directions. | - -The default high-level spine preset is `benchmark-linear`. It applies the -spineFE benchmark linear material settings for model construction, then -`ogoFEA` updates the generated model to solve at Crawford `0.68%` strain. -The `0.68%` endpoint follows Crawford RP, Cann CE, Keaveny TM. Finite element -models predict in vitro vertebral body compressive strength better than -quantitative computed tomography. Bone. 2003;33(4):744-50. -doi:10.1016/s8756-3282(03)00210-2. - -Available spine presets: - -| Preset | Use | -| --- | --- | -| `benchmark-linear` | Default linear spineFE benchmark-style run. | -| `benchmark-nonlinear` | Nonlinear material/yield settings for benchmark-style runs. | -| `none` | Pass only explicit lower-level options. | - -Spine registration can be switched without editing code. The maintained default -uses dense deterministic NumPy ICP on the vertebral-body surface: - -Initialization matches centroids while retaining native physical anatomical -directions. PCA is used to estimate reference size, not to swap registration -axes. The rigid fit is rejected if the superior-inferior axis or body-to-process -direction changes by more than 60 degrees, or if posterior-process QC fails -on the final resampled and cleaned masks. This initialization applies to both -body-only and full-vertebra registration targets. -There are no axis-permutation retries or least-bad fallback models. Images must -have a superior-inferior physical z axis; NIfTI array ordering alone is not an -orientation check. The same checks apply to the optional VTK backend. -Boundary-contact QC remains separate: a correctly aligned model with a missing -support must not be solved. - -```bash -ogoFEA spine image.nii.gz spine_labels.nii.gz \ - --vertebra L1:20:48 \ - --registration_backend numpy \ - --registration_landmarks 8000 \ - --registration_iterations 50 -``` - -For cases where the box-like vertebral body gives ambiguous rotations, ICP can -instead use the whole vertebra surface. This needs a reference surface generated -from the same aligned L4 reference, including body and posterior processes: - -```bash -ogoFEA spine image.nii.gz spine_labels.nii.gz \ - --vertebra L1:20:48 \ - --spine_icp_target vertebra \ - --reference_path /path/to/L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk -``` - -If `--reference_path` is omitted with `--spine_icp_target vertebra`, Ogo looks -for the bundled `ogo/dat/L4_FULL_VERTEBRA_SPINE_COMPRESSION_REF.vtk` file and -fails clearly if it is not installed. - -VTK ICP remains available for sensitivity checks, but it is not the maintained -default for the spine workflow: - -```bash -ogoFEA spine image.nii.gz spine_labels.nii.gz \ - --vertebra L1:20:48 \ - --registration_backend vtk \ - --registration_landmarks 8000 \ - --registration_iterations 50 -``` - -## Femur Model - -Full-bone and regional Pistoia criteria can differ: use `--critical_volume` for -the full bone and `--masked_critical_volume` for the supplied regional mask. -For example, the L1 study uses `--critical_volume 12 --masked_critical_volume 35`. -If the regional criterion is omitted, it inherits the full-bone value. The -results CSV records both criteria and both failure loads separately. - -The femur workflow builds one sideways-fall model per side: - -1. Read the calibrated image and whole-femur mask. -2. Pre-rotate the side to a stable starting orientation. -3. ICP-align to the left femur reference or the mirrored right reference. The - maintained hip default uses evenly distributed surfaces (up to 40,000 points - per surface), four axial starting rotations, and bidirectional mean surface - distance to select a rigid NumPy ICP fit. Superior-inverting candidates are - rejected. The reference may be scaled to estimate registration; the native - bone is transformed rigidly and its physical dimensions are not scaled. - This fixed hip registration recipe has no backend-selection option. -4. Apply transform and isotropic resampling through the shared VTK reslice - helper. The default output spacing is `1.0 x 1.0 x 1.0 mm`. -5. Smooth the transformed femur mask with one binary close/open pass only when - at least one input spacing dimension is coarser than 2 mm. If a compartment - mask is supplied, the derived cortical binary mask follows the same rule. -6. Generate PMMA fixtures for the femoral head and greater trochanter. The - femoral-head fixture is placed on the high-y side and the greater-trochanter - fixture is placed on the low-y side. Both fixtures are anchored to the - proximal model footprint so they do not overwrite bone voxels. -7. Standardize the distal shaft with a flat post-ICP model-grid cut. The - rough 120-mm crop is used only for ICP and the - final model uses the transformed full scan. The requested shaft length starts - at the actual generated GT disk's distal face. Crop all model-grid images - together, preserving the proximal fixtures and their coordinates. - The default retained shaft is 10 mm. Scans with less available coverage are - recorded as too short; they are not solved as shorter substitutes. -8. Convert density to material IDs and construct the N88 model. -9. Apply sideways-fall boundary conditions. - -Default femur boundary conditions: - -| Region | Node set | Constraint | -| --- | --- | --- | -| Femoral head PMMA | `Femoral_Head_PMMA_Nodes` | Prescribed displacement toward the greater trochanter. | -| Greater trochanter PMMA | `Greater_Trochanter_PMMA_Nodes` | Constrained in the loading direction. | -| Distal shaft cut face | `Distal_Femur_Nodes` | Constrained to remove rigid-body motion. | - -Femur reporting defaults to stiffness and force at `4%` displacement. When -`--run_pistoia` is supplied, the high-level wrapper also reports full-model -Pistoia failure load and, if `--pistoia_mask` is supplied, masked-region -Pistoia failure load. - -Femur masked Pistoia follows the same ROI convention as spine. A separate -femoral-neck mask can be passed with `--pistoia_mask`. If the femoral-neck ROI -is a label inside the same segmentation used as the femur mask, pass -`--pistoia_mask_label LABEL` and omit `--pistoia_mask`; Ogo will use the main -segmentation as the ROI source and keep only that label. - -The maintained 10-mm reporting configuration is: - -```bash -ogoFEA hip image.nii.gz femur_mask.nii.gz \ - --side left \ - --femur_greater_trochanter_distal_length 10 \ - --femur_shaft_length 120 \ - --run_pistoia \ - --critical_volume 11.2 \ - --critical_strain 0.009 -``` - -There is one maintained crop recipe. The command no longer exposes aspect-ratio, -lesser-trochanter or oblique-midpoint alternatives. `--femur_cut_mode` remains a -hidden compatibility argument accepting only `greater_trochanter_length`; -old unsupported modes fail rather than silently using a different definition. - -The 11.2% full-bone criterion is specific to the adopted 10-mm shaft protocol; -it is not automatically valid for other shaft lengths. Use `--no-solve` for -geometry inspection. Length-specific calibration does not change the material -law or critical strain. - -The generated model can be audited with `ogoFEA check-bc`. For hip sideways -fall, the post-GT-support shaft length is measured from final boundary-condition -node sets as: - -```text -min(z of Greater_Trochanter_PMMA_Nodes) - median(z of Distal_Femur_Nodes) -``` - -The GT minimum is the distal edge; a percentile inside the disk would shift the -length origin. The distal median represents the flat external support plane. - -### Geometry Records And 3D QC - -Every generated hip model has three inspectable sidecars: - -| File suffix | Contents | -| --- | --- | -| `_shaft_geometry.json` | Full available shaft before cropping, requested length, retained voxel-face length, verified length from the saved N88 BC nodes, GT/cut coordinates, registration diagnostics and export paths. | -| `_surface.vtp` | External FE mesh surface in physical coordinates. `ModelRegion`: 0 bone, 1 GT support, 2 FH loading support. Material IDs are preserved. | -| `_3d.png` | Four opaque mesh views: blue bone, gold GT support, red FH loading support, green distal constraint nodes. | - -`_icp.json` records the selected transform and all starting-fit scores. Explicit -`--femur_icp_transform_out` overrides its path. A supplied transform remains -useful for fixed-registration length sweeps, but cohort regeneration should -estimate a fresh transform rather than reuse an unreviewed historical fit. - -`available_shaft_length_mm` is measured on the transformed full-scan model -after generating the actual connected GT support and before the final crop. -Each proximal support retains its own largest 26-connected PMMA component -before the disks are combined. This removes disconnected shaft islands without -discarding either main disk; connected unwanted extensions still require QC. -It measures **usable complete-section coverage**, not the distance to the -lowest corner of an angled scan end. Before any resampling, rough crop or -padding, `shaft_geometry.capture_distal_scan_face` records the physical voxel -corners of the first occupied distal femur slice. The inverse rigid ICP carries -this original full-scan face into the model frame. The most proximal point -(maximum aligned Z), plus a voxel-scale resampling clearance, sets the minimum -safe flat plane; it is snapped proximally to the model's voxel-face grid. -Background image padding and the separate 120-mm registration crop do not -define the usable scan end. - -The clearance is half the first resampling voxel's extent projected onto -aligned Z, plus half a final Z voxel. It bounds the two nearest-neighbor mask -resamplings and is recorded in the sidecar; there is no fitted distance or -percentile threshold. The final cut is the requested shaft length distal to the -unchanged GT disk. If that cut cannot clear the original scan end, generation -stops as `too_short`; it never substitutes a shorter shaft. No extra physical -pre-trim changes the proximal supports or the ICP input. - -Version 3 geometry records distinguish `raw_available_shaft_length_mm`, -`safe_distal_face_z_mm`, `oblique_trim_loss_mm`, and usable -`available_shaft_length_mm`. `distal_scan_boundary` records the original and -aligned limits and clearance. `complete_distal_section_verified` becomes true -only when the saved distal BC plane also clears that safe limit. This check -does not establish segmentation correctness or replace anatomical QC. -`measured_shaft_length_mm` is checked after rereading the saved N88 file. -For insufficient coverage, the JSON is still written with `status: too_short` -and a null final length. Registration failures are unmeasured, not assigned a -zero length. Coverage plots must use these records, state their denominator, -and distinguish missing measurements from genuinely short scans. Successful -geometry checks do not by themselves establish anatomical alignment quality. - -Implementation: `hip_registration.estimate_femur_icp` selects the rigid fit; -`shaft_geometry.measure_available_shaft` defines coverage; -`femur.crop_vtk_images_to_greater_trochanter_length` cuts all model-grid images; -`shaft_geometry.verify_model_shaft` verifies the saved BC nodes; and -`model_export.export_femur_model` writes the mesh and four-view image. -These helpers do not change the material law, loading endpoint or regional -Pistoia definitions. Whole-bone hip Pistoia defaults to 10% at strain 0.009. - -Use `--no-solve` to generate models and QC for inspection before solving. -`--threads` limits VTK/ITK generation as well as the FAIM solver. For an -I/O-heavy cohort generation pass, independent `--no-solve --threads 1` jobs -can use the CPU budget without multiplying each job's image-filter threads. -PNG rendering needs an X display or an EGL/OSMesa-enabled headless VTK build; -without one, the N88 model and VTP are preserved and the export record marks -PNG QC as unavailable. The surface export itself does not require a display. -On Groot the existing -`ogoloco-n88` environment provides headless VTK and the modeling dependencies. - -## Materials - -Spine and femur use the same shared material-table builder in -`ogo.fea.materials`. - -Material ID convention: - -| Material | IDs | -| --- | --- | -| Background | `0` | -| Trabecular bone | `1..128` | -| Cortical bone | `129..256` when a cortical region is present | -| PMMA fixtures/caps | Default `5000` | - -The default material law is `default_E`, defined in `ogo.fea.material_laws`: - -```text -E = 10500 * rho^2.29 MPa -``` - -where `rho` is density in g/cc. Poisson's ratio defaults to `0.3` for bone. -PMMA defaults are `E = 2500 MPa`, Poisson's ratio `0.3`. - -The default spine preset overrides the elastic law to `kopperdahl_trab_E` for -both trabecular and cortical regions. The simple femur path uses `default_E` -unless a material-law override is supplied. If a femur compartment mask is -provided, cortical and trabecular regions use the same law unless cortical -overrides are supplied. - -### Kopperdahl Spine Presets - -The spine benchmark presets use the Kopperdahl trabecular density-modulus law: - -```text -E = 2980 * rho^1.05 MPa -``` - -where `rho` is density in g/cc. In code this is -`ogo.fea.material_laws.kopperdahl_trab_E`. - -The `benchmark-linear` preset uses this elastic law only. It does not assign a -bone yield criterion, so the solved model is linear elastic. The maintained -spine workflow runs Pistoia when `--run_pistoia`, `--require_pistoia`, or -`--pistoia_mask` is supplied. - -The `benchmark-nonlinear` preset additionally assigns the Kopperdahl -compressive yield law: - -```text -sigma_y,c = 37.4 * rho^1.39 MPa -``` - -where `rho` is density in g/cc. In code this is -`ogo.fea.material_laws.kopperdahl_trab_yc`. The current nonlinear preset uses -the same function for compression and tension unless the user supplies separate -yield functions. When yield functions are present, Ogo creates -`vtkboneMohrCoulombIsotropicMaterial` entries with the resolved tensile and -compressive yield strengths. - -Material-law overrides can be passed through the high-level command because -unknown options are forwarded to the lower-level generator: - -```bash -ogoFEA hip \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-leftFemur_mask.nii.gz \ - --side left \ - --elastic_E_func kopperdahl_trab_E \ - --cort_elastic_E_func kopperdahl_trab_E -``` - -Supported named laws are defined in `ogo.fea.material_laws`, including -`default_E`, `kopperdahl_trab_E`, `kopperdahl_trab_yc`, `morgan_trab_E`, -`crawford_voxel_E`, and Bayraktar-style constants. - -## Quality Control - -Every generated model is audited by `ogo/cli/CheckFEModelBC.py` unless -`--skip_bc_audit` is used. The audit summary is embedded in `_modeling.json`. - -For manual inspection, run with `--debug`: - -```bash -ogoFEA hip \ - sub-001_desc-vqct_ct.nii.gz \ - sub-001_desc-leftFemur_mask.nii.gz \ - --side left \ - --debug \ - --no-solve -``` - -This writes a boundary-condition audit PNG next to the model. For spine, debug -mode also enables quick-look output from the lower-level generator. - -## Common Adjustments - -Change output resolution: - -```bash -ogoFEA spine image.nii.gz labels.nii.gz \ - --vertebra L1:2:3 \ - --iso_resolution 0.8 -``` - -Change femur shaft retention after the greater-trochanter support: - -```bash -ogoFEA hip image.nii.gz femur_mask.nii.gz \ - --side left \ - --femur_cut_mode greater_trochanter_length \ - --femur_greater_trochanter_distal_length 45 -``` - -Change PMMA dimensions: - -```bash -ogoFEA hip image.nii.gz femur_mask.nii.gz \ - --side left \ - --pmma_thick 10 \ - --pmma_intrusion 6 -``` - -For hip sideways fall, both PMMA fixtures use bbox-relative contact planes by -default. The maintained footprint is rectangular, centered from stored bbox -fractions, and scaled independently to `1.1 x 1.1` of the model bbox in the two -authored in-plane axes. `--pmma_thick` controls total fixture thickness; -`--pmma_intrusion` controls how far anatomy can occupy that fixed thickness -before unsupported contact columns are removed. The PMMA labels do not replace -bone voxels. - -For spine compression, `--pmma_thick` controls the total generated disk -thickness and `--pmma_intrusion` controls how far anatomy can occupy that fixed -thickness. The maintained spine defaults are 10 mm thickness and 6 mm intrusion. -Intrusion does not overwrite vertebral-body voxels; it limits how far from the -superior/inferior body surface the cap is allowed to search for supporting -anatomy before generating the flat PMMA disk. - -Stable-contact selection is enabled for the spine default. The locked recipe -ignores the superficial 10% of the projected contact columns -(`--stable_surface_trim_fraction 0.10`) and uses that deeper footprint only -when it is at least 3 mm behind the first-contact surface -(`--stable_surface_min_shift 3`). This catches osteophyte/ridge contacts while -leaving normal shallow surface variation alone. The generated disk still uses -the requested `--pmma_thick` and `--pmma_intrusion`; stable contact does not -replace or silently reduce intrusion. The disk mask is repaired with in-plane -gap closing, constrained not to overwrite anatomy, and reduced to the largest -connected support component. - -After vtkbone identifies visible cap nodes, spine compression filters each cap -node set to the dominant coordinate plane along the load axis. This removes -small rim/contact-side nodes from the jelly-bean disk surface while keeping the -full disk material geometry in the solved model. - -```bash -ogoFEA spine image.nii.gz labels.nii.gz \ - --vertebra L1:20:48 \ - --pmma_thick 10 \ - --pmma_intrusion 6 -``` - -The stable-contact values are code-level defaults in -`DEFAULT_SPINE_STABLE_CONTACT_*` and are locked by -`tests/fea/test_workflow_locked_defaults.py`. Routine spine FEA should use the -defaults rather than carrying per-run contact tuning flags. - -Run model generation only, inspect the `.n88model`, then solve later: - -```bash -ogoFEA hip image.nii.gz femur_mask.nii.gz \ - --side left \ - --no-solve \ - --debug -``` +| `.n88model` | Generated model, updated with solution fields after solving | +| `_results.csv` | Reaction force, stiffness and requested Pistoia results | +| `_modeling.json` | Inputs, processing, materials, BCs and solver settings | +| `_qc_metrics.csv` | Generation measurements, extended with solved SED checks | +| `_qc_3d.webp`, `_sed_3d.webp` | Before/after three-view previews | +| `_shaft_geometry.json` | Hip available, requested and verified retained length | +| `_pistoia_mask.nii.gz` | Regional mask transformed into model coordinates | + +CSV forces are in N and stiffness in N/mm; Pistoia also has kN fields. +Reaction-force sign follows the model convention; use magnitudes for comparisons. +Linear stiffness is reaction-force magnitude divided by applied displacement; +Pistoia stiffness is reported separately. Missing results are blank, not zero. + +Previews use white backgrounds, gold supports and red constrained surfaces. +Solved bone uses a shared linear Jet SED scale. Display smoothing does not +change FE results. A graphics failure warns but does not invalidate the model. +Compact WebP previews are for QC, not quantitative analysis. Legacy PNGs are +supported. Too-short hips receive an available-anatomy preview, not a solved model. + +```bash +ogoValidateFEA derivatives/fea --output derivatives/qc --gallery-zip --workers 4 +``` + +This writes `qc_summary.csv`, `study_inclusion.csv` and portable `gallery.zip`. +Extract the ZIP and open `gallery.html`; models are not bundled. Use `--gallery` +instead for an unpacked gallery and `--reviews study_inclusion.csv` to restore +manual decisions. Filters, sorting, page size and individual before/after views +support large-cohort review. Include/exclude records a reason and reviewer. +Export the reviewed CSV: browser edits do not update the project CSV or ZIP. + +QC fails construction errors, flags uncertain anatomy or missing evidence for +review, and does not apply strength thresholds. A pass is not proof of correct +segmentation. Hip tests include shaft length, complete distal section, intended +support-patch coverage and X/Z fixation; spine tests include cap contact and +body/process orientation. Substantial body cleanup is flagged for review. + +Old models without measurements are backfilled once and cached. Use `--site` +when anatomy cannot be inferred and `--evidence evidence.csv` for registered +body/process masks or shaft sidecars stored elsewhere. Evidence columns are +`site,model_id,body_mask,process_mask,shaft_geometry`; paths are relative to the +CSV. Native-space masks cannot substitute for model-space evidence. Missing +scan coverage stays unknown; backfill neither rewrites models nor renders images. +Failures before meshing require job logs for participant accounting. + +For full-size resegmentation of reviewed failures, see the +[cohort recovery example](../../examples/fea/cohort_recovery/README.md). diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 00dcaa4..dac8ef6 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -1,41 +1,8 @@ -"""Femur-specific defaults and helpers for sideways-fall FE generation. - -Shared point-cloud and boundary-contact primitives live in ``alignment`` and -``boundary``. They are re-exported here so older femur workflow imports keep -working while this module stays focused on femur defaults and femur-only -geometry. - -Default femur workflow: -1. Read the calibrated density image and whole-femur mask. The public wrapper - chooses left/right from ``--side`` and the lower-level script uses - ``femur_side=1`` for left and ``2`` for right. -2. Run ICP to the bundled side-specific femur reference from the cropped and - padded input geometry, then build an explicit reference-frame output grid - from the transformed femur surface. Density is resampled with B-spline - interpolation; bone, crop-face, and compartment labels use nearest-neighbor - interpolation on the same grid. -3. Smooth the transformed femur mask with one binary close/open pass only when - at least one input spacing dimension is coarser than 2 mm. If a compartment - mask is supplied, the derived cortical binary mask follows the same rule. -4. Stabilize registration with a fixed 120 mm proximal rough crop after - isotropic resampling. After ICP, apply the final flat distal crop on the - transformed full scan so the shaft extends the requested length distal to the edge - of the greater-trochanter PMMA support. This final crop face becomes the - distal support surface. -5. Generate two geometric PMMA fixtures from proximal contact planes: - a femoral-head loading fixture on the high-y side and a greater-trochanter - contact fixture on the low-y side. Defaults are 10 mm PMMA thickness and - 6 mm intrusion through that fixed thickness. The fixture footprints are - anchored at the proximal femur so they cannot contact the distal shaft crop - face, and the fixture masks themselves do not overwrite bone voxels. -6. Apply sideways-fall boundary conditions: prescribed displacement at the - femoral-head PMMA cap toward the greater trochanter, loading-direction - constraint at the greater-trochanter PMMA cap, and distal shaft constraints - to remove rigid-body motion. -7. Build materials with the same shared bone/PMMA material-table helper used by - the spine workflow. If no compartment mask is supplied, femur bone is one - trabecular-style region. If supplied, cortical=1 and trabecular=2 by default. -""" +"""Sideways-fall femur models: registration, GT-based shaft crop and supports. + +Density uses cubic interpolation and labels use nearest-neighbor. Shared +geometry helpers live in alignment and boundary; the ogoFEA wrapper solves +and reports results. See docs/fea/implementation.md for the code map.""" from pathlib import Path @@ -2333,6 +2300,7 @@ def expand_xz_footprint(bounds, *, x_extension_mm=0.0, z_extension_mm=0.0): def remove_extension(filename): + """Remove all filename suffixes, including compound NIfTI extensions.""" while True: filename, ext = os.path.splitext(filename) if not ext: @@ -2343,6 +2311,7 @@ def remove_extension(filename): ## # Start script def sidewaysFallFe(args): + """Build a sideways-fall model and its shaft geometry and QC sidecars.""" ogo.message("Start of Script...") ## @@ -3229,7 +3198,7 @@ def sidewaysFallFe(args): def main(): - # Setup description + """Parse anatomy-builder arguments and generate a sideways-fall femur model.""" description=''' This script sets up the sideways fall FE model on the hip from the density (K2HPO4) calibrated image. This script sets up the model for either a left or diff --git a/ogo/fea/gallery.py b/ogo/fea/gallery.py index 285c16f..dede2e5 100644 --- a/ogo/fea/gallery.py +++ b/ogo/fea/gallery.py @@ -37,6 +37,7 @@ def _camera_images(path, output): def generate_gallery(rows, output, decisions=None, image_root=None): + """Write a paginated review gallery from measurements and saved decisions.""" output = Path(output).absolute() rows = [dict(row) for row in rows] outcome_keys = ('stiffness_N_per_mm', 'reaction_force_N', 'pistoia_failure_load_N', diff --git a/ogo/fea/legacy_qc.py b/ogo/fea/legacy_qc.py index a3d42f4..6d81d66 100644 --- a/ogo/fea/legacy_qc.py +++ b/ogo/fea/legacy_qc.py @@ -40,6 +40,7 @@ def load_evidence(path): def evidence_paths(path, evidence=None): + """Resolve registered-mask and geometry sidecars, allowing explicit overrides.""" path = Path(path) return {key: Path((evidence or {}).get(key) or path.with_name(path.stem + suffix)) for key, suffix in SUFFIXES.items()} @@ -57,6 +58,7 @@ def evidence_signature(path, evidence=None): def needs_backfill(path, row, evidence=None): + """Check whether legacy measurements are missing or their evidence changed.""" if not row: return True if not row.get('measurement_source', '').startswith('legacy_backfill'): @@ -111,6 +113,7 @@ def read_mask(path): def backfill_measurements(path, site=None, evidence=None): + """Recover QC measurements from a saved model without modifying its geometry.""" import vtkbone from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtkIdTypeArray diff --git a/ogo/fea/material_laws.py b/ogo/fea/material_laws.py index 351db1f..6e55203 100644 --- a/ogo/fea/material_laws.py +++ b/ogo/fea/material_laws.py @@ -1,39 +1,50 @@ -# material_laws.py -# Material law functions for trabecular and cortical bone -# Input density is in mg/cc (same as your bin_centers) +"""Scalar bone laws: density in mg/cm3, modulus and yield stress in MPa. + +Density convention is set by the anatomy builder (ash for hip, calibrated QCT +for spine); these functions only convert mg/cm3 to g/cm3. +""" + +from typing import Callable # === Morgan et al. (2003) === -def morgan_trab_E(density_mgcc): +def morgan_trab_E(density_mgcc: float) -> float: + """Return Morgan elastic modulus (MPa) from density (mg/cm3).""" if density_mgcc < 0: return 0.0001 rho = density_mgcc / 1000.0 # Convert to g/cc return 4730 * rho ** 1.56 # MPa # === Bayraktar et al. (2004) tissue-level constants === -def bayraktar_trab_E(density_mgcc): +def bayraktar_trab_E(density_mgcc: float) -> float: + """Return the constant trabecular tissue modulus (MPa); density is unused.""" return 18000 # MPa -def bayraktar_trab_yc(density_mgcc): +def bayraktar_trab_yc(density_mgcc: float) -> float: + """Return the constant trabecular compressive yield stress (MPa).""" return 134 # MPa (compressive) -def bayraktar_trab_yt(density_mgcc): +def bayraktar_trab_yt(density_mgcc: float) -> float: + """Return the constant trabecular tensile yield stress (MPa).""" return 83 # MPa (tensile) # === Kopperdahl et al. (2002) === -def kopperdahl_trab_E(density_mgcc): +def kopperdahl_trab_E(density_mgcc: float) -> float: + """Return spine elastic modulus (MPa) from calibrated density (mg/cm3).""" if density_mgcc < 0: return 0.0001 rho = density_mgcc / 1000.0 return 2980 * rho ** 1.05 # MPa -def kopperdahl_trab_yc(density_mgcc): +def kopperdahl_trab_yc(density_mgcc: float) -> float: + """Return compressive yield stress (MPa) from density (mg/cm3).""" if density_mgcc < 0: return 0.0001 rho = density_mgcc / 1000.0 return 37.4 * rho ** 1.39 # MPa # === Crawford et al. (2003) === -def crawford_voxel_E(density_mgcc): +def crawford_voxel_E(density_mgcc: float) -> float: + """Return nonnegative Crawford voxel modulus (MPa) from density (mg/cm3).""" if density_mgcc < 0: return 0.0001 rho = density_mgcc / 1000.0 @@ -41,38 +52,47 @@ def crawford_voxel_E(density_mgcc): # === Cortical estimates using Bayraktar === -def bayraktar_cort_E(density_mgcc): +def bayraktar_cort_E(density_mgcc: float) -> float: + """Return the constant cortical tissue modulus (MPa); density is unused.""" return 20000 # MPa -def bayraktar_cort_yc(density_mgcc): +def bayraktar_cort_yc(density_mgcc: float) -> float: + """Return the constant cortical compressive yield stress (MPa).""" return 134 # MPa -def bayraktar_cort_yt(density_mgcc): +def bayraktar_cort_yt(density_mgcc: float) -> float: + """Return the constant cortical tensile yield stress (MPa).""" return 83 # MPa # === Default functions (adapted from trabecular with different scaling) === -def default_E(density_mgcc): +def default_E(density_mgcc: float) -> float: + """Return hip elastic modulus (MPa) from ash density (mg/cm3).""" if density_mgcc < 0: return 0.0001 rho = density_mgcc / 1000.0 return 10500 * rho ** 2.29 # MPa -def default_yc(density_mgcc): +def default_yc(density_mgcc: float) -> float: + """Return the default compressive yield stress (MPa).""" return 120 -def default_yt(density_mgcc): +def default_yt(density_mgcc: float) -> float: + """Return the default tensile yield stress (MPa).""" return 100 # === Define custom functions via command line === # -def constant(value): +def constant(value: float) -> Callable[[float], float]: + """Return a density-independent scalar law.""" # use as: --elastic_E_func constant_Value return lambda density_mgcc: value -def linear(A, B): +def linear(A: float, B: float) -> Callable[[float], float]: + """Return A + B * density, with density converted to g/cm3.""" # use as: --elastic_E_func linear_A_b return lambda density_mgcc: A + B * (density_mgcc / 1000.0) -def power_law(A, b): +def power_law(A: float, b: float) -> Callable[[float], float]: + """Return A * density**b, with density converted to g/cm3.""" # use as: --elastic_E_func power_A_b return lambda density_mgcc: A * (density_mgcc / 1000.0) ** b diff --git a/ogo/fea/qc_render.py b/ogo/fea/qc_render.py index 52d42a0..700d8bb 100644 --- a/ogo/fea/qc_render.py +++ b/ogo/fea/qc_render.py @@ -46,6 +46,7 @@ def smooth_bone_sed(values, indices, bone, spacing): def preview(model_path, output_dir, site, body_mask=None, view="oblique", sed=False, model_override=None, ineligible=False): + """Render one model view with anatomy or SED colours and coloured supports.""" start = perf_counter() if model_override is None: reader = vtkbone.vtkboneN88ModelReader() diff --git a/ogo/fea/review.py b/ogo/fea/review.py index 0ff77c7..eb10cc4 100644 --- a/ogo/fea/review.py +++ b/ogo/fea/review.py @@ -11,10 +11,12 @@ def identity(row): + """Return the site and model ID used to match review decisions.""" return (row['site'], row['model_id']) def inclusion_row(measurement, decision=None): + """Combine automatic QC with a manual decision, retaining both for provenance.""" decision = decision or {} manual = decision.get('manual_decision') or 'automatic' if manual not in ('automatic', 'include', 'exclude'): @@ -50,6 +52,7 @@ def load_decisions(path): def write_inclusion(rows, path, decisions=None): + """Write study decisions separately from the model measurement CSV.""" decisions = decisions or {} with open(path, 'w', newline='') as stream: writer = csv.DictWriter(stream, fieldnames=FIELDS) diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index f102cd9..ba9b721 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -1,38 +1,7 @@ -"""Spine-specific defaults, helpers, and spineFE benchmark presets. - -Default spine workflow: -1. Read the calibrated density image and labelled vertebra mask. The high-level - wrapper uses ``--vertebra LEVEL:BODY_LABEL:PROCESS_LABEL`` so body and - posterior-process labels are explicit and traceable. -2. Retain the largest face-connected body-label component, leaving the process - and other labels unchanged. Record the removed volume for segmentation QC. - Crop the vertebral body, posterior process, calibrated image, and combined - vertebra mask to the same bounding box. -3. ICP-align the requested registration target to a bundled L4 compression - reference surface. The maintained body-only target uses the vertebral body; - the optional full-vertebra target uses body plus posterior-process labels - when the matching reference surface is available. By default the reference - is PCA-scaled within 0.8,0.8,0.75 to 1.2,1.2,1.3 before deterministic NumPy - rigid ICP (8,000 surface points, 50 iterations), initialized by centroids - without PCA axis swaps. Anatomical orientation checks reject invalid fits. - Reference scaling does not scale the physical dimensions of the solved bone. -4. Apply the ICP transform and set the final output spacing to - 1.0 x 1.0 x 1.0 mm in one shared VTK reslice helper. Image data use cubic - interpolation; body/process labels use nearest-neighbor interpolation. -5. Smooth the transformed body/process masks with one binary close/open pass - only when at least one input spacing dimension is coarser than 2 mm. Then - generate fixed-thickness anatomy PMMA caps on the superior and inferior body - surfaces. The maintained cap thickness is 10 mm and the intrusion depth is - 6 mm; projected contact starts at the first stable body footprint so small - osteophytes do not define the support plane. -6. Build materials with the same shared bone/PMMA material-table helper used by - the femur workflow. The spine convention is trabecular material IDs 1..128 - and cortical IDs 129..256; PMMA is a separate material ID. -7. Apply axial compression boundary conditions: prescribed displacement at the - superior PMMA cap toward the inferior cap and fixed displacement at the - inferior cap. The wrapper solves and reports at 0.68 percent strain by - default. -""" +"""Spine compression models: preparation, registration, supports and defaults. + +Shared geometry and mesh helpers live in alignment, boundary and model. +The public ogoFEA wrapper handles solving and reporting; see docs/fea/implementation.md.""" import os from pathlib import Path @@ -416,6 +385,7 @@ def _as_bool(value): ###################################################################### HELPERS (Python) def remove_extension(filename): + """Remove all filename suffixes, including compound NIfTI extensions.""" while True: filename, ext = os.path.splitext(filename) if not ext: @@ -423,14 +393,14 @@ def remove_extension(filename): return filename def print_matrix(matrix): + """Log a VTK transformation matrix row by row.""" for i in range(4): - # Create a string for the entire row row_message = " ".join(f"{matrix.GetElement(i, j):0.4f}" for j in range(4)) - # Send the row as a single message ogo.message(row_message) def parse_scale_triplet(value, name="scale"): + """Parse one shared or three axis-specific reference-scale factors.""" if value is None: return None if isinstance(value, (int, float)): @@ -463,13 +433,16 @@ def GetOutput(self): def read(input_mask): + """Read a NIfTI mask through the shared image reader.""" return _NiftiImageHandle(ogo.readNii(input_mask)) def check_vertebra_presence(mask_reader, vertebra): + """Raise if the requested label is absent from the segmentation.""" if vertebra not in vtk_to_numpy(mask_reader.GetOutput().GetPointData().GetScalars()).ravel(): raise ValueError(f'Mask does not contain label {vertebra} for "{vertebra}" vertebra.') def threshold(mask_output, label): + """Return a binary VTK filter for one segmentation label.""" threshold = vtk.vtkImageThreshold() threshold.SetInputData(mask_output) threshold.ThresholdBetween(float(label), float(label)) @@ -482,8 +455,7 @@ def threshold(mask_output, label): return threshold def label_mask(mask, label_value): - # Use vtkImageMathematics to change 1 values to a specific label value - + """Assign a label value to a binary mask.""" caster = vtk.vtkImageCast() caster.SetInputData(mask) caster.SetOutputScalarTypeToUnsignedChar() @@ -498,7 +470,7 @@ def label_mask(mask, label_value): return math def combine_mask(mask1, mask2): - # Combine two masks using vtkImageLogic with a logical OR operation + """Return the logical union of two binary masks.""" logic = vtk.vtkImageLogic() logic.SetInput1Data(mask1) logic.SetInput2Data(mask2) @@ -507,7 +479,7 @@ def combine_mask(mask1, mask2): return logic def add_masks(mask1, mask2): - # Combine two masks by adding their values + """Add two label images and return the resulting VTK image.""" math = vtk.vtkImageMathematics() math.SetInput1Data(mask1) math.SetInput2Data(mask2) @@ -529,6 +501,7 @@ def add_masks(mask1, mask2): return threshold def crop_to_bounding_box(image_data, bb=None): + """Crop to a supplied or nonzero voxel extent; return image and extent.""" if bb is None: extents = image_data.GetExtent() @@ -710,6 +683,7 @@ def crop_and_transform( extra_label_outputs=None, ): + """Crop density and labels to one shared extent, with an optional mm margin.""" _, bb = crop_to_bounding_box(fullvertebra.GetOutput()) if float(margin_mm) > 0.0: bb = _expand_bounding_box_by_margin(bb, fullvertebra.GetOutput(), margin_mm) @@ -727,6 +701,7 @@ def crop_and_transform( return tuple(out) def perform_marching_cubes(body_output): + """Extract the binary body surface for registration.""" mcubes = vtk.vtkImageMarchingCubes() mcubes.SetInputConnection(body_output.GetOutputPort()) mcubes.SetValue(1, 1.0) @@ -792,6 +767,7 @@ def get_icp_with_scaling( iterations=DEFAULT_SPINE_REGISTRATION_ITERATIONS, ): + """Scale the reference, then estimate rigid ICP without scaling the input bone.""" sample_surface_points = surface_points_from_vtk_mask( body.GetOutput(), max_points=landmarks, @@ -876,6 +852,7 @@ def get_icp_with_scaling( def get_icp(body, reference_path): + """Return the legacy VTK reference-to-body ICP transform.""" mcubes = perform_marching_cubes(body) mcubes_output = mcubes.GetOutput() reference_bone = ogo.readPolyData(reference_path) @@ -898,6 +875,7 @@ def get_icp(body, reference_path): return icp def transform_resample(image, matrix, iso_resolution, interpolation='cubic'): + """Reslice an image using the shared physical-space transform helper.""" output = ogo.transformResample(image, matrix, iso_resolution, interpolation=interpolation) if output.GetNumberOfPoints() == 0: ogo.message("Reslice output contains no points. Check the input and transformation.") @@ -1043,6 +1021,7 @@ def generate_cortical_mask(image_vtk, mask_vtk, threshold=0.2, min_th=1, max_th= def convert_image_to_material(image, mask, n_bins=128, cort_mask=None): + """Bin calibrated spine density into material IDs; no ash conversion is applied.""" change = ogo.prepareFiniteElementImage(image) mask_change = ogo.prepareFiniteElementImage(mask) cort_mask_change = ogo.prepareFiniteElementImage(cort_mask) if cort_mask is not None else None @@ -1068,11 +1047,13 @@ def resolve_func(func_or_name, module): ## Functions to check the results def check_image_values(image): + """Log the distinct scalar values in a VTK image.""" array = vtk.util.numpy_support.vtk_to_numpy(image.GetPointData().GetScalars()) unique_values = np.unique(array) ogo.message("Unique values in the image:", unique_values) def calculate_features(image, label): + """Measure a label's physical volume, centroid, axes and bounding box.""" label_map = sitk.BinaryThreshold(image, lowerThreshold=label, upperThreshold=label, insideValue=1, outsideValue=0) stats = sitk.LabelShapeStatisticsImageFilter() stats.Execute(label_map) @@ -1083,6 +1064,7 @@ def calculate_features(image, label): return {'volume': volume, 'centroid': centroid, 'principal_axes': principal_axes, 'extent': extent} def check_values(features, checks): + """Evaluate trusted QC expressions against measured label features.""" results = {} pass_all = True for check, condition in checks.items(): @@ -1094,6 +1076,7 @@ def check_values(features, checks): return results def parse_filename(filepath): + """Extract legacy underscore-delimited metadata from a filename.""" base_name = os.path.splitext(os.path.basename(filepath))[0] parts = base_name.split('_') return { @@ -1105,7 +1088,7 @@ def parse_filename(filepath): } def visualize_slice(image, filepath): - # Get the dimensions of the vtkImageData object + """Save a middle x-slice PNG for optional legacy debugging.""" dimensions = image.GetDimensions() # (x, y, z) mid_x = dimensions[0] // 2 # Middle slice along the X-axis @@ -1138,6 +1121,7 @@ def visualize_slice(image, filepath): ogo.message(f"Slice image saved to {filepath}") def check_image(vtkimage, output_filename=None): + """Run legacy body/process image checks and optionally save their results.""" try: # Assuming 'image' is a vtkImageData object numpy_array = vtk_to_numpy(vtkimage.GetPointData().GetScalars()) @@ -1199,6 +1183,7 @@ def check_image(vtkimage, output_filename=None): return data['pass'] def resample_to_match(target_image, source_image, interpolation='nearest'): + """Resample source spacing to target spacing using the requested interpolation.""" resampler = vtk.vtkImageResample() resampler.SetInputData(source_image) @@ -1334,6 +1319,7 @@ def clean_body_component(mask_image, body_label): def process_vertebra(input_mask, input_image, n88model_output_path, body_label, process_label, reference_path, **kwargs): + """Build one vertebra model, registered masks and generation QC sidecars.""" pmma_mat_id = kwargs.get("pmma_mat_id", 5000) pistoia_mask = kwargs.get("pistoia_mask") pistoia_mask_label = kwargs.get("pistoia_mask_label") or [] @@ -1875,6 +1861,7 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, def main(): + """Parse anatomy-builder arguments and generate the requested vertebra model.""" description = ''' This script sets up the L4 vertebral compression FE model from the density (K2HPO4) calibrated image. This script sets up the model for a L4 vertebra From ad553573d257cf42cd6ec1ecc196b60dfdbd0a78 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:56:18 -0600 Subject: [PATCH 59/67] Use upstream conda setup for CI dependencies --- .github/workflows/main.yml | 3 +-- tests/test_packaging_compatibility.py | 11 +++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 672d4eb..4ab5052 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,7 +36,6 @@ jobs: with: python-version: ${{ matrix.python-version }} miniconda-version: "latest" - mamba-version: "*" activate-environment: test channels: numerics88,simpleitk,conda-forge auto-activate-base: false @@ -48,7 +47,7 @@ jobs: - name: Create and install dependencies if: steps.conda-env-cache.outputs.cache-hit != 'true' run: | - mamba install -y -n test n88tools click flake8 matplotlib nose numpy opencv pandas pbr pytest scipy simpleitk six sphinx pydicom gdcm "setuptools<65.6" + conda install -y -n test n88tools click flake8 matplotlib nose numpy opencv pandas pbr pytest scipy simpleitk six sphinx pydicom gdcm "setuptools<65.6" - name: Install package run: | conda activate test diff --git a/tests/test_packaging_compatibility.py b/tests/test_packaging_compatibility.py index a12626c..850c655 100644 --- a/tests/test_packaging_compatibility.py +++ b/tests/test_packaging_compatibility.py @@ -44,11 +44,14 @@ def test_ci_conda_install_is_non_interactive(): ) -def test_ci_uses_mamba_for_dependency_resolution(): - """Cold CI environments should use the faster conda-compatible solver.""" +def test_ci_uses_conda_with_required_test_dependencies(): + """Keep the upstream conda setup, pytest and the setuptools compatibility pin.""" repo_root = Path(__file__).resolve().parents[1] workflow = repo_root / ".github" / "workflows" / "main.yml" text = workflow.read_text(encoding="utf-8") - assert "mamba-version:" in text - assert re.search(r"^\s*mamba install\b", text, flags=re.MULTILINE) + assert "mamba-version:" not in text + install_line = re.search(r"^\s*conda install\b.*$", text, flags=re.MULTILINE) + assert install_line is not None + assert "pytest" in install_line.group() + assert '"setuptools<65.6"' in install_line.group() From e0520a7cfcc7d1b9cdd546a442192122403213c1 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:06:01 -0600 Subject: [PATCH 60/67] Simplify FEA crop workflow and clarify generation stages --- docs/fea/implementation.md | 8 +- ogo/cli/GenerateFEM.py | 12 + ogo/fea/femur.py | 1695 +++-------------- ogo/fea/spine.py | 469 +++-- tests/cli/test_GenerateFEM.py | 32 + .../fixtures/fe_workflow_defaults_locked.json | 26 - tests/fea/test_femur.py | 605 +----- tests/fea/test_module_layout.py | 16 + tests/fea/test_sideways_fall.py | 42 +- tests/fea/test_spine.py | 41 + tests/fea/test_spinefe_benchmark.py | 9 +- tests/fea/test_workflow_locked_defaults.py | 11 - 12 files changed, 676 insertions(+), 2290 deletions(-) diff --git a/docs/fea/implementation.md b/docs/fea/implementation.md index 18583a4..06dadf8 100644 --- a/docs/fea/implementation.md +++ b/docs/fea/implementation.md @@ -7,8 +7,8 @@ audits the model, runs FAIM/N88 and writes modelling metadata. | --- | --- | | CLI and site arguments | `GenerateFEM.build_parser`, `build_spine_command`, `build_femur_command` | | Reporting endpoint and criteria | `GenerateFEM.solve_report_profile`, `critical_volume_percent`, `critical_strain` | -| Spine preparation and model | `spine.clean_body_component`, `get_icp_with_scaling`, `process_vertebra` | -| Hip preparation and model | `femur.sidewaysFallFe`, `crop_vtk_images_to_greater_trochanter_length` | +| Spine preparation and model | `spine._prepare_spine_inputs`, `_register_spine_inputs`, `process_vertebra` | +| Hip preparation and model | `femur._prepare_femur_inputs`, `_register_femur_inputs`, `sidewaysFallFe` | | Hip multistart alignment | `hip_registration.estimate_femur_icp` | | Physical scan-end/shaft measurements | `shaft_geometry.capture_distal_scan_face`, `measure_available_shaft`, `verify_model_shaft` | | Shared transforms | `alignment.py`, `image_io.py` | @@ -24,12 +24,16 @@ audits the model, runs FAIM/N88 and writes modelling metadata. Anatomy defaults are constants at the top of `spine.py` and `femur.py`. Shared helpers own geometry/material logic; the CLI owns solving and reporting. Changing a reporting criterion must not silently change model construction. +Hip supports only the GT-relative flat shaft crop; the initial fixed-length crop +is used for registration only. Spine settings are explicit keyword arguments; +misspelled setting names raise an error before reading inputs. Spine cleans the body label, crops, estimates centroid-initialized ICP, resamples, builds caps/materials and applies compression. Hip captures the original distal scan face, rough-crops for registration, transforms the full scan, builds proximal supports, checks available coverage, then crops and constrains the shaft. Reference scaling changes the registration target, not native bone dimensions. +The modelling JSON records spine label smoothing and resolved contact settings. For hip, the saved-model length check is `min(z of Greater_Trochanter_PMMA_Nodes) - median(z of Distal_Femur_Nodes)`. diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index 69c3095..c20cce6 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -29,6 +29,7 @@ SPINE_ALIGNMENT_METHOD, SPINE_ICP_TARGETS, default_spine_reference_path, + generation_settings as spine_generation_settings, solve_report_profile as spine_solve_report_profile, ) from ogo.fea.femur import ( @@ -438,6 +439,13 @@ def write_modeling_metadata( return None output_path = model_path.with_name(model_path.with_suffix("").name + "_modeling.json") + generation_settings = {} + if model_type == "spine": + generation_settings = spine_generation_settings() + if output_path.exists(): + generation_settings = json.loads(output_path.read_text()).get( + "generation_settings", generation_settings + ) common = { "schema_version": 1, "model_file": str(model_path), @@ -558,6 +566,7 @@ def write_modeling_metadata( }, "image_processing": { "iso_resolution_mm": option_float(generator_argv, "--iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM), + "registration_label_smoothing": generation_settings["registration_label_smoothing"], "spatial_operations": "ICP transform and isotropic output spacing in one shared VTK reslice", "image_interpolation": "cubic", "label_interpolation": "nearest-neighbor", @@ -636,6 +645,7 @@ def write_modeling_metadata( }, }, "boundary_conditions": { + "stable_contact": generation_settings["stable_contact"], "fixture_geometry": { "superior_cap": { "label_id": option_int(generator_argv, "--top_node_set_id", 4), @@ -896,6 +906,8 @@ def write_modeling_metadata( }, }) + if generation_settings: + metadata["generation_settings"] = generation_settings output_path.write_text(json.dumps(metadata, indent=2, sort_keys=True) + "\n") print(f"Wrote {output_path}") return output_path diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index dac8ef6..e168566 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -4,6 +4,32 @@ geometry helpers live in alignment and boundary; the ogoFEA wrapper solves and reports results. See docs/fea/implementation.md for the code map.""" +import ogo.util.Helper as ogo +import os +import sys +import argparse +import json +import numpy as np +import vtk +import vtkbone + +from ogo.fea.boundary import ( + fit_vtk_images_to_physical_bounds, + generate_projected_material_disk_vtk, + projected_material_disk_required_bounds, + should_smooth_resampled_mask, + smooth_binary_mask_vtk, +) +from ogo.fea.materials import build_femur_material_table +from ogo.fea.model import ( + append_postprocessing_sets, + directional_face_node_ids_from_voxel_mask, + find_nodes_on_coordinate_plane, + interface_node_ids_from_voxel_mask, + write_model, +) +from ogo.util.echo_arguments import echo_arguments + from pathlib import Path from ogo.fea.alignment import ( @@ -46,15 +72,7 @@ SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION = (1.0, 1.0) FEMORAL_HEAD_FIXTURE_CENTER_FRACTION = (0.5, 1.1, 0.5) GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION = (0.5, -0.1, 0.5) -DISTAL_SHAFT_FIXTURE_CENTER_FRACTION = (0.5, 0.5, -0.1) -DISTAL_SHAFT_FIXTURE_SIZE_FRACTION = (1.0, 1.0) -DISTAL_SHAFT_FIXTURE_NORMAL = (0.0, -0.2588190451025207, 0.9659258262890683) POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION = 0.9 -DEFAULT_FEMUR_BBOX_RATIO = (1.0, 1.3, None) -DEFAULT_FEMUR_BBOX_CROP_FROM = (None, "max", None) -DEFAULT_FEMUR_EXPERIMENTAL_RATIO = 1.2 -DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM = 40.0 -DEFAULT_FEMUR_PROXIMAL_REFERENCE_WIDTH = "max_xy" DEFAULT_FEMUR_REFERENCE_MIN_SCALE = (0.8, 0.8, 0.75) DEFAULT_FEMUR_REFERENCE_MAX_SCALE = (1.2, 1.2, 1.3) DEFAULT_FEMUR_REGISTRATION_BACKEND = "numpy" @@ -70,10 +88,6 @@ DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM = 0.0 DEFAULT_FEMUR_PISTOIA_CRITICAL_VOLUME_PERCENT = 11.2 DEFAULT_FEMUR_PISTOIA_CRITICAL_STRAIN = 0.009 -PRE_ICP_CROP_MODES = {"bbox_ratio", "proximal_box_ratio"} -ROUGH_PRE_ICP_CROP_MODES = {"post_icp_flat_ratio", "post_icp_oblique_ratio", "greater_trochanter_length"} -FULL_SCAN_AFTER_ROUGH_CROP_MODES = {"greater_trochanter_length"} -DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM = 50.0 DEFAULT_CORTICAL_LABEL = 1 DEFAULT_TRABECULAR_LABEL = 2 @@ -444,73 +458,6 @@ def scale_reference_to_sample_principal_lengths( } -def _parse_bbox_ratio_recipe(values): - """Return bbox-ratio values in Ogo's x/y/z image order. - - Saved workflows store bbox ratios in recipe order: - ``reference, constrained, free``. For the hip sideways-fall recipe the - reference axis is y, the constrained crop axis is z, and x is left free. - Ogo image arrays are x/y/z, so the mapped order is ``free, reference, - constrained``. - """ - if len(values) != 3: - raise ValueError("bbox_ratio must contain reference/constrained/free values.") - parsed = [] - for item in values: - if item is None: - parsed.append(None) - continue - token = str(item).strip().lower() - if token in {"", "none", "null", "auto"}: - parsed.append(None) - continue - value = float(item) - if value <= 0: - raise ValueError("bbox_ratio values must be positive or null.") - parsed.append(value) - reference, constrained, free = parsed - return free, reference, constrained - - -def _crop_from_value(value): - if value is None: - return None - token = str(value).strip().lower() - if token in {"", "none", "null", "auto", "center", "centre"}: - return None - if token in {"min", "low", "lo", "start"}: - return "min" - if token in {"max", "high", "hi", "end"}: - return "max" - raise ValueError("bbox_crop_from values must be min, max, center, or null.") - - -def _opposite_crop_end(value): - if value == "min": - return "max" - if value == "max": - return "min" - return value - - -def _parse_bbox_crop_from_recipe(values): - """Return bbox crop-end values in Ogo's x/y/z image order. - - The recipe uses the same reference/constrained/free convention as the - Slicer-authored workflow. For the hip sideways-fall recipe the mapped order - is ``free, reference, constrained`` in Ogo's x/y/z image axes. The workflow - crop ends are stored in patient-space axis directions, while VTK image - storage runs opposite on the free and constrained axes used by this recipe; - those two ends are therefore flipped during the axis-order conversion. - """ - if values is None: - return None, None, None - if len(values) != 3: - raise ValueError("bbox_crop_from must contain reference/constrained/free values.") - reference, constrained, free = (_crop_from_value(item) for item in values) - return _opposite_crop_end(free), reference, _opposite_crop_end(constrained) - - def _vtk_image_from_array(array, template_vtk_image, *, origin, vtk_array_type=None): """Create a zero-based VTK image from an x/y/z NumPy array.""" import numpy as np @@ -573,112 +520,6 @@ def resample_vtk_image_like_workflow(vtk_image, target_spacing_mm, *, interpolat return out -def crop_vtk_images_to_bbox_ratio( - vtk_images, - vtk_mask, - *, - bbox_ratio=DEFAULT_FEMUR_BBOX_RATIO, - bbox_crop_from=None, - labels=None, -): - """Crop VTK images to a mask-bbox ratio and return the retained crop face. - - Ratios are interpreted in Ogo's x/y/z VTK image order. The returned - crop-face image has value 1 on the newly exposed one-sided crop surface. - For the hip recipe this is the exposed shaft face created by - ``bbox_ratio=(1, 1.3, None)`` and ``bbox_crop_from=(None, "max", None)``. - """ - import numpy as np - import vtk - - from ogo.util.vtk_image import vtk_image_to_numpy - - images = list(vtk_images) - mask_data = vtk_image_to_numpy(vtk_mask) - if labels: - active = np.isin(mask_data, sorted(int(label) for label in labels)) - else: - active = mask_data != 0 - if not np.any(active): - raise ValueError("Cannot apply bbox-ratio crop to an empty femur mask.") - active_for_crop = _largest_connected_component_mask(active) - - ratio_xyz = _parse_bbox_ratio_recipe(bbox_ratio) - crop_from_xyz = _parse_bbox_crop_from_recipe(bbox_crop_from) - numeric_axes = [axis for axis, value in enumerate(ratio_xyz) if value is not None] - if not numeric_axes: - empty_face = np.zeros(mask_data.shape, dtype=np.uint8) - return images, _vtk_image_from_array(empty_face, vtk_mask, origin=vtk_mask.GetOrigin(), vtk_array_type=vtk.VTK_UNSIGNED_CHAR), { - "enabled": False, - "ratio_recipe": tuple(bbox_ratio), - "ratio_xyz": ratio_xyz, - "crop_from_xyz": crop_from_xyz, - } - - reference_axes = [axis for axis in numeric_axes if np.isclose(float(ratio_xyz[axis]), 1.0)] - if not reference_axes: - raise ValueError("bbox_ratio must contain one preserved axis with value 1.") - - spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) - out_lo, out_hi, crop_surface, crop_meta = _stable_bbox_ratio_crop_bounds( - active=active_for_crop, - spacing=spacing, - ratio_xyz=ratio_xyz, - crop_from_xyz=crop_from_xyz, - reference_axes=reference_axes, - ) - - slices = tuple(slice(int(out_lo[axis]), int(out_hi[axis])) for axis in range(3)) - origin = tuple( - float(vtk_mask.GetOrigin()[axis]) + float(out_lo[axis]) * float(spacing[axis]) - for axis in range(3) - ) - cropped_images = [ - _vtk_image_from_array(vtk_image_to_numpy(image)[slices], image, origin=origin) - for image in images - ] - - cropped_active = active_for_crop[slices] - crop_face = np.zeros(cropped_active.shape, dtype=np.uint8) - if crop_surface is not None: - axis = int(crop_surface["axis"]) - index = int(crop_surface["local_index"]) - face_slice = [slice(None), slice(None), slice(None)] - face_slice[axis] = index - crop_face[tuple(face_slice)] = cropped_active[tuple(face_slice)].astype(np.uint8) - - crop_face_image = _vtk_image_from_array( - crop_face, - vtk_mask, - origin=origin, - vtk_array_type=vtk.VTK_UNSIGNED_CHAR, - ) - meta = { - "enabled": True, - "ratio_recipe": tuple(bbox_ratio), - "crop_from_recipe": tuple(bbox_crop_from) if bbox_crop_from is not None else None, - "ratio_xyz": ratio_xyz, - "crop_from_xyz": crop_from_xyz, - "reference_axis": crop_meta["reference_axis"], - "reference_length_mm": crop_meta["reference_length_mm"], - "input_bbox_xyz": crop_meta["input_bbox_xyz"], - "crop_slices_xyz": tuple((int(out_lo[axis]), int(out_hi[axis])) for axis in range(3)), - "output_shape_xyz": tuple(int(value) for value in cropped_active.shape), - "output_origin": origin, - "crop_iterations": crop_meta["crop_iterations"], - "final_bbox_ratio_xyz": crop_meta["final_bbox_ratio_xyz"], - "crop_surface": None - if crop_surface is None - else { - "axis": "xyz"[int(crop_surface["axis"])], - "side": crop_surface["side"], - "normal_sign": crop_surface["normal_sign"], - }, - "crop_face_voxels": int(crop_face.sum()), - } - return cropped_images, crop_face_image, meta - - def _largest_connected_component_mask(active): import numpy as np @@ -695,91 +536,6 @@ def _largest_connected_component_mask(active): return labeled == int(np.argmax(sizes)) -def crop_vtk_images_to_proximal_box_ratio( - vtk_images, - vtk_mask, - *, - ratio=DEFAULT_FEMUR_EXPERIMENTAL_RATIO, - proximal_reference_distance_mm=DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, - reference_width=DEFAULT_FEMUR_PROXIMAL_REFERENCE_WIDTH, - labels=None, -): - """Crop from the distal side using proximal transverse width as reference.""" - import numpy as np - - from ogo.util.vtk_image import vtk_image_to_numpy - - mask_data = vtk_image_to_numpy(vtk_mask) - active = _active_crop_mask(mask_data, labels) - spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) - coords = np.argwhere(active) - lo = coords.min(axis=0).astype(np.int64) - hi = (coords.max(axis=0) + 1).astype(np.int64) - size = hi - lo - proximal_voxels = min( - int(size[2]), - max(1, int(round(float(proximal_reference_distance_mm) / float(spacing[2])))), - ) - proximal_start = int(hi[2]) - proximal_voxels - proximal = active[:, :, proximal_start : int(hi[2])] - proximal_coords = np.argwhere(proximal) - if not len(proximal_coords): - proximal_coords = coords - np.asarray([0, 0, proximal_start], dtype=np.int64) - x_width_mm = _percentile_index_width_mm( - proximal_coords[:, 0] + 0, - float(spacing[0]), - ) - y_width_mm = _percentile_index_width_mm( - proximal_coords[:, 1] + 0, - float(spacing[1]), - ) - reference_width = str(reference_width).strip().lower() - if reference_width == "max_xy": - reference_axis = 0 if x_width_mm >= y_width_mm else 1 - reference_width_mm = x_width_mm if reference_axis == 0 else y_width_mm - elif reference_width == "rms_xy": - reference_axis = None - reference_width_mm = float(np.sqrt((x_width_mm**2 + y_width_mm**2) / 2.0)) - else: - raise ValueError("reference_width must be max_xy or rms_xy.") - target_length_mm = float(ratio) * float(reference_width_mm) - target_voxels = min(int(size[2]), max(1, int(round(target_length_mm / float(spacing[2]))))) - status = "short" if int(size[2]) <= target_voxels else "cropped" - keep = np.zeros(active.shape, dtype=bool) - out_lo = lo.copy() - out_hi = hi.copy() - if status == "short": - keep[tuple(slice(int(lo[axis]), int(hi[axis])) for axis in range(3))] = True - else: - out_lo[2] = int(hi[2]) - target_voxels - keep[ - int(out_lo[0]) : int(out_hi[0]), - int(out_lo[1]) : int(out_hi[1]), - int(out_lo[2]) : int(out_hi[2]), - ] = True - return _crop_vtk_images_with_keep_mask( - vtk_images, - vtk_mask, - active=active, - keep=keep, - meta={ - "enabled": True, - "method": "proximal_box_ratio", - "ratio": float(ratio), - "proximal_reference_distance_mm": float(proximal_reference_distance_mm), - "status": status, - "reference_width": reference_width, - "reference_axis": "rms_xy" if reference_axis is None else "xyz"[reference_axis], - "reference_width_mm": float(reference_width_mm), - "proximal_x_width_mm": float(x_width_mm), - "proximal_y_width_mm": float(y_width_mm), - "target_length_mm": float(target_length_mm), - "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), - "crop_slices_xyz": tuple((int(out_lo[axis]), int(out_hi[axis])) for axis in range(3)), - }, - ) - - def crop_vtk_images_to_fixed_proximal_length( vtk_images, vtk_mask, @@ -837,69 +593,6 @@ def crop_vtk_images_to_fixed_proximal_length( return cropped_images, crop_face_image, meta -def crop_vtk_images_to_flat_post_icp_ratio( - vtk_images, - vtk_mask, - *, - ratio=DEFAULT_FEMUR_EXPERIMENTAL_RATIO, - labels=None, -): - """Apply a flat aligned-frame distal crop using z length over y width.""" - import numpy as np - - from ogo.util.vtk_image import vtk_image_to_numpy - - mask_data = vtk_image_to_numpy(vtk_mask) - active = _active_crop_mask(mask_data, labels) - spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) - coords = np.argwhere(active) - lo = coords.min(axis=0).astype(np.int64) - hi = (coords.max(axis=0) + 1).astype(np.int64) - size = hi - lo - y_width_mm = float(size[1]) * float(spacing[1]) - z_length_mm = float(size[2]) * float(spacing[2]) - ratio = float(ratio) - if ratio <= 0.0: - raise ValueError("ratio must be positive.") - target_length_mm = ratio * y_width_mm - target_voxels = max(1, int(round(target_length_mm / float(spacing[2])))) - if int(size[2]) <= target_voxels: - raise ValueError( - "Post-ICP flat femur crop is too short for the requested aspect ratio: " - "input z length %.4f mm, required %.4f mm for ratio %.4f." - % (z_length_mm, target_length_mm, ratio) - ) - status = "cropped" - keep = np.zeros(active.shape, dtype=bool) - out_lo = lo.copy() - out_hi = hi.copy() - out_lo[2] = int(hi[2]) - target_voxels - keep[ - int(out_lo[0]) : int(out_hi[0]), - int(out_lo[1]) : int(out_hi[1]), - int(out_lo[2]) : int(out_hi[2]), - ] = True - cropped_images, crop_face_image, meta = _crop_vtk_images_with_keep_mask( - vtk_images, - vtk_mask, - active=active, - keep=keep, - meta={ - "enabled": True, - "method": "post_icp_flat_ratio", - "ratio": ratio, - "reference_axis": "y", - "reference_width_mm": y_width_mm, - "input_z_length_mm": z_length_mm, - "target_length_mm": float(target_voxels) * float(spacing[2]), - "status": status, - "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), - "crop_stage": "after ICP on aligned reference grid", - }, - ) - return cropped_images, crop_face_image, meta - - def crop_vtk_images_to_greater_trochanter_length( vtk_images, vtk_mask, @@ -983,164 +676,6 @@ def crop_vtk_images_to_greater_trochanter_length( return cropped_images, crop_face_image, meta -def crop_vtk_images_to_oblique_post_icp_ratio( - vtk_images, - vtk_mask, - transformed_crop_face, - *, - ratio=DEFAULT_FEMUR_EXPERIMENTAL_RATIO, - labels=None, -): - """Apply a post-ICP distal crop using the transformed rough-crop face angle.""" - import numpy as np - import vtk - - from ogo.util.vtk_image import vtk_image_to_numpy - - mask_data = vtk_image_to_numpy(vtk_mask) - active = _active_crop_mask(mask_data, labels) - spacing = np.asarray(vtk_mask.GetSpacing(), dtype=np.float64) - origin = np.asarray(vtk_mask.GetOrigin(), dtype=np.float64) - coords = np.argwhere(active) - lo = coords.min(axis=0).astype(np.int64) - hi = (coords.max(axis=0) + 1).astype(np.int64) - size = hi - lo - y_width_mm = float(size[1]) * float(spacing[1]) - z_length_mm = float(size[2]) * float(spacing[2]) - ratio = float(ratio) - if ratio <= 0.0: - raise ValueError("ratio must be positive.") - target_length_mm = min(z_length_mm, ratio * y_width_mm) - target_voxels = min( - int(size[2]), - max(1, int(round(target_length_mm / float(spacing[2])))), - ) - status = "short" if int(size[2]) <= target_voxels else "cropped" - cut_z_mm = float(origin[2]) + float(int(hi[2]) - target_voxels) * float(spacing[2]) - - transformed_face_data = vtk_image_to_numpy(transformed_crop_face) != 0 - if not np.any(transformed_face_data): - raise ValueError("Cannot apply oblique post-ICP crop from an empty transformed rough crop face.") - - if status == "short": - keep = np.zeros(active.shape, dtype=bool) - keep[ - int(lo[0]) : int(hi[0]), - int(lo[1]) : int(hi[1]), - int(lo[2]) : int(hi[2]), - ] = True - cropped_images, _unused_crop_face, meta = _crop_vtk_images_with_keep_mask( - vtk_images, - vtk_mask, - active=active, - keep=keep, - meta={ - "enabled": True, - "method": "post_icp_oblique_ratio", - "ratio": ratio, - "reference_axis": "y", - "reference_width_mm": y_width_mm, - "input_z_length_mm": z_length_mm, - "target_length_mm": float(target_voxels) * float(spacing[2]), - "target_cut_z_mm": cut_z_mm, - "status": status, - "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), - "crop_stage": "after ICP using transformed rough-crop face angle", - "source_plane_center": None, - "source_plane_normal": None, - }, - ) - slices = tuple( - slice(int(start), int(stop)) - for start, stop in meta["crop_slices_xyz"] - ) - crop_face = (transformed_face_data & active)[slices].astype(np.uint8) - crop_face_image = _vtk_image_from_array( - crop_face, - vtk_mask, - origin=meta["output_origin"], - vtk_array_type=vtk.VTK_UNSIGNED_CHAR, - ) - meta["crop_face_voxels"] = int(crop_face.sum()) - return cropped_images, crop_face_image, meta - - plane = crop_face_contact_plane(transformed_crop_face, vtk_mask) - normal = _unit_vector(plane["normal"], "post-ICP oblique crop normal") - if abs(float(normal[2])) < 1.0e-6: - raise ValueError("Transformed rough crop face is nearly vertical in model z.") - if float(normal[2]) < 0.0: - normal = -normal - center = np.asarray(plane["center"], dtype=np.float64) - shifted_center = center + normal * ((cut_z_mm - float(center[2])) / float(normal[2])) - - active_indices = np.argwhere(active) - active_points = _image_index_points(vtk_mask, active_indices) - distances = (active_points - shifted_center) @ normal - tolerance = max(float(np.min(spacing)) * 0.75, 1.0e-6) - keep_values = distances >= -tolerance - keep = np.zeros(active.shape, dtype=bool) - keep[tuple(active_indices[keep_values].T)] = True - cropped_images, crop_face_image, meta = _crop_vtk_images_with_keep_mask( - vtk_images, - vtk_mask, - active=active, - keep=keep, - meta={ - "enabled": True, - "method": "post_icp_oblique_ratio", - "ratio": ratio, - "reference_axis": "y", - "reference_width_mm": y_width_mm, - "input_z_length_mm": z_length_mm, - "target_length_mm": float(target_voxels) * float(spacing[2]), - "target_cut_z_mm": cut_z_mm, - "status": status, - "input_bbox_xyz": tuple((int(lo[axis]), int(hi[axis])) for axis in range(3)), - "crop_stage": "after ICP using transformed rough-crop face angle", - "plane_center": tuple(float(value) for value in shifted_center), - "plane_normal": tuple(float(value) for value in normal), - "plane_u_axis": tuple(float(value) for value in plane["u_axis"]), - "plane_v_axis": tuple(float(value) for value in plane["v_axis"]), - "plane_size": tuple(float(value) for value in plane["size"]), - "plane_shape": plane.get("shape", "anatomy"), - "source_plane_center": tuple(float(value) for value in plane["center"]), - "source_plane_normal": tuple(float(value) for value in plane["normal"]), - }, - ) - return cropped_images, crop_face_image, meta - - -def thicken_z_crop_face_into_mask(crop_face_vtk, mask_vtk, *, thickness_voxels=3): - """Return a thin mask-constrained z slab so a crop face survives resampling.""" - import numpy as np - import vtk - - from ogo.util.vtk_image import vtk_image_to_numpy - - face = vtk_image_to_numpy(crop_face_vtk) != 0 - active = vtk_image_to_numpy(mask_vtk) != 0 - out = np.zeros(face.shape, dtype=np.uint8) - coords = np.argwhere(face) - if coords.size == 0: - coords = np.argwhere(active) - if coords.size == 0: - return _vtk_image_from_array( - out, - crop_face_vtk, - origin=crop_face_vtk.GetOrigin(), - vtk_array_type=vtk.VTK_UNSIGNED_CHAR, - ) - z0 = int(coords[:, 2].min()) - z1 = min(active.shape[2], z0 + max(1, int(thickness_voxels))) - out[:, :, z0:z1] = active[:, :, z0:z1].astype(np.uint8) - return _vtk_image_from_array( - out, - crop_face_vtk, - origin=crop_face_vtk.GetOrigin(), - vtk_array_type=vtk.VTK_UNSIGNED_CHAR, - ) - - def _active_crop_mask(mask_data, labels): import numpy as np @@ -1153,13 +688,6 @@ def _active_crop_mask(mask_data, labels): return _largest_connected_component_mask(active) -def _percentile_index_width_mm(indices, spacing, *, low=1.0, high=99.0): - import numpy as np - - lo, hi = np.percentile(indices.astype(np.float64), [float(low), float(high)]) - return float(hi - lo + 1.0) * float(spacing) - - def _crop_vtk_images_with_keep_mask(vtk_images, vtk_mask, *, active, keep, meta): import numpy as np import vtk @@ -1218,436 +746,6 @@ def _touches_removed_or_background(kept_active, removed_active): return face -def _stable_bbox_ratio_crop_bounds( - *, - active, - spacing, - ratio_xyz, - crop_from_xyz, - reference_axes, - max_iterations=8, -): - import numpy as np - - coords = np.argwhere(active) - lo = coords.min(axis=0).astype(np.int64) - hi = (coords.max(axis=0) + 1).astype(np.int64) - input_lo = lo.copy() - input_hi = hi.copy() - out_lo = lo.copy() - out_hi = hi.copy() - crop_surface = None - input_size = input_hi - input_lo - input_physical_size = input_size.astype(np.float64) * spacing - reference_axis = min(reference_axes, key=lambda axis: float(input_physical_size[axis])) - reference_length_mm = 0.0 - - for iteration in range(1, int(max_iterations) + 1): - retained = active[tuple(slice(int(out_lo[axis]), int(out_hi[axis])) for axis in range(3))] - retained_coords = np.argwhere(retained) - if not len(retained_coords): - raise ValueError("bbox-ratio crop removed all foreground voxels.") - lo = out_lo + retained_coords.min(axis=0).astype(np.int64) - hi = out_lo + (retained_coords.max(axis=0) + 1).astype(np.int64) - size = hi - lo - physical_size = size.astype(np.float64) * spacing - reference_axis = min( - reference_axes, - key=lambda axis: (float(physical_size[axis]), axis != reference_axis), - ) - reference_length_mm = float(size[reference_axis]) * float(spacing[reference_axis]) - - next_lo = lo.copy() - next_hi = hi.copy() - crop_surface = None - for axis, axis_ratio in enumerate(ratio_xyz): - if axis_ratio is None: - continue - target_mm = reference_length_mm * float(axis_ratio) - target_voxels = max(1, int(round(target_mm / float(spacing[axis])))) - target_voxels = min(int(size[axis]), target_voxels) - mode = crop_from_xyz[axis] - if mode == "min": - start = int(hi[axis]) - target_voxels - crop_surface = {"axis": axis, "side": "min", "normal_sign": -1.0} - elif mode == "max": - start = int(lo[axis]) - crop_surface = {"axis": axis, "side": "max", "normal_sign": 1.0} - else: - center = 0.5 * (float(lo[axis]) + float(hi[axis])) - start = int(round(center - 0.5 * float(target_voxels))) - start = max(int(lo[axis]), min(start, int(hi[axis]) - target_voxels)) - next_lo[axis] = start - next_hi[axis] = start + target_voxels - - if np.array_equal(next_lo, out_lo) and np.array_equal(next_hi, out_hi): - break - out_lo, out_hi = next_lo, next_hi - - if crop_surface is not None: - axis = int(crop_surface["axis"]) - crop_surface["local_index"] = ( - 0 if crop_surface["side"] == "min" else int(out_hi[axis] - out_lo[axis] - 1) - ) - - final_size = out_hi - out_lo - final_physical = final_size.astype(np.float64) * spacing - final_ratio = tuple( - float(final_physical[axis] / reference_length_mm) if reference_length_mm > 0 else 0.0 - for axis in range(3) - ) - return out_lo, out_hi, crop_surface, { - "reference_axis": "xyz"[reference_axis], - "reference_length_mm": reference_length_mm, - "input_bbox_xyz": tuple((int(input_lo[axis]), int(input_hi[axis])) for axis in range(3)), - "crop_iterations": iteration, - "final_bbox_ratio_xyz": final_ratio, - } - - -def crop_face_support_vector(crop_face_vtk, mask_vtk): - """Estimate the outward normal of a transformed crop-face label image.""" - import numpy as np - - from ogo.util.vtk_image import vtk_image_to_numpy - - face = vtk_image_to_numpy(crop_face_vtk) != 0 - bone = vtk_image_to_numpy(mask_vtk) != 0 - if not np.any(face): - raise ValueError("Cannot estimate crop-face support vector from an empty face mask.") - if not np.any(bone): - raise ValueError("Cannot orient crop-face support vector from an empty femur mask.") - - spacing = np.asarray(crop_face_vtk.GetSpacing(), dtype=np.float64) - origin = np.asarray(crop_face_vtk.GetOrigin(), dtype=np.float64) - face_coords = np.argwhere(face).astype(np.float64) - bone_coords = np.argwhere(bone).astype(np.float64) - face_points = origin + face_coords * spacing - bone_points = origin + bone_coords * spacing - if face_points.shape[0] < 3: - raise ValueError("At least three crop-face voxels are required to estimate a surface normal.") - - centered = face_points - face_points.mean(axis=0) - _u, _s, vh = np.linalg.svd(centered, full_matrices=False) - normal = np.asarray(vh[-1], dtype=np.float64) - norm = float(np.linalg.norm(normal)) - if norm <= 0.0: - raise ValueError("Cannot estimate crop-face support vector from degenerate face points.") - normal /= norm - - # The vector should point out of the retained bone, toward the cropped-away - # side. Orient it by comparing the face centroid to the retained-bone - # centroid. - into_bone = bone_points.mean(axis=0) - face_points.mean(axis=0) - if float(np.dot(normal, into_bone)) > 0.0: - normal = -normal - return tuple(float(value) for value in normal) - - -def _image_index_points(vtk_image, indices): - """Return physical center coordinates for VTK image array indices.""" - import numpy as np - - extent = vtk_image.GetExtent() - offset = np.asarray([extent[0], extent[2], extent[4]], dtype=np.float64) - spacing = np.asarray(vtk_image.GetSpacing(), dtype=np.float64) - origin = np.asarray(vtk_image.GetOrigin(), dtype=np.float64) - return origin + (np.asarray(indices, dtype=np.float64) + offset) * spacing - - -def _unit_vector(vector, name): - import numpy as np - - values = np.asarray(vector, dtype=np.float64) - if values.shape != (3,) or not np.all(np.isfinite(values)): - raise ValueError(f"{name} must be a finite 3-vector.") - norm = float(np.linalg.norm(values)) - if norm <= 0.0: - raise ValueError(f"{name} must be non-zero.") - return values / norm - - -def bbox_relative_oriented_contact_plane( - model_bounds, - *, - center_fraction, - size_fraction, - normal, - size_bounds_axes=None, - width_axis=(-1.0, 0.0, 0.0), - shape="anatomy", -): - """Return a bbox-relative plane that keeps a measured contact normal. - - The center and footprint scale with the generated model bounding box. The - normal comes from measured geometry, such as a transformed crop face, so the - plane can follow an oblique distal shaft cut while still using a stable - recipe-sized contact patch. - """ - import numpy as np - - if len(model_bounds) != 6: - raise ValueError("model_bounds must contain x/y/z min/max values.") - if len(center_fraction) != 3: - raise ValueError("center_fraction must contain x/y/z fractions.") - if len(size_fraction) != 2: - raise ValueError("size_fraction must contain the two in-plane scale factors.") - - normal_vec = _unit_vector(normal, "contact plane normal") - width_vec = np.asarray(width_axis, dtype=np.float64) - if width_vec.shape != (3,) or not np.all(np.isfinite(width_vec)): - raise ValueError("width_axis must be a finite 3-vector.") - - width_vec = width_vec - normal_vec * float(np.dot(width_vec, normal_vec)) - if float(np.linalg.norm(width_vec)) <= 1.0e-8: - fallback_axes = np.eye(3, dtype=np.float64) - width_vec = max( - (axis - normal_vec * float(np.dot(axis, normal_vec)) for axis in fallback_axes), - key=lambda candidate: float(np.linalg.norm(candidate)), - ) - v_axis = _unit_vector(width_vec, "contact plane width axis") - u_axis = _unit_vector(np.cross(v_axis, normal_vec), "contact plane long axis") - - center = [] - for axis in range(3): - lo, hi = _axis_bounds(model_bounds, axis) - center.append(lo + float(center_fraction[axis]) * (hi - lo)) - - if size_bounds_axes is None: - size = ( - _axis_extent_from_vector(model_bounds, u_axis) * float(size_fraction[0]), - _axis_extent_from_vector(model_bounds, v_axis) * float(size_fraction[1]), - ) - else: - size = _oriented_bbox_face_size( - model_bounds, - center=center, - u_axis=u_axis, - v_axis=v_axis, - varying_axes=size_bounds_axes, - size_fraction=size_fraction, - ) - if size[0] <= 0.0 or size[1] <= 0.0: - raise ValueError("size_fraction values must produce positive plane dimensions.") - - return { - "center": tuple(float(value) for value in center), - "normal": tuple(float(value) for value in normal_vec), - "outward_normal": tuple(float(value) for value in -normal_vec), - "u_axis": tuple(float(value) for value in u_axis), - "v_axis": tuple(float(value) for value in v_axis), - "size": tuple(float(value) for value in size), - "shape": str(shape).strip().lower(), - } - - -def _oriented_bbox_face_size( - model_bounds, - *, - center, - u_axis, - v_axis, - varying_axes, - size_fraction, -): - """Project a bbox face into an oblique plane's local coordinates.""" - import itertools - import numpy as np - - axis_tokens = { - "x": 0, - "y": 1, - "z": 2, - 0: 0, - 1: 1, - 2: 2, - } - axes = [] - for axis in varying_axes: - key = str(axis).strip().lower() if isinstance(axis, str) else axis - if key not in axis_tokens: - raise ValueError("size_bounds_axes values must be x, y, z, 0, 1, or 2.") - axes.append(axis_tokens[key]) - if not axes: - raise ValueError("size_bounds_axes must contain at least one axis.") - - center_arr = np.asarray(center, dtype=np.float64) - corners = [] - for choices in itertools.product((0, 1), repeat=len(axes)): - point = center_arr.copy() - for axis, choice in zip(axes, choices): - lo, hi = _axis_bounds(model_bounds, axis) - point[axis] = hi if choice else lo - corners.append(point) - relative = np.vstack(corners) - center_arr - u_values = relative @ np.asarray(u_axis, dtype=np.float64) - v_values = relative @ np.asarray(v_axis, dtype=np.float64) - return ( - float(np.ptp(u_values)) * float(size_fraction[0]), - float(np.ptp(v_values)) * float(size_fraction[1]), - ) - - -def _inside_plane_shape(shape, u_values, v_values, half_u, half_v, *, tolerance): - import numpy as np - - token = str(shape).strip().lower() - if token == "square": - half = min(float(half_u), float(half_v)) - return (np.abs(u_values) <= half + tolerance) & (np.abs(v_values) <= half + tolerance) - if token in {"round", "circle", "circular", "oval"}: - safe_u = max(float(half_u) + tolerance, 1.0e-9) - safe_v = max(float(half_v) + tolerance, 1.0e-9) - return ((u_values / safe_u) ** 2 + (v_values / safe_v) ** 2) <= 1.0 - return (np.abs(u_values) <= float(half_u) + tolerance) & ( - np.abs(v_values) <= float(half_v) + tolerance - ) - - -def _plane_bucket_key(u_value, v_value, *, spacing): - import math - - resolution = max(min(float(value) for value in spacing), 1.0e-6) - return ( - int(math.floor(float(u_value) / resolution + 0.5)), - int(math.floor(float(v_value) / resolution + 0.5)), - ) - - -def crop_face_contact_plane(crop_face_vtk, mask_vtk): - """Fit the transformed distal crop face as a contact plane. - - The returned ``normal`` points from the crop face into the retained femur. - ``outward_normal`` points toward the cropped-away shaft. Both are derived - from the transformed crop-face label, so the shaft support angle follows the - same registration transform as the femur. - """ - import numpy as np - - from ogo.util.vtk_image import vtk_image_to_numpy - - face = vtk_image_to_numpy(crop_face_vtk) != 0 - bone = vtk_image_to_numpy(mask_vtk) != 0 - if not np.any(face): - raise ValueError("Cannot fit a contact plane from an empty crop-face mask.") - if not np.any(bone): - raise ValueError("Cannot orient a crop-face contact plane from an empty femur mask.") - - face_indices = np.argwhere(face) - if face_indices.shape[0] < 3: - raise ValueError("At least three crop-face voxels are required to fit a contact plane.") - face_points = _image_index_points(crop_face_vtk, face_indices) - bone_points = _image_index_points(mask_vtk, np.argwhere(bone)) - - center = face_points.mean(axis=0) - centered = face_points - center - _u_svd, _s_svd, vh = np.linalg.svd(centered, full_matrices=False) - u_axis = _unit_vector(vh[0], "crop-face u axis") - v_axis = _unit_vector(vh[1], "crop-face v axis") - normal = _unit_vector(vh[-1], "crop-face normal") - - into_bone = bone_points.mean(axis=0) - center - if float(np.dot(normal, into_bone)) < 0.0: - normal = -normal - if float(np.dot(np.cross(u_axis, v_axis), normal)) < 0.0: - v_axis = -v_axis - - u_values = centered @ u_axis - v_values = centered @ v_axis - min_spacing = min(float(value) for value in crop_face_vtk.GetSpacing()) - size = ( - max(float(np.ptp(u_values)) + min_spacing, min_spacing), - max(float(np.ptp(v_values)) + min_spacing, min_spacing), - ) - return { - "center": tuple(float(value) for value in center), - "normal": tuple(float(value) for value in normal), - "outward_normal": tuple(float(value) for value in -normal), - "u_axis": tuple(float(value) for value in u_axis), - "v_axis": tuple(float(value) for value in v_axis), - "size": tuple(float(value) for value in size), - "shape": "anatomy", - } - - -def projected_crop_face_surface_vtk(material_vtk, plane, *, intrusion=0.0, output_value=1): - """Project a fitted crop-face plane onto the first active femur surface.""" - import numpy as np - import vtk - - from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy - - active = vtk_image_to_numpy(material_vtk) != 0 - out = np.zeros(active.shape, dtype=np.uint8) - if not np.any(active): - return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) - - center = np.asarray(plane["center"], dtype=np.float64) - normal = _unit_vector(plane["normal"], "plane normal") - u_axis = _unit_vector(plane["u_axis"], "plane u axis") - v_axis = _unit_vector(plane["v_axis"], "plane v axis") - size = plane.get("size", (0.0, 0.0)) - half_u = max(float(size[0]) / 2.0, 0.5) - half_v = max(float(size[1]) / 2.0, 0.5) - spacing = tuple(float(value) for value in material_vtk.GetSpacing()) - tolerance = max(min(spacing) * 0.75, 1.0e-6) - - indices = np.argwhere(active) - points = _image_index_points(material_vtk, indices) - relative = points - center - distances = relative @ normal - u_values = relative @ u_axis - v_values = relative @ v_axis - inside = _inside_plane_shape( - plane.get("shape", "anatomy"), - u_values, - v_values, - half_u, - half_v, - tolerance=tolerance, - ) - candidates = inside & (distances >= -tolerance) - if not np.any(candidates): - return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) - - best = {} - candidate_indices = indices[candidates] - candidate_distances = distances[candidates] - candidate_u_values = u_values[candidates] - candidate_v_values = v_values[candidates] - candidate_lengths = { - len(candidate_indices), - len(candidate_distances), - len(candidate_u_values), - len(candidate_v_values), - } - if len(candidate_lengths) != 1: - raise ValueError("candidate indices, distances, and plane coordinates must have the same length.") - - for voxel, distance, u_value, v_value in zip( - candidate_indices, - candidate_distances, - candidate_u_values, - candidate_v_values, - ): - if float(distance) < -tolerance: - continue - key = _plane_bucket_key(u_value, v_value, spacing=spacing) - current = best.get(key) - if current is None or float(distance) < current[0]: - best[key] = (float(distance), np.asarray(voxel, dtype=np.int64)) - if not best: - return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) - - first_distance = min(value[0] for value in best.values()) - max_distance = first_distance + max(float(intrusion), 0.0) + tolerance - kept = [voxel for distance, voxel in best.values() if distance <= max_distance] - if kept: - kept_indices = np.stack(kept, axis=0) - out[tuple(kept_indices.T)] = np.uint8(output_value) - return numpy_to_vtk_image(out, material_vtk, vtk_array_type=vtk.VTK_UNSIGNED_CHAR) - - def straight_crop_face_support_surface_vtk( crop_face_vtk, material_vtk, @@ -1724,21 +822,6 @@ def mirror_polydata_x(polydata): return mirrored -def tilted_side_support_vector(angle_degrees=-20): - """Return the distal support vector used for the sideways-fall fixture.""" - import numpy as np - - theta = np.radians(angle_degrees) - rotation_x = np.array( - [ - [1, 0, 0], - [0, np.cos(theta), -np.sin(theta)], - [0, np.sin(theta), np.cos(theta)], - ] - ) - return rotation_x @ np.array([0, 0, -1]) - - def cortical_compartment_mask(compartment_vtk, *, cortical_label=1, trabecular_label=2): """Return a binary VTK mask for cortical voxels from a trab/cort label image.""" import numpy as np @@ -1767,105 +850,6 @@ def cortical_compartment_mask(compartment_vtk, *, cortical_label=1, trabecular_l ) -def distal_cut_z_from_reference(reference_bounds, retained_length_mm): - """Return a flat distal cut plane in aligned reference coordinates.""" - retained_length_mm = float(retained_length_mm) - if retained_length_mm <= 0: - raise ValueError("retained_length_mm must be positive.") - return float(reference_bounds[5]) - retained_length_mm - - -def femur_z_coverage(vtk_mask): - """Return physical z coverage for nonzero voxels in a VTK femur mask.""" - import numpy as np - from ogo.util.vtk_image import vtk_image_to_numpy - - dims = vtk_mask.GetDimensions() - extent = vtk_mask.GetExtent() - spacing = vtk_mask.GetSpacing() - origin = vtk_mask.GetOrigin() - data = vtk_image_to_numpy(vtk_mask) - occupied = np.where(np.any(data != 0, axis=(0, 1)))[0] - if occupied.size == 0: - raise ValueError("Cannot standardize femur shaft length from an empty mask.") - z = origin[2] + (occupied + extent[4]) * spacing[2] - return float(z.min()), float(z.max()) - - -def femur_z_profile(vtk_mask): - """Return per-z cross-section profile arrays for a VTK femur mask.""" - import numpy as np - from ogo.util.vtk_image import vtk_image_to_numpy - - dims = vtk_mask.GetDimensions() - extent = vtk_mask.GetExtent() - spacing = vtk_mask.GetSpacing() - origin = vtk_mask.GetOrigin() - data = vtk_image_to_numpy(vtk_mask) != 0 - z_coords = origin[2] + (np.arange(dims[2]) + extent[4]) * spacing[2] - y_coords = origin[1] + (np.arange(dims[1]) + extent[2]) * spacing[1] - - rows = [] - for z_index, z_value in enumerate(z_coords): - plane = data[:, :, z_index] - if not plane.any(): - continue - y_indices = np.where(plane)[1] - rows.append( - ( - float(z_value), - float(plane.sum()), - float(y_coords[y_indices].min()), - float(y_coords[y_indices].max()), - ) - ) - if not rows: - raise ValueError("Cannot compute femur profile from an empty mask.") - - profile = np.asarray(rows, dtype=float) - return { - "z": profile[:, 0], - "area": profile[:, 1], - "y_min": profile[:, 2], - "y_max": profile[:, 3], - "width_y": profile[:, 3] - profile[:, 2], - } - - -def femoral_head_side_z_bounds(vtk_mask, *, y_fraction=0.2): - """Estimate the z extent of the femoral-head-side y band in an aligned femur mask.""" - import numpy as np - - from ogo.util.vtk_image import vtk_image_to_numpy - - data = vtk_image_to_numpy(vtk_mask) != 0 - coords = np.array(np.where(data)) - if coords.size == 0: - raise ValueError("Cannot estimate femoral-head-side bounds from an empty mask.") - - dims = vtk_mask.GetDimensions() - extent = vtk_mask.GetExtent() - spacing = vtk_mask.GetSpacing() - origin = vtk_mask.GetOrigin() - y = origin[1] + (np.arange(dims[1]) + extent[2]) * spacing[1] - z = origin[2] + (np.arange(dims[2]) + extent[4]) * spacing[2] - - y_min = float(y[coords[1].min()]) - y_max = float(y[coords[1].max()]) - y_limit = y_min + (y_max - y_min) * float(y_fraction) - head_side = data & (y[None, :, None] >= y_min) & (y[None, :, None] <= y_limit) - head_coords = np.array(np.where(head_side)) - if head_coords.size == 0: - raise ValueError("Cannot estimate femoral-head-side bounds from an empty y band.") - - return { - "y_min": y_min, - "y_max": y_limit, - "z_min": float(z[head_coords[2].min()]), - "z_max": float(z[head_coords[2].max()]), - } - - def pad_vtk_images_to_foreground_margin( vtk_images, vtk_mask, @@ -1949,250 +933,6 @@ def pad_vtk_images_to_foreground_margin( } -def _smooth_profile(values, window=7): - """Smooth one profile with an odd moving-average window.""" - import numpy as np - - values = np.asarray(values, dtype=float) - window = max(1, int(window)) - if window % 2 == 0: - window += 1 - if values.size < window or window == 1: - return values.copy() - pad = window // 2 - padded = np.pad(values, (pad, pad), mode="edge") - return np.convolve(padded, np.ones(window, dtype=float) / float(window), mode="valid") - - -def _peak_center_z(z, values, indices, *, relative_height=0.95): - """Return the center of the high plateau around a dominant local peak.""" - import numpy as np - - peak_index = int(indices[np.argmax(values[indices])]) - threshold = float(values[peak_index]) * float(relative_height) - left = peak_index - right = peak_index - valid = set(int(i) for i in indices) - while left - 1 in valid and values[left - 1] >= threshold: - left -= 1 - while right + 1 in valid and values[right + 1] >= threshold: - right += 1 - plateau = np.arange(left, right + 1, dtype=int) - weights = np.maximum(values[plateau], 0.0) - if float(weights.sum()) <= 0.0: - return peak_index, float(z[peak_index]) - return peak_index, float(np.average(z[plateau], weights=weights)) - - -def _peak_disk_bounds_z(z, raw_values, peak_index, indices, *, relative_height=0.5): - """Return the distal and proximal z bounds of the dominant peak region.""" - import numpy as np - - values = np.asarray(raw_values, dtype=float) - valid = set(int(i) for i in indices) - peak_value = float(values[int(peak_index)]) - baseline = float(np.percentile(values[list(valid)], 25.0)) - threshold = baseline + max(0.0, peak_value - baseline) * float(relative_height) - left = int(peak_index) - right = int(peak_index) - while left - 1 in valid and float(values[left - 1]) >= threshold: - left -= 1 - while right + 1 in valid and float(values[right + 1]) >= threshold: - right += 1 - return float(z[left]), float(z[right]) - - -def detect_lesser_trochanter_cut_z( - vtk_mask, - *, - distal_offset_mm=0.0, - distal_offset_percent=None, - min_distal_to_greater_mm=8.0, - max_distal_to_greater_mm=45.0, - min_distal_coverage_mm=0.0, - distal_fov_tolerance_mm=2.0, -): - """Detect a lesser-trochanter-based flat distal cut plane. - - The aligned sideways-fall reference frame has femur length along z and the - lateral greater-trochanter side at high y. The greater trochanter is found - from the smoothed high-y profile. The lesser-trochanter/transition level is - then the dominant smoothed cross-sectional area peak distal to that point. - """ - import numpy as np - - profile = femur_z_profile(vtk_mask) - z = profile["z"] - area = _smooth_profile(profile["area"], window=7) - y_max = _smooth_profile(profile["y_max"], window=7) - - z_min = float(z.min()) - z_max = float(z.max()) - if z_max - z_min < max_distal_to_greater_mm: - raise ValueError( - "Femur scan does not include enough proximal-distal coverage to identify " - "the lesser trochanter." - ) - - proximal_mask = z >= (z_min + 0.55 * (z_max - z_min)) - if not np.any(proximal_mask): - raise ValueError("Cannot identify greater trochanter from femur profile.") - proximal_indices = np.where(proximal_mask)[0] - greater_index, greater_z = _peak_center_z(z, y_max, proximal_indices) - gt_disk_distal_z, gt_disk_proximal_z = _peak_disk_bounds_z( - z, - profile["y_max"], - greater_index, - proximal_indices, - ) - - distal_mask = ( - (z <= greater_z - float(min_distal_to_greater_mm)) - & (z >= greater_z - float(max_distal_to_greater_mm)) - ) - distal_indices = np.where(distal_mask)[0] - if distal_indices.size < 5: - raise ValueError( - "Femur scan does not include the distal profile needed to identify " - "the lesser trochanter." - ) - - lesser_index, lesser_z = _peak_center_z(z, area, distal_indices) - if distal_offset_percent is not None: - distal_offset_mm = abs(greater_z - lesser_z) * float(distal_offset_percent) / 100.0 - cut_z = lesser_z - float(distal_offset_mm) - - required_z_min = cut_z - float(min_distal_coverage_mm) - if z_min > required_z_min: - missing = z_min - required_z_min - if missing <= float(distal_fov_tolerance_mm): - cut_z = z_min + float(min_distal_coverage_mm) - else: - available = max(0.0, cut_z - z_min) - raise ValueError( - "Femur scan does not extend distal to the lesser-trochanter cut plane " - f"(required {float(min_distal_coverage_mm):.1f} mm, available {available:.1f} mm)." - ) - - shaft_area = float(np.median(area[z < lesser_z - 10.0])) if np.any(z < lesser_z - 10.0) else 0.0 - if shaft_area > 0 and float(area[lesser_index]) < 1.08 * shaft_area: - raise ValueError( - "Could not identify a clear lesser-trochanter cross-section peak; " - "the femur field of view is likely incomplete or the alignment failed." - ) - - return { - "cut_z": float(cut_z), - "lesser_trochanter_z": lesser_z, - "greater_trochanter_z": greater_z, - "greater_trochanter_disk_distal_z": gt_disk_distal_z, - "greater_trochanter_disk_proximal_z": gt_disk_proximal_z, - "mask_z_min": z_min, - "mask_z_max": z_max, - "retained_length_mm": float(z_max - cut_z), - "distal_offset_mm": float(distal_offset_mm), - "distal_offset_percent": None if distal_offset_percent is None else float(distal_offset_percent), - "lesser_area": float(area[lesser_index]), - "shaft_area_median": shaft_area, - } - - -def flat_crop_vtk_image_below_z(vtk_image, cut_z): - """Zero voxels below a physical z cut plane while preserving image metadata.""" - import numpy as np - from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy - - dims = vtk_image.GetDimensions() - extent = vtk_image.GetExtent() - spacing = vtk_image.GetSpacing() - origin = vtk_image.GetOrigin() - data = vtk_image_to_numpy(vtk_image, copy=True) - z = origin[2] + (np.arange(dims[2]) + extent[4]) * spacing[2] - data[:, :, z < float(cut_z)] = 0 - - return numpy_to_vtk_image(data, vtk_image) - - -def standardize_femur_shaft_length( - image_vtk, - mask_vtk, - *, - reference_bounds, - retained_length_mm=None, - cut_mode="lesser_trochanter", - lesser_trochanter_distal_offset_mm=50.0, - lesser_trochanter_distal_offset_percent=None, -): - """Apply a flat distal crop to aligned femur image and mask. - - In the default mode, the cut plane is detected from the lesser-trochanter - cross-section profile. A fixed retained length can be requested explicitly - for controlled debugging runs. - """ - cut_mode = str(cut_mode).lower() - if cut_mode == "lesser_trochanter": - meta = detect_lesser_trochanter_cut_z( - mask_vtk, - distal_offset_mm=lesser_trochanter_distal_offset_mm, - distal_offset_percent=lesser_trochanter_distal_offset_percent, - ) - cut_z = meta["cut_z"] - retained_length_mm = meta["retained_length_mm"] - elif cut_mode == "fixed_length": - if retained_length_mm is None: - raise ValueError("retained_length_mm is required for fixed_length femur cuts.") - cut_z = distal_cut_z_from_reference(reference_bounds, retained_length_mm) - meta = { - "cut_z": cut_z, - "retained_length_mm": float(retained_length_mm), - } - else: - raise ValueError("cut_mode must be 'lesser_trochanter' or 'fixed_length'.") - - z_min, z_max = femur_z_coverage(mask_vtk) - warnings = [] - try: - head_side_bounds = femoral_head_side_z_bounds(mask_vtk) - except ValueError: - head_side_bounds = None - if head_side_bounds is not None: - head_side_z_min = float(head_side_bounds["z_min"]) - if float(cut_z) > head_side_z_min + 2.0: - warnings.append( - "distal cut plane appears to enter the femoral-head-side region " - f"(cut_z={float(cut_z):.3f}, head_side_z_min={head_side_z_min:.3f})" - ) - meta["femoral_head_side_z_min"] = head_side_z_min - meta["femoral_head_side_z_max"] = float(head_side_bounds["z_max"]) - if retained_length_mm is not None and float(retained_length_mm) < 40.0: - warnings.append( - "retained proximal femur length is unusually short " - f"({float(retained_length_mm):.3f} mm)" - ) - if z_min > cut_z: - available = max(0.0, float(reference_bounds[5]) - z_min) - raise ValueError( - "Femur scan does not include enough distal shaft for the standard crop " - f"(required retained length {float(retained_length_mm):.1f} mm, " - f"available approximately {available:.1f} mm)." - ) - meta.update( - { - "cut_z": cut_z, - "mask_z_min": z_min, - "mask_z_max": z_max, - "retained_length_mm": float(retained_length_mm), - "cut_mode": cut_mode, - "warnings": warnings, - } - ) - return ( - flat_crop_vtk_image_below_z(image_vtk, cut_z), - flat_crop_vtk_image_below_z(mask_vtk, cut_z), - meta, - ) - - def swap_xz_footprint(bounds): """Swap x/z footprint dimensions of physical bounds around the same center.""" x_center = (float(bounds[0]) + float(bounds[1])) / 2.0 @@ -2265,38 +1005,10 @@ def expand_xz_footprint(bounds, *, x_extension_mm=0.0, z_extension_mm=0.0): # Modified to Py3: March 25, 2020 ##### -script_version = 1.0 - -## -# Import the required modules -import ogo.util.Helper as ogo -import os -import sys -import argparse -import time -import json -from datetime import date -from collections import OrderedDict -import numpy as np -import vtk -import vtkbone - -from ogo.fea.boundary import ( - fit_vtk_images_to_physical_bounds, - generate_projected_material_disk_vtk, - projected_material_disk_required_bounds, - should_smooth_resampled_mask, - smooth_binary_mask_vtk, -) -from ogo.fea.materials import build_femur_material_table -from ogo.fea.model import ( - append_postprocessing_sets, - directional_face_node_ids_from_voxel_mask, - find_nodes_on_coordinate_plane, - interface_node_ids_from_voxel_mask, - write_model, -) -from ogo.util.echo_arguments import echo_arguments +script_version = 1.0 + +## +# Import the required modules def remove_extension(filename): @@ -2310,104 +1022,19 @@ def remove_extension(filename): ## # Start script -def sidewaysFallFe(args): - """Build a sideways-fall model and its shaft geometry and QC sidecars.""" - ogo.message("Start of Script...") - - ## - # Collect the input arguments +def _prepare_femur_inputs( + args, +): + """Resample full inputs and prepare a registration-only proximal crop.""" image = args.calibrated_image mask = args.bone_mask + mask_threshold = args.mask_threshold + iso_resolution = args.iso_resolution compartment_mask = args.compartment_mask pistoia_mask = args.pistoia_mask pistoia_mask_label = args.pistoia_mask_label or [] - - mask_threshold = args.mask_threshold - iso_resolution = args.iso_resolution - femur_side = args.femur_side - poissons_ratio = args.poissons_ratio - pmma_E = args.pmma_E - pmma_v = args.pmma_v - pmma_thick = args.pmma_thick - pmma_intrusion = args.pmma_intrusion - pmma_mat_id = args.pmma_mat_id - fe_displacement = args.fe_displacement - left_femur_reference = args.left_femur_reference - right_femur_reference = args.right_femur_reference - output_file = args.output_file - pmma_yield_compression = args.pmma_yield_compression - pmma_yield_tension = args.pmma_yield_tension femur_shaft_length = args.femur_shaft_length - femur_greater_trochanter_distal_length = args.femur_greater_trochanter_distal_length - femur_greater_trochanter_inclusion_length = args.femur_greater_trochanter_inclusion_length femur_input_margin = args.femur_input_margin - femur_icp_transform_in = args.femur_icp_transform_in - femur_icp_transform_out = args.femur_icp_transform_out - cortical_label = args.cortical_label - trabecular_label = args.trabecular_label - mask_smoothing_spacing_threshold = args.mask_smoothing_spacing_threshold - - - - ## - # Determine image locations and names of files - image_pathname = os.path.dirname(image) - image_basename = os.path.basename(image) - mask_pathname = os.path.dirname(mask) - mask_basename = os.path.basename(mask) - script_name = sys.argv[0] - - - try: - N88_fileName = sideways_fall_output_name(output_file, femur_side) - except ValueError: - ogo.message("Femur side not recognized. Terminating...") - sys.exit() - - - ## - # Message the input parameters to the terminal - ogo.message("Image Path: %s" % image_pathname) - ogo.message("Image File: %s" % image_basename) - ogo.message("Mask Path: %s" % mask_pathname) - ogo.message("Mask File: %s" % mask_basename) - ogo.message("Isotropic Voxel Size: %8.4f" % iso_resolution) - if femur_side == 1: - ogo.message("Femur Side for Model: Left") - elif femur_side == 2: - ogo.message("Femur Side for Model: Right") - else: - ogo.message("Femur side not recognized. Terminating...") - sys.exit() - ogo.message("Trabecular Elastic Function: %s" % (args.elastic_E_func or "default_E")) - ogo.message("Cortical Elastic Function: %s" % (args.cort_elastic_E_func or args.elastic_E_func or "default_E")) - ogo.message("Bone Poissons Ratio: %1.1f" % poissons_ratio) - ogo.message("PMMA Elastic Modulus [MPa]: %8.4f" % pmma_E) - ogo.message("PMMA Poissons Ration: %1.1f" % pmma_v) - ogo.message("PMMA Thickness [mm]: %8.4f" % pmma_thick) - ogo.message("PMMA Intrusion [mm]: %8.4f" % pmma_intrusion) - ogo.message("PMMA Material ID: %d" % pmma_mat_id) - ogo.message("Applied Displacement [mm]: %8.4f" % fe_displacement) - ogo.message("Femur Crop Recipe: registration-only rough crop, then a complete flat shaft distal to the GT disk edge") - if femur_icp_transform_in: - ogo.message("Femur ICP Transform In: %s" % femur_icp_transform_in) - if femur_icp_transform_out: - ogo.message("Femur ICP Transform Out: %s" % femur_icp_transform_out) - if args.femur_cut_mode != DEFAULT_FEMUR_CUT_MODE: - raise ValueError("Only the GT-disk-relative complete flat shaft recipe is supported.") - if femur_greater_trochanter_inclusion_length != 0: - raise ValueError("The shaft length origin must be the GT disk distal edge, without an extra offset.") - ogo.message("Femur Rough Pre-ICP Retained Length [mm]: %8.4f" % femur_shaft_length) - ogo.message("Femur Shaft Length Distal To GT Disk [mm]: %8.4f" % femur_greater_trochanter_distal_length) - if compartment_mask is not None: - ogo.message("Compartment Mask: %s" % compartment_mask) - ogo.message("Cortical Label: %d" % cortical_label) - ogo.message("Trabecular Label: %d" % trabecular_label) - if pistoia_mask is not None: - ogo.message("Pistoia ROI Mask: %s" % pistoia_mask) - if pistoia_mask_label: - ogo.message("Pistoia ROI Label(s): %s" % pistoia_mask_label) - ogo.message("Input Foreground Safety Margin [mm]: %8.4f" % femur_input_margin) ## # Read input image @@ -2432,7 +1059,6 @@ def sidewaysFallFe(args): if pistoia_mask_label: pistoiaMaskData = ogo.maskThreshold(pistoiaMaskData, pistoia_mask_label) - distal_crop_face = None bbox_crop_meta = None ogo.message("Resampling femur inputs to isotropic spacing before custom crop and ICP...") @@ -2446,12 +1072,6 @@ def sidewaysFallFe(args): iso_resolution, interpolation="nearest", ) - if distal_crop_face is not None: - distal_crop_face = resample_vtk_image_like_workflow( - distal_crop_face, - iso_resolution, - interpolation="nearest", - ) if compartmentData is not None: compartmentData = resample_vtk_image_like_workflow( compartmentData, @@ -2484,8 +1104,6 @@ def sidewaysFallFe(args): ) images_to_pad = [imageData, maskThres] - if distal_crop_face is not None: - images_to_pad.append(distal_crop_face) if compartmentData is not None: images_to_pad.append(compartmentData) if pistoiaMaskData is not None: @@ -2500,9 +1118,6 @@ def sidewaysFallFe(args): imageData = padded_images[0] maskThres = padded_images[1] next_padded_index = 2 - if distal_crop_face is not None: - distal_crop_face = padded_images[next_padded_index] - next_padded_index += 1 if compartmentData is not None: compartmentData = padded_images[next_padded_index] next_padded_index += 1 @@ -2516,11 +1131,44 @@ def sidewaysFallFe(args): else: ogo.message("Isotropic input image already has sufficient foreground safety margin.") + return ( + imageData, + maskThres, + compartmentData, + pistoiaMaskData, + registration_mask, + native_distal_face, + input_spacing, + bbox_crop_meta, + ) + + +def _register_femur_inputs( + args, + N88_fileName, + imageData, + maskThres, + compartmentData, + pistoiaMaskData, + registration_mask, + native_distal_face, + input_spacing, + bbox_crop_meta, +): + """Estimate or reuse ICP, retaining full anatomy for the GT-relative final crop.""" + iso_resolution = args.iso_resolution + femur_side = args.femur_side + left_femur_reference = args.left_femur_reference + right_femur_reference = args.right_femur_reference + femur_icp_transform_in = args.femur_icp_transform_in + femur_icp_transform_out = args.femur_icp_transform_out + femur_input_margin = args.femur_input_margin + mask_smoothing_spacing_threshold = args.mask_smoothing_spacing_threshold + ## # Use the cropped, isotropic input geometry directly for ICP. image_rot = imageData mask_rot = maskThres - distal_crop_face_rot = distal_crop_face compartment_rot = compartmentData pistoia_mask_rot = pistoiaMaskData @@ -2550,7 +1198,6 @@ def sidewaysFallFe(args): sample_mode="linspace", sample_offset=0, ) - mask_surface = polydata_from_points(sample_surface_points) if femur_side == 1: ref_poly = ogo.readPolyData(left_femur_reference) elif femur_side == 2: @@ -2639,18 +1286,6 @@ def sidewaysFallFe(args): output_spacing=output_spacing, interpolation="nearest", ) - distal_crop_face_trans = ( - transform_resample_vtk_image_to_reference_grid( - distal_crop_face_rot, - icp, - output_origin=output_origin, - output_size=output_size, - output_spacing=output_spacing, - interpolation="nearest", - ) - if distal_crop_face_rot is not None - else None - ) compartment_trans = ( transform_resample_vtk_image_to_reference_grid( compartment_rot, @@ -2691,24 +1326,149 @@ def sidewaysFallFe(args): f"{input_spacing} is <= {mask_smoothing_spacing_threshold} mm in all dimensions..." ) + return ( + image_trans, + mask_trans, + compartment_trans, + pistoia_mask_trans, + smooth_resampled_masks, + distal_scan_boundary, + icp_transform, + reference_scale, + ) + + +def sidewaysFallFe(args): + """Build a sideways-fall model and its shaft geometry and QC sidecars.""" + ogo.message("Start of Script...") + + ## + # Collect the input arguments + image = args.calibrated_image + mask = args.bone_mask + compartment_mask = args.compartment_mask + pistoia_mask = args.pistoia_mask + pistoia_mask_label = args.pistoia_mask_label or [] + + iso_resolution = args.iso_resolution + femur_side = args.femur_side + poissons_ratio = args.poissons_ratio + pmma_E = args.pmma_E + pmma_v = args.pmma_v + pmma_thick = args.pmma_thick + pmma_intrusion = args.pmma_intrusion + pmma_mat_id = args.pmma_mat_id + fe_displacement = args.fe_displacement + output_file = args.output_file + pmma_yield_compression = args.pmma_yield_compression + pmma_yield_tension = args.pmma_yield_tension + femur_shaft_length = args.femur_shaft_length + femur_greater_trochanter_distal_length = args.femur_greater_trochanter_distal_length + femur_greater_trochanter_inclusion_length = args.femur_greater_trochanter_inclusion_length + femur_input_margin = args.femur_input_margin + femur_icp_transform_in = args.femur_icp_transform_in + femur_icp_transform_out = args.femur_icp_transform_out + cortical_label = args.cortical_label + trabecular_label = args.trabecular_label + + + ## + # Determine image locations and names of files + image_pathname = os.path.dirname(image) + image_basename = os.path.basename(image) + mask_pathname = os.path.dirname(mask) + mask_basename = os.path.basename(mask) + script_name = sys.argv[0] + + + try: + N88_fileName = sideways_fall_output_name(output_file, femur_side) + except ValueError: + ogo.message("Femur side not recognized. Terminating...") + sys.exit() + + + ## + # Message the input parameters to the terminal + ogo.message("Image Path: %s" % image_pathname) + ogo.message("Image File: %s" % image_basename) + ogo.message("Mask Path: %s" % mask_pathname) + ogo.message("Mask File: %s" % mask_basename) + ogo.message("Isotropic Voxel Size: %8.4f" % iso_resolution) + if femur_side == 1: + ogo.message("Femur Side for Model: Left") + elif femur_side == 2: + ogo.message("Femur Side for Model: Right") + else: + ogo.message("Femur side not recognized. Terminating...") + sys.exit() + ogo.message("Trabecular Elastic Function: %s" % (args.elastic_E_func or "default_E")) + ogo.message("Cortical Elastic Function: %s" % (args.cort_elastic_E_func or args.elastic_E_func or "default_E")) + ogo.message("Bone Poissons Ratio: %1.1f" % poissons_ratio) + ogo.message("PMMA Elastic Modulus [MPa]: %8.4f" % pmma_E) + ogo.message("PMMA Poissons Ration: %1.1f" % pmma_v) + ogo.message("PMMA Thickness [mm]: %8.4f" % pmma_thick) + ogo.message("PMMA Intrusion [mm]: %8.4f" % pmma_intrusion) + ogo.message("PMMA Material ID: %d" % pmma_mat_id) + ogo.message("Applied Displacement [mm]: %8.4f" % fe_displacement) + ogo.message("Femur Crop Recipe: registration-only rough crop, then a complete flat shaft distal to the GT disk edge") + if femur_icp_transform_in: + ogo.message("Femur ICP Transform In: %s" % femur_icp_transform_in) + if femur_icp_transform_out: + ogo.message("Femur ICP Transform Out: %s" % femur_icp_transform_out) + if args.femur_cut_mode != DEFAULT_FEMUR_CUT_MODE: + raise ValueError("Only the GT-disk-relative complete flat shaft recipe is supported.") + if femur_greater_trochanter_inclusion_length != 0: + raise ValueError("The shaft length origin must be the GT disk distal edge, without an extra offset.") + ogo.message("Femur Rough Pre-ICP Retained Length [mm]: %8.4f" % femur_shaft_length) + ogo.message("Femur Shaft Length Distal To GT Disk [mm]: %8.4f" % femur_greater_trochanter_distal_length) + if compartment_mask is not None: + ogo.message("Compartment Mask: %s" % compartment_mask) + ogo.message("Cortical Label: %d" % cortical_label) + ogo.message("Trabecular Label: %d" % trabecular_label) + if pistoia_mask is not None: + ogo.message("Pistoia ROI Mask: %s" % pistoia_mask) + if pistoia_mask_label: + ogo.message("Pistoia ROI Label(s): %s" % pistoia_mask_label) + ogo.message("Input Foreground Safety Margin [mm]: %8.4f" % femur_input_margin) + + ( + imageData, + maskThres, + compartmentData, + pistoiaMaskData, + registration_mask, + native_distal_face, + input_spacing, + bbox_crop_meta, + ) = _prepare_femur_inputs( + args, + ) + + ( + image_trans, + mask_trans, + compartment_trans, + pistoia_mask_trans, + smooth_resampled_masks, + distal_scan_boundary, + icp_transform, + reference_scale, + ) = _register_femur_inputs( + args, + N88_fileName, + imageData, + maskThres, + compartmentData, + pistoiaMaskData, + registration_mask, + native_distal_face, + input_spacing, + bbox_crop_meta, + ) + # Keep the aligned anatomy intact until the actual support disks exist. - # ogo.message("Writing out temp images...") - # ogo.writeNii(image_trans, "temp_image.nii", image_pathname) - # ogo.writeNii(mask_trans, "temp_mask.nii", image_pathname) - # - # left_femur_reference_image = "/Users/andrew.michalski/Development/Ogo/LT_FEMUR_SIDEWAYS_FALL_REF.nii" - # right_femur_reference_image = "/Users/andrew.michalski/Development/Ogo/RT_FEMUR_SIDEWAYS_FALL_REF.nii" - # ogo.message("Applying SimpleITK registration of the images...") - # if femur_side == 1: - # ogo.finalRegistration(left_femur_reference_image) - # elif femur_side == 2: - # ogo.finalRegistration(right_femur_reference_image) - # ogo.message("Reading temp images...") - # image_trans = ogo.readNii("temp_image2.nii") - # mask_trans = ogo.readNii("temp_mask2.nii") - # os.remove("temp_image2.nii") - # os.remove("temp_mask2.nii") # replace any negative values less than -31 to be equivalent to -31. # -31 is used as that converts to a minumum elastic modulus value of 0.1 MPa @@ -2756,12 +1516,7 @@ def sidewaysFallFe(args): change = ogo.prepareFiniteElementImage(conn) femur_mask_on_model_grid = ogo.prepareFiniteElementImage(cast_mask) - distal_crop_face_change = ( - ogo.prepareFiniteElementImage(distal_crop_face_trans) if distal_crop_face_trans is not None else None - ) - if distal_crop_face_change is not None: - distal_crop_face_change = ogo.applyMask(distal_crop_face_change, ogo.cast2unsignchar(change)) - ogo.message("Skipping model-grid distal shaft cut; final post-ICP crop face is already on the model grid.") + distal_crop_face_change = None # Convert image data to hexahedral elements ogo.message("Meshing image data to elements...") @@ -2874,8 +1629,6 @@ def sidewaysFallFe(args): float(bounds_array[:, 5].max()), ) images_to_fit = [change, femur_mask_on_model_grid] - if distal_crop_face_change is not None: - images_to_fit.append(distal_crop_face_change) if pistoia_mask_on_model_grid is not None: images_to_fit.append(pistoia_mask_on_model_grid) fitted_images, contact_fit = fit_vtk_images_to_physical_bounds( @@ -2885,11 +1638,7 @@ def sidewaysFallFe(args): ) change = fitted_images[0] femur_mask_on_model_grid = fitted_images[1] - if distal_crop_face_change is not None: - distal_crop_face_change = fitted_images[2] - next_fit_index = 3 - else: - next_fit_index = 2 + next_fit_index = 2 if pistoia_mask_on_model_grid is not None: pistoia_mask_on_model_grid = fitted_images[next_fit_index] model_bounds = foreground_voxel_center_bounds(femur_mask_on_model_grid) @@ -3199,23 +1948,7 @@ def sidewaysFallFe(args): def main(): """Parse anatomy-builder arguments and generate a sideways-fall femur model.""" - description=''' -This script sets up the sideways fall FE model on the hip from the - density (K2HPO4) calibrated image. This script sets up the model for either a left or - right femur, as specified by the user. The analysis resamples the image to isotropic - voxels, transforms the image, applies the bone mask and bins the data. It then - creates the FE model for solving using FAIM (v8.1, Numerics Solutions Ltd, Calgary, - Canada - Steven Boyd). - - Input: Calibrated K2HPO4 Image (*.nii), Bone Mask (*_MASK.nii) - - Optional Parameters: - 1) Isotropic resample voxel size - 2) Left or Right Femur Bone Mask - - Output: N88 Model (*.n88model) - -''' + description = """Build a sideways-fall femur model from calibrated K2HPO4 density\n and a femur label mask. Estimate ICP from a proximal crop, then retain a\n complete flat shaft measured from the distal GT support edge. Use ogoFEA\n hip to also solve and report results.""" # Setup argument parsing diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index ba9b721..4e2da02 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -3,7 +3,58 @@ Shared geometry and mesh helpers live in alignment, boundary and model. The public ogoFEA wrapper handles solving and reporting; see docs/fea/implementation.md.""" +import json import os +import argparse +import ogo.fea.material_laws + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import SimpleITK as sitk +from scipy.ndimage import ( + binary_dilation, + binary_erosion, + find_objects, + label, +) +import vtk +import vtkbone +from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + +from ogo.fea.alignment import ( + estimate_rigid_icp, + estimate_rigid_icp_vtk, + invert_point_transform, + output_grid_for_point_transform, + point_cloud_axis_lengths, + polydata_points, + resample_vtk_image_with_point_transform, + sample_points, + surface_points_from_vtk_mask, +) +from ogo.fea.boundary import ( + bbox_relative_contact_plane, + foreground_voxel_center_bounds, + generate_projected_material_disk_vtk, + pad_vtk_images_to_physical_bounds, + projected_material_disk_required_bounds, + resample_vtk_image_to_spacing, + should_smooth_resampled_mask, + smooth_binary_mask_vtk, + smooth_label_mask_vtk, +) +from ogo.fea.image_io import write_vtk_image_with_sitk_geometry +from ogo.fea.model import ( + apply_spine_boundary_conditions as apply_boundary_conditions, + create_microfe_model, + find_and_add_visible_nodes, + log_fe_arguments, +) +import ogo.util.Helper as ogo +from ogo.util.echo_arguments import echo_arguments + + from pathlib import Path @@ -95,6 +146,40 @@ def pistoia_mask_output_path(output_file): return output_path.with_name(f"{output_path.stem}_pistoia_mask.nii.gz") +def generation_settings( + *, + label_smoothing_sigma_mm=DEFAULT_SPINE_LABEL_SMOOTHING_SIGMA_MM, + label_smoothing_threshold=DEFAULT_SPINE_LABEL_SMOOTHING_THRESHOLD, + stable_surface_fraction=DEFAULT_SPINE_STABLE_CONTACT_FRACTION, + stable_surface_trim_fraction=DEFAULT_SPINE_STABLE_CONTACT_TRIM_FRACTION, + stable_surface_min_area_fraction=DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION, + stable_surface_max_depth=DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM, + stable_surface_min_shift=DEFAULT_SPINE_STABLE_CONTACT_MIN_SHIFT_MM, + stable_surface_close_gaps_3d=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D, + stable_surface_close_gaps_max_gap=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_MAX_GAP, + stable_surface_keep_largest_component=DEFAULT_SPINE_STABLE_CONTACT_KEEP_LARGEST_COMPONENT, +): + """Describe resolved label preparation and support construction settings.""" + return { + "registration_label_smoothing": { + "sigma_mm": label_smoothing_sigma_mm, + "threshold": label_smoothing_threshold, + "stage": "before ICP", + }, + "stable_contact": { + "enabled": stable_surface_fraction > 0 or stable_surface_trim_fraction > 0, + "surface_fraction": stable_surface_fraction, + "trim_fraction": stable_surface_trim_fraction, + "minimum_area_fraction": stable_surface_min_area_fraction, + "maximum_depth_mm": stable_surface_max_depth, + "minimum_shift_mm": stable_surface_min_shift, + "close_gaps_3d": stable_surface_close_gaps_3d, + "maximum_gap_voxels": stable_surface_close_gaps_max_gap, + "keep_largest_component": stable_surface_keep_largest_component, + }, + } + + def _mask_centroid_physical(vtk_image): """Return the physical centroid of nonzero voxels in a VTK image.""" mask = vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape( @@ -310,61 +395,6 @@ def find_spinefe_benchmark_dir(start_dir=None, env_var="SPINEFE_BENCHMARK_DIR"): # Spine compression workflow builder # ----------------------------------------------------------------------------- -import os -import argparse -import os -from glob import glob -import ogo.fea.material_laws - -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -import SimpleITK as sitk -from matplotlib import pyplot as plt -from scipy.ndimage import ( - binary_dilation, - binary_erosion, - find_objects, - label, -) -from skimage.exposure import rescale_intensity -import vtk -import vtkbone -from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy - -from ogo.fea.alignment import ( - estimate_rigid_icp, - estimate_rigid_icp_vtk, - invert_point_transform, - output_grid_for_point_transform, - point_cloud_axis_lengths, - polydata_points, - resample_vtk_image_with_point_transform, - sample_points, - surface_points_from_vtk_mask, -) -from ogo.fea.boundary import ( - bbox_relative_contact_plane, - foreground_voxel_center_bounds, - generate_projected_material_disk_vtk, - pad_vtk_images_to_physical_bounds, - projected_material_disk_required_bounds, - resample_vtk_image_to_spacing, - should_smooth_resampled_mask, - smooth_binary_mask_vtk, - smooth_label_mask_vtk, -) -from ogo.fea.image_io import write_vtk_image_with_sitk_geometry -from ogo.fea.model import ( - apply_spine_boundary_conditions as apply_boundary_conditions, - create_microfe_model, - find_and_add_visible_nodes, - log_fe_arguments, -) -import ogo.util.Helper as ogo -from ogo.util.echo_arguments import echo_arguments -from scipy.ndimage import gaussian_filter - vtk.vtkObject.GlobalWarningDisplayOff() @@ -1317,72 +1347,19 @@ def clean_body_component(mask_image, body_label): return numpy_to_vtk_image(data, mask_image), metrics -def process_vertebra(input_mask, input_image, n88model_output_path, body_label, process_label, reference_path, **kwargs): - - """Build one vertebra model, registered masks and generation QC sidecars.""" - pmma_mat_id = kwargs.get("pmma_mat_id", 5000) - pistoia_mask = kwargs.get("pistoia_mask") - pistoia_mask_label = kwargs.get("pistoia_mask_label") or [] - iso_resolution = kwargs.get("iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM) - pmma_thick = kwargs.get("pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM) - pmma_intrusion = kwargs.get("pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM) - stable_surface_fraction = float( - kwargs.get("stable_surface_fraction", DEFAULT_SPINE_STABLE_CONTACT_FRACTION) - ) - stable_surface_trim_fraction = float( - kwargs.get("stable_surface_trim_fraction", DEFAULT_SPINE_STABLE_CONTACT_TRIM_FRACTION) - ) - stable_surface_min_area_fraction = float( - kwargs.get( - "stable_surface_min_area_fraction", - DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION, - ) - ) - stable_surface_max_depth = float( - kwargs.get("stable_surface_max_depth", DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM) - ) - stable_surface_min_shift = float( - kwargs.get("stable_surface_min_shift", DEFAULT_SPINE_STABLE_CONTACT_MIN_SHIFT_MM) - ) - stable_surface_close_gaps_3d = kwargs.get( - "stable_surface_close_gaps_3d", - DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D, - ) - stable_surface_close_gaps_max_gap = int( - kwargs.get( - "stable_surface_close_gaps_max_gap", - DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_MAX_GAP, - ) - ) - stable_surface_keep_largest_component = kwargs.get( - "stable_surface_keep_largest_component", - DEFAULT_SPINE_STABLE_CONTACT_KEEP_LARGEST_COMPONENT, - ) - stable_surface_enabled = stable_surface_fraction > 0.0 or stable_surface_trim_fraction > 0.0 - top_node_set_id = kwargs.get("top_node_set_id", DEFAULT_SPINE_TOP_NODE_SET_ID) - bottom_node_set_id = kwargs.get("bottom_node_set_id", DEFAULT_SPINE_BOTTOM_NODE_SET_ID) - quality_control = kwargs.get("quality_control", True) - process_orientation_qc = kwargs.get("process_orientation_qc", True) - registration_scale = kwargs.get("registration_scale", DEFAULT_SPINE_REGISTRATION_SCALE) - registration_min_scale = kwargs.get("registration_min_scale", DEFAULT_SPINE_REGISTRATION_MIN_SCALE) - registration_max_scale = kwargs.get("registration_max_scale", DEFAULT_SPINE_REGISTRATION_MAX_SCALE) - registration_backend = kwargs.get("registration_backend", DEFAULT_SPINE_REGISTRATION_BACKEND) - registration_landmarks = kwargs.get("registration_landmarks", DEFAULT_SPINE_REGISTRATION_LANDMARKS) - registration_iterations = kwargs.get("registration_iterations", DEFAULT_SPINE_REGISTRATION_ITERATIONS) - spine_icp_target = str(kwargs.get("spine_icp_target", DEFAULT_SPINE_ICP_TARGET)).strip().lower() - if spine_icp_target not in SPINE_ICP_TARGETS: - raise ValueError("spine_icp_target must be 'body' or 'vertebra'.") - mask_smoothing_spacing_threshold = kwargs.get( - "mask_smoothing_spacing_threshold", - DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - ) - label_smoothing_sigma_mm = float( - kwargs.get("label_smoothing_sigma_mm", DEFAULT_SPINE_LABEL_SMOOTHING_SIGMA_MM) - ) - label_smoothing_threshold = float( - kwargs.get("label_smoothing_threshold", DEFAULT_SPINE_LABEL_SMOOTHING_THRESHOLD) - ) - +def _prepare_spine_inputs( + input_image, + input_mask, + body_label, + process_label, + pistoia_mask, + pistoia_mask_label, + iso_resolution, + label_smoothing_sigma_mm, + label_smoothing_threshold, + spine_icp_target, +): + """Read, clean and resample labels and density on a common isotropic grid.""" #Read Image and Mask ogo.message(f"Reading image...: {input_image}") image_reader = read(input_image) @@ -1474,6 +1451,38 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, if spine_icp_target == "vertebra": registration_target = combine_mask(registration_body.GetOutput(), isolated_process.GetOutput()) + return ( + preprocessed_image, + preprocessed_pistoia_mask, + registration_target, + isolated_vertebra, + isolated_process, + input_spacing, + body_cleanup, + ) + + +def _register_spine_inputs( + preprocessed_image, + preprocessed_pistoia_mask, + registration_target, + isolated_vertebra, + isolated_process, + input_spacing, + reference_path, + iso_resolution, + registration_scale, + registration_min_scale, + registration_max_scale, + registration_backend, + registration_landmarks, + registration_iterations, + registration_margin_voxels, + mask_smoothing_spacing_threshold, + process_orientation_qc, + spine_icp_target, +): + """Register the chosen surface and resample full anatomy into the reference frame.""" # Marching Cubes and Registration if not os.path.exists(reference_path): raise FileNotFoundError( @@ -1514,7 +1523,7 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, rotation=sample_to_reference_rotation, translation=sample_to_reference_translation, spacing=output_spacing, - margin_voxels=int(kwargs.get("registration_margin_voxels", 4)), + margin_voxels=int(registration_margin_voxels), ) transformed_vertebra = resample_vtk_image_with_point_transform( isolated_vertebra.GetOutput(), @@ -1581,6 +1590,161 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, f"transverse_offset={process_qc['transverse_offset_mm']:.2f} mm" ) + return ( + transformed_vertebra, + transformed_process, + transformed_image, + transformed_pistoia_mask, + sample_to_reference_rotation, + ) + + +def process_vertebra( + input_mask, input_image, n88model_output_path, body_label, process_label, reference_path, + *, + iso_resolution=DEFAULT_SPINE_ISO_RESOLUTION_MM, + pistoia_mask=None, + pistoia_mask_label=None, + pmma_thick=DEFAULT_SPINE_PMMA_THICKNESS_MM, + pmma_intrusion=DEFAULT_SPINE_PMMA_INTRUSION_MM, + stable_surface_fraction=DEFAULT_SPINE_STABLE_CONTACT_FRACTION, + stable_surface_trim_fraction=DEFAULT_SPINE_STABLE_CONTACT_TRIM_FRACTION, + stable_surface_min_area_fraction=DEFAULT_SPINE_STABLE_CONTACT_MIN_AREA_FRACTION, + stable_surface_max_depth=DEFAULT_SPINE_STABLE_CONTACT_MAX_DEPTH_MM, + stable_surface_min_shift=DEFAULT_SPINE_STABLE_CONTACT_MIN_SHIFT_MM, + stable_surface_close_gaps_3d=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_3D, + stable_surface_close_gaps_max_gap=DEFAULT_SPINE_STABLE_CONTACT_CLOSE_GAPS_MAX_GAP, + stable_surface_keep_largest_component=DEFAULT_SPINE_STABLE_CONTACT_KEEP_LARGEST_COMPONENT, + quality_control=True, + process_orientation_qc=True, + registration_scale=DEFAULT_SPINE_REGISTRATION_SCALE, + registration_min_scale=DEFAULT_SPINE_REGISTRATION_MIN_SCALE, + registration_max_scale=DEFAULT_SPINE_REGISTRATION_MAX_SCALE, + registration_backend=DEFAULT_SPINE_REGISTRATION_BACKEND, + registration_landmarks=DEFAULT_SPINE_REGISTRATION_LANDMARKS, + registration_iterations=DEFAULT_SPINE_REGISTRATION_ITERATIONS, + registration_margin_voxels=4, + spine_icp_target=DEFAULT_SPINE_ICP_TARGET, + mask_smoothing_spacing_threshold=DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + label_smoothing_sigma_mm=DEFAULT_SPINE_LABEL_SMOOTHING_SIGMA_MM, + label_smoothing_threshold=DEFAULT_SPINE_LABEL_SMOOTHING_THRESHOLD, + export_nifti=False, + poissons_ratio=DEFAULT_SPINE_POISSONS_RATIO, + elastic_E_func=None, + yield_comp_func=None, + yield_tens_func=None, + cort_elastic_E_func=None, + cort_yield_comp_func=None, + cort_yield_tens_func=None, + cort_poissons_ratio=None, + pmma_mat_id=DEFAULT_SPINE_PMMA_MATERIAL_ID, + pmma_E=DEFAULT_SPINE_PMMA_E_MPA, + pmma_v=DEFAULT_SPINE_PMMA_POISSONS_RATIO, + pmma_yield_compression=None, + pmma_yield_tension=None, + fe_displacement=DEFAULT_SPINE_FE_DISPLACEMENT_MM, + top_node_set_id=DEFAULT_SPINE_TOP_NODE_SET_ID, + bottom_node_set_id=DEFAULT_SPINE_BOTTOM_NODE_SET_ID, + top_node_set_name="body_top", + bottom_node_set_name="body_bottom", + top_direction=(0, 0, 1), + bottom_direction=(0, 0, -1), + top_displacement="top_displacement", + bottom_fixed_senses=("x", "y", "z"), + filter_bc_node_sets=True, + bc_filter_axis="z", + bc_filter_tolerance=1.0e-5, +): + """Build one vertebra model, registered masks and generation QC sidecars.""" + pistoia_mask_label = pistoia_mask_label or [] + stable_surface_fraction = float(stable_surface_fraction) + stable_surface_trim_fraction = float(stable_surface_trim_fraction) + stable_surface_min_area_fraction = float(stable_surface_min_area_fraction) + stable_surface_max_depth = float(stable_surface_max_depth) + stable_surface_min_shift = float(stable_surface_min_shift) + stable_surface_close_gaps_max_gap = int(stable_surface_close_gaps_max_gap) + stable_surface_enabled = stable_surface_fraction > 0.0 or stable_surface_trim_fraction > 0.0 + label_smoothing_sigma_mm = float(label_smoothing_sigma_mm) + label_smoothing_threshold = float(label_smoothing_threshold) + spine_icp_target = str(spine_icp_target).strip().lower() + if spine_icp_target not in SPINE_ICP_TARGETS: + raise ValueError("spine_icp_target must be 'body' or 'vertebra'.") + model_parameters = { + "poissons_ratio": poissons_ratio, + "elastic_E_func": elastic_E_func, + "yield_comp_func": yield_comp_func, + "yield_tens_func": yield_tens_func, + "cort_elastic_E_func": cort_elastic_E_func, + "cort_yield_comp_func": cort_yield_comp_func, + "cort_yield_tens_func": cort_yield_tens_func, + "cort_poissons_ratio": cort_poissons_ratio, + "pmma_mat_id": pmma_mat_id, + "pmma_E": pmma_E, + "pmma_v": pmma_v, + "pmma_yield_compression": pmma_yield_compression, + "pmma_yield_tension": pmma_yield_tension, + "fe_displacement": fe_displacement, + "top_node_set_id": top_node_set_id, + "bottom_node_set_id": bottom_node_set_id, + "top_node_set_name": top_node_set_name, + "bottom_node_set_name": bottom_node_set_name, + "top_direction": top_direction, + "bottom_direction": bottom_direction, + "top_displacement": top_displacement, + "bottom_fixed_senses": bottom_fixed_senses, + "filter_bc_node_sets": filter_bc_node_sets, + "bc_filter_axis": bc_filter_axis, + "bc_filter_tolerance": bc_filter_tolerance, + } + + ( + preprocessed_image, + preprocessed_pistoia_mask, + registration_target, + isolated_vertebra, + isolated_process, + input_spacing, + body_cleanup, + ) = _prepare_spine_inputs( + input_image, + input_mask, + body_label, + process_label, + pistoia_mask, + pistoia_mask_label, + iso_resolution, + label_smoothing_sigma_mm, + label_smoothing_threshold, + spine_icp_target, + ) + + ( + transformed_vertebra, + transformed_process, + transformed_image, + transformed_pistoia_mask, + sample_to_reference_rotation, + ) = _register_spine_inputs( + preprocessed_image, + preprocessed_pistoia_mask, + registration_target, + isolated_vertebra, + isolated_process, + input_spacing, + reference_path, + iso_resolution, + registration_scale, + registration_min_scale, + registration_max_scale, + registration_backend, + registration_landmarks, + registration_iterations, + registration_margin_voxels, + mask_smoothing_spacing_threshold, + process_orientation_qc, + spine_icp_target, + ) + ogo.message(f"relabelling mask and identifying boundary surfaces...") # Assuming mask1_data and mask2_data are your initial binary masks mask1_labeled = label_mask(transformed_vertebra, 2) # Apply label 1 to the first mask @@ -1803,10 +1967,10 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, bottom_boundary_mask_image=inferior_disk, top_boundary_mask_label=1, bottom_boundary_mask_label=1, - **kwargs, + **model_parameters, ) - if kwargs.get("export_nifti", False): + if export_nifti: ogo.message("generating nifti files...") export_nifti_outputs( @@ -1847,6 +2011,20 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, writer.SetFileName(n88model_output_path) writer.Update() + settings = generation_settings( + label_smoothing_sigma_mm=label_smoothing_sigma_mm, + label_smoothing_threshold=label_smoothing_threshold, + stable_surface_fraction=stable_surface_fraction, + stable_surface_trim_fraction=stable_surface_trim_fraction, + stable_surface_min_area_fraction=stable_surface_min_area_fraction, + stable_surface_max_depth=stable_surface_max_depth, + stable_surface_min_shift=stable_surface_min_shift, + stable_surface_close_gaps_3d=stable_surface_close_gaps_3d, + stable_surface_close_gaps_max_gap=stable_surface_close_gaps_max_gap, + stable_surface_keep_largest_component=stable_surface_keep_largest_component, + ) + settings_path = Path(n88model_output_path).with_name(Path(n88model_output_path).stem + "_modeling.json") + settings_path.write_text(json.dumps({"generation_settings": settings}, indent=2) + "\n") # Retain the aligned body ROI so post-solve QC uses the identical anatomy. body_qc_path = n88model_output_path.replace(".n88model", "_qc_body_mask.nii.gz") write_vtk_image_with_sitk_geometry(padded_mask1, body_qc_path) @@ -1854,7 +2032,6 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, try_export_model_qc(n88model_output_path, "spine", body_mask=body_qc_path, model=model) - ###################################################################### MAIN ## Main function containing file I/O via argparse ###################################################################### MAIN @@ -1862,30 +2039,7 @@ def process_vertebra(input_mask, input_image, n88model_output_path, body_label, def main(): """Parse anatomy-builder arguments and generate the requested vertebra model.""" - description = ''' - This script sets up the L4 vertebral compression FE model from the - density (K2HPO4) calibrated image. This script sets up the model for a L4 vertebra - (including arch and pedicles). The analysis resamples the image to isotropic voxels, - transforms the image, applies the bone mask and bins the data. It then creates the FE - model for solving using FAIM (v8.1, Numerics Solutions Ltd, Calgary, Canada - Steven - Boyd). - - Input: Calibrated K2HPO4 Image (*.nii), Bone Mask (*_MASK.nii) - - Optional Parameters: - 1) Mask Threshold - 2) Isotropic resample voxel size - 3) Power-law exponent - 4) Power-law coefficient - 5) Bone Poissons ratio - 6) PMMA Elastic Modulus - 7) PMMA Poissons ratio - 8) PMMA pmma_thick - 9) PMMA material ID - 10) FE displacement - - Output: N88 Model (*.n88model) - ''' + description = """Build a vertebral compression model from calibrated K2HPO4 density\n and body/process labels. Resample, register, place PMMA supports and write\n an n88model. Use ogoFEA spine to also solve and report results.""" parser = argparse.ArgumentParser( formatter_class=argparse.RawTextHelpFormatter, @@ -1912,10 +2066,6 @@ def main(): help="Set the isotropic voxel size [in mm]. (default: %(default)s [mm])") parser.add_argument("--mask_smoothing_spacing_threshold", type=float, default=DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, help="Smooth resampled body/process masks only when an input spacing dimension exceeds this value. (default: %(default)s [mm])") - #parser.add_argument("--elastic_exponent", type=float, default=2.29, - # help="Sets the exponent (b) for power law: E=A(den)^b. (default: %(default)s)") - #parser.add_argument("--elastic_Emax", type=float, default=10500, - # help="Sets the coefficient (A) Elastic Modulus value for the power law: E=A(den)^b. (default: %(default)s [MPa])") parser.add_argument("--poissons_ratio", type=float, default=DEFAULT_SPINE_POISSONS_RATIO, help="Sets the Poisson's ratio for the material(s) in the FE model. (default: %(default)s)") parser.add_argument("--pmma_E", type=float, default=DEFAULT_SPINE_PMMA_E_MPA, @@ -2014,13 +2164,10 @@ def main(): if reference_path is None: reference_path = str(default_spine_reference_path(args.spine_icp_target)) - # Extract all kwargs - kwargs = vars(args).copy() # Convert parsed arguments to a dictionary - kwargs.pop("calibrated_image") # Remove positional argument - kwargs.pop("bone_mask") # Remove positional argument - kwargs.pop("mask_threshold") # Remove required argument - kwargs.pop("process_mask_threshold") # Remove required argument - kwargs.pop("reference_path") # Ensure updated reference path + options = vars(args).copy() + for name in ("calibrated_image", "bone_mask", "mask_threshold", + "process_mask_threshold", "reference_path", "output_path", "appendix"): + options.pop(name) # Run the vertebra processing process_vertebra( @@ -2030,7 +2177,7 @@ def main(): args.mask_threshold, args.process_mask_threshold, reference_path, - **kwargs # Pass remaining arguments as kwargs + **options, ) ###################################################################### MAIN diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index b7dfd01..5a0de4b 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -827,6 +827,16 @@ def test_spine_modeling_metadata_records_materials_and_bcs(tmp_path): assert data["alignment"]["registration_backend"] == "numpy" assert data["alignment"]["registration_landmarks"] == 8000 assert data["alignment"]["registration_iterations"] == 50 + assert data["image_processing"]["registration_label_smoothing"] == { + "sigma_mm": 1.0, "threshold": 0.5, "stage": "before ICP", + } + contact = data["boundary_conditions"]["stable_contact"] + assert contact["enabled"] is True + assert contact["trim_fraction"] == 0.10 + assert contact["minimum_shift_mm"] == 3.0 + assert contact["close_gaps_3d"] is True + assert contact["maximum_gap_voxels"] == 2 + assert contact["keep_largest_component"] is True assert data["geometry"]["model_coordinates"] == "preprocessed_image_physical_space" assert data["materials"]["trabecular"]["elastic_E_func"] == "kopperdahl_trab_E" assert data["materials"]["cortical"]["material_id_range"] == [129, 256] @@ -844,6 +854,28 @@ def test_spine_modeling_metadata_records_materials_and_bcs(tmp_path): assert data["solve_and_reporting"]["run_pistoia"] is False +def test_spine_metadata_preserves_resolved_generation_settings(tmp_path): + from ogo.fea.spine import generation_settings + + model = tmp_path / "density_vertebra_2_L1.n88model" + model.write_text("dummy") + settings = generation_settings( + label_smoothing_sigma_mm=0.75, + stable_surface_trim_fraction=0.3, + stable_surface_min_shift=4.0, + stable_surface_close_gaps_3d=False, + ) + sidecar = model.with_name(model.stem + "_modeling.json") + sidecar.write_text(json.dumps({"generation_settings": settings})) + path = GenerateFEM.write_modeling_metadata( + model, "spine", ["density.nii.gz", "mask.nii.gz"], _metadata_args(tmp_path) + ) + data = json.loads(path.read_text()) + assert data["image_processing"]["registration_label_smoothing"] == settings["registration_label_smoothing"] + assert data["boundary_conditions"]["stable_contact"] == settings["stable_contact"] + assert data["generation_settings"] == settings + + def test_femur_modeling_metadata_records_materials_shaft_and_bcs(tmp_path): model = tmp_path / "density_LT_FEMUR_SF.n88model" model.write_text("dummy") diff --git a/tests/fea/fixtures/fe_workflow_defaults_locked.json b/tests/fea/fixtures/fe_workflow_defaults_locked.json index c65fee1..4bae40f 100644 --- a/tests/fea/fixtures/fe_workflow_defaults_locked.json +++ b/tests/fea/fixtures/fe_workflow_defaults_locked.json @@ -2,32 +2,8 @@ "schema_version": 1, "workflows": { "hip-sideways-fall": { - "bbox_crop_from": [ - null, - "max", - null - ], - "bbox_ratio": [ - 1.0, - 1.3, - null - ], "cortical_label": 1, "cut_mode": "greater_trochanter_length", - "distal_shaft_center_fraction": [ - 0.5, - 0.5, - -0.1 - ], - "distal_shaft_normal": [ - 0.0, - -0.2588190451025207, - 0.9659258262890683 - ], - "distal_shaft_size_fraction": [ - 1.0, - 1.0 - ], "post_icp_distal_shaft_support_fraction": 0.9, "fe_displacement_mm": -4.0, "femoral_head_center_fraction": [ @@ -48,7 +24,6 @@ "input_margin_mm": 16.0, "iso_resolution_mm": 1.0, "left_femur_code": 1, - "lesser_trochanter_distal_offset_mm": 50.0, "mask_smoothing_spacing_threshold_mm": 2.0, "node_sets": [ "Femoral_Head_PMMA_Nodes", @@ -57,7 +32,6 @@ ], "pmma_intrusion_mm": 6.0, "pmma_thickness_mm": 10.0, - "proximal_reference_distance_mm": 40.0, "reference_max_scale": [ 1.2, 1.2, diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index c9cb23b..e58682f 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -66,6 +66,38 @@ def _polydata_from_points(points): return polydata +def test_preparation_and_fixed_registration_preserve_full_femur(tmp_path, monkeypatch): + np = pytest.importorskip("numpy") + from ogo.fea.boundary import vtk_image_to_numpy + + captured = [] + monkeypatch.setattr("sys.argv", ["hip", "density.nii.gz", "mask.nii.gz"]) + monkeypatch.setattr(femur, "sidewaysFallFe", captured.append) + femur.main() + args = captured[0] + density = _vtk_image_from_array(np.full((12, 12, 150), 200, dtype=np.float32)) + labels = np.zeros((12, 12, 150), dtype=np.uint8) + labels[3:9, 3:9, 5:145] = 1 + mask = _vtk_image_from_array(labels) + monkeypatch.setattr(femur.ogo, "readNii", lambda path: density if path == args.calibrated_image else mask) + transform = tmp_path / "fixed_icp.json" + femur.write_icp_transform( + transform, matrix=np.eye(4), + icp_transform={"rotation": np.eye(3).tolist(), "iterations": 0, "mean_distance": 0}, + reference_scale={}, femur_side=1, rough_crop={}, + ) + args.femur_icp_transform_in = str(transform) + + prepared = femur._prepare_femur_inputs(args) + full_mask = prepared[1] + registration_mask = prepared[4] + assert np.count_nonzero(vtk_image_to_numpy(full_mask)) == np.count_nonzero(labels) + assert np.count_nonzero(vtk_image_to_numpy(registration_mask)) < np.count_nonzero(labels) + registered = femur._register_femur_inputs(args, str(tmp_path / "model.n88model"), *prepared) + assert np.count_nonzero(vtk_image_to_numpy(registered[1])) == np.count_nonzero(labels) + assert registered[4] is False + + def test_side_suffix_matches_compact_outputs(): assert femur.side_suffix(1) == "LF" assert femur.side_suffix(2) == "RF" @@ -134,18 +166,6 @@ def test_hip_fixture_defaults_use_fixed_thickness_and_intrusion(): assert femur.DEFAULT_PMMA_INTRUSION_MM == pytest.approx(6.0) -def test_tilted_side_support_vector_has_unit_length(): - np = pytest.importorskip("numpy") - - vector = femur.tilted_side_support_vector(-20) - - assert np.linalg.norm(vector) == pytest.approx(1.0) - - -def test_distal_cut_z_from_reference_uses_superior_bound(): - assert femur.distal_cut_z_from_reference((0, 1, 0, 1, -20, 120), 100) == pytest.approx(20) - - def test_swap_xz_footprint_preserves_center_and_swaps_lengths(): swapped = femur.swap_xz_footprint((10, 50, 1, 3, 100, 120)) @@ -289,27 +309,6 @@ def test_proximal_sideways_fall_fixture_plane_does_not_reach_distal_shaft(): assert plane["center"][2] + plane["size"][0] / 2.0 == pytest.approx(120.0) -def test_bbox_relative_oriented_contact_plane_keeps_measured_normal(): - np = pytest.importorskip("numpy") - bounds = (10, 50, -20, 80, 100, 200) - - plane = femur.bbox_relative_oriented_contact_plane( - bounds, - center_fraction=(0.25, 0.5, 0.1), - size_fraction=(0.4, 0.5), - normal=(0.0, -0.5, 0.8660254038), - shape="anatomy", - ) - - assert plane["center"] == pytest.approx((20.0, 30.0, 110.0)) - assert plane["normal"] == pytest.approx((0.0, -0.5, 0.8660254038)) - assert plane["outward_normal"] == pytest.approx((0.0, 0.5, -0.8660254038)) - assert plane["v_axis"] == pytest.approx((-1.0, 0.0, 0.0)) - assert np.cross(plane["u_axis"], plane["v_axis"]) == pytest.approx(plane["normal"]) - assert plane["size"] == pytest.approx((54.641016152, 20.0)) - assert plane["shape"] == "anatomy" - - def test_scale_reference_to_sample_principal_lengths_clips_and_scales_origin(): reference = _polydata_from_points( [ @@ -436,118 +435,6 @@ def test_scale_reference_point_cloud_to_sample_matches_voxel_surface_lengths(): assert scaled.GetBounds() == pytest.approx((-0.8, 0.8, -2.4, 2.4, -3.9, 3.9)) -def test_bbox_ratio_crop_matches_recipe_order_and_tracks_cut_face(): - np = pytest.importorskip("numpy") - from ogo.util.vtk_image import vtk_image_to_numpy - - density = np.zeros((24, 20, 24), dtype=np.float32) - mask = np.zeros_like(density, dtype=np.uint8) - density[2:22, 4:14, 2:18] = 700.0 - mask[2:22, 4:14, 2:18] = 2 - - cropped, crop_face, meta = femur.crop_vtk_images_to_bbox_ratio( - [_vtk_image_from_array(density)], - _vtk_image_from_array(mask), - bbox_ratio=femur.DEFAULT_FEMUR_BBOX_RATIO, - bbox_crop_from=femur.DEFAULT_FEMUR_BBOX_CROP_FROM, - labels={2}, - ) - - cropped_density = vtk_image_to_numpy(cropped[0]) - crop_face_data = vtk_image_to_numpy(crop_face) - - assert cropped_density.shape == (20, 10, 13) - assert cropped[0].GetOrigin() == pytest.approx((2, 4, 5)) - assert meta["ratio_xyz"] == (None, 1.0, 1.3) - assert meta["crop_from_xyz"] == (None, None, "min") - assert meta["crop_slices_xyz"] == ((2, 22), (4, 14), (5, 18)) - assert crop_face_data.sum() == 20 * 10 - assert np.all(crop_face_data[:, :, 0] == 1) - assert not np.any(crop_face_data[:, :, 1:]) - - -def test_bbox_ratio_crop_uses_shortest_unit_axis_as_reference(): - np = pytest.importorskip("numpy") - from ogo.util.vtk_image import vtk_image_to_numpy - - density = np.zeros((24, 20, 24), dtype=np.float32) - mask = np.zeros_like(density, dtype=np.uint8) - density[2:22, 4:14, 2:18] = 700.0 - mask[2:22, 4:14, 2:18] = 2 - - cropped, _crop_face, meta = femur.crop_vtk_images_to_bbox_ratio( - [_vtk_image_from_array(density)], - _vtk_image_from_array(mask), - bbox_ratio=(1.0, 1.0, 1.0), - labels={2}, - ) - - assert vtk_image_to_numpy(cropped[0]).shape == (10, 10, 10) - assert cropped[0].GetOrigin() == pytest.approx((7, 4, 5)) - assert meta["reference_axis"] == "y" - assert meta["reference_length_mm"] == pytest.approx(10.0) - - -def test_bbox_ratio_crop_recomputes_reference_after_distal_shaft_crop(): - np = pytest.importorskip("numpy") - from ogo.util.vtk_image import vtk_image_to_numpy - - density = np.zeros((12, 60, 90), dtype=np.float32) - mask = np.zeros_like(density, dtype=np.uint8) - density[2:10, 5:55, 0:10] = 700.0 - mask[2:10, 5:55, 0:10] = 2 - density[2:10, 20:40, 10:80] = 700.0 - mask[2:10, 20:40, 10:80] = 2 - - cropped, _crop_face, meta = femur.crop_vtk_images_to_bbox_ratio( - [_vtk_image_from_array(density)], - _vtk_image_from_array(mask), - bbox_ratio=femur.DEFAULT_FEMUR_BBOX_RATIO, - bbox_crop_from=femur.DEFAULT_FEMUR_BBOX_CROP_FROM, - labels={2}, - ) - - cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 - coords = np.argwhere(cropped_mask) - final_size = coords.max(axis=0) - coords.min(axis=0) + 1 - - assert final_size[1] == 20 - assert final_size[2] <= 26 - assert meta["crop_iterations"] > 1 - assert meta["reference_length_mm"] == pytest.approx(20.0) - assert meta["final_bbox_ratio_xyz"][2] <= 1.3 - - -def test_proximal_box_ratio_crop_uses_proximal_transverse_width(): - np = pytest.importorskip("numpy") - from ogo.util.vtk_image import vtk_image_to_numpy - - density = np.zeros((110, 120, 120), dtype=np.float32) - mask = np.zeros_like(density, dtype=np.uint8) - density[30:82, 45:73, 70:110] = 700.0 - mask[30:82, 45:73, 70:110] = 2 - density[46:66, 45:105, 10:70] = 700.0 - mask[46:66, 45:105, 10:70] = 2 - - cropped, crop_face, meta = femur.crop_vtk_images_to_proximal_box_ratio( - [_vtk_image_from_array(density)], - _vtk_image_from_array(mask), - ratio=1.2, - labels={2}, - ) - - cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 - coords = np.argwhere(cropped_mask) - final_size = coords.max(axis=0) - coords.min(axis=0) + 1 - - assert meta["reference_width_mm"] == pytest.approx(52.0) - assert meta["reference_axis"] == "x" - assert meta["target_length_mm"] == pytest.approx(62.4) - assert final_size[2] == 62 - assert final_size[1] == 60 - assert vtk_image_to_numpy(crop_face).sum() > 0 - - def test_fixed_proximal_length_crop_keeps_proximal_side(): np = pytest.importorskip("numpy") from ogo.util.vtk_image import vtk_image_to_numpy @@ -575,261 +462,6 @@ def test_fixed_proximal_length_crop_keeps_proximal_side(): assert vtk_image_to_numpy(crop_face).sum() > 0 -def test_post_icp_flat_ratio_crop_uses_y_width_for_z_length(): - np = pytest.importorskip("numpy") - from ogo.util.vtk_image import vtk_image_to_numpy - - density = np.zeros((32, 80, 160), dtype=np.float32) - mask = np.zeros_like(density, dtype=np.uint8) - density[5:25, 10:70, 20:150] = 700.0 - mask[5:25, 10:70, 20:150] = 2 - - cropped, crop_face, meta = femur.crop_vtk_images_to_flat_post_icp_ratio( - [_vtk_image_from_array(density)], - _vtk_image_from_array(mask), - ratio=1.2, - labels={2}, - ) - - cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 - coords = np.argwhere(cropped_mask) - final_size = coords.max(axis=0) - coords.min(axis=0) + 1 - - assert meta["method"] == "post_icp_flat_ratio" - assert meta["reference_width_mm"] == pytest.approx(60.0) - assert meta["target_length_mm"] == pytest.approx(72.0) - assert meta["status"] == "cropped" - assert final_size[1] == 60 - assert final_size[2] == 72 - assert final_size[2] / final_size[1] == pytest.approx(1.2) - assert vtk_image_to_numpy(crop_face).sum() > 0 - - -def test_post_icp_flat_ratio_crop_rejects_short_femur(): - np = pytest.importorskip("numpy") - - density = np.zeros((32, 80, 90), dtype=np.float32) - mask = np.zeros_like(density, dtype=np.uint8) - density[5:25, 10:70, 20:80] = 700.0 - mask[5:25, 10:70, 20:80] = 2 - - with pytest.raises(ValueError, match="too short for the requested aspect ratio"): - femur.crop_vtk_images_to_flat_post_icp_ratio( - [_vtk_image_from_array(density)], - _vtk_image_from_array(mask), - ratio=1.2, - labels={2}, - ) - - -def test_post_icp_oblique_ratio_crop_uses_transformed_crop_face_angle(): - np = pytest.importorskip("numpy") - from ogo.util.vtk_image import vtk_image_to_numpy - - density = np.zeros((32, 80, 160), dtype=np.float32) - mask = np.zeros_like(density, dtype=np.uint8) - rough_face = np.zeros_like(mask) - for x in range(5, 25): - for y in range(10, 70): - z = 20 + (x - 5) // 5 - density[x, y, z:150] = 700.0 - mask[x, y, z:150] = 2 - rough_face[x, y, z] = 1 - - cropped, crop_face, meta = femur.crop_vtk_images_to_oblique_post_icp_ratio( - [_vtk_image_from_array(density)], - _vtk_image_from_array(mask), - _vtk_image_from_array(rough_face), - ratio=1.2, - labels={2}, - ) - - cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 - crop_face_mask = vtk_image_to_numpy(crop_face) != 0 - crop_face_coords = np.argwhere(crop_face_mask) - - assert meta["method"] == "post_icp_oblique_ratio" - assert meta["reference_width_mm"] == pytest.approx(60.0) - assert meta["target_length_mm"] == pytest.approx(72.0) - assert meta["status"] == "cropped" - assert meta["plane_normal"][2] > 0.8 - assert meta["plane_u_axis"] is not None - assert meta["plane_v_axis"] is not None - assert meta["plane_size"][0] > 0 - assert meta["plane_size"][1] > 0 - assert crop_face_coords[:, 2].max() > crop_face_coords[:, 2].min() - assert cropped_mask.sum() > 0 - assert crop_face_mask.sum() > 0 - - -def test_post_icp_oblique_ratio_short_case_keeps_transformed_crop_face(): - np = pytest.importorskip("numpy") - from ogo.util.vtk_image import vtk_image_to_numpy - - density = np.zeros((20, 40, 50), dtype=np.float32) - mask = np.zeros_like(density, dtype=np.uint8) - rough_face = np.zeros_like(mask) - density[4:16, 5:35, 12:42] = 700.0 - mask[4:16, 5:35, 12:42] = 2 - rough_face[4:16, 5:35, 12] = 1 - - cropped, crop_face, meta = femur.crop_vtk_images_to_oblique_post_icp_ratio( - [_vtk_image_from_array(density)], - _vtk_image_from_array(mask), - _vtk_image_from_array(rough_face), - ratio=1.2, - labels={2}, - ) - - cropped_mask = vtk_image_to_numpy(cropped[0]) != 0 - crop_face_mask = vtk_image_to_numpy(crop_face) != 0 - - assert meta["method"] == "post_icp_oblique_ratio" - assert meta["status"] == "short" - assert cropped_mask.sum() == np.count_nonzero(mask) - assert crop_face_mask.sum() == np.count_nonzero(rough_face) - assert np.unique(np.argwhere(crop_face_mask)[:, 2]).tolist() == [0] - - -def test_post_icp_oblique_ratio_allows_resampled_face_offset_from_mask(): - np = pytest.importorskip("numpy") - from ogo.util.vtk_image import vtk_image_to_numpy - - density = np.zeros((32, 80, 160), dtype=np.float32) - mask = np.zeros_like(density, dtype=np.uint8) - rough_face = np.zeros_like(mask) - density[5:25, 10:70, 20:150] = 700.0 - mask[5:25, 10:70, 20:150] = 2 - rough_face[5:25, 10:70, 19] = 1 - - cropped, crop_face, meta = femur.crop_vtk_images_to_oblique_post_icp_ratio( - [_vtk_image_from_array(density)], - _vtk_image_from_array(mask), - _vtk_image_from_array(rough_face), - ratio=1.2, - labels={2}, - ) - - assert meta["method"] == "post_icp_oblique_ratio" - assert meta["status"] == "cropped" - assert vtk_image_to_numpy(cropped[0]).sum() > 0 - assert vtk_image_to_numpy(crop_face).sum() > 0 - - -def test_thicken_z_crop_face_into_mask_returns_mask_constrained_slab(): - np = pytest.importorskip("numpy") - from ogo.util.vtk_image import vtk_image_to_numpy - - mask = np.zeros((8, 8, 8), dtype=np.uint8) - mask[2:6, 2:6, 1:7] = 1 - face = np.zeros_like(mask) - face[2:6, 2:6, 1] = 1 - - slab = femur.thicken_z_crop_face_into_mask( - _vtk_image_from_array(face), - _vtk_image_from_array(mask), - thickness_voxels=3, - ) - slab_data = vtk_image_to_numpy(slab) != 0 - - assert slab_data.sum() == 4 * 4 * 3 - assert np.all(slab_data[2:6, 2:6, 1:4]) - assert not np.any(slab_data[:, :, 0]) - assert not np.any(slab_data[:, :, 4:]) - - -def test_thicken_z_crop_face_falls_back_to_distal_mask_slice(): - np = pytest.importorskip("numpy") - from ogo.util.vtk_image import vtk_image_to_numpy - - mask = np.zeros((8, 8, 8), dtype=np.uint8) - mask[2:6, 2:6, 2:7] = 1 - empty_face = np.zeros_like(mask) - - slab = femur.thicken_z_crop_face_into_mask( - _vtk_image_from_array(empty_face), - _vtk_image_from_array(mask), - thickness_voxels=2, - ) - slab_data = vtk_image_to_numpy(slab) != 0 - - assert slab_data.sum() == 4 * 4 * 2 - assert np.all(slab_data[2:6, 2:6, 2:4]) - assert not np.any(slab_data[:, :, :2]) - assert not np.any(slab_data[:, :, 4:]) - - -def test_crop_face_support_vector_points_away_from_retained_bone(): - np = pytest.importorskip("numpy") - - mask = np.zeros((10, 10, 10), dtype=np.uint8) - crop_face = np.zeros_like(mask) - for x in range(2, 8): - for y in range(2, 8): - z = 2 + (x - 2) // 3 - crop_face[x, y, z] = 1 - mask[x, y, z:9] = 1 - - vector = femur.crop_face_support_vector( - _vtk_image_from_array(crop_face), - _vtk_image_from_array(mask), - ) - - assert np.linalg.norm(vector) == pytest.approx(1.0) - assert vector[2] < -0.6 - - -def test_crop_face_contact_plane_uses_transformed_crop_face_angle(): - np = pytest.importorskip("numpy") - - mask = np.zeros((12, 12, 12), dtype=np.uint8) - crop_face = np.zeros_like(mask) - for x in range(3, 9): - for y in range(3, 9): - z = 2 + (x - 3) // 3 - crop_face[x, y, z] = 1 - mask[x, y, z:10] = 1 - - plane = femur.crop_face_contact_plane( - _vtk_image_from_array(crop_face), - _vtk_image_from_array(mask), - ) - - assert np.linalg.norm(plane["normal"]) == pytest.approx(1.0) - assert plane["normal"][2] > 0.6 - assert plane["normal"][0] < -0.2 - assert plane["outward_normal"][2] < -0.6 - assert plane["size"][0] > 0 - assert plane["size"][1] > 0 - - -def test_projected_crop_face_surface_mask_keeps_first_contact_layer(): - np = pytest.importorskip("numpy") - from ogo.util.vtk_image import vtk_image_to_numpy - - active = np.zeros((10, 10, 10), dtype=np.uint8) - active[3:7, 3:7, 4:8] = 1 - plane = { - "center": (4.5, 4.5, 3.0), - "normal": (0.0, 0.0, 1.0), - "u_axis": (1.0, 0.0, 0.0), - "v_axis": (0.0, 1.0, 0.0), - "size": (4.0, 4.0), - "shape": "anatomy", - } - - surface = femur.projected_crop_face_surface_vtk( - _vtk_image_from_array(active), - plane, - intrusion=6.0, - ) - surface_data = vtk_image_to_numpy(surface) != 0 - - assert surface_data.sum() == 16 - assert np.all(surface_data[3:7, 3:7, 4]) - assert not np.any(surface_data[:, :, 5:]) - - def test_straight_crop_face_support_uses_central_fraction_on_flat_face(): np = pytest.importorskip("numpy") from ogo.util.vtk_image import vtk_image_to_numpy @@ -852,106 +484,6 @@ def test_straight_crop_face_support_uses_central_fraction_on_flat_face(): assert not np.any(surface_data[:, :, 4:]) -def test_flat_femur_shaft_crop_rejects_short_distal_coverage(): - np = pytest.importorskip("numpy") - vtk = pytest.importorskip("vtk") - from vtk.util.numpy_support import numpy_to_vtk - - data = np.zeros((4, 4, 4), dtype=np.uint8) - data[:, :, 2:] = 1 - image = vtk.vtkImageData() - image.SetDimensions(4, 4, 4) - image.SetOrigin(0, 0, 0) - image.SetSpacing(1, 1, 1) - image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) - - with pytest.raises(ValueError, match="does not include enough distal shaft"): - femur.standardize_femur_shaft_length( - image, - image, - reference_bounds=(0, 0, 0, 0, 0, 10), - retained_length_mm=9, - cut_mode="fixed_length", - ) - - -def test_flat_femur_shaft_crop_zeroes_voxels_below_cut(): - np = pytest.importorskip("numpy") - vtk = pytest.importorskip("vtk") - from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy - - data = np.ones((3, 3, 5), dtype=np.uint8) - image = vtk.vtkImageData() - image.SetDimensions(3, 3, 5) - image.SetOrigin(0, 0, 0) - image.SetSpacing(1, 1, 1) - image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) - - _, cropped, meta = femur.standardize_femur_shaft_length( - image, - image, - reference_bounds=(0, 0, 0, 0, 0, 4), - retained_length_mm=2, - cut_mode="fixed_length", - ) - out = vtk_to_numpy(cropped.GetPointData().GetScalars()).reshape((3, 3, 5), order="F") - - assert meta["cut_z"] == pytest.approx(2) - assert not any("femoral-head-side region" in warning for warning in meta["warnings"]) - assert not np.any(out[:, :, :2]) - assert np.all(out[:, :, 2:]) - - -def test_femur_crop_warns_when_cut_enters_head_side_region(): - np = pytest.importorskip("numpy") - vtk = pytest.importorskip("vtk") - from vtk.util.numpy_support import numpy_to_vtk - - data = np.zeros((40, 40, 100), dtype=np.uint8) - data[:, 0:8, 20:70] = 1 - data[:, 8:25, 10:95] = 1 - image = vtk.vtkImageData() - image.SetDimensions(data.shape) - image.SetOrigin(0, 0, 0) - image.SetSpacing(1, 1, 1) - image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) - - _, _, meta = femur.standardize_femur_shaft_length( - image, - image, - reference_bounds=(0, 0, 0, 0, 0, 90), - retained_length_mm=30, - cut_mode="fixed_length", - ) - - assert any("femoral-head-side region" in warning for warning in meta["warnings"]) - assert any("unusually short" in warning for warning in meta["warnings"]) - - -def test_femur_crop_does_not_warn_short_length_above_threshold(): - np = pytest.importorskip("numpy") - vtk = pytest.importorskip("vtk") - from vtk.util.numpy_support import numpy_to_vtk - - data = np.zeros((40, 40, 100), dtype=np.uint8) - data[:, 10:30, 20:95] = 1 - image = vtk.vtkImageData() - image.SetDimensions(data.shape) - image.SetOrigin(0, 0, 0) - image.SetSpacing(1, 1, 1) - image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) - - _, _, meta = femur.standardize_femur_shaft_length( - image, - image, - reference_bounds=(0, 0, 0, 0, 0, 90), - retained_length_mm=56, - cut_mode="fixed_length", - ) - - assert not any("unusually short" in warning for warning in meta["warnings"]) - - def test_femur_input_padding_adds_only_missing_foreground_margin(): np = pytest.importorskip("numpy") vtk = pytest.importorskip("vtk") @@ -982,40 +514,6 @@ def test_femur_input_padding_adds_only_missing_foreground_margin(): assert out.shape[2] - 1 - coords.max(axis=1)[2] >= 2 -def test_lesser_trochanter_cut_uses_distal_area_peak(): - np = pytest.importorskip("numpy") - vtk = pytest.importorskip("vtk") - from vtk.util.numpy_support import numpy_to_vtk - - data = np.zeros((60, 80, 90), dtype=np.uint8) - x = np.arange(data.shape[0])[:, None] - y = np.arange(data.shape[1])[None, :] - for z in range(10, 86): - radius = 10 - y_center = 35 - if 58 <= z <= 64: - radius = 19 - if 73 <= z <= 79: - y_center = 50 - radius = 12 - section = ((x - 30) ** 2 + (y - y_center) ** 2) <= radius**2 - data[:, :, z] = section.astype(np.uint8) - - image = vtk.vtkImageData() - image.SetDimensions(data.shape) - image.SetOrigin(0, 0, 0) - image.SetSpacing(1, 1, 1) - image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) - - meta = femur.detect_lesser_trochanter_cut_z(image) - - assert meta["greater_trochanter_z"] == pytest.approx(76) - assert meta["greater_trochanter_disk_distal_z"] == pytest.approx(73) - assert meta["greater_trochanter_disk_proximal_z"] == pytest.approx(79) - assert meta["lesser_trochanter_z"] == pytest.approx(61) - assert meta["cut_z"] == pytest.approx(61) - - def test_greater_trochanter_length_crop_uses_gt_support_distal_edge_origin(): np = pytest.importorskip("numpy") vtk = pytest.importorskip("vtk") @@ -1099,45 +597,6 @@ def test_greater_trochanter_length_crop_rejects_short_femur(): ) -def test_lesser_trochanter_cut_uses_peak_plateau_center_and_percent_offset(): - np = pytest.importorskip("numpy") - vtk = pytest.importorskip("vtk") - from vtk.util.numpy_support import numpy_to_vtk - - data = np.zeros((80, 90, 100), dtype=np.uint8) - x = np.arange(data.shape[0])[:, None] - y = np.arange(data.shape[1])[None, :] - for z in range(10, 96): - radius = 10 - y_center = 35 - if 54 <= z <= 62: - radius = 20 - if 78 <= z <= 82: - y_center = 58 - radius = 12 - section = ((x - 40) ** 2 + (y - y_center) ** 2) <= radius**2 - data[:, :, z] = section.astype(np.uint8) - - image = vtk.vtkImageData() - image.SetDimensions(data.shape) - image.SetOrigin(0, 0, 0) - image.SetSpacing(1, 1, 1) - image.GetPointData().SetScalars(numpy_to_vtk(data.ravel(order="F"), deep=True)) - - meta = femur.detect_lesser_trochanter_cut_z( - image, - distal_offset_percent=50.0, - max_distal_to_greater_mm=45.0, - ) - - assert meta["greater_trochanter_z"] == pytest.approx(80) - assert meta["greater_trochanter_disk_distal_z"] == pytest.approx(78) - assert meta["greater_trochanter_disk_proximal_z"] == pytest.approx(82) - assert meta["lesser_trochanter_z"] == pytest.approx(58) - assert meta["distal_offset_mm"] == pytest.approx(11) - assert meta["cut_z"] == pytest.approx(47) - - def test_cortical_compartment_mask_uses_default_labels(): np = pytest.importorskip("numpy") vtk = pytest.importorskip("vtk") diff --git a/tests/fea/test_module_layout.py b/tests/fea/test_module_layout.py index 8d987fb..8d4e009 100644 --- a/tests/fea/test_module_layout.py +++ b/tests/fea/test_module_layout.py @@ -18,3 +18,19 @@ def test_domain_modules_expose_workflow_entry_points(): assert callable(femur.sidewaysFallFe) assert callable(spine.main) assert callable(spine.process_vertebra) + + +def test_femur_exposes_only_current_cropping_helpers(): + from ogo.fea import femur + + assert callable(femur.crop_vtk_images_to_fixed_proximal_length) + assert callable(femur.crop_vtk_images_to_greater_trochanter_length) + for name in ( + "crop_vtk_images_to_bbox_ratio", + "crop_vtk_images_to_proximal_box_ratio", + "crop_vtk_images_to_flat_post_icp_ratio", + "crop_vtk_images_to_oblique_post_icp_ratio", + "standardize_femur_shaft_length", + "detect_lesser_trochanter_cut_z", + ): + assert not hasattr(femur, name), name diff --git a/tests/fea/test_sideways_fall.py b/tests/fea/test_sideways_fall.py index 64bf1c3..ba23716 100644 --- a/tests/fea/test_sideways_fall.py +++ b/tests/fea/test_sideways_fall.py @@ -11,7 +11,7 @@ def test_sideways_fall_icp_path_does_not_pre_rotate_input_images(): from ogo.fea import femur - source = textwrap.dedent(inspect.getsource(femur.sidewaysFallFe)) + source = textwrap.dedent(inspect.getsource(femur._register_femur_inputs)) tree = ast.parse(source) pre_rotation_calls = [ node @@ -28,7 +28,7 @@ def test_sideways_fall_icp_path_uses_voxel_surface_points(): from ogo.fea import femur - source = textwrap.dedent(inspect.getsource(femur.sidewaysFallFe)) + source = textwrap.dedent(inspect.getsource(femur._register_femur_inputs)) tree = ast.parse(source) called_names = { node.func.id @@ -52,7 +52,7 @@ def test_sideways_fall_icp_path_uses_deterministic_point_cloud_transform(): from ogo.fea import femur - source = textwrap.dedent(inspect.getsource(femur.sidewaysFallFe)) + source = textwrap.dedent(inspect.getsource(femur._register_femur_inputs)) tree = ast.parse(source) called_names = { node.func.id @@ -69,39 +69,13 @@ def test_sideways_fall_icp_path_uses_deterministic_point_cloud_transform(): assert helper_calls == [] -def test_sideways_fall_distal_support_is_bbox_relative_recipe_plane(): - pytest.importorskip("vtk") - pytest.importorskip("vtkbone") - +def test_sideways_fall_distal_support_uses_final_flat_crop_face(): from ogo.fea import femur - bounds = ( - -76.45140061071066, - -31.451400610710657, - -102.34001525211478, - -36.34001525211478, - -0.39488812795613626, - 83.60511187204386, - ) - plane = femur.bbox_relative_oriented_contact_plane( - bounds, - center_fraction=femur.DISTAL_SHAFT_FIXTURE_CENTER_FRACTION, - size_fraction=femur.DISTAL_SHAFT_FIXTURE_SIZE_FRACTION, - normal=femur.DISTAL_SHAFT_FIXTURE_NORMAL, - size_bounds_axes=("x", "y"), - shape="anatomy", - ) - - assert femur.DISTAL_SHAFT_FIXTURE_CENTER_FRACTION == (0.5, 0.5, -0.1) - assert femur.DISTAL_SHAFT_FIXTURE_SIZE_FRACTION == (1.0, 1.0) - assert plane["center"] == pytest.approx( - (-53.95140061071066, -69.34001525211478, -8.794888127956137) - ) - assert plane["normal"] == pytest.approx( - femur.DISTAL_SHAFT_FIXTURE_NORMAL, - abs=1.0e-12, - ) - assert plane["size"] == pytest.approx((63.750, 45.0), abs=0.2) + source = inspect.getsource(femur.sidewaysFallFe) + assert "straight_crop_face_support_surface_vtk(" in source + assert "support_fraction=POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION" in source + assert femur.POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION == 0.9 def test_sideways_fall_fits_contact_canvas_before_generating_pmma(): diff --git a/tests/fea/test_spine.py b/tests/fea/test_spine.py index c1920e0..20e277d 100644 --- a/tests/fea/test_spine.py +++ b/tests/fea/test_spine.py @@ -13,6 +13,41 @@ def _mock_registration_inputs(monkeypatch): return SimpleNamespace(GetOutput=lambda: None) +def test_preparation_and_registration_preserve_spine_labels(tmp_path, monkeypatch): + vtk = pytest.importorskip("vtk") + from vtk.util.numpy_support import numpy_to_vtk + from ogo.fea.boundary import vtk_image_to_numpy + + def image(array): + result = vtk.vtkImageData() + result.SetDimensions(array.shape) + result.SetSpacing(1, 1, 1) + result.GetPointData().SetScalars(numpy_to_vtk(array.ravel(order="F"), deep=True)) + return result + + labels = np.zeros((30, 40, 30), dtype=np.uint8) + labels[10:20, 10:20, 10:20] = 2 + labels[13:17, 20:30, 13:17] = 3 + density = image(np.full(labels.shape, 200, dtype=np.float32)) + mask = image(labels) + monkeypatch.setattr(spine, "read", lambda path: SimpleNamespace( + GetOutput=lambda: density if path == "density" else mask)) + prepared = spine._prepare_spine_inputs( + "density", "mask", 2, 3, None, None, 1.0, 0.0, 0.5, "body") + assert prepared[6]["body_cleanup_removed_voxels"] == 0 + reference = tmp_path / "reference.vtk" + reference.touch() + identity = vtk.vtkTransform() + monkeypatch.setattr(spine, "get_icp_with_scaling", lambda *a, **k: identity) + registered = spine._register_spine_inputs( + *prepared[:6], str(reference), 1.0, None, "0.8,0.8,0.75", + "1.2,1.2,1.3", "numpy", 8000, 50, 4, 2.0, True, "body") + assert np.count_nonzero(vtk_image_to_numpy(registered[0])) == np.count_nonzero(labels == 2) + assert np.count_nonzero(vtk_image_to_numpy(registered[1])) == np.count_nonzero(labels == 3) + assert registered[3] is None + np.testing.assert_array_equal(registered[4], np.eye(3)) + + def test_spine_icp_preserves_native_orientation_at_initialization(monkeypatch): body = _mock_registration_inputs(monkeypatch) calls = [] @@ -89,3 +124,9 @@ def test_benchmark_presets_match_spinefe_notebook_settings(): assert nonlinear["target_displacement_percent"] == 4.0 assert nonlinear["yield_comp_func"] == "kopperdahl_trab_yc" assert nonlinear["pmma_yield_compression"] == 70.0 + + +def test_process_vertebra_rejects_unknown_settings_before_reading_inputs(): + with pytest.raises(TypeError, match="registration_landmakrs"): + spine.process_vertebra("missing_mask", "missing_image", "model.n88model", 2, 3, + "reference.vtk", registration_landmakrs=8000) diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py index 6e65758..b6146e5 100644 --- a/tests/fea/test_spinefe_benchmark.py +++ b/tests/fea/test_spinefe_benchmark.py @@ -267,7 +267,12 @@ def test_spine_registration_uses_preprocessed_isotropic_body(): from ogo.fea import spine - source = textwrap.dedent(inspect.getsource(spine.process_vertebra)) + builder = inspect.getsource(spine.process_vertebra) + source = ( + textwrap.dedent(inspect.getsource(spine._prepare_spine_inputs)) + + textwrap.dedent(inspect.getsource(spine._register_spine_inputs)) + ) + assert builder.index("_prepare_spine_inputs(") < builder.index("_register_spine_inputs(") assert "SPINE_PREPROCESSING_CROP_MARGIN_MM" in source assert "resample_vtk_image_to_spacing" in source @@ -287,7 +292,7 @@ def test_spine_workflow_resamples_to_explicit_reference_grid(): from ogo.fea import spine - source = textwrap.dedent(inspect.getsource(spine.process_vertebra)) + source = textwrap.dedent(inspect.getsource(spine._register_spine_inputs)) assert "output_grid_for_point_transform" in source assert "resample_vtk_image_with_point_transform" in source diff --git a/tests/fea/test_workflow_locked_defaults.py b/tests/fea/test_workflow_locked_defaults.py index 47937fb..40e00e7 100644 --- a/tests/fea/test_workflow_locked_defaults.py +++ b/tests/fea/test_workflow_locked_defaults.py @@ -72,17 +72,7 @@ def _locked_workflow_defaults(): "greater_trochanter_center_fraction": list( femur.GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION ), - "distal_shaft_center_fraction": list( - femur.DISTAL_SHAFT_FIXTURE_CENTER_FRACTION - ), - "distal_shaft_size_fraction": list( - femur.DISTAL_SHAFT_FIXTURE_SIZE_FRACTION - ), - "distal_shaft_normal": list(femur.DISTAL_SHAFT_FIXTURE_NORMAL), "post_icp_distal_shaft_support_fraction": femur.POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, - "bbox_ratio": list(femur.DEFAULT_FEMUR_BBOX_RATIO), - "bbox_crop_from": list(femur.DEFAULT_FEMUR_BBOX_CROP_FROM), - "proximal_reference_distance_mm": femur.DEFAULT_FEMUR_PROXIMAL_REFERENCE_DISTANCE_MM, "reference_min_scale": list(femur.DEFAULT_FEMUR_REFERENCE_MIN_SCALE), "reference_max_scale": list(femur.DEFAULT_FEMUR_REFERENCE_MAX_SCALE), "registration_backend": femur.DEFAULT_FEMUR_REGISTRATION_BACKEND, @@ -94,7 +84,6 @@ def _locked_workflow_defaults(): "cut_mode": femur.DEFAULT_FEMUR_CUT_MODE, "shaft_length_mm": femur.DEFAULT_FEMUR_SHAFT_LENGTH_MM, "rough_pre_icp_length_mm": femur.DEFAULT_FEMUR_ROUGH_PRE_ICP_LENGTH_MM, - "lesser_trochanter_distal_offset_mm": femur.DEFAULT_LESSER_TROCHANTER_DISTAL_OFFSET_MM, "cortical_label": femur.DEFAULT_CORTICAL_LABEL, "trabecular_label": femur.DEFAULT_TRABECULAR_LABEL, "node_sets": list(femur.SIDEWAYS_FALL_NODE_SETS), From 938195b7b9662f306484d3fe0d3bca8b6d475057 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:55:07 -0600 Subject: [PATCH 61/67] Simplify FEA orchestration and protect shared helper contracts --- ogo/cli/GenerateFEM.py | 577 +---------------------------- ogo/fea/README.md | 14 + ogo/fea/femur.py | 12 +- ogo/fea/metadata.py | 575 ++++++++++++++++++++++++++++ ogo/fea/qc_render.py | 65 +++- ogo/fea/spine.py | 11 +- ogo/fea/validation.py | 112 +++--- tests/cli/test_GenerateFEM.py | 31 +- tests/fea/test_module_layout.py | 18 + tests/fea/test_qc_render.py | 25 ++ tests/fea/test_shared_contracts.py | 82 ++++ 11 files changed, 891 insertions(+), 631 deletions(-) create mode 100644 ogo/fea/metadata.py create mode 100644 tests/fea/test_shared_contracts.py diff --git a/ogo/cli/GenerateFEM.py b/ogo/cli/GenerateFEM.py index c20cce6..b7286ab 100644 --- a/ogo/cli/GenerateFEM.py +++ b/ogo/cli/GenerateFEM.py @@ -8,7 +8,6 @@ import argparse from collections import namedtuple import importlib.util -import json from pathlib import Path import sys from typing import Callable, List, Optional, Sequence @@ -16,37 +15,14 @@ from ogo.fea.spine import ( BENCHMARK_LINEAR_FE_DISPLACEMENT_MM, BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM, - DEFAULT_SPINE_ISO_RESOLUTION_MM, - DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - DEFAULT_SPINE_PMMA_INTRUSION_MM, - DEFAULT_SPINE_PMMA_THICKNESS_MM, - DEFAULT_SPINE_REGISTRATION_MAX_SCALE, - DEFAULT_SPINE_REGISTRATION_MIN_SCALE, DEFAULT_SPINE_REGISTRATION_BACKEND, DEFAULT_SPINE_REGISTRATION_ITERATIONS, DEFAULT_SPINE_REGISTRATION_LANDMARKS, DEFAULT_SPINE_ICP_TARGET, - SPINE_ALIGNMENT_METHOD, SPINE_ICP_TARGETS, - default_spine_reference_path, - generation_settings as spine_generation_settings, solve_report_profile as spine_solve_report_profile, ) from ogo.fea.femur import ( - DEFAULT_FEMUR_CUT_MODE, - DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, - DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, - DEFAULT_FEMUR_ISO_RESOLUTION_MM, - DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - DEFAULT_FEMUR_REGISTRATION_BACKEND, - DEFAULT_FEMUR_REGISTRATION_ITERATIONS, - DEFAULT_FEMUR_REGISTRATION_LANDMARKS, - DEFAULT_PMMA_INTRUSION_MM, - DEFAULT_PMMA_THICKNESS_MM, - POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, - FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, - GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, - SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, solve_report_profile as femur_solve_report_profile, ) @@ -311,54 +287,6 @@ def critical_strain(args: argparse.Namespace, model_type: str) -> float: return float(solve_report_profile(args, model_type).get("critical_strain", 0.007)) -def percent_displacement_metadata( - model_path: Path, - *, - report_profile: str, - failure_axis: str, - target_percent: float, -) -> dict: - """Describe the solve displacement derived from model geometry.""" - metadata = { - "value_mm": None, - "target_displacement_percent": target_percent, - "characteristic_length_mm": None, - "value_source": "target_displacement_percent * characteristic_length_mm / 100", - } - try: - from ogo.util.faim import ( - infer_profile_characteristic_length_mm, - read_prescribed_displacement, - ) - - characteristic_length = infer_profile_characteristic_length_mm( - model_path, - report_profile, - failure_axis, - ) - current_value = read_prescribed_displacement(model_path, report_profile) - sign = -1.0 if current_value != "" and float(current_value) < 0 else 1.0 - target_mm = abs(float(target_percent)) * characteristic_length / 100.0 - metadata["value_mm"] = sign * target_mm - metadata["characteristic_length_mm"] = characteristic_length - except Exception as exc: - metadata["value_note"] = "available after model generation with netCDF4: {}".format(exc) - return metadata - - -def absolute_displacement_metadata(generator_argv: Sequence[str], default_value: str) -> dict: - """Describe a solve displacement kept as an explicit absolute mm value.""" - value = option_value(generator_argv, "--fe_displacement", default_value) - try: - value_mm = float(value) - except (TypeError, ValueError): - value_mm = value - return { - "value_mm": value_mm, - "target_displacement_percent": None, - "characteristic_length_mm": None, - "value_source": "explicit --fe_displacement used as absolute model displacement", - } def solve_model( @@ -432,485 +360,16 @@ def write_modeling_metadata( args: argparse.Namespace, bc_audit_summary: Optional[dict] = None, ) -> Optional[Path]: - """Write a traceable model-building record next to a generated n88model.""" - if args.dry_run: - return None - if not model_path.exists(): - return None + """Record resolved construction settings and the requested solve profile.""" + from ogo.fea.metadata import write_modeling_metadata as write_record - output_path = model_path.with_name(model_path.with_suffix("").name + "_modeling.json") - generation_settings = {} - if model_type == "spine": - generation_settings = spine_generation_settings() - if output_path.exists(): - generation_settings = json.loads(output_path.read_text()).get( - "generation_settings", generation_settings - ) - common = { - "schema_version": 1, - "model_file": str(model_path), - "generator": { - "entry_point": "ogoFEA", - "model_type": model_type, - "lower_level_argv": list(generator_argv), - }, - "inputs": { - "calibrated_image": str(args.calibrated_image), - "bone_mask": str(args.bone_mask), - "output_path": str(args.output_path) if args.output_path is not None else str(args.calibrated_image.parent), - }, - "geometry": { - "model_coordinates": "preprocessed_image_physical_space", - "origin_policy": "cropping and resampling update the image origin; FE meshing preserves that origin", - "boundary_condition_coordinates": "defined from the generated model bounding box in the same physical space", - }, - "post_generation_validation": { - "bc_audit": { - "enabled": bc_audit_summary is not None, - "flat_tolerance": args.bc_audit_flat_tolerance, - "summary": bc_audit_summary, - "json_path": None, - "csv_path": None, - "png_path": None - if not args.debug - else str(model_path.with_name(model_path.with_suffix("").name + "_bc_audit.png")), - } - }, - "solve_and_reporting": { - "solve_requested": not args.no_solve, - "critical_volume_pct": critical_volume_percent(args, model_type), - "masked_critical_volume_pct": ( - critical_volume_percent(args, model_type) - if getattr(args, "masked_critical_volume", None) is None - else args.masked_critical_volume - ), - "critical_strain": critical_strain(args, model_type), - "target_displacement_percent": target_displacement_percent(args, model_type), - "target_displacement_definition": "percent strain converted from model geometry for spine; percent of femur length for femur", - "run_pistoia": args.run_pistoia or args.require_pistoia or pistoia_mask_source_path(args) is not None, - "source_pistoia_mask": None - if pistoia_mask_source_path(args) is None - else str(pistoia_mask_source_path(args)), - "pistoia_mask_labels": list(args.pistoia_mask_label or []), - "model_space_pistoia_mask": None - if pistoia_mask_source_path(args) is None - else str(pistoia_mask_output_path(model_path)), - "compress_solved_model": not args.no_compress, - }, + reporting = { + "critical_volume_pct": critical_volume_percent(args, model_type), + "critical_strain": critical_strain(args, model_type), + "target_displacement_percent": target_displacement_percent(args, model_type), } - - if model_type == "spine": - body_label = option_int(generator_argv, "--mask_threshold", 0) - process_label = option_int(generator_argv, "--process_mask_threshold", 0) - appendix = option_value(generator_argv, "--appendix") - spine_icp_target = option_value( - generator_argv, - "--spine_icp_target", - DEFAULT_SPINE_ICP_TARGET, - ) - spine_reference_path = option_value( - generator_argv, - "--reference_path", - str(default_spine_reference_path(spine_icp_target)), - ) - if args.use_absolute_fe_displacement: - spine_displacement = absolute_displacement_metadata( - generator_argv, - str(spine_solve_report_profile(preset=getattr(args, "preset", None))["default_applied_displacement"]), - ) - else: - spine_displacement = percent_displacement_metadata( - model_path, - report_profile="spine", - failure_axis="z", - target_percent=common["solve_and_reporting"]["target_displacement_percent"], - ) - metadata = common.copy() - metadata.update({ - "model": "spine-compression", - "target": { - "vertebra": appendix, - "body_label": body_label, - "process_label": process_label, - }, - "alignment": { - "method": SPINE_ALIGNMENT_METHOD, - "registration_target": spine_icp_target, - "reference_path": spine_reference_path, - "registration_scale": option_value(generator_argv, "--registration_scale", "auto"), - "registration_min_scale": option_value( - generator_argv, - "--registration_min_scale", - DEFAULT_SPINE_REGISTRATION_MIN_SCALE, - ), - "registration_max_scale": option_value( - generator_argv, - "--registration_max_scale", - DEFAULT_SPINE_REGISTRATION_MAX_SCALE, - ), - "registration_backend": option_value( - generator_argv, - "--registration_backend", - DEFAULT_SPINE_REGISTRATION_BACKEND, - ), - "registration_landmarks": option_int( - generator_argv, - "--registration_landmarks", - DEFAULT_SPINE_REGISTRATION_LANDMARKS, - ), - "registration_iterations": option_int( - generator_argv, - "--registration_iterations", - DEFAULT_SPINE_REGISTRATION_ITERATIONS, - ), - }, - "image_processing": { - "iso_resolution_mm": option_float(generator_argv, "--iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM), - "registration_label_smoothing": generation_settings["registration_label_smoothing"], - "spatial_operations": "ICP transform and isotropic output spacing in one shared VTK reslice", - "image_interpolation": "cubic", - "label_interpolation": "nearest-neighbor", - "mask_smoothing": { - "operation": "binary close/open after ICP resampling", - "condition": "enabled only when any input spacing dimension exceeds threshold_mm", - "threshold_mm": option_float( - generator_argv, - "--mask_smoothing_spacing_threshold", - DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - ), - }, - "density_preprocessing": { - "connectivity_filter": True, - "bmd_preprocess_threshold": -31, - "density_binning": { - "n_bins": 128, - "background_material_id": 0, - "trabecular_material_ids": [1, 128], - "cortical_material_ids": [129, 256], - }, - }, - }, - "segmentation": { - "input_mask": str(args.bone_mask), - "labels": { - "vertebral_body": body_label, - "posterior_process": process_label, - }, - "derived_masks": { - "body": "threshold body label", - "process": "threshold process label", - "cortical": "density/surface-derived cortical shell generated after alignment", - "pmma_caps": "fixed-thickness anatomy superior and inferior caps", - }, - }, - "materials": { - "builder": "ogo.fea.materials.build_spine_material_table", - "convention": "region material IDs; 0 background, 1..128 trabecular, 129..256 cortical, PMMA explicit", - "poissons_ratio": option_float(generator_argv, "--poissons_ratio", 0.3), - "trabecular": { - "elastic_E_func": option_value(generator_argv, "--elastic_E_func", "default_E"), - "yield_comp_func": option_value(generator_argv, "--yield_comp_func"), - "yield_tens_func": option_value(generator_argv, "--yield_tens_func"), - "material_id_range": [1, 128], - }, - "cortical": { - "elastic_E_func": option_value( - generator_argv, - "--cort_elastic_E_func", - option_value(generator_argv, "--elastic_E_func", "default_E"), - ), - "yield_comp_func": option_value( - generator_argv, - "--cort_yield_comp_func", - option_value(generator_argv, "--yield_comp_func"), - ), - "yield_tens_func": option_value( - generator_argv, - "--cort_yield_tens_func", - option_value(generator_argv, "--yield_tens_func"), - ), - "poissons_ratio": option_float( - generator_argv, - "--cort_poissons_ratio", - option_float(generator_argv, "--poissons_ratio", 0.3), - ), - "material_id_range": [129, 256], - }, - "pmma": { - "material_id": option_int(generator_argv, "--pmma_mat_id", 5000), - "elastic_E_MPa": option_float(generator_argv, "--pmma_E", 2500), - "poissons_ratio": option_float(generator_argv, "--pmma_v", 0.3), - "yield_compression_MPa": option_optional_float(generator_argv, "--pmma_yield_compression"), - "yield_tension_MPa": option_optional_float(generator_argv, "--pmma_yield_tension"), - }, - }, - "boundary_conditions": { - "stable_contact": generation_settings["stable_contact"], - "fixture_geometry": { - "superior_cap": { - "label_id": option_int(generator_argv, "--top_node_set_id", 4), - "node_set": "body_top", - "shape": "fixed-thickness anatomy cap", - "pmma_thickness_mm": option_float( - generator_argv, "--pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM - ), - "pmma_intrusion_mm": option_float( - generator_argv, "--pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM - ), - "meaning": ( - "fixed-thickness anatomy cap: pmma_thickness_mm is total cap thickness; " - "pmma_intrusion_mm controls how far anatomy can occupy that fixed " - "thickness without overwriting body bone" - ), - }, - "inferior_cap": { - "label_id": option_int(generator_argv, "--bottom_node_set_id", 3), - "node_set": "body_bottom", - "shape": "fixed-thickness anatomy cap", - "pmma_thickness_mm": option_float( - generator_argv, "--pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM - ), - "pmma_intrusion_mm": option_float( - generator_argv, "--pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM - ), - "meaning": ( - "fixed-thickness anatomy cap: pmma_thickness_mm is total cap thickness; " - "pmma_intrusion_mm controls how far anatomy can occupy that fixed " - "thickness without overwriting body bone" - ), - }, - }, - "constraints": [ - { - "name": "top_displacement", - "node_set": "body_top", - "axis": "z", - **spine_displacement, - "meaning": "superior PMMA cap prescribed toward inferior cap", - }, - { - "name": "bottom_fixed", - "node_set": "body_bottom", - "axes": ["x", "y", "z"], - "value_mm": 0.0, - "meaning": "inferior PMMA cap fixed in all displacement directions", - }, - ], - }, - }) - else: - femur_side = option_value(generator_argv, "--femur_side", "1") - side = "left" if femur_side == "1" else "right" if femur_side == "2" else "unknown" - compartment_mask = option_path(generator_argv, "--compartment_mask") - if args.use_absolute_fe_displacement: - femur_displacement = absolute_displacement_metadata( - generator_argv, - str(femur_solve_report_profile()["default_applied_displacement"]), - ) - else: - femur_displacement = percent_displacement_metadata( - model_path, - report_profile="femur", - failure_axis="y", - target_percent=common["solve_and_reporting"]["target_displacement_percent"], - ) - metadata = common.copy() - metadata.update({ - "model": "femur-sideways", - "target": { - "side": side, - "femur_side_code": int(femur_side) if femur_side.isdigit() else femur_side, - "mask_threshold": option_int(generator_argv, "--mask_threshold", 1), - }, - "alignment": { - "method": "ICP to side-specific femur reference from cropped/padded input geometry", - "reference_path": option_value(generator_argv, "--reference_path", "bundled side-specific femur reference"), - "registration_backend": DEFAULT_FEMUR_REGISTRATION_BACKEND, - "registration_landmarks": DEFAULT_FEMUR_REGISTRATION_LANDMARKS, - "registration_iterations": DEFAULT_FEMUR_REGISTRATION_ITERATIONS, - }, - "image_processing": { - "iso_resolution_mm": option_float(generator_argv, "--iso_resolution", DEFAULT_FEMUR_ISO_RESOLUTION_MM), - "spatial_operations": ( - "ICP transform and isotropic output spacing in one shared VTK reslice" - ), - "image_interpolation": "cubic", - "label_interpolation": "nearest-neighbor", - "mask_smoothing": { - "operation": "binary close/open after ICP resampling", - "condition": "enabled only when any input spacing dimension exceeds threshold_mm", - "threshold_mm": option_float( - generator_argv, - "--mask_smoothing_spacing_threshold", - DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, - ), - "applies_to": [ - "whole-femur mask", - "derived cortical binary mask when compartment_mask is supplied", - ], - }, - "density_preprocessing": { - "connectivity_filter": True, - "bmd_preprocess_threshold": -31, - "density_binning": { - "n_bins": 128, - "background_material_id": 0, - "trabecular_material_ids": [1, 128], - "cortical_material_ids": [129, 256] if compartment_mask is not None else None, - }, - }, - }, - "segmentation": { - "input_mask": str(args.bone_mask), - "whole_bone_mask_threshold": option_int(generator_argv, "--mask_threshold", 1), - "compartment_mask": compartment_mask, - "compartment_labels": None - if compartment_mask is None - else { - "cortical": option_int(generator_argv, "--cortical_label", 1), - "trabecular": option_int(generator_argv, "--trabecular_label", 2), - }, - }, - "shaft_standardization": { - "cut_mode": DEFAULT_FEMUR_CUT_MODE, - "crop_stage": "fixed rough crop before ICP for registration, final GT-disk-relative flat crop after ICP on the full scan", - "coverage_definition": "GT disk distal voxel face minus safe flat face clearing the full transformed native scan end", - "geometry_sidecar": str(model_path.with_name(model_path.stem + "_shaft_geometry.json")), - "rough_pre_icp_crop": { - "enabled": True, - "retained_length_mm": option_float(generator_argv, "--femur_shaft_length", 120.0), - "registration_only": True, - }, - "greater_trochanter_length": { - "retained_length_mm": option_float( - generator_argv, - "--femur_greater_trochanter_distal_length", - DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, - ), - "gt_inclusion_length_mm": option_float( - generator_argv, - "--femur_greater_trochanter_inclusion_length", - DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, - ), - "length_origin": "detected greater-trochanter disk distal edge", - }, - "cut_plane": "flat post-ICP aligned-frame crop face at fixed shaft length below detected GT-disk distal edge", - "incomplete_fov_behavior": "fail model generation", - }, - "materials": { - "builder": "ogo.fea.materials.build_femur_material_table", - "convention": "region material IDs; 0 background, 1..128 trabecular, optional 129..256 cortical, PMMA explicit", - "include_cortical_region": compartment_mask is not None, - "poissons_ratio": option_float(generator_argv, "--poissons_ratio", 0.3), - "trabecular": { - "elastic_E_func": option_value(generator_argv, "--elastic_E_func", "default_E"), - "yield_comp_func": option_value(generator_argv, "--yield_comp_func"), - "yield_tens_func": option_value(generator_argv, "--yield_tens_func"), - "material_id_range": [1, 128], - }, - "cortical": None - if compartment_mask is None - else { - "elastic_E_func": option_value( - generator_argv, - "--cort_elastic_E_func", - option_value(generator_argv, "--elastic_E_func", "default_E"), - ), - "yield_comp_func": option_value( - generator_argv, - "--cort_yield_comp_func", - option_value(generator_argv, "--yield_comp_func"), - ), - "yield_tens_func": option_value( - generator_argv, - "--cort_yield_tens_func", - option_value(generator_argv, "--yield_tens_func"), - ), - "poissons_ratio": option_float( - generator_argv, - "--cort_poissons_ratio", - option_float(generator_argv, "--poissons_ratio", 0.3), - ), - "material_id_range": [129, 256], - }, - "pmma": { - "material_id": option_int(generator_argv, "--pmma_mat_id", 5000), - "elastic_E_MPa": option_float(generator_argv, "--pmma_E", 2500), - "poissons_ratio": option_float(generator_argv, "--pmma_v", 0.3), - "yield_compression_MPa": option_optional_float(generator_argv, "--pmma_yield_compression"), - "yield_tension_MPa": option_optional_float(generator_argv, "--pmma_yield_tension"), - }, - }, - "boundary_conditions": { - "fixture_geometry": { - "femoral_head": { - "node_set": "Femoral_Head_PMMA_Nodes", - "shape": "rectangle fixture cap", - "relative_to": "model_bbox", - "center_fraction": list(FEMORAL_HEAD_FIXTURE_CENTER_FRACTION), - "size_fraction": list(SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION), - "projection_axis": "y", - "pmma_thickness_mm": option_float(generator_argv, "--pmma_thick", DEFAULT_PMMA_THICKNESS_MM), - "pmma_intrusion_mm": option_float(generator_argv, "--pmma_intrusion", DEFAULT_PMMA_INTRUSION_MM), - "meaning": "bbox-scaled high-y contact fixture with unsupported columns cropped", - }, - "greater_trochanter": { - "node_set": "Greater_Trochanter_PMMA_Nodes", - "shape": "rectangle fixture cap", - "relative_to": "model_bbox", - "center_fraction": list(GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION), - "size_fraction": list(SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION), - "projection_axis": "y", - "pmma_thickness_mm": option_float(generator_argv, "--pmma_thick", DEFAULT_PMMA_THICKNESS_MM), - "pmma_intrusion_mm": option_float(generator_argv, "--pmma_intrusion", DEFAULT_PMMA_INTRUSION_MM), - "meaning": "bbox-scaled low-y contact fixture with unsupported columns cropped", - }, - "distal_shaft": { - "node_set": "Distal_Femur_Nodes", - "support_surface": "central 90% straight patch on the post-ICP flat distal shaft crop face", - "relative_to": "model_grid", - "support_fraction": POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, - "normal_source": "model-grid distal z direction", - }, - }, - "constraints": [ - { - "name": "top_displacement", - "node_set": "Femoral_Head_PMMA_Nodes", - "axis": "y", - **femur_displacement, - "meaning": "femoral head PMMA cap prescribed toward greater trochanter", - }, - { - "name": "bottom_fixed_y_PMMA", - "node_set": "Greater_Trochanter_PMMA_Nodes", - "axis": "y", - "value_mm": 0.0, - "meaning": "greater trochanter PMMA cap constrained in loading direction", - }, - { - "name": "bottom_fixed_x", - "node_set": "Distal_Femur_Nodes", - "axis": "x", - "value_mm": 0.0, - "meaning": "distal shaft rigid-body constraint", - }, - { - "name": "bottom_fixed_z", - "node_set": "Distal_Femur_Nodes", - "axis": "z", - "value_mm": 0.0, - "meaning": "distal shaft rigid-body constraint", - }, - ], - }, - }) - - if generation_settings: - metadata["generation_settings"] = generation_settings - output_path.write_text(json.dumps(metadata, indent=2, sort_keys=True) + "\n") - print(f"Wrote {output_path}") - return output_path + return write_record(model_path, model_type, generator_argv, args, + bc_audit_summary, reporting=reporting) def audit_generated_model(model_path: Path, model_type: str, args: argparse.Namespace) -> Optional[dict]: @@ -939,29 +398,25 @@ def audit_generated_model(model_path: Path, model_type: str, args: argparse.Name return result["summary"] -def _call_cli(main_func: Callable[[], None], program: str, argv: Sequence[str]) -> None: - old_argv = sys.argv +def _call_cli(main_func: Callable, argv: Sequence[str]) -> None: + """Call an anatomy builder without changing process-global arguments.""" try: - sys.argv = [program, *argv] - try: - main_func() - except SystemExit as exc: - if exc.code not in (None, 0): - raise - finally: - sys.argv = old_argv + main_func(list(argv)) + except SystemExit as exc: + if exc.code not in (None, 0): + raise def run_spine_command(argv: Sequence[str]) -> None: from ogo.fea.spine import main as spine_main - _call_cli(spine_main, "ogoFEA-spine-builder", argv) + _call_cli(spine_main, argv) def run_femur_command(argv: Sequence[str]) -> None: from ogo.fea.femur import main as femur_main - _call_cli(femur_main, "ogoFEA-hip-builder", argv) + _call_cli(femur_main, argv) def print_dry_run(program: str, argv: Sequence[str]) -> None: diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 5b5d762..726a04e 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -148,3 +148,17 @@ Failures before meshing require job logs for participant accounting. For full-size resegmentation of reviewed failures, see the [cohort recovery example](../../examples/fea/cohort_recovery/README.md). + +## Maintenance + +Anatomy-specific builders are in `femur.py` and `spine.py`; `metadata.py` records +their parsed settings. `qc_render.py` prepares geometry once per three-view +preview, and `validation.py` applies the QC policy without changing FE results. +Shared image helpers remain in `ogo.util.Helper` and `ogo.util.vtk_image`; +rendering uses `ogo.cli.Visualize`, and reference surfaces ship in `ogo/dat`. + +Run the FE and shared-helper regression tests after changing these dependencies: + +```bash +python -m pytest tests/fea tests/cli/test_GenerateFEM.py tests/util/test_Helper.py +``` diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index e168566..c655f60 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -1946,8 +1946,8 @@ def sidewaysFallFe(args): sys.exit() -def main(): - """Parse anatomy-builder arguments and generate a sideways-fall femur model.""" +def build_parser(): + """Define the femur builder's arguments and defaults.""" description = """Build a sideways-fall femur model from calibrated K2HPO4 density\n and a femur label mask. Estimate ICP from a proximal crop, then retain a\n complete flat shaft measured from the distal GT support edge. Use ogoFEA\n hip to also solve and report results.""" @@ -2034,8 +2034,12 @@ def main(): help="Sets the yield strength in compression for PMMA material in the FE model. (default: %(default)s [MPa])") parser.add_argument("--pmma_yield_tension", type=float, default=None, help="Sets the yield strength in tension for PMMA material in the FE model. (default: %(default)s [MPa])") - # Parse and display - args = parser.parse_args() + return parser + + +def main(argv=None): + """Generate a femur model from explicit arguments or the command line.""" + args = build_parser().parse_args(argv) # Set default reference paths if args.reference_path is None: diff --git a/ogo/fea/metadata.py b/ogo/fea/metadata.py new file mode 100644 index 0000000..c9143a6 --- /dev/null +++ b/ogo/fea/metadata.py @@ -0,0 +1,575 @@ +"""Model construction records using resolved anatomy-builder settings.""" + +import argparse +import json +from pathlib import Path +from typing import Optional, Sequence + +from ogo.fea.spine import ( + DEFAULT_SPINE_ISO_RESOLUTION_MM, + DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + DEFAULT_SPINE_PMMA_INTRUSION_MM, + DEFAULT_SPINE_PMMA_THICKNESS_MM, + DEFAULT_SPINE_REGISTRATION_MAX_SCALE, + DEFAULT_SPINE_REGISTRATION_MIN_SCALE, + DEFAULT_SPINE_REGISTRATION_BACKEND, + DEFAULT_SPINE_REGISTRATION_ITERATIONS, + DEFAULT_SPINE_REGISTRATION_LANDMARKS, + DEFAULT_SPINE_ICP_TARGET, + SPINE_ALIGNMENT_METHOD, + default_spine_reference_path, + generation_settings as spine_generation_settings, + solve_report_profile as spine_solve_report_profile, +) +from ogo.fea.femur import ( + DEFAULT_FEMUR_CUT_MODE, + DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, + DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, + DEFAULT_FEMUR_ISO_RESOLUTION_MM, + DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + DEFAULT_FEMUR_REGISTRATION_BACKEND, + DEFAULT_FEMUR_REGISTRATION_ITERATIONS, + DEFAULT_FEMUR_REGISTRATION_LANDMARKS, + DEFAULT_PMMA_INTRUSION_MM, + DEFAULT_PMMA_THICKNESS_MM, + POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, + FEMORAL_HEAD_FIXTURE_CENTER_FRACTION, + GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION, + SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION, + solve_report_profile as femur_solve_report_profile, +) + +def option_value(options, name, default=None): + """Read an already-parsed builder setting, with material fallback support.""" + value = options.get(name.lstrip("-")) + return default if value is None else value + + +def option_float(options, name, default): + return float(option_value(options, name, default)) + + +def option_int(options, name, default): + return int(option_value(options, name, default)) + + +def option_optional_float(options, name): + value = option_value(options, name) + return None if value is None else float(value) + + +def option_path(options, name): + return option_value(options, name) + + +def percent_displacement_metadata( + model_path: Path, + *, + report_profile: str, + failure_axis: str, + target_percent: float, +) -> dict: + """Describe the solve displacement derived from model geometry.""" + metadata = { + "value_mm": None, + "target_displacement_percent": target_percent, + "characteristic_length_mm": None, + "value_source": "target_displacement_percent * characteristic_length_mm / 100", + } + try: + from ogo.util.faim import ( + infer_profile_characteristic_length_mm, + read_prescribed_displacement, + ) + + characteristic_length = infer_profile_characteristic_length_mm( + model_path, + report_profile, + failure_axis, + ) + current_value = read_prescribed_displacement(model_path, report_profile) + sign = -1.0 if current_value != "" and float(current_value) < 0 else 1.0 + target_mm = abs(float(target_percent)) * characteristic_length / 100.0 + metadata["value_mm"] = sign * target_mm + metadata["characteristic_length_mm"] = characteristic_length + except Exception as exc: + metadata["value_note"] = "available after model generation with netCDF4: {}".format(exc) + return metadata + + +def absolute_displacement_metadata(options: dict, default_value: str) -> dict: + """Describe a solve displacement kept as an explicit absolute mm value.""" + value = option_value(options, "--fe_displacement", default_value) + try: + value_mm = float(value) + except (TypeError, ValueError): + value_mm = value + return { + "value_mm": value_mm, + "target_displacement_percent": None, + "characteristic_length_mm": None, + "value_source": "explicit --fe_displacement used as absolute model displacement", + } + + + +def write_modeling_metadata( + model_path: Path, + model_type: str, + generator_argv: Sequence[str], + args: argparse.Namespace, + bc_audit_summary: Optional[dict] = None, + *, reporting: dict, +) -> Optional[Path]: + """Write a traceable model-building record next to a generated n88model.""" + if args.dry_run: + return None + if not model_path.exists(): + return None + + output_path = model_path.with_name(model_path.with_suffix("").name + "_modeling.json") + generation_settings = {} + if model_type == "spine": + generation_settings = spine_generation_settings() + if output_path.exists(): + generation_settings = json.loads(output_path.read_text()).get( + "generation_settings", generation_settings + ) + mask_source = args.pistoia_mask or (args.bone_mask if args.pistoia_mask_label else None) + mask_output = model_path.with_name(model_path.stem + "_pistoia_mask.nii.gz") + from ogo.fea import femur, spine + builder = spine if model_type == "spine" else femur + options = vars(builder.build_parser().parse_args(list(generator_argv))) + common = { + "schema_version": 1, + "model_file": str(model_path), + "generator": { + "entry_point": "ogoFEA", + "model_type": model_type, + "lower_level_argv": list(generator_argv), + }, + "inputs": { + "calibrated_image": str(args.calibrated_image), + "bone_mask": str(args.bone_mask), + "output_path": str(args.output_path) if args.output_path is not None else str(args.calibrated_image.parent), + }, + "geometry": { + "model_coordinates": "preprocessed_image_physical_space", + "origin_policy": "cropping and resampling update the image origin; FE meshing preserves that origin", + "boundary_condition_coordinates": "defined from the generated model bounding box in the same physical space", + }, + "post_generation_validation": { + "bc_audit": { + "enabled": bc_audit_summary is not None, + "flat_tolerance": args.bc_audit_flat_tolerance, + "summary": bc_audit_summary, + "json_path": None, + "csv_path": None, + "png_path": None + if not args.debug + else str(model_path.with_name(model_path.with_suffix("").name + "_bc_audit.png")), + } + }, + "solve_and_reporting": { + "solve_requested": not args.no_solve, + "critical_volume_pct": reporting["critical_volume_pct"], + "masked_critical_volume_pct": ( + reporting["critical_volume_pct"] + if getattr(args, "masked_critical_volume", None) is None + else args.masked_critical_volume + ), + "critical_strain": reporting["critical_strain"], + "target_displacement_percent": reporting["target_displacement_percent"], + "target_displacement_definition": "percent strain converted from model geometry for spine; percent of femur length for femur", + "run_pistoia": args.run_pistoia or args.require_pistoia or mask_source is not None, + "source_pistoia_mask": None + if mask_source is None + else str(mask_source), + "pistoia_mask_labels": list(args.pistoia_mask_label or []), + "model_space_pistoia_mask": None + if mask_source is None + else str(mask_output), + "compress_solved_model": not args.no_compress, + }, + } + + + build = _spine_metadata if model_type == "spine" else _femur_metadata + metadata = build(common, model_path, options, args, generation_settings) + metadata["generation_arguments"] = options + if generation_settings: + metadata["generation_settings"] = generation_settings + output_path.write_text(json.dumps(metadata, indent=2, sort_keys=True) + "\n") + print(f"Wrote {output_path}") + return output_path + + +def material_metadata(options, site, *, include_cortical): + """Record regional material laws and the shared PMMA settings.""" + trabecular = { + "elastic_E_func": option_value(options, "--elastic_E_func", "default_E"), + "yield_comp_func": option_value(options, "--yield_comp_func"), + "yield_tens_func": option_value(options, "--yield_tens_func"), + "material_id_range": [1, 128], + } + cortical = None + if include_cortical: + cortical = { + key: option_value(options, "--cort_" + key, trabecular[key]) + for key in ("elastic_E_func", "yield_comp_func", "yield_tens_func") + } + cortical["poissons_ratio"] = option_float( + options, "--cort_poissons_ratio", option_float(options, "--poissons_ratio", 0.3) + ) + cortical["material_id_range"] = [129, 256] + record = { + "builder": "ogo.fea.materials.build_" + site + "_material_table", + "convention": ( + "region material IDs; 0 background, 1..128 trabecular, " + + ("optional " if site == "femur" else "") + + "129..256 cortical, PMMA explicit" + ), + "poissons_ratio": option_float(options, "--poissons_ratio", 0.3), + "trabecular": trabecular, + "cortical": cortical, + "pmma": { + "material_id": option_int(options, "--pmma_mat_id", 5000), + "elastic_E_MPa": option_float(options, "--pmma_E", 2500), + "poissons_ratio": option_float(options, "--pmma_v", 0.3), + "yield_compression_MPa": option_optional_float(options, "--pmma_yield_compression"), + "yield_tension_MPa": option_optional_float(options, "--pmma_yield_tension"), + }, + } + if site == "femur": + record["include_cortical_region"] = include_cortical + return record + + +def _spine_metadata(common, model_path, options, args, generation_settings): + body_label = option_int(options, "--mask_threshold", 0) + process_label = option_int(options, "--process_mask_threshold", 0) + appendix = option_value(options, "--appendix") + spine_icp_target = option_value( + options, + "--spine_icp_target", + DEFAULT_SPINE_ICP_TARGET, + ) + spine_reference_path = option_value( + options, + "--reference_path", + str(default_spine_reference_path(spine_icp_target)), + ) + if args.use_absolute_fe_displacement: + spine_displacement = absolute_displacement_metadata( + options, + str(spine_solve_report_profile(preset=getattr(args, "preset", None))["default_applied_displacement"]), + ) + else: + spine_displacement = percent_displacement_metadata( + model_path, + report_profile="spine", + failure_axis="z", + target_percent=common["solve_and_reporting"]["target_displacement_percent"], + ) + metadata = common.copy() + metadata.update({ + "model": "spine-compression", + "target": { + "vertebra": appendix, + "body_label": body_label, + "process_label": process_label, + }, + "alignment": { + "method": SPINE_ALIGNMENT_METHOD, + "registration_target": spine_icp_target, + "reference_path": spine_reference_path, + "registration_scale": option_value(options, "--registration_scale", "auto"), + "registration_min_scale": option_value( + options, + "--registration_min_scale", + DEFAULT_SPINE_REGISTRATION_MIN_SCALE, + ), + "registration_max_scale": option_value( + options, + "--registration_max_scale", + DEFAULT_SPINE_REGISTRATION_MAX_SCALE, + ), + "registration_backend": option_value( + options, + "--registration_backend", + DEFAULT_SPINE_REGISTRATION_BACKEND, + ), + "registration_landmarks": option_int( + options, + "--registration_landmarks", + DEFAULT_SPINE_REGISTRATION_LANDMARKS, + ), + "registration_iterations": option_int( + options, + "--registration_iterations", + DEFAULT_SPINE_REGISTRATION_ITERATIONS, + ), + }, + "image_processing": { + "iso_resolution_mm": option_float(options, "--iso_resolution", DEFAULT_SPINE_ISO_RESOLUTION_MM), + "registration_label_smoothing": generation_settings["registration_label_smoothing"], + "spatial_operations": "ICP transform and isotropic output spacing in one shared VTK reslice", + "image_interpolation": "cubic", + "label_interpolation": "nearest-neighbor", + "mask_smoothing": { + "operation": "binary close/open after ICP resampling", + "condition": "enabled only when any input spacing dimension exceeds threshold_mm", + "threshold_mm": option_float( + options, + "--mask_smoothing_spacing_threshold", + DEFAULT_SPINE_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + ), + }, + "density_preprocessing": { + "connectivity_filter": True, + "bmd_preprocess_threshold": -31, + "density_binning": { + "n_bins": 128, + "background_material_id": 0, + "trabecular_material_ids": [1, 128], + "cortical_material_ids": [129, 256], + }, + }, + }, + "segmentation": { + "input_mask": str(args.bone_mask), + "labels": { + "vertebral_body": body_label, + "posterior_process": process_label, + }, + "derived_masks": { + "body": "threshold body label", + "process": "threshold process label", + "cortical": "density/surface-derived cortical shell generated after alignment", + "pmma_caps": "fixed-thickness anatomy superior and inferior caps", + }, + }, + "materials": material_metadata(options, "spine", include_cortical=True), + "boundary_conditions": { + "stable_contact": generation_settings["stable_contact"], + "fixture_geometry": { + "superior_cap": { + "label_id": option_int(options, "--top_node_set_id", 4), + "node_set": "body_top", + "shape": "fixed-thickness anatomy cap", + "pmma_thickness_mm": option_float( + options, "--pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM + ), + "pmma_intrusion_mm": option_float( + options, "--pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM + ), + "meaning": ( + "fixed-thickness anatomy cap: pmma_thickness_mm is total cap thickness; " + "pmma_intrusion_mm controls how far anatomy can occupy that fixed " + "thickness without overwriting body bone" + ), + }, + "inferior_cap": { + "label_id": option_int(options, "--bottom_node_set_id", 3), + "node_set": "body_bottom", + "shape": "fixed-thickness anatomy cap", + "pmma_thickness_mm": option_float( + options, "--pmma_thick", DEFAULT_SPINE_PMMA_THICKNESS_MM + ), + "pmma_intrusion_mm": option_float( + options, "--pmma_intrusion", DEFAULT_SPINE_PMMA_INTRUSION_MM + ), + "meaning": ( + "fixed-thickness anatomy cap: pmma_thickness_mm is total cap thickness; " + "pmma_intrusion_mm controls how far anatomy can occupy that fixed " + "thickness without overwriting body bone" + ), + }, + }, + "constraints": [ + { + "name": "top_displacement", + "node_set": "body_top", + "axis": "z", + **spine_displacement, + "meaning": "superior PMMA cap prescribed toward inferior cap", + }, + { + "name": "bottom_fixed", + "node_set": "body_bottom", + "axes": ["x", "y", "z"], + "value_mm": 0.0, + "meaning": "inferior PMMA cap fixed in all displacement directions", + }, + ], + }, + }) + return metadata + + +def _femur_metadata(common, model_path, options, args, generation_settings): + femur_side = str(option_value(options, "--femur_side", "1")) + side = "left" if femur_side == "1" else "right" if femur_side == "2" else "unknown" + compartment_mask = option_path(options, "--compartment_mask") + if args.use_absolute_fe_displacement: + femur_displacement = absolute_displacement_metadata( + options, + str(femur_solve_report_profile()["default_applied_displacement"]), + ) + else: + femur_displacement = percent_displacement_metadata( + model_path, + report_profile="femur", + failure_axis="y", + target_percent=common["solve_and_reporting"]["target_displacement_percent"], + ) + metadata = common.copy() + metadata.update({ + "model": "femur-sideways", + "target": { + "side": side, + "femur_side_code": int(femur_side) if femur_side.isdigit() else femur_side, + "mask_threshold": option_int(options, "--mask_threshold", 1), + }, + "alignment": { + "method": "ICP to side-specific femur reference from cropped/padded input geometry", + "reference_path": option_value(options, "--reference_path", "bundled side-specific femur reference"), + "registration_backend": DEFAULT_FEMUR_REGISTRATION_BACKEND, + "registration_landmarks": DEFAULT_FEMUR_REGISTRATION_LANDMARKS, + "registration_iterations": DEFAULT_FEMUR_REGISTRATION_ITERATIONS, + }, + "image_processing": { + "iso_resolution_mm": option_float(options, "--iso_resolution", DEFAULT_FEMUR_ISO_RESOLUTION_MM), + "spatial_operations": ( + "ICP transform and isotropic output spacing in one shared VTK reslice" + ), + "image_interpolation": "cubic", + "label_interpolation": "nearest-neighbor", + "mask_smoothing": { + "operation": "binary close/open after ICP resampling", + "condition": "enabled only when any input spacing dimension exceeds threshold_mm", + "threshold_mm": option_float( + options, + "--mask_smoothing_spacing_threshold", + DEFAULT_FEMUR_MASK_SMOOTHING_SPACING_THRESHOLD_MM, + ), + "applies_to": [ + "whole-femur mask", + "derived cortical binary mask when compartment_mask is supplied", + ], + }, + "density_preprocessing": { + "connectivity_filter": True, + "bmd_preprocess_threshold": -31, + "density_binning": { + "n_bins": 128, + "background_material_id": 0, + "trabecular_material_ids": [1, 128], + "cortical_material_ids": [129, 256] if compartment_mask is not None else None, + }, + }, + }, + "segmentation": { + "input_mask": str(args.bone_mask), + "whole_bone_mask_threshold": option_int(options, "--mask_threshold", 1), + "compartment_mask": compartment_mask, + "compartment_labels": None + if compartment_mask is None + else { + "cortical": option_int(options, "--cortical_label", 1), + "trabecular": option_int(options, "--trabecular_label", 2), + }, + }, + "shaft_standardization": { + "cut_mode": DEFAULT_FEMUR_CUT_MODE, + "crop_stage": "fixed rough crop before ICP for registration, final GT-disk-relative flat crop after ICP on the full scan", + "coverage_definition": "GT disk distal voxel face minus safe flat face clearing the full transformed native scan end", + "geometry_sidecar": str(model_path.with_name(model_path.stem + "_shaft_geometry.json")), + "rough_pre_icp_crop": { + "enabled": True, + "retained_length_mm": option_float(options, "--femur_shaft_length", 120.0), + "registration_only": True, + }, + "greater_trochanter_length": { + "retained_length_mm": option_float( + options, + "--femur_greater_trochanter_distal_length", + DEFAULT_FEMUR_GREATER_TROCHANTER_DISTAL_LENGTH_MM, + ), + "gt_inclusion_length_mm": option_float( + options, + "--femur_greater_trochanter_inclusion_length", + DEFAULT_FEMUR_GREATER_TROCHANTER_INCLUSION_LENGTH_MM, + ), + "length_origin": "detected greater-trochanter disk distal edge", + }, + "cut_plane": "flat post-ICP aligned-frame crop face at fixed shaft length below detected GT-disk distal edge", + "incomplete_fov_behavior": "fail model generation", + }, + "materials": material_metadata(options, "femur", include_cortical=compartment_mask is not None), + "boundary_conditions": { + "fixture_geometry": { + "femoral_head": { + "node_set": "Femoral_Head_PMMA_Nodes", + "shape": "rectangle fixture cap", + "relative_to": "model_bbox", + "center_fraction": list(FEMORAL_HEAD_FIXTURE_CENTER_FRACTION), + "size_fraction": list(SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION), + "projection_axis": "y", + "pmma_thickness_mm": option_float(options, "--pmma_thick", DEFAULT_PMMA_THICKNESS_MM), + "pmma_intrusion_mm": option_float(options, "--pmma_intrusion", DEFAULT_PMMA_INTRUSION_MM), + "meaning": "bbox-scaled high-y contact fixture with unsupported columns cropped", + }, + "greater_trochanter": { + "node_set": "Greater_Trochanter_PMMA_Nodes", + "shape": "rectangle fixture cap", + "relative_to": "model_bbox", + "center_fraction": list(GREATER_TROCHANTER_FIXTURE_CENTER_FRACTION), + "size_fraction": list(SIDEWAYS_FALL_FIXTURE_SIZE_FRACTION), + "projection_axis": "y", + "pmma_thickness_mm": option_float(options, "--pmma_thick", DEFAULT_PMMA_THICKNESS_MM), + "pmma_intrusion_mm": option_float(options, "--pmma_intrusion", DEFAULT_PMMA_INTRUSION_MM), + "meaning": "bbox-scaled low-y contact fixture with unsupported columns cropped", + }, + "distal_shaft": { + "node_set": "Distal_Femur_Nodes", + "support_surface": "central 90% straight patch on the post-ICP flat distal shaft crop face", + "relative_to": "model_grid", + "support_fraction": POST_ICP_DISTAL_SHAFT_SUPPORT_FRACTION, + "normal_source": "model-grid distal z direction", + }, + }, + "constraints": [ + { + "name": "top_displacement", + "node_set": "Femoral_Head_PMMA_Nodes", + "axis": "y", + **femur_displacement, + "meaning": "femoral head PMMA cap prescribed toward greater trochanter", + }, + { + "name": "bottom_fixed_y_PMMA", + "node_set": "Greater_Trochanter_PMMA_Nodes", + "axis": "y", + "value_mm": 0.0, + "meaning": "greater trochanter PMMA cap constrained in loading direction", + }, + { + "name": "bottom_fixed_x", + "node_set": "Distal_Femur_Nodes", + "axis": "x", + "value_mm": 0.0, + "meaning": "distal shaft rigid-body constraint", + }, + { + "name": "bottom_fixed_z", + "node_set": "Distal_Femur_Nodes", + "axis": "z", + "value_mm": 0.0, + "meaning": "distal shaft rigid-body constraint", + }, + ], + }, + }) + + return metadata diff --git a/ogo/fea/qc_render.py b/ogo/fea/qc_render.py index 700d8bb..75c610a 100644 --- a/ogo/fea/qc_render.py +++ b/ogo/fea/qc_render.py @@ -6,6 +6,7 @@ """ import argparse +from dataclasses import dataclass import json import os import warnings @@ -44,10 +45,24 @@ def smooth_bone_sed(values, indices, bone, spacing): return numerator[locations] / denominator[locations] -def preview(model_path, output_dir, site, body_mask=None, view="oblique", sed=False, - model_override=None, ineligible=False): - """Render one model view with anatomy or SED colours and coloured supports.""" - start = perf_counter() +@dataclass +class PreparedQC: + """Geometry and display-only SED shared by the three rendered views.""" + + model: object + labels: np.ndarray + volume: np.ndarray + affine: np.ndarray + bone_tree: object + display_sed: object + lookup: object + sed_max: object + + +def prepare_model_qc(model_path, site, body_mask=None, sed=False, + model_override=None, ineligible=False): + """Load and prepare geometry and display values once for all camera views.""" + model_path = Path(model_path) if model_override is None: reader = vtkbone.vtkboneN88ModelReader() reader.SetFileName(str(model_path)) @@ -72,6 +87,7 @@ def preview(model_path, output_dir, site, body_mask=None, view="oblique", sed=Fa materials = vtk_to_numpy(model.GetCellData().GetScalars()) disk = materials == 5000 sed_values = None + bone_tree = display_sed = lookup = sed_max = None if sed: from scipy.spatial import cKDTree from matplotlib import colormaps @@ -80,24 +96,24 @@ def preview(model_path, output_dir, site, body_mask=None, view="oblique", sed=Fa if array is None: raise ValueError("SED rendering requires a solved model with StrainEnergyDensity") sed_values = vtk_to_numpy(array) - if view == "oblique": - from ogo.fea.validation import write_measurements + from ogo.fea.validation import write_measurements - finite = np.isfinite(sed_values) - write_measurements(model_path, { - "sed_available": True, - "sed_finite": bool(np.all(finite)), - "sed_nonnegative": bool(np.all(sed_values[finite] >= 0)), - "bone_sed_p99": float(np.percentile(sed_values[~disk & finite], 99)) - if np.any(~disk & finite) else None, - }) + finite = np.isfinite(sed_values) + write_measurements(model_path, { + "sed_available": True, + "sed_finite": bool(np.all(finite)), + "sed_nonnegative": bool(np.all(sed_values[finite] >= 0)), + "bone_sed_p99": float(np.percentile(sed_values[~disk & finite], 99)) + if np.any(~disk & finite) else None, + }) if not np.all(np.isfinite(sed_values)) or np.any(sed_values < 0): raise ValueError("Nonfinite or negative SED values") sed_max = float(np.percentile(sed_values[~disk], 99)) if sed_max <= 0: raise ValueError("No positive bone SED") display_sed = smooth_bone_sed(sed_values, indices, ~disk, spacing) - assert np.all(np.isfinite(display_sed)) + if not np.all(np.isfinite(display_sed)): + raise ValueError("Nonfinite smoothed display SED") bone_tree = cKDTree(centers[~disk]) lookup = vtk.vtkLookupTable() lookup.SetNumberOfTableValues(256) @@ -157,10 +173,24 @@ def preview(model_path, output_dir, site, body_mask=None, view="oblique", sed=Fa labels[~disk & (membership[connectivity].sum(axis=1) >= 4)] = 5 volume = np.zeros(tuple(indices.max(axis=0) + 1), dtype=np.uint8) volume[tuple(indices.T)] = labels - assert np.count_nonzero(volume) == model.GetNumberOfCells() + if np.count_nonzero(volume) != model.GetNumberOfCells(): + raise ValueError("Duplicate or missing FE voxel centres") affine = np.eye(4) affine[:3, :3] = np.diag(spacing) affine[:3, 3] = origin + return PreparedQC(model, labels, volume, affine, bone_tree, display_sed, lookup, sed_max) + + +def preview(model_path, output_dir, site, body_mask=None, view="oblique", sed=False, + model_override=None, ineligible=False, *, prepared_data=None): + """Render a camera view from prepared anatomy or solved SED.""" + start = perf_counter() + model_path, output_dir = Path(model_path), Path(output_dir) + data = prepared_data if prepared_data is not None else prepare_model_qc( + model_path, site, body_mask, sed, model_override, ineligible) + model, labels, volume, affine = data.model, data.labels, data.volume, data.affine + bone_tree, display_sed, lookup, sed_max = ( + data.bone_tree, data.display_sed, data.lookup, data.sed_max) output_dir.mkdir(parents=True, exist_ok=True) prefix = output_dir / model_path.stem nifti_path = Path(str(prefix) + "_debug_labels.nii.gz") @@ -298,10 +328,11 @@ def rotate_camera(caller, event): def panel(model_path, output_dir, site, body_mask=None, sed=False, model_override=None, ineligible=False): """Stack three opaque views, each independently framed for inspection.""" + prepared = prepare_model_qc(model_path, site, body_mask, sed, model_override, ineligible) rows = [] for view in ("oblique", "top", "bottom"): path = preview(model_path, output_dir, site, body_mask, view, sed, - model_override, ineligible) + model_override, ineligible, prepared_data=prepared) with Image.open(path) as source: rgb = source.convert("RGB") pixels = np.asarray(rgb) diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 4e2da02..35313f2 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -2037,8 +2037,8 @@ def process_vertebra( ###################################################################### MAIN -def main(): - """Parse anatomy-builder arguments and generate the requested vertebra model.""" +def build_parser(): + """Define the spine builder's arguments and defaults.""" description = """Build a vertebral compression model from calibrated K2HPO4 density\n and body/process labels. Resample, register, place PMMA supports and write\n an n88model. Use ogoFEA spine to also solve and report results.""" parser = argparse.ArgumentParser( @@ -2138,7 +2138,12 @@ def main(): parser.add_argument("--export_nifti", action='store_true', help="If set, exports resampled grayscale image and labeled segmentation.") - args = parser.parse_args() + return parser + + +def main(argv=None): + """Generate a spine model from explicit arguments or the command line.""" + args = build_parser().parse_args(argv) # Print arguments ogo.message(echo_arguments("ogoFEA-spine-builder", vars(args))) diff --git a/ogo/fea/validation.py b/ogo/fea/validation.py index 64c586c..97c8731 100644 --- a/ogo/fea/validation.py +++ b/ogo/fea/validation.py @@ -214,6 +214,16 @@ def write_measurements(model_path, measurements): return path +QC_POLICY_VERSION = 1 +SHAFT_LENGTH_TOLERANCE_MM = 0.01 +BOUNDARY_PLANARITY_TOLERANCE_MM = 1e-4 +COVERAGE_ROUNDING_TOLERANCE = 1e-6 +LEGACY_DISTAL_FACE_MINIMUM_COVERAGE = 0.89 +BODY_CLEANUP_REVIEW_FRACTION = 0.05 +PROCESS_AXIAL_OFFSET_RATIO = 0.75 +PROCESS_AXIAL_OFFSET_MARGIN_MM = 2.0 + + def evaluate(measurements): """Apply versioned construction tests. Missing evidence requires review.""" row = dict(measurements) @@ -258,38 +268,7 @@ def number(key): elif not truth(row['loading_axis_matches']): failures.append('incorrect_loading_axis') if row.get('site') == 'hip' and 'too_short' not in failures: - measured, requested = number('measured_shaft_length_mm'), number('requested_shaft_length_mm') - if measured is None or requested is None: - review.append('missing_shaft_length') - elif abs(measured - requested) > 0.01: - failures.append('shaft_length_mismatch') - if 'complete_distal_section_verified' not in row: - review.append('missing_distal_section_check') - elif not truth(row['complete_distal_section_verified']): - failures.append('incomplete_distal_section') - span = number('distal_boundary_span_mm') - if span is None: - review.append('missing_distal_planarity') - elif span > 1e-4: - failures.append('nonplanar_distal_boundary') - coverage = number('distal_support_patch_coverage_fraction') - minimum = 1 - 1e-6 - if coverage is None: - # Older CSVs only describe the full face. Allow the established - # inset patch without requiring model reopening for gallery export. - coverage = number('distal_boundary_coverage_fraction') - minimum = 0.89 - if coverage is None: - review.append('missing_distal_boundary_coverage') - elif coverage < minimum: - failures.append('incomplete_distal_boundary') - shaft_nodes = number('shaft_bc_node_count') - for direction in ('x', 'z'): - fixed_nodes = number('shaft_bc_fixed_' + direction + '_node_count') - if shaft_nodes is None or fixed_nodes is None: - review.append('missing_shaft_fixation_' + direction) - elif fixed_nodes != shaft_nodes: - failures.append('incomplete_shaft_fixation_' + direction) + _hip_checks(row, number, truth, failures, review) for key in row: if key.endswith('_contact_area_mm2') and number(key) == 0: failures.append('no_axial_contact_' + key.split('_contact')[0]) @@ -303,25 +282,68 @@ def number(key): if key in row and not truth(row[key]): failures.append('invalid_' + key) transverse = number('process_body_transverse_offset_mm') - if axial is not None and transverse is not None and axial > 0.75 * transverse + 2: + if (axial is not None and transverse is not None + and axial > PROCESS_AXIAL_OFFSET_RATIO * transverse + PROCESS_AXIAL_OFFSET_MARGIN_MM): review.append('process_orientation') if row.get('site') == 'spine': - # Review triage only: substantial cleanup can indicate a second vertebra - # or a disconnected anatomical region, not necessarily a failed model. - removed = number('body_cleanup_removed_fraction') - if removed is not None and removed > 0.05: - review.append('substantial_body_cleanup') - for name in ('body_top', 'body_bottom'): - span = number(name + '_span_mm') - if span is None: - review.append('missing_' + name + '_planarity') - elif span > 1e-4: - failures.append('nonplanar_' + name) + _spine_checks(row, number, failures, review) row.update(qc_status='fail' if failures else 'review' if review else 'pass', - qc_reasons=';'.join(failures + review), qc_policy_version=1) + qc_reasons=';'.join(failures + review), qc_policy_version=QC_POLICY_VERSION) return row + +def _hip_checks(row, number, truth, failures, review): + """Check shaft length, the flat distal face and its fixation.""" + measured, requested = number('measured_shaft_length_mm'), number('requested_shaft_length_mm') + if measured is None or requested is None: + review.append('missing_shaft_length') + elif abs(measured - requested) > SHAFT_LENGTH_TOLERANCE_MM: + failures.append('shaft_length_mismatch') + if 'complete_distal_section_verified' not in row: + review.append('missing_distal_section_check') + elif not truth(row['complete_distal_section_verified']): + failures.append('incomplete_distal_section') + span = number('distal_boundary_span_mm') + if span is None: + review.append('missing_distal_planarity') + elif span > BOUNDARY_PLANARITY_TOLERANCE_MM: + failures.append('nonplanar_distal_boundary') + coverage = number('distal_support_patch_coverage_fraction') + minimum = 1 - COVERAGE_ROUNDING_TOLERANCE + if coverage is None: + # Older CSVs only describe the full face. Allow the established + # inset patch without requiring model reopening for gallery export. + coverage = number('distal_boundary_coverage_fraction') + minimum = LEGACY_DISTAL_FACE_MINIMUM_COVERAGE + if coverage is None: + review.append('missing_distal_boundary_coverage') + elif coverage < minimum: + failures.append('incomplete_distal_boundary') + shaft_nodes = number('shaft_bc_node_count') + for direction in ('x', 'z'): + fixed_nodes = number('shaft_bc_fixed_' + direction + '_node_count') + if shaft_nodes is None or fixed_nodes is None: + review.append('missing_shaft_fixation_' + direction) + elif fixed_nodes != shaft_nodes: + failures.append('incomplete_shaft_fixation_' + direction) + + +def _spine_checks(row, number, failures, review): + """Check support planarity and triage substantial body cleanup.""" + # Review triage only: substantial cleanup can indicate a second vertebra + # or a disconnected anatomical region, not necessarily a failed model. + removed = number('body_cleanup_removed_fraction') + if removed is not None and removed > BODY_CLEANUP_REVIEW_FRACTION: + review.append('substantial_body_cleanup') + for name in ('body_top', 'body_bottom'): + span = number(name + '_span_mm') + if span is None: + review.append('missing_' + name + '_planarity') + elif span > BOUNDARY_PLANARITY_TOLERANCE_MM: + failures.append('nonplanar_' + name) + + def generate_gallery(rows, output, decisions=None): """Build the standalone review gallery without opening any FE model.""" from ogo.fea.gallery import generate_gallery as render diff --git a/tests/cli/test_GenerateFEM.py b/tests/cli/test_GenerateFEM.py index 5a0de4b..6788423 100644 --- a/tests/cli/test_GenerateFEM.py +++ b/tests/cli/test_GenerateFEM.py @@ -868,7 +868,8 @@ def test_spine_metadata_preserves_resolved_generation_settings(tmp_path): sidecar = model.with_name(model.stem + "_modeling.json") sidecar.write_text(json.dumps({"generation_settings": settings})) path = GenerateFEM.write_modeling_metadata( - model, "spine", ["density.nii.gz", "mask.nii.gz"], _metadata_args(tmp_path) + model, "spine", ["density.nii.gz", "mask.nii.gz", "--mask_threshold", "2", + "--process_mask_threshold", "3"], _metadata_args(tmp_path) ) data = json.loads(path.read_text()) assert data["image_processing"]["registration_label_smoothing"] == settings["registration_label_smoothing"] @@ -985,3 +986,31 @@ def test_femur_modeling_metadata_records_absolute_displacement(tmp_path): assert constraint["value_mm"] == -2.56 assert constraint["target_displacement_percent"] is None assert constraint["value_source"] == "explicit --fe_displacement used as absolute model displacement" + + +def test_builder_call_does_not_replace_sys_argv(): + import sys + + original = sys.argv + received = [] + + def builder(argv): + assert sys.argv is original + received.extend(argv) + + GenerateFEM._call_cli(builder, ["density", "mask"]) + assert received == ["density", "mask"] + + +def test_metadata_uses_builder_parser_defaults(tmp_path, monkeypatch): + from ogo.fea import femur + + parser = femur.build_parser() + parser.set_defaults(pmma_E=3100) + monkeypatch.setattr(femur, "build_parser", lambda: parser) + model = tmp_path / "density_LF.n88model" + model.write_text("dummy") + path = GenerateFEM.write_modeling_metadata( + model, "hip", ["density.nii.gz", "mask.nii.gz"], _metadata_args(tmp_path) + ) + assert json.loads(path.read_text())["materials"]["pmma"]["elastic_E_MPa"] == 3100 diff --git a/tests/fea/test_module_layout.py b/tests/fea/test_module_layout.py index 8d4e009..fcf9a48 100644 --- a/tests/fea/test_module_layout.py +++ b/tests/fea/test_module_layout.py @@ -34,3 +34,21 @@ def test_femur_exposes_only_current_cropping_helpers(): "detect_lesser_trochanter_cut_z", ): assert not hasattr(femur, name), name + + +def test_fea_shared_dependencies_are_available(): + import inspect + from ogo.util import Helper + from ogo.cli.Visualize import vis3d + from ogo.fea import femur, spine + + for name in ("readNii", "readPolyData", "transformResample", + "prepareFiniteElementImage", "imageConnectivity", "bmd_preprocess", + "cast2short", "applyMaskByArray", "density2materialID", "maskThreshold"): + assert callable(getattr(Helper, name)), name + assert "interpolation" in inspect.signature(Helper.transformResample).parameters + assert {"renderer_setup", "label_palette"} <= set(inspect.signature(vis3d).parameters) + data = Path(femur.__file__).parents[1] / "dat" + assert (data / "LT_FEMUR_SIDEWAYS_FALL_REF.vtk").is_file() + for target in spine.SPINE_ICP_TARGETS: + assert spine.default_spine_reference_path(target).is_file() diff --git a/tests/fea/test_qc_render.py b/tests/fea/test_qc_render.py index 7c2149e..c611227 100644 --- a/tests/fea/test_qc_render.py +++ b/tests/fea/test_qc_render.py @@ -35,6 +35,31 @@ def test_qc_uses_existing_visualizer_extension(): assert "label_palette" in signature.parameters +def test_panel_prepares_geometry_once_for_all_views(tmp_path, monkeypatch): + from PIL import Image + from ogo.fea import qc_render + + prepared = object() + calls = [] + + def prepare(*args): + calls.append("prepare") + return prepared + + def render(model_path, output_dir, site, body_mask, view, sed, + model_override, ineligible, *, prepared_data): + assert prepared_data is prepared + calls.append(view) + path = output_dir / (view + ".png") + Image.new("RGB", (20, 20), "blue").save(path) + return path + + monkeypatch.setattr(qc_render, "prepare_model_qc", prepare) + monkeypatch.setattr(qc_render, "preview", render) + qc_render.panel(tmp_path / "case.n88model", tmp_path, "hip") + assert calls == ["prepare", "oblique", "top", "bottom"] + + def test_ineligible_qc_is_anatomy_only(tmp_path): from ogo.fea.qc_render import export_model_qc diff --git a/tests/fea/test_shared_contracts.py b/tests/fea/test_shared_contracts.py new file mode 100644 index 0000000..a3e90cb --- /dev/null +++ b/tests/fea/test_shared_contracts.py @@ -0,0 +1,82 @@ +"""Contracts between FE builders and shared image helpers.""" + +import numpy as np +import pytest +import vtk + +from ogo.util import Helper +from ogo.util.vtk_image import numpy_to_vtk_image, vtk_image_to_numpy + + +def image(values): + template = vtk.vtkImageData() + template.SetExtent(2, 3, -1, 0, 4, 5) + template.SetOrigin(10, -20, 30) + template.SetSpacing(0.7, 1.2, 2.0) + template.AllocateScalars(vtk.VTK_FLOAT, 1) + return numpy_to_vtk_image(np.asarray(values, dtype=np.float32), template) + + +@pytest.mark.parametrize("processing_order", [False, True]) +def test_array_roundtrip_preserves_physical_geometry(processing_order): + source = image(np.arange(8).reshape(2, 2, 2)) + source.SetDirectionMatrix(0, -1, 0, 1, 0, 0, 0, 0, 1) + values = vtk_image_to_numpy(source, processing_order=processing_order, copy=True) + output = numpy_to_vtk_image(values, source, processing_order=processing_order) + np.testing.assert_array_equal(vtk_image_to_numpy(output), vtk_image_to_numpy(source)) + assert output.GetExtent() == source.GetExtent() + assert output.GetOrigin() == source.GetOrigin() + assert output.GetSpacing() == source.GetSpacing() + assert output.GetBounds() == source.GetBounds() + for i in range(3): + for j in range(3): + assert output.GetDirectionMatrix().GetElement(i, j) == source.GetDirectionMatrix().GetElement(i, j) + + +def test_material_binning_preserves_background_and_geometry(): + source = image([0, 100, 200, 300, 400, 0, 100, 400]) + output, centers = Helper.density2materialID(source, n_bins=2) + np.testing.assert_array_equal(vtk_image_to_numpy(output).ravel(order="F"), + [0, 1, 1, 2, 2, 0, 1, 2]) + np.testing.assert_allclose(centers, [175, 325]) + assert output.GetBounds() == source.GetBounds() + assert output.GetExtent() == source.GetExtent() + with pytest.raises(ValueError, match="empty"): + Helper.density2materialID(image(np.zeros(8))) + + +def test_nearest_resampling_keeps_label_ids_and_world_bounds(): + source = image([0, 20, 48, 20, 48, 0, 20, 48]) + transform = vtk.vtkMatrix4x4() + transform.Identity() + output = Helper.labelTransformResample(source, transform, 0.5) + assert output.GetSpacing() == (0.5, 0.5, 0.5) + assert set(np.unique(vtk_image_to_numpy(output))) <= {0, 20, 48} + np.testing.assert_allclose(output.GetBounds(), source.GetBounds(), atol=0.5) + + +def test_bundled_registration_surfaces_are_nonempty(): + from pathlib import Path + from ogo.fea import femur, spine + + paths = [Path(femur.__file__).parents[1] / "dat" / "LT_FEMUR_SIDEWAYS_FALL_REF.vtk"] + paths.extend(spine.default_spine_reference_path(target) for target in spine.SPINE_ICP_TARGETS) + for path in paths: + surface = Helper.readPolyData(str(path)) + assert surface.GetNumberOfPoints() > 250, path + assert surface.GetNumberOfCells() > 0, path + assert np.isfinite(surface.GetBounds()).all(), path + + +def test_density_resampling_defaults_to_cubic_not_nearest(): + template = vtk.vtkImageData() + template.SetDimensions(8, 8, 8) + template.AllocateScalars(vtk.VTK_FLOAT, 1) + source = numpy_to_vtk_image(np.indices((8, 8, 8)).sum(axis=0).astype(np.float32), template) + transform = vtk.vtkMatrix4x4() + transform.Identity() + default = Helper.transformResample(source, transform, 0.5) + cubic = Helper.transformResample(source, transform, 0.5, interpolation="cubic") + nearest = Helper.labelTransformResample(source, transform, 0.5) + np.testing.assert_array_equal(vtk_image_to_numpy(default), vtk_image_to_numpy(cubic)) + assert not np.array_equal(vtk_image_to_numpy(default), vtk_image_to_numpy(nearest)) From 16d90f0f5c59eb98cb17d3e47f4696c619a6afb8 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:00:20 -0600 Subject: [PATCH 62/67] Group alignment and QC modules and remove study-specific examples --- docs/fea/implementation.md | 13 +- examples/fea/cohort_recovery/README.md | 44 ---- .../build_manual_spine_gallery.py | 51 ---- .../fea/cohort_recovery/recover_spines.py | 222 ------------------ ogo/cli/ValidateFEA.py | 10 +- ogo/fea/README.md | 8 +- ogo/fea/alignment/__init__.py | 1 + ogo/fea/{alignment.py => alignment/core.py} | 2 +- .../{hip_registration.py => alignment/hip.py} | 2 +- ogo/fea/femur.py | 12 +- ogo/fea/qc/__init__.py | 1 + ogo/fea/{ => qc}/gallery.py | 4 +- ogo/fea/{ => qc}/gallery_review.js | 0 ogo/fea/{qc_images.py => qc/images.py} | 0 ogo/fea/{legacy_qc.py => qc/legacy.py} | 2 +- ogo/fea/{qc_render.py => qc/render.py} | 6 +- ogo/fea/{ => qc}/review.py | 0 ogo/fea/{ => qc}/validation.py | 4 +- ogo/fea/spine.py | 6 +- ogo/util/faim.py | 4 +- setup.cfg | 2 +- tests/fea/test_alignment.py | 12 +- tests/fea/test_cohort_recovery.py | 31 --- tests/fea/test_hip_registration.py | 6 +- tests/fea/test_legacy_evidence.py | 12 +- tests/fea/test_module_layout.py | 9 + tests/fea/test_qc_images.py | 10 +- tests/fea/test_qc_render.py | 14 +- tests/fea/test_review.py | 28 +-- tests/fea/test_spine_body_cleanup.py | 2 +- tests/fea/test_validation.py | 2 +- 31 files changed, 91 insertions(+), 429 deletions(-) delete mode 100644 examples/fea/cohort_recovery/README.md delete mode 100644 examples/fea/cohort_recovery/build_manual_spine_gallery.py delete mode 100644 examples/fea/cohort_recovery/recover_spines.py create mode 100644 ogo/fea/alignment/__init__.py rename ogo/fea/{alignment.py => alignment/core.py} (99%) rename ogo/fea/{hip_registration.py => alignment/hip.py} (98%) create mode 100644 ogo/fea/qc/__init__.py rename ogo/fea/{ => qc}/gallery.py (98%) rename ogo/fea/{ => qc}/gallery_review.js (100%) rename ogo/fea/{qc_images.py => qc/images.py} (100%) rename ogo/fea/{legacy_qc.py => qc/legacy.py} (99%) rename ogo/fea/{qc_render.py => qc/render.py} (99%) rename ogo/fea/{ => qc}/review.py (100%) rename ogo/fea/{ => qc}/validation.py (99%) delete mode 100644 tests/fea/test_cohort_recovery.py diff --git a/docs/fea/implementation.md b/docs/fea/implementation.md index 06dadf8..85e6ec6 100644 --- a/docs/fea/implementation.md +++ b/docs/fea/implementation.md @@ -9,17 +9,18 @@ audits the model, runs FAIM/N88 and writes modelling metadata. | Reporting endpoint and criteria | `GenerateFEM.solve_report_profile`, `critical_volume_percent`, `critical_strain` | | Spine preparation and model | `spine._prepare_spine_inputs`, `_register_spine_inputs`, `process_vertebra` | | Hip preparation and model | `femur._prepare_femur_inputs`, `_register_femur_inputs`, `sidewaysFallFe` | -| Hip multistart alignment | `hip_registration.estimate_femur_icp` | +| Hip multistart alignment | `alignment/hip.py: estimate_femur_icp` | | Physical scan-end/shaft measurements | `shaft_geometry.capture_distal_scan_face`, `measure_available_shaft`, `verify_model_shaft` | -| Shared transforms | `alignment.py`, `image_io.py` | +| Shared transforms | `alignment/`, `image_io.py` | | PMMA/contact geometry | `boundary.generate_bone_cap_mask`, `generate_projected_material_disk_vtk` | | Density laws and material tables | `material_laws.py`, `materials.build_bone_pmma_material_table` | | N88 mesh and file writing | `model.create_microfe_model`, `write_model` | | Solver discovery, Pistoia and results | `ogo/util/faim.py` | -| Generation measurements / QC rules | `validation.measure_model`, `evaluate` | -| Legacy measurement recovery | `legacy_qc.backfill_measurements` | -| Three-view previews | `qc_render.py`, using `ogo/cli/Visualize.py` | -| Gallery and review records | `gallery.py`, `review.py`, `gallery_review.js` | +| Model construction record | `metadata.py` | +| Generation measurements / QC rules | `qc/validation.py: measure_model`, `evaluate` | +| Legacy measurement recovery | `qc/legacy.py: backfill_measurements` | +| Three-view previews | `qc/render.py`, using `ogo/cli/Visualize.py` | +| Gallery and review records | `qc/gallery.py`, `qc/review.py`, `qc/gallery_review.js` | Anatomy defaults are constants at the top of `spine.py` and `femur.py`. Shared helpers own geometry/material logic; the CLI owns solving and reporting. diff --git a/examples/fea/cohort_recovery/README.md b/examples/fea/cohort_recovery/README.md deleted file mode 100644 index 06195f7..0000000 --- a/examples/fea/cohort_recovery/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# Spine Cohort Recovery - -This study-specific example resegments selected cases on full native HU CTs, -isolates L1 (vertebral-level label 20), and maps body 2 / process 1 into the -calibrated QCT's physical grid. Only then are inputs cropped with 15 mm padding. -It preserves original study outputs and builds a separate after-only review ZIP. -Successful solving is not evidence of anatomical validity: review the new models. - -Use a GPU compute node, never an HPC login node. Install Ogo and spine-segment -separately and use the validated segmentation model bundle for your study. - -```bash -python examples/fea/cohort_recovery/recover_spines.py \ - --root /scratch/study/spine_recovery \ - --review-csv /scratch/study/study_inclusion.csv \ - --selection manual-exclusions \ - --library /scratch/study/ct-library \ - --qct-dir /scratch/study/spine_QCT \ - --segment-cli /path/to/spine-segment \ - --model-bundle /path/to/model-bundle-pytorch \ - --faim-bin-dir /path/to/n88/bin \ - --gpu-workers 2 --fea-workers 4 -``` - -Use `--selection incomplete` for `incomplete_fea` cases not already manually -excluded. Use `--preflight` to check selected inputs without starting inference. -Native layout: `sub-ID/ses-1/ct/sub-ID_ses-1_ct.nii.gz`; QCT: `ID_QCT.nii.gz`; -review model ID: `ID_QCT_L1`. This example is not a generic BIDS discovery tool. - -Defaults reproduce the study recovery: four CPU threads per solve, full-bone -critical volume 12%, body critical volume 35%, critical strain 0.007. Registration -and supports use the checked-out Ogo defaults. The launch record saves the Ogo -commit and segmentation bundle manifest hash. Preserve the bundle and its source -release separately. Do not change code or bundle mid-batch. - -Per-case status and logs support resuming the same command. Use a new output root -when changing the selected cases or processing settings. Existing completed stages -are retained. Failures stay visible in `after_review.zip`; old manual decisions -are provenance only, not new inclusion decisions. `models/` contains n88models, -individual result and measurement CSVs, and before/after-solve WebP previews. - -Participant lists, trained weights, scans, and study results are not included in -this repository. Local delivery/download utilities are operational helpers rather -than part of the scientific model-generation pipeline. diff --git a/examples/fea/cohort_recovery/build_manual_spine_gallery.py b/examples/fea/cohort_recovery/build_manual_spine_gallery.py deleted file mode 100644 index e4f50db..0000000 --- a/examples/fea/cohort_recovery/build_manual_spine_gallery.py +++ /dev/null @@ -1,51 +0,0 @@ -"""Package selected rerun spine cases, with fresh manual-review decisions.""" -import csv -import json -from pathlib import Path -import sys - -from ogo.fea.gallery import export_gallery_zip -from ogo.fea.validation import evaluate - - -def main(root): - with (root / 'rerun_selection.csv').open() as stream: - selected = list(csv.DictReader(stream)) - expected = len(selected) - rows = [] - for original in selected: - stem = original['model_id'] - subject = stem.removesuffix('_QCT_L1') - model = root / 'models' / subject / (stem + '.n88model') - metric = model.with_name(stem + '_qc_metrics.csv') - row = {} - if metric.is_file(): - with metric.open() as stream: - row = next(csv.DictReader(stream)) - row.update(model_id=stem, site='spine', model_path=str(model)) - status = json.loads((root / 'cases' / subject / 'status.json').read_text()) - row['rerun_stage'] = status['stage'] - row['previous_manual_decision'] = original['manual_decision'] - row['rerun_error'] = status.get('error', '') - if status['stage'] != 'complete': - row['generation_status'] = 'construction_failed' - for suffix, field in [('_qc_3d.webp', 'anatomy_image'), ('_sed_3d.webp', 'sed_image')]: - image = model.with_name(stem + suffix) - row[field] = str(image) if image.is_file() else '' - result = model.with_name(stem + '_results.csv') - if status['stage'] == 'complete' and result.is_file(): - with result.open() as stream: - row.update(next(csv.DictReader(stream))) - row = evaluate(row) - if status['stage'] != 'complete': - row.update(qc_status='fail', qc_reasons='rerun_failed;' + row['qc_reasons']) - rows.append(row) - if len(rows) != expected: - raise ValueError(f'Rerun gallery must retain all {expected} cases, including failures') - export_gallery_zip(rows, root / 'after_review.zip') - (root / 'gallery_ready.json').write_text(json.dumps({'cases': expected, 'zip': 'after_review.zip'})) - print(f'After-only gallery packaged: {expected} cases; original decisions not imported', flush=True) - - -if __name__ == '__main__': - main(Path(sys.argv[1])) diff --git a/examples/fea/cohort_recovery/recover_spines.py b/examples/fea/cohort_recovery/recover_spines.py deleted file mode 100644 index 37fa8e3..0000000 --- a/examples/fea/cohort_recovery/recover_spines.py +++ /dev/null @@ -1,222 +0,0 @@ -"""Resegment manually excluded L1 cases without replacing selected study models. - -Run on Groot, not an ARC login node. Two GPU workers segment full native CTs; -four CPU workers generate and solve the isolated L1 models. Completed stages -are resumable, and original review decisions are retained as provenance only. -""" - -import argparse -import csv -import fcntl -import hashlib -import json -import os -from concurrent.futures import ThreadPoolExecutor -from pathlib import Path -import subprocess -import time -import sys - -import nibabel as nib -import numpy as np -import SimpleITK as sitk - -PYTHON = sys.executable -SOURCE = str(Path(__file__).resolve().parents[3]) -SEGMENT = BUNDLE = FAIM = LIBRARY = QCT = None - - -def save_json(path, value): - temporary = path.with_suffix('.tmp') - temporary.write_text(json.dumps(value, indent=2) + '\n') - temporary.replace(path) - - -def isolate_l1(level, parts, level_affine, parts_affine): - """The level map identifies L1=20; the parts map uses body=2/process=1.""" - if level.shape != parts.shape or not np.allclose(level_affine, parts_affine, atol=1e-5): - raise ValueError('Vertebral-level and body/process grids do not match') - selected = level == 20 - if set(np.unique(parts[selected])) - {0, 1, 2}: - raise ValueError('Unexpected process/body labels inside L1') - result = np.where(selected, parts, 0).astype(np.uint8) - if not np.any(result == 2) or not np.any(result == 1): - raise ValueError('L1 body or process is missing; do not model other levels') - return result - - -def run(command, log, environment): - with log.open('a') as stream: - stream.write('\n' + json.dumps(command) + '\n') - stream.flush() - subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT, - env=environment, check=True) - - -def inputs(subject): - native = LIBRARY / f'sub-{subject}/ses-1/ct/sub-{subject}_ses-1_ct.nii.gz' - qct = QCT / f'{subject}_QCT.nii.gz' - for path in (native, qct): - if not path.is_file(): - raise FileNotFoundError(path) - return native, qct - - -def segment_case(row, root, gpu): - subject = row['model_id'].removesuffix('_QCT_L1') - folder = root / 'cases' / subject - folder.mkdir(parents=True, exist_ok=True) - status_path = folder / 'status.json' - status = json.loads(status_path.read_text()) if status_path.exists() else {'subject': subject} - if status.get('stage') in ('segmented', 'complete', 'fea_failed'): - return row - native, qct = inputs(subject) - status.update(stage='segmenting', original_review=row, native_ct=str(native), - calibrated_qct=str(qct), model_bundle=BUNDLE, started=time.time()) - save_json(status_path, status) - environment = dict(os.environ, CUDA_VISIBLE_DEVICES=str(gpu), OMP_NUM_THREADS='4', - OPENBLAS_NUM_THREADS='1', MKL_NUM_THREADS='1') - try: - output = folder / 'segmentation' - output.mkdir(exist_ok=True) - run([SEGMENT, '--output', str(output), '--device', 'cuda', - '--model-bundle', BUNDLE, '--overwrite', str(native)], folder / 'segmentation.log', environment) - stem = native.name.removesuffix('.nii.gz') - level = nib.load(output / f'{stem}_vertebral-level.nii.gz') - parts = nib.load(output / f'{stem}_process-body.nii.gz') - isolated = isolate_l1(np.asarray(level.dataobj), np.asarray(parts.dataobj), - level.affine, parts.affine) - native_header = nib.load(native) - if parts.shape != native_header.shape or not np.allclose(parts.affine, native_header.affine, atol=1e-5): - raise ValueError('Segmentation does not preserve the native CT grid') - label_path = folder / 'L1_native_body2_process1.nii.gz' - nib.save(nib.Nifti1Image(isolated, parts.affine), label_path) - reference = sitk.ReadImage(str(qct)) - labels = sitk.Resample(sitk.ReadImage(str(label_path)), reference, - sitk.Transform(), sitk.sitkNearestNeighbor, 0, sitk.sitkUInt8) - full_path = folder / 'L1_qct_full_body2_process1.nii.gz' - sitk.WriteImage(labels, str(full_path)) - array = sitk.GetArrayFromImage(labels) - if not np.any(array == 2) or not np.any(array == 1): - raise ValueError('L1 does not overlap calibrated QCT') - # Crop only after full-size inference and physical-coordinate mapping. - coordinates = np.argwhere(array > 0)[:, ::-1] - margin = np.ceil(15 / np.array(reference.GetSpacing())).astype(int) - start = np.maximum(0, coordinates.min(axis=0) - margin) - end = np.minimum(reference.GetSize(), coordinates.max(axis=0) + margin + 1) - size = (end - start).tolist() - sitk.WriteImage(sitk.RegionOfInterest(reference, size, start.tolist()), - str(folder / f'{subject}_QCT.nii.gz')) - sitk.WriteImage(sitk.RegionOfInterest(labels, size, start.tolist()), - str(folder / f'{subject}_labels.nii.gz')) - status.update(stage='segmented', body_voxels=int((array == 2).sum()), - process_voxels=int((array == 1).sum()), crop_start_xyz=start.tolist(), - crop_size_xyz=size, native_shape=list(parts.shape), - qct_size=list(reference.GetSize())) - except Exception as error: - status.update(stage='segmentation_failed', error=str(error)) - save_json(status_path, status) - return row - - -def solve_case(row, root): - subject = row['model_id'].removesuffix('_QCT_L1') - folder = root / 'cases' / subject - status_path = folder / 'status.json' - status = json.loads(status_path.read_text()) - if status['stage'] not in ('segmented', 'fea_failed', 'solving'): - return - output = root / 'models' / subject - output.mkdir(parents=True, exist_ok=True) - environment = dict(os.environ, PYTHONPATH=SOURCE, OMP_NUM_THREADS='4', - OPENBLAS_NUM_THREADS='1', MKL_NUM_THREADS='1', - PATH=str(Path(PYTHON).parent) + os.pathsep + os.environ['PATH']) - command = [PYTHON, '-m', 'ogo.cli.GenerateFEM', 'spine', - str(folder / f'{subject}_QCT.nii.gz'), str(folder / f'{subject}_labels.nii.gz'), - '--vertebra', 'L1:2:1', '--pistoia_mask_label', '2', '--output_path', str(output), - '--threads', '4', '--faim_bin_dir', FAIM, '--run_pistoia', '--require_pistoia', - '--critical_volume', '12', '--masked_critical_volume', '35', '--critical_strain', '0.007'] - status.update(stage='solving', fea_command=command, ogo_source=SOURCE) - save_json(status_path, status) - try: - run(command, folder / 'fea.log', environment) - prefix = output / f'{subject}_QCT_L1' - for suffix in ('.n88model', '_results.csv', '_qc_metrics.csv', '_qc_3d.webp', '_sed_3d.webp'): - if not Path(str(prefix) + suffix).is_file(): - raise FileNotFoundError(str(prefix) + suffix) - status.update(stage='complete', finished=time.time()) - status.pop('error', None) - except Exception as error: - status.update(stage='fea_failed', error=str(error)) - save_json(status_path, status) - - -def main(): - global SEGMENT, BUNDLE, FAIM, LIBRARY, QCT - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('--root', type=Path, required=True) - parser.add_argument('--review-csv', type=Path, required=True) - parser.add_argument('--selection', choices=('manual-exclusions', 'incomplete'), default='manual-exclusions') - parser.add_argument('--library', type=Path, required=True) - parser.add_argument('--qct-dir', type=Path, required=True) - parser.add_argument('--segment-cli', required=True) - parser.add_argument('--model-bundle', required=True) - parser.add_argument('--faim-bin-dir', required=True) - parser.add_argument('--gpu-workers', type=int, choices=(1, 2), default=2) - parser.add_argument('--fea-workers', type=int, default=4) - parser.add_argument('--preflight', action='store_true') - args = parser.parse_args() - LIBRARY, QCT = args.library, args.qct_dir - SEGMENT, BUNDLE, FAIM = args.segment_cli, args.model_bundle, args.faim_bin_dir - if args.fea_workers < 1: - parser.error('--fea-workers must be positive') - root = args.root - with args.review_csv.open() as stream: - rows = [row for row in csv.DictReader(stream) - if row['site'] == 'spine' and ( - row['manual_decision'] == 'exclude' if args.selection == 'manual-exclusions' else - row['manual_decision'] != 'exclude' and - 'incomplete_fea' in row['automatic_qc_reasons'].split(';'))] - if not rows or len({row['model_id'] for row in rows}) != len(rows): - raise ValueError('Selection must contain nonempty, unique spine model IDs') - for row in rows: - inputs(row['model_id'].removesuffix('_QCT_L1')) - if args.preflight: - print(f'All {len(rows)} full-size native CTs and calibrated QCTs exist', flush=True) - return - root.mkdir(parents=True, exist_ok=True) - with (root / 'coordinator.lock').open('w') as lock: - fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) - manifest = root / 'rerun_selection.csv' - if manifest.exists(): - with manifest.open() as stream: - saved = list(csv.DictReader(stream)) - if saved != rows: - raise ValueError('Selection changed; use a new recovery output root') - with manifest.open('w', newline='') as stream: - writer = csv.DictWriter(stream, fieldnames=list(rows[0])) - writer.writeheader() - writer.writerows(rows) - commit = subprocess.check_output(['git', '-C', SOURCE, 'rev-parse', 'HEAD'], text=True).strip() - save_json(root / 'launch.json', {'pid': os.getpid(), 'started': time.time(), 'cases': len(rows), - 'ogo_commit': commit, 'segmentation_cli': SEGMENT, 'selection': args.selection, - 'bundle_manifest_sha256': hashlib.sha256((Path(BUNDLE) / 'manifest.json').read_bytes()).hexdigest()}) - def segment_chunk(gpu): - for row in rows[gpu::args.gpu_workers]: - segment_case(row, root, gpu) - print('Segmentation', row['model_id'], flush=True) - with ThreadPoolExecutor(max_workers=args.gpu_workers) as pool: - list(pool.map(segment_chunk, range(args.gpu_workers))) - with ThreadPoolExecutor(max_workers=args.fea_workers) as pool: - list(pool.map(lambda row: solve_case(row, root), rows)) - states = [json.loads(path.read_text()) for path in (root / 'cases').glob('*/status.json')] - save_json(root / 'summary.json', {'cases': len(states), 'stages': { - stage: sum(item['stage'] == stage for item in states) - for stage in sorted({item['stage'] for item in states})}}) - run([PYTHON, str(Path(__file__).with_name('build_manual_spine_gallery.py')), str(root)], - root / 'gallery.log', dict(os.environ, PYTHONPATH=SOURCE)) - print('Rerun complete; build the after-only review gallery', flush=True) - - -if __name__ == '__main__': - main() diff --git a/ogo/cli/ValidateFEA.py b/ogo/cli/ValidateFEA.py index 67c6c2c..0d279a9 100644 --- a/ogo/cli/ValidateFEA.py +++ b/ogo/cli/ValidateFEA.py @@ -5,11 +5,11 @@ from concurrent.futures import ProcessPoolExecutor from pathlib import Path -from ogo.fea.validation import evaluate, generate_gallery -from ogo.fea.validation import write_measurements -from ogo.fea.gallery import export_gallery_zip -from ogo.fea.legacy_qc import backfill_measurements, evidence_signature, load_evidence, needs_backfill -from ogo.fea.review import identity, load_decisions, write_inclusion +from ogo.fea.qc.validation import evaluate, generate_gallery +from ogo.fea.qc.validation import write_measurements +from ogo.fea.qc.gallery import export_gallery_zip +from ogo.fea.qc.legacy import backfill_measurements, evidence_signature, load_evidence, needs_backfill +from ogo.fea.qc.review import identity, load_decisions, write_inclusion def _backfill_task(task): diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 726a04e..7f28631 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -146,14 +146,12 @@ CSV. Native-space masks cannot substitute for model-space evidence. Missing scan coverage stays unknown; backfill neither rewrites models nor renders images. Failures before meshing require job logs for participant accounting. -For full-size resegmentation of reviewed failures, see the -[cohort recovery example](../../examples/fea/cohort_recovery/README.md). - ## Maintenance Anatomy-specific builders are in `femur.py` and `spine.py`; `metadata.py` records -their parsed settings. `qc_render.py` prepares geometry once per three-view -preview, and `validation.py` applies the QC policy without changing FE results. +their parsed settings. `alignment/` contains shared ICP and hip initialization. +`qc/` contains validation, rendering, gallery export and review decisions; +`qc/render.py` prepares geometry once per three-view preview. Shared image helpers remain in `ogo.util.Helper` and `ogo.util.vtk_image`; rendering uses `ogo.cli.Visualize`, and reference surfaces ship in `ogo/dat`. diff --git a/ogo/fea/alignment/__init__.py b/ogo/fea/alignment/__init__.py new file mode 100644 index 0000000..691116f --- /dev/null +++ b/ogo/fea/alignment/__init__.py @@ -0,0 +1 @@ +"""Reference-frame alignment: shared transforms and anatomy-specific fitting.""" diff --git a/ogo/fea/alignment.py b/ogo/fea/alignment/core.py similarity index 99% rename from ogo/fea/alignment.py rename to ogo/fea/alignment/core.py index 516821f..ecc6aa9 100644 --- a/ogo/fea/alignment.py +++ b/ogo/fea/alignment/core.py @@ -1,4 +1,4 @@ -"""Shared point-cloud helpers for reference-frame FE alignment.""" +"""Shared point-cloud transforms and ICP for reference-frame FE alignment.""" def polydata_points(polydata): diff --git a/ogo/fea/hip_registration.py b/ogo/fea/alignment/hip.py similarity index 98% rename from ogo/fea/hip_registration.py rename to ogo/fea/alignment/hip.py index ebb250f..238f453 100644 --- a/ogo/fea/hip_registration.py +++ b/ogo/fea/alignment/hip.py @@ -7,7 +7,7 @@ import numpy as np from scipy.spatial import cKDTree -from ogo.fea.alignment import estimate_rigid_icp +from ogo.fea.alignment.core import estimate_rigid_icp def estimate_femur_icp(*, moving_points, fixed_points, iterations=50): diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index c655f60..8750c8a 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -32,7 +32,7 @@ from pathlib import Path -from ogo.fea.alignment import ( +from ogo.fea.alignment.core import ( estimate_rigid_icp, point_cloud_axis_lengths, polydata_from_points, @@ -50,7 +50,7 @@ foreground_voxel_center_bounds_from_mask, ) from ogo.fea.image_io import write_vtk_image_with_sitk_geometry -from ogo.fea.hip_registration import estimate_femur_icp +from ogo.fea.alignment.hip import estimate_femur_icp from ogo.fea.shaft_geometry import ( aligned_distal_scan_boundary, capture_distal_scan_face, @@ -1724,10 +1724,10 @@ def sidewaysFallFe(args): # Preserve a visual explanation of coverage failure without inventing # a distal boundary or sending an ineligible model to the solver. if shaft_crop.get("status") == "too_short": - from ogo.fea.qc_render import try_export_model_qc + from ogo.fea.qc.render import try_export_model_qc available_model = ogo.applyTestBase(ogo.Image2Mesh(conn2), material_table) - from ogo.fea.validation import measure_model, write_measurements + from ogo.fea.qc.validation import measure_model, write_measurements write_measurements(N88_fileName, measure_model( available_model, "hip", shaft=shaft_crop, ineligible=True)) @@ -1921,7 +1921,7 @@ def sidewaysFallFe(args): # Write out n88model file ogo.message("Writing out n88model file: %s" % N88_fileName) shaft_crop = verify_model_shaft(model2, shaft_crop) - from ogo.fea.validation import measure_model, write_measurements + from ogo.fea.qc.validation import measure_model, write_measurements write_measurements(N88_fileName, measure_model( model2, "hip", shaft=shaft_crop, registration_rotation=icp_transform["rotation"])) @@ -1931,7 +1931,7 @@ def sidewaysFallFe(args): saved_model.Update() shaft_crop = verify_model_shaft(saved_model.GetOutput(), shaft_crop) - from ogo.fea.qc_render import try_export_model_qc + from ogo.fea.qc.render import try_export_model_qc shaft_crop["exports"] = {"qc_3d": try_export_model_qc(N88_fileName, "hip", model=model2)} with open(str(N88_fileName).replace(".n88model", "_shaft_geometry.json"), "w") as stream: diff --git a/ogo/fea/qc/__init__.py b/ogo/fea/qc/__init__.py new file mode 100644 index 0000000..5567e06 --- /dev/null +++ b/ogo/fea/qc/__init__.py @@ -0,0 +1 @@ +"""FE model measurements, validation, previews and review galleries.""" diff --git a/ogo/fea/gallery.py b/ogo/fea/qc/gallery.py similarity index 98% rename from ogo/fea/gallery.py rename to ogo/fea/qc/gallery.py index dede2e5..c2073bb 100644 --- a/ogo/fea/gallery.py +++ b/ogo/fea/qc/gallery.py @@ -12,8 +12,8 @@ from tempfile import TemporaryDirectory from urllib.parse import quote -from ogo.fea.review import FIELDS, REASONS, identity, inclusion_row, write_inclusion -from ogo.fea.qc_images import REVIEW_PANEL_SIZE, save_qc_panel, save_review_image +from ogo.fea.qc.review import FIELDS, REASONS, identity, inclusion_row, write_inclusion +from ogo.fea.qc.images import REVIEW_PANEL_SIZE, save_qc_panel, save_review_image def _camera_images(path, output): diff --git a/ogo/fea/gallery_review.js b/ogo/fea/qc/gallery_review.js similarity index 100% rename from ogo/fea/gallery_review.js rename to ogo/fea/qc/gallery_review.js diff --git a/ogo/fea/qc_images.py b/ogo/fea/qc/images.py similarity index 100% rename from ogo/fea/qc_images.py rename to ogo/fea/qc/images.py diff --git a/ogo/fea/legacy_qc.py b/ogo/fea/qc/legacy.py similarity index 99% rename from ogo/fea/legacy_qc.py rename to ogo/fea/qc/legacy.py index 6d81d66..137679b 100644 --- a/ogo/fea/legacy_qc.py +++ b/ogo/fea/qc/legacy.py @@ -11,7 +11,7 @@ import numpy as np -from ogo.fea.validation import measure_model, model_grid, write_measurements +from ogo.fea.qc.validation import measure_model, model_grid, write_measurements BACKFILL_VERSION = 3 diff --git a/ogo/fea/qc_render.py b/ogo/fea/qc/render.py similarity index 99% rename from ogo/fea/qc_render.py rename to ogo/fea/qc/render.py index 75c610a..5b9dd34 100644 --- a/ogo/fea/qc_render.py +++ b/ogo/fea/qc/render.py @@ -24,7 +24,7 @@ from vtk.util.numpy_support import vtk_to_numpy, numpy_to_vtk from ogo.cli.Visualize import vis3d -from ogo.fea.qc_images import REVIEW_PANEL_SIZE, save_qc_panel +from ogo.fea.qc.images import REVIEW_PANEL_SIZE, save_qc_panel def smooth_bone_sed(values, indices, bone, spacing): @@ -96,7 +96,7 @@ def prepare_model_qc(model_path, site, body_mask=None, sed=False, if array is None: raise ValueError("SED rendering requires a solved model with StrainEnergyDensity") sed_values = vtk_to_numpy(array) - from ogo.fea.validation import write_measurements + from ogo.fea.qc.validation import write_measurements finite = np.isfinite(sed_values) write_measurements(model_path, { @@ -384,7 +384,7 @@ def export_model_qc(model_path, site, body_mask=None, sed=False, {"site": site, "views": views, "image_format": "WEBP", "image_quality": 85, "image_size_pixels": list(REVIEW_PANEL_SIZE), "review_only": True}, indent=2)) if model_path.with_name(model_path.stem + '_qc_metrics.csv').exists(): - from ogo.fea.validation import write_measurements + from ogo.fea.qc.validation import write_measurements write_measurements(model_path, {'sed_image' if sed else 'anatomy_image': str(output)}) return str(output) diff --git a/ogo/fea/review.py b/ogo/fea/qc/review.py similarity index 100% rename from ogo/fea/review.py rename to ogo/fea/qc/review.py diff --git a/ogo/fea/validation.py b/ogo/fea/qc/validation.py similarity index 99% rename from ogo/fea/validation.py rename to ogo/fea/qc/validation.py index 97c8731..baae30e 100644 --- a/ogo/fea/validation.py +++ b/ogo/fea/qc/validation.py @@ -201,7 +201,7 @@ def write_measurements(model_path, measurements): with path.open(newline='') as stream: row.update(next(csv.DictReader(stream))) row.update(measurements) - from ogo.fea.qc_images import qc_image_path + from ogo.fea.qc.images import qc_image_path row.update(model_id=model_path.stem, model_path=str(model_path), anatomy_image=str(qc_image_path(model_path)), sed_image=str(qc_image_path(model_path, sed=True))) @@ -346,6 +346,6 @@ def _spine_checks(row, number, failures, review): def generate_gallery(rows, output, decisions=None): """Build the standalone review gallery without opening any FE model.""" - from ogo.fea.gallery import generate_gallery as render + from ogo.fea.qc.gallery import generate_gallery as render return render(rows, output, decisions=decisions) diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 35313f2..94efb07 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -22,7 +22,7 @@ import vtkbone from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy -from ogo.fea.alignment import ( +from ogo.fea.alignment.core import ( estimate_rigid_icp, estimate_rigid_icp_vtk, invert_point_transform, @@ -1999,7 +1999,7 @@ def process_vertebra( test = check_image(boundary_masks, dataframe_path) ogo.message(f"Writing n88model file: {n88model_output_path}") - from ogo.fea.validation import measure_model, write_measurements + from ogo.fea.qc.validation import measure_model, write_measurements measurements = measure_model( model, "spine", body_image=padded_mask1, process_image=padded_mask2, @@ -2028,7 +2028,7 @@ def process_vertebra( # Retain the aligned body ROI so post-solve QC uses the identical anatomy. body_qc_path = n88model_output_path.replace(".n88model", "_qc_body_mask.nii.gz") write_vtk_image_with_sitk_geometry(padded_mask1, body_qc_path) - from ogo.fea.qc_render import try_export_model_qc + from ogo.fea.qc.render import try_export_model_qc try_export_model_qc(n88model_output_path, "spine", body_mask=body_qc_path, model=model) diff --git a/ogo/util/faim.py b/ogo/util/faim.py index 8a31651..ce8960f 100644 --- a/ogo/util/faim.py +++ b/ogo/util/faim.py @@ -950,8 +950,8 @@ def run_faim_pipeline( log_file.write_text(log_text) profile = str(report_profile or "").strip().lower() if profile in ("spine", "femur"): - from ogo.fea.qc_render import try_export_model_qc - from ogo.fea.validation import write_measurements + from ogo.fea.qc.render import try_export_model_qc + from ogo.fea.qc.validation import write_measurements write_measurements(model_file, { "site": "hip" if profile == "femur" else "spine", diff --git a/setup.cfg b/setup.cfg index a01cdde..11ce1ca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ keywords = imaging [options.package_data] -ogo.fea = gallery_review.js +ogo.fea.qc = gallery_review.js [entry_points] console_scripts = diff --git a/tests/fea/test_alignment.py b/tests/fea/test_alignment.py index 3b0a139..51aa2c9 100644 --- a/tests/fea/test_alignment.py +++ b/tests/fea/test_alignment.py @@ -16,7 +16,7 @@ def _vtk_image_from_array(data, *, origin=(0, 0, 0), spacing=(1, 1, 1)): def test_surface_points_from_vtk_mask_uses_voxel_surface_centers(): np = pytest.importorskip("numpy") - from ogo.fea.alignment import surface_points_from_vtk_mask + from ogo.fea.alignment.core import surface_points_from_vtk_mask mask = np.ones((3, 3, 3), dtype=np.uint8) image = _vtk_image_from_array(mask, origin=(10, 20, 30), spacing=(2, 3, 4)) @@ -36,7 +36,7 @@ def test_surface_points_from_vtk_mask_uses_voxel_surface_centers(): def test_estimate_rigid_icp_recovers_simple_translation(): np = pytest.importorskip("numpy") - from ogo.fea.alignment import estimate_rigid_icp + from ogo.fea.alignment.core import estimate_rigid_icp moving = np.asarray( [ @@ -63,7 +63,7 @@ def test_estimate_rigid_icp_vtk_recovers_simple_translation(): pytest.importorskip("vtk") np = pytest.importorskip("numpy") - from ogo.fea.alignment import estimate_rigid_icp_vtk + from ogo.fea.alignment.core import estimate_rigid_icp_vtk moving = np.asarray( [ @@ -91,7 +91,7 @@ def test_estimate_rigid_icp_vtk_recovers_simple_translation(): def test_invert_point_transform_reverses_row_vector_transform(): np = pytest.importorskip("numpy") - from ogo.fea.alignment import invert_point_transform + from ogo.fea.alignment.core import invert_point_transform rotation = np.asarray( [ @@ -113,7 +113,7 @@ def test_invert_point_transform_reverses_row_vector_transform(): def test_output_grid_for_point_transform_uses_transformed_voxel_centers(): np = pytest.importorskip("numpy") - from ogo.fea.alignment import output_grid_for_point_transform + from ogo.fea.alignment.core import output_grid_for_point_transform points = np.asarray( [ @@ -141,7 +141,7 @@ def test_output_grid_for_point_transform_uses_transformed_voxel_centers(): def test_resample_vtk_image_with_point_transform_uses_explicit_output_grid(): np = pytest.importorskip("numpy") - from ogo.fea.alignment import resample_vtk_image_with_point_transform + from ogo.fea.alignment.core import resample_vtk_image_with_point_transform from ogo.util.vtk_image import vtk_image_to_numpy data = np.zeros((3, 3, 3), dtype=np.uint8) diff --git a/tests/fea/test_cohort_recovery.py b/tests/fea/test_cohort_recovery.py deleted file mode 100644 index 6047a17..0000000 --- a/tests/fea/test_cohort_recovery.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Verify the study example never substitutes neighboring vertebral levels.""" -import importlib.util -from pathlib import Path - -import numpy as np -import pytest - -pytest.importorskip('SimpleITK') -source = Path(__file__).resolve().parents[2] / 'examples/fea/cohort_recovery/recover_spines.py' -spec = importlib.util.spec_from_file_location('recover_spines', source) -recovery = importlib.util.module_from_spec(spec) -spec.loader.exec_module(recovery) - - -def test_only_l1_body_and_process_are_retained(): - level = np.array([20, 20, 19, 21]) - parts = np.array([2, 1, 2, 1]) - np.testing.assert_array_equal(recovery.isolate_l1(level, parts, np.eye(4), np.eye(4)), [2, 1, 0, 0]) - - -def test_mismatched_affine_is_rejected(): - shifted = np.eye(4) - shifted[0, 3] = 2 - with pytest.raises(ValueError, match='grids'): - recovery.isolate_l1(np.array([20, 20]), np.array([2, 1]), np.eye(4), shifted) - - -def test_missing_l1_and_unknown_part_are_rejected(): - for level, parts in [([19, 21], [2, 1]), ([20, 20], [2, 3])]: - with pytest.raises(ValueError): - recovery.isolate_l1(np.array(level), np.array(parts), np.eye(4), np.eye(4)) diff --git a/tests/fea/test_hip_registration.py b/tests/fea/test_hip_registration.py index 2658e01..000ecbb 100644 --- a/tests/fea/test_hip_registration.py +++ b/tests/fea/test_hip_registration.py @@ -3,7 +3,7 @@ import numpy as np import pytest -from ogo.fea import alignment +from ogo.fea.alignment import core as alignment def test_public_hip_defaults_are_the_locked_ten_mm_recipe(): @@ -24,7 +24,7 @@ def test_stride_sampling_does_not_truncate_the_proximal_end(): def test_multistart_recovers_axial_rotation_without_scaling_native_geometry(): - from ogo.fea.hip_registration import estimate_femur_icp + from ogo.fea.alignment.hip import estimate_femur_icp rng = np.random.default_rng(42) points = rng.normal(size=(800, 3)) * [5, 12, 30] @@ -41,7 +41,7 @@ def test_multistart_recovers_axial_rotation_without_scaling_native_geometry(): def test_superior_inversion_cannot_win_candidate_selection(monkeypatch): - from ogo.fea import hip_registration + from ogo.fea.alignment import hip as hip_registration calls = iter([0, 1, 2, 3]) diff --git a/tests/fea/test_legacy_evidence.py b/tests/fea/test_legacy_evidence.py index fc3d9f0..af81d35 100644 --- a/tests/fea/test_legacy_evidence.py +++ b/tests/fea/test_legacy_evidence.py @@ -8,7 +8,7 @@ def test_evidence_cache_retries_only_when_inputs_change(tmp_path): - from ogo.fea.legacy_qc import evidence_signature, needs_backfill + from ogo.fea.qc.legacy import evidence_signature, needs_backfill model = tmp_path / 'case.n88model' model.touch() row = {'measurement_source': 'legacy_backfill'} @@ -21,7 +21,7 @@ def test_evidence_cache_retries_only_when_inputs_change(tmp_path): def test_evidence_manifest_resolves_relative_paths_and_rejects_duplicates(tmp_path): - from ogo.fea.legacy_qc import load_evidence + from ogo.fea.qc.legacy import load_evidence path = tmp_path / 'sources.csv' path.write_text('site,model_id,body_mask\nspine,case,body.nii.gz\n') assert load_evidence(path)[('spine', 'case')]['body_mask'] == str(tmp_path / 'body.nii.gz') @@ -32,7 +32,7 @@ def test_evidence_manifest_resolves_relative_paths_and_rejects_duplicates(tmp_pa def test_registered_mask_geometry_and_process_recovery(tmp_path): sitk = pytest.importorskip('SimpleITK') - from ogo.fea.legacy_qc import registered_anatomy + from ogo.fea.qc.legacy import registered_anatomy mask = sitk.GetImageFromArray(np.ones((2, 2, 2), dtype=np.uint8)) body = tmp_path / 'body.nii.gz' sitk.WriteImage(mask, str(body)) @@ -50,7 +50,7 @@ def test_registered_mask_geometry_and_process_recovery(tmp_path): def test_registered_mask_must_match_voxel_lattice(tmp_path): sitk = pytest.importorskip('SimpleITK') - from ogo.fea.legacy_qc import registered_anatomy + from ogo.fea.qc.legacy import registered_anatomy mask = sitk.GetImageFromArray(np.ones((2, 2, 2), dtype=np.uint8)) mask.SetOrigin((0.3, 0, 0)) path = tmp_path / 'body.nii.gz' @@ -60,7 +60,7 @@ def test_registered_mask_must_match_voxel_lattice(tmp_path): def test_zero_anatomy_offset_is_valid_evidence(): - from ogo.fea.validation import evaluate + from ogo.fea.qc.validation import evaluate row = evaluate({'site': 'spine', 'measurement_source': 'legacy_backfill', 'process_body_axial_offset_mm': 0, 'process_body_transverse_offset_mm': 10}) @@ -69,7 +69,7 @@ def test_zero_anatomy_offset_is_valid_evidence(): def test_hip_distal_set_is_recovered_from_saved_plane_not_union(tmp_path, monkeypatch, hip_model): import vtkbone - from ogo.fea.legacy_qc import backfill_measurements + from ogo.fea.qc.legacy import backfill_measurements model = hip_model model.ApplyBoundaryCondition('Femoral_Head_PMMA_Nodes', 1, -.1, 'top_displacement') model.ApplyBoundaryCondition('Greater_Trochanter_PMMA_Nodes', 1, 0, 'bottom_fixed_y_PMMA') diff --git a/tests/fea/test_module_layout.py b/tests/fea/test_module_layout.py index fcf9a48..3e2b5d6 100644 --- a/tests/fea/test_module_layout.py +++ b/tests/fea/test_module_layout.py @@ -52,3 +52,12 @@ def test_fea_shared_dependencies_are_available(): assert (data / "LT_FEMUR_SIDEWAYS_FALL_REF.vtk").is_file() for target in spine.SPINE_ICP_TARGETS: assert spine.default_spine_reference_path(target).is_file() + + +def test_alignment_and_qc_are_grouped_by_responsibility(): + from ogo.fea.alignment import hip + from ogo.fea.qc import gallery, images, legacy, render, review, validation + + assert Path(hip.__file__).parent.name == "alignment" + for module in (gallery, images, legacy, render, review, validation): + assert Path(module.__file__).parent.name == "qc" diff --git a/tests/fea/test_qc_images.py b/tests/fea/test_qc_images.py index e4be943..e3beda7 100644 --- a/tests/fea/test_qc_images.py +++ b/tests/fea/test_qc_images.py @@ -7,7 +7,7 @@ def test_review_image_is_webp_without_resizing(tmp_path): - from ogo.fea.qc_images import save_review_image + from ogo.fea.qc.images import save_review_image path = tmp_path / 'case_qc_3d.webp' save_review_image(Image.new('RGB', (1400, 2550), 'white'), path) @@ -17,7 +17,7 @@ def test_review_image_is_webp_without_resizing(tmp_path): def test_default_qc_panel_is_compact_webp(tmp_path): - from ogo.fea.qc_images import save_qc_panel + from ogo.fea.qc.images import save_qc_panel path = tmp_path / 'case_qc_3d.webp' save_qc_panel(Image.new('RGB', (1400, 2550), 'white'), path) @@ -27,7 +27,7 @@ def test_default_qc_panel_is_compact_webp(tmp_path): def test_gallery_splits_compact_panel_without_mixing_views(tmp_path): - from ogo.fea.gallery import _camera_images + from ogo.fea.qc.gallery import _camera_images source = tmp_path / 'case_qc_3d.webp' panel = Image.new('RGB', (600, 1092)) @@ -46,7 +46,7 @@ def test_gallery_splits_compact_panel_without_mixing_views(tmp_path): @pytest.mark.parametrize('extension', ['.png', '.webp']) def test_gallery_splits_both_formats_into_webp(tmp_path, extension): - from ogo.fea.gallery import _camera_images + from ogo.fea.qc.gallery import _camera_images source = tmp_path / ('case_qc_3d' + extension) Image.new('RGB', (1400, 2550), 'white').save(source) @@ -60,7 +60,7 @@ def test_gallery_splits_both_formats_into_webp(tmp_path, extension): def test_measurements_discover_legacy_png_and_prefer_webp(tmp_path): import csv - from ogo.fea.validation import write_measurements + from ogo.fea.qc.validation import write_measurements model = tmp_path / 'case.n88model' png = tmp_path / 'case_qc_3d.png' diff --git a/tests/fea/test_qc_render.py b/tests/fea/test_qc_render.py index c611227..109db6c 100644 --- a/tests/fea/test_qc_render.py +++ b/tests/fea/test_qc_render.py @@ -7,7 +7,7 @@ def test_masked_sed_smoothing_excludes_disks_and_preserves_source(): - from ogo.fea.qc_render import smooth_bone_sed + from ogo.fea.qc.render import smooth_bone_sed indices = np.indices((3, 3, 3)).reshape(3, -1).T bone = indices[:, 0] < 2 @@ -19,7 +19,7 @@ def test_masked_sed_smoothing_excludes_disks_and_preserves_source(): def test_sed_missing_or_invalid_is_rejected(): - from ogo.fea.qc_render import smooth_bone_sed + from ogo.fea.qc.render import smooth_bone_sed with pytest.raises(ValueError, match="finite"): smooth_bone_sed(np.array([np.nan]), np.zeros((1, 3), dtype=int), @@ -37,7 +37,7 @@ def test_qc_uses_existing_visualizer_extension(): def test_panel_prepares_geometry_once_for_all_views(tmp_path, monkeypatch): from PIL import Image - from ogo.fea import qc_render + from ogo.fea.qc import render as qc_render prepared = object() calls = [] @@ -61,14 +61,14 @@ def render(model_path, output_dir, site, body_mask, view, sed, def test_ineligible_qc_is_anatomy_only(tmp_path): - from ogo.fea.qc_render import export_model_qc + from ogo.fea.qc.render import export_model_qc with pytest.raises(ValueError, match="Ineligible"): export_model_qc(tmp_path / "short.n88model", "hip", sed=True, ineligible=True) def test_render_failure_warns_without_interrupting_solver(monkeypatch): - from ogo.fea import qc_render + from ogo.fea.qc import render as qc_render def fail(*args, **kwargs): raise RuntimeError("No graphics display") @@ -92,7 +92,7 @@ def test_too_short_branch_exports_before_exit(): def test_export_keeps_only_panel_and_settings(tmp_path, monkeypatch): import json from PIL import Image - from ogo.fea import qc_render + from ogo.fea.qc import render as qc_render def fake_panel(model_path, temporary, site, body_mask, sed, model, ineligible): assert ineligible @@ -122,7 +122,7 @@ def Finalize(self): assert image.format == "WEBP" assert image.size == (20, 60) assert all(not v["eligible_for_solve"] for v in json.loads((tmp_path / "short_qc_3d.json").read_text())["views"]) - from ogo.fea.validation import write_measurements + from ogo.fea.qc.validation import write_measurements import csv legacy = tmp_path / "short_qc_3d.png" diff --git a/tests/fea/test_review.py b/tests/fea/test_review.py index 701617f..f5fd491 100644 --- a/tests/fea/test_review.py +++ b/tests/fea/test_review.py @@ -6,7 +6,7 @@ import pytest -from ogo.fea.review import inclusion_row, load_decisions +from ogo.fea.qc.review import inclusion_row, load_decisions def gallery_data(content): @@ -15,7 +15,7 @@ def gallery_data(content): def test_gallery_pages_filters_and_compact_data(tmp_path): - from ogo.fea.gallery import generate_gallery + from ogo.fea.qc.gallery import generate_gallery rows = [{'site': 'spine', 'model_id': f'case{i}', 'qc_status': 'pass', 'qc_reasons': ''} for i in range(501)] content = generate_gallery(rows, tmp_path / 'gallery.html').read_text() @@ -35,7 +35,7 @@ def test_gallery_pagination_and_flags_do_not_change_decisions(): node = shutil.which('node') if not node: pytest.skip('Node is required') - from ogo.fea import gallery + from ogo.fea.qc import gallery script = Path(gallery.__file__).with_name('gallery_review.js') code = '''const assert = require('node:assert/strict'); const {pageIndices, excludedByFlags, inclusionRecord} = require(SCRIPT); @@ -58,7 +58,7 @@ def test_review_decision_restores_scroll_after_refresh(): node = shutil.which('node') if not node: pytest.skip('Node is required') - from ogo.fea import gallery + from ogo.fea.qc import gallery script = Path(gallery.__file__).with_name('gallery_review.js') code = '''const assert = require('node:assert/strict'); const {preserveScroll} = require(SCRIPT); @@ -75,7 +75,7 @@ def test_review_decision_restores_scroll_after_refresh(): def test_gallery_sorting_is_numeric_and_puts_missing_results_last(tmp_path): - from ogo.fea import gallery + from ogo.fea.qc import gallery node = shutil.which('node') if not node: pytest.skip('Node is required') @@ -97,7 +97,7 @@ def test_gallery_sorting_is_numeric_and_puts_missing_results_last(tmp_path): def test_gallery_has_quick_decisions_on_each_card(tmp_path): - from ogo.fea.gallery import generate_gallery + from ogo.fea.qc.gallery import generate_gallery output = tmp_path / 'gallery.html' generate_gallery([{'site': 'hip', 'model_id': 'case', 'qc_status': 'fail', 'qc_reasons': 'incomplete_distal_boundary'}], output) @@ -137,13 +137,13 @@ def test_import_requires_unambiguous_identity(tmp_path): def test_command_backfills_only_missing_measurements(tmp_path, monkeypatch): from ogo.cli import ValidateFEA - from ogo.fea.validation import write_measurements + from ogo.fea.qc.validation import write_measurements write_measurements(tmp_path / 'existing.n88model', {'site': 'hip'}) (tmp_path / 'existing.n88model').touch() (tmp_path / 'legacy.n88model').touch() calls = [] def backfill(path, site=None, evidence=None): - from ogo.fea.legacy_qc import evidence_signature + from ogo.fea.qc.legacy import evidence_signature calls.append(path.name) return write_measurements(path, {'site': 'spine', 'measurement_source': 'legacy_backfill', 'legacy_evidence_signature': evidence_signature(path, evidence)}) @@ -160,7 +160,7 @@ def test_javascript_csv_roundtrip_and_manual_provenance(): node = shutil.which('node') if not node: pytest.skip('Node is required for browser-independent JavaScript tests') - from ogo.fea import gallery + from ogo.fea.qc import gallery script = Path(gallery.__file__).with_name('gallery_review.js') code = '''const assert = require('node:assert/strict'); const {inclusionRecord, encodeCSV, parseCSV, validateReviews, imageVisible} = require(SCRIPT); @@ -187,21 +187,21 @@ def test_javascript_csv_roundtrip_and_manual_provenance(): def test_gallery_does_not_interpret_template_words_in_subject(tmp_path): - from ogo.fea.validation import evaluate, generate_gallery + from ogo.fea.qc.validation import evaluate, generate_gallery row = evaluate({'site': 'hip', 'model_id': 'DATA_SCRIPT_CARDS', 'qc_reasons': ''}) path = generate_gallery([row], tmp_path / 'review.html') assert 'DATA_SCRIPT_CARDS' in path.read_text() def test_legacy_anatomy_is_not_silently_certified(): - from ogo.fea.validation import evaluate + from ogo.fea.qc.validation import evaluate row = evaluate({'site': 'spine', 'measurement_source': 'legacy_backfill'}) assert 'legacy_body_process_metrics_missing' in row['qc_reasons'] def test_gallery_individual_views_keep_panel_order_and_cache(tmp_path): from PIL import Image - from ogo.fea.gallery import generate_gallery + from ogo.fea.qc.gallery import generate_gallery image = tmp_path / 'case_qc_3d.png' panel = Image.new('RGB', (1400, 2550)) for index, color in enumerate(('red', 'green', 'blue')): @@ -227,7 +227,7 @@ def test_gallery_individual_views_keep_panel_order_and_cache(tmp_path): def test_gallery_unknown_montage_is_not_split(tmp_path): from PIL import Image - from ogo.fea.gallery import generate_gallery + from ogo.fea.qc.gallery import generate_gallery image = tmp_path / 'legacy.png' Image.new('RGB', (400, 400), 'white').save(image) row = {'site': 'spine', 'model_id': 'legacy', 'qc_status': 'review', @@ -240,7 +240,7 @@ def test_gallery_unknown_montage_is_not_split(tmp_path): def test_gallery_zip_is_portable_and_has_review_records(tmp_path): import zipfile from PIL import Image - from ogo.fea.gallery import export_gallery_zip + from ogo.fea.qc.gallery import export_gallery_zip image = tmp_path / 'case_qc_3d.png' Image.new('RGB', (1400, 2550), 'red').save(image) row = {'site': 'hip', 'model_id': 'case', 'qc_status': 'review', diff --git a/tests/fea/test_spine_body_cleanup.py b/tests/fea/test_spine_body_cleanup.py index eb1b960..277b849 100644 --- a/tests/fea/test_spine_body_cleanup.py +++ b/tests/fea/test_spine_body_cleanup.py @@ -59,7 +59,7 @@ def test_empty_body_is_rejected(): def test_substantial_body_cleanup_requires_review_not_exclusion(): - from ogo.fea.validation import evaluate + from ogo.fea.qc.validation import evaluate row = {'site': 'spine', 'body_cleanup_removed_fraction': 0.06} assert 'substantial_body_cleanup' in evaluate(row)['qc_reasons'] diff --git a/tests/fea/test_validation.py b/tests/fea/test_validation.py index 32931dd..6c3954f 100644 --- a/tests/fea/test_validation.py +++ b/tests/fea/test_validation.py @@ -2,7 +2,7 @@ import numpy as np -from ogo.fea.validation import evaluate, grid_measurements, write_measurements, generate_gallery +from ogo.fea.qc.validation import evaluate, grid_measurements, write_measurements, generate_gallery def test_disk_contact_uses_faces_not_diagonal_neighbors(): From c95060b310145de80726000a502ec504121a3079 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:08:24 -0600 Subject: [PATCH 63/67] Remove study-specific documentation and unused FEA helpers --- .gitattributes | 1 - docs/fea/vertebra_fea_quickstart.md | 8 -- ogo/fea/README.md | 2 - ogo/fea/femur.py | 50 ------------ ogo/fea/spine.py | 106 +------------------------ output/pdf/vertebra_fea_quickstart.pdf | Bin 14316 -> 0 bytes tests/fea/test_femur.py | 24 +----- 7 files changed, 3 insertions(+), 188 deletions(-) delete mode 100644 .gitattributes delete mode 100644 output/pdf/vertebra_fea_quickstart.pdf diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index d72fd52..0000000 --- a/.gitattributes +++ /dev/null @@ -1 +0,0 @@ -*.pdf binary diff --git a/docs/fea/vertebra_fea_quickstart.md b/docs/fea/vertebra_fea_quickstart.md index 35440f0..44daba7 100644 --- a/docs/fea/vertebra_fea_quickstart.md +++ b/docs/fea/vertebra_fea_quickstart.md @@ -20,14 +20,6 @@ provides vtkbone; solving additionally requires FAIM/N88 tools and a license. Use tools already on `PATH`, or add `--faim_bin_dir /path/to/n88/bin` to commands. A separate solver conda environment can be selected with `--faim_env NAME`. -On ARC, use the shared installation instead: - -```bash -source /work/boyd_lab/conda_init/ogo_fea.sh -``` - -Run generation and solving on a compute node, not the login node. - ## Generate and inspect Use density-calibrated QCT in mg/cm3 K2HPO4-equivalent units and a segmentation diff --git a/ogo/fea/README.md b/ogo/fea/README.md index 7f28631..15032a7 100644 --- a/ogo/fea/README.md +++ b/ogo/fea/README.md @@ -48,8 +48,6 @@ FAIM/N88 tools must be on `PATH`, or supplied through `--faim_bin_dir DIR`, `--faim_install_root DIR` or `--faim_env ENV`. Individual command overrides are also available. Ogo does not search the filesystem for installations. Use `--faim_license_dir DIR` when the license is not already configured. -On ARC, `source /work/boyd_lab/conda_init/ogo_fea.sh` configures the shared -environment; run generation and solving on a compute node. ## Model conventions diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 8750c8a..3d3e290 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -933,56 +933,6 @@ def pad_vtk_images_to_foreground_margin( } -def swap_xz_footprint(bounds): - """Swap x/z footprint dimensions of physical bounds around the same center.""" - x_center = (float(bounds[0]) + float(bounds[1])) / 2.0 - z_center = (float(bounds[4]) + float(bounds[5])) / 2.0 - x_length = float(bounds[1]) - float(bounds[0]) - z_length = float(bounds[5]) - float(bounds[4]) - return ( - x_center - z_length / 2.0, - x_center + z_length / 2.0, - float(bounds[2]), - float(bounds[3]), - z_center - x_length / 2.0, - z_center + x_length / 2.0, - ) - - -def expand_z_footprint(bounds, extension_mm): - """Expand physical bounds along z around the same center.""" - z_center = (float(bounds[4]) + float(bounds[5])) / 2.0 - z_length = float(bounds[5]) - float(bounds[4]) + float(extension_mm) - if z_length <= 0: - raise ValueError("Expanded z footprint length must be positive.") - return ( - float(bounds[0]), - float(bounds[1]), - float(bounds[2]), - float(bounds[3]), - z_center - z_length / 2.0, - z_center + z_length / 2.0, - ) - - -def expand_xz_footprint(bounds, *, x_extension_mm=0.0, z_extension_mm=0.0): - """Expand physical bounds along x and z around the same center.""" - x_center = (float(bounds[0]) + float(bounds[1])) / 2.0 - z_center = (float(bounds[4]) + float(bounds[5])) / 2.0 - x_length = float(bounds[1]) - float(bounds[0]) + float(x_extension_mm) - z_length = float(bounds[5]) - float(bounds[4]) + float(z_extension_mm) - if x_length <= 0 or z_length <= 0: - raise ValueError("Expanded x/z footprint lengths must be positive.") - return ( - x_center - x_length / 2.0, - x_center + x_length / 2.0, - float(bounds[2]), - float(bounds[3]), - z_center - z_length / 2.0, - z_center + z_length / 2.0, - ) - - # ----------------------------------------------------------------------------- # Hip sideways-fall workflow builder # ----------------------------------------------------------------------------- diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index 94efb07..cde7cd9 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -8,7 +8,6 @@ import argparse import ogo.fea.material_laws -import matplotlib.pyplot as plt import numpy as np import pandas as pd import SimpleITK as sitk @@ -880,38 +879,6 @@ def get_icp_with_scaling( return vtk_transform -def get_icp(body, reference_path): - - """Return the legacy VTK reference-to-body ICP transform.""" - mcubes = perform_marching_cubes(body) - mcubes_output = mcubes.GetOutput() - reference_bone = ogo.readPolyData(reference_path) - - icp = vtk.vtkIterativeClosestPointTransform() - icp.SetTarget(mcubes_output) - icp.SetSource(reference_bone) - - icp.StartByMatchingCentroidsOn() - icp.GetLandmarkTransform().SetModeToRigidBody() - icp.SetMeanDistanceModeToRMS() - icp.SetMaximumMeanDistance(0.05) - icp.CheckMeanDistanceOn() - icp.SetMaximumNumberOfLandmarks(DEFAULT_SPINE_REGISTRATION_LANDMARKS) - icp.SetMaximumNumberOfIterations(DEFAULT_SPINE_REGISTRATION_ITERATIONS) - icp.Update() - - ogo.message("ICP Matrix:") - print_matrix(icp.GetMatrix()) - return icp - -def transform_resample(image, matrix, iso_resolution, interpolation='cubic'): - """Reslice an image using the shared physical-space transform helper.""" - output = ogo.transformResample(image, matrix, iso_resolution, interpolation=interpolation) - if output.GetNumberOfPoints() == 0: - ogo.message("Reslice output contains no points. Check the input and transformation.") - return output - - def _matrix_rotation_translation(matrix): rotation = np.asarray( [[matrix.GetElement(row, col) for col in range(3)] for row in range(3)], @@ -943,7 +910,6 @@ def identify_boundary_surface(input_vtk_image, top_value, bottom_value): # Apply boundary conditions from the original method eroded_mask = binary_erosion(binary_mask, iterations=1) - #dilated_mask = binary_dilation(binary_mask, iterations=1) outline_mask = (eroded_mask == 0) & (binary_mask == 1) # Highlight the outline @@ -1004,7 +970,6 @@ def generate_cortical_mask(image_vtk, mask_vtk, threshold=0.2, min_th=1, max_th= # Convert VTK images to numpy arrays image_np = vtk_to_numpy(image_vtk.GetPointData().GetScalars()).reshape(image_vtk.GetDimensions(), order='F').astype(np.float32) - #image_np = gaussian_filter(image_np, sigma=1) # Adjust sigma as needed mask_np = vtk_to_numpy(mask_vtk.GetPointData().GetScalars()).reshape(mask_vtk.GetDimensions(), order='F') > 0 @@ -1057,31 +1022,17 @@ def convert_image_to_material(image, mask, n_bins=128, cort_mask=None): cort_mask_change = ogo.prepareFiniteElementImage(cort_mask) if cort_mask is not None else None connected_image = ogo.imageConnectivity(change) thr_image = ogo.bmd_preprocess(connected_image, -31) - #this was done previously - but if someone wants this they should just do it in the calib functions - #ash_image = ogo.bmd_K2hpo4ToAsh(thr_image) cast_image = ogo.cast2short(thr_image) bone_image = ogo.applyMaskByArray(cast_image, mask_change) binned_image, bin_centers = ogo.density2materialID(bone_image, n_bins=n_bins, cort_mask=cort_mask_change) return binned_image, bin_centers -def resolve_func(func_or_name, module): - """Resolve a material-law function by object or configured name.""" - from ogo.fea.materials import resolve_material_func - - return resolve_material_func(func_or_name, module) - ###################################################################### QUALITY CONTROL ## Functions to check image and boundary conditions ###################################################################### QUALITY CONTROL ## Functions to check the results -def check_image_values(image): - """Log the distinct scalar values in a VTK image.""" - array = vtk.util.numpy_support.vtk_to_numpy(image.GetPointData().GetScalars()) - unique_values = np.unique(array) - ogo.message("Unique values in the image:", unique_values) - def calculate_features(image, label): """Measure a label's physical volume, centroid, axes and bounding box.""" label_map = sitk.BinaryThreshold(image, lowerThreshold=label, upperThreshold=label, insideValue=1, outsideValue=0) @@ -1117,39 +1068,6 @@ def parse_filename(filepath): 'filename': '_'.join(parts[:-3]) if len(parts) > 3 else base_name, } -def visualize_slice(image, filepath): - """Save a middle x-slice PNG for optional legacy debugging.""" - dimensions = image.GetDimensions() # (x, y, z) - mid_x = dimensions[0] // 2 # Middle slice along the X-axis - - # Convert vtkImageData to a NumPy array - vtk_array = vtk_to_numpy(image.GetPointData().GetScalars()) - numpy_array = vtk_array.reshape(dimensions[::-1]) # Reverse dimensions to match NumPy order (z, y, x) - - # Extract the middle X slice - slice_img = numpy_array[:, :, mid_x] - - # Clip values at the 90th percentile - percentile_90 = np.percentile(slice_img, 90) - clipped_slice = np.clip(slice_img, 0, percentile_90) # Cap values at the 90th percentile - - # Normalize the clipped slice to [0, 1] range - normalized_slice = (clipped_slice - clipped_slice.min()) / (clipped_slice.max() - clipped_slice.min() + 1e-8) - - # Plot the normalized slice - plt.figure(figsize=(4.0, 4.0)) - plt.imshow(normalized_slice, cmap='viridis', origin="lower") - plt.title('Middle X', fontsize=9, pad=2) - plt.xlabel('Y', fontsize=8) - plt.ylabel('Z', fontsize=8) - plt.tick_params(axis='both', which='major', labelsize=8, length=2.5, pad=1.5) - - # Save the output image - plt.tight_layout(pad=0.2) - plt.savefig(filepath, dpi=300) - plt.close() - ogo.message(f"Slice image saved to {filepath}") - def check_image(vtkimage, output_filename=None): """Run legacy body/process image checks and optionally save their results.""" try: @@ -1746,9 +1664,8 @@ def process_vertebra( ) ogo.message(f"relabelling mask and identifying boundary surfaces...") - # Assuming mask1_data and mask2_data are your initial binary masks - mask1_labeled = label_mask(transformed_vertebra, 2) # Apply label 1 to the first mask - mask2_labeled = label_mask(transformed_process, 1) # Apply label 2 to the second mask + mask1_labeled = label_mask(transformed_vertebra, 2) + mask2_labeled = label_mask(transformed_process, 1) # Combine masks here (otherwise it does weird interpolations) combined_masks = add_masks(mask1_labeled.GetOutput(), mask2_labeled.GetOutput()) # Combine the labeled masks @@ -1757,15 +1674,9 @@ def process_vertebra( boundary_masks = identify_boundary_surface(combined_masks.GetOutput(), bottom_node_set_id, top_node_set_id) ogo.message("creating cortical mask....") - #1 - 3 alterantive (to create a continous cortical shell - changed to 0 5 ) cort_mask = generate_cortical_mask(transformed_image, combined_masks.GetOutput(), threshold=1, min_th=2, max_th=5) - #writer = vtk.vtkXMLImageDataWriter() - #writer.SetFileName('/home/matthias.walle/work/fem/CT_FE_TEMPLATE/MODELS/10001_QCT_vertebra_20_cortmask.vti') - #writer.SetInputData(cort_mask) # For VTK 8+ - #writer.Write() - ogo.message(f"converting image to material ID...") # This converts the raw image to a material ID mapped image n_bins = 128 @@ -1957,7 +1868,6 @@ def process_vertebra( padded_mask, inferior_disk, superior_disk], [None, bottom_node_set_id, top_node_set_id]) # This is the final model that we will use for the FEA. - # For the future --> pull out material table to have option without disks ogo.message("generating n88model file...") model = create_microfe_model( image_with_pads, @@ -2106,20 +2016,8 @@ def build_parser(): help="Sets the yield strength in compression for PMMA material in the FE model. (default: %(default)s [MPa])") parser.add_argument("--pmma_yield_tension", type=float, default=None, help="Sets the yield strength in tension for PMMA material in the FE model. (default: %(default)s [MPa])") -# parser.add_argument("--bone_yield_compression", type=float, default=None, -# help="Sets the yield strength in compression for bone material in the FE model. (default: %(default)s [MPa])") -# parser.add_argument("--bone_yield_tension", type=float, default=None, -# help="Sets the yield strength in tension for bone material in the FE model. (default: %(default)s [MPa])") -# parser.add_argument("--cort_elastic_Emax", type=float, -# help="Sets the maximum elastic modulus for cortical bone in the FE model. (default: %(default)s)") -# parser.add_argument("--cort_elastic_exponent", type=float, -# help="Sets the elastic exponent for cortical bone in the FE model.") parser.add_argument("--cort_poissons_ratio", type=float, help="Sets the Poisson's ratio for cortical bone in the FE model.") -# parser.add_argument("--cort_yield_compression", type=float, -# help="Sets the yield strength in compression for cortical bone in the FE model.") -# parser.add_argument("--cort_yield_tension", type=float, -# help="Sets the yield strength in tension for cortical bone in the FE model.") parser.add_argument("--elastic_E_func", type=str, default="default_E", help="Function name for trabecular bone Young’s modulus. (default: default_E)") parser.add_argument("--yield_comp_func", type=str, diff --git a/output/pdf/vertebra_fea_quickstart.pdf b/output/pdf/vertebra_fea_quickstart.pdf deleted file mode 100644 index 92029bdceaa352e15565503fbe8b4faf142405c8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14316 zcmdVB%f7P8mM_>}PcZ>eKoL+75K$DoASj9mB7!0!;$76L%+ok8P}Qq`J?r6Hd+*GZ zd#&F&na#8@ftZ64j2I*SF)k7COc~%*mHrL>|Nh_q=l}mPA!6Ro%jNW5f-_n&`#<_}i>H-G+h$^Vn{`a@U0KH`7V72bPX5C56|YX8apeu?_${^WeD(m4Ci zbX5OOcEtRlBRQ8j*US5d`umtq;zJtt9{=*=EV|?a|O<_N3@7PIS1mG?g=aYmt05cEllFx->>rDuj-GVkN6K^K0djv z=L}33?gkQSJhw#6L2;5(a z@Jl!UZA9SzQiNZ+`EMfv^=qE~V>0}jCzHn^N3-QSo<8pR&9A0>e*b;i{tfEn@DpAi z$scAE^+z!7em|#2_JeGY@cy{}>+SJsDuYqBCrvqsh$Bu`QlNi#Hs zlZ_Hdmi|HaHwNGP`x!^+F^vzuv(Vyy{l6dC;r)Z;borP5pIGH!a`;9$03!dsw){dB zJ>6$v_OCzq&*l$n%C9bdfkqDS=Qzrjn+@{MA7G`QYyR>7ELx5`&u6|`5rgph{v*`< zK5+jS-W-i{oMV0nEzL#n^1EM#@;8hBo6-E56Xwq^voe2y=HG_ZKW4ASc- zmoG=l?m|{oj#1)#3H!AkUGri7`$Nu3kMGIn^iyLv`WLJBCs)Spw_*6XwG?7QBqj7d zCdo47ifI9=f{P383euGk3ytlhwGtk5+sLUvRM|`ClrY?* zv(@E82iKYRuFs9cRVs`;(vz_lY&~Anv}R4+W^s1v(xE-uxXFZlRgZoGygohJ96bb+ z=rD#3^+=QU-;I?oysHhBqy&SA4ECH>4)Uv20=Yr2Y*2E)P}IdUl6K%x)RnqWLoIIP zPK2>OrdNpIocRs6wW>K`yNgiA>zdxZh}CEr4BDFBp^PN_-se+kG++i@7gmaW%XJ3g z5>C6wd(uGPbSyTVM#p@DP^oH=^)`@5*AwOz337vMALZhtY)@`0Lei#;^%)PMmL``vDvt(xO!<+*)T zw*A87J*(Bmqq^u5<5n9rSUwGNRZu`P|v9f1JtB5*ej3Z;R`yz_U@q&V< zg4ry%U2}d{rh5uLP}pf|r^z=KLriY(R6p2yG_t=x+yM>x>k~g`zhR+X$$*WgPhqUMq$$atgHrEQfLH9LDX42%?Ia7D-RcFh+K2b-x66`);9orF2^ zeCH*NdUP*dI-h!)RnCGiSl5N;8>Ge_BShuvmzsVYxLfL!Al- z+x||Gmok2#-?t(14e{y-_ZB^SM17N?kXnNkTE4fEMrm<5ad!POGR^PI=k7*K)(~WL z&0dKqu36NW`Br^*cES{Npq>Lkt^~A#PHMECMQ*f{_jT~qEA1*LZ2W3o24_BrI(E(R zXm18rNqpz^lp5NS=H0e@D&vGiju!8!|7aHENpKYm+|bZ+3EWBwtnssheIl;w#;Z@Q z4d--Vk$vG*Z|=>uTZubo8eY>hH=g%L(i}f;$QEQmPDVCw#A}v_>jr<` z-bLUQNwMr6u90;;0wx%5-(^SkFY$9-;c;dqH9p=BLFj#79&W&8W<1~ldau1}=ro{| zsR~=&g>`ZSZn7r!z+JDsb_MwLtwFPKW{jBqbD@M3@>KWQAq&2@*V&p0?L)J4t`IMS zYqtu|iU-wx}3!D$JAn-{pk@xHFD9}i@0QrDhWi3Zx0-G$6>bBNmImGkz8 zo8ufcs23pD1y4CXNkk~um-9h7d7>Vc%?;l)OVuqd3s~oS8hp32Ml$JYv)u|_(S^p& zEnljgVZpovxjkO!?H^*S??uSWo zGH>1#Su*PfBcii5c@b|&`LRlf?6ZN^hq8tOQh5~&s5^}>X1lXaLLc86Zq{4C=~sKH zlOjiDJ9+Es3n(0`#q;Yail|FIfv4+>FXz;lOlw*r`~f zPf!s8e;cyZ@t7yrU3Gb7oWTw@N`-wJ!~r#3A6k@7-_SEdf6zO-+*iA5*=<=>qH-L6 z!eIDaXJYZax+tWkZX_ovj?RP{ZV}$Ndhk&{MAe5ojS-(8uL1|JoJO?Gxum3&;%cYR zu3fWt(J3DuTsUDD0x`!p(PPuaZsqTMo^ z$ZHcl3XImV#z<=ifU7bS-HF!ff&|^Zq;Gw1+pVv9ALYf*sx$d{xwvz`c<7e0rja8v z-u&p;=T>NX+W&0l`I5hTh=0$VP=CxX{Fi-By7ot(a}Nk{yPL6#l6mjd>+q%5F8f;v#Eb6*+s^&NX!&HX+_3qI-o3tzU5Z0Q zX2BY?xxQfMl?Ev(-Pe|e`oVXPj1HfB;u^PQHQO1DvjZQO%SyR+^O_gdyLj%iTlo+al#$<+3i>r!95fIa42SQ9dF~IgMJU&gvEv z&2q5aCOeh`sKi}v6~|$weGzunuewmIUi9y+78xVCN=`>z6rgu{cn`4co4zLwR9vxd zxp=f)+mlhF&wXoz^U$f3EQD{g)7HUZq`|wz_H3<;T5;0WTE(ftyda>f(5hCrM+oTDgx}4!bw^`pZ4MwuUiZCW=ZU^ zc>xw7&&?_=a%dP2j+CX#q*DSvHm~;R_kImePmH-bmAi}p0&@-QhbYn*P%#uGy*7>{V1qeTA;PF@3r=Ntv5}4 zK;_<%{FuUEu&!?CdKZxN0umuI^U18XS-@YBrb*HN~-0)fs20POY zH3Ku0bK}-Pg=+N(p6dN@BsdW!H%fuX%n=O^j4+)elXu+n?< zx!zIgXwgD`)jqq6so}a)BrjY5l+!o5d{Y@I}%4RKI4+H6$MfR@Z^m@y! zpe|DNAUfX!k%pVzVbN8#Pw`sp%TFYA;)q0h#O4&92885|fMaubet3I6>c&30hS~*t zBJtjU=)ck{ahM#4Hw5_Y^!B7Cz5Nq3$Afk|Rj8#Xl%Q&Vs*G1`0X)%qfLBpXIjfs^ zJawLXG`dgyI|>#lrSHXfpoc%Um`podL(2 zd@mzM>SL}N5faOVkv$sKJ^D;s@HNqBv;!o#P_6DuHDb~1^T-7E?!*k_zyjCOQ?jnt zZHb|kV~48&n?>rU4LZf}Eet{%M}cf7m&e9dRV?PXq> z$`OT=%g!d|m<1~DK3zR3%~f?D+bksj)%`3e*DsUJzO=*V(6*UK!a7tF8G_4vW_C6( z_#VIgE3)hl$^DdHwM&y}$McI|t810OR+(@0WYB3=k_qz|7Qajt9S-L^VI$+)`!cth z>w^6Rh$BxQPogtgz2qnv8Xf8~<1x#)6%4##w+_>6u^MbA*TYTSC))%Iy7U?S(9&M^ z``4y2-L+OnwKq&$TmquQh9npDZ0?Kh!xmFu+7&yj*CF6#iKHvR&_$-nLE99b{mp{s zqfh;?#FR7t;@VrNB}~%2rs4cwd6IuDApKW8Nt$K<*^^ABC|oHvjppL|5&&rmkT;x~ z;t;;Ugc-VkRv8kBv52pG6j^BSkA>tK!VJ5t;Jey|SocxtY+d8TnRI%6+PRBJIj5?r z>9x+b$$h@pp;Q-R@-P$?hXO|F3usfb2_(022#O}_Vw@b+-B=}=}Uz! zR=rL))Z-Q9nL+$DN2jOc!e5>kYwvOF%mi-S1d_oze4an&Ji!T9ei3H=OYeRu1`By(`|7tgGHQb&yn75*^K4r81gF#7?o?P_W~22q z`M!^cbQZ%#dK`+)6jbihTMv>d>$pK;&5FxZhT$b29rIxolhL)}h;F2eveVM(f^qq# zxtj+eVC<*Rd63t12XsnYo&`{d{#mlNwOb3Ny-OWsi#tt?U*%`{fTMw5_vUX1Uyl1b zdaNEXp!MEcIPj*1N-y5!+Q=7-(RqLbh+z{E<&r( zW&prYz%~GK}=Jgr!ll{EC`QzT~zhkr>akb znLtPP+;2l}?*Eo9SGm#|7GmzuI$$NztIzI*>rn`SD^qO~$Ay|+kSiFuLYC^p7p5Kk z5f4B1#CFG}`(3HtG34|#Mu-n=H$xUv`V29oIpi)eQnfTLl)%ZHrMpU zLZd)FtCRi`ZLFUKfLsperJ2^j%2jWi&1tfNd=}qL5gJwW3hWPOIQF8duz;_1>l({* z&#XZ~{jhj|yK=y$Zki)YmK4dyv3|#f%)Y7`_)C3C%i)M4tcNFUV!hRm=#jiv$@jPD zL&$KpcpX1r9=JoN>TBB@1WekqfB^zqVR~wubw9@-+P)C;6AC`p;|V;aL;-HLy>4bR z;LBP@&f2Yh_WWBWtfQM7MHeQE(`X8h^+QeTf|9cT(wm##RL1+2``c|CoLtsJwCVGr!Om|SRtb2 zcgWL=awAy2FqFN`m*;M9>LtzDDCw@>;3YARA*IsuQy;Jb9|!84p1~B9D7qQTM&F83 z-`pNg@^@lLR^YPav;h}|P9Gn0wfpzfvBTpF>h;!-1w1*^%B$5qjqESql{<-l+k94~ zTkb+>7Zs@Vv@|wuy2rj=T`I$lUGZ@lOl)e|!U1Sn0=C1~K*In+G@fkq1)EvB3S2DT zIlUU>5DrUd4=+pn3CY?|DgN&79$v8~r(@j8pNPK?XM8yIj!ZS|=q0grf1J28(gL&T0g3xT`@8g4Q)Z7Jbc#!!Vyj_Z z5w_fg3r=(>Zk=6m=Z^>BXa+NQbJ?)StCCbrsc&Sy3d8rrarYV7F#TI!;S?k_pZ?Jjdt=_H_2nz3#)~+Iou)}SZWY4-%u=P zK?yK>(4V&?sb3=P>;aSS`vz1{mf@8|2*S?=mRc^o<27IG%`3_^EtlJHRlRK%90fhhJwG{>7MmC`ob?**_H=6f+E!}cU`uXhg_pA0Pq@yN zO8Q(?CY%~)OV+Lh?brN1Kzi|qiZ*L#z5kA&lk-^x6yx8-K^a-*nK7FE2KvI6!BG_X zMRs5dpLsD4=%qEc=wlQRT$|ZpR`uZh8Q`0nb&^?iUUz-?89VpORNO8ydEA1bv^-zz zA9mY*cr5*_R%>AR!J%XNXfk~LdU5lrS+~s}Z|F_8yN079#2R0&uRxm2QQRArx#>x6 zK0kn0y1dKsTG>AjMJ<}e#rSSF*YD%9>B#^!B)mcSuzaRzZIVN}+^XbpuKpf4L*jcz z8_TR%zD#wec>_+*~t>>(_wk{89NI}Ev0V@W*3vub*4{OrnBVMX$9Hx29O0( z`}QhH1E~kt^Tzd4DShYnuet9<%dv#&C~(he^8{-bwg-KMw!U?FX~VZmd|^!`QY)X; z8>oZXLC>@edM7g5r$50h5yZ4+2COLht3bVaG$WOm&rM)JE#G&Y=z%PN;4WU4gA6@Ka{P~YCZ zad5cVdMobQXBy*K_wbIy7a{lji(njvO`f?R@_zW0Js0|F_ds#zJY(@R>)w?*ucp-M z=6x8l4y`=?uyy(ijI37a&1T#%3jNuW%NA4YTX2KCFbipmCwU)PEnuqV)zpgpo#Ihg z=r=I5V;uUllGK(DtC#HT!FAa>FV6wT>KeAN@ty5$ z5Cm%lWYN6ltrZjPpiR0xFkYuweNcKY<%v)lOks5C& z5p4}p4QTXTZUq}>phbSL5_`R}TnFm+L8%Ttuij|1I?PO}*J6x_>9ESS__-`f`KUH_ zjcIYie+qMi6s@xL1(QnwjTR&<_#B?j-$LUuP6jOzs1_@`dt}ImZ;yi-mQviGSLaC! zOpn5v8oOGwTvJs+BuB7HKJUSTD2e)w0hvP4tnZbYk+-XNA29#XNc5 z<)2o6aAmuLQ!`L?yRuPPd7=*1qgI7iP6sjE>9g4#{K{BXcOTWd!!O~1CRPyelV1F7 zbMZKT@4BBhHvTF+_J*#)Zz_8hVBr;$9=%RxuA6hyp6@oFZg*2=+2>hVc*BzL4Wc~i zvYS)|>2uulyT$Y;NpqbWm^MnMO~tzM(gLe-2-S$VZ3`W6Mrm2?lt|IeObM99+j-X$ z0vPXVV_~3vTQ586v|hq`X<0xVd#OTzvc%stmCWk*jwgkVm|3CZhiNc$ZeQ! zKu@XXZ8+AzCRix}gJPaCG&FNrfRaydJBFn%nLZv#X><*RAmUZRBecdL%(BJnE6Fh9{$SD&Xr;yPD-Ecgucpouq^YQ z&~Z%OORx{Z)kb6W7Cgr>D$J%k-DR?=$iTN_d$z$qQi#r`2)Y-gq(sk$ZMt0d>6mHb z9S}fQ;%lEZ2hCi}tM`CkzIsFReB0=XRJqR^5uEP4 zJAuc3aWiv^(-U2iYi0#lKSxQ$oY3}|=T%{*KGl#w^s;u}RAdh^%YX)E_IS?^uymaW zmFr@RO})i^PA~LlXSjySPRZ`zrc_!~-%QlrllW-_1zt4a=BTnB`g2#P&2hWk=gbYw z79mY(p8T~g>A-#fo?8_bSk<#{)kIG#j_lnQb)nJ2%XylArnH{0c9sf0n(Vu1EpCCy zW^5L##A6A~>P>>1<@j`p!}7a_%>{cc?2 z`pE6htcRsRULhHf<${cLJ}@fq$gTo3HkmcsHN+3u4%d*QjxFAp<*j+tsEgl!W3BT(bz4(iJQbnAq_>D$>B6`*cuB{!%QX(UmQcS@ z5ixY;zHcA%LSQ_SC$It3svWOa!jf8Wo+Wuo!67Jhc4&{=&Bz%Rsj~OJKpc=R{MqZ1 z?**xBuoe~BGa}x+*rQnd*4mZAvsy5P-7$MhX=KyRsCyR}%D;MUcU)2jI`Bb>YGudl9`nr7_tj zOnotd!9HHYkafGsTDhC1oEp@@fk1>G?n%}w^t;>6CI0O5Fj89Nsm84~sC9~^U!i{S zGKeNno#5}wW@Y}s+l_AI94lAUJ>RaIQrGQrit?P~!8WI6st}{z@*JaX?}JRm%-ja^ z6SL)$ap4MD&XV%q$*bbNZUbb~+VM$xx#Wvf=Zel`9JPP|1Wy-Joz6T`S*TxIu_oaVz2LGB14nD z*$o|6#HW-!6E=EV%#_eAxg=tlbEXT@6^ynAsHU28y3w29lK})Uz+jYbnW$Cp+qQi| zh+_poD19P1txHBY+pKvjA>8;Lgtt%6t7}%c672+-cj;XNKT-FTgmU!ug$huK&fW9%NZx!RUW@N zF>@bD$Me?^xqSZtj#k!glPVZtW&DowYMe7hF-)QJov1wzYfXqwz)80_v+DuZ?d>o9 zNppES6By}oo8h2{w~nx;^+@kRz|+S`p?8*uAsh=7q-++>e+K`Hz`y?a|+v89td*g+CDIMG)C(~1)~U~b;B(K-RSCo)3x zP-OW9ud@s-4@4V&SKvi|-URGx=X<;BT+?pQqfea>1F`BhjLCSxHBVzElu++|H2cux z`jUx5`?Ni`4GZGbZQNqAnY**`i}t~=!y4W6)Me`FA8~v81OPhB8RZ*q9Bik=aQ(n- z@iL5!u669(e1y<-QQmTtz}8BAr~)?HU4HfO?W}uZ!dbRn5rep{T?g~A=ZLq>sZi+% z=h61oDjjF0*XKI}g^j^kJo>hZWzjzG!qT#-9iH*pI5$ee^QOs$v*zl3-?RqBd92O& zv!J)qU02NF(gD?{i?%Pvo$@F<3#onbr4h80s$axe4}d}CqISCAR6fo7C&@+DwbJcD zfcPk8Sgq-a3P9?v<0`FxV%P znR3f3gHhOnM`z##Oz`S36k}Gd7I6o^$_rpuX=Y;m(wZYogPm59_$7Id^IuHzf63zG zZ{!cf{YL)V@29~qb>`>z^KWg9{2;)u-_QNu+NfW9;Qy(uM&*0l|EaBB|MRz+ z6J6sk=(yUS=qR%GCw^3o`ZFE<>+r&V?1Q4YKew@sKgmIH^q=_AOzlr}EXDkZj%RCm zCG0=+OVwC9-`oF>woewO*EswAfSaoP{N}Y7ileBQq>jf%SP%E{p4_tsjz1pgi09+H zO8S4M@PC8vMKE{&Pt4b07b7;_f#>y&oSxzaRE6f6I?uJ@Qjqzs`^34~z7F c#J`N=_p=3I_W9jhs5(oNzrk?d82t9X07&#E^Z)<= diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index e58682f..48e69db 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -137,7 +137,7 @@ def test_icp_transform_sidecar_round_trips_matrix(tmp_path): matrix = np.eye(4) matrix[:3, 3] = [1.5, -2.0, 3.25] - path = tmp_path / "sub-RETRO2_00000_left_icp.json" + path = tmp_path / "sub-001_left_icp.json" femur.write_icp_transform( path, @@ -166,28 +166,6 @@ def test_hip_fixture_defaults_use_fixed_thickness_and_intrusion(): assert femur.DEFAULT_PMMA_INTRUSION_MM == pytest.approx(6.0) -def test_swap_xz_footprint_preserves_center_and_swaps_lengths(): - swapped = femur.swap_xz_footprint((10, 50, 1, 3, 100, 120)) - - assert swapped == pytest.approx((20, 40, 1, 3, 90, 130)) - - -def test_expand_z_footprint_preserves_center(): - expanded = femur.expand_z_footprint((20, 40, 1, 3, 90, 130), 20) - - assert expanded == pytest.approx((20, 40, 1, 3, 80, 140)) - - -def test_expand_xz_footprint_preserves_center(): - expanded = femur.expand_xz_footprint( - (20, 40, 1, 3, 90, 130), - x_extension_mm=10, - z_extension_mm=20, - ) - - assert expanded == pytest.approx((15, 45, 1, 3, 80, 140)) - - def test_bbox_relative_fixture_bounds_scale_lateral_axes_to_model_bbox(): bounds = (10, 50, -20, 80, 100, 200) From cfd3cb2d79f4618c9bcce024a13018c783c66d89 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Fri, 9 Oct 2026 20:14:43 -0600 Subject: [PATCH 64/67] Remove obsolete FEA exports and move benchmark builders to tests --- ogo/fea/boundary.py | 48 ------ ogo/fea/femur.py | 35 ----- ogo/fea/model_export.py | 163 -------------------- ogo/fea/spine.py | 126 --------------- tests/fea/helpers.py | 171 +++++++++++++++++++++ tests/fea/test_boundary.py | 34 ---- tests/fea/test_femur.py | 33 ---- tests/fea/test_legacy_evidence.py | 2 +- tests/fea/test_model_export.py | 73 --------- tests/fea/test_spine.py | 5 +- tests/fea/test_spinefe_benchmark.py | 5 +- tests/fea/test_workflow_locked_defaults.py | 5 +- 12 files changed, 181 insertions(+), 519 deletions(-) delete mode 100644 ogo/fea/model_export.py create mode 100644 tests/fea/helpers.py delete mode 100644 tests/fea/test_model_export.py diff --git a/ogo/fea/boundary.py b/ogo/fea/boundary.py index d2a4226..b633b4a 100644 --- a/ogo/fea/boundary.py +++ b/ogo/fea/boundary.py @@ -165,24 +165,6 @@ def bbox_relative_contact_plane( } -def bounds_with_reference_extent(current_bounds, reference_bounds): - """Return bounds centered on ``current_bounds`` with ``reference_bounds`` extents.""" - import numpy as np - - current = np.asarray(current_bounds, dtype=float) - reference = np.asarray(reference_bounds, dtype=float) - if current.shape != (6,) or reference.shape != (6,): - raise ValueError("bounds must contain x/y/z min/max values.") - out = [] - for axis in range(3): - current_lo, current_hi = current[2 * axis], current[2 * axis + 1] - reference_lo, reference_hi = reference[2 * axis], reference[2 * axis + 1] - center = (current_lo + current_hi) / 2.0 - extent = reference_hi - reference_lo - out.extend([center - extent / 2.0, center + extent / 2.0]) - return tuple(float(value) for value in out) - - def foreground_voxel_center_bounds_from_mask(mask, *, origin, spacing): """Return x/y/z physical bounds of nonzero voxel centers.""" import numpy as np @@ -1702,36 +1684,6 @@ def generate_bone_cap_mask(mask, *, axis="x", direction="up", thickness=5, shape return largest_connected_component(output) -def generate_bone_cap_vtk( - labelled_vtk_image, - *, - label_value, - axis="x", - direction="up", - thickness=5, - shape="fit", - intrusion=None, - output_value=1, -): - """Generate a VTK PMMA/support cap from a labelled bone mask.""" - import numpy as np - - labels = vtk_image_to_numpy(labelled_vtk_image, processing_order=True) - cap = generate_bone_cap_mask( - labels == label_value, - axis=axis, - direction=direction, - thickness=thickness, - shape=shape, - intrusion=intrusion, - ) - return numpy_to_vtk_image( - (cap.astype(np.uint16) * int(output_value)), - labelled_vtk_image, - processing_order=True, - ) - - def generate_projected_material_disk_vtk( material_vtk_image, *, diff --git a/ogo/fea/femur.py b/ogo/fea/femur.py index 3d3e290..0994cbd 100644 --- a/ogo/fea/femur.py +++ b/ogo/fea/femur.py @@ -423,41 +423,6 @@ def scale_reference_point_cloud_to_sample( } -def scale_reference_to_sample_principal_lengths( - reference_polydata, - sample_polydata, - *, - min_scale=DEFAULT_FEMUR_REFERENCE_MIN_SCALE, - max_scale=DEFAULT_FEMUR_REFERENCE_MAX_SCALE, -): - """Scale a femur reference so its principal lengths match the sample.""" - import numpy as np - import vtk - - reference_lengths = principal_axis_lengths(reference_polydata) - sample_lengths = principal_axis_lengths(sample_polydata) - scale = sample_lengths / np.maximum(reference_lengths, 1.0e-6) - min_values = _scale_triplet(min_scale, "min_scale") - max_values = _scale_triplet(max_scale, "max_scale") - scale = np.clip(scale, min_values, max_values) - - transform = vtk.vtkTransform() - transform.Scale(float(scale[0]), float(scale[1]), float(scale[2])) - - transform_filter = vtk.vtkTransformPolyDataFilter() - transform_filter.SetInputData(reference_polydata) - transform_filter.SetTransform(transform) - transform_filter.Update() - - return transform_filter.GetOutput(), { - "reference_axis_lengths": reference_lengths.tolist(), - "sample_axis_lengths": sample_lengths.tolist(), - "scale_factors": scale.tolist(), - "min_scale": min_values.tolist(), - "max_scale": max_values.tolist(), - } - - def _vtk_image_from_array(array, template_vtk_image, *, origin, vtk_array_type=None): """Create a zero-based VTK image from an x/y/z NumPy array.""" import numpy as np diff --git a/ogo/fea/model_export.py b/ogo/fea/model_export.py deleted file mode 100644 index f15a9a1..0000000 --- a/ogo/fea/model_export.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Inspectable hip surfaces and opaque four-view QC from the actual FE mesh. - -These are visualization artifacts, not replacement solver models. No mesh -smoothing, scaling, or voxel modification is performed during export. -""" - -from pathlib import Path -import os -import warnings - -import numpy as np -import vtk -from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy - - -def export_femur_model(model, model_path, *, pmma_mat_id): - """Write a colored VTP surface and a four-view PNG beside an n88model. - - ModelRegion cell labels: 0 bone, 1 GT support, 2 FH loading support. The - supports are identified using their actual BC-node loading-axis positions. - Distal constraint nodes are shown in green in each QC view. - """ - prefix = Path(model_path).with_suffix("") - surface_path = prefix.with_name(prefix.name + "_surface.vtp") - png_path = prefix.with_name(prefix.name + "_3d.png") - points = vtk_to_numpy(model.GetPoints().GetData()) - bc = {} - for key, name in (("gt", "Greater_Trochanter_PMMA_Nodes"), - ("fh", "Femoral_Head_PMMA_Nodes"), - ("distal", "Distal_Femur_Nodes")): - ids = model.GetNodeSet(name) - if ids is None or not ids.GetNumberOfTuples(): - raise ValueError("Cannot export hip QC: missing %s." % name) - bc[key] = points[vtk_to_numpy(ids).astype(int)] - grid = vtk.vtkUnstructuredGrid() - grid.DeepCopy(model) - centers = vtk.vtkCellCenters() - centers.SetInputData(grid) - centers.Update() - y = vtk_to_numpy(centers.GetOutput().GetPoints().GetData())[:, 1] - material = grid.GetCellData().GetArray("MaterialID") or grid.GetCellData().GetScalars() - if material is None: - raise ValueError("Cannot export hip QC without cell material IDs.") - pmma = vtk_to_numpy(material) == pmma_mat_id - region = np.zeros(grid.GetNumberOfCells(), dtype=np.uint8) - gt_y, fh_y = bc["gt"][:, 1].mean(), bc["fh"][:, 1].mean() - near_gt = np.abs(y - gt_y) < np.abs(y - fh_y) - region[pmma & near_gt] = 1 - region[pmma & ~near_gt] = 2 - array = numpy_to_vtk(region, deep=True) - array.SetName("ModelRegion") - grid.GetCellData().AddArray(array) - geometry = vtk.vtkDataSetSurfaceFilter() - geometry.SetInputData(grid) - geometry.Update() - surface = geometry.GetOutput() - writer = vtk.vtkXMLPolyDataWriter() - writer.SetFileName(str(surface_path)) - writer.SetInputData(surface) - writer.SetDataModeToAppended() - writer.SetCompressorTypeToZLib() - if not writer.Write(): - raise RuntimeError("Could not write hip surface: %s" % surface_path) - outputs = {"surface_vtp": str(surface_path), "qc_3d_png": None, - "render_status": "unavailable", - "region_labels": {"0": "bone", "1": "GT support", "2": "FH loading support"}} - - colors = vtk.vtkLookupTable() - colors.SetNumberOfTableValues(3) - colors.Build() - for index, rgb in enumerate(((0.46, 0.65, 0.80), (0.89, 0.64, 0.25), (0.76, 0.24, 0.26))): - colors.SetTableValue(index, *rgb, 1) - normals = vtk.vtkPolyDataNormals() - normals.SetInputData(surface) - normals.SplittingOff() - normals.ConsistencyOn() - bounds = np.asarray(grid.GetBounds()).reshape(3, 2) - center = bounds.mean(axis=1) - size = np.ptp(bounds, axis=1) - radius = float(np.linalg.norm(size)) - window = vtk.vtkRenderWindow() - if window.GetClassName() == "vtkXOpenGLRenderWindow" and not os.environ.get("DISPLAY"): - window.Finalize() - outputs["render_message"] = "3D QC needs an X display or a headless EGL/OSMesa VTK build; surface VTP was saved." - warnings.warn(outputs["render_message"], RuntimeWarning, stacklevel=2) - return outputs - window.SetOffScreenRendering(1) - window.SetSize(1600, 1000) - window.SetMultiSamples(4) - views = (("Lateral", (3, 0, 0), (0, 0.5, 0.5, 1)), - ("Medial", (-3, 0, 0), (0.5, 0.5, 1, 1)), - ("Oblique anterior", (2.5, 0.4, -0.8), (0, 0, 0.5, 0.5)), - ("Oblique posterior", (-2.5, 0.4, 0.8), (0.5, 0, 1, 0.5))) - try: - for label, direction, viewport in views: - renderer = vtk.vtkRenderer() - renderer.SetBackground(1, 1, 1) - renderer.SetViewport(*viewport) - mapper = vtk.vtkPolyDataMapper() - mapper.SetInputConnection(normals.GetOutputPort()) - mapper.SetScalarModeToUseCellFieldData() - mapper.SetColorModeToMapScalars() - mapper.SelectColorArray("ModelRegion") - mapper.SetLookupTable(colors) - mapper.SetScalarRange(0, 2) - actor = vtk.vtkActor() - actor.SetMapper(mapper) - actor.GetProperty().SetOpacity(1) - actor.GetProperty().SetAmbient(0.3) - actor.GetProperty().SetDiffuse(0.7) - renderer.AddActor(actor) - cloud = vtk.vtkPolyData() - cloud_points = vtk.vtkPoints() - cloud_points.SetData(numpy_to_vtk(bc["distal"], deep=True)) - cloud.SetPoints(cloud_points) - vertices = vtk.vtkVertexGlyphFilter() - vertices.SetInputData(cloud) - node_mapper = vtk.vtkPolyDataMapper() - node_mapper.SetInputConnection(vertices.GetOutputPort()) - node_actor = vtk.vtkActor() - node_actor.SetMapper(node_mapper) - node_actor.GetProperty().SetColor(0.09, 0.39, 0.36) - node_actor.GetProperty().SetPointSize(3) - renderer.AddActor(node_actor) - text = vtk.vtkTextActor() - text.SetInput(label) - text.GetPositionCoordinate().SetCoordinateSystemToNormalizedViewport() - text.SetPosition(0.025, 0.94) - text.GetTextProperty().SetFontSize(23) - text.GetTextProperty().SetColor(0.15, 0.15, 0.15) - renderer.AddActor2D(text) - if label == "Lateral": - length_text = vtk.vtkTextActor() - length = float(bc["gt"][:, 2].min() - np.median(bc["distal"][:, 2])) - length_text.SetInput("GT disk edge to distal BC: %.1f mm" % length) - length_text.GetPositionCoordinate().SetCoordinateSystemToNormalizedViewport() - length_text.SetPosition(0.025, 0.035) - length_text.GetTextProperty().SetFontSize(22) - length_text.GetTextProperty().SetColor(0.15, 0.15, 0.15) - renderer.AddActor2D(length_text) - camera = renderer.GetActiveCamera() - camera.ParallelProjectionOn() - camera.SetFocalPoint(*center) - camera.SetPosition(*(center + np.asarray(direction) * radius)) - camera.SetViewUp(0, 1, 0) - window.AddRenderer(renderer) - # Fit the projected bounds in each viewport, including long shafts. - renderer.ResetCamera() - camera.SetParallelScale(camera.GetParallelScale() * 1.1) - renderer.ResetCameraClippingRange() - window.Render() - capture = vtk.vtkWindowToImageFilter() - capture.SetInput(window) - capture.Update() - png = vtk.vtkPNGWriter() - png.SetFileName(str(png_path)) - png.SetInputConnection(capture.GetOutputPort()) - png.Write() - if png.GetErrorCode(): - raise RuntimeError("Could not write hip QC: %s" % png_path) - finally: - window.Finalize() - return dict(outputs, qc_3d_png=str(png_path), render_status="rendered") diff --git a/ogo/fea/spine.py b/ogo/fea/spine.py index cde7cd9..e5eaa4c 100644 --- a/ogo/fea/spine.py +++ b/ogo/fea/spine.py @@ -264,132 +264,6 @@ def spine_process_orientation_metrics_for_rotation( } -def benchmark_linear_params(): - """Return the linear spineFE-benchmark model parameters.""" - return { - "poissons_ratio": DEFAULT_SPINE_POISSONS_RATIO, - "pmma_mat_id": BENCHMARK_PMMA_MATERIAL_ID, - "pmma_E": DEFAULT_SPINE_PMMA_E_MPA, - "pmma_v": DEFAULT_SPINE_PMMA_POISSONS_RATIO, - "top_node_set_id": 6, - "bottom_node_set_id": 5, - "top_direction": (0, 0, 1), - "bottom_direction": (0, 0, -1), - "fe_displacement": BENCHMARK_LINEAR_FE_DISPLACEMENT_MM, - "target_displacement_percent": BENCHMARK_LINEAR_TARGET_DISPLACEMENT_PERCENT, - "pmma_yield_compression": None, - "pmma_yield_tension": None, - "cort_poissons_ratio": None, - "elastic_E_func": "kopperdahl_trab_E", - "yield_comp_func": None, - "yield_tens_func": None, - "cort_elastic_E_func": "kopperdahl_trab_E", - "cort_yield_comp_func": None, - "cort_yield_tens_func": None, - } - - -def benchmark_nonlinear_params(): - """Return the nonlinear spineFE-benchmark model parameters.""" - params = benchmark_linear_params() - params.update( - { - "fe_displacement": BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM, - "target_displacement_percent": BENCHMARK_NONLINEAR_TARGET_DISPLACEMENT_PERCENT, - "pmma_yield_compression": 70.0, - "pmma_yield_tension": 70.0, - "yield_comp_func": "kopperdahl_trab_yc", - "yield_tens_func": "kopperdahl_trab_yc", - "cort_yield_comp_func": "kopperdahl_trab_yc", - "cort_yield_tens_func": "kopperdahl_trab_yc", - } - ) - return params - - -def label_mask_from_vtk(vtk_image, condition_fn): - """Create a binary VTK mask using a NumPy condition over voxel labels.""" - import numpy as np - import vtk - from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy - - arr = vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape( - vtk_image.GetDimensions(), order="F" - ) - out_arr = condition_fn(arr).astype(np.uint8) - out_vtk = vtk_image.NewInstance() - out_vtk.DeepCopy(vtk_image) - out_vtk.GetPointData().SetScalars( - numpy_to_vtk(out_arr.ravel(order="F"), deep=True, array_type=vtk.VTK_UNSIGNED_CHAR) - ) - return out_vtk - - -def prepare_benchmark_images(input_image_path, input_mask_path, n_bins=128): - """Reproduce the spineFE-benchmark notebook image and mask preparation.""" - import numpy as np - - input_image = read(str(input_image_path)).GetOutput() - input_mask_with_disk = read(str(input_mask_path)).GetOutput() - - cortical_mask = label_mask_from_vtk(input_mask_with_disk, lambda x: np.isin(x, [2, 4])) - disk_mask = label_mask_from_vtk(input_mask_with_disk, lambda x: np.isin(x, [5, 6])) - vertebra_mask = label_mask_from_vtk( - input_mask_with_disk, - lambda x: np.where(np.isin(x, [1, 2]), 1, np.where(np.isin(x, [3, 4]), 2, 0)), - ) - - binned_image, bin_centers = convert_image_to_material( - input_image, vertebra_mask, n_bins=n_bins, cort_mask=cortical_mask - ) - image_with_disk = merge_vtk_images( - [binned_image, disk_mask], - [None, 300], - overwrite_existing=False, - ) - return image_with_disk, input_mask_with_disk, bin_centers - - -def build_benchmark_sample_model(input_image_path, input_mask_path, output_model_path, nonlinear=False): - """Build the public spineFE-benchmark sample model and write it to disk.""" - from ogo.fea.model import create_microfe_model, write_model - from ogo.util.faim import set_prescribed_displacement_from_percent - - image_with_disk, mask_with_disk, bin_centers = prepare_benchmark_images( - input_image_path, input_mask_path - ) - params = benchmark_nonlinear_params() if nonlinear else benchmark_linear_params() - target_displacement_percent = params.pop("target_displacement_percent") - model = create_microfe_model(image_with_disk, mask_with_disk, bin_centers, **params) - write_model(model, output_model_path) - set_prescribed_displacement_from_percent( - output_model_path, - report_profile="spine", - failure_axis="z", - target_displacement_percent=target_displacement_percent, - displacement_sign=-1.0 if params["fe_displacement"] < 0 else 1.0, - ) - return model - - -def find_spinefe_benchmark_dir(start_dir=None, env_var="SPINEFE_BENCHMARK_DIR"): - """Locate the optional public spineFE-benchmark checkout.""" - import os - - env_path = os.environ.get(env_var) - if env_path: - candidate = Path(env_path).expanduser() - if candidate.exists(): - return candidate - - base = Path(start_dir or Path.cwd()).resolve() - for parent in [base] + list(base.parents): - candidate = parent / "spineFE-benchmark" - if candidate.exists(): - return candidate - return None - - # ----------------------------------------------------------------------------- # Spine compression workflow builder # ----------------------------------------------------------------------------- diff --git a/tests/fea/helpers.py b/tests/fea/helpers.py new file mode 100644 index 0000000..f88cabb --- /dev/null +++ b/tests/fea/helpers.py @@ -0,0 +1,171 @@ +"""Fixtures and builders for FEA regression tests.""" + +from pathlib import Path + +import numpy as np +import pytest +import vtk +import vtkbone +from vtk.util.numpy_support import numpy_to_vtkIdTypeArray, vtk_to_numpy + +from ogo.fea.spine import ( + DEFAULT_SPINE_POISSONS_RATIO, + BENCHMARK_PMMA_MATERIAL_ID, + DEFAULT_SPINE_PMMA_E_MPA, + DEFAULT_SPINE_PMMA_POISSONS_RATIO, + BENCHMARK_LINEAR_FE_DISPLACEMENT_MM, + BENCHMARK_LINEAR_TARGET_DISPLACEMENT_PERCENT, + BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM, + BENCHMARK_NONLINEAR_TARGET_DISPLACEMENT_PERCENT, + read, + convert_image_to_material, + merge_vtk_images, +) +from tests.fea.test_gt_disk_crop import vtk_image + + +@pytest.fixture +def hip_model(): + import ogo.util.Helper as helper + + data = np.zeros((4, 8, 12), dtype=np.uint16) + data[1:3, 2:6, 1:11] = 1 + data[1:3, 1:2, 7:10] = 5000 + data[1:3, 6:7, 8:11] = 5000 + grid = helper.Image2Mesh(vtk_image(data)) + model = vtkbone.vtkboneFiniteElementModel() + model.ShallowCopy(grid) + xyz = vtk_to_numpy(model.GetPoints().GetData()) + for name, active in (("Femoral_Head_PMMA_Nodes", xyz[:, 1] == xyz[:, 1].max()), + ("Greater_Trochanter_PMMA_Nodes", xyz[:, 1] == xyz[:, 1].min()), + ("Distal_Femur_Nodes", xyz[:, 2] == xyz[:, 2].min())): + ids = numpy_to_vtkIdTypeArray(np.flatnonzero(active), deep=True) + ids.SetName(name) + model.AddNodeSet(ids) + return model + + +def benchmark_linear_params(): + """Return the linear spineFE-benchmark model parameters.""" + return { + "poissons_ratio": DEFAULT_SPINE_POISSONS_RATIO, + "pmma_mat_id": BENCHMARK_PMMA_MATERIAL_ID, + "pmma_E": DEFAULT_SPINE_PMMA_E_MPA, + "pmma_v": DEFAULT_SPINE_PMMA_POISSONS_RATIO, + "top_node_set_id": 6, + "bottom_node_set_id": 5, + "top_direction": (0, 0, 1), + "bottom_direction": (0, 0, -1), + "fe_displacement": BENCHMARK_LINEAR_FE_DISPLACEMENT_MM, + "target_displacement_percent": BENCHMARK_LINEAR_TARGET_DISPLACEMENT_PERCENT, + "pmma_yield_compression": None, + "pmma_yield_tension": None, + "cort_poissons_ratio": None, + "elastic_E_func": "kopperdahl_trab_E", + "yield_comp_func": None, + "yield_tens_func": None, + "cort_elastic_E_func": "kopperdahl_trab_E", + "cort_yield_comp_func": None, + "cort_yield_tens_func": None, + } + + +def benchmark_nonlinear_params(): + """Return the nonlinear spineFE-benchmark model parameters.""" + params = benchmark_linear_params() + params.update( + { + "fe_displacement": BENCHMARK_NONLINEAR_FE_DISPLACEMENT_MM, + "target_displacement_percent": BENCHMARK_NONLINEAR_TARGET_DISPLACEMENT_PERCENT, + "pmma_yield_compression": 70.0, + "pmma_yield_tension": 70.0, + "yield_comp_func": "kopperdahl_trab_yc", + "yield_tens_func": "kopperdahl_trab_yc", + "cort_yield_comp_func": "kopperdahl_trab_yc", + "cort_yield_tens_func": "kopperdahl_trab_yc", + } + ) + return params + + +def label_mask_from_vtk(vtk_image, condition_fn): + """Create a binary VTK mask using a NumPy condition over voxel labels.""" + import numpy as np + import vtk + from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy + + arr = vtk_to_numpy(vtk_image.GetPointData().GetScalars()).reshape( + vtk_image.GetDimensions(), order="F" + ) + out_arr = condition_fn(arr).astype(np.uint8) + out_vtk = vtk_image.NewInstance() + out_vtk.DeepCopy(vtk_image) + out_vtk.GetPointData().SetScalars( + numpy_to_vtk(out_arr.ravel(order="F"), deep=True, array_type=vtk.VTK_UNSIGNED_CHAR) + ) + return out_vtk + + +def prepare_benchmark_images(input_image_path, input_mask_path, n_bins=128): + """Reproduce the spineFE-benchmark notebook image and mask preparation.""" + import numpy as np + + input_image = read(str(input_image_path)).GetOutput() + input_mask_with_disk = read(str(input_mask_path)).GetOutput() + + cortical_mask = label_mask_from_vtk(input_mask_with_disk, lambda x: np.isin(x, [2, 4])) + disk_mask = label_mask_from_vtk(input_mask_with_disk, lambda x: np.isin(x, [5, 6])) + vertebra_mask = label_mask_from_vtk( + input_mask_with_disk, + lambda x: np.where(np.isin(x, [1, 2]), 1, np.where(np.isin(x, [3, 4]), 2, 0)), + ) + + binned_image, bin_centers = convert_image_to_material( + input_image, vertebra_mask, n_bins=n_bins, cort_mask=cortical_mask + ) + image_with_disk = merge_vtk_images( + [binned_image, disk_mask], + [None, 300], + overwrite_existing=False, + ) + return image_with_disk, input_mask_with_disk, bin_centers + + +def build_benchmark_sample_model(input_image_path, input_mask_path, output_model_path, nonlinear=False): + """Build the public spineFE-benchmark sample model and write it to disk.""" + from ogo.fea.model import create_microfe_model, write_model + from ogo.util.faim import set_prescribed_displacement_from_percent + + image_with_disk, mask_with_disk, bin_centers = prepare_benchmark_images( + input_image_path, input_mask_path + ) + params = benchmark_nonlinear_params() if nonlinear else benchmark_linear_params() + target_displacement_percent = params.pop("target_displacement_percent") + model = create_microfe_model(image_with_disk, mask_with_disk, bin_centers, **params) + write_model(model, output_model_path) + set_prescribed_displacement_from_percent( + output_model_path, + report_profile="spine", + failure_axis="z", + target_displacement_percent=target_displacement_percent, + displacement_sign=-1.0 if params["fe_displacement"] < 0 else 1.0, + ) + return model + + +def find_spinefe_benchmark_dir(start_dir=None, env_var="SPINEFE_BENCHMARK_DIR"): + """Locate the optional public spineFE-benchmark checkout.""" + import os + + env_path = os.environ.get(env_var) + if env_path: + candidate = Path(env_path).expanduser() + if candidate.exists(): + return candidate + + base = Path(start_dir or Path.cwd()).resolve() + for parent in [base] + list(base.parents): + candidate = parent / "spineFE-benchmark" + if candidate.exists(): + return candidate + return None diff --git a/tests/fea/test_boundary.py b/tests/fea/test_boundary.py index 8195635..0f22c5c 100644 --- a/tests/fea/test_boundary.py +++ b/tests/fea/test_boundary.py @@ -6,7 +6,6 @@ bbox_relative_contact_bounds, bbox_relative_contact_direction, bbox_relative_contact_plane, - bounds_with_reference_extent, foreground_voxel_center_bounds_from_mask, generate_bone_cap_mask, generate_projected_material_disk_mask, @@ -102,15 +101,6 @@ def test_bbox_relative_contact_bounds_can_enforce_square_footprint(): assert contact_bounds == pytest.approx((8, 52, -20, 80, 103, 147)) -def test_bounds_with_reference_extent_preserves_current_center(): - current_bounds = (10, 20, -5, 35, 100, 133) - reference_bounds = (0, 49, 0, 43, 0, 34) - - bounds = bounds_with_reference_extent(current_bounds, reference_bounds) - - assert bounds == pytest.approx((-9.5, 39.5, -6.5, 36.5, 99.5, 133.5)) - - def test_foreground_voxel_center_bounds_from_mask_uses_xyz_array_order(): mask = np.zeros((8, 9, 10), dtype=np.uint8) mask[2:6, 3:8, 1:7] = 1 @@ -432,29 +422,6 @@ def test_anatomy_bone_cap_uses_axis_aligned_surface_depth_inferior(): assert np.flatnonzero(cap[:, y, z]).size <= 5 -def test_vtk_bone_cap_uses_requested_output_value(): - vtk = pytest.importorskip("vtk") - from vtk.util.numpy_support import numpy_to_vtk, vtk_to_numpy - - from ogo.fea.boundary import generate_bone_cap_vtk - - labels = np.zeros((12, 8, 8), dtype=np.uint16) - labels[4:7, 2:6, 2:6] = 12 - vtk_arr = numpy_to_vtk(np.swapaxes(labels, 0, 2).ravel(order="F"), deep=True) - image = vtk.vtkImageData() - image.SetDimensions(list(np.swapaxes(labels, 0, 2).shape)) - image.SetSpacing((1, 1, 1)) - image.SetOrigin((0, 0, 0)) - image.GetPointData().SetScalars(vtk_arr) - - cap = generate_bone_cap_vtk( - image, label_value=12, axis="x", direction="up", thickness=2, shape="box", output_value=5000 - ) - cap_np = vtk_to_numpy(cap.GetPointData().GetScalars()).reshape(cap.GetDimensions(), order="F") - - assert set(np.unique(cap_np)) == {0, 5000} - - def test_projected_material_disk_uses_fixed_thickness_and_anatomy_intrusion(): active = np.zeros((14, 14, 14), dtype=bool) active[4:10, 6:9, 4:10] = True @@ -655,7 +622,6 @@ def test_projected_material_disk_trimmed_stable_surface_excludes_protrusion_colu assert np.any(disk[5, :, 5]) - def test_projected_material_disk_trim_keeps_bumps_below_min_shift(): active = np.zeros((16, 16, 16), dtype=bool) active[4:12, 7:11, 4:12] = True diff --git a/tests/fea/test_femur.py b/tests/fea/test_femur.py index 48e69db..1505ac5 100644 --- a/tests/fea/test_femur.py +++ b/tests/fea/test_femur.py @@ -287,39 +287,6 @@ def test_proximal_sideways_fall_fixture_plane_does_not_reach_distal_shaft(): assert plane["center"][2] + plane["size"][0] / 2.0 == pytest.approx(120.0) -def test_scale_reference_to_sample_principal_lengths_clips_and_scales_origin(): - reference = _polydata_from_points( - [ - (-1.0, 0.0, 0.0), - (1.0, 0.0, 0.0), - (0.0, -2.0, 0.0), - (0.0, 2.0, 0.0), - (0.0, 0.0, -3.0), - (0.0, 0.0, 3.0), - ] - ) - sample = _polydata_from_points( - [ - (-0.5, 0.0, 0.0), - (0.5, 0.0, 0.0), - (0.0, -4.0, 0.0), - (0.0, 4.0, 0.0), - (0.0, 0.0, -6.0), - (0.0, 0.0, 6.0), - ] - ) - - scaled, metadata = femur.scale_reference_to_sample_principal_lengths( - reference, - sample, - min_scale=(0.8, 0.8, 0.75), - max_scale=(1.2, 1.2, 1.3), - ) - - assert metadata["scale_factors"] == pytest.approx([0.8, 1.2, 1.3]) - assert scaled.GetBounds() == pytest.approx((-0.8, 0.8, -2.4, 2.4, -3.9, 3.9)) - - def test_scale_reference_point_cloud_to_sample_preserves_reference_center(): np = pytest.importorskip("numpy") diff --git a/tests/fea/test_legacy_evidence.py b/tests/fea/test_legacy_evidence.py index af81d35..5396240 100644 --- a/tests/fea/test_legacy_evidence.py +++ b/tests/fea/test_legacy_evidence.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from tests.fea.test_model_export import hip_model +from tests.fea.helpers import hip_model def test_evidence_cache_retries_only_when_inputs_change(tmp_path): diff --git a/tests/fea/test_model_export.py b/tests/fea/test_model_export.py deleted file mode 100644 index 67ee111..0000000 --- a/tests/fea/test_model_export.py +++ /dev/null @@ -1,73 +0,0 @@ -"""Surface exports preserve model geometry and distinguish actual supports.""" - -import numpy as np -import pytest -import vtk -import vtkbone -from vtk.util.numpy_support import numpy_to_vtkIdTypeArray, vtk_to_numpy - -from tests.fea.test_gt_disk_crop import vtk_image - - -@pytest.fixture -def hip_model(): - import ogo.util.Helper as helper - - data = np.zeros((4, 8, 12), dtype=np.uint16) - data[1:3, 2:6, 1:11] = 1 - data[1:3, 1:2, 7:10] = 5000 - data[1:3, 6:7, 8:11] = 5000 - grid = helper.Image2Mesh(vtk_image(data)) - model = vtkbone.vtkboneFiniteElementModel() - model.ShallowCopy(grid) - xyz = vtk_to_numpy(model.GetPoints().GetData()) - for name, active in (("Femoral_Head_PMMA_Nodes", xyz[:, 1] == xyz[:, 1].max()), - ("Greater_Trochanter_PMMA_Nodes", xyz[:, 1] == xyz[:, 1].min()), - ("Distal_Femur_Nodes", xyz[:, 2] == xyz[:, 2].min())): - ids = numpy_to_vtkIdTypeArray(np.flatnonzero(active), deep=True) - ids.SetName(name) - model.AddNodeSet(ids) - return model - - -def test_hip_exports_are_colored_nonblank_and_in_physical_coordinates(tmp_path, hip_model): - from ogo.fea.model_export import export_femur_model - - model = hip_model - outputs = export_femur_model(model, tmp_path / "test.n88model", pmma_mat_id=5000) - reader = vtk.vtkXMLPolyDataReader() - reader.SetFileName(outputs["surface_vtp"]) - reader.Update() - surface = reader.GetOutput() - assert surface.GetNumberOfPolys() > 0 - assert set(vtk_to_numpy(surface.GetCellData().GetArray("ModelRegion"))) == {0, 1, 2} - assert surface.GetBounds() == pytest.approx(model.GetBounds()) - png = vtk.vtkPNGReader() - png.SetFileName(outputs["qc_3d_png"]) - png.Update() - pixels = vtk_to_numpy(png.GetOutput().GetPointData().GetScalars()) - assert (pixels.min(axis=1) < 200).sum() > 1000 - assert ((pixels[:, 2] > pixels[:, 0].astype(float) * 1.25) - & (pixels[:, 2] > pixels[:, 1])).sum() > 1000 - assert (pixels[:, 0].astype(float) > pixels[:, 1].astype(float) * 1.5).sum() > 100 - assert png.GetOutput().GetDimensions()[:2] == (1600, 1000) - - -def test_missing_display_preserves_surface_and_records_missing_png(tmp_path, hip_model, monkeypatch): - from pathlib import Path - from ogo.fea.model_export import export_femur_model - - class NoDisplayWindow: - def GetClassName(self): - return "vtkXOpenGLRenderWindow" - - def Finalize(self): - pass - - monkeypatch.delenv("DISPLAY", raising=False) - monkeypatch.setattr(vtk, "vtkRenderWindow", NoDisplayWindow) - with pytest.warns(RuntimeWarning, match="headless"): - outputs = export_femur_model(hip_model, tmp_path / "test.n88model", pmma_mat_id=5000) - assert Path(outputs["surface_vtp"]).is_file() - assert outputs["qc_3d_png"] is None - assert outputs["render_status"] == "unavailable" diff --git a/tests/fea/test_spine.py b/tests/fea/test_spine.py index 20e277d..4f2bedb 100644 --- a/tests/fea/test_spine.py +++ b/tests/fea/test_spine.py @@ -1,3 +1,4 @@ +from tests.fea import helpers from ogo.fea import spine import numpy as np import pytest @@ -112,8 +113,8 @@ def test_spine_reference_path_depends_on_icp_target(): def test_benchmark_presets_match_spinefe_notebook_settings(): - linear = spine.benchmark_linear_params() - nonlinear = spine.benchmark_nonlinear_params() + linear = helpers.benchmark_linear_params() + nonlinear = helpers.benchmark_nonlinear_params() assert linear["fe_displacement"] == -0.2 assert linear["target_displacement_percent"] == 0.68 diff --git a/tests/fea/test_spinefe_benchmark.py b/tests/fea/test_spinefe_benchmark.py index b6146e5..5d47cd9 100644 --- a/tests/fea/test_spinefe_benchmark.py +++ b/tests/fea/test_spinefe_benchmark.py @@ -9,7 +9,8 @@ import pytest from ogo.fea.model import filter_node_set_to_dominant_coordinate_plane -from ogo.fea.spine import build_benchmark_sample_model, find_spinefe_benchmark_dir +from tests.fea import helpers +from tests.fea.helpers import build_benchmark_sample_model, find_spinefe_benchmark_dir def test_benchmark_sample_model_updates_prescribed_displacement(monkeypatch, tmp_path): @@ -19,7 +20,7 @@ def test_benchmark_sample_model_updates_prescribed_displacement(monkeypatch, tmp updates = [] monkeypatch.setattr( - spine, + helpers, "prepare_benchmark_images", lambda input_image_path, input_mask_path: ("image", "mask", [1.0]), ) diff --git a/tests/fea/test_workflow_locked_defaults.py b/tests/fea/test_workflow_locked_defaults.py index 40e00e7..ce22db2 100644 --- a/tests/fea/test_workflow_locked_defaults.py +++ b/tests/fea/test_workflow_locked_defaults.py @@ -1,3 +1,4 @@ +from tests.fea import helpers import json from pathlib import Path @@ -51,10 +52,10 @@ def _locked_workflow_defaults(): spine.SPINE_INFERIOR_CONTACT_CENTER_FRACTION ), "benchmark_linear": _selected_spine_benchmark( - spine.benchmark_linear_params() + helpers.benchmark_linear_params() ), "benchmark_nonlinear": _selected_spine_benchmark( - spine.benchmark_nonlinear_params() + helpers.benchmark_nonlinear_params() ), }, "hip-sideways-fall": { From e47acac175232450f8a3d68d7171e8f57f317293 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Sat, 10 Oct 2026 06:35:22 -0600 Subject: [PATCH 65/67] Pin Ogo macOS CI to Intel runner --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4ab5052..4700626 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: - os: [ubuntu-latest, macOS-latest] + os: [ubuntu-latest, macos-15-intel] python-version: [3.7] name: ${{ matrix.os }}, Python ${{ matrix.python-version }} From 6f0a25d6ae3f9aea95a6b6e81e6d3fe52d397611 Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Sat, 10 Oct 2026 07:15:24 -0600 Subject: [PATCH 66/67] Run FEA CI with Python 3.10 and VTK 9 dependencies --- .github/workflows/main.yml | 5 +++-- tests/test_packaging_compatibility.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4700626..f84a426 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -20,9 +20,10 @@ jobs: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, macos-15-intel] - python-version: [3.7] + python-version: ["3.10"] name: ${{ matrix.os }}, Python ${{ matrix.python-version }} @@ -47,7 +48,7 @@ jobs: - name: Create and install dependencies if: steps.conda-env-cache.outputs.cache-hit != 'true' run: | - conda install -y -n test n88tools click flake8 matplotlib nose numpy opencv pandas pbr pytest scipy simpleitk six sphinx pydicom gdcm "setuptools<65.6" + conda install -y -n test -c numerics88 -c conda-forge vtkbone "vtk>=9.2" click flake8 matplotlib nose "numpy<2" pandas pbr pytest "scipy<1.13" simpleitk six sphinx pydicom gdcm netcdf4 pillow "setuptools<65.6" - name: Install package run: | conda activate test diff --git a/tests/test_packaging_compatibility.py b/tests/test_packaging_compatibility.py index 850c655..51c141e 100644 --- a/tests/test_packaging_compatibility.py +++ b/tests/test_packaging_compatibility.py @@ -55,3 +55,13 @@ def test_ci_uses_conda_with_required_test_dependencies(): assert install_line is not None assert "pytest" in install_line.group() assert '"setuptools<65.6"' in install_line.group() + + +def test_ci_uses_vtk9_and_supported_python(): + """FEA uses VTK 9 image directions and reference-surface readers.""" + workflow = Path(__file__).resolve().parents[1] / ".github/workflows/main.yml" + text = workflow.read_text(encoding="utf-8") + assert 'python-version: ["3.10"]' in text + assert '"vtk>=9.2"' in text + assert "vtkbone" in text + assert "fail-fast: false" in text From 898ffb7024788b8ee4df703dc132d9d5b13f2e8a Mon Sep 17 00:00:00 2001 From: Matthias Walle <92020703+wallematthias@users.noreply.github.com> Date: Sat, 10 Oct 2026 07:28:43 -0600 Subject: [PATCH 67/67] Declare QC image dependencies and simplify CI environment solving --- .github/workflows/main.yml | 4 ++-- requirements.txt | 2 ++ tests/test_packaging_compatibility.py | 6 ++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f84a426..a01da78 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,7 +38,7 @@ jobs: python-version: ${{ matrix.python-version }} miniconda-version: "latest" activate-environment: test - channels: numerics88,simpleitk,conda-forge + channels: numerics88,conda-forge auto-activate-base: false - uses: actions/cache@v3 id: conda-env-cache @@ -48,7 +48,7 @@ jobs: - name: Create and install dependencies if: steps.conda-env-cache.outputs.cache-hit != 'true' run: | - conda install -y -n test -c numerics88 -c conda-forge vtkbone "vtk>=9.2" click flake8 matplotlib nose "numpy<2" pandas pbr pytest "scipy<1.13" simpleitk six sphinx pydicom gdcm netcdf4 pillow "setuptools<65.6" + conda install -y -n test --override-channels -c numerics88 -c conda-forge vtkbone "vtk>=9.2" "numpy<2" pandas nibabel pyyaml netcdf4 pbr pytest wheel "setuptools<65.6" - name: Install package run: | conda activate test diff --git a/requirements.txt b/requirements.txt index 6fa92ba..3ec3dd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,6 +7,8 @@ simpleitk numpy<2.0 matplotlib netCDF4 +nibabel +Pillow # Statistics scipy>=1.7,<1.13 diff --git a/tests/test_packaging_compatibility.py b/tests/test_packaging_compatibility.py index 51c141e..73a82a7 100644 --- a/tests/test_packaging_compatibility.py +++ b/tests/test_packaging_compatibility.py @@ -65,3 +65,9 @@ def test_ci_uses_vtk9_and_supported_python(): assert '"vtk>=9.2"' in text assert "vtkbone" in text assert "fail-fast: false" in text + + +def test_qc_image_dependencies_are_declared(): + requirements = Path(__file__).resolve().parents[1] / "requirements.txt" + names = {line.strip().lower() for line in requirements.read_text().splitlines()} + assert {"nibabel", "pillow", "netcdf4"} <= names